add anime tab in updates

TODO: fix anime updates not being displayed
This commit is contained in:
jmir1 2021-05-19 02:13:21 +02:00
parent f32a8ca396
commit 0b95cc7be8
13 changed files with 1038 additions and 2 deletions

View file

@ -52,7 +52,7 @@ import eu.kanade.tachiyomi.ui.download.DownloadController
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.ui.recent.UpdatesTabsController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.InternalResourceHelper
@ -163,7 +163,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
when (id) {
R.id.nav_library -> setRoot(LibraryController(), id)
R.id.nav_animelib -> setRoot(AnimelibController(), id)
R.id.nav_updates -> setRoot(UpdatesController(), id)
R.id.nav_updates -> setRoot(UpdatesTabsController(), id)
R.id.nav_browse -> setRoot(BrowseController(), id)
R.id.nav_more -> setRoot(MoreController(), id)
}

View file

@ -0,0 +1,94 @@
package eu.kanade.tachiyomi.ui.recent
import android.view.LayoutInflater
import android.view.View
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.support.RouterPagerAdapter
import com.google.android.material.tabs.TabLayout
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PagerControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.RxController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.recent.animeupdates.AnimeUpdatesController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
class UpdatesTabsController() :
RxController<PagerControllerBinding>(),
RootController,
TabbedController {
private var adapter: UpdatesTabsAdapter? = null
override fun getTitle(): String {
return resources!!.getString(R.string.label_recent_updates)
}
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = UpdatesTabsAdapter()
binding.pager.adapter = adapter
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isEnter) {
(activity as? MainActivity)?.binding?.toolbarLayout?.tabs?.apply {
setupWithViewPager(binding.pager)
}
}
}
override fun configureTabs(tabs: TabLayout) {
with(tabs) {
tabGravity = TabLayout.GRAVITY_FILL
tabMode = TabLayout.MODE_FIXED
}
}
private inner class UpdatesTabsAdapter : RouterPagerAdapter(this@UpdatesTabsController) {
private val tabTitles = listOf(
R.string.label_updates,
R.string.label_animeupdates
)
.map { resources!!.getString(it) }
override fun getCount(): Int {
return tabTitles.size
}
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
val controller: Controller = when (position) {
UPDATES_CONTROLLER -> UpdatesController()
ANIME_UPDATES_CONTROLLER -> AnimeUpdatesController()
else -> error("Wrong position $position")
}
router.setRoot(RouterTransaction.with(controller))
}
}
override fun getPageTitle(position: Int): CharSequence {
return tabTitles[position]
}
}
companion object {
const val UPDATES_CONTROLLER = 0
const val ANIME_UPDATES_CONTROLLER = 1
}
}

View file

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.ui.recent.animeupdates
import android.content.Context
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.anime.episode.base.BaseEpisodesAdapter
import eu.kanade.tachiyomi.util.system.getResourceColor
class AnimeUpdatesAdapter(
val controller: AnimeUpdatesController,
context: Context
) : BaseEpisodesAdapter<IFlexible<*>>(controller) {
var seenColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f)
var unseenColor = context.getResourceColor(R.attr.colorOnSurface)
val coverClickListener: OnCoverClickListener = controller
init {
setDisplayHeadersAtStartUp(true)
}
interface OnCoverClickListener {
fun onCoverClick(position: Int)
}
}

View file

