From dbf5424b77985495f2d5e119e20efd58d221b3f1 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 11 Mar 2020 00:37:44 -0700 Subject: [PATCH] Deleting the old manga detail controllers --- .../data/notification/NotificationReceiver.kt | 8 +- .../tachiyomi/ui/manga/ChooseShapeDialog.kt | 6 - .../tachiyomi/ui/manga/MangaController.kt | 273 ------ .../ui/manga/MangaDetailsController.kt | 24 +- .../ui/manga/chapter/ChapterHolder.kt | 136 --- .../ui/manga/chapter/ChaptersController.kt | 603 ------------ .../ui/manga/chapter/ChaptersPresenter.kt | 443 --------- .../ui/manga/chapter/DeleteChaptersDialog.kt | 31 - .../ui/manga/info/MangaInfoController.kt | 860 ------------------ .../ui/manga/info/MangaInfoPresenter.kt | 290 ------ .../ui/manga/track/TrackController.kt | 168 ---- .../ui/manga/track/TrackPresenter.kt | 130 --- .../ui/manga/track/TrackSearchDialog.kt | 9 - 13 files changed, 22 insertions(+), 2959 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index b72283abec..67c4dad27a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -21,14 +21,14 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.getUriCompat -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.toast +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID @@ -405,7 +405,7 @@ class NotificationReceiver : BroadcastReceiver() { val newIntent = Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(MangaController.MANGA_EXTRA, manga.id) + .putExtra(MangaDetailsController.MANGA_EXTRA, manga.id) .putExtra("notificationId", manga.id.hashCode()) .putExtra("groupId", groupId) return PendingIntent.getActivity( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChooseShapeDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChooseShapeDialog.kt index 866b0df394..0f8271de3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChooseShapeDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChooseShapeDialog.kt @@ -6,17 +6,12 @@ import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItemsSingleChoice import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController /** * Dialog to choose a shape for the icon. */ class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) { - constructor(target: MangaInfoController) : this() { - targetController = target - } - constructor(target: MangaDetailsController) : this() { targetController = target } @@ -35,7 +30,6 @@ class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) { items = modes.map { activity?.getString(it) as CharSequence }, waitForPositiveButton = false) { _, i, _ -> - (targetController as? MangaInfoController)?.createShortcutForShape(i) (targetController as? MangaDetailsController)?.createShortcutForShape(i) dismissDialog() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt deleted file mode 100644 index 0b7d1fcdb1..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ /dev/null @@ -1,273 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.app.Activity -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat -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.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.notification.NotificationReceiver -import eu.kanade.tachiyomi.data.track.TrackManager -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.catalogue.CatalogueController -import eu.kanade.tachiyomi.ui.main.BottomNavBarInterface -import eu.kanade.tachiyomi.ui.main.SearchActivity -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController -import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController -import eu.kanade.tachiyomi.ui.manga.track.TrackController -import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController -import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.applyWindowInsetsForController -import kotlinx.android.synthetic.main.manga_controller.* -import rx.Subscription -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.Date - -class MangaController : RxController, TabbedController, BottomNavBarInterface { - - constructor(manga: Manga?, - fromCatalogue: Boolean = false, - smartSearchConfig: CatalogueController.SmartSearchConfig? = null, - update: Boolean = false) : super(Bundle().apply { - putLong(MANGA_EXTRA, manga?.id ?: 0) - putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) - putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig) - putBoolean(UPDATE_EXTRA, update) - }) { - this.manga = manga - if (manga != null) { - source = Injekt.get().getOrStub(manga.source) - } - } - - constructor(manga: Manga?, fromCatalogue: Boolean = false, fromExtension: Boolean = false) : - super - (Bundle() - .apply { - putLong(MANGA_EXTRA, manga?.id ?: 0) - putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) - }) { - this.manga = manga - if (manga != null) { - source = Injekt.get().getOrStub(manga.source) - } - } - - constructor(manga: Manga?, startY:Float?) : super(Bundle().apply { - putLong(MANGA_EXTRA, manga?.id ?: 0) - putBoolean(FROM_CATALOGUE_EXTRA, false) - }) { - this.manga = manga - startingChapterYPos = startY - if (manga != null) { - source = Injekt.get().getOrStub(manga.source) - } - } - - constructor(mangaId: Long) : this( - Injekt.get().getManga(mangaId).executeAsBlocking()) - - constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) { - val notificationId = bundle.getInt("notificationId", -1) - val context = applicationContext ?: return - if (notificationId > -1) NotificationReceiver.dismissNotification( - context, notificationId, bundle.getInt("groupId", 0) - ) - } - - var manga: Manga? = null - private set - - var source: Source? = null - private set - - var startingChapterYPos:Float? = null - - var isLockedFromSearch = false - - private var adapter: MangaDetailAdapter? = null - - val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) - - val lastUpdateRelay: BehaviorRelay = BehaviorRelay.create() - - val chapterCountRelay: BehaviorRelay = BehaviorRelay.create() - - val mangaFavoriteRelay: PublishRelay = PublishRelay.create() - - private val trackingIconRelay: BehaviorRelay = BehaviorRelay.create() - - private var trackingIconSubscription: Subscription? = null - - override fun getTitle(): String? { - return manga?.currentTitle() - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.manga_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - view.applyWindowInsetsForController() - - if (manga == null || source == null) return - - requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) - - adapter = MangaDetailAdapter() - manga_pager.offscreenPageLimit = 3 - manga_pager.adapter = adapter - - isLockedFromSearch = activity is SearchActivity && - SecureActivityDelegate.shouldBeLocked() - - if (!fromCatalogue) - manga_pager.currentItem = CHAPTERS_CONTROLLER - } - - override fun onDestroyView(view: View) { - super.onDestroyView(view) - adapter = null - } - - override fun onActivityResumed(activity: Activity) { - super.onActivityResumed(activity) - isLockedFromSearch = activity is SearchActivity && - SecureActivityDelegate.shouldBeLocked() - } - - override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeStarted(handler, type) - if (type.isEnter) { - tabLayout()?.setupWithViewPager(manga_pager) - checkInitialTrackState() - trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } - } - } - - private fun checkInitialTrackState() { - val manga = manga ?: return - val loggedServices by lazy { Injekt.get().services.filter { it.isLogged } } - val db = Injekt.get() - val tracks = db.getTracks(manga).executeAsBlocking() - - if (loggedServices.any { service -> tracks.any { it.sync_id == service.id } }) { - setTrackingIcon(true) - } - } - - fun tabLayout():TabLayout? { - return null - } - - fun updateTitle(manga: Manga) { - this.manga?.title = manga.title - setTitle() - } - - override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeEnded(handler, type) - if (manga == null || source == null) { - activity?.toast(R.string.manga_not_in_db) - router.popController(this) - } - } - - override fun configureTabs(tabs: TabLayout) { - with(tabs) { - tabGravity = TabLayout.GRAVITY_FILL - tabMode = TabLayout.MODE_FIXED - } - } - - override fun cleanupTabs(tabs: TabLayout) { - trackingIconSubscription?.unsubscribe() - setTrackingIconInternal(false) - } - - fun setTrackingIcon(visible: Boolean) { - trackingIconRelay.call(visible) - } - - private fun setTrackingIconInternal(visible: Boolean) { - val tab = tabLayout()?.getTabAt(TRACK_CONTROLLER) ?: return - val drawable = if (visible) - VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null) - else null - - //tab.icon = drawable - } - - override fun canChangeTabs(block: () -> Unit): Boolean { - val migrationListController = router.getControllerWithTag(MigrationListController.TAG) - as? BottomNavBarInterface - if (migrationListController != null) return migrationListController.canChangeTabs(block) - return true - } - - private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { - - private val tabCount = if (Injekt.get().hasLoggedServices()) 3 else 2 - - private val tabTitles = listOf( - R.string.manga_detail_tab, - R.string.manga_chapters_tab, - R.string.manga_tracking_tab) - .map { resources!!.getString(it) } - - override fun getCount(): Int { - return tabCount - } - - override fun configureRouter(router: Router, position: Int) { - val touchOffset = if (tabLayout()?.height == 0) 144f else 0f - if (!router.hasRootController()) { - val controller = when (position) { - INFO_CONTROLLER -> MangaInfoController() - CHAPTERS_CONTROLLER -> ChaptersController(startingChapterYPos?.minus(touchOffset)) - TRACK_CONTROLLER -> TrackController() - else -> error("Wrong position $position") - } - router.setRoot(RouterTransaction.with(controller)) - } - } - - override fun getPageTitle(position: Int): CharSequence { - return tabTitles[position] - } - - } - - companion object { - - const val UPDATE_EXTRA = "update" - const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig" - - const val FROM_CATALOGUE_EXTRA = "from_catalogue" - const val MANGA_EXTRA = "manga" - - const val INFO_CONTROLLER = 0 - const val CHAPTERS_CONTROLLER = 1 - const val TRACK_CONTROLLER = 2 - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 42e293b689..1e7ba08d0d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -73,7 +73,6 @@ 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.SearchActivity -import eu.kanade.tachiyomi.ui.manga.MangaController.Companion.FROM_CATALOGUE_EXTRA import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.chapter.ChapterMatHolder import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter @@ -114,10 +113,10 @@ class MangaDetailsController : BaseController, fromCatalogue: Boolean = false, smartSearchConfig: CatalogueController.SmartSearchConfig? = null, update: Boolean = false) : super(Bundle().apply { - putLong(MangaController.MANGA_EXTRA, manga?.id ?: 0) + putLong(MANGA_EXTRA, manga?.id ?: 0) putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) - putParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig) - putBoolean(MangaController.UPDATE_EXTRA, update) + putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig) + putBoolean(UPDATE_EXTRA, update) }) { this.manga = manga if (manga != null) { @@ -128,7 +127,7 @@ class MangaDetailsController : BaseController, constructor(mangaId: Long) : this( Injekt.get().getManga(mangaId).executeAsBlocking()) - constructor(bundle: Bundle) : this(bundle.getLong(MangaController.MANGA_EXTRA)) { + constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) { val notificationId = bundle.getInt("notificationId", -1) val context = applicationContext ?: return if (notificationId > -1) NotificationReceiver.dismissNotification( @@ -641,7 +640,7 @@ class MangaDetailsController : BaseController, val shortcutIntent = activity.intent .setAction(MainActivity.SHORTCUT_MANGA) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(MangaController.MANGA_EXTRA, presenter.manga.id) + .putExtra(MANGA_EXTRA, presenter.manga.id) // Check if shortcut placement is supported if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) { @@ -1033,4 +1032,17 @@ class MangaDetailsController : BaseController, } } } + + companion object { + + const val UPDATE_EXTRA = "update" + const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig" + + const val FROM_CATALOGUE_EXTRA = "from_catalogue" + const val MANGA_EXTRA = "manga" + + const val INFO_CONTROLLER = 0 + const val CHAPTERS_CONTROLLER = 1 + const val TRACK_CONTROLLER = 2 + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt deleted file mode 100644 index 0c6fff4216..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt +++ /dev/null @@ -1,136 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.view.View -import androidx.appcompat.widget.PopupMenu -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.util.view.invisible -import eu.kanade.tachiyomi.util.view.setVectorCompat -import eu.kanade.tachiyomi.util.view.visible -import kotlinx.android.synthetic.main.chapters_item.* -import java.util.Date - -class ChapterHolder( - private val view: View, - private val adapter: ChaptersAdapter -) : BaseFlexibleViewHolder(view, adapter) { - - init { - // We need to post a Runnable to show the popup to make sure that the PopupMenu is - // correctly positioned. The reason being that the view may change position before the - // PopupMenu is shown. - chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } - } - - fun bind(item: ChapterItem, manga: Manga) { - val chapter = item.chapter ?: return - val isLocked = item.isLocked - chapter_title.text = when (manga.displayMode) { - Manga.DISPLAY_NUMBER -> { - val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) - itemView.context.getString(R.string.display_mode_chapter, number) - } - else -> chapter.name - } - - chapter_menu.visible() - // Set the correct drawable for dropdown and update the tint to match theme. - chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color)) - - if (isLocked) chapter_menu.invisible() - - // Set correct text color - chapter_title.setTextColor(if (chapter.read && !isLocked) - adapter.readColor else adapter.unreadColor) - if (chapter.bookmark && !isLocked) chapter_title.setTextColor(adapter.bookmarkedColor) - - if (chapter.date_upload > 0) { - chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload)) - chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) - } else { - chapter_date.text = "" - } - - //add scanlator if exists - chapter_scanlator.text = chapter.scanlator - //allow longer titles if there is no scanlator (most sources) - if (chapter_scanlator.text.isNullOrBlank()) { - chapter_title.maxLines = 2 - chapter_scanlator.gone() - } else { - chapter_title.maxLines = 1 - } - - chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0 && !isLocked) { - itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1) - } else { - "" - } - - notifyStatus(item.status, item.isLocked) - } - - fun notifyStatus(status: Int, locked: Boolean) = with(download_text) { - if (locked) { - text = "" - return - } - when (status) { - Download.QUEUE -> setText(R.string.chapter_queued) - Download.DOWNLOADING -> setText(R.string.chapter_downloading) - Download.DOWNLOADED -> setText(R.string.chapter_downloaded) - Download.ERROR -> setText(R.string.chapter_error) - else -> text = "" - } - } - - private fun showPopupMenu(view: View) { - val item = adapter.getItem(adapterPosition) ?: return - val chapter = item.chapter ?: return - - if (item.isLocked) { - adapter.unlock() - return - } - // Create a PopupMenu, giving it the clicked view for an anchor - val popup = PopupMenu(view.context, view) - - // Inflate our menu resource into the PopupMenu's Menu - popup.menuInflater.inflate(R.menu.chapter_single, popup.menu) - - - // Hide download and show delete if the chapter is downloaded - if (item.isDownloaded) { - popup.menu.findItem(R.id.action_download).isVisible = false - popup.menu.findItem(R.id.action_delete).isVisible = true - } - - // Hide bookmark if bookmark - popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark - popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark - - // Hide mark as unread when the chapter is unread - if (!chapter.read && chapter.last_page_read == 0) { - popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false - } - - // Hide mark as read when the chapter is read - if (chapter.read) { - popup.menu.findItem(R.id.action_mark_as_read).isVisible = false - } - - // Set a listener so we are notified if a menu item is clicked - popup.setOnMenuItemClickListener { menuItem -> - adapter.menuItemListener?.onMenuItemClick(adapterPosition, menuItem) - true - } - - // Finally show the PopupMenu - popup.show() - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt deleted file mode 100644 index 49128d07da..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt +++ /dev/null @@ -1,603 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.annotation.SuppressLint -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.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.view.clicks -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.kanade.tachiyomi.R -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.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.main.SearchActivity -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.view.getCoordinates -import eu.kanade.tachiyomi.util.view.getText -import eu.kanade.tachiyomi.util.view.marginBottom -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.updateLayoutParams -import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import kotlinx.android.synthetic.main.chapters_controller.* -import timber.log.Timber - -class ChaptersController() : NucleusController(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - ChaptersAdapter.OnMenuItemClickListener, - DownloadCustomChaptersDialog.Listener, - DeleteChaptersDialog.Listener { - - constructor(startY: Float?) : this() { - this.startingChapterYPos = startY - } - - /** - * Adapter containing a list of chapters. - */ - private var adapter: ChaptersAdapter? = null - - private var scrollToUnread = true - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - private var snack:Snackbar? = null - /** - * Selected items. Used to restore selections after a rotation. - */ - private val selectedItems = mutableSetOf() - - private var lastClickPosition = -1 - - init { - setHasOptionsMenu(true) - setOptionsMenuHidden(true) - } - var startingChapterYPos:Float? = null - - override fun createPresenter(): ChaptersPresenter { - val ctrl = parentController as MangaController - return ChaptersPresenter(ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.chapters_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // Init RecyclerView and adapter - adapter = ChaptersAdapter(this, view.context) - setReadingDrawable() - - recycler.adapter = adapter - recycler.layoutManager = LinearLayoutManager(view.context) - recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - recycler.setHasFixedSize(true) - adapter?.fastScroller = fast_scroller - - val fabBaseMarginBottom = fab?.marginBottom ?: 0 - recycler.doOnApplyWindowInsets { v, insets, _ -> - fab?.updateLayoutParams { - bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom - } - fast_scroller?.updateLayoutParams { - bottomMargin = insets.systemWindowInsetBottom - } - // offset the recycler by the fab's inset + some inset on top - v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom + - v.context.resources.getDimensionPixelSize(R.dimen.fab_list_padding)) - } - swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } - - fab.clicks().subscribeUntilDestroy { - if (activity is SearchActivity && presenter.isLockedFromSearch) { - SecureActivityDelegate.promptLockIfNeeded(activity) - return@subscribeUntilDestroy - } - 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 = fab.getCoordinates() - if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { - openChapter(item.chapter) - } - } else if (snack == null || snack?.getText() != view.context.getString(R.string.no_next_chapter)) { - snack = view.snack(R.string.no_next_chapter, Snackbar.LENGTH_LONG) { - addCallback(object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (snack == transientBottomBar) snack = null - } - }) - } - } - } - } - - override fun onDestroyView(view: View) { - adapter = null - actionMode = null - super.onDestroyView(view) - } - /** - * Update FAB with correct drawable. - * - * @param isFavorite determines if manga is favorite or not. - */ - private fun setReadingDrawable() { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - fab.setImageResource( - when { - (parentController as MangaController).isLockedFromSearch -> R.drawable.ic_lock_white_24dp - else -> R.drawable.ic_play_arrow_white_24dp - } - ) - } - - override fun onActivityResumed(activity: Activity) { - super.onActivityResumed(activity) - if (view == null) return - if (activity is SearchActivity) { - presenter.updateLockStatus() - setReadingDrawable() - } - - // Check if animation view is visible - if (reveal_view.visibility == View.VISIBLE) { - // Show the unReveal effect - val coordinates = fab.getCoordinates() - reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - if (!(parentController as MangaController).isLockedFromSearch) - 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) - - // Set correct checkbox values. - menuFilterRead.isChecked = presenter.onlyRead() - menuFilterUnread.isChecked = presenter.onlyUnread() - menuFilterDownloaded.isChecked = presenter.onlyDownloaded() - menuFilterBookmarked.isChecked = presenter.onlyBookmarked() - - if (presenter.onlyRead()) - //Disable unread filter option if read filter is enabled. - menuFilterUnread.isEnabled = false - if (presenter.onlyUnread()) - //Disable read filter option if unread filter is enabled. - menuFilterRead.isEnabled = false - - // 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 - if (presenter.manga.sorting == Manga.SORTING_SOURCE) { - menu.findItem(R.id.sort_by_source).isChecked = true - } else { - menu.findItem(R.id.sort_by_number).isChecked = true - } - } - - 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.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) - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity?.invalidateOptionsMenu() - } - R.id.action_sort -> presenter.revertSortOrder() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - fun onNextChapters(chapters: List) { - // If the list is empty, fetch chapters from source if the conditions are met - // We use presenter chapters instead because they are always unfiltered - if (presenter.chapters.isEmpty()) { - initialFetchChapters() - } - - val adapter = adapter ?: return - adapter.updateDataSet(chapters) - - if (selectedItems.isNotEmpty()) { - adapter.clearSelection() // we need to start from a clean state, index may have changed - createActionModeIfNeeded() - selectedItems.forEach { item -> - val position = adapter.indexOf(item) - if (position != -1 && !adapter.isSelected(position)) { - adapter.toggleSelection(position) - } - } - actionMode?.invalidate() - } - scrollToUnread() - } - - private fun scrollToUnread() { - if (adapter?.items.isNullOrEmpty()) return - if (scrollToUnread) { - val index = presenter.getFirstUnreadIndex() ?: return - val centerOfScreen = - if (startingChapterYPos != null) startingChapterYPos!!.toInt() - recycler.top - 96 - else recycler.height / 2 - 96 - (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - index, centerOfScreen - ) - } - scrollToUnread = false - } - - private fun initialFetchChapters() { - // Only fetch if this view is from the catalog and it hasn't requested previously - if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) { - fetchChaptersFromSource() - } - } - - private fun fetchChaptersFromSource() { - swipe_refresh?.isRefreshing = true - presenter.fetchChaptersFromSource() - } - - fun onFetchChaptersDone() { - swipe_refresh?.isRefreshing = false - } - - fun onFetchChaptersError(error: Throwable) { - swipe_refresh?.isRefreshing = false - activity?.toast(error.message) - } - - fun onChapterStatusChange(download: Download) { - getHolder(download.chapter)?.notifyStatus(download.status, presenter.isLockedFromSearch) - } - - private fun getHolder(chapter: Chapter): ChapterHolder? { - return 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 = adapter ?: return false - val item = adapter.getItem(position) ?: return false - if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { - lastClickPosition = position - toggleSelection(position) - return true - } else { - openChapter(item.chapter) - return 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 - adapter?.notifyDataSetChanged() - } - - // SELECTIONS & ACTION MODE - - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) ?: return - adapter.toggleSelection(position) - adapter.notifyDataSetChanged() - if (adapter.isSelected(position)) { - selectedItems.add(item) - } else { - selectedItems.remove(item) - } - actionMode?.invalidate() - } - - private fun setSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) ?: return - if (!adapter.isSelected(position)) { - adapter.toggleSelection(position) - selectedItems.add(item) - actionMode?.invalidate() - } - } - - private fun getSelectedChapters(): List { - val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } - } - - private fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) - } - } - - private fun destroyActionModeIfNeeded() { - lastClickPosition = -1 - actionMode?.finish() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_selection, menu) - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - @SuppressLint("StringFormatInvalid") - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = adapter?.selectedItemCount ?: 0 - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = resources?.getString(R.string.label_selected, count) - } - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_select_all -> selectAll() - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> showDeleteChaptersConfirmationDialog() - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - adapter?.mode = SelectableAdapter.Mode.SINGLE - adapter?.clearSelection() - selectedItems.clear() - actionMode = null - } - - override fun onDetach(view: View) { - destroyActionModeIfNeeded() - super.onDetach(view) - } - - override fun onMenuItemClick(position: Int, item: MenuItem) { - val chapter = adapter?.getItem(position) ?: return - val chapters = listOf(chapter) - - when (item.itemId) { - R.id.action_download -> downloadChapters(chapters) - R.id.action_bookmark -> bookmarkChapters(chapters, true) - R.id.action_remove_bookmark -> bookmarkChapters(chapters, false) - R.id.action_delete -> deleteChapters(chapters) - R.id.action_mark_as_read -> markAsRead(chapters) - R.id.action_mark_as_unread -> markAsUnread(chapters) - R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter) - } - } - - // SELECTION MODE ACTIONS - - private fun selectAll() { - val adapter = adapter ?: return - adapter.selectAll() - selectedItems.addAll(adapter.items) - actionMode?.invalidate() - } - - private fun markAsRead(chapters: List) { - presenter.markChaptersRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - } - - private fun markAsUnread(chapters: List) { - presenter.markChaptersRead(chapters, false) - } - - private fun downloadChapters(chapters: List) { - val view = view - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - if (view != null && !presenter.manga.favorite && (snack == null || - snack?.getText() != view.context.getString(R.string.snack_add_to_library))) { - snack = view.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_add) { - presenter.addToLibrary() - } - addCallback(object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (snack == transientBottomBar) snack = null - } - }) - } - (activity as? MainActivity)?.setUndoSnackBar(snack) - } - } - - private fun showDeleteChaptersConfirmationDialog() { - DeleteChaptersDialog(this).showDialog(router) - } - - override fun deleteChapters() { - deleteChapters(getSelectedChapters()) - } - - private fun markPreviousAsRead(chapter: ChapterItem) { - val adapter = adapter ?: return - val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items - val chapterPos = chapters.indexOf(chapter) - if (chapterPos != -1) { - markAsRead(chapters.take(chapterPos)) - } - } - - private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { - destroyActionModeIfNeeded() - presenter.bookmarkChapters(chapters, bookmarked) - } - - fun deleteChapters(chapters: List) { - destroyActionModeIfNeeded() - if (chapters.isEmpty()) return - presenter.deleteChapters(chapters) - } - - fun onChaptersDeleted(chapters: List) { - //this is needed so the downloaded text gets removed from the item - chapters.forEach { - adapter?.updateItem(it) - } - adapter?.notifyDataSetChanged() - } - - fun onChaptersDeletedError(error: Throwable) { - Timber.e(error) - } - - // OVERFLOW MENU DIALOGS - - private fun setDisplayMode(id: Int) { - presenter.setDisplayMode(id) - adapter?.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) - } - } - - 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) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt deleted file mode 100644 index e172e4d7fd..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ /dev/null @@ -1,443 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate -import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource -import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.Date - -/** - * Presenter of [ChaptersController]. - */ -class ChaptersPresenter( - val manga: Manga, - val source: Source, - private val chapterCountRelay: BehaviorRelay, - private val lastUpdateRelay: BehaviorRelay, - private val mangaFavoriteRelay: PublishRelay, - val preferences: PreferencesHelper = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get() -) : BasePresenter() { - - /** - * List of chapters of the manga. It's always unfiltered and unsorted. - */ - var chapters: List = emptyList() - private set - - /** - * Subject of list of chapters to allow updating the view without going to DB. - */ - val chaptersRelay: PublishRelay> - by lazy { PublishRelay.create>() } - - /** - * Whether the chapter list has been requested to the source. - */ - var hasRequested = false - private set - - /** - * Subscription to retrieve the new list of chapters from the source. - */ - private var fetchChaptersSubscription: Subscription? = null - - /** - * Subscription to observe download status changes. - */ - private var observeDownloadsSubscription: Subscription? = null - - var isLockedFromSearch = false - - fun updateLockStatus() { - val lastCheck = isLockedFromSearch - isLockedFromSearch = SecureActivityDelegate.shouldBeLocked() - if (lastCheck && lastCheck != isLockedFromSearch) { - chapters.forEach { - it.isLocked = false - } - chaptersRelay.call(chapters) - } - } - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - isLockedFromSearch = SecureActivityDelegate.shouldBeLocked() - - // Prepare the relay. - chaptersRelay.flatMap { applyChapterFilters(it) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(ChaptersController::onNextChapters - ) { _, error -> Timber.e(error) } - - // Add the subscription that retrieves the chapters from the database, keeps subscribed to - // changes, and sends the list of chapters to the relay. - add(db.getChapters(manga).asRxObservable() - .map { chapters -> - // Convert every chapter to a model. - chapters.map { it.toModel() } - } - .doOnNext { chapters -> - // Find downloaded chapters - setDownloadedChapters(chapters) - - // Store the last emission - this.chapters = chapters - - // Listen for download status changes - observeDownloads() - - // Emit the number of chapters to the info tab. - chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number - ?: 0f) - - // Emit the upload date of the most recent chapter - lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload - ?: 0)) - - } - .subscribe { chaptersRelay.call(it) }) - } - - private fun observeDownloads() { - observeDownloadsSubscription?.let { remove(it) } - observeDownloadsSubscription = downloadManager.queue.getStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .filter { download -> download.manga.id == manga.id } - .doOnNext { onDownloadStatusChange(it) } - .subscribeLatestCache(ChaptersController::onChapterStatusChange) { - _, error -> Timber.e(error) - } - } - - /** - * Converts a chapter from the database to an extended model, allowing to store new fields. - */ - private fun Chapter.toModel(): ChapterItem { - // Create the model object. - val model = ChapterItem(this, manga) - model.isLocked = isLockedFromSearch - - // Find an active download for this chapter. - val download = downloadManager.queue.find { it.chapter.id == id } - - if (download != null) { - // If there's an active download, assign it. - model.download = download - } - return model - } - - /** - * Finds and assigns the list of downloaded chapters. - * - * @param chapters the list of chapter from the database. - */ - private fun setDownloadedChapters(chapters: List) { - for (chapter in chapters) { - if (downloadManager.isChapterDownloaded(chapter, manga)) { - chapter.status = Download.DOWNLOADED - } - } - } - - /** - * Requests an updated list of chapters from the source. - */ - fun fetchChaptersFromSource() { - hasRequested = true - - if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return - fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } - .subscribeOn(Schedulers.io()) - .map { syncChaptersWithSource(db, it, manga, source) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onFetchChaptersDone() - }, ChaptersController::onFetchChaptersError) - } - - /** - * Updates the UI after applying the filters. - */ - private fun refreshChapters() { - chaptersRelay.call(chapters) - } - - /** - * Applies the view filters to the list of chapters obtained from the database. - * @param chapters the list of chapters from the database - * @return an observable of the list of chapters filtered and sorted. - */ - private fun applyChapterFilters(chapters: List): Observable> { - var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) - if (onlyUnread()) { - observable = observable.filter { !it.read } - } else if (onlyRead()) { - observable = observable.filter { it.read } - } - if (onlyDownloaded()) { - observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID } - } - if (onlyBookmarked()) { - observable = observable.filter { it.bookmark } - } - val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { - Manga.SORTING_SOURCE -> when (sortDescending()) { - true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } - false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } - } - Manga.SORTING_NUMBER -> when (sortDescending()) { - true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } - false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } - } - else -> throw NotImplementedError("Unimplemented sorting method") - } - return observable.toSortedList(sortFunction) - } - - /** - * Called when a download for the active manga changes status. - * @param download the download whose status changed. - */ - fun onDownloadStatusChange(download: Download) { - // Assign the download to the model object. - if (download.status == Download.QUEUE) { - chapters.find { it.id == download.chapter.id }?.let { - if (it.download == null) { - it.download = download - } - } - } - - // Force UI update if downloaded filter active and download finished. - if (onlyDownloaded() && download.status == Download.DOWNLOADED) - refreshChapters() - } - - /** - * Returns the next unread chapter or null if everything is read. - */ - fun getNextUnreadChapter(): ChapterItem? { - return chapters.sortedByDescending { it.source_order }.find { !it.read } - } - - /** - * Mark the selected chapter list as read/unread. - * @param selectedChapters the list of selected chapters. - * @param read whether to mark chapters as read or unread. - */ - fun markChaptersRead(selectedChapters: List, read: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.read = read - if (!read) { - chapter.last_page_read = 0 - chapter.pages_left = 0 - } - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Downloads the given list of chapters with the manager. - * @param chapters the list of chapters to download. - */ - fun downloadChapters(chapters: List) { - downloadManager.downloadChapters(manga, chapters) - } - - /** - * Bookmarks the given list of chapters. - * @param selectedChapters the list of chapters to bookmark. - */ - fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.bookmark = bookmarked - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Deletes the given list of chapter. - * @param chapters the list of chapters to delete. - */ - fun deleteChapters(chapters: List) { - Observable.just(chapters) - .doOnNext { deleteChaptersInternal(chapters) } - .doOnNext { if (onlyDownloaded()) refreshChapters() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onChaptersDeleted(chapters) - }, ChaptersController::onChaptersDeletedError) - } - - /** - * Deletes a list of chapters from disk. This method is called in a background thread. - * @param chapters the chapters to delete. - */ - private fun deleteChaptersInternal(chapters: List) { - downloadManager.deleteChapters(chapters, manga, source) - chapters.forEach { - it.status = Download.NOT_DOWNLOADED - it.download = null - } - } - - /** - * Reverses the sorting and requests an UI update. - */ - fun revertSortOrder() { - manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyUnread whether to display only unread chapters or all chapters. - */ - fun setUnreadFilter(onlyUnread: Boolean) { - manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyRead whether to display only read chapters or all chapters. - */ - fun setReadFilter(onlyRead: Boolean) { - manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the download filter and requests an UI update. - * @param onlyDownloaded whether to display only downloaded chapters or all chapters. - */ - fun setDownloadedFilter(onlyDownloaded: Boolean) { - manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the bookmark filter and requests an UI update. - * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. - */ - fun setBookmarkedFilter(onlyBookmarked: Boolean) { - manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Removes all filters and requests an UI update. - */ - fun removeFilters() { - manga.readFilter = Manga.SHOW_ALL - manga.downloadedFilter = Manga.SHOW_ALL - manga.bookmarkedFilter = Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Adds manga to library - */ - fun addToLibrary() { - mangaFavoriteRelay.call(true) - } - - /** - * Sets the active display mode. - * @param mode the mode to set. - */ - fun setDisplayMode(mode: Int) { - manga.displayMode = mode - db.updateFlags(manga).executeAsBlocking() - } - - /** - * Sets the sorting method and requests an UI update. - * @param sort the sorting mode. - */ - fun setSorting(sort: Int) { - manga.sorting = sort - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyDownloaded(): Boolean { - return manga.downloadedFilter == Manga.SHOW_DOWNLOADED - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyBookmarked(): Boolean { - return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED - } - - /** - * Whether the display only unread filter is enabled. - */ - fun onlyUnread(): Boolean { - return manga.readFilter == Manga.SHOW_UNREAD - } - - /** - * Whether the display only read filter is enabled. - */ - fun onlyRead(): Boolean { - return manga.readFilter == Manga.SHOW_READ - } - - /** - * Whether the sorting method is descending or ascending. - */ - fun sortDescending(): Boolean { - return manga.sortDescending() - } - - fun getFirstUnreadIndex(): Int? { - if (!manga.favorite) { - return null - } - val index = chapters.sortedByDescending { it.source_order }.indexOfFirst { !it.read } - return if (sortDescending()) (chapters.size - 1) - index - else index - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt deleted file mode 100644 index 234fd88c6d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt +++ /dev/null @@ -1,31 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.app.Dialog -import android.os.Bundle -import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.Controller -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -class DeleteChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : DeleteChaptersDialog.Listener { - - constructor(target: T) : this() { - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialDialog(activity!!).show { - message(R.string.confirm_delete_chapters) - positiveButton(android.R.string.yes) { - (targetController as? Listener)?.deleteChapters() - } - negativeButton(android.R.string.no) - } - } - - interface Listener { - fun deleteChapters() - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt deleted file mode 100644 index 43c355ea1b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ /dev/null @@ -1,860 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.app.Activity -import android.app.PendingIntent -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.os.Build -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 android.view.animation.DecelerateInterpolator -import android.widget.ImageView -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import androidx.transition.ChangeBounds -import androidx.transition.ChangeImageTransform -import androidx.transition.TransitionManager -import androidx.transition.TransitionSet -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition -import com.bumptech.glide.signature.ObjectKey -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.view.clicks -import com.jakewharton.rxbinding.view.longClicks -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.data.notification.NotificationReceiver -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController -import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.ChooseShapeDialog -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate -import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import eu.kanade.tachiyomi.util.storage.getUriCompat -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.view.marginBottom -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.updateLayoutParams -import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import jp.wasabeef.glide.transformations.CropSquareTransformation -import jp.wasabeef.glide.transformations.MaskTransformation -import kotlinx.android.synthetic.main.manga_info_controller.* -import uy.kohesive.injekt.injectLazy -import java.io.File -import java.text.DateFormat -import java.text.DecimalFormat -import java.util.Date -import kotlin.math.max - -/** - * Fragment that shows manga information. - * Uses R.layout.manga_info_controller. - * UI related actions should be called from here. - */ -class MangaInfoController : NucleusController(), - ChangeMangaCategoriesDialog.Listener { - - /** - * Preferences helper. - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * Snackbar containing an error message when a request fails. - */ - private var snack: Snackbar? = null - - private var container:View? = null - - // Hold a reference to the current animator, - // so that it can be canceled mid-way. - private var currentAnimator: Animator? = null - - // The system "short" animation time duration, in milliseconds. This - // duration is ideal for subtle animations or animations that occur - // very frequently. - private var shortAnimationDuration: Int = 0 - - private var setUpFullCover = false - - var fullRes:Drawable? = null - - private val dateFormat: DateFormat by lazy { - preferences.dateFormat().getOrDefault() - } - - init { - setHasOptionsMenu(true) - setOptionsMenuHidden(true) - } - - override fun createPresenter(): MangaInfoPresenter { - val ctrl = parentController as MangaController - return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.manga_info_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - setUpFullCover = false - // Set onclickListener to toggle favorite when FAB clicked. - fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } - - // Set onLongClickListener to manage categories when FAB is clicked. - fab_favorite.longClicks().subscribeUntilDestroy { onFabLongClick() } - - // Set SwipeRefresh to refresh manga data. - swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() } - - manga_full_title.longClicks().subscribeUntilDestroy { - copyToClipboard(view.context.getString(R.string.title), manga_full_title.text - .toString(), R.string.manga_info_full_title_label) - } - - manga_full_title.clicks().subscribeUntilDestroy { - performGlobalSearch(manga_full_title.text.toString()) - } - - manga_artist.longClicks().subscribeUntilDestroy { - copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString(), R - .string.manga_info_artist_label) - } - - manga_artist.clicks().subscribeUntilDestroy { - performGlobalSearch(manga_artist.text.toString()) - } - - manga_author.longClicks().subscribeUntilDestroy { - copyToClipboard(manga_author.text.toString(), manga_author.text.toString(), R.string - .manga_info_author_label) - } - - manga_author.clicks().subscribeUntilDestroy { - performGlobalSearch(manga_author.text.toString()) - } - - manga_summary.longClicks().subscribeUntilDestroy { - copyToClipboard(view.context.getString(R.string.description), manga_summary.text - .toString(), R.string.description) - } - - manga_genres_tags.setOnTagClickListener { tag -> performLocalSearch(tag) } - - manga_cover.clicks().subscribeUntilDestroy { - if (manga_cover.drawable != null) zoomImageFromThumb(manga_cover, manga_cover.drawable) - } - - // Retrieve and cache the system's default "short" animation time. - shortAnimationDuration = resources?.getInteger(android.R.integer.config_shortAnimTime) ?: 0 - - manga_cover.longClicks().subscribeUntilDestroy { - copyToClipboard(view.context.getString(R.string.title), presenter.manga.currentTitle(), R.string - .manga_info_full_title_label) - } - container = (view as ViewGroup).findViewById(R.id.manga_info_layout) as? View - val bottomM = manga_genres_tags.marginBottom - val fabBaseMarginBottom = fab_favorite.marginBottom - val mangaCoverMarginBottom = manga_cover.marginBottom - val fullMarginBottom = manga_cover_full?.marginBottom ?: 0 - manga_cover.viewTreeObserver.addOnGlobalLayoutListener { - setFullCoverToThumb() - } - container?.setOnApplyWindowInsetsListener { _, insets -> - if (resources?.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) { - fab_favorite?.updateLayoutParams { - bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom - } - manga_cover?.updateLayoutParams { - bottomMargin = mangaCoverMarginBottom + insets.systemWindowInsetBottom - } - } else { - manga_genres_tags?.updateLayoutParams { - bottomMargin = bottomM + insets.systemWindowInsetBottom - } - } - manga_cover_full?.updateLayoutParams { - bottomMargin = fullMarginBottom + insets.systemWindowInsetBottom - } - insets - } - info_scrollview.doOnApplyWindowInsets { v, insets, padding -> - if (resources?.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) { - v.updatePaddingRelative( - bottom = max(padding.bottom, insets.systemWindowInsetBottom) - ) - } - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.manga_info, menu) - - val editItem = menu.findItem(R.id.action_edit) - editItem.isVisible = presenter.manga.favorite && - !(parentController as MangaController).isLockedFromSearch - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - //R.id.action_edit -> EditMangaDialog(this, presenter.manga).showDialog(router) - R.id.action_open_in_web_view -> openInWebView() - R.id.action_share -> prepareToShareManga() - R.id.action_add_to_home_screen -> addToHomeScreen() - } - return super.onOptionsItemSelected(item) - } - - /** - * Check if manga is initialized. - * If true update view with manga information, - * if false fetch manga information - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - fun onNextManga(manga: Manga, source: Source) { - if (manga.initialized) { - // Update view. - setMangaInfo(manga, source) - - } else { - // Initialize manga. - fetchMangaFromSource() - } - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - private fun setMangaInfo(manga: Manga, source: Source?) { - val view = view ?: return - - //update full title TextView. - manga_full_title.text = if (manga.currentTitle().isBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.currentTitle() - } - - // Update artist TextView. - manga_artist.text = if (manga.currentArtist().isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.currentArtist() - } - - // Update author TextView. - manga_author.text = if (manga.currentAuthor().isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.currentAuthor() - } - - // If manga source is known update source TextView. - manga_source.text = source?.toString() ?: view.context.getString(R.string.unknown) - - // Update genres list - if (manga.currentGenres().isNullOrBlank().not()) { - manga_genres_tags.setTags(manga.currentGenres()?.split(", ")) - } - else manga_genres_tags.setTags(emptyList()) - - // Update description TextView. - manga_summary.text = if (manga.currentDesc().isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.currentDesc() - } - - // Update status TextView. - manga_status.setText(when (manga.status) { - SManga.ONGOING -> R.string.ongoing - SManga.COMPLETED -> R.string.completed - SManga.LICENSED -> R.string.licensed - else -> R.string.unknown - }) - - // Set the favorite drawable to the correct one. - setFavoriteDrawable(manga.favorite) - activity?.invalidateOptionsMenu() - - // Set cover if it wasn't already. - if (!manga.thumbnail_url.isNullOrEmpty()) { - GlideApp.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - .transition(DrawableTransitionOptions.withCrossFade()) - //.centerCrop() - .into(manga_cover) - if (manga_cover_full != null) { - GlideApp.with(view.context).asDrawable().load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - .override(CustomTarget.SIZE_ORIGINAL, CustomTarget.SIZE_ORIGINAL) - .into(object : CustomTarget() { - override fun onResourceReady(resource: Drawable, - transition: Transition? - ) { - fullRes = resource - } - - override fun onLoadCleared(placeholder: Drawable?) { } - }) - } - - if (backdrop != null) { - GlideApp.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - .transition(DrawableTransitionOptions.withCrossFade()) - .centerCrop() - .into(backdrop) - } - } - } - - override fun onActivityResumed(activity: Activity) { - super.onActivityResumed(activity) - setFavoriteDrawable(presenter.manga.favorite) - } - - override fun onDestroyView(view: View) { - manga_genres_tags.setOnTagClickListener(null) - snack?.dismiss() - super.onDestroyView(view) - } - - /** - * Update chapter count TextView. - * - * @param count number of chapters. - */ - fun setChapterCount(count: Float) { - if (count > 0f) { - manga_chapters?.text = DecimalFormat("#.#").format(count) - } else { - manga_chapters?.text = resources?.getString(R.string.unknown) - } - } - - fun setLastUpdateDate(date: Date) { - if (date.time != 0L) { - manga_status?.text = dateFormat.format(date) - } else { - manga_status?.text = resources?.getString(R.string.unknown) - } - } - - /** - * Toggles the favorite status and asks for confirmation to delete downloaded chapters. - */ - private fun toggleFavorite() { - presenter.toggleFavorite() - } - - private fun openInWebView() { - val source = presenter.source as? HttpSource ?: return - - val url = try { - source.mangaDetailsRequest(presenter.manga).url.toString() - } catch (e: Exception) { - return - } - - val activity = activity ?: return - val intent = WebViewActivity.newIntent(activity.applicationContext, source.id, url, presenter.manga - .originalTitle()) - startActivity(intent) - } - - /** - * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. - */ - private fun prepareToShareManga() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && manga_cover.drawable != null) - GlideApp.with(activity!!).asBitmap().load(presenter.manga).into(object : - CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - presenter.shareManga(resource) - } - override fun onLoadCleared(placeholder: Drawable?) {} - - override fun onLoadFailed(errorDrawable: Drawable?) { - shareManga() - } - }) - else shareManga() - } - - /** - * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. - */ - fun shareManga(cover: File? = null) { - val context = view?.context ?: return - - val source = presenter.source as? HttpSource ?: return - val stream = cover?.getUriCompat(context) - try { - val url = source.mangaDetailsRequest(presenter.manga).url.toString() - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/*" - putExtra(Intent.EXTRA_TEXT, url) - putExtra(Intent.EXTRA_TITLE, presenter.manga.currentTitle()) - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - if (stream != null) { - clipData = ClipData.newRawUri(null, stream) - } - } - startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Update FAB with correct drawable. - * - * @param isFavorite determines if manga is favorite or not. - */ - private fun setFavoriteDrawable(isFavorite: Boolean) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - fab_favorite?.setImageResource( - when { - (parentController as MangaController).isLockedFromSearch -> R.drawable.ic_lock_white_24dp - isFavorite -> R.drawable.ic_bookmark_white_24dp - else -> R.drawable.ic_add_to_library_24dp - } - ) - } - - /** - * Start fetching manga information from source. - */ - private fun fetchMangaFromSource() { - setRefreshing(true) - // Call presenter and start fetching manga information - presenter.fetchMangaFromSource() - } - - - /** - * Update swipe refresh to stop showing refresh in progress spinner. - */ - fun onFetchMangaDone() { - setRefreshing(false) - } - - /** - * Update swipe refresh to start showing refresh in progress spinner. - */ - fun onFetchMangaError(error: Throwable) { - setRefreshing(false) - activity?.toast(error.message) - } - - /** - * Set swipe refresh status. - * - * @param value whether it should be refreshing or not. - */ - private fun setRefreshing(value: Boolean) { - swipe_refresh?.isRefreshing = value - } - - /** - * Called when the fab is clicked. - */ - private fun onFabClick() { - if ((parentController as MangaController).isLockedFromSearch) { - SecureActivityDelegate.promptLockIfNeeded(activity) - return - } - val manga = presenter.manga - toggleFavorite() - if (manga.favorite) { - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } - when { - defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) - defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category - presenter.moveMangaToCategory(manga, null) - 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) - } - } - showAddedSnack() - } else { - showRemovedSnack() - } - } - - private fun showAddedSnack() { - val view = container - snack?.dismiss() - snack = view?.snack(view.context.getString(R.string.manga_added_library)) - } - - private fun showRemovedSnack() { - val view = container - snack?.dismiss() - if (view != null) { - snack = view.snack(view.context.getString(R.string.manga_removed_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_undo) { - presenter.setFavorite(true) - } - addCallback(object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (!presenter.manga.favorite) - presenter.confirmDeletion() - } - }) - } - (activity as? MainActivity)?.setUndoSnackBar(snack, fab_favorite) - } - } - - /** - * Called when the fab is long clicked. - */ - private fun onFabLongClick() { - val manga = presenter.manga - if (!manga.favorite) { - toggleFavorite() - showAddedSnack() - } - val categories = presenter.getCategories() - if (categories.isEmpty()) { - // no categories exist, display a message about adding categories - snack = container?.snack(R.string.action_add_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) - } - } - - override fun updateCategoriesForMangas(mangas: List, categories: List) { - val manga = mangas.firstOrNull() ?: return - presenter.moveMangaToCategories(manga, categories) - } - - /** - * Add a shortcut of the manga to the home screen - */ - private fun addToHomeScreen() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // TODO are transformations really unsupported or is it just the Pixel Launcher? - createShortcutForShape() - } else { - ChooseShapeDialog(this).showDialog(router) - } - } - - /** - * Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when - * the resource is available. - * - * @param i The shape index to apply. Defaults to circle crop transformation. - */ - fun createShortcutForShape(i: Int = 0) { - if (activity == null) return - GlideApp.with(activity!!) - .asBitmap() - .load(presenter.manga) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .apply { - when (i) { - 0 -> circleCrop() - 1 -> transform(RoundedCorners(5)) - 2 -> transform(CropSquareTransformation()) - 3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star)) - } - } - .into(object : CustomTarget(96, 96) { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - createShortcut(resource) - } - - override fun onLoadCleared(placeholder: Drawable?) { } - - override fun onLoadFailed(errorDrawable: Drawable?) { - activity?.toast(R.string.icon_creation_fail) - } - }) - } - - /** - * Copies a string to clipboard - * - * @param label Label to show to the user describing the content - * @param content the actual text to copy to the board - */ - private fun copyToClipboard(label: String, content: String, resId: Int) { - if (content.isBlank()) return - - val activity = activity ?: return - val view = view ?: return - - val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setPrimaryClip(ClipData.newPlainText(label, content)) - - snack = container?.snack(view.context.getString(R.string.copied_to_clipboard, view.context - .getString(resId))) - } - - /** - * Perform a global search using the provided query. - * - * @param query the search query to pass to the search controller - */ - private fun performGlobalSearch(query: String) { - if ((parentController as MangaController).isLockedFromSearch) - return - val router = parentController?.router ?: return - router.pushController(CatalogueSearchController(query).withFadeTransaction()) - } - - /** - * Perform a local search using the provided query. - * - * @param query the search query to pass to the library controller - */ - private fun performLocalSearch(query: String) { - val router = parentController?.router ?: return - val firstController = router.backstack.first()?.controller() - if (firstController is LibraryController && router.backstack.size == 2) { - router.handleBack() - firstController.search(query) - } - } - - /** - * Create shortcut using ShortcutManager. - * - * @param icon The image of the shortcut. - */ - private fun createShortcut(icon: Bitmap) { - val activity = activity ?: return - val mangaControllerArgs = parentController?.args ?: return - - // Create the shortcut intent. - val shortcutIntent = activity.intent - .setAction(MainActivity.SHORTCUT_MANGA) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(MangaController.MANGA_EXTRA, - mangaControllerArgs.getLong(MangaController.MANGA_EXTRA)) - - // Check if shortcut placement is supported - if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) { - val shortcutId = "manga-shortcut-${presenter.manga.originalTitle()}-${presenter.source.name}" - - // Create shortcut info - val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId) - .setShortLabel(presenter.manga.currentTitle()) - .setIcon(IconCompat.createWithBitmap(icon)) - .setIntent(shortcutIntent) - .build() - - val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create the CallbackIntent. - val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo) - - // Configure the intent so that the broadcast receiver gets the callback successfully. - PendingIntent.getBroadcast(activity, 0, intent, 0) - } else { - NotificationReceiver.shortcutCreatedBroadcast(activity) - } - - // Request shortcut. - ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo, - successCallback.intentSender) - } - } - - fun updateTitle() { - setMangaInfo(presenter.manga, presenter.source) - (parentController as? MangaController)?.updateTitle(presenter.manga) - } - - private fun setFullCoverToThumb() { - if (setUpFullCover) return - val expandedImageView = manga_cover_full ?: return - val thumbView = manga_cover - expandedImageView.pivotX = 0f - expandedImageView.pivotY = 0f - - val layoutParams = expandedImageView.layoutParams - layoutParams.height = thumbView.height - layoutParams.width = thumbView.width - expandedImageView.layoutParams = layoutParams - expandedImageView.scaleType = ImageView.ScaleType.FIT_CENTER - setUpFullCover = expandedImageView.height > 0 - } - - override fun handleBack(): Boolean { - if (manga_cover_full?.visibility == View.VISIBLE && - (parentController as? MangaController)?.tabLayout()?.selectedTabPosition == 0) - { - manga_cover_full?.performClick() - return true - } - return super.handleBack() - } - - private fun zoomImageFromThumb(thumbView: ImageView, cover: Drawable) { - // If there's an animation in progress, cancel it immediately and proceed with this one. - currentAnimator?.cancel() - - // Load the high-resolution "zoomed-in" image. - val expandedImageView = manga_cover_full ?: return - val fullBackdrop = full_backdrop - val image = fullRes ?: return - expandedImageView.setImageDrawable(image) - - // Hide the thumbnail and show the zoomed-in view. When the animation - // begins, it will position the zoomed-in view in the place of the - // thumbnail. - thumbView.alpha = 0f - expandedImageView.visibility = View.VISIBLE - fullBackdrop.visibility = View.VISIBLE - - // Set the pivot point to 0 to match thumbnail - - swipe_refresh.isEnabled = false - - val layoutParams = expandedImageView.layoutParams - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - expandedImageView.layoutParams = layoutParams - - // TransitionSet for the full cover because using animation for this SUCKS - val transitionSet = TransitionSet() - val bound = ChangeBounds() - transitionSet.addTransition(bound) - val changeImageTransform = ChangeImageTransform() - transitionSet.addTransition(changeImageTransform) - transitionSet.duration = shortAnimationDuration.toLong() - TransitionManager.beginDelayedTransition(manga_info_layout, transitionSet) - - // AnimationSet for backdrop because idk how to use TransitionSet - currentAnimator = AnimatorSet().apply { - play( - ObjectAnimator.ofFloat(fullBackdrop, View.ALPHA, 0f, 0.5f) - ) - duration = shortAnimationDuration.toLong() - interpolator = DecelerateInterpolator() - addListener(object : AnimatorListenerAdapter() { - - override fun onAnimationEnd(animation: Animator) { - TransitionManager.endTransitions(manga_info_layout) - currentAnimator = null - } - - override fun onAnimationCancel(animation: Animator) { - TransitionManager.endTransitions(manga_info_layout) - currentAnimator = null - } - }) - start() - } - - expandedImageView.setOnClickListener { - currentAnimator?.cancel() - - val layoutParams = expandedImageView.layoutParams - layoutParams.height = thumbView.height - layoutParams.width = thumbView.width - expandedImageView.layoutParams = layoutParams - - // Zoom out back to tc thumbnail - val transitionSet = TransitionSet() - val bound = ChangeBounds() - transitionSet.addTransition(bound) - val changeImageTransform = ChangeImageTransform() - transitionSet.addTransition(changeImageTransform) - transitionSet.duration = shortAnimationDuration.toLong() - TransitionManager.beginDelayedTransition(manga_info_layout, transitionSet) - - // Animation to remove backdrop and hide the full cover - currentAnimator = AnimatorSet().apply { - play(ObjectAnimator.ofFloat(fullBackdrop, View.ALPHA, 0f)) - duration = shortAnimationDuration.toLong() - interpolator = DecelerateInterpolator() - addListener(object : AnimatorListenerAdapter() { - - override fun onAnimationEnd(animation: Animator) { - thumbView.alpha = 1f - expandedImageView.visibility = View.GONE - fullBackdrop.visibility = View.GONE - swipe_refresh.isEnabled = true - currentAnimator = null - } - - override fun onAnimationCancel(animation: Animator) { - thumbView.alpha = 1f - expandedImageView.visibility = View.GONE - fullBackdrop.visibility = View.GONE - swipe_refresh.isEnabled = true - currentAnimator = null - } - }) - start() - } - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt deleted file mode 100644 index 43e6353e5a..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ /dev/null @@ -1,290 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.app.Application -import android.graphics.Bitmap -import android.net.Uri -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed -import eu.kanade.tachiyomi.util.storage.DiskUtil -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream -import java.util.Date - -/** - * Presenter of MangaInfoFragment. - * Contains information and data for fragment. - * Observable updates should be called from here. - */ -class MangaInfoPresenter( - val manga: Manga, - val source: Source, - private val chapterCountRelay: BehaviorRelay, - private val lastUpdateRelay: BehaviorRelay, - private val mangaFavoriteRelay: PublishRelay, - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get(), - private val coverCache: CoverCache = Injekt.get() -) : BasePresenter() { - - /** - * Subscription to send the manga to the view. - */ - private var viewMangaSubscription: Subscription? = null - - /** - * Subscription to update the manga from the source. - */ - private var fetchMangaSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - sendMangaToView() - - // Update chapter count - chapterCountRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoController::setChapterCount) - - // Update favorite status - mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribe { setFavorite(it) } - .apply { add(this) } - - //update last update date - lastUpdateRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoController::setLastUpdateDate) - } - - /** - * Sends the active manga to the view. - */ - fun sendMangaToView() { - viewMangaSubscription?.let { remove(it) } - viewMangaSubscription = Observable.just(manga) - .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) - } - - /** - * Fetch manga information from source. - */ - fun fetchMangaFromSource() { - if (!fetchMangaSubscription.isNullOrUnsubscribed()) return - fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } - .map { networkManga -> - manga.copyFrom(networkManga) - manga.initialized = true - db.insertManga(manga).executeAsBlocking() - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) - manga - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { sendMangaToView() } - .subscribeFirst({ view, _ -> - view.onFetchMangaDone() - }, MangaInfoController::onFetchMangaError) - } - - /** - * Update favorite status of manga, (removes / adds) manga (to / from) library. - * - * @return the new status of the manga. - */ - fun toggleFavorite(): Boolean { - manga.favorite = !manga.favorite - db.insertManga(manga).executeAsBlocking() - sendMangaToView() - return manga.favorite - } - - fun confirmDeletion() { - coverCache.deleteFromCache(manga.thumbnail_url) - db.resetMangaInfo(manga).executeAsBlocking() - downloadManager.deleteManga(manga, source) - } - - fun setFavorite(favorite: Boolean) { - if (manga.favorite == favorite) { - return - } - toggleFavorite() - } - - fun shareManga(cover: Bitmap) { - val context = Injekt.get() - - val destDir = File(context.cacheDir, "shared_image") - - Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file - .map { saveImage(cover, destDir, manga) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, file -> view.shareManga(file) }, - { view, error -> view.shareManga() } - ) - } - - private fun saveImage(cover:Bitmap, directory: File, manga: Manga): File? { - directory.mkdirs() - - // Build destination file. - val filename = DiskUtil.buildValidFilename("${manga.originalTitle()} - Cover.jpg") - - val destFile = File(directory, filename) - val stream: OutputStream = FileOutputStream(destFile) - cover.compress(Bitmap.CompressFormat.JPEG, 75, stream) - stream.flush() - stream.close() - return destFile - } - - /** - * Get user categories. - * - * @return List of categories, not including the default category - */ - fun getCategories(): List { - return db.getCategories().executeAsBlocking() - } - - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(manga: Manga): Array { - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - return categories.mapNotNull { it.id }.toTypedArray() - } - - /** - * Move the given manga to categories. - * - * @param manga the manga to move. - * @param categories the selected categories. - */ - fun moveMangaToCategories(manga: Manga, categories: List) { - val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } - db.setMangaCategories(mc, listOf(manga)) - } - - /** - * Move the given manga to the category. - * - * @param manga the manga to move. - * @param category the selected category, or null for default category. - */ - fun moveMangaToCategory(manga: Manga, category: Category?) { - moveMangaToCategories(manga, listOfNotNull(category)) - } - - fun updateManga(title:String?, author:String?, artist: String?, uri: Uri?, - description: String?, tags: Array?) { - if (manga.source == LocalSource.ID) { - manga.title = if (title.isNullOrBlank()) manga.url else title.trim() - manga.author = author?.trim() - manga.artist = artist?.trim() - manga.description = description?.trim() - val tagsString = tags?.joinToString(", ") { it.capitalize() } - manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim() - LocalSource(downloadManager.context).updateMangaInfo(manga) - db.updateMangaInfo(manga).executeAsBlocking() - } - else { - var changed = false - val title = title?.trim() - if (!title.isNullOrBlank() && manga.originalTitle().isBlank()) { - manga.title = title - changed = true - } - else if (title.isNullOrBlank() && manga.currentTitle() != manga.originalTitle()) { - manga.title = manga.originalTitle() - changed = true - } else if (!title.isNullOrBlank() && title != manga.currentTitle()) { - manga.title = "${title}${SManga.splitter}${manga.originalTitle()}" - changed = true - } - - val author = author?.trim() - if (author.isNullOrBlank() && manga.currentAuthor() != manga.originalAuthor()) { - manga.author = manga.originalAuthor() - changed = true - } else if (!author.isNullOrBlank() && author != manga.currentAuthor()) { - manga.author = "${author}${SManga.splitter}${manga.originalAuthor() ?: ""}" - changed = true - } - - val artist = artist?.trim() - if (artist.isNullOrBlank() && manga.currentArtist() != manga.originalArtist()) { - manga.artist = manga.originalArtist() - changed = true - } else if (!artist.isNullOrBlank() && artist != manga.currentArtist()) { - manga.artist = "${artist}${SManga.splitter}${manga.originalArtist() ?: ""}" - changed = true - } - - val description = description?.trim() - if (description.isNullOrBlank() && manga.currentDesc() != manga.originalDesc()) { - manga.description = manga.originalDesc() - changed = true - } else if (!description.isNullOrBlank() && description != manga.currentDesc()) { - manga.description = "${description}${SManga.splitter}${manga.originalDesc() ?: ""}" - changed = true - } - - var tagsString = tags?.joinToString(", ") - if ((tagsString.isNullOrBlank() && manga.currentGenres() != manga.originalGenres()) - || tagsString == manga.originalGenres()) { - manga.genre = manga.originalGenres() - changed = true - } else if (!tagsString.isNullOrBlank() && tagsString != manga.currentGenres()) { - tagsString = tags?.joinToString(", ") { it.capitalize() } - manga.genre = "${tagsString}${SManga.splitter}${manga.originalGenres() ?: ""}" - changed = true - } - if (changed) db.updateMangaInfo(manga).executeAsBlocking() - } - if (uri != null) editCoverWithStream(uri) - - } - - private fun editCoverWithStream(uri: Uri): Boolean { - val inputStream = downloadManager.context.contentResolver.openInputStream(uri) ?: - return false - if (manga.source == LocalSource.ID) { - LocalSource.updateCover(downloadManager.context, manga, inputStream) - return true - } - - if (manga.thumbnail_url != null && manga.favorite) { - Injekt.get().refreshCoversToo().set(false) - coverCache.copyToCache(manga.thumbnail_url!!, inputStream) - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) - return true - } - return false - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt deleted file mode 100644 index 5776015311..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt +++ /dev/null @@ -1,168 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener -import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.util.view.invisible -import eu.kanade.tachiyomi.util.view.visible -import kotlinx.android.synthetic.main.track_controller.* -import timber.log.Timber - -class TrackController : NucleusController(), - TrackAdapter.OnClickListener, - SetTrackStatusDialog.Listener, - SetTrackChaptersDialog.Listener, - SetTrackScoreDialog.Listener { - - private var adapter: TrackAdapter? = null - - init { - // There's no menu, but this avoids a bug when coming from the catalogue, where the menu - // disappears if the searchview is expanded - setHasOptionsMenu(true) - } - - override fun createPresenter(): TrackPresenter { - return TrackPresenter((parentController as MangaController).manga!!) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.track_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - if ((parentController as MangaController).isLockedFromSearch) { - swipe_refresh.invisible() - unlock_button.visible() - unlock_button.setOnClickListener { - SecureActivityDelegate.promptLockIfNeeded(activity) - } - } - - adapter = TrackAdapter(this) - track_recycler.layoutManager = LinearLayoutManager(view.context) - track_recycler.adapter = adapter - track_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - swipe_refresh.isEnabled = false - swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() } - } - - private fun showTracking() { - swipe_refresh.visible() - unlock_button.gone() - } - - override fun onActivityResumed(activity: Activity) { - super.onActivityResumed(activity) - if (!(parentController as MangaController).isLockedFromSearch) { - showTracking() - } - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - fun onNextTrackings(trackings: List) { - val atLeastOneLink = trackings.any { it.track != null } - adapter?.items = trackings - swipe_refresh?.isEnabled = atLeastOneLink - (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) - } - - fun onSearchResults(results: List) { - getSearchDialog()?.onSearchResults(results) - } - - @Suppress("UNUSED_PARAMETER") - fun onSearchResultsError(error: Throwable) { - Timber.e(error) - getSearchDialog()?.onSearchResultsError() - } - - private fun getSearchDialog(): TrackSearchDialog? { - return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog - } - - fun onRefreshDone() { - swipe_refresh?.isRefreshing = false - } - - fun onRefreshError(error: Throwable) { - swipe_refresh?.isRefreshing = false - activity?.toast(error.message) - } - - override fun onLogoClick(position: Int) { - val track = adapter?.getItem(position)?.track ?: return - - if (track.tracking_url.isNullOrBlank()) { - activity?.toast(R.string.url_not_set) - } else { - activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url))) - } - } - - override fun onSetClick(position: Int) { - val item = adapter?.getItem(position) ?: return - TrackSearchDialog(this, item.service, item.track != null).showDialog(router, - TAG_SEARCH_CONTROLLER) - } - - override fun onStatusClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackStatusDialog(this, item).showDialog(router) - } - - override fun onChaptersClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackChaptersDialog(this, item).showDialog(router) - } - - override fun onScoreClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackScoreDialog(this, item).showDialog(router) - } - - override fun setStatus(item: TrackItem, selection: Int) { - presenter.setStatus(item, selection) - swipe_refresh?.isRefreshing = true - } - - override fun setScore(item: TrackItem, score: Int) { - presenter.setScore(item, score) - swipe_refresh?.isRefreshing = true - } - - override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { - presenter.setLastChapterRead(item, chaptersRead) - swipe_refresh?.isRefreshing = true - } - - private companion object { - const val TAG_SEARCH_CONTROLLER = "track_search_controller" - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt deleted file mode 100644 index 5978c758f1..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt +++ /dev/null @@ -1,130 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.os.Bundle -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.system.toast -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - - -class TrackPresenter( - val manga: Manga, - preferences: PreferencesHelper = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val trackManager: TrackManager = Injekt.get() -) : BasePresenter() { - - private val context = preferences.context - - private var trackList: List = emptyList() - - private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } - - private var trackSubscription: Subscription? = null - - private var searchSubscription: Subscription? = null - - private var refreshSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - fetchTrackings() - } - - fun fetchTrackings() { - trackSubscription?.let { remove(it) } - trackSubscription = db.getTracks(manga) - .asRxObservable() - .map { tracks -> - loggedServices.map { service -> - TrackItem(tracks.find { it.sync_id == service.id }, service) - } - } - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { trackList = it } - .subscribeLatestCache(TrackController::onNextTrackings) - } - - fun refresh() { - refreshSubscription?.let { remove(it) } - refreshSubscription = Observable.from(trackList) - .filter { it.track != null } - .concatMap { item -> - item.service.refresh(item.track!!) - .flatMap { db.insertTrack(it).asRxObservable() } - .map { item } - .onErrorReturn { item } - } - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> view.onRefreshDone() }, - TrackController::onRefreshError) - } - - fun search(query: String, service: TrackService) { - searchSubscription?.let { remove(it) } - searchSubscription = service.search(query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(TrackController::onSearchResults, - TrackController::onSearchResultsError) - } - - fun registerTracking(item: Track?, service: TrackService) { - if (item != null) { - item.manga_id = manga.id!! - add(service.bind(item) - .flatMap { db.insertTrack(item).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> view.onRefreshDone() }, - TrackController::onRefreshError)) - } else { - db.deleteTrackForManga(manga, service).executeAsBlocking() - } - } - - private fun updateRemote(track: Track, service: TrackService) { - service.update(track) - .flatMap { db.insertTrack(track).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> view.onRefreshDone() }, - { view, error -> - view.onRefreshError(error) - - // Restart on error to set old values - fetchTrackings() - }) - } - - fun setStatus(item: TrackItem, index: Int) { - val track = item.track!! - track.status = item.service.getStatusList()[index] - updateRemote(track, item.service) - } - - fun setScore(item: TrackItem, index: Int) { - val track = item.track!! - track.score = item.service.indexToScore(index) - updateRemote(track, item.service) - } - - fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { - val track = item.track!! - track.last_chapter_read = chapterNumber - updateRemote(track, item.service) - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt index d5c5ab3c41..2cb775a555 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt @@ -49,15 +49,6 @@ class TrackSearchDialog : DialogController { private var wasPreviouslyTracked:Boolean = false private lateinit var presenter:MangaDetailsPresenter - constructor(target: TrackController, service: TrackService, wasTracked:Boolean) : super(Bundle() - .apply { - putInt(KEY_SERVICE, service.id) - }) { - wasPreviouslyTracked = wasTracked - targetController = target - this.service = service - } - constructor(target: TrackingBottomSheet, service: TrackService, wasTracked:Boolean) : super(Bundle() .apply { putInt(KEY_SERVICE, service.id)