Many updates and fixes

- Use multiplatform dialogs library(WIP)
- Port accompanist to multiplatform
- Use new gradle type-safe project accessors
- Cleanup and improve a few viewmodels
- Start moving presentation to multiplatform
This commit is contained in:
Syer10
2022-02-26 14:36:45 -05:00
parent e1a269ca40
commit a423090052
70 changed files with 1046 additions and 839 deletions

View File

@@ -8,7 +8,7 @@ plugins {
kotlin("plugin.serialization") version "1.6.10" apply false kotlin("plugin.serialization") version "1.6.10" apply false
id("com.android.library") version "7.0.4" apply false id("com.android.library") version "7.0.4" apply false
id("com.android.application") 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.google.devtools.ksp") version "1.6.10-1.0.2"
id("com.github.gmazzo.buildconfig") version "3.0.3" apply false id("com.github.gmazzo.buildconfig") version "3.0.3" apply false
id("com.codingfeline.buildkonfig") version "0.11.0" apply false id("com.codingfeline.buildkonfig") version "0.11.0" apply false

View File

@@ -44,8 +44,8 @@ kotlin {
api(libs.ktorWebsockets) api(libs.ktorWebsockets)
api(libs.ktorOkHttp) api(libs.ktorOkHttp)
api(libs.okio) api(libs.okio)
api(project(":core")) api(projects.core)
api(project(":i18n")) api(projects.i18n)
} }
} }
val commonTest by getting { val commonTest by getting {

View File

@@ -10,7 +10,11 @@ import ca.gosyer.core.prefs.Preference
import ca.gosyer.core.prefs.PreferenceStore import ca.gosyer.core.prefs.PreferenceStore
import ca.gosyer.data.server.host.ServerHostPreference import ca.gosyer.data.server.host.ServerHostPreference
class ServerHostPreferences(preferenceStore: PreferenceStore) { class ServerHostPreferences(private val preferenceStore: PreferenceStore) {
fun host(): Preference<Boolean> {
return preferenceStore.getBoolean("host", true)
}
private val ip = ServerHostPreference.IP(preferenceStore) private val ip = ServerHostPreference.IP(preferenceStore)
fun ip(): Preference<String> { fun ip(): Preference<String> {

View File

@@ -40,11 +40,10 @@ import kotlin.io.path.isExecutable
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
class ServerService @Inject constructor( class ServerService @Inject constructor(
serverPreferences: ServerPreferences,
private val serverHostPreferences: ServerHostPreferences private val serverHostPreferences: ServerHostPreferences
) { ) {
private val restartServerFlow = MutableSharedFlow<Unit>() private val restartServerFlow = MutableSharedFlow<Unit>()
private val host = serverPreferences.host().stateIn(GlobalScope) private val host = serverHostPreferences.host().stateIn(GlobalScope)
private val _initialized = MutableStateFlow( private val _initialized = MutableStateFlow(
if (host.value) { if (host.value) {
ServerResult.STARTING ServerResult.STARTING

View File

@@ -13,10 +13,6 @@ import ca.gosyer.data.server.model.Proxy
class ServerPreferences(private val preferenceStore: PreferenceStore) { class ServerPreferences(private val preferenceStore: PreferenceStore) {
fun host(): Preference<Boolean> {
return preferenceStore.getBoolean("host", true)
}
fun server(): Preference<String> { fun server(): Preference<String> {
return preferenceStore.getString("server_url", "http://localhost") return preferenceStore.getString("server_url", "http://localhost")
} }

View File

@@ -16,11 +16,11 @@ plugins {
} }
dependencies { dependencies {
implementation(project(":core")) implementation(projects.core)
implementation(project(":i18n")) implementation(projects.i18n)
implementation(project(":data")) implementation(projects.data)
implementation(project(":ui-core")) implementation(projects.uiCore)
implementation(project(":presentation")) implementation(projects.presentation)
// UI (Compose) // UI (Compose)
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
@@ -33,6 +33,7 @@ dependencies {
implementation(libs.accompanistPager) implementation(libs.accompanistPager)
implementation(libs.accompanistFlowLayout) implementation(libs.accompanistFlowLayout)
implementation(libs.kamel) implementation(libs.kamel)
implementation(libs.materialDialogsCore)
// UI (Swing) // UI (Swing)
implementation(libs.darklaf) implementation(libs.darklaf)

View File

@@ -9,7 +9,6 @@ package ca.gosyer
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState 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.key
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.painterResource 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.Window
import androidx.compose.ui.window.awaitApplication import androidx.compose.ui.window.awaitApplication
import androidx.compose.ui.window.rememberWindowState 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.desktop.build.BuildConfig
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.AppComponent 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.base.theme.AppTheme
import ca.gosyer.ui.main.MainMenu import ca.gosyer.ui.main.MainMenu
import ca.gosyer.ui.main.components.DebugOverlay 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.LafManager
import com.github.weisj.darklaf.theme.DarculaTheme import com.github.weisj.darklaf.theme.DarculaTheme
import com.github.weisj.darklaf.theme.IntelliJTheme 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.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -130,15 +135,12 @@ suspend fun main() {
Tray(icon) Tray(icon)
val confirmExitDialogState = rememberMaterialDialogState()
Window( Window(
onCloseRequest = { onCloseRequest = {
if (confirmExit.value) { if (confirmExit.value) {
WindowDialog( confirmExitDialogState.show()
title = MR.strings.confirm_exit.localized(),
onPositiveButton = ::exitApplication
) {
Text(stringResource(MR.strings.confirm_exit_message))
}
} else { } else {
exitApplication() 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))
}
} }
} }
} }

View File

@@ -9,8 +9,9 @@ xmlUtil = "0.84.0"
# Compose # Compose
voyager = "1.0.0-beta15" voyager = "1.0.0-beta15"
accompanist = "0.18.1" accompanist = "0.20.1"
kamel = "0.3.0" kamel = "0.3.0"
materialDialogs = "0.6.4"
# Swing # Swing
darklaf = "2.7.3" 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" } accompanistPager = { module = "ca.gosyer:accompanist-pager", version.ref = "accompanist" }
accompanistFlowLayout = { module = "ca.gosyer:accompanist-flowlayout", version.ref = "accompanist" } accompanistFlowLayout = { module = "ca.gosyer:accompanist-flowlayout", version.ref = "accompanist" }
kamel = { module = "com.alialbaali.kamel:kamel-image", version.ref = "kamel" } kamel = { module = "com.alialbaali.kamel:kamel-image", version.ref = "kamel" }
materialDialogsCore = { module = "ca.gosyer:compose-material-dialogs-core", version.ref = "materialDialogs" }
# Swing # Swing
darklaf = { module = "com.github.weisj:darklaf-core", version.ref = "darklaf" } darklaf = { module = "com.github.weisj:darklaf-core", version.ref = "darklaf" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -41,6 +41,8 @@
<string name="action_close">Close</string> <string name="action_close">Close</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_searching">Search…</string> <string name="action_searching">Search…</string>
<string name="action_more_actions">More actions</string>
<string name="action_ok">Ok</string>
<!-- Locations --> <!-- Locations -->
<string name="location_library">Library</string> <string name="location_library">Library</string>

View File

@@ -46,10 +46,11 @@ kotlin {
api(libs.voyagerCore) api(libs.voyagerCore)
api(libs.voyagerNavigation) api(libs.voyagerNavigation)
api(libs.voyagerTransitions) api(libs.voyagerTransitions)
api(project(":core")) api(libs.materialDialogsCore)
api(project(":i18n")) api(projects.core)
api(project(":data")) api(projects.i18n)
api(project(":ui-core")) api(projects.data)
api(projects.uiCore)
api(compose.desktop.currentOs) api(compose.desktop.currentOs)
api(compose("org.jetbrains.compose.ui:ui-util")) api(compose("org.jetbrains.compose.ui:ui-util"))
api(compose.materialIconsExtended) api(compose.materialIconsExtended)

View File

@@ -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<ScrollbarStyle> = 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() {}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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 <VM : ViewModel> instantiate(klass: KClass<VM>, 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
}
}

View File

@@ -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<Key, (KeyEvent) -> 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<Key, (KeyEvent) -> 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)
}
}
}
}
}
}
}

View File

@@ -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<ScrollbarStyle>
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)
}

View File

@@ -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)
}
}
}

View File

@@ -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)
)
}
}

View File

