diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7412ff043..c51e90dd8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -209,7 +209,6 @@ dependencies { implementation(projects.i18n) implementation(projects.core.archive) implementation(projects.core.common) - implementation(projects.core.viewmodel) implementation(projects.coreMetadata) implementation(projects.sourceApi) implementation(projects.sourceLocal) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt index 439b656d7..5db2acb7b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel 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.util.Screen 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.update -import mihon.core.viewmodel.StateViewModel import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.toLong @@ -220,7 +222,11 @@ class ClearDatabaseScreen : Screen() { } } -class ClearDatabaseViewModel : StateViewModel(State.Loading) { +class ClearDatabaseViewModel : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State.Loading) + private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get() private val database: Database = Injekt.get() @@ -228,7 +234,7 @@ class ClearDatabaseViewModel : StateViewModel(Stat viewModelScope.launchIO { getSourcesWithNonLibraryManga.subscribe() .collectLatest { list -> - mutableState.update { old -> + state.update { old -> val items = list.sortedBy { it.name } when (old) { State.Loading -> State.Ready(items) @@ -245,7 +251,7 @@ class ClearDatabaseViewModel : StateViewModel(Stat 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 val mutableList = state.selection.toMutableList() if (mutableList.contains(source.id)) { @@ -256,17 +262,17 @@ class ClearDatabaseViewModel : StateViewModel(Stat state.copy(selection = mutableList) } - fun clearSelection() = mutableState.update { state -> + fun clearSelection() = state.update { state -> if (state !is State.Ready) return@update state state.copy(selection = emptyList()) } - fun selectAll() = mutableState.update { state -> + fun selectAll() = state.update { state -> if (state !is State.Ready) return@update state 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 state.copy( selection = state.items @@ -275,12 +281,12 @@ class ClearDatabaseViewModel : StateViewModel(Stat ) } - fun showConfirmation() = mutableState.update { state -> + fun showConfirmation() = state.update { state -> if (state !is State.Ready) return@update state state.copy(showConfirmation = true) } - fun hideConfirmation() = mutableState.update { state -> + fun hideConfirmation() = state.update { state -> if (state !is State.Ready) return@update state state.copy(showConfirmation = false) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index eaf96a3d5..de8167d24 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import cafe.adriel.voyager.navigator.LocalNavigator 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.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import mihon.core.viewmodel.StateViewModel import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LazyColumnWithAction @@ -119,10 +121,13 @@ class CreateBackupScreen : Screen() { } } -class CreateBackupViewModel : StateViewModel(State()) { +class CreateBackupViewModel : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State()) fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) { - mutableState.update { + state.update { it.copy( options = setter(it.options, enabled), ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index a0a05eec7..734be7c9b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.core.net.toUri +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.compose.viewModel 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.RestoreOptions import eu.kanade.tachiyomi.util.system.DeviceUtil +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import mihon.core.viewmodel.StateViewModel import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LazyColumnWithAction @@ -176,7 +178,10 @@ class RestoreBackupScreen( class RestoreBackupViewModel( private val context: Context, private val uri: String, -) : StateViewModel(State()) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State()) companion object { val URI_KEY = CreationExtras.Key() @@ -196,7 +201,7 @@ class RestoreBackupViewModel( } fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { - mutableState.update { + state.update { it.copy( options = setter(it.options, enabled), ) @@ -234,7 +239,7 @@ class RestoreBackupViewModel( } private fun setError(error: Any?, canRestore: Boolean) { - mutableState.update { + state.update { it.copy( error = error, canRestore = canRestore, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchViewModel.kt index 4c9b65506..33c3181d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchViewModel.kt @@ -8,7 +8,6 @@ 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.SearchViewModel -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.source.service.SourceManager @@ -46,7 +45,7 @@ class MigrateSearchViewModel( init { viewModelScope.launch { val manga = getManga.await(mangaId)!! - mutableState.update { + updateState { it.copy( from = manga, searchQuery = manga.title, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt index bfbf9865c..bb0616bbd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras 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.source.model.FilterList import eu.kanade.tachiyomi.util.removeCovers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.firstOrNull @@ -31,7 +34,6 @@ 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 @@ -68,7 +70,10 @@ class BrowseSourceViewModel( private val updateManga: UpdateManga = Injekt.get(), private val addTracks: AddTracks = Injekt.get(), getIncognitoState: GetIncognitoState = Injekt.get(), -) : StateViewModel(State(Listing.valueOf(listingQuery))) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State(Listing.valueOf(listingQuery))) companion object { val SOURCE_ID_KEY = CreationExtras.Key() @@ -89,7 +94,7 @@ class BrowseSourceViewModel( val source = sourceManager.getOrStub(sourceId) init { - mutableState.update { + state.update { var query: String? = null var listing = it.listing @@ -142,15 +147,15 @@ class BrowseSourceViewModel( } fun resetFilters() { - mutableState.update { it.copy(filters = source.getFilterList()) } + state.update { it.copy(filters = source.getFilterList()) } } fun setListing(listing: Listing) { - mutableState.update { it.copy(listing = listing, toolbarQuery = null) } + state.update { it.copy(listing = listing, toolbarQuery = null) } } fun setFilters(filters: FilterList) { - mutableState.update { + state.update { it.copy( filters = filters, ) @@ -161,7 +166,7 @@ class BrowseSourceViewModel( val input = state.value.listing as? Listing.Search ?: Listing.Search(query = null, filters = source.getFilterList()) - mutableState.update { + state.update { it.copy( listing = input.copy( query = query ?: input.query, @@ -201,7 +206,7 @@ class BrowseSourceViewModel( } } - mutableState.update { + state.update { val listing = if (genreExists) { Listing.Search(query = null, filters = defaultFilters) } else { @@ -310,11 +315,11 @@ class BrowseSourceViewModel( } fun setDialog(dialog: Dialog?) { - mutableState.update { it.copy(dialog = dialog) } + state.update { it.copy(dialog = dialog) } } 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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchViewModel.kt index 0ea99dab2..06783ab5b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchViewModel.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.produceState +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.ExtensionManager @@ -11,13 +12,14 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull 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 @@ -37,7 +39,16 @@ abstract class SearchViewModel( private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val getManga: GetManga = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(), -) : StateViewModel(initialState) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(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 var searchJob: Job? = null @@ -61,8 +72,8 @@ abstract class SearchViewModel( init { viewModelScope.launch { - preferences.globalSearchFilterState.changes().collectLatest { state -> - mutableState.update { it.copy(onlyShowHasResults = state) } + preferences.globalSearchFilterState.changes().collectLatest { onlyShowHasResults -> + state.update { it.copy(onlyShowHasResults = onlyShowHasResults) } } } } @@ -104,11 +115,11 @@ abstract class SearchViewModel( } fun updateSearchQuery(query: String?) { - mutableState.update { it.copy(searchQuery = query) } + state.update { it.copy(searchQuery = query) } } fun setSourceFilter(filter: SourceFilter) { - mutableState.update { it.copy(sourceFilter = filter) } + state.update { it.copy(sourceFilter = filter) } search() } @@ -178,7 +189,7 @@ abstract class SearchViewModel( } private fun updateItems(items: Map) { - mutableState.update { + state.update { it.copy( items = items .toSortedMap(sortComparator(items)), @@ -193,12 +204,12 @@ abstract class SearchViewModel( fun setMigrateDialog(currentId: Long, target: Manga) { viewModelScope.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() { - mutableState.update { it.copy(dialog = null) } + state.update { it.copy(dialog = null) } } @Immutable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkViewModel.kt index 600cef1ab..bb243ac47 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkViewModel.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.deeplink import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras 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.online.ResolvableSource import eu.kanade.tachiyomi.source.online.UriType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow 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 @@ -28,7 +30,10 @@ class DeepLinkViewModel( private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), -) : StateViewModel(State.Loading) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State.Loading) companion object { val QUERY_KEY = CreationExtras.Key() @@ -58,7 +63,7 @@ class DeepLinkViewModel( null } - mutableState.update { + state.update { if (manga == null) { State.NoResults } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt index be45bea9e..f61daa2ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.ui.util.fastAny +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras 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.system.toast import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -50,7 +53,6 @@ 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 @@ -116,7 +118,10 @@ class MangaViewModel( private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), -) : StateViewModel(State.Loading) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State.Loading) companion object { val MANGA_ID_KEY = CreationExtras.Key() @@ -168,7 +173,7 @@ class MangaViewModel( * Helper function to update the UI state only if it's currently in success state */ private inline fun updateSuccessState(func: (State.Success) -> State.Success) { - mutableState.update { + state.update { when (it) { State.Loading -> it is State.Success -> func(it) @@ -228,7 +233,7 @@ class MangaViewModel( val needRefreshChapter = chapters.isEmpty() // Show what we have earlier - mutableState.update { + state.update { State.Success( manga = manga, source = Injekt.get().getOrStub(manga.source), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt index b1d3b75c4..b113eeda2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.compose.viewModel @@ -13,8 +14,9 @@ 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.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow 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 @@ -46,7 +48,10 @@ class MangaNotesScreen( class Model( private val manga: Manga, private val updateMangaNotes: UpdateMangaNotes = Injekt.get(), - ) : StateViewModel(State(manga, manga.notes)) { + ) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State(manga, manga.notes)) companion object { val MANGA_KEY = CreationExtras.Key() @@ -63,7 +68,7 @@ class MangaNotesScreen( fun updateNotes(content: String) { if (content == state.value.notes) return - mutableState.update { + state.update { it.copy(notes = content) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index 930cd57e3..9b1ed955f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -59,6 +59,8 @@ import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.openInBrowser 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.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged @@ -68,7 +70,6 @@ import kotlinx.coroutines.launch import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime 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 @@ -205,7 +206,10 @@ data class TrackInfoDialogHomeScreen( private val mangaId: Long, private val sourceId: Long, private val getTracks: GetTracks = Injekt.get(), - ) : StateViewModel(State()) { + ) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State()) companion object { val MANGA_ID_KEY = CreationExtras.Key() @@ -231,7 +235,7 @@ data class TrackInfoDialogHomeScreen( .catch { logcat(LogPriority.ERROR, it) } .distinctUntilChanged() .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( private val track: Track, private val tracker: Tracker, - ) : StateViewModel(State(track.status)) { + ) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State(track.status)) companion object { val TRACK_KEY = CreationExtras.Key() @@ -345,7 +352,7 @@ private data class TrackStatusSelectorScreen( } fun setSelection(selection: Long) { - mutableState.update { it.copy(selection = selection) } + state.update { it.copy(selection = selection) } } fun setStatus() { @@ -393,7 +400,10 @@ private data class TrackChapterSelectorScreen( class Model( private val track: Track, private val tracker: Tracker, - ) : StateViewModel(State(track.lastChapterRead.toInt())) { + ) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State(track.lastChapterRead.toInt())) companion object { val TRACK_KEY = CreationExtras.Key() @@ -419,7 +429,7 @@ private data class TrackChapterSelectorScreen( } fun setSelection(selection: Int) { - mutableState.update { it.copy(selection = selection) } + state.update { it.copy(selection = selection) } } fun setChapter() { @@ -467,7 +477,10 @@ private data class TrackScoreSelectorScreen( class Model( private val track: Track, private val tracker: Tracker, - ) : StateViewModel(State(tracker.displayScore(track))) { + ) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State(tracker.displayScore(track))) companion object { val TRACK_KEY = CreationExtras.Key() @@ -488,7 +501,7 @@ private data class TrackScoreSelectorScreen( } fun setSelection(selection: String) { - mutableState.update { it.copy(selection = selection) } + state.update { it.copy(selection = selection) } } fun setScore() { @@ -788,7 +801,10 @@ data class TrackerSearchScreen( private val currentUrl: String?, initialQuery: String, private val tracker: Tracker, - ) : StateViewModel(State()) { + ) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State()) companion object { val MANGA_ID_KEY = CreationExtras.Key() @@ -820,7 +836,7 @@ data class TrackerSearchScreen( fun trackingSearch(query: String) { viewModelScope.launch { // To show loading state - mutableState.update { it.copy(queryResult = null, selected = null) } + state.update { it.copy(queryResult = null, selected = null) } val result = withIOContext { try { @@ -830,7 +846,7 @@ data class TrackerSearchScreen( Result.failure(e) } } - mutableState.update { oldState -> + state.update { oldState -> oldState.copy( queryResult = result, selected = result.getOrNull()?.find { it.tracking_url == currentUrl }, @@ -844,7 +860,7 @@ data class TrackerSearchScreen( } fun updateSelection(selected: TrackSearch) { - mutableState.update { it.copy(selected = selected) } + state.update { it.copy(selected = selected) } } @Immutable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreenModel.kt index 580e480ea..281f3b5fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreenModel.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.more import android.content.Context import android.content.Intent import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.initializer @@ -17,10 +18,11 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.saveTo import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority -import mihon.core.viewmodel.StateViewModel import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.Injekt @@ -32,7 +34,10 @@ class NewUpdateScreenModel( private val downloadLink: String, private val context: Context = Injekt.get(), private val network: NetworkHelper = Injekt.get(), -) : StateViewModel(State(changelogInfo = changelogInfo)) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State(changelogInfo = changelogInfo)) private val apkFile: File get() = File(context.externalCacheDir, "update.apk") @@ -43,16 +48,16 @@ class NewUpdateScreenModel( if (downloadJob?.isActive == true) return downloadJob = viewModelScope.launch { - mutableState.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) } + state.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) } try { withIOContext { downloadApk() } - mutableState.update { it.copy(downloadProgress = 100, stage = Stage.Downloaded) } + state.update { it.copy(downloadProgress = 100, stage = Stage.Downloaded) } } catch (e: CancellationException) { throw e } catch (e: Exception) { logcat(LogPriority.ERROR, e) 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) { savedProgress = progress lastTick = currentTime - mutableState.update { it.copy(downloadProgress = progress) } + state.update { it.copy(downloadProgress = progress) } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsViewModel.kt index 56b3eee1a..94df2ea5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsViewModel.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.stats import androidx.compose.ui.util.fastDistinctBy import androidx.compose.ui.util.fastFilter +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import eu.kanade.core.util.fastCountNot 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.track.TrackerManager import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow 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 @@ -32,7 +34,10 @@ class StatsViewModel( private val getTracks: GetTracks = Injekt.get(), private val preferences: LibraryPreferences = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), -) : StateViewModel(StatsScreenState.Loading) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(StatsScreenState.Loading) private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() } @@ -73,7 +78,7 @@ class StatsViewModel( trackerCount = loggedInTrackers.size, ) - mutableState.update { + state.update { StatsScreenState.Success( overview = overviewStatData, titles = titlesStatData, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt index f9c00b213..47e71940d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt @@ -2,17 +2,16 @@ package eu.kanade.tachiyomi.ui.webview import android.content.Context import androidx.core.net.toUri +import androidx.lifecycle.ViewModel 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 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 @@ -23,7 +22,7 @@ class WebViewViewModel( val sourceId: Long?, private val sourceManager: SourceManager = Injekt.get(), private val network: NetworkHelper = Injekt.get(), -) : StateViewModel(StatsScreenState.Loading) { +) : ViewModel() { companion object { val SOURCE_ID_KEY = CreationExtras.Key() diff --git a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt index 33410fef8..abca9a40d 100644 --- a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt +++ b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel 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.ui.browse.migration.search.MigrateSearchScreen import eu.kanade.tachiyomi.util.system.LocaleHelper +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import mihon.core.viewmodel.StateViewModel import mihon.feature.migration.list.MigrationListScreen import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableItem @@ -309,7 +311,10 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { class Model( val sourcePreferences: SourcePreferences = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), - ) : StateViewModel(State()) { + ) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State()) private val sourcesComparator = { includedSources: List -> compareBy( @@ -322,12 +327,12 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { init { viewModelScope.launchIO { initSources() - mutableState.update { it.copy(isLoading = false) } + state.update { it.copy(isLoading = false) } } } private fun updateSources(action: (List) -> List) { - mutableState.update { state -> + state.update { state -> val updatedSources = action(state.sources) val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id } state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources))) @@ -364,7 +369,7 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { } .toList() - mutableState.update { state -> + state.update { state -> state.copy(sources = sources.sortedWith(sourcesComparator(includedSources))) } } diff --git a/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt b/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt index 2d46bcdc8..f943f3ebd 100644 --- a/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt +++ b/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt @@ -18,14 +18,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.util.fastForEach +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import cafe.adriel.voyager.core.screen.Screen import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.download.DownloadManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import mihon.core.viewmodel.StateViewModel import mihon.domain.migration.models.MigrationFlag import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.feature.common.utils.getLabel @@ -128,7 +130,10 @@ class MigrateDialogViewModel( private val coverCache: CoverCache = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(), -) : StateViewModel(State()) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State()) fun init(current: Manga, target: Manga) { val applicableFlags = buildList { @@ -144,7 +149,7 @@ class MigrateDialogViewModel( } } val selectedFlags = sourcePreference.migrationFlags.get() - mutableState.update { + state.update { State( current = current, target = target, @@ -155,7 +160,7 @@ class MigrateDialogViewModel( } fun toggleSelection(flag: MigrationFlag) { - mutableState.update { + state.update { val selectedFlags = it.selectedFlags.toMutableSet() .apply { if (contains(flag)) remove(flag) else add(flag) } .toSet() @@ -164,13 +169,13 @@ class MigrateDialogViewModel( } suspend fun migrateManga(replace: Boolean) { - val state = state.value - val current = state.current ?: return - val target = state.target ?: return - sourcePreference.migrationFlags.set(state.selectedFlags) - mutableState.update { it.copy(isMigrating = true) } + val currentState = state.value + val current = currentState.current ?: return + val target = currentState.target ?: return + sourcePreference.migrationFlags.set(currentState.selectedFlags) + state.update { it.copy(isMigrating = true) } migrateManga(current, target, replace) - mutableState.update { it.copy(isMigrating = false, isMigrated = true) } + state.update { it.copy(isMigrating = false, isMigrated = true) } } data class State( diff --git a/app/src/main/java/mihon/feature/migration/list/MigrationListViewModel.kt b/app/src/main/java/mihon/feature/migration/list/MigrationListViewModel.kt index 71338488c..25db7e733 100644 --- a/app/src/main/java/mihon/feature/migration/list/MigrationListViewModel.kt +++ b/app/src/main/java/mihon/feature/migration/list/MigrationListViewModel.kt @@ -1,6 +1,7 @@ package mihon.feature.migration.list import androidx.annotation.FloatRange +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.initializer @@ -16,13 +17,14 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority -import mihon.core.viewmodel.StateViewModel import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.source.interactor.UpdateMangaFromRemote import mihon.feature.migration.list.models.MigratingManga @@ -49,7 +51,10 @@ class MigrationListViewModel( private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(), private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(), -) : StateViewModel(State()) { +) : ViewModel() { + + val state: StateFlow + field = MutableStateFlow(State()) companion object { val MANGA_IDS_KEY = CreationExtras.Key>() @@ -97,7 +102,7 @@ class MigrationListViewModel( } .awaitAll() .filterNotNull() - mutableState.update { it.copy(items = manga) } + state.update { it.copy(items = manga) } runMigrations(manga) } } @@ -215,7 +220,7 @@ class MigrationListViewModel( } private suspend fun updateMigrationProgress() { - mutableState.update { state -> + state.update { state -> state.copy( finishedCount = items.count { it.searchResult.value != SearchResult.Searching }, migrationComplete = migrationComplete(), @@ -265,7 +270,7 @@ class MigrationListViewModel( private fun migrateMangas(replace: Boolean) { migrateJob = viewModelScope.launchIO { - mutableState.update { it.copy(dialog = Dialog.Progress(0f)) } + state.update { it.copy(dialog = Dialog.Progress(0f)) } val items = items try { items.forEachIndexed { index, manga -> @@ -285,14 +290,14 @@ class MigrationListViewModel( if (e is CancellationException) throw e logcat(LogPriority.WARN, throwable = e) } - mutableState.update { + state.update { it.copy(dialog = Dialog.Progress((index.toFloat() / items.size).coerceAtMost(1f))) } } navigateBack() } finally { - mutableState.update { it.copy(dialog = null) } + state.update { it.copy(dialog = null) } migrateJob = null } } @@ -327,7 +332,7 @@ class MigrationListViewModel( } 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() { @@ -337,7 +342,7 @@ class MigrationListViewModel( } fun showMigrateDialog(copy: Boolean) { - mutableState.update { state -> + state.update { state -> state.copy( dialog = Dialog.Migrate( copy = copy, @@ -349,13 +354,13 @@ class MigrationListViewModel( } fun showExitDialog() { - mutableState.update { + state.update { it.copy(dialog = Dialog.Exit) } } fun dismissDialog() { - mutableState.update { it.copy(dialog = null) } + state.update { it.copy(dialog = null) } } data class ChapterInfo( diff --git a/core/viewmodel/build.gradle.kts b/core/viewmodel/build.gradle.kts deleted file mode 100644 index 55e13f2ff..000000000 --- a/core/viewmodel/build.gradle.kts +++ /dev/null @@ -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) -} diff --git a/core/viewmodel/src/main/AndroidManifest.xml b/core/viewmodel/src/main/AndroidManifest.xml deleted file mode 100644 index 8072ee00d..000000000 --- a/core/viewmodel/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/core/viewmodel/src/main/kotlin/mihon/core/viewmodel/StateViewModel.kt b/core/viewmodel/src/main/kotlin/mihon/core/viewmodel/StateViewModel.kt deleted file mode 100644 index dcafea9e0..000000000 --- a/core/viewmodel/src/main/kotlin/mihon/core/viewmodel/StateViewModel.kt +++ /dev/null @@ -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(initialState: S) : ViewModel() { - protected val mutableState: MutableStateFlow = MutableStateFlow(initialState) - val state: StateFlow = mutableState.asStateFlow() -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0769ac35a..7ca344609 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,7 +34,6 @@ include(":baseline-profile") include(":core-metadata") include(":core:archive") include(":core:common") -include(":core:viewmodel") include(":data") include(":domain") include(":i18n")