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