Replace StateViewModel with explicit backing fields (#3763)

Assisted-by: Claude:claude-opus-5
This commit is contained in:
AntsyLich
2026-08-15 01:53:06 +06:00
committed by GitHub
parent d00efa4bfc
commit b1efee47f5
21 changed files with 184 additions and 127 deletions
-1
View File
@@ -209,7 +209,6 @@ dependencies {
implementation(projects.i18n) implementation(projects.i18n)
implementation(projects.core.archive) implementation(projects.core.archive)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.viewmodel)
implementation(projects.coreMetadata) implementation(projects.coreMetadata)
implementation(projects.sourceApi) implementation(projects.sourceApi)
implementation(projects.sourceLocal) implementation(projects.sourceLocal)
@@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@@ -39,9 +40,10 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.toLong import tachiyomi.core.common.util.lang.toLong
@@ -220,7 +222,11 @@ class ClearDatabaseScreen : Screen() {
} }
} }
class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(State.Loading) { class ClearDatabaseViewModel : ViewModel() {
val state: StateFlow<ClearDatabaseViewModel.State>
field = MutableStateFlow<ClearDatabaseViewModel.State>(State.Loading)
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get() private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get()
private val database: Database = Injekt.get() private val database: Database = Injekt.get()
@@ -228,7 +234,7 @@ class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(Stat
viewModelScope.launchIO { viewModelScope.launchIO {
getSourcesWithNonLibraryManga.subscribe() getSourcesWithNonLibraryManga.subscribe()
.collectLatest { list -> .collectLatest { list ->
mutableState.update { old -> state.update { old ->
val items = list.sortedBy { it.name } val items = list.sortedBy { it.name }
when (old) { when (old) {
State.Loading -> State.Ready(items) State.Loading -> State.Ready(items)
@@ -245,7 +251,7 @@ class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(Stat
database.historyQueries.removeResettedHistory() database.historyQueries.removeResettedHistory()
} }
fun toggleSelection(source: Source) = mutableState.update { state -> fun toggleSelection(source: Source) = state.update { state ->
if (state !is State.Ready) return@update state if (state !is State.Ready) return@update state
val mutableList = state.selection.toMutableList() val mutableList = state.selection.toMutableList()
if (mutableList.contains(source.id)) { if (mutableList.contains(source.id)) {
@@ -256,17 +262,17 @@ class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(Stat
state.copy(selection = mutableList) state.copy(selection = mutableList)
} }
fun clearSelection() = mutableState.update { state -> fun clearSelection() = state.update { state ->
if (state !is State.Ready) return@update state if (state !is State.Ready) return@update state
state.copy(selection = emptyList()) state.copy(selection = emptyList())
} }
fun selectAll() = mutableState.update { state -> fun selectAll() = state.update { state ->
if (state !is State.Ready) return@update state if (state !is State.Ready) return@update state
state.copy(selection = state.items.fastMap { it.id }) state.copy(selection = state.items.fastMap { it.id })
} }
fun invertSelection() = mutableState.update { state -> fun invertSelection() = state.update { state ->
if (state !is State.Ready) return@update state if (state !is State.Ready) return@update state
state.copy( state.copy(
selection = state.items selection = state.items
@@ -275,12 +281,12 @@ class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(Stat
) )
} }
fun showConfirmation() = mutableState.update { state -> fun showConfirmation() = state.update { state ->
if (state !is State.Ready) return@update state if (state !is State.Ready) return@update state
state.copy(showConfirmation = true) state.copy(showConfirmation = true)
} }
fun hideConfirmation() = mutableState.update { state -> fun hideConfirmation() = state.update { state ->
if (state !is State.Ready) return@update state if (state !is State.Ready) return@update state
state.copy(showConfirmation = false) state.copy(showConfirmation = false)
} }
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -22,8 +23,9 @@ import eu.kanade.tachiyomi.data.backup.create.BackupCreator
import eu.kanade.tachiyomi.data.backup.create.BackupOptions import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.LazyColumnWithAction
@@ -119,10 +121,13 @@ class CreateBackupScreen : Screen() {
} }
} }
class CreateBackupViewModel : StateViewModel<CreateBackupViewModel.State>(State()) { class CreateBackupViewModel : ViewModel() {
val state: StateFlow<CreateBackupViewModel.State>
field = MutableStateFlow<CreateBackupViewModel.State>(State())
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) { fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
mutableState.update { state.update {
it.copy( it.copy(
options = setter(it.options, enabled), options = setter(it.options, enabled),
) )
@@ -20,6 +20,7 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
@@ -33,8 +34,9 @@ import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.LazyColumnWithAction
@@ -176,7 +178,10 @@ class RestoreBackupScreen(
class RestoreBackupViewModel( class RestoreBackupViewModel(
private val context: Context, private val context: Context,
private val uri: String, private val uri: String,
) : StateViewModel<RestoreBackupViewModel.State>(State()) { ) : ViewModel() {
val state: StateFlow<RestoreBackupViewModel.State>
field = MutableStateFlow<RestoreBackupViewModel.State>(State())
companion object { companion object {
val URI_KEY = CreationExtras.Key<String>() val URI_KEY = CreationExtras.Key<String>()
@@ -196,7 +201,7 @@ class RestoreBackupViewModel(
} }
fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) {
mutableState.update { state.update {
it.copy( it.copy(
options = setter(it.options, enabled), options = setter(it.options, enabled),
) )
@@ -234,7 +239,7 @@ class RestoreBackupViewModel(
} }
private fun setError(error: Any?, canRestore: Boolean) { private fun setError(error: Any?, canRestore: Boolean) {
mutableState.update { state.update {
it.copy( it.copy(
error = error, error = error,
canRestore = canRestore, canRestore = canRestore,
@@ -8,7 +8,6 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@@ -46,7 +45,7 @@ class MigrateSearchViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
val manga = getManga.await(mangaId)!! val manga = getManga.await(mangaId)!!
mutableState.update { updateState {
it.copy( it.copy(
from = manga, from = manga,
searchQuery = manga.title, searchQuery = manga.title,
@@ -6,6 +6,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
@@ -23,7 +24,9 @@ import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
@@ -31,7 +34,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -68,7 +70,10 @@ class BrowseSourceViewModel(
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(),
getIncognitoState: GetIncognitoState = Injekt.get(), getIncognitoState: GetIncognitoState = Injekt.get(),
) : StateViewModel<BrowseSourceViewModel.State>(State(Listing.valueOf(listingQuery))) { ) : ViewModel() {
val state: StateFlow<BrowseSourceViewModel.State>
field = MutableStateFlow<BrowseSourceViewModel.State>(State(Listing.valueOf(listingQuery)))
companion object { companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>() val SOURCE_ID_KEY = CreationExtras.Key<Long>()
@@ -89,7 +94,7 @@ class BrowseSourceViewModel(
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
init { init {
mutableState.update { state.update {
var query: String? = null var query: String? = null
var listing = it.listing var listing = it.listing
@@ -142,15 +147,15 @@ class BrowseSourceViewModel(
} }
fun resetFilters() { fun resetFilters() {
mutableState.update { it.copy(filters = source.getFilterList()) } state.update { it.copy(filters = source.getFilterList()) }
} }
fun setListing(listing: Listing) { fun setListing(listing: Listing) {
mutableState.update { it.copy(listing = listing, toolbarQuery = null) } state.update { it.copy(listing = listing, toolbarQuery = null) }
} }
fun setFilters(filters: FilterList) { fun setFilters(filters: FilterList) {
mutableState.update { state.update {
it.copy( it.copy(
filters = filters, filters = filters,
) )
@@ -161,7 +166,7 @@ class BrowseSourceViewModel(
val input = state.value.listing as? Listing.Search val input = state.value.listing as? Listing.Search
?: Listing.Search(query = null, filters = source.getFilterList()) ?: Listing.Search(query = null, filters = source.getFilterList())
mutableState.update { state.update {
it.copy( it.copy(
listing = input.copy( listing = input.copy(
query = query ?: input.query, query = query ?: input.query,
@@ -201,7 +206,7 @@ class BrowseSourceViewModel(
} }
} }
mutableState.update { state.update {
val listing = if (genreExists) { val listing = if (genreExists) {
Listing.Search(query = null, filters = defaultFilters) Listing.Search(query = null, filters = defaultFilters)
} else { } else {
@@ -310,11 +315,11 @@ class BrowseSourceViewModel(
} }
fun setDialog(dialog: Dialog?) { fun setDialog(dialog: Dialog?) {
mutableState.update { it.copy(dialog = dialog) } state.update { it.copy(dialog = dialog) }
} }
fun setToolbarQuery(query: String?) { fun setToolbarQuery(query: String?) {
mutableState.update { it.copy(toolbarQuery = query) } state.update { it.copy(toolbarQuery = query) }
} }
sealed class Listing(open val query: String?, open val filters: FilterList) { sealed class Listing(open val query: String?, open val filters: FilterList) {
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
@@ -11,13 +12,14 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mihon.core.viewmodel.StateViewModel
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.preference.toggle import tachiyomi.core.common.preference.toggle
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -37,7 +39,16 @@ abstract class SearchViewModel(
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
) : StateViewModel<SearchViewModel.State>(initialState) { ) : ViewModel() {
val state: StateFlow<State>
field = MutableStateFlow<State>(initialState)
// Subclasses can't touch the backing field (Kotlin forbids a visibility modifier on one),
// so state writes from them go through here.
protected fun updateState(function: (State) -> State) {
state.update(function)
}
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
private var searchJob: Job? = null private var searchJob: Job? = null
@@ -61,8 +72,8 @@ abstract class SearchViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
preferences.globalSearchFilterState.changes().collectLatest { state -> preferences.globalSearchFilterState.changes().collectLatest { onlyShowHasResults ->
mutableState.update { it.copy(onlyShowHasResults = state) } state.update { it.copy(onlyShowHasResults = onlyShowHasResults) }
} }
} }
} }
@@ -104,11 +115,11 @@ abstract class SearchViewModel(
} }
fun updateSearchQuery(query: String?) { fun updateSearchQuery(query: String?) {
mutableState.update { it.copy(searchQuery = query) } state.update { it.copy(searchQuery = query) }
} }
fun setSourceFilter(filter: SourceFilter) { fun setSourceFilter(filter: SourceFilter) {
mutableState.update { it.copy(sourceFilter = filter) } state.update { it.copy(sourceFilter = filter) }
search() search()
} }
@@ -178,7 +189,7 @@ abstract class SearchViewModel(
} }
private fun updateItems(items: Map<Source, SearchItemResult>) { private fun updateItems(items: Map<Source, SearchItemResult>) {
mutableState.update { state.update {
it.copy( it.copy(
items = items items = items
.toSortedMap(sortComparator(items)), .toSortedMap(sortComparator(items)),
@@ -193,12 +204,12 @@ abstract class SearchViewModel(
fun setMigrateDialog(currentId: Long, target: Manga) { fun setMigrateDialog(currentId: Long, target: Manga) {
viewModelScope.launchIO { viewModelScope.launchIO {
val current = getManga.await(currentId) ?: return@launchIO val current = getManga.await(currentId) ?: return@launchIO
mutableState.update { it.copy(dialog = Dialog.Migrate(target, current)) } state.update { it.copy(dialog = Dialog.Migrate(target, current)) }
} }
} }
fun clearDialog() { fun clearDialog() {
mutableState.update { it.copy(dialog = null) } state.update { it.copy(dialog = null) }
} }
@Immutable @Immutable
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.deeplink package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
@@ -9,8 +10,9 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.ResolvableSource import eu.kanade.tachiyomi.source.online.ResolvableSource
import eu.kanade.tachiyomi.source.online.UriType import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -28,7 +30,10 @@ class DeepLinkViewModel(
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(), private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
) : StateViewModel<DeepLinkViewModel.State>(State.Loading) { ) : ViewModel() {
val state: StateFlow<DeepLinkViewModel.State>
field = MutableStateFlow<DeepLinkViewModel.State>(State.Loading)
companion object { companion object {
val QUERY_KEY = CreationExtras.Key<String>() val QUERY_KEY = CreationExtras.Key<String>()
@@ -58,7 +63,7 @@ class DeepLinkViewModel(
null null
} }
mutableState.update { state.update {
if (manga == null) { if (manga == null) {
State.NoResults State.NoResults
} else { } else {
@@ -8,6 +8,7 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
@@ -41,6 +42,8 @@ import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -50,7 +53,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@@ -116,7 +118,10 @@ class MangaViewModel(
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(), private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateViewModel<MangaViewModel.State>(State.Loading) { ) : ViewModel() {
val state: StateFlow<State>
field = MutableStateFlow<State>(State.Loading)
companion object { companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>() val MANGA_ID_KEY = CreationExtras.Key<Long>()
@@ -168,7 +173,7 @@ class MangaViewModel(
* Helper function to update the UI state only if it's currently in success state * Helper function to update the UI state only if it's currently in success state
*/ */
private inline fun updateSuccessState(func: (State.Success) -> State.Success) { private inline fun updateSuccessState(func: (State.Success) -> State.Success) {
mutableState.update { state.update {
when (it) { when (it) {
State.Loading -> it State.Loading -> it
is State.Success -> func(it) is State.Success -> func(it)
@@ -228,7 +233,7 @@ class MangaViewModel(
val needRefreshChapter = chapters.isEmpty() val needRefreshChapter = chapters.isEmpty()
// Show what we have earlier // Show what we have earlier
mutableState.update { state.update {
State.Success( State.Success(
manga = manga, manga = manga,
source = Injekt.get<SourceManager>().getOrStub(manga.source), source = Injekt.get<SourceManager>().getOrStub(manga.source),
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -13,8 +14,9 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.manga.MangaNotesScreen import eu.kanade.presentation.manga.MangaNotesScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.manga.interactor.UpdateMangaNotes import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@@ -46,7 +48,10 @@ class MangaNotesScreen(
class Model( class Model(
private val manga: Manga, private val manga: Manga,
private val updateMangaNotes: UpdateMangaNotes = Injekt.get(), private val updateMangaNotes: UpdateMangaNotes = Injekt.get(),
) : StateViewModel<State>(State(manga, manga.notes)) { ) : ViewModel() {
val state: StateFlow<State>
field = MutableStateFlow<State>(State(manga, manga.notes))
companion object { companion object {
val MANGA_KEY = CreationExtras.Key<Manga>() val MANGA_KEY = CreationExtras.Key<Manga>()
@@ -63,7 +68,7 @@ class MangaNotesScreen(
fun updateNotes(content: String) { fun updateNotes(content: String) {
if (content == state.value.notes) return if (content == state.value.notes) return
mutableState.update { state.update {
it.copy(notes = content) it.copy(notes = content)
} }
@@ -59,6 +59,8 @@ import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -68,7 +70,6 @@ import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@@ -205,7 +206,10 @@ data class TrackInfoDialogHomeScreen(
private val mangaId: Long, private val mangaId: Long,
private val sourceId: Long, private val sourceId: Long,
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
) : StateViewModel<Model.State>(State()) { ) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State())
companion object { companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>() val MANGA_ID_KEY = CreationExtras.Key<Long>()
@@ -231,7 +235,7 @@ data class TrackInfoDialogHomeScreen(
.catch { logcat(LogPriority.ERROR, it) } .catch { logcat(LogPriority.ERROR, it) }
.distinctUntilChanged() .distinctUntilChanged()
.map { it.mapToTrackItem() } .map { it.mapToTrackItem() }
.collectLatest { trackItems -> mutableState.update { it.copy(trackItems = trackItems) } } .collectLatest { trackItems -> state.update { it.copy(trackItems = trackItems) } }
} }
} }
@@ -324,7 +328,10 @@ private data class TrackStatusSelectorScreen(
class Model( class Model(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
) : StateViewModel<Model.State>(State(track.status)) { ) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State(track.status))
companion object { companion object {
val TRACK_KEY = CreationExtras.Key<Track>() val TRACK_KEY = CreationExtras.Key<Track>()
@@ -345,7 +352,7 @@ private data class TrackStatusSelectorScreen(
} }
fun setSelection(selection: Long) { fun setSelection(selection: Long) {
mutableState.update { it.copy(selection = selection) } state.update { it.copy(selection = selection) }
} }
fun setStatus() { fun setStatus() {
@@ -393,7 +400,10 @@ private data class TrackChapterSelectorScreen(
class Model( class Model(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
) : StateViewModel<Model.State>(State(track.lastChapterRead.toInt())) { ) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State(track.lastChapterRead.toInt()))
companion object { companion object {
val TRACK_KEY = CreationExtras.Key<Track>() val TRACK_KEY = CreationExtras.Key<Track>()
@@ -419,7 +429,7 @@ private data class TrackChapterSelectorScreen(
} }
fun setSelection(selection: Int) { fun setSelection(selection: Int) {
mutableState.update { it.copy(selection = selection) } state.update { it.copy(selection = selection) }
} }
fun setChapter() { fun setChapter() {
@@ -467,7 +477,10 @@ private data class TrackScoreSelectorScreen(
class Model( class Model(
private val track: Track, private val track: Track,
private val tracker: Tracker, private val tracker: Tracker,
) : StateViewModel<Model.State>(State(tracker.displayScore(track))) { ) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State(tracker.displayScore(track)))
companion object { companion object {
val TRACK_KEY = CreationExtras.Key<Track>() val TRACK_KEY = CreationExtras.Key<Track>()
@@ -488,7 +501,7 @@ private data class TrackScoreSelectorScreen(
} }
fun setSelection(selection: String) { fun setSelection(selection: String) {
mutableState.update { it.copy(selection = selection) } state.update { it.copy(selection = selection) }
} }
fun setScore() { fun setScore() {
@@ -788,7 +801,10 @@ data class TrackerSearchScreen(
private val currentUrl: String?, private val currentUrl: String?,
initialQuery: String, initialQuery: String,
private val tracker: Tracker, private val tracker: Tracker,
) : StateViewModel<Model.State>(State()) { ) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State())
companion object { companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>() val MANGA_ID_KEY = CreationExtras.Key<Long>()
@@ -820,7 +836,7 @@ data class TrackerSearchScreen(
fun trackingSearch(query: String) { fun trackingSearch(query: String) {
viewModelScope.launch { viewModelScope.launch {
// To show loading state // To show loading state
mutableState.update { it.copy(queryResult = null, selected = null) } state.update { it.copy(queryResult = null, selected = null) }
val result = withIOContext { val result = withIOContext {
try { try {
@@ -830,7 +846,7 @@ data class TrackerSearchScreen(
Result.failure(e) Result.failure(e)
} }
} }
mutableState.update { oldState -> state.update { oldState ->
oldState.copy( oldState.copy(
queryResult = result, queryResult = result,
selected = result.getOrNull()?.find { it.tracking_url == currentUrl }, selected = result.getOrNull()?.find { it.tracking_url == currentUrl },
@@ -844,7 +860,7 @@ data class TrackerSearchScreen(
} }
fun updateSelection(selected: TrackSearch) { fun updateSelection(selected: TrackSearch) {
mutableState.update { it.copy(selected = selected) } state.update { it.copy(selected = selected) }
} }
@Immutable @Immutable
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.more
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
@@ -17,10 +18,11 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -32,7 +34,10 @@ class NewUpdateScreenModel(
private val downloadLink: String, private val downloadLink: String,
private val context: Context = Injekt.get(), private val context: Context = Injekt.get(),
private val network: NetworkHelper = Injekt.get(), private val network: NetworkHelper = Injekt.get(),
) : StateViewModel<NewUpdateScreenModel.State>(State(changelogInfo = changelogInfo)) { ) : ViewModel() {
val state: StateFlow<NewUpdateScreenModel.State>
field = MutableStateFlow<NewUpdateScreenModel.State>(State(changelogInfo = changelogInfo))
private val apkFile: File private val apkFile: File
get() = File(context.externalCacheDir, "update.apk") get() = File(context.externalCacheDir, "update.apk")
@@ -43,16 +48,16 @@ class NewUpdateScreenModel(
if (downloadJob?.isActive == true) return if (downloadJob?.isActive == true) return
downloadJob = viewModelScope.launch { downloadJob = viewModelScope.launch {
mutableState.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) } state.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) }
try { try {
withIOContext { downloadApk() } withIOContext { downloadApk() }
mutableState.update { it.copy(downloadProgress = 100, stage = Stage.Downloaded) } state.update { it.copy(downloadProgress = 100, stage = Stage.Downloaded) }
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
apkFile.delete() apkFile.delete()
mutableState.update { it.copy(stage = Stage.Failed) } state.update { it.copy(stage = Stage.Failed) }
} }
} }
} }
@@ -71,7 +76,7 @@ class NewUpdateScreenModel(
if (progress > savedProgress && currentTime - 200 > lastTick) { if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress savedProgress = progress
lastTick = currentTime lastTick = currentTime
mutableState.update { it.copy(downloadProgress = progress) } state.update { it.copy(downloadProgress = progress) }
} }
} }
} }
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.stats
import androidx.compose.ui.util.fastDistinctBy import androidx.compose.ui.util.fastDistinctBy
import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastFilter
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import eu.kanade.core.util.fastCountNot import eu.kanade.core.util.fastCountNot
import eu.kanade.presentation.more.stats.StatsScreenState import eu.kanade.presentation.more.stats.StatsScreenState
@@ -9,8 +10,9 @@ import eu.kanade.presentation.more.stats.data.StatsData
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.history.interactor.GetTotalReadDuration import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
@@ -32,7 +34,10 @@ class StatsViewModel(
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
private val preferences: LibraryPreferences = Injekt.get(), private val preferences: LibraryPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(),
) : StateViewModel<StatsScreenState>(StatsScreenState.Loading) { ) : ViewModel() {
val state: StateFlow<StatsScreenState>
field = MutableStateFlow<StatsScreenState>(StatsScreenState.Loading)
private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() } private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() }
@@ -73,7 +78,7 @@ class StatsViewModel(
trackerCount = loggedInTrackers.size, trackerCount = loggedInTrackers.size,
) )
mutableState.update { state.update {
StatsScreenState.Success( StatsScreenState.Success(
overview = overviewStatData, overview = overviewStatData,
titles = titlesStatData, titles = titlesStatData,
@@ -2,17 +2,16 @@ package eu.kanade.tachiyomi.ui.webview
import android.content.Context import android.content.Context
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.presentation.more.stats.StatsScreenState
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@@ -23,7 +22,7 @@ class WebViewViewModel(
val sourceId: Long?, val sourceId: Long?,
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val network: NetworkHelper = Injekt.get(), private val network: NetworkHelper = Injekt.get(),
) : StateViewModel<StatsScreenState>(StatsScreenState.Loading) { ) : ViewModel() {
companion object { companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long?>() val SOURCE_ID_KEY = CreationExtras.Key<Long?>()
@@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachIndexed
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@@ -48,8 +49,9 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import mihon.feature.migration.list.MigrationListScreen import mihon.feature.migration.list.MigrationListScreen
import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableCollectionItemScope
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
@@ -309,7 +311,10 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
class Model( class Model(
val sourcePreferences: SourcePreferences = Injekt.get(), val sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
) : StateViewModel<Model.State>(State()) { ) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State())
private val sourcesComparator = { includedSources: List<Long> -> private val sourcesComparator = { includedSources: List<Long> ->
compareBy<MigrationSource>( compareBy<MigrationSource>(
@@ -322,12 +327,12 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
init { init {
viewModelScope.launchIO { viewModelScope.launchIO {
initSources() initSources()
mutableState.update { it.copy(isLoading = false) } state.update { it.copy(isLoading = false) }
} }
} }
private fun updateSources(action: (List<MigrationSource>) -> List<MigrationSource>) { private fun updateSources(action: (List<MigrationSource>) -> List<MigrationSource>) {
mutableState.update { state -> state.update { state ->
val updatedSources = action(state.sources) val updatedSources = action(state.sources)
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id } val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources))) state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
@@ -364,7 +369,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
} }
.toList() .toList()
mutableState.update { state -> state.update { state ->
state.copy(sources = sources.sortedWith(sourcesComparator(includedSources))) state.copy(sources = sources.sortedWith(sourcesComparator(includedSources)))
} }
} }
@@ -18,14 +18,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import mihon.domain.migration.models.MigrationFlag import mihon.domain.migration.models.MigrationFlag
import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.feature.common.utils.getLabel import mihon.feature.common.utils.getLabel
@@ -128,7 +130,10 @@ class MigrateDialogViewModel(
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val migrateManga: MigrateMangaUseCase = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(),
) : StateViewModel<MigrateDialogViewModel.State>(State()) { ) : ViewModel() {
val state: StateFlow<MigrateDialogViewModel.State>
field = MutableStateFlow<MigrateDialogViewModel.State>(State())
fun init(current: Manga, target: Manga) { fun init(current: Manga, target: Manga) {
val applicableFlags = buildList { val applicableFlags = buildList {
@@ -144,7 +149,7 @@ class MigrateDialogViewModel(
} }
} }
val selectedFlags = sourcePreference.migrationFlags.get() val selectedFlags = sourcePreference.migrationFlags.get()
mutableState.update { state.update {
State( State(
current = current, current = current,
target = target, target = target,
@@ -155,7 +160,7 @@ class MigrateDialogViewModel(
} }
fun toggleSelection(flag: MigrationFlag) { fun toggleSelection(flag: MigrationFlag) {
mutableState.update { state.update {
val selectedFlags = it.selectedFlags.toMutableSet() val selectedFlags = it.selectedFlags.toMutableSet()
.apply { if (contains(flag)) remove(flag) else add(flag) } .apply { if (contains(flag)) remove(flag) else add(flag) }
.toSet() .toSet()
@@ -164,13 +169,13 @@ class MigrateDialogViewModel(
} }
suspend fun migrateManga(replace: Boolean) { suspend fun migrateManga(replace: Boolean) {
val state = state.value val currentState = state.value
val current = state.current ?: return val current = currentState.current ?: return
val target = state.target ?: return val target = currentState.target ?: return
sourcePreference.migrationFlags.set(state.selectedFlags) sourcePreference.migrationFlags.set(currentState.selectedFlags)
mutableState.update { it.copy(isMigrating = true) } state.update { it.copy(isMigrating = true) }
migrateManga(current, target, replace) migrateManga(current, target, replace)
mutableState.update { it.copy(isMigrating = false, isMigrated = true) } state.update { it.copy(isMigrating = false, isMigrated = true) }
} }
data class State( data class State(
@@ -1,6 +1,7 @@
package mihon.feature.migration.list package mihon.feature.migration.list
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
@@ -16,13 +17,14 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.domain.source.interactor.UpdateMangaFromRemote
import mihon.feature.migration.list.models.MigratingManga import mihon.feature.migration.list.models.MigratingManga
@@ -49,7 +51,10 @@ class MigrationListViewModel(
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val migrateManga: MigrateMangaUseCase = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
) : StateViewModel<MigrationListViewModel.State>(State()) { ) : ViewModel() {
val state: StateFlow<MigrationListViewModel.State>
field = MutableStateFlow<MigrationListViewModel.State>(State())
companion object { companion object {
val MANGA_IDS_KEY = CreationExtras.Key<Collection<Long>>() val MANGA_IDS_KEY = CreationExtras.Key<Collection<Long>>()
@@ -97,7 +102,7 @@ class MigrationListViewModel(
} }
.awaitAll() .awaitAll()
.filterNotNull() .filterNotNull()
mutableState.update { it.copy(items = manga) } state.update { it.copy(items = manga) }
runMigrations(manga) runMigrations(manga)
} }
} }
@@ -215,7 +220,7 @@ class MigrationListViewModel(
} }
private suspend fun updateMigrationProgress() { private suspend fun updateMigrationProgress() {
mutableState.update { state -> state.update { state ->
state.copy( state.copy(
finishedCount = items.count { it.searchResult.value != SearchResult.Searching }, finishedCount = items.count { it.searchResult.value != SearchResult.Searching },
migrationComplete = migrationComplete(), migrationComplete = migrationComplete(),
@@ -265,7 +270,7 @@ class MigrationListViewModel(
private fun migrateMangas(replace: Boolean) { private fun migrateMangas(replace: Boolean) {
migrateJob = viewModelScope.launchIO { migrateJob = viewModelScope.launchIO {
mutableState.update { it.copy(dialog = Dialog.Progress(0f)) } state.update { it.copy(dialog = Dialog.Progress(0f)) }
val items = items val items = items
try { try {
items.forEachIndexed { index, manga -> items.forEachIndexed { index, manga ->
@@ -285,14 +290,14 @@ class MigrationListViewModel(
if (e is CancellationException) throw e if (e is CancellationException) throw e
logcat(LogPriority.WARN, throwable = e) logcat(LogPriority.WARN, throwable = e)
} }
mutableState.update { state.update {
it.copy(dialog = Dialog.Progress((index.toFloat() / items.size).coerceAtMost(1f))) it.copy(dialog = Dialog.Progress((index.toFloat() / items.size).coerceAtMost(1f)))
} }
} }
navigateBack() navigateBack()
} finally { } finally {
mutableState.update { it.copy(dialog = null) } state.update { it.copy(dialog = null) }
migrateJob = null migrateJob = null
} }
} }
@@ -327,7 +332,7 @@ class MigrationListViewModel(
} }
private fun removeManga(item: MigratingManga) { private fun removeManga(item: MigratingManga) {
mutableState.update { it.copy(items = items.toMutableList().apply { remove(item) }) } state.update { it.copy(items = items.toMutableList().apply { remove(item) }) }
} }
override fun onCleared() { override fun onCleared() {
@@ -337,7 +342,7 @@ class MigrationListViewModel(
} }
fun showMigrateDialog(copy: Boolean) { fun showMigrateDialog(copy: Boolean) {
mutableState.update { state -> state.update { state ->
state.copy( state.copy(
dialog = Dialog.Migrate( dialog = Dialog.Migrate(
copy = copy, copy = copy,
@@ -349,13 +354,13 @@ class MigrationListViewModel(
} }
fun showExitDialog() { fun showExitDialog() {
mutableState.update { state.update {
it.copy(dialog = Dialog.Exit) it.copy(dialog = Dialog.Exit)
} }
} }
fun dismissDialog() { fun dismissDialog() {
mutableState.update { it.copy(dialog = null) } state.update { it.copy(dialog = null) }
} }
data class ChapterInfo( data class ChapterInfo(
-14
View File
@@ -1,14 +0,0 @@
plugins {
alias(mihonx.plugins.android.library)
alias(mihonx.plugins.spotless)
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "mihon.core.viewmodel"
}
dependencies {
implementation(libs.androidx.lifecycle.viewmodel)
}
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
@@ -1,11 +0,0 @@
package mihon.core.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
abstract class StateViewModel<S>(initialState: S) : ViewModel() {
protected val mutableState: MutableStateFlow<S> = MutableStateFlow(initialState)
val state: StateFlow<S> = mutableState.asStateFlow()
}
-1
View File
@@ -34,7 +34,6 @@ include(":baseline-profile")
include(":core-metadata") include(":core-metadata")
include(":core:archive") include(":core:archive")
include(":core:common") include(":core:common")
include(":core:viewmodel")
include(":data") include(":data")
include(":domain") include(":domain")
include(":i18n") include(":i18n")