mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-09 19:55:11 +01:00
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.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
@ -405,7 +405,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
val newIntent =
|
||||
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(MangaController.MANGA_EXTRA, manga.id)
|
||||
.putExtra(MangaDetailsController.MANGA_EXTRA, manga.id)
|
||||
.putExtra("notificationId", manga.id.hashCode())
|
||||
.putExtra("groupId", groupId)
|
||||
return PendingIntent.getActivity(
|
||||
|
@ -6,17 +6,12 @@ import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
||||
|
||||
/**
|
||||
* Dialog to choose a shape for the icon.
|
||||
*/
|
||||
class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
constructor(target: MangaInfoController) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
constructor(target: MangaDetailsController) : this() {
|
||||
targetController = target
|
||||
}
|
||||
@ -35,7 +30,6 @@ class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
items = modes.map { activity?.getString(it) as CharSequence },
|
||||
waitForPositiveButton = false)
|
||||
{ _, i, _ ->
|
||||
(targetController as? MangaInfoController)?.createShortcutForShape(i)
|
||||
(targetController as? MangaDetailsController)?.createShortcutForShape(i)
|
||||
dismissDialog()
|
||||
}
|
||||
|
@ -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.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController.Companion.FROM_CATALOGUE_EXTRA
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterMatHolder
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter
|
||||
@ -114,10 +113,10 @@ class MangaDetailsController : BaseController,
|
||||
fromCatalogue: Boolean = false,
|
||||
smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
|
||||
update: Boolean = false) : super(Bundle().apply {
|
||||
putLong(MangaController.MANGA_EXTRA, manga?.id ?: 0)
|
||||
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
||||
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
||||
putParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
|
||||
putBoolean(MangaController.UPDATE_EXTRA, update)
|
||||
putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
|
||||
putBoolean(UPDATE_EXTRA, update)
|
||||
}) {
|
||||
this.manga = manga
|
||||
if (manga != null) {
|
||||
@ -128,7 +127,7 @@ class MangaDetailsController : BaseController,
|
||||
constructor(mangaId: Long) : this(
|
||||
Injekt.get<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 context = applicationContext ?: return
|
||||
if (notificationId > -1) NotificationReceiver.dismissNotification(
|
||||
@ -641,7 +640,7 @@ class MangaDetailsController : BaseController,
|
||||
val shortcutIntent = activity.intent
|
||||
.setAction(MainActivity.SHORTCUT_MANGA)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(MangaController.MANGA_EXTRA, presenter.manga.id)
|
||||
.putExtra(MANGA_EXTRA, presenter.manga.id)
|
||||
|
||||
// Check if shortcut placement is supported
|
||||
if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
|
||||
@ -1033,4 +1032,17 @@ class MangaDetailsController : BaseController,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val UPDATE_EXTRA = "update"
|
||||
const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
|
||||
|
||||
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
||||
const val MANGA_EXTRA = "manga"
|
||||
|
||||
const val INFO_CONTROLLER = 0
|
||||
const val CHAPTERS_CONTROLLER = 1
|
||||
const val TRACK_CONTROLLER = 2
|
||||
}
|
||||
}
|
@ -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 lateinit var presenter:MangaDetailsPresenter
|
||||
|
||||
constructor(target: TrackController, service: TrackService, wasTracked:Boolean) : super(Bundle()
|
||||
.apply {
|
||||
putInt(KEY_SERVICE, service.id)
|
||||
}) {
|
||||
wasPreviouslyTracked = wasTracked
|
||||
targetController = target
|
||||
this.service = service
|
||||
}
|
||||
|
||||
constructor(target: TrackingBottomSheet, service: TrackService, wasTracked:Boolean) : super(Bundle()
|
||||
.apply {
|
||||
putInt(KEY_SERVICE, service.id)
|
||||
|
Loading…
Reference in New Issue
Block a user