@ -0,0 +1,428 @@
package eu.kanade.tachiyomi.ui.recent.animeupdates
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateService
import eu.kanade.tachiyomi.data.download.AnimeDownloadService
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.databinding.AnimeUpdatesControllerBinding
import eu.kanade.tachiyomi.source.AnimeSourceManager
import eu.kanade.tachiyomi.source.model.toEpisodeInfo
import eu.kanade.tachiyomi.ui.anime.AnimeController
import eu.kanade.tachiyomi.ui.anime.episode.EpisodeItem
import eu.kanade.tachiyomi.ui.anime.episode.base.BaseEpisodesAdapter
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.watcher.WatcherActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* Fragment that shows recent episodes.
*/
class AnimeUpdatesController :
NucleusController<AnimeUpdatesControllerBinding, AnimeUpdatesPresenter>(),
RootController,
ActionMode.Callback,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.OnUpdateListener,
BaseEpisodesAdapter.OnEpisodeClickListener,
ConfirmDeleteEpisodesDialog.Listener,
AnimeUpdatesAdapter.OnCoverClickListener {
/**
* Action mode for multiple selection.
*/
private var actionMode: ActionMode? = null
/**
* Adapter containing the recent episodes.
*/
var adapter: AnimeUpdatesAdapter? = null
private set
init {
setHasOptionsMenu(true)
}
override fun getTitle(): String? {
return resources?.getString(R.string.label_recent_updates)
}
override fun createPresenter(): AnimeUpdatesPresenter {
return AnimeUpdatesPresenter()
}
override fun createBinding(inflater: LayoutInflater) = AnimeUpdatesControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
}
view.context.notificationManager.cancel(Notifications.ID_NEW_EPISODES)
// Init RecyclerView and adapter
val layoutManager = LinearLayoutManager(view.context)
binding.recycler.layoutManager = layoutManager
binding.recycler.setHasFixedSize(true)
adapter = AnimeUpdatesAdapter(this@AnimeUpdatesController, view.context)
binding.recycler.adapter = adapter
adapter?.fastScroller = binding.fastScroller
binding.recycler.scrollStateChanges()
.onEach {
// Disable swipe refresh when view is not at the top
val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
binding.swipeRefresh.isEnabled = firstPos <= 0
}
.launchIn(viewScope)
binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
binding.swipeRefresh.refreshes()
.onEach {
updateLibrary()
// It can be a very long operation, so we disable swipe refresh and show a toast.
binding.swipeRefresh.isRefreshing = false
}
.launchIn(viewScope)
(activity as? MainActivity)?.fixViewToBottom(binding.actionToolbar)
}
override fun onDestroyView(view: View) {
destroyActionModeIfNeeded()
(activity as? MainActivity)?.clearFixViewToBottom(binding.actionToolbar)
binding.actionToolbar.destroy()
adapter = null
super.onDestroyView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.updates, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_update_library -> updateLibrary()
}
return super.onOptionsItemSelected(item)
}
private fun updateLibrary() {
activity?.let {
if (AnimelibUpdateService.start(it)) {
it.toast(R.string.updating_library)
}
}
}
/**
* Returns selected episodes
* @return list of selected episodes
*/
private fun getSelectedEpisodes(): List<AnimeUpdatesItem> {
val adapter = adapter ?: return emptyList()
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? AnimeUpdatesItem }
}
/**
* Called when item in list is clicked
* @param position position of clicked item
*/
override fun onItemClick(view: View, position: Int): Boolean {
val adapter = adapter ?: return false
// Get item from position
val item = adapter.getItem(position) as? AnimeUpdatesItem ?: return false
return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
toggleSelection(position)
true
} else {
openEpisode(item)
false
}
}
/**
* Called when item in list is long clicked
* @param position position of clicked item
*/
override fun onItemLongClick(position: Int) {
if (actionMode == null) {
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
binding.actionToolbar.show(
actionMode!!,
R.menu.updates_episode_selection
) { onActionItemClicked(it!!) }
(activity as? MainActivity)?.showBottomNav(visible = false, collapse = true)
}
toggleSelection(position)
}
/**
* Called to toggle selection
* @param position position of selected item
*/
private fun toggleSelection(position: Int) {
val adapter = adapter ?: return
adapter.toggleSelection(position)
actionMode?.invalidate()
}
/**
* Open episode in reader
* @param episode selected episode
*/
private fun openEpisode(item: AnimeUpdatesItem) {
val activity = activity ?: return
val source = Injekt.get<AnimeSourceManager>().getOrStub(item.anime.source)
val link = runBlocking {
return@runBlocking suspendCoroutine<String> { continuation ->
var link: String
launchIO {
try {
link = source.getEpisodeLink(item.episode.toEpisodeInfo())
continuation.resume(link)
} catch (e: Throwable) {
withUIContext { throw e }
}
}
}
}
val episodeList: List<EpisodeItem> = Collections.emptyList()
val intent = WatcherActivity.newIntent(activity, item.anime, item.episode, episodeList, link)
startActivity(intent)
}
/**
* Download selected items
* @param episodes list of selected [AnimeUpdatesItem]s
*/
private fun downloadEpisodes(episodes: List<AnimeUpdatesItem>) {
presenter.downloadEpisodes(episodes)
destroyActionModeIfNeeded()
}
/**
* Populate adapter with episodes
* @param episodes list of [Any]
*/
fun onNextRecentEpisodes(episodes: List<IFlexible<*>>) {
destroyActionModeIfNeeded()
adapter?.updateDataSet(episodes)
}
override fun onUpdateEmptyView(size: Int) {
if (size > 0) {
binding.emptyView.hide()
} else {
binding.emptyView.show(R.string.information_no_recent)
}
}
/**
* Update download status of episode
* @param download [Download] object containing download progress.
*/
fun onEpisodeDownloadUpdate(download: AnimeDownload) {
adapter?.currentItems
?.filterIsInstance<AnimeUpdatesItem>()
?.find { it.episode.id == download.episode.id }?.let {
adapter?.updateItem(it, it.status)
}
}
/**
* Mark episode as read
* @param episodes list of episodes
*/
private fun markAsRead(episodes: List<AnimeUpdatesItem>) {
presenter.markEpisodeRead(episodes, true)
if (presenter.preferences.removeAfterMarkedAsRead()) {
deleteEpisodes(episodes)
}
destroyActionModeIfNeeded()
}
/**
* Mark episode as unread
* @param episodes list of selected [AnimeUpdatesItem]
*/
private fun markAsUnread(episodes: List<AnimeUpdatesItem>) {
presenter.markEpisodeRead(episodes, false)
destroyActionModeIfNeeded()
}
override fun deleteEpisodes(episodesToDelete: List<AnimeUpdatesItem>) {
presenter.deleteEpisodes(episodesToDelete)
destroyActionModeIfNeeded()
}
private fun destroyActionModeIfNeeded() {
actionMode?.finish()
}
override fun onCoverClick(position: Int) {
destroyActionModeIfNeeded()
val episodeClicked = adapter?.getItem(position) as? AnimeUpdatesItem ?: return
openAnime(episodeClicked)
}
private fun openAnime(episode: AnimeUpdatesItem) {
router.pushController(AnimeController(episode.anime).withFadeTransaction())
}
/**
* Called when episodes are deleted
*/
fun onEpisodesDeleted() {
adapter?.notifyDataSetChanged()
}
/**
* Called when error while deleting
* @param error error message
*/
fun onEpisodesDeletedError(error: Throwable) {
Timber.e(error)
}
override fun downloadEpisode(position: Int) {
val item = adapter?.getItem(position) as? AnimeUpdatesItem ?: return
if (item.status == AnimeDownload.State.ERROR) {
AnimeDownloadService.start(activity!!)
} else {
downloadEpisodes(listOf(item))
}
adapter?.updateItem(item)
}
override fun deleteEpisode(position: Int) {
val item = adapter?.getItem(position) as? AnimeUpdatesItem ?: return
deleteEpisodes(listOf(item))
adapter?.updateItem(item)
}
/**
* Called when ActionMode created.
* @param mode the ActionMode object
* @param menu menu object of ActionMode
*/
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.generic_selection, menu)
adapter?.mode = SelectableAdapter.Mode.MULTI
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = adapter?.selectedItemCount ?: 0
if (count == 0) {
// Destroy action mode if there are no items selected.
destroyActionModeIfNeeded()
} else {
mode.title = count.toString()
val episodes = getSelectedEpisodes()
binding.actionToolbar.findItem(R.id.action_download)?.isVisible = episodes.any { !it.isDownloaded }
binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = episodes.any { it.isDownloaded }
binding.actionToolbar.findItem(R.id.action_mark_as_seen)?.isVisible = episodes.any { !it.episode.seen }
binding.actionToolbar.findItem(R.id.action_mark_as_unseen)?.isVisible = episodes.all { it.episode.seen }
}
return false
}
/**
* Called when ActionMode item clicked
* @param mode the ActionMode object
* @param item item from ActionMode.
*/
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return onActionItemClicked(item)
}
private fun onActionItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_select_all -> selectAll()
R.id.action_select_inverse -> selectInverse()
R.id.action_download -> downloadEpisodes(getSelectedEpisodes())
R.id.action_delete ->
ConfirmDeleteEpisodesDialog(this, getSelectedEpisodes())
.showDialog(router)
R.id.action_mark_as_read -> markAsRead(getSelectedEpisodes())
R.id.action_mark_as_unread -> markAsUnread(getSelectedEpisodes())
else -> return false
}
return true
}
/**
* Called when ActionMode destroyed
* @param mode the ActionMode object
*/
override fun onDestroyActionMode(mode: ActionMode?) {
adapter?.mode = SelectableAdapter.Mode.IDLE
adapter?.clearSelection()
binding.actionToolbar.hide()
(activity as? MainActivity)?.showBottomNav(visible = true, collapse = true)
actionMode = null
}
private fun selectAll() {
val adapter = adapter ?: return
adapter.selectAll()
actionMode?.invalidate()
}
private fun selectInverse() {
val adapter = adapter ?: return
for (i in 0..adapter.itemCount) {
adapter.toggleSelection(i)
}
actionMode?.invalidate()
adapter.notifyDataSetChanged()
}
}

