Migrate to AndroidX ViewModel from Voyager ScreenModel (#3594)

This commit is contained in:
AntsyLich
2026-07-15 21:19:11 +06:00
committed by GitHub
parent ef1c529672
commit c3b99aea07
91 changed files with 1503 additions and 1091 deletions
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
) {
@@ -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,
@@ -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(
@@ -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<HistoryScreenModel.State> {
class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.State> {
private val multiPage = HistoryScreenModel.State(
private val multiPage = HistoryViewModel.State(
searchQuery = null,
list =
listOf(HistoryUiModelExamples.headerToday)
@@ -26,7 +26,7 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
dialog = null,
)
private val shortRecent = HistoryScreenModel.State(
private val shortRecent = HistoryViewModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerToday,
@@ -35,7 +35,7 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
dialog = null,
)
private val shortFuture = HistoryScreenModel.State(
private val shortFuture = HistoryViewModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerTomorrow,
@@ -44,23 +44,23 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
dialog = null,
)
private val empty = HistoryScreenModel.State(
private val empty = HistoryViewModel.State(
searchQuery = null,
list = listOf(),
dialog = null,
)
private val loadingWithSearchQuery = HistoryScreenModel.State(
private val loadingWithSearchQuery = HistoryViewModel.State(
searchQuery = "Example Search Query",
)
private val loading = HistoryScreenModel.State(
private val loading = HistoryViewModel.State(
searchQuery = null,
list = null,
dialog = null,
)
override val values: Sequence<HistoryScreenModel.State> = sequenceOf(
override val values: Sequence<HistoryViewModel.State> = sequenceOf(
multiPage,
shortRecent,
shortFuture,
@@ -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,
)
}
@@ -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,
@@ -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<ClearDatabaseViewModel>()
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<ClearDatabaseScreenModel.State>(State.Loading) {
private class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(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 ->
@@ -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<ExtensionStoresViewModel>()
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,
@@ -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>(ExtensionStoreScreenState.Loading) {
) : StateViewModel<ExtensionStoreScreenState>(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()
}
@@ -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<CreateBackupViewModel>()
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<BackupOptions.Entry>,
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<CreateBackupScreenModel.State>(State()) {
private class CreateBackupViewModel : StateViewModel<CreateBackupViewModel.State>(State()) {
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
mutableState.update {
@@ -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<RestoreBackupViewModel>(
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<RestoreBackupScreenModel.State>(State()) {
) : StateViewModel<RestoreBackupViewModel.State>(State()) {
companion object {
val URI_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
RestoreBackupViewModel(
context = Injekt.get<Application>(),
uri = get(URI_KEY)!!,
)
}
}
}
init {
validate(uri.toUri())
@@ -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<WorkerInfoViewModel>(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<Application>(),
)
}
}
}
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<WorkInfo>) = buildString {
if (list.isEmpty()) {
@@ -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()
},
@@ -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()
},
@@ -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,
)
}
@@ -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(
@@ -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)
}
}
}
@@ -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,
)
}
@@ -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
@@ -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,
@@ -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?
}
@@ -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<ExtensionsViewModel>()
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()
@@ -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<ExtensionFilterViewModel>()
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)
@@ -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>(ExtensionFilterState.Loading) {
) : StateViewModel<ExtensionFilterState>(ExtensionFilterState.Loading) {
private val _events: Channel<ExtensionFilterEvent> = Channel()
val events: Flow<ExtensionFilterEvent> = _events.receiveAsFlow()
init {
screenModelScope.launch {
viewModelScope.launch {
combine(
getExtensionLanguages.subscribe(),
preferences.enabledLanguages.changes(),
@@ -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<Extension?>(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
@@ -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<ExtensionsScreenModel.State>(State()) {
) : StateViewModel<ExtensionsViewModel.State>(State()) {
private val currentDownloads = MutableStateFlow<Map<String, InstallStep>>(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<Extension.Installed>()
@@ -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)
}
}
@@ -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<ExtensionDetailsViewModel>(
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()
}
@@ -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<ExtensionDetailsScreenModel.State>(State()) {
) : StateViewModel<ExtensionDetailsViewModel.State>(State()) {
companion object {
val PKG_NAME_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
ExtensionDetailsViewModel(
pkgName = get(PKG_NAME_KEY)!!,
context = Injekt.get<Application>(),
)
}
}
}
private val _events: Channel<ExtensionDetailsEvent> = Channel()
val events: Flow<ExtensionDetailsEvent> = _events.receiveAsFlow()
init {
screenModelScope.launch {
viewModelScope.launch {
launch {
extensionManager.installedExtensionsFlow
.map { it.firstOrNull { extension -> extension.pkgName == pkgName } }
@@ -19,7 +19,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.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<MigrateMangaViewModel>(
factory = MigrateMangaViewModel.Factory,
extras = CreationExtras {
set(MigrateMangaViewModel.SOURCE_ID_KEY, sourceId)
},
)
val state by screenModel.state.collectAsState()
val state by viewModel.state.collectAsState()
if (state.isLoading) {
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,
) {
@@ -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<MigrateMangaScreenModel.State>(State()) {
) : StateViewModel<MigrateMangaViewModel.State>(State()) {
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateMangaViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
)
}
}
}
private val _events: Channel<MigrationMangaEvent> = Channel()
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
init {
screenModelScope.launch {
viewModelScope.launch {
mutableState.update { state ->
state.copy(source = sourceManager.getOrStub(sourceId))
}
@@ -3,12 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.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<MigrateSearchViewModel>(
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
@@ -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<Long>()
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(
@@ -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<BrowseSourceViewModel>(
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,
@@ -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<MigrateSourceViewModel>()
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,
)
},
)
@@ -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<MigrateSourceScreenModel.State>(State()) {
) : StateViewModel<MigrateSourceViewModel.State>(State()) {
private val _channel = Channel<Event>(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() {
@@ -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<SourcesFilterViewModel>()
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,
)
}
}
@@ -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<SourcesFilterScreenModel.State>(State.Loading) {
) : StateViewModel<SourcesFilterViewModel.State>(State.Loading) {
init {
screenModelScope.launch {
viewModelScope.launch {
combine(
getLanguagesWithSources.subscribe(),
preferences.enabledLanguages.changes(),
@@ -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<SourcesViewModel>()
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) }
}
}
@@ -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<SourcesScreenModel.State>(State()) {
) : StateViewModel<SourcesViewModel.State>(State()) {
private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow()
init {
screenModelScope.launchIO {
viewModelScope.launchIO {
getEnabledSources.subscribe()
.catch {
logcat(LogPriority.ERROR, it)
@@ -32,7 +32,8 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.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<BrowseSourceViewModel>(
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)
}
}
}
@@ -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<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) {
) : StateViewModel<BrowseSourceViewModel.State>(State(Listing.valueOf(listingQuery))) {
var displayMode by sourcePreferences.sourceDisplayMode.asState(screenModelScope)
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val LISTING_QUERY_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
BrowseSourceViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
listingQuery = get(LISTING_QUERY_KEY),
)
}
}
}
var displayMode by sourcePreferences.sourceDisplayMode.asState(viewModelScope)
val source = sourceManager.getOrStub(sourceId)
@@ -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<Long>) {
screenModelScope.launchIO {
viewModelScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
@@ -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<GlobalSearchViewModel>(
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))
},
@@ -1,25 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import eu.kanade.tachiyomi.source.Source
class GlobalSearchScreenModel(
initialQuery: String = "",
initialExtensionFilter: String? = null,
) : SearchScreenModel(State(searchQuery = initialQuery)) {
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
if (extensionFilter != null) {
// we're going to use custom extension filter instead
setSourceFilter(SourceFilter.All)
}
search()
}
}
override fun getEnabledSources(): List<Source> {
return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
}
}
@@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.tachiyomi.source.Source
class GlobalSearchViewModel(
initialQuery: String,
initialExtensionFilter: String?,
) : SearchViewModel(State(searchQuery = initialQuery)) {
companion object {
val INITIAL_QUERY_KEY = CreationExtras.Key<String>()
val INITIAL_EXTENSION_FILTER_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
GlobalSearchViewModel(
initialQuery = get(INITIAL_QUERY_KEY)!!,
initialExtensionFilter = get(INITIAL_EXTENSION_FILTER_KEY),
)
}
}
}
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
if (extensionFilter != null) {
// we're going to use custom extension filter instead
setSourceFilter(SourceFilter.All)
}
search()
}
}
override fun getEnabledSources(): List<Source> {
return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
}
}
@@ -3,10 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.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<SearchScreenModel.State>(initialState) {
) : StateViewModel<SearchViewModel.State>(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)) }
}
@@ -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<CategoryViewModel>()
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)
}
@@ -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>(CategoryScreenState.Loading) {
) : StateViewModel<CategoryScreenState>(CategoryScreenState.Loading) {
private val _events: Channel<CategoryEvent> = 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 -> {}
@@ -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<DeepLinkViewModel>(
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(
@@ -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<DeepLinkScreenModel.State>(State.Loading) {
) : StateViewModel<DeepLinkViewModel.State>(State.Loading) {
companion object {
val QUERY_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
DeepLinkViewModel(
query = get(QUERY_KEY)!!,
)
}
}
}
init {
screenModelScope.launchIO {
viewModelScope.launchIO {
val source = sourceManager.getAll()
.filterIsInstance<ResolvableSource>()
.firstOrNull { it.getUriType(query) != UriType.Unknown }
@@ -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<DownloadQueueViewModel>()
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)
},
)
}
@@ -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<DownloadHeaderItem>())
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)
}
@@ -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<HistoryViewModel>()
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())
}
}
}
@@ -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<HistoryScreenModel.State>(State()) {
) : StateViewModel<HistoryViewModel.State>(State()) {
private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
val events: Flow<Event> = _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<Long>) {
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 ->
@@ -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)
}
}
@@ -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<LibraryViewModel>()
val settingsViewModel = viewModel<LibrarySettingsViewModel>()
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() } }
}
}
@@ -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<LibraryScreenModel.State>(State()) {
) : StateViewModel<LibraryViewModel.State>(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<LibraryItem>.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<Manga>, 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<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
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<LibraryDisplayMode> {
return libraryPreferences.displayMode.asState(screenModelScope)
return libraryPreferences.displayMode.asState(viewModelScope)
}
fun getColumnsForOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
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
@@ -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<Manga?>(null) {
) : StateViewModel<Manga?>(null) {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MangaCoverViewModel(
mangaId = get(MANGA_ID_KEY)!!,
)
}
}
}
init {
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,
@@ -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<MangaViewModel>(
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<MangaCoverViewModel>(
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,
)
}
}
@@ -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<MangaScreenModel.State>(State.Loading) {
) : StateViewModel<MangaViewModel.State>(State.Loading) {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val IS_FROM_SOURCE_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
MangaViewModel(
context = Injekt.get<Application>(),
mangaId = get(MANGA_ID_KEY)!!,
isFromSource = get(IS_FROM_SOURCE_KEY)!!,
)
}
}
}
private val successState: State.Success?
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<Long>) {
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<Chapter>, 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<Chapter>, 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<Chapter>) {
screenModelScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
try {
successState?.let { state ->
downloadManager.deleteChapters(
@@ -827,7 +839,7 @@ class MangaScreenModel(
}
private fun downloadNewChapters(chapters: List<Chapter>) {
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<String>) {
screenModelScope.launchIO {
viewModelScope.launchIO {
setExcludedScanlators.await(mangaId, excludedScanlators)
}
}
@@ -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<Model>(
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>(State(manga, manga.notes)) {
) : StateViewModel<State>(State(manga, manga.notes)) {
companion object {
val MANGA_KEY = CreationExtras.Key<Manga>()
val Factory = viewModelFactory {
initializer {
Model(
manga = get(MANGA_KEY)!!,
)
}
}
}
fun updateNotes(content: String) {
if (content == state.value.notes) return
@@ -47,7 +67,7 @@ class MangaNotesScreen(
it.copy(notes = content)
}
screenModelScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
updateMangaNotes(manga.id, content)
}
}
@@ -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<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.MANGA_ID_KEY, mangaId)
set(Model.SOURCE_ID_KEY, sourceId)
},
)
val dateFormat = remember { UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat.get()) }
val 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<Model.State>(State()) {
) : StateViewModel<Model.State>(State()) {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY)!!,
sourceId = get(SOURCE_ID_KEY)!!,
)
}
}
}
init {
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<GetManga>().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<TrackerManager>().get(serviceId)!!,
)
}
val state by screenModel.state.collectAsState()
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().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<Model.State>(State(track.status)) {
) : StateViewModel<Model.State>(State(track.status)) {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
}
fun getSelections(): Map<Long, StringResource?> {
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<TrackerManager>().get(serviceId)!!,
)
}
val state by screenModel.state.collectAsState()
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().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<Model.State>(State(track.lastChapterRead.toInt())) {
) : StateViewModel<Model.State>(State(track.lastChapterRead.toInt())) {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
}
fun getRange(): Iterable<Int> {
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<TrackerManager>().get(serviceId)!!,
)
}
val state by screenModel.state.collectAsState()
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().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<Model.State>(State(tracker.displayScore(track))) {
) : StateViewModel<Model.State>(State(tracker.displayScore(track))) {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
}
fun getSelections(): List<String> {
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<TrackerManager>().get(serviceId)!!,
start = start,
)
}
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().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<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val START_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
start = get(START_KEY) as Boolean,
)
}
}
}
// In UTC
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<TrackerManager>().get(serviceId)!!,
start = start,
)
}
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().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<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val START_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
start = get(START_KEY) as Boolean,
)
}
}
}
fun getServiceName() = tracker.name
fun 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<TrackerManager>().get(serviceId)!!,
)
}
val viewModel = viewModel<Model>(
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<TrackerManager>().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<Model.State>(State()) {
) : StateViewModel<Model.State>(State()) {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val CURRENT_URL_KEY = CreationExtras.Key<String?>()
val INITIAL_QUERY_KEY = CreationExtras.Key<String>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY)!!,
currentUrl = get(CURRENT_URL_KEY),
initialQuery = get(INITIAL_QUERY_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
}
val supportsPrivateTracking = tracker.supportsPrivateTracking
@@ -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<TrackerManager>().get(serviceId)!!,
)
}
val serviceName = screenModel.getName()
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.MANGA_ID_KEY, mangaId)
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().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<Long>()
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY) as Long,
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
)
}
}
}
fun getName() = tracker.name
fun 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) }
}
}
}
@@ -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<MoreViewModel>()
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<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped)
val downloadQueueState: StateFlow<DownloadQueueState> = _downloadQueueState.asStateFlow()
init {
// Handle running/paused status change and queue progress updating
screenModelScope.launchIO {
viewModelScope.launchIO {
combine(
downloadManager.isDownloaderRunning,
downloadManager.queueState,
@@ -74,7 +74,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.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)
@@ -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<ReaderViewModel.State>,
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)
}
@@ -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<StatsViewModel>()
val state by viewModel.state.collectAsState()
Scaffold(
topBar = { scrollBehavior ->
@@ -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>(StatsScreenState.Loading) {
) : StateViewModel<StatsScreenState>(StatsScreenState.Loading) {
private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() }
init {
screenModelScope.launchIO {
viewModelScope.launchIO {
val libraryManga = getLibraryManga.await()
val distinctLibraryManga = libraryManga.fastDistinctBy { it.id }
@@ -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<TriState>) {
preference(updatesPreferences).getAndSet {
@@ -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<UpdatesViewModel>()
val settingsViewModel = viewModel<UpdatesSettingsViewModel>()
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()
}
}
}
@@ -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<UpdatesScreenModel.State>(State()) {
) : StateViewModel<UpdatesViewModel.State>(State()) {
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
val events: Flow<Event> = _events.receiveAsFlow()
val lastUpdated by libraryPreferences.lastUpdatedTimestamp.asState(screenModelScope)
val lastUpdated by libraryPreferences.lastUpdatedTimestamp.asState(viewModelScope)
// First and last selected index in list
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
private val selectedChapterIds: HashSet<Long> = 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<UpdatesItem>.applyFilters(
@@ -181,7 +181,7 @@ class UpdatesScreenModel(
fun updateLibrary(): Boolean {
val started = LibraryUpdateJob.startNow(Injekt.get<Application>())
screenModelScope.launch {
viewModelScope.launch {
_events.send(Event.LibraryUpdateTriggered(started))
}
return started
@@ -210,7 +210,7 @@ class UpdatesScreenModel(
fun downloadChapters(items: List<UpdatesItem>, 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<UpdatesItem>, 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<UpdatesItem>, 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<UpdatesItem>) {
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<UpdatesItem>) {
screenModelScope.launchNonCancellable {
viewModelScope.launchNonCancellable {
updatesItem
.groupBy { it.update.mangaId }
.entries
@@ -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
@@ -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<WebViewViewModel>(
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,
)
}
}
@@ -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>(StatsScreenState.Loading) {
) : StateViewModel<StatsScreenState>(StatsScreenState.Loading) {
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long?>()
val Factory = viewModelFactory {
initializer {
WebViewViewModel(
sourceId = get(SOURCE_ID_KEY),
)
}
}
}
var headers = emptyMap<String, String>()
@@ -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)
@@ -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<Long>) : Screen() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ScreenModel() }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<ViewModel>()
val state by viewModel.state.collectAsState()
var migrationSheetOpen by rememberSaveable { mutableStateOf(false) }
@@ -121,20 +121,20 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : 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<Long>) : 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<Long>) : 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<Long>) : 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<Long>) : 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<Long>) : Screen() {
}
}
private class ScreenModel(
private class ViewModel(
val sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) : StateScreenModel<ScreenModel.State>(State()) {
) : StateViewModel<ViewModel.State>(State()) {
private val sourcesComparator = { includedSources: List<Long> ->
compareBy<MigrationSource>(
@@ -320,7 +320,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
}
init {
screenModelScope.launchIO {
viewModelScope.launchIO {
initSources()
mutableState.update { it.copy(isLoading = false) }
}
@@ -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<MigrateDialogViewModel>()
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<MigrateDialogScreenModel.State>(State()) {
) : StateViewModel<MigrateDialogViewModel.State>(State()) {
fun init(current: Manga, target: Manga) {
val applicableFlags = buildList {
@@ -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<Long>, 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<MigrationListViewModel>(
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<Long>, 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<Long>, 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<Long>, private val ex
}
BackHandler(true) {
screenModel.showExitDialog()
viewModel.showExitDialog()
}
}
}
@@ -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<Long>,
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<MigrationListScreenModel.State>(State()) {
) : StateViewModel<MigrationListViewModel.State>(State()) {
companion object {
val MANGA_IDS_KEY = CreationExtras.Key<Collection<Long>>()
val EXTRA_SEARCH_QUERY_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
MigrationListViewModel(
mangaIds = get(MANGA_IDS_KEY)!!,
extraSearchQuery = get(EXTRA_SEARCH_QUERY_KEY),
)
}
}
}
private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery)
@@ -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()
}
@@ -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<UpcomingViewModel>()
val state by viewModel.state.collectAsState()
UpcomingScreenContent(
state = state,
setSelectedYearMonth = screenModel::setSelectedYearMonth,
setSelectedYearMonth = viewModel::setSelectedYearMonth,
onClickUpcoming = { navigator.push(MangaScreen(it.id)) },
)
}
@@ -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,
@@ -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<UpcomingScreenModel.State>(State()) {
) : StateViewModel<UpcomingViewModel.State>(State()) {
init {
screenModelScope.launch {
viewModelScope.launch {
getUpcomingManga.subscribe().collectLatest {
mutableState.update { state ->
val upcomingItems = it.toUpcomingUIModels()