From a42309005217d3937114bf99919cf8cea889c232 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 26 Feb 2022 14:36:45 -0500 Subject: [PATCH] Many updates and fixes - Use multiplatform dialogs library(WIP) - Port accompanist to multiplatform - Use new gradle type-safe project accessors - Cleanup and improve a few viewmodels - Start moving presentation to multiplatform --- build.gradle.kts | 2 +- data/build.gradle.kts | 4 +- .../data/server/ServerHostPreferences.kt | 6 +- .../ca/gosyer/data/server/ServerService.kt | 3 +- .../gosyer/data/server/ServerPreferences.kt | 4 - desktop/build.gradle.kts | 11 +- desktop/src/main/kotlin/ca/gosyer/main.kt | 32 ++- gradle/libs.versions.toml | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../resources/MR/images/icon@1x.png | Bin 0 -> 13093 bytes .../resources/MR/values/base/strings.xml | 2 + presentation/build.gradle.kts | 9 +- .../ca/gosyer/ui/base/components/Scrollbar.kt | 55 +++++ .../gosyer/ui/base/navigation/ActionIcon.kt | 20 ++ .../ui/base/theme/ThemeScrollbarStyle.kt | 17 ++ .../ca/gosyer/ui/base/vm/ViewModelFactory.kt | 30 +++ .../kotlin/ca/gosyer/ui/base/WindowDialog.kt | 180 ---------------- .../ca/gosyer/ui/base/components/Scrollbar.kt | 47 +++++ .../gosyer/ui/base/navigation/ActionIcon.kt | 30 +++ .../ui/base/theme/ThemeScrollbarStyle.kt | 26 +++ .../ca/gosyer/ui/base/vm/ViewModelFactory.kt | 5 +- .../components/CategoriesDialogs.kt | 93 ++++---- .../components/CategoriesScreenContent.kt | 25 ++- .../ca/gosyer/ui/downloads/DownloadsScreen.kt | 22 -- .../gosyer/ui/extensions/ExtensionsScreen.kt | 32 +-- .../extensions/ExtensionsScreenViewModel.kt | 59 +++--- .../components/ExtensionsScreenContent.kt | 87 ++++---- .../ca/gosyer/ui/library/LibraryScreen.kt | 22 -- .../ui/library/components/LibraryPager.kt | 4 +- .../kotlin/ca/gosyer/ui/main/TopLevelMenus.kt | 16 +- .../gosyer/ui/main/components/SideMenuItem.kt | 8 +- .../ui/main/components/TrayViewModel.kt | 6 + .../kotlin/ca/gosyer/ui/manga/MangaScreen.kt | 24 +-- .../gosyer/ui/manga/MangaScreenViewModel.kt | 26 ++- .../gosyer/ui/manga/components/MangaMenu.kt | 42 ++-- .../ui/manga/components/MangaScreenContent.kt | 11 +- .../ca/gosyer/ui/reader/viewer/Pager.kt | 16 +- .../ui/settings/SettingsBackupScreen.kt | 66 ++++-- .../ui/settings/SettingsServerScreen.kt | 133 +++++++----- .../ca/gosyer/ui/sources/SourcesScreen.kt | 22 -- .../ui/sources/home/SourceHomeScreen.kt | 4 +- .../sources/home/SourceHomeScreenViewModel.kt | 31 +-- .../components/SourceHomeScreenContent.kt | 28 +-- .../sources/settings/SourceSettingsScreen.kt | 19 -- .../components/SourceSettingsScreenContent.kt | 90 ++++---- .../kotlin/ca/gosyer/ui/util/compose/Flow.kt | 19 -- .../kotlin/ca/gosyer/ui/base/UiComponent.kt | 0 .../ui/base/chapter/ChapterDownloadButtons.kt | 0 .../gosyer/ui/base/components/ScrollBarR.kt | 22 ++ .../ca/gosyer/ui/base/components/Scrollbar.kt | 40 ++++ .../base/dialog/MaterialDialogProperties.kt | 45 ++++ .../ui/base/image/KamelConfigProvider.kt | 0 .../gosyer/ui/base/navigation/ActionIcon.kt | 13 ++ .../gosyer/ui/base/navigation/ActionMenu.kt | 17 +- .../ui/base/navigation/DisplayController.kt | 0 .../ca/gosyer/ui/base/navigation/Toolbar.kt | 16 -- .../gosyer/ui/base/prefs/ColorPickerDialog.kt | 82 ++++---- .../ui/base/prefs/PreferencesUiBuilder.kt | 198 +++++++++++------- .../ui/base/theme/AppColorsPreference.kt | 0 .../ca/gosyer/ui/base/theme/AppTheme.kt | 17 +- .../ui/base/theme/ThemeScrollbarStyle.kt | 15 ++ .../ca/gosyer/ui/base/vm/ViewModelFactory.kt | 13 ++ .../ca/gosyer/ui/updates/UpdatesScreen.kt | 0 .../ui/updates/UpdatesScreenViewModel.kt | 0 .../components/UpdatesScreenContent.kt | 4 +- .../kotlin/ca/gosyer/ui/util/compose/Color.kt | 0 .../ca/gosyer/ui/util/compose/Offset.kt | 0 .../kotlin/ca/gosyer/ui/util/system/Flow.kt | 0 settings.gradle.kts | 5 +- ui-core/build.gradle.kts | 4 +- 70 files changed, 1046 insertions(+), 839 deletions(-) create mode 100644 i18n/src/commonMain/resources/MR/images/icon@1x.png create mode 100644 presentation/src/androidMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt create mode 100644 presentation/src/androidMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt create mode 100644 presentation/src/androidMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt create mode 100644 presentation/src/androidMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt delete mode 100644 presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/WindowDialog.kt create mode 100644 presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt create mode 100644 presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt create mode 100644 presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt delete mode 100644 presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Flow.kt rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/UiComponent.kt (100%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt (100%) create mode 100644 presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/components/ScrollBarR.kt create mode 100644 presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt create mode 100644 presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/dialog/MaterialDialogProperties.kt rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/image/KamelConfigProvider.kt (100%) create mode 100644 presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt (93%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/navigation/DisplayController.kt (100%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt (97%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/prefs/ColorPickerDialog.kt (90%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt (75%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt (100%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt (88%) create mode 100644 presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt create mode 100644 presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt (100%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt (100%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt (97%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/util/compose/Color.kt (100%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/util/compose/Offset.kt (100%) rename presentation/src/{desktopMain => jvmMain}/kotlin/ca/gosyer/ui/util/system/Flow.kt (100%) diff --git a/build.gradle.kts b/build.gradle.kts index 91ce20d0..8efa9066 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { kotlin("plugin.serialization") version "1.6.10" apply false id("com.android.library") version "7.0.4" apply false id("com.android.application") version "7.0.4" apply false - id("org.jetbrains.compose") version "1.0.1" apply false + id("org.jetbrains.compose") version "1.1.0-alpha03" apply false id("com.google.devtools.ksp") version "1.6.10-1.0.2" id("com.github.gmazzo.buildconfig") version "3.0.3" apply false id("com.codingfeline.buildkonfig") version "0.11.0" apply false diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 0e1d4dd2..27d55f8a 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -44,8 +44,8 @@ kotlin { api(libs.ktorWebsockets) api(libs.ktorOkHttp) api(libs.okio) - api(project(":core")) - api(project(":i18n")) + api(projects.core) + api(projects.i18n) } } val commonTest by getting { diff --git a/data/src/desktopMain/kotlin/ca/gosyer/data/server/ServerHostPreferences.kt b/data/src/desktopMain/kotlin/ca/gosyer/data/server/ServerHostPreferences.kt index 1d9a638c..c3837045 100644 --- a/data/src/desktopMain/kotlin/ca/gosyer/data/server/ServerHostPreferences.kt +++ b/data/src/desktopMain/kotlin/ca/gosyer/data/server/ServerHostPreferences.kt @@ -10,7 +10,11 @@ import ca.gosyer.core.prefs.Preference import ca.gosyer.core.prefs.PreferenceStore import ca.gosyer.data.server.host.ServerHostPreference -class ServerHostPreferences(preferenceStore: PreferenceStore) { +class ServerHostPreferences(private val preferenceStore: PreferenceStore) { + + fun host(): Preference { + return preferenceStore.getBoolean("host", true) + } private val ip = ServerHostPreference.IP(preferenceStore) fun ip(): Preference { diff --git a/data/src/desktopMain/kotlin/ca/gosyer/data/server/ServerService.kt b/data/src/desktopMain/kotlin/ca/gosyer/data/server/ServerService.kt index 7aff3a9c..7b4fa79d 100644 --- a/data/src/desktopMain/kotlin/ca/gosyer/data/server/ServerService.kt +++ b/data/src/desktopMain/kotlin/ca/gosyer/data/server/ServerService.kt @@ -40,11 +40,10 @@ import kotlin.io.path.isExecutable @OptIn(DelicateCoroutinesApi::class) class ServerService @Inject constructor( - serverPreferences: ServerPreferences, private val serverHostPreferences: ServerHostPreferences ) { private val restartServerFlow = MutableSharedFlow() - private val host = serverPreferences.host().stateIn(GlobalScope) + private val host = serverHostPreferences.host().stateIn(GlobalScope) private val _initialized = MutableStateFlow( if (host.value) { ServerResult.STARTING diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/ServerPreferences.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/ServerPreferences.kt index 8915771a..9a2b12af 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/ServerPreferences.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/ServerPreferences.kt @@ -13,10 +13,6 @@ import ca.gosyer.data.server.model.Proxy class ServerPreferences(private val preferenceStore: PreferenceStore) { - fun host(): Preference { - return preferenceStore.getBoolean("host", true) - } - fun server(): Preference { return preferenceStore.getString("server_url", "http://localhost") } diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 0cd6b485..f00bc3bf 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -16,11 +16,11 @@ plugins { } dependencies { - implementation(project(":core")) - implementation(project(":i18n")) - implementation(project(":data")) - implementation(project(":ui-core")) - implementation(project(":presentation")) + implementation(projects.core) + implementation(projects.i18n) + implementation(projects.data) + implementation(projects.uiCore) + implementation(projects.presentation) // UI (Compose) implementation(compose.desktop.currentOs) @@ -33,6 +33,7 @@ dependencies { implementation(libs.accompanistPager) implementation(libs.accompanistFlowLayout) implementation(libs.kamel) + implementation(libs.materialDialogsCore) // UI (Swing) implementation(libs.darklaf) diff --git a/desktop/src/main/kotlin/ca/gosyer/main.kt b/desktop/src/main/kotlin/ca/gosyer/main.kt index 3ce8dbd4..6e263810 100644 --- a/desktop/src/main/kotlin/ca/gosyer/main.kt +++ b/desktop/src/main/kotlin/ca/gosyer/main.kt @@ -9,7 +9,6 @@ package ca.gosyer import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Box import androidx.compose.material.Surface -import androidx.compose.material.Text import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState @@ -20,6 +19,8 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.awaitApplication import androidx.compose.ui.window.rememberWindowState @@ -32,7 +33,7 @@ import ca.gosyer.data.ui.model.ThemeMode import ca.gosyer.desktop.build.BuildConfig import ca.gosyer.i18n.MR import ca.gosyer.ui.AppComponent -import ca.gosyer.ui.base.WindowDialog +import ca.gosyer.ui.base.dialog.getMaterialDialogProperties import ca.gosyer.ui.base.theme.AppTheme import ca.gosyer.ui.main.MainMenu import ca.gosyer.ui.main.components.DebugOverlay @@ -44,6 +45,10 @@ 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.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.message +import com.vanpra.composematerialdialogs.rememberMaterialDialogState +import com.vanpra.composematerialdialogs.title import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow @@ -130,15 +135,12 @@ suspend fun main() { Tray(icon) + val confirmExitDialogState = rememberMaterialDialogState() + Window( onCloseRequest = { if (confirmExit.value) { - WindowDialog( - title = MR.strings.confirm_exit.localized(), - onPositiveButton = ::exitApplication - ) { - Text(stringResource(MR.strings.confirm_exit_message)) - } + confirmExitDialogState.show() } else { exitApplication() } @@ -186,6 +188,20 @@ suspend fun main() { } } } + + MaterialDialog( + confirmExitDialogState, + buttons = { + positiveButton(stringResource(MR.strings.action_ok), onClick = ::exitApplication) + negativeButton(stringResource(MR.strings.action_cancel)) + }, + properties = getMaterialDialogProperties( + size = DpSize(400.dp, 200.dp) + ), + ) { + title(stringResource(MR.strings.confirm_exit)) + message(stringResource(MR.strings.confirm_exit_message)) + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 014b8017..4962161b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,9 @@ xmlUtil = "0.84.0" # Compose voyager = "1.0.0-beta15" -accompanist = "0.18.1" +accompanist = "0.20.1" kamel = "0.3.0" +materialDialogs = "0.6.4" # Swing darklaf = "2.7.3" @@ -58,6 +59,7 @@ voyagerTransitions = { module = "cafe.adriel.voyager:voyager-transitions", versi 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" } +materialDialogsCore = { module = "ca.gosyer:compose-material-dialogs-core", version.ref = "materialDialogs" } # Swing darklaf = { module = "com.github.weisj:darklaf-core", version.ref = "darklaf" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897..41dfb879 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/i18n/src/commonMain/resources/MR/images/icon@1x.png b/i18n/src/commonMain/resources/MR/images/icon@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..f2bb178069cf7823f5a8b14cc0ac0f3c75c53812 GIT binary patch literal 13093 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 xe|c3#Close Search Search… + More actions + Ok Library diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index b0d8b475..6bf16227 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -46,10 +46,11 @@ kotlin { api(libs.voyagerCore) api(libs.voyagerNavigation) api(libs.voyagerTransitions) - api(project(":core")) - api(project(":i18n")) - api(project(":data")) - api(project(":ui-core")) + api(libs.materialDialogsCore) + api(projects.core) + api(projects.i18n) + api(projects.data) + api(projects.uiCore) api(compose.desktop.currentOs) api(compose("org.jetbrains.compose.ui:ui-util")) api(compose.materialIconsExtended) diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt new file mode 100644 index 00000000..8bf7b1e7 --- /dev/null +++ b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt @@ -0,0 +1,55 @@ +/* + * 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.base.components + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier + +actual interface ScrollbarAdapter { + fun noop() +} + +actual class ScrollbarStyle + +actual val LocalScrollbarStyle: ProvidableCompositionLocal = staticCompositionLocalOf { ScrollbarStyle() } + +@Composable +actual fun VerticalScrollbar( + adapter: ScrollbarAdapter, + modifier: Modifier, + reverseLayout: Boolean, + style: ScrollbarStyle, + interactionSource: MutableInteractionSource +) {} + +@Composable +actual fun rememberScrollbarAdapter( + scrollState: ScrollState +): ScrollbarAdapter { + return remember { + object : ScrollbarAdapter { + override fun noop() {} + } + } +} + +@Composable +actual fun rememberScrollbarAdapter( + scrollState: LazyListState, +): ScrollbarAdapter { + return remember { + object : ScrollbarAdapter { + override fun noop() {} + } + } +} \ No newline at end of file diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt new file mode 100644 index 00000000..55078f08 --- /dev/null +++ b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt @@ -0,0 +1,20 @@ +/* + * 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.base.navigation + +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector + +// todo +@Composable +actual fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) { + IconButton(onClick = onClick) { + Icon(icon, contentDescription) + } +} diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt new file mode 100644 index 00000000..16a7ea3d --- /dev/null +++ b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt @@ -0,0 +1,17 @@ +/* + * 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.base.theme + +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.ScrollbarStyle + +actual object ThemeScrollbarStyle { + @Composable + actual fun getScrollbarStyle(): ScrollbarStyle { + return ScrollbarStyle() + } +} diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt new file mode 100644 index 00000000..d1923647 --- /dev/null +++ b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.base.vm + +import ca.gosyer.ui.base.theme.AppThemeViewModel +import ca.gosyer.ui.updates.UpdatesScreenViewModel +import ca.gosyer.uicore.vm.ViewModel +import ca.gosyer.uicore.vm.ViewModelFactory +import me.tatarka.inject.annotations.Inject +import kotlin.reflect.KClass + +@Inject +actual class ViewModelFactoryImpl( + private val appThemeFactory: () -> AppThemeViewModel, + 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() + UpdatesScreenViewModel::class -> updatesFactory() + else -> throw IllegalArgumentException("Unknown ViewModel $klass") + } as VM + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/WindowDialog.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/WindowDialog.kt deleted file mode 100644 index 0777fd2f..00000000 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/WindowDialog.kt +++ /dev/null @@ -1,180 +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.base - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.key -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window -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.ui.util.lang.launchApplication -import kotlinx.coroutines.DelicateCoroutinesApi - -@OptIn(DelicateCoroutinesApi::class) -@Suppress("FunctionName") -fun WindowDialog( - title: String = "Dialog", - size: DpSize = DpSize(400.dp, 200.dp), - onCloseRequest: (() -> Unit)? = null, - forceFocus: Boolean = true, - showNegativeButton: Boolean = true, - negativeButtonText: String = "Cancel", - onNegativeButton: (() -> Unit)? = null, - positiveButtonText: String = "OK", - onPositiveButton: (() -> Unit)? = null, - keyboardShortcuts: Map Boolean> = emptyMap(), - row: @Composable RowScope.() -> Unit -) = launchApplication { - DisposableEffect(Unit) { - onDispose { - onCloseRequest?.invoke() - } - } - - fun (() -> Unit)?.plusClose(): (() -> Unit) = { - this?.invoke() - exitApplication() - } - - val icon = painterResource("icon.png") - val hooks = AppComponent.getInstance().uiComponent.getHooks() - val windowState = rememberWindowState(size = size, position = WindowPosition(Alignment.Center)) - - Window( - title = title, - icon = icon, - state = windowState, - onCloseRequest = ::exitApplication, - onKeyEvent = { - when { - it.key == Key.Enter -> { - onPositiveButton.plusClose()() - true - } - it.key == Key.Escape -> { - onNegativeButton.plusClose()() - true - } - keyboardShortcuts[it.key] != null -> { - keyboardShortcuts[it.key]?.invoke(it) ?: false - } - else -> false - } - }, - alwaysOnTop = forceFocus - ) { - CompositionLocalProvider( - *hooks - ) { - AppTheme { - Surface { - Box(modifier = Modifier.fillMaxSize()) { - Row( - content = row, - modifier = Modifier.fillMaxWidth() - ) - Row( - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.End, - modifier = Modifier.height(70.dp) - .align(Alignment.BottomEnd) - ) { - if (showNegativeButton) { - OutlinedButton(onNegativeButton.plusClose(), modifier = Modifier.padding(end = 8.dp, bottom = 8.dp)) { - Text(negativeButtonText) - } - } - - OutlinedButton(onPositiveButton.plusClose(), modifier = Modifier.padding(end = 8.dp, bottom = 8.dp)) { - Text(positiveButtonText) - } - } - } - } - } - } - } -} - -@OptIn(DelicateCoroutinesApi::class) -fun WindowDialog( - title: String = "Dialog", - size: DpSize = DpSize(400.dp, 200.dp), - onCloseRequest: (() -> Unit)? = null, - forceFocus: Boolean = true, - keyboardShortcuts: Map Boolean> = emptyMap(), - buttons: @Composable BoxWithConstraintsScope.(() -> Unit) -> Unit, - content: @Composable BoxWithConstraintsScope.(() -> Unit) -> Unit -) = launchApplication { - DisposableEffect(Unit) { - onDispose { - onCloseRequest?.invoke() - } - } - - val icon = painterResource("icon.png") - val hooks = AppComponent.getInstance().uiComponent.getHooks() - val windowState = rememberWindowState(size = size, position = WindowPosition.Aligned(Alignment.Center)) - - Window( - title = title, - icon = icon, - state = windowState, - onCloseRequest = ::exitApplication, - onKeyEvent = { - when { - keyboardShortcuts[it.key] != null -> { - keyboardShortcuts[it.key]?.invoke(it) ?: false - } - else -> false - } - }, - alwaysOnTop = forceFocus, - ) { - CompositionLocalProvider( - *hooks - ) { - AppTheme { - Surface { - Column { - BoxWithConstraints( - modifier = Modifier.fillMaxSize() - ) { - content(::exitApplication) - buttons(::exitApplication) - } - } - } - } - } - } -} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt new file mode 100644 index 00000000..5f5db328 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt @@ -0,0 +1,47 @@ +/* + * 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.base.components + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.ui.Modifier + +actual typealias ScrollbarAdapter = androidx.compose.foundation.ScrollbarAdapter + +actual typealias ScrollbarStyle = androidx.compose.foundation.ScrollbarStyle + +actual val LocalScrollbarStyle: ProvidableCompositionLocal + get() = androidx.compose.foundation.LocalScrollbarStyle + +@Composable +actual fun VerticalScrollbar( + adapter: ScrollbarAdapter, + modifier: Modifier, + reverseLayout: Boolean, + style: ScrollbarStyle, + interactionSource: MutableInteractionSource +) = androidx.compose.foundation.VerticalScrollbar( + adapter, modifier, reverseLayout, style, interactionSource +) + + +@Composable +actual fun rememberScrollbarAdapter( + scrollState: ScrollState +): ScrollbarAdapter { + return androidx.compose.foundation.rememberScrollbarAdapter(scrollState) +} + +@Composable +actual fun rememberScrollbarAdapter( + scrollState: LazyListState, +): ScrollbarAdapter { + return androidx.compose.foundation.rememberScrollbarAdapter(scrollState) +} \ No newline at end of file diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt new file mode 100644 index 00000000..6878446f --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt @@ -0,0 +1,30 @@ +/* + * 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.base.navigation + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import ca.gosyer.uicore.components.BoxWithTooltipSurface + +@Composable +actual fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) { + BoxWithTooltipSurface( + { + Text(contentDescription, modifier = Modifier.padding(10.dp)) + } + ) { + IconButton(onClick = onClick) { + Icon(icon, contentDescription) + } + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt new file mode 100644 index 00000000..b7b265d4 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt @@ -0,0 +1,26 @@ +/* + * 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.base.theme + +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import ca.gosyer.ui.base.components.ScrollbarStyle + +actual object ThemeScrollbarStyle { + @Composable + actual fun getScrollbarStyle(): ScrollbarStyle { + return androidx.compose.foundation.ScrollbarStyle( + minimalHeight = 16.dp, + thickness = 8.dp, + shape = MaterialTheme.shapes.small, + hoverDurationMillis = 300, + unhoverColor = MaterialTheme.colors.onSurface.copy(alpha = 0.30f), + hoverColor = MaterialTheme.colors.onSurface.copy(alpha = 0.70f) + ) + } +} 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 94ba0485..c24e6e67 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 @@ -21,6 +21,7 @@ import ca.gosyer.ui.settings.SettingsBackupViewModel import ca.gosyer.ui.settings.SettingsGeneralViewModel import ca.gosyer.ui.settings.SettingsLibraryViewModel import ca.gosyer.ui.settings.SettingsReaderViewModel +import ca.gosyer.ui.settings.SettingsServerHostViewModel import ca.gosyer.ui.settings.SettingsServerViewModel import ca.gosyer.ui.settings.ThemesViewModel import ca.gosyer.ui.sources.SourcesScreenViewModel @@ -35,7 +36,7 @@ import me.tatarka.inject.annotations.Inject import kotlin.reflect.KClass @Inject -class ViewModelFactoryImpl( +actual class ViewModelFactoryImpl( private val appThemeFactory: () -> AppThemeViewModel, private val categoryFactory: () -> CategoriesScreenViewModel, private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel, @@ -53,6 +54,7 @@ class ViewModelFactoryImpl( private val settingsLibraryFactory: () -> SettingsLibraryViewModel, private val settingsReaderFactory: () -> SettingsReaderViewModel, private val settingsServerFactory: () -> SettingsServerViewModel, + private val settingsServerHostFactory: () -> SettingsServerHostViewModel, private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel, private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel, private val sourceHomeFactory: () -> SourceHomeScreenViewModel, @@ -81,6 +83,7 @@ class ViewModelFactoryImpl( SettingsLibraryViewModel::class -> settingsLibraryFactory() SettingsReaderViewModel::class -> settingsReaderFactory() SettingsServerViewModel::class -> settingsServerFactory() + SettingsServerHostViewModel::class -> settingsServerHostFactory() SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params) SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params) SourceHomeScreenViewModel::class -> sourceHomeFactory() diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesDialogs.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesDialogs.kt index b5e08751..ac42d6dd 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesDialogs.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesDialogs.kt @@ -6,81 +6,98 @@ package ca.gosyer.ui.categories.components -import androidx.compose.material.Text import androidx.compose.material.TextField -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier 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.base.dialog.getMaterialDialogProperties import ca.gosyer.ui.categories.CategoriesScreenViewModel import ca.gosyer.uicore.components.keyboardHandler import ca.gosyer.uicore.resources.stringResource -import kotlinx.coroutines.flow.MutableStateFlow +import com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.MaterialDialogState +import com.vanpra.composematerialdialogs.message +import com.vanpra.composematerialdialogs.title -fun openRenameDialog( +@Composable +fun RenameDialog( + state: MaterialDialogState, category: CategoriesScreenViewModel.MenuCategory, onRename: (String) -> Unit ) { - val newName = MutableStateFlow(TextFieldValue(category.name)) + var newName by remember { mutableStateOf(TextFieldValue(category.name)) } - WindowDialog( - title = "${BuildKonfig.NAME} - Categories - Rename Dialog", - positiveButtonText = "Rename", - onPositiveButton = { - if (newName.value.text != category.name) { - onRename(newName.value.text) + MaterialDialog( + state, + buttons = { + positiveButton(stringResource(MR.strings.action_rename)) { + if (newName.text != category.name) { + onRename(newName.text) + } } - } + negativeButton(stringResource(MR.strings.action_cancel)) + }, + properties = getMaterialDialogProperties(), ) { - val newNameState by newName.collectAsState() - + title("Rename Category") TextField( - newNameState, + newName, onValueChange = { - newName.value = it + newName = it }, modifier = Modifier.keyboardHandler(singleLine = true) ) } } -fun openDeleteDialog( +@Composable +fun DeleteDialog( + state: MaterialDialogState, category: CategoriesScreenViewModel.MenuCategory, onDelete: (CategoriesScreenViewModel.MenuCategory) -> Unit ) { - WindowDialog( - title = "${BuildKonfig.NAME} - Categories - Delete Dialog", - positiveButtonText = "Yes", - onPositiveButton = { - onDelete(category) + MaterialDialog( + state, + buttons = { + positiveButton(stringResource(MR.strings.action_yes)) { + onDelete(category) + } + negativeButton(stringResource(MR.strings.action_no)) }, - negativeButtonText = "No" + properties = getMaterialDialogProperties(), ) { - Text(stringResource(MR.strings.categories_delete_confirm, category.name)) + title("Delete Category") + message(stringResource(MR.strings.categories_delete_confirm, category.name)) } } -fun openCreateDialog( +@Composable +fun CreateDialog( + state: MaterialDialogState, onCreate: (String) -> Unit ) { - val name = MutableStateFlow(TextFieldValue("")) + var name by remember { mutableStateOf(TextFieldValue("")) } - WindowDialog( - title = "${BuildKonfig.NAME} - Categories - Create Dialog", - positiveButtonText = "Create", - onPositiveButton = { - onCreate(name.value.text) - } + MaterialDialog( + state, + buttons = { + positiveButton(stringResource(MR.strings.action_create)) { + onCreate(name.text) + } + negativeButton(stringResource(MR.strings.action_cancel)) + }, + properties = getMaterialDialogProperties(), ) { - val nameState by name.collectAsState() - + title("Create Category") TextField( - nameState, + name, onValueChange = { - name.value = it + name = it }, singleLine = true, modifier = Modifier.keyboardHandler(singleLine = true) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesScreenContent.kt index 1e587475..7e73f8d5 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesScreenContent.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/components/CategoriesScreenContent.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import ca.gosyer.i18n.MR import ca.gosyer.ui.categories.CategoriesScreenViewModel.MenuCategory import ca.gosyer.uicore.resources.stringResource +import com.vanpra.composematerialdialogs.rememberMaterialDialogState import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -77,11 +78,16 @@ fun CategoriesScreenContent( } } + + val createDialogState = rememberMaterialDialogState() + Surface { Box { val state = rememberLazyListState() LazyColumn(modifier = Modifier.fillMaxSize(), state = state,) { itemsIndexed(categories) { i, category -> + val renameDialogState = rememberMaterialDialogState() + val deleteDialogState = rememberMaterialDialogState() CategoryRow( category = category, moveUpEnabled = i != 0, @@ -89,16 +95,18 @@ fun CategoriesScreenContent( onMoveUp = { moveCategoryUp(category) }, onMoveDown = { moveCategoryDown(category) }, onRename = { - openRenameDialog(category) { - renameCategory(category, it) - } + renameDialogState.show() }, onDelete = { - openDeleteDialog(category) { - deleteCategory(category) - } + deleteDialogState.show() }, ) + RenameDialog(renameDialogState, category) { + renameCategory(category, it) + } + DeleteDialog(deleteDialogState, category) { + deleteCategory(category) + } } item { Spacer(Modifier.height(80.dp).fillMaxWidth()) @@ -109,9 +117,7 @@ fun CategoriesScreenContent( icon = { Icon(imageVector = Icons.Rounded.Add, contentDescription = null) }, modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp), onClick = { - openCreateDialog { - createCategory(it) - } + createDialogState.show() } ) VerticalScrollbar( @@ -122,6 +128,7 @@ fun CategoriesScreenContent( ) } } + CreateDialog(createDialogState, createCategory) } @Composable diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreen.kt index f39d046c..8e19f011 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreen.kt @@ -6,38 +6,16 @@ package ca.gosyer.ui.downloads -import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.AppComponent 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 { - CompositionLocalProvider(*remember { AppComponent.getInstance().uiComponent.getHooks() }) { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - Surface { - Navigator(remember { DownloadsScreen() }) - } - } - } - } -} class DownloadsScreen : Screen { override val key: ScreenKey = uniqueScreenKey diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreen.kt index 8ab99fdc..522f799e 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreen.kt @@ -6,39 +6,13 @@ package ca.gosyer.ui.extensions -import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider 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.AppComponent 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 { - CompositionLocalProvider(*remember { AppComponent.getInstance().uiComponent.getHooks() }) { - val state = rememberWindowState(size = DpSize(550.dp, 700.dp)) - ThemedWindow(::exitApplication, state, title = BuildKonfig.NAME) { - Surface { - Navigator(remember { ExtensionsScreen() }) - } - } - } - } -} class ExtensionsScreen : Screen { @@ -52,9 +26,9 @@ class ExtensionsScreen : Screen { extensions = vm.extensions.collectAsState().value, isLoading = vm.isLoading.collectAsState().value, query = vm.searchQuery.collectAsState().value, - setQuery = vm::search, - enabledLangs = vm.enabledLangs, - getSourceLanguages = vm::getSourceLanguages, + setQuery = vm::setQuery, + enabledLangs = vm.enabledLangs.collectAsState().value, + availableLangs = vm.availableLangs.collectAsState().value, setEnabledLanguages = vm::setEnabledLanguages, installExtension = vm::install, updateExtension = vm::update, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt index 57ead3b9..9cc91054 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt @@ -14,10 +14,12 @@ import ca.gosyer.data.server.interactions.ExtensionInteractionHandler import ca.gosyer.i18n.MR import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import java.util.Locale @@ -26,36 +28,43 @@ class ExtensionsScreenViewModel @Inject constructor( private val extensionHandler: ExtensionInteractionHandler, extensionPreferences: ExtensionPreferences ) : ViewModel() { + private val extensionList = MutableStateFlow?>(null) + private val _enabledLangs = extensionPreferences.languages().asStateFlow() val enabledLangs = _enabledLangs.asStateFlow() - private var extensionList: List? = null + private val _searchQuery = MutableStateFlow(null) + val searchQuery = _searchQuery.asStateFlow() - private val _extensions = MutableStateFlow(emptyMap>()) - val extensions = _extensions.asStateFlow() + val extensions = combine( + searchQuery, + extensionList, + enabledLangs + ) { searchQuery, extensions, enabledLangs -> + search(searchQuery, extensions, enabledLangs) + }.stateIn(scope, SharingStarted.Eagerly, emptyMap()) + + val availableLangs = extensionList.filterNotNull().map { langs -> + langs.map { it.lang }.toSet() + }.stateIn(scope, SharingStarted.Eagerly, emptySet()) private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow() - val searchQuery = MutableStateFlow(null) + init { scope.launch { getExtensions() } - - enabledLangs.drop(1).onEach { - search(searchQuery.value.orEmpty()) - }.launchIn(scope) } private suspend fun getExtensions() { try { _isLoading.value = true - extensionList = extensionHandler.getExtensionList() - search(searchQuery.value.orEmpty()) + extensionList.value = extensionHandler.getExtensionList() } catch (e: Exception) { e.throwIfCancellation() - extensionList = emptyList() + extensionList.value = emptyList() } finally { _isLoading.value = false } @@ -97,26 +106,26 @@ class ExtensionsScreenViewModel @Inject constructor( } } - fun getSourceLanguages() = extensionList?.map { it.lang }?.toSet().orEmpty() - fun setEnabledLanguages(langs: Set) { - info { langs } _enabledLangs.value = langs } - fun search(searchQuery: String) { - this.searchQuery.value = searchQuery.takeUnless { it.isBlank() } - val extensionList = extensionList?.filter { it.lang in enabledLangs.value } + fun setQuery(query: String) { + _searchQuery.value = query + } + + private fun search(searchQuery: String?, extensionList: List?, enabledLangs: Set): Map> { + val extensions = extensionList?.filter { it.lang in enabledLangs } .orEmpty() - if (searchQuery.isBlank()) { - _extensions.value = extensionList.splitSort() + return if (searchQuery.isNullOrBlank()) { + extensions.splitSort() } else { val queries = searchQuery.split(" ") - val extensions = extensionList.toMutableList() + val filteredExtensions = extensions.toMutableList() queries.forEach { query -> - extensions.removeIf { !it.name.contains(query, true) } + filteredExtensions.removeIf { !it.name.contains(query, true) } } - _extensions.value = extensions.toList().splitSort() + filteredExtensions.toList().splitSort() } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt index ff0014bf..28435511 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt @@ -33,9 +33,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Translate import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -48,15 +47,17 @@ import androidx.compose.ui.unit.sp import ca.gosyer.data.models.Extension import ca.gosyer.i18n.MR import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.base.WindowDialog +import ca.gosyer.ui.base.dialog.getMaterialDialogProperties import ca.gosyer.ui.base.navigation.ActionItem import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.resources.stringResource +import com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.MaterialDialogState +import com.vanpra.composematerialdialogs.rememberMaterialDialogState +import com.vanpra.composematerialdialogs.title import io.kamel.image.lazyPainterResource -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import java.util.Locale @Composable @@ -65,26 +66,25 @@ fun ExtensionsScreenContent( isLoading: Boolean, query: String?, setQuery: (String) -> Unit, - enabledLangs: StateFlow>, - getSourceLanguages: () -> Set, + enabledLangs: Set, + availableLangs: Set, setEnabledLanguages: (Set) -> Unit, installExtension: (Extension) -> Unit, updateExtension: (Extension) -> Unit, uninstallExtension: (Extension) -> Unit ) { + val languageDialogState = rememberMaterialDialogState() Scaffold( topBar = { ExtensionsToolbar( query, setQuery, - enabledLangs, - getSourceLanguages, - setEnabledLanguages + languageDialogState::show ) } ) { if (isLoading) { - LoadingScreen(isLoading) + LoadingScreen() } else { val state = rememberLazyListState() @@ -118,26 +118,21 @@ fun ExtensionsScreenContent( } } } + LanguageDialog(languageDialogState, enabledLangs, availableLangs, setEnabledLanguages) } @Composable fun ExtensionsToolbar( searchText: String?, search: (String) -> Unit, - currentEnabledLangs: StateFlow>, - getSourceLanguages: () -> Set, - setEnabledLanguages: (Set) -> Unit + openLanguageDialog: () -> Unit ) { Toolbar( stringResource(MR.strings.location_extensions), searchText = searchText, search = search, actions = { - getActionItems( - currentEnabledLangs = currentEnabledLangs, - getSourceLanguages = getSourceLanguages, - setEnabledLanguages = setEnabledLanguages - ) + getActionItems(openLanguageDialog) } ) } @@ -195,26 +190,42 @@ fun ExtensionItem( } } -fun LanguageDialog(enabledLangsFlow: MutableStateFlow>, availableLangs: List, setLangs: () -> Unit) { - WindowDialog(BuildKonfig.NAME, onPositiveButton = setLangs) { - val locale = Locale.getDefault() - val enabledLangs by enabledLangsFlow.collectAsState() - val state = rememberLazyListState() +@Composable +fun LanguageDialog( + state: MaterialDialogState, + enabledLangs: Set, + availableLangs: Set, + setLangs: (Set) -> Unit +) { + val modifiedLangs = remember(enabledLangs) { enabledLangs.toMutableStateList() } + MaterialDialog( + state, + buttons = { + positiveButton(stringResource(MR.strings.action_ok)) { + setLangs(modifiedLangs.toSet()) + } + negativeButton(stringResource(MR.strings.action_cancel)) + }, + properties = getMaterialDialogProperties(), + ) { + title(BuildKonfig.NAME) Box { - LazyColumn(Modifier.fillMaxWidth(), state) { - items(availableLangs) { lang -> + val locale = remember { Locale.getDefault() } + val listState = rememberLazyListState() + LazyColumn(Modifier.fillMaxWidth(), listState) { + items(availableLangs.toList()) { lang -> Row { val langName = remember(lang) { Locale.forLanguageTag(lang)?.getDisplayName(locale) ?: lang } Text(langName) Switch( - lang in enabledLangs, - { + checked = lang in modifiedLangs, + onCheckedChange = { if (it) { - enabledLangsFlow.value += lang + modifiedLangs += lang } else { - enabledLangsFlow.value -= lang + modifiedLangs -= lang } } ) @@ -223,7 +234,7 @@ fun LanguageDialog(enabledLangsFlow: MutableStateFlow>, availableLan item { Spacer(Modifier.height(70.dp)) } } VerticalScrollbar( - rememberScrollbarAdapter(state), + rememberScrollbarAdapter(listState), Modifier.align(Alignment.CenterEnd) .fillMaxHeight() .padding(horizontal = 4.dp, vertical = 8.dp) @@ -235,19 +246,13 @@ fun LanguageDialog(enabledLangsFlow: MutableStateFlow>, availableLan @Stable @Composable private fun getActionItems( - currentEnabledLangs: StateFlow>, - getSourceLanguages: () -> Set, - setEnabledLanguages: (Set) -> Unit + openLanguageDialog: () -> Unit ): List { return listOf( ActionItem( stringResource(MR.strings.enabled_languages), - Icons.Rounded.Translate - ) { - val enabledLangs = MutableStateFlow(currentEnabledLangs.value) - LanguageDialog(enabledLangs, getSourceLanguages().toList()) { - setEnabledLanguages(enabledLangs.value) - } - } + Icons.Rounded.Translate, + doAction = openLanguageDialog + ) ) } 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 8b1030e6..80d90026 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/LibraryScreen.kt @@ -6,38 +6,16 @@ package ca.gosyer.ui.library -import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.AppComponent 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 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 { - CompositionLocalProvider(*remember { AppComponent.getInstance().uiComponent.getHooks() }) { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - Surface { - Navigator(remember { LibraryScreen() }) - } - } - } - } -} class LibraryScreen : Screen { 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 index 98ba4302..ea7accb0 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryPager.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/library/components/LibraryPager.kt @@ -29,7 +29,7 @@ fun LibraryPager( ) { if (categories.isEmpty()) return - val state = rememberPagerState(categories.size, selectedPage) + val state = rememberPagerState(selectedPage) LaunchedEffect(state.currentPage) { if (state.currentPage != selectedPage) { onPageChanged(state.currentPage) @@ -40,7 +40,7 @@ fun LibraryPager( state.animateScrollToPage(selectedPage) } } - HorizontalPager(state = state) { + HorizontalPager(categories.size, state = state) { val library by getLibraryForPage(categories[it].id) when (displayMode) { DisplayMode.CompactGrid -> LibraryMangaCompactGrid( 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 cb2341c9..7f1ff242 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/TopLevelMenus.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/TopLevelMenus.kt @@ -26,14 +26,11 @@ 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.main.more.MoreScreen import ca.gosyer.ui.settings.SettingsScreen import ca.gosyer.ui.sources.SourcesScreen -import ca.gosyer.ui.sources.openSourcesMenu import ca.gosyer.ui.updates.UpdatesScreen import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.Navigator @@ -46,7 +43,6 @@ interface Menu { val selectedIcon: ImageVector val screen: KClass<*> val createScreen: () -> Screen - val openInNewWindow: (() -> Unit)? val extraInfo: (@Composable () -> Unit)? fun isSelected(navigator: Navigator) = navigator.items.first()::class == screen @@ -58,13 +54,12 @@ enum class TopLevelMenus( override val selectedIcon: ImageVector, override val screen: KClass<*>, override val createScreen: () -> Screen, - override val openInNewWindow: (() -> Unit)? = null, override val extraInfo: (@Composable () -> Unit)? = null ) : Menu { - Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { LibraryScreen() }, ::openLibraryMenu), - Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, UpdatesScreen::class, { UpdatesScreen() }, ::openLibraryMenu), - Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, SourcesScreen::class, { SourcesScreen() }, ::openSourcesMenu), - Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, ExtensionsScreen::class, { ExtensionsScreen() }, ::openExtensionsMenu), + Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { LibraryScreen() }), + Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, UpdatesScreen::class, { UpdatesScreen() }), + Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, SourcesScreen::class, { SourcesScreen() }), + Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, ExtensionsScreen::class, { ExtensionsScreen() }), More(MR.strings.location_more, Icons.Outlined.MoreHoriz, Icons.Rounded.MoreHoriz, MoreScreen::class, { MoreScreen() }); } @@ -74,9 +69,8 @@ enum class MoreMenus( override val selectedIcon: ImageVector, override val screen: KClass<*>, override val createScreen: () -> Screen, - override val openInNewWindow: (() -> Unit)? = null, override val extraInfo: (@Composable () -> Unit)? = null ) : Menu { Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, DownloadsScreen::class, { DownloadsScreen() }, extraInfo = { DownloadsExtraInfo() }), Settings(MR.strings.location_settings, Icons.Outlined.Settings, Icons.Rounded.Settings, SettingsScreen::class, { SettingsScreen() }); -} \ No newline at end of file +} 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 b8fb90cc..bc1f6f6a 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 @@ -6,6 +6,7 @@ package ca.gosyer.ui.main.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -25,7 +26,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import ca.gosyer.ui.main.Menu -import ca.gosyer.uicore.components.combinedMouseClickable import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.Screen @@ -37,7 +37,6 @@ fun SideMenuItem(selected: Boolean, topLevelMenu: Menu, newRoot: (Screen) -> Uni topLevelMenu.createScreen, topLevelMenu.selectedIcon, topLevelMenu.unselectedIcon, - topLevelMenu.openInNewWindow, topLevelMenu.extraInfo, newRoot ) @@ -50,7 +49,6 @@ private fun SideMenuItem( createScreen: () -> Screen, selectedIcon: ImageVector, unselectedIcon: ImageVector, - onMiddleClick: (() -> Unit)?, extraInfo: (@Composable () -> Unit)? = null, onClick: (Screen) -> Unit ) { @@ -68,9 +66,9 @@ private fun SideMenuItem( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() .defaultMinSize(minHeight = 40.dp) - .combinedMouseClickable( + .clickable( onClick = { onClick(createScreen()) }, - onMiddleClick = { onMiddleClick?.invoke() } + // onMiddleClick = { onMiddleClick?.invoke() } todo ) ) { Spacer(Modifier.width(16.dp)) 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 df8ccd78..c53d51e0 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 @@ -9,6 +9,7 @@ package ca.gosyer.ui.main.components import ca.gosyer.data.update.UpdateChecker import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel import me.tatarka.inject.annotations.Inject class TrayViewModel @Inject constructor( @@ -21,4 +22,9 @@ class TrayViewModel @Inject constructor( } val updateFound get() = updateChecker.updateFound + + override fun onDispose() { + super.onDispose() + scope.cancel() + } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreen.kt index 2a31bc14..87855bb8 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreen.kt @@ -6,35 +6,13 @@ package ca.gosyer.ui.manga -import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.AppComponent 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 { - CompositionLocalProvider(*remember { AppComponent.getInstance().uiComponent.getHooks() }) { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - Surface { - Navigator(remember { MangaScreen(mangaId) }) - } - } - } - } -} class MangaScreen(private val mangaId: Long) : Screen { @@ -53,6 +31,8 @@ class MangaScreen(private val mangaId: Long) : Screen { dateTimeFormatter = vm.dateTimeFormatter.collectAsState().value, categoriesExist = vm.categoriesExist.collectAsState().value, chooseCategoriesFlow = vm.chooseCategoriesFlow, + availableCategories = vm.categories.collectAsState().value, + mangaCategories = vm.mangaCategories.collectAsState().value, addFavorite = vm::addFavorite, setCategories = vm::setCategories, toggleFavorite = vm::toggleFavorite, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt index 68d5cf65..f1d4d081 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt @@ -22,10 +22,12 @@ import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import java.time.ZoneId @@ -53,10 +55,16 @@ class MangaScreenViewModel @Inject constructor( private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow() - private val _categoriesExist = MutableStateFlow(true) - val categoriesExist = _categoriesExist.asStateFlow() + private val _categories = MutableStateFlow(emptyList()) + val categories = _categories.asStateFlow() - val chooseCategoriesFlow = MutableSharedFlow, List>>() + private val _mangaCategories = MutableStateFlow(emptyList()) + val mangaCategories = _mangaCategories.asStateFlow() + + val categoriesExist = categories.map { it.isNotEmpty() } + .stateIn(scope, SharingStarted.Eagerly, true) + + val chooseCategoriesFlow = MutableSharedFlow() val dateTimeFormatter = uiPreferences.dateFormat().changes() .map { @@ -77,7 +85,7 @@ class MangaScreenViewModel @Inject constructor( } scope.launch { - _categoriesExist.value = categoryHandler.getCategories(true).isNotEmpty() + _categories.value = categoryHandler.getCategories(true) } } @@ -108,9 +116,7 @@ class MangaScreenViewModel @Inject constructor( fun setCategories() { scope.launch { manga.value?.let { manga -> - val categories = async { categoryHandler.getCategories(true) } - val oldCategories = async { categoryHandler.getMangaCategories(manga) } - chooseCategoriesFlow.emit(categories.await() to oldCategories.await()) + chooseCategoriesFlow.emit(Unit) } } } @@ -119,6 +125,7 @@ class MangaScreenViewModel @Inject constructor( async { try { _manga.value = mangaHandler.getManga(mangaId, refresh) + _mangaCategories.value = categoryHandler.getMangaCategories(mangaId) } catch (e: Exception) { e.throwIfCancellation() } @@ -142,11 +149,10 @@ class MangaScreenViewModel @Inject constructor( libraryHandler.removeMangaFromLibrary(manga) refreshMangaAsync(manga.id).await() } else { - val categories = categoryHandler.getCategories(true) - if (categories.isEmpty()) { + if (categories.value.isEmpty()) { addFavorite(emptyList(), emptyList()) } else { - chooseCategoriesFlow.emit(categories to emptyList()) + chooseCategoriesFlow.emit(Unit) } } } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaMenu.kt index 79e1ce5a..09905d0a 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaMenu.kt @@ -32,8 +32,8 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface 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.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality @@ -43,11 +43,15 @@ 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.ui.base.WindowDialog +import ca.gosyer.i18n.MR +import ca.gosyer.ui.base.dialog.getMaterialDialogProperties import ca.gosyer.uicore.image.KamelImage +import ca.gosyer.uicore.resources.stringResource import com.google.accompanist.flowlayout.FlowRow +import com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.MaterialDialogState +import com.vanpra.composematerialdialogs.title import io.kamel.image.lazyPainterResource -import kotlinx.coroutines.flow.MutableStateFlow @Composable fun MangaItem(manga: Manga) { @@ -121,28 +125,36 @@ private fun Chip(text: String) { } } -fun openCategorySelectDialog( +@Composable +fun CategorySelectDialog( + state: MaterialDialogState, categories: List, oldCategories: List, onPositiveClick: (List, List) -> Unit ) { - val enabledCategoriesFlow = MutableStateFlow(oldCategories) - WindowDialog( - "Select Categories", - onPositiveButton = { onPositiveClick(enabledCategoriesFlow.value, oldCategories) } + val enabledCategories = remember(oldCategories) { oldCategories.toMutableStateList() } + MaterialDialog( + state, + buttons = { + positiveButton(stringResource(MR.strings.action_ok)) { + onPositiveClick(enabledCategories.toList(), oldCategories) + } + negativeButton(stringResource(MR.strings.action_cancel)) + }, + properties = getMaterialDialogProperties(), ) { - val enabledCategories by enabledCategoriesFlow.collectAsState() - val state = rememberLazyListState() + title("Select Categories") + val listState = rememberLazyListState() Box { - LazyColumn(state = state) { + LazyColumn(state = listState) { items(categories) { category -> Row( Modifier.fillMaxWidth().padding(8.dp) .clickable { if (category in enabledCategories) { - enabledCategoriesFlow.value -= category + enabledCategories -= category } else { - enabledCategoriesFlow.value += category + enabledCategories += category } }, horizontalArrangement = Arrangement.SpaceBetween @@ -159,7 +171,7 @@ fun openCategorySelectDialog( modifier = Modifier.align(Alignment.CenterEnd) .fillMaxHeight() .padding(horizontal = 4.dp, vertical = 8.dp), - adapter = rememberScrollbarAdapter(state) + adapter = rememberScrollbarAdapter(listState) ) } } 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 index 069cef4f..255fa3dd 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt @@ -38,6 +38,7 @@ 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 com.vanpra.composematerialdialogs.rememberMaterialDialogState import kotlinx.coroutines.flow.SharedFlow import java.time.format.DateTimeFormatter @@ -48,7 +49,9 @@ fun MangaScreenContent( chapters: List, dateTimeFormatter: DateTimeFormatter, categoriesExist: Boolean, - chooseCategoriesFlow: SharedFlow, List>>, + chooseCategoriesFlow: SharedFlow, + availableCategories: List, + mangaCategories: List, addFavorite: (List, List) -> Unit, setCategories: () -> Unit, toggleFavorite: () -> Unit, @@ -62,9 +65,10 @@ fun MangaScreenContent( loadChapters: () -> Unit, loadManga: () -> Unit ) { + val categoryDialogState = rememberMaterialDialogState() LaunchedEffect(Unit) { - chooseCategoriesFlow.collect { (availableCategories, usedCategories) -> - openCategorySelectDialog(availableCategories, usedCategories, addFavorite) + chooseCategoriesFlow.collect { + categoryDialogState.show() } } @@ -135,6 +139,7 @@ fun MangaScreenContent( } } } + CategorySelectDialog(categoryDialogState, availableCategories, mangaCategories, addFavorite) } @Composable diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/viewer/Pager.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/viewer/Pager.kt index 7a0ab699..ed589582 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/viewer/Pager.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/viewer/Pager.kt @@ -41,7 +41,7 @@ fun PagerReader( retry: (ReaderPage) -> Unit, progress: (Int) -> Unit ) { - val state = rememberPagerState(pages.size + 2, initialPage = currentPage) + val state = rememberPagerState(initialPage = currentPage) LaunchedEffect(Unit) { val pageRange = 0..(pages.size + 1) @@ -77,7 +77,12 @@ fun PagerReader( val modifier = parentModifier then Modifier.fillMaxSize() if (direction == Direction.Down || direction == Direction.Up) { - VerticalPager(state, reverseLayout = direction == Direction.Up, modifier = modifier) { + VerticalPager( + count = pages.size + 2, + state = state, + reverseLayout = direction == Direction.Up, + modifier = modifier + ) { HandlePager( pages, it, @@ -90,7 +95,12 @@ fun PagerReader( ) } } else { - HorizontalPager(state, reverseLayout = direction == Direction.Left, modifier = modifier) { + HorizontalPager( + count = pages.size + 2, + state = state, + reverseLayout = direction == Direction.Left, + modifier = modifier + ) { HandlePager( pages, it, 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 edd91d36..b511db7b 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt @@ -27,6 +27,10 @@ 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.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -36,7 +40,7 @@ import ca.gosyer.core.lang.throwIfCancellation 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.dialog.getMaterialDialogProperties import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.util.system.filePicker @@ -47,6 +51,10 @@ 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 com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.MaterialDialogState +import com.vanpra.composematerialdialogs.rememberMaterialDialogState +import com.vanpra.composematerialdialogs.title import io.ktor.client.features.onDownload import io.ktor.client.features.onUpload import io.ktor.http.isSuccess @@ -225,10 +233,15 @@ private fun SettingsBackupScreenContent( stopRestore: () -> Unit, exportBackup: () -> Unit ) { + var backupFile by remember { mutableStateOf(null) } + var missingSources by remember { mutableStateOf(emptyList()) } + val dialogState = rememberMaterialDialogState() LaunchedEffect(Unit) { launch { missingSourceFlow.collect { (backup, sources) -> - openMissingSourcesDialog(sources, { restoreBackup(backup) }, stopRestore) + backupFile = backup + missingSources = sources + dialogState.show() } } launch { @@ -278,21 +291,48 @@ private fun SettingsBackupScreenContent( ) } } + MissingSourcesDialog( + dialogState, + missingSources, + onPositiveClick = { + restoreBackup(backupFile ?: return@MissingSourcesDialog) + }, + onNegativeClick = stopRestore + ) } -private fun openMissingSourcesDialog(missingSources: List, onPositiveClick: () -> Unit, onNegativeClick: () -> Unit) { - WindowDialog( - "Missing Sources", - onPositiveButton = onPositiveClick, - onNegativeButton = onNegativeClick +@Composable +private fun MissingSourcesDialog( + state: MaterialDialogState, + missingSources: List, + onPositiveClick: () -> Unit, + onNegativeClick: () -> Unit +) { + MaterialDialog( + state, + buttons = { + positiveButton(stringResource(MR.strings.action_ok), onClick = onPositiveClick) + negativeButton(stringResource(MR.strings.action_cancel), onClick = onNegativeClick) + }, + properties = getMaterialDialogProperties(), ) { - LazyColumn { - item { - Text(stringResource(MR.strings.missing_sources), style = MaterialTheme.typography.subtitle2) - } - items(missingSources) { - Text(it) + title("Missing Sources") + Box { + val listState = rememberLazyListState() + LazyColumn(Modifier.fillMaxSize(), state = listState) { + item { + Text(stringResource(MR.strings.missing_sources), style = MaterialTheme.typography.subtitle2) + } + items(missingSources) { + Text(it) + } } + VerticalScrollbar( + rememberScrollbarAdapter(listState), + Modifier.align(Alignment.CenterEnd) + .fillMaxHeight() + .padding(horizontal = 4.dp, vertical = 8.dp) + ) } } } 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 9dce8f33..7066022b 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt @@ -59,71 +59,47 @@ class SettingsServerScreen : Screen { @Composable override fun Content() { - val vm = viewModel() + val connectionVM = viewModel() + val serverVm = 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 + hostValue = serverVm.host.collectAsState().value, + basicAuthEnabledValue = serverVm.basicAuthEnabled.collectAsState().value, + proxyValue = connectionVM.proxy.collectAsState().value, + authValue = connectionVM.auth.collectAsState().value, + restartServer = serverVm::restartServer, + serverSettingChanged = serverVm::serverSettingChanged, + host = serverVm.host, + ip = serverVm.ip, + port = serverVm.port, + socksProxyEnabled = serverVm.socksProxyEnabled, + socksProxyHost = serverVm.socksProxyHost, + socksProxyPort = serverVm.socksProxyPort, + debugLogsEnabled = serverVm.debugLogsEnabled, + systemTrayEnabled = serverVm.systemTrayEnabled, + webUIEnabled = serverVm.webUIEnabled, + openInBrowserEnabled = serverVm.openInBrowserEnabled, + basicAuthEnabled = serverVm.basicAuthEnabled, + basicAuthUsername = serverVm.basicAuthUsername, + basicAuthPassword = serverVm.basicAuthPassword, + serverUrl = connectionVM.serverUrl, + serverPort = connectionVM.serverPort, + proxy = connectionVM.proxy, + proxyChoices = connectionVM.getProxyChoices(), + httpHost = connectionVM.httpHost, + httpPort = connectionVM.httpPort, + socksHost = connectionVM.socksHost, + socksPort = connectionVM.socksPort, + auth = connectionVM.auth, + authChoices = connectionVM.getAuthChoices(), + authUsername = connectionVM.authUsername, + authPassword = connectionVM.authPassword ) } } class SettingsServerViewModel @Inject constructor( - serverPreferences: ServerPreferences, - serverHostPreferences: ServerHostPreferences, - private val serverService: ServerService + serverPreferences: ServerPreferences ) : ViewModel() { - val host = serverPreferences.host().asStateIn(scope) - val ip = serverHostPreferences.ip().asStateIn(scope) - val port = serverHostPreferences.port().asStringStateIn(scope) - - // Proxy - val socksProxyEnabled = serverHostPreferences.socksProxyEnabled().asStateIn(scope) - val socksProxyHost = serverHostPreferences.socksProxyHost().asStateIn(scope) - val socksProxyPort = serverHostPreferences.socksProxyPort().asStringStateIn(scope) - - // Misc - val debugLogsEnabled = serverHostPreferences.debugLogsEnabled().asStateIn(scope) - val systemTrayEnabled = serverHostPreferences.systemTrayEnabled().asStateIn(scope) - - // WebUI - val webUIEnabled = serverHostPreferences.webUIEnabled().asStateIn(scope) - val openInBrowserEnabled = serverHostPreferences.openInBrowserEnabled().asStateIn(scope) - - // Authentication - val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) - val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope) - val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope) - - // JUI connection val serverUrl = serverPreferences.server().asStateIn(scope) val serverPort = serverPreferences.port().asStringStateIn(scope) @@ -158,12 +134,53 @@ class SettingsServerViewModel @Inject constructor( _serverSettingChanged.value = true } + private companion object : CKLogger({}) +} + +class SettingsServerHostViewModel @Inject constructor( + serverPreferences: ServerPreferences, + serverHostPreferences: ServerHostPreferences, + private val serverService: ServerService +) : ViewModel() { + val host = serverHostPreferences.host().asStateIn(scope) + val ip = serverHostPreferences.ip().asStateIn(scope) + val port = serverHostPreferences.port().asStringStateIn(scope) + + // Proxy + val socksProxyEnabled = serverHostPreferences.socksProxyEnabled().asStateIn(scope) + val socksProxyHost = serverHostPreferences.socksProxyHost().asStateIn(scope) + val socksProxyPort = serverHostPreferences.socksProxyPort().asStringStateIn(scope) + + // Misc + val debugLogsEnabled = serverHostPreferences.debugLogsEnabled().asStateIn(scope) + val systemTrayEnabled = serverHostPreferences.systemTrayEnabled().asStateIn(scope) + + // WebUI + val webUIEnabled = serverHostPreferences.webUIEnabled().asStateIn(scope) + val openInBrowserEnabled = serverHostPreferences.openInBrowserEnabled().asStateIn(scope) + + // Authentication + val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) + val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope) + val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope) + + private val _serverSettingChanged = MutableStateFlow(false) + val serverSettingChanged = _serverSettingChanged.asStateFlow() + fun serverSettingChanged() { + _serverSettingChanged.value = true + } + fun restartServer() { if (serverSettingChanged.value) { serverService.restartServer() } } + // Handle password connection to hosted server + val auth = serverPreferences.auth().asStateIn(scope) + val authUsername = serverPreferences.authUsername().asStateIn(scope) + val authPassword = serverPreferences.authPassword().asStateIn(scope) + init { combine(basicAuthEnabled, basicAuthUsername, basicAuthPassword) { enabled, username, password -> if (enabled) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreen.kt index 18425b14..b9fc2f78 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/SourcesScreen.kt @@ -6,35 +6,13 @@ package ca.gosyer.ui.sources -import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.AppComponent 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 { - CompositionLocalProvider(*remember { AppComponent.getInstance().uiComponent.getHooks() }) { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - Surface { - Navigator(remember { SourcesScreen() }) - } - } - } - } -} class SourcesScreen : Screen { 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 index 2718f0c7..589818dd 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreen.kt @@ -27,8 +27,8 @@ class SourceHomeScreen : Screen { onAddSource = sourcesNavigator::select, isLoading = vm.isLoading.collectAsState().value, sources = vm.sources.collectAsState().value, - languages = vm.languages, - getSourceLanguages = vm::getSourceLanguages, + languages = vm.languages.collectAsState().value, + sourceLanguages = vm.sourceLanguages.collectAsState().value, setEnabledLanguages = vm::setEnabledLanguages ) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt index ed8908a4..67bb3980 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt @@ -13,7 +13,11 @@ import ca.gosyer.data.models.Source import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject @@ -24,13 +28,21 @@ class SourceHomeScreenViewModel @Inject constructor( private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow() + private val installedSources = MutableStateFlow(emptyList()) + private val _languages = catalogPreferences.languages().asStateFlow() val languages = _languages.asStateFlow() - private val _sources = MutableStateFlow(emptyList()) - val sources = _sources.asStateFlow() + val sources = combine(installedSources, languages) { installedSources, languages -> + installedSources.filter { + it.lang in languages || it.lang == Source.LOCAL_SOURCE_LANG + } + }.stateIn(scope, SharingStarted.Eagerly, emptyList()) + + val sourceLanguages = installedSources.map { sources -> + sources.map { it.lang }.toSet() - setOf(Source.LOCAL_SOURCE_LANG) + }.stateIn(scope, SharingStarted.Eagerly, emptySet()) - private var installedSources = emptyList() init { getSources() @@ -39,9 +51,7 @@ class SourceHomeScreenViewModel @Inject constructor( private fun getSources() { scope.launch { try { - installedSources = sourceHandler.getSourceList() - setSources(_languages.value) - info { _sources.value } + installedSources.value = sourceHandler.getSourceList() } catch (e: Exception) { e.throwIfCancellation() } finally { @@ -50,18 +60,9 @@ class SourceHomeScreenViewModel @Inject constructor( } } - private fun setSources(langs: Set) { - _sources.value = installedSources.filter { it.lang in langs || it.lang == Source.LOCAL_SOURCE_LANG } - } - - fun getSourceLanguages(): Set { - return installedSources.map { it.lang }.toSet() - setOf(Source.LOCAL_SOURCE_LANG) - } - fun setEnabledLanguages(langs: Set) { info { langs } _languages.value = langs - setSources(langs) } private companion object : CKLogger({}) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt index 45494348..dec296bf 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt @@ -48,25 +48,23 @@ import ca.gosyer.ui.extensions.components.LanguageDialog import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.resources.stringResource +import com.vanpra.composematerialdialogs.rememberMaterialDialogState import io.kamel.image.lazyPainterResource -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow @Composable fun SourceHomeScreenContent( onAddSource: (Source) -> Unit, isLoading: Boolean, sources: List, - languages: StateFlow>, - getSourceLanguages: () -> Set, + languages: Set, + sourceLanguages: Set, setEnabledLanguages: (Set) -> Unit ) { + val languageDialogState = rememberMaterialDialogState() Scaffold( topBar = { SourceHomeScreenToolbar( - languages, - getSourceLanguages, - setEnabledLanguages + languageDialogState::show ) } ) { @@ -97,24 +95,18 @@ fun SourceHomeScreenContent( } } } + LanguageDialog(languageDialogState, languages, sourceLanguages, setEnabledLanguages) } @Composable fun SourceHomeScreenToolbar( - sourceLanguages: StateFlow>, - onGetEnabledLanguages: () -> Set, - onSetEnabledLanguages: (Set) -> Unit + openEnabledLanguagesClick: () -> Unit ) { Toolbar( stringResource(MR.strings.location_sources), actions = { getActionItems( - onEnabledLanguagesClick = { - val enabledLangs = MutableStateFlow(sourceLanguages.value) - LanguageDialog(enabledLangs, onGetEnabledLanguages().toList()) { - onSetEnabledLanguages(enabledLangs.value) - } - } + openEnabledLanguagesClick = openEnabledLanguagesClick ) } ) @@ -177,13 +169,13 @@ fun SourceItem( @Composable @Stable private fun getActionItems( - onEnabledLanguagesClick: () -> Unit + openEnabledLanguagesClick: () -> Unit ): List { return listOf( ActionItem( stringResource(MR.strings.enabled_languages), Icons.Rounded.Translate, - doAction = onEnabledLanguagesClick + doAction = openEnabledLanguagesClick ) ) } 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 index 4b2d95b7..ea2eb99d 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreen.kt @@ -7,31 +7,12 @@ package ca.gosyer.ui.sources.settings import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import ca.gosyer.presentation.build.BuildKonfig -import ca.gosyer.ui.AppComponent 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 { - CompositionLocalProvider(*remember { AppComponent.getInstance().uiComponent.getHooks() }) { - ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { - Navigator(remember { SourceSettingsScreen(sourceId) }) - } - } - } -} class SourceSettingsScreen(private val sourceId: Long) : Screen { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/components/SourceSettingsScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/components/SourceSettingsScreenContent.kt index af59f5a1..979c550e 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/components/SourceSettingsScreenContent.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/settings/components/SourceSettingsScreenContent.kt @@ -8,10 +8,8 @@ package ca.gosyer.ui.sources.settings.components import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -21,18 +19,19 @@ import androidx.compose.material.Checkbox import androidx.compose.material.OutlinedTextField import androidx.compose.material.Scaffold import androidx.compose.material.Switch -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.TextFieldValue 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.dialog.getMaterialDialogProperties import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.prefs.ChoiceDialog import ca.gosyer.ui.base.prefs.MultiSelectDialog @@ -46,7 +45,10 @@ import ca.gosyer.ui.sources.settings.model.SourceSettingsView.Switch import ca.gosyer.ui.sources.settings.model.SourceSettingsView.TwoState import ca.gosyer.uicore.components.keyboardHandler import ca.gosyer.uicore.resources.stringResource -import kotlinx.coroutines.flow.MutableStateFlow +import com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.message +import com.vanpra.composematerialdialogs.rememberMaterialDialogState +import com.vanpra.composematerialdialogs.title import kotlin.collections.List as KtList @Composable @@ -124,18 +126,21 @@ private fun ListPreference(list: List) { list.summary } } + val dialogState = rememberMaterialDialogState() PreferenceRow( title, subtitle = subtitle, onClick = { - ChoiceDialog( - list.getOptions(), - state, - onSelected = list::updateState, - title = title - ) + dialogState.show() } ) + ChoiceDialog( + dialogState, + list.getOptions(), + state, + onSelected = list::updateState, + title = title + ) } @Composable @@ -150,18 +155,21 @@ private fun MultiSelectPreference(multiSelect: MultiSelect) { } } val dialogTitle = remember(state) { multiSelect.props.dialogTitle ?: multiSelect.title ?: multiSelect.summary ?: "No title" } + val dialogState = rememberMaterialDialogState() PreferenceRow( title, subtitle = subtitle, onClick = { - MultiSelectDialog( - multiSelect.getOptions(), - state, - onFinished = multiSelect::updateState, - title = dialogTitle - ) + dialogState.show() } ) + MultiSelectDialog( + dialogState, + multiSelect.getOptions(), + state, + onFinished = multiSelect::updateState, + title = dialogTitle + ) } @Composable @@ -175,31 +183,33 @@ private fun EditTextPreference(editText: EditText) { editText.summary } } + val dialogState = rememberMaterialDialogState() PreferenceRow( title, subtitle = subtitle, - onClick = { - val editTextFlow = MutableStateFlow(TextFieldValue(state)) - WindowDialog( - editText.dialogTitle ?: BuildKonfig.NAME, - onPositiveButton = { - editText.updateState(editTextFlow.value.text) - } - ) { - if (editText.dialogMessage != null) { - Text(editText.dialogMessage) - Spacer(Modifier.height(8.dp)) - } - - val text by editTextFlow.collectAsState() - OutlinedTextField( - text, - onValueChange = { - editTextFlow.value = it - }, - modifier = Modifier.keyboardHandler(singleLine = true) - ) - } - } + onClick = dialogState::show ) + var text by remember(state) { mutableStateOf(TextFieldValue(state)) } + MaterialDialog( + dialogState, + buttons = { + positiveButton(stringResource(MR.strings.action_ok)) { + editText.updateState(text.text) + } + negativeButton(stringResource(MR.strings.action_cancel)) + }, + properties = getMaterialDialogProperties(), + ) { + title(editText.dialogTitle ?: BuildKonfig.NAME) + if (editText.dialogMessage != null) { + message(editText.dialogMessage) + } + OutlinedTextField( + text, + onValueChange = { + text = it + }, + modifier = Modifier.keyboardHandler(singleLine = true) + ) + } } 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 deleted file mode 100644 index 4fbd9a4a..00000000 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Flow.kt +++ /dev/null @@ -1,19 +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.util.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import kotlinx.coroutines.flow.StateFlow -import kotlin.reflect.KProperty - -@Composable -operator fun StateFlow.getValue(thisObj: Any?, property: KProperty<*>): T { - val item by collectAsState() - return item -} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/UiComponent.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/UiComponent.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/UiComponent.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/UiComponent.kt diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/components/ScrollBarR.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/components/ScrollBarR.kt new file mode 100644 index 00000000..5f8ac7cc --- /dev/null +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/components/ScrollBarR.kt @@ -0,0 +1,22 @@ +/* + * 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.base.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier + +@Composable +fun VerticalScrollbar( + adapter: ScrollbarAdapter, + modifier: Modifier = Modifier, + reverseLayout: Boolean = false, + style: ScrollbarStyle = LocalScrollbarStyle.current, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + real: Boolean = false +) = VerticalScrollbar(adapter, modifier, reverseLayout, style, interactionSource) \ No newline at end of file diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.kt new file mode 100644 index 00000000..34429a60 --- /dev/null +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/components/Scrollbar.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.base.components + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.ui.Modifier + +expect interface ScrollbarAdapter + +expect class ScrollbarStyle + +expect val LocalScrollbarStyle: ProvidableCompositionLocal + +@Composable +expect fun VerticalScrollbar( + adapter: ScrollbarAdapter, + modifier: Modifier, + reverseLayout: Boolean, + style: ScrollbarStyle, + interactionSource: MutableInteractionSource, +) + + +@Composable +expect fun rememberScrollbarAdapter( + scrollState: ScrollState +): ScrollbarAdapter + +@Composable +expect fun rememberScrollbarAdapter( + scrollState: LazyListState, +): ScrollbarAdapter \ No newline at end of file diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/dialog/MaterialDialogProperties.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/dialog/MaterialDialogProperties.kt new file mode 100644 index 00000000..9d9d730f --- /dev/null +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/dialog/MaterialDialogProperties.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.base.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toPainter +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import ca.gosyer.i18n.MR +import ca.gosyer.presentation.build.BuildKonfig +import com.vanpra.composematerialdialogs.DesktopWindowPosition +import com.vanpra.composematerialdialogs.MaterialDialogProperties +import com.vanpra.composematerialdialogs.SecurePolicy + +@Composable +fun getMaterialDialogProperties( + dismissOnBackPress: Boolean = true, + dismissOnClickOutside: Boolean = true, + securePolicy: SecurePolicy = SecurePolicy.Inherit, + usePlatformDefaultWidth : Boolean = false, + position: DesktopWindowPosition = DesktopWindowPosition(Alignment.Center), + size: DpSize = DpSize(400.dp, 300.dp), + title: String = BuildKonfig.NAME, + icon: Painter = remember { MR.images.icon.image.toPainter() }, + resizable: Boolean = true +): MaterialDialogProperties { + return MaterialDialogProperties( + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + securePolicy = securePolicy, + usePlatformDefaultWidth = usePlatformDefaultWidth, + position = position, + size = size, + title = title, + icon = icon, + resizable = resizable + ) +} \ No newline at end of file diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/image/KamelConfigProvider.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/image/KamelConfigProvider.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/image/KamelConfigProvider.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/image/KamelConfigProvider.kt diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt new file mode 100644 index 00000000..586c8118 --- /dev/null +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/ActionIcon.kt @@ -0,0 +1,13 @@ +/* + * 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.base.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector + +@Composable +expect fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt similarity index 93% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt index d0d31c3d..3b22996f 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt @@ -8,8 +8,6 @@ package ca.gosyer.ui.base.navigation import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -25,6 +23,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.util.fastForEach +import ca.gosyer.i18n.MR +import ca.gosyer.uicore.resources.stringResource // Originally from https://gist.github.com/MachFour/369ebb56a66e2f583ebfb988dda2decf @@ -82,9 +82,12 @@ fun ActionMenu( } if (overflowActions.isNotEmpty()) { - IconButton(onClick = { menuVisible.value = true }) { - Icon(Icons.Default.MoreVert, "More actions") - } + iconItem( + { menuVisible.value = true }, + stringResource(MR.strings.action_more_actions), + Icons.Default.MoreVert, + true + ) DropdownMenu( expanded = menuVisible.value, onDismissRequest = { menuVisible.value = false }, @@ -98,7 +101,7 @@ fun ActionMenu( }, enabled = item.enabled ) { - //Icon(item.icon, item.name) just have text in the overflow menu + // Icon(item.icon, item.name) just have text in the overflow menu Text(item.name) } } @@ -150,4 +153,4 @@ private fun separateIntoIconAndOverflow( } } return iconActions to overflowActions -} \ No newline at end of file +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/DisplayController.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/DisplayController.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/DisplayController.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/DisplayController.kt diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt similarity index 97% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt index 7ea70c16..68fdfab9 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt @@ -69,7 +69,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ca.gosyer.i18n.MR -import ca.gosyer.uicore.components.BoxWithTooltipSurface import ca.gosyer.uicore.components.keyboardHandler import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.navigator.LocalNavigator @@ -192,8 +191,6 @@ private fun WideToolbar( } } - - @Composable private fun ThinToolbar( name: String, @@ -358,19 +355,6 @@ private fun SearchBox( } } -@Composable -fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) { - BoxWithTooltipSurface( - { - Text(contentDescription, modifier = Modifier.padding(10.dp)) - } - ) { - IconButton(onClick = onClick) { - Icon(icon, contentDescription) - } - } -} - @Composable fun TextActionIcon( onClick: () -> Unit, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/prefs/ColorPickerDialog.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/prefs/ColorPickerDialog.kt similarity index 90% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/prefs/ColorPickerDialog.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/prefs/ColorPickerDialog.kt index 3d6a8e5d..0eae9e30 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/prefs/ColorPickerDialog.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/prefs/ColorPickerDialog.kt @@ -34,11 +34,9 @@ import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Check import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -66,62 +64,56 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed -import ca.gosyer.ui.base.WindowDialog +import ca.gosyer.ui.base.dialog.getMaterialDialogProperties import ca.gosyer.uicore.components.keyboardHandler -import kotlinx.coroutines.flow.MutableStateFlow +import com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.MaterialDialogState +import com.vanpra.composematerialdialogs.title import kotlin.math.round +@Composable fun ColorPickerDialog( + state: MaterialDialogState, title: String, onCloseRequest: () -> Unit = {}, onSelected: (Color) -> Unit, initialColor: Color = Color.Unspecified, ) { - val currentColor = MutableStateFlow(initialColor) - val showPresets = MutableStateFlow(true) + var currentColor by remember(initialColor) { mutableStateOf(initialColor) } + var showPresets by remember { mutableStateOf(true) } - WindowDialog( - onCloseRequest = onCloseRequest, - size = DpSize(300.dp, 520.dp), - title = title, - content = { - val showPresetsState by showPresets.collectAsState() - val currentColorState by currentColor.collectAsState() - if (showPresetsState) { - ColorPresets( - initialColor = currentColorState, - onColorChanged = { currentColor.value = it } - ) - } else { - ColorPalette( - initialColor = currentColorState, - onColorChanged = { currentColor.value = it } - ) - } - }, + MaterialDialog( + state, buttons = { - val showPresetsState by showPresets.collectAsState() - val currentColorState by currentColor.collectAsState() - Row(Modifier.fillMaxWidth().padding(8.dp).align(Alignment.BottomCenter)) { - TextButton( - onClick = { - showPresets.value = !showPresetsState - } - ) { - Text(if (showPresetsState) "Custom" else "Presets") + positiveButton("Select", onClick = { onSelected(currentColor) }) + button( + if (showPresets) "Custom" else "Presets", + onClick = { + showPresets = !showPresets } - Spacer(Modifier.weight(1f)) - TextButton( - onClick = { - onSelected(currentColorState) - it() - } - ) { - Text("Select") - } - } + ) + }, + properties = getMaterialDialogProperties( + size = DpSize(300.dp, 520.dp) + ), + onCloseRequest = { + it.hide() + onCloseRequest() } - ) + ) { + title(title) + if (showPresets) { + ColorPresets( + initialColor = currentColor, + onColorChanged = { currentColor = it } + ) + } else { + ColorPalette( + initialColor = currentColor, + onColorChanged = { currentColor = it } + ) + } + } } @OptIn(ExperimentalFoundationApi::class) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt similarity index 75% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt index 94b3ee48..7d0c649a 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt @@ -20,14 +20,12 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -44,7 +42,6 @@ import androidx.compose.foundation.layout.widthIn 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.foundation.shape.CircleShape import androidx.compose.material.Checkbox import androidx.compose.material.ContentAlpha @@ -65,6 +62,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -77,10 +75,18 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import ca.gosyer.ui.base.WindowDialog +import ca.gosyer.i18n.MR +import ca.gosyer.ui.base.components.VerticalScrollbar +import ca.gosyer.ui.base.components.rememberScrollbarAdapter +import ca.gosyer.ui.base.dialog.getMaterialDialogProperties import ca.gosyer.uicore.components.keyboardHandler import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow -import kotlinx.coroutines.flow.MutableStateFlow +import ca.gosyer.uicore.resources.stringResource +import com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.MaterialDialogButtons +import com.vanpra.composematerialdialogs.MaterialDialogState +import com.vanpra.composematerialdialogs.rememberMaterialDialogState +import com.vanpra.composematerialdialogs.title @Composable fun PreferenceRow( @@ -180,31 +186,39 @@ fun EditTextPreference( enabled: Boolean = true, visualTransformation: VisualTransformation = VisualTransformation.None ) { + val dialogState = rememberMaterialDialogState() PreferenceRow( title = title, subtitle = subtitle, icon = icon, onClick = { - var editText by mutableStateOf(TextFieldValue(preference.value)) - WindowDialog( - title, - onPositiveButton = { - preference.value = editText.text - changeListener() - } - ) { - OutlinedTextField( - editText, - onValueChange = { - editText = it - }, - visualTransformation = visualTransformation, - modifier = Modifier.keyboardHandler() - ) - } + dialogState.show() }, enabled = enabled ) + val value by preference.collectAsState() + var editText by remember(value) { mutableStateOf(TextFieldValue(preference.value)) } + MaterialDialog( + dialogState, + buttons = { + positiveButton(stringResource(MR.strings.action_ok)) { + preference.value = editText.text + changeListener() + } + negativeButton(stringResource(MR.strings.action_cancel)) + }, + properties = getMaterialDialogProperties(), + ) { + title(title) + OutlinedTextField( + editText, + onValueChange = { + editText = it + }, + visualTransformation = visualTransformation, + modifier = Modifier.keyboardHandler() + ) + } } @Composable @@ -217,96 +231,117 @@ fun ChoicePreference( enabled: Boolean = true ) { val prefValue by preference.collectAsState() + val dialogState = rememberMaterialDialogState() PreferenceRow( title = title, subtitle = subtitle ?: choices[prefValue], onClick = { - ChoiceDialog( - items = choices.toList(), - selected = prefValue, - title = title, - onSelected = { selected -> - preference.value = selected - changeListener() - } - ) + dialogState.show() }, enabled = enabled ) + ChoiceDialog( + state = dialogState, + items = choices.toList(), + selected = prefValue, + title = title, + onSelected = { selected -> + preference.value = selected + changeListener() + } + ) } +@Composable fun ChoiceDialog( + state: MaterialDialogState, items: List>, selected: T?, onCloseRequest: () -> Unit = {}, onSelected: (T) -> Unit, title: String, - buttons: @Composable BoxWithConstraintsScope.(() -> Unit) -> Unit = { } + buttons: @Composable MaterialDialogButtons.() -> Unit = { } ) { - WindowDialog( - onCloseRequest = onCloseRequest, + MaterialDialog( + state, buttons = buttons, - title = title + properties = getMaterialDialogProperties(), + onCloseRequest = { + state.hide() + onCloseRequest() + } ) { - val state = rememberLazyListState() - LazyColumn(Modifier.fillMaxSize(), state) { - items(items) { (value, text) -> - Row( - modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable( - onClick = { - onSelected(value) - it() - } - ), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = value == selected, - onClick = { - onSelected(value) - it() - }, - ) - Text(text = text, modifier = Modifier.padding(start = 24.dp)) + title(title) + Box { + val listState = rememberLazyListState() + LazyColumn(Modifier.fillMaxSize(), listState) { + items(items) { (value, text) -> + Row( + modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable( + onClick = { + onSelected(value) + state.hide() + } + ), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = value == selected, + onClick = { + onSelected(value) + state.hide() + }, + ) + Text(text = text, modifier = Modifier.padding(start = 24.dp)) + } } } + VerticalScrollbar( + rememberScrollbarAdapter(listState), + Modifier.align(Alignment.CenterEnd) + .fillMaxHeight() + .padding(horizontal = 4.dp, vertical = 8.dp) + ) } - VerticalScrollbar( - rememberScrollbarAdapter(state), - Modifier.align(Alignment.CenterEnd) - .fillMaxHeight() - .padding(horizontal = 4.dp, vertical = 8.dp) - ) } } +@Composable fun MultiSelectDialog( + state: MaterialDialogState, items: List>, selected: List?, onCloseRequest: () -> Unit = {}, onFinished: (List) -> Unit, title: String, ) { - val checkedFlow = MutableStateFlow(selected.orEmpty()) - WindowDialog( - onCloseRequest = onCloseRequest, - title = title, - onPositiveButton = { - onFinished(checkedFlow.value) + val checked = remember(selected) { selected.orEmpty().toMutableStateList() } + MaterialDialog( + state, + buttons = { + positiveButton(stringResource(MR.strings.action_ok)) { + onFinished(checked) + } + negativeButton(stringResource(MR.strings.action_cancel)) + }, + properties = getMaterialDialogProperties(), + onCloseRequest = { + state.hide() + onCloseRequest() } ) { - val checked by checkedFlow.collectAsState() - val state = rememberLazyListState() + title(title) + val listState = rememberLazyListState() Box { - LazyColumn(Modifier.fillMaxSize(), state) { + LazyColumn(Modifier.fillMaxSize(), listState) { items(items) { (value, text) -> Row( modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable( onClick = { if (value in checked) { - checkedFlow.value -= value + checked -= value } else { - checkedFlow.value += value + checked += value } } ), @@ -322,7 +357,7 @@ fun MultiSelectDialog( item { Spacer(Modifier.height(80.dp)) } } VerticalScrollbar( - rememberScrollbarAdapter(state), + rememberScrollbarAdapter(listState), Modifier.align(Alignment.CenterEnd) .fillMaxHeight() .padding(horizontal = 4.dp, vertical = 8.dp) @@ -340,17 +375,12 @@ fun ColorPreference( unsetColor: Color = Color.Unspecified ) { val initialColor = preference.value.takeOrElse { unsetColor } + val dialogState = rememberMaterialDialogState() PreferenceRow( title = title, subtitle = subtitle, onClick = { - ColorPickerDialog( - title = title, - onSelected = { - preference.value = it - }, - initialColor = initialColor - ) + dialogState.show() }, onLongClick = { preference.value = Color.Unspecified }, action = { @@ -369,6 +399,14 @@ fun ColorPreference( }, enabled = enabled ) + ColorPickerDialog( + state = dialogState, + title = title, + onSelected = { + preference.value = it + }, + initialColor = initialColor + ) } const val EXPAND_ANIMATION_DURATION = 300 diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt similarity index 88% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt index 890557b7..453da206 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt @@ -4,11 +4,10 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +@file:JvmName("ThemeScrollbarStyleKt") + package ca.gosyer.ui.base.theme -import androidx.compose.desktop.DesktopMaterialTheme -import androidx.compose.foundation.LocalScrollbarStyle -import androidx.compose.foundation.ScrollbarStyle import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme @@ -20,9 +19,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.unit.dp import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.model.ThemeMode +import ca.gosyer.ui.base.components.LocalScrollbarStyle +import ca.gosyer.ui.base.theme.ThemeScrollbarStyle.getScrollbarStyle import ca.gosyer.uicore.theme.Theme import ca.gosyer.uicore.theme.themes import ca.gosyer.uicore.vm.LocalViewModelFactory @@ -47,14 +47,7 @@ fun AppTheme(content: @Composable () -> Unit) { MaterialTheme(colors = colors) { CompositionLocalProvider( - LocalScrollbarStyle provides ScrollbarStyle( - minimalHeight = 16.dp, - thickness = 8.dp, - shape = MaterialTheme.shapes.small, - hoverDurationMillis = 300, - unhoverColor = MaterialTheme.colors.onSurface.copy(alpha = 0.30f), - hoverColor = MaterialTheme.colors.onSurface.copy(alpha = 0.70f) - ), + LocalScrollbarStyle provides getScrollbarStyle(), content = content ) } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt new file mode 100644 index 00000000..092cf34c --- /dev/null +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/theme/ThemeScrollbarStyle.kt @@ -0,0 +1,15 @@ +/* + * 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.base.theme + +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.ScrollbarStyle + +expect object ThemeScrollbarStyle { + @Composable + fun getScrollbarStyle(): ScrollbarStyle +} diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt new file mode 100644 index 00000000..6ab19100 --- /dev/null +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/vm/ViewModelFactory.kt @@ -0,0 +1,13 @@ +/* + * 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.base.vm + +import ca.gosyer.uicore.vm.ViewModelFactory +import me.tatarka.inject.annotations.Inject + +@Inject +expect class ViewModelFactoryImpl : ViewModelFactory \ No newline at end of file diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt similarity index 97% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt index deafb746..a5ef2440 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/components/UpdatesScreenContent.kt @@ -6,7 +6,6 @@ package ca.gosyer.ui.updates.components -import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio @@ -18,7 +17,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable @@ -34,6 +32,8 @@ import ca.gosyer.data.models.Chapter import ca.gosyer.i18n.MR import ca.gosyer.ui.base.chapter.ChapterDownloadIcon import ca.gosyer.ui.base.chapter.ChapterDownloadItem +import ca.gosyer.ui.base.components.VerticalScrollbar +import ca.gosyer.ui.base.components.rememberScrollbarAdapter import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.MangaListItem diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Color.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/util/compose/Color.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Color.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/util/compose/Color.kt diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Offset.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/util/compose/Offset.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/compose/Offset.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/util/compose/Offset.kt diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/Flow.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/util/system/Flow.kt similarity index 100% rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/util/system/Flow.kt rename to presentation/src/jvmMain/kotlin/ca/gosyer/ui/util/system/Flow.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index cbc4de58..0c9d2e32 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,7 +13,8 @@ include("desktop") include("core") include("i18n") include("data") - -enableFeaturePreview("VERSION_CATALOGS") include("ui-core") include("presentation") + +enableFeaturePreview("VERSION_CATALOGS") +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/ui-core/build.gradle.kts b/ui-core/build.gradle.kts index 7d0602e8..8391f909 100644 --- a/ui-core/build.gradle.kts +++ b/ui-core/build.gradle.kts @@ -38,8 +38,8 @@ kotlin { api(libs.coroutinesCore) api(libs.kamel) api(libs.voyagerCore) - api(project(":core")) - api(project(":i18n")) + api(projects.core) + api(projects.i18n) api(compose.desktop.currentOs) api(compose.materialIconsExtended) }