Deleting the old manga detail controllers
This commit is contained in:
parent
e68a1ae48d
commit
dbf5424b77
@ -21,14 +21,14 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
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.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
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.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
@ -405,7 +405,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
val newIntent =
|
val newIntent =
|
||||||
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
.putExtra(MangaController.MANGA_EXTRA, manga.id)
|
.putExtra(MangaDetailsController.MANGA_EXTRA, manga.id)
|
||||||
.putExtra("notificationId", manga.id.hashCode())
|
.putExtra("notificationId", manga.id.hashCode())
|
||||||
.putExtra("groupId", groupId)
|
.putExtra("groupId", groupId)
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
|
@ -6,17 +6,12 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog to choose a shape for the icon.
|
* Dialog to choose a shape for the icon.
|
||||||
*/
|
*/
|
||||||
class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
constructor(target: MangaInfoController) : this() {
|
|
||||||
targetController = target
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(target: MangaDetailsController) : this() {
|
constructor(target: MangaDetailsController) : this() {
|
||||||
targetController = target
|
targetController = target
|
||||||
}
|
}
|
||||||
@ -35,7 +30,6 @@ class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
|||||||
items = modes.map { activity?.getString(it) as CharSequence },
|
items = modes.map { activity?.getString(it) as CharSequence },
|
||||||
waitForPositiveButton = false)
|
waitForPositiveButton = false)
|
||||||
{ _, i, _ ->
|
{ _, i, _ ->
|
||||||
(targetController as? MangaInfoController)?.createShortcutForShape(i)
|
|
||||||
(targetController as? MangaDetailsController)?.createShortcutForShape(i)
|
(targetController as? MangaDetailsController)?.createShortcutForShape(i)
|
||||||
dismissDialog()
|
dismissDialog()
|
||||||
}
|
}
|
||||||
|
@ -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<SourceManager>().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<SourceManager>().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<SourceManager>().getOrStub(manga.source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(mangaId: Long) : this(
|
|
||||||
Injekt.get<DatabaseHelper>().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<Date> = BehaviorRelay.create()
|
|
||||||
|
|
||||||
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
|
|
||||||
|
|
||||||
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
|
|
||||||
|
|
||||||
private val trackingIconRelay: BehaviorRelay<Boolean> = 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<TrackManager>().services.filter { it.isLogged } }
|
|
||||||
val db = Injekt.get<DatabaseHelper>()
|
|
||||||
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<TrackManager>().hasLoggedServices()) 3 else 2
|
|
||||||
|
|
||||||
private val tabTitles = listOf(
|
|
||||||
R.string.manga_detail_tab,
|
|
||||||
R.string.manga_chapters_tab,
|
|
||||||
R.string.manga_tracking_tab)
|
|
||||||
.map { resources!!.getString(it) }
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -73,7 +73,6 @@ import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
|||||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
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.ChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterMatHolder
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterMatHolder
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter
|
||||||
@ -114,10 +113,10 @@ class MangaDetailsController : BaseController,
|
|||||||
fromCatalogue: Boolean = false,
|
fromCatalogue: Boolean = false,
|
||||||
smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
|
smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
|
||||||
update: Boolean = false) : super(Bundle().apply {
|
update: Boolean = false) : super(Bundle().apply {
|
||||||
putLong(MangaController.MANGA_EXTRA, manga?.id ?: 0)
|
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
||||||
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
||||||
putParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
|
putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
|
||||||
putBoolean(MangaController.UPDATE_EXTRA, update)
|
putBoolean(UPDATE_EXTRA, update)
|
||||||
}) {
|
}) {
|
||||||
this.manga = manga
|
this.manga = manga
|
||||||
if (manga != null) {
|
if (manga != null) {
|
||||||
@ -128,7 +127,7 @@ class MangaDetailsController : BaseController,
|
|||||||
constructor(mangaId: Long) : this(
|
constructor(mangaId: Long) : this(
|
||||||
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
|
Injekt.get<DatabaseHelper>().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 notificationId = bundle.getInt("notificationId", -1)
|
||||||
val context = applicationContext ?: return
|
val context = applicationContext ?: return
|
||||||
if (notificationId > -1) NotificationReceiver.dismissNotification(
|
if (notificationId > -1) NotificationReceiver.dismissNotification(
|
||||||
@ -641,7 +640,7 @@ class MangaDetailsController : BaseController,
|
|||||||
val shortcutIntent = activity.intent
|
val shortcutIntent = activity.intent
|
||||||
.setAction(MainActivity.SHORTCUT_MANGA)
|
.setAction(MainActivity.SHORTCUT_MANGA)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
.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
|
// Check if shortcut placement is supported
|
||||||
if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<ChaptersPresenter>(),
|
|
||||||
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<ChapterItem>()
|
|
||||||
|
|
||||||
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<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
fast_scroller?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
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<Snackbar>() {
|
|
||||||
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<ChapterItem>) {
|
|
||||||
// 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<ChapterItem> {
|
|
||||||
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<ChapterItem>) {
|
|
||||||
presenter.markChaptersRead(chapters, true)
|
|
||||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
|
||||||
deleteChapters(chapters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun markAsUnread(chapters: List<ChapterItem>) {
|
|
||||||
presenter.markChaptersRead(chapters, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadChapters(chapters: List<ChapterItem>) {
|
|
||||||
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<Snackbar>() {
|
|
||||||
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<ChapterItem>, bookmarked: Boolean) {
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
presenter.bookmarkChapters(chapters, bookmarked)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
if (chapters.isEmpty()) return
|
|
||||||
presenter.deleteChapters(chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onChaptersDeleted(chapters: List<ChapterItem>) {
|
|
||||||
//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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Float>,
|
|
||||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
|
||||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
|
||||||
private val downloadManager: DownloadManager = Injekt.get()
|
|
||||||
) : BasePresenter<ChaptersController>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
|
||||||
*/
|
|
||||||
var chapters: List<ChapterItem> = emptyList()
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subject of list of chapters to allow updating the view without going to DB.
|
|
||||||
*/
|
|
||||||
val chaptersRelay: PublishRelay<List<ChapterItem>>
|
|
||||||
by lazy { PublishRelay.create<List<ChapterItem>>() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<ChapterItem>) {
|
|
||||||
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<ChapterItem>): Observable<List<ChapterItem>> {
|
|
||||||
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<ChapterItem>, 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<ChapterItem>) {
|
|
||||||
downloadManager.downloadChapters(manga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bookmarks the given list of chapters.
|
|
||||||
* @param selectedChapters the list of chapters to bookmark.
|
|
||||||
*/
|
|
||||||
fun bookmarkChapters(selectedChapters: List<ChapterItem>, 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<ChapterItem>) {
|
|
||||||
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<ChapterItem>) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<T>(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<MangaInfoPresenter>(),
|
|
||||||
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<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
manga_cover?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = mangaCoverMarginBottom + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
manga_genres_tags?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = bottomM + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manga_cover_full?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
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<Drawable>() {
|
|
||||||
override fun onResourceReady(resource: Drawable,
|
|
||||||
transition: Transition<in Drawable>?
|
|
||||||
) {
|
|
||||||
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<Bitmap>() {
|
|
||||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
|
||||||
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<Snackbar>() {
|
|
||||||
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<Manga>, categories: List<Category>) {
|
|
||||||
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<Bitmap>(96, 96) {
|
|
||||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<Float>,
|
|
||||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
|
||||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
|
||||||
private val coverCache: CoverCache = Injekt.get()
|
|
||||||
) : BasePresenter<MangaInfoController>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to 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<Application>()
|
|
||||||
|
|
||||||
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<Category> {
|
|
||||||
return db.getCategories().executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
|
||||||
*
|
|
||||||
* @param manga the manga to get categories from.
|
|
||||||
* @return Array of category ids the manga is in, if none returns default id
|
|
||||||
*/
|
|
||||||
fun getMangaCategoryIds(manga: Manga): Array<Int> {
|
|
||||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
|
||||||
return categories.mapNotNull { it.id }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the given manga to categories.
|
|
||||||
*
|
|
||||||
* @param manga the manga to move.
|
|
||||||
* @param categories the selected categories.
|
|
||||||
*/
|
|
||||||
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
|
||||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
|
||||||
db.setMangaCategories(mc, listOf(manga))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the given manga to the category.
|
|
||||||
*
|
|
||||||
* @param manga the manga to move.
|
|
||||||
* @param category the selected category, or null for default category.
|
|
||||||
*/
|
|
||||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
|
||||||
moveMangaToCategories(manga, listOfNotNull(category))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateManga(title:String?, author:String?, artist: String?, uri: Uri?,
|
|
||||||
description: String?, tags: Array<String>?) {
|
|
||||||
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<PreferencesHelper>().refreshCoversToo().set(false)
|
|
||||||
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
|
|
||||||
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<TrackPresenter>(),
|
|
||||||
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<TrackItem>) {
|
|
||||||
val atLeastOneLink = trackings.any { it.track != null }
|
|
||||||
adapter?.items = trackings
|
|
||||||
swipe_refresh?.isEnabled = atLeastOneLink
|
|
||||||
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSearchResults(results: List<TrackSearch>) {
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<TrackController>() {
|
|
||||||
|
|
||||||
private val context = preferences.context
|
|
||||||
|
|
||||||
private var trackList: List<TrackItem> = 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -49,15 +49,6 @@ class TrackSearchDialog : DialogController {
|
|||||||
private var wasPreviouslyTracked:Boolean = false
|
private var wasPreviouslyTracked:Boolean = false
|
||||||
private lateinit var presenter:MangaDetailsPresenter
|
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()
|
constructor(target: TrackingBottomSheet, service: TrackService, wasTracked:Boolean) : super(Bundle()
|
||||||
.apply {
|
.apply {
|
||||||
putInt(KEY_SERVICE, service.id)
|
putInt(KEY_SERVICE, service.id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user