diff --git a/core/src/jvmMain/kotlin/ca/gosyer/core/prefs/PreferenceStoreFactory.kt b/core/src/jvmMain/kotlin/ca/gosyer/core/prefs/PreferenceStoreFactory.kt index b289cf21..6a17355d 100644 --- a/core/src/jvmMain/kotlin/ca/gosyer/core/prefs/PreferenceStoreFactory.kt +++ b/core/src/jvmMain/kotlin/ca/gosyer/core/prefs/PreferenceStoreFactory.kt @@ -8,4 +8,4 @@ package ca.gosyer.core.prefs expect class PreferenceStoreFactory() { fun create(vararg names: String): PreferenceStore -} \ No newline at end of file +} diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 46c6f689..67ccc5c0 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -29,7 +29,8 @@ dependencies { implementation(compose.uiTooling) implementation(compose.materialIconsExtended) implementation(compose("org.jetbrains.compose.ui:ui-util")) - implementation(libs.composeRouter) + implementation(libs.voyagerCore) + implementation(libs.voyagerNavigation) implementation(libs.accompanistPager) implementation(libs.accompanistFlowLayout) implementation(libs.kamel) diff --git a/desktop/src/main/kotlin/ca/gosyer/main.kt b/desktop/src/main/kotlin/ca/gosyer/main.kt index 77a655a0..3ce8dbd4 100644 --- a/desktop/src/main/kotlin/ca/gosyer/main.kt +++ b/desktop/src/main/kotlin/ca/gosyer/main.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.configureSwingGlobalsForCompose import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType @@ -35,19 +34,16 @@ import ca.gosyer.i18n.MR import ca.gosyer.ui.AppComponent import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.theme.AppTheme -import ca.gosyer.ui.main.DebugOverlay import ca.gosyer.ui.main.MainMenu +import ca.gosyer.ui.main.components.DebugOverlay import ca.gosyer.ui.main.components.Tray +import ca.gosyer.ui.util.compose.WindowGet import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.prefs.asStateIn -import ca.gosyer.util.compose.WindowGet +import ca.gosyer.uicore.resources.stringResource import com.github.weisj.darklaf.LafManager import com.github.weisj.darklaf.theme.DarculaTheme import com.github.weisj.darklaf.theme.IntelliJTheme -import com.github.zsoltk.compose.backpress.BackPressHandler -import com.github.zsoltk.compose.backpress.LocalBackPressHandler -import com.github.zsoltk.compose.savedinstancestate.Bundle -import ca.gosyer.uicore.resources.stringResource import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow @@ -124,10 +120,6 @@ suspend fun main() { exitProcess(0) } } - - val backPressHandler = remember { BackPressHandler() } - - val rootBundle = remember { Bundle() } val windowState = rememberWindowState( size = size, position = position, @@ -158,7 +150,8 @@ suspend fun main() { if (it.type == KeyEventType.KeyUp) { when (it.key) { Key.Home -> { - backPressHandler.handle() + // backPressHandler.handle() + false } Key.F3 -> { displayDebugInfoFlow.value = !displayDebugInfoFlow.value @@ -170,29 +163,25 @@ suspend fun main() { } ) { AppTheme { - CompositionLocalProvider( - LocalBackPressHandler provides backPressHandler, - ) { - Crossfade(serverService.initialized.collectAsState().value) { initialized -> - when (initialized) { - ServerResult.STARTED, ServerResult.UNUSED -> { - Box { - MainMenu(rootBundle) - val displayDebugInfo by displayDebugInfoFlow.collectAsState() - if (displayDebugInfo) { - DebugOverlay() - } + Crossfade(serverService.initialized.collectAsState().value) { initialized -> + when (initialized) { + ServerResult.STARTED, ServerResult.UNUSED -> { + Box { + MainMenu() + val displayDebugInfo by displayDebugInfoFlow.collectAsState() + if (displayDebugInfo) { + DebugOverlay() } } - ServerResult.STARTING, ServerResult.FAILED -> { - Surface { - LoadingScreen( - initialized == ServerResult.STARTING, - errorMessage = stringResource(MR.strings.unable_to_start_server), - retryMessage = stringResource(MR.strings.action_start_anyway), - retry = serverService::startAnyway - ) - } + } + ServerResult.STARTING, ServerResult.FAILED -> { + Surface { + LoadingScreen( + initialized == ServerResult.STARTING, + errorMessage = stringResource(MR.strings.unable_to_start_server), + retryMessage = stringResource(MR.strings.action_start_anyway), + retry = serverService::startAnyway + ) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 42f97814..0c096129 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ json = "1.3.2" xmlUtil = "0.84.0" # Compose -composeRouter = "0.24.2-jetbrains-2" +voyager = "1.0.0-beta15" accompanist = "0.18.1" kamel = "0.3.0" @@ -52,7 +52,8 @@ xmlUtilCore = { module = "io.github.pdvrieze.xmlutil:core", version.ref = "xmlUt xmlUtilSerialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlUtil" } # Compose -composeRouter = { module = "ca.gosyer:compose-router", version.ref = "composeRouter" } +voyagerCore = { module = "cafe.adriel.voyager:voyager-core", version.ref = "voyager" } +voyagerNavigation = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } accompanistPager = { module = "ca.gosyer:accompanist-pager", version.ref = "accompanist" } accompanistFlowLayout = { module = "ca.gosyer:accompanist-flowlayout", version.ref = "accompanist" } kamel = { module = "com.alialbaali.kamel:kamel-image", version.ref = "kamel" } diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index df40e153..15cdfeac 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -23,6 +23,7 @@ kotlin { all { languageSettings { optIn("kotlin.RequiresOptIn") + optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") optIn("com.google.accompanist.pager.ExperimentalPagerApi") optIn("androidx.compose.foundation.ExperimentalFoundationApi") optIn("androidx.compose.material.ExperimentalMaterialApi") @@ -35,6 +36,8 @@ kotlin { api(kotlin("stdlib-common")) api(libs.coroutinesCore) api(libs.kamel) + api(libs.voyagerCore) + api(libs.voyagerNavigation) api(project(":core")) api(project(":i18n")) api(project(":data")) @@ -60,7 +63,6 @@ kotlin { api(libs.coroutinesSwing) api(libs.accompanistPager) api(libs.accompanistFlowLayout) - api(libs.composeRouter) api(libs.krokiCoroutines) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/WindowDialog.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/WindowDialog.kt index a9f79f21..0777fd2f 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/WindowDialog.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/WindowDialog.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.rememberWindowState import ca.gosyer.ui.AppComponent import ca.gosyer.ui.base.theme.AppTheme -import ca.gosyer.util.lang.launchApplication +import ca.gosyer.ui.util.lang.launchApplication import kotlinx.coroutines.DelicateCoroutinesApi @OptIn(DelicateCoroutinesApi::class) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/MenuController.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/DisplayController.kt similarity index 64% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/MenuController.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/DisplayController.kt index f5b75a18..68260a76 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/MenuController.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/DisplayController.kt @@ -12,14 +12,11 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import ca.gosyer.ui.main.Routes -import com.github.zsoltk.compose.router.BackStack -val LocalMenuController = - compositionLocalOf { null } +val LocalDisplayController = + compositionLocalOf { null } -class MenuController( - val backStack: BackStack, +class DisplayController( private val _sideMenuVisible: MutableState = mutableStateOf(true), private val _isDrawer: MutableState = mutableStateOf(false), ) { @@ -38,25 +35,12 @@ class MenuController( fun setAsNotDrawer() { _isDrawer.value = false } - - fun push(route: Routes) { - backStack.push(route) - if (isDrawer) { - closeSideMenu() - } - } - fun newRoot(route: Routes) { - backStack.newRoot(route) - if (isDrawer) { - closeSideMenu() - } - } } @Composable -fun withMenuController(controller: MenuController, content: @Composable () -> Unit) { +fun withDisplayController(controller: DisplayController, content: @Composable () -> Unit) { CompositionLocalProvider( - LocalMenuController provides controller, + LocalDisplayController provides controller, content = content ) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt index 6cc3273d..4b7ffe7b 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt @@ -66,13 +66,16 @@ import androidx.compose.ui.unit.sp import ca.gosyer.i18n.MR import ca.gosyer.uicore.components.BoxWithTooltipSurface import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator @Composable fun Toolbar( name: String, - menuController: MenuController? = LocalMenuController.current, - closable: Boolean, - onClose: () -> Unit = { menuController?.backStack?.pop() }, + displayController: DisplayController? = LocalDisplayController.current, + navigator: Navigator? = LocalNavigator.current, + closable: Boolean = (navigator?.size ?: 0) > 1, + onClose: () -> Unit = { navigator?.pop() }, modifier: Modifier = Modifier, actions: @Composable RowScope.() -> Unit = {}, backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars, @@ -98,14 +101,14 @@ fun Toolbar( Modifier.fillMaxHeight().animateContentSize(), verticalAlignment = Alignment.CenterVertically ) { - if (menuController != null) { - if (menuController.isDrawer) { - ActionIcon(menuController::openSideMenu, "Open nav", Icons.Rounded.Menu) + if (displayController != null) { + if (displayController.isDrawer) { + ActionIcon(displayController::openSideMenu, "Open nav", Icons.Rounded.Menu) } else { AnimatedVisibility( - !menuController.sideMenuVisible + !displayController.sideMenuVisible ) { - ActionIcon(menuController::openSideMenu, "Open nav", Icons.Rounded.Sort) + ActionIcon(displayController::openSideMenu, "Open nav", Icons.Rounded.Sort) } } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt index ecf09bb3..890557b7 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt @@ -25,9 +25,10 @@ import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.model.ThemeMode import ca.gosyer.uicore.theme.Theme import ca.gosyer.uicore.theme.themes +import ca.gosyer.uicore.vm.LocalViewModelFactory import ca.gosyer.uicore.vm.ViewModel -import ca.gosyer.uicore.vm.viewModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren @@ -39,7 +40,8 @@ import me.tatarka.inject.annotations.Inject */ @Composable fun AppTheme(content: @Composable () -> Unit) { - val vm = viewModel() + val vmFactory = LocalViewModelFactory.current + val vm = remember { vmFactory.instantiate() } val colors = vm.getColors() /*val systemUiController = rememberSystemUiController()*/ @@ -61,6 +63,8 @@ fun AppTheme(content: @Composable () -> Unit) { class AppThemeViewModel @Inject constructor( private val uiPreferences: UiPreferences ) : ViewModel() { + override val scope = MainScope() + private val themeMode = uiPreferences.themeMode().asStateFlow() private val lightTheme = uiPreferences.lightTheme().asStateFlow() private val darkTheme = uiPreferences.darkTheme().asStateFlow() @@ -130,7 +134,8 @@ class AppThemeViewModel @Inject constructor( ) } - override fun onDestroy() { + override fun onDispose() { baseThemeScope.cancel() + scope.cancel() } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt index a7aabf7a..94ba0485 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt @@ -7,14 +7,14 @@ package ca.gosyer.ui.base.vm import ca.gosyer.ui.base.theme.AppThemeViewModel -import ca.gosyer.ui.categories.CategoriesMenuViewModel -import ca.gosyer.ui.downloads.DownloadsMenuViewModel -import ca.gosyer.ui.extensions.ExtensionsMenuViewModel +import ca.gosyer.ui.categories.CategoriesScreenViewModel +import ca.gosyer.ui.downloads.DownloadsScreenViewModel +import ca.gosyer.ui.extensions.ExtensionsScreenViewModel import ca.gosyer.ui.library.LibraryScreenViewModel import ca.gosyer.ui.main.MainViewModel import ca.gosyer.ui.main.components.DebugOverlayViewModel import ca.gosyer.ui.main.components.TrayViewModel -import ca.gosyer.ui.manga.MangaMenuViewModel +import ca.gosyer.ui.manga.MangaScreenViewModel import ca.gosyer.ui.reader.ReaderMenuViewModel import ca.gosyer.ui.settings.SettingsAdvancedViewModel import ca.gosyer.ui.settings.SettingsBackupViewModel @@ -23,29 +23,28 @@ import ca.gosyer.ui.settings.SettingsLibraryViewModel import ca.gosyer.ui.settings.SettingsReaderViewModel import ca.gosyer.ui.settings.SettingsServerViewModel import ca.gosyer.ui.settings.ThemesViewModel -import ca.gosyer.ui.sources.SourcesMenuViewModel -import ca.gosyer.ui.sources.components.SourceHomeScreenViewModel -import ca.gosyer.ui.sources.components.SourceScreenViewModel -import ca.gosyer.ui.sources.components.filter.SourceFiltersViewModel -import ca.gosyer.ui.sources.settings.SourceSettingsViewModel -import ca.gosyer.ui.updates.UpdatesMenuViewModel +import ca.gosyer.ui.sources.SourcesScreenViewModel +import ca.gosyer.ui.sources.browse.SourceScreenViewModel +import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel +import ca.gosyer.ui.sources.home.SourceHomeScreenViewModel +import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel +import ca.gosyer.ui.updates.UpdatesScreenViewModel import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModelFactory -import com.github.zsoltk.compose.savedinstancestate.Bundle import me.tatarka.inject.annotations.Inject import kotlin.reflect.KClass @Inject class ViewModelFactoryImpl( private val appThemeFactory: () -> AppThemeViewModel, - private val categoryFactory: () -> CategoriesMenuViewModel, - private val downloadsFactory: () -> DownloadsMenuViewModel, - private val extensionsFactory: () -> ExtensionsMenuViewModel, - private val libraryFactory: (bundle: Bundle) -> LibraryScreenViewModel, + private val categoryFactory: () -> CategoriesScreenViewModel, + private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel, + private val extensionsFactory: () -> ExtensionsScreenViewModel, + private val libraryFactory: () -> LibraryScreenViewModel, private val debugOverlayFactory: () -> DebugOverlayViewModel, private val trayFactory: () -> TrayViewModel, private val mainFactory: () -> MainViewModel, - private val mangaFactory: (params: MangaMenuViewModel.Params) -> MangaMenuViewModel, + private val mangaFactory: (params: MangaScreenViewModel.Params) -> MangaScreenViewModel, private val readerFactory: (params: ReaderMenuViewModel.Params) -> ReaderMenuViewModel, private val settingsAdvancedFactory: () -> SettingsAdvancedViewModel, private val themesFactory: () -> ThemesViewModel, @@ -55,25 +54,25 @@ class ViewModelFactoryImpl( private val settingsReaderFactory: () -> SettingsReaderViewModel, private val settingsServerFactory: () -> SettingsServerViewModel, private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel, - private val sourceSettingsFactory: (params: SourceSettingsViewModel.Params) -> SourceSettingsViewModel, - private val sourceHomeFactory: (bundle: Bundle) -> SourceHomeScreenViewModel, + private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel, + private val sourceHomeFactory: () -> SourceHomeScreenViewModel, private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel, - private val sourcesFactory: (bundle: Bundle) -> SourcesMenuViewModel, - private val updatesFactory: () -> UpdatesMenuViewModel -): ViewModelFactory() { + private val sourcesFactory: () -> SourcesScreenViewModel, + private val updatesFactory: () -> UpdatesScreenViewModel +) : ViewModelFactory() { override fun instantiate(klass: KClass, arg1: Any?): VM { @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY") return when (klass) { AppThemeViewModel::class -> appThemeFactory() - CategoriesMenuViewModel::class -> categoryFactory() - DownloadsMenuViewModel::class -> downloadsFactory() - ExtensionsMenuViewModel::class -> extensionsFactory() - LibraryScreenViewModel::class -> libraryFactory(arg1 as Bundle) + CategoriesScreenViewModel::class -> categoryFactory() + DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean) + ExtensionsScreenViewModel::class -> extensionsFactory() + LibraryScreenViewModel::class -> libraryFactory() DebugOverlayViewModel::class -> debugOverlayFactory() TrayViewModel::class -> trayFactory() MainViewModel::class -> mainFactory() - MangaMenuViewModel::class -> mangaFactory(arg1 as MangaMenuViewModel.Params) + MangaScreenViewModel::class -> mangaFactory(arg1 as MangaScreenViewModel.Params) ReaderMenuViewModel::class -> readerFactory(arg1 as ReaderMenuViewModel.Params) SettingsAdvancedViewModel::class -> settingsAdvancedFactory() ThemesViewModel::class -> themesFactory() @@ -83,11 +82,11 @@ class ViewModelFactoryImpl( SettingsReaderViewModel::class -> settingsReaderFactory() SettingsServerViewModel::class -> settingsServerFactory() SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params) - SourceSettingsViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsViewModel.Params) - SourceHomeScreenViewModel::class -> sourceHomeFactory(arg1 as Bundle) + SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params) + SourceHomeScreenViewModel::class -> sourceHomeFactory() SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params) - SourcesMenuViewModel::class -> sourcesFactory(arg1 as Bundle) - UpdatesMenuViewModel::class -> updatesFactory() + SourcesScreenViewModel::class -> sourcesFactory() + UpdatesScreenViewModel::class -> updatesFactory() else -> throw IllegalArgumentException("Unknown ViewModel $klass") } as VM } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesScreen.kt new file mode 100644 index 00000000..bad7d4b0 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesScreen.kt @@ -0,0 +1,56 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.categories + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import ca.gosyer.presentation.build.BuildKonfig +import ca.gosyer.ui.categories.components.CategoriesScreenContent +import ca.gosyer.ui.util.compose.ThemedWindow +import ca.gosyer.ui.util.lang.launchApplication +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.Navigator +import kotlinx.coroutines.DelicateCoroutinesApi + +@OptIn(DelicateCoroutinesApi::class) +fun openCategoriesMenu(notifyFinished: (() -> Unit)? = null) { + launchApplication { + ThemedWindow( + ::exitApplication, + title = "${BuildKonfig.NAME} - Categories" + ) { + Navigator(remember { CategoriesScreen(notifyFinished) }) + } + } +} + +class CategoriesScreen( + @Transient + private val notifyFinished: (() -> Unit)? = null +) : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + CategoriesScreenContent( + categories = vm.categories.collectAsState().value, + updateRemoteCategories = vm::updateRemoteCategories, + moveCategoryUp = vm::moveUp, + moveCategoryDown = vm::moveDown, + renameCategory = vm::renameCategory, + deleteCategory = vm::deleteCategory, + createCategory = vm::createCategory, + notifyFinished = notifyFinished + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesMenuViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesScreenViewModel.kt similarity index 98% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesMenuViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesScreenViewModel.kt index b9615988..23e0887f 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesMenuViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesScreenViewModel.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject -class CategoriesMenuViewModel @Inject constructor( +class CategoriesScreenViewModel @Inject constructor( private val categoryHandler: CategoryInteractionHandler ) : ViewModel() { private var originalCategories = emptyList() diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesDialogs.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesDialogs.kt similarity index 88% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesDialogs.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesDialogs.kt index e60e9878..d21c716f 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesDialogs.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesDialogs.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.categories +package ca.gosyer.ui.categories.components import androidx.compose.material.Text import androidx.compose.material.TextField @@ -14,11 +14,12 @@ import androidx.compose.ui.text.input.TextFieldValue import ca.gosyer.i18n.MR import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.ui.base.WindowDialog +import ca.gosyer.ui.categories.CategoriesScreenViewModel import ca.gosyer.uicore.resources.stringResource import kotlinx.coroutines.flow.MutableStateFlow fun openRenameDialog( - category: CategoriesMenuViewModel.MenuCategory, + category: CategoriesScreenViewModel.MenuCategory, onRename: (String) -> Unit ) { val newName = MutableStateFlow(TextFieldValue(category.name)) @@ -44,8 +45,8 @@ fun openRenameDialog( } fun openDeleteDialog( - category: CategoriesMenuViewModel.MenuCategory, - onDelete: (CategoriesMenuViewModel.MenuCategory) -> Unit + category: CategoriesScreenViewModel.MenuCategory, + onDelete: (CategoriesScreenViewModel.MenuCategory) -> Unit ) { WindowDialog( title = "${BuildKonfig.NAME} - Categories - Delete Dialog", diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesScreenContent.kt similarity index 85% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesMenu.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesScreenContent.kt index c0273571..1e587475 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesScreenContent.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.categories +package ca.gosyer.ui.categories.components import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.layout.Box @@ -40,16 +40,11 @@ import androidx.compose.material.icons.rounded.KeyboardArrowUp import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR -import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.lang.launchApplication +import ca.gosyer.ui.categories.CategoriesScreenViewModel.MenuCategory import ca.gosyer.uicore.resources.stringResource import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.DelicateCoroutinesApi @@ -57,23 +52,18 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import mu.KotlinLogging -@OptIn(DelicateCoroutinesApi::class) -fun openCategoriesMenu(notifyFinished: (() -> Unit)? = null) { - launchApplication { - ThemedWindow( - ::exitApplication, - title = "${BuildKonfig.NAME} - Categories" - ) { - CategoriesMenu(notifyFinished) - } - } -} - @OptIn(DelicateCoroutinesApi::class) @Composable -fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) { - val vm = viewModel() - val categories by vm.categories.collectAsState() +fun CategoriesScreenContent( + categories: List, + updateRemoteCategories: suspend () -> Unit, + moveCategoryUp: (MenuCategory) -> Unit, + moveCategoryDown: (MenuCategory) -> Unit, + renameCategory: (MenuCategory, String) -> Unit, + deleteCategory: (MenuCategory) -> Unit, + createCategory: (String) -> Unit, + notifyFinished: (() -> Unit)? = null +) { DisposableEffect(Unit) { onDispose { val logger = KotlinLogging.logger {} @@ -81,7 +71,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) { logger.debug { throwable } } GlobalScope.launch(handler) { - vm.updateRemoteCategories() + updateRemoteCategories() notifyFinished?.invoke() } } @@ -96,16 +86,16 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) { category = category, moveUpEnabled = i != 0, moveDownEnabled = i != categories.lastIndex, - onMoveUp = { vm.moveUp(category) }, - onMoveDown = { vm.moveDown(category) }, + onMoveUp = { moveCategoryUp(category) }, + onMoveDown = { moveCategoryDown(category) }, onRename = { openRenameDialog(category) { - vm.renameCategory(category, it) + renameCategory(category, it) } }, onDelete = { openDeleteDialog(category) { - vm.deleteCategory(category) + deleteCategory(category) } }, ) @@ -120,7 +110,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) { modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp), onClick = { openCreateDialog { - vm.createCategory(it) + createCategory(it) } } ) @@ -136,7 +126,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) { @Composable private fun CategoryRow( - category: CategoriesMenuViewModel.MenuCategory, + category: MenuCategory, moveUpEnabled: Boolean = true, moveDownEnabled: Boolean = true, onMoveUp: () -> Unit = {}, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreen.kt new file mode 100644 index 00000000..3291e81f --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreen.kt @@ -0,0 +1,58 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.downloads + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import ca.gosyer.presentation.build.BuildKonfig +import ca.gosyer.ui.downloads.components.DownloadsScreenContent +import ca.gosyer.ui.manga.MangaScreen +import ca.gosyer.ui.util.compose.ThemedWindow +import ca.gosyer.ui.util.lang.launchApplication +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import kotlinx.coroutines.DelicateCoroutinesApi + +@OptIn(DelicateCoroutinesApi::class) +fun openDownloadsMenu() { + launchApplication { + ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { + Surface { + Navigator(remember { DownloadsScreen() }) + } + } + } +} + +class DownloadsScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel { + instantiate(false) + } + val navigator = LocalNavigator.currentOrThrow + DownloadsScreenContent( + downloadQueue = vm.downloadQueue.collectAsState().value, + downloadStatus = vm.downloaderStatus.collectAsState().value, + startDownloading = vm::start, + pauseDownloading = vm::pause, + clearQueue = vm::clear, + onMangaClick = { navigator push MangaScreen(it) }, + stopDownload = vm::stopDownload, + moveDownloadToBottom = vm::moveToBottom + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsMenuViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt similarity index 81% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsMenuViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt index 6b9daba2..73d4e1e4 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsMenuViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt @@ -11,14 +11,24 @@ import ca.gosyer.data.models.Chapter import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.DownloadInteractionHandler import ca.gosyer.uicore.vm.ViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject -class DownloadsMenuViewModel @Inject constructor( +class DownloadsScreenViewModel @Inject constructor( private val downloadService: DownloadService, private val downloadsHandler: DownloadInteractionHandler, - private val chapterHandler: ChapterInteractionHandler + private val chapterHandler: ChapterInteractionHandler, + standalone: Boolean ) : ViewModel() { + private val uiScope = if (standalone) { + MainScope() + } else null + + override val scope: CoroutineScope + get() = uiScope ?: super.scope + val serviceStatus get() = downloadService.status val downloaderStatus get() = downloadService.downloaderStatus val downloadQueue get() = downloadService.downloadQueue diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/components/DownloadsScreenContent.kt similarity index 82% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsMenu.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/components/DownloadsScreenContent.kt index dfb69a31..45fa5abd 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/components/DownloadsScreenContent.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.downloads +package ca.gosyer.ui.downloads.components import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.VerticalScrollbar @@ -28,7 +28,6 @@ import androidx.compose.material.Icon import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.ProgressIndicatorDefaults -import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ClearAll @@ -36,7 +35,6 @@ import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -48,10 +46,8 @@ import ca.gosyer.data.download.model.DownloadChapter import ca.gosyer.data.download.model.DownloaderStatus import ca.gosyer.data.models.Chapter import ca.gosyer.i18n.MR -import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.ui.base.navigation.ActionIcon import ca.gosyer.ui.base.navigation.Toolbar -import ca.gosyer.ui.manga.openMangaMenu import ca.gosyer.uicore.components.DropdownIconButton import ca.gosyer.uicore.components.MangaListItem import ca.gosyer.uicore.components.MangaListItemColumn @@ -59,41 +55,31 @@ import ca.gosyer.uicore.components.MangaListItemImage import ca.gosyer.uicore.components.MangaListItemSubtitle import ca.gosyer.uicore.components.MangaListItemTitle import ca.gosyer.uicore.components.mangaAspectRatio -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.lang.launchApplication import ca.gosyer.uicore.resources.stringResource import io.kamel.image.lazyPainterResource -import kotlinx.coroutines.DelicateCoroutinesApi - -@OptIn(DelicateCoroutinesApi::class) -fun openDownloadsMenu() { - launchApplication { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - Surface { - DownloadsMenu(::openMangaMenu) - } - } - } -} @Composable -fun DownloadsMenu(onMangaClick: (Long) -> Unit) { - val vm = viewModel() - val downloadQueue by vm.downloadQueue.collectAsState() - +fun DownloadsScreenContent( + downloadQueue: List, + downloadStatus: DownloaderStatus, + startDownloading: () -> Unit, + pauseDownloading: () -> Unit, + clearQueue: () -> Unit, + onMangaClick: (Long) -> Unit, + stopDownload: (Chapter) -> Unit, + moveDownloadToBottom: (Chapter) -> Unit +) { Column { Toolbar( stringResource(MR.strings.location_downloads), closable = false, actions = { - val downloadStatus by vm.downloaderStatus.collectAsState() if (downloadStatus == DownloaderStatus.Started) { - ActionIcon(onClick = vm::pause, stringResource(MR.strings.action_pause), Icons.Rounded.Pause) + ActionIcon(onClick = pauseDownloading, stringResource(MR.strings.action_pause), Icons.Rounded.Pause) } else { - ActionIcon(onClick = vm::start, stringResource(MR.strings.action_continue), Icons.Rounded.PlayArrow) + ActionIcon(onClick = startDownloading, stringResource(MR.strings.action_continue), Icons.Rounded.PlayArrow) } - ActionIcon(onClick = vm::clear, stringResource(MR.strings.action_clear_queue), Icons.Rounded.ClearAll) + ActionIcon(onClick = clearQueue, stringResource(MR.strings.action_clear_queue), Icons.Rounded.ClearAll) } ) Box { @@ -103,8 +89,8 @@ fun DownloadsMenu(onMangaClick: (Long) -> Unit) { DownloadsItem( it, { onMangaClick(it.mangaId) }, - vm::stopDownload, - vm::moveToBottom + stopDownload, + moveDownloadToBottom ) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreen.kt new file mode 100644 index 00000000..7c27d34a --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreen.kt @@ -0,0 +1,60 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.extensions + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.rememberWindowState +import ca.gosyer.presentation.build.BuildKonfig +import ca.gosyer.ui.extensions.components.ExtensionsScreenContent +import ca.gosyer.ui.util.compose.ThemedWindow +import ca.gosyer.ui.util.lang.launchApplication +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.Navigator +import kotlinx.coroutines.DelicateCoroutinesApi + +@OptIn(DelicateCoroutinesApi::class) +fun openExtensionsMenu() { + launchApplication { + val state = rememberWindowState(size = DpSize(550.dp, 700.dp)) + ThemedWindow(::exitApplication, state, title = BuildKonfig.NAME) { + Surface { + Navigator(remember { ExtensionsScreen() }) + } + } + } +} + +class ExtensionsScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + + ExtensionsScreenContent( + extensions = vm.extensions.collectAsState().value, + isLoading = vm.isLoading.collectAsState().value, + query = vm.searchQuery.collectAsState().value, + setQuery = vm::search, + enabledLangs = vm.enabledLangs, + getSourceLanguages = vm::getSourceLanguages, + setEnabledLanguages = vm::setEnabledLanguages, + installExtension = vm::install, + updateExtension = vm::update, + uninstallExtension = vm::uninstall + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsMenuViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt similarity index 98% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsMenuViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt index 4e3a436b..57ead3b9 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsMenuViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import java.util.Locale -class ExtensionsMenuViewModel @Inject constructor( +class ExtensionsScreenViewModel @Inject constructor( private val extensionHandler: ExtensionInteractionHandler, extensionPreferences: ExtensionPreferences ) : ViewModel() { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt similarity index 84% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsMenu.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt index 531294b7..6d5acb9e 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.extensions +package ca.gosyer.ui.extensions.components import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background @@ -26,7 +26,6 @@ import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.material.Button import androidx.compose.material.ContentAlpha import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -42,10 +41,8 @@ import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.rememberWindowState import ca.gosyer.data.models.Extension import ca.gosyer.i18n.MR import ca.gosyer.presentation.build.BuildKonfig @@ -54,59 +51,48 @@ import ca.gosyer.ui.base.navigation.TextActionIcon import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.image.KamelImage -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.compose.persistentLazyListState -import ca.gosyer.util.lang.launchApplication import ca.gosyer.uicore.resources.stringResource import io.kamel.image.lazyPainterResource -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import java.util.Locale -@OptIn(DelicateCoroutinesApi::class) -fun openExtensionsMenu() { - launchApplication { - val state = rememberWindowState(size = DpSize(550.dp, 700.dp)) - ThemedWindow(::exitApplication, state, title = BuildKonfig.NAME) { - Surface { - ExtensionsMenu() - } - } - } -} - @Composable -fun ExtensionsMenu() { - val vm = viewModel() - val extensions by vm.extensions.collectAsState() - val isLoading by vm.isLoading.collectAsState() - val search by vm.searchQuery.collectAsState() - +fun ExtensionsScreenContent( + extensions: Map>, + isLoading: Boolean, + query: String?, + setQuery: (String) -> Unit, + enabledLangs: StateFlow>, + getSourceLanguages: () -> Set, + setEnabledLanguages: (Set) -> Unit, + installExtension: (Extension) -> Unit, + updateExtension: (Extension) -> Unit, + uninstallExtension: (Extension) -> Unit +) { if (isLoading) { Column { ExtensionsToolbar( - search, - vm::search, - vm.enabledLangs, - vm::getSourceLanguages, - vm::setEnabledLanguages + query, + setQuery, + enabledLangs, + getSourceLanguages, + setEnabledLanguages ) LoadingScreen(isLoading) } } else { - val state = persistentLazyListState() + val state = rememberLazyListState() Box(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize(), state) { item { ExtensionsToolbar( - search, - vm::search, - vm.enabledLangs, - vm::getSourceLanguages, - vm::setEnabledLanguages + query, + setQuery, + enabledLangs, + getSourceLanguages, + setEnabledLanguages ) } extensions.forEach { (header, items) -> @@ -120,9 +106,9 @@ fun ExtensionsMenu() { items(items) { extension -> ExtensionItem( extension, - onInstallClicked = vm::install, - onUpdateClicked = vm::update, - onUninstallClicked = vm::uninstall + onInstallClicked = installExtension, + onUpdateClicked = updateExtension, + onUninstallClicked = uninstallExtension ) Spacer(Modifier.height(8.dp)) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreen.kt index 26d1d85e..14bb69cf 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreen.kt @@ -6,197 +6,55 @@ package ca.gosyer.ui.library -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ScrollableTabRow import androidx.compose.material.Surface -import androidx.compose.material.Tab -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEachIndexed -import ca.gosyer.data.library.model.DisplayMode -import ca.gosyer.data.models.Category -import ca.gosyer.data.models.Manga -import ca.gosyer.i18n.MR +import androidx.compose.runtime.remember import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.base.navigation.Toolbar -import ca.gosyer.ui.manga.openMangaMenu -import ca.gosyer.uicore.components.LoadingScreen +import ca.gosyer.ui.library.components.LibraryScreenContent +import ca.gosyer.ui.manga.MangaScreen +import ca.gosyer.ui.util.compose.ThemedWindow +import ca.gosyer.ui.util.lang.launchApplication import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.lang.launchApplication -import com.github.zsoltk.compose.savedinstancestate.Bundle -import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState -import com.google.accompanist.pager.HorizontalPager -import com.google.accompanist.pager.rememberPagerState -import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow import kotlinx.coroutines.DelicateCoroutinesApi @OptIn(DelicateCoroutinesApi::class) fun openLibraryMenu() { launchApplication { ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - CompositionLocalProvider( - LocalSavedInstanceState provides Bundle() - ) { - Surface { - LibraryScreen() - } + Surface { + Navigator(remember { LibraryScreen() }) } } } } -@Composable -fun LibraryScreen(onClickManga: (Long) -> Unit = ::openMangaMenu) { - LibraryScreen(LocalSavedInstanceState.current, onClickManga) -} +class LibraryScreen : Screen { -@Composable -fun LibraryScreen(bundle: Bundle, onClickManga: (Long) -> Unit = ::openMangaMenu) { - val vm = viewModel { - instantiate(bundle) - } - val categories by vm.categories.collectAsState() - val selectedCategoryIndex by vm.selectedCategoryIndex.collectAsState() - val displayMode by vm.displayMode.collectAsState() - val isLoading by vm.isLoading.collectAsState() - val error by vm.error.collectAsState() - val query by vm.query.collectAsState() - // val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) + override val key: ScreenKey = uniqueScreenKey - if (categories.isEmpty()) { - LoadingScreen(isLoading, errorMessage = error) - } else { - /*ModalBottomSheetLayout( - sheetState = sheetState, - sheetContent = { *//*LibrarySheet()*//* } - ) {*/ - Column(Modifier.fillMaxWidth()) { - /*Toolbar( - title = { - val text = if (vm.showCategoryTabs) { - stringResource(R.string.library_label) - } else { - vm.selectedCategory?.visibleName.orEmpty() - } - Text(text) - }, - actions = { - IconButton(onClick = { scope.launch { sheetState.show() }}) { - Icon(Icons.Rounded.FilterList, contentDescription = null) - } - } - )*/ - Toolbar( - stringResource(MR.strings.location_library), - closable = false, - searchText = query, - search = vm::updateQuery - ) - LibraryTabs( - visible = true, // vm.showCategoryTabs, - categories = categories, - selectedPage = selectedCategoryIndex, - onPageChanged = vm::setSelectedPage - ) - LibraryPager( - categories = categories, - displayMode = displayMode, - selectedPage = selectedCategoryIndex, - getLibraryForPage = { vm.getLibraryForCategoryId(it).collectAsState() }, - onPageChanged = vm::setSelectedPage, - onClickManga = onClickManga, - onRemoveMangaClicked = vm::removeManga - ) - } - // } - } -} - -@Composable -private fun LibraryTabs( - visible: Boolean, - categories: List, - selectedPage: Int, - onPageChanged: (Int) -> Unit -) { - if (categories.isEmpty()) return - - AnimatedVisibility( - visible = visible, - enter = expandVertically(), - exit = shrinkVertically() - ) { - ScrollableTabRow( - selectedTabIndex = selectedPage, - backgroundColor = MaterialTheme.colors.surface, - // contentColor = CustomColors.current.onBars, - edgePadding = 0.dp - ) { - categories.fastForEachIndexed { i, category -> - Tab( - selected = selectedPage == i, - onClick = { onPageChanged(i) }, - text = { Text(category.name) } - ) - } - } - } -} - -@Composable -private fun LibraryPager( - categories: List, - displayMode: DisplayMode, - selectedPage: Int, - getLibraryForPage: @Composable (Long) -> State>, - onPageChanged: (Int) -> Unit, - onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit -) { - if (categories.isEmpty()) return - - val state = rememberPagerState(categories.size, selectedPage) - LaunchedEffect(state.currentPage) { - if (state.currentPage != selectedPage) { - onPageChanged(state.currentPage) - } - } - LaunchedEffect(selectedPage) { - if (state.currentPage != selectedPage) { - state.animateScrollToPage(selectedPage) - } - } - HorizontalPager(state = state) { - val library by getLibraryForPage(categories[it].id) - when (displayMode) { - DisplayMode.CompactGrid -> LibraryMangaCompactGrid( - library = library, - onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked - ) - /*DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid( - library = library, - onClickManga = onClickManga - ) - DisplayMode.List -> LibraryMangaList( - library = library, - onClickManga = onClickManga - )*/ - else -> Box {} - } + @Composable + override fun Content() { + val vm = viewModel() + val navigator = LocalNavigator.currentOrThrow + LibraryScreenContent( + categories = vm.categories.collectAsState().value, + selectedCategoryIndex = vm.selectedCategoryIndex.collectAsState().value, + displayMode = vm.displayMode.collectAsState().value, + isLoading = vm.isLoading.collectAsState().value, + error = vm.error.collectAsState().value, + query = vm.query.collectAsState().value, + updateQuery = vm::updateQuery, + getLibraryForPage = { vm.getLibraryForCategoryId(it).collectAsState() }, + onPageChanged = vm::setSelectedPage, + onClickManga = { navigator push MangaScreen(it) }, + onRemoveMangaClicked = vm::removeManga + ) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt index 0f1a835b..0beafb4a 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt @@ -15,9 +15,6 @@ import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.data.server.interactions.LibraryInteractionHandler import ca.gosyer.data.server.interactions.UpdatesInteractionHandler import ca.gosyer.uicore.vm.ViewModel -import ca.gosyer.util.compose.saveIntInBundle -import ca.gosyer.util.compose.saveStringInBundle -import com.github.zsoltk.compose.savedinstancestate.Bundle import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow @@ -74,13 +71,12 @@ class LibraryScreenViewModel @Inject constructor( private val categoryHandler: CategoryInteractionHandler, private val libraryHandler: LibraryInteractionHandler, private val updatesHandler: UpdatesInteractionHandler, - libraryPreferences: LibraryPreferences, - private val bundle: Bundle, + libraryPreferences: LibraryPreferences ) : ViewModel() { private val library = Library(MutableStateFlow(emptyList()), mutableMapOf()) val categories = library.categories.asStateFlow() - private val _selectedCategoryIndex = saveIntInBundle(scope, bundle, SELECTED_CATEGORY_KEY, 0) + private val _selectedCategoryIndex = MutableStateFlow(0) val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() val displayMode = libraryPreferences.displayMode().stateIn(scope) @@ -91,7 +87,7 @@ class LibraryScreenViewModel @Inject constructor( private val _error = MutableStateFlow(null) val error = _error.asStateFlow() - private val _query = saveStringInBundle(scope, bundle, QUERY_KEY) + private val _query = MutableStateFlow("") val query = _query.asStateFlow() init { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryMangaBadges.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryMangaBadges.kt similarity index 93% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryMangaBadges.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryMangaBadges.kt index cff96583..35538cb6 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryMangaBadges.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryMangaBadges.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.library +package ca.gosyer.ui.library.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row @@ -36,7 +36,9 @@ fun LibraryMangaBadges( if (downloaded != null && downloaded > 0) { Text( text = downloaded.toString(), - modifier = Modifier.background(MaterialTheme.colors.secondary).then(BadgesInnerPadding), + modifier = Modifier.background(MaterialTheme.colors.secondary).then( + BadgesInnerPadding + ), style = MaterialTheme.typography.caption, color = MaterialTheme.colors.onSecondary ) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryPager.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryPager.kt new file mode 100644 index 00000000..98ba4302 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryPager.kt @@ -0,0 +1,62 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.library.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import ca.gosyer.data.library.model.DisplayMode +import ca.gosyer.data.models.Category +import ca.gosyer.data.models.Manga +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState + +@Composable +fun LibraryPager( + categories: List, + displayMode: DisplayMode, + selectedPage: Int, + getLibraryForPage: @Composable (Long) -> State>, + onPageChanged: (Int) -> Unit, + onClickManga: (Long) -> Unit, + onRemoveMangaClicked: (Long) -> Unit +) { + if (categories.isEmpty()) return + + val state = rememberPagerState(categories.size, selectedPage) + LaunchedEffect(state.currentPage) { + if (state.currentPage != selectedPage) { + onPageChanged(state.currentPage) + } + } + LaunchedEffect(selectedPage) { + if (state.currentPage != selectedPage) { + state.animateScrollToPage(selectedPage) + } + } + HorizontalPager(state = state) { + val library by getLibraryForPage(categories[it].id) + when (displayMode) { + DisplayMode.CompactGrid -> LibraryMangaCompactGrid( + library = library, + onClickManga = onClickManga, + onRemoveMangaClicked = onRemoveMangaClicked + ) + /*DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid( + library = library, + onClickManga = onClickManga + ) + DisplayMode.List -> LibraryMangaList( + library = library, + onClickManga = onClickManga + )*/ + else -> Box {} + } + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryScreenContent.kt new file mode 100644 index 00000000..dcbcb043 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryScreenContent.kt @@ -0,0 +1,85 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.library.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import ca.gosyer.data.library.model.DisplayMode +import ca.gosyer.data.models.Category +import ca.gosyer.data.models.Manga +import ca.gosyer.i18n.MR +import ca.gosyer.ui.base.navigation.Toolbar +import ca.gosyer.uicore.components.LoadingScreen +import ca.gosyer.uicore.resources.stringResource + +@Composable +fun LibraryScreenContent( + categories: List, + selectedCategoryIndex: Int, + displayMode: DisplayMode, + isLoading: Boolean, + error: String?, + query: String, + updateQuery: (String) -> Unit, + getLibraryForPage: @Composable (Long) -> State>, + onPageChanged: (Int) -> Unit, + onClickManga: (Long) -> Unit, + onRemoveMangaClicked: (Long) -> Unit +) { + // val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) + + if (categories.isEmpty()) { + LoadingScreen(isLoading, errorMessage = error) + } else { + /*ModalBottomSheetLayout( + sheetState = sheetState, + sheetContent = { *//*LibrarySheet()*//* } + ) {*/ + Column(Modifier.fillMaxWidth()) { + /*Toolbar( + title = { + val text = if (vm.showCategoryTabs) { + stringResource(R.string.library_label) + } else { + vm.selectedCategory?.visibleName.orEmpty() + } + Text(text) + }, + actions = { + IconButton(onClick = { scope.launch { sheetState.show() }}) { + Icon(Icons.Rounded.FilterList, contentDescription = null) + } + } + )*/ + Toolbar( + stringResource(MR.strings.location_library), + closable = false, + searchText = query, + search = updateQuery + ) + LibraryTabs( + visible = true, // vm.showCategoryTabs, + categories = categories, + selectedPage = selectedCategoryIndex, + onPageChanged = onPageChanged + ) + LibraryPager( + categories = categories, + displayMode = displayMode, + selectedPage = selectedCategoryIndex, + getLibraryForPage = getLibraryForPage, + onPageChanged = onPageChanged, + onClickManga = onClickManga, + onRemoveMangaClicked = onRemoveMangaClicked + ) + } + // } + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryTabs.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryTabs.kt new file mode 100644 index 00000000..0b26d6a3 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryTabs.kt @@ -0,0 +1,50 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.library.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ScrollableTabRow +import androidx.compose.material.Tab +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import ca.gosyer.data.models.Category + +@Composable +fun LibraryTabs( + visible: Boolean, + categories: List, + selectedPage: Int, + onPageChanged: (Int) -> Unit +) { + if (categories.isEmpty()) return + + AnimatedVisibility( + visible = visible, + enter = expandVertically(), + exit = shrinkVertically() + ) { + ScrollableTabRow( + selectedTabIndex = selectedPage, + backgroundColor = MaterialTheme.colors.surface, + // contentColor = CustomColors.current.onBars, + edgePadding = 0.dp + ) { + categories.fastForEachIndexed { i, category -> + Tab( + selected = selectedPage == i, + onClick = { onPageChanged(i) }, + text = { Text(category.name) } + ) + } + } + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/MangaCompactGrid.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/MangaCompactGrid.kt similarity index 99% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/MangaCompactGrid.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/MangaCompactGrid.kt index e5c7baad..d89d7dd2 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/MangaCompactGrid.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/MangaCompactGrid.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.library +package ca.gosyer.ui.library.components import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.VerticalScrollbar diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/MainMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/MainMenu.kt index a51f19c0..a377eb64 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/MainMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/MainMenu.kt @@ -6,7 +6,6 @@ package ca.gosyer.ui.main -import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box @@ -26,45 +25,25 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ca.gosyer.ui.base.navigation.LocalMenuController -import ca.gosyer.ui.base.navigation.MenuController -import ca.gosyer.ui.base.navigation.withMenuController -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.ui.downloads.DownloadsMenu -import ca.gosyer.ui.extensions.ExtensionsMenu -import ca.gosyer.ui.library.LibraryScreen +import ca.gosyer.ui.base.navigation.DisplayController +import ca.gosyer.ui.base.navigation.withDisplayController import ca.gosyer.ui.main.components.SideMenu -import ca.gosyer.ui.manga.MangaMenu -import ca.gosyer.ui.reader.openReaderMenu -import ca.gosyer.ui.settings.SettingsAdvancedScreen -import ca.gosyer.ui.settings.SettingsAppearance -import ca.gosyer.ui.settings.SettingsBackupScreen -import ca.gosyer.ui.settings.SettingsBrowseScreen -import ca.gosyer.ui.settings.SettingsGeneralScreen -import ca.gosyer.ui.settings.SettingsLibraryScreen -import ca.gosyer.ui.settings.SettingsReaderScreen -import ca.gosyer.ui.settings.SettingsScreen -import ca.gosyer.ui.settings.SettingsServerScreen -import ca.gosyer.ui.sources.SourcesMenu -import ca.gosyer.ui.sources.settings.SourceSettingsMenu -import ca.gosyer.ui.updates.UpdatesMenu -import com.github.zsoltk.compose.router.Router -import com.github.zsoltk.compose.savedinstancestate.Bundle -import com.github.zsoltk.compose.savedinstancestate.BundleScope +import ca.gosyer.uicore.vm.LocalViewModelFactory +import cafe.adriel.voyager.navigator.CurrentScreen +import cafe.adriel.voyager.navigator.Navigator const val SIDE_MENU_EXPAND_DURATION = 500 @Composable -fun MainMenu(rootBundle: Bundle) { - val vm = viewModel() +fun MainMenu() { + val vmFactory = LocalViewModelFactory.current + val vm = remember { vmFactory.instantiate() } Surface { - Router("TopLevel", vm.startScreen.toRoute()) { backStack -> - val controller = remember { - MenuController(backStack) - } + Navigator(vm.startScreen.toScreen()) { navigator -> + val controller = remember { DisplayController() } BoxWithConstraints { // if (maxWidth > 720.dp) { - WideMainMenu(rootBundle, controller) + WideMainMenu(navigator, controller) // } else { // SkinnyMainMenu(rootBundle, controller) // } @@ -75,8 +54,8 @@ fun MainMenu(rootBundle: Bundle) { @Composable fun SkinnyMainMenu( - rootBundle: Bundle, - controller: MenuController + navigator: Navigator, + controller: DisplayController ) { val drawerState = rememberDrawerState(DrawerValue.Closed) LaunchedEffect(controller.sideMenuVisible) { @@ -104,21 +83,21 @@ fun SkinnyMainMenu( ModalDrawer( { - SideMenu(Modifier.fillMaxWidth(), controller) + SideMenu(Modifier.fillMaxWidth(), controller, navigator) }, drawerState = drawerState, gesturesEnabled = drawerState.isOpen ) { - withMenuController(controller) { - MainWindow(Modifier, rootBundle) + withDisplayController(controller) { + MainWindow(Modifier) } } } @Composable fun WideMainMenu( - rootBundle: Bundle, - controller: MenuController + navigator: Navigator, + controller: DisplayController ) { Box { val startPadding by animateDpAsState( @@ -130,72 +109,17 @@ fun WideMainMenu( animationSpec = tween(SIDE_MENU_EXPAND_DURATION) ) if (startPadding != 0.dp) { - SideMenu(Modifier.width(200.dp), controller) + SideMenu(Modifier.width(200.dp), controller, navigator) } - withMenuController(controller) { - MainWindow(Modifier.padding(start = startPadding), rootBundle) + withDisplayController(controller) { + MainWindow(Modifier.padding(start = startPadding)) } } } @Composable -fun MainWindow(modifier: Modifier, rootBundle: Bundle) { - Surface(Modifier.fillMaxSize().then(modifier)) { - val menuController = LocalMenuController.current!! - BundleScope("K${menuController.backStack.lastIndex}", rootBundle, false) { - Crossfade(menuController.backStack.last()) { routing -> - when (routing) { - is Routes.Library -> LibraryScreen { - menuController.push(Routes.Manga(it)) - } - is Routes.Updates -> UpdatesMenu( - openChapter = ::openReaderMenu, - openManga = { menuController.push(Routes.Manga(it)) } - ) - is Routes.Sources -> SourcesMenu( - { - menuController.push(Routes.SourceSettings(it)) - } - ) { - menuController.push(Routes.Manga(it)) - } - is Routes.Extensions -> ExtensionsMenu() - is Routes.Manga -> MangaMenu(routing.mangaId) - is Routes.Downloads -> DownloadsMenu { - menuController.push(Routes.Manga(it)) - } - - is Routes.SourceSettings -> SourceSettingsMenu(routing.sourceId) - - is Routes.Settings -> SettingsScreen(menuController) - is Routes.SettingsGeneral -> SettingsGeneralScreen(menuController) - is Routes.SettingsAppearance -> SettingsAppearance(menuController) - is Routes.SettingsServer -> SettingsServerScreen(menuController) - is Routes.SettingsLibrary -> SettingsLibraryScreen(menuController) - is Routes.SettingsReader -> SettingsReaderScreen(menuController) - /*is Route.SettingsDownloads -> SettingsDownloadsScreen(menuController) - is Route.SettingsTracking -> SettingsTrackingScreen(menuController)*/ - is Routes.SettingsBrowse -> SettingsBrowseScreen(menuController) - is Routes.SettingsBackup -> SettingsBackupScreen(menuController) - /*is Route.SettingsSecurity -> SettingsSecurityScreen(menuController) - is Route.SettingsParentalControls -> SettingsParentalControlsScreen(menuController)*/ - is Routes.SettingsAdvanced -> SettingsAdvancedScreen(menuController) - } - } - } - /*Box(Modifier.padding(bottom = 32.dp).align(Alignment.BottomCenter)) { - val shape = RoundedCornerShape(50.dp) - Box( - Modifier - .width(200.dp) - .defaultMinSize(minHeight = 64.dp) - .shadow(4.dp, shape) - .background(SolidColor(Color.Gray), alpha = 0.2F) - .clip(shape), - contentAlignment = Alignment.Center - ) { - Text("Test text") - } - }*/ +fun MainWindow(modifier: Modifier) { + Surface(Modifier.fillMaxSize() then modifier) { + CurrentScreen() } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/MainViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/MainViewModel.kt index ee53009e..9b29fcb8 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/MainViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/MainViewModel.kt @@ -8,10 +8,13 @@ package ca.gosyer.ui.main import ca.gosyer.data.ui.UiPreferences import ca.gosyer.uicore.vm.ViewModel +import kotlinx.coroutines.MainScope import me.tatarka.inject.annotations.Inject class MainViewModel @Inject constructor( uiPreferences: UiPreferences ) : ViewModel() { + override val scope = MainScope() + val startScreen = uiPreferences.startScreen().get() } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/Routes.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/Routes.kt index f00e170c..b22096f1 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/Routes.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/Routes.kt @@ -7,37 +7,14 @@ package ca.gosyer.ui.main import ca.gosyer.data.ui.model.StartScreen +import ca.gosyer.ui.extensions.ExtensionsScreen +import ca.gosyer.ui.library.LibraryScreen +import ca.gosyer.ui.sources.SourcesScreen +import ca.gosyer.ui.updates.UpdatesScreen -sealed class Routes { - object Library : Routes() - object Updates : Routes() - object Sources : Routes() - object Extensions : Routes() - data class Manga(val mangaId: Long) : Routes() - object Downloads : Routes() - - data class SourceSettings(val sourceId: Long) : Routes() - - object Settings : Routes() - object SettingsGeneral : Routes() - object SettingsAppearance : Routes() - object SettingsLibrary : Routes() - object SettingsReader : Routes() - - /*object SettingsDownloads : Route() - object SettingsTracking : Route()*/ - object SettingsBrowse : Routes() - object SettingsBackup : Routes() - object SettingsServer : Routes() - - /*object SettingsSecurity : Route() - object SettingsParentalControls : Route()*/ - object SettingsAdvanced : Routes() -} - -fun StartScreen.toRoute() = when (this) { - StartScreen.Library -> Routes.Library - StartScreen.Updates -> Routes.Updates - StartScreen.Sources -> Routes.Sources - StartScreen.Extensions -> Routes.Extensions +fun StartScreen.toScreen() = when (this) { + StartScreen.Library -> LibraryScreen() + StartScreen.Updates -> UpdatesScreen() + StartScreen.Sources -> SourcesScreen() + StartScreen.Extensions -> ExtensionsScreen() } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/TopLevelMenus.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/TopLevelMenus.kt index 33a8fd26..96b67415 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/TopLevelMenus.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/TopLevelMenus.kt @@ -22,28 +22,37 @@ import androidx.compose.material.icons.rounded.Store import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector import ca.gosyer.i18n.MR +import ca.gosyer.ui.downloads.DownloadsScreen +import ca.gosyer.ui.extensions.ExtensionsScreen import ca.gosyer.ui.extensions.openExtensionsMenu +import ca.gosyer.ui.library.LibraryScreen import ca.gosyer.ui.library.openLibraryMenu import ca.gosyer.ui.main.components.DownloadsExtraInfo +import ca.gosyer.ui.settings.SettingsScreen +import ca.gosyer.ui.sources.SourcesScreen import ca.gosyer.ui.sources.openSourcesMenu -import com.github.zsoltk.compose.router.BackStack +import ca.gosyer.ui.updates.UpdatesScreen +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.Navigator import dev.icerock.moko.resources.StringResource +import kotlin.reflect.KClass enum class TopLevelMenus( val textKey: StringResource, val unselectedIcon: ImageVector, val selectedIcon: ImageVector, - val menu: Routes, + val screen: KClass<*>, + val createScreen: () -> Screen, val top: Boolean, val openInNewWindow: () -> Unit = {}, val extraInfo: (@Composable () -> Unit)? = null ) { - Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, Routes.Library, true, ::openLibraryMenu), - Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, Routes.Updates, true, ::openLibraryMenu), - Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, Routes.Sources, true, ::openSourcesMenu), - Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, Routes.Extensions, true, ::openExtensionsMenu), - Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, Routes.Downloads, false, extraInfo = { DownloadsExtraInfo() }), - Settings(MR.strings.location_settings, Icons.Outlined.Settings, Icons.Rounded.Settings, Routes.Settings, false); + Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { LibraryScreen() }, true, ::openLibraryMenu), + Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, UpdatesScreen::class, { UpdatesScreen() }, true, ::openLibraryMenu), + Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, SourcesScreen::class, { SourcesScreen() }, true, ::openSourcesMenu), + Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, ExtensionsScreen::class, { ExtensionsScreen() }, true, ::openExtensionsMenu), + Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, DownloadsScreen::class, { DownloadsScreen() }, false, extraInfo = { DownloadsExtraInfo() }), + Settings(MR.strings.location_settings, Icons.Outlined.Settings, Icons.Rounded.Settings, SettingsScreen::class, { SettingsScreen() }, false); - fun isSelected(backStack: BackStack) = backStack.elements.first() == menu + fun isSelected(navigator: Navigator) = navigator.items.first()::class == screen } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DebugOverlay.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DebugOverlay.kt index 7761eeb3..8c84d7d6 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DebugOverlay.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DebugOverlay.kt @@ -4,20 +4,21 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.main +package ca.gosyer.ui.main.components import androidx.compose.foundation.layout.Column import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.ui.main.components.DebugOverlayViewModel +import ca.gosyer.uicore.vm.LocalViewModelFactory @Composable fun DebugOverlay() { - val vm = viewModel() + val vmFactory = LocalViewModelFactory.current + val vm = remember { vmFactory.instantiate() } val usedMemory by vm.usedMemoryFlow.collectAsState() Column { Text("$usedMemory/${vm.maxMemory}", color = Color.White) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DebugOverlayViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DebugOverlayViewModel.kt index 311040cd..d8adaf09 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DebugOverlayViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DebugOverlayViewModel.kt @@ -7,6 +7,7 @@ package ca.gosyer.ui.main.components import ca.gosyer.uicore.vm.ViewModel +import kotlinx.coroutines.MainScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -14,6 +15,8 @@ import me.tatarka.inject.annotations.Inject import kotlin.time.Duration.Companion.milliseconds class DebugOverlayViewModel @Inject constructor() : ViewModel() { + override val scope = MainScope() + val runtime: Runtime = Runtime.getRuntime() val maxMemory = runtime.maxMemory().formatSize() val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize()) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DownloadsExtraInfo.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DownloadsExtraInfo.kt index d25b1cc5..6ab4a0c4 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DownloadsExtraInfo.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/DownloadsExtraInfo.kt @@ -15,17 +15,19 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import ca.gosyer.data.base.WebsocketService import ca.gosyer.i18n.MR -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.ui.downloads.DownloadsMenuViewModel +import ca.gosyer.ui.downloads.DownloadsScreenViewModel import ca.gosyer.uicore.resources.stringResource +import ca.gosyer.uicore.vm.LocalViewModelFactory @Composable fun DownloadsExtraInfo() { - val vm = viewModel() + val vmFactory = LocalViewModelFactory.current + val vm = remember { vmFactory.instantiate(true) } val status by vm.serviceStatus.collectAsState() val list by vm.downloadQueue.collectAsState() val text = when (status) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/SideMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/SideMenu.kt index d3a2a86e..5bee55c2 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/SideMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/SideMenu.kt @@ -29,11 +29,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.base.navigation.MenuController +import ca.gosyer.ui.base.navigation.DisplayController import ca.gosyer.ui.main.TopLevelMenus +import cafe.adriel.voyager.navigator.Navigator @Composable -fun SideMenu(modifier: Modifier, controller: MenuController) { +fun SideMenu(modifier: Modifier, controller: DisplayController, navigator: Navigator) { Surface(modifier then Modifier.fillMaxHeight(), elevation = 2.dp) { Box(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize().padding(horizontal = 4.dp)) { @@ -54,19 +55,17 @@ fun SideMenu(modifier: Modifier, controller: MenuController) { Spacer(Modifier.height(20.dp)) remember { TopLevelMenus.values().filter(TopLevelMenus::top) }.forEach { topLevelMenu -> SideMenuItem( - topLevelMenu.isSelected(controller.backStack), - topLevelMenu, - controller::newRoot - ) + topLevelMenu.isSelected(navigator), + topLevelMenu + ) { navigator replaceAll it } } Box(Modifier.fillMaxSize()) { Column(Modifier.align(Alignment.BottomStart).padding(bottom = 8.dp)) { remember { TopLevelMenus.values().filterNot(TopLevelMenus::top) }.forEach { topLevelMenu -> SideMenuItem( - topLevelMenu.isSelected(controller.backStack), + topLevelMenu.isSelected(navigator), topLevelMenu, - controller::newRoot - ) + ) { navigator replaceAll it } } } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/SideMenuItem.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/SideMenuItem.kt index bb1966ad..ff571c31 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/SideMenuItem.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/SideMenuItem.kt @@ -25,17 +25,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import ca.gosyer.ui.main.Routes import ca.gosyer.ui.main.TopLevelMenus import ca.gosyer.uicore.components.combinedMouseClickable import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen @Composable -fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Routes) -> Unit) { +fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Screen) -> Unit) { SideMenuItem( selected, stringResource(topLevelMenu.textKey), - topLevelMenu.menu, + topLevelMenu.createScreen, topLevelMenu.selectedIcon, topLevelMenu.unselectedIcon, topLevelMenu.openInNewWindow, @@ -48,12 +48,12 @@ fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Route private fun SideMenuItem( selected: Boolean, text: String, - menu: Routes, + createScreen: () -> Screen, selectedIcon: ImageVector, unselectedIcon: ImageVector, onMiddleClick: () -> Unit, extraInfo: (@Composable () -> Unit)? = null, - onClick: (Routes) -> Unit + onClick: (Screen) -> Unit ) { Card( Modifier.fillMaxWidth(), @@ -70,7 +70,7 @@ private fun SideMenuItem( modifier = Modifier.fillMaxWidth() .height(40.dp) .combinedMouseClickable( - onClick = { onClick(menu) }, + onClick = { onClick(createScreen()) }, onMiddleClick = { onMiddleClick() } ) ) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/Tray.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/Tray.kt index 92ecef8a..ed4d1da6 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/Tray.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/Tray.kt @@ -8,6 +8,7 @@ package ca.gosyer.ui.main.components import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.Notification @@ -15,13 +16,14 @@ import androidx.compose.ui.window.Tray import androidx.compose.ui.window.rememberTrayState import ca.gosyer.i18n.MR import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.uicore.vm.viewModel +import ca.gosyer.uicore.vm.LocalViewModelFactory import kotlinx.coroutines.launch import java.util.Locale @Composable fun ApplicationScope.Tray(icon: Painter) { - val vm = viewModel() + val vmFactory = LocalViewModelFactory.current + val vm = remember { vmFactory.instantiate() } val trayState = rememberTrayState() Tray( icon, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/TrayViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/TrayViewModel.kt index 16727d98..df8ccd78 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/TrayViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/TrayViewModel.kt @@ -8,11 +8,14 @@ package ca.gosyer.ui.main.components import ca.gosyer.data.update.UpdateChecker import ca.gosyer.uicore.vm.ViewModel +import kotlinx.coroutines.MainScope import me.tatarka.inject.annotations.Inject class TrayViewModel @Inject constructor( private val updateChecker: UpdateChecker ) : ViewModel() { + override val scope = MainScope() + init { updateChecker.checkForUpdates() } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreen.kt new file mode 100644 index 00000000..0045e0c9 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreen.kt @@ -0,0 +1,66 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.manga + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import ca.gosyer.presentation.build.BuildKonfig +import ca.gosyer.ui.manga.components.MangaScreenContent +import ca.gosyer.ui.util.compose.ThemedWindow +import ca.gosyer.ui.util.lang.launchApplication +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.Navigator +import kotlinx.coroutines.DelicateCoroutinesApi + +@OptIn(DelicateCoroutinesApi::class) +fun openMangaMenu(mangaId: Long) { + launchApplication { + ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { + Surface { + Navigator(remember { MangaScreen(mangaId) }) + } + } + } +} + +class MangaScreen(private val mangaId: Long) : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel { + instantiate(MangaScreenViewModel.Params(mangaId)) + } + + MangaScreenContent( + isLoading = vm.isLoading.collectAsState().value, + manga = vm.manga.collectAsState().value, + chapters = vm.chapters.collectAsState().value, + dateTimeFormatter = vm.dateTimeFormatter.collectAsState().value, + categoriesExist = vm.categoriesExist.collectAsState().value, + chooseCategoriesFlow = vm.chooseCategoriesFlow, + addFavorite = vm::addFavorite, + setCategories = vm::setCategories, + toggleFavorite = vm::toggleFavorite, + refreshManga = vm::refreshManga, + toggleRead = vm::toggleRead, + toggleBookmarked = vm::toggleBookmarked, + markPreviousRead = vm::markPreviousRead, + downloadChapter = vm::downloadChapter, + deleteDownload = vm::deleteDownload, + stopDownloadingChapter = vm::stopDownloadingChapter, + loadChapters = vm::loadChapters, + loadManga = vm::loadManga + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt similarity index 99% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt index 349392f6..68d5cf65 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt @@ -33,7 +33,7 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Locale -class MangaMenuViewModel @Inject constructor( +class MangaScreenViewModel @Inject constructor( private val mangaHandler: MangaInteractionHandler, private val chapterHandler: ChapterInteractionHandler, private val categoryHandler: CategoryInteractionHandler, @@ -226,7 +226,7 @@ class MangaMenuViewModel @Inject constructor( } } - override fun onDestroy() { + override fun onDispose() { downloadService.removeWatch(params.mangaId) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/ChapterItem.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/ChapterItem.kt similarity index 99% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/ChapterItem.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/ChapterItem.kt index da9f0f74..93b56ba5 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/ChapterItem.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/ChapterItem.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.manga +package ca.gosyer.ui.manga.components import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.layout.Arrangement diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaMenu.kt similarity index 51% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaMenu.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaMenu.kt index 5c9e9b9d..79e1ce5a 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaMenu.kt @@ -4,9 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.manga +package ca.gosyer.ui.manga.components -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -32,13 +31,7 @@ import androidx.compose.material.Checkbox import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Favorite -import androidx.compose.material.icons.rounded.FavoriteBorder -import androidx.compose.material.icons.rounded.Label -import androidx.compose.material.icons.rounded.Refresh import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -50,137 +43,12 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastForEach import ca.gosyer.data.models.Category import ca.gosyer.data.models.Manga -import ca.gosyer.i18n.MR -import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.ui.base.WindowDialog -import ca.gosyer.ui.base.navigation.LocalMenuController -import ca.gosyer.ui.base.navigation.MenuController -import ca.gosyer.ui.base.navigation.TextActionIcon -import ca.gosyer.ui.base.navigation.Toolbar -import ca.gosyer.ui.reader.openReaderMenu -import ca.gosyer.uicore.components.ErrorScreen -import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.image.KamelImage -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.lang.launchApplication import com.google.accompanist.flowlayout.FlowRow -import ca.gosyer.uicore.resources.stringResource import io.kamel.image.lazyPainterResource -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -@OptIn(DelicateCoroutinesApi::class) -fun openMangaMenu(mangaId: Long) { - launchApplication { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - Surface { - MangaMenu(mangaId) - } - } - } -} - -@Composable -fun MangaMenu(mangaId: Long, menuController: MenuController? = LocalMenuController.current) { - val vm = viewModel { - instantiate(MangaMenuViewModel.Params(mangaId)) - } - val manga by vm.manga.collectAsState() - val chapters by vm.chapters.collectAsState() - val isLoading by vm.isLoading.collectAsState() - val dateTimeFormatter by vm.dateTimeFormatter.collectAsState() - val categoriesExist by vm.categoriesExist.collectAsState() - - LaunchedEffect(Unit) { - vm.chooseCategoriesFlow.collect { (availableCategories, usedCategories) -> - openCategorySelectDialog(availableCategories, usedCategories, vm::addFavorite) - } - } - - Box { - Column { - Toolbar( - stringResource(MR.strings.location_manga), - menuController, - menuController != null, - actions = { - AnimatedVisibility(categoriesExist && manga?.inLibrary == true) { - TextActionIcon( - vm::setCategories, - stringResource(MR.strings.edit_categories), - Icons.Rounded.Label - ) - } - TextActionIcon( - vm::toggleFavorite, - stringResource(if (manga?.inLibrary == true) MR.strings.action_remove_favorite else MR.strings.action_favorite), - if (manga?.inLibrary == true) { - Icons.Rounded.Favorite - } else { - Icons.Rounded.FavoriteBorder - }, - manga != null - ) - TextActionIcon( - vm::refreshManga, - stringResource(MR.strings.action_refresh_manga), - Icons.Rounded.Refresh, - !isLoading - ) - } - ) - - manga.let { manga -> - if (manga != null) { - Box { - val state = rememberLazyListState() - LazyColumn(state = state) { - item { - MangaItem(manga) - } - if (chapters.isNotEmpty()) { - items(chapters) { chapter -> - ChapterItem( - chapter, - dateTimeFormatter::format, - onClick = { openReaderMenu(it, manga.id) }, - toggleRead = vm::toggleRead, - toggleBookmarked = vm::toggleBookmarked, - markPreviousAsRead = vm::markPreviousRead, - onClickDownload = vm::downloadChapter, - onClickDeleteChapter = vm::deleteDownload, - onClickStopDownload = vm::stopDownloadingChapter - ) - } - } else if (!isLoading) { - item { - ErrorScreen( - stringResource(MR.strings.no_chapters_found), - Modifier.height(400.dp).fillMaxWidth(), - retry = vm::loadChapters - ) - } - } - } - VerticalScrollbar( - modifier = Modifier.align(Alignment.CenterEnd) - .fillMaxHeight() - .padding(horizontal = 4.dp, vertical = 8.dp), - adapter = rememberScrollbarAdapter(state) - ) - } - } else if (!isLoading) { - ErrorScreen(stringResource(MR.strings.failed_manga_fetch), retry = vm::loadManga) - } - } - } - if (isLoading) { - LoadingScreen() - } - } -} - @Composable fun MangaItem(manga: Manga) { BoxWithConstraints(Modifier.padding(8.dp)) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt new file mode 100644 index 00000000..cedeac86 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt @@ -0,0 +1,150 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.manga.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Favorite +import androidx.compose.material.icons.rounded.FavoriteBorder +import androidx.compose.material.icons.rounded.Label +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ca.gosyer.data.models.Category +import ca.gosyer.data.models.Manga +import ca.gosyer.i18n.MR +import ca.gosyer.ui.base.chapter.ChapterDownloadItem +import ca.gosyer.ui.base.navigation.TextActionIcon +import ca.gosyer.ui.base.navigation.Toolbar +import ca.gosyer.ui.reader.openReaderMenu +import ca.gosyer.uicore.components.ErrorScreen +import ca.gosyer.uicore.components.LoadingScreen +import ca.gosyer.uicore.resources.stringResource +import kotlinx.coroutines.flow.SharedFlow +import java.time.format.DateTimeFormatter + +@Composable +fun MangaScreenContent( + isLoading: Boolean, + manga: Manga?, + chapters: List, + dateTimeFormatter: DateTimeFormatter, + categoriesExist: Boolean, + chooseCategoriesFlow: SharedFlow, List>>, + addFavorite: (List, List) -> Unit, + setCategories: () -> Unit, + toggleFavorite: () -> Unit, + refreshManga: () -> Unit, + toggleRead: (Int) -> Unit, + toggleBookmarked: (Int) -> Unit, + markPreviousRead: (Int) -> Unit, + downloadChapter: (Int) -> Unit, + deleteDownload: (Int) -> Unit, + stopDownloadingChapter: (Int) -> Unit, + loadChapters: () -> Unit, + loadManga: () -> Unit +) { + LaunchedEffect(Unit) { + chooseCategoriesFlow.collect { (availableCategories, usedCategories) -> + openCategorySelectDialog(availableCategories, usedCategories, addFavorite) + } + } + + Box { + Column { + Toolbar( + stringResource(MR.strings.location_manga), + actions = { + AnimatedVisibility(categoriesExist && manga?.inLibrary == true) { + TextActionIcon( + setCategories, + stringResource(MR.strings.edit_categories), + Icons.Rounded.Label + ) + } + TextActionIcon( + toggleFavorite, + stringResource(if (manga?.inLibrary == true) MR.strings.action_remove_favorite else MR.strings.action_favorite), + if (manga?.inLibrary == true) { + Icons.Rounded.Favorite + } else { + Icons.Rounded.FavoriteBorder + }, + manga != null + ) + TextActionIcon( + refreshManga, + stringResource(MR.strings.action_refresh_manga), + Icons.Rounded.Refresh, + !isLoading + ) + } + ) + + manga.let { manga -> + if (manga != null) { + Box { + val state = rememberLazyListState() + LazyColumn(state = state) { + item { + MangaItem(manga) + } + if (chapters.isNotEmpty()) { + items(chapters) { chapter -> + ChapterItem( + chapter, + dateTimeFormatter::format, + onClick = { openReaderMenu(it, manga.id) }, + toggleRead = toggleRead, + toggleBookmarked = toggleBookmarked, + markPreviousAsRead = markPreviousRead, + onClickDownload = downloadChapter, + onClickDeleteChapter = deleteDownload, + onClickStopDownload = stopDownloadingChapter + ) + } + } else if (!isLoading) { + item { + ErrorScreen( + stringResource(MR.strings.no_chapters_found), + Modifier.height(400.dp).fillMaxWidth(), + retry = loadChapters + ) + } + } + } + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd) + .fillMaxHeight() + .padding(horizontal = 4.dp, vertical = 8.dp), + adapter = rememberScrollbarAdapter(state) + ) + } + } else if (!isLoading) { + ErrorScreen(stringResource(MR.strings.failed_manga_fetch), retry = loadManga) + } + } + } + if (isLoading) { + LoadingScreen() + } + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt index 0398a773..e0b31be8 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt @@ -71,13 +71,13 @@ import ca.gosyer.ui.reader.navigation.RightAndLeftNavigation import ca.gosyer.ui.reader.navigation.navigationClickable import ca.gosyer.ui.reader.viewer.ContinuousReader import ca.gosyer.ui.reader.viewer.PagerReader +import ca.gosyer.ui.util.compose.WindowGet +import ca.gosyer.ui.util.lang.launchApplication import ca.gosyer.uicore.components.ErrorScreen import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.mangaAspectRatio -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.WindowGet -import ca.gosyer.util.lang.launchApplication import ca.gosyer.uicore.resources.stringResource +import ca.gosyer.uicore.vm.LocalViewModelFactory import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -156,9 +156,8 @@ fun ReaderMenu( mangaId: Long, hotkeyFlow: SharedFlow ) { - val vm = viewModel { - instantiate(ReaderMenuViewModel.Params(chapterIndex, mangaId)) - } + val vmFactory = LocalViewModelFactory.current + val vm = remember { vmFactory.instantiate(ReaderMenuViewModel.Params(chapterIndex, mangaId)) } val state by vm.state.collectAsState() val previousChapter by vm.previousChapter.collectAsState() diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt index 562ca359..674a46f7 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt @@ -17,7 +17,6 @@ import ca.gosyer.data.reader.ReaderPreferences import ca.gosyer.data.reader.model.Direction import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.MangaInteractionHandler -import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.ui.reader.model.MoveTo import ca.gosyer.ui.reader.model.Navigation import ca.gosyer.ui.reader.model.PageMove @@ -25,6 +24,7 @@ import ca.gosyer.ui.reader.model.ReaderChapter import ca.gosyer.ui.reader.model.ReaderPage import ca.gosyer.ui.reader.model.ViewerChapters import ca.gosyer.uicore.prefs.asStateIn +import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -279,7 +279,7 @@ class ReaderMenuViewModel @Inject constructor( } } - override fun onDestroy() { + override fun onDispose() { viewerChapters.recycle() } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/loader/TachideskPageLoader.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/loader/TachideskPageLoader.kt index 8ea987f0..8bd388ed 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/loader/TachideskPageLoader.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/loader/TachideskPageLoader.kt @@ -12,7 +12,7 @@ import ca.gosyer.data.reader.ReaderPreferences import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.ui.reader.model.ReaderChapter import ca.gosyer.ui.reader.model.ReaderPage -import ca.gosyer.util.compose.toImageBitmap +import ca.gosyer.ui.util.compose.toImageBitmap import io.github.kerubistan.kroki.coroutines.priorityChannel import io.ktor.client.features.onDownload import kotlinx.coroutines.CoroutineScope diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/navigation/NavigationClickable.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/navigation/NavigationClickable.kt index 00ff2ff8..c262c2ab 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/navigation/NavigationClickable.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/navigation/NavigationClickable.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.toSize import ca.gosyer.ui.reader.model.Navigation -import ca.gosyer.util.compose.contains +import ca.gosyer.ui.util.compose.contains fun Modifier.navigationClickable( navigation: ViewerNavigation, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt index dee0cd36..b9db287b 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt @@ -21,14 +21,29 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.data.update.UpdatePreferences import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.SwitchPreference +import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow +import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey import me.tatarka.inject.annotations.Inject +class SettingsAdvancedScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + SettingsAdvancedScreenContent( + updatesEnabled = vm.updatesEnabled + ) + } +} + class SettingsAdvancedViewModel @Inject constructor( updatePreferences: UpdatePreferences, ) : ViewModel() { @@ -36,15 +51,16 @@ class SettingsAdvancedViewModel @Inject constructor( } @Composable -fun SettingsAdvancedScreen(menuController: MenuController) { - val vm = viewModel() +fun SettingsAdvancedScreenContent( + updatesEnabled: PreferenceMutableStateFlow +) { Column { - Toolbar(stringResource(MR.strings.settings_advanced_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_advanced_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { item { - SwitchPreference(preference = vm.updatesEnabled, title = stringResource(MR.strings.update_checker)) + SwitchPreference(preference = updatesEnabled, title = stringResource(MR.strings.update_checker)) } } VerticalScrollbar( diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsAppearanceScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsAppearanceScreen.kt index c0f0770c..6212a954 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsAppearanceScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsAppearanceScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.unit.sp import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.model.ThemeMode import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.ChoicePreference import ca.gosyer.ui.base.prefs.ColorPreference @@ -46,13 +45,33 @@ import ca.gosyer.ui.base.theme.AppColorsPreferenceState import ca.gosyer.ui.base.theme.asStateFlow import ca.gosyer.ui.base.theme.getDarkColors import ca.gosyer.ui.base.theme.getLightColors +import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow +import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.theme.Theme import ca.gosyer.uicore.theme.themes import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey import me.tatarka.inject.annotations.Inject +class SettingsAppearanceScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + SettingsAppearanceScreenContent( + activeColors = vm.getActiveColors(), + themeMode = vm.themeMode, + lightTheme = vm.lightTheme, + darkTheme = vm.darkTheme, + windowDecorations = vm.windowDecorations + ) + } +} + class ThemesViewModel @Inject constructor( private val uiPreferences: UiPreferences, ) : ViewModel() { @@ -72,23 +91,26 @@ class ThemesViewModel @Inject constructor( } @Composable -fun SettingsAppearance(menuController: MenuController) { - val vm = viewModel() - - val activeColors = vm.getActiveColors() +fun SettingsAppearanceScreenContent( + activeColors: AppColorsPreferenceState, + themeMode: PreferenceMutableStateFlow, + lightTheme: PreferenceMutableStateFlow, + darkTheme: PreferenceMutableStateFlow, + windowDecorations: PreferenceMutableStateFlow +) { val isLight = MaterialTheme.colors.isLight val themesForCurrentMode = remember(isLight) { themes.filter { it.colors.isLight == isLight } } Column { - Toolbar(stringResource(MR.strings.settings_appearance_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_appearance_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { item { ChoicePreference( - preference = vm.themeMode, + preference = themeMode, choices = mapOf( ThemeMode.System to stringResource(MR.strings.theme_follow_system), ThemeMode.Light to stringResource(MR.strings.theme_light), @@ -107,7 +129,7 @@ fun SettingsAppearance(menuController: MenuController) { ThemeItem( theme, onClick = { - (if (isLight) vm.lightTheme else vm.darkTheme).value = it.id + (if (isLight) lightTheme else darkTheme).value = it.id activeColors.primaryStateFlow.value = it.colors.primary activeColors.secondaryStateFlow.value = it.colors.secondary } @@ -133,7 +155,7 @@ fun SettingsAppearance(menuController: MenuController) { } item { SwitchPreference( - vm.windowDecorations, + windowDecorations, stringResource(MR.strings.window_decorations), stringResource(MR.strings.window_decorations_sub) ) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt index 610f0b03..cc19822d 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.material.icons.rounded.Warning import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -38,20 +37,23 @@ import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.server.interactions.BackupInteractionHandler import ca.gosyer.i18n.MR import ca.gosyer.ui.base.WindowDialog -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.PreferenceRow +import ca.gosyer.ui.util.system.filePicker +import ca.gosyer.ui.util.system.fileSaver +import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.system.filePicker -import ca.gosyer.util.system.fileSaver -import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey import io.ktor.client.features.onDownload import io.ktor.client.features.onUpload import io.ktor.http.isSuccess import io.ktor.utils.io.jvm.javaio.toInputStream import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -62,6 +64,29 @@ import okio.Path.Companion.toOkioPath import okio.buffer import okio.source +class SettingsBackupScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + SettingsBackupScreenContent( + restoring = vm.restoring.collectAsState().value, + restoringProgress = vm.restoringProgress.collectAsState().value, + restoreStatus = vm.restoreStatus.collectAsState().value, + creating = vm.creating.collectAsState().value, + creatingProgress = vm.creatingProgress.collectAsState().value, + creatingStatus = vm.creatingStatus.collectAsState().value, + missingSourceFlow = vm.missingSourceFlow, + createFlow = vm.createFlow, + restoreFile = vm::restoreFile, + restoreBackup = vm::restoreBackup, + stopRestore = vm::stopRestore, + exportBackup = vm::exportBackup + ) + } +} + class SettingsBackupViewModel @Inject constructor( private val backupHandler: BackupInteractionHandler ) : ViewModel() { @@ -186,22 +211,28 @@ class SettingsBackupViewModel @Inject constructor( } @Composable -fun SettingsBackupScreen(menuController: MenuController) { - val vm = viewModel() - val restoring by vm.restoring.collectAsState() - val restoringProgress by vm.restoringProgress.collectAsState() - val restoreStatus by vm.restoreStatus.collectAsState() - val creating by vm.creating.collectAsState() - val creatingProgress by vm.creatingProgress.collectAsState() - val creatingStatus by vm.creatingStatus.collectAsState() +private fun SettingsBackupScreenContent( + restoring: Boolean, + restoringProgress: Float?, + restoreStatus: SettingsBackupViewModel.Status, + creating: Boolean, + creatingProgress: Float?, + creatingStatus: SettingsBackupViewModel.Status, + missingSourceFlow: SharedFlow>>, + createFlow: SharedFlow Unit>>, + restoreFile: (Path?) -> Unit, + restoreBackup: (Path) -> Unit, + stopRestore: () -> Unit, + exportBackup: () -> Unit +) { LaunchedEffect(Unit) { launch { - vm.missingSourceFlow.collect { (backup, sources) -> - openMissingSourcesDialog(sources, { vm.restoreBackup(backup) }, vm::stopRestore) + missingSourceFlow.collect { (backup, sources) -> + openMissingSourcesDialog(sources, { restoreBackup(backup) }, stopRestore) } } launch { - vm.createFlow.collect { (filename, function) -> + createFlow.collect { (filename, function) -> fileSaver(filename, "proto.gz") { function(it.selectedFile.toOkioPath()) } @@ -210,7 +241,7 @@ fun SettingsBackupScreen(menuController: MenuController) { } Column { - Toolbar(stringResource(MR.strings.settings_backup_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_backup_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { @@ -223,7 +254,7 @@ fun SettingsBackupScreen(menuController: MenuController) { restoreStatus ) { filePicker("gz") { - vm.restoreFile(it.selectedFile.toOkioPath()) + restoreFile(it.selectedFile.toOkioPath()) } } PreferenceFile( @@ -232,7 +263,7 @@ fun SettingsBackupScreen(menuController: MenuController) { creating, creatingProgress, creatingStatus, - vm::exportBackup + exportBackup ) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBrowseScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBrowseScreen.kt index 3fe8ecfc..9a9cb2e2 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBrowseScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBrowseScreen.kt @@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +class SettingsBrowseScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + SettingsBrowseScreenContent() + } +} @Composable -fun SettingsBrowseScreen(menuController: MenuController) { +fun SettingsBrowseScreenContent() { Column { - Toolbar(stringResource(MR.strings.settings_browse_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_browse_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsDownloadsScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsDownloadsScreen.kt index 41625eca..8e27fdd7 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsDownloadsScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsDownloadsScreen.kt @@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +class SettingsDownloadsScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + SettingsDownloadsScreenContent() + } +} @Composable -fun SettingsDownloadsScreen(menuController: MenuController) { +fun SettingsDownloadsScreenContent() { Column { - Toolbar(stringResource(MR.strings.settings_download_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_download_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt index 0411f142..89589e6d 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt @@ -23,13 +23,16 @@ import androidx.compose.ui.unit.dp import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.model.StartScreen import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.ChoicePreference import ca.gosyer.ui.base.prefs.SwitchPreference +import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow +import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey import me.tatarka.inject.annotations.Inject import okio.Path.Companion.toPath import okio.asResourceFileSystem @@ -39,6 +42,24 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Locale +class SettingsGeneralScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + SettingsGeneralScreenContent( + startScreen = vm.startScreen, + startScreenChoices = vm.getStartScreenChoices(), + confirmExit = vm.confirmExit, + language = vm.language, + languageChoices = vm.getLanguageChoices(), + dateFormat = vm.dateFormat, + dateFormatChoices = vm.getDateChoices() + ) + } +} + class SettingsGeneralViewModel @Inject constructor( uiPreferences: UiPreferences, ) : ViewModel() { @@ -97,23 +118,30 @@ class SettingsGeneralViewModel @Inject constructor( } @Composable -fun SettingsGeneralScreen(menuController: MenuController) { - val vm = viewModel() +fun SettingsGeneralScreenContent( + startScreen: PreferenceMutableStateFlow, + startScreenChoices: Map, + confirmExit: PreferenceMutableStateFlow, + language: PreferenceMutableStateFlow, + languageChoices: Map, + dateFormat: PreferenceMutableStateFlow, + dateFormatChoices: Map +) { Column { - Toolbar(stringResource(MR.strings.settings_general_screen), menuController, closable = true) + Toolbar(stringResource(MR.strings.settings_general_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { item { ChoicePreference( - preference = vm.startScreen, + preference = startScreen, title = stringResource(MR.strings.start_screen), - choices = vm.getStartScreenChoices() + choices = startScreenChoices ) } item { SwitchPreference( - preference = vm.confirmExit, + preference = confirmExit, title = stringResource(MR.strings.confirm_exit) ) } @@ -122,16 +150,16 @@ fun SettingsGeneralScreen(menuController: MenuController) { } item { ChoicePreference( - preference = vm.language, + preference = language, title = stringResource(MR.strings.language), - choices = vm.getLanguageChoices(), + choices = languageChoices, ) } item { ChoicePreference( - preference = vm.dateFormat, + preference = dateFormat, title = stringResource(MR.strings.date_format), - choices = vm.getDateChoices() + choices = dateFormatChoices ) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt index 6c2fdb1d..ecc58193 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt @@ -23,19 +23,36 @@ import androidx.compose.ui.unit.dp import ca.gosyer.data.library.LibraryPreferences import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.SwitchPreference +import ca.gosyer.ui.categories.openCategoriesMenu +import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow +import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.ui.categories.openCategoriesMenu -import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject +class SettingsLibraryScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + SettingsLibraryScreenContent( + showAllCategory = vm.showAllCategory, + refreshCategoryCount = vm::refreshCategoryCount, + categoriesSize = vm.categories.collectAsState().value + ) + } +} + class SettingsLibraryViewModel @Inject constructor( libraryPreferences: LibraryPreferences, private val categoryHandler: CategoryInteractionHandler @@ -57,25 +74,27 @@ class SettingsLibraryViewModel @Inject constructor( } @Composable -fun SettingsLibraryScreen(menuController: MenuController) { - val vm = viewModel() - +fun SettingsLibraryScreenContent( + showAllCategory: PreferenceMutableStateFlow, + refreshCategoryCount: () -> Unit, + categoriesSize: Int +) { Column { - Toolbar(stringResource(MR.strings.settings_library_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_library_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { item { SwitchPreference( - preference = vm.showAllCategory, + preference = showAllCategory, title = stringResource(MR.strings.show_all_category) ) } item { PreferenceRow( stringResource(MR.strings.location_categories), - onClick = { openCategoriesMenu(vm::refreshCategoryCount) }, - subtitle = vm.categories.collectAsState().value.toString() + onClick = { openCategoriesMenu(refreshCategoryCount) }, + subtitle = categoriesSize.toString() ) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsParentalControlsScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsParentalControlsScreen.kt index c67807fd..2fa4d857 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsParentalControlsScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsParentalControlsScreen.kt @@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +class SettingsParentalControlsScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + SettingsParentalControlsScreenContent() + } +} @Composable -fun SettingsParentalControlsScreen(menuController: MenuController) { +fun SettingsParentalControlsScreenContent() { Column { - Toolbar(stringResource(MR.strings.settings_parental_control_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_parental_control_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsReaderScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsReaderScreen.kt index 3d9687fd..39bcc877 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsReaderScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsReaderScreen.kt @@ -29,16 +29,18 @@ import ca.gosyer.data.reader.model.Direction import ca.gosyer.data.reader.model.ImageScale import ca.gosyer.data.reader.model.NavigationMode import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.ChoicePreference import ca.gosyer.ui.base.prefs.ExpandablePreference import ca.gosyer.ui.base.prefs.SwitchPreference import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow import ca.gosyer.uicore.prefs.asStateIn +import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -46,6 +48,25 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject +class SettingsReaderScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + SettingsReaderScreenContent( + modes = vm.modes.collectAsState().value.associateWith { it }, + selectedMode = vm.selectedMode, + modeSettings = vm.modeSettings.collectAsState().value, + directionChoices = vm.getDirectionChoices(), + paddingChoices = vm.getPaddingChoices(), + getMaxSizeChoices = vm::getMaxSizeChoices, + imageScaleChoices = vm.getImageScaleChoices(), + navigationModeChoices = vm.getNavigationModeChoices() + ) + } +} + class SettingsReaderViewModel @Inject constructor( readerPreferences: ReaderPreferences ) : ViewModel() { @@ -125,18 +146,25 @@ data class ReaderModePreference( } @Composable -fun SettingsReaderScreen(menuController: MenuController) { - val vm = viewModel() - val modeSettings by vm.modeSettings.collectAsState() +fun SettingsReaderScreenContent( + modes: Map, + selectedMode: PreferenceMutableStateFlow, + modeSettings: List, + directionChoices: Map, + paddingChoices: Map, + getMaxSizeChoices: (Direction) -> Map, + imageScaleChoices: Map, + navigationModeChoices: Map +) { Column { - Toolbar(stringResource(MR.strings.settings_reader), menuController, true) + Toolbar(stringResource(MR.strings.settings_reader)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { item { ChoicePreference( - vm.selectedMode, - vm.modes.collectAsState().value.associateWith { it }, + selectedMode, + modes, stringResource(MR.strings.reader_mode) ) } @@ -148,7 +176,7 @@ fun SettingsReaderScreen(menuController: MenuController) { ExpandablePreference(it.mode) { ChoicePreference( it.direction, - vm.getDirectionChoices(), + directionChoices, stringResource(MR.strings.direction), enabled = !it.defaultMode ) @@ -162,7 +190,7 @@ fun SettingsReaderScreen(menuController: MenuController) { if (continuous) { ChoicePreference( it.padding, - vm.getPaddingChoices(), + paddingChoices, stringResource(MR.strings.page_padding) ) val direction by it.direction.collectAsState() @@ -190,20 +218,20 @@ fun SettingsReaderScreen(menuController: MenuController) { } ChoicePreference( it.maxSize, - vm.getMaxSizeChoices(direction), + getMaxSizeChoices(direction), maxSizeTitle, maxSizeSubtitle ) } else { ChoicePreference( it.imageScale, - vm.getImageScaleChoices(), + imageScaleChoices, stringResource(MR.strings.image_scale) ) } ChoicePreference( it.navigationMode, - vm.getNavigationModeChoices(), + navigationModeChoices, stringResource(MR.strings.navigation_mode) ) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt index a9db70c4..224587a5 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt @@ -29,14 +29,28 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.PreferenceRow -import ca.gosyer.ui.main.Routes import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow + +class SettingsScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + SettingsScreenContent(navigator = LocalNavigator.currentOrThrow) + } +} @Composable -fun SettingsScreen(menuController: MenuController) { +fun SettingsScreenContent(navigator: Navigator) { Column { Toolbar(stringResource(MR.strings.location_settings), closable = false) Box { @@ -46,84 +60,84 @@ fun SettingsScreen(menuController: MenuController) { PreferenceRow( title = stringResource(MR.strings.settings_general), icon = Icons.Rounded.Tune, - onClick = { menuController.push(Routes.SettingsGeneral) } + onClick = { navigator push SettingsGeneralScreen() } ) } item { PreferenceRow( title = stringResource(MR.strings.settings_appearance), icon = Icons.Rounded.Palette, - onClick = { menuController.push(Routes.SettingsAppearance) } + onClick = { navigator push SettingsAppearanceScreen() } ) } item { PreferenceRow( title = stringResource(MR.strings.settings_server), icon = Icons.Rounded.Computer, - onClick = { menuController.push(Routes.SettingsServer) } + onClick = { navigator push SettingsServerScreen() } ) } item { PreferenceRow( title = stringResource(MR.strings.settings_library), icon = Icons.Rounded.CollectionsBookmark, - onClick = { menuController.push(Routes.SettingsLibrary) } + onClick = { navigator push SettingsLibraryScreen() } ) } item { PreferenceRow( title = stringResource(MR.strings.settings_reader), icon = Icons.Rounded.ChromeReaderMode, - onClick = { menuController.push(Routes.SettingsReader) } + onClick = { navigator push SettingsReaderScreen() } ) } /*item { Pref( title = stringResource(MR.strings.settings_download), icon = Icons.Rounded.GetApp, - onClick = { navController.push(Route.SettingsDownloads) } + onClick = { navigator push SettingsDownloadsScreen() } ) } item { Pref( title = stringResource(MR.strings.settings_tracking), icon = Icons.Rounded.Sync, - onClick = { navController.push(Route.SettingsTracking) } + onClick = { navigator push SettingsTrackingScreen() } ) }*/ item { PreferenceRow( title = stringResource(MR.strings.settings_browse), icon = Icons.Rounded.Explore, - onClick = { menuController.push(Routes.SettingsBrowse) } + onClick = { navigator push SettingsBrowseScreen() } ) } item { PreferenceRow( title = stringResource(MR.strings.settings_backup), icon = Icons.Rounded.Backup, - onClick = { menuController.push(Routes.SettingsBackup) } + onClick = { navigator push SettingsBackupScreen() } ) } /*item { Pref( title = stringResource(MR.strings.settings_security), icon = Icons.Rounded.Security, - onClick = { navController.push(Route.SettingsSecurity) } + onClick = { navigator push SettingsSecurityScreen() } ) } item { Pref( title = stringResource(MR.strings.settings_parental_controls), icon = Icons.Rounded.PeopleOutline, - onClick = { navController.push(Route.SettingsParentalControls) } + onClick = { navigator push SettingsParentalControlsScreen() } ) }*/ item { PreferenceRow( title = stringResource(MR.strings.settings_advanced), icon = Icons.Rounded.Code, - onClick = { menuController.push(Routes.SettingsAdvanced) } + onClick = { navigator push SettingsAdvancedScreen() } ) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsSecurityScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsSecurityScreen.kt index 95f653fe..84c60c65 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsSecurityScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsSecurityScreen.kt @@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +class SettingsSecurityScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + SettingsSecurityScreenContent() + } +} @Composable -fun SettingsSecurityScreen(menuController: MenuController) { +fun SettingsSecurityScreenContent() { Column { - Toolbar(stringResource(MR.strings.settings_security_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_security_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt index ed56a31b..2bf46122 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt @@ -34,23 +34,68 @@ import ca.gosyer.data.server.ServerService import ca.gosyer.data.server.model.Auth import ca.gosyer.data.server.model.Proxy import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.ChoicePreference import ca.gosyer.ui.base.prefs.EditTextPreference import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.SwitchPreference +import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow import ca.gosyer.uicore.prefs.asStateIn import ca.gosyer.uicore.prefs.asStringStateIn +import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import me.tatarka.inject.annotations.Inject +class SettingsServerScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + SettingsServerScreenContent( + hostValue = vm.host.collectAsState().value, + basicAuthEnabledValue = vm.basicAuthEnabled.collectAsState().value, + proxyValue = vm.proxy.collectAsState().value, + authValue = vm.auth.collectAsState().value, + restartServer = vm::restartServer, + serverSettingChanged = vm::serverSettingChanged, + host = vm.host, + ip = vm.ip, + port = vm.port, + socksProxyEnabled = vm.socksProxyEnabled, + socksProxyHost = vm.socksProxyHost, + socksProxyPort = vm.socksProxyPort, + debugLogsEnabled = vm.debugLogsEnabled, + systemTrayEnabled = vm.systemTrayEnabled, + webUIEnabled = vm.webUIEnabled, + openInBrowserEnabled = vm.openInBrowserEnabled, + basicAuthEnabled = vm.basicAuthEnabled, + basicAuthUsername = vm.basicAuthUsername, + basicAuthPassword = vm.basicAuthPassword, + serverUrl = vm.serverUrl, + serverPort = vm.serverPort, + proxy = vm.proxy, + proxyChoices = vm.getProxyChoices(), + httpHost = vm.httpHost, + httpPort = vm.httpPort, + socksHost = vm.socksHost, + socksPort = vm.socksPort, + auth = vm.auth, + authChoices = vm.getAuthChoices(), + authUsername = vm.authUsername, + authPassword = vm.authPassword + ) + } +} + class SettingsServerViewModel @Inject constructor( serverPreferences: ServerPreferences, serverHostPreferences: ServerHostPreferences, @@ -136,26 +181,53 @@ class SettingsServerViewModel @Inject constructor( } @Composable -fun SettingsServerScreen(menuController: MenuController) { - val vm = viewModel() - val host by vm.host.collectAsState() - val basicAuthEnabled by vm.basicAuthEnabled.collectAsState() - val proxy by vm.proxy.collectAsState() - val auth by vm.auth.collectAsState() +fun SettingsServerScreenContent( + hostValue: Boolean, + basicAuthEnabledValue: Boolean, + proxyValue: Proxy, + authValue: Auth, + restartServer: () -> Unit, + serverSettingChanged: () -> Unit, + host: PreferenceMutableStateFlow, + ip: PreferenceMutableStateFlow, + port: PreferenceMutableStateFlow, + socksProxyEnabled: PreferenceMutableStateFlow, + socksProxyHost: PreferenceMutableStateFlow, + socksProxyPort: PreferenceMutableStateFlow, + debugLogsEnabled: PreferenceMutableStateFlow, + systemTrayEnabled: PreferenceMutableStateFlow, + webUIEnabled: PreferenceMutableStateFlow, + openInBrowserEnabled: PreferenceMutableStateFlow, + basicAuthEnabled: PreferenceMutableStateFlow, + basicAuthUsername: PreferenceMutableStateFlow, + basicAuthPassword: PreferenceMutableStateFlow, + serverUrl: PreferenceMutableStateFlow, + serverPort: PreferenceMutableStateFlow, + proxy: PreferenceMutableStateFlow, + proxyChoices: Map, + httpHost: PreferenceMutableStateFlow, + httpPort: PreferenceMutableStateFlow, + socksHost: PreferenceMutableStateFlow, + socksPort: PreferenceMutableStateFlow, + auth: PreferenceMutableStateFlow, + authChoices: Map, + authUsername: PreferenceMutableStateFlow, + authPassword: PreferenceMutableStateFlow +) { DisposableEffect(Unit) { onDispose { - vm.restartServer() + restartServer() } } Column { - Toolbar(stringResource(MR.strings.settings_server_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_server_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { item { - SwitchPreference(preference = vm.host, title = stringResource(MR.strings.host_server)) + SwitchPreference(preference = host, title = stringResource(MR.strings.host_server)) } - if (host) { + if (hostValue) { item { PreferenceRow( stringResource(MR.strings.host_settings), @@ -164,105 +236,105 @@ fun SettingsServerScreen(menuController: MenuController) { ) } item { - val ip by vm.ip.collectAsState() + val ipValue by ip.collectAsState() EditTextPreference( - preference = vm.ip, + preference = ip, title = stringResource(MR.strings.host_ip), - subtitle = stringResource(MR.strings.host_ip_sub, ip), - changeListener = vm::serverSettingChanged + subtitle = stringResource(MR.strings.host_ip_sub, ipValue), + changeListener = serverSettingChanged ) } item { - val port by vm.port.collectAsState() + val portValue by port.collectAsState() EditTextPreference( - preference = vm.port, + preference = port, title = stringResource(MR.strings.host_port), - subtitle = stringResource(MR.strings.host_port_sub, port), - changeListener = vm::serverSettingChanged + subtitle = stringResource(MR.strings.host_port_sub, portValue), + changeListener = serverSettingChanged ) } item { SwitchPreference( - preference = vm.socksProxyEnabled, + preference = socksProxyEnabled, title = stringResource(MR.strings.host_socks_enabled), - changeListener = vm::serverSettingChanged + changeListener = serverSettingChanged ) } item { - val proxyHost by vm.socksProxyHost.collectAsState() + val proxyHost by socksProxyHost.collectAsState() EditTextPreference( - preference = vm.socksProxyHost, + preference = socksProxyHost, title = stringResource(MR.strings.host_socks_host), subtitle = stringResource(MR.strings.host_socks_host_sub, proxyHost), - changeListener = vm::serverSettingChanged + changeListener = serverSettingChanged ) } item { - val proxyPort by vm.socksProxyPort.collectAsState() + val proxyPort by socksProxyPort.collectAsState() EditTextPreference( - preference = vm.socksProxyPort, + preference = socksProxyPort, title = stringResource(MR.strings.host_socks_port), subtitle = stringResource(MR.strings.host_socks_port_sub, proxyPort), - changeListener = vm::serverSettingChanged + changeListener = serverSettingChanged ) } item { SwitchPreference( - preference = vm.debugLogsEnabled, + preference = debugLogsEnabled, title = stringResource(MR.strings.host_debug_logging), subtitle = stringResource(MR.strings.host_debug_logging_sub), - changeListener = vm::serverSettingChanged + changeListener = serverSettingChanged ) } item { SwitchPreference( - preference = vm.systemTrayEnabled, + preference = systemTrayEnabled, title = stringResource(MR.strings.host_system_tray), subtitle = stringResource(MR.strings.host_system_tray_sub), - changeListener = vm::serverSettingChanged + changeListener = serverSettingChanged ) } item { SwitchPreference( - preference = vm.webUIEnabled, + preference = webUIEnabled, title = stringResource(MR.strings.host_webui), subtitle = stringResource(MR.strings.host_webui_sub), - changeListener = vm::serverSettingChanged + changeListener = serverSettingChanged ) } item { - val webUIEnabled by vm.webUIEnabled.collectAsState() + val webUIEnabledValue by webUIEnabled.collectAsState() SwitchPreference( - preference = vm.openInBrowserEnabled, + preference = openInBrowserEnabled, title = stringResource(MR.strings.host_open_in_browser), subtitle = stringResource(MR.strings.host_open_in_browser_sub), - changeListener = vm::serverSettingChanged, - enabled = webUIEnabled + changeListener = serverSettingChanged, + enabled = webUIEnabledValue ) } item { SwitchPreference( - preference = vm.basicAuthEnabled, + preference = basicAuthEnabled, title = stringResource(MR.strings.basic_auth), subtitle = stringResource(MR.strings.host_basic_auth_sub), - changeListener = vm::serverSettingChanged + changeListener = serverSettingChanged ) } item { EditTextPreference( - preference = vm.basicAuthUsername, + preference = basicAuthUsername, title = stringResource(MR.strings.host_basic_auth_username), - changeListener = vm::serverSettingChanged, - enabled = basicAuthEnabled + changeListener = serverSettingChanged, + enabled = basicAuthEnabledValue ) } item { EditTextPreference( - preference = vm.basicAuthPassword, + preference = basicAuthPassword, title = stringResource(MR.strings.host_basic_auth_password), - changeListener = vm::serverSettingChanged, + changeListener = serverSettingChanged, visualTransformation = PasswordVisualTransformation(), - enabled = basicAuthEnabled + enabled = basicAuthEnabledValue ) } } @@ -271,16 +343,16 @@ fun SettingsServerScreen(menuController: MenuController) { } item { EditTextPreference( - vm.serverUrl, + serverUrl, stringResource(MR.strings.server_url), - subtitle = vm.serverUrl.collectAsState().value + subtitle = serverUrl.collectAsState().value ) } item { EditTextPreference( - vm.serverPort, + serverPort, stringResource(MR.strings.server_port), - subtitle = vm.serverPort.collectAsState().value + subtitle = serverPort.collectAsState().value ) } @@ -292,53 +364,53 @@ fun SettingsServerScreen(menuController: MenuController) { ) } item { - ChoicePreference(vm.proxy, vm.getProxyChoices(), stringResource(MR.strings.server_proxy)) + ChoicePreference(proxy, proxyChoices, stringResource(MR.strings.server_proxy)) } - when (proxy) { + when (proxyValue) { Proxy.NO_PROXY -> Unit Proxy.HTTP_PROXY -> { item { EditTextPreference( - vm.httpHost, + httpHost, stringResource(MR.strings.http_proxy), - vm.httpHost.collectAsState().value + httpHost.collectAsState().value ) } item { EditTextPreference( - vm.httpPort, + httpPort, stringResource(MR.strings.http_port), - vm.httpPort.collectAsState().value + httpPort.collectAsState().value ) } } Proxy.SOCKS_PROXY -> { item { EditTextPreference( - vm.socksHost, + socksHost, stringResource(MR.strings.socks_proxy), - vm.socksHost.collectAsState().value + socksHost.collectAsState().value ) } item { EditTextPreference( - vm.socksPort, + socksPort, stringResource(MR.strings.socks_port), - vm.socksPort.collectAsState().value + socksPort.collectAsState().value ) } } } item { - ChoicePreference(vm.auth, vm.getAuthChoices(), stringResource(MR.strings.authentication)) + ChoicePreference(auth, authChoices, stringResource(MR.strings.authentication)) } - if (auth != Auth.NONE) { + if (authValue != Auth.NONE) { item { - EditTextPreference(vm.authUsername, stringResource(MR.strings.auth_username)) + EditTextPreference(authUsername, stringResource(MR.strings.auth_username)) } item { EditTextPreference( - vm.authPassword, + authPassword, stringResource(MR.strings.auth_password), visualTransformation = PasswordVisualTransformation() ) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsTrackingScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsTrackingScreen.kt index 97153a87..0442bc6e 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsTrackingScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsTrackingScreen.kt @@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.uicore.resources.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +class SettingsTrackingScreen : Screen { + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + SettingsTrackingScreenContent() + } +} @Composable -fun SettingsTrackingScreen(menuController: MenuController) { +fun SettingsTrackingScreenContent() { Column { - Toolbar(stringResource(MR.strings.settings_tracking_screen), menuController, true) + Toolbar(stringResource(MR.strings.settings_tracking_screen)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt deleted file mode 100644 index caec3fa8..00000000 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package ca.gosyer.ui.sources - -import ca.gosyer.core.logging.CKLogger -import ca.gosyer.data.models.Source -import ca.gosyer.uicore.vm.ViewModel -import com.github.zsoltk.compose.savedinstancestate.Bundle -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import me.tatarka.inject.annotations.Inject - -class SourcesMenuViewModel @Inject constructor( - private val bundle: Bundle -) : ViewModel() { - private val _sourceTabs = MutableStateFlow>(listOf(null)) - val sourceTabs = _sourceTabs.asStateFlow() - - private val _selectedSourceTab = MutableStateFlow(null) - val selectedSourceTab = _selectedSourceTab.asStateFlow() - - init { - _sourceTabs.drop(1) - .onEach { sources -> - bundle.putLongArray(SOURCE_TABS_KEY, sources.mapNotNull { it?.id }.toLongArray()) - } - .launchIn(scope) - - _selectedSourceTab.drop(1) - .onEach { - if (it != null) { - bundle.putLong(SELECTED_SOURCE_TAB, it.id) - } else { - bundle.remove(SELECTED_SOURCE_TAB) - } - } - .launchIn(scope) - } - - fun selectTab(source: Source?) { - _selectedSourceTab.value = source - } - - fun addTab(source: Source) { - if (source !in _sourceTabs.value) { - _sourceTabs.value += source - } - selectTab(source) - } - - fun closeTab(source: Source) { - _sourceTabs.value -= source - if (selectedSourceTab.value?.id == source.id) { - _selectedSourceTab.value = null - } - bundle.remove(source.id.toString()) - } - - fun setLoadedSources(sources: List) { - val sourceTabs = bundle.getLongArray(SOURCE_TABS_KEY) - if (sourceTabs != null) { - _sourceTabs.value = listOf(null) + sourceTabs.toList() - .mapNotNull { sourceId -> - sources.find { it.id == sourceId } - } - _selectedSourceTab.value = bundle.getLong(SELECTED_SOURCE_TAB, -1).let { id -> - if (id != -1L) { - sources.find { it.id == id } - } else null - } - } - } - - private companion object : CKLogger({}) { - const val SOURCE_TABS_KEY = "source_tabs" - const val SELECTED_SOURCE_TAB = "selected_tab" - } -} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreen.kt new file mode 100644 index 00000000..9dcb8a60 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreen.kt @@ -0,0 +1,49 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import ca.gosyer.presentation.build.BuildKonfig +import ca.gosyer.ui.sources.components.SourcesMenu +import ca.gosyer.ui.util.compose.ThemedWindow +import ca.gosyer.ui.util.lang.launchApplication +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.Navigator +import kotlinx.coroutines.DelicateCoroutinesApi + +@OptIn(DelicateCoroutinesApi::class) +fun openSourcesMenu() { + launchApplication { + ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { + Surface { + Navigator(remember { SourcesScreen() }) + } + } + } +} + +class SourcesScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + SourcesMenu( + sourceTabs = vm.sourceTabs.collectAsState().value, + selectedSourceTab = vm.selectedSourceTab.collectAsState().value, + selectTab = vm::selectTab, + closeTab = vm::closeTab + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreenViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreenViewModel.kt new file mode 100644 index 00000000..3f8bf11a --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreenViewModel.kt @@ -0,0 +1,35 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources + +import ca.gosyer.data.models.Source +import ca.gosyer.uicore.vm.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import me.tatarka.inject.annotations.Inject + +class SourcesScreenViewModel @Inject constructor() : ViewModel() { + private val _sourceTabs = MutableStateFlow>(listOf(null)) + val sourceTabs = _sourceTabs.asStateFlow() + + private val _selectedSourceTab = MutableStateFlow(null) + val selectedSourceTab = _selectedSourceTab.asStateFlow() + + fun selectTab(source: Source?) { + if (source !in _sourceTabs.value) { + _sourceTabs.value += source + } + _selectedSourceTab.value = source + } + + fun closeTab(source: Source) { + _sourceTabs.value -= source + if (selectedSourceTab.value?.id == source.id) { + _selectedSourceTab.value = null + } + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreen.kt new file mode 100644 index 00000000..bfee4c0c --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreen.kt @@ -0,0 +1,65 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources.browse + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import ca.gosyer.data.models.Source +import ca.gosyer.ui.manga.MangaScreen +import ca.gosyer.ui.sources.browse.components.SourceScreenContent +import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel +import ca.gosyer.ui.sources.components.LocalSourcesNavigator +import ca.gosyer.ui.sources.settings.SourceSettingsScreen +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow + +class SourceScreen(val source: Source) : Screen { + + override val key: ScreenKey = source.id.toString() + + @Composable + override fun Content() { + val sourceVM = viewModel { + instantiate(SourceScreenViewModel.Params(source)) + } + val filterVM = viewModel { + instantiate(SourceFiltersViewModel.Params(source.id)) + } + val navigator = LocalNavigator.currentOrThrow + val sourcesNavigator = LocalSourcesNavigator.current + SourceScreenContent( + source = source, + onMangaClick = { navigator push MangaScreen(it) }, + onCloseSourceTabClick = sourcesNavigator::remove, + onSourceSettingsClick = { navigator push SourceSettingsScreen(it) }, + mangas = sourceVM.mangas.collectAsState().value, + hasNextPage = sourceVM.hasNextPage.collectAsState().value, + loading = sourceVM.loading.collectAsState().value, + isLatest = sourceVM.isLatest.collectAsState().value, + showLatestButton = source.supportsLatest, + sourceSearchQuery = sourceVM.sourceSearchQuery.collectAsState().value, + enableLatest = sourceVM::enableLatest, + search = sourceVM::search, + submitSearch = sourceVM::submitSearch, + setMode = sourceVM::setMode, + loadNextPage = sourceVM::loadNextPage, + setUsingFilters = sourceVM::setUsingFilters, + // FilterVM + filters = filterVM.filters.collectAsState().value, + showingFilters = filterVM.showingFilters.collectAsState().value, + showFilterButton = filterVM.filterButtonEnabled.collectAsState().value, + setShowingFilters = filterVM::showingFilters, + resetFiltersClicked = { + sourceVM.setUsingFilters(false) + filterVM.resetFilters() + } + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreenViewModel.kt similarity index 61% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreenViewModel.kt index 294e5ea3..23bd6af6 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreenViewModel.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources.components +package ca.gosyer.ui.sources.browse import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.data.models.Manga @@ -12,11 +12,6 @@ import ca.gosyer.data.models.MangaPage import ca.gosyer.data.models.Source import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.uicore.vm.ViewModel -import ca.gosyer.util.compose.saveBooleanInBundle -import ca.gosyer.util.compose.saveIntInBundle -import ca.gosyer.util.compose.saveObjectInBundle -import ca.gosyer.util.compose.saveStringInBundle -import com.github.zsoltk.compose.savedinstancestate.Bundle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -24,7 +19,6 @@ import me.tatarka.inject.annotations.Inject class SourceScreenViewModel( private val source: Source, - private val bundle: Bundle, private val sourceHandler: SourceInteractionHandler ) : ViewModel() { @@ -33,49 +27,40 @@ class SourceScreenViewModel( params: Params ) : this( params.source, - params.bundle, sourceHandler ) - private val _mangas = saveObjectInBundle(scope, bundle, MANGAS_KEY) { emptyList() } + private val _mangas = MutableStateFlow(emptyList()) val mangas = _mangas.asStateFlow() - private val _hasNextPage = saveBooleanInBundle(scope, bundle, NEXT_PAGE_KEY, false) + private val _hasNextPage = MutableStateFlow(false) val hasNextPage = _hasNextPage.asStateFlow() private val _loading = MutableStateFlow(true) val loading = _loading.asStateFlow() - private val _isLatest = saveBooleanInBundle(scope, bundle, IS_LATEST_KEY, false) + private val _isLatest = MutableStateFlow(false) val isLatest = _isLatest.asStateFlow() - private val _filterButtonEnabled = saveBooleanInBundle(scope, bundle, SHOW_FILTERS, false) - val filterButtonEnabled = _filterButtonEnabled.asStateFlow() - - private val _latestButtonEnabled = saveBooleanInBundle(scope, bundle, SHOW_LATEST, false) + private val _latestButtonEnabled = MutableStateFlow(false) val latestButtonEnabled = _latestButtonEnabled.asStateFlow() - private val _showingFilters = MutableStateFlow(false) - val showingFilters = _showingFilters.asStateFlow() - private val _usingFilters = MutableStateFlow(false) private val _sourceSearchQuery = MutableStateFlow(null) val sourceSearchQuery = _sourceSearchQuery.asStateFlow() - private val _query = saveStringInBundle(scope, bundle, QUERY_KEY) { null } + private val _query = MutableStateFlow(null) - private val _pageNum = saveIntInBundle(scope, bundle, PAGE_NUM_KEY, 1) + private val _pageNum = MutableStateFlow(1) val pageNum = _pageNum.asStateFlow() init { scope.launch { try { - if (bundle[MANGAS_KEY] == null) { - val (mangas, hasNextPage) = getPage() - _mangas.value = mangas - _hasNextPage.value = hasNextPage - } + val (mangas, hasNextPage) = getPage() + _mangas.value = mangas + _hasNextPage.value = hasNextPage } catch (e: Exception) { e.throwIfCancellation() } finally { @@ -103,19 +88,8 @@ class SourceScreenViewModel( } } - private fun cleanBundle(removeMode: Boolean = true) { - bundle.remove(MANGAS_KEY) - bundle.remove(NEXT_PAGE_KEY) - bundle.remove(PAGE_NUM_KEY) - if (removeMode) { - bundle.remove(IS_LATEST_KEY) - } - bundle.remove(QUERY_KEY) - } - fun setMode(toLatest: Boolean) { if (isLatest.value != toLatest) { - cleanBundle() _isLatest.value = toLatest // [loadNextPage] increments by 1 _pageNum.value = 0 @@ -135,7 +109,6 @@ class SourceScreenViewModel( } fun startSearch(query: String?) { - cleanBundle(false) _pageNum.value = 0 _hasNextPage.value = true _loading.value = true @@ -144,15 +117,9 @@ class SourceScreenViewModel( loadNextPage() } - fun showingFilters(show: Boolean) { - _showingFilters.value = show - } fun setUsingFilters(usingFilters: Boolean) { _usingFilters.value = usingFilters } - fun enableFilters(enabled: Boolean) { - _filterButtonEnabled.value = enabled - } fun enableLatest(enabled: Boolean) { _latestButtonEnabled.value = enabled } @@ -164,15 +131,5 @@ class SourceScreenViewModel( startSearch(sourceSearchQuery.value) } - data class Params(val source: Source, val bundle: Bundle) - - private companion object { - const val MANGAS_KEY = "mangas" - const val NEXT_PAGE_KEY = "next_page" - const val PAGE_NUM_KEY = "page_num" - const val IS_LATEST_KEY = "is_latest" - const val SHOW_FILTERS = "show_filters" - const val SHOW_LATEST = "show_latest" - const val QUERY_KEY = "query" - } + data class Params(val source: Source) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/components/SourceScreenContent.kt similarity index 69% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/components/SourceScreenContent.kt index 4bda9be4..f9d57994 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/components/SourceScreenContent.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources.components +package ca.gosyer.ui.sources.browse.components import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.layout.Box @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Explore @@ -22,8 +23,6 @@ import androidx.compose.material.icons.rounded.NewReleases import androidx.compose.material.icons.rounded.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality @@ -33,38 +32,40 @@ import ca.gosyer.data.models.Source import ca.gosyer.i18n.MR import ca.gosyer.ui.base.navigation.TextActionIcon import ca.gosyer.ui.base.navigation.Toolbar -import ca.gosyer.ui.sources.components.filter.SourceFiltersMenu +import ca.gosyer.ui.sources.browse.filter.SourceFiltersMenu +import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.MangaGridItem -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.persistentLazyListState -import com.github.zsoltk.compose.savedinstancestate.Bundle -import com.github.zsoltk.compose.savedinstancestate.BundleScope import ca.gosyer.uicore.resources.stringResource import io.kamel.image.lazyPainterResource @Composable -fun SourceScreen( - bundle: Bundle, +fun SourceScreenContent( source: Source, onMangaClick: (Long) -> Unit, onCloseSourceTabClick: (Source) -> Unit, - onSourceSettingsClick: (Long) -> Unit + onSourceSettingsClick: (Long) -> Unit, + mangas: List, + hasNextPage: Boolean, + loading: Boolean, + isLatest: Boolean, + showLatestButton: Boolean, + sourceSearchQuery: String?, + enableLatest: (Boolean) -> Unit, + search: (String) -> Unit, + submitSearch: () -> Unit, + setMode: (Boolean) -> Unit, + loadNextPage: () -> Unit, + setUsingFilters: (Boolean) -> Unit, + // filter + filters: List>, + showingFilters: Boolean, + showFilterButton: Boolean, + setShowingFilters: (Boolean) -> Unit, + resetFiltersClicked: () -> Unit ) { - val vm = viewModel(source.id) { - instantiate(SourceScreenViewModel.Params(source, bundle)) - } - val mangas by vm.mangas.collectAsState() - val hasNextPage by vm.hasNextPage.collectAsState() - val loading by vm.loading.collectAsState() - val isLatest by vm.isLatest.collectAsState() - val showingFilters by vm.showingFilters.collectAsState() - val showFilterButton by vm.filterButtonEnabled.collectAsState() - val showLatestButton by vm.latestButtonEnabled.collectAsState() - val sourceSearchQuery by vm.sourceSearchQuery.collectAsState() - - LaunchedEffect(vm to source) { - vm.enableLatest(source.supportsLatest) + LaunchedEffect(source) { + enableLatest(source.supportsLatest) } Column { @@ -72,44 +73,35 @@ fun SourceScreen( source = source, onCloseSourceTabClick = onCloseSourceTabClick, sourceSearchQuery = sourceSearchQuery, - onSearch = vm::search, - onSubmitSearch = vm::submitSearch, + onSearch = search, + onSubmitSearch = submitSearch, onSourceSettingsClick = onSourceSettingsClick, showFilterButton = showFilterButton, showLatestButton = showLatestButton, isLatest = isLatest, showingFilters = showingFilters, - onClickMode = vm::setMode, - onToggleFiltersClick = vm::showingFilters, + onClickMode = setMode, + onToggleFiltersClick = setShowingFilters, ) Box { MangaTable( - bundle = bundle, mangas = mangas, isLoading = loading, hasNextPage = hasNextPage, - onLoadNextPage = vm::loadNextPage, + onLoadNextPage = loadNextPage, onMangaClick = onMangaClick, ) - BundleScope("filters", autoDispose = false) { - SourceFiltersMenu( - bundle = bundle, - modifier = Modifier.align(Alignment.TopEnd), - sourceId = source.id, - showFilters = showingFilters && !isLatest, - onSearchClicked = { - vm.setUsingFilters(true) - vm.showingFilters(false) - vm.submitSearch() - }, - onResetClicked = { - vm.setUsingFilters(false) - vm.showingFilters(false) - vm.submitSearch() - }, - showFiltersButton = vm::enableFilters - ) - } + SourceFiltersMenu( + modifier = Modifier.align(Alignment.TopEnd), + showFilters = showingFilters && !isLatest, + filters = filters, + onSearchClicked = { + setUsingFilters(true) + setShowingFilters(false) + submitSearch() + }, + resetFiltersClicked = resetFiltersClicked + ) } } } @@ -183,7 +175,6 @@ fun SourceToolbar( @Composable private fun MangaTable( - bundle: Bundle, mangas: List, isLoading: Boolean = false, hasNextPage: Boolean = false, @@ -193,9 +184,9 @@ private fun MangaTable( if (isLoading || mangas.isEmpty()) { LoadingScreen(isLoading) } else { - val persistentState = persistentLazyListState(bundle) + val lazyListState = rememberLazyListState() Box { - LazyVerticalGrid(GridCells.Adaptive(160.dp), state = persistentState) { + LazyVerticalGrid(GridCells.Adaptive(160.dp), state = lazyListState) { itemsIndexed(mangas) { index, manga -> if (hasNextPage && index == mangas.lastIndex) { LaunchedEffect(Unit) { onLoadNextPage() } @@ -210,7 +201,7 @@ private fun MangaTable( } } VerticalScrollbar( - rememberScrollbarAdapter(persistentState), + rememberScrollbarAdapter(lazyListState), Modifier.align(Alignment.CenterEnd) .fillMaxHeight() .padding(horizontal = 4.dp, vertical = 8.dp) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/SourceFiltersMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersMenu.kt similarity index 92% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/SourceFiltersMenu.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersMenu.kt index ae5651fa..d8f12978 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/SourceFiltersMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersMenu.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources.components.filter +package ca.gosyer.ui.sources.browse.filter import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.LinearEasing @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.material.Button import androidx.compose.material.Checkbox @@ -48,7 +49,6 @@ import androidx.compose.material.TriStateCheckbox import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowUpward import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -67,41 +67,19 @@ import androidx.compose.ui.util.fastForEach import ca.gosyer.data.models.sourcefilters.SortFilter import ca.gosyer.i18n.MR import ca.gosyer.ui.base.prefs.ExpandablePreference -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.ui.sources.components.filter.model.SourceFiltersView +import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView import ca.gosyer.uicore.components.Spinner -import ca.gosyer.util.compose.persistentLazyListState -import com.github.zsoltk.compose.savedinstancestate.Bundle import ca.gosyer.uicore.resources.stringResource import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.launch @Composable fun SourceFiltersMenu( - bundle: Bundle, modifier: Modifier, - sourceId: Long, showFilters: Boolean, + filters: List>, onSearchClicked: () -> Unit, - onResetClicked: () -> Unit, - showFiltersButton: (Boolean) -> Unit + resetFiltersClicked: () -> Unit ) { - val vm = viewModel(sourceId) { - instantiate(SourceFiltersViewModel.Params(bundle, sourceId)) - } - val filters by vm.filters.collectAsState() - DisposableEffect(filters) { - showFiltersButton(filters.isNotEmpty()) - onDispose { showFiltersButton(false) } - } - LaunchedEffect(vm) { - launch { - vm.resetFilters.collect { - onResetClicked() - } - } - } - AnimatedVisibility( showFilters, enter = fadeIn() + slideInHorizontally(initialOffsetX = { it * 2 }), @@ -116,7 +94,7 @@ fun SourceFiltersMenu( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - TextButton(vm::resetFilters) { + TextButton(resetFiltersClicked) { Text(stringResource(MR.strings.reset_filters)) } Button(onSearchClicked) { @@ -126,7 +104,7 @@ fun SourceFiltersMenu( } val expandedGroups = remember { mutableStateListOf() } Box { - val lazyListState = persistentLazyListState() + val lazyListState = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), lazyListState) { items( items = filters, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/SourceFiltersViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersViewModel.kt similarity index 83% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/SourceFiltersViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersViewModel.kt index edf4f3c7..875747f9 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/SourceFiltersViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersViewModel.kt @@ -4,15 +4,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources.components.filter +package ca.gosyer.ui.sources.browse.filter import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.models.sourcefilters.SourceFilter import ca.gosyer.data.server.interactions.SourceInteractionHandler +import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView import ca.gosyer.uicore.vm.ViewModel -import ca.gosyer.ui.sources.components.filter.model.SourceFiltersView -import com.github.zsoltk.compose.savedinstancestate.Bundle import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -27,7 +26,6 @@ import me.tatarka.inject.annotations.Inject import java.util.concurrent.CopyOnWriteArrayList class SourceFiltersViewModel( - private val bundle: Bundle, private val sourceId: Long, private val sourceHandler: SourceInteractionHandler ) : ViewModel() { @@ -35,7 +33,6 @@ class SourceFiltersViewModel( sourceHandler: SourceInteractionHandler, params: Params, ) : this( - params.bundle, params.sourceId, sourceHandler ) @@ -49,14 +46,21 @@ class SourceFiltersViewModel( private val _resetFilters = MutableSharedFlow() val resetFilters = _resetFilters.asSharedFlow() + private val _showingFilters = MutableStateFlow(false) + val showingFilters = _showingFilters.asStateFlow() + + private val _filterButtonEnabled = MutableStateFlow(false) + val filterButtonEnabled = _filterButtonEnabled.asStateFlow() + private val subscriptions: CopyOnWriteArrayList = CopyOnWriteArrayList() init { - getFilters(initialLoad = !bundle.getBoolean(FILTERING, false)) + getFilters(initialLoad = true) filters.onEach { settings -> subscriptions.forEach { it.cancel() } subscriptions.clear() + _filterButtonEnabled.value = settings.isNotEmpty() subscriptions += settings.flatMap { filter -> if (filter is SourceFiltersView.Group) { filter.state.value.map { childFilter -> @@ -81,15 +85,18 @@ class SourceFiltersViewModel( }.launchIn(scope) } + fun showingFilters(show: Boolean) { + _showingFilters.value = show + } + fun enableFilters(enabled: Boolean) { + _filterButtonEnabled.value = enabled + } + private fun getFilters(initialLoad: Boolean = false) { scope.launch { try { _filters.value = sourceHandler.getFilterList(sourceId, reset = initialLoad).toView() - if (!initialLoad) { - bundle.putBoolean(FILTERING, true) - } else { - _resetFilters.emit(Unit) - } + _resetFilters.emit(Unit) } catch (e: Exception) { e.throwIfCancellation() } finally { @@ -100,12 +107,11 @@ class SourceFiltersViewModel( fun resetFilters() { scope.launch { - bundle.remove(FILTERING) getFilters(initialLoad = true) } } - data class Params(val bundle: Bundle, val sourceId: Long) + data class Params(val sourceId: Long) private fun List.toView() = mapIndexed { index, sourcePreference -> SourceFiltersView(index, sourcePreference) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/model/SourceFiltersView.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/model/SourceFiltersView.kt similarity index 99% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/model/SourceFiltersView.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/model/SourceFiltersView.kt index 4e655ffd..7e339d3d 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/filter/model/SourceFiltersView.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/filter/model/SourceFiltersView.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources.components.filter.model +package ca.gosyer.ui.sources.browse.filter.model import ca.gosyer.data.models.sourcefilters.CheckBoxFilter import ca.gosyer.data.models.sourcefilters.GroupFilter diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourcesMenu.kt similarity index 59% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourcesMenu.kt index 6140ccc6..ac4a9a0b 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourcesMenu.kt @@ -4,9 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources +package ca.gosyer.ui.sources.components -import androidx.compose.animation.Crossfade import androidx.compose.foundation.TooltipArea import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.VerticalScrollbar @@ -29,9 +28,8 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Home import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -40,68 +38,43 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import ca.gosyer.data.models.Source import ca.gosyer.i18n.MR -import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.manga.openMangaMenu -import ca.gosyer.ui.sources.components.SourceHomeScreen -import ca.gosyer.ui.sources.components.SourceScreen -import ca.gosyer.ui.sources.settings.openSourceSettingsMenu +import ca.gosyer.ui.sources.browse.SourceScreen +import ca.gosyer.ui.sources.home.SourceHomeScreen import ca.gosyer.uicore.components.combinedMouseClickable import ca.gosyer.uicore.image.KamelImage -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.lang.launchApplication -import com.github.zsoltk.compose.savedinstancestate.Bundle -import com.github.zsoltk.compose.savedinstancestate.BundleScope -import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState import ca.gosyer.uicore.resources.stringResource import io.kamel.image.lazyPainterResource -import kotlinx.coroutines.DelicateCoroutinesApi -@OptIn(DelicateCoroutinesApi::class) -fun openSourcesMenu() { - launchApplication { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - Surface { - CompositionLocalProvider( - LocalSavedInstanceState provides Bundle() - ) { - SourcesMenu( - ::openSourceSettingsMenu, - ::openMangaMenu - ) - } - } +@Composable +fun SourcesMenu( + sourceTabs: List, + selectedSourceTab: Source?, + selectTab: (Source?) -> Unit, + closeTab: (Source) -> Unit +) { + val homeScreen = remember { SourceHomeScreen() } + SourcesNavigator( + homeScreen, + removeSource = closeTab, + selectSource = selectTab + ) { navigator -> + LaunchedEffect(selectedSourceTab) { + navigator.current = if (selectedSourceTab == null) { + homeScreen + } else SourceScreen(selectedSourceTab) } - } -} + Row { + SourcesSideMenu( + sourceTabs = sourceTabs, + onSourceTabClick = selectTab, + onCloseSourceTabClick = { + closeTab(it) + navigator.stateHolder.removeState(it.id) + } + ) -@Composable -fun SourcesMenu(onSourceSettingsClick: (Long) -> Unit, onMangaClick: (Long) -> Unit) { - SourcesMenu(LocalSavedInstanceState.current, onSourceSettingsClick, onMangaClick) -} - -@Composable -fun SourcesMenu(bundle: Bundle, onSourceSettingsClick: (Long) -> Unit, onMangaClick: (Long) -> Unit) { - val vm = viewModel { - instantiate(bundle) - } - val sourceTabs by vm.sourceTabs.collectAsState() - val selectedSourceTab by vm.selectedSourceTab.collectAsState() - Row { - SourcesSideMenu( - sourceTabs = sourceTabs, - onSourceTabClick = vm::selectTab, - onCloseSourceTabClick = vm::closeTab - ) - - SourceTab( - onLoadSources = vm::setLoadedSources, - onSourceClicked = vm::addTab, - selectedSourceTab = selectedSourceTab, - onMangaClick = onMangaClick, - onCloseSourceTabClick = vm::closeTab, - onSourceSettingsClick = onSourceSettingsClick - ) + CurrentSource() + } } } @@ -169,33 +142,3 @@ fun SourcesSideMenu( } } } - -@Composable -fun SourceTab( - onLoadSources: (List) -> Unit, - onSourceClicked: (Source) -> Unit, - selectedSourceTab: Source?, - onMangaClick: (Long) -> Unit, - onCloseSourceTabClick: (Source) -> Unit, - onSourceSettingsClick: (Long) -> Unit -) { - Crossfade(selectedSourceTab) { selectedSource -> - BundleScope(selectedSource?.id.toString(), autoDispose = false) { - if (selectedSource != null) { - SourceScreen( - bundle = it, - source = selectedSource, - onMangaClick = onMangaClick, - onCloseSourceTabClick = onCloseSourceTabClick, - onSourceSettingsClick = onSourceSettingsClick - ) - } else { - SourceHomeScreen( - bundle = it, - onAddSource = onSourceClicked, - onLoadSources = onLoadSources - ) - } - } - } -} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourcesNavigator.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourcesNavigator.kt new file mode 100644 index 00000000..07869dae --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourcesNavigator.kt @@ -0,0 +1,75 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.SaveableStateHolder +import androidx.compose.runtime.staticCompositionLocalOf +import ca.gosyer.data.models.Source +import ca.gosyer.ui.sources.browse.SourceScreen +import ca.gosyer.ui.sources.home.SourceHomeScreen +import cafe.adriel.voyager.navigator.Navigator + +typealias SourcesNavigatorContent = @Composable (sourcesNavigator: SourcesNavigator) -> Unit + +val LocalSourcesNavigator: ProvidableCompositionLocal = + staticCompositionLocalOf { error("SourcesNavigator not initialized") } + +@Composable +fun SourcesNavigator( + homeScreen: SourceHomeScreen, + removeSource: (Source) -> Unit, + selectSource: (Source) -> Unit, + content: SourcesNavigatorContent = { CurrentSource() } +) { + Navigator(homeScreen, autoDispose = false, onBackPressed = null) { navigator -> + val sourcesNavigator = remember(navigator) { + SourcesNavigator(navigator, homeScreen, removeSource, selectSource) + } + + CompositionLocalProvider(LocalSourcesNavigator provides sourcesNavigator) { + content(sourcesNavigator) + } + } +} + +class SourcesNavigator internal constructor( + private val navigator: Navigator, + private val homeScreen: SourceHomeScreen, + private val removeSource: (Source) -> Unit, + private val selectSource: (Source) -> Unit, + val stateHolder: SaveableStateHolder = navigator.stateHolder +) { + + fun remove(source: Source) { + removeSource(source) + navigator replaceAll homeScreen + stateHolder.removeState(source.id) + } + + fun select(source: Source) { + selectSource(source) + navigator replaceAll SourceScreen(source) + } + + var current + get() = navigator.lastItem + set(value) = navigator replaceAll value +} + +@Composable +fun CurrentSource() { + val sourcesNavigator = LocalSourcesNavigator.current + val currentSource = sourcesNavigator.current + + sourcesNavigator.stateHolder.SaveableStateProvider(currentSource.key) { + currentSource.Content() + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreen.kt new file mode 100644 index 00000000..2718f0c7 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreen.kt @@ -0,0 +1,35 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources.home + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import ca.gosyer.ui.sources.components.LocalSourcesNavigator +import ca.gosyer.ui.sources.home.components.SourceHomeScreenContent +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +class SourceHomeScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + val sourcesNavigator = LocalSourcesNavigator.current + SourceHomeScreenContent( + onAddSource = sourcesNavigator::select, + isLoading = vm.isLoading.collectAsState().value, + sources = vm.sources.collectAsState().value, + languages = vm.languages, + getSourceLanguages = vm::getSourceLanguages, + setEnabledLanguages = vm::setEnabledLanguages + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreenViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt similarity index 92% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreenViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt index 477f88ce..ed8908a4 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreenViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources.components +package ca.gosyer.ui.sources.home import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.logging.CKLogger @@ -12,7 +12,6 @@ import ca.gosyer.data.catalog.CatalogPreferences import ca.gosyer.data.models.Source import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.uicore.vm.ViewModel -import com.github.zsoltk.compose.savedinstancestate.Bundle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -20,8 +19,7 @@ import me.tatarka.inject.annotations.Inject class SourceHomeScreenViewModel @Inject constructor( private val sourceHandler: SourceInteractionHandler, - catalogPreferences: CatalogPreferences, - private val bundle: Bundle, + catalogPreferences: CatalogPreferences ) : ViewModel() { private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow() diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt similarity index 87% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreen.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt index fdc336c3..440aeea4 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources.components +package ca.gosyer.ui.sources.home.components import androidx.compose.foundation.TooltipArea import androidx.compose.foundation.VerticalScrollbar @@ -32,9 +32,6 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Translate import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -45,41 +42,31 @@ import ca.gosyer.data.models.Source import ca.gosyer.i18n.MR import ca.gosyer.ui.base.navigation.TextActionIcon import ca.gosyer.ui.base.navigation.Toolbar -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.ui.extensions.LanguageDialog +import ca.gosyer.ui.extensions.components.LanguageDialog import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.image.KamelImage -import com.github.zsoltk.compose.savedinstancestate.Bundle import ca.gosyer.uicore.resources.stringResource import io.kamel.image.lazyPainterResource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @Composable -fun SourceHomeScreen( - bundle: Bundle, +fun SourceHomeScreenContent( onAddSource: (Source) -> Unit, - onLoadSources: (List) -> Unit + isLoading: Boolean, + sources: List, + languages: StateFlow>, + getSourceLanguages: () -> Set, + setEnabledLanguages: (Set) -> Unit ) { - val vm = viewModel { - instantiate(bundle) - } - val sources by vm.sources.collectAsState() - val isLoading by vm.isLoading.collectAsState() - LaunchedEffect(sources) { - if (sources.isNotEmpty()) { - onLoadSources(sources) - } - } - if (sources.isEmpty()) { LoadingScreen(isLoading) } else { Column { SourceHomeScreenToolbar( - vm.languages, - vm::getSourceLanguages, - vm::setEnabledLanguages + languages, + getSourceLanguages, + setEnabledLanguages ) Box(Modifier.fillMaxSize(), Alignment.TopCenter) { val state = rememberLazyListState() diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreen.kt new file mode 100644 index 00000000..419fa50f --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreen.kt @@ -0,0 +1,45 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources.settings + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import ca.gosyer.presentation.build.BuildKonfig +import ca.gosyer.ui.sources.settings.components.SourceSettingsScreenContent +import ca.gosyer.ui.util.compose.ThemedWindow +import ca.gosyer.ui.util.lang.launchApplication +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.Navigator +import kotlinx.coroutines.DelicateCoroutinesApi + +@OptIn(DelicateCoroutinesApi::class) +fun openSourceSettingsMenu(sourceId: Long) { + launchApplication { + ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { + Navigator(remember { SourceSettingsScreen(sourceId) }) + } + } +} + +class SourceSettingsScreen(private val sourceId: Long) : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel { + instantiate(SourceSettingsScreenViewModel.Params(sourceId)) + } + SourceSettingsScreenContent( + settings = vm.sourceSettings.collectAsState().value + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreenViewModel.kt similarity index 97% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreenViewModel.kt index f1f8651c..0fbefe6a 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreenViewModel.kt @@ -9,8 +9,8 @@ package ca.gosyer.ui.sources.settings import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.data.models.sourcepreference.SourcePreference import ca.gosyer.data.server.interactions.SourceInteractionHandler -import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.ui.sources.settings.model.SourceSettingsView +import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -22,7 +22,7 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import java.util.concurrent.CopyOnWriteArrayList -class SourceSettingsViewModel @Inject constructor( +class SourceSettingsScreenViewModel @Inject constructor( private val sourceHandler: SourceInteractionHandler, private val params: Params ) : ViewModel() { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/components/SourceSettingsScreenContent.kt similarity index 87% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsMenu.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/components/SourceSettingsScreenContent.kt index 58eafa61..2f9a421d 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/components/SourceSettingsScreenContent.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.sources.settings +package ca.gosyer.ui.sources.settings.components import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.layout.Box @@ -33,43 +33,27 @@ import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.ui.base.WindowDialog -import ca.gosyer.ui.base.navigation.LocalMenuController -import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.ChoiceDialog import ca.gosyer.ui.base.prefs.MultiSelectDialog import ca.gosyer.ui.base.prefs.PreferenceRow +import ca.gosyer.ui.sources.settings.model.SourceSettingsView import ca.gosyer.ui.sources.settings.model.SourceSettingsView.CheckBox import ca.gosyer.ui.sources.settings.model.SourceSettingsView.EditText import ca.gosyer.ui.sources.settings.model.SourceSettingsView.List import ca.gosyer.ui.sources.settings.model.SourceSettingsView.MultiSelect import ca.gosyer.ui.sources.settings.model.SourceSettingsView.Switch import ca.gosyer.ui.sources.settings.model.SourceSettingsView.TwoState -import ca.gosyer.uicore.vm.viewModel -import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.lang.launchApplication import ca.gosyer.uicore.resources.stringResource -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow - -@OptIn(DelicateCoroutinesApi::class) -fun openSourceSettingsMenu(sourceId: Long) { - launchApplication { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - SourceSettingsMenu(sourceId) - } - } -} +import kotlin.collections.List as KtList @Composable -fun SourceSettingsMenu(sourceId: Long, menuController: MenuController? = LocalMenuController.current) { - val vm = viewModel { - instantiate(SourceSettingsViewModel.Params(sourceId)) - } - val settings by vm.sourceSettings.collectAsState() - +fun SourceSettingsScreenContent( + settings: KtList> +) { Column { - Toolbar(stringResource(MR.strings.location_settings), menuController, menuController != null) + Toolbar(stringResource(MR.strings.location_settings)) Box { val state = rememberLazyListState() LazyColumn(Modifier.fillMaxSize(), state) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt new file mode 100644 index 00000000..5832ef9d --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.updates + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import ca.gosyer.ui.manga.MangaScreen +import ca.gosyer.ui.reader.openReaderMenu +import ca.gosyer.ui.updates.components.UpdatesScreenContent +import ca.gosyer.uicore.vm.viewModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow + +class UpdatesScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val vm = viewModel() + val navigator = LocalNavigator.currentOrThrow + UpdatesScreenContent( + isLoading = vm.isLoading.collectAsState().value, + updates = vm.updates.collectAsState().value, + loadNextPage = vm::loadNextPage, + openChapter = ::openReaderMenu, + openManga = { navigator push MangaScreen(it) }, + downloadChapter = vm::downloadChapter, + deleteDownloadedChapter = vm::deleteDownloadedChapter, + stopDownloadingChapter = vm::stopDownloadingChapter + ) + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesMenuViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt similarity index 97% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesMenuViewModel.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt index fb715078..7cd2aa8d 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesMenuViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import me.tatarka.inject.annotations.Inject -class UpdatesMenuViewModel @Inject constructor( +class UpdatesScreenViewModel @Inject constructor( private val chapterHandler: ChapterInteractionHandler, private val updatesHandler: UpdatesInteractionHandler, private val downloadService: DownloadService @@ -113,7 +113,7 @@ class UpdatesMenuViewModel @Inject constructor( } } - override fun onDestroy() { + override fun onDispose() { downloadService.removeWatches(mangaIds) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt similarity index 88% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesMenu.kt rename to presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt index 8a9d15b9..f63919c4 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.ui.updates +package ca.gosyer.ui.updates.components import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.clickable @@ -23,8 +23,6 @@ import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -44,18 +42,20 @@ import ca.gosyer.uicore.components.MangaListItemImage import ca.gosyer.uicore.components.MangaListItemSubtitle import ca.gosyer.uicore.components.MangaListItemTitle import ca.gosyer.uicore.components.mangaAspectRatio -import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.resources.stringResource import io.kamel.image.lazyPainterResource @Composable -fun UpdatesMenu( +fun UpdatesScreenContent( + isLoading: Boolean, + updates: List, + loadNextPage: () -> Unit, openChapter: (Int, Long) -> Unit, - openManga: (Long) -> Unit + openManga: (Long) -> Unit, + downloadChapter: (Chapter) -> Unit, + deleteDownloadedChapter: (Chapter) -> Unit, + stopDownloadingChapter: (Chapter) -> Unit ) { - val vm = viewModel() - val isLoading by vm.isLoading.collectAsState() - val updates by vm.updates.collectAsState() Column { Toolbar(stringResource(MR.strings.location_updates), closable = false) if (isLoading || updates.isEmpty()) { @@ -67,7 +67,7 @@ fun UpdatesMenu( itemsIndexed(updates) { index, item -> LaunchedEffect(Unit) { if (index == updates.lastIndex) { - vm.loadNextPage() + loadNextPage() } } val manga = item.manga!! @@ -76,9 +76,9 @@ fun UpdatesMenu( item, onClickItem = { openChapter(chapter.index, chapter.mangaId) }, onClickCover = { openManga(manga.id) }, - onClickDownload = vm::downloadChapter, - onClickDeleteDownload = vm::deleteDownloadedChapter, - onClickStopDownload = vm::stopDownloadingChapter + onClickDownload = downloadChapter, + onClickDeleteDownload = deleteDownloadedChapter, + onClickStopDownload = stopDownloadingChapter ) } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Bundle.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Bundle.kt deleted file mode 100644 index 20875e7b..00000000 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Bundle.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package ca.gosyer.util.compose - -import com.github.zsoltk.compose.savedinstancestate.Bundle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapLatest -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -inline fun Bundle.putJsonObject(key: String, item: T) { - putString(key, Json.encodeToString(item)) -} - -inline fun Bundle.getJsonObject(key: String): T? { - return getString(key)?.let { Json.decodeFromString(it) } -} - -inline fun Bundle.getJsonObjectArray(key: String): List? { - return getString(key)?.let { Json.decodeFromString(it) } -} - -inline fun saveAnyInBundle( - scope: CoroutineScope, - bundle: Bundle, - key: String, - getValue: Bundle.(String) -> T?, - crossinline putValue: Bundle.(itemKey: String, item: T) -> Unit, - initialValue: () -> T -): MutableStateFlow { - val item = bundle.getValue(key) - val flow: MutableStateFlow = if (item != null) { - MutableStateFlow(item) - } else { - MutableStateFlow(initialValue()) - } - flow.drop(1) - .mapLatest { bundle.putValue(key, it) } - .launchIn(scope) - - return flow -} - -inline fun saveObjectInBundle( - scope: CoroutineScope, - bundle: Bundle, - key: String, - initialValue: () -> T -): MutableStateFlow { - return saveAnyInBundle( - scope, - bundle, - key, - { getJsonObject(it) }, - { itemKey, item -> - putJsonObject(itemKey, item) - }, - initialValue - ) -} - -fun saveIntInBundle( - scope: CoroutineScope, - bundle: Bundle, - key: String, - initialValue: Int -): MutableStateFlow { - return saveAnyInBundle( - scope, - bundle, - key, - { getInt(key, initialValue) }, - { itemKey, item -> - putInt(itemKey, item) - }, - { initialValue } - ) -} - -fun saveBooleanInBundle( - scope: CoroutineScope, - bundle: Bundle, - key: String, - initialValue: Boolean -): MutableStateFlow { - return saveAnyInBundle( - scope, - bundle, - key, - { getBoolean(key, initialValue) }, - { itemKey, item -> - putBoolean(itemKey, item) - }, - { initialValue } - ) -} - -fun saveStringInBundle( - scope: CoroutineScope, - bundle: Bundle, - key: String, - initialValue: () -> String? = { null } -): MutableStateFlow { - return saveAnyInBundle( - scope, - bundle, - key, - { getString(key) ?: initialValue() }, - { itemKey, item -> - if (item != null) { - putString(itemKey, item) - } else remove(itemKey) - }, - initialValue - ) -} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Color.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Color.kt index f7fdf066..75625ec9 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Color.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Color.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.compose +package ca.gosyer.ui.util.compose import androidx.compose.ui.graphics.Color diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Flow.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Flow.kt index 2b45d9da..4fbd9a4a 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Flow.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Flow.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.compose +package ca.gosyer.ui.util.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Image.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Image.kt index ec298ab4..c435d817 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Image.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Image.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.compose +package ca.gosyer.ui.util.compose import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Offset.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Offset.kt index 4b029245..332f143f 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Offset.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Offset.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.compose +package ca.gosyer.ui.util.compose import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/State.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/State.kt deleted file mode 100644 index 950ceb99..00000000 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/State.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package ca.gosyer.util.compose - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember -import com.github.zsoltk.compose.savedinstancestate.Bundle -import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState - -const val LAZY_LIST_ITEM = "lazy_list_item" -const val LAZY_LIST_OFFSET = "lazy_list_offset" - -@Composable -fun persistentLazyListState(bundle: Bundle = LocalSavedInstanceState.current): LazyListState { - val state = rememberLazyListState( - remember { bundle.getInt(LAZY_LIST_ITEM, 0) }, - remember { bundle.getInt(LAZY_LIST_OFFSET, 0) } - ) - DisposableEffect(Unit) { - onDispose { - bundle.putInt(LAZY_LIST_ITEM, state.firstVisibleItemIndex) - bundle.putInt(LAZY_LIST_OFFSET, state.firstVisibleItemScrollOffset) - } - } - - return state -} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Theme.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Theme.kt index 9f43aad8..1a7f78ee 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Theme.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Theme.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.compose +package ca.gosyer.ui.util.compose import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.painter.Painter diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/WindowGet.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/WindowGet.kt index bc7bb960..bfa18383 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/WindowGet.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/WindowGet.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.compose +package ca.gosyer.ui.util.compose import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/lang/CoroutineExtensions.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/lang/CoroutineExtensions.kt index 48842464..1bbfa7e4 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/lang/CoroutineExtensions.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/lang/CoroutineExtensions.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.lang +package ca.gosyer.ui.util.lang import androidx.compose.runtime.Composable import androidx.compose.ui.window.ApplicationScope diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/File.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/File.kt index 0b8276ee..cfe417e4 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/File.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/File.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.system +package ca.gosyer.ui.util.system import ca.gosyer.core.lang.launchUI import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/Flow.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/Flow.kt index eb5eac4e..f97b94ac 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/Flow.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/Flow.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.system +package ca.gosyer.ui.util.system import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow diff --git a/ui-core/build.gradle.kts b/ui-core/build.gradle.kts index d3c3b95b..9a3ba7e7 100644 --- a/ui-core/build.gradle.kts +++ b/ui-core/build.gradle.kts @@ -31,6 +31,7 @@ kotlin { api(kotlin("stdlib-common")) api(libs.coroutinesCore) api(libs.kamel) + api(libs.voyagerCore) api(project(":core")) api(project(":i18n")) api(compose.desktop.currentOs) diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/uicore/vm/ComposeViewModel.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/uicore/vm/ComposeViewModel.kt index eac1cfbb..d0f65d1c 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/uicore/vm/ComposeViewModel.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/uicore/vm/ComposeViewModel.kt @@ -8,36 +8,20 @@ package ca.gosyer.uicore.vm import androidx.compose.runtime.Composable import androidx.compose.runtime.DisallowComposableCalls -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen @Composable -inline fun viewModel(key: Any? = Unit): VM { +inline fun Screen.viewModel(tag: String? = null): VM { val viewModelFactory = LocalViewModelFactory.current - val viewModel = remember(key) { - viewModelFactory.instantiate() - } - DisposableEffect(viewModel) { - onDispose { - viewModel.destroy() - } - } - return viewModel + return rememberScreenModel(tag) { viewModelFactory.instantiate() } } @Composable -inline fun viewModel( - key: Any? = Unit, +inline fun Screen.viewModel( + tag: String? = null, crossinline factory: @DisallowComposableCalls ViewModelFactory.() -> VM ): VM { val viewModelFactory = LocalViewModelFactory.current - val viewModel = remember(key) { - viewModelFactory.factory() - } - DisposableEffect(viewModel) { - onDispose { - viewModel.destroy() - } - } - return viewModel + return rememberScreenModel(tag) { viewModelFactory.factory() } } diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/uicore/vm/ViewModel.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/uicore/vm/ViewModel.kt index 53839f29..384af12a 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/uicore/vm/ViewModel.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/uicore/vm/ViewModel.kt @@ -8,23 +8,17 @@ package ca.gosyer.uicore.vm import ca.gosyer.core.prefs.Preference import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -abstract class ViewModel { +abstract class ViewModel : ScreenModel { - protected val scope = MainScope() - - fun destroy() { - scope.cancel() - onDestroy() - } - - open fun onDestroy() {} + protected open val scope + get() = coroutineScope fun Preference.asStateFlow() = PreferenceMutableStateFlow(this, scope)