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
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@ -6,8 +6,6 @@
|
|||||||
"ignoreDeps": [
|
"ignoreDeps": [
|
||||||
"androidx.core:core-splashscreen",
|
"androidx.core:core-splashscreen",
|
||||||
"androidx.work:work-runtime-ktx",
|
"androidx.work:work-runtime-ktx",
|
||||||
"info.android15.nucleus:nucleus-support-v7",
|
|
||||||
"info.android15.nucleus:nucleus",
|
|
||||||
"com.android.tools:r8",
|
"com.android.tools:r8",
|
||||||
"com.google.guava:guava",
|
"com.google.guava:guava",
|
||||||
"com.github.commandiron:WheelPickerCompose"
|
"com.github.commandiron:WheelPickerCompose"
|
||||||
|
@ -239,9 +239,6 @@ dependencies {
|
|||||||
// Preferences
|
// Preferences
|
||||||
implementation(libs.preferencektx)
|
implementation(libs.preferencektx)
|
||||||
|
|
||||||
// Model View Presenter
|
|
||||||
implementation(libs.bundles.nucleus)
|
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt.core)
|
||||||
|
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
package eu.kanade.domain.manga.model
|
package eu.kanade.domain.manga.model
|
||||||
|
|
||||||
import eu.kanade.data.listOfStringsAdapter
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
|
|
||||||
|
|
||||||
data class Manga(
|
data class Manga(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@ -49,6 +48,12 @@ data class Manga(
|
|||||||
val bookmarkedFilterRaw: Long
|
val bookmarkedFilterRaw: Long
|
||||||
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
|
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
|
||||||
|
|
||||||
|
val readingModeType: Long
|
||||||
|
get() = viewerFlags and ReadingModeType.MASK.toLong()
|
||||||
|
|
||||||
|
val orientationType: Long
|
||||||
|
get() = viewerFlags and OrientationType.MASK.toLong()
|
||||||
|
|
||||||
val unreadFilter: TriStateFilter
|
val unreadFilter: TriStateFilter
|
||||||
get() = when (unreadFilterRaw) {
|
get() = when (unreadFilterRaw) {
|
||||||
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
|
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
|
||||||
@ -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 {
|
fun Manga.toMangaUpdate(): MangaUpdate {
|
||||||
return MangaUpdate(
|
return MangaUpdate(
|
||||||
id = id,
|
id = id,
|
||||||
|
@ -29,6 +29,8 @@ import android.view.animation.Animation
|
|||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.transition.doOnEnd
|
import androidx.core.transition.doOnEnd
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
@ -45,9 +47,9 @@ import com.google.android.material.transition.platform.MaterialContainerTransfor
|
|||||||
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
||||||
@ -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.ThemingDelegate
|
||||||
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegateImpl
|
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegateImpl
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
|
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.AddToLibraryFirst
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
|
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Error
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
|
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Success
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
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.reader.viewer.pager.R2LPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.Constants
|
import eu.kanade.tachiyomi.util.Constants
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.preference.toggle
|
import eu.kanade.tachiyomi.util.preference.toggle
|
||||||
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||||
@ -85,14 +89,16 @@ import eu.kanade.tachiyomi.util.view.copy
|
|||||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||||
import eu.kanade.tachiyomi.util.view.setTooltip
|
import eu.kanade.tachiyomi.util.view.setTooltip
|
||||||
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nucleus.factory.RequiresPresenter
|
|
||||||
import nucleus.view.NucleusAppCompatActivity
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@ -101,9 +107,8 @@ import kotlin.math.max
|
|||||||
* Activity containing the reader of Tachiyomi. This activity is mostly a container of the
|
* Activity containing the reader of Tachiyomi. This activity is mostly a container of the
|
||||||
* viewers, to which calls from the presenter or UI events are delegated.
|
* viewers, to which calls from the presenter or UI events are delegated.
|
||||||
*/
|
*/
|
||||||
@RequiresPresenter(ReaderPresenter::class)
|
|
||||||
class ReaderActivity :
|
class ReaderActivity :
|
||||||
NucleusAppCompatActivity<ReaderPresenter>(),
|
AppCompatActivity(),
|
||||||
SecureActivityDelegate by SecureActivityDelegateImpl(),
|
SecureActivityDelegate by SecureActivityDelegateImpl(),
|
||||||
ThemingDelegate by ThemingDelegateImpl() {
|
ThemingDelegate by ThemingDelegateImpl() {
|
||||||
|
|
||||||
@ -128,6 +133,8 @@ class ReaderActivity :
|
|||||||
|
|
||||||
lateinit var binding: ReaderActivityBinding
|
lateinit var binding: ReaderActivityBinding
|
||||||
|
|
||||||
|
val viewModel by viewModels<ReaderViewModel>()
|
||||||
|
|
||||||
val hasCutout by lazy { hasDisplayCutout() }
|
val hasCutout by lazy { hasDisplayCutout() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -194,7 +201,7 @@ class ReaderActivity :
|
|||||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
binding = ReaderActivityBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
if (presenter.needsInit()) {
|
if (viewModel.needsInit()) {
|
||||||
val manga = intent.extras!!.getLong("manga", -1)
|
val manga = intent.extras!!.getLong("manga", -1)
|
||||||
val chapter = intent.extras!!.getLong("chapter", -1)
|
val chapter = intent.extras!!.getLong("chapter", -1)
|
||||||
if (manga == -1L || chapter == -1L) {
|
if (manga == -1L || chapter == -1L) {
|
||||||
@ -202,7 +209,16 @@ class ReaderActivity :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
|
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
|
||||||
presenter.init(manga, chapter)
|
|
||||||
|
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) {
|
if (savedInstanceState != null) {
|
||||||
@ -217,6 +233,48 @@ class ReaderActivity :
|
|||||||
.drop(1)
|
.drop(1)
|
||||||
.onEach { if (!it) finish() }
|
.onEach { if (!it) finish() }
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
viewModel.state
|
||||||
|
.map { it.isLoadingAdjacentChapter }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach(::setProgressDialog)
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
viewModel.state
|
||||||
|
.map { it.manga }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.filterNotNull()
|
||||||
|
.onEach(::setManga)
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
viewModel.state
|
||||||
|
.map { it.viewerChapters }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.filterNotNull()
|
||||||
|
.onEach(::setChapters)
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
viewModel.eventFlow
|
||||||
|
.onEach { event ->
|
||||||
|
when (event) {
|
||||||
|
ReaderViewModel.Event.ReloadViewerChapters -> {
|
||||||
|
viewModel.state.value.viewerChapters?.let(::setChapters)
|
||||||
|
}
|
||||||
|
is ReaderViewModel.Event.SetOrientation -> {
|
||||||
|
setOrientation(event.orientation)
|
||||||
|
}
|
||||||
|
is ReaderViewModel.Event.SavedImage -> {
|
||||||
|
onSaveImageResult(event.result)
|
||||||
|
}
|
||||||
|
is ReaderViewModel.Event.ShareImage -> {
|
||||||
|
onShareImageResult(event.uri, event.page)
|
||||||
|
}
|
||||||
|
is ReaderViewModel.Event.SetCoverResult -> {
|
||||||
|
onSetAsCoverResult(event.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -240,13 +298,13 @@ class ReaderActivity :
|
|||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean(::menuVisible.name, menuVisible)
|
outState.putBoolean(::menuVisible.name, menuVisible)
|
||||||
if (!isChangingConfigurations) {
|
if (!isChangingConfigurations) {
|
||||||
presenter.onSaveInstanceStateNonConfigurationChange()
|
viewModel.onSaveInstanceStateNonConfigurationChange()
|
||||||
}
|
}
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
presenter.saveCurrentChapterReadingProgress()
|
viewModel.saveCurrentChapterReadingProgress()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,7 +314,7 @@ class ReaderActivity :
|
|||||||
*/
|
*/
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
presenter.setReadStartTime()
|
viewModel.setReadStartTime()
|
||||||
setMenuVisibility(menuVisible, animate = false)
|
setMenuVisibility(menuVisible, animate = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +335,7 @@ class ReaderActivity :
|
|||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
menuInflater.inflate(R.menu.reader, menu)
|
||||||
|
|
||||||
val isChapterBookmarked = presenter?.getCurrentChapter()?.chapter?.bookmark ?: false
|
val isChapterBookmarked = viewModel.getCurrentChapter()?.chapter?.bookmark ?: false
|
||||||
menu.findItem(R.id.action_bookmark).isVisible = !isChapterBookmarked
|
menu.findItem(R.id.action_bookmark).isVisible = !isChapterBookmarked
|
||||||
menu.findItem(R.id.action_remove_bookmark).isVisible = isChapterBookmarked
|
menu.findItem(R.id.action_remove_bookmark).isVisible = isChapterBookmarked
|
||||||
|
|
||||||
@ -294,11 +352,11 @@ class ReaderActivity :
|
|||||||
openChapterInWebview()
|
openChapterInWebview()
|
||||||
}
|
}
|
||||||
R.id.action_bookmark -> {
|
R.id.action_bookmark -> {
|
||||||
presenter.bookmarkCurrentChapter(true)
|
viewModel.bookmarkCurrentChapter(true)
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
R.id.action_remove_bookmark -> {
|
R.id.action_remove_bookmark -> {
|
||||||
presenter.bookmarkCurrentChapter(false)
|
viewModel.bookmarkCurrentChapter(false)
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,17 +367,17 @@ class ReaderActivity :
|
|||||||
* Called when the user clicks the back key or the button on the toolbar. The call is
|
* Called when the user clicks the back key or the button on the toolbar. The call is
|
||||||
* delegated to the presenter.
|
* delegated to the presenter.
|
||||||
*/
|
*/
|
||||||
override fun onBackPressed() {
|
override fun finish() {
|
||||||
presenter.onBackPressed()
|
viewModel.onActivityFinish()
|
||||||
super.onBackPressed()
|
super.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
if (keyCode == KeyEvent.KEYCODE_N) {
|
if (keyCode == KeyEvent.KEYCODE_N) {
|
||||||
presenter.loadNextChapter()
|
loadNextChapter()
|
||||||
return true
|
return true
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_P) {
|
} else if (keyCode == KeyEvent.KEYCODE_P) {
|
||||||
presenter.loadPreviousChapter()
|
loadPreviousChapter()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return super.onKeyUp(keyCode, event)
|
return super.onKeyUp(keyCode, event)
|
||||||
@ -356,7 +414,7 @@ class ReaderActivity :
|
|||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
binding.toolbar.setNavigationOnClickListener {
|
binding.toolbar.setNavigationOnClickListener {
|
||||||
onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.toolbar.applyInsetter {
|
binding.toolbar.applyInsetter {
|
||||||
@ -371,7 +429,7 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.toolbar.setOnClickListener {
|
binding.toolbar.setOnClickListener {
|
||||||
presenter.manga?.id?.let { id ->
|
viewModel.manga?.id?.let { id ->
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(this, MainActivity::class.java).apply {
|
Intent(this, MainActivity::class.java).apply {
|
||||||
action = MainActivity.SHORTCUT_MANGA
|
action = MainActivity.SHORTCUT_MANGA
|
||||||
@ -461,11 +519,11 @@ class ReaderActivity :
|
|||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
popupMenu(
|
popupMenu(
|
||||||
items = ReadingModeType.values().map { it.flagValue to it.stringRes },
|
items = ReadingModeType.values().map { it.flagValue to it.stringRes },
|
||||||
selectedItemId = presenter.getMangaReadingMode(resolveDefault = false),
|
selectedItemId = viewModel.getMangaReadingMode(resolveDefault = false),
|
||||||
) {
|
) {
|
||||||
val newReadingMode = ReadingModeType.fromPreference(itemId)
|
val newReadingMode = ReadingModeType.fromPreference(itemId)
|
||||||
|
|
||||||
presenter.setMangaReadingMode(newReadingMode.flagValue)
|
viewModel.setMangaReadingMode(newReadingMode.flagValue)
|
||||||
|
|
||||||
menuToggleToast?.cancel()
|
menuToggleToast?.cancel()
|
||||||
if (!readerPreferences.showReadingMode().get()) {
|
if (!readerPreferences.showReadingMode().get()) {
|
||||||
@ -482,7 +540,7 @@ class ReaderActivity :
|
|||||||
setTooltip(R.string.pref_crop_borders)
|
setTooltip(R.string.pref_crop_borders)
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaReadingMode())
|
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
|
||||||
val enabled = if (isPagerType) {
|
val enabled = if (isPagerType) {
|
||||||
readerPreferences.cropBorders().toggle()
|
readerPreferences.cropBorders().toggle()
|
||||||
} else {
|
} else {
|
||||||
@ -514,12 +572,12 @@ class ReaderActivity :
|
|||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
popupMenu(
|
popupMenu(
|
||||||
items = OrientationType.values().map { it.flagValue to it.stringRes },
|
items = OrientationType.values().map { it.flagValue to it.stringRes },
|
||||||
selectedItemId = presenter.manga?.orientationType
|
selectedItemId = viewModel.manga?.orientationType?.toInt()
|
||||||
?: readerPreferences.defaultOrientationType().get(),
|
?: readerPreferences.defaultOrientationType().get(),
|
||||||
) {
|
) {
|
||||||
val newOrientation = OrientationType.fromPreference(itemId)
|
val newOrientation = OrientationType.fromPreference(itemId)
|
||||||
|
|
||||||
presenter.setMangaOrientationType(newOrientation.flagValue)
|
viewModel.setMangaOrientationType(newOrientation.flagValue)
|
||||||
|
|
||||||
menuToggleToast?.cancel()
|
menuToggleToast?.cancel()
|
||||||
menuToggleToast = toast(newOrientation.stringRes)
|
menuToggleToast = toast(newOrientation.stringRes)
|
||||||
@ -550,7 +608,7 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCropBordersShortcut() {
|
private fun updateCropBordersShortcut() {
|
||||||
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaReadingMode())
|
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
|
||||||
val enabled = if (isPagerType) {
|
val enabled = if (isPagerType) {
|
||||||
readerPreferences.cropBorders().get()
|
readerPreferences.cropBorders().get()
|
||||||
} else {
|
} else {
|
||||||
@ -633,19 +691,19 @@ class ReaderActivity :
|
|||||||
fun setManga(manga: Manga) {
|
fun setManga(manga: Manga) {
|
||||||
val prevViewer = viewer
|
val prevViewer = viewer
|
||||||
|
|
||||||
val viewerMode = ReadingModeType.fromPreference(presenter.getMangaReadingMode(resolveDefault = false))
|
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
|
||||||
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
|
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
|
||||||
|
|
||||||
val newViewer = ReadingModeType.toViewer(presenter.getMangaReadingMode(), this)
|
val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this)
|
||||||
|
|
||||||
updateCropBordersShortcut()
|
updateCropBordersShortcut()
|
||||||
if (window.sharedElementEnterTransition is MaterialContainerTransform) {
|
if (window.sharedElementEnterTransition is MaterialContainerTransform) {
|
||||||
// Wait until transition is complete to avoid crash on API 26
|
// Wait until transition is complete to avoid crash on API 26
|
||||||
window.sharedElementEnterTransition.doOnEnd {
|
window.sharedElementEnterTransition.doOnEnd {
|
||||||
setOrientation(presenter.getMangaOrientationType())
|
setOrientation(viewModel.getMangaOrientationType())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setOrientation(presenter.getMangaOrientationType())
|
setOrientation(viewModel.getMangaOrientationType())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy previous viewer if there was one
|
// Destroy previous viewer if there was one
|
||||||
@ -658,10 +716,10 @@ class ReaderActivity :
|
|||||||
binding.viewerContainer.addView(newViewer.getView())
|
binding.viewerContainer.addView(newViewer.getView())
|
||||||
|
|
||||||
if (readerPreferences.showReadingMode().get()) {
|
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
|
binding.pageSlider.isRTL = newViewer is R2LPagerViewer
|
||||||
if (newViewer is R2LPagerViewer) {
|
if (newViewer is R2LPagerViewer) {
|
||||||
@ -684,9 +742,9 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openChapterInWebview() {
|
private fun openChapterInWebview() {
|
||||||
val manga = presenter.manga ?: return
|
val manga = viewModel.manga ?: return
|
||||||
val source = presenter.getSource() ?: return
|
val source = viewModel.getSource() ?: return
|
||||||
val url = presenter.getChapterUrl() ?: return
|
val url = viewModel.getChapterUrl() ?: return
|
||||||
|
|
||||||
val intent = WebViewActivity.newIntent(this, url, source.id, manga.title)
|
val intent = WebViewActivity.newIntent(this, url, source.id, manga.title)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
@ -707,7 +765,7 @@ class ReaderActivity :
|
|||||||
* method to the current viewer, but also set the subtitle on the toolbar, and
|
* method to the current viewer, but also set the subtitle on the toolbar, and
|
||||||
* hides or disables the reader prev/next buttons if there's a prev or next chapter
|
* hides or disables the reader prev/next buttons if there's a prev or next chapter
|
||||||
*/
|
*/
|
||||||
fun setChapters(viewerChapters: ViewerChapters) {
|
private fun setChapters(viewerChapters: ViewerChapters) {
|
||||||
binding.readerContainer.removeView(loadingIndicator)
|
binding.readerContainer.removeView(loadingIndicator)
|
||||||
viewer?.setChapters(viewerChapters)
|
viewer?.setChapters(viewerChapters)
|
||||||
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name
|
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name
|
||||||
@ -765,7 +823,7 @@ class ReaderActivity :
|
|||||||
*/
|
*/
|
||||||
fun moveToPageIndex(index: Int) {
|
fun moveToPageIndex(index: Int) {
|
||||||
val viewer = viewer ?: return
|
val viewer = viewer ?: return
|
||||||
val currentChapter = presenter.getCurrentChapter() ?: return
|
val currentChapter = viewModel.getCurrentChapter() ?: return
|
||||||
val page = currentChapter.pages?.getOrNull(index) ?: return
|
val page = currentChapter.pages?.getOrNull(index) ?: return
|
||||||
viewer.moveToPage(page)
|
viewer.moveToPage(page)
|
||||||
}
|
}
|
||||||
@ -775,7 +833,10 @@ class ReaderActivity :
|
|||||||
* should be automatically shown.
|
* should be automatically shown.
|
||||||
*/
|
*/
|
||||||
private fun loadNextChapter() {
|
private fun loadNextChapter() {
|
||||||
presenter.loadNextChapter()
|
lifecycleScope.launch {
|
||||||
|
viewModel.loadNextChapter()
|
||||||
|
moveToPageIndex(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -783,7 +844,10 @@ class ReaderActivity :
|
|||||||
* should be automatically shown.
|
* should be automatically shown.
|
||||||
*/
|
*/
|
||||||
private fun loadPreviousChapter() {
|
private fun loadPreviousChapter() {
|
||||||
presenter.loadPreviousChapter()
|
lifecycleScope.launch {
|
||||||
|
viewModel.loadPreviousChapter()
|
||||||
|
moveToPageIndex(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -792,7 +856,7 @@ class ReaderActivity :
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
fun onPageSelected(page: ReaderPage) {
|
fun onPageSelected(page: ReaderPage) {
|
||||||
presenter.onPageSelected(page)
|
viewModel.onPageSelected(page)
|
||||||
val pages = page.chapter.pages ?: return
|
val pages = page.chapter.pages ?: return
|
||||||
|
|
||||||
// Set bottom page number
|
// 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.
|
* the viewer is reaching the beginning or end of a chapter or the transition page is active.
|
||||||
*/
|
*/
|
||||||
fun requestPreloadChapter(chapter: ReaderChapter) {
|
fun requestPreloadChapter(chapter: ReaderChapter) {
|
||||||
presenter.preloadChapter(chapter)
|
lifecycleScope.launch { viewModel.preloadChapter(chapter) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -860,15 +924,15 @@ class ReaderActivity :
|
|||||||
* will call [onShareImageResult] with the path the image was saved on when it's ready.
|
* will call [onShareImageResult] with the path the image was saved on when it's ready.
|
||||||
*/
|
*/
|
||||||
fun shareImage(page: ReaderPage) {
|
fun shareImage(page: ReaderPage) {
|
||||||
presenter.shareImage(page)
|
viewModel.shareImage(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the presenter when a page is ready to be shared. It shows Android's default
|
* Called from the presenter when a page is ready to be shared. It shows Android's default
|
||||||
* sharing tool.
|
* sharing tool.
|
||||||
*/
|
*/
|
||||||
fun onShareImageResult(uri: Uri, page: ReaderPage) {
|
private fun onShareImageResult(uri: Uri, page: ReaderPage) {
|
||||||
val manga = presenter.manga ?: return
|
val manga = viewModel.manga ?: return
|
||||||
val chapter = page.chapter.chapter
|
val chapter = page.chapter.chapter
|
||||||
|
|
||||||
val intent = uri.toShareIntent(
|
val intent = uri.toShareIntent(
|
||||||
@ -883,19 +947,19 @@ class ReaderActivity :
|
|||||||
* storage to the presenter.
|
* storage to the presenter.
|
||||||
*/
|
*/
|
||||||
fun saveImage(page: ReaderPage) {
|
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
|
* Called from the presenter when a page is saved or fails. It shows a message or logs the
|
||||||
* event depending on the [result].
|
* event depending on the [result].
|
||||||
*/
|
*/
|
||||||
fun onSaveImageResult(result: ReaderPresenter.SaveImageResult) {
|
private fun onSaveImageResult(result: ReaderViewModel.SaveImageResult) {
|
||||||
when (result) {
|
when (result) {
|
||||||
is ReaderPresenter.SaveImageResult.Success -> {
|
is ReaderViewModel.SaveImageResult.Success -> {
|
||||||
toast(R.string.picture_saved)
|
toast(R.string.picture_saved)
|
||||||
}
|
}
|
||||||
is ReaderPresenter.SaveImageResult.Error -> {
|
is ReaderViewModel.SaveImageResult.Error -> {
|
||||||
logcat(LogPriority.ERROR, result.error)
|
logcat(LogPriority.ERROR, result.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -906,14 +970,14 @@ class ReaderActivity :
|
|||||||
* cover to the presenter.
|
* cover to the presenter.
|
||||||
*/
|
*/
|
||||||
fun setAsCover(page: ReaderPage) {
|
fun setAsCover(page: ReaderPage) {
|
||||||
presenter.setAsCover(this, page)
|
viewModel.setAsCover(this, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the presenter when a page is set as cover or fails. It shows a different message
|
* Called from the presenter when a page is set as cover or fails. It shows a different message
|
||||||
* depending on the [result].
|
* depending on the [result].
|
||||||
*/
|
*/
|
||||||
fun onSetAsCoverResult(result: ReaderPresenter.SetAsCoverResult) {
|
private fun onSetAsCoverResult(result: ReaderViewModel.SetAsCoverResult) {
|
||||||
toast(
|
toast(
|
||||||
when (result) {
|
when (result) {
|
||||||
Success -> R.string.cover_updated
|
Success -> R.string.cover_updated
|
||||||
@ -926,12 +990,12 @@ class ReaderActivity :
|
|||||||
/**
|
/**
|
||||||
* Forces the user preferred [orientation] on the activity.
|
* Forces the user preferred [orientation] on the activity.
|
||||||
*/
|
*/
|
||||||
fun setOrientation(orientation: Int) {
|
private fun setOrientation(orientation: Int) {
|
||||||
val newOrientation = OrientationType.fromPreference(orientation)
|
val newOrientation = OrientationType.fromPreference(orientation)
|
||||||
if (newOrientation.flag != requestedOrientation) {
|
if (newOrientation.flag != requestedOrientation) {
|
||||||
requestedOrientation = newOrientation.flag
|
requestedOrientation = newOrientation.flag
|
||||||
}
|
}
|
||||||
updateOrientationShortcut(presenter.getMangaOrientationType(resolveDefault = false))
|
updateOrientationShortcut(viewModel.getMangaOrientationType(resolveDefault = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,8 +3,10 @@ package eu.kanade.tachiyomi.ui.reader
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import eu.kanade.core.util.asFlow
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
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.history.model.HistoryUpdate
|
||||||
import eu.kanade.domain.manga.interactor.GetManga
|
import eu.kanade.domain.manga.interactor.GetManga
|
||||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.isLocal
|
import eu.kanade.domain.manga.model.isLocal
|
||||||
import eu.kanade.domain.manga.model.toDbManga
|
|
||||||
import eu.kanade.domain.track.interactor.GetTracks
|
import eu.kanade.domain.track.interactor.GetTracks
|
||||||
import eu.kanade.domain.track.interactor.InsertTrack
|
import eu.kanade.domain.track.interactor.InsertTrack
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
|
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
|
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
@ -54,37 +54,41 @@ import eu.kanade.tachiyomi.util.lang.byteSize
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||||
import eu.kanade.tachiyomi.util.lang.takeBytes
|
import eu.kanade.tachiyomi.util.lang.takeBytes
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
import eu.kanade.tachiyomi.util.system.isOnline
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.MainScope
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nucleus.presenter.RxPresenter
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import eu.kanade.domain.manga.model.Manga as DomainManga
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter used by the activity to perform background operations.
|
* Presenter used by the activity to perform background operations.
|
||||||
*/
|
*/
|
||||||
class ReaderPresenter(
|
class ReaderViewModel(
|
||||||
|
private val savedState: SavedStateHandle = SavedStateHandle(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val downloadProvider: DownloadProvider = Injekt.get(),
|
private val downloadProvider: DownloadProvider = Injekt.get(),
|
||||||
@ -102,20 +106,28 @@ class ReaderPresenter(
|
|||||||
private val upsertHistory: UpsertHistory = Injekt.get(),
|
private val upsertHistory: UpsertHistory = Injekt.get(),
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||||
private val setMangaViewerFlags: SetMangaViewerFlags = 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.
|
* The manga loaded in the reader. It can be null when instantiated for a short time.
|
||||||
*/
|
*/
|
||||||
var manga: Manga? = null
|
val manga: Manga?
|
||||||
private set
|
get() = state.value.manga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The chapter id of the currently loaded chapter. Used to restore from process kill.
|
* The chapter id of the currently loaded chapter. Used to restore from process kill.
|
||||||
*/
|
*/
|
||||||
private var chapterId = -1L
|
private var chapterId = savedState.get<Long>("chapter_id") ?: -1L
|
||||||
|
set(value) {
|
||||||
|
savedState["chapter_id"] = value
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The chapter loader for the loaded manga. It'll be null until [manga] is set.
|
* The chapter loader for the loaded manga. It'll be null until [manga] is set.
|
||||||
@ -132,16 +144,6 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
private var activeChapterSubscription: Subscription? = null
|
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
|
private var chapterToDownload: Download? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,7 +151,7 @@ class ReaderPresenter(
|
|||||||
* time in a background thread to avoid blocking the UI.
|
* time in a background thread to avoid blocking the UI.
|
||||||
*/
|
*/
|
||||||
private val chapterList by lazy {
|
private val chapterList by lazy {
|
||||||
val manga = manga!!.toDomainManga()!!
|
val manga = manga!!
|
||||||
val chapters = runBlocking { getChapterByMangaId.await(manga.id) }
|
val chapters = runBlocking { getChapterByMangaId.await(manga.id) }
|
||||||
|
|
||||||
val selectedChapter = chapters.find { it.id == chapterId }
|
val selectedChapter = chapters.find { it.id == chapterId }
|
||||||
@ -161,12 +163,12 @@ class ReaderPresenter(
|
|||||||
when {
|
when {
|
||||||
readerPreferences.skipRead().get() && it.read -> true
|
readerPreferences.skipRead().get() && it.read -> true
|
||||||
readerPreferences.skipFiltered().get() -> {
|
readerPreferences.skipFiltered().get() -> {
|
||||||
(manga.unreadFilterRaw == DomainManga.CHAPTER_SHOW_READ && !it.read) ||
|
(manga.unreadFilterRaw == Manga.CHAPTER_SHOW_READ && !it.read) ||
|
||||||
(manga.unreadFilterRaw == DomainManga.CHAPTER_SHOW_UNREAD && it.read) ||
|
(manga.unreadFilterRaw == Manga.CHAPTER_SHOW_UNREAD && it.read) ||
|
||||||
(manga.downloadedFilterRaw == DomainManga.CHAPTER_SHOW_DOWNLOADED && !downloadManager.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source)) ||
|
(manga.downloadedFilterRaw == Manga.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.downloadedFilterRaw == Manga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source)) ||
|
||||||
(manga.bookmarkedFilterRaw == DomainManga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) ||
|
(manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) ||
|
||||||
(manga.bookmarkedFilterRaw == DomainManga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark)
|
(manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark)
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
@ -188,32 +190,15 @@ class ReaderPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var hasTrackers: Boolean = false
|
private var hasTrackers: Boolean = false
|
||||||
private val checkTrackers: (DomainManga) -> Unit = { manga ->
|
private val checkTrackers: (Manga) -> Unit = { manga ->
|
||||||
val tracks = runBlocking { getTracks.await(manga.id) }
|
val tracks = runBlocking { getTracks.await(manga.id) }
|
||||||
hasTrackers = tracks.isNotEmpty()
|
hasTrackers = tracks.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val incognitoMode = preferences.incognitoMode().get()
|
private val incognitoMode = preferences.incognitoMode().get()
|
||||||
|
|
||||||
/**
|
override fun onCleared() {
|
||||||
* Called when the presenter is created. It retrieves the saved active chapter if the process
|
val currentChapters = state.value.viewerChapters
|
||||||
* was restored.
|
|
||||||
*/
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
if (savedState != null) {
|
|
||||||
chapterId = savedState.getLong(::chapterId.name, -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the presenter is destroyed. It saves the current progress and cleans up
|
|
||||||
* references on the currently active chapters.
|
|
||||||
*/
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
coroutineScope.cancel()
|
|
||||||
val currentChapters = viewerChaptersRelay.value
|
|
||||||
if (currentChapters != null) {
|
if (currentChapters != null) {
|
||||||
currentChapters.unref()
|
currentChapters.unref()
|
||||||
saveReadingProgress(currentChapters.currChapter)
|
saveReadingProgress(currentChapters.currChapter)
|
||||||
@ -223,24 +208,24 @@ class ReaderPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
init {
|
||||||
* Called when the presenter instance is being saved. It saves the currently active chapter
|
// To save state
|
||||||
* id and the last page read.
|
state.map { it.viewerChapters?.currChapter }
|
||||||
*/
|
.distinctUntilChanged()
|
||||||
override fun onSave(state: Bundle) {
|
.onEach { currentChapter ->
|
||||||
super.onSave(state)
|
if (currentChapter != null) {
|
||||||
val currentChapter = getCurrentChapter()
|
currentChapter.requestedPage = currentChapter.chapter.last_page_read
|
||||||
if (currentChapter != null) {
|
chapterId = currentChapter.chapter.id!!
|
||||||
currentChapter.requestedPage = currentChapter.chapter.last_page_read
|
}
|
||||||
state.putLong(::chapterId.name, currentChapter.chapter.id!!)
|
}
|
||||||
}
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user pressed the back button and is going to leave the reader. Used to
|
* Called when the user pressed the back button and is going to leave the reader. Used to
|
||||||
* trigger deletion of the downloaded chapters.
|
* trigger deletion of the downloaded chapters.
|
||||||
*/
|
*/
|
||||||
fun onBackPressed() {
|
fun onActivityFinish() {
|
||||||
deletePendingChapters()
|
deletePendingChapters()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +235,7 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
fun onSaveInstanceStateNonConfigurationChange() {
|
fun onSaveInstanceStateNonConfigurationChange() {
|
||||||
val currentChapter = getCurrentChapter() ?: return
|
val currentChapter = getCurrentChapter() ?: return
|
||||||
coroutineScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
saveChapterProgress(currentChapter)
|
saveChapterProgress(currentChapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,60 +251,35 @@ class ReaderPresenter(
|
|||||||
* Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will
|
* Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will
|
||||||
* fetch the manga from the database and initialize the initial chapter.
|
* fetch the manga from the database and initialize the initial chapter.
|
||||||
*/
|
*/
|
||||||
fun init(mangaId: Long, initialChapterId: Long) {
|
suspend fun init(mangaId: Long, initialChapterId: Long): Result<Boolean> {
|
||||||
if (!needsInit()) return
|
if (!needsInit()) return Result.success(true)
|
||||||
|
return withIOContext {
|
||||||
coroutineScope.launchIO {
|
|
||||||
try {
|
try {
|
||||||
val manga = getManga.await(mangaId)
|
val manga = getManga.await(mangaId)
|
||||||
withUIContext {
|
if (manga != null) {
|
||||||
manga?.let { init(it.toDbManga(), initialChapterId) }
|
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) {
|
} 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
|
* Returns an observable that loads the given [chapter] with this [loader]. This observable
|
||||||
* handles main thread synchronization and updating the currently active chapters on
|
* handles main thread synchronization and updating the currently active chapters on
|
||||||
@ -345,14 +305,14 @@ class ReaderPresenter(
|
|||||||
)
|
)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnNext { newChapters ->
|
.doOnNext { newChapters ->
|
||||||
val oldChapters = viewerChaptersRelay.value
|
mutableState.update {
|
||||||
|
// Add new references first to avoid unnecessary recycling
|
||||||
|
newChapters.ref()
|
||||||
|
it.viewerChapters?.unref()
|
||||||
|
|
||||||
// Add new references first to avoid unnecessary recycling
|
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
|
||||||
newChapters.ref()
|
it.copy(viewerChapters = newChapters)
|
||||||
oldChapters?.unref()
|
}
|
||||||
|
|
||||||
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
|
|
||||||
viewerChaptersRelay.call(newChapters)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,17 +320,17 @@ class ReaderPresenter(
|
|||||||
* Called when the user changed to the given [chapter] when changing pages from the viewer.
|
* Called when the user changed to the given [chapter] when changing pages from the viewer.
|
||||||
* It's used only to set this chapter as active.
|
* It's used only to set this chapter as active.
|
||||||
*/
|
*/
|
||||||
private fun loadNewChapter(chapter: ReaderChapter) {
|
private suspend fun loadNewChapter(chapter: ReaderChapter) {
|
||||||
val loader = loader ?: return
|
val loader = loader ?: return
|
||||||
|
|
||||||
logcat { "Loading ${chapter.chapter.url}" }
|
logcat { "Loading ${chapter.chapter.url}" }
|
||||||
|
|
||||||
activeChapterSubscription?.unsubscribe()
|
withIOContext {
|
||||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
getLoadObservable(loader, chapter)
|
||||||
.toCompletable()
|
.asFlow()
|
||||||
.onErrorComplete()
|
.catch { logcat(LogPriority.ERROR, it) }
|
||||||
.subscribe()
|
.first()
|
||||||
.also(::add)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -378,30 +338,25 @@ class ReaderPresenter(
|
|||||||
* sets the [isLoadingAdjacentChapterRelay] that the view uses to prevent any further
|
* sets the [isLoadingAdjacentChapterRelay] that the view uses to prevent any further
|
||||||
* interaction until the chapter is loaded.
|
* interaction until the chapter is loaded.
|
||||||
*/
|
*/
|
||||||
private fun loadAdjacent(chapter: ReaderChapter) {
|
private suspend fun loadAdjacent(chapter: ReaderChapter) {
|
||||||
val loader = loader ?: return
|
val loader = loader ?: return
|
||||||
|
|
||||||
logcat { "Loading adjacent ${chapter.chapter.url}" }
|
logcat { "Loading adjacent ${chapter.chapter.url}" }
|
||||||
|
|
||||||
activeChapterSubscription?.unsubscribe()
|
mutableState.update { it.copy(isLoadingAdjacentChapter = true) }
|
||||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
withIOContext {
|
||||||
.doOnSubscribe { coroutineScope.launch { isLoadingAdjacentChapterEvent.send(true) } }
|
getLoadObservable(loader, chapter)
|
||||||
.doOnUnsubscribe { coroutineScope.launch { isLoadingAdjacentChapterEvent.send(false) } }
|
.asFlow()
|
||||||
.subscribeFirst(
|
.first()
|
||||||
{ view, _ ->
|
}
|
||||||
view.moveToPageIndex(0)
|
mutableState.update { it.copy(isLoadingAdjacentChapter = false) }
|
||||||
},
|
|
||||||
{ _, _ ->
|
|
||||||
// Ignore onError event, viewers handle that state
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so
|
* Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so
|
||||||
* that the user doesn't have to wait too long to continue reading.
|
* that the user doesn't have to wait too long to continue reading.
|
||||||
*/
|
*/
|
||||||
private fun preload(chapter: ReaderChapter) {
|
private suspend fun preload(chapter: ReaderChapter) {
|
||||||
if (chapter.pageLoader is HttpPageLoader) {
|
if (chapter.pageLoader is HttpPageLoader) {
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
val dbChapter = chapter.chapter
|
val dbChapter = chapter.chapter
|
||||||
@ -424,13 +379,14 @@ class ReaderPresenter(
|
|||||||
logcat { "Preloading ${chapter.chapter.url}" }
|
logcat { "Preloading ${chapter.chapter.url}" }
|
||||||
|
|
||||||
val loader = loader ?: return
|
val loader = loader ?: return
|
||||||
loader.loadChapter(chapter)
|
withIOContext {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
loader.loadChapter(chapter)
|
||||||
// Update current chapters whenever a chapter is preloaded
|
.doOnCompleted { eventChannel.trySend(Event.ReloadViewerChapters) }
|
||||||
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
.onErrorComplete()
|
||||||
.onErrorComplete()
|
.toObservable<Unit>()
|
||||||
.subscribe()
|
.asFlow()
|
||||||
.also(::add)
|
.firstOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -439,7 +395,7 @@ class ReaderPresenter(
|
|||||||
* [page]'s chapter is different from the currently active.
|
* [page]'s chapter is different from the currently active.
|
||||||
*/
|
*/
|
||||||
fun onPageSelected(page: ReaderPage) {
|
fun onPageSelected(page: ReaderPage) {
|
||||||
val currentChapters = viewerChaptersRelay.value ?: return
|
val currentChapters = state.value.viewerChapters ?: return
|
||||||
|
|
||||||
val selectedChapter = page.chapter
|
val selectedChapter = page.chapter
|
||||||
|
|
||||||
@ -461,7 +417,7 @@ class ReaderPresenter(
|
|||||||
logcat { "Setting ${selectedChapter.chapter.url} as active" }
|
logcat { "Setting ${selectedChapter.chapter.url} as active" }
|
||||||
saveReadingProgress(currentChapters.currChapter)
|
saveReadingProgress(currentChapters.currChapter)
|
||||||
setReadStartTime()
|
setReadStartTime()
|
||||||
loadNewChapter(selectedChapter)
|
viewModelScope.launch { loadNewChapter(selectedChapter) }
|
||||||
}
|
}
|
||||||
val pages = page.chapter.pages ?: return
|
val pages = page.chapter.pages ?: return
|
||||||
val inDownloadRange = page.number.toDouble() / pages.size > 0.25
|
val inDownloadRange = page.number.toDouble() / pages.size > 0.25
|
||||||
@ -477,9 +433,9 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
// Only download ahead if current + next chapter is already downloaded too to avoid jank
|
// Only download ahead if current + next chapter is already downloaded too to avoid jank
|
||||||
if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return
|
if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return
|
||||||
val nextChapter = viewerChaptersRelay.value?.nextChapter?.chapter ?: return
|
val nextChapter = state.value.viewerChapters?.nextChapter?.chapter ?: return
|
||||||
|
|
||||||
coroutineScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val isNextChapterDownloaded = downloadManager.isChapterDownloaded(
|
val isNextChapterDownloaded = downloadManager.isChapterDownloaded(
|
||||||
nextChapter.name,
|
nextChapter.name,
|
||||||
nextChapter.scanlator,
|
nextChapter.scanlator,
|
||||||
@ -488,10 +444,10 @@ class ReaderPresenter(
|
|||||||
)
|
)
|
||||||
if (!isNextChapterDownloaded) return@launchIO
|
if (!isNextChapterDownloaded) return@launchIO
|
||||||
|
|
||||||
val chaptersToDownload = getNextChapters.await(manga.id!!, nextChapter.id!!)
|
val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!)
|
||||||
.take(amount)
|
.take(amount)
|
||||||
downloadManager.downloadChapters(
|
downloadManager.downloadChapters(
|
||||||
manga.toDomainManga()!!,
|
manga,
|
||||||
chaptersToDownload,
|
chaptersToDownload,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -535,7 +491,7 @@ class ReaderPresenter(
|
|||||||
* Called when reader chapter is changed in reader or when activity is paused.
|
* Called when reader chapter is changed in reader or when activity is paused.
|
||||||
*/
|
*/
|
||||||
private fun saveReadingProgress(readerChapter: ReaderChapter) {
|
private fun saveReadingProgress(readerChapter: ReaderChapter) {
|
||||||
coroutineScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
saveChapterProgress(readerChapter)
|
saveChapterProgress(readerChapter)
|
||||||
saveChapterHistory(readerChapter)
|
saveChapterHistory(readerChapter)
|
||||||
}
|
}
|
||||||
@ -583,23 +539,23 @@ class ReaderPresenter(
|
|||||||
/**
|
/**
|
||||||
* Called from the activity to preload the given [chapter].
|
* Called from the activity to preload the given [chapter].
|
||||||
*/
|
*/
|
||||||
fun preloadChapter(chapter: ReaderChapter) {
|
suspend fun preloadChapter(chapter: ReaderChapter) {
|
||||||
preload(chapter)
|
preload(chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the activity to load and set the next chapter as active.
|
* Called from the activity to load and set the next chapter as active.
|
||||||
*/
|
*/
|
||||||
fun loadNextChapter() {
|
suspend fun loadNextChapter() {
|
||||||
val nextChapter = viewerChaptersRelay.value?.nextChapter ?: return
|
val nextChapter = state.value.viewerChapters?.nextChapter ?: return
|
||||||
loadAdjacent(nextChapter)
|
loadAdjacent(nextChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the activity to load and set the previous chapter as active.
|
* Called from the activity to load and set the previous chapter as active.
|
||||||
*/
|
*/
|
||||||
fun loadPreviousChapter() {
|
suspend fun loadPreviousChapter() {
|
||||||
val prevChapter = viewerChaptersRelay.value?.prevChapter ?: return
|
val prevChapter = state.value.viewerChapters?.prevChapter ?: return
|
||||||
loadAdjacent(prevChapter)
|
loadAdjacent(prevChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,7 +563,7 @@ class ReaderPresenter(
|
|||||||
* Returns the currently active chapter.
|
* Returns the currently active chapter.
|
||||||
*/
|
*/
|
||||||
fun getCurrentChapter(): ReaderChapter? {
|
fun getCurrentChapter(): ReaderChapter? {
|
||||||
return viewerChaptersRelay.value?.currChapter
|
return state.value.viewerChapters?.currChapter
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
|
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
|
||||||
@ -625,7 +581,7 @@ class ReaderPresenter(
|
|||||||
fun bookmarkCurrentChapter(bookmarked: Boolean) {
|
fun bookmarkCurrentChapter(bookmarked: Boolean) {
|
||||||
val chapter = getCurrentChapter()?.chapter ?: return
|
val chapter = getCurrentChapter()?.chapter ?: return
|
||||||
chapter.bookmark = bookmarked // Otherwise the bookmark icon doesn't update
|
chapter.bookmark = bookmarked // Otherwise the bookmark icon doesn't update
|
||||||
coroutineScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
updateChapter.await(
|
updateChapter.await(
|
||||||
ChapterUpdate(
|
ChapterUpdate(
|
||||||
id = chapter.id!!.toLong(),
|
id = chapter.id!!.toLong(),
|
||||||
@ -640,10 +596,10 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
fun getMangaReadingMode(resolveDefault: Boolean = true): Int {
|
fun getMangaReadingMode(resolveDefault: Boolean = true): Int {
|
||||||
val default = readerPreferences.defaultReadingMode().get()
|
val default = readerPreferences.defaultReadingMode().get()
|
||||||
val readingMode = ReadingModeType.fromPreference(manga?.readingModeType)
|
val readingMode = ReadingModeType.fromPreference(manga?.readingModeType?.toInt())
|
||||||
return when {
|
return when {
|
||||||
resolveDefault && readingMode == ReadingModeType.DEFAULT -> default
|
resolveDefault && readingMode == ReadingModeType.DEFAULT -> default
|
||||||
else -> manga?.readingModeType ?: default
|
else -> manga?.readingModeType?.toInt() ?: default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,22 +608,21 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
fun setMangaReadingMode(readingModeType: Int) {
|
fun setMangaReadingMode(readingModeType: Int) {
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
manga.readingModeType = readingModeType
|
viewModelScope.launchIO {
|
||||||
|
setMangaViewerFlags.awaitSetMangaReadingMode(manga.id, readingModeType.toLong())
|
||||||
coroutineScope.launchIO {
|
val currChapters = state.value.viewerChapters
|
||||||
setMangaViewerFlags.awaitSetMangaReadingMode(manga.id!!.toLong(), readingModeType.toLong())
|
|
||||||
delay(250)
|
|
||||||
val currChapters = viewerChaptersRelay.value
|
|
||||||
if (currChapters != null) {
|
if (currChapters != null) {
|
||||||
// Save current page
|
// Save current page
|
||||||
val currChapter = currChapters.currChapter
|
val currChapter = currChapters.currChapter
|
||||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||||
|
|
||||||
withUIContext {
|
mutableState.update {
|
||||||
// Emit manga and chapters to the new viewer
|
it.copy(
|
||||||
view?.setManga(manga)
|
manga = getManga.await(manga.id),
|
||||||
view?.setChapters(currChapters)
|
viewerChapters = currChapters,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
eventChannel.send(Event.ReloadViewerChapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -677,10 +632,10 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
fun getMangaOrientationType(resolveDefault: Boolean = true): Int {
|
fun getMangaOrientationType(resolveDefault: Boolean = true): Int {
|
||||||
val default = readerPreferences.defaultOrientationType().get()
|
val default = readerPreferences.defaultOrientationType().get()
|
||||||
val orientation = OrientationType.fromPreference(manga?.orientationType)
|
val orientation = OrientationType.fromPreference(manga?.orientationType?.toInt())
|
||||||
return when {
|
return when {
|
||||||
resolveDefault && orientation == OrientationType.DEFAULT -> default
|
resolveDefault && orientation == OrientationType.DEFAULT -> default
|
||||||
else -> manga?.orientationType ?: default
|
else -> manga?.orientationType?.toInt() ?: default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -689,14 +644,22 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
fun setMangaOrientationType(rotationType: Int) {
|
fun setMangaOrientationType(rotationType: Int) {
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
manga.orientationType = rotationType
|
viewModelScope.launchIO {
|
||||||
|
setMangaViewerFlags.awaitSetOrientationType(manga.id, rotationType.toLong())
|
||||||
coroutineScope.launchIO {
|
val currChapters = state.value.viewerChapters
|
||||||
setMangaViewerFlags.awaitSetOrientationType(manga.id!!.toLong(), rotationType.toLong())
|
|
||||||
delay(250)
|
|
||||||
val currChapters = viewerChaptersRelay.value
|
|
||||||
if (currChapters != null) {
|
if (currChapters != null) {
|
||||||
withUIContext { view?.setOrientation(getMangaOrientationType()) }
|
// Save current page
|
||||||
|
val currChapter = currChapters.currChapter
|
||||||
|
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||||
|
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
manga = getManga.await(manga.id),
|
||||||
|
viewerChapters = currChapters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
eventChannel.send(Event.SetOrientation(getMangaOrientationType()))
|
||||||
|
eventChannel.send(Event.ReloadViewerChapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -733,8 +696,8 @@ class ReaderPresenter(
|
|||||||
val relativePath = if (readerPreferences.folderPerManga().get()) DiskUtil.buildValidFilename(manga.title) else ""
|
val relativePath = if (readerPreferences.folderPerManga().get()) DiskUtil.buildValidFilename(manga.title) else ""
|
||||||
|
|
||||||
// Copy file in background.
|
// Copy file in background.
|
||||||
try {
|
viewModelScope.launchNonCancellable {
|
||||||
coroutineScope.launchNonCancellable {
|
try {
|
||||||
val uri = imageSaver.save(
|
val uri = imageSaver.save(
|
||||||
image = Image.Page(
|
image = Image.Page(
|
||||||
inputStream = page.stream!!,
|
inputStream = page.stream!!,
|
||||||
@ -744,12 +707,12 @@ class ReaderPresenter(
|
|||||||
)
|
)
|
||||||
withUIContext {
|
withUIContext {
|
||||||
notifier.onComplete(uri)
|
notifier.onComplete(uri)
|
||||||
view?.onSaveImageResult(SaveImageResult.Success(uri))
|
eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri)))
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
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)
|
val filename = generateFilename(manga, page)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
coroutineScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
destDir.deleteRecursively()
|
destDir.deleteRecursively()
|
||||||
val uri = imageSaver.save(
|
val uri = imageSaver.save(
|
||||||
image = Image.Page(
|
image = Image.Page(
|
||||||
@ -779,9 +742,7 @@ class ReaderPresenter(
|
|||||||
location = Location.Cache,
|
location = Location.Cache,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
withUIContext {
|
eventChannel.send(Event.ShareImage(uri, page))
|
||||||
view?.onShareImageResult(uri, page)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
@ -793,24 +754,21 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
fun setAsCover(context: Context, page: ReaderPage) {
|
fun setAsCover(context: Context, page: ReaderPage) {
|
||||||
if (page.status != Page.State.READY) return
|
if (page.status != Page.State.READY) return
|
||||||
val manga = manga?.toDomainManga() ?: return
|
val manga = manga ?: return
|
||||||
val stream = page.stream ?: return
|
val stream = page.stream ?: return
|
||||||
|
|
||||||
coroutineScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
try {
|
val result = try {
|
||||||
manga.editCover(context, stream())
|
manga.editCover(context, stream())
|
||||||
withUIContext {
|
if (manga.isLocal() || manga.favorite) {
|
||||||
view?.onSetAsCoverResult(
|
SetAsCoverResult.Success
|
||||||
if (manga.isLocal() || manga.favorite) {
|
} else {
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.AddToLibraryFirst
|
||||||
} else {
|
|
||||||
SetAsCoverResult.AddToLibraryFirst
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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 trackManager = Injekt.get<TrackManager>()
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
|
|
||||||
coroutineScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
getTracks.await(manga.id!!)
|
getTracks.await(manga.id)
|
||||||
.mapNotNull { track ->
|
.mapNotNull { track ->
|
||||||
val service = trackManager.getService(track.syncId)
|
val service = trackManager.getService(track.syncId)
|
||||||
if (service != null && service.isLogged && chapterRead > track.lastChapterRead) {
|
if (service != null && service.isLogged && chapterRead > track.lastChapterRead) {
|
||||||
@ -882,8 +840,8 @@ class ReaderPresenter(
|
|||||||
if (!chapter.chapter.read) return
|
if (!chapter.chapter.read) return
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
|
|
||||||
coroutineScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga.toDomainManga()!!)
|
downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -892,35 +850,26 @@ class ReaderPresenter(
|
|||||||
* are ignored.
|
* are ignored.
|
||||||
*/
|
*/
|
||||||
private fun deletePendingChapters() {
|
private fun deletePendingChapters() {
|
||||||
coroutineScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
downloadManager.deletePendingChapters()
|
downloadManager.deletePendingChapters()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're trying to avoid using Rx, so we "undeprecate" this
|
data class State(
|
||||||
@Suppress("DEPRECATION")
|
val manga: Manga? = null,
|
||||||
override fun getView(): ReaderActivity? {
|
val viewerChapters: ViewerChapters? = null,
|
||||||
return super.getView()
|
val isLoadingAdjacentChapter: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
companion object {
|
||||||
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)
|
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)
|
||||||
private const val MAX_FILE_NAME_BYTES = 250
|
private const val MAX_FILE_NAME_BYTES = 250
|
@ -44,22 +44,22 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
|
|||||||
private fun initGeneralPreferences() {
|
private fun initGeneralPreferences() {
|
||||||
binding.viewer.onItemSelectedListener = { position ->
|
binding.viewer.onItemSelectedListener = { position ->
|
||||||
val readingModeType = ReadingModeType.fromSpinner(position)
|
val readingModeType = ReadingModeType.fromSpinner(position)
|
||||||
(context as ReaderActivity).presenter.setMangaReadingMode(readingModeType.flagValue)
|
(context as ReaderActivity).viewModel.setMangaReadingMode(readingModeType.flagValue)
|
||||||
|
|
||||||
val mangaViewer = (context as ReaderActivity).presenter.getMangaReadingMode()
|
val mangaViewer = (context as ReaderActivity).viewModel.getMangaReadingMode()
|
||||||
if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) {
|
if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) {
|
||||||
initWebtoonPreferences()
|
initWebtoonPreferences()
|
||||||
} else {
|
} else {
|
||||||
initPagerPreferences()
|
initPagerPreferences()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.viewer.setSelection((context as ReaderActivity).presenter.manga?.readingModeType?.let { ReadingModeType.fromPreference(it).prefValue } ?: ReadingModeType.DEFAULT.prefValue)
|
binding.viewer.setSelection((context as ReaderActivity).viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue)
|
||||||
|
|
||||||
binding.rotationMode.onItemSelectedListener = { position ->
|
binding.rotationMode.onItemSelectedListener = { position ->
|
||||||
val rotationType = OrientationType.fromSpinner(position)
|
val rotationType = OrientationType.fromSpinner(position)
|
||||||
(context as ReaderActivity).presenter.setMangaOrientationType(rotationType.flagValue)
|
(context as ReaderActivity).viewModel.setMangaOrientationType(rotationType.flagValue)
|
||||||
}
|
}
|
||||||
binding.rotationMode.setSelection((context as ReaderActivity).presenter.manga?.orientationType?.let { OrientationType.fromPreference(it).prefValue } ?: OrientationType.DEFAULT.prefValue)
|
binding.rotationMode.setSelection((context as ReaderActivity).viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,8 +11,8 @@ import androidx.core.text.bold
|
|||||||
import androidx.core.text.buildSpannedString
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.text.inSpans
|
import androidx.core.text.inSpans
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
|
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
|
||||||
|
@ -62,7 +62,7 @@ class PagerTransitionHolder(
|
|||||||
addView(transitionView)
|
addView(transitionView)
|
||||||
addView(pagesContainer)
|
addView(pagesContainer)
|
||||||
|
|
||||||
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
|
transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga)
|
||||||
|
|
||||||
transition.to?.let { observeStatus(it) }
|
transition.to?.let { observeStatus(it) }
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ class WebtoonTransitionHolder(
|
|||||||
* Binds the given [transition] with this view holder, subscribing to its state.
|
* Binds the given [transition] with this view holder, subscribing to its state.
|
||||||
*/
|
*/
|
||||||
fun bind(transition: ChapterTransition) {
|
fun bind(transition: ChapterTransition) {
|
||||||
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
|
transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga)
|
||||||
|
|
||||||
transition.to?.let { observeStatus(it, transition) }
|
transition.to?.let { observeStatus(it, transition) }
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
aboutlib_version = "10.5.2"
|
aboutlib_version = "10.5.2"
|
||||||
okhttp_version = "5.0.0-alpha.10"
|
okhttp_version = "5.0.0-alpha.10"
|
||||||
nucleus_version = "3.0.0"
|
|
||||||
coil_version = "2.2.2"
|
coil_version = "2.2.2"
|
||||||
shizuku_version = "12.2.0"
|
shizuku_version = "12.2.0"
|
||||||
sqlite = "2.3.0-rc01"
|
sqlite = "2.3.0-rc01"
|
||||||
@ -41,9 +40,6 @@ sqlite-android = "com.github.requery:sqlite-android:3.39.2"
|
|||||||
|
|
||||||
preferencektx = "androidx.preference:preference-ktx:1.2.0"
|
preferencektx = "androidx.preference:preference-ktx:1.2.0"
|
||||||
|
|
||||||
nucleus-core = { module = "info.android15.nucleus:nucleus", version.ref = "nucleus_version" }
|
|
||||||
nucleus-supportv7 = { module = "info.android15.nucleus:nucleus-support-v7", version.ref = "nucleus_version" }
|
|
||||||
|
|
||||||
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
|
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||||
|
|
||||||
coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
|
coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
|
||||||
@ -97,7 +93,6 @@ reactivex = ["rxandroid", "rxjava", "rxrelay"]
|
|||||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
||||||
js-engine = ["quickjs-android"]
|
js-engine = ["quickjs-android"]
|
||||||
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
|
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
|
||||||
nucleus = ["nucleus-core", "nucleus-supportv7"]
|
|
||||||
coil = ["coil-core", "coil-gif", "coil-compose"]
|
coil = ["coil-core", "coil-gif", "coil-compose"]
|
||||||
shizuku = ["shizuku-api", "shizuku-provider"]
|
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||||
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]
|
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]
|
||||||
|
Loading…
Reference in New Issue
Block a user