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 00000000..f2bb1780 Binary files /dev/null and b/i18n/src/commonMain/resources/MR/images/icon@1x.png differ diff --git a/i18n/src/commonMain/resources/MR/values/base/strings.xml b/i18n/src/commonMain/resources/MR/values/base/strings.xml index 7b94ba1b..a0042bff 100644 --- a/i18n/src/commonMain/resources/MR/values/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/values/base/strings.xml @@ -41,6 +41,8 @@ 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) }