mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Move tracking to manga info actions
Currently just opens a separate view. To be iterated upon later.
This commit is contained in:
		| @@ -75,7 +75,6 @@ open class GlobalSearchController( | ||||
|      * @param manga clicked item containing manga information. | ||||
|      */ | ||||
|     override fun onMangaClick(manga: Manga) { | ||||
|         // Open MangaController. | ||||
|         router.pushController(MangaController(manga, true).withFadeTransaction()) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,37 +1,84 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga | ||||
|  | ||||
| import android.Manifest.permission.WRITE_EXTERNAL_STORAGE | ||||
| import android.animation.Animator | ||||
| import android.animation.AnimatorListenerAdapter | ||||
| import android.app.Activity | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| 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.vectordrawable.graphics.drawable.VectorDrawableCompat | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.appcompat.view.ActionMode | ||||
| import androidx.core.graphics.drawable.DrawableCompat | ||||
| import androidx.recyclerview.widget.ConcatAdapter | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import com.bluelinelabs.conductor.Router | ||||
| import com.bluelinelabs.conductor.RouterTransaction | ||||
| import com.bluelinelabs.conductor.support.RouterPagerAdapter | ||||
| import com.google.android.material.tabs.TabLayout | ||||
| import com.jakewharton.rxrelay.BehaviorRelay | ||||
| 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.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.track.TrackManager | ||||
| import eu.kanade.tachiyomi.databinding.PagerControllerBinding | ||||
| 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.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.MangaInfoChaptersController | ||||
| 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.chapter.ChapterHolder | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.DeleteChaptersDialog | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter | ||||
| import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter | ||||
| import eu.kanade.tachiyomi.ui.manga.track.TrackController | ||||
| 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 kotlinx.android.synthetic.main.main_activity.tabs | ||||
| import rx.Subscription | ||||
| import eu.kanade.tachiyomi.util.view.getCoordinates | ||||
| import eu.kanade.tachiyomi.util.view.gone | ||||
| import eu.kanade.tachiyomi.util.view.shrinkOnScroll | ||||
| import eu.kanade.tachiyomi.util.view.snack | ||||
| import eu.kanade.tachiyomi.util.view.visible | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import reactivecircus.flowbinding.android.view.clicks | ||||
| import reactivecircus.flowbinding.swiperefreshlayout.refreshes | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
| class MangaController : | ||||
|     NucleusController<ChaptersControllerBinding, MangaPresenter>, | ||||
|     ActionMode.Callback, | ||||
|     FlexibleAdapter.OnItemClickListener, | ||||
|     FlexibleAdapter.OnItemLongClickListener, | ||||
|     ChangeMangaCategoriesDialog.Listener, | ||||
|     DownloadCustomChaptersDialog.Listener, | ||||
|     DeleteChaptersDialog.Listener { | ||||
|  | ||||
|     constructor(manga: Manga?, fromSource: Boolean = false) : super( | ||||
|         Bundle().apply { | ||||
| @@ -58,20 +105,48 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
|     var source: Source? = null | ||||
|         private set | ||||
|  | ||||
|     private var adapter: MangaDetailAdapter? = null | ||||
|     private val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false) | ||||
|  | ||||
|     val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false) | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create() | ||||
|     private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null | ||||
|     private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null | ||||
|     private var chaptersAdapter: ChaptersAdapter? = null | ||||
|  | ||||
|     private var trackingIconSubscription: Subscription? = null | ||||
|     /** | ||||
|      * Action mode for multiple selection. | ||||
|      */ | ||||
|     private var actionMode: ActionMode? = null | ||||
|  | ||||
|     /** | ||||
|      * Selected items. Used to restore selections after a rotation. | ||||
|      */ | ||||
|     private val selectedChapters = mutableSetOf<ChapterItem>() | ||||
|  | ||||
|     private val isLocalSource by lazy { presenter.source.id == LocalSource.ID } | ||||
|  | ||||
|     private var lastClickPosition = -1 | ||||
|  | ||||
|     private var isRefreshingInfo = false | ||||
|     private var isRefreshingChapters = false | ||||
|  | ||||
|     init { | ||||
|         setHasOptionsMenu(true) | ||||
|     } | ||||
|  | ||||
|     override fun getTitle(): String? { | ||||
|         return manga?.title | ||||
|     } | ||||
|  | ||||
|     override fun createPresenter(): MangaPresenter { | ||||
|         return MangaPresenter( | ||||
|             manga!!, | ||||
|             source!! | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { | ||||
|         binding = PagerControllerBinding.inflate(inflater) | ||||
|         binding = ChaptersControllerBinding.inflate(inflater) | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
| @@ -80,23 +155,65 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
|  | ||||
|         if (manga == null || source == null) return | ||||
|  | ||||
|         requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) | ||||
|         // Init RecyclerView and adapter | ||||
|         mangaInfoAdapter = | ||||
|             MangaInfoHeaderAdapter( | ||||
|                 this, | ||||
|                 fromSource | ||||
|             ) | ||||
|         chaptersHeaderAdapter = | ||||
|             MangaChaptersHeaderAdapter() | ||||
|         chaptersAdapter = ChaptersAdapter( | ||||
|             this, | ||||
|             view.context | ||||
|         ) | ||||
|  | ||||
|         adapter = MangaDetailAdapter() | ||||
|         binding.pager.adapter = adapter | ||||
|     } | ||||
|         binding.recycler.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter) | ||||
|         binding.recycler.layoutManager = LinearLayoutManager(view.context) | ||||
|         binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) | ||||
|         binding.recycler.setHasFixedSize(true) | ||||
|         chaptersAdapter?.fastScroller = binding.fastScroller | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         adapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
|         super.onChangeStarted(handler, type) | ||||
|         if (type.isEnter) { | ||||
|             activity?.tabs?.setupWithViewPager(binding.pager) | ||||
|             trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } | ||||
|         // Skips directly to chapters list if navigated to from the library | ||||
|         binding.recycler.post { | ||||
|             if (!fromSource && preferences.jumpToChapters()) { | ||||
|                 (binding.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(1, 0) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         binding.swipeRefresh.refreshes() | ||||
|             .onEach { | ||||
|                 fetchMangaInfoFromSource(manualFetch = true) | ||||
|                 fetchChaptersFromSource(manualFetch = true) | ||||
|             } | ||||
|             .launchIn(scope) | ||||
|  | ||||
|         binding.fab.clicks() | ||||
|             .onEach { | ||||
|                 val item = presenter.getNextUnreadChapter() | ||||
|                 if (item != null) { | ||||
|                     // Create animation listener | ||||
|                     val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { | ||||
|                         override fun onAnimationStart(animation: Animator?) { | ||||
|                             openChapter(item.chapter, true) | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     // Get coordinates and start animation | ||||
|                     val coordinates = binding.fab.getCoordinates() | ||||
|                     if (!binding.revealView.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { | ||||
|                         openChapter(item.chapter) | ||||
|                     } | ||||
|                 } else { | ||||
|                     view.context.toast(R.string.no_next_chapter) | ||||
|                 } | ||||
|             } | ||||
|             .launchIn(scope) | ||||
|  | ||||
|         binding.fab.shrinkOnScroll(binding.recycler) | ||||
|  | ||||
|         binding.actionToolbar.offsetAppbarHeight(activity!!) | ||||
|         binding.fab.offsetAppbarHeight(activity!!) | ||||
|     } | ||||
|  | ||||
|     override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
| @@ -107,68 +224,704 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun configureTabs(tabs: TabLayout) { | ||||
|         with(tabs) { | ||||
|             tabGravity = TabLayout.GRAVITY_FILL | ||||
|             tabMode = TabLayout.MODE_FIXED | ||||
|     override fun onDestroyView(view: View) { | ||||
|         destroyActionModeIfNeeded() | ||||
|         binding.actionToolbar.destroy() | ||||
|         mangaInfoAdapter = null | ||||
|         chaptersHeaderAdapter = null | ||||
|         chaptersAdapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     override fun onActivityResumed(activity: Activity) { | ||||
|         if (view == null) return | ||||
|  | ||||
|         // Check if animation view is visible | ||||
|         if (binding.revealView.visibility == View.VISIBLE) { | ||||
|             // Show the unreveal effect | ||||
|             val coordinates = binding.fab.getCoordinates() | ||||
|             binding.revealView.hideRevealEffect(coordinates.x, coordinates.y, 1920) | ||||
|         } | ||||
|  | ||||
|         super.onActivityResumed(activity) | ||||
|     } | ||||
|  | ||||
|     override fun cleanupTabs(tabs: TabLayout) { | ||||
|         trackingIconSubscription?.unsubscribe() | ||||
|         setTrackingIconInternal(false) | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         inflater.inflate(R.menu.chapters, menu) | ||||
|     } | ||||
|  | ||||
|     fun setTrackingIcon(visible: Boolean) { | ||||
|         trackingIconRelay.call(visible) | ||||
|     } | ||||
|     override fun onPrepareOptionsMenu(menu: Menu) { | ||||
|         // Initialize menu items. | ||||
|         val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return | ||||
|         val menuFilterUnread = menu.findItem(R.id.action_filter_unread) | ||||
|         val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) | ||||
|         val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) | ||||
|         val menuFilterEmpty = menu.findItem(R.id.action_filter_empty) | ||||
|  | ||||
|     private fun setTrackingIconInternal(visible: Boolean) { | ||||
|         val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return | ||||
|         val drawable = if (visible) { | ||||
|             VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null) | ||||
|         // Set correct checkbox values. | ||||
|         menuFilterRead.isChecked = presenter.onlyRead() | ||||
|         menuFilterUnread.isChecked = presenter.onlyUnread() | ||||
|         menuFilterDownloaded.isChecked = presenter.onlyDownloaded() | ||||
|         menuFilterDownloaded.isEnabled = !presenter.forceDownloaded() | ||||
|         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) | ||||
|         } | ||||
|  | ||||
|         // Only show remove filter option if there's a filter set. | ||||
|         menuFilterEmpty.isVisible = filterSet | ||||
|  | ||||
|         // Display mode submenu | ||||
|         if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { | ||||
|             menu.findItem(R.id.display_title).isChecked = true | ||||
|         } else { | ||||
|             null | ||||
|             menu.findItem(R.id.display_chapter_number).isChecked = true | ||||
|         } | ||||
|  | ||||
|         tab.icon = drawable | ||||
|         // Sorting mode submenu | ||||
|         val sortingItem = when (presenter.manga.sorting) { | ||||
|             Manga.SORTING_SOURCE -> R.id.sort_by_source | ||||
|             Manga.SORTING_NUMBER -> R.id.sort_by_number | ||||
|             Manga.SORTING_UPLOAD_DATE -> R.id.sort_by_upload_date | ||||
|             else -> throw NotImplementedError("Unimplemented sorting method") | ||||
|         } | ||||
|         menu.findItem(sortingItem).isChecked = true | ||||
|         menu.findItem(R.id.action_sort_descending).isChecked = presenter.manga.sortDescending() | ||||
|  | ||||
|         // Hide download options for local manga | ||||
|         menu.findItem(R.id.download_group).isVisible = !isLocalSource | ||||
|     } | ||||
|  | ||||
|     private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.display_title -> { | ||||
|                 item.isChecked = true | ||||
|                 setDisplayMode(Manga.DISPLAY_NAME) | ||||
|             } | ||||
|             R.id.display_chapter_number -> { | ||||
|                 item.isChecked = true | ||||
|                 setDisplayMode(Manga.DISPLAY_NUMBER) | ||||
|             } | ||||
|  | ||||
|         private val tabTitles = listOf( | ||||
|             R.string.manga_chapters_tab, | ||||
|             R.string.manga_tracking_tab | ||||
|         ) | ||||
|             .map { resources!!.getString(it) } | ||||
|             R.id.sort_by_source -> { | ||||
|                 item.isChecked = true | ||||
|                 presenter.setSorting(Manga.SORTING_SOURCE) | ||||
|             } | ||||
|             R.id.sort_by_number -> { | ||||
|                 item.isChecked = true | ||||
|                 presenter.setSorting(Manga.SORTING_NUMBER) | ||||
|             } | ||||
|             R.id.sort_by_upload_date -> { | ||||
|                 item.isChecked = true | ||||
|                 presenter.setSorting(Manga.SORTING_UPLOAD_DATE) | ||||
|             } | ||||
|             R.id.action_sort_descending -> { | ||||
|                 presenter.reverseSortOrder() | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|  | ||||
|         private val tabCount = tabTitles.size - if (Injekt.get<TrackManager>().hasLoggedServices()) 0 else 1 | ||||
|             R.id.download_next, R.id.download_next_5, R.id.download_next_10, | ||||
|             R.id.download_custom, R.id.download_unread, R.id.download_all | ||||
|             -> downloadChapters(item.itemId) | ||||
|  | ||||
|         override fun getCount(): Int { | ||||
|             return tabCount | ||||
|             R.id.action_filter_unread -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 presenter.setUnreadFilter(item.isChecked) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_filter_read -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 presenter.setReadFilter(item.isChecked) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_filter_downloaded -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 presenter.setDownloadedFilter(item.isChecked) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_filter_bookmarked -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 presenter.setBookmarkedFilter(item.isChecked) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_filter_empty -> { | ||||
|                 presenter.removeFilters() | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|  | ||||
|             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. | ||||
|             mangaInfoAdapter?.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 | ||||
|         } | ||||
|  | ||||
|         override fun configureRouter(router: Router, position: Int) { | ||||
|             if (!router.hasRootController()) { | ||||
|                 val controller = when (position) { | ||||
|                     INFO_CHAPTERS_CONTROLLER -> MangaInfoChaptersController(fromSource) | ||||
|                     TRACK_CONTROLLER -> TrackController() | ||||
|                     else -> error("Wrong position $position") | ||||
|         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 { | ||||
|             addToLibrary(manga) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun onTrackingClick() { | ||||
|         router.pushController(TrackController(manga).withFadeTransaction()) | ||||
|     } | ||||
|  | ||||
|     private fun addToLibrary(manga: Manga) { | ||||
|         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() | ||||
|                 } | ||||
|                 router.setRoot(RouterTransaction.with(controller)) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         override fun getPageTitle(position: Int): CharSequence { | ||||
|             return tabTitles[position] | ||||
|         mangaInfoAdapter?.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) { | ||||
|         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) { | ||||
|         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 | ||||
|         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 | ||||
|         if (!presenter.hasRequested && presenter.chapters.isEmpty()) { | ||||
|             fetchChaptersFromSource() | ||||
|         } | ||||
|  | ||||
|         val chaptersHeader = chaptersHeaderAdapter ?: return | ||||
|         chaptersHeader.setNumChapters(chapters.size) | ||||
|  | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         adapter.updateDataSet(chapters) | ||||
|  | ||||
|         if (selectedChapters.isNotEmpty()) { | ||||
|             adapter.clearSelection() // we need to start from a clean state, index may have changed | ||||
|             createActionModeIfNeeded() | ||||
|             selectedChapters.forEach { item -> | ||||
|                 val position = adapter.indexOf(item) | ||||
|                 if (position != -1 && !adapter.isSelected(position)) { | ||||
|                     adapter.toggleSelection(position) | ||||
|                 } | ||||
|             } | ||||
|             actionMode?.invalidate() | ||||
|         } | ||||
|  | ||||
|         val context = view?.context | ||||
|         if (context != null && chapters.any { it.read }) { | ||||
|             binding.fab.text = context.getString(R.string.action_resume) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun fetchChaptersFromSource(manualFetch: Boolean = false) { | ||||
|         isRefreshingChapters = true | ||||
|         updateRefreshing() | ||||
|  | ||||
|         presenter.fetchChaptersFromSource(manualFetch) | ||||
|     } | ||||
|  | ||||
|     fun onFetchChaptersDone() { | ||||
|         isRefreshingChapters = false | ||||
|         updateRefreshing() | ||||
|     } | ||||
|  | ||||
|     fun onFetchChaptersError(error: Throwable) { | ||||
|         isRefreshingChapters = false | ||||
|         updateRefreshing() | ||||
|         activity?.toast(error.message) | ||||
|     } | ||||
|  | ||||
|     fun onChapterStatusChange(download: Download) { | ||||
|         getHolder(download.chapter)?.notifyStatus(download.status) | ||||
|     } | ||||
|  | ||||
|     private fun getHolder(chapter: Chapter): ChapterHolder? { | ||||
|         return binding.recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder | ||||
|     } | ||||
|  | ||||
|     fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { | ||||
|         val activity = activity ?: return | ||||
|         val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) | ||||
|         if (hasAnimation) { | ||||
|             intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) | ||||
|         } | ||||
|         startActivity(intent) | ||||
|     } | ||||
|  | ||||
|     override fun onItemClick(view: View?, position: Int): Boolean { | ||||
|         val adapter = chaptersAdapter ?: return false | ||||
|         val item = adapter.getItem(position) ?: return false | ||||
|         return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { | ||||
|             lastClickPosition = position | ||||
|             toggleSelection(position) | ||||
|             true | ||||
|         } else { | ||||
|             openChapter(item.chapter) | ||||
|             false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onItemLongClick(position: Int) { | ||||
|         createActionModeIfNeeded() | ||||
|         when { | ||||
|             lastClickPosition == -1 -> setSelection(position) | ||||
|             lastClickPosition > position -> | ||||
|                 for (i in position until lastClickPosition) | ||||
|                     setSelection(i) | ||||
|             lastClickPosition < position -> | ||||
|                 for (i in lastClickPosition + 1..position) | ||||
|                     setSelection(i) | ||||
|             else -> setSelection(position) | ||||
|         } | ||||
|         lastClickPosition = position | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     // SELECTIONS & ACTION MODE | ||||
|  | ||||
|     private fun toggleSelection(position: Int) { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         val item = adapter.getItem(position) ?: return | ||||
|         adapter.toggleSelection(position) | ||||
|         adapter.notifyDataSetChanged() | ||||
|         if (adapter.isSelected(position)) { | ||||
|             selectedChapters.add(item) | ||||
|         } else { | ||||
|             selectedChapters.remove(item) | ||||
|         } | ||||
|         actionMode?.invalidate() | ||||
|     } | ||||
|  | ||||
|     private fun setSelection(position: Int) { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         val item = adapter.getItem(position) ?: return | ||||
|         if (!adapter.isSelected(position)) { | ||||
|             adapter.toggleSelection(position) | ||||
|             selectedChapters.add(item) | ||||
|             actionMode?.invalidate() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getSelectedChapters(): List<ChapterItem> { | ||||
|         val adapter = chaptersAdapter ?: return emptyList() | ||||
|         return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } | ||||
|     } | ||||
|  | ||||
|     private fun createActionModeIfNeeded() { | ||||
|         if (actionMode == null) { | ||||
|             actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) | ||||
|             binding.actionToolbar.show( | ||||
|                 actionMode!!, | ||||
|                 R.menu.chapter_selection | ||||
|             ) { onActionItemClicked(it!!) } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun destroyActionModeIfNeeded() { | ||||
|         lastClickPosition = -1 | ||||
|         actionMode?.finish() | ||||
|     } | ||||
|  | ||||
|     override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { | ||||
|         mode.menuInflater.inflate(R.menu.generic_selection, menu) | ||||
|         chaptersAdapter?.mode = SelectableAdapter.Mode.MULTI | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { | ||||
|         val count = chaptersAdapter?.selectedItemCount ?: 0 | ||||
|         if (count == 0) { | ||||
|             // Destroy action mode if there are no items selected. | ||||
|             destroyActionModeIfNeeded() | ||||
|         } else { | ||||
|             mode.title = count.toString() | ||||
|  | ||||
|             val chapters = getSelectedChapters() | ||||
|             binding.actionToolbar.findItem(R.id.action_download)?.isVisible = !isLocalSource && chapters.any { !it.isDownloaded } | ||||
|             binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = !isLocalSource && chapters.any { it.isDownloaded } | ||||
|             binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark } | ||||
|             binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark } | ||||
|             binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } | ||||
|             binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } | ||||
|  | ||||
|             // Hide FAB to avoid interfering with the bottom action toolbar | ||||
|             // binding.fab.hide() | ||||
|             binding.fab.gone() | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { | ||||
|         return onActionItemClicked(item) | ||||
|     } | ||||
|  | ||||
|     private fun onActionItemClicked(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.action_select_all -> selectAll() | ||||
|             R.id.action_select_inverse -> selectInverse() | ||||
|             R.id.action_download -> downloadChapters(getSelectedChapters()) | ||||
|             R.id.action_delete -> showDeleteChaptersConfirmationDialog() | ||||
|             R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true) | ||||
|             R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false) | ||||
|             R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) | ||||
|             R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) | ||||
|             R.id.action_mark_previous_as_read -> markPreviousAsRead(getSelectedChapters()) | ||||
|             else -> return false | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyActionMode(mode: ActionMode) { | ||||
|         binding.actionToolbar.hide() | ||||
|         chaptersAdapter?.mode = SelectableAdapter.Mode.SINGLE | ||||
|         chaptersAdapter?.clearSelection() | ||||
|         selectedChapters.clear() | ||||
|         actionMode = null | ||||
|  | ||||
|         // TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton] | ||||
|         // fails to show up properly | ||||
|         // binding.fab.show() | ||||
|         binding.fab.visible() | ||||
|     } | ||||
|  | ||||
|     override fun onDetach(view: View) { | ||||
|         destroyActionModeIfNeeded() | ||||
|         super.onDetach(view) | ||||
|     } | ||||
|  | ||||
|     // SELECTION MODE ACTIONS | ||||
|  | ||||
|     private fun selectAll() { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         adapter.selectAll() | ||||
|         selectedChapters.addAll(adapter.items) | ||||
|         actionMode?.invalidate() | ||||
|     } | ||||
|  | ||||
|     private fun selectInverse() { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|  | ||||
|         selectedChapters.clear() | ||||
|         for (i in 0..adapter.itemCount) { | ||||
|             adapter.toggleSelection(i) | ||||
|         } | ||||
|         selectedChapters.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) | ||||
|  | ||||
|         actionMode?.invalidate() | ||||
|         adapter.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     private fun markAsRead(chapters: List<ChapterItem>) { | ||||
|         presenter.markChaptersRead(chapters, true) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun markAsUnread(chapters: List<ChapterItem>) { | ||||
|         presenter.markChaptersRead(chapters, false) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun downloadChapters(chapters: List<ChapterItem>) { | ||||
|         val view = view | ||||
|         val manga = presenter.manga | ||||
|         presenter.downloadChapters(chapters) | ||||
|         if (view != null && !manga.favorite) { | ||||
|             binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { | ||||
|                 setAction(R.string.action_add) { | ||||
|                     addToLibrary(manga) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun showDeleteChaptersConfirmationDialog() { | ||||
|         DeleteChaptersDialog(this).showDialog(router) | ||||
|     } | ||||
|  | ||||
|     override fun deleteChapters() { | ||||
|         deleteChapters(getSelectedChapters()) | ||||
|     } | ||||
|  | ||||
|     private fun markPreviousAsRead(chapters: List<ChapterItem>) { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items | ||||
|         val chapterPos = prevChapters.indexOf(chapters.last()) | ||||
|         if (chapterPos != -1) { | ||||
|             markAsRead(prevChapters.take(chapterPos)) | ||||
|         } | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) { | ||||
|         presenter.bookmarkChapters(chapters, bookmarked) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     fun deleteChapters(chapters: List<ChapterItem>) { | ||||
|         if (chapters.isEmpty()) return | ||||
|  | ||||
|         presenter.deleteChapters(chapters) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     fun onChaptersDeleted(chapters: List<ChapterItem>) { | ||||
|         // this is needed so the downloaded text gets removed from the item | ||||
|         chapters.forEach { | ||||
|             chaptersAdapter?.updateItem(it) | ||||
|         } | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     fun onChaptersDeletedError(error: Throwable) { | ||||
|         Timber.e(error) | ||||
|     } | ||||
|  | ||||
|     // OVERFLOW MENU DIALOGS | ||||
|  | ||||
|     private fun setDisplayMode(id: Int) { | ||||
|         presenter.setDisplayMode(id) | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     private fun getUnreadChaptersSorted() = presenter.chapters | ||||
|         .filter { !it.read && it.status == Download.NOT_DOWNLOADED } | ||||
|         .distinctBy { it.name } | ||||
|         .sortedByDescending { it.source_order } | ||||
|  | ||||
|     private fun downloadChapters(choice: Int) { | ||||
|         val chaptersToDownload = when (choice) { | ||||
|             R.id.download_next -> getUnreadChaptersSorted().take(1) | ||||
|             R.id.download_next_5 -> getUnreadChaptersSorted().take(5) | ||||
|             R.id.download_next_10 -> getUnreadChaptersSorted().take(10) | ||||
|             R.id.download_custom -> { | ||||
|                 showCustomDownloadDialog() | ||||
|                 return | ||||
|             } | ||||
|             R.id.download_unread -> presenter.chapters.filter { !it.read } | ||||
|             R.id.download_all -> presenter.chapters | ||||
|             else -> emptyList() | ||||
|         } | ||||
|         if (chaptersToDownload.isNotEmpty()) { | ||||
|             downloadChapters(chaptersToDownload) | ||||
|         } | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun showCustomDownloadDialog() { | ||||
|         DownloadCustomChaptersDialog( | ||||
|             this, | ||||
|             presenter.chapters.size | ||||
|         ).showDialog(router) | ||||
|     } | ||||
|  | ||||
|     override fun downloadCustomChapters(amount: Int) { | ||||
|         val chaptersToDownload = getUnreadChaptersSorted().take(amount) | ||||
|         if (chaptersToDownload.isNotEmpty()) { | ||||
|             downloadChapters(chaptersToDownload) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Chapters list - end | ||||
|  | ||||
|     companion object { | ||||
|         const val FROM_SOURCE_EXTRA = "from_source" | ||||
|         const val MANGA_EXTRA = "manga" | ||||
|  | ||||
|         const val INFO_CHAPTERS_CONTROLLER = 0 | ||||
|         const val TRACK_CONTROLLER = 1 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
| package eu.kanade.tachiyomi.ui.manga | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import com.jakewharton.rxrelay.PublishRelay | ||||
| @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| import eu.kanade.tachiyomi.util.isLocal | ||||
| import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed | ||||
| @@ -29,14 +30,14 @@ import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| 
 | ||||
| class MangaInfoChaptersPresenter( | ||||
| class MangaPresenter( | ||||
|     val manga: Manga, | ||||
|     val source: Source, | ||||
|     val preferences: PreferencesHelper = Injekt.get(), | ||||
|     private val db: DatabaseHelper = Injekt.get(), | ||||
|     private val downloadManager: DownloadManager = Injekt.get(), | ||||
|     private val coverCache: CoverCache = Injekt.get() | ||||
| ) : BasePresenter<MangaInfoChaptersController>() { | ||||
| ) : BasePresenter<MangaController>() { | ||||
| 
 | ||||
|     /** | ||||
|      * Subscription to update the manga from the source. | ||||
| @@ -83,7 +84,7 @@ class MangaInfoChaptersPresenter( | ||||
|         // Prepare the relay. | ||||
|         chaptersRelay.flatMap { applyChapterFilters(it) } | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribeLatestCache(MangaInfoChaptersController::onNextChapters) { _, error -> Timber.e(error) } | ||||
|             .subscribeLatestCache(MangaController::onNextChapters) { _, error -> Timber.e(error) } | ||||
| 
 | ||||
|         // Manga info - end | ||||
| 
 | ||||
| @@ -139,7 +140,7 @@ class MangaInfoChaptersPresenter( | ||||
|                 { view, _ -> | ||||
|                     view.onFetchMangaInfoDone() | ||||
|                 }, | ||||
|                 MangaInfoChaptersController::onFetchMangaInfoError | ||||
|                 MangaController::onFetchMangaInfoError | ||||
|             ) | ||||
|     } | ||||
| 
 | ||||
| @@ -226,7 +227,7 @@ class MangaInfoChaptersPresenter( | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .filter { download -> download.manga.id == manga.id } | ||||
|             .doOnNext { onDownloadStatusChange(it) } | ||||
|             .subscribeLatestCache(MangaInfoChaptersController::onChapterStatusChange) { _, error -> | ||||
|             .subscribeLatestCache(MangaController::onChapterStatusChange) { _, error -> | ||||
|                 Timber.e(error) | ||||
|             } | ||||
|     } | ||||
| @@ -279,7 +280,7 @@ class MangaInfoChaptersPresenter( | ||||
|                 { view, _ -> | ||||
|                     view.onFetchChaptersDone() | ||||
|                 }, | ||||
|                 MangaInfoChaptersController::onFetchChaptersError | ||||
|                 MangaController::onFetchChaptersError | ||||
|             ) | ||||
|     } | ||||
| 
 | ||||
| @@ -413,7 +414,7 @@ class MangaInfoChaptersPresenter( | ||||
|                 { view, _ -> | ||||
|                     view.onChaptersDeleted(chapters) | ||||
|                 }, | ||||
|                 MangaInfoChaptersController::onChaptersDeletedError | ||||
|                 MangaController::onChaptersDeletedError | ||||
|             ) | ||||
|     } | ||||
| 
 | ||||
| @@ -4,6 +4,7 @@ import android.content.Context | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.util.system.getResourceColor | ||||
| import java.text.DateFormat | ||||
| import java.text.DecimalFormat | ||||
| @@ -11,7 +12,7 @@ import java.text.DecimalFormatSymbols | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class ChaptersAdapter( | ||||
|     controller: MangaInfoChaptersController, | ||||
|     controller: MangaController, | ||||
|     context: Context | ||||
| ) : FlexibleAdapter<ChapterItem>(null, controller, true) { | ||||
|  | ||||
|   | ||||
| @@ -1,859 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
|  | ||||
| import android.animation.Animator | ||||
| import android.animation.AnimatorListenerAdapter | ||||
| import android.app.Activity | ||||
| import android.content.Intent | ||||
| 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.appcompat.app.AppCompatActivity | ||||
| import androidx.appcompat.view.ActionMode | ||||
| import androidx.core.graphics.drawable.DrawableCompat | ||||
| import androidx.recyclerview.widget.ConcatAdapter | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| 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 | ||||
| import eu.kanade.tachiyomi.util.view.gone | ||||
| import eu.kanade.tachiyomi.util.view.shrinkOnScroll | ||||
| import eu.kanade.tachiyomi.util.view.snack | ||||
| import eu.kanade.tachiyomi.util.view.visible | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| 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 MangaInfoChaptersController(private val fromSource: Boolean = false) : | ||||
|     NucleusController<ChaptersControllerBinding, MangaInfoChaptersPresenter>(), | ||||
|     ActionMode.Callback, | ||||
|     FlexibleAdapter.OnItemClickListener, | ||||
|     FlexibleAdapter.OnItemLongClickListener, | ||||
|     ChangeMangaCategoriesDialog.Listener, | ||||
|     DownloadCustomChaptersDialog.Listener, | ||||
|     DeleteChaptersDialog.Listener { | ||||
|  | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null | ||||
|     private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null | ||||
|     private var chaptersAdapter: ChaptersAdapter? = null | ||||
|  | ||||
|     /** | ||||
|      * Action mode for multiple selection. | ||||
|      */ | ||||
|     private var actionMode: ActionMode? = null | ||||
|  | ||||
|     /** | ||||
|      * Selected items. Used to restore selections after a rotation. | ||||
|      */ | ||||
|     private val selectedChapters = mutableSetOf<ChapterItem>() | ||||
|  | ||||
|     private val isLocalSource by lazy { presenter.source.id == LocalSource.ID } | ||||
|  | ||||
|     private var lastClickPosition = -1 | ||||
|  | ||||
|     private var isRefreshingInfo = false | ||||
|     private var isRefreshingChapters = false | ||||
|  | ||||
|     init { | ||||
|         setHasOptionsMenu(true) | ||||
|         setOptionsMenuHidden(true) | ||||
|     } | ||||
|  | ||||
|     override fun createPresenter(): MangaInfoChaptersPresenter { | ||||
|         val ctrl = parentController as MangaController | ||||
|         return MangaInfoChaptersPresenter( | ||||
|             ctrl.manga!!, ctrl.source!! | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { | ||||
|         binding = ChaptersControllerBinding.inflate(inflater) | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|  | ||||
|         val ctrl = parentController as MangaController | ||||
|         if (ctrl.manga == null || ctrl.source == null) return | ||||
|  | ||||
|         // Init RecyclerView and adapter | ||||
|         mangaInfoAdapter = MangaInfoHeaderAdapter(this, fromSource) | ||||
|         chaptersHeaderAdapter = MangaChaptersHeaderAdapter() | ||||
|         chaptersAdapter = ChaptersAdapter(this, view.context) | ||||
|  | ||||
|         binding.recycler.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter) | ||||
|         binding.recycler.layoutManager = LinearLayoutManager(view.context) | ||||
|         binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) | ||||
|         binding.recycler.setHasFixedSize(true) | ||||
|         chaptersAdapter?.fastScroller = binding.fastScroller | ||||
|  | ||||
|         // Skips directly to chapters list if navigated to from the library | ||||
|         binding.recycler.post { | ||||
|             if (!fromSource && preferences.jumpToChapters()) { | ||||
|                 (binding.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(1, 0) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         binding.swipeRefresh.refreshes() | ||||
|             .onEach { | ||||
|                 fetchMangaInfoFromSource(manualFetch = true) | ||||
|                 fetchChaptersFromSource(manualFetch = true) | ||||
|             } | ||||
|             .launchIn(scope) | ||||
|  | ||||
|         binding.fab.clicks() | ||||
|             .onEach { | ||||
|                 val item = presenter.getNextUnreadChapter() | ||||
|                 if (item != null) { | ||||
|                     // Create animation listener | ||||
|                     val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { | ||||
|                         override fun onAnimationStart(animation: Animator?) { | ||||
|                             openChapter(item.chapter, true) | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     // Get coordinates and start animation | ||||
|                     val coordinates = binding.fab.getCoordinates() | ||||
|                     if (!binding.revealView.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { | ||||
|                         openChapter(item.chapter) | ||||
|                     } | ||||
|                 } else { | ||||
|                     view.context.toast(R.string.no_next_chapter) | ||||
|                 } | ||||
|             } | ||||
|             .launchIn(scope) | ||||
|  | ||||
|         binding.fab.shrinkOnScroll(binding.recycler) | ||||
|  | ||||
|         binding.actionToolbar.offsetAppbarHeight(activity!!) | ||||
|         binding.fab.offsetAppbarHeight(activity!!) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         destroyActionModeIfNeeded() | ||||
|         binding.actionToolbar.destroy() | ||||
|         mangaInfoAdapter = null | ||||
|         chaptersHeaderAdapter = null | ||||
|         chaptersAdapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     override fun onActivityResumed(activity: Activity) { | ||||
|         if (view == null) return | ||||
|  | ||||
|         // Check if animation view is visible | ||||
|         if (binding.revealView.visibility == View.VISIBLE) { | ||||
|             // Show the unreveal effect | ||||
|             val coordinates = binding.fab.getCoordinates() | ||||
|             binding.revealView.hideRevealEffect(coordinates.x, coordinates.y, 1920) | ||||
|         } | ||||
|  | ||||
|         super.onActivityResumed(activity) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         inflater.inflate(R.menu.chapters, menu) | ||||
|     } | ||||
|  | ||||
|     override fun onPrepareOptionsMenu(menu: Menu) { | ||||
|         // Initialize menu items. | ||||
|         val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return | ||||
|         val menuFilterUnread = menu.findItem(R.id.action_filter_unread) | ||||
|         val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) | ||||
|         val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) | ||||
|         val menuFilterEmpty = menu.findItem(R.id.action_filter_empty) | ||||
|  | ||||
|         // Set correct checkbox values. | ||||
|         menuFilterRead.isChecked = presenter.onlyRead() | ||||
|         menuFilterUnread.isChecked = presenter.onlyUnread() | ||||
|         menuFilterDownloaded.isChecked = presenter.onlyDownloaded() | ||||
|         menuFilterDownloaded.isEnabled = !presenter.forceDownloaded() | ||||
|         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) | ||||
|         } | ||||
|  | ||||
|         // Only show remove filter option if there's a filter set. | ||||
|         menuFilterEmpty.isVisible = filterSet | ||||
|  | ||||
|         // Display mode submenu | ||||
|         if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { | ||||
|             menu.findItem(R.id.display_title).isChecked = true | ||||
|         } else { | ||||
|             menu.findItem(R.id.display_chapter_number).isChecked = true | ||||
|         } | ||||
|  | ||||
|         // Sorting mode submenu | ||||
|         val sortingItem = when (presenter.manga.sorting) { | ||||
|             Manga.SORTING_SOURCE -> R.id.sort_by_source | ||||
|             Manga.SORTING_NUMBER -> R.id.sort_by_number | ||||
|             Manga.SORTING_UPLOAD_DATE -> R.id.sort_by_upload_date | ||||
|             else -> throw NotImplementedError("Unimplemented sorting method") | ||||
|         } | ||||
|         menu.findItem(sortingItem).isChecked = true | ||||
|         menu.findItem(R.id.action_sort_descending).isChecked = presenter.manga.sortDescending() | ||||
|  | ||||
|         // Hide download options for local manga | ||||
|         menu.findItem(R.id.download_group).isVisible = !isLocalSource | ||||
|     } | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.display_title -> { | ||||
|                 item.isChecked = true | ||||
|                 setDisplayMode(Manga.DISPLAY_NAME) | ||||
|             } | ||||
|             R.id.display_chapter_number -> { | ||||
|                 item.isChecked = true | ||||
|                 setDisplayMode(Manga.DISPLAY_NUMBER) | ||||
|             } | ||||
|  | ||||
|             R.id.sort_by_source -> { | ||||
|                 item.isChecked = true | ||||
|                 presenter.setSorting(Manga.SORTING_SOURCE) | ||||
|             } | ||||
|             R.id.sort_by_number -> { | ||||
|                 item.isChecked = true | ||||
|                 presenter.setSorting(Manga.SORTING_NUMBER) | ||||
|             } | ||||
|             R.id.sort_by_upload_date -> { | ||||
|                 item.isChecked = true | ||||
|                 presenter.setSorting(Manga.SORTING_UPLOAD_DATE) | ||||
|             } | ||||
|             R.id.action_sort_descending -> { | ||||
|                 presenter.reverseSortOrder() | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|  | ||||
|             R.id.download_next, R.id.download_next_5, R.id.download_next_10, | ||||
|             R.id.download_custom, R.id.download_unread, R.id.download_all | ||||
|             -> downloadChapters(item.itemId) | ||||
|  | ||||
|             R.id.action_filter_unread -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 presenter.setUnreadFilter(item.isChecked) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_filter_read -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 presenter.setReadFilter(item.isChecked) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_filter_downloaded -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 presenter.setDownloadedFilter(item.isChecked) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_filter_bookmarked -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 presenter.setBookmarkedFilter(item.isChecked) | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|             R.id.action_filter_empty -> { | ||||
|                 presenter.removeFilters() | ||||
|                 activity?.invalidateOptionsMenu() | ||||
|             } | ||||
|  | ||||
|             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. | ||||
|             mangaInfoAdapter?.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 { | ||||
|             addToLibrary(manga) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun addToLibrary(manga: Manga) { | ||||
|         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() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         mangaInfoAdapter?.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 | ||||
|         if (!presenter.hasRequested && presenter.chapters.isEmpty()) { | ||||
|             fetchChaptersFromSource() | ||||
|         } | ||||
|  | ||||
|         val chaptersHeader = chaptersHeaderAdapter ?: return | ||||
|         chaptersHeader.setNumChapters(chapters.size) | ||||
|  | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         adapter.updateDataSet(chapters) | ||||
|  | ||||
|         if (selectedChapters.isNotEmpty()) { | ||||
|             adapter.clearSelection() // we need to start from a clean state, index may have changed | ||||
|             createActionModeIfNeeded() | ||||
|             selectedChapters.forEach { item -> | ||||
|                 val position = adapter.indexOf(item) | ||||
|                 if (position != -1 && !adapter.isSelected(position)) { | ||||
|                     adapter.toggleSelection(position) | ||||
|                 } | ||||
|             } | ||||
|             actionMode?.invalidate() | ||||
|         } | ||||
|  | ||||
|         val context = view?.context | ||||
|         if (context != null && chapters.any { it.read }) { | ||||
|             binding.fab.text = context.getString(R.string.action_resume) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun fetchChaptersFromSource(manualFetch: Boolean = false) { | ||||
|         isRefreshingChapters = true | ||||
|         updateRefreshing() | ||||
|  | ||||
|         presenter.fetchChaptersFromSource(manualFetch) | ||||
|     } | ||||
|  | ||||
|     fun onFetchChaptersDone() { | ||||
|         isRefreshingChapters = false | ||||
|         updateRefreshing() | ||||
|     } | ||||
|  | ||||
|     fun onFetchChaptersError(error: Throwable) { | ||||
|         isRefreshingChapters = false | ||||
|         updateRefreshing() | ||||
|         activity?.toast(error.message) | ||||
|     } | ||||
|  | ||||
|     fun onChapterStatusChange(download: Download) { | ||||
|         getHolder(download.chapter)?.notifyStatus(download.status) | ||||
|     } | ||||
|  | ||||
|     private fun getHolder(chapter: Chapter): ChapterHolder? { | ||||
|         return binding.recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder | ||||
|     } | ||||
|  | ||||
|     fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { | ||||
|         val activity = activity ?: return | ||||
|         val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) | ||||
|         if (hasAnimation) { | ||||
|             intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) | ||||
|         } | ||||
|         startActivity(intent) | ||||
|     } | ||||
|  | ||||
|     override fun onItemClick(view: View?, position: Int): Boolean { | ||||
|         val adapter = chaptersAdapter ?: return false | ||||
|         val item = adapter.getItem(position) ?: return false | ||||
|         return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { | ||||
|             lastClickPosition = position | ||||
|             toggleSelection(position) | ||||
|             true | ||||
|         } else { | ||||
|             openChapter(item.chapter) | ||||
|             false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onItemLongClick(position: Int) { | ||||
|         createActionModeIfNeeded() | ||||
|         when { | ||||
|             lastClickPosition == -1 -> setSelection(position) | ||||
|             lastClickPosition > position -> | ||||
|                 for (i in position until lastClickPosition) | ||||
|                     setSelection(i) | ||||
|             lastClickPosition < position -> | ||||
|                 for (i in lastClickPosition + 1..position) | ||||
|                     setSelection(i) | ||||
|             else -> setSelection(position) | ||||
|         } | ||||
|         lastClickPosition = position | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     // SELECTIONS & ACTION MODE | ||||
|  | ||||
|     private fun toggleSelection(position: Int) { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         val item = adapter.getItem(position) ?: return | ||||
|         adapter.toggleSelection(position) | ||||
|         adapter.notifyDataSetChanged() | ||||
|         if (adapter.isSelected(position)) { | ||||
|             selectedChapters.add(item) | ||||
|         } else { | ||||
|             selectedChapters.remove(item) | ||||
|         } | ||||
|         actionMode?.invalidate() | ||||
|     } | ||||
|  | ||||
|     private fun setSelection(position: Int) { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         val item = adapter.getItem(position) ?: return | ||||
|         if (!adapter.isSelected(position)) { | ||||
|             adapter.toggleSelection(position) | ||||
|             selectedChapters.add(item) | ||||
|             actionMode?.invalidate() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getSelectedChapters(): List<ChapterItem> { | ||||
|         val adapter = chaptersAdapter ?: return emptyList() | ||||
|         return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } | ||||
|     } | ||||
|  | ||||
|     private fun createActionModeIfNeeded() { | ||||
|         if (actionMode == null) { | ||||
|             actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) | ||||
|             binding.actionToolbar.show( | ||||
|                 actionMode!!, | ||||
|                 R.menu.chapter_selection | ||||
|             ) { onActionItemClicked(it!!) } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun destroyActionModeIfNeeded() { | ||||
|         lastClickPosition = -1 | ||||
|         actionMode?.finish() | ||||
|     } | ||||
|  | ||||
|     override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { | ||||
|         mode.menuInflater.inflate(R.menu.generic_selection, menu) | ||||
|         chaptersAdapter?.mode = SelectableAdapter.Mode.MULTI | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { | ||||
|         val count = chaptersAdapter?.selectedItemCount ?: 0 | ||||
|         if (count == 0) { | ||||
|             // Destroy action mode if there are no items selected. | ||||
|             destroyActionModeIfNeeded() | ||||
|         } else { | ||||
|             mode.title = count.toString() | ||||
|  | ||||
|             val chapters = getSelectedChapters() | ||||
|             binding.actionToolbar.findItem(R.id.action_download)?.isVisible = !isLocalSource && chapters.any { !it.isDownloaded } | ||||
|             binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = !isLocalSource && chapters.any { it.isDownloaded } | ||||
|             binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark } | ||||
|             binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark } | ||||
|             binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } | ||||
|             binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } | ||||
|  | ||||
|             // Hide FAB to avoid interfering with the bottom action toolbar | ||||
|             // binding.fab.hide() | ||||
|             binding.fab.gone() | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { | ||||
|         return onActionItemClicked(item) | ||||
|     } | ||||
|  | ||||
|     private fun onActionItemClicked(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.action_select_all -> selectAll() | ||||
|             R.id.action_select_inverse -> selectInverse() | ||||
|             R.id.action_download -> downloadChapters(getSelectedChapters()) | ||||
|             R.id.action_delete -> showDeleteChaptersConfirmationDialog() | ||||
|             R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true) | ||||
|             R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false) | ||||
|             R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) | ||||
|             R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) | ||||
|             R.id.action_mark_previous_as_read -> markPreviousAsRead(getSelectedChapters()) | ||||
|             else -> return false | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyActionMode(mode: ActionMode) { | ||||
|         binding.actionToolbar.hide() | ||||
|         chaptersAdapter?.mode = SelectableAdapter.Mode.SINGLE | ||||
|         chaptersAdapter?.clearSelection() | ||||
|         selectedChapters.clear() | ||||
|         actionMode = null | ||||
|  | ||||
|         // TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton] | ||||
|         // fails to show up properly | ||||
|         // binding.fab.show() | ||||
|         binding.fab.visible() | ||||
|     } | ||||
|  | ||||
|     override fun onDetach(view: View) { | ||||
|         destroyActionModeIfNeeded() | ||||
|         super.onDetach(view) | ||||
|     } | ||||
|  | ||||
|     // SELECTION MODE ACTIONS | ||||
|  | ||||
|     private fun selectAll() { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         adapter.selectAll() | ||||
|         selectedChapters.addAll(adapter.items) | ||||
|         actionMode?.invalidate() | ||||
|     } | ||||
|  | ||||
|     private fun selectInverse() { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|  | ||||
|         selectedChapters.clear() | ||||
|         for (i in 0..adapter.itemCount) { | ||||
|             adapter.toggleSelection(i) | ||||
|         } | ||||
|         selectedChapters.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) | ||||
|  | ||||
|         actionMode?.invalidate() | ||||
|         adapter.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     private fun markAsRead(chapters: List<ChapterItem>) { | ||||
|         presenter.markChaptersRead(chapters, true) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun markAsUnread(chapters: List<ChapterItem>) { | ||||
|         presenter.markChaptersRead(chapters, false) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun downloadChapters(chapters: List<ChapterItem>) { | ||||
|         val view = view | ||||
|         val manga = presenter.manga | ||||
|         presenter.downloadChapters(chapters) | ||||
|         if (view != null && !manga.favorite) { | ||||
|             binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { | ||||
|                 setAction(R.string.action_add) { | ||||
|                     addToLibrary(manga) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun showDeleteChaptersConfirmationDialog() { | ||||
|         DeleteChaptersDialog(this).showDialog(router) | ||||
|     } | ||||
|  | ||||
|     override fun deleteChapters() { | ||||
|         deleteChapters(getSelectedChapters()) | ||||
|     } | ||||
|  | ||||
|     private fun markPreviousAsRead(chapters: List<ChapterItem>) { | ||||
|         val adapter = chaptersAdapter ?: return | ||||
|         val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items | ||||
|         val chapterPos = prevChapters.indexOf(chapters.last()) | ||||
|         if (chapterPos != -1) { | ||||
|             markAsRead(prevChapters.take(chapterPos)) | ||||
|         } | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) { | ||||
|         presenter.bookmarkChapters(chapters, bookmarked) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     fun deleteChapters(chapters: List<ChapterItem>) { | ||||
|         if (chapters.isEmpty()) return | ||||
|  | ||||
|         presenter.deleteChapters(chapters) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     fun onChaptersDeleted(chapters: List<ChapterItem>) { | ||||
|         // this is needed so the downloaded text gets removed from the item | ||||
|         chapters.forEach { | ||||
|             chaptersAdapter?.updateItem(it) | ||||
|         } | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     fun onChaptersDeletedError(error: Throwable) { | ||||
|         Timber.e(error) | ||||
|     } | ||||
|  | ||||
|     // OVERFLOW MENU DIALOGS | ||||
|  | ||||
|     private fun setDisplayMode(id: Int) { | ||||
|         presenter.setDisplayMode(id) | ||||
|         chaptersAdapter?.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     private fun getUnreadChaptersSorted() = presenter.chapters | ||||
|         .filter { !it.read && it.status == Download.NOT_DOWNLOADED } | ||||
|         .distinctBy { it.name } | ||||
|         .sortedByDescending { it.source_order } | ||||
|  | ||||
|     private fun downloadChapters(choice: Int) { | ||||
|         val chaptersToDownload = when (choice) { | ||||
|             R.id.download_next -> getUnreadChaptersSorted().take(1) | ||||
|             R.id.download_next_5 -> getUnreadChaptersSorted().take(5) | ||||
|             R.id.download_next_10 -> getUnreadChaptersSorted().take(10) | ||||
|             R.id.download_custom -> { | ||||
|                 showCustomDownloadDialog() | ||||
|                 return | ||||
|             } | ||||
|             R.id.download_unread -> presenter.chapters.filter { !it.read } | ||||
|             R.id.download_all -> presenter.chapters | ||||
|             else -> emptyList() | ||||
|         } | ||||
|         if (chaptersToDownload.isNotEmpty()) { | ||||
|             downloadChapters(chaptersToDownload) | ||||
|         } | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     private fun showCustomDownloadDialog() { | ||||
|         DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router) | ||||
|     } | ||||
|  | ||||
|     override fun downloadCustomChapters(amount: Int) { | ||||
|         val chaptersToDownload = getUnreadChaptersSorted().take(amount) | ||||
|         if (chaptersToDownload.isNotEmpty()) { | ||||
|             downloadChapters(chaptersToDownload) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Chapters list - end | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
| package eu.kanade.tachiyomi.ui.manga.info | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| @@ -1,4 +1,4 @@ | ||||
| package eu.kanade.tachiyomi.ui.manga.chapter | ||||
| package eu.kanade.tachiyomi.ui.manga.info | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.text.TextUtils | ||||
| @@ -13,11 +13,13 @@ 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.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding | ||||
| 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.manga.MangaController | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import eu.kanade.tachiyomi.util.view.gone | ||||
| import eu.kanade.tachiyomi.util.view.setChips | ||||
| @@ -35,7 +37,7 @@ import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| 
 | ||||
| class MangaInfoHeaderAdapter( | ||||
|     private val controller: MangaInfoChaptersController, | ||||
|     private val controller: MangaController, | ||||
|     private val fromSource: Boolean | ||||
| ) : | ||||
|     RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() { | ||||
| @@ -81,13 +83,24 @@ class MangaInfoHeaderAdapter( | ||||
|                 .onEach { controller.onFavoriteClick() } | ||||
|                 .launchIn(scope) | ||||
| 
 | ||||
|             if (controller.presenter.manga.favorite && Injekt.get<TrackManager>().hasLoggedServices()) { | ||||
|                 binding.btnTracking.visible() | ||||
|                 binding.btnTracking.clicks() | ||||
|                     .onEach { controller.onTrackingClick() } | ||||
|                     .launchIn(scope) | ||||
|             } else { | ||||
|                 binding.btnTracking.gone() | ||||
|             } | ||||
| 
 | ||||
|             if (controller.presenter.manga.favorite && controller.presenter.getCategories().isNotEmpty()) { | ||||
|                 binding.btnCategories.visible() | ||||
|                 binding.btnCategories.clicks() | ||||
|                     .onEach { controller.onCategoriesClick() } | ||||
|                     .launchIn(scope) | ||||
|                 binding.btnCategories.setTooltip(R.string.action_move_category) | ||||
|             } else { | ||||
|                 binding.btnCategories.gone() | ||||
|             } | ||||
|             binding.btnCategories.clicks() | ||||
|                 .onEach { controller.onCategoriesClick() } | ||||
|                 .launchIn(scope) | ||||
|             binding.btnCategories.setTooltip(R.string.action_move_category) | ||||
| 
 | ||||
|             if (controller.presenter.source is HttpSource) { | ||||
|                 binding.btnWebview.visible() | ||||
| @@ -2,29 +2,51 @@ package eu.kanade.tachiyomi.ui.manga.track | ||||
|  | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| import eu.kanade.tachiyomi.databinding.TrackControllerBinding | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NucleusController | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import reactivecircus.flowbinding.swiperefreshlayout.refreshes | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class TrackController : | ||||
|     NucleusController<TrackControllerBinding, TrackPresenter>(), | ||||
|     NucleusController<TrackControllerBinding, TrackPresenter>, | ||||
|     TrackAdapter.OnClickListener, | ||||
|     SetTrackStatusDialog.Listener, | ||||
|     SetTrackChaptersDialog.Listener, | ||||
|     SetTrackScoreDialog.Listener, | ||||
|     SetTrackReadingDatesDialog.Listener { | ||||
|  | ||||
|     constructor(manga: Manga?) : super( | ||||
|         Bundle().apply { | ||||
|             putLong(MANGA_EXTRA, manga?.id ?: 0) | ||||
|         } | ||||
|     ) { | ||||
|         this.manga = manga | ||||
|     } | ||||
|  | ||||
|     constructor(mangaId: Long) : this( | ||||
|         Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking() | ||||
|     ) | ||||
|  | ||||
|     @Suppress("unused") | ||||
|     constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) | ||||
|  | ||||
|     var manga: Manga? = null | ||||
|         private set | ||||
|  | ||||
|     private var adapter: TrackAdapter? = null | ||||
|  | ||||
|     init { | ||||
| @@ -33,8 +55,12 @@ class TrackController : | ||||
|         setHasOptionsMenu(true) | ||||
|     } | ||||
|  | ||||
|     override fun getTitle(): String? { | ||||
|         return manga?.title | ||||
|     } | ||||
|  | ||||
|     override fun createPresenter(): TrackPresenter { | ||||
|         return TrackPresenter((parentController as MangaController).manga!!) | ||||
|         return TrackPresenter(manga!!) | ||||
|     } | ||||
|  | ||||
|     override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { | ||||
| @@ -45,6 +71,8 @@ class TrackController : | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|  | ||||
|         if (manga == null) return | ||||
|  | ||||
|         adapter = TrackAdapter(this) | ||||
|         binding.trackRecycler.layoutManager = LinearLayoutManager(view.context) | ||||
|         binding.trackRecycler.adapter = adapter | ||||
| @@ -63,7 +91,6 @@ class TrackController : | ||||
|         val atLeastOneLink = trackings.any { it.track != null } | ||||
|         adapter?.items = trackings | ||||
|         binding.swipeRefresh.isEnabled = atLeastOneLink | ||||
|         (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) | ||||
|     } | ||||
|  | ||||
|     fun onSearchResults(results: List<TrackSearch>) { | ||||
| @@ -167,6 +194,7 @@ class TrackController : | ||||
|     } | ||||
|  | ||||
|     private companion object { | ||||
|         const val MANGA_EXTRA = "manga" | ||||
|         const val TAG_SEARCH_CONTROLLER = "track_search_controller" | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user