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
implementation(libs.preferencektx)
// Model View Presenter
implementation(libs.bundles.nucleus)
// Dependency injection
implementation(libs.injekt.core)

View File

@ -1,20 +1,18 @@
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.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.SManga
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.Serializable
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
data class Manga(
val id: Long,
@ -83,6 +81,12 @@ data class Manga(
val bookmarkedFilterRaw: Long
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
get() = when (unreadFilterRaw) {
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 {
return MangaUpdate(
id = id,

View File

@ -34,7 +34,9 @@ import android.widget.FrameLayout
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.ColorUtils
import androidx.core.transition.doOnEnd
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 dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.BuildConfig
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.Notifications
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.ThemingDelegateImpl
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Success
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterDialog
import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader
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.webview.WebViewActivity
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.system.applySystemAnimatorScale
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.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import logcat.LogPriority
import nucleus.factory.RequiresPresenter
import nucleus.view.NucleusAppCompatActivity
import uy.kohesive.injekt.injectLazy
import kotlin.math.abs
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
* viewers, to which calls from the presenter or UI events are delegated.
*/
@RequiresPresenter(ReaderPresenter::class)
class ReaderActivity :
NucleusAppCompatActivity<ReaderPresenter>(),
AppCompatActivity(),
SecureActivityDelegate by SecureActivityDelegateImpl(),
ThemingDelegate by ThemingDelegateImpl() {
@ -169,6 +174,8 @@ class ReaderActivity :
lateinit var binding: ReaderActivityBinding
val viewModel by viewModels<ReaderViewModel>()
val hasCutout by lazy { hasDisplayCutout() }
/**
@ -245,7 +252,7 @@ class ReaderActivity :
binding = ReaderActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
if (presenter.needsInit()) {
if (viewModel.needsInit()) {
val manga = intent.extras!!.getLong("manga", -1)
val chapter = intent.extras!!.getLong("chapter", -1)
// SY -->
@ -256,7 +263,16 @@ class ReaderActivity :
return
}
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) {
@ -279,6 +295,48 @@ class ReaderActivity :
.drop(1)
.onEach { if (!it) finish() }
.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 -->
@ -329,13 +387,13 @@ class ReaderActivity :
}
// SY <--
if (!isChangingConfigurations) {
presenter.onSaveInstanceStateNonConfigurationChange()
viewModel.onSaveInstanceStateNonConfigurationChange()
}
super.onSaveInstanceState(outState)
}
override fun onPause() {
presenter.saveCurrentChapterReadingProgress()
viewModel.saveCurrentChapterReadingProgress()
super.onPause()
}
@ -345,7 +403,7 @@ class ReaderActivity :
*/
override fun onResume() {
super.onResume()
presenter.setReadStartTime()
viewModel.setReadStartTime()
setMenuVisibility(menuVisible, animate = false)
}
@ -366,7 +424,7 @@ class ReaderActivity :
override fun onCreateOptionsMenu(menu: Menu): Boolean {
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_remove_bookmark).isVisible = isChapterBookmarked*/
@ -383,11 +441,11 @@ class ReaderActivity :
openChapterInWebview()
}
R.id.action_bookmark -> {
presenter.bookmarkCurrentChapter(true)
viewModel.bookmarkCurrentChapter(true)
invalidateOptionsMenu()
}
R.id.action_remove_bookmark -> {
presenter.bookmarkCurrentChapter(false)
viewModel.bookmarkCurrentChapter(false)
invalidateOptionsMenu()
}
}
@ -398,17 +456,17 @@ class ReaderActivity :
* Called when the user clicks the back key or the button on the toolbar. The call is
* delegated to the presenter.
*/
override fun onBackPressed() {
presenter.onBackPressed()
super.onBackPressed()
override fun finish() {
viewModel.onActivityFinish()
super.finish()
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_N) {
presenter.loadNextChapter()
loadNextChapter()
return true
} else if (keyCode == KeyEvent.KEYCODE_P) {
presenter.loadPreviousChapter()
loadPreviousChapter()
return true
}
return super.onKeyUp(keyCode, event)
@ -475,7 +533,7 @@ class ReaderActivity :
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
onBackPressedDispatcher.onBackPressed()
}
binding.header.applyInsetter {
@ -490,7 +548,7 @@ class ReaderActivity :
}
binding.toolbar.setOnClickListener {
presenter.manga?.id?.let { id ->
viewModel.manga?.id?.let { id ->
startActivity(
Intent(this, MainActivity::class.java).apply {
action = MainActivity.SHORTCUT_MANGA
@ -612,11 +670,11 @@ class ReaderActivity :
setOnClickListener {
popupMenu(
items = ReadingModeType.values().map { it.flagValue to it.stringRes },
selectedItemId = presenter.getMangaReadingMode(resolveDefault = false),
selectedItemId = viewModel.getMangaReadingMode(resolveDefault = false),
) {
val newReadingMode = ReadingModeType.fromPreference(itemId)
presenter.setMangaReadingMode(newReadingMode.flagValue)
viewModel.setMangaReadingMode(newReadingMode.flagValue)
menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) {
@ -634,7 +692,7 @@ class ReaderActivity :
setOnClickListener {
// SY -->
val mangaViewer = presenter.getMangaReadingMode()
val mangaViewer = viewModel.getMangaReadingMode()
// SY <--
val isPagerType = ReadingModeType.isPagerType(mangaViewer)
val enabled = if (isPagerType) {
@ -674,12 +732,12 @@ class ReaderActivity :
setOnClickListener {
popupMenu(
items = OrientationType.values().map { it.flagValue to it.stringRes },
selectedItemId = presenter.manga?.orientationType
selectedItemId = viewModel.manga?.orientationType?.toInt()
?: readerPreferences.defaultOrientationType().get(),
) {
val newOrientation = OrientationType.fromPreference(itemId)
presenter.setMangaOrientationType(newOrientation.flagValue)
viewModel.setMangaOrientationType(newOrientation.flagValue)
menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes)
@ -804,9 +862,9 @@ class ReaderActivity :
binding.ehRetryAll.setOnClickListener {
var retried = 0
presenter.viewerChaptersRelay.value
.currChapter
.pages
viewModel.state.value.viewerChapters
?.currChapter
?.pages
?.forEachIndexed { _, page ->
var shouldQueuePage = false
if (page.status == Page.State.ERROR) {
@ -823,7 +881,7 @@ class ReaderActivity :
}
// 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)
if (src?.isEhBasedSource() == true) {
page.imageUrl = null
@ -865,7 +923,7 @@ class ReaderActivity :
} else if (curPage.status == Page.State.READY) {
toast(R.string.eh_boost_page_downloaded)
} else {
val loader = (presenter.viewerChaptersRelay.value.currChapter.pageLoader as? HttpPageLoader)
val loader = (viewModel.state.value.viewerChapters?.currChapter?.pageLoader as? HttpPageLoader)
if (loader != null) {
loader.boostPage(curPage)
toast(R.string.eh_boost_boosted)
@ -886,7 +944,7 @@ class ReaderActivity :
private fun exhCurrentpage(): ReaderPage? {
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() {
@ -924,7 +982,7 @@ class ReaderActivity :
} else {
pViewer.config.doublePages = doublePages
}
val currentChapter = presenter.getCurrentChapter()
val currentChapter = viewModel.getCurrentChapter()
if (doublePages) {
// If we're moving from singe to double, we want the current page to be the first page
pViewer.config.shiftDoublePage = (
@ -932,7 +990,7 @@ class ReaderActivity :
(currentChapter?.pages?.take(binding.pageSlider.value.floor())?.count { it.fullPage || it.isolatedPage } ?: 0)
) % 2 != 0
}
presenter.viewerChaptersRelay.value?.let {
viewModel.state.value.viewerChapters?.let {
pViewer.setChaptersDoubleShift(it)
}
}
@ -945,7 +1003,7 @@ class ReaderActivity :
private fun shiftDoublePages() {
(viewer as? PagerViewer)?.config?.let { config ->
config.shiftDoublePage = !config.shiftDoublePage
presenter.viewerChaptersRelay.value?.let {
viewModel.state.value.viewerChapters?.let {
(viewer as? PagerViewer)?.updateShifting()
(viewer as? PagerViewer)?.setChaptersDoubleShift(it)
invalidateOptionsMenu()
@ -960,7 +1018,7 @@ class ReaderActivity :
}
private fun updateCropBordersShortcut() {
val mangaViewer = presenter.getMangaReadingMode()
val mangaViewer = viewModel.getMangaReadingMode()
val isPagerType = ReadingModeType.isPagerType(mangaViewer)
val enabled = if (isPagerType) {
readerPreferences.cropBorders().get()
@ -1070,19 +1128,19 @@ class ReaderActivity :
fun setManga(manga: Manga) {
val prevViewer = viewer
val viewerMode = ReadingModeType.fromPreference(presenter.getMangaReadingMode(resolveDefault = false))
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
val newViewer = ReadingModeType.toViewer(presenter.getMangaReadingMode(), this)
val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this)
updateCropBordersShortcut()
if (window.sharedElementEnterTransition is MaterialContainerTransform) {
// Wait until transition is complete to avoid crash on API 26
window.sharedElementEnterTransition.doOnEnd {
setOrientation(presenter.getMangaOrientationType())
setOrientation(viewModel.getMangaOrientationType())
}
} else {
setOrientation(presenter.getMangaOrientationType())
setOrientation(viewModel.getMangaOrientationType())
}
// 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))
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 = toast(resources.getString(R.string.eh_auto_webtoon_snack))
} else if (readerPreferences.showReadingMode().get()) {
// SY <--
showReadingModeToast(presenter.getMangaReadingMode())
showReadingModeToast(viewModel.getMangaReadingMode())
}
// SY -->
@ -1171,9 +1229,9 @@ class ReaderActivity :
}
private fun openChapterInWebview() {
val manga = presenter.manga ?: return
val source = presenter.getSource() ?: return
val url = presenter.getChapterUrl() ?: return
val manga = viewModel.manga ?: return
val source = viewModel.getSource() ?: return
val url = viewModel.getChapterUrl() ?: return
val intent = WebViewActivity.newIntent(this, url, source.id, manga.title)
startActivity(intent)
@ -1194,7 +1252,7 @@ class ReaderActivity :
* 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
*/
fun setChapters(viewerChapters: ViewerChapters) {
private fun setChapters(viewerChapters: ViewerChapters) {
binding.readerContainer.removeView(loadingIndicator)
// SY -->
if (indexChapterToShift != null && indexPageToShift != null) {
@ -1280,7 +1338,7 @@ class ReaderActivity :
*/
fun moveToPageIndex(index: Int) {
val viewer = viewer ?: return
val currentChapter = presenter.getCurrentChapter() ?: return
val currentChapter = viewModel.getCurrentChapter() ?: return
val page = currentChapter.pages?.getOrNull(index) ?: return
viewer.moveToPage(page)
}
@ -1290,7 +1348,10 @@ class ReaderActivity :
* should be automatically shown.
*/
private fun loadNextChapter() {
presenter.loadNextChapter()
lifecycleScope.launch {
viewModel.loadNextChapter()
moveToPageIndex(0)
}
}
/**
@ -1298,7 +1359,10 @@ class ReaderActivity :
* should be automatically shown.
*/
private fun loadPreviousChapter() {
presenter.loadPreviousChapter()
lifecycleScope.launch {
viewModel.loadPreviousChapter()
moveToPageIndex(0)
}
}
/**
@ -1307,7 +1371,7 @@ class ReaderActivity :
*/
@SuppressLint("SetTextI18n")
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 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.
*/
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.
*/
fun shareImage(page: ReaderPage) {
presenter.shareImage(page)
viewModel.shareImage(page)
}
// SY -->
fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
presenter.shareImages(firstPage, secondPage, isLTR, bg)
viewModel.shareImages(firstPage, secondPage, isLTR, bg)
}
// SY <--
@ -1420,7 +1484,7 @@ class ReaderActivity :
* sharing tool.
*/
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
// SY -->
@ -1443,12 +1507,12 @@ class ReaderActivity :
* storage to the presenter.
*/
fun saveImage(page: ReaderPage) {
presenter.saveImage(page)
viewModel.saveImage(page)
}
// SY -->
fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
presenter.saveImages(firstPage, secondPage, isLTR, bg)
viewModel.saveImages(firstPage, secondPage, isLTR, bg)
}
// 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
* event depending on the [result].
*/
fun onSaveImageResult(result: ReaderPresenter.SaveImageResult) {
private fun onSaveImageResult(result: ReaderViewModel.SaveImageResult) {
when (result) {
is ReaderPresenter.SaveImageResult.Success -> {
is ReaderViewModel.SaveImageResult.Success -> {
toast(R.string.picture_saved)
}
is ReaderPresenter.SaveImageResult.Error -> {
is ReaderViewModel.SaveImageResult.Error -> {
logcat(LogPriority.ERROR, result.error)
}
}
@ -1472,14 +1536,14 @@ class ReaderActivity :
* cover to the presenter.
*/
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
* depending on the [result].
*/
fun onSetAsCoverResult(result: ReaderPresenter.SetAsCoverResult) {
private fun onSetAsCoverResult(result: ReaderViewModel.SetAsCoverResult) {
toast(
when (result) {
Success -> R.string.cover_updated
@ -1492,12 +1556,12 @@ class ReaderActivity :
/**
* Forces the user preferred [orientation] on the activity.
*/
fun setOrientation(orientation: Int) {
private fun setOrientation(orientation: Int) {
val newOrientation = OrientationType.fromPreference(orientation)
if (newOrientation.flag != requestedOrientation) {
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.content.Context
import android.net.Uri
import android.os.Bundle
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.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId
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.toDbChapter
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.GetMergedReferencesById
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.toDbManga
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.InsertTrack
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.store.DelayedTrackingStore
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.toDomainManga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
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.launchNonCancellable
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.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
@ -77,36 +79,38 @@ import exh.source.getMainSource
import exh.source.isEhBasedManga
import exh.util.defaultReaderType
import exh.util.mangaType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.MutableStateFlow
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.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import nucleus.presenter.RxPresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import tachiyomi.decoder.ImageDecoder
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
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.
*/
class ReaderPresenter(
class ReaderViewModel(
private val savedState: SavedStateHandle = SavedStateHandle(),
private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val downloadProvider: DownloadProvider = Injekt.get(),
@ -131,27 +135,28 @@ class ReaderPresenter(
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
private val getMergedChapterByMangaId: GetMergedChapterByMangaId = Injekt.get(),
// 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.
*/
var manga: Manga? = null
private set
// SY -->
var meta: RaisedSearchMetadata? = null
private set
var mergedManga: Map<Long, DomainManga>? = null
private set
// SY <--
val manga: Manga?
get() = state.value.manga
/**
* 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.
@ -168,17 +173,6 @@ class ReaderPresenter(
*/
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
/**
@ -186,7 +180,7 @@ class ReaderPresenter(
* time in a background thread to avoid blocking the UI.
*/
private val chapterList by lazy {
val manga = manga!!.toDomainManga()!!
val manga = manga!!
val chapters = runBlocking {
/* SY --> */ if (manga.source == MERGED_SOURCE_ID) {
getMergedChapterByMangaId.await(manga.id)
@ -204,12 +198,12 @@ class ReaderPresenter(
when {
readerPreferences.skipRead().get() && it.read -> true
readerPreferences.skipFiltered().get() -> {
(manga.unreadFilterRaw == DomainManga.CHAPTER_SHOW_READ && !it.read) ||
(manga.unreadFilterRaw == DomainManga.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 == DomainManga.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 == DomainManga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark) ||
(manga.unreadFilterRaw == Manga.CHAPTER_SHOW_READ && !it.read) ||
(manga.unreadFilterRaw == Manga.CHAPTER_SHOW_UNREAD && it.read) ||
(manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_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 == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) ||
(manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark) ||
// SY -->
(manga.filteredScanlators != null && MdUtil.getScanlators(it.scanlator).none { group -> manga.filteredScanlators.contains(group) })
// SY <--
@ -234,32 +228,15 @@ class ReaderPresenter(
}
private var hasTrackers: Boolean = false
private val checkTrackers: (DomainManga) -> Unit = { manga ->
private val checkTrackers: (Manga) -> Unit = { manga ->
val tracks = runBlocking { getTracks.await(manga.id) }
hasTrackers = tracks.isNotEmpty()
}
private val incognitoMode = preferences.incognitoMode().get()
/**
* Called when the presenter is created. It retrieves the saved active chapter if the process
* 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
override fun onCleared() {
val currentChapters = state.value.viewerChapters
if (currentChapters != null) {
currentChapters.unref()
saveReadingProgress(currentChapters.currChapter)
@ -269,24 +246,24 @@ class ReaderPresenter(
}
}
/**
* Called when the presenter instance is being saved. It saves the currently active chapter
* id and the last page read.
*/
override fun onSave(state: Bundle) {
super.onSave(state)
val currentChapter = getCurrentChapter()
if (currentChapter != null) {
currentChapter.requestedPage = currentChapter.chapter.last_page_read
state.putLong(::chapterId.name, currentChapter.chapter.id!!)
}
init {
// To save state
state.map { it.viewerChapters?.currChapter }
.distinctUntilChanged()
.onEach { currentChapter ->
if (currentChapter != null) {
currentChapter.requestedPage = currentChapter.chapter.last_page_read
chapterId = currentChapter.chapter.id!!
}
}
.launchIn(viewModelScope)
}
/**
* Called when the user pressed the back button and is going to leave the reader. Used to
* trigger deletion of the downloaded chapters.
*/
fun onBackPressed() {
fun onActivityFinish() {
deletePendingChapters()
}
@ -296,7 +273,7 @@ class ReaderPresenter(
*/
fun onSaveInstanceStateNonConfigurationChange() {
val currentChapter = getCurrentChapter() ?: return
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
saveChapterProgress(currentChapter)
}
}
@ -312,73 +289,46 @@ class ReaderPresenter(
* Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will
* fetch the manga from the database and initialize the initial chapter.
*/
fun init(mangaId: Long, initialChapterId: Long /* SY --> */, page: Int?/* SY <-- */) {
if (!needsInit()) return
coroutineScope.launchIO {
suspend fun init(mangaId: Long, initialChapterId: Long /* SY --> */, page: Int?/* SY <-- */): Result<Boolean> {
if (!needsInit()) return Result.success(true)
return withIOContext {
try {
// SY -->
val manga = getManga.await(mangaId) ?: return@launchIO
val source = sourceManager.get(manga.source)?.getMainSource<MetadataSource<*, *>>()
val metadata = if (source != null) {
getFlatMetadataById.await(mangaId)?.raise(source.metaClass)
val manga = getManga.await(mangaId)
if (manga != null) {
// SY -->
val source = sourceManager.getOrStub(manga.source)
val metadataSource = source.getMainSource<MetadataSource<*, *>>()
val metadata = if (metadataSource != null) {
getFlatMetadataById.await(mangaId)?.raise(metadataSource.metaClass)
} else {
null
}
val mergedReferences = if (source is MergedSource) runBlocking { getMergedReferencesById.await(manga.id) } else emptyList()
val mergedManga = if (source is MergedSource) runBlocking { getMergedManga.await() /* <-- TODO */ }.associateBy { it.id } else emptyMap()
// SY <--
mutableState.update { it.copy(manga = manga /* SY --> */, meta = metadata, mergedManga = mergedManga/* SY <-- */) }
if (chapterId == -1L) chapterId = initialChapterId
checkTrackers(manga)
val context = Injekt.get<Application>()
// val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source, /* SY --> */sourceManager, mergedReferences, mergedManga/* SY <-- */)
getLoadObservable(loader!!, chapterList.first { chapterId == it.chapter.id } /* SY --> */, page/* SY <-- */)
.asFlow()
.first()
Result.success(true)
} else {
null
// Unlikely but okay
Result.success(false)
}
withUIContext {
init(manga.toDbManga(), initialChapterId, metadata, page)
}
// SY <--
} catch (e: Throwable) {
view?.setInitialChapterError(e)
Result.failure(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 <--
if (chapterId == -1L) chapterId = initialChapterId
checkTrackers(manga.toDomainManga()!!)
val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source)
val mergedReferences = if (source is MergedSource) runBlocking { getMergedReferencesById.await(manga.id!!) } else emptyList()
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)
viewerChaptersRelay.subscribeLatestCache(ReaderActivity::setChapters)
coroutineScope.launch {
isLoadingAdjacentChapterEvent.receiveAsFlow().collectLatest {
view?.setProgressDialog(it)
}
}
// 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 -->
fun getChapters(context: Context): List<ReaderChapterItem> {
val currentChapter = getCurrentChapter()
@ -391,7 +341,7 @@ class ReaderPresenter(
return chapterList.map {
ReaderChapterItem(
it.chapter.toDomainChapter()!!,
manga!!.toDomainManga()!!,
manga!!,
it.chapter.id == currentChapter?.chapter?.id,
context,
UiPreferences.dateFormat(uiPreferences.dateFormat().get()),
@ -429,14 +379,14 @@ class ReaderPresenter(
)
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { newChapters ->
val oldChapters = viewerChaptersRelay.value
mutableState.update {
// Add new references first to avoid unnecessary recycling
newChapters.ref()
it.viewerChapters?.unref()
// Add new references first to avoid unnecessary recycling
newChapters.ref()
oldChapters?.unref()
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
viewerChaptersRelay.call(newChapters)
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
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.
* 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
logcat { "Loading ${chapter.chapter.url}" }
activeChapterSubscription?.unsubscribe()
activeChapterSubscription = getLoadObservable(loader, chapter)
.toCompletable()
.onErrorComplete()
.subscribe()
.also(::add)
withIOContext {
getLoadObservable(loader, chapter)
.asFlow()
.catch { logcat(LogPriority.ERROR, it) }
.first()
}
}
fun loadNewChapterFromDialog(chapter: DomainChapter) {
suspend fun loadNewChapterFromDialog(chapter: Chapter) {
val newChapter = chapterList.firstOrNull { it.chapter.id == chapter.id } ?: return
loadAdjacent(newChapter)
}
@ -467,37 +417,32 @@ class ReaderPresenter(
* sets the [isLoadingAdjacentChapterRelay] that the view uses to prevent any further
* interaction until the chapter is loaded.
*/
private fun loadAdjacent(chapter: ReaderChapter) {
private suspend fun loadAdjacent(chapter: ReaderChapter) {
val loader = loader ?: return
logcat { "Loading adjacent ${chapter.chapter.url}" }
activeChapterSubscription?.unsubscribe()
activeChapterSubscription = getLoadObservable(loader, chapter)
.doOnSubscribe { coroutineScope.launch { isLoadingAdjacentChapterEvent.send(true) } }
.doOnUnsubscribe { coroutineScope.launch { isLoadingAdjacentChapterEvent.send(false) } }
.subscribeFirst(
{ view, _ ->
view.moveToPageIndex(0)
},
{ _, _ ->
// Ignore onError event, viewers handle that state
},
)
mutableState.update { it.copy(isLoadingAdjacentChapter = true) }
withIOContext {
getLoadObservable(loader, chapter)
.asFlow()
.first()
}
mutableState.update { it.copy(isLoadingAdjacentChapter = false) }
}
/**
* 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.
*/
private fun preload(chapter: ReaderChapter) {
private suspend fun preload(chapter: ReaderChapter) {
if (chapter.pageLoader is HttpPageLoader) {
val manga = manga ?: return
val dbChapter = chapter.chapter
val isDownloaded = downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
/* SY --> */ manga.originalTitle /* SY <-- */,
/* SY --> */ manga.ogTitle /* SY <-- */,
manga.source,
skipCache = true,
)
@ -513,13 +458,14 @@ class ReaderPresenter(
logcat { "Preloading ${chapter.chapter.url}" }
val loader = loader ?: return
loader.loadChapter(chapter)
.observeOn(AndroidSchedulers.mainThread())
// Update current chapters whenever a chapter is preloaded
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
.onErrorComplete()
.subscribe()
.also(::add)
withIOContext {
loader.loadChapter(chapter)
.doOnCompleted { eventChannel.trySend(Event.ReloadViewerChapters) }
.onErrorComplete()
.toObservable<Unit>()
.asFlow()
.firstOrNull()
}
}
/**
@ -528,7 +474,7 @@ class ReaderPresenter(
* [page]'s chapter is different from the currently active.
*/
fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) {
val currentChapters = viewerChaptersRelay.value ?: return
val currentChapters = state.value.viewerChapters ?: return
val selectedChapter = page.chapter
@ -547,7 +493,7 @@ class ReaderPresenter(
selectedChapter.chapter.read = true
// SY -->
if (manga?.isEhBasedManga() == true) {
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
chapterList
.filter { it.chapter.source_order > selectedChapter.chapter.source_order }
.onEach {
@ -565,7 +511,7 @@ class ReaderPresenter(
logcat { "Setting ${selectedChapter.chapter.url} as active" }
saveReadingProgress(currentChapters.currChapter)
setReadStartTime()
loadNewChapter(selectedChapter)
viewModelScope.launch { loadNewChapter(selectedChapter) }
}
val pages = page.chapter.pages ?: return
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
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(
nextChapter.name,
nextChapter.scanlator,
// SY -->
manga.originalTitle,
manga.ogTitle,
// SY <--
manga.source,
)
if (!isNextChapterDownloaded) return@launchIO
val chaptersToDownload = getNextChapters.await(manga.id!!, nextChapter.id!!)
val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!)
.take(amount)
downloadManager.downloadChapters(
manga.toDomainManga()!!,
manga,
chaptersToDownload,
)
}
@ -641,7 +587,7 @@ class ReaderPresenter(
* Called when reader chapter is changed in reader or when activity is paused.
*/
private fun saveReadingProgress(readerChapter: ReaderChapter) {
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
saveChapterProgress(readerChapter)
saveChapterHistory(readerChapter)
}
@ -689,23 +635,23 @@ class ReaderPresenter(
/**
* Called from the activity to preload the given [chapter].
*/
fun preloadChapter(chapter: ReaderChapter) {
suspend fun preloadChapter(chapter: ReaderChapter) {
preload(chapter)
}
/**
* Called from the activity to load and set the next chapter as active.
*/
fun loadNextChapter() {
val nextChapter = viewerChaptersRelay.value?.nextChapter ?: return
suspend fun loadNextChapter() {
val nextChapter = state.value.viewerChapters?.nextChapter ?: return
loadAdjacent(nextChapter)
}
/**
* Called from the activity to load and set the previous chapter as active.
*/
fun loadPreviousChapter() {
val prevChapter = viewerChaptersRelay.value?.prevChapter ?: return
suspend fun loadPreviousChapter() {
val prevChapter = state.value.viewerChapters?.prevChapter ?: return
loadAdjacent(prevChapter)
}
@ -713,7 +659,7 @@ class ReaderPresenter(
* Returns the currently active chapter.
*/
fun getCurrentChapter(): ReaderChapter? {
return viewerChaptersRelay.value?.currChapter
return state.value.viewerChapters?.currChapter
}
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
@ -731,7 +677,7 @@ class ReaderPresenter(
fun bookmarkCurrentChapter(bookmarked: Boolean) {
val chapter = getCurrentChapter()?.chapter ?: return
chapter.bookmark = bookmarked // Otherwise the bookmark icon doesn't update
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
updateChapter.await(
ChapterUpdate(
id = chapter.id!!.toLong(),
@ -745,7 +691,7 @@ class ReaderPresenter(
fun toggleBookmark(chapterId: Long, bookmarked: Boolean) {
val chapter = chapterList.find { it.chapter.id == chapterId }?.chapter ?: return
chapter.bookmark = bookmarked
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
updateChapter.await(
ChapterUpdate(
id = chapter.id!!.toLong(),
@ -762,7 +708,7 @@ class ReaderPresenter(
fun getMangaReadingMode(resolveDefault: Boolean = true): Int {
val default = readerPreferences.defaultReadingMode().get()
val manga = manga ?: return default
val readingMode = ReadingModeType.fromPreference(manga.readingModeType)
val readingMode = ReadingModeType.fromPreference(manga.readingModeType.toInt())
// SY -->
return when {
resolveDefault && readingMode == ReadingModeType.DEFAULT && readerPreferences.useAutoWebtoon().get() -> {
@ -770,7 +716,7 @@ class ReaderPresenter(
?: default
}
resolveDefault && readingMode == ReadingModeType.DEFAULT -> default
else -> manga.readingModeType
else -> manga.readingModeType.toInt()
}
// SY <--
}
@ -780,22 +726,21 @@ class ReaderPresenter(
*/
fun setMangaReadingMode(readingModeType: Int) {
val manga = manga ?: return
manga.readingModeType = readingModeType
coroutineScope.launchIO {
setMangaViewerFlags.awaitSetMangaReadingMode(manga.id!!.toLong(), readingModeType.toLong())
delay(250)
val currChapters = viewerChaptersRelay.value
viewModelScope.launchIO {
setMangaViewerFlags.awaitSetMangaReadingMode(manga.id, readingModeType.toLong())
val currChapters = state.value.viewerChapters
if (currChapters != null) {
// Save current page
val currChapter = currChapters.currChapter
currChapter.requestedPage = currChapter.chapter.last_page_read
withUIContext {
// Emit manga and chapters to the new viewer
view?.setManga(manga)
view?.setChapters(currChapters)
mutableState.update {
it.copy(
manga = getManga.await(manga.id),
viewerChapters = currChapters,
)
}
eventChannel.send(Event.ReloadViewerChapters)
}
}
}
@ -805,10 +750,10 @@ class ReaderPresenter(
*/
fun getMangaOrientationType(resolveDefault: Boolean = true): Int {
val default = readerPreferences.defaultOrientationType().get()
val orientation = OrientationType.fromPreference(manga?.orientationType)
val orientation = OrientationType.fromPreference(manga?.orientationType?.toInt())
return when {
resolveDefault && orientation == OrientationType.DEFAULT -> default
else -> manga?.orientationType ?: default
else -> manga?.orientationType?.toInt() ?: default
}
}
@ -817,14 +762,22 @@ class ReaderPresenter(
*/
fun setMangaOrientationType(rotationType: Int) {
val manga = manga ?: return
manga.orientationType = rotationType
coroutineScope.launchIO {
setMangaViewerFlags.awaitSetOrientationType(manga.id!!.toLong(), rotationType.toLong())
delay(250)
val currChapters = viewerChaptersRelay.value
viewModelScope.launchIO {
setMangaViewerFlags.awaitSetOrientationType(manga.id, rotationType.toLong())
val currChapters = state.value.viewerChapters
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 ""
// Copy file in background.
try {
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
try {
val uri = imageSaver.save(
image = Image.Page(
inputStream = page.stream!!,
@ -872,12 +825,12 @@ class ReaderPresenter(
)
withUIContext {
notifier.onComplete(uri)
view?.onSaveImageResult(SaveImageResult.Success(uri))
eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri)))
}
} catch (e: Throwable) {
notifier.onError(e.message)
eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
}
} catch (e: Throwable) {
notifier.onError(e.message)
view?.onSaveImageResult(SaveImageResult.Error(e))
}
}
@ -895,8 +848,8 @@ class ReaderPresenter(
val relativePath = if (readerPreferences.folderPerManga().get()) DiskUtil.buildValidFilename(manga.title) else ""
// Copy file in background.
try {
coroutineScope.launchIO {
viewModelScope.launchNonCancellable {
try {
val uri = saveImages(
page1 = firstPage,
page2 = secondPage,
@ -905,14 +858,11 @@ class ReaderPresenter(
location = Location.Pictures.create(relativePath),
manga = manga,
)
withUIContext {
notifier.onComplete(uri)
view!!.onSaveImageResult(SaveImageResult.Success(uri))
}
eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri)))
} catch (e: Throwable) {
notifier.onError(e.message)
eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
}
} catch (e: Throwable) {
notifier.onError(e.message)
view!!.onSaveImageResult(SaveImageResult.Error(e))
}
}
@ -966,7 +916,7 @@ class ReaderPresenter(
val filename = generateFilename(manga, page)
try {
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
destDir.deleteRecursively()
val uri = imageSaver.save(
image = Image.Page(
@ -975,9 +925,7 @@ class ReaderPresenter(
location = Location.Cache,
),
)
withUIContext {
view?.onShareImageResult(uri, page)
}
eventChannel.send(Event.ShareImage(uri, page))
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
@ -994,7 +942,7 @@ class ReaderPresenter(
val destDir = context.cacheImageDir
try {
coroutineScope.launchIO {
viewModelScope.launchNonCancellable {
destDir.deleteRecursively()
val uri = saveImages(
page1 = firstPage,
@ -1004,9 +952,7 @@ class ReaderPresenter(
location = Location.Cache,
manga = manga,
)
withUIContext {
view!!.onShareImageResult(uri, firstPage, secondPage)
}
eventChannel.send(Event.ShareImage(uri, firstPage, secondPage))
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
@ -1019,24 +965,21 @@ class ReaderPresenter(
*/
fun setAsCover(context: Context, page: ReaderPage) {
if (page.status != Page.State.READY) return
val manga = manga?.toDomainManga() ?: return
val manga = manga ?: return
val stream = page.stream ?: return
coroutineScope.launchNonCancellable {
try {
viewModelScope.launchNonCancellable {
val result = try {
manga.editCover(context, stream())
withUIContext {
view?.onSetAsCoverResult(
if (manga.isLocal() || manga.favorite) {
SetAsCoverResult.Success
} else {
SetAsCoverResult.AddToLibraryFirst
},
)
if (manga.isLocal() || manga.favorite) {
SetAsCoverResult.Success
} else {
SetAsCoverResult.AddToLibraryFirst
}
} 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 context = Injekt.get<Application>()
coroutineScope.launchNonCancellable {
getTracks.await(manga.id!!)
viewModelScope.launchNonCancellable {
getTracks.await(manga.id)
.mapNotNull { track ->
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 <-- */) {
@ -1106,16 +1049,17 @@ class ReaderPresenter(
*/
private fun enqueueDeleteReadChapters(chapter: ReaderChapter) {
if (!chapter.chapter.read) return
val mergedManga = state.value.mergedManga
// SY -->
val manga = if (mergedManga.isNullOrEmpty()) {
manga
} else {
mergedManga.orEmpty()[chapter.chapter.manga_id]?.toDbManga()
mergedManga[chapter.chapter.manga_id]
} ?: return
// SY <--
coroutineScope.launchNonCancellable {
downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga.toDomainManga()!!)
viewModelScope.launchNonCancellable {
downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga)
}
}
@ -1124,35 +1068,30 @@ class ReaderPresenter(
* are ignored.
*/
private fun deletePendingChapters() {
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
downloadManager.deletePendingChapters()
}
}
// We're trying to avoid using Rx, so we "undeprecate" this
@Suppress("DEPRECATION")
override fun getView(): ReaderActivity? {
return super.getView()
data class State(
val manga: Manga? = null,
val viewerChapters: ViewerChapters? = null,
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 {
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)
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.kanade.domain.chapter.model.Chapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.databinding.ReaderChaptersDialogBinding
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.system.dpToPx
import kotlinx.coroutines.launch
@ -18,7 +17,7 @@ import kotlinx.coroutines.launch
class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterAdapter.OnBookmarkClickListener {
private val binding = ReaderChaptersDialogBinding.inflate(activity.layoutInflater)
var presenter: ReaderPresenter = activity.presenter
var viewModel: ReaderViewModel = activity.viewModel
var adapter: FlexibleAdapter<ReaderChapterItem>? = null
var dialog: AlertDialog
@ -35,9 +34,11 @@ class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterA
adapter?.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, 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()
presenter.loadNewChapterFromDialog(item.chapter)
activity.lifecycleScope.launch {
viewModel.loadNewChapterFromDialog(item.chapter)
}
}
true
}
@ -51,8 +52,8 @@ class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterA
}
private fun refreshList(scroll: Boolean = true) {
val chapterSort = getChapterSort(presenter.manga!!.toDomainManga()!!)
val chapters = presenter.getChapters(activity)
val chapterSort = getChapterSort(viewModel.manga!!)
val chapters = viewModel.getChapters(activity)
.sortedWith { a, b ->
chapterSort(a.chapter, b.chapter)
}
@ -73,7 +74,7 @@ class ReaderChapterDialog(private val activity: ReaderActivity) : ReaderChapterA
}
override fun bookmarkChapter(chapter: Chapter) {
presenter.toggleBookmark(chapter.id, !chapter.bookmark)
viewModel.toggleBookmark(chapter.id, !chapter.bookmark)
refreshList(scroll = false)
}
}

View File

@ -44,22 +44,22 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
private fun initGeneralPreferences() {
binding.viewer.onItemSelectedListener = { 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) {
initWebtoonPreferences()
} else {
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 ->
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.inSpans
import androidx.core.view.isVisible
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
@ -55,7 +55,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
val isPrevDownloaded = downloadManager.isChapterDownloaded(
prevChapter.name,
prevChapter.scanlator,
/* SY --> */ manga.originalTitle /* SY <-- */,
/* SY --> */ manga.ogTitle /* SY <-- */,
manga.source,
skipCache = true,
)
@ -93,7 +93,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
val isNextDownloaded = downloadManager.isChapterDownloaded(
nextChapter.name,
nextChapter.scanlator,
/* SY --> */ manga.originalTitle /* SY <-- */,
/* SY --> */ manga.ogTitle /* SY <-- */,
manga.source,
skipCache = true,
)

View File

@ -62,7 +62,7 @@ class PagerTransitionHolder(
addView(transitionView)
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) }
}

View File

@ -64,7 +64,7 @@ class WebtoonTransitionHolder(
* Binds the given [transition] with this view holder, subscribing to its state.
*/
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) }
}

View File

@ -1,14 +1,13 @@
package exh.util
import android.content.Context
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
import eu.kanade.domain.manga.model.Manga as DomainManga
fun Manga.mangaType(context: Context): String {
return context.getString(
@ -26,30 +25,6 @@ fun Manga.mangaType(context: Context): String {
* The type of comic the manga is (ie. manga, manhwa, manhua)
*/
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()
return when {
currentTags.any { tag -> isMangaTag(tag) } -> {

View File

@ -1,7 +1,6 @@
[versions]
aboutlib_version = "10.5.2"
okhttp_version = "5.0.0-alpha.10"
nucleus_version = "3.0.0"
coil_version = "2.2.2"
shizuku_version = "12.2.0"
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"
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"
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"]
js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
nucleus = ["nucleus-core", "nucleus-supportv7"]
coil = ["coil-core", "coil-gif", "coil-compose"]
shizuku = ["shizuku-api", "shizuku-provider"]
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]