View file

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.ui.recent.animeupdates
import android.view.View
import androidx.core.view.isVisible
import coil.clear
import coil.loadAny
import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.AnimeUpdatesItemBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.anime.episode.base.BaseEpisodeHolder
/**
* Holder that contains episode item
* UI related actions should be called from here.
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events.
* @constructor creates a new recent episode holder.
*/
class AnimeUpdatesHolder(private val view: View, private val adapter: AnimeUpdatesAdapter) :
BaseEpisodeHolder(view, adapter) {
private val binding = AnimeUpdatesItemBinding.bind(view)
init {
binding.animeCover.setOnClickListener {
adapter.coverClickListener.onCoverClick(bindingAdapterPosition)
}
binding.download.setOnClickListener {
onAnimeDownloadClick(it, bindingAdapterPosition)
}
}
fun bind(item: AnimeUpdatesItem) {
// Set episode title
binding.episodeTitle.text = item.episode.name
// Set anime title
binding.animeTitle.text = item.anime.title
// Check if episode is read and set correct color
if (item.episode.seen) {
binding.episodeTitle.setTextColor(adapter.seenColor)
binding.animeTitle.setTextColor(adapter.seenColor)
} else {
binding.episodeTitle.setTextColor(adapter.unseenColor)
binding.animeTitle.setTextColor(adapter.unseenColor)
}
// Set episode status
binding.download.isVisible = item.anime.source != LocalSource.ID
binding.download.setState(item.status, item.progress)
// Set cover
val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
binding.animeCover.clear()
binding.animeCover.loadAny(item.anime) {
transformations(RoundedCornersTransformation(radius))
}
}
}

