Replace reader's Presenter with ViewModel (#8698)

includes:
* Use coroutines in more places
* Use domain Manga data class and effectively changing the state system
* Replace deprecated onBackPress method

Co-authored-by: arkon <arkon@users.noreply.github.com>
(cherry picked from commit f7a92cf6ac58cae26b09b02578318e12cd888f4c)

# Conflicts:
#	.github/renovate.json
#	app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
This commit is contained in:
Ivan Iskandar 2022-12-08 11:00:01 +07:00 committed by Jobobby04
parent 3d8f3b34b7
commit de6a5bf67b
11 changed files with 372 additions and 424 deletions

View File

@ -236,9 +236,6 @@ dependencies {
// Preferences // Preferences
implementation(libs.preferencektx) implementation(libs.preferencektx)
// Model View Presenter
implementation(libs.bundles.nucleus)
// Dependency injection // Dependency injection
implementation(libs.injekt.core) implementation(libs.injekt.core)

View File

@ -1,20 +1,18 @@
package eu.kanade.domain.manga.model package eu.kanade.domain.manga.model
import eu.kanade.data.listOfStringsAdapter
import eu.kanade.data.listOfStringsAndAdapter
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.Serializable import java.io.Serializable
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
data class Manga( data class Manga(
val id: Long, val id: Long,
@ -83,6 +81,12 @@ data class Manga(
val bookmarkedFilterRaw: Long val bookmarkedFilterRaw: Long
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
val readingModeType: Long
get() = viewerFlags and ReadingModeType.MASK.toLong()
val orientationType: Long
get() = viewerFlags and OrientationType.MASK.toLong()
val unreadFilter: TriStateFilter val unreadFilter: TriStateFilter
get() = when (unreadFilterRaw) { get() = when (unreadFilterRaw) {
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
@ -240,33 +244,6 @@ fun TriStateFilter.toTriStateGroupState(): ExtendedNavigationView.Item.TriStateG
} }
} }
// TODO: Remove when all deps are migrated
fun Manga.toDbManga(): DbManga = MangaImpl().also {
it.id = id
it.source = source
it.favorite = favorite
it.last_update = lastUpdate
it.date_added = dateAdded
it.viewer_flags = viewerFlags.toInt()
it.chapter_flags = chapterFlags.toInt()
it.cover_last_modified = coverLastModified
it.url = url
// SY -->
it.title = ogTitle
it.artist = ogArtist
it.author = ogAuthor
it.description = ogDescription
it.genre = ogGenre?.let(listOfStringsAdapter::encode)
it.status = ogStatus.toInt()
// SY <--
it.thumbnail_url = thumbnailUrl
it.update_strategy = updateStrategy
it.initialized = initialized
// SY -->
it.filtered_scanlators = filteredScanlators?.let(listOfStringsAndAdapter::encode)
// SY <--
}
fun Manga.toMangaUpdate(): MangaUpdate { fun Manga.toMangaUpdate(): MangaUpdate {
return MangaUpdate( return MangaUpdate(
id = id, id = id,

View File

@ -34,7 +34,9 @@ import android.widget.FrameLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.transition.doOnEnd import androidx.core.transition.doOnEnd
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@ -54,9 +56,9 @@ import com.google.android.material.transition.platform.MaterialContainerTransfor
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
@ -67,9 +69,9 @@ import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegateImpl
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegateImpl import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegateImpl
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Success
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterDialog import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterDialog
import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -89,6 +91,8 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.Constants import eu.kanade.tachiyomi.util.Constants
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.preference.toggle import eu.kanade.tachiyomi.util.preference.toggle
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.createReaderThemeContext import eu.kanade.tachiyomi.util.system.createReaderThemeContext
@ -115,16 +119,18 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import nucleus.factory.RequiresPresenter
import nucleus.view.NucleusAppCompatActivity
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
@ -134,9 +140,8 @@ import kotlin.time.Duration.Companion.seconds
* Activity containing the reader of Tachiyomi. This activity is mostly a container of the * Activity containing the reader of Tachiyomi. This activity is mostly a container of the
* viewers, to which calls from the presenter or UI events are delegated. * viewers, to which calls from the presenter or UI events are delegated.
*/ */
@RequiresPresenter(ReaderPresenter::class)
class ReaderActivity : class ReaderActivity :
NucleusAppCompatActivity<ReaderPresenter>(), AppCompatActivity(),
SecureActivityDelegate by SecureActivityDelegateImpl(), SecureActivityDelegate by SecureActivityDelegateImpl(),
ThemingDelegate by ThemingDelegateImpl() { ThemingDelegate by ThemingDelegateImpl() {
@ -169,6 +174,8 @@ class ReaderActivity :
lateinit var binding: ReaderActivityBinding lateinit var binding: ReaderActivityBinding
val viewModel by viewModels<ReaderViewModel>()
val hasCutout by lazy { hasDisplayCutout() } val hasCutout by lazy { hasDisplayCutout() }
/** /**
@ -245,7 +252,7 @@ class ReaderActivity :
binding = ReaderActivityBinding.inflate(layoutInflater) binding = ReaderActivityBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
if (presenter.needsInit()) { if (viewModel.needsInit()) {
val manga = intent.extras!!.getLong("manga", -1) val manga = intent.extras!!.getLong("manga", -1)
val chapter = intent.extras!!.getLong("chapter", -1) val chapter = intent.extras!!.getLong("chapter", -1)
// SY --> // SY -->
@ -256,7 +263,16 @@ class ReaderActivity :
return return
} }
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS) NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
presenter.init(manga, chapter /* SY --> */, page/* SY <-- */)
lifecycleScope.launchNonCancellable {
val initResult = viewModel.init(manga, chapter/* SY --> */, page/* SY <-- */)
if (!initResult.getOrDefault(false)) {
val exception = initResult.exceptionOrNull() ?: IllegalStateException("Unknown err")
withUIContext {
setInitialChapterError(exception)
}
}
}
} }
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -279,6 +295,48 @@ class ReaderActivity :
.drop(1) .drop(1)
.onEach { if (!it) finish() } .onEach { if (!it) finish() }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
viewModel.state
.map { it.isLoadingAdjacentChapter }
.distinctUntilChanged()
.onEach(::setProgressDialog)
.launchIn(lifecycleScope)
viewModel.state
.map { it.manga }
.distinctUntilChanged()
.filterNotNull()
.onEach(::setManga)
.launchIn(lifecycleScope)
viewModel.state
.map { it.viewerChapters }
.distinctUntilChanged()
.filterNotNull()
.onEach(::setChapters)
.launchIn(lifecycleScope)
viewModel.eventFlow
.onEach { event ->
when (event) {
ReaderViewModel.Event.ReloadViewerChapters -> {
viewModel.state.value.viewerChapters?.let(::setChapters)
}
is ReaderViewModel.Event.SetOrientation -> {
setOrientation(event.orientation)
}
is ReaderViewModel.Event.SavedImage -> {
onSaveImageResult(event.result)
}
is ReaderViewModel.Event.ShareImage -> {
onShareImageResult(event.uri, event.page /* SY --> */, event.secondPage /* SY <-- */)
}
is ReaderViewModel.Event.SetCoverResult -> {
onSetAsCoverResult(event.result)
}
}
}
.launchIn(lifecycleScope)
} }
// SY --> // SY -->
@ -329,13 +387,13 @@ class ReaderActivity :
} }
// SY <-- // SY <--
if (!isChangingConfigurations) { if (!isChangingConfigurations) {
presenter.onSaveInstanceStateNonConfigurationChange() viewModel.onSaveInstanceStateNonConfigurationChange()
} }
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
override fun onPause() { override fun onPause() {
presenter.saveCurrentChapterReadingProgress() viewModel.saveCurrentChapterReadingProgress()
super.onPause() super.onPause()
} }
@ -345,7 +403,7 @@ class ReaderActivity :
*/ */
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
presenter.setReadStartTime() viewModel.setReadStartTime()
setMenuVisibility(menuVisible, animate = false) setMenuVisibility(menuVisible, animate = false)
} }
@ -366,7 +424,7 @@ class ReaderActivity :
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.reader, menu) menuInflater.inflate(R.menu.reader, menu)
/*val isChapterBookmarked = presenter?.getCurrentChapter()?.chapter?.bookmark ?: false /*val isChapterBookmarked = viewModel.getCurrentChapter()?.chapter?.bookmark ?: false
menu.findItem(R.id.action_bookmark).isVisible = !isChapterBookmarked menu.findItem(R.id.action_bookmark).isVisible = !isChapterBookmarked
menu.findItem(R.id.action_remove_bookmark).isVisible = isChapterBookmarked*/ menu.findItem(R.id.action_remove_bookmark).isVisible = isChapterBookmarked*/
@ -383,11 +441,11 @@ class ReaderActivity :
openChapterInWebview() openChapterInWebview()
} }
R.id.action_bookmark -> { R.id.action_bookmark -> {
presenter.bookmarkCurrentChapter(true) viewModel.bookmarkCurrentChapter(true)
invalidateOptionsMenu() invalidateOptionsMenu()
} }
R.id.action_remove_bookmark -> { R.id.action_remove_bookmark -> {
presenter.bookmarkCurrentChapter(false) viewModel.bookmarkCurrentChapter(false)
invalidateOptionsMenu() invalidateOptionsMenu()
} }
} }
@ -398,17 +456,17 @@ class ReaderActivity :
* Called when the user clicks the back key or the button on the toolbar. The call is * Called when the user clicks the back key or the button on the toolbar. The call is
* delegated to the presenter. * delegated to the presenter.
*/ */
override fun onBackPressed() { override fun finish() {
presenter.onBackPressed() viewModel.onActivityFinish()
super.onBackPressed() super.finish()
} }
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_N) { if (keyCode == KeyEvent.KEYCODE_N) {
presenter.loadNextChapter() loadNextChapter()
return true return true
} else if (keyCode == KeyEvent.KEYCODE_P) { } else if (keyCode == KeyEvent.KEYCODE_P) {
presenter.loadPreviousChapter() loadPreviousChapter()
return true return true
} }
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
@ -475,7 +533,7 @@ class ReaderActivity :
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener { binding.toolbar.setNavigationOnClickListener {
onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
binding.header.applyInsetter { binding.header.applyInsetter {
@ -490,7 +548,7 @@ class ReaderActivity :
} }
binding.toolbar.setOnClickListener { binding.toolbar.setOnClickListener {
presenter.manga?.id?.let { id -> viewModel.manga?.id?.let { id ->
startActivity( startActivity(
Intent(this, MainActivity::class.java).apply { Intent(this, MainActivity::class.java).apply {
action = MainActivity.SHORTCUT_MANGA action = MainActivity.SHORTCUT_MANGA
@ -612,11 +670,11 @@ class ReaderActivity :
setOnClickListener { setOnClickListener {
popupMenu( popupMenu(
items = ReadingModeType.values().map { it.flagValue to it.stringRes }, items = ReadingModeType.values().map { it.flagValue to it.stringRes },
selectedItemId = presenter.getMangaReadingMode(resolveDefault = false), selectedItemId = viewModel.getMangaReadingMode(resolveDefault = false),
) { ) {
val newReadingMode = ReadingModeType.fromPreference(itemId) val newReadingMode = ReadingModeType.fromPreference(itemId)
presenter.setMangaReadingMode(newReadingMode.flagValue) viewModel.setMangaReadingMode(newReadingMode.flagValue)
menuToggleToast?.cancel() menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) { if (!readerPreferences.showReadingMode().get()) {
@ -634,7 +692,7 @@ class ReaderActivity :
setOnClickListener { setOnClickListener {
// SY --> // SY -->
val mangaViewer = presenter.getMangaReadingMode() val mangaViewer = viewModel.getMangaReadingMode()
// SY <-- // SY <--
val isPagerType = ReadingModeType.isPagerType(mangaViewer) val isPagerType = ReadingModeType.isPagerType(mangaViewer)
val enabled = if (isPagerType) { val enabled = if (isPagerType) {
@ -674,12 +732,12 @@ class ReaderActivity :
setOnClickListener { setOnClickListener {
popupMenu( popupMenu(
items = OrientationType.values().map { it.flagValue to it.stringRes }, items = OrientationType.values().map { it.flagValue to it.stringRes },
selectedItemId = presenter.manga?.orientationType selectedItemId = viewModel.manga?.orientationType?.toInt()
?: readerPreferences.defaultOrientationType().get(), ?: readerPreferences.defaultOrientationType().get(),
) { ) {
val newOrientation = OrientationType.fromPreference(itemId) val newOrientation = OrientationType.fromPreference(itemId)
presenter.setMangaOrientationType(newOrientation.flagValue) viewModel.setMangaOrientationType(newOrientation.flagValue)
menuToggleToast?.cancel() menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes) menuToggleToast = toast(newOrientation.stringRes)
@ -804,9 +862,9 @@ class ReaderActivity :
binding.ehRetryAll.setOnClickListener { binding.ehRetryAll.setOnClickListener {
var retried = 0 var retried = 0
presenter.viewerChaptersRelay.value viewModel.state.value.viewerChapters
.currChapter ?.currChapter
.pages ?.pages
?.forEachIndexed { _, page -> ?.forEachIndexed { _, page ->
var shouldQueuePage = false var shouldQueuePage = false
if (page.status == Page.State.ERROR) { if (page.status == Page.State.ERROR) {
@ -823,7 +881,7 @@ class ReaderActivity :
} }
// If we are using EHentai/ExHentai, get a new image URL // If we are using EHentai/ExHentai, get a new image URL
presenter.manga?.let { m -> viewModel.manga?.let { m ->
val src = sourceManager.get(m.source) val src = sourceManager.get(m.source)
if (src?.isEhBasedSource() == true) { if (src?.isEhBasedSource() == true) {
page.imageUrl = null page.imageUrl = null
@ -865,7 +923,7 @@ class ReaderActivity :
} else if (curPage.status == Page.State.READY) { } else if (curPage.status == Page.State.READY) {
toast(R.string.eh_boost_page_downloaded) toast(R.string.eh_boost_page_downloaded)
} else { } else {
val loader = (presenter.viewerChaptersRelay.value.currChapter.pageLoader as? HttpPageLoader) val loader = (viewModel.state.value.viewerChapters?.currChapter?.pageLoader as? HttpPageLoader)
if (loader != null) { if (loader != null) {
loader.boostPage(curPage) loader.boostPage(curPage)
toast(R.string.eh_boost_boosted) toast(R.string.eh_boost_boosted)
@ -886,7 +944,7 @@ class ReaderActivity :
private fun exhCurrentpage(): ReaderPage? { private fun exhCurrentpage(): ReaderPage? {
val currentPage = (((viewer as? PagerViewer)?.currentPage ?: (viewer as? WebtoonViewer)?.currentPage) as? ReaderPage)?.index val currentPage = (((viewer as? PagerViewer)?.currentPage ?: (viewer as? WebtoonViewer)?.currentPage) as? ReaderPage)?.index
return currentPage?.let { presenter.viewerChaptersRelay.value.currChapter.pages?.getOrNull(it) } return currentPage?.let { viewModel.state.value.viewerChapters?.currChapter?.pages?.getOrNull(it) }
} }
fun updateBottomButtons() { fun updateBottomButtons() {
@ -924,7 +982,7 @@ class ReaderActivity :
} else { } else {
pViewer.config.doublePages = doublePages pViewer.config.doublePages = doublePages
} }
val currentChapter = presenter.getCurrentChapter() val currentChapter = viewModel.getCurrentChapter()
if (doublePages) { if (doublePages) {
// If we're moving from singe to double, we want the current page to be the first page // If we're moving from singe to double, we want the current page to be the first page
pViewer.config.shiftDoublePage = ( pViewer.config.shiftDoublePage = (
@ -932,7 +990,7 @@ class ReaderActivity :
(currentChapter?.pages?.take(binding.pageSlider.value.floor())?.count { it.fullPage || it.isolatedPage } ?: 0) (currentChapter?.pages?.take(binding.pageSlider.value.floor())?.count { it.fullPage || it.isolatedPage } ?: 0)
) % 2 != 0 ) % 2 != 0
} }
presenter.viewerChaptersRelay.value?.let { viewModel.state.value.viewerChapters?.let {
pViewer.setChaptersDoubleShift(it) pViewer.setChaptersDoubleShift(it)
} }
} }
@ -945,7 +1003,7 @@ class ReaderActivity :
private fun shiftDoublePages() { private fun shiftDoublePages() {
(viewer as? PagerViewer)?.config?.let { config -> (viewer as? PagerViewer)?.config?.let { config ->
config.shiftDoublePage = !config.shiftDoublePage config.shiftDoublePage = !config.shiftDoublePage
presenter.viewerChaptersRelay.value?.let { viewModel.state.value.viewerChapters?.let {
(viewer as? PagerViewer)?.updateShifting() (viewer as? PagerViewer)?.updateShifting()
(viewer as? PagerViewer)?.setChaptersDoubleShift(it) (viewer as? PagerViewer)?.setChaptersDoubleShift(it)
invalidateOptionsMenu() invalidateOptionsMenu()
@ -960,7 +1018,7 @@ class ReaderActivity :
} }
private fun updateCropBordersShortcut() { private fun updateCropBordersShortcut() {
val mangaViewer = presenter.getMangaReadingMode() val mangaViewer = viewModel.getMangaReadingMode()
val isPagerType = ReadingModeType.isPagerType(mangaViewer) val isPagerType = ReadingModeType.isPagerType(mangaViewer)
val enabled = if (isPagerType) { val enabled = if (isPagerType) {
readerPreferences.cropBorders().get() readerPreferences.cropBorders().get()
@ -1070,19 +1128,19 @@ class ReaderActivity :
fun setManga(manga: Manga) { fun setManga(manga: Manga) {
val prevViewer = viewer val prevViewer = viewer
val viewerMode = ReadingModeType.fromPreference(presenter.getMangaReadingMode(resolveDefault = false)) val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
binding.actionReadingMode.setImageResource(viewerMode.iconRes) binding.actionReadingMode.setImageResource(viewerMode.iconRes)
val newViewer = ReadingModeType.toViewer(presenter.getMangaReadingMode(), this) val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this)
updateCropBordersShortcut() updateCropBordersShortcut()
if (window.sharedElementEnterTransition is MaterialContainerTransform) { if (window.sharedElementEnterTransition is MaterialContainerTransform) {
// Wait until transition is complete to avoid crash on API 26 // Wait until transition is complete to avoid crash on API 26
window.sharedElementEnterTransition.doOnEnd { window.sharedElementEnterTransition.doOnEnd {
setOrientation(presenter.getMangaOrientationType()) setOrientation(viewModel.getMangaOrientationType())
} }
} else { } else {
setOrientation(presenter.getMangaOrientationType()) setOrientation(viewModel.getMangaOrientationType())
} }
// Destroy previous viewer if there was one // Destroy previous viewer if there was one
@ -1103,12 +1161,12 @@ class ReaderActivity :
} }
val defaultReaderType = manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.get(manga.source)?.name)) val defaultReaderType = manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.get(manga.source)?.name))
if (readerPreferences.useAutoWebtoon().get() && manga.readingModeType == ReadingModeType.DEFAULT.flagValue && defaultReaderType != null && defaultReaderType == ReadingModeType.WEBTOON.prefValue) { if (readerPreferences.useAutoWebtoon().get() && manga.readingModeType.toInt() == ReadingModeType.DEFAULT.flagValue && defaultReaderType != null && defaultReaderType == ReadingModeType.WEBTOON.prefValue) {
readingModeToast?.cancel() readingModeToast?.cancel()
readingModeToast = toast(resources.getString(R.string.eh_auto_webtoon_snack)) readingModeToast = toast(resources.getString(R.string.eh_auto_webtoon_snack))
} else if (readerPreferences.showReadingMode().get()) { } else if (readerPreferences.showReadingMode().get()) {
// SY <-- // SY <--
showReadingModeToast(presenter.getMangaReadingMode()) showReadingModeToast(viewModel.getMangaReadingMode())
} }
// SY --> // SY -->
@ -1171,9 +1229,9 @@ class ReaderActivity :
} }
private fun openChapterInWebview() { private fun openChapterInWebview() {
val manga = presenter.manga ?: return val manga = viewModel.manga ?: return
val source = presenter.getSource() ?: return val source = viewModel.getSource() ?: return
val url = presenter.getChapterUrl() ?: return val url = viewModel.getChapterUrl() ?: return
val intent = WebViewActivity.newIntent(this, url, source.id, manga.title) val intent = WebViewActivity.newIntent(this, url, source.id, manga.title)
startActivity(intent) startActivity(intent)
@ -1194,7 +1252,7 @@ class ReaderActivity :
* method to the current viewer, but also set the subtitle on the toolbar, and * method to the current viewer, but also set the subtitle on the toolbar, and
* hides or disables the reader prev/next buttons if there's a prev or next chapter * hides or disables the reader prev/next buttons if there's a prev or next chapter
*/ */
fun setChapters(viewerChapters: ViewerChapters) { private fun setChapters(viewerChapters: ViewerChapters) {
binding.readerContainer.removeView(loadingIndicator) binding.readerContainer.removeView(loadingIndicator)
// SY --> // SY -->
if (indexChapterToShift != null && indexPageToShift != null) { if (indexChapterToShift != null && indexPageToShift != null) {
@ -1280,7 +1338,7 @@ class ReaderActivity :
*/ */
fun moveToPageIndex(index: Int) { fun moveToPageIndex(index: Int) {
val viewer = viewer ?: return val viewer = viewer ?: return
val currentChapter = presenter.getCurrentChapter() ?: return val currentChapter = viewModel.getCurrentChapter() ?: return
val page = currentChapter.pages?.getOrNull(index) ?: return val page = currentChapter.pages?.getOrNull(index) ?: return
viewer.moveToPage(page) viewer.moveToPage(page)
} }
@ -1290,7 +1348,10 @@ class ReaderActivity :
* should be automatically shown. * should be automatically shown.
*/ */
private fun loadNextChapter() { private fun loadNextChapter() {
presenter.loadNextChapter() lifecycleScope.launch {
viewModel.loadNextChapter()
moveToPageIndex(0)
}
} }
/** /**
@ -1298,7 +1359,10 @@ class ReaderActivity :
* should be automatically shown. * should be automatically shown.
*/ */
private fun loadPreviousChapter() { private fun loadPreviousChapter() {
presenter.loadPreviousChapter() lifecycleScope.launch {
viewModel.loadPreviousChapter()
moveToPageIndex(0)
}
} }
/** /**
@ -1307,7 +1371,7 @@ class ReaderActivity :
*/ */
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean = false) { fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean = false) {
val newChapter = presenter.onPageSelected(page, hasExtraPage) val newChapter = viewModel.onPageSelected(page, hasExtraPage)
val pages = page.chapter.pages ?: return val pages = page.chapter.pages ?: return
val currentPage = if (hasExtraPage) { val currentPage = if (hasExtraPage) {
@ -1372,7 +1436,7 @@ class ReaderActivity :
* the viewer is reaching the beginning or end of a chapter or the transition page is active. * the viewer is reaching the beginning or end of a chapter or the transition page is active.
*/ */
fun requestPreloadChapter(chapter: ReaderChapter) { fun requestPreloadChapter(chapter: ReaderChapter) {
presenter.preloadChapter(chapter) lifecycleScope.launch { viewModel.preloadChapter(chapter) }
} }
/** /**
@ -1406,12 +1470,12 @@ class ReaderActivity :
* will call [onShareImageResult] with the path the image was saved on when it's ready. * will call [onShareImageResult] with the path the image was saved on when it's ready.
*/ */
fun shareImage(page: ReaderPage) { fun shareImage(page: ReaderPage) {
presenter.shareImage(page) viewModel.shareImage(page)
} }
// SY --> // SY -->
fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
presenter.shareImages(firstPage, secondPage, isLTR, bg) viewModel.shareImages(firstPage, secondPage, isLTR, bg)
} }
// SY <-- // SY <--
@ -1420,7 +1484,7 @@ class ReaderActivity :
* sharing tool. * sharing tool.
*/ */
fun onShareImageResult(uri: Uri, page: ReaderPage /* SY --> */, secondPage: ReaderPage? = null /* SY <-- */) { fun onShareImageResult(uri: Uri, page: ReaderPage /* SY --> */, secondPage: ReaderPage? = null /* SY <-- */) {
val manga = presenter.manga ?: return val manga = viewModel.manga ?: return
val chapter = page.chapter.chapter val chapter = page.chapter.chapter
// SY --> // SY -->
@ -1443,12 +1507,12 @@ class ReaderActivity :
* storage to the presenter. * storage to the presenter.
*/ */
fun saveImage(page: ReaderPage) { fun saveImage(page: ReaderPage) {
presenter.saveImage(page) viewModel.saveImage(page)
} }
// SY --> // SY -->
fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
presenter.saveImages(firstPage, secondPage, isLTR, bg) viewModel.saveImages(firstPage, secondPage, isLTR, bg)
} }
// SY <-- // SY <--
@ -1456,12 +1520,12 @@ class ReaderActivity :
* Called from the presenter when a page is saved or fails. It shows a message or logs the * Called from the presenter when a page is saved or fails. It shows a message or logs the
* event depending on the [result]. * event depending on the [result].
*/ */
fun onSaveImageResult(result: ReaderPresenter.SaveImageResult) { private fun onSaveImageResult(result: ReaderViewModel.SaveImageResult) {
when (result) { when (result) {
is ReaderPresenter.SaveImageResult.Success -> { is ReaderViewModel.SaveImageResult.Success -> {
toast(R.string.picture_saved) toast(R.string.picture_saved)
} }
is ReaderPresenter.SaveImageResult.Error -> { is ReaderViewModel.SaveImageResult.Error -> {
logcat(LogPriority.ERROR, result.error) logcat(LogPriority.ERROR, result.error)
} }
} }
@ -1472,14 +1536,14 @@ class ReaderActivity :
* cover to the presenter. * cover to the presenter.
*/ */
fun setAsCover(page: ReaderPage) { fun setAsCover(page: ReaderPage) {
presenter.setAsCover(this, page) viewModel.setAsCover(this, page)
} }
/** /**
* Called from the presenter when a page is set as cover or fails. It shows a different message * Called from the presenter when a page is set as cover or fails. It shows a different message
* depending on the [result]. * depending on the [result].
*/ */
fun onSetAsCoverResult(result: ReaderPresenter.SetAsCoverResult) { private fun onSetAsCoverResult(result: ReaderViewModel.SetAsCoverResult) {
toast( toast(
when (result) { when (result) {
Success -> R.string.cover_updated Success -> R.string.cover_updated
@ -1492,12 +1556,12 @@ class ReaderActivity :
/** /**
* Forces the user preferred [orientation] on the activity. * Forces the user preferred [orientation] on the activity.
*/ */
fun setOrientation(orientation: Int) { private fun setOrientation(orientation: Int) {
val newOrientation = OrientationType.fromPreference(orientation) val newOrientation = OrientationType.fromPreference(orientation)
if (newOrientation.flag != requestedOrientation) { if (newOrientation.flag != requestedOrientation) {
requestedOrientation = newOrientation.flag requestedOrientation = newOrientation.flag
} }
updateOrientationShortcut(presenter.getMangaOrientationType(resolveDefault = false)) updateOrientationShortcut(viewModel.getMangaOrientationType(resolveDefault = false))
} }
/** /**

View File

@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.ui.reader
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import com.jakewharton.rxrelay.BehaviorRelay import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import eu.kanade.core.util.asFlow
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId
import eu.kanade.domain.chapter.interactor.UpdateChapter import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.model.toDbChapter import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.download.service.DownloadPreferences import eu.kanade.domain.download.service.DownloadPreferences
@ -21,8 +24,8 @@ import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.GetMergedManga import eu.kanade.domain.manga.interactor.GetMergedManga
import eu.kanade.domain.manga.interactor.GetMergedReferencesById import eu.kanade.domain.manga.interactor.GetMergedReferencesById
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.manga.model.isLocal
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.track.interactor.GetTracks import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.InsertTrack import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
@ -30,9 +33,7 @@ import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.track.store.DelayedTrackingStore import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toDomainChapter import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@ -63,6 +64,7 @@ import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.lang.takeBytes import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir import eu.kanade.tachiyomi.util.storage.cacheImageDir
@ -77,36 +79,38 @@ import exh.source.getMainSource
import exh.source.isEhBasedManga import exh.source.isEhBasedManga
import exh.util.defaultReaderType import exh.util.defaultReaderType
import exh.util.mangaType import exh.util.mangaType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import logcat.LogPriority import logcat.LogPriority
import nucleus.presenter.RxPresenter
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
import java.util.Date import java.util.Date
import eu.kanade.domain.chapter.model.Chapter as DomainChapter
import eu.kanade.domain.manga.model.Manga as DomainManga
/** /**
* Presenter used by the activity to perform background operations. * Presenter used by the activity to perform background operations.
*/ */
class ReaderPresenter( class ReaderViewModel(
private val savedState: SavedStateHandle = SavedStateHandle(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val downloadProvider: DownloadProvider = Injekt.get(), private val downloadProvider: DownloadProvider = Injekt.get(),
@ -131,27 +135,28 @@ class ReaderPresenter(
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(), private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
private val getMergedChapterByMangaId: GetMergedChapterByMangaId = Injekt.get(), private val getMergedChapterByMangaId: GetMergedChapterByMangaId = Injekt.get(),
// SY <-- // SY <--
) : RxPresenter<ReaderActivity>() { ) : ViewModel() {
private val coroutineScope: CoroutineScope = MainScope() private val mutableState = MutableStateFlow(State())
val state = mutableState.asStateFlow()
private val eventChannel = Channel<Event>()
val eventFlow = eventChannel.receiveAsFlow()
/** /**
* The manga loaded in the reader. It can be null when instantiated for a short time. * The manga loaded in the reader. It can be null when instantiated for a short time.
*/ */
var manga: Manga? = null val manga: Manga?
private set get() = state.value.manga
// SY -->
var meta: RaisedSearchMetadata? = null
private set
var mergedManga: Map<Long, DomainManga>? = null
private set
// SY <--
/** /**
* The chapter id of the currently loaded chapter. Used to restore from process kill. * The chapter id of the currently loaded chapter. Used to restore from process kill.
*/ */
private var chapterId = -1L private var chapterId = savedState.get<Long>("chapter_id") ?: -1L
set(value) {
savedState["chapter_id"] = value
field = value
}
/** /**
* The chapter loader for the loaded manga. It'll be null until [manga] is set. * The chapter loader for the loaded manga. It'll be null until [manga] is set.
@ -168,17 +173,6 @@ class ReaderPresenter(
*/ */
private var activeChapterSubscription: Subscription? = null private var activeChapterSubscription: Subscription? = null
/**
* Relay for currently active viewer chapters.
*/
/* [EXH] private */
val viewerChaptersRelay = BehaviorRelay.create<ViewerChapters>()
/**
* Used when loading prev/next chapter needed to lock the UI (with a dialog).
*/
private val isLoadingAdjacentChapterEvent = Channel<Boolean>()
private var chapterToDownload: Download? = null private var chapterToDownload: Download? = null
/** /**
@ -186,7 +180,7 @@ class ReaderPresenter(
* time in a background thread to avoid blocking the UI. * time in a background thread to avoid blocking the UI.
*/ */
private val chapterList by lazy { private val chapterList by lazy {
val manga = manga!!.toDomainManga()!! val manga = manga!!
val chapters = runBlocking { val chapters = runBlocking {
/* SY --> */ if (manga.source == MERGED_SOURCE_ID) { /* SY --> */ if (manga.source == MERGED_SOURCE_ID) {
getMergedChapterByMangaId.await(manga.id) getMergedChapterByMangaId.await(manga.id)
@ -204,12 +198,12 @@ class ReaderPresenter(
when { when {
readerPreferences.skipRead().get() && it.read -> true readerPreferences.skipRead().get() && it.read -> true
readerPreferences.skipFiltered().get() -> { readerPreferences.skipFiltered().get() -> {
(manga.unreadFilterRaw == DomainManga.CHAPTER_SHOW_READ && !it.read) || (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_READ && !it.read) ||
(manga.unreadFilterRaw == DomainManga.CHAPTER_SHOW_UNREAD && it.read) || (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_UNREAD && it.read) ||
(manga.downloadedFilterRaw == DomainManga.CHAPTER_SHOW_DOWNLOADED && !downloadManager.isChapterDownloaded(it.name, it.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, manga.source)) || (manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_DOWNLOADED && !downloadManager.isChapterDownloaded(it.name, it.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, manga.source)) ||
(manga.downloadedFilterRaw == DomainManga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded(it.name, it.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, manga.source)) || (manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded(it.name, it.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, manga.source)) ||
(manga.bookmarkedFilterRaw == DomainManga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) || (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) ||
(manga.bookmarkedFilterRaw == DomainManga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark) || (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark) ||
// SY --> // SY -->
(manga.filteredScanlators != null && MdUtil.getScanlators(it.scanlator).none { group -> manga.filteredScanlators.contains(group) }) (manga.filteredScanlators != null && MdUtil.getScanlators(it.scanlator).none { group -> manga.filteredScanlators.contains(group) })
// SY <-- // SY <--
@ -234,32 +228,15 @@ class ReaderPresenter(
} }
private var hasTrackers: Boolean = false private var hasTrackers: Boolean = false
private val checkTrackers: (DomainManga) -> Unit = { manga -> private val checkTrackers: (Manga) -> Unit = { manga ->
val tracks = runBlocking { getTracks.await(manga.id) } val tracks = runBlocking { getTracks.await(manga.id) }
hasTrackers = tracks.isNotEmpty() hasTrackers = tracks.isNotEmpty()
} }
private val incognitoMode = preferences.incognitoMode().get() private val incognitoMode = preferences.incognitoMode().get()
/** override fun onCleared() {
* Called when the presenter is created. It retrieves the saved active chapter if the process val currentChapters = state.value.viewerChapters
* was restored.
*/
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
if (savedState != null) {
chapterId = savedState.getLong(::chapterId.name, -1)
}
}
/**
* Called when the presenter is destroyed. It saves the current progress and cleans up
* references on the currently active chapters.
*/
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
val currentChapters = viewerChaptersRelay.value
if (currentChapters != null) { if (currentChapters != null) {
currentChapters.unref() currentChapters.unref()
saveReadingProgress(currentChapters.currChapter) saveReadingProgress(currentChapters.currChapter)
@ -269,24 +246,24 @@ class ReaderPresenter(
} }
} }
/** init {
* Called when the presenter instance is being saved. It saves the currently active chapter // To save state
* id and the last page read. state.map { it.viewerChapters?.currChapter }
*/ .distinctUntilChanged()
override fun onSave(state: Bundle) { .onEach { currentChapter ->
super.onSave(state)
val currentChapter = getCurrentChapter()
if (currentChapter != null) { if (currentChapter != null) {
currentChapter.requestedPage = currentChapter.chapter.last_page_read currentChapter.requestedPage = currentChapter.chapter.last_page_read
state.putLong(::chapterId.name, currentChapter.chapter.id!!) chapterId = currentChapter.chapter.id!!
} }
} }
.launchIn(viewModelScope)
}
/** /**
* Called when the user pressed the back button and is going to leave the reader. Used to * Called when the user pressed the back button and is going to leave the reader. Used to
* trigger deletion of the downloaded chapters. * trigger deletion of the downloaded chapters.
*/ */
fun onBackPressed() { fun onActivityFinish() {
deletePendingChapters() deletePendingChapters()
} }
@ -296,7 +273,7 @@ class ReaderPresenter(
*/ */
fun onSaveInstanceStateNonConfigurationChange() { fun onSaveInstanceStateNonConfigurationChange() {
val currentChapter = getCurrentChapter() ?: return val currentChapter = getCurrentChapter() ?: return
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
saveChapterProgress(currentChapter) saveChapterProgress(currentChapter)
} }
} }
@ -312,71 +289,44 @@ class ReaderPresenter(
* Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will * Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will
* fetch the manga from the database and initialize the initial chapter. * fetch the manga from the database and initialize the initial chapter.
*/ */
fun init(mangaId: Long, initialChapterId: Long /* SY --> */, page: Int?/* SY <-- */) { suspend fun init(mangaId: Long, initialChapterId: Long /* SY --> */, page: Int?/* SY <-- */): Result<Boolean> {
if (!needsInit()) return if (!needsInit()) return Result.success(true)
return withIOContext {
coroutineScope.launchIO {
try { try {
val manga = getManga.await(mangaId)
if (manga != null) {
// SY --> // SY -->
val manga = getManga.await(mangaId) ?: return@launchIO val source = sourceManager.getOrStub(manga.source)
val source = sourceManager.get(manga.source)?.getMainSource<MetadataSource<*, *>>() val metadataSource = source.getMainSource<MetadataSource<*, *>>()
val metadata = if (source != null) { val metadata = if (metadataSource != null) {
getFlatMetadataById.await(mangaId)?.raise(source.metaClass) getFlatMetadataById.await(mangaId)?.raise(metadataSource.metaClass)
} else { } else {
null null
} }
withUIContext { val mergedReferences = if (source is MergedSource) runBlocking { getMergedReferencesById.await(manga.id) } else emptyList()
init(manga.toDbManga(), initialChapterId, metadata, page) val mergedManga = if (source is MergedSource) runBlocking { getMergedManga.await() /* <-- TODO */ }.associateBy { it.id } else emptyMap()
}
// SY <--
} catch (e: Throwable) {
view?.setInitialChapterError(e)
}
}
}
/**
* Initializes this presenter with the given [manga] and [initialChapterId]. This method will
* set the chapter loader, view subscriptions and trigger an initial load.
*/
private fun init(manga: Manga, initialChapterId: Long /* SY --> */, metadata: RaisedSearchMetadata?, page: Int?/* SY <-- */) {
if (!needsInit()) return
this.manga = manga
// SY -->
this.meta = metadata
// SY <-- // SY <--
mutableState.update { it.copy(manga = manga /* SY --> */, meta = metadata, mergedManga = mergedManga/* SY <-- */) }
if (chapterId == -1L) chapterId = initialChapterId if (chapterId == -1L) chapterId = initialChapterId
checkTrackers(manga.toDomainManga()!!) checkTrackers(manga)
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source) // val source = sourceManager.getOrStub(manga.source)
val mergedReferences = if (source is MergedSource) runBlocking { getMergedReferencesById.await(manga.id!!) } else emptyList() loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source, /* SY --> */sourceManager, mergedReferences, mergedManga/* SY <-- */)
mergedManga = if (source is MergedSource) runBlocking { getMergedManga.await() }.associateBy { it.id } else emptyMap()
loader = ChapterLoader(context, downloadManager, downloadProvider, manga.toDomainManga()!!, source, sourceManager, mergedReferences, mergedManga ?: emptyMap())
Observable.just(manga).subscribeLatestCache(ReaderActivity::setManga) getLoadObservable(loader!!, chapterList.first { chapterId == it.chapter.id } /* SY --> */, page/* SY <-- */)
viewerChaptersRelay.subscribeLatestCache(ReaderActivity::setChapters) .asFlow()
coroutineScope.launch { .first()
isLoadingAdjacentChapterEvent.receiveAsFlow().collectLatest { Result.success(true)
view?.setProgressDialog(it) } else {
// Unlikely but okay
Result.success(false)
}
} catch (e: Throwable) {
Result.failure(e)
} }
} }
// Read chapterList from an io thread because it's retrieved lazily and would block main.
activeChapterSubscription?.unsubscribe()
activeChapterSubscription = Observable
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
.flatMap { getLoadObservable(loader!!, it /* SY --> */, page/* SY <-- */) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ _, _ ->
// Ignore onNext event
},
ReaderActivity::setInitialChapterError,
)
} }
// SY --> // SY -->
@ -391,7 +341,7 @@ class ReaderPresenter(
return chapterList.map { return chapterList.map {
ReaderChapterItem( ReaderChapterItem(
it.chapter.toDomainChapter()!!, it.chapter.toDomainChapter()!!,
manga!!.toDomainManga()!!, manga!!,
it.chapter.id == currentChapter?.chapter?.id, it.chapter.id == currentChapter?.chapter?.id,
context, context,
UiPreferences.dateFormat(uiPreferences.dateFormat().get()), UiPreferences.dateFormat(uiPreferences.dateFormat().get()),
@ -429,14 +379,14 @@ class ReaderPresenter(
) )
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { newChapters -> .doOnNext { newChapters ->
val oldChapters = viewerChaptersRelay.value mutableState.update {
// Add new references first to avoid unnecessary recycling // Add new references first to avoid unnecessary recycling
newChapters.ref() newChapters.ref()
oldChapters?.unref() it.viewerChapters?.unref()
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter) chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
viewerChaptersRelay.call(newChapters) it.copy(viewerChapters = newChapters)
}
} }
} }
@ -444,20 +394,20 @@ class ReaderPresenter(
* Called when the user changed to the given [chapter] when changing pages from the viewer. * Called when the user changed to the given [chapter] when changing pages from the viewer.
* It's used only to set this chapter as active. * It's used only to set this chapter as active.
*/ */
private fun loadNewChapter(chapter: ReaderChapter) { private suspend fun loadNewChapter(chapter: ReaderChapter) {
val loader = loader ?: return val loader = loader ?: return
logcat { "Loading ${chapter.chapter.url}" } logcat { "Loading ${chapter.chapter.url}" }
activeChapterSubscription?.unsubscribe() withIOContext {
activeChapterSubscription = getLoadObservable(loader, chapter) getLoadObservable(loader, chapter)
.toCompletable() .asFlow()
.onErrorComplete() .catch { logcat(LogPriority.ERROR, it) }
.subscribe() .first()
.also(::add) }
} }
fun loadNewChapterFromDialog(chapter: DomainChapter) { suspend fun loadNewChapterFromDialog(chapter: Chapter) {
val newChapter = chapterList.firstOrNull { it.chapter.id == chapter.id } ?: return val newChapter = chapterList.firstOrNull { it.chapter.id == chapter.id } ?: return
loadAdjacent(newChapter) loadAdjacent(newChapter)
} }
@ -467,37 +417,32 @@ class ReaderPresenter(
* sets the [isLoadingAdjacentChapterRelay] that the view uses to prevent any further * sets the [isLoadingAdjacentChapterRelay] that the view uses to prevent any further
* interaction until the chapter is loaded. * interaction until the chapter is loaded.
*/ */
private fun loadAdjacent(chapter: ReaderChapter) { private suspend fun loadAdjacent(chapter: ReaderChapter) {
val loader = loader ?: return val loader = loader ?: return
logcat { "Loading adjacent ${chapter.chapter.url}" } logcat { "Loading adjacent ${chapter.chapter.url}" }
activeChapterSubscription?.unsubscribe() mutableState.update { it.copy(isLoadingAdjacentChapter = true) }
activeChapterSubscription = getLoadObservable(loader, chapter) withIOContext {
.doOnSubscribe { coroutineScope.launch { isLoadingAdjacentChapterEvent.send(true) } } getLoadObservable(loader, chapter)
.doOnUnsubscribe { coroutineScope.launch { isLoadingAdjacentChapterEvent.send(false) } } .asFlow()
.subscribeFirst( .first()
{ view, _ -> }
view.moveToPageIndex(0) mutableState.update { it.copy(isLoadingAdjacentChapter = false) }
},
{ _, _ ->
// Ignore onError event, viewers handle that state
},
)
} }
/** /**
* Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so * Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so
* that the user doesn't have to wait too long to continue reading. * that the user doesn't have to wait too long to continue reading.
*/ */
private fun preload(chapter: ReaderChapter) { private suspend fun preload(chapter: ReaderChapter) {
if (chapter.pageLoader is HttpPageLoader) { if (chapter.pageLoader is HttpPageLoader) {
val manga = manga ?: return val manga = manga ?: return
val dbChapter = chapter.chapter val dbChapter = chapter.chapter
val isDownloaded = downloadManager.isChapterDownloaded( val isDownloaded = downloadManager.isChapterDownloaded(
dbChapter.name, dbChapter.name,
dbChapter.scanlator, dbChapter.scanlator,
/* SY --> */ manga.originalTitle /* SY <-- */, /* SY --> */ manga.ogTitle /* SY <-- */,
manga.source, manga.source,
skipCache = true, skipCache = true,
) )
@ -513,13 +458,14 @@ class ReaderPresenter(
logcat { "Preloading ${chapter.chapter.url}" } logcat { "Preloading ${chapter.chapter.url}" }
val loader = loader ?: return val loader = loader ?: return
withIOContext {
loader.loadChapter(chapter) loader.loadChapter(chapter)
.observeOn(AndroidSchedulers.mainThread()) .doOnCompleted { eventChannel.trySend(Event.ReloadViewerChapters) }
// Update current chapters whenever a chapter is preloaded
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
.onErrorComplete() .onErrorComplete()
.subscribe() .toObservable<Unit>()
.also(::add) .asFlow()
.firstOrNull()
}
} }
/** /**
@ -528,7 +474,7 @@ class ReaderPresenter(
* [page]'s chapter is different from the currently active. * [page]'s chapter is different from the currently active.
*/ */
fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) { fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) {
val currentChapters = viewerChaptersRelay.value ?: return val currentChapters = state.value.viewerChapters ?: return
val selectedChapter = page.chapter val selectedChapter = page.chapter
@ -547,7 +493,7 @@ class ReaderPresenter(
selectedChapter.chapter.read = true selectedChapter.chapter.read = true
// SY --> // SY -->
if (manga?.isEhBasedManga() == true) { if (manga?.isEhBasedManga() == true) {
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
chapterList chapterList
.filter { it.chapter.source_order > selectedChapter.chapter.source_order } .filter { it.chapter.source_order > selectedChapter.chapter.source_order }
.onEach { .onEach {
@ -565,7 +511,7 @@ class ReaderPresenter(
logcat { "Setting ${selectedChapter.chapter.url} as active" } logcat { "Setting ${selectedChapter.chapter.url} as active" }
saveReadingProgress(currentChapters.currChapter) saveReadingProgress(currentChapters.currChapter)
setReadStartTime() setReadStartTime()
loadNewChapter(selectedChapter) viewModelScope.launch { loadNewChapter(selectedChapter) }
} }
val pages = page.chapter.pages ?: return val pages = page.chapter.pages ?: return
val inDownloadRange = page.number.toDouble() / pages.size > 0.25 val inDownloadRange = page.number.toDouble() / pages.size > 0.25
@ -581,23 +527,23 @@ class ReaderPresenter(
// Only download ahead if current + next chapter is already downloaded too to avoid jank // Only download ahead if current + next chapter is already downloaded too to avoid jank
if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return
val nextChapter = viewerChaptersRelay.value?.nextChapter?.chapter ?: return val nextChapter = state.value.viewerChapters?.nextChapter?.chapter ?: return
coroutineScope.launchIO { viewModelScope.launchIO {
val isNextChapterDownloaded = downloadManager.isChapterDownloaded( val isNextChapterDownloaded = downloadManager.isChapterDownloaded(
nextChapter.name, nextChapter.name,
nextChapter.scanlator, nextChapter.scanlator,
// SY --> // SY -->
manga.originalTitle, manga.ogTitle,
// SY <-- // SY <--
manga.source, manga.source,
) )
if (!isNextChapterDownloaded) return@launchIO if (!isNextChapterDownloaded) return@launchIO
val chaptersToDownload = getNextChapters.await(manga.id!!, nextChapter.id!!) val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!)
.take(amount) .take(amount)
downloadManager.downloadChapters( downloadManager.downloadChapters(
manga.toDomainManga()!!, manga,
chaptersToDownload, chaptersToDownload,
) )
} }
@ -641,7 +587,7 @@ class ReaderPresenter(
* Called when reader chapter is changed in reader or when activity is paused. * Called when reader chapter is changed in reader or when activity is paused.
*/ */
private fun saveReadingProgress(readerChapter: ReaderChapter) { private fun saveReadingProgress(readerChapter: ReaderChapter) {
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
saveChapterProgress(readerChapter) saveChapterProgress(readerChapter)
saveChapterHistory(readerChapter) saveChapterHistory(readerChapter)
} }
@ -689,23 +635,23 @@ class ReaderPresenter(
/** /**
* Called from the activity to preload the given [chapter]. * Called from the activity to preload the given [chapter].
*/ */
fun preloadChapter(chapter: ReaderChapter) { suspend fun preloadChapter(chapter: ReaderChapter) {
preload(chapter) preload(chapter)
} }
/** /**
* Called from the activity to load and set the next chapter as active. * Called from the activity to load and set the next chapter as active.
*/ */
fun loadNextChapter() { suspend fun loadNextChapter() {
val nextChapter = viewerChaptersRelay.value?.nextChapter ?: return val nextChapter = state.value.viewerChapters?.nextChapter ?: return
loadAdjacent(nextChapter) loadAdjacent(nextChapter)
} }
/** /**
* Called from the activity to load and set the previous chapter as active. * Called from the activity to load and set the previous chapter as active.
*/ */
fun loadPreviousChapter() { suspend fun loadPreviousChapter() {
val prevChapter = viewerChaptersRelay.value?.prevChapter ?: return val prevChapter = state.value.viewerChapters?.prevChapter ?: return
loadAdjacent(prevChapter) loadAdjacent(prevChapter)
} }
@ -713,7 +659,7 @@ class ReaderPresenter(
* Returns the currently active chapter. * Returns the currently active chapter.
*/ */
fun getCurrentChapter(): ReaderChapter? { fun getCurrentChapter(): ReaderChapter? {
return viewerChaptersRelay.value?.currChapter return state.value.viewerChapters?.currChapter
} }
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
@ -731,7 +677,7 @@ class ReaderPresenter(
fun bookmarkCurrentChapter(bookmarked: Boolean) { fun bookmarkCurrentChapter(bookmarked: Boolean) {
val chapter = getCurrentChapter()?.chapter ?: return val chapter = getCurrentChapter()?.chapter ?: return
chapter.bookmark = bookmarked // Otherwise the bookmark icon doesn't update chapter.bookmark = bookmarked // Otherwise the bookmark icon doesn't update
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
updateChapter.await( updateChapter.await(
ChapterUpdate( ChapterUpdate(
id = chapter.id!!.toLong(), id = chapter.id!!.toLong(),
@ -745,7 +691,7 @@ class ReaderPresenter(
fun toggleBookmark(chapterId: Long, bookmarked: Boolean) { fun toggleBookmark(chapterId: Long, bookmarked: Boolean) {
val chapter = chapterList.find { it.chapter.id == chapterId }?.chapter ?: return val chapter = chapterList.find { it.chapter.id == chapterId }?.chapter ?: return
chapter.bookmark = bookmarked chapter.bookmark = bookmarked
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
updateChapter.await( updateChapter.await(
ChapterUpdate( ChapterUpdate(
id = chapter.id!!.toLong(), id = chapter.id!!.toLong(),
@ -762,7 +708,7 @@ class ReaderPresenter(
fun getMangaReadingMode(resolveDefault: Boolean = true): Int { fun getMangaReadingMode(resolveDefault: Boolean = true): Int {
val default = readerPreferences.defaultReadingMode().get() val default = readerPreferences.defaultReadingMode().get()
val manga = manga ?: return default val manga = manga ?: return default
val readingMode = ReadingModeType.fromPreference(manga.readingModeType) val readingMode = ReadingModeType.fromPreference(manga.readingModeType.toInt())
// SY --> // SY -->
return when { return when {
resolveDefault && readingMode == ReadingModeType.DEFAULT && readerPreferences.useAutoWebtoon().get() -> { resolveDefault && readingMode == ReadingModeType.DEFAULT && readerPreferences.useAutoWebtoon().get() -> {
@ -770,7 +716,7 @@ class ReaderPresenter(
?: default ?: default
} }
resolveDefault && readingMode == ReadingModeType.DEFAULT -> default resolveDefault && readingMode == ReadingModeType.DEFAULT -> default
else -> manga.readingModeType else -> manga.readingModeType.toInt()
} }
// SY <-- // SY <--
} }
@ -780,22 +726,21 @@ class ReaderPresenter(
*/ */
fun setMangaReadingMode(readingModeType: Int) { fun setMangaReadingMode(readingModeType: Int) {
val manga = manga ?: return val manga = manga ?: return
manga.readingModeType = readingModeType viewModelScope.launchIO {
setMangaViewerFlags.awaitSetMangaReadingMode(manga.id, readingModeType.toLong())
coroutineScope.launchIO { val currChapters = state.value.viewerChapters
setMangaViewerFlags.awaitSetMangaReadingMode(manga.id!!.toLong(), readingModeType.toLong())
delay(250)
val currChapters = viewerChaptersRelay.value
if (currChapters != null) { if (currChapters != null) {
// Save current page // Save current page
val currChapter = currChapters.currChapter val currChapter = currChapters.currChapter
currChapter.requestedPage = currChapter.chapter.last_page_read currChapter.requestedPage = currChapter.chapter.last_page_read
withUIContext { mutableState.update {
// Emit manga and chapters to the new viewer it.copy(
view?.setManga(manga) manga = getManga.await(manga.id),
view?.setChapters(currChapters) viewerChapters = currChapters,
)
} }
eventChannel.send(Event.ReloadViewerChapters)
} }
} }
} }
@ -805,10 +750,10 @@ class ReaderPresenter(
*/ */
fun getMangaOrientationType(resolveDefault: Boolean = true): Int { fun getMangaOrientationType(resolveDefault: Boolean = true): Int {
val default = readerPreferences.defaultOrientationType().get() val default = readerPreferences.defaultOrientationType().get()
val orientation = OrientationType.fromPreference(manga?.orientationType) val orientation = OrientationType.fromPreference(manga?.orientationType?.toInt())
return when { return when {
resolveDefault && orientation == OrientationType.DEFAULT -> default resolveDefault && orientation == OrientationType.DEFAULT -> default
else -> manga?.orientationType ?: default else -> manga?.orientationType?.toInt() ?: default
} }
} }
@ -817,14 +762,22 @@ class ReaderPresenter(
*/ */
fun setMangaOrientationType(rotationType: Int) { fun setMangaOrientationType(rotationType: Int) {
val manga = manga ?: return val manga = manga ?: return
manga.orientationType = rotationType viewModelScope.launchIO {
setMangaViewerFlags.awaitSetOrientationType(manga.id, rotationType.toLong())
coroutineScope.launchIO { val currChapters = state.value.viewerChapters
setMangaViewerFlags.awaitSetOrientationType(manga.id!!.toLong(), rotationType.toLong())
delay(250)
val currChapters = viewerChaptersRelay.value
if (currChapters != null) { if (currChapters != null) {
withUIContext { view?.setOrientation(getMangaOrientationType()) } // Save current page
val currChapter = currChapters.currChapter
currChapter.requestedPage = currChapter.chapter.last_page_read
mutableState.update {
it.copy(
manga = getManga.await(manga.id),
viewerChapters = currChapters,
)
}
eventChannel.send(Event.SetOrientation(getMangaOrientationType()))
eventChannel.send(Event.ReloadViewerChapters)
} }
} }
} }
@ -861,8 +814,8 @@ class ReaderPresenter(
val relativePath = if (readerPreferences.folderPerManga().get()) DiskUtil.buildValidFilename(manga.title) else "" val relativePath = if (readerPreferences.folderPerManga().get()) DiskUtil.buildValidFilename(manga.title) else ""
// Copy file in background. // Copy file in background.
viewModelScope.launchNonCancellable {
try { try {
coroutineScope.launchNonCancellable {
val uri = imageSaver.save( val uri = imageSaver.save(
image = Image.Page( image = Image.Page(
inputStream = page.stream!!, inputStream = page.stream!!,
@ -872,12 +825,12 @@ class ReaderPresenter(
) )
withUIContext { withUIContext {
notifier.onComplete(uri) notifier.onComplete(uri)
view?.onSaveImageResult(SaveImageResult.Success(uri)) eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri)))
}
} }
} catch (e: Throwable) { } catch (e: Throwable) {
notifier.onError(e.message) notifier.onError(e.message)
view?.onSaveImageResult(SaveImageResult.Error(e)) eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
}
} }
} }
@ -895,8 +848,8 @@ class ReaderPresenter(
val relativePath = if (readerPreferences.folderPerManga().get()) DiskUtil.buildValidFilename(manga.title) else "" val relativePath = if (readerPreferences.folderPerManga().get()) DiskUtil.buildValidFilename(manga.title) else ""
// Copy file in background. // Copy file in background.
viewModelScope.launchNonCancellable {
try { try {
coroutineScope.launchIO {
val uri = saveImages( val uri = saveImages(
page1 = firstPage, page1 = firstPage,
page2 = secondPage, page2 = secondPage,
@ -905,14 +858,11 @@ class ReaderPresenter(
location = Location.Pictures.create(relativePath), location = Location.Pictures.create(relativePath),
manga = manga, manga = manga,
) )
withUIContext { eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri)))
notifier.onComplete(uri)
view!!.onSaveImageResult(SaveImageResult.Success(uri))
}
}
} catch (e: Throwable) { } catch (e: Throwable) {
notifier.onError(e.message) notifier.onError(e.message)
view!!.onSaveImageResult(SaveImageResult.Error(e)) eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
}
} }
} }
@ -966,7 +916,7 @@ class ReaderPresenter(
val filename = generateFilename(manga, page) val filename = generateFilename(manga, page)
try { try {
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
destDir.deleteRecursively() destDir.deleteRecursively()
val uri = imageSaver.save( val uri = imageSaver.save(
image = Image.Page( image = Image.Page(
@ -975,9 +925,7 @@ class ReaderPresenter(
location = Location.Cache, location = Location.Cache,
), ),
) )
withUIContext { eventChannel.send(Event.ShareImage(uri, page))
view?.onShareImageResult(uri, page)
}
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
@ -994,7 +942,7 @@ class ReaderPresenter(
val destDir = context.cacheImageDir val destDir = context.cacheImageDir
try { try {
coroutineScope.launchIO { viewModelScope.launchNonCancellable {
destDir.deleteRecursively() destDir.deleteRecursively()
val uri = saveImages( val uri = saveImages(
page1 = firstPage, page1 = firstPage,
@ -1004,9 +952,7 @@ class ReaderPresenter(
location = Location.Cache, location = Location.Cache,
manga = manga, manga = manga,
) )
withUIContext { eventChannel.send(Event.ShareImage(uri, firstPage, secondPage))
view!!.onShareImageResult(uri, firstPage, secondPage)
}
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
@ -1019,24 +965,21 @@ class ReaderPresenter(
*/ */
fun setAsCover(context: Context, page: ReaderPage) { fun setAsCover(context: Context, page: ReaderPage) {
if (page.status != Page.State.READY) return if (page.status != Page.State.READY) return
val manga = manga?.toDomainManga() ?: return val manga = manga ?: return
val stream = page.stream ?: return val stream = page.stream ?: return
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
try { val result = try {
manga.editCover(context, stream()) manga.editCover(context, stream())
withUIContext {
view?.onSetAsCoverResult(
if (manga.isLocal() || manga.favorite) { if (manga.isLocal() || manga.favorite) {
SetAsCoverResult.Success SetAsCoverResult.Success
} else { } else {
SetAsCoverResult.AddToLibraryFirst SetAsCoverResult.AddToLibraryFirst
},
)
} }
} catch (e: Exception) { } catch (e: Exception) {
withUIContext { view?.onSetAsCoverResult(SetAsCoverResult.Error) } SetAsCoverResult.Error
} }
eventChannel.send(Event.SetCoverResult(result))
} }
} }
@ -1068,8 +1011,8 @@ class ReaderPresenter(
val trackManager = Injekt.get<TrackManager>() val trackManager = Injekt.get<TrackManager>()
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
getTracks.await(manga.id!!) getTracks.await(manga.id)
.mapNotNull { track -> .mapNotNull { track ->
val service = trackManager.getService(track.syncId) val service = trackManager.getService(track.syncId)
if (service != null && service.isLogged && chapterRead > track.lastChapterRead /* SY --> */ && ((service.id == TrackManager.MDLIST && track.status != FollowStatus.UNFOLLOWED.int.toLong()) || service.id != TrackManager.MDLIST)/* SY <-- */) { if (service != null && service.isLogged && chapterRead > track.lastChapterRead /* SY --> */ && ((service.id == TrackManager.MDLIST && track.status != FollowStatus.UNFOLLOWED.int.toLong()) || service.id != TrackManager.MDLIST)/* SY <-- */) {
@ -1106,16 +1049,17 @@ class ReaderPresenter(
*/ */
private fun enqueueDeleteReadChapters(chapter: ReaderChapter) { private fun enqueueDeleteReadChapters(chapter: ReaderChapter) {
if (!chapter.chapter.read) return if (!chapter.chapter.read) return
val mergedManga = state.value.mergedManga
// SY --> // SY -->
val manga = if (mergedManga.isNullOrEmpty()) { val manga = if (mergedManga.isNullOrEmpty()) {
manga manga
} else { } else {
mergedManga.orEmpty()[chapter.chapter.manga_id]?.toDbManga() mergedManga[chapter.chapter.manga_id]
} ?: return } ?: return
// SY <-- // SY <--
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga.toDomainManga()!!) downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga)
} }
} }
@ -1124,35 +1068,30 @@ class ReaderPresenter(
* are ignored. * are ignored.
*/ */
private fun deletePendingChapters() { private fun deletePendingChapters() {
coroutineScope.launchNonCancellable { viewModelScope.launchNonCancellable {
downloadManager.deletePendingChapters() downloadManager.deletePendingChapters()
} }
} }
// We're trying to avoid using Rx, so we "undeprecate" this data class State(
@Suppress("DEPRECATION") val manga: Manga? = null,
override fun getView(): ReaderActivity? { val viewerChapters: ViewerChapters? = null,
return super.getView() val isLoadingAdjacentChapter: Boolean = false,
// SY -->
val meta: RaisedSearchMetadata? = null,
val mergedManga: Map<Long, Manga>? = null,
// SY <--
)
sealed class Event {
object ReloadViewerChapters : Event()
data class SetOrientation(val orientation: Int) : Event()
data class SetCoverResult(val result: SetAsCoverResult) : Event()
data class SavedImage(val result: SaveImageResult) : Event()
data class ShareImage(val uri: Uri, val page: ReaderPage, val secondPage: ReaderPage? = null) : Event()
} }
/**
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
* subscription list.
*
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
private fun <T> Observable<T>.subscribeFirst(onNext: (ReaderActivity, T) -> Unit, onError: ((ReaderActivity, Throwable) -> Unit) = { _, _ -> }) = compose(deliverFirst<T>()).subscribe(split(onNext, onError)).apply { add(this) }
/**
* Subscribes an observable with [deliverLatestCache] and adds it to the presenter's lifecycle
* subscription list.
*
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
private fun <T> Observable<T>.subscribeLatestCache(onNext: (ReaderActivity, T) -> Unit, onError: ((ReaderActivity, Throwable) -> Unit) = { _, _ -> }) = compose(deliverLatestCache<T>()).subscribe(split(onNext, onError)).apply { add(this) }
companion object { companion object {
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8) // Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)
private const val MAX_FILE_NAME_BYTES = 250 private const val MAX_FILE_NAME_BYTES = 250

View File

@ -7,10 +7,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.databinding.ReaderChaptersDialogBinding import eu.kanade.tachiyomi.databinding.ReaderChaptersDialogBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter import eu.kanade.tachiyomi.ui.reader.ReaderViewModel
import eu.kanade.tachiyomi.util.chapter.getChapterSort import eu.kanade.tachiyomi.util.chapter.getChapterSort
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -18,7 +17,7 @@ import kotlinx.coroutines.launch
class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterAdapter.OnBookmarkClickListener { class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterAdapter.OnBookmarkClickListener {
private val binding = ReaderChaptersDialogBinding.inflate(activity.layoutInflater) private val binding = ReaderChaptersDialogBinding.inflate(activity.layoutInflater)
var presenter: ReaderPresenter = activity.presenter var viewModel: ReaderViewModel = activity.viewModel
var adapter: FlexibleAdapter<ReaderChapterItem>? = null var adapter: FlexibleAdapter<ReaderChapterItem>? = null
var dialog: AlertDialog var dialog: AlertDialog
@ -35,9 +34,11 @@ class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterA
adapter?.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, position -> adapter?.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, position ->
val item = adapter?.getItem(position) val item = adapter?.getItem(position)
if (item != null && item.chapter.id != presenter.getCurrentChapter()?.chapter?.id) { if (item != null && item.chapter.id != viewModel.getCurrentChapter()?.chapter?.id) {
dialog.dismiss() dialog.dismiss()
presenter.loadNewChapterFromDialog(item.chapter) activity.lifecycleScope.launch {
viewModel.loadNewChapterFromDialog(item.chapter)
}
} }
true true
} }
@ -51,8 +52,8 @@ class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterA
} }
private fun refreshList(scroll: Boolean = true) { private fun refreshList(scroll: Boolean = true) {
val chapterSort = getChapterSort(presenter.manga!!.toDomainManga()!!) val chapterSort = getChapterSort(viewModel.manga!!)
val chapters = presenter.getChapters(activity) val chapters = viewModel.getChapters(activity)
.sortedWith { a, b -> .sortedWith { a, b ->
chapterSort(a.chapter, b.chapter) chapterSort(a.chapter, b.chapter)
} }
@ -73,7 +74,7 @@ class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterA
} }
override fun bookmarkChapter(chapter: Chapter) { override fun bookmarkChapter(chapter: Chapter) {
presenter.toggleBookmark(chapter.id, !chapter.bookmark) viewModel.toggleBookmark(chapter.id, !chapter.bookmark)
refreshList(scroll = false) refreshList(scroll = false)
} }
} }

View File

@ -44,22 +44,22 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
private fun initGeneralPreferences() { private fun initGeneralPreferences() {
binding.viewer.onItemSelectedListener = { position -> binding.viewer.onItemSelectedListener = { position ->
val readingModeType = ReadingModeType.fromSpinner(position) val readingModeType = ReadingModeType.fromSpinner(position)
(context as ReaderActivity).presenter.setMangaReadingMode(readingModeType.flagValue) (context as ReaderActivity).viewModel.setMangaReadingMode(readingModeType.flagValue)
val mangaViewer = (context as ReaderActivity).presenter.getMangaReadingMode() val mangaViewer = (context as ReaderActivity).viewModel.getMangaReadingMode()
if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) { if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) {
initWebtoonPreferences() initWebtoonPreferences()
} else { } else {
initPagerPreferences() initPagerPreferences()
} }
} }
binding.viewer.setSelection((context as ReaderActivity).presenter.manga?.readingModeType?.let { ReadingModeType.fromPreference(it).prefValue } ?: ReadingModeType.DEFAULT.prefValue) binding.viewer.setSelection((context as ReaderActivity).viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue)
binding.rotationMode.onItemSelectedListener = { position -> binding.rotationMode.onItemSelectedListener = { position ->
val rotationType = OrientationType.fromSpinner(position) val rotationType = OrientationType.fromSpinner(position)
(context as ReaderActivity).presenter.setMangaOrientationType(rotationType.flagValue) (context as ReaderActivity).viewModel.setMangaOrientationType(rotationType.flagValue)
} }
binding.rotationMode.setSelection((context as ReaderActivity).presenter.manga?.orientationType?.let { OrientationType.fromPreference(it).prefValue } ?: OrientationType.DEFAULT.prefValue) binding.rotationMode.setSelection((context as ReaderActivity).viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue)
} }
/** /**

View File

@ -11,8 +11,8 @@ import androidx.core.text.bold
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans import androidx.core.text.inSpans
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
@ -55,7 +55,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
val isPrevDownloaded = downloadManager.isChapterDownloaded( val isPrevDownloaded = downloadManager.isChapterDownloaded(
prevChapter.name, prevChapter.name,
prevChapter.scanlator, prevChapter.scanlator,
/* SY --> */ manga.originalTitle /* SY <-- */, /* SY --> */ manga.ogTitle /* SY <-- */,
manga.source, manga.source,
skipCache = true, skipCache = true,
) )
@ -93,7 +93,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
val isNextDownloaded = downloadManager.isChapterDownloaded( val isNextDownloaded = downloadManager.isChapterDownloaded(
nextChapter.name, nextChapter.name,
nextChapter.scanlator, nextChapter.scanlator,
/* SY --> */ manga.originalTitle /* SY <-- */, /* SY --> */ manga.ogTitle /* SY <-- */,
manga.source, manga.source,
skipCache = true, skipCache = true,
) )

View File

@ -62,7 +62,7 @@ class PagerTransitionHolder(
addView(transitionView) addView(transitionView)
addView(pagesContainer) addView(pagesContainer)
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga) transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga)
transition.to?.let { observeStatus(it) } transition.to?.let { observeStatus(it) }
} }

View File

@ -64,7 +64,7 @@ class WebtoonTransitionHolder(
* Binds the given [transition] with this view holder, subscribing to its state. * Binds the given [transition] with this view holder, subscribing to its state.
*/ */
fun bind(transition: ChapterTransition) { fun bind(transition: ChapterTransition) {
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga) transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga)
transition.to?.let { observeStatus(it, transition) } transition.to?.let { observeStatus(it, transition) }
} }

View File

@ -1,14 +1,13 @@
package exh.util package exh.util
import android.content.Context import android.content.Context
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Locale import java.util.Locale
import eu.kanade.domain.manga.model.Manga as DomainManga
fun Manga.mangaType(context: Context): String { fun Manga.mangaType(context: Context): String {
return context.getString( return context.getString(
@ -26,30 +25,6 @@ fun Manga.mangaType(context: Context): String {
* The type of comic the manga is (ie. manga, manhwa, manhua) * The type of comic the manga is (ie. manga, manhwa, manhua)
*/ */
fun Manga.mangaType(sourceName: String? = Injekt.get<SourceManager>().get(source)?.name): MangaType { fun Manga.mangaType(sourceName: String? = Injekt.get<SourceManager>().get(source)?.name): MangaType {
val currentTags = getGenres().orEmpty()
return when {
currentTags.any { tag -> isMangaTag(tag) } -> {
MangaType.TYPE_MANGA
}
currentTags.any { tag -> isWebtoonTag(tag) } || sourceName?.let { isWebtoonSource(it) } == true -> {
MangaType.TYPE_WEBTOON
}
currentTags.any { tag -> isComicTag(tag) } || sourceName?.let { isComicSource(it) } == true -> {
MangaType.TYPE_COMIC
}
currentTags.any { tag -> isManhuaTag(tag) } || sourceName?.let { isManhuaSource(it) } == true -> {
MangaType.TYPE_MANHUA
}
currentTags.any { tag -> isManhwaTag(tag) } || sourceName?.let { isManhwaSource(it) } == true -> {
MangaType.TYPE_MANHWA
}
else -> {
MangaType.TYPE_MANGA
}
}
}
fun DomainManga.mangaType(sourceName: String? = Injekt.get<SourceManager>().get(source)?.name): MangaType {
val currentTags = genre.orEmpty() val currentTags = genre.orEmpty()
return when { return when {
currentTags.any { tag -> isMangaTag(tag) } -> { currentTags.any { tag -> isMangaTag(tag) } -> {

View File

@ -1,7 +1,6 @@
[versions] [versions]
aboutlib_version = "10.5.2" aboutlib_version = "10.5.2"
okhttp_version = "5.0.0-alpha.10" okhttp_version = "5.0.0-alpha.10"
nucleus_version = "3.0.0"
coil_version = "2.2.2" coil_version = "2.2.2"
shizuku_version = "12.2.0" shizuku_version = "12.2.0"
sqlite = "2.3.0-rc01" sqlite = "2.3.0-rc01"
@ -41,9 +40,6 @@ sqlite-android = "com.github.requery:sqlite-android:3.39.2"
preferencektx = "androidx.preference:preference-ktx:1.2.0" preferencektx = "androidx.preference:preference-ktx:1.2.0"
nucleus-core = { module = "info.android15.nucleus:nucleus", version.ref = "nucleus_version" }
nucleus-supportv7 = { module = "info.android15.nucleus:nucleus-support-v7", version.ref = "nucleus_version" }
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440" injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" } coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
@ -97,7 +93,6 @@ reactivex = ["rxandroid", "rxjava", "rxrelay"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
js-engine = ["quickjs-android"] js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
nucleus = ["nucleus-core", "nucleus-supportv7"]
coil = ["coil-core", "coil-gif", "coil-compose"] coil = ["coil-core", "coil-gif", "coil-compose"]
shizuku = ["shizuku-api", "shizuku-provider"] shizuku = ["shizuku-api", "shizuku-provider"]
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"] voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]