Merge manga info and chapters views

This commit is contained in:
arkon 2020-06-18 22:15:05 -04:00
parent a768280d82
commit 4605e14729
12 changed files with 1152 additions and 1401 deletions

View File

@ -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
} }
} }

View File

@ -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) {

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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))
}
}

View File

@ -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"
@ -287,6 +215,4 @@
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>

View File

@ -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>
@ -309,8 +233,4 @@
android:text="@string/manga_info_expand" android:text="@string/manga_info_expand"
android:textSize="12sp" /> android:textSize="12sp" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView>
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>

View File

@ -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>

View File

@ -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>

View File

@ -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>