View file

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.recent.animeupdates
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.ui.anime.episode.base.BaseEpisodeItem
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
class AnimeUpdatesItem(episode: Episode, val anime: Anime, header: DateSectionItem) :
BaseEpisodeItem<AnimeUpdatesHolder, DateSectionItem>(episode, header) {
override fun getLayoutRes(): Int {
return R.layout.updates_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): AnimeUpdatesHolder {
return AnimeUpdatesHolder(view, adapter as AnimeUpdatesAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: AnimeUpdatesHolder,
position: Int,
payloads: List<Any?>?
) {
holder.bind(this)
}
}

View file

@ -0,0 +1,202 @@
package eu.kanade.tachiyomi.ui.recent.animeupdates
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
import eu.kanade.tachiyomi.data.database.models.AnimeEpisode
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.AnimeSourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
import eu.kanade.tachiyomi.util.lang.toDateKey
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Date
import java.util.TreeMap
class AnimeUpdatesPresenter(
val preferences: PreferencesHelper = Injekt.get(),
private val db: AnimeDatabaseHelper = Injekt.get(),
private val downloadManager: AnimeDownloadManager = Injekt.get(),
private val sourceManager: AnimeSourceManager = Injekt.get()
) : BasePresenter<AnimeUpdatesController>() {
/**
* List containing episode and anime information
*/
private var episodes: List<AnimeUpdatesItem> = emptyList()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
getUpdatesObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(AnimeUpdatesController::onNextRecentEpisodes)
downloadManager.queue.getStatusObservable()
.observeOn(Schedulers.io())
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(
{ view, it ->
onDownloadStatusChange(it)
view.onEpisodeDownloadUpdate(it)
},
{ _, error ->
Timber.e(error)
}
)
downloadManager.queue.getProgressObservable()
.observeOn(Schedulers.io())
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(AnimeUpdatesController::onEpisodeDownloadUpdate) { _, error ->
Timber.e(error)
}
}
/**
* Get observable containing recent episodes and date
*
* @return observable containing recent episodes and date
*/
private fun getUpdatesObservable(): Observable<List<AnimeUpdatesItem>> {
// Set date limit for recent episodes
val cal = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
return db.getRecentEpisodes(cal.time).asRxObservable()
// Convert to a list of recent episodes.
.map { animeEpisodes ->
val map = TreeMap<Date, MutableList<AnimeEpisode>> { d1, d2 -> d2.compareTo(d1) }
val byDay = animeEpisodes
.groupByTo(map, { it.episode.date_fetch.toDateKey() })
byDay.flatMap { entry ->
val dateItem = DateSectionItem(entry.key)
entry.value
.sortedWith(compareBy({ it.episode.date_fetch }, { it.episode.episode_number })).asReversed()
.map { AnimeUpdatesItem(it.episode, it.anime, dateItem) }
}
}
.doOnNext { list ->
list.forEach { item ->
// Find an active download for this episode.
val download = downloadManager.queue.find { it.episode.id == item.episode.id }
// If there's an active download, assign it, otherwise ask the manager if
// the episode is downloaded and assign it to the status.
if (download != null) {
item.download = download
}
}
setDownloadedEpisodes(list)
episodes = list
}
}
/**
* Finds and assigns the list of downloaded episodes.
*
* @param items the list of episode from the database.
*/
private fun setDownloadedEpisodes(items: List<AnimeUpdatesItem>) {
for (item in items) {
val anime = item.anime
val episode = item.episode
if (downloadManager.isEpisodeDownloaded(episode, anime)) {
item.status = AnimeDownload.State.DOWNLOADED
}
}
}
/**
* Update status of episodes.
*
* @param download download object containing progress.
*/
private fun onDownloadStatusChange(download: AnimeDownload) {
// Assign the download to the model object.
if (download.status == AnimeDownload.State.QUEUE) {
val episode = episodes.find { it.episode.id == download.episode.id }
if (episode != null && episode.download == null) {
episode.download = download
}
}
}
/**
* Mark selected episode as read
*
* @param items list of selected episodes
* @param seen seen/unseen status
*/
fun markEpisodeRead(items: List<AnimeUpdatesItem>, seen: Boolean) {
val episodes = items.map { it.episode }
episodes.forEach {
it.seen = seen
if (!seen) {
it.last_second_seen = 0
}
}
Observable.fromCallable { db.updateEpisodesProgress(episodes).executeAsBlocking() }
.subscribeOn(Schedulers.io())
.subscribe()
}
/**
* Delete selected episodes
*
* @param episodes list of episodes
*/
fun deleteEpisodes(episodes: List<AnimeUpdatesItem>) {
Observable.just(episodes)
.doOnNext { deleteEpisodesInternal(it) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, _ ->
view.onEpisodesDeleted()
},
AnimeUpdatesController::onEpisodesDeletedError
)
}
/**
* Download selected episodes
* @param items list of recent episodes seleted.
*/
fun downloadEpisodes(items: List<AnimeUpdatesItem>) {
items.forEach { downloadManager.downloadEpisodes(it.anime, listOf(it.episode)) }
}
/**
* Delete selected episodes
*
* @param items episodes selected
*/
private fun deleteEpisodesInternal(episodeItems: List<AnimeUpdatesItem>) {
val itemsByAnime = episodeItems.groupBy { it.anime.id }
for ((_, items) in itemsByAnime) {
val anime = items.first().anime
val source = sourceManager.get(anime.source) ?: continue
val episodes = items.map { it.episode }
downloadManager.deleteEpisodes(episodes, anime, source)
items.forEach {
it.status = AnimeDownload.State.NOT_DOWNLOADED
it.download = null
}
}
}
}

View file

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.recent.animeupdates
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
class ConfirmDeleteEpisodesDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
where T : Controller, T : ConfirmDeleteEpisodesDialog.Listener {
private var episodesToDelete = emptyList<AnimeUpdatesItem>()
constructor(target: T, episodesToDelete: List<AnimeUpdatesItem>) : this() {
this.episodesToDelete = episodesToDelete
targetController = target
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.message(R.string.confirm_delete_episodes)
.positiveButton(android.R.string.ok) {
(targetController as? Listener)?.deleteEpisodes(episodesToDelete)
}
.negativeButton(android.R.string.cancel)
}
interface Listener {
fun deleteEpisodes(episodesToDelete: List<AnimeUpdatesItem>)
}
}

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/action_toolbar_list_padding"
tools:listitem="@layout/anime_updates_item" />
<eu.kanade.tachiyomi.widget.MaterialFastScroll
android:id="@+id/fast_scroller"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
app:fastScrollerBubbleEnabled="false"
tools:visibility="visible" />
<eu.kanade.tachiyomi.widget.ActionToolbar
android:id="@+id/action_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_dodgeInsetEdges="bottom" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/material_component_lists_two_line_height"
android:background="@drawable/list_item_selector_background"
android:paddingEnd="4dp">
<ImageView
android:id="@+id/anime_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/anime_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Medium.Body2"
app:layout_constraintBottom_toTopOf="@+id/episode_title"
app:layout_constraintEnd_toStartOf="@+id/download"
app:layout_constraintStart_toEndOf="@+id/anime_cover"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Anime title" />
<TextView
android:id="@+id/episode_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/download"
app:layout_constraintStart_toEndOf="@+id/anime_cover"
app:layout_constraintTop_toBottomOf="@+id/anime_title"
tools:text="Episode title" />
<eu.kanade.tachiyomi.ui.anime.episode.EpisodeDownloadView
android:id="@+id/download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,13 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/action_update_library"
android:icon="@drawable/ic_refresh_24dp"
android:title="@string/action_update_library"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" />
</menu>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_download"
android:icon="@drawable/ic_get_app_24dp"
android:title="@string/action_download"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="always" />
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_24dp"
android:title="@string/action_delete"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="always" />
<item
android:id="@+id/action_mark_as_seen"
android:icon="@drawable/ic_done_24dp"
android:title="@string/action_mark_as_seen"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="always" />
<item
android:id="@+id/action_mark_as_unseen"
android:icon="@drawable/ic_done_outline_24dp"
android:title="@string/action_mark_as_unseen"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="always" />
</menu>

View file

@ -53,6 +53,8 @@
<string name="action_select_inverse">Select inverse</string>
<string name="action_mark_as_read">Mark as read</string>
<string name="action_mark_as_unread">Mark as unread</string>
<string name="action_mark_as_seen">Mark as watched</string>
<string name="action_mark_as_unseen">Mark as unwatched</string>
<string name="action_mark_previous_as_read">Mark previous as read</string>
<string name="action_download">Download</string>
<string name="action_download_unread">Download unread chapters</string>
@ -582,6 +584,7 @@
<string name="download_unread">Unread</string>
<string name="download_unseen">Unseen</string>
<string name="confirm_delete_chapters">Are you sure you want to delete the selected chapters?</string>
<string name="confirm_delete_episodes">Are you sure you want to delete the selected episodes?</string>
<string name="invalid_download_dir">Invalid download location</string>
<string name="chapter_settings">Chapter settings</string>
<string name="confirm_set_chapter_settings">Are you sure you want to save these settings as default?</string>
@ -812,4 +815,6 @@
<string name="anime_description_cover">Cover of Anime</string>
<string name="label_history">Manga</string>
<string name="label_animehistory">Anime</string>
<string name="label_updates">Manga</string>
<string name="label_animeupdates">Anime</string>
</resources>