mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-29 01:29:02 +03:00
add anime tab in updates
TODO: fix anime updates not being displayed
This commit is contained in:
parent
f32a8ca396
commit
0b95cc7be8
13 changed files with 1038 additions and 2 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>)
|
||||
}
|
||||
}
|
45
app/src/main/res/layout/anime_updates_controller.xml
Normal file
45
app/src/main/res/layout/anime_updates_controller.xml
Normal 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>
|
62
app/src/main/res/layout/anime_updates_item.xml
Normal file
62
app/src/main/res/layout/anime_updates_item.xml
Normal 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>
|
13
app/src/main/res/menu/anime_updates.xml
Normal file
13
app/src/main/res/menu/anime_updates.xml
Normal 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>
|
33
app/src/main/res/menu/updates_episode_selection.xml
Normal file
33
app/src/main/res/menu/updates_episode_selection.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue