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
@@ -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)