mirror of
https://github.com/mihonapp/mihon.git
synced 2024-12-25 02:18:24 +01:00
Merge manga info and chapters views
This commit is contained in:
parent
a768280d82
commit
4605e14729
@ -24,11 +24,9 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
|
import eu.kanade.tachiyomi.ui.manga.chapter.MangaInfoChaptersController
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import java.util.Date
|
|
||||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
import kotlinx.android.synthetic.main.main_activity.tabs
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -65,10 +63,6 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
|
|||||||
|
|
||||||
val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false)
|
val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false)
|
||||||
|
|
||||||
val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
|
|
||||||
|
|
||||||
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
|
|
||||||
|
|
||||||
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
|
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
|
||||||
|
|
||||||
private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
|
private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
|
||||||
@ -92,17 +86,12 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
|
|||||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||||
|
|
||||||
adapter = MangaDetailAdapter()
|
adapter = MangaDetailAdapter()
|
||||||
binding.pager.offscreenPageLimit = 3
|
|
||||||
binding.pager.adapter = adapter
|
binding.pager.adapter = adapter
|
||||||
|
|
||||||
if (!fromSource) {
|
|
||||||
binding.pager.currentItem = CHAPTERS_CONTROLLER
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
super.onDestroyView(view)
|
|
||||||
adapter = null
|
adapter = null
|
||||||
|
super.onDestroyView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
@ -150,15 +139,14 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
|
|||||||
|
|
||||||
private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
|
private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
|
||||||
|
|
||||||
private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
|
|
||||||
|
|
||||||
private val tabTitles = listOf(
|
private val tabTitles = listOf(
|
||||||
R.string.manga_detail_tab,
|
|
||||||
R.string.manga_chapters_tab,
|
R.string.manga_chapters_tab,
|
||||||
R.string.manga_tracking_tab
|
R.string.manga_tracking_tab
|
||||||
)
|
)
|
||||||
.map { resources!!.getString(it) }
|
.map { resources!!.getString(it) }
|
||||||
|
|
||||||
|
private val tabCount = tabTitles.size - if (Injekt.get<TrackManager>().hasLoggedServices()) 0 else 1
|
||||||
|
|
||||||
override fun getCount(): Int {
|
override fun getCount(): Int {
|
||||||
return tabCount
|
return tabCount
|
||||||
}
|
}
|
||||||
@ -166,8 +154,7 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
|
|||||||
override fun configureRouter(router: Router, position: Int) {
|
override fun configureRouter(router: Router, position: Int) {
|
||||||
if (!router.hasRootController()) {
|
if (!router.hasRootController()) {
|
||||||
val controller = when (position) {
|
val controller = when (position) {
|
||||||
INFO_CONTROLLER -> MangaInfoController(fromSource)
|
INFO_CHAPTERS_CONTROLLER -> MangaInfoChaptersController(fromSource)
|
||||||
CHAPTERS_CONTROLLER -> ChaptersController()
|
|
||||||
TRACK_CONTROLLER -> TrackController()
|
TRACK_CONTROLLER -> TrackController()
|
||||||
else -> error("Wrong position $position")
|
else -> error("Wrong position $position")
|
||||||
}
|
}
|
||||||
@ -184,8 +171,7 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
|
|||||||
const val FROM_SOURCE_EXTRA = "from_source"
|
const val FROM_SOURCE_EXTRA = "from_source"
|
||||||
const val MANGA_EXTRA = "manga"
|
const val MANGA_EXTRA = "manga"
|
||||||
|
|
||||||
const val INFO_CONTROLLER = 0
|
const val INFO_CHAPTERS_CONTROLLER = 0
|
||||||
const val CHAPTERS_CONTROLLER = 1
|
const val TRACK_CONTROLLER = 1
|
||||||
const val TRACK_CONTROLLER = 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import java.text.DecimalFormatSymbols
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ChaptersAdapter(
|
class ChaptersAdapter(
|
||||||
controller: ChaptersController,
|
controller: MangaInfoChaptersController,
|
||||||
context: Context
|
context: Context
|
||||||
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
||||||
|
|
||||||
|
@ -15,19 +15,34 @@ import androidx.appcompat.view.ActionMode
|
|||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.MergeAdapter
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.ChaptersControllerBinding
|
import eu.kanade.tachiyomi.databinding.ChaptersControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
|
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.recent.history.HistoryController
|
||||||
|
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
|
||||||
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.getCoordinates
|
import eu.kanade.tachiyomi.util.view.getCoordinates
|
||||||
@ -40,19 +55,21 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import reactivecircus.flowbinding.android.view.clicks
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ChaptersController :
|
class MangaInfoChaptersController(private val fromSource: Boolean = false) :
|
||||||
NucleusController<ChaptersControllerBinding, ChaptersPresenter>(),
|
NucleusController<ChaptersControllerBinding, MangaInfoChaptersPresenter>(),
|
||||||
ActionMode.Callback,
|
ActionMode.Callback,
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
|
ChangeMangaCategoriesDialog.Listener,
|
||||||
DownloadCustomChaptersDialog.Listener,
|
DownloadCustomChaptersDialog.Listener,
|
||||||
DeleteChaptersDialog.Listener {
|
DeleteChaptersDialog.Listener {
|
||||||
|
|
||||||
/**
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
* Adapter containing a list of chapters.
|
|
||||||
*/
|
private var headerAdapter: MangaInfoHeaderAdapter? = null
|
||||||
private var adapter: ChaptersAdapter? = null
|
private var chaptersAdapter: ChaptersAdapter? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action mode for multiple selection.
|
* Action mode for multiple selection.
|
||||||
@ -62,20 +79,22 @@ class ChaptersController :
|
|||||||
/**
|
/**
|
||||||
* Selected items. Used to restore selections after a rotation.
|
* Selected items. Used to restore selections after a rotation.
|
||||||
*/
|
*/
|
||||||
private val selectedItems = mutableSetOf<ChapterItem>()
|
private val selectedChapters = mutableSetOf<ChapterItem>()
|
||||||
|
|
||||||
private var lastClickPosition = -1
|
private var lastClickPosition = -1
|
||||||
|
|
||||||
|
private var isRefreshingInfo = false
|
||||||
|
private var isRefreshingChapters = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
setOptionsMenuHidden(true)
|
setOptionsMenuHidden(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createPresenter(): ChaptersPresenter {
|
override fun createPresenter(): MangaInfoChaptersPresenter {
|
||||||
val ctrl = parentController as MangaController
|
val ctrl = parentController as MangaController
|
||||||
return ChaptersPresenter(
|
return MangaInfoChaptersPresenter(
|
||||||
ctrl.manga!!, ctrl.source!!,
|
ctrl.manga!!, ctrl.source!!, ctrl.mangaFavoriteRelay
|
||||||
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,16 +110,20 @@ class ChaptersController :
|
|||||||
if (ctrl.manga == null || ctrl.source == null) return
|
if (ctrl.manga == null || ctrl.source == null) return
|
||||||
|
|
||||||
// Init RecyclerView and adapter
|
// Init RecyclerView and adapter
|
||||||
adapter = ChaptersAdapter(this, view.context)
|
headerAdapter = MangaInfoHeaderAdapter(this, fromSource)
|
||||||
|
chaptersAdapter = ChaptersAdapter(this, view.context)
|
||||||
|
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = MergeAdapter(headerAdapter, chaptersAdapter)
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||||
binding.recycler.setHasFixedSize(true)
|
binding.recycler.setHasFixedSize(true)
|
||||||
adapter?.fastScroller = binding.fastScroller
|
chaptersAdapter?.fastScroller = binding.fastScroller
|
||||||
|
|
||||||
binding.swipeRefresh.refreshes()
|
binding.swipeRefresh.refreshes()
|
||||||
.onEach { fetchChaptersFromSource(manualFetch = true) }
|
.onEach {
|
||||||
|
fetchMangaInfoFromSource(manualFetch = true)
|
||||||
|
fetchChaptersFromSource(manualFetch = true)
|
||||||
|
}
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
binding.fab.clicks()
|
binding.fab.clicks()
|
||||||
@ -134,7 +157,8 @@ class ChaptersController :
|
|||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
binding.actionToolbar.destroy()
|
binding.actionToolbar.destroy()
|
||||||
adapter = null
|
headerAdapter = null
|
||||||
|
chaptersAdapter = null
|
||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +195,6 @@ class ChaptersController :
|
|||||||
menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
|
menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
|
||||||
|
|
||||||
val filterSet = presenter.onlyRead() || presenter.onlyUnread() || presenter.onlyDownloaded() || presenter.onlyBookmarked()
|
val filterSet = presenter.onlyRead() || presenter.onlyUnread() || presenter.onlyDownloaded() || presenter.onlyBookmarked()
|
||||||
|
|
||||||
if (filterSet) {
|
if (filterSet) {
|
||||||
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
|
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
|
||||||
DrawableCompat.setTint(menu.findItem(R.id.action_filter).icon, filterColor)
|
DrawableCompat.setTint(menu.findItem(R.id.action_filter).icon, filterColor)
|
||||||
@ -259,10 +282,228 @@ class ChaptersController :
|
|||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
R.id.action_sort -> presenter.revertSortOrder()
|
R.id.action_sort -> presenter.revertSortOrder()
|
||||||
|
|
||||||
|
R.id.action_migrate -> migrateManga()
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateRefreshing() {
|
||||||
|
binding.swipeRefresh.isRefreshing = isRefreshingInfo || isRefreshingChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manga info - start
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if manga is initialized.
|
||||||
|
* If true update header with manga information,
|
||||||
|
* if false fetch manga information
|
||||||
|
*
|
||||||
|
* @param manga manga object containing information about manga.
|
||||||
|
* @param source the source of the manga.
|
||||||
|
*/
|
||||||
|
fun onNextMangaInfo(manga: Manga, source: Source) {
|
||||||
|
if (manga.initialized) {
|
||||||
|
// Update view.
|
||||||
|
headerAdapter?.update(manga, source)
|
||||||
|
} else {
|
||||||
|
// Initialize manga.
|
||||||
|
fetchMangaInfoFromSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start fetching manga information from source.
|
||||||
|
*/
|
||||||
|
private fun fetchMangaInfoFromSource(manualFetch: Boolean = false) {
|
||||||
|
isRefreshingInfo = true
|
||||||
|
updateRefreshing()
|
||||||
|
|
||||||
|
// Call presenter and start fetching manga information
|
||||||
|
presenter.fetchMangaFromSource(manualFetch)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFetchMangaInfoDone() {
|
||||||
|
isRefreshingInfo = false
|
||||||
|
updateRefreshing()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFetchMangaInfoError(error: Throwable) {
|
||||||
|
isRefreshingInfo = false
|
||||||
|
updateRefreshing()
|
||||||
|
activity?.toast(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openMangaInWebView() {
|
||||||
|
val source = presenter.source as? HttpSource ?: return
|
||||||
|
|
||||||
|
val url = try {
|
||||||
|
source.mangaDetailsRequest(presenter.manga).url.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val activity = activity ?: return
|
||||||
|
val intent = WebViewActivity.newIntent(activity, url, source.id, presenter.manga.title)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareManga() {
|
||||||
|
val context = view?.context ?: return
|
||||||
|
|
||||||
|
val source = presenter.source as? HttpSource ?: return
|
||||||
|
try {
|
||||||
|
val url = source.mangaDetailsRequest(presenter.manga).url.toString()
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_TEXT, url)
|
||||||
|
}
|
||||||
|
startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
context.toast(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFavoriteClick() {
|
||||||
|
val manga = presenter.manga
|
||||||
|
|
||||||
|
if (manga.favorite) {
|
||||||
|
toggleFavorite()
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
||||||
|
} else {
|
||||||
|
val categories = presenter.getCategories()
|
||||||
|
val defaultCategoryId = preferences.defaultCategory()
|
||||||
|
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||||
|
|
||||||
|
when {
|
||||||
|
// Default category set
|
||||||
|
defaultCategory != null -> {
|
||||||
|
toggleFavorite()
|
||||||
|
presenter.moveMangaToCategory(manga, defaultCategory)
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatic 'Default' or no categories
|
||||||
|
defaultCategoryId == 0 || categories.isEmpty() -> {
|
||||||
|
toggleFavorite()
|
||||||
|
presenter.moveMangaToCategory(manga, null)
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose a category
|
||||||
|
else -> {
|
||||||
|
val ids = presenter.getMangaCategoryIds(manga)
|
||||||
|
val preselected = ids.mapNotNull { id ->
|
||||||
|
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||||
|
.showDialog(router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the favorite status and asks for confirmation to delete downloaded chapters.
|
||||||
|
*/
|
||||||
|
private fun toggleFavorite() {
|
||||||
|
val view = view
|
||||||
|
|
||||||
|
val isNowFavorite = presenter.toggleFavorite()
|
||||||
|
if (view != null && !isNowFavorite && presenter.hasDownloads()) {
|
||||||
|
view.snack(view.context.getString(R.string.delete_downloads_for_manga)) {
|
||||||
|
setAction(R.string.action_delete) {
|
||||||
|
presenter.deleteDownloads()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headerAdapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCategoriesClick() {
|
||||||
|
val manga = presenter.manga
|
||||||
|
val categories = presenter.getCategories()
|
||||||
|
|
||||||
|
val ids = presenter.getMangaCategoryIds(manga)
|
||||||
|
val preselected = ids.mapNotNull { id ->
|
||||||
|
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||||
|
.showDialog(router)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||||
|
val manga = mangas.firstOrNull() ?: return
|
||||||
|
|
||||||
|
if (!manga.favorite) {
|
||||||
|
toggleFavorite()
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
|
}
|
||||||
|
|
||||||
|
presenter.moveMangaToCategories(manga, categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a global search using the provided query.
|
||||||
|
*
|
||||||
|
* @param query the search query to pass to the search controller
|
||||||
|
*/
|
||||||
|
fun performGlobalSearch(query: String) {
|
||||||
|
val router = parentController?.router ?: return
|
||||||
|
router.pushController(GlobalSearchController(query).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a search using the provided query.
|
||||||
|
*
|
||||||
|
* @param query the search query to the parent controller
|
||||||
|
*/
|
||||||
|
fun performSearch(query: String) {
|
||||||
|
val router = parentController?.router ?: return
|
||||||
|
|
||||||
|
if (router.backstackSize < 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val previousController = router.backstack[router.backstackSize - 2].controller()) {
|
||||||
|
is LibraryController -> {
|
||||||
|
router.handleBack()
|
||||||
|
previousController.search(query)
|
||||||
|
}
|
||||||
|
is UpdatesController,
|
||||||
|
is HistoryController -> {
|
||||||
|
// Manually navigate to LibraryController
|
||||||
|
router.handleBack()
|
||||||
|
(router.activity as MainActivity).setSelectedNavItem(R.id.nav_library)
|
||||||
|
val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController
|
||||||
|
controller.search(query)
|
||||||
|
}
|
||||||
|
is BrowseSourceController -> {
|
||||||
|
router.handleBack()
|
||||||
|
previousController.searchWithQuery(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manga info - end
|
||||||
|
|
||||||
|
// Chapters list - start
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates source migration for the specific manga.
|
||||||
|
*/
|
||||||
|
private fun migrateManga() {
|
||||||
|
val controller =
|
||||||
|
SearchController(
|
||||||
|
presenter.manga
|
||||||
|
)
|
||||||
|
controller.targetController = this
|
||||||
|
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
fun onNextChapters(chapters: List<ChapterItem>) {
|
fun onNextChapters(chapters: List<ChapterItem>) {
|
||||||
// If the list is empty and it hasn't requested previously, fetch chapters from source
|
// If the list is empty and it hasn't requested previously, fetch chapters from source
|
||||||
// We use presenter chapters instead because they are always unfiltered
|
// We use presenter chapters instead because they are always unfiltered
|
||||||
@ -270,13 +511,13 @@ class ChaptersController :
|
|||||||
fetchChaptersFromSource()
|
fetchChaptersFromSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
val adapter = adapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
adapter.updateDataSet(chapters)
|
adapter.updateDataSet(chapters)
|
||||||
|
|
||||||
if (selectedItems.isNotEmpty()) {
|
if (selectedChapters.isNotEmpty()) {
|
||||||
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
||||||
createActionModeIfNeeded()
|
createActionModeIfNeeded()
|
||||||
selectedItems.forEach { item ->
|
selectedChapters.forEach { item ->
|
||||||
val position = adapter.indexOf(item)
|
val position = adapter.indexOf(item)
|
||||||
if (position != -1 && !adapter.isSelected(position)) {
|
if (position != -1 && !adapter.isSelected(position)) {
|
||||||
adapter.toggleSelection(position)
|
adapter.toggleSelection(position)
|
||||||
@ -292,16 +533,20 @@ class ChaptersController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchChaptersFromSource(manualFetch: Boolean = false) {
|
private fun fetchChaptersFromSource(manualFetch: Boolean = false) {
|
||||||
binding.swipeRefresh.isRefreshing = true
|
isRefreshingChapters = true
|
||||||
|
updateRefreshing()
|
||||||
|
|
||||||
presenter.fetchChaptersFromSource(manualFetch)
|
presenter.fetchChaptersFromSource(manualFetch)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onFetchChaptersDone() {
|
fun onFetchChaptersDone() {
|
||||||
binding.swipeRefresh.isRefreshing = false
|
isRefreshingChapters = false
|
||||||
|
updateRefreshing()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onFetchChaptersError(error: Throwable) {
|
fun onFetchChaptersError(error: Throwable) {
|
||||||
binding.swipeRefresh.isRefreshing = false
|
isRefreshingChapters = false
|
||||||
|
updateRefreshing()
|
||||||
activity?.toast(error.message)
|
activity?.toast(error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +568,7 @@ class ChaptersController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||||
val adapter = adapter ?: return false
|
val adapter = chaptersAdapter ?: return false
|
||||||
val item = adapter.getItem(position) ?: return false
|
val item = adapter.getItem(position) ?: return false
|
||||||
return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
|
return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||||
lastClickPosition = position
|
lastClickPosition = position
|
||||||
@ -348,36 +593,36 @@ class ChaptersController :
|
|||||||
else -> setSelection(position)
|
else -> setSelection(position)
|
||||||
}
|
}
|
||||||
lastClickPosition = position
|
lastClickPosition = position
|
||||||
adapter?.notifyDataSetChanged()
|
chaptersAdapter?.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SELECTIONS & ACTION MODE
|
// SELECTIONS & ACTION MODE
|
||||||
|
|
||||||
private fun toggleSelection(position: Int) {
|
private fun toggleSelection(position: Int) {
|
||||||
val adapter = adapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
val item = adapter.getItem(position) ?: return
|
val item = adapter.getItem(position) ?: return
|
||||||
adapter.toggleSelection(position)
|
adapter.toggleSelection(position)
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
if (adapter.isSelected(position)) {
|
if (adapter.isSelected(position)) {
|
||||||
selectedItems.add(item)
|
selectedChapters.add(item)
|
||||||
} else {
|
} else {
|
||||||
selectedItems.remove(item)
|
selectedChapters.remove(item)
|
||||||
}
|
}
|
||||||
actionMode?.invalidate()
|
actionMode?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setSelection(position: Int) {
|
private fun setSelection(position: Int) {
|
||||||
val adapter = adapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
val item = adapter.getItem(position) ?: return
|
val item = adapter.getItem(position) ?: return
|
||||||
if (!adapter.isSelected(position)) {
|
if (!adapter.isSelected(position)) {
|
||||||
adapter.toggleSelection(position)
|
adapter.toggleSelection(position)
|
||||||
selectedItems.add(item)
|
selectedChapters.add(item)
|
||||||
actionMode?.invalidate()
|
actionMode?.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSelectedChapters(): List<ChapterItem> {
|
private fun getSelectedChapters(): List<ChapterItem> {
|
||||||
val adapter = adapter ?: return emptyList()
|
val adapter = chaptersAdapter ?: return emptyList()
|
||||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
|
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,12 +643,12 @@ class ChaptersController :
|
|||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
mode.menuInflater.inflate(R.menu.generic_selection, menu)
|
mode.menuInflater.inflate(R.menu.generic_selection, menu)
|
||||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
chaptersAdapter?.mode = SelectableAdapter.Mode.MULTI
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
val count = adapter?.selectedItemCount ?: 0
|
val count = chaptersAdapter?.selectedItemCount ?: 0
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
// Destroy action mode if there are no items selected.
|
// Destroy action mode if there are no items selected.
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
@ -448,9 +693,9 @@ class ChaptersController :
|
|||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
binding.actionToolbar.hide()
|
binding.actionToolbar.hide()
|
||||||
adapter?.mode = SelectableAdapter.Mode.SINGLE
|
chaptersAdapter?.mode = SelectableAdapter.Mode.SINGLE
|
||||||
adapter?.clearSelection()
|
chaptersAdapter?.clearSelection()
|
||||||
selectedItems.clear()
|
selectedChapters.clear()
|
||||||
actionMode = null
|
actionMode = null
|
||||||
|
|
||||||
// TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton]
|
// TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton]
|
||||||
@ -467,20 +712,20 @@ class ChaptersController :
|
|||||||
// SELECTION MODE ACTIONS
|
// SELECTION MODE ACTIONS
|
||||||
|
|
||||||
private fun selectAll() {
|
private fun selectAll() {
|
||||||
val adapter = adapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
adapter.selectAll()
|
adapter.selectAll()
|
||||||
selectedItems.addAll(adapter.items)
|
selectedChapters.addAll(adapter.items)
|
||||||
actionMode?.invalidate()
|
actionMode?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectInverse() {
|
private fun selectInverse() {
|
||||||
val adapter = adapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
|
|
||||||
selectedItems.clear()
|
selectedChapters.clear()
|
||||||
for (i in 0..adapter.itemCount) {
|
for (i in 0..adapter.itemCount) {
|
||||||
adapter.toggleSelection(i)
|
adapter.toggleSelection(i)
|
||||||
}
|
}
|
||||||
selectedItems.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) })
|
selectedChapters.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) })
|
||||||
|
|
||||||
actionMode?.invalidate()
|
actionMode?.invalidate()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
@ -521,7 +766,7 @@ class ChaptersController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun markPreviousAsRead(chapters: List<ChapterItem>) {
|
private fun markPreviousAsRead(chapters: List<ChapterItem>) {
|
||||||
val adapter = adapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
||||||
val chapterPos = prevChapters.indexOf(chapters.last())
|
val chapterPos = prevChapters.indexOf(chapters.last())
|
||||||
if (chapterPos != -1) {
|
if (chapterPos != -1) {
|
||||||
@ -545,9 +790,9 @@ class ChaptersController :
|
|||||||
fun onChaptersDeleted(chapters: List<ChapterItem>) {
|
fun onChaptersDeleted(chapters: List<ChapterItem>) {
|
||||||
// this is needed so the downloaded text gets removed from the item
|
// this is needed so the downloaded text gets removed from the item
|
||||||
chapters.forEach {
|
chapters.forEach {
|
||||||
adapter?.updateItem(it)
|
chaptersAdapter?.updateItem(it)
|
||||||
}
|
}
|
||||||
adapter?.notifyDataSetChanged()
|
chaptersAdapter?.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onChaptersDeletedError(error: Throwable) {
|
fun onChaptersDeletedError(error: Throwable) {
|
||||||
@ -558,7 +803,7 @@ class ChaptersController :
|
|||||||
|
|
||||||
private fun setDisplayMode(id: Int) {
|
private fun setDisplayMode(id: Int) {
|
||||||
presenter.setDisplayMode(id)
|
presenter.setDisplayMode(id)
|
||||||
adapter?.notifyDataSetChanged()
|
chaptersAdapter?.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUnreadChaptersSorted() = presenter.chapters
|
private fun getUnreadChaptersSorted() = presenter.chapters
|
||||||
@ -595,4 +840,6 @@ class ChaptersController :
|
|||||||
downloadChapters(chaptersToDownload)
|
downloadChapters(chaptersToDownload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chapters list - end
|
||||||
}
|
}
|
@ -1,11 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -14,8 +16,9 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
||||||
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
import java.util.Date
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -24,16 +27,20 @@ import timber.log.Timber
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class ChaptersPresenter(
|
class MangaInfoChaptersPresenter(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val source: Source,
|
val source: Source,
|
||||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
|
||||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
|
||||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get()
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
) : BasePresenter<ChaptersController>() {
|
private val coverCache: CoverCache = Injekt.get()
|
||||||
|
) : BasePresenter<MangaInfoChaptersController>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to update the manga from the source.
|
||||||
|
*/
|
||||||
|
private var fetchMangaSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
* List of chapters of the manga. It's always unfiltered and unsorted.
|
||||||
@ -67,10 +74,24 @@ class ChaptersPresenter(
|
|||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
// Manga info - start
|
||||||
|
|
||||||
|
getMangaObservable()
|
||||||
|
.subscribeLatestCache({ view, manga -> view.onNextMangaInfo(manga, source) })
|
||||||
|
|
||||||
|
// Update favorite status
|
||||||
|
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { setFavorite(it) }
|
||||||
|
.apply { add(this) }
|
||||||
|
|
||||||
// Prepare the relay.
|
// Prepare the relay.
|
||||||
chaptersRelay.flatMap { applyChapterFilters(it) }
|
chaptersRelay.flatMap { applyChapterFilters(it) }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeLatestCache(ChaptersController::onNextChapters) { _, error -> Timber.e(error) }
|
.subscribeLatestCache(MangaInfoChaptersController::onNextChapters) { _, error -> Timber.e(error) }
|
||||||
|
|
||||||
|
// Manga info - end
|
||||||
|
|
||||||
|
// Chapters list - start
|
||||||
|
|
||||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
||||||
// changes, and sends the list of chapters to the relay.
|
// changes, and sends the list of chapters to the relay.
|
||||||
@ -89,32 +110,130 @@ class ChaptersPresenter(
|
|||||||
|
|
||||||
// Listen for download status changes
|
// Listen for download status changes
|
||||||
observeDownloads()
|
observeDownloads()
|
||||||
|
|
||||||
// Emit the number of chapters to the info tab.
|
|
||||||
chapterCountRelay.call(
|
|
||||||
chapters.maxBy { it.chapter_number }?.chapter_number
|
|
||||||
?: 0f
|
|
||||||
)
|
|
||||||
|
|
||||||
// Emit the upload date of the most recent chapter
|
|
||||||
lastUpdateRelay.call(
|
|
||||||
Date(
|
|
||||||
chapters.maxBy { it.date_upload }?.date_upload
|
|
||||||
?: 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.subscribe { chaptersRelay.call(it) }
|
.subscribe { chaptersRelay.call(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Chapters list - end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manga info - start
|
||||||
|
|
||||||
|
private fun getMangaObservable(): Observable<Manga> {
|
||||||
|
return db.getManga(manga.url, manga.source).asRxObservable()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch manga information from source.
|
||||||
|
*/
|
||||||
|
fun fetchMangaFromSource(manualFetch: Boolean = false) {
|
||||||
|
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
||||||
|
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
||||||
|
.map { networkManga ->
|
||||||
|
manga.prepUpdateCover(coverCache, networkManga, manualFetch)
|
||||||
|
manga.copyFrom(networkManga)
|
||||||
|
manga.initialized = true
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeFirst(
|
||||||
|
{ view, _ ->
|
||||||
|
view.onFetchMangaInfoDone()
|
||||||
|
},
|
||||||
|
MangaInfoChaptersController::onFetchMangaInfoError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update favorite status of manga, (removes / adds) manga (to / from) library.
|
||||||
|
*
|
||||||
|
* @return the new status of the manga.
|
||||||
|
*/
|
||||||
|
fun toggleFavorite(): Boolean {
|
||||||
|
manga.favorite = !manga.favorite
|
||||||
|
if (!manga.favorite) {
|
||||||
|
manga.removeCovers(coverCache)
|
||||||
|
}
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
return manga.favorite
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setFavorite(favorite: Boolean) {
|
||||||
|
if (manga.favorite == favorite) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toggleFavorite()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the manga has any downloads.
|
||||||
|
*/
|
||||||
|
fun hasDownloads(): Boolean {
|
||||||
|
return downloadManager.getDownloadCount(manga) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all the downloads for the manga.
|
||||||
|
*/
|
||||||
|
fun deleteDownloads() {
|
||||||
|
downloadManager.deleteManga(manga, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user categories.
|
||||||
|
*
|
||||||
|
* @return List of categories, not including the default category
|
||||||
|
*/
|
||||||
|
fun getCategories(): List<Category> {
|
||||||
|
return db.getCategories().executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||||
|
*
|
||||||
|
* @param manga the manga to get categories from.
|
||||||
|
* @return Array of category ids the manga is in, if none returns default id
|
||||||
|
*/
|
||||||
|
fun getMangaCategoryIds(manga: Manga): Array<Int> {
|
||||||
|
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
return categories.mapNotNull { it.id }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the given manga to categories.
|
||||||
|
*
|
||||||
|
* @param manga the manga to move.
|
||||||
|
* @param categories the selected categories.
|
||||||
|
*/
|
||||||
|
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||||
|
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||||
|
db.setMangaCategories(mc, listOf(manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the given manga to the category.
|
||||||
|
*
|
||||||
|
* @param manga the manga to move.
|
||||||
|
* @param category the selected category, or null for default category.
|
||||||
|
*/
|
||||||
|
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||||
|
moveMangaToCategories(manga, listOfNotNull(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manga info - end
|
||||||
|
|
||||||
|
// Chapters list - start
|
||||||
|
|
||||||
private fun observeDownloads() {
|
private fun observeDownloads() {
|
||||||
observeDownloadsSubscription?.let { remove(it) }
|
observeDownloadsSubscription?.let { remove(it) }
|
||||||
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.filter { download -> download.manga.id == manga.id }
|
.filter { download -> download.manga.id == manga.id }
|
||||||
.doOnNext { onDownloadStatusChange(it) }
|
.doOnNext { onDownloadStatusChange(it) }
|
||||||
.subscribeLatestCache(ChaptersController::onChapterStatusChange) { _, error ->
|
.subscribeLatestCache(MangaInfoChaptersController::onChapterStatusChange) { _, error ->
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,7 +286,7 @@ class ChaptersPresenter(
|
|||||||
{ view, _ ->
|
{ view, _ ->
|
||||||
view.onFetchChaptersDone()
|
view.onFetchChaptersDone()
|
||||||
},
|
},
|
||||||
ChaptersController::onFetchChaptersError
|
MangaInfoChaptersController::onFetchChaptersError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +416,7 @@ class ChaptersPresenter(
|
|||||||
{ view, _ ->
|
{ view, _ ->
|
||||||
view.onChaptersDeleted(chapters)
|
view.onChaptersDeleted(chapters)
|
||||||
},
|
},
|
||||||
ChaptersController::onChaptersDeletedError
|
MangaInfoChaptersController::onChaptersDeletedError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,4 +565,6 @@ class ChaptersPresenter(
|
|||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean {
|
||||||
return manga.sortDescending()
|
return manga.sortDescending()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chapters list - end
|
||||||
}
|
}
|
@ -0,0 +1,316 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
|
import eu.kanade.tachiyomi.databinding.MangaInfoControllerBinding
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import eu.kanade.tachiyomi.util.view.gone
|
||||||
|
import eu.kanade.tachiyomi.util.view.setChips
|
||||||
|
import eu.kanade.tachiyomi.util.view.visible
|
||||||
|
import eu.kanade.tachiyomi.util.view.visibleIf
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
|
import reactivecircus.flowbinding.android.view.longClicks
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class MangaInfoHeaderAdapter(
|
||||||
|
private val controller: MangaInfoChaptersController,
|
||||||
|
private val fromSource: Boolean
|
||||||
|
) :
|
||||||
|
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
|
||||||
|
|
||||||
|
private var manga: Manga? = null
|
||||||
|
private var source: Source? = null
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
private lateinit var binding: MangaInfoControllerBinding
|
||||||
|
|
||||||
|
private var initialLoad: Boolean = true
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
|
||||||
|
binding = MangaInfoControllerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return HeaderViewHolder(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
|
||||||
|
holder.bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the view with manga information.
|
||||||
|
*
|
||||||
|
* @param manga manga object containing information about manga.
|
||||||
|
* @param source the source of the manga.
|
||||||
|
*/
|
||||||
|
fun update(manga: Manga, source: Source?) {
|
||||||
|
this.manga = manga
|
||||||
|
this.source = source
|
||||||
|
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
fun bind() {
|
||||||
|
if (manga == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For rounded corners
|
||||||
|
binding.mangaCover.clipToOutline = true
|
||||||
|
|
||||||
|
binding.btnFavorite.clicks()
|
||||||
|
.onEach { controller.onFavoriteClick() }
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
if (controller.presenter.manga.favorite && controller.presenter.getCategories().isNotEmpty()) {
|
||||||
|
binding.btnCategories.visible()
|
||||||
|
}
|
||||||
|
binding.btnCategories.clicks()
|
||||||
|
.onEach { controller.onCategoriesClick() }
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
if (controller.presenter.source is HttpSource) {
|
||||||
|
binding.btnWebview.visible()
|
||||||
|
binding.btnShare.visible()
|
||||||
|
|
||||||
|
binding.btnWebview.clicks()
|
||||||
|
.onEach { controller.openMangaInWebView() }
|
||||||
|
.launchIn(scope)
|
||||||
|
binding.btnShare.clicks()
|
||||||
|
.onEach { controller.shareManga() }
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.mangaFullTitle.longClicks()
|
||||||
|
.onEach {
|
||||||
|
controller.activity?.copyToClipboard(
|
||||||
|
view.context.getString(R.string.title),
|
||||||
|
binding.mangaFullTitle.text.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
binding.mangaFullTitle.clicks()
|
||||||
|
.onEach {
|
||||||
|
controller.performGlobalSearch(binding.mangaFullTitle.text.toString())
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
binding.mangaAuthor.longClicks()
|
||||||
|
.onEach {
|
||||||
|
controller.activity?.copyToClipboard(
|
||||||
|
binding.mangaAuthor.text.toString(),
|
||||||
|
binding.mangaAuthor.text.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
binding.mangaAuthor.clicks()
|
||||||
|
.onEach {
|
||||||
|
controller.performGlobalSearch(binding.mangaAuthor.text.toString())
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
binding.mangaSummary.longClicks()
|
||||||
|
.onEach {
|
||||||
|
controller.activity?.copyToClipboard(
|
||||||
|
view.context.getString(R.string.description),
|
||||||
|
binding.mangaSummary.text.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
binding.mangaCover.longClicks()
|
||||||
|
.onEach {
|
||||||
|
controller.activity?.copyToClipboard(
|
||||||
|
view.context.getString(R.string.title),
|
||||||
|
controller.presenter.manga.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
setMangaInfo(manga!!, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the view with manga information.
|
||||||
|
*
|
||||||
|
* @param manga manga object containing information about manga.
|
||||||
|
* @param source the source of the manga.
|
||||||
|
*/
|
||||||
|
private fun setMangaInfo(manga: Manga, source: Source?) {
|
||||||
|
// update full title TextView.
|
||||||
|
binding.mangaFullTitle.text = if (manga.title.isBlank()) {
|
||||||
|
view.context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
manga.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update author/artist TextView.
|
||||||
|
val authors = listOf(manga.author, manga.artist).filter { !it.isNullOrBlank() }.distinct()
|
||||||
|
binding.mangaAuthor.text = if (authors.isEmpty()) {
|
||||||
|
view.context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
authors.joinToString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If manga source is known update source TextView.
|
||||||
|
val mangaSource = source?.toString()
|
||||||
|
with(binding.mangaSource) {
|
||||||
|
if (mangaSource != null) {
|
||||||
|
text = mangaSource
|
||||||
|
setOnClickListener {
|
||||||
|
val sourceManager = Injekt.get<SourceManager>()
|
||||||
|
controller.performSearch(sourceManager.getOrStub(source.id).name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = view.context.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status TextView.
|
||||||
|
binding.mangaStatus.setText(
|
||||||
|
when (manga.status) {
|
||||||
|
SManga.ONGOING -> R.string.ongoing
|
||||||
|
SManga.COMPLETED -> R.string.completed
|
||||||
|
SManga.LICENSED -> R.string.licensed
|
||||||
|
else -> R.string.unknown
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set the favorite drawable to the correct one.
|
||||||
|
setFavoriteButtonState(manga.favorite)
|
||||||
|
|
||||||
|
// Set cover if it wasn't already.
|
||||||
|
val mangaThumbnail = manga.toMangaThumbnail()
|
||||||
|
|
||||||
|
GlideApp.with(view.context)
|
||||||
|
.load(mangaThumbnail)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
|
.centerCrop()
|
||||||
|
.into(binding.mangaCover)
|
||||||
|
|
||||||
|
binding.backdrop?.let {
|
||||||
|
GlideApp.with(view.context)
|
||||||
|
.load(mangaThumbnail)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
|
.centerCrop()
|
||||||
|
.into(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manga info section
|
||||||
|
if (manga.description.isNullOrBlank() && manga.genre.isNullOrBlank()) {
|
||||||
|
hideMangaInfo()
|
||||||
|
} else {
|
||||||
|
// Update description TextView.
|
||||||
|
binding.mangaSummary.text = if (manga.description.isNullOrBlank()) {
|
||||||
|
view.context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
manga.description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update genres list
|
||||||
|
if (!manga.genre.isNullOrBlank()) {
|
||||||
|
binding.mangaGenresTagsCompactChips.setChips(manga.getGenres(), controller::performSearch)
|
||||||
|
binding.mangaGenresTagsFullChips.setChips(manga.getGenres(), controller::performSearch)
|
||||||
|
} else {
|
||||||
|
binding.mangaGenresTagsWrapper.gone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle showing more or less info
|
||||||
|
binding.mangaSummary.clicks()
|
||||||
|
.onEach { toggleMangaInfo(view.context) }
|
||||||
|
.launchIn(scope)
|
||||||
|
binding.mangaInfoToggle.clicks()
|
||||||
|
.onEach { toggleMangaInfo(view.context) }
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
// Expand manga info if navigated from source listing
|
||||||
|
if (initialLoad && fromSource) {
|
||||||
|
toggleMangaInfo(view.context)
|
||||||
|
initialLoad = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnCategories.visibleIf { manga.favorite && controller.presenter.getCategories().isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideMangaInfo() {
|
||||||
|
binding.mangaSummaryLabel.gone()
|
||||||
|
binding.mangaSummary.gone()
|
||||||
|
binding.mangaGenresTagsWrapper.gone()
|
||||||
|
binding.mangaInfoToggle.gone()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleMangaInfo(context: Context) {
|
||||||
|
val isExpanded =
|
||||||
|
binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse)
|
||||||
|
|
||||||
|
binding.mangaInfoToggle.text =
|
||||||
|
if (isExpanded) {
|
||||||
|
context.getString(R.string.manga_info_expand)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.manga_info_collapse)
|
||||||
|
}
|
||||||
|
|
||||||
|
with(binding.mangaSummary) {
|
||||||
|
maxLines =
|
||||||
|
if (isExpanded) {
|
||||||
|
3
|
||||||
|
} else {
|
||||||
|
Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
ellipsize =
|
||||||
|
if (isExpanded) {
|
||||||
|
TextUtils.TruncateAt.END
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.mangaGenresTagsCompact.visibleIf { isExpanded }
|
||||||
|
binding.mangaGenresTagsFullChips.visibleIf { !isExpanded }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update favorite button with correct drawable and text.
|
||||||
|
*
|
||||||
|
* @param isFavorite determines if manga is favorite or not.
|
||||||
|
*/
|
||||||
|
private fun setFavoriteButtonState(isFavorite: Boolean) {
|
||||||
|
// Set the Favorite drawable to the correct one.
|
||||||
|
// Border drawable if false, filled drawable if true.
|
||||||
|
binding.btnFavorite.apply {
|
||||||
|
icon = ContextCompat.getDrawable(
|
||||||
|
context,
|
||||||
|
if (isFavorite) R.drawable.ic_favorite_24dp else R.drawable.ic_favorite_border_24dp
|
||||||
|
)
|
||||||
|
text =
|
||||||
|
context.getString(if (isFavorite) R.string.in_library else R.string.add_to_library)
|
||||||
|
isChecked = isFavorite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,585 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.databinding.MangaInfoControllerBinding
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
|
||||||
import eu.kanade.tachiyomi.ui.recent.history.HistoryController
|
|
||||||
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
|
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.setChips
|
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import eu.kanade.tachiyomi.util.view.visibleIf
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.util.Date
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
|
||||||
import reactivecircus.flowbinding.android.view.longClicks
|
|
||||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment that shows manga information.
|
|
||||||
* Uses R.layout.manga_info_controller.
|
|
||||||
* UI related actions should be called from here.
|
|
||||||
*/
|
|
||||||
class MangaInfoController(private val fromSource: Boolean = false) :
|
|
||||||
NucleusController<MangaInfoControllerBinding, MangaInfoPresenter>(),
|
|
||||||
ChangeMangaCategoriesDialog.Listener {
|
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
private val dateFormat: DateFormat by lazy {
|
|
||||||
preferences.dateFormat()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var initialLoad: Boolean = true
|
|
||||||
|
|
||||||
init {
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
setOptionsMenuHidden(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createPresenter(): MangaInfoPresenter {
|
|
||||||
val ctrl = parentController as MangaController
|
|
||||||
return MangaInfoPresenter(
|
|
||||||
ctrl.manga!!, ctrl.source!!,
|
|
||||||
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
|
||||||
binding = MangaInfoControllerBinding.inflate(inflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View) {
|
|
||||||
super.onViewCreated(view)
|
|
||||||
|
|
||||||
// For rounded corners
|
|
||||||
binding.mangaCover.clipToOutline = true
|
|
||||||
|
|
||||||
binding.btnFavorite.clicks()
|
|
||||||
.onEach { onFavoriteClick() }
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
if (presenter.manga.favorite && presenter.getCategories().isNotEmpty()) {
|
|
||||||
binding.btnCategories.visible()
|
|
||||||
}
|
|
||||||
binding.btnCategories.clicks()
|
|
||||||
.onEach { onCategoriesClick() }
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
if (presenter.source is HttpSource) {
|
|
||||||
binding.btnWebview.visible()
|
|
||||||
binding.btnShare.visible()
|
|
||||||
|
|
||||||
binding.btnWebview.clicks()
|
|
||||||
.onEach { openInWebView() }
|
|
||||||
.launchIn(scope)
|
|
||||||
binding.btnShare.clicks()
|
|
||||||
.onEach { shareManga() }
|
|
||||||
.launchIn(scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set SwipeRefresh to refresh manga data.
|
|
||||||
binding.swipeRefresh.refreshes()
|
|
||||||
.onEach { fetchMangaFromSource(manualFetch = true) }
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
binding.mangaFullTitle.longClicks()
|
|
||||||
.onEach {
|
|
||||||
activity?.copyToClipboard(
|
|
||||||
view.context.getString(R.string.title),
|
|
||||||
binding.mangaFullTitle.text.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
binding.mangaFullTitle.clicks()
|
|
||||||
.onEach {
|
|
||||||
performGlobalSearch(binding.mangaFullTitle.text.toString())
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
binding.mangaAuthor.longClicks()
|
|
||||||
.onEach {
|
|
||||||
activity?.copyToClipboard(
|
|
||||||
binding.mangaAuthor.text.toString(),
|
|
||||||
binding.mangaAuthor.text.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
binding.mangaAuthor.clicks()
|
|
||||||
.onEach {
|
|
||||||
performGlobalSearch(binding.mangaAuthor.text.toString())
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
binding.mangaSummary.longClicks()
|
|
||||||
.onEach {
|
|
||||||
activity?.copyToClipboard(
|
|
||||||
view.context.getString(R.string.description),
|
|
||||||
binding.mangaSummary.text.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
binding.mangaCover.longClicks()
|
|
||||||
.onEach {
|
|
||||||
activity?.copyToClipboard(
|
|
||||||
view.context.getString(R.string.title),
|
|
||||||
presenter.manga.title
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
inflater.inflate(R.menu.manga_info, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_migrate -> migrateManga()
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if manga is initialized.
|
|
||||||
* If true update view with manga information,
|
|
||||||
* if false fetch manga information
|
|
||||||
*
|
|
||||||
* @param manga manga object containing information about manga.
|
|
||||||
* @param source the source of the manga.
|
|
||||||
*/
|
|
||||||
fun onNextManga(manga: Manga, source: Source) {
|
|
||||||
if (manga.initialized) {
|
|
||||||
// Update view.
|
|
||||||
setMangaInfo(manga, source)
|
|
||||||
} else {
|
|
||||||
// Initialize manga.
|
|
||||||
fetchMangaFromSource()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the view with manga information.
|
|
||||||
*
|
|
||||||
* @param manga manga object containing information about manga.
|
|
||||||
* @param source the source of the manga.
|
|
||||||
*/
|
|
||||||
private fun setMangaInfo(manga: Manga, source: Source?) {
|
|
||||||
val view = view ?: return
|
|
||||||
|
|
||||||
// update full title TextView.
|
|
||||||
binding.mangaFullTitle.text = if (manga.title.isBlank()) {
|
|
||||||
view.context.getString(R.string.unknown)
|
|
||||||
} else {
|
|
||||||
manga.title
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update author/artist TextView.
|
|
||||||
val authors = listOf(manga.author, manga.artist).filter { !it.isNullOrBlank() }.distinct()
|
|
||||||
binding.mangaAuthor.text = if (authors.isEmpty()) {
|
|
||||||
view.context.getString(R.string.unknown)
|
|
||||||
} else {
|
|
||||||
authors.joinToString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If manga source is known update source TextView.
|
|
||||||
val mangaSource = source?.toString()
|
|
||||||
with(binding.mangaSource) {
|
|
||||||
if (mangaSource != null) {
|
|
||||||
text = mangaSource
|
|
||||||
setOnClickListener {
|
|
||||||
val sourceManager = Injekt.get<SourceManager>()
|
|
||||||
performSearch(sourceManager.getOrStub(source.id).name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text = view.context.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status TextView.
|
|
||||||
binding.mangaStatus.setText(
|
|
||||||
when (manga.status) {
|
|
||||||
SManga.ONGOING -> R.string.ongoing
|
|
||||||
SManga.COMPLETED -> R.string.completed
|
|
||||||
SManga.LICENSED -> R.string.licensed
|
|
||||||
else -> R.string.unknown
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Set the favorite drawable to the correct one.
|
|
||||||
setFavoriteButtonState(manga.favorite)
|
|
||||||
|
|
||||||
// Set cover if it wasn't already.
|
|
||||||
val mangaThumbnail = manga.toMangaThumbnail()
|
|
||||||
|
|
||||||
GlideApp.with(view.context)
|
|
||||||
.load(mangaThumbnail)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
|
||||||
.centerCrop()
|
|
||||||
.into(binding.mangaCover)
|
|
||||||
|
|
||||||
binding.backdrop?.let {
|
|
||||||
GlideApp.with(view.context)
|
|
||||||
.load(mangaThumbnail)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
|
||||||
.centerCrop()
|
|
||||||
.into(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manga info section
|
|
||||||
if (manga.description.isNullOrBlank() && manga.genre.isNullOrBlank()) {
|
|
||||||
hideMangaInfo()
|
|
||||||
} else {
|
|
||||||
// Update description TextView.
|
|
||||||
binding.mangaSummary.text = if (manga.description.isNullOrBlank()) {
|
|
||||||
view.context.getString(R.string.unknown)
|
|
||||||
} else {
|
|
||||||
manga.description
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update genres list
|
|
||||||
if (!manga.genre.isNullOrBlank()) {
|
|
||||||
binding.mangaGenresTagsCompactChips.setChips(manga.getGenres(), this::performSearch)
|
|
||||||
binding.mangaGenresTagsFullChips.setChips(manga.getGenres(), this::performSearch)
|
|
||||||
} else {
|
|
||||||
binding.mangaGenresTagsWrapper.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle showing more or less info
|
|
||||||
binding.mangaSummary.clicks()
|
|
||||||
.onEach { toggleMangaInfo(view.context) }
|
|
||||||
.launchIn(scope)
|
|
||||||
binding.mangaInfoToggle.clicks()
|
|
||||||
.onEach { toggleMangaInfo(view.context) }
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
// Expand manga info if navigated from source listing
|
|
||||||
if (initialLoad && fromSource) {
|
|
||||||
toggleMangaInfo(view.context)
|
|
||||||
initialLoad = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideMangaInfo() {
|
|
||||||
binding.mangaSummaryLabel.gone()
|
|
||||||
binding.mangaSummary.gone()
|
|
||||||
binding.mangaGenresTagsWrapper.gone()
|
|
||||||
binding.mangaInfoToggle.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleMangaInfo(context: Context) {
|
|
||||||
val isExpanded =
|
|
||||||
binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse)
|
|
||||||
|
|
||||||
binding.mangaInfoToggle.text =
|
|
||||||
if (isExpanded) {
|
|
||||||
context.getString(R.string.manga_info_expand)
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.manga_info_collapse)
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.mangaSummary) {
|
|
||||||
maxLines =
|
|
||||||
if (isExpanded) {
|
|
||||||
3
|
|
||||||
} else {
|
|
||||||
Int.MAX_VALUE
|
|
||||||
}
|
|
||||||
|
|
||||||
ellipsize =
|
|
||||||
if (isExpanded) {
|
|
||||||
TextUtils.TruncateAt.END
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.mangaGenresTagsCompact.visibleIf { isExpanded }
|
|
||||||
binding.mangaGenresTagsFullChips.visibleIf { !isExpanded }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update chapter count TextView.
|
|
||||||
*
|
|
||||||
* @param count number of chapters.
|
|
||||||
*/
|
|
||||||
fun setChapterCount(count: Float) {
|
|
||||||
if (count > 0f) {
|
|
||||||
binding.mangaChapters.text = DecimalFormat("#.#").format(count)
|
|
||||||
} else {
|
|
||||||
binding.mangaChapters.text = resources?.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLastUpdateDate(date: Date) {
|
|
||||||
if (date.time != 0L) {
|
|
||||||
binding.mangaLastUpdate.text = dateFormat.format(date)
|
|
||||||
} else {
|
|
||||||
binding.mangaLastUpdate.text = resources?.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the favorite status and asks for confirmation to delete downloaded chapters.
|
|
||||||
*/
|
|
||||||
private fun toggleFavorite() {
|
|
||||||
val view = view
|
|
||||||
|
|
||||||
val isNowFavorite = presenter.toggleFavorite()
|
|
||||||
if (view != null && !isNowFavorite && presenter.hasDownloads()) {
|
|
||||||
view.snack(view.context.getString(R.string.delete_downloads_for_manga)) {
|
|
||||||
setAction(R.string.action_delete) {
|
|
||||||
presenter.deleteDownloads()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnCategories.visibleIf { isNowFavorite && presenter.getCategories().isNotEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openInWebView() {
|
|
||||||
val source = presenter.source as? HttpSource ?: return
|
|
||||||
|
|
||||||
val url = try {
|
|
||||||
source.mangaDetailsRequest(presenter.manga).url.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val activity = activity ?: return
|
|
||||||
val intent = WebViewActivity.newIntent(activity, url, source.id, presenter.manga.title)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
|
|
||||||
*/
|
|
||||||
private fun shareManga() {
|
|
||||||
val context = view?.context ?: return
|
|
||||||
|
|
||||||
val source = presenter.source as? HttpSource ?: return
|
|
||||||
try {
|
|
||||||
val url = source.mangaDetailsRequest(presenter.manga).url.toString()
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "text/plain"
|
|
||||||
putExtra(Intent.EXTRA_TEXT, url)
|
|
||||||
}
|
|
||||||
startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.toast(e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update favorite button with correct drawable and text.
|
|
||||||
*
|
|
||||||
* @param isFavorite determines if manga is favorite or not.
|
|
||||||
*/
|
|
||||||
private fun setFavoriteButtonState(isFavorite: Boolean) {
|
|
||||||
// Set the Favorite drawable to the correct one.
|
|
||||||
// Border drawable if false, filled drawable if true.
|
|
||||||
binding.btnFavorite.apply {
|
|
||||||
icon = ContextCompat.getDrawable(
|
|
||||||
context,
|
|
||||||
if (isFavorite) R.drawable.ic_favorite_24dp else R.drawable.ic_favorite_border_24dp
|
|
||||||
)
|
|
||||||
text =
|
|
||||||
context.getString(if (isFavorite) R.string.in_library else R.string.add_to_library)
|
|
||||||
isChecked = isFavorite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start fetching manga information from source.
|
|
||||||
*/
|
|
||||||
private fun fetchMangaFromSource(manualFetch: Boolean = false) {
|
|
||||||
setRefreshing(true)
|
|
||||||
// Call presenter and start fetching manga information
|
|
||||||
presenter.fetchMangaFromSource(manualFetch)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update swipe refresh to stop showing refresh in progress spinner.
|
|
||||||
*/
|
|
||||||
fun onFetchMangaDone() {
|
|
||||||
setRefreshing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update swipe refresh to start showing refresh in progress spinner.
|
|
||||||
*/
|
|
||||||
fun onFetchMangaError(error: Throwable) {
|
|
||||||
setRefreshing(false)
|
|
||||||
activity?.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set swipe refresh status.
|
|
||||||
*
|
|
||||||
* @param value whether it should be refreshing or not.
|
|
||||||
*/
|
|
||||||
private fun setRefreshing(value: Boolean) {
|
|
||||||
binding.swipeRefresh.isRefreshing = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onFavoriteClick() {
|
|
||||||
val manga = presenter.manga
|
|
||||||
|
|
||||||
if (manga.favorite) {
|
|
||||||
toggleFavorite()
|
|
||||||
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
|
||||||
} else {
|
|
||||||
val categories = presenter.getCategories()
|
|
||||||
val defaultCategoryId = preferences.defaultCategory()
|
|
||||||
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
|
||||||
|
|
||||||
when {
|
|
||||||
// Default category set
|
|
||||||
defaultCategory != null -> {
|
|
||||||
toggleFavorite()
|
|
||||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
|
||||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Automatic 'Default' or no categories
|
|
||||||
defaultCategoryId == 0 || categories.isEmpty() -> {
|
|
||||||
toggleFavorite()
|
|
||||||
presenter.moveMangaToCategory(manga, null)
|
|
||||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose a category
|
|
||||||
else -> {
|
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
|
||||||
val preselected = ids.mapNotNull { id ->
|
|
||||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
|
||||||
.showDialog(router)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCategoriesClick() {
|
|
||||||
val manga = presenter.manga
|
|
||||||
val categories = presenter.getCategories()
|
|
||||||
|
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
|
||||||
val preselected = ids.mapNotNull { id ->
|
|
||||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
|
||||||
.showDialog(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
|
||||||
val manga = mangas.firstOrNull() ?: return
|
|
||||||
|
|
||||||
if (!manga.favorite) {
|
|
||||||
toggleFavorite()
|
|
||||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
|
||||||
}
|
|
||||||
|
|
||||||
presenter.moveMangaToCategories(manga, categories)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates source migration for the specific manga.
|
|
||||||
*/
|
|
||||||
private fun migrateManga() {
|
|
||||||
val controller =
|
|
||||||
SearchController(
|
|
||||||
presenter.manga
|
|
||||||
)
|
|
||||||
controller.targetController = this
|
|
||||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform a global search using the provided query.
|
|
||||||
*
|
|
||||||
* @param query the search query to pass to the search controller
|
|
||||||
*/
|
|
||||||
private fun performGlobalSearch(query: String) {
|
|
||||||
val router = parentController?.router ?: return
|
|
||||||
router.pushController(GlobalSearchController(query).withFadeTransaction())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform a search using the provided query.
|
|
||||||
*
|
|
||||||
* @param query the search query to the parent controller
|
|
||||||
*/
|
|
||||||
private fun performSearch(query: String) {
|
|
||||||
val router = parentController?.router ?: return
|
|
||||||
|
|
||||||
if (router.backstackSize < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (val previousController = router.backstack[router.backstackSize - 2].controller()) {
|
|
||||||
is LibraryController -> {
|
|
||||||
router.handleBack()
|
|
||||||
previousController.search(query)
|
|
||||||
}
|
|
||||||
is UpdatesController,
|
|
||||||
is HistoryController -> {
|
|
||||||
// Manually navigate to LibraryController
|
|
||||||
router.handleBack()
|
|
||||||
(router.activity as MainActivity).setSelectedNavItem(R.id.nav_library)
|
|
||||||
val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController
|
|
||||||
controller.search(query)
|
|
||||||
}
|
|
||||||
is BrowseSourceController -> {
|
|
||||||
router.handleBack()
|
|
||||||
previousController.searchWithQuery(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
|
||||||
import java.util.Date
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Presenter of MangaInfoFragment.
|
|
||||||
* Contains information and data for fragment.
|
|
||||||
* Observable updates should be called from here.
|
|
||||||
*/
|
|
||||||
class MangaInfoPresenter(
|
|
||||||
val manga: Manga,
|
|
||||||
val source: Source,
|
|
||||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
|
||||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
|
||||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
|
||||||
private val coverCache: CoverCache = Injekt.get()
|
|
||||||
) : BasePresenter<MangaInfoController>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to update the manga from the source.
|
|
||||||
*/
|
|
||||||
private var fetchMangaSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
getMangaObservable()
|
|
||||||
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
|
||||||
|
|
||||||
// Update chapter count
|
|
||||||
chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeLatestCache(MangaInfoController::setChapterCount)
|
|
||||||
|
|
||||||
// Update favorite status
|
|
||||||
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { setFavorite(it) }
|
|
||||||
.apply { add(this) }
|
|
||||||
|
|
||||||
// update last update date
|
|
||||||
lastUpdateRelay.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeLatestCache(MangaInfoController::setLastUpdateDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMangaObservable(): Observable<Manga> {
|
|
||||||
return db.getManga(manga.url, manga.source).asRxObservable()
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch manga information from source.
|
|
||||||
*/
|
|
||||||
fun fetchMangaFromSource(manualFetch: Boolean = false) {
|
|
||||||
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
|
||||||
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
|
||||||
.map { networkManga ->
|
|
||||||
manga.prepUpdateCover(coverCache, networkManga, manualFetch)
|
|
||||||
manga.copyFrom(networkManga)
|
|
||||||
manga.initialized = true
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst(
|
|
||||||
{ view, _ ->
|
|
||||||
view.onFetchMangaDone()
|
|
||||||
},
|
|
||||||
MangaInfoController::onFetchMangaError
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update favorite status of manga, (removes / adds) manga (to / from) library.
|
|
||||||
*
|
|
||||||
* @return the new status of the manga.
|
|
||||||
*/
|
|
||||||
fun toggleFavorite(): Boolean {
|
|
||||||
manga.favorite = !manga.favorite
|
|
||||||
if (!manga.favorite) {
|
|
||||||
manga.removeCovers(coverCache)
|
|
||||||
}
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
return manga.favorite
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setFavorite(favorite: Boolean) {
|
|
||||||
if (manga.favorite == favorite) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toggleFavorite()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the manga has any downloads.
|
|
||||||
*/
|
|
||||||
fun hasDownloads(): Boolean {
|
|
||||||
return downloadManager.getDownloadCount(manga) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes all the downloads for the manga.
|
|
||||||
*/
|
|
||||||
fun deleteDownloads() {
|
|
||||||
downloadManager.deleteManga(manga, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user categories.
|
|
||||||
*
|
|
||||||
* @return List of categories, not including the default category
|
|
||||||
*/
|
|
||||||
fun getCategories(): List<Category> {
|
|
||||||
return db.getCategories().executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
|
||||||
*
|
|
||||||
* @param manga the manga to get categories from.
|
|
||||||
* @return Array of category ids the manga is in, if none returns default id
|
|
||||||
*/
|
|
||||||
fun getMangaCategoryIds(manga: Manga): Array<Int> {
|
|
||||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
|
||||||
return categories.mapNotNull { it.id }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the given manga to categories.
|
|
||||||
*
|
|
||||||
* @param manga the manga to move.
|
|
||||||
* @param categories the selected categories.
|
|
||||||
*/
|
|
||||||
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
|
||||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
|
||||||
db.setMangaCategories(mc, listOf(manga))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the given manga to the category.
|
|
||||||
*
|
|
||||||
* @param manga the manga to move.
|
|
||||||
* @param category the selected category, or null for default category.
|
|
||||||
*/
|
|
||||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
|
||||||
moveMangaToCategories(manga, listOfNotNull(category))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@id/swipe_refresh"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.browse.source.browse.BrowseSourceController">
|
tools:context=".ui.browse.source.browse.BrowseSourceController">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/manga_cover"
|
android:id="@+id/manga_cover"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -58,103 +53,36 @@
|
|||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
|
app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
|
||||||
tools:text="Author" />
|
tools:text="Author" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_chapters_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="@string/manga_info_last_chapter_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_author" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_chapters"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintBaseline_toBaselineOf="@+id/manga_chapters_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_chapters_label" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_last_update_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/manga_info_latest_data_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_chapters_label" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_last_update"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintBaseline_toBaselineOf="@+id/manga_last_update_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_last_update_label" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_status_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/manga_info_status_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_last_update_label" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manga_status"
|
android:id="@+id/manga_status"
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
app:layout_constraintBaseline_toBaselineOf="@+id/manga_status_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_status_label" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_source_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/manga_info_source_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" />
|
app:layout_constraintTop_toBottomOf="@+id/manga_author" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manga_source"
|
android:id="@+id/manga_source"
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
app:layout_constraintBaseline_toBaselineOf="@+id/manga_source_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_source_label" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_status" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/actions_bar"
|
android:id="@+id/actions_bar"
|
||||||
@ -288,5 +216,3 @@
|
|||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>
|
|
||||||
|
@ -1,21 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@id/swipe_refresh"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.browse.source.browse.BrowseSourceController">
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="8dp">
|
android:paddingBottom="8dp"
|
||||||
|
tools:context=".ui.browse.source.browse.BrowseSourceController">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -84,104 +75,37 @@
|
|||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
|
app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
|
||||||
tools:text="Author" />
|
tools:text="Author" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_chapters_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="@string/manga_info_last_chapter_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_author" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_chapters"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintBaseline_toBaselineOf="@+id/manga_chapters_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_chapters_label" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_last_update_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/manga_info_latest_data_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_chapters_label" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_last_update"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintBaseline_toBaselineOf="@+id/manga_last_update_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_last_update_label" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_status_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/manga_info_status_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_last_update_label" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manga_status"
|
android:id="@+id/manga_status"
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
app:layout_constraintBaseline_toBaselineOf="@+id/manga_status_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_status_label" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_source_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/manga_info_source_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" />
|
app:layout_constraintTop_toBottomOf="@+id/manga_author" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manga_source"
|
android:id="@+id/manga_source"
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
app:layout_constraintBaseline_toBaselineOf="@+id/manga_source_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_source_label" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_status" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
@ -310,7 +234,3 @@
|
|||||||
android:textSize="12sp" />
|
android:textSize="12sp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
|
|
||||||
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>
|
|
||||||
|
@ -95,4 +95,9 @@
|
|||||||
android:title="@string/download_all" />
|
android:title="@string/download_all" />
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_migrate"
|
||||||
|
android:title="@string/action_migrate"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
<?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_migrate"
|
|
||||||
android:title="@string/action_migrate"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
</menu>
|
|
@ -458,12 +458,6 @@
|
|||||||
<string name="manga_info_full_title_label">Title</string>
|
<string name="manga_info_full_title_label">Title</string>
|
||||||
<string name="manga_added_library">Added to library</string>
|
<string name="manga_added_library">Added to library</string>
|
||||||
<string name="manga_removed_library">Removed from library</string>
|
<string name="manga_removed_library">Removed from library</string>
|
||||||
<string name="manga_info_chapters_label">Chapters</string>
|
|
||||||
<string name="manga_info_last_chapter_label">Last chapter</string>
|
|
||||||
<string name="manga_info_latest_data_label">Updated</string>
|
|
||||||
<string name="manga_info_status_label">Status</string>
|
|
||||||
<string name="manga_info_source_label">Source</string>
|
|
||||||
<string name="manga_info_genres_label">Genres</string>
|
|
||||||
<string name="manga_info_about_label">About</string>
|
<string name="manga_info_about_label">About</string>
|
||||||
<string name="manga_info_expand">Show more info</string>
|
<string name="manga_info_expand">Show more info</string>
|
||||||
<string name="manga_info_collapse">Show less info</string>
|
<string name="manga_info_collapse">Show less info</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user