mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Merge manga info and chapters views
This commit is contained in:
		| @@ -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.TabbedController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController | ||||
| import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.MangaInfoChaptersController | ||||
| import eu.kanade.tachiyomi.ui.manga.track.TrackController | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import java.util.Date | ||||
| import kotlinx.android.synthetic.main.main_activity.tabs | ||||
| import rx.Subscription | ||||
| import uy.kohesive.injekt.Injekt | ||||
| @@ -65,10 +63,6 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
|  | ||||
|     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() | ||||
|  | ||||
|     private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create() | ||||
| @@ -92,17 +86,12 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
|         requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) | ||||
|  | ||||
|         adapter = MangaDetailAdapter() | ||||
|         binding.pager.offscreenPageLimit = 3 | ||||
|         binding.pager.adapter = adapter | ||||
|  | ||||
|         if (!fromSource) { | ||||
|             binding.pager.currentItem = CHAPTERS_CONTROLLER | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         super.onDestroyView(view) | ||||
|         adapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
| @@ -150,15 +139,14 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
|  | ||||
|     private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { | ||||
|  | ||||
|         private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2 | ||||
|  | ||||
|         private val tabTitles = listOf( | ||||
|             R.string.manga_detail_tab, | ||||
|             R.string.manga_chapters_tab, | ||||
|             R.string.manga_tracking_tab | ||||
|         ) | ||||
|             .map { resources!!.getString(it) } | ||||
|  | ||||
|         private val tabCount = tabTitles.size - if (Injekt.get<TrackManager>().hasLoggedServices()) 0 else 1 | ||||
|  | ||||
|         override fun getCount(): Int { | ||||
|             return tabCount | ||||
|         } | ||||
| @@ -166,8 +154,7 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
|         override fun configureRouter(router: Router, position: Int) { | ||||
|             if (!router.hasRootController()) { | ||||
|                 val controller = when (position) { | ||||
|                     INFO_CONTROLLER -> MangaInfoController(fromSource) | ||||
|                     CHAPTERS_CONTROLLER -> ChaptersController() | ||||
|                     INFO_CHAPTERS_CONTROLLER -> MangaInfoChaptersController(fromSource) | ||||
|                     TRACK_CONTROLLER -> TrackController() | ||||
|                     else -> error("Wrong position $position") | ||||
|                 } | ||||
| @@ -184,8 +171,7 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
|         const val FROM_SOURCE_EXTRA = "from_source" | ||||
|         const val MANGA_EXTRA = "manga" | ||||
|  | ||||
|         const val INFO_CONTROLLER = 0 | ||||
|         const val CHAPTERS_CONTROLLER = 1 | ||||
|         const val TRACK_CONTROLLER = 2 | ||||
|         const val INFO_CHAPTERS_CONTROLLER = 0 | ||||
|         const val TRACK_CONTROLLER = 1 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import java.text.DecimalFormatSymbols | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class ChaptersAdapter( | ||||
|     controller: ChaptersController, | ||||
|     controller: MangaInfoChaptersController, | ||||
|     context: Context | ||||
| ) : FlexibleAdapter<ChapterItem>(null, controller, true) { | ||||
|  | ||||
|   | ||||
| @@ -15,19 +15,34 @@ import androidx.appcompat.view.ActionMode | ||||
| import androidx.core.graphics.drawable.DrawableCompat | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.MergeAdapter | ||||
| import com.google.android.material.snackbar.Snackbar | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.SelectableAdapter | ||||
| 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.Manga | ||||
| 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.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.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.manga.MangaController | ||||
| 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.toast | ||||
| 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.swiperefreshlayout.refreshes | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| 
 | ||||
| class ChaptersController : | ||||
|     NucleusController<ChaptersControllerBinding, ChaptersPresenter>(), | ||||
| class MangaInfoChaptersController(private val fromSource: Boolean = false) : | ||||
|     NucleusController<ChaptersControllerBinding, MangaInfoChaptersPresenter>(), | ||||
|     ActionMode.Callback, | ||||
|     FlexibleAdapter.OnItemClickListener, | ||||
|     FlexibleAdapter.OnItemLongClickListener, | ||||
|     ChangeMangaCategoriesDialog.Listener, | ||||
|     DownloadCustomChaptersDialog.Listener, | ||||
|     DeleteChaptersDialog.Listener { | ||||
| 
 | ||||
|     /** | ||||
|      * Adapter containing a list of chapters. | ||||
|      */ | ||||
|     private var adapter: ChaptersAdapter? = null | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
| 
 | ||||
|     private var headerAdapter: MangaInfoHeaderAdapter? = null | ||||
|     private var chaptersAdapter: ChaptersAdapter? = null | ||||
| 
 | ||||
|     /** | ||||
|      * Action mode for multiple selection. | ||||
| @@ -62,20 +79,22 @@ class ChaptersController : | ||||
|     /** | ||||
|      * 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 isRefreshingInfo = false | ||||
|     private var isRefreshingChapters = false | ||||
| 
 | ||||
|     init { | ||||
|         setHasOptionsMenu(true) | ||||
|         setOptionsMenuHidden(true) | ||||
|     } | ||||
| 
 | ||||
|     override fun createPresenter(): ChaptersPresenter { | ||||
|     override fun createPresenter(): MangaInfoChaptersPresenter { | ||||
|         val ctrl = parentController as MangaController | ||||
|         return ChaptersPresenter( | ||||
|             ctrl.manga!!, ctrl.source!!, | ||||
|             ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay | ||||
|         return MangaInfoChaptersPresenter( | ||||
|             ctrl.manga!!, ctrl.source!!, ctrl.mangaFavoriteRelay | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| @@ -91,16 +110,20 @@ class ChaptersController : | ||||
|         if (ctrl.manga == null || ctrl.source == null) return | ||||
| 
 | ||||
|         // 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.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) | ||||
|         binding.recycler.setHasFixedSize(true) | ||||
|         adapter?.fastScroller = binding.fastScroller | ||||
|         chaptersAdapter?.fastScroller = binding.fastScroller | ||||
| 
 | ||||
|         binding.swipeRefresh.refreshes() | ||||
|             .onEach { fetchChaptersFromSource(manualFetch = true) } | ||||
|             .onEach { | ||||
|                 fetchMangaInfoFromSource(manualFetch = true) | ||||
|                 fetchChaptersFromSource(manualFetch = true) | ||||
|             } | ||||
|             .launchIn(scope) | ||||
| 
 | ||||
|         binding.fab.clicks() | ||||
| @@ -134,7 +157,8 @@ class ChaptersController : | ||||
|     override fun onDestroyView(view: View) { | ||||
|         destroyActionModeIfNeeded() | ||||
|         binding.actionToolbar.destroy() | ||||
|         adapter = null | ||||
|         headerAdapter = null | ||||
|         chaptersAdapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
| 
 | ||||
| @@ -171,7 +195,6 @@ class ChaptersController : | ||||
|         menuFilterBookmarked.isChecked = presenter.onlyBookmarked() | ||||
| 
 | ||||
|         val filterSet = presenter.onlyRead() || presenter.onlyUnread() || presenter.onlyDownloaded() || presenter.onlyBookmarked() | ||||
| 
 | ||||
|         if (filterSet) { | ||||
|             val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) | ||||
|             DrawableCompat.setTint(menu.findItem(R.id.action_filter).icon, filterColor) | ||||
| @@ -259,10 +282,228 @@ class ChaptersController : | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_sort -> presenter.revertSortOrder() | ||||
| 
 | ||||
|             R.id.action_migrate -> migrateManga() | ||||
|         } | ||||
|         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>) { | ||||
|         // 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 | ||||
| @@ -270,13 +511,13 @@ class ChaptersController : | ||||
|             fetchChaptersFromSource() | ||||
|         } | ||||
| 
 | ||||
|         val adapter = adapter ?: return | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         adapter.updateDataSet(chapters) | ||||
| 
 | ||||
|         if (selectedItems.isNotEmpty()) { | ||||
|         if (selectedChapters.isNotEmpty()) { | ||||
|             adapter.clearSelection() // we need to start from a clean state, index may have changed | ||||
|             createActionModeIfNeeded() | ||||
|             selectedItems.forEach { item -> | ||||
|             selectedChapters.forEach { item -> | ||||
|                 val position = adapter.indexOf(item) | ||||
|                 if (position != -1 && !adapter.isSelected(position)) { | ||||
|                     adapter.toggleSelection(position) | ||||
| @@ -292,16 +533,20 @@ class ChaptersController : | ||||
|     } | ||||
| 
 | ||||
|     private fun fetchChaptersFromSource(manualFetch: Boolean = false) { | ||||
|         binding.swipeRefresh.isRefreshing = true | ||||
|         isRefreshingChapters = true | ||||
|         updateRefreshing() | ||||
| 
 | ||||
|         presenter.fetchChaptersFromSource(manualFetch) | ||||
|     } | ||||
| 
 | ||||
|     fun onFetchChaptersDone() { | ||||
|         binding.swipeRefresh.isRefreshing = false | ||||
|         isRefreshingChapters = false | ||||
|         updateRefreshing() | ||||
|     } | ||||
| 
 | ||||
|     fun onFetchChaptersError(error: Throwable) { | ||||
|         binding.swipeRefresh.isRefreshing = false | ||||
|         isRefreshingChapters = false | ||||
|         updateRefreshing() | ||||
|         activity?.toast(error.message) | ||||
|     } | ||||
| 
 | ||||
| @@ -323,7 +568,7 @@ class ChaptersController : | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|         return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { | ||||
|             lastClickPosition = position | ||||
| @@ -348,36 +593,36 @@ class ChaptersController : | ||||
|             else -> setSelection(position) | ||||
|         } | ||||
|         lastClickPosition = position | ||||
|         adapter?.notifyDataSetChanged() | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
| 
 | ||||
|     // SELECTIONS & ACTION MODE | ||||
| 
 | ||||
|     private fun toggleSelection(position: Int) { | ||||
|         val adapter = adapter ?: return | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         val item = adapter.getItem(position) ?: return | ||||
|         adapter.toggleSelection(position) | ||||
|         adapter.notifyDataSetChanged() | ||||
|         if (adapter.isSelected(position)) { | ||||
|             selectedItems.add(item) | ||||
|             selectedChapters.add(item) | ||||
|         } else { | ||||
|             selectedItems.remove(item) | ||||
|             selectedChapters.remove(item) | ||||
|         } | ||||
|         actionMode?.invalidate() | ||||
|     } | ||||
| 
 | ||||
|     private fun setSelection(position: Int) { | ||||
|         val adapter = adapter ?: return | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         val item = adapter.getItem(position) ?: return | ||||
|         if (!adapter.isSelected(position)) { | ||||
|             adapter.toggleSelection(position) | ||||
|             selectedItems.add(item) | ||||
|             selectedChapters.add(item) | ||||
|             actionMode?.invalidate() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getSelectedChapters(): List<ChapterItem> { | ||||
|         val adapter = adapter ?: return emptyList() | ||||
|         val adapter = chaptersAdapter ?: return emptyList() | ||||
|         return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } | ||||
|     } | ||||
| 
 | ||||
| @@ -398,12 +643,12 @@ class ChaptersController : | ||||
| 
 | ||||
|     override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { | ||||
|         mode.menuInflater.inflate(R.menu.generic_selection, menu) | ||||
|         adapter?.mode = SelectableAdapter.Mode.MULTI | ||||
|         chaptersAdapter?.mode = SelectableAdapter.Mode.MULTI | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { | ||||
|         val count = adapter?.selectedItemCount ?: 0 | ||||
|         val count = chaptersAdapter?.selectedItemCount ?: 0 | ||||
|         if (count == 0) { | ||||
|             // Destroy action mode if there are no items selected. | ||||
|             destroyActionModeIfNeeded() | ||||
| @@ -448,9 +693,9 @@ class ChaptersController : | ||||
| 
 | ||||
|     override fun onDestroyActionMode(mode: ActionMode) { | ||||
|         binding.actionToolbar.hide() | ||||
|         adapter?.mode = SelectableAdapter.Mode.SINGLE | ||||
|         adapter?.clearSelection() | ||||
|         selectedItems.clear() | ||||
|         chaptersAdapter?.mode = SelectableAdapter.Mode.SINGLE | ||||
|         chaptersAdapter?.clearSelection() | ||||
|         selectedChapters.clear() | ||||
|         actionMode = null | ||||
| 
 | ||||
|         // TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton] | ||||
| @@ -467,20 +712,20 @@ class ChaptersController : | ||||
|     // SELECTION MODE ACTIONS | ||||
| 
 | ||||
|     private fun selectAll() { | ||||
|         val adapter = adapter ?: return | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         adapter.selectAll() | ||||
|         selectedItems.addAll(adapter.items) | ||||
|         selectedChapters.addAll(adapter.items) | ||||
|         actionMode?.invalidate() | ||||
|     } | ||||
| 
 | ||||
|     private fun selectInverse() { | ||||
|         val adapter = adapter ?: return | ||||
|         val adapter = chaptersAdapter ?: return | ||||
| 
 | ||||
|         selectedItems.clear() | ||||
|         selectedChapters.clear() | ||||
|         for (i in 0..adapter.itemCount) { | ||||
|             adapter.toggleSelection(i) | ||||
|         } | ||||
|         selectedItems.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) | ||||
|         selectedChapters.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) | ||||
| 
 | ||||
|         actionMode?.invalidate() | ||||
|         adapter.notifyDataSetChanged() | ||||
| @@ -521,7 +766,7 @@ class ChaptersController : | ||||
|     } | ||||
| 
 | ||||
|     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 chapterPos = prevChapters.indexOf(chapters.last()) | ||||
|         if (chapterPos != -1) { | ||||
| @@ -545,9 +790,9 @@ class ChaptersController : | ||||
|     fun onChaptersDeleted(chapters: List<ChapterItem>) { | ||||
|         // this is needed so the downloaded text gets removed from the item | ||||
|         chapters.forEach { | ||||
|             adapter?.updateItem(it) | ||||
|             chaptersAdapter?.updateItem(it) | ||||
|         } | ||||
|         adapter?.notifyDataSetChanged() | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
| 
 | ||||
|     fun onChaptersDeletedError(error: Throwable) { | ||||
| @@ -558,7 +803,7 @@ class ChaptersController : | ||||
| 
 | ||||
|     private fun setDisplayMode(id: Int) { | ||||
|         presenter.setDisplayMode(id) | ||||
|         adapter?.notifyDataSetChanged() | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
| 
 | ||||
|     private fun getUnreadChaptersSorted() = presenter.chapters | ||||
| @@ -595,4 +840,6 @@ class ChaptersController : | ||||
|             downloadChapters(chaptersToDownload) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Chapters list - end | ||||
| } | ||||
| @@ -1,11 +1,13 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
| 
 | ||||
| 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.Chapter | ||||
| 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.model.Download | ||||
| 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.isLocal | ||||
| 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 java.util.Date | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| @@ -24,16 +27,20 @@ import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| 
 | ||||
| class ChaptersPresenter( | ||||
| class MangaInfoChaptersPresenter( | ||||
|     val manga: Manga, | ||||
|     val source: Source, | ||||
|     private val chapterCountRelay: BehaviorRelay<Float>, | ||||
|     private val lastUpdateRelay: BehaviorRelay<Date>, | ||||
|     private val mangaFavoriteRelay: PublishRelay<Boolean>, | ||||
|     val preferences: PreferencesHelper = Injekt.get(), | ||||
|     private val db: DatabaseHelper = Injekt.get(), | ||||
|     private val downloadManager: DownloadManager = Injekt.get() | ||||
| ) : BasePresenter<ChaptersController>() { | ||||
|     private val downloadManager: DownloadManager = Injekt.get(), | ||||
|     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. | ||||
| @@ -67,10 +74,24 @@ class ChaptersPresenter( | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         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. | ||||
|         chaptersRelay.flatMap { applyChapterFilters(it) } | ||||
|             .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 | ||||
|         // changes, and sends the list of chapters to the relay. | ||||
| @@ -89,32 +110,130 @@ class ChaptersPresenter( | ||||
| 
 | ||||
|                     // Listen for download status changes | ||||
|                     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) } | ||||
|         ) | ||||
| 
 | ||||
|         // 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() { | ||||
|         observeDownloadsSubscription?.let { remove(it) } | ||||
|         observeDownloadsSubscription = downloadManager.queue.getStatusObservable() | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .filter { download -> download.manga.id == manga.id } | ||||
|             .doOnNext { onDownloadStatusChange(it) } | ||||
|             .subscribeLatestCache(ChaptersController::onChapterStatusChange) { _, error -> | ||||
|             .subscribeLatestCache(MangaInfoChaptersController::onChapterStatusChange) { _, error -> | ||||
|                 Timber.e(error) | ||||
|             } | ||||
|     } | ||||
| @@ -167,7 +286,7 @@ class ChaptersPresenter( | ||||
|                 { view, _ -> | ||||
|                     view.onFetchChaptersDone() | ||||
|                 }, | ||||
|                 ChaptersController::onFetchChaptersError | ||||
|                 MangaInfoChaptersController::onFetchChaptersError | ||||
|             ) | ||||
|     } | ||||
| 
 | ||||
| @@ -297,7 +416,7 @@ class ChaptersPresenter( | ||||
|                 { view, _ -> | ||||
|                     view.onChaptersDeleted(chapters) | ||||
|                 }, | ||||
|                 ChaptersController::onChaptersDeletedError | ||||
|                 MangaInfoChaptersController::onChaptersDeletedError | ||||
|             ) | ||||
|     } | ||||
| 
 | ||||
| @@ -446,4 +565,6 @@ class ChaptersPresenter( | ||||
|     fun sortDescending(): Boolean { | ||||
|         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)) | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user