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>
This commit is contained in:
Ivan Iskandar 2022-12-08 11:00:01 +07:00 committed by GitHub
parent e748d91d4a
commit f7a92cf6ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 318 additions and 332 deletions

View File

@ -6,8 +6,6 @@
"ignoreDeps": [
"androidx.core:core-splashscreen",
"androidx.work:work-runtime-ktx",
"info.android15.nucleus:nucleus-support-v7",
"info.android15.nucleus:nucleus",
"com.android.tools:r8",
"com.google.guava:guava",
"com.github.commandiron:WheelPickerCompose"

View File

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

View File

@ -1,17 +1,16 @@
package eu.kanade.domain.manga.model
import eu.kanade.data.listOfStringsAdapter
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.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 java.io.Serializable
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
data class Manga(
val id: Long,
@ -49,6 +48,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
@ -187,28 +192,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
it.title = title
it.artist = artist
it.author = author
it.description = description
it.genre = genre?.let(listOfStringsAdapter::encode)
it.status = status.toInt()
it.thumbnail_url = thumbnailUrl
it.update_strategy = updateStrategy
it.initialized = initialized
}
fun Manga.toMangaUpdate(): MangaUpdate {
return MangaUpdate(
id = id,

View File

@ -29,6 +29,8 @@ import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.FrameLayout
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.ColorUtils
import androidx.core.transition.doOnEnd
import androidx.core.view.WindowCompat
@ -45,9 +47,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
@ -56,9 +58,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.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
@ -71,6 +73,8 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
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
@ -85,14 +89,16 @@ import eu.kanade.tachiyomi.util.view.copy
import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
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.merge
import kotlinx.coroutines.flow.onEach
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
@ -101,9 +107,8 @@ import kotlin.math.max
* 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() {
@ -128,6 +133,8 @@ class ReaderActivity :
lateinit var binding: ReaderActivityBinding
val viewModel by viewModels<ReaderViewModel>()
val hasCutout by lazy { hasDisplayCutout() }
/**
@ -194,7 +201,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)
if (manga == -1L || chapter == -1L) {
@ -202,7 +209,16 @@ class ReaderActivity :
return
}
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
presenter.init(manga, chapter)
lifecycleScope.launchNonCancellable {
val initResult = viewModel.init(manga, chapter)
if (!initResult.getOrDefault(false)) {
val exception = initResult.exceptionOrNull() ?: IllegalStateException("Unknown err")
withUIContext {
setInitialChapterError(exception)
}
}
}
}
if (savedInstanceState != null) {
@ -217,6 +233,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)
}
is ReaderViewModel.Event.SetCoverResult -> {
onSetAsCoverResult(event.result)
}
}
}
.launchIn(lifecycleScope)
}
/**
@ -240,13 +298,13 @@ class ReaderActivity :
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(::menuVisible.name, menuVisible)
if (!isChangingConfigurations) {
presenter.onSaveInstanceStateNonConfigurationChange()
viewModel.onSaveInstanceStateNonConfigurationChange()
}
super.onSaveInstanceState(outState)
}
override fun onPause() {
presenter.saveCurrentChapterReadingProgress()
viewModel.saveCurrentChapterReadingProgress()
super.onPause()
}
@ -256,7 +314,7 @@ class ReaderActivity :
*/
override fun onResume() {
super.onResume()
presenter.setReadStartTime()
viewModel.setReadStartTime()
setMenuVisibility(menuVisible, animate = false)
}
@ -277,7 +335,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
@ -294,11 +352,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()
}
}
@ -309,17 +367,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)
@ -356,7 +414,7 @@ class ReaderActivity :
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
onBackPressedDispatcher.onBackPressed()
}
binding.toolbar.applyInsetter {
@ -371,7 +429,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
@ -461,11 +519,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()) {
@ -482,7 +540,7 @@ class ReaderActivity :
setTooltip(R.string.pref_crop_borders)
setOnClickListener {
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaReadingMode())
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val enabled = if (isPagerType) {
readerPreferences.cropBorders().toggle()
} else {
@ -514,12 +572,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)
@ -550,7 +608,7 @@ class ReaderActivity :
}
private fun updateCropBordersShortcut() {
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaReadingMode())
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val enabled = if (isPagerType) {
readerPreferences.cropBorders().get()
} else {
@ -633,19 +691,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
@ -658,10 +716,10 @@ class ReaderActivity :
binding.viewerContainer.addView(newViewer.getView())
if (readerPreferences.showReadingMode().get()) {
showReadingModeToast(presenter.getMangaReadingMode())
showReadingModeToast(viewModel.getMangaReadingMode())
}
binding.toolbar.title = manga.title
supportActionBar?.title = manga.title
binding.pageSlider.isRTL = newViewer is R2LPagerViewer
if (newViewer is R2LPagerViewer) {
@ -684,9 +742,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)
@ -707,7 +765,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)
viewer?.setChapters(viewerChapters)
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name
@ -765,7 +823,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)
}
@ -775,7 +833,10 @@ class ReaderActivity :
* should be automatically shown.
*/
private fun loadNextChapter() {
presenter.loadNextChapter()
lifecycleScope.launch {
viewModel.loadNextChapter()
moveToPageIndex(0)
}
}
/**
@ -783,7 +844,10 @@ class ReaderActivity :
* should be automatically shown.
*/
private fun loadPreviousChapter() {
presenter.loadPreviousChapter()
lifecycleScope.launch {
viewModel.loadPreviousChapter()
moveToPageIndex(0)
}
}
/**
@ -792,7 +856,7 @@ class ReaderActivity :
*/
@SuppressLint("SetTextI18n")
fun onPageSelected(page: ReaderPage) {
presenter.onPageSelected(page)
viewModel.onPageSelected(page)
val pages = page.chapter.pages ?: return
// Set bottom page number
@ -826,7 +890,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) }
}
/**
@ -860,15 +924,15 @@ 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)
}
/**
* Called from the presenter when a page is ready to be shared. It shows Android's default
* sharing tool.
*/
fun onShareImageResult(uri: Uri, page: ReaderPage) {
val manga = presenter.manga ?: return
private fun onShareImageResult(uri: Uri, page: ReaderPage) {
val manga = viewModel.manga ?: return
val chapter = page.chapter.chapter
val intent = uri.toShareIntent(
@ -883,19 +947,19 @@ class ReaderActivity :
* storage to the presenter.
*/
fun saveImage(page: ReaderPage) {
presenter.saveImage(page)
viewModel.saveImage(page)
}
/**
* 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)
}
}
@ -906,14 +970,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
@ -926,12 +990,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,8 +3,10 @@ package eu.kanade.tachiyomi.ui.reader
import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Bundle
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.UpdateChapter
@ -16,17 +18,15 @@ import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.manga.interactor.GetManga
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
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.track.store.DelayedTrackingStore
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
@ -54,37 +54,41 @@ 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
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.logcat
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
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(),
@ -102,20 +106,28 @@ class ReaderPresenter(
private val upsertHistory: UpsertHistory = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(),
) : 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
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.
@ -132,16 +144,6 @@ class ReaderPresenter(
*/
private var activeChapterSubscription: Subscription? = null
/**
* Relay for currently active viewer chapters.
*/
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
/**
@ -149,7 +151,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 { getChapterByMangaId.await(manga.id) }
val selectedChapter = chapters.find { it.id == chapterId }
@ -161,12 +163,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, manga.title, manga.source)) ||
(manga.downloadedFilterRaw == DomainManga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded(it.name, it.scanlator, manga.title, 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, manga.title, manga.source)) ||
(manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source)) ||
(manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) ||
(manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark)
}
else -> false
}
@ -188,32 +190,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)
@ -223,24 +208,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()
}
@ -250,7 +235,7 @@ class ReaderPresenter(
*/
fun onSaveInstanceStateNonConfigurationChange() {
val currentChapter = getCurrentChapter() ?: return
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
saveChapterProgress(currentChapter)
}
}
@ -266,60 +251,35 @@ 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) {
if (!needsInit()) return
coroutineScope.launchIO {
suspend fun init(mangaId: Long, initialChapterId: Long): Result<Boolean> {
if (!needsInit()) return Result.success(true)
return withIOContext {
try {
val manga = getManga.await(mangaId)
withUIContext {
manga?.let { init(it.toDbManga(), initialChapterId) }
if (manga != null) {
mutableState.update { it.copy(manga = manga) }
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)
getLoadObservable(loader!!, chapterList.first { chapterId == it.chapter.id })
.asFlow()
.first()
Result.success(true)
} else {
// Unlikely but okay
Result.success(false)
}
} 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) {
if (!needsInit()) return
this.manga = manga
if (chapterId == -1L) chapterId = initialChapterId
checkTrackers(manga.toDomainManga()!!)
val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga.toDomainManga()!!, source)
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) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ _, _ ->
// Ignore onNext event
},
ReaderActivity::setInitialChapterError,
)
}
/**
* Returns an observable that loads the given [chapter] with this [loader]. This observable
* handles main thread synchronization and updating the currently active chapters on
@ -345,14 +305,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)
}
}
}
@ -360,17 +320,17 @@ 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()
}
}
/**
@ -378,30 +338,25 @@ 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
@ -424,13 +379,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()
}
}
/**
@ -439,7 +395,7 @@ class ReaderPresenter(
* [page]'s chapter is different from the currently active.
*/
fun onPageSelected(page: ReaderPage) {
val currentChapters = viewerChaptersRelay.value ?: return
val currentChapters = state.value.viewerChapters ?: return
val selectedChapter = page.chapter
@ -461,7 +417,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
@ -477,9 +433,9 @@ 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,
@ -488,10 +444,10 @@ class ReaderPresenter(
)
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,
)
}
@ -535,7 +491,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)
}
@ -583,23 +539,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)
}
@ -607,7 +563,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
@ -625,7 +581,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(),
@ -640,10 +596,10 @@ class ReaderPresenter(
*/
fun getMangaReadingMode(resolveDefault: Boolean = true): Int {
val default = readerPreferences.defaultReadingMode().get()
val readingMode = ReadingModeType.fromPreference(manga?.readingModeType)
val readingMode = ReadingModeType.fromPreference(manga?.readingModeType?.toInt())
return when {
resolveDefault && readingMode == ReadingModeType.DEFAULT -> default
else -> manga?.readingModeType ?: default
else -> manga?.readingModeType?.toInt() ?: default
}
}
@ -652,22 +608,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)
}
}
}
@ -677,10 +632,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
}
}
@ -689,14 +644,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)
}
}
}
@ -733,8 +696,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!!,
@ -744,12 +707,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))
}
}
@ -770,7 +733,7 @@ class ReaderPresenter(
val filename = generateFilename(manga, page)
try {
coroutineScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
destDir.deleteRecursively()
val uri = imageSaver.save(
image = Image.Page(
@ -779,9 +742,7 @@ class ReaderPresenter(
location = Location.Cache,
),
)
withUIContext {
view?.onShareImageResult(uri, page)
}
eventChannel.send(Event.ShareImage(uri, page))
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
@ -793,24 +754,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))
}
}
@ -842,8 +800,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) {
@ -882,8 +840,8 @@ class ReaderPresenter(
if (!chapter.chapter.read) return
val manga = manga ?: return
coroutineScope.launchNonCancellable {
downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga.toDomainManga()!!)
viewModelScope.launchNonCancellable {
downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga)
}
}
@ -892,35 +850,26 @@ 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,
)
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) : 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

@ -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

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,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"]