Migrate to AndroidX ViewModel from Voyager ScreenModel (#3594)

This commit is contained in:
AntsyLich
2026-07-15 21:19:11 +06:00
committed by GitHub
parent ef1c529672
commit c3b99aea07
91 changed files with 1503 additions and 1091 deletions
+1
View File
@@ -209,6 +209,7 @@ dependencies {
implementation(projects.i18n) implementation(projects.i18n)
implementation(projects.core.archive) implementation(projects.core.archive)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.viewmodel)
implementation(projects.coreMetadata) implementation(projects.coreMetadata)
implementation(projects.sourceApi) implementation(projects.sourceApi)
implementation(projects.sourceLocal) implementation(projects.sourceLocal)
@@ -53,7 +53,7 @@ import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsViewModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -66,7 +66,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable @Composable
fun ExtensionDetailsScreen( fun ExtensionDetailsScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: ExtensionDetailsScreenModel.State, state: ExtensionDetailsViewModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickEnableAll: () -> Unit, onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit, onClickDisableAll: () -> Unit,
@@ -53,7 +53,7 @@ import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -71,7 +71,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun ExtensionScreen( fun ExtensionScreen(
state: ExtensionsScreenModel.State, state: ExtensionsViewModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
searchQuery: String?, searchQuery: String?,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
@@ -133,7 +133,7 @@ fun ExtensionScreen(
@Composable @Composable
private fun ExtensionContent( private fun ExtensionContent(
state: ExtensionsScreenModel.State, state: ExtensionsViewModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
@@ -12,7 +12,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@@ -20,7 +20,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
@Composable @Composable
fun GlobalSearchScreen( fun GlobalSearchScreen(
state: SearchScreenModel.State, state: SearchViewModel.State,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
@@ -4,14 +4,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@Composable @Composable
fun MigrateSearchScreen( fun MigrateSearchScreen(
state: SearchScreenModel.State, state: SearchViewModel.State,
fromSourceId: Long?, fromSourceId: Long?,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
@@ -24,7 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceViewModel
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -43,7 +43,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun MigrateSourceScreen( fun MigrateSourceScreen(
state: MigrateSourceScreenModel.State, state: MigrateSourceViewModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onToggleSortingDirection: () -> Unit, onToggleSortingDirection: () -> Unit,
@@ -11,7 +11,7 @@ import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterViewModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -23,7 +23,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable @Composable
fun SourcesFilterScreen( fun SourcesFilterScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: SourcesFilterScreenModel.State.Success, state: SourcesFilterViewModel.State.Success,
onClickLanguage: (String) -> Unit, onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit, onClickSource: (Source) -> Unit,
) { ) {
@@ -55,7 +55,7 @@ fun SourcesFilterScreen(
@Composable @Composable
private fun SourcesFilterContent( private fun SourcesFilterContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: SourcesFilterScreenModel.State.Success, state: SourcesFilterViewModel.State.Success,
onClickLanguage: (String) -> Unit, onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit, onClickSource: (Source) -> Unit,
) { ) {
@@ -21,8 +21,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesViewModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel.Listing
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.model.Pin import tachiyomi.domain.source.model.Pin
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
@@ -40,7 +40,7 @@ import tachiyomi.source.local.isLocal
@Composable @Composable
fun SourcesScreen( fun SourcesScreen(
state: SourcesScreenModel.State, state: SourcesViewModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (Source, Listing) -> Unit, onClickItem: (Source, Listing) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
@@ -19,7 +19,7 @@ import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel import eu.kanade.tachiyomi.ui.history.HistoryViewModel
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
@@ -32,13 +32,13 @@ import java.time.LocalDate
@Composable @Composable
fun HistoryScreen( fun HistoryScreen(
state: HistoryScreenModel.State, state: HistoryViewModel.State,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
onSearchQueryChange: (String?) -> Unit, onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit, onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit, onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, onDialogChange: (HistoryViewModel.Dialog?) -> Unit,
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
@@ -53,7 +53,7 @@ fun HistoryScreen(
title = stringResource(MR.strings.pref_clear_history), title = stringResource(MR.strings.pref_clear_history),
icon = Icons.Outlined.DeleteSweep, icon = Icons.Outlined.DeleteSweep,
onClick = { onClick = {
onDialogChange(HistoryScreenModel.Dialog.DeleteAll) onDialogChange(HistoryViewModel.Dialog.DeleteAll)
}, },
), ),
), ),
@@ -83,7 +83,7 @@ fun HistoryScreen(
contentPadding = contentPadding, contentPadding = contentPadding,
onClickCover = { history -> onClickCover(history.mangaId) }, onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickDelete = { item -> onDialogChange(HistoryViewModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) }, onClickFavorite = { history -> onClickFavorite(history.mangaId) },
) )
} }
@@ -144,8 +144,8 @@ sealed interface HistoryUiModel {
@PreviewLightDark @PreviewLightDark
@Composable @Composable
internal fun HistoryScreenPreviews( internal fun HistoryScreenPreviews(
@PreviewParameter(HistoryScreenModelStateProvider::class) @PreviewParameter(HistoryviewModelStateProvider::class)
historyState: HistoryScreenModel.State, historyState: HistoryViewModel.State,
) { ) {
TachiyomiPreviewTheme { TachiyomiPreviewTheme {
HistoryScreen( HistoryScreen(
@@ -1,7 +1,7 @@
package eu.kanade.presentation.history package eu.kanade.presentation.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel import eu.kanade.tachiyomi.ui.history.HistoryViewModel
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
import java.time.Instant import java.time.Instant
@@ -10,9 +10,9 @@ import java.time.temporal.ChronoUnit
import java.util.Date import java.util.Date
import kotlin.random.Random import kotlin.random.Random
class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenModel.State> { class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.State> {
private val multiPage = HistoryScreenModel.State( private val multiPage = HistoryViewModel.State(
searchQuery = null, searchQuery = null,
list = list =
listOf(HistoryUiModelExamples.headerToday) listOf(HistoryUiModelExamples.headerToday)
@@ -26,7 +26,7 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
dialog = null, dialog = null,
) )
private val shortRecent = HistoryScreenModel.State( private val shortRecent = HistoryViewModel.State(
searchQuery = null, searchQuery = null,
list = listOf( list = listOf(
HistoryUiModelExamples.headerToday, HistoryUiModelExamples.headerToday,
@@ -35,7 +35,7 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
dialog = null, dialog = null,
) )
private val shortFuture = HistoryScreenModel.State( private val shortFuture = HistoryViewModel.State(
searchQuery = null, searchQuery = null,
list = listOf( list = listOf(
HistoryUiModelExamples.headerTomorrow, HistoryUiModelExamples.headerTomorrow,
@@ -44,23 +44,23 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
dialog = null, dialog = null,
) )
private val empty = HistoryScreenModel.State( private val empty = HistoryViewModel.State(
searchQuery = null, searchQuery = null,
list = listOf(), list = listOf(),
dialog = null, dialog = null,
) )
private val loadingWithSearchQuery = HistoryScreenModel.State( private val loadingWithSearchQuery = HistoryViewModel.State(
searchQuery = "Example Search Query", searchQuery = "Example Search Query",
) )
private val loading = HistoryScreenModel.State( private val loading = HistoryViewModel.State(
searchQuery = null, searchQuery = null,
list = null, list = null,
dialog = null, dialog = null,
) )
override val values: Sequence<HistoryScreenModel.State> = sequenceOf( override val values: Sequence<HistoryViewModel.State> = sequenceOf(
multiPage, multiPage,
shortRecent, shortRecent,
shortFuture, shortFuture,
@@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel import eu.kanade.tachiyomi.ui.library.LibrarySettingsViewModel
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@@ -41,7 +41,7 @@ import tachiyomi.presentation.core.util.collectAsState
@Composable @Composable
fun LibrarySettingsDialog( fun LibrarySettingsDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
screenModel: LibrarySettingsScreenModel, viewModel: LibrarySettingsViewModel,
category: Category?, category: Category?,
) { ) {
TabbedDialog( TabbedDialog(
@@ -59,14 +59,14 @@ fun LibrarySettingsDialog(
) { ) {
when (page) { when (page) {
0 -> FilterPage( 0 -> FilterPage(
screenModel = screenModel, viewModel = viewModel,
) )
1 -> SortPage( 1 -> SortPage(
category = category, category = category,
screenModel = screenModel, viewModel = viewModel,
) )
2 -> DisplayPage( 2 -> DisplayPage(
screenModel = screenModel, viewModel = viewModel,
) )
} }
} }
@@ -75,11 +75,11 @@ fun LibrarySettingsDialog(
@Composable @Composable
private fun ColumnScope.FilterPage( private fun ColumnScope.FilterPage(
screenModel: LibrarySettingsScreenModel, viewModel: LibrarySettingsViewModel,
) { ) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded.collectAsState() val filterDownloaded by viewModel.libraryPreferences.filterDownloaded.collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly.collectAsState() val downloadedOnly by viewModel.preferences.downloadedOnly.collectAsState()
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions.collectAsState() val autoUpdateMangaRestrictions by viewModel.libraryPreferences.autoUpdateMangaRestrictions.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_downloaded), label = stringResource(MR.strings.label_downloaded),
@@ -89,64 +89,64 @@ private fun ColumnScope.FilterPage(
filterDownloaded filterDownloaded
}, },
enabled = !downloadedOnly, enabled = !downloadedOnly,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterDownloaded) }, onClick = { viewModel.toggleFilter(LibraryPreferences::filterDownloaded) },
) )
val filterUnread by screenModel.libraryPreferences.filterUnread.collectAsState() val filterUnread by viewModel.libraryPreferences.filterUnread.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_unread), label = stringResource(MR.strings.action_filter_unread),
state = filterUnread, state = filterUnread,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) }, onClick = { viewModel.toggleFilter(LibraryPreferences::filterUnread) },
) )
val filterStarted by screenModel.libraryPreferences.filterStarted.collectAsState() val filterStarted by viewModel.libraryPreferences.filterStarted.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_started), label = stringResource(MR.strings.label_started),
state = filterStarted, state = filterStarted,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) }, onClick = { viewModel.toggleFilter(LibraryPreferences::filterStarted) },
) )
val filterBookmarked by screenModel.libraryPreferences.filterBookmarked.collectAsState() val filterBookmarked by viewModel.libraryPreferences.filterBookmarked.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_bookmarked), label = stringResource(MR.strings.action_filter_bookmarked),
state = filterBookmarked, state = filterBookmarked,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) }, onClick = { viewModel.toggleFilter(LibraryPreferences::filterBookmarked) },
) )
val filterCompleted by screenModel.libraryPreferences.filterCompleted.collectAsState() val filterCompleted by viewModel.libraryPreferences.filterCompleted.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.completed), label = stringResource(MR.strings.completed),
state = filterCompleted, state = filterCompleted,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) }, onClick = { viewModel.toggleFilter(LibraryPreferences::filterCompleted) },
) )
// TODO: re-enable when custom intervals are ready for stable // TODO: re-enable when custom intervals are ready for stable
if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) { if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) {
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom.collectAsState() val filterIntervalCustom by viewModel.libraryPreferences.filterIntervalCustom.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_interval_custom), label = stringResource(MR.strings.action_filter_interval_custom),
state = filterIntervalCustom, state = filterIntervalCustom,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) }, onClick = { viewModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
) )
} }
val trackers by screenModel.trackersFlow.collectAsState() val trackers by viewModel.trackersFlow.collectAsState()
when (trackers.size) { when (trackers.size) {
0 -> { 0 -> {
// No trackers // No trackers
} }
1 -> { 1 -> {
val service = trackers[0] val service = trackers[0]
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() val filterTracker by viewModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_tracked), label = stringResource(MR.strings.action_filter_tracked),
state = filterTracker, state = filterTracker,
onClick = { screenModel.toggleTracker(service.id.toInt()) }, onClick = { viewModel.toggleTracker(service.id.toInt()) },
) )
} }
else -> { else -> {
HeadingItem(MR.strings.action_filter_tracked) HeadingItem(MR.strings.action_filter_tracked)
trackers.map { service -> trackers.map { service ->
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() val filterTracker by viewModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem( TriStateItem(
label = service.name, label = service.name,
state = filterTracker, state = filterTracker,
onClick = { screenModel.toggleTracker(service.id.toInt()) }, onClick = { viewModel.toggleTracker(service.id.toInt()) },
) )
} }
} }
@@ -156,9 +156,9 @@ private fun ColumnScope.FilterPage(
@Composable @Composable
private fun ColumnScope.SortPage( private fun ColumnScope.SortPage(
category: Category?, category: Category?,
screenModel: LibrarySettingsScreenModel, viewModel: LibrarySettingsViewModel,
) { ) {
val trackers by screenModel.trackersFlow.collectAsState() val trackers by viewModel.trackersFlow.collectAsState()
val sortingMode = category.sort.type val sortingMode = category.sort.type
val sortDescending = !category.sort.isAscending val sortDescending = !category.sort.isAscending
@@ -189,7 +189,7 @@ private fun ColumnScope.SortPage(
icon = Icons.Default.Refresh icon = Icons.Default.Refresh
.takeIf { sortingMode == LibrarySort.Type.Random }, .takeIf { sortingMode == LibrarySort.Type.Random },
onClick = { onClick = {
screenModel.setSort(category, mode, LibrarySort.Direction.Ascending) viewModel.setSort(category, mode, LibrarySort.Direction.Ascending)
}, },
) )
return@map return@map
@@ -211,7 +211,7 @@ private fun ColumnScope.SortPage(
LibrarySort.Direction.Ascending LibrarySort.Direction.Ascending
} }
} }
screenModel.setSort(category, mode, direction) viewModel.setSort(category, mode, direction)
}, },
) )
} }
@@ -226,14 +226,14 @@ private val displayModes = listOf(
@Composable @Composable
private fun ColumnScope.DisplayPage( private fun ColumnScope.DisplayPage(
screenModel: LibrarySettingsScreenModel, viewModel: LibrarySettingsViewModel,
) { ) {
val displayMode by screenModel.libraryPreferences.displayMode.collectAsState() val displayMode by viewModel.libraryPreferences.displayMode.collectAsState()
SettingsChipRow(MR.strings.action_display_mode) { SettingsChipRow(MR.strings.action_display_mode) {
displayModes.map { (titleRes, mode) -> displayModes.map { (titleRes, mode) ->
FilterChip( FilterChip(
selected = displayMode == mode, selected = displayMode == mode,
onClick = { screenModel.setDisplayMode(mode) }, onClick = { viewModel.setDisplayMode(mode) },
label = { Text(stringResource(titleRes)) }, label = { Text(stringResource(titleRes)) },
) )
} }
@@ -243,9 +243,9 @@ private fun ColumnScope.DisplayPage(
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val columnPreference = remember { val columnPreference = remember {
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
screenModel.libraryPreferences.landscapeColumns viewModel.libraryPreferences.landscapeColumns
} else { } else {
screenModel.libraryPreferences.portraitColumns viewModel.libraryPreferences.portraitColumns
} }
} }
@@ -267,32 +267,32 @@ private fun ColumnScope.DisplayPage(
HeadingItem(MR.strings.overlay_header) HeadingItem(MR.strings.overlay_header)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_download_badge), label = stringResource(MR.strings.action_display_download_badge),
pref = screenModel.libraryPreferences.downloadBadge, pref = viewModel.libraryPreferences.downloadBadge,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_unread_badge), label = stringResource(MR.strings.action_display_unread_badge),
pref = screenModel.libraryPreferences.unreadBadge, pref = viewModel.libraryPreferences.unreadBadge,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge), label = stringResource(MR.strings.action_display_local_badge),
pref = screenModel.libraryPreferences.localBadge, pref = viewModel.libraryPreferences.localBadge,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_language_badge), label = stringResource(MR.strings.action_display_language_badge),
pref = screenModel.libraryPreferences.languageBadge, pref = viewModel.libraryPreferences.languageBadge,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_show_continue_reading_button), label = stringResource(MR.strings.action_display_show_continue_reading_button),
pref = screenModel.libraryPreferences.showContinueReadingButton, pref = viewModel.libraryPreferences.showContinueReadingButton,
) )
HeadingItem(MR.strings.tabs_header) HeadingItem(MR.strings.tabs_header)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_show_tabs), label = stringResource(MR.strings.action_display_show_tabs),
pref = screenModel.libraryPreferences.categoryTabs, pref = viewModel.libraryPreferences.categoryTabs,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_show_number_of_items), label = stringResource(MR.strings.action_display_show_number_of_items),
pref = screenModel.libraryPreferences.categoryNumberOfItems, pref = viewModel.libraryPreferences.categoryNumberOfItems,
) )
} }
@@ -60,7 +60,7 @@ import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.ui.manga.MangaViewModel
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.missingChaptersCount import tachiyomi.domain.chapter.service.missingChaptersCount
@@ -79,7 +79,7 @@ import java.time.Instant
@Composable @Composable
fun MangaScreen( fun MangaScreen(
state: MangaScreenModel.State.Success, state: MangaViewModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
nextUpdate: Instant?, nextUpdate: Instant?,
isTabletUi: Boolean, isTabletUi: Boolean,
@@ -210,7 +210,7 @@ fun MangaScreen(
@Composable @Composable
private fun MangaScreenSmallImpl( private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success, state: MangaViewModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
nextUpdate: Instant?, nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -452,7 +452,7 @@ private fun MangaScreenSmallImpl(
@Composable @Composable
fun MangaScreenLargeImpl( fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success, state: MangaViewModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
nextUpdate: Instant?, nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -30,9 +30,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
@@ -42,6 +41,7 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.toLong import tachiyomi.core.common.util.lang.toLong
@@ -67,13 +67,13 @@ class ClearDatabaseScreen : Screen() {
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { ClearDatabaseScreenModel() } val viewModel = viewModel<ClearDatabaseViewModel>()
val state by model.state.collectAsState() val state by viewModel.state.collectAsState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
when (val s = state) { when (val s = state) {
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen() is ClearDatabaseViewModel.State.Loading -> LoadingScreen()
is ClearDatabaseScreenModel.State.Ready -> { is ClearDatabaseViewModel.State.Ready -> {
if (s.showConfirmation) { if (s.showConfirmation) {
var keepReadManga by remember { mutableStateOf(true) } var keepReadManga by remember { mutableStateOf(true) }
AlertDialog( AlertDialog(
@@ -108,14 +108,14 @@ class ClearDatabaseScreen : Screen() {
} }
} }
}, },
onDismissRequest = model::hideConfirmation, onDismissRequest = viewModel::hideConfirmation,
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
scope.launchUI { scope.launchUI {
model.removeMangaBySourceId(keepReadManga) viewModel.removeMangaBySourceId(keepReadManga)
model.clearSelection() viewModel.clearSelection()
model.hideConfirmation() viewModel.hideConfirmation()
context.toast(MR.strings.clear_database_completed) context.toast(MR.strings.clear_database_completed)
} }
}, },
@@ -124,7 +124,7 @@ class ClearDatabaseScreen : Screen() {
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = model::hideConfirmation) { TextButton(onClick = viewModel::hideConfirmation) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(MR.strings.action_cancel))
} }
}, },
@@ -143,12 +143,12 @@ class ClearDatabaseScreen : Screen() {
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_all), title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll, icon = Icons.Outlined.SelectAll,
onClick = model::selectAll, onClick = viewModel::selectAll,
), ),
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_inverse), title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack, icon = Icons.Outlined.FlipToBack,
onClick = model::invertSelection, onClick = viewModel::invertSelection,
), ),
), ),
) )
@@ -168,14 +168,14 @@ class ClearDatabaseScreen : Screen() {
contentPadding = contentPadding, contentPadding = contentPadding,
actionLabel = stringResource(MR.strings.action_delete), actionLabel = stringResource(MR.strings.action_delete),
actionEnabled = s.selection.isNotEmpty(), actionEnabled = s.selection.isNotEmpty(),
onClickAction = model::showConfirmation, onClickAction = viewModel::showConfirmation,
) { ) {
items(s.items) { sourceWithCount -> items(s.items) { sourceWithCount ->
ClearDatabaseItem( ClearDatabaseItem(
source = sourceWithCount.source, source = sourceWithCount.source,
count = sourceWithCount.count, count = sourceWithCount.count,
isSelected = s.selection.contains(sourceWithCount.id), isSelected = s.selection.contains(sourceWithCount.id),
onClickSelect = { model.toggleSelection(sourceWithCount.source) }, onClickSelect = { viewModel.toggleSelection(sourceWithCount.source) },
) )
} }
} }
@@ -220,12 +220,12 @@ class ClearDatabaseScreen : Screen() {
} }
} }
private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenModel.State>(State.Loading) { private class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(State.Loading) {
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get() private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get()
private val database: Database = Injekt.get() private val database: Database = Injekt.get()
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
getSourcesWithNonLibraryManga.subscribe() getSourcesWithNonLibraryManga.subscribe()
.collectLatest { list -> .collectLatest { list ->
mutableState.update { old -> mutableState.update { old ->
@@ -5,7 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreConfirmDialog import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreConfirmDialog
@@ -26,11 +26,11 @@ class ExtensionStoresScreen(
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ExtensionStoresScreenModel() } val viewModel = viewModel<ExtensionStoresViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
LaunchedEffect(url) { LaunchedEffect(url) {
url?.let { screenModel.addFromDeeplink(url) } url?.let { viewModel.addFromDeeplink(url) }
} }
if (state is ExtensionStoreScreenState.Loading) { if (state is ExtensionStoreScreenState.Loading) {
@@ -42,12 +42,12 @@ class ExtensionStoresScreen(
ExtensionStoresScreen( ExtensionStoresScreen(
state = successState, state = successState,
onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) }, onClickCreate = { viewModel.showDialog(ExtensionStoreDialog.Create()) },
onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) }, onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) },
onOpenWebsite = { it.contact.website.let(context::openInBrowser) }, onOpenWebsite = { it.contact.website.let(context::openInBrowser) },
onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) }, onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) },
onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) }, onClickDelete = { viewModel.showDialog(ExtensionStoreDialog.Delete(it)) },
onClickRefresh = { screenModel.refreshRepos() }, onClickRefresh = { viewModel.refreshRepos() },
navigateUp = navigator::pop, navigateUp = navigator::pop,
) )
@@ -55,8 +55,8 @@ class ExtensionStoresScreen(
null -> {} null -> {}
is ExtensionStoreDialog.Create -> { is ExtensionStoreDialog.Create -> {
ExtensionStoreCreateDialog( ExtensionStoreCreateDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = viewModel::dismissDialog,
onCreate = { screenModel.createRepo(it) }, onCreate = { viewModel.createRepo(it) },
storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(), storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(),
processing = dialog.processing, processing = dialog.processing,
errorMessage = dialog.errorMessage, errorMessage = dialog.errorMessage,
@@ -64,16 +64,16 @@ class ExtensionStoresScreen(
} }
is ExtensionStoreDialog.Delete -> { is ExtensionStoreDialog.Delete -> {
ExtensionStoreDeleteDialog( ExtensionStoreDeleteDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = viewModel::dismissDialog,
onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) }, onDelete = { viewModel.deleteRepo(dialog.store.indexUrl) },
storeName = dialog.store.name, storeName = dialog.store.name,
storeIndexUrl = dialog.store.indexUrl, storeIndexUrl = dialog.store.indexUrl,
) )
} }
is ExtensionStoreDialog.Confirm -> { is ExtensionStoreDialog.Confirm -> {
ExtensionStoreConfirmDialog( ExtensionStoreConfirmDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = viewModel::dismissDialog,
onCreate = { screenModel.createRepo(dialog.url) }, onCreate = { viewModel.createRepo(dialog.url) },
storeIndexUrl = dialog.url, storeIndexUrl = dialog.url,
storeAlreadyExists = dialog.alreadyExists, storeAlreadyExists = dialog.alreadyExists,
processing = dialog.processing, processing = dialog.processing,
@@ -1,12 +1,12 @@
package eu.kanade.presentation.more.settings.screen.browse package eu.kanade.presentation.more.settings.screen.browse
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import mihon.domain.extension.interactor.AddExtensionStore import mihon.domain.extension.interactor.AddExtensionStore
import mihon.domain.extension.interactor.GetExtensionStores import mihon.domain.extension.interactor.GetExtensionStores
import mihon.domain.extension.interactor.RemoveExtensionStore import mihon.domain.extension.interactor.RemoveExtensionStore
@@ -16,13 +16,13 @@ import tachiyomi.core.common.util.lang.launchIO
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionStoresScreenModel( class ExtensionStoresViewModel(
private val getExtensionStores: GetExtensionStores = Injekt.get(), private val getExtensionStores: GetExtensionStores = Injekt.get(),
private val addExtensionStore: AddExtensionStore = Injekt.get(), private val addExtensionStore: AddExtensionStore = Injekt.get(),
private val removeExtensionStore: RemoveExtensionStore = Injekt.get(), private val removeExtensionStore: RemoveExtensionStore = Injekt.get(),
private val updateExtensionStores: UpdateExtensionStores = Injekt.get(), private val updateExtensionStores: UpdateExtensionStores = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
) : StateScreenModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) { ) : StateViewModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
private inline fun updateSuccessState( private inline fun updateSuccessState(
func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success, func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success,
@@ -36,7 +36,7 @@ class ExtensionStoresScreenModel(
} }
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
getExtensionStores.subscribe() getExtensionStores.subscribe()
.collectLatest { stores -> .collectLatest { stores ->
mutableState.update { mutableState.update {
@@ -55,7 +55,7 @@ class ExtensionStoresScreenModel(
* @param baseUrl The baseUrl of the repo to create. * @param baseUrl The baseUrl of the repo to create.
*/ */
fun createRepo(baseUrl: String) { fun createRepo(baseUrl: String) {
screenModelScope.launch { viewModelScope.launch {
updateSuccessState { updateSuccessState {
it.copy( it.copy(
dialog = when (it.dialog) { dialog = when (it.dialog) {
@@ -97,7 +97,7 @@ class ExtensionStoresScreenModel(
val status = state.value val status = state.value
if (status is ExtensionStoreScreenState.Success) { if (status is ExtensionStoreScreenState.Success) {
screenModelScope.launchIO { viewModelScope.launchIO {
updateExtensionStores() updateExtensionStores()
} }
} }
@@ -107,7 +107,7 @@ class ExtensionStoresScreenModel(
* Deletes the given repo from the database * Deletes the given repo from the database
*/ */
fun deleteRepo(baseUrl: String) { fun deleteRepo(baseUrl: String) {
screenModelScope.launchIO { viewModelScope.launchIO {
removeExtensionStore(baseUrl) removeExtensionStore(baseUrl)
extensionManager.findAvailableExtensions() extensionManager.findAvailableExtensions()
} }
@@ -11,8 +11,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@@ -24,6 +23,7 @@ import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.LazyColumnWithAction
@@ -37,8 +37,8 @@ class CreateBackupScreen : Screen() {
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { CreateBackupScreenModel() } val viewModel = viewModel<CreateBackupViewModel>()
val state by model.state.collectAsState() val state by viewModel.state.collectAsState()
val chooseBackupDir = rememberLauncherForActivityResult( val chooseBackupDir = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/*"), contract = ActivityResultContracts.CreateDocument("application/*"),
@@ -49,7 +49,7 @@ class CreateBackupScreen : Screen() {
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION, Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
) )
model.createBackup(context, it) viewModel.createBackup(context, it)
navigator.pop() navigator.pop()
} }
} }
@@ -87,13 +87,13 @@ class CreateBackupScreen : Screen() {
item { item {
SectionCard(MR.strings.label_library) { SectionCard(MR.strings.label_library) {
Options(BackupOptions.libraryOptions, state, model) Options(BackupOptions.libraryOptions, state, viewModel)
} }
} }
item { item {
SectionCard(MR.strings.label_settings) { SectionCard(MR.strings.label_settings) {
Options(BackupOptions.settingsOptions, state, model) Options(BackupOptions.settingsOptions, state, viewModel)
} }
} }
} }
@@ -103,8 +103,8 @@ class CreateBackupScreen : Screen() {
@Composable @Composable
private fun Options( private fun Options(
options: List<BackupOptions.Entry>, options: List<BackupOptions.Entry>,
state: CreateBackupScreenModel.State, state: CreateBackupViewModel.State,
model: CreateBackupScreenModel, model: CreateBackupViewModel,
) { ) {
options.forEach { option -> options.forEach { option ->
LabeledCheckbox( LabeledCheckbox(
@@ -119,7 +119,7 @@ class CreateBackupScreen : Screen() {
} }
} }
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) { private class CreateBackupViewModel : StateViewModel<CreateBackupViewModel.State>(State()) {
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) { fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
mutableState.update { mutableState.update {
@@ -1,5 +1,6 @@
package eu.kanade.presentation.more.settings.screen.data package eu.kanade.presentation.more.settings.screen.data
import android.app.Application
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -14,14 +15,15 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.core.net.toUri import androidx.core.net.toUri
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@@ -32,6 +34,7 @@ import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.LazyColumnWithAction
@@ -39,6 +42,8 @@ import tachiyomi.presentation.core.components.SectionCard
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class RestoreBackupScreen( class RestoreBackupScreen(
private val uri: String, private val uri: String,
@@ -46,10 +51,14 @@ class RestoreBackupScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) } val viewModel = viewModel<RestoreBackupViewModel>(
val state by model.state.collectAsState() factory = RestoreBackupViewModel.Factory,
extras = CreationExtras {
set(RestoreBackupViewModel.URI_KEY, uri)
},
)
val state by viewModel.state.collectAsState()
Scaffold( Scaffold(
topBar = { topBar = {
@@ -65,7 +74,7 @@ class RestoreBackupScreen(
actionLabel = stringResource(MR.strings.action_restore), actionLabel = stringResource(MR.strings.action_restore),
actionEnabled = state.canRestore && state.options.canRestore(), actionEnabled = state.canRestore && state.options.canRestore(),
onClickAction = { onClickAction = {
model.startRestore() viewModel.startRestore()
navigator.pop() navigator.pop()
}, },
) { ) {
@@ -83,7 +92,7 @@ class RestoreBackupScreen(
label = stringResource(option.label), label = stringResource(option.label),
checked = option.getter(state.options), checked = option.getter(state.options),
onCheckedChange = { onCheckedChange = {
model.toggle(option.setter, it) viewModel.toggle(option.setter, it)
}, },
) )
} }
@@ -164,10 +173,23 @@ class RestoreBackupScreen(
} }
} }
private class RestoreBackupScreenModel( private class RestoreBackupViewModel(
private val context: Context, private val context: Context,
private val uri: String, private val uri: String,
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) { ) : StateViewModel<RestoreBackupViewModel.State>(State()) {
companion object {
val URI_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
RestoreBackupViewModel(
context = Injekt.get<Application>(),
uri = get(URI_KEY)!!,
)
}
}
}
init { init {
validate(uri.toUri()) validate(uri.toUri())
@@ -1,5 +1,6 @@
package eu.kanade.presentation.more.settings.screen.debug package eu.kanade.presentation.more.settings.screen.debug
import android.app.Application
import android.content.Context import android.content.Context
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -18,17 +19,19 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkQuery import androidx.work.WorkQuery
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.util.lang.toDateTimestampString import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
@@ -56,10 +59,10 @@ class WorkerInfoScreen : Screen() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { Model(context) } val viewModel = viewModel<WorkerInfoViewModel>(factory = WorkerInfoViewModel.Factory)
val enqueued by screenModel.enqueued.collectAsState() val enqueued by viewModel.enqueued.collectAsState()
val finished by screenModel.finished.collectAsState() val finished by viewModel.finished.collectAsState()
val running by screenModel.running.collectAsState() val running by viewModel.running.collectAsState()
Scaffold( Scaffold(
topBar = { topBar = {
@@ -117,7 +120,20 @@ class WorkerInfoScreen : Screen() {
) )
} }
private class Model(context: Context) : ScreenModel { private class WorkerInfoViewModel(
context: Context,
) : ViewModel() {
companion object {
val Factory = viewModelFactory {
initializer {
WorkerInfoViewModel(
context = Injekt.get<Application>(),
)
}
}
}
private val workManager = context.workManager private val workManager = context.workManager
val finished = workManager val finished = workManager
@@ -125,17 +141,17 @@ class WorkerInfoScreen : Screen() {
WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED), WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED),
) )
.map(::constructString) .map(::constructString)
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
val running = workManager val running = workManager
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING)) .getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.map(::constructString) .map(::constructString)
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
val enqueued = workManager val enqueued = workManager
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED)) .getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
.map(::constructString) .map(::constructString)
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
private fun constructString(list: List<WorkInfo>) = buildString { private fun constructString(list: List<WorkInfo>) = buildString {
if (list.isEmpty()) { if (list.isEmpty()) {
@@ -18,7 +18,7 @@ import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.reader.components.ModeSelectionDialog import eu.kanade.presentation.reader.components.ModeSelectionDialog
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SettingsIconGrid import tachiyomi.presentation.core.components.SettingsIconGrid
import tachiyomi.presentation.core.components.material.IconToggleButton import tachiyomi.presentation.core.components.material.IconToggleButton
@@ -29,17 +29,17 @@ private val ReaderOrientationsWithoutDefault = ReaderOrientation.entries - Reade
@Composable @Composable
fun OrientationSelectDialog( fun OrientationSelectDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
screenModel: ReaderSettingsScreenModel, viewModel: ReaderSettingsViewModel,
onChange: (StringResource) -> Unit, onChange: (StringResource) -> Unit,
) { ) {
val manga by screenModel.mangaFlow.collectAsState() val manga by viewModel.mangaFlow.collectAsState()
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) } val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
AdaptiveSheet(onDismissRequest = onDismissRequest) { AdaptiveSheet(onDismissRequest = onDismissRequest) {
DialogContent( DialogContent(
orientation = orientation, orientation = orientation,
onChangeOrientation = { onChangeOrientation = {
screenModel.onChangeOrientation(it) viewModel.onChangeOrientation(it)
onChange(it.stringRes) onChange(it.stringRes)
onDismissRequest() onDismissRequest()
}, },
@@ -19,7 +19,7 @@ import eu.kanade.domain.manga.model.readingMode
import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.reader.components.ModeSelectionDialog import eu.kanade.presentation.reader.components.ModeSelectionDialog
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SettingsIconGrid import tachiyomi.presentation.core.components.SettingsIconGrid
@@ -31,17 +31,17 @@ private val ReadingModesWithoutDefault = ReadingMode.entries - ReadingMode.DEFAU
@Composable @Composable
fun ReadingModeSelectDialog( fun ReadingModeSelectDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
screenModel: ReaderSettingsScreenModel, viewModel: ReaderSettingsViewModel,
onChange: (StringResource) -> Unit, onChange: (StringResource) -> Unit,
) { ) {
val manga by screenModel.mangaFlow.collectAsState() val manga by viewModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) } val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
AdaptiveSheet(onDismissRequest = onDismissRequest) { AdaptiveSheet(onDismissRequest = onDismissRequest) {
DialogContent( DialogContent(
readingMode = readingMode, readingMode = readingMode,
onChangeReadingMode = { onChangeReadingMode = {
screenModel.onChangeReadingMode(it) viewModel.onChangeReadingMode(it)
onChange(it.stringRes) onChange(it.stringRes)
onDismissRequest() onDismissRequest()
}, },
@@ -11,7 +11,7 @@ import androidx.core.graphics.blue
import androidx.core.graphics.green import androidx.core.graphics.green
import androidx.core.graphics.red import androidx.core.graphics.red
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.Companion.ColorFilterMode import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.Companion.ColorFilterMode
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
@@ -21,11 +21,11 @@ import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
@Composable @Composable
internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) { internal fun ColumnScope.ColorFilterPage(viewModel: ReaderSettingsViewModel) {
val customBrightness by screenModel.preferences.customBrightness.collectAsState() val customBrightness by viewModel.preferences.customBrightness.collectAsState()
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_custom_brightness), label = stringResource(MR.strings.pref_custom_brightness),
pref = screenModel.preferences.customBrightness, pref = viewModel.preferences.customBrightness,
) )
/* /*
@@ -35,31 +35,31 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
* 0 sets system brightness and hides the overlay. * 0 sets system brightness and hides the overlay.
*/ */
if (customBrightness) { if (customBrightness) {
val customBrightnessValue by screenModel.preferences.customBrightnessValue.collectAsState() val customBrightnessValue by viewModel.preferences.customBrightnessValue.collectAsState()
SliderItem( SliderItem(
value = customBrightnessValue, value = customBrightnessValue,
valueRange = -75..100, valueRange = -75..100,
steps = 0, steps = 0,
label = stringResource(MR.strings.pref_custom_brightness), label = stringResource(MR.strings.pref_custom_brightness),
onChange = { screenModel.preferences.customBrightnessValue.set(it) }, onChange = { viewModel.preferences.customBrightnessValue.set(it) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
} }
val colorFilter by screenModel.preferences.colorFilter.collectAsState() val colorFilter by viewModel.preferences.colorFilter.collectAsState()
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_custom_color_filter), label = stringResource(MR.strings.pref_custom_color_filter),
pref = screenModel.preferences.colorFilter, pref = viewModel.preferences.colorFilter,
) )
if (colorFilter) { if (colorFilter) {
val colorFilterValue by screenModel.preferences.colorFilterValue.collectAsState() val colorFilterValue by viewModel.preferences.colorFilterValue.collectAsState()
SliderItem( SliderItem(
value = colorFilterValue.red, value = colorFilterValue.red,
valueRange = 0..255, valueRange = 0..255,
steps = 0, steps = 0,
label = stringResource(MR.strings.color_filter_r_value), label = stringResource(MR.strings.color_filter_r_value),
onChange = { newRValue -> onChange = { newRValue ->
screenModel.preferences.colorFilterValue.getAndSet { viewModel.preferences.colorFilterValue.getAndSet {
getColorValue(it, newRValue, RED_MASK, 16) getColorValue(it, newRValue, RED_MASK, 16)
} }
}, },
@@ -71,7 +71,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
steps = 0, steps = 0,
label = stringResource(MR.strings.color_filter_g_value), label = stringResource(MR.strings.color_filter_g_value),
onChange = { newGValue -> onChange = { newGValue ->
screenModel.preferences.colorFilterValue.getAndSet { viewModel.preferences.colorFilterValue.getAndSet {
getColorValue(it, newGValue, GREEN_MASK, 8) getColorValue(it, newGValue, GREEN_MASK, 8)
} }
}, },
@@ -83,7 +83,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
steps = 0, steps = 0,
label = stringResource(MR.strings.color_filter_b_value), label = stringResource(MR.strings.color_filter_b_value),
onChange = { newBValue -> onChange = { newBValue ->
screenModel.preferences.colorFilterValue.getAndSet { viewModel.preferences.colorFilterValue.getAndSet {
getColorValue(it, newBValue, BLUE_MASK, 0) getColorValue(it, newBValue, BLUE_MASK, 0)
} }
}, },
@@ -95,19 +95,19 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
steps = 0, steps = 0,
label = stringResource(MR.strings.color_filter_a_value), label = stringResource(MR.strings.color_filter_a_value),
onChange = { newAValue -> onChange = { newAValue ->
screenModel.preferences.colorFilterValue.getAndSet { viewModel.preferences.colorFilterValue.getAndSet {
getColorValue(it, newAValue, ALPHA_MASK, 24) getColorValue(it, newAValue, ALPHA_MASK, 24)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
val colorFilterMode by screenModel.preferences.colorFilterMode.collectAsState() val colorFilterMode by viewModel.preferences.colorFilterMode.collectAsState()
SettingsChipRow(MR.strings.pref_color_filter_mode) { SettingsChipRow(MR.strings.pref_color_filter_mode) {
ColorFilterMode.mapIndexed { index, it -> ColorFilterMode.mapIndexed { index, it ->
FilterChip( FilterChip(
selected = colorFilterMode == index, selected = colorFilterMode == index,
onClick = { screenModel.preferences.colorFilterMode.set(index) }, onClick = { viewModel.preferences.colorFilterMode.set(index) },
label = { Text(stringResource(it.first)) }, label = { Text(stringResource(it.first)) },
) )
} }
@@ -116,11 +116,11 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_grayscale), label = stringResource(MR.strings.pref_grayscale),
pref = screenModel.preferences.grayscale, pref = viewModel.preferences.grayscale,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_inverted_colors), label = stringResource(MR.strings.pref_inverted_colors),
pref = screenModel.preferences.invertedColors, pref = viewModel.preferences.invertedColors,
) )
} }
@@ -8,7 +8,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.util.system.hasDisplayCutout import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -33,25 +33,25 @@ private val flashColors = listOf(
) )
@Composable @Composable
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) { internal fun ColumnScope.GeneralPage(viewModel: ReaderSettingsViewModel) {
val readerTheme by screenModel.preferences.readerTheme.collectAsState() val readerTheme by viewModel.preferences.readerTheme.collectAsState()
val flashPageState by screenModel.preferences.flashOnPageChange.collectAsState() val flashPageState by viewModel.preferences.flashOnPageChange.collectAsState()
val flashMillisPref = screenModel.preferences.flashDurationMillis val flashMillisPref = viewModel.preferences.flashDurationMillis
val flashMillis by flashMillisPref.collectAsState() val flashMillis by flashMillisPref.collectAsState()
val flashIntervalPref = screenModel.preferences.flashPageInterval val flashIntervalPref = viewModel.preferences.flashPageInterval
val flashInterval by flashIntervalPref.collectAsState() val flashInterval by flashIntervalPref.collectAsState()
val flashColorPref = screenModel.preferences.flashColor val flashColorPref = viewModel.preferences.flashColor
val flashColor by flashColorPref.collectAsState() val flashColor by flashColorPref.collectAsState()
SettingsChipRow(MR.strings.pref_reader_theme) { SettingsChipRow(MR.strings.pref_reader_theme) {
themes.map { (labelRes, value) -> themes.map { (labelRes, value) ->
FilterChip( FilterChip(
selected = readerTheme == value, selected = readerTheme == value,
onClick = { screenModel.preferences.readerTheme.set(value) }, onClick = { viewModel.preferences.readerTheme.set(value) },
label = { Text(stringResource(labelRes)) }, label = { Text(stringResource(labelRes)) },
) )
} }
@@ -59,10 +59,10 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_show_page_number), label = stringResource(MR.strings.pref_show_page_number),
pref = screenModel.preferences.showPageNumber, pref = viewModel.preferences.showPageNumber,
) )
val verticalNavigatorModes by screenModel.preferences.verticalNavigator.collectAsState() val verticalNavigatorModes by viewModel.preferences.verticalNavigator.collectAsState()
SettingsChipRow(MR.strings.pref_vertical_navigator) { SettingsChipRow(MR.strings.pref_vertical_navigator) {
ReadingMode.entries.filter { it != ReadingMode.DEFAULT }.forEach { mode -> ReadingMode.entries.filter { it != ReadingMode.DEFAULT }.forEach { mode ->
@@ -74,7 +74,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
} else { } else {
verticalNavigatorModes + mode verticalNavigatorModes + mode
} }
screenModel.preferences.verticalNavigator.set(newModes) viewModel.preferences.verticalNavigator.set(newModes)
}, },
label = { Text(stringResource(mode.stringRes)) }, label = { Text(stringResource(mode.stringRes)) },
) )
@@ -82,12 +82,12 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
} }
if (verticalNavigatorModes.isNotEmpty()) { if (verticalNavigatorModes.isNotEmpty()) {
val verticalNavigatorHeightPref = screenModel.preferences.verticalNavigatorHeight val verticalNavigatorHeightPref = viewModel.preferences.verticalNavigatorHeight
val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState() val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState()
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left), label = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
pref = screenModel.preferences.verticalNavigatorOnLeft, pref = viewModel.preferences.verticalNavigatorOnLeft,
) )
SliderItem( SliderItem(
@@ -101,40 +101,40 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_fullscreen), label = stringResource(MR.strings.pref_fullscreen),
pref = screenModel.preferences.fullscreen, pref = viewModel.preferences.fullscreen,
) )
val isFullscreen by screenModel.preferences.fullscreen.collectAsState() val isFullscreen by viewModel.preferences.fullscreen.collectAsState()
if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) { if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) {
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_cutout_short), label = stringResource(MR.strings.pref_cutout_short),
pref = screenModel.preferences.drawUnderCutout, pref = viewModel.preferences.drawUnderCutout,
) )
} }
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_keep_screen_on), label = stringResource(MR.strings.pref_keep_screen_on),
pref = screenModel.preferences.keepScreenOn, pref = viewModel.preferences.keepScreenOn,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_read_with_long_tap), label = stringResource(MR.strings.pref_read_with_long_tap),
pref = screenModel.preferences.readWithLongTap, pref = viewModel.preferences.readWithLongTap,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_always_show_chapter_transition), label = stringResource(MR.strings.pref_always_show_chapter_transition),
pref = screenModel.preferences.alwaysShowChapterTransition, pref = viewModel.preferences.alwaysShowChapterTransition,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_page_transitions), label = stringResource(MR.strings.pref_page_transitions),
pref = screenModel.preferences.pageTransitions, pref = viewModel.preferences.pageTransitions,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_flash_page), label = stringResource(MR.strings.pref_flash_page),
pref = screenModel.preferences.flashOnPageChange, pref = viewModel.preferences.flashOnPageChange,
) )
if (flashPageState) { if (flashPageState) {
SliderItem( SliderItem(
@@ -14,7 +14,7 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -23,7 +23,7 @@ fun ReaderSettingsDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onShowMenus: () -> Unit, onShowMenus: () -> Unit,
onHideMenus: () -> Unit, onHideMenus: () -> Unit,
screenModel: ReaderSettingsScreenModel, viewModel: ReaderSettingsViewModel,
) { ) {
val tabTitles = listOf( val tabTitles = listOf(
stringResource(MR.strings.pref_category_reading_mode), stringResource(MR.strings.pref_category_reading_mode),
@@ -60,9 +60,9 @@ fun ReaderSettingsDialog(
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
when (page) { when (page) {
0 -> ReadingModePage(screenModel) 0 -> ReadingModePage(viewModel)
1 -> GeneralPage(screenModel) 1 -> GeneralPage(viewModel)
2 -> ColorFilterPage(screenModel) 2 -> ColorFilterPage(viewModel)
} }
} }
} }
@@ -12,7 +12,7 @@ import eu.kanade.domain.manga.model.readerOrientation
import eu.kanade.domain.manga.model.readingMode import eu.kanade.domain.manga.model.readingMode
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -25,16 +25,16 @@ import tachiyomi.presentation.core.util.collectAsState
import java.text.NumberFormat import java.text.NumberFormat
@Composable @Composable
internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) { internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
HeadingItem(MR.strings.pref_category_for_this_series) HeadingItem(MR.strings.pref_category_for_this_series)
val manga by screenModel.mangaFlow.collectAsState() val manga by viewModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) } val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
SettingsChipRow(MR.strings.pref_category_reading_mode) { SettingsChipRow(MR.strings.pref_category_reading_mode) {
ReadingMode.entries.map { ReadingMode.entries.map {
FilterChip( FilterChip(
selected = it == readingMode, selected = it == readingMode,
onClick = { screenModel.onChangeReadingMode(it) }, onClick = { viewModel.onChangeReadingMode(it) },
label = { Text(stringResource(it.stringRes)) }, label = { Text(stringResource(it.stringRes)) },
) )
} }
@@ -45,50 +45,50 @@ internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel)
ReaderOrientation.entries.map { ReaderOrientation.entries.map {
FilterChip( FilterChip(
selected = it == orientation, selected = it == orientation,
onClick = { screenModel.onChangeOrientation(it) }, onClick = { viewModel.onChangeOrientation(it) },
label = { Text(stringResource(it.stringRes)) }, label = { Text(stringResource(it.stringRes)) },
) )
} }
} }
val viewer by screenModel.viewerFlow.collectAsState() val viewer by viewModel.viewerFlow.collectAsState()
if (viewer is WebtoonViewer) { if (viewer is WebtoonViewer) {
WebtoonViewerSettings(screenModel) WebtoonViewerSettings(viewModel)
} else { } else {
PagerViewerSettings(screenModel) PagerViewerSettings(viewModel)
} }
} }
@Composable @Composable
private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) { private fun ColumnScope.PagerViewerSettings(viewModel: ReaderSettingsViewModel) {
HeadingItem(MR.strings.pager_viewer) HeadingItem(MR.strings.pager_viewer)
val navigationModePager by screenModel.preferences.navigationModePager.collectAsState() val navigationModePager by viewModel.preferences.navigationModePager.collectAsState()
val pagerNavInverted by screenModel.preferences.pagerNavInverted.collectAsState() val pagerNavInverted by viewModel.preferences.pagerNavInverted.collectAsState()
TapZonesItems( TapZonesItems(
selected = navigationModePager, selected = navigationModePager,
onSelect = screenModel.preferences.navigationModePager::set, onSelect = viewModel.preferences.navigationModePager::set,
invertMode = pagerNavInverted, invertMode = pagerNavInverted,
onSelectInvertMode = screenModel.preferences.pagerNavInverted::set, onSelectInvertMode = viewModel.preferences.pagerNavInverted::set,
) )
val imageScaleType by screenModel.preferences.imageScaleType.collectAsState() val imageScaleType by viewModel.preferences.imageScaleType.collectAsState()
SettingsChipRow(MR.strings.pref_image_scale_type) { SettingsChipRow(MR.strings.pref_image_scale_type) {
ReaderPreferences.ImageScaleType.mapIndexed { index, it -> ReaderPreferences.ImageScaleType.mapIndexed { index, it ->
FilterChip( FilterChip(
selected = imageScaleType == index + 1, selected = imageScaleType == index + 1,
onClick = { screenModel.preferences.imageScaleType.set(index + 1) }, onClick = { viewModel.preferences.imageScaleType.set(index + 1) },
label = { Text(stringResource(it)) }, label = { Text(stringResource(it)) },
) )
} }
} }
val zoomStart by screenModel.preferences.zoomStart.collectAsState() val zoomStart by viewModel.preferences.zoomStart.collectAsState()
SettingsChipRow(MR.strings.pref_zoom_start) { SettingsChipRow(MR.strings.pref_zoom_start) {
ReaderPreferences.ZoomStart.mapIndexed { index, it -> ReaderPreferences.ZoomStart.mapIndexed { index, it ->
FilterChip( FilterChip(
selected = zoomStart == index + 1, selected = zoomStart == index + 1,
onClick = { screenModel.preferences.zoomStart.set(index + 1) }, onClick = { viewModel.preferences.zoomStart.set(index + 1) },
label = { Text(stringResource(it)) }, label = { Text(stringResource(it)) },
) )
} }
@@ -96,111 +96,111 @@ private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenMod
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_crop_borders), label = stringResource(MR.strings.pref_crop_borders),
pref = screenModel.preferences.cropBorders, pref = viewModel.preferences.cropBorders,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_landscape_zoom), label = stringResource(MR.strings.pref_landscape_zoom),
pref = screenModel.preferences.landscapeZoom, pref = viewModel.preferences.landscapeZoom,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_navigate_pan), label = stringResource(MR.strings.pref_navigate_pan),
pref = screenModel.preferences.navigateToPan, pref = viewModel.preferences.navigateToPan,
) )
val dualPageSplitPaged by screenModel.preferences.dualPageSplitPaged.collectAsState() val dualPageSplitPaged by viewModel.preferences.dualPageSplitPaged.collectAsState()
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_dual_page_split), label = stringResource(MR.strings.pref_dual_page_split),
pref = screenModel.preferences.dualPageSplitPaged, pref = viewModel.preferences.dualPageSplitPaged,
) )
if (dualPageSplitPaged) { if (dualPageSplitPaged) {
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_dual_page_invert), label = stringResource(MR.strings.pref_dual_page_invert),
pref = screenModel.preferences.dualPageInvertPaged, pref = viewModel.preferences.dualPageInvertPaged,
) )
} }
val dualPageRotateToFit by screenModel.preferences.dualPageRotateToFit.collectAsState() val dualPageRotateToFit by viewModel.preferences.dualPageRotateToFit.collectAsState()
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_page_rotate), label = stringResource(MR.strings.pref_page_rotate),
pref = screenModel.preferences.dualPageRotateToFit, pref = viewModel.preferences.dualPageRotateToFit,
) )
if (dualPageRotateToFit) { if (dualPageRotateToFit) {
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_page_rotate_invert), label = stringResource(MR.strings.pref_page_rotate_invert),
pref = screenModel.preferences.dualPageRotateToFitInvert, pref = viewModel.preferences.dualPageRotateToFitInvert,
) )
} }
} }
@Composable @Composable
private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) { private fun ColumnScope.WebtoonViewerSettings(viewModel: ReaderSettingsViewModel) {
val numberFormat = remember { NumberFormat.getPercentInstance() } val numberFormat = remember { NumberFormat.getPercentInstance() }
HeadingItem(MR.strings.webtoon_viewer) HeadingItem(MR.strings.webtoon_viewer)
val navigationModeWebtoon by screenModel.preferences.navigationModeWebtoon.collectAsState() val navigationModeWebtoon by viewModel.preferences.navigationModeWebtoon.collectAsState()
val webtoonNavInverted by screenModel.preferences.webtoonNavInverted.collectAsState() val webtoonNavInverted by viewModel.preferences.webtoonNavInverted.collectAsState()
TapZonesItems( TapZonesItems(
selected = navigationModeWebtoon, selected = navigationModeWebtoon,
onSelect = screenModel.preferences.navigationModeWebtoon::set, onSelect = viewModel.preferences.navigationModeWebtoon::set,
invertMode = webtoonNavInverted, invertMode = webtoonNavInverted,
onSelectInvertMode = screenModel.preferences.webtoonNavInverted::set, onSelectInvertMode = viewModel.preferences.webtoonNavInverted::set,
) )
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding.collectAsState() val webtoonSidePadding by viewModel.preferences.webtoonSidePadding.collectAsState()
SliderItem( SliderItem(
value = webtoonSidePadding, value = webtoonSidePadding,
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX }, valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
label = stringResource(MR.strings.pref_webtoon_side_padding), label = stringResource(MR.strings.pref_webtoon_side_padding),
valueString = numberFormat.format(webtoonSidePadding / 100f), valueString = numberFormat.format(webtoonSidePadding / 100f),
onChange = { onChange = {
screenModel.preferences.webtoonSidePadding.set(it) viewModel.preferences.webtoonSidePadding.set(it)
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_crop_borders), label = stringResource(MR.strings.pref_crop_borders),
pref = screenModel.preferences.cropBordersWebtoon, pref = viewModel.preferences.cropBordersWebtoon,
) )
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon.collectAsState() val dualPageSplitWebtoon by viewModel.preferences.dualPageSplitWebtoon.collectAsState()
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_dual_page_split), label = stringResource(MR.strings.pref_dual_page_split),
pref = screenModel.preferences.dualPageSplitWebtoon, pref = viewModel.preferences.dualPageSplitWebtoon,
) )
if (dualPageSplitWebtoon) { if (dualPageSplitWebtoon) {
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_dual_page_invert), label = stringResource(MR.strings.pref_dual_page_invert),
pref = screenModel.preferences.dualPageInvertWebtoon, pref = viewModel.preferences.dualPageInvertWebtoon,
) )
} }
val dualPageRotateToFitWebtoon by screenModel.preferences.dualPageRotateToFitWebtoon.collectAsState() val dualPageRotateToFitWebtoon by viewModel.preferences.dualPageRotateToFitWebtoon.collectAsState()
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_page_rotate), label = stringResource(MR.strings.pref_page_rotate),
pref = screenModel.preferences.dualPageRotateToFitWebtoon, pref = viewModel.preferences.dualPageRotateToFitWebtoon,
) )
if (dualPageRotateToFitWebtoon) { if (dualPageRotateToFitWebtoon) {
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_page_rotate_invert), label = stringResource(MR.strings.pref_page_rotate_invert),
pref = screenModel.preferences.dualPageRotateToFitInvertWebtoon, pref = viewModel.preferences.dualPageRotateToFitInvertWebtoon,
) )
} }
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_double_tap_zoom), label = stringResource(MR.strings.pref_double_tap_zoom),
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled, pref = viewModel.preferences.webtoonDoubleTapZoomEnabled,
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out), label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
pref = screenModel.preferences.webtoonDisableZoomOut, pref = viewModel.preferences.webtoonDisableZoomOut,
) )
} }
@@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsViewModel
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
import tachiyomi.domain.updates.service.UpdatesPreferences import tachiyomi.domain.updates.service.UpdatesPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -32,7 +32,7 @@ import tachiyomi.presentation.core.util.collectAsState
@Composable @Composable
fun UpdatesFilterDialog( fun UpdatesFilterDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
screenModel: UpdatesSettingsScreenModel, viewModel: UpdatesSettingsViewModel,
) { ) {
TabbedDialog( TabbedDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@@ -45,48 +45,48 @@ fun UpdatesFilterDialog(
.padding(vertical = TabbedDialogPaddings.Vertical) .padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
FilterSheet(screenModel = screenModel) FilterSheet(viewModel = viewModel)
} }
} }
} }
@Composable @Composable
private fun ColumnScope.FilterSheet( private fun ColumnScope.FilterSheet(
screenModel: UpdatesSettingsScreenModel, viewModel: UpdatesSettingsViewModel,
) { ) {
val filterDownloaded by screenModel.updatesPreferences.filterDownloaded.collectAsState() val filterDownloaded by viewModel.updatesPreferences.filterDownloaded.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_downloaded), label = stringResource(MR.strings.label_downloaded),
state = filterDownloaded, state = filterDownloaded,
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) }, onClick = { viewModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
) )
val filterUnread by screenModel.updatesPreferences.filterUnread.collectAsState() val filterUnread by viewModel.updatesPreferences.filterUnread.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_unread), label = stringResource(MR.strings.action_filter_unread),
state = filterUnread, state = filterUnread,
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) }, onClick = { viewModel.toggleFilter(UpdatesPreferences::filterUnread) },
) )
val filterStarted by screenModel.updatesPreferences.filterStarted.collectAsState() val filterStarted by viewModel.updatesPreferences.filterStarted.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_started), label = stringResource(MR.strings.label_started),
state = filterStarted, state = filterStarted,
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) }, onClick = { viewModel.toggleFilter(UpdatesPreferences::filterStarted) },
) )
val filterBookmarked by screenModel.updatesPreferences.filterBookmarked.collectAsState() val filterBookmarked by viewModel.updatesPreferences.filterBookmarked.collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_bookmarked), label = stringResource(MR.strings.action_filter_bookmarked),
state = filterBookmarked, state = filterBookmarked,
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) }, onClick = { viewModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
) )
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small)) HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small))
val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators.collectAsState() val filterExcludedScanlators by viewModel.updatesPreferences.filterExcludedScanlators.collectAsState()
fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators.getAndSet { !it } fun toggleScanlatorFilter() = viewModel.updatesPreferences.filterExcludedScanlators.getAndSet { !it }
Row( Row(
modifier = Modifier modifier = Modifier
@@ -29,7 +29,7 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaBottomActionMenu
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.updates.UpdatesItem import eu.kanade.tachiyomi.ui.updates.UpdatesItem
import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel import eu.kanade.tachiyomi.ui.updates.UpdatesViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -45,7 +45,7 @@ import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun UpdateScreen( fun UpdateScreen(
state: UpdatesScreenModel.State, state: UpdatesViewModel.State,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
lastUpdated: Long, lastUpdated: Long,
onClickCover: (UpdatesItem) -> Unit, onClickCover: (UpdatesItem) -> Unit,
@@ -8,20 +8,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.ScreenModelStore
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.core.stack.StackEvent import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransitionContent import cafe.adriel.voyager.transitions.ScreenTransitionContent
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.plus
import soup.compose.material.motion.animation.materialSharedAxisX import soup.compose.material.motion.animation.materialSharedAxisX
import soup.compose.material.motion.animation.rememberSlideDistance import soup.compose.material.motion.animation.rememberSlideDistance
@@ -39,18 +31,6 @@ abstract class Screen : Screen {
override val key: ScreenKey = uniqueScreenKey override val key: ScreenKey = uniqueScreenKey
} }
/**
* A variant of ScreenModel.coroutineScope except with the IO dispatcher instead of the
* main dispatcher.
*/
val ScreenModel.ioCoroutineScope: CoroutineScope
get() = ScreenModelStore.getOrPutDependency(
screenModel = this,
name = "ScreenModelIoCoroutineScope",
factory = { key -> CoroutineScope(Dispatchers.IO + SupervisorJob()) + CoroutineName(key) },
onDispose = { scope -> scope.cancel() },
)
interface AssistContentScreen { interface AssistContentScreen {
fun onProvideAssistUrl(): String? fun onProvideAssistUrl(): String?
} }
@@ -9,14 +9,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.util.Tab import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourceTab import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourceTab
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
@@ -58,12 +58,12 @@ data object BrowseTab : Tab {
val context = LocalContext.current val context = LocalContext.current
// Hoisted for extensions tab's search bar // Hoisted for extensions tab's search bar
val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() } val extensionsViewModel = viewModel<ExtensionsViewModel>()
val extensionsState by extensionsScreenModel.state.collectAsState() val extensionsState by extensionsViewModel.state.collectAsState()
val tabs = listOf( val tabs = listOf(
sourcesTab(), sourcesTab(),
extensionsTab(extensionsScreenModel), extensionsTab(extensionsViewModel),
migrateSourceTab(), migrateSourceTab(),
) )
@@ -74,7 +74,7 @@ data object BrowseTab : Tab {
tabs = tabs, tabs = tabs,
state = state, state = state,
searchQuery = extensionsState.searchQuery, searchQuery = extensionsState.searchQuery,
onChangeSearchQuery = extensionsScreenModel::search, onChangeSearchQuery = extensionsViewModel::search,
) )
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
switchToExtensionTabChannel.receiveAsFlow() switchToExtensionTabChannel.receiveAsFlow()
@@ -5,7 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.ExtensionFilterScreen import eu.kanade.presentation.browse.ExtensionFilterScreen
@@ -21,8 +21,8 @@ class ExtensionFilterScreen : Screen() {
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ExtensionFilterScreenModel() } val viewModel = viewModel<ExtensionFilterViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
if (state is ExtensionFilterState.Loading) { if (state is ExtensionFilterState.Loading) {
LoadingScreen() LoadingScreen()
@@ -34,11 +34,11 @@ class ExtensionFilterScreen : Screen() {
ExtensionFilterScreen( ExtensionFilterScreen(
navigateUp = navigator::pop, navigateUp = navigator::pop,
state = successState, state = successState,
onClickToggle = screenModel::toggle, onClickToggle = viewModel::toggle,
) )
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.events.collectLatest { viewModel.events.collectLatest {
when (it) { when (it) {
ExtensionFilterEvent.FailedFetchingLanguages -> { ExtensionFilterEvent.FailedFetchingLanguages -> {
context.stringResource(MR.strings.internal_error) context.stringResource(MR.strings.internal_error)
@@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.extension package eu.kanade.tachiyomi.ui.browse.extension
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
@@ -15,21 +14,22 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionFilterScreenModel( class ExtensionFilterViewModel(
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(), private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(), private val toggleLanguage: ToggleLanguage = Injekt.get(),
) : StateScreenModel<ExtensionFilterState>(ExtensionFilterState.Loading) { ) : StateViewModel<ExtensionFilterState>(ExtensionFilterState.Loading) {
private val _events: Channel<ExtensionFilterEvent> = Channel() private val _events: Channel<ExtensionFilterEvent> = Channel()
val events: Flow<ExtensionFilterEvent> = _events.receiveAsFlow() val events: Flow<ExtensionFilterEvent> = _events.receiveAsFlow()
init { init {
screenModelScope.launch { viewModelScope.launch {
combine( combine(
getExtensionLanguages.subscribe(), getExtensionLanguages.subscribe(),
preferences.enabledLanguages.changes(), preferences.enabledLanguages.changes(),
@@ -26,12 +26,12 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun extensionsTab( fun extensionsTab(
extensionsScreenModel: ExtensionsScreenModel, extensionsViewModel: ExtensionsViewModel,
): TabContent { ): TabContent {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val state by extensionsScreenModel.state.collectAsState() val state by extensionsViewModel.state.collectAsState()
var privateExtensionToUninstall by remember { mutableStateOf<Extension?>(null) } var privateExtensionToUninstall by remember { mutableStateOf<Extension?>(null) }
return TabContent( return TabContent(
@@ -50,7 +50,7 @@ fun extensionsTab(
), ),
content = { contentPadding, _ -> content = { contentPadding, _ ->
BackHandler(enabled = state.searchQuery != null) { BackHandler(enabled = state.searchQuery != null) {
extensionsScreenModel.search(null) extensionsViewModel.search(null)
} }
ExtensionScreen( ExtensionScreen(
@@ -59,18 +59,18 @@ fun extensionsTab(
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
onLongClickItem = { extension -> onLongClickItem = { extension ->
when (extension) { when (extension) {
is Extension.Available -> extensionsScreenModel.installExtension(extension) is Extension.Available -> extensionsViewModel.installExtension(extension)
else -> { else -> {
if (context.isPackageInstalled(extension.pkgName)) { if (context.isPackageInstalled(extension.pkgName)) {
extensionsScreenModel.uninstallExtension(extension) extensionsViewModel.uninstallExtension(extension)
} else { } else {
privateExtensionToUninstall = extension privateExtensionToUninstall = extension
} }
} }
} }
}, },
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension, onClickItemCancel = extensionsViewModel::cancelInstallUpdateExtension,
onClickUpdateAll = extensionsScreenModel::updateAllExtensions, onClickUpdateAll = extensionsViewModel::updateAllExtensions,
onOpenWebView = { extension -> onOpenWebView = { extension ->
extension.sources.getOrNull(0)?.let { extension.sources.getOrNull(0)?.let {
navigator.push( navigator.push(
@@ -82,19 +82,19 @@ fun extensionsTab(
) )
} }
}, },
onInstallExtension = extensionsScreenModel::installExtension, onInstallExtension = extensionsViewModel::installExtension,
onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) }, onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) },
onTrustExtension = { extensionsScreenModel.trustExtension(it) }, onTrustExtension = { extensionsViewModel.trustExtension(it) },
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) }, onUninstallExtension = { extensionsViewModel.uninstallExtension(it) },
onUpdateExtension = extensionsScreenModel::updateExtension, onUpdateExtension = extensionsViewModel::updateExtension,
onRefresh = extensionsScreenModel::findAvailableExtensions, onRefresh = extensionsViewModel::findAvailableExtensions,
) )
privateExtensionToUninstall?.let { extension -> privateExtensionToUninstall?.let { extension ->
ExtensionUninstallConfirmation( ExtensionUninstallConfirmation(
extensionName = extension.name, extensionName = extension.name,
onClickConfirm = { onClickConfirm = {
extensionsScreenModel.uninstallExtension(extension) extensionsViewModel.uninstallExtension(extension)
}, },
onDismissRequest = { onDismissRequest = {
privateExtensionToUninstall = null privateExtensionToUninstall = null
@@ -2,8 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application import android.app.Application
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.extension.interactor.GetExtensionsByType import eu.kanade.domain.extension.interactor.GetExtensionsByType
@@ -28,18 +27,19 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class ExtensionsScreenModel( class ExtensionsViewModel(
preferences: SourcePreferences = Injekt.get(), preferences: SourcePreferences = Injekt.get(),
basePreferences: BasePreferences = Injekt.get(), basePreferences: BasePreferences = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensions: GetExtensionsByType = Injekt.get(), private val getExtensions: GetExtensionsByType = Injekt.get(),
) : StateScreenModel<ExtensionsScreenModel.State>(State()) { ) : StateViewModel<ExtensionsViewModel.State>(State()) {
private val currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf()) private val currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
@@ -51,7 +51,7 @@ class ExtensionsScreenModel(
} }
} }
screenModelScope.launchIO { viewModelScope.launchIO {
combine( combine(
state.map { it.searchQuery } state.map { it.searchQuery }
.distinctUntilChanged() .distinctUntilChanged()
@@ -95,15 +95,15 @@ class ExtensionsScreenModel(
} }
} }
screenModelScope.launchIO { findAvailableExtensions() } viewModelScope.launchIO { findAvailableExtensions() }
preferences.extensionUpdatesCount.changes() preferences.extensionUpdatesCount.changes()
.onEach { mutableState.update { state -> state.copy(updates = it) } } .onEach { mutableState.update { state -> state.copy(updates = it) } }
.launchIn(screenModelScope) .launchIn(viewModelScope)
basePreferences.extensionInstaller.changes() basePreferences.extensionInstaller.changes()
.onEach { mutableState.update { state -> state.copy(installer = it) } } .onEach { mutableState.update { state -> state.copy(installer = it) } }
.launchIn(screenModelScope) .launchIn(viewModelScope)
} }
fun searchQueryPredicate(query: String): (Extension) -> Boolean { fun searchQueryPredicate(query: String): (Extension) -> Boolean {
@@ -143,7 +143,7 @@ class ExtensionsScreenModel(
} }
fun updateAllExtensions() { fun updateAllExtensions() {
screenModelScope.launchIO { viewModelScope.launchIO {
state.value.items.values.flatten() state.value.items.values.flatten()
.map { it.extension } .map { it.extension }
.filterIsInstance<Extension.Installed>() .filterIsInstance<Extension.Installed>()
@@ -153,13 +153,13 @@ class ExtensionsScreenModel(
} }
fun installExtension(extension: Extension.Available) { fun installExtension(extension: Extension.Available) {
screenModelScope.launchIO { viewModelScope.launchIO {
extensionManager.installExtension(extension).collectToInstallUpdate(extension) extensionManager.installExtension(extension).collectToInstallUpdate(extension)
} }
} }
fun updateExtension(extension: Extension.Installed) { fun updateExtension(extension: Extension.Installed) {
screenModelScope.launchIO { viewModelScope.launchIO {
extensionManager.updateExtension(extension).collectToInstallUpdate(extension) extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
} }
} }
@@ -189,7 +189,7 @@ class ExtensionsScreenModel(
} }
fun findAvailableExtensions() { fun findAvailableExtensions() {
screenModelScope.launchIO { viewModelScope.launchIO {
mutableState.update { it.copy(isRefreshing = true) } mutableState.update { it.copy(isRefreshing = true) }
extensionManager.findAvailableExtensions() extensionManager.findAvailableExtensions()
@@ -202,7 +202,7 @@ class ExtensionsScreenModel(
} }
fun trustExtension(extension: Extension.Untrusted) { fun trustExtension(extension: Extension.Untrusted) {
screenModelScope.launch { viewModelScope.launch {
extensionManager.trust(extension) extensionManager.trust(extension)
} }
} }
@@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.CreationExtras
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.ExtensionDetailsScreen import eu.kanade.presentation.browse.ExtensionDetailsScreen
@@ -19,9 +19,13 @@ data class ExtensionDetailsScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current val viewModel = viewModel<ExtensionDetailsViewModel>(
val screenModel = rememberScreenModel { ExtensionDetailsScreenModel(pkgName = pkgName, context = context) } factory = ExtensionDetailsViewModel.Factory,
val state by screenModel.state.collectAsState() extras = CreationExtras {
set(ExtensionDetailsViewModel.PKG_NAME_KEY, pkgName)
},
)
val state by viewModel.state.collectAsState()
if (state.isLoading) { if (state.isLoading) {
LoadingScreen() LoadingScreen()
@@ -34,16 +38,16 @@ data class ExtensionDetailsScreen(
navigateUp = navigator::pop, navigateUp = navigator::pop,
state = state, state = state,
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) }, onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
onClickEnableAll = { screenModel.toggleSources(true) }, onClickEnableAll = { viewModel.toggleSources(true) },
onClickDisableAll = { screenModel.toggleSources(false) }, onClickDisableAll = { viewModel.toggleSources(false) },
onClickClearCookies = screenModel::clearCookies, onClickClearCookies = viewModel::clearCookies,
onClickUninstall = screenModel::uninstallExtension, onClickUninstall = viewModel::uninstallExtension,
onClickSource = screenModel::toggleSource, onClickSource = viewModel::toggleSource,
onClickIncognito = screenModel::toggleIncognito, onClickIncognito = viewModel::toggleIncognito,
) )
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.events.collectLatest { event -> viewModel.events.collectLatest { event ->
if (event is ExtensionDetailsEvent.Uninstalled) { if (event is ExtensionDetailsEvent.Uninstalled) {
navigator.pop() navigator.pop()
} }
@@ -1,9 +1,12 @@
package eu.kanade.tachiyomi.ui.browse.extension.details package eu.kanade.tachiyomi.ui.browse.extension.details
import android.app.Application
import android.content.Context import android.content.Context
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.domain.extension.interactor.ExtensionSourceItem import eu.kanade.domain.extension.interactor.ExtensionSourceItem
import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.source.interactor.ToggleIncognito import eu.kanade.domain.source.interactor.ToggleIncognito
@@ -24,12 +27,13 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionDetailsScreenModel( class ExtensionDetailsViewModel(
pkgName: String, pkgName: String,
context: Context, context: Context,
private val network: NetworkHelper = Injekt.get(), private val network: NetworkHelper = Injekt.get(),
@@ -38,13 +42,26 @@ class ExtensionDetailsScreenModel(
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleIncognito: ToggleIncognito = Injekt.get(), private val toggleIncognito: ToggleIncognito = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
) : StateScreenModel<ExtensionDetailsScreenModel.State>(State()) { ) : StateViewModel<ExtensionDetailsViewModel.State>(State()) {
companion object {
val PKG_NAME_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
ExtensionDetailsViewModel(
pkgName = get(PKG_NAME_KEY)!!,
context = Injekt.get<Application>(),
)
}
}
}
private val _events: Channel<ExtensionDetailsEvent> = Channel() private val _events: Channel<ExtensionDetailsEvent> = Channel()
val events: Flow<ExtensionDetailsEvent> = _events.receiveAsFlow() val events: Flow<ExtensionDetailsEvent> = _events.receiveAsFlow()
init { init {
screenModelScope.launch { viewModelScope.launch {
launch { launch {
extensionManager.installedExtensionsFlow extensionManager.installedExtensionsFlow
.map { it.firstOrNull { extension -> extension.pkgName == pkgName } } .map { it.firstOrNull { extension -> extension.pkgName == pkgName } }
@@ -19,7 +19,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@@ -47,9 +48,14 @@ data class MigrateMangaScreen(
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MigrateMangaScreenModel(sourceId) } val viewModel = viewModel<MigrateMangaViewModel>(
factory = MigrateMangaViewModel.Factory,
extras = CreationExtras {
set(MigrateMangaViewModel.SOURCE_ID_KEY, sourceId)
},
)
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
if (state.isLoading) { if (state.isLoading) {
LoadingScreen() LoadingScreen()
@@ -57,7 +63,7 @@ data class MigrateMangaScreen(
} }
BackHandler(enabled = state.selectionMode) { BackHandler(enabled = state.selectionMode) {
screenModel.clearSelection() viewModel.clearSelection()
} }
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@@ -68,7 +74,7 @@ data class MigrateMangaScreen(
title = state.source!!.name, title = state.source!!.name,
navigateUp = { navigateUp = {
if (state.selectionMode) { if (state.selectionMode) {
screenModel.clearSelection() viewModel.clearSelection()
} else { } else {
navigator.pop() navigator.pop()
} }
@@ -84,7 +90,7 @@ data class MigrateMangaScreen(
}, },
onClick = { onClick = {
val selection = state.selection val selection = state.selection
screenModel.clearSelection() viewModel.clearSelection()
navigator.push(MigrationConfigScreen(selection)) navigator.push(MigrationConfigScreen(selection))
}, },
expanded = lazyListState.shouldExpandFAB(), expanded = lazyListState.shouldExpandFAB(),
@@ -107,13 +113,13 @@ data class MigrateMangaScreen(
lazyListState = lazyListState, lazyListState = lazyListState,
contentPadding = contentPadding, contentPadding = contentPadding,
state = state, state = state,
onClickItem = screenModel::toggleSelection, onClickItem = viewModel::toggleSelection,
onClickCover = { navigator.push(MangaScreen(it.id)) }, onClickCover = { navigator.push(MangaScreen(it.id)) },
) )
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.events.collectLatest { event -> viewModel.events.collectLatest { event ->
when (event) { when (event) {
MigrationMangaEvent.FailedFetchingFavorites -> { MigrationMangaEvent.FailedFetchingFavorites -> {
context.toast(MR.strings.internal_error) context.toast(MR.strings.internal_error)
@@ -127,7 +133,7 @@ data class MigrateMangaScreen(
private fun MigrateMangaContent( private fun MigrateMangaContent(
lazyListState: LazyListState, lazyListState: LazyListState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: MigrateMangaScreenModel.State, state: MigrateMangaViewModel.State,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit, onClickCover: (Manga) -> Unit,
) { ) {
@@ -1,8 +1,10 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga package eu.kanade.tachiyomi.ui.browse.migration.manga
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -14,6 +16,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.common.utils.mutate import mihon.core.common.utils.mutate
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@@ -21,17 +24,29 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrateMangaScreenModel( class MigrateMangaViewModel(
private val sourceId: Long, private val sourceId: Long,
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(), private val getFavorites: GetFavorites = Injekt.get(),
) : StateScreenModel<MigrateMangaScreenModel.State>(State()) { ) : StateViewModel<MigrateMangaViewModel.State>(State()) {
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateMangaViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
)
}
}
}
private val _events: Channel<MigrationMangaEvent> = Channel() private val _events: Channel<MigrationMangaEvent> = Channel()
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow() val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
init { init {
screenModelScope.launch { viewModelScope.launch {
mutableState.update { state -> mutableState.update { state ->
state.copy(source = sourceManager.getOrStub(sourceId)) state.copy(source = sourceManager.getOrStub(sourceId))
} }
@@ -3,12 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrateSearchScreen import eu.kanade.presentation.browse.MigrateSearchScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import mihon.feature.migration.dialog.MigrateMangaDialog import mihon.feature.migration.dialog.MigrateMangaDialog
import mihon.feature.migration.list.MigrationListScreen import mihon.feature.migration.list.MigrationListScreen
@@ -19,18 +20,23 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) } val viewModel = viewModel<MigrateSearchViewModel>(
val state by screenModel.state.collectAsState() factory = MigrateSearchViewModel.Factory,
extras = CreationExtras {
set(MigrateSearchViewModel.MANGA_ID_KEY, mangaId)
},
)
val state by viewModel.state.collectAsState()
MigrateSearchScreen( MigrateSearchScreen(
state = state, state = state,
fromSourceId = state.from?.source, fromSourceId = state.from?.source,
navigateUp = navigator::pop, navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery, onChangeSearchQuery = viewModel::updateSearchQuery,
onSearch = { screenModel.search() }, onSearch = { viewModel.search() },
getManga = { screenModel.getManga(it) }, getManga = { viewModel.getManga(it) },
onChangeSearchFilter = screenModel::setSourceFilter, onChangeSearchFilter = viewModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults, onToggleResults = viewModel::toggleFilterResults,
onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) }, onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
onClickItem = { onClickItem = {
val migrateListScreen = navigator.items val migrateListScreen = navigator.items
@@ -38,7 +44,7 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
.lastOrNull() .lastOrNull()
if (migrateListScreen == null) { if (migrateListScreen == null) {
screenModel.setMigrateDialog(mangaId, it) viewModel.setMigrateDialog(mangaId, it)
} else { } else {
migrateListScreen.addMatchOverride(current = mangaId, target = it.id) migrateListScreen.addMatchOverride(current = mangaId, target = it.id)
navigator.popUntil { screen -> screen is MigrationListScreen } navigator.popUntil { screen -> screen is MigrationListScreen }
@@ -48,13 +54,13 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
) )
when (val dialog = state.dialog) { when (val dialog = state.dialog) {
is SearchScreenModel.Dialog.Migrate -> { is SearchViewModel.Dialog.Migrate -> {
MigrateMangaDialog( MigrateMangaDialog(
current = dialog.current, current = dialog.current,
target = dialog.target, target = dialog.target,
// Initiated from the context of [dialog.current] so we show [dialog.target]. // Initiated from the context of [dialog.current] so we show [dialog.target].
onClickTitle = { navigator.push(MangaScreen(dialog.target.id, true)) }, onClickTitle = { navigator.push(MangaScreen(dialog.target.id, true)) },
onDismissRequest = { screenModel.clearDialog() }, onDismissRequest = { viewModel.clearDialog() },
onComplete = { onComplete = {
if (navigator.lastItem is MangaScreen) { if (navigator.lastItem is MangaScreen) {
val lastItem = navigator.lastItem val lastItem = navigator.lastItem
@@ -1,10 +1,13 @@
package eu.kanade.tachiyomi.ui.browse.migration.search package eu.kanade.tachiyomi.ui.browse.migration.search
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
@@ -12,12 +15,24 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrateSearchScreenModel( class MigrateSearchViewModel(
val mangaId: Long, val mangaId: Long,
getManga: GetManga = Injekt.get(), getManga: GetManga = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(),
) : SearchScreenModel() { ) : SearchViewModel() {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateSearchViewModel(
mangaId = get(MANGA_ID_KEY)!!,
)
}
}
}
private val migrationSources by lazy { sourcePreferences.migrationSources.get() } private val migrationSources by lazy { sourcePreferences.migrationSources.get() }
@@ -29,7 +44,7 @@ class MigrateSearchScreenModel(
} }
init { init {
screenModelScope.launch { viewModelScope.launch {
val manga = getManga.await(mangaId)!! val manga = getManga.await(mangaId)!!
mutableState.update { mutableState.update {
it.copy( it.copy(
@@ -17,7 +17,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.core.util.ifSourcesLoaded
@@ -25,7 +26,7 @@ import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog
import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
@@ -59,8 +60,14 @@ data class MigrateSourceSearchScreen(
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId, query) } val viewModel = viewModel<BrowseSourceViewModel>(
val state by screenModel.state.collectAsState() factory = BrowseSourceViewModel.Factory,
extras = CreationExtras {
set(BrowseSourceViewModel.SOURCE_ID_KEY, sourceId)
set(BrowseSourceViewModel.LISTING_QUERY_KEY, query)
},
)
val state by viewModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
@@ -68,9 +75,9 @@ data class MigrateSourceSearchScreen(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
SearchToolbar( SearchToolbar(
searchQuery = state.toolbarQuery ?: "", searchQuery = state.toolbarQuery ?: "",
onChangeSearchQuery = screenModel::setToolbarQuery, onChangeSearchQuery = viewModel::setToolbarQuery,
onClickCloseSearch = navigator::pop, onClickCloseSearch = navigator::pop,
onSearch = screenModel::search, onSearch = viewModel::search,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
}, },
@@ -78,7 +85,7 @@ data class MigrateSourceSearchScreen(
SmallExtendedFloatingActionButton( SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_filter)) }, text = { Text(text = stringResource(MR.strings.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) }, icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
onClick = screenModel::openFilterSheet, onClick = viewModel::openFilterSheet,
modifier = Modifier.animateFloatingActionButton( modifier = Modifier.animateFloatingActionButton(
visible = state.filters.isNotEmpty(), visible = state.filters.isNotEmpty(),
alignment = Alignment.BottomEnd, alignment = Alignment.BottomEnd,
@@ -93,21 +100,21 @@ data class MigrateSourceSearchScreen(
.lastOrNull() .lastOrNull()
if (migrateListScreen == null) { if (migrateListScreen == null) {
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(target = it, current = currentManga)) viewModel.setDialog(BrowseSourceViewModel.Dialog.Migrate(target = it, current = currentManga))
} else { } else {
migrateListScreen.addMatchOverride(current = currentManga.id, target = it.id) migrateListScreen.addMatchOverride(current = currentManga.id, target = it.id)
navigator.popUntil { screen -> screen is MigrationListScreen } navigator.popUntil { screen -> screen is MigrationListScreen }
} }
} }
BrowseSourceContent( BrowseSourceContent(
source = screenModel.source, source = viewModel.source,
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(), mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation), columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode, displayMode = viewModel.displayMode,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
contentPadding = paddingValues, contentPadding = paddingValues,
onWebViewClick = { onWebViewClick = {
val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent val source = viewModel.source as? HttpSource ?: return@BrowseSourceContent
navigator.push( navigator.push(
WebViewScreen( WebViewScreen(
url = source.getHomeUrl(), url = source.getHomeUrl(),
@@ -123,18 +130,18 @@ data class MigrateSourceSearchScreen(
) )
} }
val onDismissRequest = { screenModel.setDialog(null) } val onDismissRequest = { viewModel.setDialog(null) }
when (val dialog = state.dialog) { when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Filter -> { is BrowseSourceViewModel.Dialog.Filter -> {
SourceFilterDialog( SourceFilterDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
filters = state.filters, filters = state.filters,
onReset = screenModel::resetFilters, onReset = viewModel::resetFilters,
onFilter = { screenModel.search(filters = state.filters) }, onFilter = { viewModel.search(filters = state.filters) },
onUpdate = screenModel::setFilters, onUpdate = viewModel::setFilters,
) )
} }
is BrowseSourceScreenModel.Dialog.Migrate -> { is BrowseSourceViewModel.Dialog.Migrate -> {
MigrateMangaDialog( MigrateMangaDialog(
current = currentManga, current = currentManga,
target = dialog.target, target = dialog.target,
@@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -21,8 +21,8 @@ import tachiyomi.presentation.core.i18n.stringResource
fun Screen.migrateSourceTab(): TabContent { fun Screen.migrateSourceTab(): TabContent {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MigrateSourceScreenModel() } val viewModel = viewModel<MigrateSourceViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
return TabContent( return TabContent(
titleRes = MR.strings.label_migration, titleRes = MR.strings.label_migration,
@@ -42,8 +42,8 @@ fun Screen.migrateSourceTab(): TabContent {
onClickItem = { source -> onClickItem = { source ->
navigator.push(MigrateMangaScreen(source.id)) navigator.push(MigrateMangaScreen(source.id))
}, },
onToggleSortingDirection = screenModel::toggleSortingDirection, onToggleSortingDirection = viewModel::toggleSortingDirection,
onToggleSortingMode = screenModel::toggleSortingMode, onToggleSortingMode = viewModel::toggleSortingMode,
) )
}, },
) )
@@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
@@ -14,23 +13,24 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrateSourceScreenModel( class MigrateSourceViewModel(
preferences: SourcePreferences = Injekt.get(), preferences: SourcePreferences = Injekt.get(),
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val setMigrateSorting: SetMigrateSorting = Injekt.get(), private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
) : StateScreenModel<MigrateSourceScreenModel.State>(State()) { ) : StateViewModel<MigrateSourceViewModel.State>(State()) {
private val _channel = Channel<Event>(Int.MAX_VALUE) private val _channel = Channel<Event>(Int.MAX_VALUE)
val channel = _channel.receiveAsFlow() val channel = _channel.receiveAsFlow()
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
getSourcesWithFavoriteCount.subscribe() getSourcesWithFavoriteCount.subscribe()
.catch { .catch {
logcat(LogPriority.ERROR, it) logcat(LogPriority.ERROR, it)
@@ -48,11 +48,11 @@ class MigrateSourceScreenModel(
preferences.migrationSortingDirection.changes() preferences.migrationSortingDirection.changes()
.onEach { mutableState.update { state -> state.copy(sortingDirection = it) } } .onEach { mutableState.update { state -> state.copy(sortingDirection = it) } }
.launchIn(screenModelScope) .launchIn(viewModelScope)
preferences.migrationSortingMode.changes() preferences.migrationSortingMode.changes()
.onEach { mutableState.update { state -> state.copy(sortingMode = it) } } .onEach { mutableState.update { state -> state.copy(sortingMode = it) } }
.launchIn(screenModelScope) .launchIn(viewModelScope)
} }
fun toggleSortingMode() { fun toggleSortingMode() {
@@ -5,7 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.SourcesFilterScreen import eu.kanade.presentation.browse.SourcesFilterScreen
@@ -19,15 +19,15 @@ class SourcesFilterScreen : Screen() {
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { SourcesFilterScreenModel() } val viewModel = viewModel<SourcesFilterViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
if (state is SourcesFilterScreenModel.State.Loading) { if (state is SourcesFilterViewModel.State.Loading) {
LoadingScreen() LoadingScreen()
return return
} }
if (state is SourcesFilterScreenModel.State.Error) { if (state is SourcesFilterViewModel.State.Error) {
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
context.toast(MR.strings.internal_error) context.toast(MR.strings.internal_error)
@@ -36,13 +36,13 @@ class SourcesFilterScreen : Screen() {
return return
} }
val successState = state as SourcesFilterScreenModel.State.Success val successState = state as SourcesFilterViewModel.State.Success
SourcesFilterScreen( SourcesFilterScreen(
navigateUp = navigator::pop, navigateUp = navigator::pop,
state = successState, state = successState,
onClickLanguage = screenModel::toggleLanguage, onClickLanguage = viewModel::toggleLanguage,
onClickSource = screenModel::toggleSource, onClickSource = viewModel::toggleSource,
) )
} }
} }
@@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetLanguagesWithSources import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
@@ -12,20 +11,21 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
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.SortedMap import java.util.SortedMap
class SourcesFilterScreenModel( class SourcesFilterViewModel(
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(), private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(), private val toggleLanguage: ToggleLanguage = Injekt.get(),
) : StateScreenModel<SourcesFilterScreenModel.State>(State.Loading) { ) : StateViewModel<SourcesFilterViewModel.State>(State.Loading) {
init { init {
screenModelScope.launch { viewModelScope.launch {
combine( combine(
getLanguagesWithSources.subscribe(), getLanguagesWithSources.subscribe(),
preferences.enabledLanguages.changes(), preferences.enabledLanguages.changes(),
@@ -7,7 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -25,8 +25,8 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun Screen.sourcesTab(): TabContent { fun Screen.sourcesTab(): TabContent {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { SourcesScreenModel() } val viewModel = viewModel<SourcesViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
return TabContent( return TabContent(
titleRes = MR.strings.label_sources, titleRes = MR.strings.label_sources,
@@ -49,8 +49,8 @@ fun Screen.sourcesTab(): TabContent {
onClickItem = { source, listing -> onClickItem = { source, listing ->
navigator.push(BrowseSourceScreen(source.id, listing.query)) navigator.push(BrowseSourceScreen(source.id, listing.query))
}, },
onClickPin = screenModel::togglePin, onClickPin = viewModel::togglePin,
onLongClickItem = screenModel::showSourceDialog, onLongClickItem = viewModel::showSourceDialog,
) )
state.dialog?.let { dialog -> state.dialog?.let { dialog ->
@@ -58,22 +58,22 @@ fun Screen.sourcesTab(): TabContent {
SourceOptionsDialog( SourceOptionsDialog(
source = source, source = source,
onClickPin = { onClickPin = {
screenModel.togglePin(source) viewModel.togglePin(source)
screenModel.closeDialog() viewModel.closeDialog()
}, },
onClickDisable = { onClickDisable = {
screenModel.toggleSource(source) viewModel.toggleSource(source)
screenModel.closeDialog() viewModel.closeDialog()
}, },
onDismiss = screenModel::closeDialog, onDismiss = viewModel::closeDialog,
) )
} }
val internalErrString = stringResource(MR.strings.internal_error) val internalErrString = stringResource(MR.strings.internal_error)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.events.collectLatest { event -> viewModel.events.collectLatest { event ->
when (event) { when (event) {
SourcesScreenModel.Event.FailedFetchingSources -> { SourcesViewModel.Event.FailedFetchingSources -> {
launch { snackbarHostState.showSnackbar(internalErrString) } launch { snackbarHostState.showSnackbar(internalErrString) }
} }
} }
@@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
@@ -13,6 +12,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.Pin import tachiyomi.domain.source.model.Pin
@@ -21,17 +21,17 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.TreeMap import java.util.TreeMap
class SourcesScreenModel( class SourcesViewModel(
private val getEnabledSources: GetEnabledSources = Injekt.get(), private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
) : StateScreenModel<SourcesScreenModel.State>(State()) { ) : StateViewModel<SourcesViewModel.State>(State()) {
private val _events = Channel<Event>(Int.MAX_VALUE) private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow() val events = _events.receiveAsFlow()
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
getEnabledSources.subscribe() getEnabledSources.subscribe()
.catch { .catch {
logcat(LogPriority.ERROR, it) logcat(LogPriority.ERROR, it)
@@ -32,7 +32,8 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.core.util.ifSourcesLoaded
@@ -46,7 +47,7 @@ import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen
@@ -81,20 +82,26 @@ data class BrowseSourceScreen(
return return
} }
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId, listingQuery) } val viewModel = viewModel<BrowseSourceViewModel>(
val state by screenModel.state.collectAsState() factory = BrowseSourceViewModel.Factory,
extras = CreationExtras {
set(BrowseSourceViewModel.SOURCE_ID_KEY, sourceId)
set(BrowseSourceViewModel.LISTING_QUERY_KEY, listingQuery)
},
)
val state by viewModel.state.collectAsState()
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val navigateUp: () -> Unit = { val navigateUp: () -> Unit = {
when { when {
!state.isUserQuery && state.toolbarQuery != null -> screenModel.setToolbarQuery(null) !state.isUserQuery && state.toolbarQuery != null -> viewModel.setToolbarQuery(null)
else -> navigator.pop() else -> navigator.pop()
} }
} }
if (screenModel.source is StubSource) { if (viewModel.source is StubSource) {
MissingSourceScreen( MissingSourceScreen(
source = screenModel.source, source = viewModel.source,
navigateUp = navigateUp, navigateUp = navigateUp,
) )
return return
@@ -107,7 +114,7 @@ data class BrowseSourceScreen(
val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) } val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }
val onWebViewClick = f@{ val onWebViewClick = f@{
val source = screenModel.source as? HttpSource ?: return@f val source = viewModel.source as? HttpSource ?: return@f
navigator.push( navigator.push(
WebViewScreen( WebViewScreen(
url = source.getHomeUrl(), url = source.getHomeUrl(),
@@ -117,8 +124,8 @@ data class BrowseSourceScreen(
) )
} }
LaunchedEffect(screenModel.source) { LaunchedEffect(viewModel.source) {
assistUrl = (screenModel.source as? HttpSource)?.getHomeUrl() assistUrl = (viewModel.source as? HttpSource)?.getHomeUrl()
} }
Scaffold( Scaffold(
@@ -130,15 +137,15 @@ data class BrowseSourceScreen(
) { ) {
BrowseSourceToolbar( BrowseSourceToolbar(
searchQuery = state.toolbarQuery, searchQuery = state.toolbarQuery,
onSearchQueryChange = screenModel::setToolbarQuery, onSearchQueryChange = viewModel::setToolbarQuery,
source = screenModel.source, source = viewModel.source,
displayMode = screenModel.displayMode, displayMode = viewModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it }, onDisplayModeChange = { viewModel.displayMode = it },
navigateUp = navigateUp, navigateUp = navigateUp,
onWebViewClick = onWebViewClick, onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick, onHelpClick = onHelpClick,
onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) }, onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) },
onSearch = screenModel::search, onSearch = viewModel::search,
) )
Row( Row(
@@ -150,8 +157,8 @@ data class BrowseSourceScreen(
FilterChip( FilterChip(
selected = state.listing == Listing.Popular, selected = state.listing == Listing.Popular,
onClick = { onClick = {
screenModel.resetFilters() viewModel.resetFilters()
screenModel.setListing(Listing.Popular) viewModel.setListing(Listing.Popular)
}, },
leadingIcon = { leadingIcon = {
Icon( Icon(
@@ -165,12 +172,12 @@ data class BrowseSourceScreen(
Text(text = stringResource(MR.strings.popular)) Text(text = stringResource(MR.strings.popular))
}, },
) )
if (screenModel.source.supportsLatest) { if (viewModel.source.supportsLatest) {
FilterChip( FilterChip(
selected = state.listing == Listing.Latest, selected = state.listing == Listing.Latest,
onClick = { onClick = {
screenModel.resetFilters() viewModel.resetFilters()
screenModel.setListing(Listing.Latest) viewModel.setListing(Listing.Latest)
}, },
leadingIcon = { leadingIcon = {
Icon( Icon(
@@ -188,7 +195,7 @@ data class BrowseSourceScreen(
if (state.filters.isNotEmpty()) { if (state.filters.isNotEmpty()) {
FilterChip( FilterChip(
selected = state.listing is Listing.Search, selected = state.listing is Listing.Search,
onClick = screenModel::openFilterSheet, onClick = viewModel::openFilterSheet,
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = Icons.Outlined.FilterList, imageVector = Icons.Outlined.FilterList,
@@ -210,10 +217,10 @@ data class BrowseSourceScreen(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues -> ) { paddingValues ->
BrowseSourceContent( BrowseSourceContent(
source = screenModel.source, source = viewModel.source,
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(), mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation), columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode, displayMode = viewModel.displayMode,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
contentPadding = paddingValues, contentPadding = paddingValues,
onWebViewClick = onWebViewClick, onWebViewClick = onWebViewClick,
@@ -222,13 +229,13 @@ data class BrowseSourceScreen(
onMangaClick = { navigator.push((MangaScreen(it.id, true))) }, onMangaClick = { navigator.push((MangaScreen(it.id, true))) },
onMangaLongClick = { manga -> onMangaLongClick = { manga ->
scope.launchIO { scope.launchIO {
val duplicates = screenModel.getDuplicateLibraryManga(manga) val duplicates = viewModel.getDuplicateLibraryManga(manga)
when { when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) manga.favorite -> viewModel.setDialog(BrowseSourceViewModel.Dialog.RemoveManga(manga))
duplicates.isNotEmpty() -> screenModel.setDialog( duplicates.isNotEmpty() -> viewModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates), BrowseSourceViewModel.Dialog.AddDuplicateManga(manga, duplicates),
) )
else -> screenModel.addFavorite(manga) else -> viewModel.addFavorite(manga)
} }
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
} }
@@ -236,28 +243,28 @@ data class BrowseSourceScreen(
) )
} }
val onDismissRequest = { screenModel.setDialog(null) } val onDismissRequest = { viewModel.setDialog(null) }
when (val dialog = state.dialog) { when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Filter -> { is BrowseSourceViewModel.Dialog.Filter -> {
SourceFilterDialog( SourceFilterDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
filters = state.filters, filters = state.filters,
onReset = screenModel::resetFilters, onReset = viewModel::resetFilters,
onFilter = { screenModel.search(filters = state.filters) }, onFilter = { viewModel.search(filters = state.filters) },
onUpdate = screenModel::setFilters, onUpdate = viewModel::setFilters,
) )
} }
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> { is BrowseSourceViewModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates, duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) }, onConfirm = { viewModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(it.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(dialog.manga, it)) }, onMigrate = { viewModel.setDialog(BrowseSourceViewModel.Dialog.Migrate(dialog.manga, it)) },
) )
} }
is BrowseSourceScreenModel.Dialog.Migrate -> { is BrowseSourceViewModel.Dialog.Migrate -> {
MigrateMangaDialog( MigrateMangaDialog(
current = dialog.current, current = dialog.current,
target = dialog.target, target = dialog.target,
@@ -266,23 +273,23 @@ data class BrowseSourceScreen(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
) )
} }
is BrowseSourceScreenModel.Dialog.RemoveManga -> { is BrowseSourceViewModel.Dialog.RemoveManga -> {
RemoveMangaDialog( RemoveMangaDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { onConfirm = {
screenModel.changeMangaFavorite(dialog.manga) viewModel.changeMangaFavorite(dialog.manga)
}, },
mangaToRemove = dialog.manga, mangaToRemove = dialog.manga,
) )
} }
is BrowseSourceScreenModel.Dialog.ChangeMangaCategory -> { is BrowseSourceViewModel.Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog( ChangeCategoryDialog(
initialSelection = dialog.initialSelection, initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) }, onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ -> onConfirm = { include, _ ->
screenModel.changeMangaFavorite(dialog.manga) viewModel.changeMangaFavorite(dialog.manga)
screenModel.moveMangaToCategories(dialog.manga, include) viewModel.moveMangaToCategories(dialog.manga, include)
}, },
) )
} }
@@ -293,8 +300,8 @@ data class BrowseSourceScreen(
queryEvent.receiveAsFlow() queryEvent.receiveAsFlow()
.collectLatest { .collectLatest {
when (it) { when (it) {
is SearchType.Genre -> screenModel.searchGenre(it.txt) is SearchType.Genre -> viewModel.searchGenre(it.txt)
is SearchType.Text -> screenModel.search(it.txt) is SearchType.Text -> viewModel.search(it.txt)
} }
} }
} }
@@ -6,19 +6,20 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.paging.filter import androidx.paging.filter
import androidx.paging.map import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetIncognitoState import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
@@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -50,7 +52,7 @@ import uy.kohesive.injekt.api.get
import java.time.Instant import java.time.Instant
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
class BrowseSourceScreenModel( class BrowseSourceViewModel(
private val sourceId: Long, private val sourceId: Long,
listingQuery: String?, listingQuery: String?,
sourceManager: SourceManager = Injekt.get(), sourceManager: SourceManager = Injekt.get(),
@@ -66,9 +68,23 @@ class BrowseSourceScreenModel(
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(),
getIncognitoState: GetIncognitoState = Injekt.get(), getIncognitoState: GetIncognitoState = Injekt.get(),
) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) { ) : StateViewModel<BrowseSourceViewModel.State>(State(Listing.valueOf(listingQuery))) {
var displayMode by sourcePreferences.sourceDisplayMode.asState(screenModelScope) companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val LISTING_QUERY_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
BrowseSourceViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
listingQuery = get(LISTING_QUERY_KEY),
)
}
}
}
var displayMode by sourcePreferences.sourceDisplayMode.asState(viewModelScope)
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
@@ -107,13 +123,13 @@ class BrowseSourceScreenModel(
pagingData.map { manga -> pagingData.map { manga ->
getManga.subscribe(manga.url, manga.source) getManga.subscribe(manga.url, manga.source)
.map { it ?: manga } .map { it ?: manga }
.stateIn(ioCoroutineScope) .stateIn(viewModelScope)
} }
.filter { !hideInLibraryItems || !it.value.favorite } .filter { !hideInLibraryItems || !it.value.favorite }
} }
.cachedIn(ioCoroutineScope) .cachedIn(viewModelScope)
} }
.stateIn(ioCoroutineScope, SharingStarted.Lazily, emptyFlow()) .stateIn(viewModelScope, SharingStarted.Lazily, emptyFlow())
fun getColumnsPreference(orientation: Int): GridCells { fun getColumnsPreference(orientation: Int): GridCells {
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
@@ -205,7 +221,7 @@ class BrowseSourceScreenModel(
* @param manga the manga to update. * @param manga the manga to update.
*/ */
fun changeMangaFavorite(manga: Manga) { fun changeMangaFavorite(manga: Manga) {
screenModelScope.launch { viewModelScope.launch {
var new = manga.copy( var new = manga.copy(
favorite = !manga.favorite, favorite = !manga.favorite,
dateAdded = when (manga.favorite) { dateAdded = when (manga.favorite) {
@@ -226,7 +242,7 @@ class BrowseSourceScreenModel(
} }
fun addFavorite(manga: Manga) { fun addFavorite(manga: Manga) {
screenModelScope.launch { viewModelScope.launch {
val categories = getCategories() val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory.get() val defaultCategoryId = libraryPreferences.defaultCategory.get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@@ -281,7 +297,7 @@ class BrowseSourceScreenModel(
} }
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) { fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
screenModelScope.launchIO { viewModelScope.launchIO {
setMangaCategories.await( setMangaCategories.await(
mangaId = manga.id, mangaId = manga.id,
categoryIds = categoryIds.toList(), categoryIds = categoryIds.toList(),
@@ -7,7 +7,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.core.util.ifSourcesLoaded
@@ -31,13 +32,14 @@ class GlobalSearchScreen(
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<GlobalSearchViewModel>(
GlobalSearchScreenModel( factory = GlobalSearchViewModel.Factory,
initialQuery = searchQuery, extras = CreationExtras {
initialExtensionFilter = extensionFilter, set(GlobalSearchViewModel.INITIAL_QUERY_KEY, searchQuery)
) set(GlobalSearchViewModel.INITIAL_EXTENSION_FILTER_KEY, extensionFilter)
} },
val state by screenModel.state.collectAsState() )
val state by viewModel.state.collectAsState()
var showSingleLoadingScreen by remember { var showSingleLoadingScreen by remember {
mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1) mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1)
} }
@@ -64,11 +66,11 @@ class GlobalSearchScreen(
GlobalSearchScreen( GlobalSearchScreen(
state = state, state = state,
navigateUp = navigator::pop, navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery, onChangeSearchQuery = viewModel::updateSearchQuery,
onSearch = { screenModel.search() }, onSearch = { viewModel.search() },
getManga = { screenModel.getManga(it) }, getManga = { viewModel.getManga(it) },
onChangeSearchFilter = screenModel::setSourceFilter, onChangeSearchFilter = viewModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults, onToggleResults = viewModel::toggleFilterResults,
onClickSource = { onClickSource = {
navigator.push(BrowseSourceScreen(it.id, state.searchQuery)) navigator.push(BrowseSourceScreen(it.id, state.searchQuery))
}, },
@@ -1,25 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import eu.kanade.tachiyomi.source.Source
class GlobalSearchScreenModel(
initialQuery: String = "",
initialExtensionFilter: String? = null,
) : SearchScreenModel(State(searchQuery = initialQuery)) {
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
if (extensionFilter != null) {
// we're going to use custom extension filter instead
setSourceFilter(SourceFilter.All)
}
search()
}
}
override fun getEnabledSources(): List<Source> {
return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
}
}
@@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.tachiyomi.source.Source
class GlobalSearchViewModel(
initialQuery: String,
initialExtensionFilter: String?,
) : SearchViewModel(State(searchQuery = initialQuery)) {
companion object {
val INITIAL_QUERY_KEY = CreationExtras.Key<String>()
val INITIAL_EXTENSION_FILTER_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
GlobalSearchViewModel(
initialQuery = get(INITIAL_QUERY_KEY)!!,
initialExtensionFilter = get(INITIAL_EXTENSION_FILTER_KEY),
)
}
}
}
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
if (extensionFilter != null) {
// we're going to use custom extension filter instead
setSourceFilter(SourceFilter.All)
}
search()
}
}
override fun getEnabledSources(): List<Source> {
return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
}
}
@@ -3,10 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -19,6 +17,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mihon.core.viewmodel.StateViewModel
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.preference.toggle import tachiyomi.core.common.preference.toggle
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -30,7 +29,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.Executors import java.util.concurrent.Executors
abstract class SearchScreenModel( abstract class SearchViewModel(
initialState: State = State(), initialState: State = State(),
sourcePreferences: SourcePreferences = Injekt.get(), sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
@@ -38,7 +37,7 @@ abstract class SearchScreenModel(
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
) : StateScreenModel<SearchScreenModel.State>(initialState) { ) : StateViewModel<SearchViewModel.State>(initialState) {
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
private var searchJob: Job? = null private var searchJob: Job? = null
@@ -61,7 +60,7 @@ abstract class SearchScreenModel(
} }
init { init {
screenModelScope.launch { viewModelScope.launch {
preferences.globalSearchFilterState.changes().collectLatest { state -> preferences.globalSearchFilterState.changes().collectLatest { state ->
mutableState.update { it.copy(onlyShowHasResults = state) } mutableState.update { it.copy(onlyShowHasResults = state) }
} }
@@ -147,7 +146,7 @@ abstract class SearchScreenModel(
) )
} }
searchJob = ioCoroutineScope.launch { searchJob = viewModelScope.launchIO {
sources.map { source -> sources.map { source ->
async { async {
if (state.value.items[source] !is SearchItemResult.Loading) { if (state.value.items[source] !is SearchItemResult.Loading) {
@@ -192,7 +191,7 @@ abstract class SearchScreenModel(
} }
fun setMigrateDialog(currentId: Long, target: Manga) { fun setMigrateDialog(currentId: Long, target: Manga) {
screenModelScope.launchIO { viewModelScope.launchIO {
val current = getManga.await(currentId) ?: return@launchIO val current = getManga.await(currentId) ?: return@launchIO
mutableState.update { it.copy(dialog = Dialog.Migrate(target, current)) } mutableState.update { it.copy(dialog = Dialog.Migrate(target, current)) }
} }
@@ -6,7 +6,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.category.CategoryScreen import eu.kanade.presentation.category.CategoryScreen
@@ -24,9 +24,9 @@ class CategoryScreen : Screen() {
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { CategoryScreenModel() } val viewModel = viewModel<CategoryViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
if (state is CategoryScreenState.Loading) { if (state is CategoryScreenState.Loading) {
LoadingScreen() LoadingScreen()
@@ -37,10 +37,10 @@ class CategoryScreen : Screen() {
CategoryScreen( CategoryScreen(
state = successState, state = successState,
onClickCreate = { screenModel.showDialog(CategoryDialog.Create) }, onClickCreate = { viewModel.showDialog(CategoryDialog.Create) },
onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) }, onClickRename = { viewModel.showDialog(CategoryDialog.Rename(it)) },
onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) }, onClickDelete = { viewModel.showDialog(CategoryDialog.Delete(it)) },
onChangeOrder = screenModel::changeOrder, onChangeOrder = viewModel::changeOrder,
navigateUp = navigator::pop, navigateUp = navigator::pop,
) )
@@ -48,30 +48,30 @@ class CategoryScreen : Screen() {
null -> {} null -> {}
CategoryDialog.Create -> { CategoryDialog.Create -> {
CategoryCreateDialog( CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = viewModel::dismissDialog,
onCreate = screenModel::createCategory, onCreate = viewModel::createCategory,
categories = successState.categories.fastMap { it.name }, categories = successState.categories.fastMap { it.name },
) )
} }
is CategoryDialog.Rename -> { is CategoryDialog.Rename -> {
CategoryRenameDialog( CategoryRenameDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = viewModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) }, onRename = { viewModel.renameCategory(dialog.category, it) },
categories = successState.categories.fastMap { it.name }, categories = successState.categories.fastMap { it.name },
category = dialog.category.name, category = dialog.category.name,
) )
} }
is CategoryDialog.Delete -> { is CategoryDialog.Delete -> {
CategoryDeleteDialog( CategoryDeleteDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = viewModel::dismissDialog,
onDelete = { screenModel.deleteCategory(dialog.category.id) }, onDelete = { viewModel.deleteCategory(dialog.category.id) },
category = dialog.category.name, category = dialog.category.name,
) )
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.events.collectLatest { event -> viewModel.events.collectLatest { event ->
if (event is CategoryEvent.LocalizedMessage) { if (event is CategoryEvent.LocalizedMessage) {
context.toast(event.stringRes) context.toast(event.stringRes)
} }
@@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.domain.category.interactor.CreateCategoryWithName import tachiyomi.domain.category.interactor.CreateCategoryWithName
import tachiyomi.domain.category.interactor.DeleteCategory import tachiyomi.domain.category.interactor.DeleteCategory
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
@@ -19,19 +19,19 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class CategoryScreenModel( class CategoryViewModel(
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val createCategoryWithName: CreateCategoryWithName = Injekt.get(), private val createCategoryWithName: CreateCategoryWithName = Injekt.get(),
private val deleteCategory: DeleteCategory = Injekt.get(), private val deleteCategory: DeleteCategory = Injekt.get(),
private val reorderCategory: ReorderCategory = Injekt.get(), private val reorderCategory: ReorderCategory = Injekt.get(),
private val renameCategory: RenameCategory = Injekt.get(), private val renameCategory: RenameCategory = Injekt.get(),
) : StateScreenModel<CategoryScreenState>(CategoryScreenState.Loading) { ) : StateViewModel<CategoryScreenState>(CategoryScreenState.Loading) {
private val _events: Channel<CategoryEvent> = Channel() private val _events: Channel<CategoryEvent> = Channel()
val events = _events.receiveAsFlow() val events = _events.receiveAsFlow()
init { init {
screenModelScope.launch { viewModelScope.launch {
getCategories.subscribe() getCategories.subscribe()
.collectLatest { categories -> .collectLatest { categories ->
mutableState.update { mutableState.update {
@@ -45,7 +45,7 @@ class CategoryScreenModel(
} }
fun createCategory(name: String) { fun createCategory(name: String) {
screenModelScope.launch { viewModelScope.launch {
when (createCategoryWithName.await(name)) { when (createCategoryWithName.await(name)) {
is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError) is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@@ -54,7 +54,7 @@ class CategoryScreenModel(
} }
fun deleteCategory(categoryId: Long) { fun deleteCategory(categoryId: Long) {
screenModelScope.launch { viewModelScope.launch {
when (deleteCategory.await(categoryId = categoryId)) { when (deleteCategory.await(categoryId = categoryId)) {
is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@@ -63,7 +63,7 @@ class CategoryScreenModel(
} }
fun changeOrder(category: Category, newIndex: Int) { fun changeOrder(category: Category, newIndex: Int) {
screenModelScope.launch { viewModelScope.launch {
when (reorderCategory.await(category, newIndex)) { when (reorderCategory.await(category, newIndex)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@@ -72,7 +72,7 @@ class CategoryScreenModel(
} }
fun renameCategory(category: Category, name: String) { fun renameCategory(category: Category, name: String) {
screenModelScope.launch { viewModelScope.launch {
when (renameCategory.await(category, name)) { when (renameCategory.await(category, name)) {
is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@@ -6,7 +6,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@@ -28,10 +29,13 @@ class DeepLinkScreen(
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<DeepLinkViewModel>(
DeepLinkScreenModel(query = query) factory = DeepLinkViewModel.Factory,
} extras = CreationExtras {
val state by screenModel.state.collectAsState() set(DeepLinkViewModel.QUERY_KEY, query)
},
)
val state by viewModel.state.collectAsState()
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
@@ -42,14 +46,14 @@ class DeepLinkScreen(
}, },
) { contentPadding -> ) { contentPadding ->
when (state) { when (state) {
is DeepLinkScreenModel.State.Loading -> { is DeepLinkViewModel.State.Loading -> {
LoadingScreen(Modifier.padding(contentPadding)) LoadingScreen(Modifier.padding(contentPadding))
} }
is DeepLinkScreenModel.State.NoResults -> { is DeepLinkViewModel.State.NoResults -> {
navigator.replace(GlobalSearchScreen(query)) navigator.replace(GlobalSearchScreen(query))
} }
is DeepLinkScreenModel.State.Result -> { is DeepLinkViewModel.State.Result -> {
val resultState = state as DeepLinkScreenModel.State.Result val resultState = state as DeepLinkViewModel.State.Result
if (resultState.chapterId == null) { if (resultState.chapterId == null) {
navigator.replace( navigator.replace(
MangaScreen( MangaScreen(
@@ -1,13 +1,16 @@
package eu.kanade.tachiyomi.ui.deeplink package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.ResolvableSource import eu.kanade.tachiyomi.source.online.ResolvableSource
import eu.kanade.tachiyomi.source.online.UriType import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -19,16 +22,28 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class DeepLinkScreenModel( class DeepLinkViewModel(
query: String = "", query: String,
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(), private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) { ) : StateViewModel<DeepLinkViewModel.State>(State.Loading) {
companion object {
val QUERY_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
DeepLinkViewModel(
query = get(QUERY_KEY)!!,
)
}
}
}
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
val source = sourceManager.getAll() val source = sourceManager.getAll()
.filterIsInstance<ResolvableSource>() .filterIsInstance<ResolvableSource>()
.firstOrNull { it.getUriType(query) != UriType.Unknown } .firstOrNull { it.getUriType(query) != UriType.Unknown }
@@ -41,8 +41,8 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@@ -65,8 +65,8 @@ object DownloadQueueScreen : Screen() {
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val screenModel = rememberScreenModel { DownloadQueueScreenModel() } val viewModel = viewModel<DownloadQueueViewModel>()
val downloadList by screenModel.state.collectAsState() val downloadList by viewModel.state.collectAsState()
val downloadCount by remember { val downloadCount by remember {
derivedStateOf { downloadList.sumOf { it.subItems.size } } derivedStateOf { downloadList.sumOf { it.subItems.size } }
} }
@@ -133,7 +133,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_newest)) }, text = { Text(text = stringResource(MR.strings.action_newest)) },
onClick = { onClick = {
screenModel.reorderQueue( viewModel.reorderQueue(
{ it.download.chapter.dateUpload }, { it.download.chapter.dateUpload },
true, true,
) )
@@ -143,7 +143,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_oldest)) }, text = { Text(text = stringResource(MR.strings.action_oldest)) },
onClick = { onClick = {
screenModel.reorderQueue( viewModel.reorderQueue(
{ it.download.chapter.dateUpload }, { it.download.chapter.dateUpload },
false, false,
) )
@@ -158,7 +158,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_asc)) }, text = { Text(text = stringResource(MR.strings.action_asc)) },
onClick = { onClick = {
screenModel.reorderQueue( viewModel.reorderQueue(
{ it.download.chapter.chapterNumber }, { it.download.chapter.chapterNumber },
false, false,
) )
@@ -168,7 +168,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_desc)) }, text = { Text(text = stringResource(MR.strings.action_desc)) },
onClick = { onClick = {
screenModel.reorderQueue( viewModel.reorderQueue(
{ it.download.chapter.chapterNumber }, { it.download.chapter.chapterNumber },
true, true,
) )
@@ -188,7 +188,7 @@ object DownloadQueueScreen : Screen() {
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_cancel_all), title = stringResource(MR.strings.action_cancel_all),
onClick = { screenModel.clearQueue() }, onClick = { viewModel.clearQueue() },
), ),
), ),
) )
@@ -198,7 +198,7 @@ object DownloadQueueScreen : Screen() {
) )
}, },
floatingActionButton = { floatingActionButton = {
val isRunning by screenModel.isDownloaderRunning.collectAsState() val isRunning by viewModel.isDownloaderRunning.collectAsState()
SmallExtendedFloatingActionButton( SmallExtendedFloatingActionButton(
text = { text = {
val id = if (isRunning) { val id = if (isRunning) {
@@ -218,9 +218,9 @@ object DownloadQueueScreen : Screen() {
}, },
onClick = { onClick = {
if (isRunning) { if (isRunning) {
screenModel.pauseDownloads() viewModel.pauseDownloads()
} else { } else {
screenModel.startDownloads() viewModel.startDownloads()
} }
}, },
expanded = fabExpanded, expanded = fabExpanded,
@@ -250,27 +250,27 @@ object DownloadQueueScreen : Screen() {
AndroidView( AndroidView(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
factory = { context -> factory = { context ->
screenModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context)) viewModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context))
screenModel.adapter = DownloadAdapter(screenModel.listener) viewModel.adapter = DownloadAdapter(viewModel.listener)
screenModel.controllerBinding.root.adapter = screenModel.adapter viewModel.controllerBinding.root.adapter = viewModel.adapter
screenModel.adapter?.isHandleDragEnabled = true viewModel.adapter?.isHandleDragEnabled = true
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(context) viewModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true) ViewCompat.setNestedScrollingEnabled(viewModel.controllerBinding.root, true)
scope.launchUI { scope.launchUI {
screenModel.getDownloadStatusFlow() viewModel.getDownloadStatusFlow()
.collect(screenModel::onStatusChange) .collect(viewModel::onStatusChange)
} }
scope.launchUI { scope.launchUI {
screenModel.getDownloadProgressFlow() viewModel.getDownloadProgressFlow()
.collect(screenModel::onUpdateDownloadedPages) .collect(viewModel::onUpdateDownloadedPages)
} }
screenModel.controllerBinding.root viewModel.controllerBinding.root
}, },
update = { update = {
screenModel.controllerBinding.root viewModel.controllerBinding.root
.updatePadding( .updatePadding(
left = left, left = left,
top = top, top = top,
@@ -278,7 +278,7 @@ object DownloadQueueScreen : Screen() {
bottom = bottom, bottom = bottom,
) )
screenModel.adapter?.updateDataSet(downloadList) viewModel.adapter?.updateDataSet(downloadList)
}, },
) )
} }
@@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.download package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem import android.view.MenuItem
import cafe.adriel.voyager.core.model.ScreenModel import androidx.lifecycle.ViewModel
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewModelScope
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@@ -25,9 +25,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
class DownloadQueueScreenModel( class DownloadQueueViewModel(
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
) : ScreenModel { ) : ViewModel() {
private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>()) private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@@ -117,7 +117,7 @@ class DownloadQueueScreenModel(
} }
init { init {
screenModelScope.launch { viewModelScope.launch {
downloadManager.queueState downloadManager.queueState
.map { downloads -> .map { downloads ->
downloads downloads
@@ -132,7 +132,7 @@ class DownloadQueueScreenModel(
} }
} }
override fun onDispose() { override fun onCleared() {
for (job in progressJobs.values) { for (job in progressJobs.values) {
job.cancel() job.cancel()
} }
@@ -141,7 +141,7 @@ class DownloadQueueScreenModel(
} }
val isDownloaderRunning = downloadManager.isDownloaderRunning val isDownloaderRunning = downloadManager.isDownloaderRunning
.stateIn(screenModelScope, SharingStarted.WhileSubscribed(5000), false) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
fun getDownloadStatusFlow() = downloadManager.statusFlow() fun getDownloadStatusFlow() = downloadManager.statusFlow()
fun getDownloadProgressFlow() = downloadManager.progressFlow() fun getDownloadProgressFlow() = downloadManager.progressFlow()
@@ -211,7 +211,7 @@ class DownloadQueueScreenModel(
* @param download the download to observe its progress. * @param download the download to observe its progress.
*/ */
private fun launchProgressJob(download: Download) { private fun launchProgressJob(download: Download) {
val job = screenModelScope.launch { val job = viewModelScope.launch {
while (download.pages == null) { while (download.pages == null) {
delay(50.milliseconds) delay(50.milliseconds)
} }
@@ -10,7 +10,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -62,59 +62,59 @@ data object HistoryTab : Tab {
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val screenModel = rememberScreenModel { HistoryScreenModel() } val viewModel = viewModel<HistoryViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
HistoryScreen( HistoryScreen(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
onSearchQueryChange = screenModel::updateSearchQuery, onSearchQueryChange = viewModel::updateSearchQuery,
onClickCover = { navigator.push(MangaScreen(it)) }, onClickCover = { navigator.push(MangaScreen(it)) },
onClickResume = screenModel::getNextChapterForManga, onClickResume = viewModel::getNextChapterForManga,
onDialogChange = screenModel::setDialog, onDialogChange = viewModel::setDialog,
onClickFavorite = screenModel::addFavorite, onClickFavorite = viewModel::addFavorite,
) )
val onDismissRequest = { screenModel.setDialog(null) } val onDismissRequest = { viewModel.setDialog(null) }
when (val dialog = state.dialog) { when (val dialog = state.dialog) {
is HistoryScreenModel.Dialog.Delete -> { is HistoryViewModel.Dialog.Delete -> {
HistoryDeleteDialog( HistoryDeleteDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onDelete = { all -> onDelete = { all ->
if (all) { if (all) {
screenModel.removeAllFromHistory(dialog.history.mangaId) viewModel.removeAllFromHistory(dialog.history.mangaId)
} else { } else {
screenModel.removeFromHistory(dialog.history) viewModel.removeFromHistory(dialog.history)
} }
}, },
) )
} }
is HistoryScreenModel.Dialog.DeleteAll -> { is HistoryViewModel.Dialog.DeleteAll -> {
HistoryDeleteAllDialog( HistoryDeleteAllDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onDelete = screenModel::removeAllHistory, onDelete = viewModel::removeAllHistory,
) )
} }
is HistoryScreenModel.Dialog.DuplicateManga -> { is HistoryViewModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates, duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) }, onConfirm = { viewModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(it.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) }, onMigrate = { viewModel.showMigrateDialog(dialog.manga, it) },
) )
} }
is HistoryScreenModel.Dialog.ChangeCategory -> { is HistoryViewModel.Dialog.ChangeCategory -> {
ChangeCategoryDialog( ChangeCategoryDialog(
initialSelection = dialog.initialSelection, initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) }, onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ -> onConfirm = { include, _ ->
screenModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include) viewModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include)
}, },
) )
} }
is HistoryScreenModel.Dialog.Migrate -> { is HistoryViewModel.Dialog.Migrate -> {
MigrateMangaDialog( MigrateMangaDialog(
current = dialog.current, current = dialog.current,
target = dialog.target, target = dialog.target,
@@ -133,20 +133,20 @@ data object HistoryTab : Tab {
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.events.collectLatest { e -> viewModel.events.collectLatest { e ->
when (e) { when (e) {
HistoryScreenModel.Event.InternalError -> HistoryViewModel.Event.InternalError ->
snackbarHostState.showSnackbar(context.stringResource(MR.strings.internal_error)) snackbarHostState.showSnackbar(context.stringResource(MR.strings.internal_error))
HistoryScreenModel.Event.HistoryCleared -> HistoryViewModel.Event.HistoryCleared ->
snackbarHostState.showSnackbar(context.stringResource(MR.strings.clear_history_completed)) snackbarHostState.showSnackbar(context.stringResource(MR.strings.clear_history_completed))
is HistoryScreenModel.Event.OpenChapter -> openChapter(context, e.chapter) is HistoryViewModel.Event.OpenChapter -> openChapter(context, e.chapter)
} }
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
resumeLastChapterReadEvent.receiveAsFlow().collectLatest { resumeLastChapterReadEvent.receiveAsFlow().collectLatest {
openChapter(context, screenModel.getNextChapter()) openChapter(context, viewModel.getNextChapter())
} }
} }
} }
@@ -2,8 +2,7 @@ package eu.kanade.tachiyomi.ui.history
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.AddTracks
@@ -21,6 +20,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -43,7 +43,7 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class HistoryScreenModel( class HistoryViewModel(
private val addTracks: AddTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
@@ -56,13 +56,13 @@ class HistoryScreenModel(
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
) : StateScreenModel<HistoryScreenModel.State>(State()) { ) : StateViewModel<HistoryViewModel.State>(State()) {
private val _events: Channel<Event> = Channel(Channel.UNLIMITED) private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
val events: Flow<Event> = _events.receiveAsFlow() val events: Flow<Event> = _events.receiveAsFlow()
init { init {
screenModelScope.launch { viewModelScope.launch {
state.map { it.searchQuery } state.map { it.searchQuery }
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest { query -> .flatMapLatest { query ->
@@ -97,7 +97,7 @@ class HistoryScreenModel(
} }
fun getNextChapterForManga(mangaId: Long, chapterId: Long) { fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
screenModelScope.launchIO { viewModelScope.launchIO {
sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false)) sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))
} }
} }
@@ -108,19 +108,19 @@ class HistoryScreenModel(
} }
fun removeFromHistory(history: HistoryWithRelations) { fun removeFromHistory(history: HistoryWithRelations) {
screenModelScope.launchIO { viewModelScope.launchIO {
removeHistory.await(history) removeHistory.await(history)
} }
} }
fun removeAllFromHistory(mangaId: Long) { fun removeAllFromHistory(mangaId: Long) {
screenModelScope.launchIO { viewModelScope.launchIO {
removeHistory.await(mangaId) removeHistory.await(mangaId)
} }
} }
fun removeAllHistory() { fun removeAllHistory() {
screenModelScope.launchIO { viewModelScope.launchIO {
val result = removeHistory.awaitAll() val result = removeHistory.awaitAll()
if (!result) return@launchIO if (!result) return@launchIO
_events.send(Event.HistoryCleared) _events.send(Event.HistoryCleared)
@@ -150,7 +150,7 @@ class HistoryScreenModel(
} }
private fun moveMangaToCategory(mangaId: Long, categoryIds: List<Long>) { private fun moveMangaToCategory(mangaId: Long, categoryIds: List<Long>) {
screenModelScope.launchIO { viewModelScope.launchIO {
setMangaCategories.await(mangaId, categoryIds) setMangaCategories.await(mangaId, categoryIds)
} }
} }
@@ -159,7 +159,7 @@ class HistoryScreenModel(
moveMangaToCategory(manga.id, categories) moveMangaToCategory(manga.id, categories)
if (manga.favorite) return if (manga.favorite) return
screenModelScope.launchIO { viewModelScope.launchIO {
updateManga.awaitUpdateFavorite(manga.id, true) updateManga.awaitUpdateFavorite(manga.id, true)
} }
} }
@@ -170,7 +170,7 @@ class HistoryScreenModel(
} }
fun addFavorite(mangaId: Long) { fun addFavorite(mangaId: Long) {
screenModelScope.launchIO { viewModelScope.launchIO {
val manga = getManga.await(mangaId) ?: return@launchIO val manga = getManga.await(mangaId) ?: return@launchIO
val duplicates = getDuplicateLibraryManga(manga) val duplicates = getDuplicateLibraryManga(manga)
@@ -184,7 +184,7 @@ class HistoryScreenModel(
} }
fun addFavorite(manga: Manga) { fun addFavorite(manga: Manga) {
screenModelScope.launchIO { viewModelScope.launchIO {
// Move to default category if applicable // Move to default category if applicable
val categories = getCategories() val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory.get().toLong() val defaultCategoryId = libraryPreferences.defaultCategory.get().toLong()
@@ -221,7 +221,7 @@ class HistoryScreenModel(
} }
fun showChangeCategoryDialog(manga: Manga) { fun showChangeCategoryDialog(manga: Manga) {
screenModelScope.launch { viewModelScope.launch {
val categories = getCategories() val categories = getCategories()
val selection = getMangaCategoryIds(manga) val selection = getMangaCategoryIds(manga)
mutableState.update { currentState -> mutableState.update { currentState ->
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import cafe.adriel.voyager.core.model.ScreenModel import androidx.lifecycle.ViewModel
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewModelScope
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@@ -20,17 +20,17 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class LibrarySettingsScreenModel( class LibrarySettingsViewModel(
val preferences: BasePreferences = Injekt.get(), val preferences: BasePreferences = Injekt.get(),
val libraryPreferences: LibraryPreferences = Injekt.get(), val libraryPreferences: LibraryPreferences = Injekt.get(),
private val setDisplayMode: SetDisplayMode = Injekt.get(), private val setDisplayMode: SetDisplayMode = Injekt.get(),
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(), private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
trackerManager: TrackerManager = Injekt.get(), trackerManager: TrackerManager = Injekt.get(),
) : ScreenModel { ) : ViewModel() {
val trackersFlow = trackerManager.loggedInTrackersFlow() val trackersFlow = trackerManager.loggedInTrackersFlow()
.stateIn( .stateIn(
scope = screenModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
initialValue = trackerManager.loggedInTrackers(), initialValue = trackerManager.loggedInTrackers(),
) )
@@ -50,7 +50,7 @@ class LibrarySettingsScreenModel(
} }
fun setSort(category: Category?, mode: LibrarySort.Type, direction: LibrarySort.Direction) { fun setSort(category: Category?, mode: LibrarySort.Type, direction: LibrarySort.Direction) {
screenModelScope.launchIO { viewModelScope.launchIO {
setSortModeForCategory.await(category, mode, direction) setSortModeForCategory.await(category, mode, direction)
} }
} }
@@ -21,7 +21,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -86,9 +86,9 @@ data object LibraryTab : Tab {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val screenModel = rememberScreenModel { LibraryScreenModel() } val viewModel = viewModel<LibraryViewModel>()
val settingsScreenModel = rememberScreenModel { LibrarySettingsScreenModel() } val settingsViewModel = viewModel<LibrarySettingsViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
@@ -116,15 +116,15 @@ data object LibraryTab : Tab {
hasActiveFilters = state.hasActiveFilters, hasActiveFilters = state.hasActiveFilters,
selectedCount = state.selection.size, selectedCount = state.selection.size,
title = title, title = title,
onClickUnselectAll = screenModel::clearSelection, onClickUnselectAll = viewModel::clearSelection,
onClickSelectAll = screenModel::selectAll, onClickSelectAll = viewModel::selectAll,
onClickInvertSelection = screenModel::invertSelection, onClickInvertSelection = viewModel::invertSelection,
onClickFilter = screenModel::showSettingsDialog, onClickFilter = viewModel::showSettingsDialog,
onClickRefresh = { onClickRefresh(state.activeCategory) }, onClickRefresh = { onClickRefresh(state.activeCategory) },
onClickGlobalUpdate = { onClickRefresh(null) }, onClickGlobalUpdate = { onClickRefresh(null) },
onClickOpenRandomManga = { onClickOpenRandomManga = {
scope.launch { scope.launch {
val randomItem = screenModel.getRandomLibraryItemForCurrentCategory() val randomItem = viewModel.getRandomLibraryItemForCurrentCategory()
if (randomItem != null) { if (randomItem != null) {
navigator.push(MangaScreen(randomItem.libraryManga.manga.id)) navigator.push(MangaScreen(randomItem.libraryManga.manga.id))
} else { } else {
@@ -135,7 +135,7 @@ data object LibraryTab : Tab {
} }
}, },
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
onSearchQueryChange = screenModel::search, onSearchQueryChange = viewModel::search,
// For scroll overlay when no tab // For scroll overlay when no tab
scrollBehavior = scrollBehavior.takeIf { !state.showCategoryTabs }, scrollBehavior = scrollBehavior.takeIf { !state.showCategoryTabs },
) )
@@ -143,15 +143,15 @@ data object LibraryTab : Tab {
bottomBar = { bottomBar = {
LibraryBottomActionMenu( LibraryBottomActionMenu(
visible = state.selectionMode, visible = state.selectionMode,
onChangeCategoryClicked = screenModel::openChangeCategoryDialog, onChangeCategoryClicked = viewModel::openChangeCategoryDialog,
onMarkAsReadClicked = { screenModel.markReadSelection(true) }, onMarkAsReadClicked = { viewModel.markReadSelection(true) },
onMarkAsUnreadClicked = { screenModel.markReadSelection(false) }, onMarkAsUnreadClicked = { viewModel.markReadSelection(false) },
onDownloadClicked = screenModel::performDownloadAction onDownloadClicked = viewModel::performDownloadAction
.takeIf { state.selectedManga.fastAll { !it.isLocal() } }, .takeIf { state.selectedManga.fastAll { !it.isLocal() } },
onDeleteClicked = screenModel::openDeleteMangaDialog, onDeleteClicked = viewModel::openDeleteMangaDialog,
onMigrateClicked = { onMigrateClicked = {
val selection = state.selection val selection = state.selection
screenModel.clearSelection() viewModel.clearSelection()
navigator.push(MigrationConfigScreen(selection)) navigator.push(MigrationConfigScreen(selection))
}, },
) )
@@ -185,11 +185,11 @@ data object LibraryTab : Tab {
currentPage = state.coercedActiveCategoryIndex, currentPage = state.coercedActiveCategoryIndex,
hasActiveFilters = state.hasActiveFilters, hasActiveFilters = state.hasActiveFilters,
showPageTabs = state.showCategoryTabs || !state.searchQuery.isNullOrEmpty(), showPageTabs = state.showCategoryTabs || !state.searchQuery.isNullOrEmpty(),
onChangeCurrentPage = screenModel::updateActiveCategoryIndex, onChangeCurrentPage = viewModel::updateActiveCategoryIndex,
onClickManga = { navigator.push(MangaScreen(it)) }, onClickManga = { navigator.push(MangaScreen(it)) },
onContinueReadingClicked = { it: LibraryManga -> onContinueReadingClicked = { it: LibraryManga ->
scope.launchIO { scope.launchIO {
val chapter = screenModel.getNextUnreadChapter(it.manga) val chapter = viewModel.getNextUnreadChapter(it.manga)
if (chapter != null) { if (chapter != null) {
context.startActivity( context.startActivity(
ReaderActivity.newIntent(context, chapter.mangaId, chapter.id), ReaderActivity.newIntent(context, chapter.mangaId, chapter.id),
@@ -200,54 +200,54 @@ data object LibraryTab : Tab {
} }
Unit Unit
}.takeIf { state.showMangaContinueButton }, }.takeIf { state.showMangaContinueButton },
onToggleSelection = screenModel::toggleSelection, onToggleSelection = viewModel::toggleSelection,
onToggleRangeSelection = { category, manga -> onToggleRangeSelection = { category, manga ->
screenModel.toggleRangeSelection(category, manga) viewModel.toggleRangeSelection(category, manga)
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}, },
onRefresh = { onClickRefresh(state.activeCategory) }, onRefresh = { onClickRefresh(state.activeCategory) },
onGlobalSearchClicked = { onGlobalSearchClicked = {
navigator.push(GlobalSearchScreen(screenModel.state.value.searchQuery ?: "")) navigator.push(GlobalSearchScreen(viewModel.state.value.searchQuery ?: ""))
}, },
getItemCountForCategory = { state.getItemCountForCategory(it) }, getItemCountForCategory = { state.getItemCountForCategory(it) },
getDisplayMode = { screenModel.getDisplayMode() }, getDisplayMode = { viewModel.getDisplayMode() },
getColumnsForOrientation = { screenModel.getColumnsForOrientation(it) }, getColumnsForOrientation = { viewModel.getColumnsForOrientation(it) },
getItemsForCategory = { state.getItemsForCategory(it) }, getItemsForCategory = { state.getItemsForCategory(it) },
) )
} }
} }
} }
val onDismissRequest = screenModel::closeDialog val onDismissRequest = viewModel::closeDialog
when (val dialog = state.dialog) { when (val dialog = state.dialog) {
is LibraryScreenModel.Dialog.SettingsSheet -> run { is LibraryViewModel.Dialog.SettingsSheet -> run {
LibrarySettingsDialog( LibrarySettingsDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel, viewModel = settingsViewModel,
category = state.activeCategory, category = state.activeCategory,
) )
} }
is LibraryScreenModel.Dialog.ChangeCategory -> { is LibraryViewModel.Dialog.ChangeCategory -> {
ChangeCategoryDialog( ChangeCategoryDialog(
initialSelection = dialog.initialSelection, initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onEditCategories = { onEditCategories = {
screenModel.clearSelection() viewModel.clearSelection()
navigator.push(CategoryScreen()) navigator.push(CategoryScreen())
}, },
onConfirm = { include, exclude -> onConfirm = { include, exclude ->
screenModel.clearSelection() viewModel.clearSelection()
screenModel.setMangaCategories(dialog.manga, include, exclude) viewModel.setMangaCategories(dialog.manga, include, exclude)
}, },
) )
} }
is LibraryScreenModel.Dialog.DeleteManga -> { is LibraryViewModel.Dialog.DeleteManga -> {
DeleteLibraryMangaDialog( DeleteLibraryMangaDialog(
containsLocalManga = dialog.manga.any(Manga::isLocal), containsLocalManga = dialog.manga.any(Manga::isLocal),
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { deleteManga, deleteChapter -> onConfirm = { deleteManga, deleteChapter ->
screenModel.removeMangas(dialog.manga, deleteManga, deleteChapter) viewModel.removeMangas(dialog.manga, deleteManga, deleteChapter)
screenModel.clearSelection() viewModel.clearSelection()
}, },
) )
} }
@@ -256,8 +256,8 @@ data object LibraryTab : Tab {
BackHandler(enabled = state.selectionMode || state.searchQuery != null) { BackHandler(enabled = state.selectionMode || state.searchQuery != null) {
when { when {
state.selectionMode -> screenModel.clearSelection() state.selectionMode -> viewModel.clearSelection()
state.searchQuery != null -> screenModel.search(null) state.searchQuery != null -> viewModel.search(null)
} }
} }
@@ -272,8 +272,8 @@ data object LibraryTab : Tab {
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch { queryEvent.receiveAsFlow().collect(screenModel::search) } launch { queryEvent.receiveAsFlow().collect(viewModel::search) }
launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { screenModel.showSettingsDialog() } } launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { viewModel.showSettingsDialog() } }
} }
} }
@@ -4,8 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.PreferenceMutableState import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.core.util.fastFilterNot import eu.kanade.core.util.fastFilterNot
@@ -36,6 +35,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.flow.updateAndGet
import mihon.core.common.utils.mutate import mihon.core.common.utils.mutate
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.util.lang.compareToWithCollator import tachiyomi.core.common.util.lang.compareToWithCollator
@@ -66,7 +66,7 @@ import uy.kohesive.injekt.api.get
import kotlin.random.Random import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class LibraryScreenModel( class LibraryViewModel(
private val getLibraryManga: GetLibraryManga = Injekt.get(), private val getLibraryManga: GetLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getTracksPerManga: GetTracksPerManga = Injekt.get(), private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
@@ -83,13 +83,13 @@ class LibraryScreenModel(
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val downloadCache: DownloadCache = Injekt.get(), private val downloadCache: DownloadCache = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(),
) : StateScreenModel<LibraryScreenModel.State>(State()) { ) : StateViewModel<LibraryViewModel.State>(State()) {
init { init {
mutableState.update { state -> mutableState.update { state ->
state.copy(activeCategoryIndex = libraryPreferences.lastUsedCategory.get()) state.copy(activeCategoryIndex = libraryPreferences.lastUsedCategory.get())
} }
screenModelScope.launchIO { viewModelScope.launchIO {
combine( combine(
state.map { it.searchQuery }.distinctUntilChanged().debounce(0.25.seconds), state.map { it.searchQuery }.distinctUntilChanged().debounce(0.25.seconds),
getCategories.subscribe(), getCategories.subscribe(),
@@ -119,7 +119,7 @@ class LibraryScreenModel(
} }
} }
screenModelScope.launchIO { viewModelScope.launchIO {
state state
.dropWhile { !it.libraryData.isInitialized } .dropWhile { !it.libraryData.isInitialized }
.map { it.libraryData } .map { it.libraryData }
@@ -153,7 +153,7 @@ class LibraryScreenModel(
) )
} }
} }
.launchIn(screenModelScope) .launchIn(viewModelScope)
combine( combine(
getLibraryItemPreferencesFlow(), getLibraryItemPreferencesFlow(),
@@ -176,7 +176,7 @@ class LibraryScreenModel(
state.copy(hasActiveFilters = it) state.copy(hasActiveFilters = it)
} }
} }
.launchIn(screenModelScope) .launchIn(viewModelScope)
} }
private fun List<LibraryItem>.applyFilters( private fun List<LibraryItem>.applyFilters(
@@ -479,7 +479,7 @@ class LibraryScreenModel(
private fun downloadNextChapters(amount: Int?) { private fun downloadNextChapters(amount: Int?) {
val mangas = state.value.selectedManga val mangas = state.value.selectedManga
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
mangas.forEach { manga -> mangas.forEach { manga ->
val chapters = getNextChapters.await(manga.id) val chapters = getNextChapters.await(manga.id)
.fastFilterNot { chapter -> .fastFilterNot { chapter ->
@@ -501,7 +501,7 @@ class LibraryScreenModel(
private fun downloadBookmarkedChapters() { private fun downloadBookmarkedChapters() {
val mangas = state.value.selectedManga val mangas = state.value.selectedManga
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
mangas.forEach { manga -> mangas.forEach { manga ->
val chapters = getBookmarkedChaptersByMangaId.await(manga.id) val chapters = getBookmarkedChaptersByMangaId.await(manga.id)
.fastFilterNot { chapter -> .fastFilterNot { chapter ->
@@ -524,7 +524,7 @@ class LibraryScreenModel(
*/ */
fun markReadSelection(read: Boolean) { fun markReadSelection(read: Boolean) {
val selection = state.value.selectedManga val selection = state.value.selectedManga
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
selection.forEach { manga -> selection.forEach { manga ->
setReadStatus.await( setReadStatus.await(
manga = manga, manga = manga,
@@ -543,7 +543,7 @@ class LibraryScreenModel(
* @param deleteChapters whether to delete downloaded chapters. * @param deleteChapters whether to delete downloaded chapters.
*/ */
fun removeMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) { fun removeMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
if (deleteFromLibrary) { if (deleteFromLibrary) {
val toDelete = mangas.map { val toDelete = mangas.map {
it.removeCovers(coverCache) it.removeCovers(coverCache)
@@ -574,7 +574,7 @@ class LibraryScreenModel(
* @param removeCategories the categories to remove in all mangas. * @param removeCategories the categories to remove in all mangas.
*/ */
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) { fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
mangaList.forEach { manga -> mangaList.forEach { manga ->
val categoryIds = getCategories.await(manga.id) val categoryIds = getCategories.await(manga.id)
.map { it.id } .map { it.id }
@@ -588,12 +588,12 @@ class LibraryScreenModel(
} }
fun getDisplayMode(): PreferenceMutableState<LibraryDisplayMode> { fun getDisplayMode(): PreferenceMutableState<LibraryDisplayMode> {
return libraryPreferences.displayMode.asState(screenModelScope) return libraryPreferences.displayMode.asState(viewModelScope)
} }
fun getColumnsForOrientation(isLandscape: Boolean): PreferenceMutableState<Int> { fun getColumnsForOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
return (if (isLandscape) libraryPreferences.landscapeColumns else libraryPreferences.portraitColumns) return (if (isLandscape) libraryPreferences.landscapeColumns else libraryPreferences.portraitColumns)
.asState(screenModelScope) .asState(viewModelScope)
} }
fun getRandomLibraryItemForCurrentCategory(): LibraryItem? { fun getRandomLibraryItemForCurrentCategory(): LibraryItem? {
@@ -689,7 +689,7 @@ class LibraryScreenModel(
} }
fun openChangeCategoryDialog() { fun openChangeCategoryDialog() {
screenModelScope.launchIO { viewModelScope.launchIO {
// Create a copy of selected manga // Create a copy of selected manga
val mangaList = state.value.selectedManga val mangaList = state.value.selectedManga
@@ -3,8 +3,10 @@ package eu.kanade.tachiyomi.ui.manga
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import coil3.asDrawable import coil3.asDrawable
import coil3.imageLoader import coil3.imageLoader
import coil3.request.ImageRequest import coil3.request.ImageRequest
@@ -20,6 +22,7 @@ import eu.kanade.tachiyomi.util.system.toShareIntent
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@@ -31,7 +34,7 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MangaCoverScreenModel( class MangaCoverViewModel(
private val mangaId: Long, private val mangaId: Long,
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val imageSaver: ImageSaver = Injekt.get(), private val imageSaver: ImageSaver = Injekt.get(),
@@ -39,17 +42,29 @@ class MangaCoverScreenModel(
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<Manga?>(null) { ) : StateViewModel<Manga?>(null) {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MangaCoverViewModel(
mangaId = get(MANGA_ID_KEY)!!,
)
}
}
}
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
getManga.subscribe(mangaId) getManga.subscribe(mangaId)
.collect { newManga -> mutableState.update { newManga } } .collect { newManga -> mutableState.update { newManga } }
} }
} }
fun saveCover(context: Context) { fun saveCover(context: Context) {
screenModelScope.launch { viewModelScope.launch {
try { try {
saveCoverInternal(context, temp = false) saveCoverInternal(context, temp = false)
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
@@ -67,7 +82,7 @@ class MangaCoverScreenModel(
} }
fun shareCover(context: Context) { fun shareCover(context: Context) {
screenModelScope.launch { viewModelScope.launch {
try { try {
val uri = saveCoverInternal(context, temp = true) ?: return@launch val uri = saveCoverInternal(context, temp = true) ?: return@launch
withUIContext { withUIContext {
@@ -119,7 +134,7 @@ class MangaCoverScreenModel(
*/ */
fun editCover(context: Context, data: Uri) { fun editCover(context: Context, data: Uri) {
val manga = state.value ?: return val manga = state.value ?: return
screenModelScope.launchIO { viewModelScope.launchIO {
context.contentResolver.openInputStream(data)?.use { context.contentResolver.openInputStream(data)?.use {
try { try {
manga.editCover(Injekt.get(), it, updateManga, coverCache) manga.editCover(Injekt.get(), it, updateManga, coverCache)
@@ -133,7 +148,7 @@ class MangaCoverScreenModel(
fun deleteCustomCover(context: Context) { fun deleteCustomCover(context: Context) {
val mangaId = state.value?.id ?: return val mangaId = state.value?.id ?: return
screenModelScope.launchIO { viewModelScope.launchIO {
try { try {
coverCache.deleteCustomCover(mangaId) coverCache.deleteCustomCover(mangaId)
updateManga.awaitUpdateCoverLastModified(mangaId) updateManga.awaitUpdateCoverLastModified(mangaId)
@@ -145,7 +160,7 @@ class MangaCoverScreenModel(
} }
private fun notifyCoverUpdated(context: Context) { private fun notifyCoverUpdated(context: Context) {
screenModelScope.launch { viewModelScope.launch {
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
context.stringResource(MR.strings.cover_updated), context.stringResource(MR.strings.cover_updated),
withDismissAction = true, withDismissAction = true,
@@ -154,7 +169,7 @@ class MangaCoverScreenModel(
} }
private fun notifyFailedCoverUpdate(context: Context, e: Throwable) { private fun notifyFailedCoverUpdate(context: Context, e: Throwable) {
screenModelScope.launch { viewModelScope.launch {
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
context.stringResource(MR.strings.notification_cover_update_failed), context.stringResource(MR.strings.notification_cover_update_failed),
withDismissAction = true, withDismissAction = true,
@@ -17,9 +17,9 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -84,26 +84,29 @@ class MangaScreen(
val context = LocalContext.current val context = LocalContext.current
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val lifecycleOwner = LocalLifecycleOwner.current val viewModel = viewModel<MangaViewModel>(
val screenModel = rememberScreenModel { factory = MangaViewModel.Factory,
MangaScreenModel(context, lifecycleOwner.lifecycle, mangaId, fromSource) extras = CreationExtras {
} set(MangaViewModel.MANGA_ID_KEY, mangaId)
set(MangaViewModel.IS_FROM_SOURCE_KEY, fromSource)
},
)
val state by screenModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
if (state is MangaScreenModel.State.Loading) { if (state is MangaViewModel.State.Loading) {
LoadingScreen() LoadingScreen()
return return
} }
val successState = state as MangaScreenModel.State.Success val successState = state as MangaViewModel.State.Success
val isHttpSource = remember { successState.source is HttpSource } val isHttpSource = remember { successState.source is HttpSource }
LaunchedEffect(successState.manga, screenModel.source) { LaunchedEffect(successState.manga, viewModel.source) {
if (isHttpSource) { if (isHttpSource) {
try { try {
withIOContext { withIOContext {
assistUrl = getMangaUrl(screenModel.manga, screenModel.source) assistUrl = getMangaUrl(viewModel.manga, viewModel.source)
} }
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to get manga URL" } logcat(LogPriority.ERROR, e) { "Failed to get manga URL" }
@@ -113,101 +116,101 @@ class MangaScreen(
MangaScreen( MangaScreen(
state = successState, state = successState,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = viewModel.snackbarHostState,
nextUpdate = successState.manga.expectedNextUpdate, nextUpdate = successState.manga.expectedNextUpdate,
isTabletUi = isTabletUi(), isTabletUi = isTabletUi(),
chapterSwipeStartAction = screenModel.chapterSwipeStartAction, chapterSwipeStartAction = viewModel.chapterSwipeStartAction,
chapterSwipeEndAction = screenModel.chapterSwipeEndAction, chapterSwipeEndAction = viewModel.chapterSwipeEndAction,
navigateUp = navigator::pop, navigateUp = navigator::pop,
onChapterClicked = { openChapter(context, it) }, onChapterClicked = { openChapter(context, it) },
onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() }, onDownloadChapter = viewModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() },
onAddToLibraryClicked = { onAddToLibraryClicked = {
screenModel.toggleFavorite() viewModel.toggleFavorite()
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}, },
onWebViewClicked = { onWebViewClicked = {
openMangaInWebView( openMangaInWebView(
navigator, navigator,
screenModel.manga, viewModel.manga,
screenModel.source, viewModel.source,
) )
}.takeIf { isHttpSource }, }.takeIf { isHttpSource },
onWebViewLongClicked = { onWebViewLongClicked = {
copyMangaUrl( copyMangaUrl(
context, context,
screenModel.manga, viewModel.manga,
screenModel.source, viewModel.source,
) )
}.takeIf { isHttpSource }, }.takeIf { isHttpSource },
onTrackingClicked = { onTrackingClicked = {
if (!successState.hasLoggedInTrackers) { if (!successState.hasLoggedInTrackers) {
navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking)) navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking))
} else { } else {
screenModel.showTrackDialog() viewModel.showTrackDialog()
} }
}, },
onTagSearch = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } }, onTagSearch = { scope.launch { performGenreSearch(navigator, it, viewModel.source!!) } },
onFilterButtonClicked = screenModel::showSettingsDialog, onFilterButtonClicked = viewModel::showSettingsDialog,
onRefresh = screenModel::fetchAllFromSource, onRefresh = viewModel::fetchAllFromSource,
onContinueReading = { continueReading(context, screenModel.getNextUnreadChapter()) }, onContinueReading = { continueReading(context, viewModel.getNextUnreadChapter()) },
onSearch = { query, global -> scope.launch { performSearch(navigator, query, global) } }, onSearch = { query, global -> scope.launch { performSearch(navigator, query, global) } },
onCoverClicked = screenModel::showCoverDialog, onCoverClicked = viewModel::showCoverDialog,
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource }, onShareClicked = { shareManga(context, viewModel.manga, viewModel.source) }.takeIf { isHttpSource },
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() }, onDownloadActionClicked = viewModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite }, onEditCategoryClicked = viewModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf { onEditFetchIntervalClicked = viewModel::showSetFetchIntervalDialog.takeIf {
successState.manga.favorite successState.manga.favorite
}, },
onMigrateClicked = { onMigrateClicked = {
navigator.push(MigrationConfigScreen(successState.manga.id)) navigator.push(MigrationConfigScreen(successState.manga.id))
}.takeIf { successState.manga.favorite }, }.takeIf { successState.manga.favorite },
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) }, onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
onMultiBookmarkClicked = screenModel::bookmarkChapters, onMultiBookmarkClicked = viewModel::bookmarkChapters,
onMultiMarkAsReadClicked = screenModel::markChaptersRead, onMultiMarkAsReadClicked = viewModel::markChaptersRead,
onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead, onMarkPreviousAsReadClicked = viewModel::markPreviousChapterRead,
onMultiDeleteClicked = screenModel::showDeleteChapterDialog, onMultiDeleteClicked = viewModel::showDeleteChapterDialog,
onChapterSwipe = screenModel::chapterSwipe, onChapterSwipe = viewModel::chapterSwipe,
onChapterSelected = screenModel::toggleSelection, onChapterSelected = viewModel::toggleSelection,
onAllChapterSelected = screenModel::toggleAllSelection, onAllChapterSelected = viewModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection, onInvertSelection = viewModel::invertSelection,
) )
var showScanlatorsDialog by remember { mutableStateOf(false) } var showScanlatorsDialog by remember { mutableStateOf(false) }
val onDismissRequest = { screenModel.dismissDialog() } val onDismissRequest = { viewModel.dismissDialog() }
when (val dialog = successState.dialog) { when (val dialog = successState.dialog) {
null -> {} null -> {}
is MangaScreenModel.Dialog.ChangeCategory -> { is MangaViewModel.Dialog.ChangeCategory -> {
ChangeCategoryDialog( ChangeCategoryDialog(
initialSelection = dialog.initialSelection, initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) }, onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ -> onConfirm = { include, _ ->
screenModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include) viewModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include)
}, },
) )
} }
is MangaScreenModel.Dialog.DeleteChapters -> { is MangaViewModel.Dialog.DeleteChapters -> {
DeleteChaptersDialog( DeleteChaptersDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { onConfirm = {
screenModel.toggleAllSelection(false) viewModel.toggleAllSelection(false)
screenModel.deleteChapters(dialog.chapters) viewModel.deleteChapters(dialog.chapters)
}, },
) )
} }
is MangaScreenModel.Dialog.DuplicateManga -> { is MangaViewModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates, duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) }, onConfirm = { viewModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
onOpenManga = { navigator.push(MangaScreen(it.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { screenModel.showMigrateDialog(it) }, onMigrate = { viewModel.showMigrateDialog(it) },
) )
} }
is MangaScreenModel.Dialog.Migrate -> { is MangaViewModel.Dialog.Migrate -> {
MigrateMangaDialog( MigrateMangaDialog(
current = dialog.current, current = dialog.current,
target = dialog.target, target = dialog.target,
@@ -216,20 +219,20 @@ class MangaScreen(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
) )
} }
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog( MangaViewModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
manga = successState.manga, manga = successState.manga,
onDownloadFilterChanged = screenModel::setDownloadedFilter, onDownloadFilterChanged = viewModel::setDownloadedFilter,
onUnreadFilterChanged = screenModel::setUnreadFilter, onUnreadFilterChanged = viewModel::setUnreadFilter,
onBookmarkedFilterChanged = screenModel::setBookmarkedFilter, onBookmarkedFilterChanged = viewModel::setBookmarkedFilter,
onSortModeChanged = screenModel::setSorting, onSortModeChanged = viewModel::setSorting,
onDisplayModeChanged = screenModel::setDisplayMode, onDisplayModeChanged = viewModel::setDisplayMode,
onSetAsDefault = screenModel::setCurrentSettingsAsDefault, onSetAsDefault = viewModel::setCurrentSettingsAsDefault,
onResetToDefault = screenModel::resetToDefaultSettings, onResetToDefault = viewModel::resetToDefaultSettings,
scanlatorFilterActive = successState.scanlatorFilterActive, scanlatorFilterActive = successState.scanlatorFilterActive,
onScanlatorFilterClicked = { showScanlatorsDialog = true }, onScanlatorFilterClicked = { showScanlatorsDialog = true },
) )
MangaScreenModel.Dialog.TrackSheet -> { MangaViewModel.Dialog.TrackSheet -> {
NavigatorAdaptiveSheet( NavigatorAdaptiveSheet(
screen = TrackInfoDialogHomeScreen( screen = TrackInfoDialogHomeScreen(
mangaId = successState.manga.id, mangaId = successState.manga.id,
@@ -240,8 +243,13 @@ class MangaScreen(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
) )
} }
MangaScreenModel.Dialog.FullCover -> { MangaViewModel.Dialog.FullCover -> {
val sm = rememberScreenModel { MangaCoverScreenModel(successState.manga.id) } val sm = viewModel<MangaCoverViewModel>(
factory = MangaCoverViewModel.Factory,
extras = CreationExtras {
set(MangaCoverViewModel.MANGA_ID_KEY, successState.manga.id)
},
)
val manga by sm.state.collectAsState() val manga by sm.state.collectAsState()
if (manga != null) { if (manga != null) {
val getContent = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { val getContent = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
@@ -266,13 +274,13 @@ class MangaScreen(
LoadingScreen(Modifier.systemBarsPadding()) LoadingScreen(Modifier.systemBarsPadding())
} }
} }
is MangaScreenModel.Dialog.SetFetchInterval -> { is MangaViewModel.Dialog.SetFetchInterval -> {
SetIntervalDialog( SetIntervalDialog(
interval = dialog.manga.fetchInterval, interval = dialog.manga.fetchInterval,
nextUpdate = dialog.manga.expectedNextUpdate, nextUpdate = dialog.manga.expectedNextUpdate,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onValueChanged = { interval: Int -> screenModel.setFetchInterval(dialog.manga, interval) } onValueChanged = { interval: Int -> viewModel.setFetchInterval(dialog.manga, interval) }
.takeIf { screenModel.isUpdateIntervalEnabled }, .takeIf { viewModel.isUpdateIntervalEnabled },
) )
} }
} }
@@ -282,7 +290,7 @@ class MangaScreen(
availableScanlators = successState.availableScanlators, availableScanlators = successState.availableScanlators,
excludedScanlators = successState.excludedScanlators, excludedScanlators = successState.excludedScanlators,
onDismissRequest = { showScanlatorsDialog = false }, onDismissRequest = { showScanlatorsDialog = false },
onConfirm = screenModel::setExcludedScanlators, onConfirm = viewModel::setExcludedScanlators,
) )
} }
} }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.manga package eu.kanade.tachiyomi.ui.manga
import android.app.Application
import android.content.Context import android.content.Context
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
@@ -7,10 +8,10 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.lifecycle.Lifecycle import androidx.lifecycle.viewModelScope
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.viewmodel.CreationExtras
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewmodel.initializer
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparators
@@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@@ -85,9 +87,8 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.math.floor import kotlin.math.floor
class MangaScreenModel( class MangaViewModel(
private val context: Context, private val context: Context,
private val lifecycle: Lifecycle,
private val mangaId: Long, private val mangaId: Long,
private val isFromSource: Boolean, private val isFromSource: Boolean,
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
@@ -115,7 +116,23 @@ class MangaScreenModel(
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(), private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<MangaScreenModel.State>(State.Loading) { ) : StateViewModel<MangaViewModel.State>(State.Loading) {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val IS_FROM_SOURCE_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
MangaViewModel(
context = Injekt.get<Application>(),
mangaId = get(MANGA_ID_KEY)!!,
isFromSource = get(IS_FROM_SOURCE_KEY)!!,
)
}
}
}
private val successState: State.Success? private val successState: State.Success?
get() = state.value as? State.Success get() = state.value as? State.Success
@@ -139,7 +156,7 @@ class MangaScreenModel(
val chapterSwipeEndAction = libraryPreferences.swipeToStartAction.get() val chapterSwipeEndAction = libraryPreferences.swipeToStartAction.get()
var autoTrackState = trackPreferences.autoUpdateTrackOnMarkRead.get() var autoTrackState = trackPreferences.autoUpdateTrackOnMarkRead.get()
private val skipFiltered by readerPreferences.skipFiltered.asState(screenModelScope) private val skipFiltered by readerPreferences.skipFiltered.asState(viewModelScope)
val isUpdateIntervalEnabled = val isUpdateIntervalEnabled =
LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateMangaRestrictions.get() LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateMangaRestrictions.get()
@@ -160,13 +177,12 @@ class MangaScreenModel(
} }
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
combine( combine(
getMangaAndChapters.subscribe(mangaId, applyScanlatorFilter = true).distinctUntilChanged(), getMangaAndChapters.subscribe(mangaId, applyScanlatorFilter = true).distinctUntilChanged(),
downloadCache.changes, downloadCache.changes,
downloadManager.queueState, downloadManager.queueState,
) { mangaAndChapters, _, _ -> mangaAndChapters } ) { mangaAndChapters, _, _ -> mangaAndChapters }
.flowWithLifecycle(lifecycle)
.collectLatest { (manga, chapters) -> .collectLatest { (manga, chapters) ->
updateSuccessState { updateSuccessState {
it.copy( it.copy(
@@ -177,9 +193,8 @@ class MangaScreenModel(
} }
} }
screenModelScope.launchIO { viewModelScope.launchIO {
getExcludedScanlators.subscribe(mangaId) getExcludedScanlators.subscribe(mangaId)
.flowWithLifecycle(lifecycle)
.distinctUntilChanged() .distinctUntilChanged()
.collectLatest { excludedScanlators -> .collectLatest { excludedScanlators ->
updateSuccessState { updateSuccessState {
@@ -188,9 +203,8 @@ class MangaScreenModel(
} }
} }
screenModelScope.launchIO { viewModelScope.launchIO {
getAvailableScanlators.subscribe(mangaId) getAvailableScanlators.subscribe(mangaId)
.flowWithLifecycle(lifecycle)
.distinctUntilChanged() .distinctUntilChanged()
.collectLatest { availableScanlators -> .collectLatest { availableScanlators ->
updateSuccessState { updateSuccessState {
@@ -201,7 +215,7 @@ class MangaScreenModel(
observeDownloads() observeDownloads()
screenModelScope.launchIO { viewModelScope.launchIO {
val manga = getMangaAndChapters.awaitManga(mangaId) val manga = getMangaAndChapters.awaitManga(mangaId)
val chapters = getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true) val chapters = getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true)
.toChapterListItems(manga) .toChapterListItems(manga)
@@ -232,7 +246,7 @@ class MangaScreenModel(
observeTrackers() observeTrackers()
// Fetch info-chapters when needed // Fetch info-chapters when needed
if ((needRefreshInfo || needRefreshChapter) && screenModelScope.isActive) { if ((needRefreshInfo || needRefreshChapter) && viewModelScope.isActive) {
fetchAllFromSource( fetchAllFromSource(
manualFetch = false, manualFetch = false,
fetchDetails = needRefreshInfo, fetchDetails = needRefreshInfo,
@@ -246,7 +260,7 @@ class MangaScreenModel(
} }
fun fetchAllFromSource(manualFetch: Boolean = true) { fun fetchAllFromSource(manualFetch: Boolean = true) {
screenModelScope.launch { viewModelScope.launch {
updateSuccessState { it.copy(isRefreshingData = true) } updateSuccessState { it.copy(isRefreshingData = true) }
fetchAllFromSource( fetchAllFromSource(
manualFetch = manualFetch, manualFetch = manualFetch,
@@ -288,7 +302,7 @@ class MangaScreenModel(
with(context) { e.formattedMessage } with(context) { e.formattedMessage }
} }
screenModelScope.launch { viewModelScope.launch {
snackbarHostState.showSnackbar(message = message) snackbarHostState.showSnackbar(message = message)
} }
} }
@@ -299,7 +313,7 @@ class MangaScreenModel(
fun toggleFavorite() { fun toggleFavorite() {
toggleFavorite( toggleFavorite(
onRemoved = { onRemoved = {
screenModelScope.launch { viewModelScope.launch {
if (!hasDownloads()) return@launch if (!hasDownloads()) return@launch
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = context.stringResource(MR.strings.delete_downloads_for_manga), message = context.stringResource(MR.strings.delete_downloads_for_manga),
@@ -322,7 +336,7 @@ class MangaScreenModel(
checkDuplicate: Boolean = true, checkDuplicate: Boolean = true,
) { ) {
val state = successState ?: return val state = successState ?: return
screenModelScope.launchIO { viewModelScope.launchIO {
val manga = state.manga val manga = state.manga
if (isFavorited) { if (isFavorited) {
@@ -377,7 +391,7 @@ class MangaScreenModel(
fun showChangeCategoryDialog() { fun showChangeCategoryDialog() {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
screenModelScope.launch { viewModelScope.launch {
val categories = getCategories() val categories = getCategories()
val selection = getMangaCategoryIds(manga) val selection = getMangaCategoryIds(manga)
updateSuccessState { successState -> updateSuccessState { successState ->
@@ -399,7 +413,7 @@ class MangaScreenModel(
} }
fun setFetchInterval(manga: Manga, interval: Int) { fun setFetchInterval(manga: Manga, interval: Int) {
screenModelScope.launchIO { viewModelScope.launchIO {
if ( if (
updateManga.awaitUpdateFetchInterval( updateManga.awaitUpdateFetchInterval(
// Custom intervals are negative // Custom intervals are negative
@@ -452,7 +466,7 @@ class MangaScreenModel(
moveMangaToCategory(categories) moveMangaToCategory(categories)
if (manga.favorite) return if (manga.favorite) return
screenModelScope.launchIO { viewModelScope.launchIO {
updateManga.awaitUpdateFavorite(manga.id, true) updateManga.awaitUpdateFavorite(manga.id, true)
} }
} }
@@ -468,7 +482,7 @@ class MangaScreenModel(
} }
private fun moveMangaToCategory(categoryIds: List<Long>) { private fun moveMangaToCategory(categoryIds: List<Long>) {
screenModelScope.launchIO { viewModelScope.launchIO {
setMangaCategories.await(mangaId, categoryIds) setMangaCategories.await(mangaId, categoryIds)
} }
} }
@@ -487,11 +501,10 @@ class MangaScreenModel(
// Chapters list - start // Chapters list - start
private fun observeDownloads() { private fun observeDownloads() {
screenModelScope.launchIO { viewModelScope.launchIO {
downloadManager.statusFlow() downloadManager.statusFlow()
.filter { it.manga.id == successState?.manga?.id } .filter { it.manga.id == successState?.manga?.id }
.catch { error -> logcat(LogPriority.ERROR, error) } .catch { error -> logcat(LogPriority.ERROR, error) }
.flowWithLifecycle(lifecycle)
.collect { .collect {
withUIContext { withUIContext {
updateDownloadState(it) updateDownloadState(it)
@@ -499,11 +512,10 @@ class MangaScreenModel(
} }
} }
screenModelScope.launchIO { viewModelScope.launchIO {
downloadManager.progressFlow() downloadManager.progressFlow()
.filter { it.manga.id == successState?.manga?.id } .filter { it.manga.id == successState?.manga?.id }
.catch { error -> logcat(LogPriority.ERROR, error) } .catch { error -> logcat(LogPriority.ERROR, error) }
.flowWithLifecycle(lifecycle)
.collect { .collect {
withUIContext { withUIContext {
updateDownloadState(it) updateDownloadState(it)
@@ -564,7 +576,7 @@ class MangaScreenModel(
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
*/ */
fun chapterSwipe(chapterItem: ChapterList.Item, swipeAction: LibraryPreferences.ChapterSwipeAction) { fun chapterSwipe(chapterItem: ChapterList.Item, swipeAction: LibraryPreferences.ChapterSwipeAction) {
screenModelScope.launch { viewModelScope.launch {
executeChapterSwipeAction(chapterItem, swipeAction) executeChapterSwipeAction(chapterItem, swipeAction)
} }
} }
@@ -637,7 +649,7 @@ class MangaScreenModel(
) { ) {
val successState = successState ?: return val successState = successState ?: return
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
if (startNow) { if (startNow) {
val chapterId = chapters.singleOrNull()?.id ?: return@launchNonCancellable val chapterId = chapters.singleOrNull()?.id ?: return@launchNonCancellable
downloadManager.startDownloadNow(chapterId) downloadManager.startDownloadNow(chapterId)
@@ -722,7 +734,7 @@ class MangaScreenModel(
fun markChaptersRead(chapters: List<Chapter>, read: Boolean) { fun markChaptersRead(chapters: List<Chapter>, read: Boolean) {
toggleAllSelection(false) toggleAllSelection(false)
if (chapters.isEmpty()) return if (chapters.isEmpty()) return
screenModelScope.launchIO { viewModelScope.launchIO {
setReadStatus.await( setReadStatus.await(
read = read, read = read,
chapters = chapters.toTypedArray(), chapters = chapters.toTypedArray(),
@@ -796,7 +808,7 @@ class MangaScreenModel(
* @param chapters the list of chapters to bookmark. * @param chapters the list of chapters to bookmark.
*/ */
fun bookmarkChapters(chapters: List<Chapter>, bookmarked: Boolean) { fun bookmarkChapters(chapters: List<Chapter>, bookmarked: Boolean) {
screenModelScope.launchIO { viewModelScope.launchIO {
chapters chapters
.filterNot { it.bookmark == bookmarked } .filterNot { it.bookmark == bookmarked }
.map { ChapterUpdate(id = it.id, bookmark = bookmarked) } .map { ChapterUpdate(id = it.id, bookmark = bookmarked) }
@@ -811,7 +823,7 @@ class MangaScreenModel(
* @param chapters the list of chapters to delete. * @param chapters the list of chapters to delete.
*/ */
fun deleteChapters(chapters: List<Chapter>) { fun deleteChapters(chapters: List<Chapter>) {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
try { try {
successState?.let { state -> successState?.let { state ->
downloadManager.deleteChapters( downloadManager.deleteChapters(
@@ -827,7 +839,7 @@ class MangaScreenModel(
} }
private fun downloadNewChapters(chapters: List<Chapter>) { private fun downloadNewChapters(chapters: List<Chapter>) {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
val manga = successState?.manga ?: return@launchNonCancellable val manga = successState?.manga ?: return@launchNonCancellable
val chaptersToDownload = filterChaptersForDownload.await(manga, chapters) val chaptersToDownload = filterChaptersForDownload.await(manga, chapters)
@@ -849,7 +861,7 @@ class MangaScreenModel(
TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD
TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ
} }
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
setMangaChapterFlags.awaitSetUnreadFilter(manga, flag) setMangaChapterFlags.awaitSetUnreadFilter(manga, flag)
} }
} }
@@ -867,7 +879,7 @@ class MangaScreenModel(
TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED
} }
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
setMangaChapterFlags.awaitSetDownloadedFilter(manga, flag) setMangaChapterFlags.awaitSetDownloadedFilter(manga, flag)
} }
} }
@@ -885,7 +897,7 @@ class MangaScreenModel(
TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED
} }
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
setMangaChapterFlags.awaitSetBookmarkFilter(manga, flag) setMangaChapterFlags.awaitSetBookmarkFilter(manga, flag)
} }
} }
@@ -897,7 +909,7 @@ class MangaScreenModel(
fun setDisplayMode(mode: Long) { fun setDisplayMode(mode: Long) {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
setMangaChapterFlags.awaitSetDisplayMode(manga, mode) setMangaChapterFlags.awaitSetDisplayMode(manga, mode)
} }
} }
@@ -909,14 +921,14 @@ class MangaScreenModel(
fun setSorting(sort: Long) { fun setSorting(sort: Long) {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
setMangaChapterFlags.awaitSetSortingModeOrFlipOrder(manga, sort) setMangaChapterFlags.awaitSetSortingModeOrFlipOrder(manga, sort)
} }
} }
fun setCurrentSettingsAsDefault(applyToExisting: Boolean) { fun setCurrentSettingsAsDefault(applyToExisting: Boolean) {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
libraryPreferences.setChapterSettingsDefault(manga) libraryPreferences.setChapterSettingsDefault(manga)
if (applyToExisting) { if (applyToExisting) {
setMangaDefaultChapterFlags.awaitAll() setMangaDefaultChapterFlags.awaitAll()
@@ -927,7 +939,7 @@ class MangaScreenModel(
fun resetToDefaultSettings() { fun resetToDefaultSettings() {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
setMangaDefaultChapterFlags.await(manga) setMangaDefaultChapterFlags.await(manga)
} }
} }
@@ -1026,7 +1038,7 @@ class MangaScreenModel(
private fun observeTrackers() { private fun observeTrackers() {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
screenModelScope.launchIO { viewModelScope.launchIO {
combine( combine(
getTracks.subscribe(manga.id).catch { logcat(LogPriority.ERROR, it) }, getTracks.subscribe(manga.id).catch { logcat(LogPriority.ERROR, it) },
trackerManager.loggedInTrackersFlow(), trackerManager.loggedInTrackersFlow(),
@@ -1037,7 +1049,6 @@ class MangaScreenModel(
val supportedTrackerTracks = mangaTracks.filter { it.trackerId in supportedTrackerIds } val supportedTrackerTracks = mangaTracks.filter { it.trackerId in supportedTrackerIds }
supportedTrackerTracks.size to supportedTrackers.isNotEmpty() supportedTrackerTracks.size to supportedTrackers.isNotEmpty()
} }
.flowWithLifecycle(lifecycle)
.distinctUntilChanged() .distinctUntilChanged()
.collectLatest { (trackingCount, hasLoggedInTrackers) -> .collectLatest { (trackingCount, hasLoggedInTrackers) ->
updateSuccessState { updateSuccessState {
@@ -1092,7 +1103,7 @@ class MangaScreenModel(
} }
fun setExcludedScanlators(excludedScanlators: Set<String>) { fun setExcludedScanlators(excludedScanlators: Set<String>) {
screenModelScope.launchIO { viewModelScope.launchIO {
setExcludedScanlators.await(mangaId, excludedScanlators) setExcludedScanlators.await(mangaId, excludedScanlators)
} }
} }
@@ -4,14 +4,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.manga.MangaNotesScreen import eu.kanade.presentation.manga.MangaNotesScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.manga.interactor.UpdateMangaNotes import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@@ -25,20 +28,37 @@ class MangaNotesScreen(
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { Model(manga) } val viewModel = viewModel<Model>(
val state by screenModel.state.collectAsState() factory = Model.Factory,
extras = CreationExtras {
set(Model.MANGA_KEY, manga)
},
)
val state by viewModel.state.collectAsState()
MangaNotesScreen( MangaNotesScreen(
state = state, state = state,
navigateUp = navigator::pop, navigateUp = navigator::pop,
onUpdate = screenModel::updateNotes, onUpdate = viewModel::updateNotes,
) )
} }
private class Model( private class Model(
private val manga: Manga, private val manga: Manga,
private val updateMangaNotes: UpdateMangaNotes = Injekt.get(), private val updateMangaNotes: UpdateMangaNotes = Injekt.get(),
) : StateScreenModel<State>(State(manga, manga.notes)) { ) : StateViewModel<State>(State(manga, manga.notes)) {
companion object {
val MANGA_KEY = CreationExtras.Key<Manga>()
val Factory = viewModelFactory {
initializer {
Model(
manga = get(MANGA_KEY)!!,
)
}
}
}
fun updateNotes(content: String) { fun updateNotes(content: String) {
if (content == state.value.notes) return if (content == state.value.notes) return
@@ -47,7 +67,7 @@ class MangaNotesScreen(
it.copy(notes = content) it.copy(notes = content)
} }
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
updateMangaNotes(manga.id, content) updateMangaNotes(manga.id, content)
} }
} }
@@ -30,10 +30,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import cafe.adriel.voyager.core.model.ScreenModel import androidx.lifecycle.ViewModel
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -65,6 +67,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@@ -96,10 +99,16 @@ data class TrackInfoDialogHomeScreen(
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val screenModel = rememberScreenModel { Model(mangaId, sourceId) } val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.MANGA_ID_KEY, mangaId)
set(Model.SOURCE_ID_KEY, sourceId)
},
)
val dateFormat = remember { UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat.get()) } val dateFormat = remember { UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat.get()) }
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
TrackInfoDialogHome( TrackInfoDialogHome(
trackItems = state.trackItems, trackItems = state.trackItems,
@@ -148,7 +157,7 @@ data class TrackInfoDialogHomeScreen(
}, },
onNewSearch = { onNewSearch = {
if (it.tracker is EnhancedTracker) { if (it.tracker is EnhancedTracker) {
screenModel.registerEnhancedTracking(it) viewModel.registerEnhancedTracking(it)
} else { } else {
navigator.push( navigator.push(
TrackerSearchScreen( TrackerSearchScreen(
@@ -171,7 +180,7 @@ data class TrackInfoDialogHomeScreen(
) )
}, },
onCopyLink = { context.copyTrackerLink(it) }, onCopyLink = { context.copyTrackerLink(it) },
onTogglePrivate = screenModel::togglePrivate, onTogglePrivate = viewModel::togglePrivate,
) )
} }
@@ -196,14 +205,28 @@ data class TrackInfoDialogHomeScreen(
private val mangaId: Long, private val mangaId: Long,
private val sourceId: Long, private val sourceId: Long,
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
) : StateScreenModel<Model.State>(State()) { ) : StateViewModel<Model.State>(State()) {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY)!!,
sourceId = get(SOURCE_ID_KEY)!!,
)
}
}
}
init { init {
screenModelScope.launch { viewModelScope.launch {
refreshTrackers() refreshTrackers()
} }
screenModelScope.launch { viewModelScope.launch {
getTracks.subscribe(mangaId) getTracks.subscribe(mangaId)
.catch { logcat(LogPriority.ERROR, it) } .catch { logcat(LogPriority.ERROR, it) }
.distinctUntilChanged() .distinctUntilChanged()
@@ -214,7 +237,7 @@ data class TrackInfoDialogHomeScreen(
fun registerEnhancedTracking(item: TrackItem) { fun registerEnhancedTracking(item: TrackItem) {
item.tracker as EnhancedTracker item.tracker as EnhancedTracker
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
val manga = Injekt.get<GetManga>().await(mangaId) ?: return@launchNonCancellable val manga = Injekt.get<GetManga>().await(mangaId) ?: return@launchNonCancellable
try { try {
val matchResult = item.tracker.match(manga) ?: throw Exception() val matchResult = item.tracker.match(manga) ?: throw Exception()
@@ -248,7 +271,7 @@ data class TrackInfoDialogHomeScreen(
} }
fun togglePrivate(item: TrackItem) { fun togglePrivate(item: TrackItem) {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
item.tracker.setRemotePrivate(item.track!!.toDbTrack(), !item.track.private) item.tracker.setRemotePrivate(item.track!!.toDbTrack(), !item.track.private)
} }
} }
@@ -278,19 +301,20 @@ private data class TrackStatusSelectorScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<Model>(
Model( factory = Model.Factory,
track = track, extras = CreationExtras {
tracker = Injekt.get<TrackerManager>().get(serviceId)!!, set(Model.TRACK_KEY, track)
) set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
} },
val state by screenModel.state.collectAsState() )
val state by viewModel.state.collectAsState()
TrackStatusSelector( TrackStatusSelector(
selection = state.selection, selection = state.selection,
onSelectionChange = screenModel::setSelection, onSelectionChange = viewModel::setSelection,
selections = remember { screenModel.getSelections() }, selections = remember { viewModel.getSelections() },
onConfirm = { onConfirm = {
screenModel.setStatus() viewModel.setStatus()
navigator.pop() navigator.pop()
}, },
onDismissRequest = navigator::pop, onDismissRequest = navigator::pop,
@@ -300,7 +324,21 @@ private data class TrackStatusSelectorScreen(
private class Model( private class Model(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
) : StateScreenModel<Model.State>(State(track.status)) { ) : StateViewModel<Model.State>(State(track.status)) {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
}
fun getSelections(): Map<Long, StringResource?> { fun getSelections(): Map<Long, StringResource?> {
return tracker.getStatusList().associateWith { tracker.getStatus(it) } return tracker.getStatusList().associateWith { tracker.getStatus(it) }
@@ -311,7 +349,7 @@ private data class TrackStatusSelectorScreen(
} }
fun setStatus() { fun setStatus() {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
tracker.setRemoteStatus(track.toDbTrack(), state.value.selection) tracker.setRemoteStatus(track.toDbTrack(), state.value.selection)
} }
} }
@@ -331,20 +369,21 @@ private data class TrackChapterSelectorScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<Model>(
Model( factory = Model.Factory,
track = track, extras = CreationExtras {
tracker = Injekt.get<TrackerManager>().get(serviceId)!!, set(Model.TRACK_KEY, track)
) set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
} },
val state by screenModel.state.collectAsState() )
val state by viewModel.state.collectAsState()
TrackChapterSelector( TrackChapterSelector(
selection = state.selection, selection = state.selection,
onSelectionChange = screenModel::setSelection, onSelectionChange = viewModel::setSelection,
range = remember { screenModel.getRange() }, range = remember { viewModel.getRange() },
onConfirm = { onConfirm = {
screenModel.setChapter() viewModel.setChapter()
navigator.pop() navigator.pop()
}, },
onDismissRequest = navigator::pop, onDismissRequest = navigator::pop,
@@ -354,7 +393,21 @@ private data class TrackChapterSelectorScreen(
private class Model( private class Model(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
) : StateScreenModel<Model.State>(State(track.lastChapterRead.toInt())) { ) : StateViewModel<Model.State>(State(track.lastChapterRead.toInt())) {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
}
fun getRange(): Iterable<Int> { fun getRange(): Iterable<Int> {
val endRange = if (track.totalChapters > 0) { val endRange = if (track.totalChapters > 0) {
@@ -370,7 +423,7 @@ private data class TrackChapterSelectorScreen(
} }
fun setChapter() { fun setChapter() {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
tracker.setRemoteLastChapterRead(track.toDbTrack(), state.value.selection) tracker.setRemoteLastChapterRead(track.toDbTrack(), state.value.selection)
} }
} }
@@ -390,20 +443,21 @@ private data class TrackScoreSelectorScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<Model>(
Model( factory = Model.Factory,
track = track, extras = CreationExtras {
tracker = Injekt.get<TrackerManager>().get(serviceId)!!, set(Model.TRACK_KEY, track)
) set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
} },
val state by screenModel.state.collectAsState() )
val state by viewModel.state.collectAsState()
TrackScoreSelector( TrackScoreSelector(
selection = state.selection, selection = state.selection,
onSelectionChange = screenModel::setSelection, onSelectionChange = viewModel::setSelection,
selections = remember { screenModel.getSelections() }, selections = remember { viewModel.getSelections() },
onConfirm = { onConfirm = {
screenModel.setScore() viewModel.setScore()
navigator.pop() navigator.pop()
}, },
onDismissRequest = navigator::pop, onDismissRequest = navigator::pop,
@@ -413,7 +467,21 @@ private data class TrackScoreSelectorScreen(
private class Model( private class Model(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
) : StateScreenModel<Model.State>(State(tracker.displayScore(track))) { ) : StateViewModel<Model.State>(State(tracker.displayScore(track))) {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
}
fun getSelections(): List<String> { fun getSelections(): List<String> {
return tracker.getScoreList() return tracker.getScoreList()
@@ -424,7 +492,7 @@ private data class TrackScoreSelectorScreen(
} }
fun setScore() { fun setScore() {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
tracker.setRemoteScore(track.toDbTrack(), state.value.selection) tracker.setRemoteScore(track.toDbTrack(), state.value.selection)
} }
} }
@@ -492,13 +560,14 @@ private data class TrackDateSelectorScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<Model>(
Model( factory = Model.Factory,
track = track, extras = CreationExtras {
tracker = Injekt.get<TrackerManager>().get(serviceId)!!, set(Model.TRACK_KEY, track)
start = start, set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
) set(Model.START_KEY, start)
} },
)
val canRemove = if (start) { val canRemove = if (start) {
track.startDate > 0 track.startDate > 0
@@ -511,13 +580,13 @@ private data class TrackDateSelectorScreen(
} else { } else {
stringResource(MR.strings.track_finished_reading_date) stringResource(MR.strings.track_finished_reading_date)
}, },
initialSelectedDateMillis = screenModel.initialSelection, initialSelectedDateMillis = viewModel.initialSelection,
selectableDates = selectableDates, selectableDates = selectableDates,
onConfirm = { onConfirm = {
screenModel.setDate(it) viewModel.setDate(it)
navigator.pop() navigator.pop()
}, },
onRemove = { screenModel.confirmRemoveDate(navigator) }.takeIf { canRemove }, onRemove = { viewModel.confirmRemoveDate(navigator) }.takeIf { canRemove },
onDismissRequest = navigator::pop, onDismissRequest = navigator::pop,
) )
} }
@@ -526,7 +595,23 @@ private data class TrackDateSelectorScreen(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
private val start: Boolean, private val start: Boolean,
) : ScreenModel { ) : ViewModel() {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val START_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
start = get(START_KEY) as Boolean,
)
}
}
}
// In UTC // In UTC
val initialSelection: Long val initialSelection: Long
@@ -541,7 +626,7 @@ private data class TrackDateSelectorScreen(
fun setDate(millis: Long) { fun setDate(millis: Long) {
// Convert to local time // Convert to local time
val localMillis = millis.convertEpochMillisZone(ZoneOffset.UTC, ZoneOffset.systemDefault()) val localMillis = millis.convertEpochMillisZone(ZoneOffset.UTC, ZoneOffset.systemDefault())
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
if (start) { if (start) {
tracker.setRemoteStartDate(track.toDbTrack(), localMillis) tracker.setRemoteStartDate(track.toDbTrack(), localMillis)
} else { } else {
@@ -565,13 +650,14 @@ private data class TrackDateRemoverScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<Model>(
Model( factory = Model.Factory,
track = track, extras = CreationExtras {
tracker = Injekt.get<TrackerManager>().get(serviceId)!!, set(Model.TRACK_KEY, track)
start = start, set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
) set(Model.START_KEY, start)
} },
)
AlertDialogContent( AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars), modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
icon = { icon = {
@@ -587,7 +673,7 @@ private data class TrackDateRemoverScreen(
) )
}, },
text = { text = {
val serviceName = screenModel.getServiceName() val serviceName = viewModel.getServiceName()
Text( Text(
text = if (start) { text = if (start) {
stringResource(MR.strings.track_remove_start_date_conf_text, serviceName) stringResource(MR.strings.track_remove_start_date_conf_text, serviceName)
@@ -606,7 +692,7 @@ private data class TrackDateRemoverScreen(
} }
FilledTonalButton( FilledTonalButton(
onClick = { onClick = {
screenModel.removeDate() viewModel.removeDate()
navigator.popUntil { it is TrackInfoDialogHomeScreen } navigator.popUntil { it is TrackInfoDialogHomeScreen }
}, },
colors = ButtonDefaults.filledTonalButtonColors( colors = ButtonDefaults.filledTonalButtonColors(
@@ -625,12 +711,28 @@ private data class TrackDateRemoverScreen(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
private val start: Boolean, private val start: Boolean,
) : ScreenModel { ) : ViewModel() {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val START_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
start = get(START_KEY) as Boolean,
)
}
}
}
fun getServiceName() = tracker.name fun getServiceName() = tracker.name
fun removeDate() { fun removeDate() {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
if (start) { if (start) {
tracker.setRemoteStartDate(track.toDbTrack(), 0) tracker.setRemoteStartDate(track.toDbTrack(), 0)
} else { } else {
@@ -651,41 +753,60 @@ data class TrackerSearchScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<Model>(
Model( factory = Model.Factory,
mangaId = mangaId, extras = CreationExtras {
currentUrl = currentUrl, set(Model.MANGA_ID_KEY, mangaId)
initialQuery = initialQuery, set(Model.CURRENT_URL_KEY, currentUrl)
tracker = Injekt.get<TrackerManager>().get(serviceId)!!, set(Model.INITIAL_QUERY_KEY, initialQuery)
) set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
} },
)
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
val textFieldState = rememberTextFieldState(initialQuery) val textFieldState = rememberTextFieldState(initialQuery)
TrackerSearch( TrackerSearch(
state = textFieldState, state = textFieldState,
onDispatchQuery = { screenModel.trackingSearch(textFieldState.text.toString()) }, onDispatchQuery = { viewModel.trackingSearch(textFieldState.text.toString()) },
queryResult = state.queryResult, queryResult = state.queryResult,
selected = state.selected, selected = state.selected,
onSelectedChange = screenModel::updateSelection, onSelectedChange = viewModel::updateSelection,
onConfirmSelection = f@{ private: Boolean -> onConfirmSelection = f@{ private: Boolean ->
val selected = state.selected ?: return@f val selected = state.selected ?: return@f
selected.private = private selected.private = private
screenModel.registerTracking(selected) viewModel.registerTracking(selected)
navigator.pop() navigator.pop()
}, },
onDismissRequest = navigator::pop, onDismissRequest = navigator::pop,
supportsPrivateTracking = screenModel.supportsPrivateTracking, supportsPrivateTracking = viewModel.supportsPrivateTracking,
) )
} }
private class Model( private class Model(
private val mangaId: Long, private val mangaId: Long,
private val currentUrl: String? = null, private val currentUrl: String?,
initialQuery: String, initialQuery: String,
private val tracker: Tracker, private val tracker: Tracker,
) : StateScreenModel<Model.State>(State()) { ) : StateViewModel<Model.State>(State()) {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val CURRENT_URL_KEY = CreationExtras.Key<String?>()
val INITIAL_QUERY_KEY = CreationExtras.Key<String>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY)!!,
currentUrl = get(CURRENT_URL_KEY),
initialQuery = get(INITIAL_QUERY_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
}
val supportsPrivateTracking = tracker.supportsPrivateTracking val supportsPrivateTracking = tracker.supportsPrivateTracking
@@ -697,7 +818,7 @@ data class TrackerSearchScreen(
} }
fun trackingSearch(query: String) { fun trackingSearch(query: String) {
screenModelScope.launch { viewModelScope.launch {
// To show loading state // To show loading state
mutableState.update { it.copy(queryResult = null, selected = null) } mutableState.update { it.copy(queryResult = null, selected = null) }
@@ -719,7 +840,7 @@ data class TrackerSearchScreen(
} }
fun registerTracking(item: TrackSearch) { fun registerTracking(item: TrackSearch) {
screenModelScope.launchNonCancellable { tracker.register(item, mangaId) } viewModelScope.launchNonCancellable { tracker.register(item, mangaId) }
} }
fun updateSelection(selected: TrackSearch) { fun updateSelection(selected: TrackSearch) {
@@ -743,14 +864,15 @@ private data class TrackerRemoveScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val viewModel = viewModel<Model>(
Model( factory = Model.Factory,
mangaId = mangaId, extras = CreationExtras {
track = track, set(Model.MANGA_ID_KEY, mangaId)
tracker = Injekt.get<TrackerManager>().get(serviceId)!!, set(Model.TRACK_KEY, track)
) set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
} },
val serviceName = screenModel.getName() )
val serviceName = viewModel.getName()
var removeRemoteTrack by remember { mutableStateOf(false) } var removeRemoteTrack by remember { mutableStateOf(false) }
AlertDialogContent( AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars), modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
@@ -774,7 +896,7 @@ private data class TrackerRemoveScreen(
text = stringResource(MR.strings.track_delete_text, serviceName), text = stringResource(MR.strings.track_delete_text, serviceName),
) )
if (screenModel.isDeletable()) { if (viewModel.isDeletable()) {
LabeledCheckbox( LabeledCheckbox(
label = stringResource(MR.strings.track_delete_remote_text, serviceName), label = stringResource(MR.strings.track_delete_remote_text, serviceName),
checked = removeRemoteTrack, checked = removeRemoteTrack,
@@ -796,8 +918,8 @@ private data class TrackerRemoveScreen(
} }
FilledTonalButton( FilledTonalButton(
onClick = { onClick = {
screenModel.unregisterTracking(serviceId) viewModel.unregisterTracking(serviceId)
if (removeRemoteTrack) screenModel.deleteMangaFromService() if (removeRemoteTrack) viewModel.deleteMangaFromService()
navigator.pop() navigator.pop()
}, },
colors = ButtonDefaults.filledTonalButtonColors( colors = ButtonDefaults.filledTonalButtonColors(
@@ -817,14 +939,30 @@ private data class TrackerRemoveScreen(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
private val deleteTrack: DeleteTrack = Injekt.get(), private val deleteTrack: DeleteTrack = Injekt.get(),
) : ScreenModel { ) : ViewModel() {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY) as Long,
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
)
}
}
}
fun getName() = tracker.name fun getName() = tracker.name
fun isDeletable() = tracker is DeletableTracker fun isDeletable() = tracker is DeletableTracker
fun deleteMangaFromService() { fun deleteMangaFromService() {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
try { try {
(tracker as DeletableTracker).delete(track) (tracker as DeletableTracker).delete(track)
} catch (e: Exception) { } catch (e: Exception) {
@@ -834,7 +972,7 @@ private data class TrackerRemoveScreen(
} }
fun unregisterTracking(serviceId: Long) { fun unregisterTracking(serviceId: Long) {
screenModelScope.launchNonCancellable { deleteTrack.await(mangaId, serviceId) } viewModelScope.launchNonCancellable { deleteTrack.await(mangaId, serviceId) }
} }
} }
} }
@@ -7,9 +7,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import cafe.adriel.voyager.core.model.ScreenModel import androidx.lifecycle.ViewModel
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -58,14 +58,14 @@ data object MoreTab : Tab {
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MoreScreenModel() } val viewModel = viewModel<MoreViewModel>()
val downloadQueueState by screenModel.downloadQueueState.collectAsState() val downloadQueueState by viewModel.downloadQueueState.collectAsState()
MoreScreen( MoreScreen(
downloadQueueStateProvider = { downloadQueueState }, downloadQueueStateProvider = { downloadQueueState },
downloadedOnly = screenModel.downloadedOnly, downloadedOnly = viewModel.downloadedOnly,
onDownloadedOnlyChange = { screenModel.downloadedOnly = it }, onDownloadedOnlyChange = { viewModel.downloadedOnly = it },
incognitoMode = screenModel.incognitoMode, incognitoMode = viewModel.incognitoMode,
onIncognitoModeChange = { screenModel.incognitoMode = it }, onIncognitoModeChange = { viewModel.incognitoMode = it },
onClickDownloadQueue = { navigator.push(DownloadQueueScreen) }, onClickDownloadQueue = { navigator.push(DownloadQueueScreen) },
onClickCategories = { navigator.push(CategoryScreen()) }, onClickCategories = { navigator.push(CategoryScreen()) },
onClickStats = { navigator.push(StatsScreen()) }, onClickStats = { navigator.push(StatsScreen()) },
@@ -77,20 +77,20 @@ data object MoreTab : Tab {
} }
} }
private class MoreScreenModel( private class MoreViewModel(
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
preferences: BasePreferences = Injekt.get(), preferences: BasePreferences = Injekt.get(),
) : ScreenModel { ) : ViewModel() {
var downloadedOnly by preferences.downloadedOnly.asState(screenModelScope) var downloadedOnly by preferences.downloadedOnly.asState(viewModelScope)
var incognitoMode by preferences.incognitoMode.asState(screenModelScope) var incognitoMode by preferences.incognitoMode.asState(viewModelScope)
private var _downloadQueueState: MutableStateFlow<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped) private var _downloadQueueState: MutableStateFlow<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped)
val downloadQueueState: StateFlow<DownloadQueueState> = _downloadQueueState.asStateFlow() val downloadQueueState: StateFlow<DownloadQueueState> = _downloadQueueState.asStateFlow()
init { init {
// Handle running/paused status change and queue progress updating // Handle running/paused status change and queue progress updating
screenModelScope.launchIO { viewModelScope.launchIO {
combine( combine(
downloadManager.isDownloaderRunning, downloadManager.isDownloaderRunning,
downloadManager.queueState, downloadManager.queueState,
@@ -74,7 +74,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator 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
@@ -252,8 +252,8 @@ class ReaderActivity : BaseActivity() {
private fun ReaderActivityBinding.setComposeOverlay(): Unit = composeOverlay.setComposeContent { private fun ReaderActivityBinding.setComposeOverlay(): Unit = composeOverlay.setComposeContent {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val showPageNumber by readerPreferences.showPageNumber.collectAsState() val showPageNumber by readerPreferences.showPageNumber.collectAsState()
val settingsScreenModel = remember { val settingsviewModel = remember {
ReaderSettingsScreenModel( ReaderSettingsViewModel(
readerState = viewModel.state, readerState = viewModel.state,
onChangeReadingMode = viewModel::setMangaReadingMode, onChangeReadingMode = viewModel::setMangaReadingMode,
onChangeOrientation = viewModel::setMangaOrientationType, onChangeOrientation = viewModel::setMangaOrientationType,
@@ -298,13 +298,13 @@ class ReaderActivity : BaseActivity() {
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onShowMenus = { setMenuVisibility(true) }, onShowMenus = { setMenuVisibility(true) },
onHideMenus = { setMenuVisibility(false) }, onHideMenus = { setMenuVisibility(false) },
screenModel = settingsScreenModel, viewModel = settingsviewModel,
) )
} }
is ReaderViewModel.Dialog.ReadingModeSelect -> { is ReaderViewModel.Dialog.ReadingModeSelect -> {
ReadingModeSelectDialog( ReadingModeSelectDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel, viewModel = settingsviewModel,
onChange = { stringRes -> onChange = { stringRes ->
menuToggleToast?.cancel() menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode.get()) { if (!readerPreferences.showReadingMode.get()) {
@@ -316,7 +316,7 @@ class ReaderActivity : BaseActivity() {
is ReaderViewModel.Dialog.OrientationModeSelect -> { is ReaderViewModel.Dialog.OrientationModeSelect -> {
OrientationSelectDialog( OrientationSelectDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel, viewModel = settingsviewModel,
onChange = { stringRes -> onChange = { stringRes ->
menuToggleToast?.cancel() menuToggleToast?.cancel()
menuToggleToast = toast(stringRes) menuToggleToast = toast(stringRes)
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.setting package eu.kanade.tachiyomi.ui.reader.setting
import cafe.adriel.voyager.core.model.ScreenModel import androidx.lifecycle.ViewModel
import eu.kanade.presentation.util.ioCoroutineScope import androidx.lifecycle.viewModelScope
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel import eu.kanade.tachiyomi.ui.reader.ReaderViewModel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -11,20 +11,20 @@ import kotlinx.coroutines.flow.stateIn
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ReaderSettingsScreenModel( class ReaderSettingsViewModel(
readerState: StateFlow<ReaderViewModel.State>, readerState: StateFlow<ReaderViewModel.State>,
val onChangeReadingMode: (ReadingMode) -> Unit, val onChangeReadingMode: (ReadingMode) -> Unit,
val onChangeOrientation: (ReaderOrientation) -> Unit, val onChangeOrientation: (ReaderOrientation) -> Unit,
val preferences: ReaderPreferences = Injekt.get(), val preferences: ReaderPreferences = Injekt.get(),
) : ScreenModel { ) : ViewModel() {
val viewerFlow = readerState val viewerFlow = readerState
.map { it.viewer } .map { it.viewer }
.distinctUntilChanged() .distinctUntilChanged()
.stateIn(ioCoroutineScope, SharingStarted.Lazily, null) .stateIn(viewModelScope, SharingStarted.Lazily, null)
val mangaFlow = readerState val mangaFlow = readerState
.map { it.manga } .map { it.manga }
.distinctUntilChanged() .distinctUntilChanged()
.stateIn(ioCoroutineScope, SharingStarted.Lazily, null) .stateIn(viewModelScope, SharingStarted.Lazily, null)
} }
@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.stats
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@@ -21,8 +21,8 @@ class StatsScreen : Screen() {
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { StatsScreenModel() } val viewModel = viewModel<StatsViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
@@ -2,8 +2,7 @@ package eu.kanade.tachiyomi.ui.stats
import androidx.compose.ui.util.fastDistinctBy import androidx.compose.ui.util.fastDistinctBy
import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastFilter
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.fastCountNot import eu.kanade.core.util.fastCountNot
import eu.kanade.presentation.more.stats.StatsScreenState import eu.kanade.presentation.more.stats.StatsScreenState
import eu.kanade.presentation.more.stats.data.StatsData import eu.kanade.presentation.more.stats.data.StatsData
@@ -11,6 +10,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.history.interactor.GetTotalReadDuration import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
@@ -25,19 +25,19 @@ import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class StatsScreenModel( class StatsViewModel(
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val getLibraryManga: GetLibraryManga = Injekt.get(), private val getLibraryManga: GetLibraryManga = Injekt.get(),
private val getTotalReadDuration: GetTotalReadDuration = Injekt.get(), private val getTotalReadDuration: GetTotalReadDuration = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
private val preferences: LibraryPreferences = Injekt.get(), private val preferences: LibraryPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(),
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) { ) : StateViewModel<StatsScreenState>(StatsScreenState.Loading) {
private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() } private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() }
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
val libraryManga = getLibraryManga.await() val libraryManga = getLibraryManga.await()
val distinctLibraryManga = libraryManga.fastDistinctBy { it.id } val distinctLibraryManga = libraryManga.fastDistinctBy { it.id }
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.updates package eu.kanade.tachiyomi.ui.updates
import cafe.adriel.voyager.core.model.ScreenModel import androidx.lifecycle.ViewModel
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
@@ -8,9 +8,9 @@ import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class UpdatesSettingsScreenModel( class UpdatesSettingsViewModel(
val updatesPreferences: UpdatesPreferences = Injekt.get(), val updatesPreferences: UpdatesPreferences = Injekt.get(),
) : ScreenModel { ) : ViewModel() {
fun toggleFilter(preference: (UpdatesPreferences) -> Preference<TriState>) { fun toggleFilter(preference: (UpdatesPreferences) -> Preference<TriState>) {
preference(updatesPreferences).getAndSet { preference(updatesPreferences).getAndSet {
@@ -9,7 +9,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -25,7 +25,7 @@ import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel.Event import eu.kanade.tachiyomi.ui.updates.UpdatesViewModel.Event
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import mihon.feature.upcoming.UpcomingScreen import mihon.feature.upcoming.UpcomingScreen
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@@ -54,53 +54,53 @@ data object UpdatesTab : Tab {
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { UpdatesScreenModel() } val viewModel = viewModel<UpdatesViewModel>()
val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() } val settingsViewModel = viewModel<UpdatesSettingsViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
UpdateScreen( UpdateScreen(
state = state, state = state,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = viewModel.snackbarHostState,
lastUpdated = screenModel.lastUpdated, lastUpdated = viewModel.lastUpdated,
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) }, onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
onSelectAll = screenModel::toggleAllSelection, onSelectAll = viewModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection, onInvertSelection = viewModel::invertSelection,
onUpdateLibrary = screenModel::updateLibrary, onUpdateLibrary = viewModel::updateLibrary,
onDownloadChapter = screenModel::downloadChapters, onDownloadChapter = viewModel::downloadChapters,
onMultiBookmarkClicked = screenModel::bookmarkUpdates, onMultiBookmarkClicked = viewModel::bookmarkUpdates,
onMultiMarkAsReadClicked = screenModel::markUpdatesRead, onMultiMarkAsReadClicked = viewModel::markUpdatesRead,
onMultiDeleteClicked = screenModel::showConfirmDeleteChapters, onMultiDeleteClicked = viewModel::showConfirmDeleteChapters,
onUpdateSelected = screenModel::toggleSelection, onUpdateSelected = viewModel::toggleSelection,
onOpenChapter = { onOpenChapter = {
val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId) val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)
context.startActivity(intent) context.startActivity(intent)
}, },
onCalendarClicked = { navigator.push(UpcomingScreen()) }, onCalendarClicked = { navigator.push(UpcomingScreen()) },
onFilterClicked = screenModel::showFilterDialog, onFilterClicked = viewModel::showFilterDialog,
hasActiveFilters = state.hasActiveFilters, hasActiveFilters = state.hasActiveFilters,
) )
val onDismissDialog = { screenModel.setDialog(null) } val onDismissDialog = { viewModel.setDialog(null) }
when (val dialog = state.dialog) { when (val dialog = state.dialog) {
is UpdatesScreenModel.Dialog.DeleteConfirmation -> { is UpdatesViewModel.Dialog.DeleteConfirmation -> {
UpdatesDeleteConfirmationDialog( UpdatesDeleteConfirmationDialog(
onDismissRequest = onDismissDialog, onDismissRequest = onDismissDialog,
onConfirm = { screenModel.deleteChapters(dialog.toDelete) }, onConfirm = { viewModel.deleteChapters(dialog.toDelete) },
) )
} }
is UpdatesScreenModel.Dialog.FilterSheet -> { is UpdatesViewModel.Dialog.FilterSheet -> {
UpdatesFilterDialog( UpdatesFilterDialog(
onDismissRequest = onDismissDialog, onDismissRequest = onDismissDialog,
screenModel = settingsScreenModel, viewModel = settingsViewModel,
) )
} }
null -> {} null -> {}
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.events.collectLatest { event -> viewModel.events.collectLatest { event ->
when (event) { when (event) {
Event.InternalError -> screenModel.snackbarHostState.showSnackbar( Event.InternalError -> viewModel.snackbarHostState.showSnackbar(
context.stringResource(MR.strings.internal_error), context.stringResource(MR.strings.internal_error),
) )
is Event.LibraryUpdateTriggered -> { is Event.LibraryUpdateTriggered -> {
@@ -109,7 +109,7 @@ data object UpdatesTab : Tab {
} else { } else {
MR.strings.update_already_running MR.strings.update_already_running
} }
screenModel.snackbarHostState.showSnackbar(context.stringResource(msg)) viewModel.snackbarHostState.showSnackbar(context.stringResource(msg))
} }
} }
} }
@@ -125,10 +125,10 @@ data object UpdatesTab : Tab {
} }
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
screenModel.resetNewUpdatesCount() viewModel.resetNewUpdatesCount()
onDispose { onDispose {
screenModel.resetNewUpdatesCount() viewModel.resetNewUpdatesCount()
} }
} }
} }
@@ -5,8 +5,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastFilter
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparators
@@ -33,6 +32,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
@@ -51,7 +51,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime import java.time.ZonedDateTime
class UpdatesScreenModel( class UpdatesViewModel(
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 downloadCache: DownloadCache = Injekt.get(), private val downloadCache: DownloadCache = Injekt.get(),
@@ -63,19 +63,19 @@ class UpdatesScreenModel(
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val updatesPreferences: UpdatesPreferences = Injekt.get(), private val updatesPreferences: UpdatesPreferences = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<UpdatesScreenModel.State>(State()) { ) : StateViewModel<UpdatesViewModel.State>(State()) {
private val _events: Channel<Event> = Channel(Int.MAX_VALUE) private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
val events: Flow<Event> = _events.receiveAsFlow() val events: Flow<Event> = _events.receiveAsFlow()
val lastUpdated by libraryPreferences.lastUpdatedTimestamp.asState(screenModelScope) val lastUpdated by libraryPreferences.lastUpdatedTimestamp.asState(viewModelScope)
// First and last selected index in list // First and last selected index in list
private val selectedPositions: Array<Int> = arrayOf(-1, -1) private val selectedPositions: Array<Int> = arrayOf(-1, -1)
private val selectedChapterIds: HashSet<Long> = HashSet() private val selectedChapterIds: HashSet<Long> = HashSet()
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
// Set date limit for recent chapters // Set date limit for recent chapters
val limit = ZonedDateTime.now().minusMonths(3).toInstant() val limit = ZonedDateTime.now().minusMonths(3).toInstant()
@@ -113,10 +113,10 @@ class UpdatesScreenModel(
} }
} }
screenModelScope.launchIO { viewModelScope.launchIO {
merge(downloadManager.statusFlow(), downloadManager.progressFlow()) merge(downloadManager.statusFlow(), downloadManager.progressFlow())
.catch { logcat(LogPriority.ERROR, it) } .catch { logcat(LogPriority.ERROR, it) }
.collect(this@UpdatesScreenModel::updateDownloadState) .collect(this@UpdatesViewModel::updateDownloadState)
} }
getUpdatesItemPreferenceFlow() getUpdatesItemPreferenceFlow()
@@ -135,7 +135,7 @@ class UpdatesScreenModel(
state.copy(hasActiveFilters = it) state.copy(hasActiveFilters = it)
} }
} }
.launchIn(screenModelScope) .launchIn(viewModelScope)
} }
private fun List<UpdatesItem>.applyFilters( private fun List<UpdatesItem>.applyFilters(
@@ -181,7 +181,7 @@ class UpdatesScreenModel(
fun updateLibrary(): Boolean { fun updateLibrary(): Boolean {
val started = LibraryUpdateJob.startNow(Injekt.get<Application>()) val started = LibraryUpdateJob.startNow(Injekt.get<Application>())
screenModelScope.launch { viewModelScope.launch {
_events.send(Event.LibraryUpdateTriggered(started)) _events.send(Event.LibraryUpdateTriggered(started))
} }
return started return started
@@ -210,7 +210,7 @@ class UpdatesScreenModel(
fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) { fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
if (items.isEmpty()) return if (items.isEmpty()) return
screenModelScope.launch { viewModelScope.launch {
when (action) { when (action) {
ChapterDownloadAction.START -> { ChapterDownloadAction.START -> {
downloadChapters(items) downloadChapters(items)
@@ -250,7 +250,7 @@ class UpdatesScreenModel(
* @param read whether to mark chapters as read or unread. * @param read whether to mark chapters as read or unread.
*/ */
fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) { fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
screenModelScope.launchIO { viewModelScope.launchIO {
setReadStatus.await( setReadStatus.await(
read = read, read = read,
chapters = updates chapters = updates
@@ -266,7 +266,7 @@ class UpdatesScreenModel(
* @param updates the list of chapters to bookmark. * @param updates the list of chapters to bookmark.
*/ */
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) { fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
screenModelScope.launchIO { viewModelScope.launchIO {
updates updates
.filterNot { it.update.bookmark == bookmark } .filterNot { it.update.bookmark == bookmark }
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) } .map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
@@ -280,7 +280,7 @@ class UpdatesScreenModel(
* @param updatesItem the list of chapters to download. * @param updatesItem the list of chapters to download.
*/ */
private fun downloadChapters(updatesItem: List<UpdatesItem>) { private fun downloadChapters(updatesItem: List<UpdatesItem>) {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
for (updates in groupedUpdates) { for (updates in groupedUpdates) {
val mangaId = updates.first().update.mangaId val mangaId = updates.first().update.mangaId
@@ -299,7 +299,7 @@ class UpdatesScreenModel(
* @param updatesItem list of chapters * @param updatesItem list of chapters
*/ */
fun deleteChapters(updatesItem: List<UpdatesItem>) { fun deleteChapters(updatesItem: List<UpdatesItem>) {
screenModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
updatesItem updatesItem
.groupBy { it.update.mangaId } .groupBy { it.update.mangaId }
.entries .entries
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.webview package eu.kanade.tachiyomi.ui.webview
import android.app.Activity
import android.app.assist.AssistContent import android.app.assist.AssistContent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.ui.webview
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.AssistContentScreen
@@ -23,17 +24,22 @@ class WebViewScreen(
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val screenModel = rememberScreenModel { WebViewScreenModel(sourceId) } val viewModel = viewModel<WebViewViewModel>(
factory = WebViewViewModel.Factory,
extras = CreationExtras {
set(WebViewViewModel.SOURCE_ID_KEY, sourceId)
},
)
WebViewScreenContent( WebViewScreenContent(
onNavigateUp = { navigator.pop() }, onNavigateUp = { navigator.pop() },
initialTitle = initialTitle, initialTitle = initialTitle,
url = url, url = url,
headers = screenModel.headers, headers = viewModel.headers,
onUrlChange = { assistUrl = it }, onUrlChange = { assistUrl = it },
onShare = { screenModel.shareWebpage(context, it) }, onShare = { viewModel.shareWebpage(context, it) },
onOpenInBrowser = { screenModel.openInBrowser(context, it) }, onOpenInBrowser = { viewModel.openInBrowser(context, it) },
onClearCookies = screenModel::clearCookies, onClearCookies = viewModel::clearCookies,
) )
} }
} }
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.ui.webview
import android.content.Context import android.content.Context
import androidx.core.net.toUri import androidx.core.net.toUri
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.presentation.more.stats.StatsScreenState import eu.kanade.presentation.more.stats.StatsScreenState
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@@ -10,17 +12,30 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class WebViewScreenModel( class WebViewViewModel(
val sourceId: Long?, val sourceId: Long?,
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val network: NetworkHelper = Injekt.get(), private val network: NetworkHelper = Injekt.get(),
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) { ) : StateViewModel<StatsScreenState>(StatsScreenState.Loading) {
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long?>()
val Factory = viewModelFactory {
initializer {
WebViewViewModel(
sourceId = get(SOURCE_ID_KEY),
)
}
}
}
var headers = emptyMap<String, String>() var headers = emptyMap<String, String>()
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util.system
import android.content.Context import android.content.Context
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesViewModel
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import java.util.Locale import java.util.Locale
@@ -30,8 +30,8 @@ object LocaleHelper {
*/ */
fun getSourceDisplayName(lang: String?, context: Context): String { fun getSourceDisplayName(lang: String?, context: Context): String {
return when (lang) { return when (lang) {
SourcesScreenModel.LAST_USED_KEY -> context.stringResource(MR.strings.last_used_source) SourcesViewModel.LAST_USED_KEY -> context.stringResource(MR.strings.last_used_source)
SourcesScreenModel.PINNED_KEY -> context.stringResource(MR.strings.pinned_sources) SourcesViewModel.PINNED_KEY -> context.stringResource(MR.strings.pinned_sources)
"other" -> context.stringResource(MR.strings.other_source) "other" -> context.stringResource(MR.strings.other_source)
"all" -> context.stringResource(MR.strings.multi_lang) "all" -> context.stringResource(MR.strings.multi_lang)
else -> getLocalizedDisplayName(lang) else -> getLocalizedDisplayName(lang)
@@ -36,9 +36,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
@@ -50,6 +49,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import mihon.feature.migration.list.MigrationListScreen import mihon.feature.migration.list.MigrationListScreen
import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableCollectionItemScope
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
@@ -77,8 +77,8 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ScreenModel() } val viewModel = viewModel<ViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
var migrationSheetOpen by rememberSaveable { mutableStateOf(false) } var migrationSheetOpen by rememberSaveable { mutableStateOf(false) }
@@ -121,20 +121,20 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.migrationConfigScreen_selectAllLabel), title = stringResource(MR.strings.migrationConfigScreen_selectAllLabel),
icon = Icons.Outlined.SelectAll, icon = Icons.Outlined.SelectAll,
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.All) }, onClick = { viewModel.toggleSelection(ViewModel.SelectionConfig.All) },
), ),
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.migrationConfigScreen_selectNoneLabel), title = stringResource(MR.strings.migrationConfigScreen_selectNoneLabel),
icon = Icons.Outlined.Deselect, icon = Icons.Outlined.Deselect,
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.None) }, onClick = { viewModel.toggleSelection(ViewModel.SelectionConfig.None) },
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.migrationConfigScreen_selectEnabledLabel), title = stringResource(MR.strings.migrationConfigScreen_selectEnabledLabel),
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.Enabled) }, onClick = { viewModel.toggleSelection(ViewModel.SelectionConfig.Enabled) },
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.migrationConfigScreen_selectPinnedLabel), title = stringResource(MR.strings.migrationConfigScreen_selectPinnedLabel),
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.Pinned) }, onClick = { viewModel.toggleSelection(ViewModel.SelectionConfig.Pinned) },
), ),
), ),
) )
@@ -146,7 +146,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) }, text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) }, icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
onClick = { onClick = {
screenModel.saveSources() viewModel.saveSources()
continueMigration(openSheet = true, extraSearchQuery = null) continueMigration(openSheet = true, extraSearchQuery = null)
}, },
expanded = lazyListState.shouldExpandFAB(), expanded = lazyListState.shouldExpandFAB(),
@@ -157,7 +157,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
val fromIndex = selectedSources.indexOfFirst { it.id == from.key } val fromIndex = selectedSources.indexOfFirst { it.id == from.key }
val toIndex = selectedSources.indexOfFirst { it.id == to.key } val toIndex = selectedSources.indexOfFirst { it.id == to.key }
if (fromIndex == -1 || toIndex == -1) return@rememberReorderableLazyListState if (fromIndex == -1 || toIndex == -1) return@rememberReorderableLazyListState
screenModel.orderSource(fromIndex, toIndex) viewModel.orderSource(fromIndex, toIndex)
} }
FastScrollLazyColumn( FastScrollLazyColumn(
@@ -197,7 +197,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
dragEnabled = selectedSourceList && sources.size > 1, dragEnabled = selectedSourceList && sources.size > 1,
state = reorderableState, state = reorderableState,
key = { if (selectedSourceList) it.id else "available-${it.id}" }, key = { if (selectedSourceList) it.id else "available-${it.id}" },
onClick = { screenModel.toggleSelection(item.id) }, onClick = { viewModel.toggleSelection(item.id) },
) )
} }
} }
@@ -206,7 +206,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
if (migrationSheetOpen) { if (migrationSheetOpen) {
MigrationConfigScreenSheet( MigrationConfigScreenSheet(
preferences = screenModel.sourcePreferences, preferences = viewModel.sourcePreferences,
onDismissRequest = { migrationSheetOpen = false }, onDismissRequest = { migrationSheetOpen = false },
onStartMigration = { extraSearchQuery -> onStartMigration = { extraSearchQuery ->
migrationSheetOpen = false migrationSheetOpen = false
@@ -306,10 +306,10 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
} }
} }
private class ScreenModel( private class ViewModel(
val sourcePreferences: SourcePreferences = Injekt.get(), val sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
) : StateScreenModel<ScreenModel.State>(State()) { ) : StateViewModel<ViewModel.State>(State()) {
private val sourcesComparator = { includedSources: List<Long> -> private val sourcesComparator = { includedSources: List<Long> ->
compareBy<MigrationSource>( compareBy<MigrationSource>(
@@ -320,7 +320,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
} }
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
initSources() initSources()
mutableState.update { it.copy(isLoading = false) } mutableState.update { it.copy(isLoading = false) }
} }
@@ -18,14 +18,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import mihon.domain.migration.models.MigrationFlag import mihon.domain.migration.models.MigrationFlag
import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.feature.common.utils.getLabel import mihon.feature.common.utils.getLabel
@@ -50,11 +50,11 @@ internal fun Screen.MigrateMangaDialog(
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val screenModel = rememberScreenModel { MigrateDialogScreenModel() } val viewModel = viewModel<MigrateDialogViewModel>()
LaunchedEffect(current, target) { LaunchedEffect(current, target) {
screenModel.init(current, target) viewModel.init(current, target)
} }
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
if (state.isMigrated) return if (state.isMigrated) return
@@ -78,7 +78,7 @@ internal fun Screen.MigrateMangaDialog(
LabeledCheckbox( LabeledCheckbox(
label = stringResource(flag.getLabel()), label = stringResource(flag.getLabel()),
checked = flag in state.selectedFlags, checked = flag in state.selectedFlags,
onCheckedChange = { screenModel.toggleSelection(flag) }, onCheckedChange = { viewModel.toggleSelection(flag) },
) )
} }
} }
@@ -101,7 +101,7 @@ internal fun Screen.MigrateMangaDialog(
TextButton( TextButton(
onClick = { onClick = {
scope.launchIO { scope.launchIO {
screenModel.migrateManga(replace = false) viewModel.migrateManga(replace = false)
withUIContext { onComplete() } withUIContext { onComplete() }
} }
}, },
@@ -111,7 +111,7 @@ internal fun Screen.MigrateMangaDialog(
TextButton( TextButton(
onClick = { onClick = {
scope.launchIO { scope.launchIO {
screenModel.migrateManga(replace = true) viewModel.migrateManga(replace = true)
withUIContext { onComplete() } withUIContext { onComplete() }
} }
}, },
@@ -123,12 +123,12 @@ internal fun Screen.MigrateMangaDialog(
) )
} }
private class MigrateDialogScreenModel( private class MigrateDialogViewModel(
private val sourcePreference: SourcePreferences = Injekt.get(), private val sourcePreference: SourcePreferences = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val migrateManga: MigrateMangaUseCase = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(),
) : StateScreenModel<MigrateDialogScreenModel.State>(State()) { ) : StateViewModel<MigrateDialogViewModel.State>(State()) {
fun init(current: Manga, target: Manga) { fun init(current: Manga, target: Manga) {
val applicableFlags = buildList { val applicableFlags = buildList {
@@ -7,7 +7,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
@@ -30,13 +31,19 @@ class MigrationListScreen(private val mangaIds: Collection<Long>, private val ex
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MigrationListScreenModel(mangaIds, extraSearchQuery) } val viewModel = viewModel<MigrationListViewModel>(
val state by screenModel.state.collectAsState() factory = MigrationListViewModel.Factory,
extras = CreationExtras {
set(MigrationListViewModel.MANGA_IDS_KEY, mangaIds)
set(MigrationListViewModel.EXTRA_SEARCH_QUERY_KEY, extraSearchQuery)
},
)
val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(matchOverride) { LaunchedEffect(matchOverride) {
val (current, target) = matchOverride ?: return@LaunchedEffect val (current, target) = matchOverride ?: return@LaunchedEffect
screenModel.useMangaForMigration( viewModel.useMangaForMigration(
current = current, current = current,
target = target, target = target,
onMissingChapters = { onMissingChapters = {
@@ -46,8 +53,8 @@ class MigrationListScreen(private val mangaIds: Collection<Long>, private val ex
matchOverride = null matchOverride = null
} }
LaunchedEffect(screenModel) { LaunchedEffect(viewModel) {
screenModel.navigateBackEvent.collect { viewModel.navigateBackEvent.collect {
navigator.pop() navigator.pop()
} }
} }
@@ -61,37 +68,37 @@ class MigrationListScreen(private val mangaIds: Collection<Long>, private val ex
onSearchManually = { migrationItem -> onSearchManually = { migrationItem ->
navigator push MigrateSearchScreen(migrationItem.manga.id) navigator push MigrateSearchScreen(migrationItem.manga.id)
}, },
onSkip = { screenModel.removeManga(it) }, onSkip = { viewModel.removeManga(it) },
onMigrate = { screenModel.migrateNow(mangaId = it, replace = true) }, onMigrate = { viewModel.migrateNow(mangaId = it, replace = true) },
onCopy = { screenModel.migrateNow(mangaId = it, replace = false) }, onCopy = { viewModel.migrateNow(mangaId = it, replace = false) },
openMigrationDialog = screenModel::showMigrateDialog, openMigrationDialog = viewModel::showMigrateDialog,
) )
when (val dialog = state.dialog) { when (val dialog = state.dialog) {
is MigrationListScreenModel.Dialog.Migrate -> { is MigrationListViewModel.Dialog.Migrate -> {
MigrationMangaDialog( MigrationMangaDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = viewModel::dismissDialog,
copy = dialog.copy, copy = dialog.copy,
totalCount = dialog.totalCount, totalCount = dialog.totalCount,
skippedCount = dialog.skippedCount, skippedCount = dialog.skippedCount,
onMigrate = { onMigrate = {
if (dialog.copy) { if (dialog.copy) {
screenModel.copyMangas() viewModel.copyMangas()
} else { } else {
screenModel.migrateMangas() viewModel.migrateMangas()
} }
}, },
) )
} }
is MigrationListScreenModel.Dialog.Progress -> { is MigrationListViewModel.Dialog.Progress -> {
MigrationProgressDialog( MigrationProgressDialog(
progress = dialog.progress, progress = dialog.progress,
exitMigration = screenModel::cancelMigrate, exitMigration = viewModel::cancelMigrate,
) )
} }
MigrationListScreenModel.Dialog.Exit -> { MigrationListViewModel.Dialog.Exit -> {
MigrationExitDialog( MigrationExitDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = viewModel::dismissDialog,
exitMigration = navigator::pop, exitMigration = navigator::pop,
) )
} }
@@ -99,7 +106,7 @@ class MigrationListScreen(private val mangaIds: Collection<Long>, private val ex
} }
BackHandler(true) { BackHandler(true) {
screenModel.showExitDialog() viewModel.showExitDialog()
} }
} }
} }
@@ -1,9 +1,10 @@
package mihon.feature.migration.list package mihon.feature.migration.list
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope import androidx.lifecycle.viewmodel.CreationExtras
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.getNameForMangaInfo
@@ -21,6 +22,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.domain.source.interactor.UpdateMangaFromRemote
import mihon.feature.migration.list.models.MigratingManga import mihon.feature.migration.list.models.MigratingManga
@@ -37,18 +39,32 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrationListScreenModel( class MigrationListViewModel(
mangaIds: Collection<Long>, mangaIds: Collection<Long>,
extraSearchQuery: String?, extraSearchQuery: String?,
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val migrateManga: MigrateMangaUseCase = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
) : StateScreenModel<MigrationListScreenModel.State>(State()) { ) : StateViewModel<MigrationListViewModel.State>(State()) {
companion object {
val MANGA_IDS_KEY = CreationExtras.Key<Collection<Long>>()
val EXTRA_SEARCH_QUERY_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
MigrationListViewModel(
mangaIds = get(MANGA_IDS_KEY)!!,
extraSearchQuery = get(EXTRA_SEARCH_QUERY_KEY),
)
}
}
}
private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery) private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery)
@@ -64,7 +80,7 @@ class MigrationListScreenModel(
private var migrateJob: Job? = null private var migrateJob: Job? = null
init { init {
screenModelScope.launchIO { viewModelScope.launchIO {
val manga = mangaIds val manga = mangaIds
.map { .map {
async { async {
@@ -75,7 +91,7 @@ class MigrationListScreenModel(
chapterCount = chapterInfo.chapterCount, chapterCount = chapterInfo.chapterCount,
latestChapter = chapterInfo.latestChapter, latestChapter = chapterInfo.latestChapter,
source = sourceManager.getOrStub(manga.source).getNameForMangaInfo(), source = sourceManager.getOrStub(manga.source).getNameForMangaInfo(),
parentContext = screenModelScope.coroutineContext, parentContext = viewModelScope.coroutineContext,
) )
} }
} }
@@ -216,7 +232,7 @@ class MigrationListScreenModel(
fun useMangaForMigration(current: Long, target: Long, onMissingChapters: () -> Unit) { fun useMangaForMigration(current: Long, target: Long, onMissingChapters: () -> Unit) {
val migratingManga = items.find { it.manga.id == current } ?: return val migratingManga = items.find { it.manga.id == current } ?: return
migratingManga.searchResult.value = SearchResult.Searching migratingManga.searchResult.value = SearchResult.Searching
screenModelScope.launchIO { viewModelScope.launchIO {
val result = migratingManga.migrationScope.async { val result = migratingManga.migrationScope.async {
val manga = getManga.await(target) ?: return@async null val manga = getManga.await(target) ?: return@async null
try { try {
@@ -248,7 +264,7 @@ class MigrationListScreenModel(
} }
private fun migrateMangas(replace: Boolean) { private fun migrateMangas(replace: Boolean) {
migrateJob = screenModelScope.launchIO { migrateJob = viewModelScope.launchIO {
mutableState.update { it.copy(dialog = Dialog.Progress(0f)) } mutableState.update { it.copy(dialog = Dialog.Progress(0f)) }
val items = items val items = items
try { try {
@@ -292,7 +308,7 @@ class MigrationListScreenModel(
} }
fun migrateNow(mangaId: Long, replace: Boolean) { fun migrateNow(mangaId: Long, replace: Boolean) {
screenModelScope.launchIO { viewModelScope.launchIO {
val manga = items.find { it.manga.id == mangaId } ?: return@launchIO val manga = items.find { it.manga.id == mangaId } ?: return@launchIO
val target = (manga.searchResult.value as? SearchResult.Success)?.manga ?: return@launchIO val target = (manga.searchResult.value as? SearchResult.Success)?.manga ?: return@launchIO
migrateManga(current = manga.manga, target = target, replace = replace) migrateManga(current = manga.manga, target = target, replace = replace)
@@ -302,7 +318,7 @@ class MigrationListScreenModel(
} }
fun removeManga(mangaId: Long) { fun removeManga(mangaId: Long) {
screenModelScope.launchIO { viewModelScope.launchIO {
val item = items.find { it.manga.id == mangaId } ?: return@launchIO val item = items.find { it.manga.id == mangaId } ?: return@launchIO
removeManga(item) removeManga(item)
item.migrationScope.cancel() item.migrationScope.cancel()
@@ -314,8 +330,7 @@ class MigrationListScreenModel(
mutableState.update { it.copy(items = items.toMutableList().apply { remove(item) }) } mutableState.update { it.copy(items = items.toMutableList().apply { remove(item) }) }
} }
override fun onDispose() { override fun onCleared() {
super.onDispose()
items.forEach { items.forEach {
it.migrationScope.cancel() it.migrationScope.cancel()
} }
@@ -3,7 +3,7 @@ package mihon.feature.upcoming
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
@@ -15,12 +15,12 @@ class UpcomingScreen : Screen() {
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { UpcomingScreenModel() } val viewModel = viewModel<UpcomingViewModel>()
val state by screenModel.state.collectAsState() val state by viewModel.state.collectAsState()
UpcomingScreenContent( UpcomingScreenContent(
state = state, state = state,
setSelectedYearMonth = screenModel::setSelectedYearMonth, setSelectedYearMonth = viewModel::setSelectedYearMonth,
onClickUpcoming = { navigator.push(MangaScreen(it.id)) }, onClickUpcoming = { navigator.push(MangaScreen(it.id)) },
) )
} }
@@ -41,7 +41,7 @@ import java.time.YearMonth
@Composable @Composable
fun UpcomingScreenContent( fun UpcomingScreenContent(
state: UpcomingScreenModel.State, state: UpcomingViewModel.State,
setSelectedYearMonth: (YearMonth) -> Unit, setSelectedYearMonth: (YearMonth) -> Unit,
onClickUpcoming: (manga: Manga) -> Unit, onClickUpcoming: (manga: Manga) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -2,13 +2,13 @@ package mihon.feature.upcoming
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapIndexedNotNull import androidx.compose.ui.util.fastMapIndexedNotNull
import cafe.adriel.voyager.core.model.StateScreenModel import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.insertSeparatorsReversed import eu.kanade.core.util.insertSeparatorsReversed
import eu.kanade.tachiyomi.util.lang.toLocalDate import eu.kanade.tachiyomi.util.lang.toLocalDate
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import mihon.domain.upcoming.interactor.GetUpcomingManga import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -16,12 +16,12 @@ import uy.kohesive.injekt.api.get
import java.time.LocalDate import java.time.LocalDate
import java.time.YearMonth import java.time.YearMonth
class UpcomingScreenModel( class UpcomingViewModel(
private val getUpcomingManga: GetUpcomingManga = Injekt.get(), private val getUpcomingManga: GetUpcomingManga = Injekt.get(),
) : StateScreenModel<UpcomingScreenModel.State>(State()) { ) : StateViewModel<UpcomingViewModel.State>(State()) {
init { init {
screenModelScope.launch { viewModelScope.launch {
getUpcomingManga.subscribe().collectLatest { getUpcomingManga.subscribe().collectLatest {
mutableState.update { state -> mutableState.update { state ->
val upcomingItems = it.toUpcomingUIModels() val upcomingItems = it.toUpcomingUIModels()
+14
View File
@@ -0,0 +1,14 @@
plugins {
alias(mihonx.plugins.android.library)
alias(mihonx.plugins.spotless)
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "mihon.core.viewmodel"
}
dependencies {
implementation(libs.androidx.lifecycle.viewmodel)
}
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
@@ -0,0 +1,11 @@
package mihon.core.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
abstract class StateViewModel<S>(initialState: S) : ViewModel() {
protected val mutableState: MutableStateFlow<S> = MutableStateFlow(initialState)
val state: StateFlow<S> = mutableState.asStateFlow()
}
+4 -3
View File
@@ -100,6 +100,8 @@ androidx-interpolator = { module = "androidx.interpolator:interpolator", version
androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "androidx-lifecycle" } androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "androidx-lifecycle" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" }
androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" } androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" }
androidx-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" } androidx-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" }
@@ -178,7 +180,6 @@ swipe = { module = "me.saket.swipe:swipe", version.ref = "swipe" }
tapmoc-gradle = { module = "com.gradleup.tapmoc:tapmoc-gradle-plugin", version.ref = "tapmoc" } tapmoc-gradle = { module = "com.gradleup.tapmoc:tapmoc-gradle-plugin", version.ref = "tapmoc" }
unifile = { module = "com.github.mihon:unifile", version.ref = "unifile" } unifile = { module = "com.github.mihon:unifile", version.ref = "unifile" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenModel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
xmlutil-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xmlutil" } xmlutil-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xmlutil" }
@@ -203,7 +204,7 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
[bundles] [bundles]
androidx-lifecycle = ["androidx-lifecycle-common", "androidx-lifecycle-process", "androidx-lifecycle-runtime"] androidx-lifecycle = ["androidx-lifecycle-common", "androidx-lifecycle-process", "androidx-lifecycle-runtime", "androidx-lifecycle-viewmodel", "androidx-lifecycle-viewmodelCompose"]
coil = ["coil-core", "coil-gif", "coil-compose", "coil-network-okhttp"] coil = ["coil-core", "coil-gif", "coil-compose", "coil-network-okhttp"]
kotlinx-coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-coroutines-guava"] kotlinx-coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-coroutines-guava"]
markdown = ["markdown-core", "markdown-coil"] markdown = ["markdown-core", "markdown-coil"]
@@ -212,4 +213,4 @@ serialization = ["kotlinx-serialization-json", "kotlinx-serialization-jsonOkio",
shizuku = ["shizuku-api", "shizuku-provider"] shizuku = ["shizuku-api", "shizuku-provider"]
sqldelight = ["sqldelight-androidxDriver", "sqldelight-async", "sqldelight-coroutines", "sqldelight-androidxPaging"] sqldelight = ["sqldelight-androidxDriver", "sqldelight-async", "sqldelight-coroutines", "sqldelight-androidxPaging"]
test = ["junit-jupiter", "kotest-assertions", "mockk"] test = ["junit-jupiter", "kotest-assertions", "mockk"]
voyager = ["voyager-navigator", "voyager-screenModel", "voyager-tabNavigator", "voyager-transitions"] voyager = ["voyager-navigator", "voyager-tabNavigator", "voyager-transitions"]
+1
View File
@@ -34,6 +34,7 @@ include(":baseline-profile")
include(":core-metadata") include(":core-metadata")
include(":core:archive") include(":core:archive")
include(":core:common") include(":core:common")
include(":core:viewmodel")
include(":data") include(":data")
include(":domain") include(":domain")
include(":i18n") include(":i18n")