From f7a92cf6ac58cae26b09b02578318e12cd888f4c Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Thu, 8 Dec 2022 11:00:01 +0700 Subject: [PATCH] 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 --- .github/renovate.json | 2 - app/build.gradle.kts | 3 - .../eu/kanade/domain/manga/model/Manga.kt | 33 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 174 +++++--- ...{ReaderPresenter.kt => ReaderViewModel.kt} | 417 ++++++++---------- .../setting/ReaderReadingModeSettings.kt | 10 +- .../ui/reader/viewer/ReaderTransitionView.kt | 2 +- .../viewer/pager/PagerTransitionHolder.kt | 2 +- .../viewer/webtoon/WebtoonTransitionHolder.kt | 2 +- gradle/libs.versions.toml | 5 - 10 files changed, 318 insertions(+), 332 deletions(-) rename app/src/main/java/eu/kanade/tachiyomi/ui/reader/{ReaderPresenter.kt => ReaderViewModel.kt} (70%) diff --git a/.github/renovate.json b/.github/renovate.json index 8682663102..8a719f2457 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -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" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6bac7251bd..0ab668f39c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -239,9 +239,6 @@ dependencies { // Preferences implementation(libs.preferencektx) - // Model View Presenter - implementation(libs.bundles.nucleus) - // Dependency injection implementation(libs.injekt.core) diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt index a5e935599a..785f57acf5 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 561affa1d1..2a45356a2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -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(), + AppCompatActivity(), SecureActivityDelegate by SecureActivityDelegateImpl(), ThemingDelegate by ThemingDelegateImpl() { @@ -128,6 +133,8 @@ class ReaderActivity : lateinit var binding: ReaderActivityBinding + val viewModel by viewModels() + 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)) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt similarity index 70% rename from app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 296031dae7..feddc07116 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -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() { +) : ViewModel() { - private val coroutineScope: CoroutineScope = MainScope() + private val mutableState = MutableStateFlow(State()) + val state = mutableState.asStateFlow() + + private val eventChannel = Channel() + 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("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() - - /** - * Used when loading prev/next chapter needed to lock the UI (with a dialog). - */ - private val isLoadingAdjacentChapterEvent = Channel() - 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 { + 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() + 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() - 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() + .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() val context = Injekt.get() - 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 Observable.subscribeFirst(onNext: (ReaderActivity, T) -> Unit, onError: ((ReaderActivity, Throwable) -> Unit) = { _, _ -> }) = compose(deliverFirst()).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 Observable.subscribeLatestCache(onNext: (ReaderActivity, T) -> Unit, onError: ((ReaderActivity, Throwable) -> Unit) = { _, _ -> }) = compose(deliverLatestCache()).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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt index 474b876d61..8ab184dea8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt @@ -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) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index 8dfd3b6d06..06bf08927c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 464b0f0f25..d69ac451d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index e8e0715734..5dd0b2880f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -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) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d8cfa1a2d..3497bd8242 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"]