@@ -21,6 +21,7 @@ import ca.gosyer.ui.settings.SettingsBackupViewModel
import ca.gosyer.ui.settings.SettingsGeneralViewModel import ca.gosyer.ui.settings.SettingsGeneralViewModel
import ca.gosyer.ui.settings.SettingsLibraryViewModel import ca.gosyer.ui.settings.SettingsLibraryViewModel
import ca.gosyer.ui.settings.SettingsReaderViewModel import ca.gosyer.ui.settings.SettingsReaderViewModel
import ca.gosyer.ui.settings.SettingsServerHostViewModel
import ca.gosyer.ui.settings.SettingsServerViewModel import ca.gosyer.ui.settings.SettingsServerViewModel
import ca.gosyer.ui.settings.ThemesViewModel import ca.gosyer.ui.settings.ThemesViewModel
import ca.gosyer.ui.sources.SourcesScreenViewModel import ca.gosyer.ui.sources.SourcesScreenViewModel
@@ -35,7 +36,7 @@ import me.tatarka.inject.annotations.Inject
import kotlin.reflect.KClass import kotlin.reflect.KClass
@Inject @Inject
class ViewModelFactoryImpl( actual class ViewModelFactoryImpl(
private val appThemeFactory: () -> AppThemeViewModel, private val appThemeFactory: () -> AppThemeViewModel,
private val categoryFactory: () -> CategoriesScreenViewModel, private val categoryFactory: () -> CategoriesScreenViewModel,
private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel, private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel,
@@ -53,6 +54,7 @@ class ViewModelFactoryImpl(
private val settingsLibraryFactory: () -> SettingsLibraryViewModel, private val settingsLibraryFactory: () -> SettingsLibraryViewModel,
private val settingsReaderFactory: () -> SettingsReaderViewModel, private val settingsReaderFactory: () -> SettingsReaderViewModel,
private val settingsServerFactory: () -> SettingsServerViewModel, private val settingsServerFactory: () -> SettingsServerViewModel,
private val settingsServerHostFactory: () -> SettingsServerHostViewModel,
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel, private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel, private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
private val sourceHomeFactory: () -> SourceHomeScreenViewModel, private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
@@ -81,6 +83,7 @@ class ViewModelFactoryImpl(
SettingsLibraryViewModel::class -> settingsLibraryFactory() SettingsLibraryViewModel::class -> settingsLibraryFactory()
SettingsReaderViewModel::class -> settingsReaderFactory() SettingsReaderViewModel::class -> settingsReaderFactory()
SettingsServerViewModel::class -> settingsServerFactory() SettingsServerViewModel::class -> settingsServerFactory()
SettingsServerHostViewModel::class -> settingsServerHostFactory()
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params) SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params) SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
SourceHomeScreenViewModel::class -> sourceHomeFactory() SourceHomeScreenViewModel::class -> sourceHomeFactory()

View File

@@ -6,81 +6,98 @@
package ca.gosyer.ui.categories.components package ca.gosyer.ui.categories.components
import androidx.compose.material.Text
import androidx.compose.material.TextField import androidx.compose.material.TextField
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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.Modifier
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.ui.base.dialog.getMaterialDialogProperties
import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.categories.CategoriesScreenViewModel import ca.gosyer.ui.categories.CategoriesScreenViewModel
import ca.gosyer.uicore.components.keyboardHandler import ca.gosyer.uicore.components.keyboardHandler
import ca.gosyer.uicore.resources.stringResource 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, category: CategoriesScreenViewModel.MenuCategory,
onRename: (String) -> Unit onRename: (String) -> Unit
) { ) {
val newName = MutableStateFlow(TextFieldValue(category.name)) var newName by remember { mutableStateOf(TextFieldValue(category.name)) }
WindowDialog( MaterialDialog(
title = "${BuildKonfig.NAME} - Categories - Rename Dialog", state,
positiveButtonText = "Rename", buttons = {
onPositiveButton = { positiveButton(stringResource(MR.strings.action_rename)) {
if (newName.value.text != category.name) { if (newName.text != category.name) {
onRename(newName.value.text) onRename(newName.text)
}
} }
} negativeButton(stringResource(MR.strings.action_cancel))
},
properties = getMaterialDialogProperties(),
) { ) {
val newNameState by newName.collectAsState() title("Rename Category")
TextField( TextField(
newNameState, newName,
onValueChange = { onValueChange = {
newName.value = it newName = it
}, },
modifier = Modifier.keyboardHandler(singleLine = true) modifier = Modifier.keyboardHandler(singleLine = true)
) )
} }
} }
fun openDeleteDialog( @Composable
fun DeleteDialog(
state: MaterialDialogState,
category: CategoriesScreenViewModel.MenuCategory, category: CategoriesScreenViewModel.MenuCategory,
onDelete: (CategoriesScreenViewModel.MenuCategory) -> Unit onDelete: (CategoriesScreenViewModel.MenuCategory) -> Unit
) { ) {
WindowDialog( MaterialDialog(
title = "${BuildKonfig.NAME} - Categories - Delete Dialog", state,
positiveButtonText = "Yes", buttons = {
onPositiveButton = { positiveButton(stringResource(MR.strings.action_yes)) {
onDelete(category) 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 onCreate: (String) -> Unit
) { ) {
val name = MutableStateFlow(TextFieldValue("")) var name by remember { mutableStateOf(TextFieldValue("")) }
WindowDialog( MaterialDialog(
title = "${BuildKonfig.NAME} - Categories - Create Dialog", state,
positiveButtonText = "Create", buttons = {
onPositiveButton = { positiveButton(stringResource(MR.strings.action_create)) {
onCreate(name.value.text) onCreate(name.text)
} }
negativeButton(stringResource(MR.strings.action_cancel))
},
properties = getMaterialDialogProperties(),
) { ) {
val nameState by name.collectAsState() title("Create Category")
TextField( TextField(
nameState, name,
onValueChange = { onValueChange = {
name.value = it name = it
}, },
singleLine = true, singleLine = true,
modifier = Modifier.keyboardHandler(singleLine = true) modifier = Modifier.keyboardHandler(singleLine = true)

View File

@@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.categories.CategoriesScreenViewModel.MenuCategory import ca.gosyer.ui.categories.CategoriesScreenViewModel.MenuCategory
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -77,11 +78,16 @@ fun CategoriesScreenContent(
} }
} }
val createDialogState = rememberMaterialDialogState()
Surface { Surface {
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(modifier = Modifier.fillMaxSize(), state = state,) { LazyColumn(modifier = Modifier.fillMaxSize(), state = state,) {
itemsIndexed(categories) { i, category -> itemsIndexed(categories) { i, category ->
val renameDialogState = rememberMaterialDialogState()
val deleteDialogState = rememberMaterialDialogState()
CategoryRow( CategoryRow(
category = category, category = category,
moveUpEnabled = i != 0, moveUpEnabled = i != 0,
@@ -89,16 +95,18 @@ fun CategoriesScreenContent(
onMoveUp = { moveCategoryUp(category) }, onMoveUp = { moveCategoryUp(category) },
onMoveDown = { moveCategoryDown(category) }, onMoveDown = { moveCategoryDown(category) },
onRename = { onRename = {
openRenameDialog(category) { renameDialogState.show()
renameCategory(category, it)
}
}, },
onDelete = { onDelete = {
openDeleteDialog(category) { deleteDialogState.show()
deleteCategory(category)
}
}, },
) )
RenameDialog(renameDialogState, category) {
renameCategory(category, it)
}
DeleteDialog(deleteDialogState, category) {
deleteCategory(category)
}
} }
item { item {
Spacer(Modifier.height(80.dp).fillMaxWidth()) Spacer(Modifier.height(80.dp).fillMaxWidth())
@@ -109,9 +117,7 @@ fun CategoriesScreenContent(
icon = { Icon(imageVector = Icons.Rounded.Add, contentDescription = null) }, icon = { Icon(imageVector = Icons.Rounded.Add, contentDescription = null) },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp), modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
onClick = { onClick = {
openCreateDialog { createDialogState.show()
createCategory(it)
}
} }
) )
VerticalScrollbar( VerticalScrollbar(
@@ -122,6 +128,7 @@ fun CategoriesScreenContent(
) )
} }
} }
CreateDialog(createDialogState, createCategory)
} }
@Composable @Composable

View File

@@ -6,38 +6,16 @@
package ca.gosyer.ui.downloads package ca.gosyer.ui.downloads
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState 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.downloads.components.DownloadsScreenContent
import ca.gosyer.ui.manga.MangaScreen import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow 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 { class DownloadsScreen : Screen {
override val key: ScreenKey = uniqueScreenKey override val key: ScreenKey = uniqueScreenKey

View File

@@ -6,39 +6,13 @@
package ca.gosyer.ui.extensions package ca.gosyer.ui.extensions
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState 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.extensions.components.ExtensionsScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey 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 { class ExtensionsScreen : Screen {
@@ -52,9 +26,9 @@ class ExtensionsScreen : Screen {
extensions = vm.extensions.collectAsState().value, extensions = vm.extensions.collectAsState().value,
isLoading = vm.isLoading.collectAsState().value, isLoading = vm.isLoading.collectAsState().value,
query = vm.searchQuery.collectAsState().value, query = vm.searchQuery.collectAsState().value,
setQuery = vm::search, setQuery = vm::setQuery,
enabledLangs = vm.enabledLangs, enabledLangs = vm.enabledLangs.collectAsState().value,
getSourceLanguages = vm::getSourceLanguages, availableLangs = vm.availableLangs.collectAsState().value,
setEnabledLanguages = vm::setEnabledLanguages, setEnabledLanguages = vm::setEnabledLanguages,
installExtension = vm::install, installExtension = vm::install,
updateExtension = vm::update, updateExtension = vm::update,

View File

@@ -14,10 +14,12 @@ import ca.gosyer.data.server.interactions.ExtensionInteractionHandler
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import java.util.Locale import java.util.Locale
@@ -26,36 +28,43 @@ class ExtensionsScreenViewModel @Inject constructor(
private val extensionHandler: ExtensionInteractionHandler, private val extensionHandler: ExtensionInteractionHandler,
extensionPreferences: ExtensionPreferences extensionPreferences: ExtensionPreferences
) : ViewModel() { ) : ViewModel() {
private val extensionList = MutableStateFlow<List<Extension>?>(null)
private val _enabledLangs = extensionPreferences.languages().asStateFlow() private val _enabledLangs = extensionPreferences.languages().asStateFlow()
val enabledLangs = _enabledLangs.asStateFlow() val enabledLangs = _enabledLangs.asStateFlow()
private var extensionList: List<Extension>? = null private val _searchQuery = MutableStateFlow<String?>(null)
val searchQuery = _searchQuery.asStateFlow()
private val _extensions = MutableStateFlow(emptyMap<String, List<Extension>>()) val extensions = combine(
val extensions = _extensions.asStateFlow() 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) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
val searchQuery = MutableStateFlow<String?>(null)
init { init {
scope.launch { scope.launch {
getExtensions() getExtensions()
} }
enabledLangs.drop(1).onEach {
search(searchQuery.value.orEmpty())
}.launchIn(scope)
} }
private suspend fun getExtensions() { private suspend fun getExtensions() {
try { try {
_isLoading.value = true _isLoading.value = true
extensionList = extensionHandler.getExtensionList() extensionList.value = extensionHandler.getExtensionList()
search(searchQuery.value.orEmpty())
} catch (e: Exception) { } catch (e: Exception) {
e.throwIfCancellation() e.throwIfCancellation()
extensionList = emptyList() extensionList.value = emptyList()
} finally { } finally {
_isLoading.value = false _isLoading.value = false
} }
@@ -97,26 +106,26 @@ class ExtensionsScreenViewModel @Inject constructor(
} }
} }
fun getSourceLanguages() = extensionList?.map { it.lang }?.toSet().orEmpty()
fun setEnabledLanguages(langs: Set<String>) { fun setEnabledLanguages(langs: Set<String>) {
info { langs }
_enabledLangs.value = langs _enabledLangs.value = langs
} }
fun search(searchQuery: String) { fun setQuery(query: String) {
this.searchQuery.value = searchQuery.takeUnless { it.isBlank() } _searchQuery.value = query
val extensionList = extensionList?.filter { it.lang in enabledLangs.value } }
private fun search(searchQuery: String?, extensionList: List<Extension>?, enabledLangs: Set<String>): Map<String, List<Extension>> {
val extensions = extensionList?.filter { it.lang in enabledLangs }
.orEmpty() .orEmpty()
if (searchQuery.isBlank()) { return if (searchQuery.isNullOrBlank()) {
_extensions.value = extensionList.splitSort() extensions.splitSort()
} else { } else {
val queries = searchQuery.split(" ") val queries = searchQuery.split(" ")
val extensions = extensionList.toMutableList() val filteredExtensions = extensions.toMutableList()
queries.forEach { query -> 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()
} }
} }

View File

@@ -33,9 +33,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Translate import androidx.compose.material.icons.rounded.Translate
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.data.models.Extension
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig 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.ActionItem
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.image.KamelImage
import ca.gosyer.uicore.resources.stringResource 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 io.kamel.image.lazyPainterResource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.Locale import java.util.Locale
@Composable @Composable
@@ -65,26 +66,25 @@ fun ExtensionsScreenContent(
isLoading: Boolean, isLoading: Boolean,
query: String?, query: String?,
setQuery: (String) -> Unit, setQuery: (String) -> Unit,
enabledLangs: StateFlow<Set<String>>, enabledLangs: Set<String>,
getSourceLanguages: () -> Set<String>, availableLangs: Set<String>,
setEnabledLanguages: (Set<String>) -> Unit, setEnabledLanguages: (Set<String>) -> Unit,
installExtension: (Extension) -> Unit, installExtension: (Extension) -> Unit,
updateExtension: (Extension) -> Unit, updateExtension: (Extension) -> Unit,
uninstallExtension: (Extension) -> Unit uninstallExtension: (Extension) -> Unit
) { ) {
val languageDialogState = rememberMaterialDialogState()
Scaffold( Scaffold(
topBar = { topBar = {
ExtensionsToolbar( ExtensionsToolbar(
query, query,
setQuery, setQuery,
enabledLangs, languageDialogState::show
getSourceLanguages,
setEnabledLanguages
) )
} }
) { ) {
if (isLoading) { if (isLoading) {
LoadingScreen(isLoading) LoadingScreen()
} else { } else {
val state = rememberLazyListState() val state = rememberLazyListState()
@@ -118,26 +118,21 @@ fun ExtensionsScreenContent(
} }
} }
} }
LanguageDialog(languageDialogState, enabledLangs, availableLangs, setEnabledLanguages)
} }
@Composable @Composable
fun ExtensionsToolbar( fun ExtensionsToolbar(
searchText: String?, searchText: String?,
search: (String) -> Unit, search: (String) -> Unit,
currentEnabledLangs: StateFlow<Set<String>>, openLanguageDialog: () -> Unit
getSourceLanguages: () -> Set<String>,
setEnabledLanguages: (Set<String>) -> Unit
) { ) {
Toolbar( Toolbar(
stringResource(MR.strings.location_extensions), stringResource(MR.strings.location_extensions),
searchText = searchText, searchText = searchText,
search = search, search = search,
actions = { actions = {
getActionItems( getActionItems(openLanguageDialog)
currentEnabledLangs = currentEnabledLangs,
getSourceLanguages = getSourceLanguages,
setEnabledLanguages = setEnabledLanguages
)
} }
) )
} }
@@ -195,26 +190,42 @@ fun ExtensionItem(
} }
} }
fun LanguageDialog(enabledLangsFlow: MutableStateFlow<Set<String>>, availableLangs: List<String>, setLangs: () -> Unit) { @Composable
WindowDialog(BuildKonfig.NAME, onPositiveButton = setLangs) { fun LanguageDialog(
val locale = Locale.getDefault() state: MaterialDialogState,
val enabledLangs by enabledLangsFlow.collectAsState() enabledLangs: Set<String>,
val state = rememberLazyListState() availableLangs: Set<String>,
setLangs: (Set<String>) -> 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 { Box {
LazyColumn(Modifier.fillMaxWidth(), state) { val locale = remember { Locale.getDefault() }
items(availableLangs) { lang -> val listState = rememberLazyListState()
LazyColumn(Modifier.fillMaxWidth(), listState) {
items(availableLangs.toList()) { lang ->
Row { Row {
val langName = remember(lang) { val langName = remember(lang) {
Locale.forLanguageTag(lang)?.getDisplayName(locale) ?: lang Locale.forLanguageTag(lang)?.getDisplayName(locale) ?: lang
} }
Text(langName) Text(langName)
Switch( Switch(
lang in enabledLangs, checked = lang in modifiedLangs,
{ onCheckedChange = {
if (it) { if (it) {
enabledLangsFlow.value += lang modifiedLangs += lang
} else { } else {
enabledLangsFlow.value -= lang modifiedLangs -= lang
} }
} }
) )
@@ -223,7 +234,7 @@ fun LanguageDialog(enabledLangsFlow: MutableStateFlow<Set<String>>, availableLan
item { Spacer(Modifier.height(70.dp)) } item { Spacer(Modifier.height(70.dp)) }
} }
VerticalScrollbar( VerticalScrollbar(
rememberScrollbarAdapter(state), rememberScrollbarAdapter(listState),
Modifier.align(Alignment.CenterEnd) Modifier.align(Alignment.CenterEnd)
.fillMaxHeight() .fillMaxHeight()
.padding(horizontal = 4.dp, vertical = 8.dp) .padding(horizontal = 4.dp, vertical = 8.dp)
@@ -235,19 +246,13 @@ fun LanguageDialog(enabledLangsFlow: MutableStateFlow<Set<String>>, availableLan
@Stable @Stable
@Composable @Composable
private fun getActionItems( private fun getActionItems(
currentEnabledLangs: StateFlow<Set<String>>, openLanguageDialog: () -> Unit
getSourceLanguages: () -> Set<String>,
setEnabledLanguages: (Set<String>) -> Unit
): List<ActionItem> { ): List<ActionItem> {
return listOf( return listOf(
ActionItem( ActionItem(
stringResource(MR.strings.enabled_languages), stringResource(MR.strings.enabled_languages),
Icons.Rounded.Translate Icons.Rounded.Translate,
) { doAction = openLanguageDialog
val enabledLangs = MutableStateFlow(currentEnabledLangs.value) )
LanguageDialog(enabledLangs, getSourceLanguages().toList()) {
setEnabledLanguages(enabledLangs.value)
}
}
) )
} }

View File

@@ -6,38 +6,16 @@
package ca.gosyer.ui.library package ca.gosyer.ui.library
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState 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.library.components.LibraryScreenContent
import ca.gosyer.ui.manga.MangaScreen import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow 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 { class LibraryScreen : Screen {

View File

@@ -29,7 +29,7 @@ fun LibraryPager(
) { ) {
if (categories.isEmpty()) return if (categories.isEmpty()) return
val state = rememberPagerState(categories.size, selectedPage) val state = rememberPagerState(selectedPage)
LaunchedEffect(state.currentPage) { LaunchedEffect(state.currentPage) {
if (state.currentPage != selectedPage) { if (state.currentPage != selectedPage) {
onPageChanged(state.currentPage) onPageChanged(state.currentPage)
@@ -40,7 +40,7 @@ fun LibraryPager(
state.animateScrollToPage(selectedPage) state.animateScrollToPage(selectedPage)
} }
} }
HorizontalPager(state = state) { HorizontalPager(categories.size, state = state) {
val library by getLibraryForPage(categories[it].id) val library by getLibraryForPage(categories[it].id)
when (displayMode) { when (displayMode) {
DisplayMode.CompactGrid -> LibraryMangaCompactGrid( DisplayMode.CompactGrid -> LibraryMangaCompactGrid(

View File

@@ -26,14 +26,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.downloads.DownloadsScreen import ca.gosyer.ui.downloads.DownloadsScreen
import ca.gosyer.ui.extensions.ExtensionsScreen import ca.gosyer.ui.extensions.ExtensionsScreen
import ca.gosyer.ui.extensions.openExtensionsMenu
import ca.gosyer.ui.library.LibraryScreen import ca.gosyer.ui.library.LibraryScreen
import ca.gosyer.ui.library.openLibraryMenu
import ca.gosyer.ui.main.components.DownloadsExtraInfo import ca.gosyer.ui.main.components.DownloadsExtraInfo
import ca.gosyer.ui.main.more.MoreScreen import ca.gosyer.ui.main.more.MoreScreen
import ca.gosyer.ui.settings.SettingsScreen import ca.gosyer.ui.settings.SettingsScreen
import ca.gosyer.ui.sources.SourcesScreen import ca.gosyer.ui.sources.SourcesScreen
import ca.gosyer.ui.sources.openSourcesMenu
import ca.gosyer.ui.updates.UpdatesScreen import ca.gosyer.ui.updates.UpdatesScreen
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
@@ -46,7 +43,6 @@ interface Menu {
val selectedIcon: ImageVector val selectedIcon: ImageVector
val screen: KClass<*> val screen: KClass<*>
val createScreen: () -> Screen val createScreen: () -> Screen
val openInNewWindow: (() -> Unit)?
val extraInfo: (@Composable () -> Unit)? val extraInfo: (@Composable () -> Unit)?
fun isSelected(navigator: Navigator) = navigator.items.first()::class == screen fun isSelected(navigator: Navigator) = navigator.items.first()::class == screen
@@ -58,13 +54,12 @@ enum class TopLevelMenus(
override val selectedIcon: ImageVector, override val selectedIcon: ImageVector,
override val screen: KClass<*>, override val screen: KClass<*>,
override val createScreen: () -> Screen, override val createScreen: () -> Screen,
override val openInNewWindow: (() -> Unit)? = null,
override val extraInfo: (@Composable () -> Unit)? = null override val extraInfo: (@Composable () -> Unit)? = null
) : Menu { ) : Menu {
Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { LibraryScreen() }, ::openLibraryMenu), 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() }, ::openLibraryMenu), 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() }, ::openSourcesMenu), 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() }, ::openExtensionsMenu), 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() }); 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 selectedIcon: ImageVector,
override val screen: KClass<*>, override val screen: KClass<*>,
override val createScreen: () -> Screen, override val createScreen: () -> Screen,
override val openInNewWindow: (() -> Unit)? = null,
override val extraInfo: (@Composable () -> Unit)? = null override val extraInfo: (@Composable () -> Unit)? = null
) : Menu { ) : Menu {
Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, DownloadsScreen::class, { DownloadsScreen() }, extraInfo = { DownloadsExtraInfo() }), 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() }); Settings(MR.strings.location_settings, Icons.Outlined.Settings, Icons.Rounded.Settings, SettingsScreen::class, { SettingsScreen() });
} }

View File

@@ -6,6 +6,7 @@
package ca.gosyer.ui.main.components package ca.gosyer.ui.main.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.ui.main.Menu import ca.gosyer.ui.main.Menu
import ca.gosyer.uicore.components.combinedMouseClickable
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
@@ -37,7 +37,6 @@ fun SideMenuItem(selected: Boolean, topLevelMenu: Menu, newRoot: (Screen) -> Uni
topLevelMenu.createScreen, topLevelMenu.createScreen,
topLevelMenu.selectedIcon, topLevelMenu.selectedIcon,
topLevelMenu.unselectedIcon, topLevelMenu.unselectedIcon,
topLevelMenu.openInNewWindow,
topLevelMenu.extraInfo, topLevelMenu.extraInfo,
newRoot newRoot
) )
@@ -50,7 +49,6 @@ private fun SideMenuItem(
createScreen: () -> Screen, createScreen: () -> Screen,
selectedIcon: ImageVector, selectedIcon: ImageVector,
unselectedIcon: ImageVector, unselectedIcon: ImageVector,
onMiddleClick: (() -> Unit)?,
extraInfo: (@Composable () -> Unit)? = null, extraInfo: (@Composable () -> Unit)? = null,
onClick: (Screen) -> Unit onClick: (Screen) -> Unit
) { ) {
@@ -68,9 +66,9 @@ private fun SideMenuItem(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.defaultMinSize(minHeight = 40.dp) .defaultMinSize(minHeight = 40.dp)
.combinedMouseClickable( .clickable(
onClick = { onClick(createScreen()) }, onClick = { onClick(createScreen()) },
onMiddleClick = { onMiddleClick?.invoke() } // onMiddleClick = { onMiddleClick?.invoke() } todo
) )
) { ) {
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))

View File

@@ -9,6 +9,7 @@ package ca.gosyer.ui.main.components
import ca.gosyer.data.update.UpdateChecker import ca.gosyer.data.update.UpdateChecker
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class TrayViewModel @Inject constructor( class TrayViewModel @Inject constructor(
@@ -21,4 +22,9 @@ class TrayViewModel @Inject constructor(
} }
val updateFound val updateFound
get() = updateChecker.updateFound get() = updateChecker.updateFound
override fun onDispose() {
super.onDispose()
scope.cancel()
}
} }

View File

@@ -6,35 +6,13 @@
package ca.gosyer.ui.manga package ca.gosyer.ui.manga
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState 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.manga.components.MangaScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey 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 { class MangaScreen(private val mangaId: Long) : Screen {
@@ -53,6 +31,8 @@ class MangaScreen(private val mangaId: Long) : Screen {
dateTimeFormatter = vm.dateTimeFormatter.collectAsState().value, dateTimeFormatter = vm.dateTimeFormatter.collectAsState().value,
categoriesExist = vm.categoriesExist.collectAsState().value, categoriesExist = vm.categoriesExist.collectAsState().value,
chooseCategoriesFlow = vm.chooseCategoriesFlow, chooseCategoriesFlow = vm.chooseCategoriesFlow,
availableCategories = vm.categories.collectAsState().value,
mangaCategories = vm.mangaCategories.collectAsState().value,
addFavorite = vm::addFavorite, addFavorite = vm::addFavorite,
setCategories = vm::setCategories, setCategories = vm::setCategories,
toggleFavorite = vm::toggleFavorite, toggleFavorite = vm::toggleFavorite,

View File

@@ -22,10 +22,12 @@ import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import java.time.ZoneId import java.time.ZoneId
@@ -53,10 +55,16 @@ class MangaScreenViewModel @Inject constructor(
private val _isLoading = MutableStateFlow(true) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
private val _categoriesExist = MutableStateFlow(true) private val _categories = MutableStateFlow(emptyList<Category>())
val categoriesExist = _categoriesExist.asStateFlow() val categories = _categories.asStateFlow()
val chooseCategoriesFlow = MutableSharedFlow<Pair<List<Category>, List<Category>>>() private val _mangaCategories = MutableStateFlow(emptyList<Category>())
val mangaCategories = _mangaCategories.asStateFlow()
val categoriesExist = categories.map { it.isNotEmpty() }
.stateIn(scope, SharingStarted.Eagerly, true)
val chooseCategoriesFlow = MutableSharedFlow<Unit>()
val dateTimeFormatter = uiPreferences.dateFormat().changes() val dateTimeFormatter = uiPreferences.dateFormat().changes()
.map { .map {
@@ -77,7 +85,7 @@ class MangaScreenViewModel @Inject constructor(
} }
scope.launch { scope.launch {
_categoriesExist.value = categoryHandler.getCategories(true).isNotEmpty() _categories.value = categoryHandler.getCategories(true)
} }
} }
@@ -108,9 +116,7 @@ class MangaScreenViewModel @Inject constructor(
fun setCategories() { fun setCategories() {
scope.launch { scope.launch {
manga.value?.let { manga -> manga.value?.let { manga ->
val categories = async { categoryHandler.getCategories(true) } chooseCategoriesFlow.emit(Unit)
val oldCategories = async { categoryHandler.getMangaCategories(manga) }
chooseCategoriesFlow.emit(categories.await() to oldCategories.await())
} }
} }
} }
@@ -119,6 +125,7 @@ class MangaScreenViewModel @Inject constructor(
async { async {
try { try {
_manga.value = mangaHandler.getManga(mangaId, refresh) _manga.value = mangaHandler.getManga(mangaId, refresh)
_mangaCategories.value = categoryHandler.getMangaCategories(mangaId)
} catch (e: Exception) { } catch (e: Exception) {
e.throwIfCancellation() e.throwIfCancellation()
} }
@@ -142,11 +149,10 @@ class MangaScreenViewModel @Inject constructor(
libraryHandler.removeMangaFromLibrary(manga) libraryHandler.removeMangaFromLibrary(manga)
refreshMangaAsync(manga.id).await() refreshMangaAsync(manga.id).await()
} else { } else {
val categories = categoryHandler.getCategories(true) if (categories.value.isEmpty()) {
if (categories.isEmpty()) {
addFavorite(emptyList(), emptyList()) addFavorite(emptyList(), emptyList())
} else { } else {
chooseCategoriesFlow.emit(categories to emptyList()) chooseCategoriesFlow.emit(Unit)
} }
} }
} }

View File

@@ -32,8 +32,8 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
@@ -43,11 +43,15 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import ca.gosyer.data.models.Category import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga 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.image.KamelImage
import ca.gosyer.uicore.resources.stringResource
import com.google.accompanist.flowlayout.FlowRow 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 io.kamel.image.lazyPainterResource
import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
fun MangaItem(manga: Manga) { fun MangaItem(manga: Manga) {
@@ -121,28 +125,36 @@ private fun Chip(text: String) {
} }
} }
fun openCategorySelectDialog( @Composable
fun CategorySelectDialog(
state: MaterialDialogState,
categories: List<Category>, categories: List<Category>,
oldCategories: List<Category>, oldCategories: List<Category>,
onPositiveClick: (List<Category>, List<Category>) -> Unit onPositiveClick: (List<Category>, List<Category>) -> Unit
) { ) {
val enabledCategoriesFlow = MutableStateFlow(oldCategories) val enabledCategories = remember(oldCategories) { oldCategories.toMutableStateList() }
WindowDialog( MaterialDialog(
"Select Categories", state,
onPositiveButton = { onPositiveClick(enabledCategoriesFlow.value, oldCategories) } buttons = {
positiveButton(stringResource(MR.strings.action_ok)) {
onPositiveClick(enabledCategories.toList(), oldCategories)
}
negativeButton(stringResource(MR.strings.action_cancel))
},
properties = getMaterialDialogProperties(),
) { ) {
val enabledCategories by enabledCategoriesFlow.collectAsState() title("Select Categories")
val state = rememberLazyListState() val listState = rememberLazyListState()
Box { Box {
LazyColumn(state = state) { LazyColumn(state = listState) {
items(categories) { category -> items(categories) { category ->
Row( Row(
Modifier.fillMaxWidth().padding(8.dp) Modifier.fillMaxWidth().padding(8.dp)
.clickable { .clickable {
if (category in enabledCategories) { if (category in enabledCategories) {
enabledCategoriesFlow.value -= category enabledCategories -= category
} else { } else {
enabledCategoriesFlow.value += category enabledCategories += category
} }
}, },
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@@ -159,7 +171,7 @@ fun openCategorySelectDialog(
modifier = Modifier.align(Alignment.CenterEnd) modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight() .fillMaxHeight()
.padding(horizontal = 4.dp, vertical = 8.dp), .padding(horizontal = 4.dp, vertical = 8.dp),
adapter = rememberScrollbarAdapter(state) adapter = rememberScrollbarAdapter(listState)
) )
} }
} }

View File

@@ -38,6 +38,7 @@ import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.uicore.components.ErrorScreen import ca.gosyer.uicore.components.ErrorScreen
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -48,7 +49,9 @@ fun MangaScreenContent(
chapters: List<ChapterDownloadItem>, chapters: List<ChapterDownloadItem>,
dateTimeFormatter: DateTimeFormatter, dateTimeFormatter: DateTimeFormatter,
categoriesExist: Boolean, categoriesExist: Boolean,
chooseCategoriesFlow: SharedFlow<Pair<List<Category>, List<Category>>>, chooseCategoriesFlow: SharedFlow<Unit>,
availableCategories: List<Category>,
mangaCategories: List<Category>,
addFavorite: (List<Category>, List<Category>) -> Unit, addFavorite: (List<Category>, List<Category>) -> Unit,
setCategories: () -> Unit, setCategories: () -> Unit,
toggleFavorite: () -> Unit, toggleFavorite: () -> Unit,
@@ -62,9 +65,10 @@ fun MangaScreenContent(
loadChapters: () -> Unit, loadChapters: () -> Unit,
loadManga: () -> Unit loadManga: () -> Unit
) { ) {
val categoryDialogState = rememberMaterialDialogState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
chooseCategoriesFlow.collect { (availableCategories, usedCategories) -> chooseCategoriesFlow.collect {
openCategorySelectDialog(availableCategories, usedCategories, addFavorite) categoryDialogState.show()
} }
} }
@@ -135,6 +139,7 @@ fun MangaScreenContent(
} }
} }
} }
CategorySelectDialog(categoryDialogState, availableCategories, mangaCategories, addFavorite)
} }
@Composable @Composable

View File

@@ -41,7 +41,7 @@ fun PagerReader(
retry: (ReaderPage) -> Unit, retry: (ReaderPage) -> Unit,
progress: (Int) -> Unit progress: (Int) -> Unit
) { ) {
val state = rememberPagerState(pages.size + 2, initialPage = currentPage) val state = rememberPagerState(initialPage = currentPage)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val pageRange = 0..(pages.size + 1) val pageRange = 0..(pages.size + 1)
@@ -77,7 +77,12 @@ fun PagerReader(
val modifier = parentModifier then Modifier.fillMaxSize() val modifier = parentModifier then Modifier.fillMaxSize()
if (direction == Direction.Down || direction == Direction.Up) { 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( HandlePager(
pages, pages,
it, it,
@@ -90,7 +95,12 @@ fun PagerReader(
) )
} }
} else { } else {
HorizontalPager(state, reverseLayout = direction == Direction.Left, modifier = modifier) { HorizontalPager(
count = pages.size + 2,
state = state,
reverseLayout = direction == Direction.Left,
modifier = modifier
) {
HandlePager( HandlePager(
pages, pages,
it, it,

View File

@@ -27,6 +27,10 @@ import androidx.compose.material.icons.rounded.Warning
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.core.logging.CKLogger
import ca.gosyer.data.server.interactions.BackupInteractionHandler import ca.gosyer.data.server.interactions.BackupInteractionHandler
import ca.gosyer.i18n.MR 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.navigation.Toolbar
import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.PreferenceRow
import ca.gosyer.ui.util.system.filePicker 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.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey 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.onDownload
import io.ktor.client.features.onUpload import io.ktor.client.features.onUpload
import io.ktor.http.isSuccess import io.ktor.http.isSuccess
@@ -225,10 +233,15 @@ private fun SettingsBackupScreenContent(
stopRestore: () -> Unit, stopRestore: () -> Unit,
exportBackup: () -> Unit exportBackup: () -> Unit
) { ) {
var backupFile by remember { mutableStateOf<Path?>(null) }
var missingSources by remember { mutableStateOf(emptyList<String>()) }
val dialogState = rememberMaterialDialogState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch { launch {
missingSourceFlow.collect { (backup, sources) -> missingSourceFlow.collect { (backup, sources) ->
openMissingSourcesDialog(sources, { restoreBackup(backup) }, stopRestore) backupFile = backup
missingSources = sources
dialogState.show()
} }
} }
launch { launch {
@@ -278,21 +291,48 @@ private fun SettingsBackupScreenContent(
) )
} }
} }
MissingSourcesDialog(
dialogState,
missingSources,
onPositiveClick = {
restoreBackup(backupFile ?: return@MissingSourcesDialog)
},
onNegativeClick = stopRestore
)
} }
private fun openMissingSourcesDialog(missingSources: List<String>, onPositiveClick: () -> Unit, onNegativeClick: () -> Unit) { @Composable
WindowDialog( private fun MissingSourcesDialog(
"Missing Sources", state: MaterialDialogState,
onPositiveButton = onPositiveClick, missingSources: List<String>,
onNegativeButton = onNegativeClick 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 { title("Missing Sources")
item { Box {
Text(stringResource(MR.strings.missing_sources), style = MaterialTheme.typography.subtitle2) val listState = rememberLazyListState()
} LazyColumn(Modifier.fillMaxSize(), state = listState) {
items(missingSources) { item {
Text(it) 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)
)
} }
} }
} }

View File

@@ -59,71 +59,47 @@ class SettingsServerScreen : Screen {
@Composable @Composable
override fun Content() { override fun Content() {
val vm = viewModel<SettingsServerViewModel>() val connectionVM = viewModel<SettingsServerViewModel>()
val serverVm = viewModel<SettingsServerHostViewModel>()
SettingsServerScreenContent( SettingsServerScreenContent(
hostValue = vm.host.collectAsState().value, hostValue = serverVm.host.collectAsState().value,
basicAuthEnabledValue = vm.basicAuthEnabled.collectAsState().value, basicAuthEnabledValue = serverVm.basicAuthEnabled.collectAsState().value,
proxyValue = vm.proxy.collectAsState().value, proxyValue = connectionVM.proxy.collectAsState().value,
authValue = vm.auth.collectAsState().value, authValue = connectionVM.auth.collectAsState().value,
restartServer = vm::restartServer, restartServer = serverVm::restartServer,
serverSettingChanged = vm::serverSettingChanged, serverSettingChanged = serverVm::serverSettingChanged,
host = vm.host, host = serverVm.host,
ip = vm.ip, ip = serverVm.ip,
port = vm.port, port = serverVm.port,
socksProxyEnabled = vm.socksProxyEnabled, socksProxyEnabled = serverVm.socksProxyEnabled,
socksProxyHost = vm.socksProxyHost, socksProxyHost = serverVm.socksProxyHost,
socksProxyPort = vm.socksProxyPort, socksProxyPort = serverVm.socksProxyPort,
debugLogsEnabled = vm.debugLogsEnabled, debugLogsEnabled = serverVm.debugLogsEnabled,
systemTrayEnabled = vm.systemTrayEnabled, systemTrayEnabled = serverVm.systemTrayEnabled,
webUIEnabled = vm.webUIEnabled, webUIEnabled = serverVm.webUIEnabled,
openInBrowserEnabled = vm.openInBrowserEnabled, openInBrowserEnabled = serverVm.openInBrowserEnabled,
basicAuthEnabled = vm.basicAuthEnabled, basicAuthEnabled = serverVm.basicAuthEnabled,
basicAuthUsername = vm.basicAuthUsername, basicAuthUsername = serverVm.basicAuthUsername,
basicAuthPassword = vm.basicAuthPassword, basicAuthPassword = serverVm.basicAuthPassword,
serverUrl = vm.serverUrl, serverUrl = connectionVM.serverUrl,
serverPort = vm.serverPort, serverPort = connectionVM.serverPort,
proxy = vm.proxy, proxy = connectionVM.proxy,
proxyChoices = vm.getProxyChoices(), proxyChoices = connectionVM.getProxyChoices(),
httpHost = vm.httpHost, httpHost = connectionVM.httpHost,
httpPort = vm.httpPort, httpPort = connectionVM.httpPort,
socksHost = vm.socksHost, socksHost = connectionVM.socksHost,
socksPort = vm.socksPort, socksPort = connectionVM.socksPort,
auth = vm.auth, auth = connectionVM.auth,
authChoices = vm.getAuthChoices(), authChoices = connectionVM.getAuthChoices(),
authUsername = vm.authUsername, authUsername = connectionVM.authUsername,
authPassword = vm.authPassword authPassword = connectionVM.authPassword
) )
} }
} }
class SettingsServerViewModel @Inject constructor( class SettingsServerViewModel @Inject constructor(
serverPreferences: ServerPreferences, serverPreferences: ServerPreferences
serverHostPreferences: ServerHostPreferences,
private val serverService: ServerService
) : ViewModel() { ) : 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 serverUrl = serverPreferences.server().asStateIn(scope)
val serverPort = serverPreferences.port().asStringStateIn(scope) val serverPort = serverPreferences.port().asStringStateIn(scope)
@@ -158,12 +134,53 @@ class SettingsServerViewModel @Inject constructor(
_serverSettingChanged.value = true _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() { fun restartServer() {
if (serverSettingChanged.value) { if (serverSettingChanged.value) {
serverService.restartServer() 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 { init {
combine(basicAuthEnabled, basicAuthUsername, basicAuthPassword) { enabled, username, password -> combine(basicAuthEnabled, basicAuthUsername, basicAuthPassword) { enabled, username, password ->
if (enabled) { if (enabled) {

View File

@@ -6,35 +6,13 @@
package ca.gosyer.ui.sources package ca.gosyer.ui.sources
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState 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.sources.components.SourcesMenu
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey 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 { class SourcesScreen : Screen {

View File

@@ -27,8 +27,8 @@ class SourceHomeScreen : Screen {
onAddSource = sourcesNavigator::select, onAddSource = sourcesNavigator::select,
isLoading = vm.isLoading.collectAsState().value, isLoading = vm.isLoading.collectAsState().value,
sources = vm.sources.collectAsState().value, sources = vm.sources.collectAsState().value,
languages = vm.languages, languages = vm.languages.collectAsState().value,
getSourceLanguages = vm::getSourceLanguages, sourceLanguages = vm.sourceLanguages.collectAsState().value,
setEnabledLanguages = vm::setEnabledLanguages setEnabledLanguages = vm::setEnabledLanguages
) )
} }

View File

@@ -13,7 +13,11 @@ import ca.gosyer.data.models.Source
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow 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 kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -24,13 +28,21 @@ class SourceHomeScreenViewModel @Inject constructor(
private val _isLoading = MutableStateFlow(true) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
private val installedSources = MutableStateFlow(emptyList<Source>())
private val _languages = catalogPreferences.languages().asStateFlow() private val _languages = catalogPreferences.languages().asStateFlow()
val languages = _languages.asStateFlow() val languages = _languages.asStateFlow()
private val _sources = MutableStateFlow(emptyList<Source>()) val sources = combine(installedSources, languages) { installedSources, languages ->
val sources = _sources.asStateFlow() 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<Source>()
init { init {
getSources() getSources()
@@ -39,9 +51,7 @@ class SourceHomeScreenViewModel @Inject constructor(
private fun getSources() { private fun getSources() {
scope.launch { scope.launch {
try { try {
installedSources = sourceHandler.getSourceList() installedSources.value = sourceHandler.getSourceList()
setSources(_languages.value)
info { _sources.value }
} catch (e: Exception) { } catch (e: Exception) {
e.throwIfCancellation() e.throwIfCancellation()
} finally { } finally {
@@ -50,18 +60,9 @@ class SourceHomeScreenViewModel @Inject constructor(
} }
} }
private fun setSources(langs: Set<String>) {
_sources.value = installedSources.filter { it.lang in langs || it.lang == Source.LOCAL_SOURCE_LANG }
}
fun getSourceLanguages(): Set<String> {
return installedSources.map { it.lang }.toSet() - setOf(Source.LOCAL_SOURCE_LANG)
}
fun setEnabledLanguages(langs: Set<String>) { fun setEnabledLanguages(langs: Set<String>) {
info { langs } info { langs }
_languages.value = langs _languages.value = langs
setSources(langs)
} }
private companion object : CKLogger({}) private companion object : CKLogger({})

View File

@@ -48,25 +48,23 @@ import ca.gosyer.ui.extensions.components.LanguageDialog
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.image.KamelImage
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
import io.kamel.image.lazyPainterResource import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun SourceHomeScreenContent( fun SourceHomeScreenContent(
onAddSource: (Source) -> Unit, onAddSource: (Source) -> Unit,
isLoading: Boolean, isLoading: Boolean,
sources: List<Source>, sources: List<Source>,
languages: StateFlow<Set<String>>, languages: Set<String>,
getSourceLanguages: () -> Set<String>, sourceLanguages: Set<String>,
setEnabledLanguages: (Set<String>) -> Unit setEnabledLanguages: (Set<String>) -> Unit
) { ) {
val languageDialogState = rememberMaterialDialogState()
Scaffold( Scaffold(
topBar = { topBar = {
SourceHomeScreenToolbar( SourceHomeScreenToolbar(
languages, languageDialogState::show
getSourceLanguages,
setEnabledLanguages
) )
} }
) { ) {
@@ -97,24 +95,18 @@ fun SourceHomeScreenContent(
} }
} }
} }
LanguageDialog(languageDialogState, languages, sourceLanguages, setEnabledLanguages)
} }
@Composable @Composable
fun SourceHomeScreenToolbar( fun SourceHomeScreenToolbar(
sourceLanguages: StateFlow<Set<String>>, openEnabledLanguagesClick: () -> Unit
onGetEnabledLanguages: () -> Set<String>,
onSetEnabledLanguages: (Set<String>) -> Unit
) { ) {
Toolbar( Toolbar(
stringResource(MR.strings.location_sources), stringResource(MR.strings.location_sources),
actions = { actions = {
getActionItems( getActionItems(
onEnabledLanguagesClick = { openEnabledLanguagesClick = openEnabledLanguagesClick
val enabledLangs = MutableStateFlow(sourceLanguages.value)
LanguageDialog(enabledLangs, onGetEnabledLanguages().toList()) {
onSetEnabledLanguages(enabledLangs.value)
}
}
) )
} }
) )
@@ -177,13 +169,13 @@ fun SourceItem(
@Composable @Composable
@Stable @Stable
private fun getActionItems( private fun getActionItems(
onEnabledLanguagesClick: () -> Unit openEnabledLanguagesClick: () -> Unit
): List<ActionItem> { ): List<ActionItem> {
return listOf( return listOf(
ActionItem( ActionItem(
stringResource(MR.strings.enabled_languages), stringResource(MR.strings.enabled_languages),
Icons.Rounded.Translate, Icons.Rounded.Translate,
doAction = onEnabledLanguagesClick doAction = openEnabledLanguagesClick
) )
) )
} }

View File

@@ -7,31 +7,12 @@
package ca.gosyer.ui.sources.settings package ca.gosyer.ui.sources.settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState 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.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 ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey 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 { class SourceSettingsScreen(private val sourceId: Long) : Screen {

View File

@@ -8,10 +8,8 @@ package ca.gosyer.ui.sources.settings.components
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -21,18 +19,19 @@ import androidx.compose.material.Checkbox
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.Switch import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig 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.navigation.Toolbar
import ca.gosyer.ui.base.prefs.ChoiceDialog import ca.gosyer.ui.base.prefs.ChoiceDialog
import ca.gosyer.ui.base.prefs.MultiSelectDialog 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.ui.sources.settings.model.SourceSettingsView.TwoState
import ca.gosyer.uicore.components.keyboardHandler import ca.gosyer.uicore.components.keyboardHandler
import ca.gosyer.uicore.resources.stringResource 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 import kotlin.collections.List as KtList
@Composable @Composable
@@ -124,18 +126,21 @@ private fun ListPreference(list: List) {
list.summary list.summary
} }
} }
val dialogState = rememberMaterialDialogState()
PreferenceRow( PreferenceRow(
title, title,
subtitle = subtitle, subtitle = subtitle,
onClick = { onClick = {
ChoiceDialog( dialogState.show()
list.getOptions(),
state,
onSelected = list::updateState,
title = title
)
} }
) )
ChoiceDialog(
dialogState,
list.getOptions(),
state,
onSelected = list::updateState,
title = title
)
} }
@Composable @Composable
@@ -150,18 +155,21 @@ private fun MultiSelectPreference(multiSelect: MultiSelect) {
} }
} }
val dialogTitle = remember(state) { multiSelect.props.dialogTitle ?: multiSelect.title ?: multiSelect.summary ?: "No title" } val dialogTitle = remember(state) { multiSelect.props.dialogTitle ?: multiSelect.title ?: multiSelect.summary ?: "No title" }
val dialogState = rememberMaterialDialogState()
PreferenceRow( PreferenceRow(
title, title,
subtitle = subtitle, subtitle = subtitle,
onClick = { onClick = {
MultiSelectDialog( dialogState.show()
multiSelect.getOptions(),
state,
onFinished = multiSelect::updateState,
title = dialogTitle
)
} }
) )
MultiSelectDialog(
dialogState,
multiSelect.getOptions(),
state,
onFinished = multiSelect::updateState,
title = dialogTitle
)
} }
@Composable @Composable
@@ -175,31 +183,33 @@ private fun EditTextPreference(editText: EditText) {
editText.summary editText.summary
} }
} }
val dialogState = rememberMaterialDialogState()
PreferenceRow( PreferenceRow(
title, title,
subtitle = subtitle, subtitle = subtitle,
onClick = { onClick = dialogState::show
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)
)
}
}
) )
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)
)
}
} }

View File

@@ -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 <T> StateFlow<T>.getValue(thisObj: Any?, property: KProperty<*>): T {
val item by collectAsState()
return item
}

View File

@@ -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)

View File

@@ -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<ScrollbarStyle>
@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

View File

@@ -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
)
}

View File

@@ -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)

View File

@@ -8,8 +8,6 @@ package ca.gosyer.ui.base.navigation
import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
@@ -25,6 +23,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.util.fastForEach 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 // Originally from https://gist.github.com/MachFour/369ebb56a66e2f583ebfb988dda2decf
@@ -82,9 +82,12 @@ fun ActionMenu(
} }
if (overflowActions.isNotEmpty()) { if (overflowActions.isNotEmpty()) {
IconButton(onClick = { menuVisible.value = true }) { iconItem(
Icon(Icons.Default.MoreVert, "More actions") { menuVisible.value = true },
} stringResource(MR.strings.action_more_actions),
Icons.Default.MoreVert,
true
)
DropdownMenu( DropdownMenu(
expanded = menuVisible.value, expanded = menuVisible.value,
onDismissRequest = { menuVisible.value = false }, onDismissRequest = { menuVisible.value = false },
@@ -98,7 +101,7 @@ fun ActionMenu(
}, },
enabled = item.enabled 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) Text(item.name)
} }
} }
@@ -150,4 +153,4 @@ private fun separateIntoIconAndOverflow(
} }
} }
return iconActions to overflowActions return iconActions to overflowActions
} }

View File

@@ -69,7 +69,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.uicore.components.BoxWithTooltipSurface
import ca.gosyer.uicore.components.keyboardHandler import ca.gosyer.uicore.components.keyboardHandler
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@@ -192,8 +191,6 @@ private fun WideToolbar(
} }
} }
@Composable @Composable
private fun ThinToolbar( private fun ThinToolbar(
name: String, 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 @Composable
fun TextActionIcon( fun TextActionIcon(
onClick: () -> Unit, onClick: () -> Unit,

View File

@@ -34,11 +34,9 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Check
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachIndexed
import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.dialog.getMaterialDialogProperties
import ca.gosyer.uicore.components.keyboardHandler 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 import kotlin.math.round
@Composable
fun ColorPickerDialog( fun ColorPickerDialog(
state: MaterialDialogState,
title: String, title: String,
onCloseRequest: () -> Unit = {}, onCloseRequest: () -> Unit = {},
onSelected: (Color) -> Unit, onSelected: (Color) -> Unit,
initialColor: Color = Color.Unspecified, initialColor: Color = Color.Unspecified,
) { ) {
val currentColor = MutableStateFlow(initialColor) var currentColor by remember(initialColor) { mutableStateOf(initialColor) }
val showPresets = MutableStateFlow(true) var showPresets by remember { mutableStateOf(true) }
WindowDialog( MaterialDialog(
onCloseRequest = onCloseRequest, state,
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 }
)
}
},
buttons = { buttons = {
val showPresetsState by showPresets.collectAsState() positiveButton("Select", onClick = { onSelected(currentColor) })
val currentColorState by currentColor.collectAsState() button(
Row(Modifier.fillMaxWidth().padding(8.dp).align(Alignment.BottomCenter)) { if (showPresets) "Custom" else "Presets",
TextButton( onClick = {
onClick = { showPresets = !showPresets
showPresets.value = !showPresetsState
}
) {
Text(if (showPresetsState) "Custom" else "Presets")
} }
Spacer(Modifier.weight(1f)) )
TextButton( },
onClick = { properties = getMaterialDialogProperties(
onSelected(currentColorState) size = DpSize(300.dp, 520.dp)
it() ),
} onCloseRequest = {
) { it.hide()
Text("Select") onCloseRequest()
}
}
} }
) ) {
title(title)
if (showPresets) {
ColorPresets(
initialColor = currentColor,
onColorChanged = { currentColor = it }
)
} else {
ColorPalette(
initialColor = currentColor,
onColorChanged = { currentColor = it }
)
}
}
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)

View File

@@ -20,14 +20,12 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row 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.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Checkbox import androidx.compose.material.Checkbox
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
@@ -65,6 +62,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import 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.components.keyboardHandler
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow 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 @Composable
fun PreferenceRow( fun PreferenceRow(
@@ -180,31 +186,39 @@ fun EditTextPreference(
enabled: Boolean = true, enabled: Boolean = true,
visualTransformation: VisualTransformation = VisualTransformation.None visualTransformation: VisualTransformation = VisualTransformation.None
) { ) {
val dialogState = rememberMaterialDialogState()
PreferenceRow( PreferenceRow(
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
icon = icon, icon = icon,
onClick = { onClick = {
var editText by mutableStateOf(TextFieldValue(preference.value)) dialogState.show()
WindowDialog(
title,
onPositiveButton = {
preference.value = editText.text
changeListener()
}
) {
OutlinedTextField(
editText,
onValueChange = {
editText = it
},
visualTransformation = visualTransformation,
modifier = Modifier.keyboardHandler()
)
}
}, },
enabled = enabled 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 @Composable
@@ -217,96 +231,117 @@ fun <Key> ChoicePreference(
enabled: Boolean = true enabled: Boolean = true
) { ) {
val prefValue by preference.collectAsState() val prefValue by preference.collectAsState()
val dialogState = rememberMaterialDialogState()
PreferenceRow( PreferenceRow(
title = title, title = title,
subtitle = subtitle ?: choices[prefValue], subtitle = subtitle ?: choices[prefValue],
onClick = { onClick = {
ChoiceDialog( dialogState.show()
items = choices.toList(),
selected = prefValue,
title = title,
onSelected = { selected ->
preference.value = selected
changeListener()
}
)
}, },
enabled = enabled enabled = enabled
) )
ChoiceDialog(
state = dialogState,
items = choices.toList(),
selected = prefValue,
title = title,
onSelected = { selected ->
preference.value = selected
changeListener()
}
)
} }
@Composable
fun <T> ChoiceDialog( fun <T> ChoiceDialog(
state: MaterialDialogState,
items: List<Pair<T, String>>, items: List<Pair<T, String>>,
selected: T?, selected: T?,
onCloseRequest: () -> Unit = {}, onCloseRequest: () -> Unit = {},
onSelected: (T) -> Unit, onSelected: (T) -> Unit,
title: String, title: String,
buttons: @Composable BoxWithConstraintsScope.(() -> Unit) -> Unit = { } buttons: @Composable MaterialDialogButtons.() -> Unit = { }
) { ) {
WindowDialog( MaterialDialog(
onCloseRequest = onCloseRequest, state,
buttons = buttons, buttons = buttons,
title = title properties = getMaterialDialogProperties(),
onCloseRequest = {
state.hide()
onCloseRequest()
}
) { ) {
val state = rememberLazyListState() title(title)
LazyColumn(Modifier.fillMaxSize(), state) { Box {
items(items) { (value, text) -> val listState = rememberLazyListState()
Row( LazyColumn(Modifier.fillMaxSize(), listState) {
modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable( items(items) { (value, text) ->
onClick = { Row(
onSelected(value) modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable(
it() onClick = {
} onSelected(value)
), state.hide()
verticalAlignment = Alignment.CenterVertically }
) { ),
RadioButton( verticalAlignment = Alignment.CenterVertically
selected = value == selected, ) {
onClick = { RadioButton(
onSelected(value) selected = value == selected,
it() onClick = {
}, onSelected(value)
) state.hide()
Text(text = text, modifier = Modifier.padding(start = 24.dp)) },
)
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 <T> MultiSelectDialog( fun <T> MultiSelectDialog(
state: MaterialDialogState,
items: List<Pair<T, String>>, items: List<Pair<T, String>>,
selected: List<T>?, selected: List<T>?,
onCloseRequest: () -> Unit = {}, onCloseRequest: () -> Unit = {},
onFinished: (List<T>) -> Unit, onFinished: (List<T>) -> Unit,
title: String, title: String,
) { ) {
val checkedFlow = MutableStateFlow(selected.orEmpty()) val checked = remember(selected) { selected.orEmpty().toMutableStateList() }
WindowDialog( MaterialDialog(
onCloseRequest = onCloseRequest, state,
title = title, buttons = {
onPositiveButton = { positiveButton(stringResource(MR.strings.action_ok)) {
onFinished(checkedFlow.value) onFinished(checked)
}
negativeButton(stringResource(MR.strings.action_cancel))
},
properties = getMaterialDialogProperties(),
onCloseRequest = {
state.hide()
onCloseRequest()
} }
) { ) {
val checked by checkedFlow.collectAsState() title(title)
val state = rememberLazyListState() val listState = rememberLazyListState()
Box { Box {
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), listState) {
items(items) { (value, text) -> items(items) { (value, text) ->
Row( Row(
modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable( modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable(
onClick = { onClick = {
if (value in checked) { if (value in checked) {
checkedFlow.value -= value checked -= value
} else { } else {
checkedFlow.value += value checked += value
} }
} }
), ),
@@ -322,7 +357,7 @@ fun <T> MultiSelectDialog(
item { Spacer(Modifier.height(80.dp)) } item { Spacer(Modifier.height(80.dp)) }
} }
VerticalScrollbar( VerticalScrollbar(
rememberScrollbarAdapter(state), rememberScrollbarAdapter(listState),
Modifier.align(Alignment.CenterEnd) Modifier.align(Alignment.CenterEnd)
.fillMaxHeight() .fillMaxHeight()
.padding(horizontal = 4.dp, vertical = 8.dp) .padding(horizontal = 4.dp, vertical = 8.dp)
@@ -340,17 +375,12 @@ fun ColorPreference(
unsetColor: Color = Color.Unspecified unsetColor: Color = Color.Unspecified
) { ) {
val initialColor = preference.value.takeOrElse { unsetColor } val initialColor = preference.value.takeOrElse { unsetColor }
val dialogState = rememberMaterialDialogState()
PreferenceRow( PreferenceRow(
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
onClick = { onClick = {
ColorPickerDialog( dialogState.show()
title = title,
onSelected = {
preference.value = it
},
initialColor = initialColor
)
}, },
onLongClick = { preference.value = Color.Unspecified }, onLongClick = { preference.value = Color.Unspecified },
action = { action = {
@@ -369,6 +399,14 @@ fun ColorPreference(
}, },
enabled = enabled enabled = enabled
) )
ColorPickerDialog(
state = dialogState,
title = title,
onSelected = {
preference.value = it
},
initialColor = initialColor
)
} }
const val EXPAND_ANIMATION_DURATION = 300 const val EXPAND_ANIMATION_DURATION = 300

View File

@@ -4,11 +4,10 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
@file:JvmName("ThemeScrollbarStyleKt")
package ca.gosyer.ui.base.theme 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.foundation.isSystemInDarkTheme
import androidx.compose.material.Colors import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme 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.Color
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.unit.dp
import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.data.ui.model.ThemeMode 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.Theme
import ca.gosyer.uicore.theme.themes import ca.gosyer.uicore.theme.themes
import ca.gosyer.uicore.vm.LocalViewModelFactory import ca.gosyer.uicore.vm.LocalViewModelFactory
@@ -47,14 +47,7 @@ fun AppTheme(content: @Composable () -> Unit) {
MaterialTheme(colors = colors) { MaterialTheme(colors = colors) {
CompositionLocalProvider( CompositionLocalProvider(
LocalScrollbarStyle provides ScrollbarStyle( LocalScrollbarStyle provides getScrollbarStyle(),
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)
),
content = content content = content
) )
} }

View File

@@ -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
}

View File

@@ -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

View File

@@ -6,7 +6,6 @@
package ca.gosyer.ui.updates.components package ca.gosyer.ui.updates.components
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio 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.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -34,6 +32,8 @@ import ca.gosyer.data.models.Chapter
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.chapter.ChapterDownloadIcon import ca.gosyer.ui.base.chapter.ChapterDownloadIcon
import ca.gosyer.ui.base.chapter.ChapterDownloadItem 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.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.components.MangaListItem import ca.gosyer.uicore.components.MangaListItem

View File

@@ -13,7 +13,8 @@ include("desktop")
include("core") include("core")
include("i18n") include("i18n")
include("data") include("data")
enableFeaturePreview("VERSION_CATALOGS")
include("ui-core") include("ui-core")
include("presentation") include("presentation")
enableFeaturePreview("VERSION_CATALOGS")
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

View File

@@ -38,8 +38,8 @@ kotlin {
api(libs.coroutinesCore) api(libs.coroutinesCore)
api(libs.kamel) api(libs.kamel)
api(libs.voyagerCore) api(libs.voyagerCore)
api(project(":core")) api(projects.core)
api(project(":i18n")) api(projects.i18n)
api(compose.desktop.currentOs) api(compose.desktop.currentOs)
api(compose.materialIconsExtended) api(compose.materialIconsExtended)
} }