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:
parent
e748d91d4a
commit
f7a92cf6ac
10 changed files with 318 additions and 332 deletions
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
|
@ -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"
|
||||
|
|
|
@ -239,9 +239,6 @@ dependencies {
|
|||
// Preferences
|
||||
implementation(libs.preferencektx)
|
||||
|
||||
// Model View Presenter
|
||||
implementation(libs.bundles.nucleus)
|
||||
|
||||
// Dependency injection
|
||||
implementation(libs.injekt.core)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
Reference in a new issue