diff --git a/app/desktop/src/desktopMain/kotlin/com/velord/composescreenexample/desktop/Main.kt b/app/desktop/src/desktopMain/kotlin/com/velord/composescreenexample/desktop/Main.kt index 4aa8dbcb..a076a444 100644 --- a/app/desktop/src/desktopMain/kotlin/com/velord/composescreenexample/desktop/Main.kt +++ b/app/desktop/src/desktopMain/kotlin/com/velord/composescreenexample/desktop/Main.kt @@ -14,6 +14,7 @@ import com.velord.core.ui.compose.component.DesktopBackDispatcher import com.velord.core.ui.compose.component.LocalDesktopBackDispatcher import com.velord.core.ui.compose.component.ToastHost import com.velord.core.ui.theme.AppThemeHost +import com.velord.core.ui.theme.LocalizationHost import com.velord.core.ui.util.ObserveSharedFlow import com.velord.infrastructure.di.createCommonAppModuleRoster import com.velord.infrastructure.navigation.NavigationHost @@ -58,15 +59,17 @@ fun main() = application { } CompositionLocalProvider(LocalDesktopBackDispatcher provides dispatcher) { - AppThemeHost { - SplashScreen(viewModel = splashVM) { - ToastHost( - toastEventFlow = mainVM.toastConfigFlow, - modifier = Modifier.fillMaxSize(), - content = { - NavigationHost(navigationLib = mainVM.navigationLib) - }, - ) + LocalizationHost { + AppThemeHost { + SplashScreen(viewModel = splashVM) { + ToastHost( + toastEventFlow = mainVM.toastConfigFlow, + modifier = Modifier.fillMaxSize(), + content = { + NavigationHost(navigationLib = mainVM.navigationLib) + }, + ) + } } } } diff --git a/core/core-resource/src/commonMain/composeResources/files/localization.json b/core/core-resource/src/commonMain/composeResources/files/localization.json index 9de1b16a..7ec35e3d 100644 --- a/core/core-resource/src/commonMain/composeResources/files/localization.json +++ b/core/core-resource/src/commonMain/composeResources/files/localization.json @@ -35,6 +35,7 @@ "use_system_dynamic_theme": "Use System Dynamic Theme", "not_available_on_android_11": "Not available on Android 11 and below", "disable_os_theme_switcher_first": "Disable Os theme switcher first", + "disable_dynamic_theme_first": "Disable system dynamic theme first", "error": "Error", "loading": "Loading", "count": "Count: %1$d", @@ -94,7 +95,15 @@ "show_one_button_dialog": "Show one button dialog", "language": "Language", "language_default": "Default", - "language_name": "English" + "language_name": "English", + "theme_light": "Light", + "theme_dark": "Dark", + "theme_negative_light": "Negative Light", + "theme_ocean": "Ocean", + "shape_rounded": "Rounded", + "shape_cut": "Cut", + "shape_square": "Square", + "shape_style_title": "Component Shapes" }, "es": { "app_name": "ComposeScreenExample", @@ -129,6 +138,7 @@ "use_system_dynamic_theme": "Usar tema dinámico del sistema", "not_available_on_android_11": "No disponible en Android 11 y versiones anteriores", "disable_os_theme_switcher_first": "Desactiva primero el selector de tema del SO", + "disable_dynamic_theme_first": "Desactiva primero el tema dinámico del sistema", "error": "Error", "loading": "Cargando", "count": "Cantidad: %1$d", @@ -188,7 +198,15 @@ "show_one_button_dialog": "Mostrar diálogo con un botón", "language": "Idioma", "language_default": "Predeterminado", - "language_name": "Español" + "language_name": "Español", + "theme_light": "Claro", + "theme_dark": "Oscuro", + "theme_negative_light": "Luz Negativa", + "theme_ocean": "Océano", + "shape_rounded": "Redondeado", + "shape_cut": "Cortado", + "shape_square": "Cuadrado", + "shape_style_title": "Formas de los componentes" } } } diff --git a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/util/AndroidSetContent.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/util/AndroidSetContent.kt index 94dab512..003849cc 100644 --- a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/util/AndroidSetContent.kt +++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/util/AndroidSetContent.kt @@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import com.velord.core.ui.compose.component.ToastHost import com.velord.core.ui.theme.AppThemeHost +import com.velord.core.ui.theme.LocalizationHost import com.velord.model.ToastConfig import kotlinx.coroutines.flow.Flow @@ -42,8 +43,10 @@ private fun ComposeView.setThemedContent( setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - AppThemeHost { - this@setThemedContent.content() + LocalizationHost { + AppThemeHost { + this@setThemedContent.content() + } } } } diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/Shape.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/Shape.kt index 27907f34..5b23aed7 100644 --- a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/Shape.kt +++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/Shape.kt @@ -1,13 +1,30 @@ package com.velord.core.ui.compose.shape +import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp -val MainShapes = Shapes( +val RoundedShapes = Shapes( extraSmall = RoundedCornerShape(4.dp), small = RoundedCornerShape(8.dp), medium = RoundedCornerShape(12.dp), large = RoundedCornerShape(16.dp), extraLarge = RoundedCornerShape(24.dp) ) + +val CutShapes = Shapes( + extraSmall = CutCornerShape(4.dp), + small = CutCornerShape(8.dp), + medium = CutCornerShape(12.dp), + large = CutCornerShape(16.dp), + extraLarge = CutCornerShape(24.dp) +) + +val SquareShapes = Shapes( + extraSmall = RoundedCornerShape(0.dp), + small = RoundedCornerShape(0.dp), + medium = RoundedCornerShape(0.dp), + large = RoundedCornerShape(0.dp), + extraLarge = RoundedCornerShape(0.dp) +) diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/MainTheme.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/MainTheme.kt index 4e26a2bd..7692fdad 100644 --- a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/MainTheme.kt +++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/MainTheme.kt @@ -3,16 +3,36 @@ package com.velord.core.ui.compose.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes import androidx.compose.runtime.Composable -import com.velord.core.ui.compose.shape.MainShapes +import com.velord.core.ui.compose.shape.CutShapes +import com.velord.core.ui.compose.shape.RoundedShapes +import com.velord.core.ui.compose.shape.SquareShapes import com.velord.core.ui.compose.theme.color.DarkColorScheme import com.velord.core.ui.compose.theme.color.LightColorScheme +import com.velord.core.ui.compose.theme.color.NegativeLightColorScheme +import com.velord.core.ui.compose.theme.color.OceanColorScheme +import com.velord.model.setting.AppShapeStyle import com.velord.model.setting.AppThemeConfig import com.velord.model.setting.SpecialTheme private fun SpecialTheme.toColorScheme(): ColorScheme = when (this) { SpecialTheme.DARK -> DarkColorScheme SpecialTheme.LIGHT -> LightColorScheme + SpecialTheme.NEGATIVE_LIGHT -> NegativeLightColorScheme + SpecialTheme.OCEAN -> OceanColorScheme +} + +private fun getDefaultDarkScheme() = SpecialTheme.DEFAULT_DARK_SCHEME.toColorScheme() +private fun getDefaultLightScheme() = SpecialTheme.DEFAULT_LIGHT_SCHEME.toColorScheme() +private fun defineDefaultColorScheme( + useDarkTheme: Boolean +): ColorScheme = if (useDarkTheme) getDefaultDarkScheme() else getDefaultLightScheme() + +private fun AppShapeStyle.toShapes(): Shapes = when(this) { + AppShapeStyle.ROUNDED -> RoundedShapes + AppShapeStyle.CUT -> CutShapes + AppShapeStyle.SQUARE -> SquareShapes } @Composable @@ -21,17 +41,20 @@ fun MainTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = true, specialTheme: SpecialTheme, + shapeStyle: AppShapeStyle = AppShapeStyle.ROUNDED, content: @Composable () -> Unit, ) { + val isDark = specialTheme.mode.isDark(useDarkTheme) + val dynamicColorScheme = platformDynamicColorScheme( - useDarkTheme = useDarkTheme, + useDarkTheme = isDark, dynamicColor = dynamicColor, apiAvailable = AppThemeConfig.DEFAULT.isSystemDynamicColorAvailable, ) val colorScheme: ColorScheme = when { abideToOsTheme.not() && dynamicColorScheme != null -> dynamicColorScheme - abideToOsTheme -> if (useDarkTheme) DarkColorScheme else LightColorScheme + abideToOsTheme -> defineDefaultColorScheme(isDark) else -> specialTheme.toColorScheme() } @@ -43,7 +66,7 @@ fun MainTheme( MaterialTheme( colorScheme = colorScheme, - shapes = MainShapes, + shapes = shapeStyle.toShapes(), typography = MainTypography, content = content, ) diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/color/ColorScheme.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/color/ColorScheme.kt index 7286ec1b..2a3f5f5e 100644 --- a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/color/ColorScheme.kt +++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/color/ColorScheme.kt @@ -2,6 +2,8 @@ package com.velord.core.ui.compose.theme.color import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color +import com.velord.core.ui.util.color.inverseColor val DarkColorScheme = darkColorScheme( primary = ColorDarkTokens.Primary, @@ -80,3 +82,82 @@ val LightColorScheme = lightColorScheme( surfaceContainerLowest = ColorLightTokens.SurfaceContainerLowest, surfaceDim = ColorLightTokens.SurfaceDim, ) + +val NegativeLightColorScheme = darkColorScheme( + primary = ColorLightTokens.Primary.inverseColor(), + onPrimary = ColorLightTokens.OnPrimary.inverseColor(), + primaryContainer = ColorLightTokens.PrimaryContainer.inverseColor(), + onPrimaryContainer = ColorLightTokens.OnPrimaryContainer.inverseColor(), + inversePrimary = ColorLightTokens.InversePrimary.inverseColor(), + secondary = ColorLightTokens.Secondary.inverseColor(), + onSecondary = ColorLightTokens.OnSecondary.inverseColor(), + secondaryContainer = ColorLightTokens.SecondaryContainer.inverseColor(), + onSecondaryContainer = ColorLightTokens.OnSecondaryContainer.inverseColor(), + tertiary = ColorLightTokens.Tertiary.inverseColor(), + onTertiary = ColorLightTokens.OnTertiary.inverseColor(), + tertiaryContainer = ColorLightTokens.TertiaryContainer.inverseColor(), + onTertiaryContainer = ColorLightTokens.OnTertiaryContainer.inverseColor(), + background = ColorLightTokens.Background.inverseColor(), + onBackground = ColorLightTokens.OnBackground.inverseColor(), + surface = ColorLightTokens.Surface.inverseColor(), + onSurface = ColorLightTokens.OnSurface.inverseColor(), + surfaceVariant = ColorLightTokens.SurfaceVariant.inverseColor(), + onSurfaceVariant = ColorLightTokens.OnSurfaceVariant.inverseColor(), + surfaceTint = ColorLightTokens.SurfaceTint.inverseColor(), + inverseSurface = ColorLightTokens.InverseSurface.inverseColor(), + inverseOnSurface = ColorLightTokens.InverseOnSurface.inverseColor(), + error = ColorLightTokens.Error.inverseColor(), + onError = ColorLightTokens.OnError.inverseColor(), + errorContainer = ColorLightTokens.ErrorContainer.inverseColor(), + onErrorContainer = ColorLightTokens.OnErrorContainer.inverseColor(), + outline = ColorLightTokens.Outline.inverseColor(), + outlineVariant = ColorLightTokens.OutlineVariant.inverseColor(), + scrim = ColorLightTokens.Scrim.inverseColor(), + surfaceBright = ColorLightTokens.SurfaceBright.inverseColor(), + surfaceContainer = ColorLightTokens.SurfaceContainer.inverseColor(), + surfaceContainerHigh = ColorLightTokens.SurfaceContainerHigh.inverseColor(), + surfaceContainerHighest = ColorLightTokens.SurfaceContainerHighest.inverseColor(), + surfaceContainerLow = ColorLightTokens.SurfaceContainerLow.inverseColor(), + surfaceContainerLowest = ColorLightTokens.SurfaceContainerLowest.inverseColor(), + surfaceDim = ColorLightTokens.SurfaceDim.inverseColor(), +) + +val OceanColorScheme = darkColorScheme( + primary = Color(0xFF4DD0E1), + onPrimary = Color(0xFF00363A), + primaryContainer = Color(0xFF004D40), + onPrimaryContainer = Color(0xFF80CBC4), + inversePrimary = Color(0xFF00838F), + secondary = Color(0xFF81C784), + onSecondary = Color(0xFF1B5E20), + secondaryContainer = Color(0xFF2E7D32), + onSecondaryContainer = Color(0xFFA5D6A7), + tertiary = Color(0xFF64B5F6), + onTertiary = Color(0xFF0D47A1), + tertiaryContainer = Color(0xFF1565C0), + onTertiaryContainer = Color(0xFFE3F2FD), + background = Color(0xFF001F24), + onBackground = Color(0xFFE0F7FA), + surface = Color(0xFF001F24), + onSurface = Color(0xFFE0F7FA), + surfaceVariant = Color(0xFF00363A), + onSurfaceVariant = Color(0xFF80CBC4), + surfaceTint = Color(0xFF4DD0E1), + inverseSurface = Color(0xFFE0F7FA), + inverseOnSurface = Color(0xFF001F24), + error = Color(0xFFE57373), + onError = Color(0xFFB71C1C), + errorContainer = Color(0xFFD32F2F), + onErrorContainer = Color(0xFFFFEBEE), + outline = Color(0xFF4DD0E1), + outlineVariant = Color(0xFF006064), + scrim = Color(0xFF000000), + surfaceBright = Color(0xFF00363A), + surfaceContainer = Color(0xFF001518), + surfaceContainerHigh = Color(0xFF001A1E), + surfaceContainerHighest = Color(0xFF001F24), + surfaceContainerLow = Color(0xFF000B0D), + surfaceContainerLowest = Color(0xFF000507), + surfaceDim = Color(0xFF001518) +) + diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/AppThemeHost.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/AppThemeHost.kt index 748eae16..84cac1cb 100644 --- a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/AppThemeHost.kt +++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/AppThemeHost.kt @@ -5,11 +5,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.State import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.velord.core.resource.LocalLocalizationState import com.velord.core.ui.compose.theme.MainTheme import com.velord.core.ui.util.LocalTheme import com.velord.model.setting.AppThemeConfig -import com.velord.ui.sharedviewmodel.LanguageVM import com.velord.ui.sharedviewmodel.ThemeUiState import com.velord.ui.sharedviewmodel.ThemeVM import org.koin.compose.viewmodel.koinViewModel @@ -17,20 +15,12 @@ import org.koin.compose.viewmodel.koinViewModel @Composable fun AppThemeHost( themeVM: ThemeVM = koinViewModel(), - languageVM: LanguageVM = koinViewModel(), content: @Composable () -> Unit, ) { val themeState: State = themeVM.uiStateFlow.collectAsStateWithLifecycle() val theme = themeState.value?.appThemeConfig ?: AppThemeConfig.DEFAULT - val localizationState = languageVM.uiStateFlow - .collectAsStateWithLifecycle() - .value - .localization - CompositionLocalProvider( - LocalTheme provides theme, - LocalLocalizationState provides localizationState, - ) { + CompositionLocalProvider(LocalTheme provides theme) { val localThemeConfig = LocalTheme.current val isDark = if (localThemeConfig.config.abideToOs) { @@ -44,6 +34,7 @@ fun AppThemeHost( useDarkTheme = isDark, dynamicColor = localThemeConfig.config.useDynamicColor, specialTheme = localThemeConfig.config.current, + shapeStyle = localThemeConfig.config.shapeStyle, content = content, ) } diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/LocalizationHost.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/LocalizationHost.kt new file mode 100644 index 00000000..bb0e39d0 --- /dev/null +++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/LocalizationHost.kt @@ -0,0 +1,23 @@ +package com.velord.core.ui.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.velord.core.resource.LocalLocalizationState +import com.velord.ui.sharedviewmodel.LanguageVM +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun LocalizationHost( + languageVM: LanguageVM = koinViewModel(), + content: @Composable () -> Unit, +) { + val localizationState = languageVM.uiStateFlow + .collectAsStateWithLifecycle() + .value + .localization + + CompositionLocalProvider(LocalLocalizationState provides localizationState) { + content() + } +} \ No newline at end of file diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/SwitchThemeConfigGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/SwitchThemeConfigGateway.kt index 1631a266..213aa1f2 100644 --- a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/SwitchThemeConfigGateway.kt +++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/SwitchThemeConfigGateway.kt @@ -1,5 +1,7 @@ package com.velord.data.gateway.setting +import com.velord.model.setting.AppShapeStyle +import com.velord.model.setting.SpecialTheme import com.velord.model.setting.ThemeConfig import org.koin.core.annotation.Single @@ -16,11 +18,25 @@ class SwitchThemeConfigGateway(private val getThemeConfigGateway: GetThemeConfig getThemeConfigGateway.save(newConfig) } + suspend fun switchSpecialTheme(config: ThemeConfig, newTheme: SpecialTheme) { + val newConfig = config.copy( + useDarkTheme = newTheme.mode.isDark(config.useDarkTheme), + current = newTheme, + ) + getThemeConfigGateway.save(newConfig) + } + suspend fun switchDarkTheme(config: ThemeConfig) { + val newUseDark = config.useDarkTheme.not() val newConfig = config.copy( - useDarkTheme = config.useDarkTheme.not(), - current = ThemeConfig.getOppositeTheme(config.current), + useDarkTheme = newUseDark, + current = config.current.getOppositeDefaultTheme(newUseDark), ) getThemeConfigGateway.save(newConfig) } + + suspend fun switchShapeStyle(config: ThemeConfig, newStyle: AppShapeStyle) { + val newConfig = config.copy(shapeStyle = newStyle) + getThemeConfigGateway.save(newConfig) + } } diff --git a/data/os/src/androidMain/kotlin/com/velord/data/os/DI.kt b/data/os/src/androidMain/kotlin/com/velord/data/os/DI.kt index a2ce9c76..61063e5b 100644 --- a/data/os/src/androidMain/kotlin/com/velord/data/os/DI.kt +++ b/data/os/src/androidMain/kotlin/com/velord/data/os/DI.kt @@ -7,6 +7,10 @@ import com.velord.data.os.camera.CameraControllerFactory import com.velord.data.os.file.AndroidFileDataSource import com.velord.data.os.file.FileDataSource import com.velord.data.os.memory.AndroidMemoryLogger +import com.velord.data.os.memory.MemoryDumpProvider +import com.velord.data.os.memory.MemoryDumpProviderImpl +import com.velord.data.os.memory.MemoryLeakMonitor +import com.velord.data.os.memory.MemoryLeakMonitorImpl import com.velord.data.os.memory.MemoryLogger import com.velord.data.os.share.AndroidShareDataSource import com.velord.data.os.share.ShareDataSource @@ -16,13 +20,16 @@ import org.koin.core.scope.Scope @Module actual class CameraPlatformModule { + @Single - actual fun provideCameraControllerFactory(scope: Scope): CameraControllerFactory = - AndroidCameraControllerFactory(scope.get()) + actual fun provideCameraControllerFactory( + scope: Scope + ): CameraControllerFactory = AndroidCameraControllerFactory(scope.get()) } @Module actual class FilePlatformModule { + @Single actual fun provideFileDataSource( scope: Scope, @@ -31,12 +38,29 @@ actual class FilePlatformModule { @Module actual class MemoryLoggerPlatformModule { + + @Single + internal actual fun provideMemoryLogger( + scope: Scope + ): MemoryLogger = AndroidMemoryLogger(scope.get()) + @Single - actual fun provideMemoryLogger(scope: Scope): MemoryLogger = AndroidMemoryLogger(scope.get()) + internal actual fun provideMemoryDumpProvider( + scope: Scope, + memoryLogger: MemoryLogger + ): MemoryDumpProvider = MemoryDumpProviderImpl(memoryLogger) + + @Single + internal actual fun provideMemoryLeakMonitor( + scope: Scope, + memoryLogger: MemoryLogger, + memoryDumpProvider: MemoryDumpProvider + ): MemoryLeakMonitor = MemoryLeakMonitorImpl(memoryLogger, memoryDumpProvider) } @Module actual class SharePlatformModule { + @Single actual fun provideShareDataSource(scope: Scope): ShareDataSource = AndroidShareDataSource(scope.get()) diff --git a/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt b/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt index fdadf49a..7036abda 100644 --- a/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt +++ b/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt @@ -7,14 +7,12 @@ import java.io.BufferedReader import java.io.FileReader import kotlin.math.roundToLong -internal actual class MemoryDumpProvider actual constructor( - private val logger: MemoryLogger, -) { +internal class MemoryDumpProviderImpl(private val logger: MemoryLogger): MemoryDumpProvider { private val memoryInfo = Debug.MemoryInfo() private val pageSize by lazy { Os.sysconf(OsConstants._SC_PAGESIZE) } - actual fun takeDump(reason: DumpReason, trimLevel: Int?): MemoryDump { + override fun takeDump(reason: DumpReason, trimLevel: Int?): MemoryDump { Debug.getMemoryInfo(memoryInfo) val (rss, shared) = getRssAndShared() diff --git a/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt b/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt index e57b08a8..3a5fb427 100644 --- a/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt +++ b/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt @@ -10,17 +10,15 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import org.koin.core.annotation.Single import org.koin.core.context.GlobalContext import kotlin.time.Duration.Companion.minutes -@Single -actual class MemoryLeakMonitor actual constructor( +internal class MemoryLeakMonitorImpl( private val logger: MemoryLogger, -) : ComponentCallbacks2 { + private val memoryDumpProvider: MemoryDumpProvider, +) : MemoryLeakMonitor, ComponentCallbacks2 { private val context: Context = GlobalContext.get().get().applicationContext - private val memoryDumpProvider = MemoryDumpProvider(logger) private val exceptionHandler = CoroutineExceptionHandler { _, exception -> logger.log("MemoryLeakMonitor coroutine error: ${exception.message}") } @@ -28,7 +26,7 @@ actual class MemoryLeakMonitor actual constructor( SupervisorJob() + Dispatchers.Default + exceptionHandler, ) - actual fun start() { + override fun start() { context.registerComponentCallbacks(this) setupIntervalLogging() diff --git a/data/os/src/commonMain/kotlin/com/velord/data/os/DI.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/DI.kt index 76d5dcff..5b2946e3 100644 --- a/data/os/src/commonMain/kotlin/com/velord/data/os/DI.kt +++ b/data/os/src/commonMain/kotlin/com/velord/data/os/DI.kt @@ -4,6 +4,8 @@ package com.velord.data.os import com.velord.data.os.camera.CameraControllerFactory import com.velord.data.os.file.FileDataSource +import com.velord.data.os.memory.MemoryDumpProvider +import com.velord.data.os.memory.MemoryLeakMonitor import com.velord.data.os.memory.MemoryLogger import com.velord.data.os.share.ShareDataSource import org.koin.core.annotation.ComponentScan @@ -24,24 +26,41 @@ class OsModule @Module expect class CameraPlatformModule() { + @Single fun provideCameraControllerFactory(scope: Scope): CameraControllerFactory } @Module expect class FilePlatformModule() { + @Single fun provideFileDataSource(scope: Scope): FileDataSource } @Module expect class MemoryLoggerPlatformModule() { + + @Single + internal fun provideMemoryLogger(scope: Scope): MemoryLogger + + @Single + internal fun provideMemoryDumpProvider( + scope: Scope, + memoryLogger: MemoryLogger + ): MemoryDumpProvider + @Single - fun provideMemoryLogger(scope: Scope): MemoryLogger + internal fun provideMemoryLeakMonitor( + scope: Scope, + memoryLogger: MemoryLogger, + memoryDumpProvider: MemoryDumpProvider, + ): MemoryLeakMonitor } @Module expect class SharePlatformModule() { + @Single fun provideShareDataSource(scope: Scope): ShareDataSource } diff --git a/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt index 77b7fa87..f3156002 100644 --- a/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt +++ b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt @@ -1,5 +1,5 @@ package com.velord.data.os.memory -internal expect class MemoryDumpProvider(logger: MemoryLogger) { +internal interface MemoryDumpProvider { fun takeDump(reason: DumpReason, trimLevel: Int? = null): MemoryDump } diff --git a/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt index 99486c04..001a1bfa 100644 --- a/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt +++ b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt @@ -1,8 +1,5 @@ package com.velord.data.os.memory -import org.koin.core.annotation.Single - -@Single -expect class MemoryLeakMonitor(logger: MemoryLogger) { +interface MemoryLeakMonitor { fun start() } diff --git a/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLogger.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLogger.kt index 51b51016..246c60d0 100644 --- a/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLogger.kt +++ b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLogger.kt @@ -1,6 +1,6 @@ package com.velord.data.os.memory -interface MemoryLogger { +internal interface MemoryLogger { fun log(message: String, isError: Boolean = false) fun recordException(throwable: Throwable) fun setCustomKey(key: String, value: Int) diff --git a/data/os/src/desktopMain/kotlin/com/velord/data/os/DI.kt b/data/os/src/desktopMain/kotlin/com/velord/data/os/DI.kt index 0f81ab73..84d34367 100644 --- a/data/os/src/desktopMain/kotlin/com/velord/data/os/DI.kt +++ b/data/os/src/desktopMain/kotlin/com/velord/data/os/DI.kt @@ -7,6 +7,10 @@ import com.velord.data.os.camera.DesktopCameraControllerFactory import com.velord.data.os.file.DesktopFileDataSource import com.velord.data.os.file.FileDataSource import com.velord.data.os.memory.DesktopMemoryLogger +import com.velord.data.os.memory.MemoryDumpProvider +import com.velord.data.os.memory.MemoryDumpProviderImpl +import com.velord.data.os.memory.MemoryLeakMonitor +import com.velord.data.os.memory.MemoryLeakMonitorImpl import com.velord.data.os.memory.MemoryLogger import com.velord.data.os.share.DesktopShareDataSource import com.velord.data.os.share.ShareDataSource @@ -16,6 +20,7 @@ import org.koin.core.scope.Scope @Module actual class CameraPlatformModule { + @Single actual fun provideCameraControllerFactory( scope: Scope @@ -24,18 +29,36 @@ actual class CameraPlatformModule { @Module actual class FilePlatformModule { + @Single actual fun provideFileDataSource(scope: Scope): FileDataSource = DesktopFileDataSource() } @Module actual class MemoryLoggerPlatformModule { + @Single - actual fun provideMemoryLogger(scope: Scope): MemoryLogger = DesktopMemoryLogger(scope.get()) + internal actual fun provideMemoryLogger( + scope: Scope + ): MemoryLogger = DesktopMemoryLogger(scope.get()) + + @Single + internal actual fun provideMemoryDumpProvider( + scope: Scope, + memoryLogger: MemoryLogger + ): MemoryDumpProvider = MemoryDumpProviderImpl(memoryLogger) + + @Single + internal actual fun provideMemoryLeakMonitor( + scope: Scope, + memoryLogger: MemoryLogger, + memoryDumpProvider: MemoryDumpProvider + ): MemoryLeakMonitor = MemoryLeakMonitorImpl(memoryLogger) } @Module actual class SharePlatformModule { + @Single actual fun provideShareDataSource(scope: Scope): ShareDataSource = DesktopShareDataSource() } diff --git a/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt b/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt index cc26d805..5e6d4d8b 100644 --- a/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt +++ b/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt @@ -1,9 +1,8 @@ package com.velord.data.os.memory -internal actual class MemoryDumpProvider actual constructor( - private val logger: MemoryLogger, -) { - actual fun takeDump(reason: DumpReason, trimLevel: Int?): MemoryDump { +internal class MemoryDumpProviderImpl(private val logger: MemoryLogger): MemoryDumpProvider { + + override fun takeDump(reason: DumpReason, trimLevel: Int?): MemoryDump { logger.log("Memory dump is not implemented for desktop target.") return MemoryDump( reason = reason, diff --git a/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt b/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt index 2383e44e..be81379c 100644 --- a/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt +++ b/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt @@ -1,11 +1,8 @@ package com.velord.data.os.memory -import org.koin.core.annotation.Single +internal class MemoryLeakMonitorImpl(private val logger: MemoryLogger): MemoryLeakMonitor { -@Single -actual class MemoryLeakMonitor actual constructor(private val logger: MemoryLogger) { - - actual fun start() { + override fun start() { logger.log("Memory leak monitor is not implemented for desktop target.") } } diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt index e675d232..17954e49 100644 --- a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt +++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt @@ -2,10 +2,4 @@ package com.velord.usecase.setting import com.velord.model.setting.ThemeConfig -/** - * Toggles dark-theme usage on the supplied [ThemeConfig] and persists the updated state. - * - * Implementations are also expected to update the concrete current theme so it stays consistent - * with the new dark/light choice. - */ fun interface SwitchDarkThemeConfigUC : suspend (ThemeConfig) -> Unit diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchShapeStyleThemeConfigUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchShapeStyleThemeConfigUC.kt new file mode 100644 index 00000000..961bd92e --- /dev/null +++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchShapeStyleThemeConfigUC.kt @@ -0,0 +1,6 @@ +package com.velord.usecase.setting + +import com.velord.model.setting.AppShapeStyle +import com.velord.model.setting.ThemeConfig + +fun interface SwitchShapeStyleThemeConfigUC : suspend (ThemeConfig, AppShapeStyle) -> Unit diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchSpecialThemeConfigUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchSpecialThemeConfigUC.kt new file mode 100644 index 00000000..34cab14f --- /dev/null +++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchSpecialThemeConfigUC.kt @@ -0,0 +1,9 @@ +package com.velord.usecase.setting + +import com.velord.model.setting.SpecialTheme +import com.velord.model.setting.ThemeConfig + +/** + * Updates the theme configuration with a specific [SpecialTheme] and persists the updated state. + */ +fun interface SwitchSpecialThemeConfigUC : suspend (ThemeConfig, SpecialTheme) -> Unit diff --git a/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/UseCaseModule.kt b/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/UseCaseModule.kt index 1b66d76d..260bad51 100644 --- a/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/UseCaseModule.kt +++ b/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/UseCaseModule.kt @@ -47,6 +47,8 @@ import com.velord.usecase.setting.SetLanguagePreferenceUC import com.velord.usecase.setting.SwitchAbideToOsThemeConfigUC import com.velord.usecase.setting.SwitchDarkThemeConfigUC import com.velord.usecase.setting.SwitchDynamicColorThemeConfigUC +import com.velord.usecase.setting.SwitchShapeStyleThemeConfigUC +import com.velord.usecase.setting.SwitchSpecialThemeConfigUC import org.koin.dsl.module val useCaseModule = module { @@ -68,6 +70,12 @@ val useCaseModule = module { single { SwitchDarkThemeConfigUC(get()::switchDarkTheme) } + single { + SwitchSpecialThemeConfigUC(get()::switchSpecialTheme) + } + single { + SwitchShapeStyleThemeConfigUC(get()::switchShapeStyle) + } single { SwitchAbideToOsThemeConfigUC(get()::switchAbideToOs) } @@ -150,3 +158,4 @@ val useCaseModule = module { OpenCameraVideoFolderUC(get()::openDirectory) } } + diff --git a/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/ViewModelModule.kt b/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/ViewModelModule.kt index b1caab36..7f2a98ff 100644 --- a/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/ViewModelModule.kt +++ b/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/ViewModelModule.kt @@ -17,7 +17,7 @@ import org.koin.dsl.module val viewModelModule = module { // VieModel that can not be \or have not to be instantiated here has their own module DI - viewModel { ThemeVM(get(), get(), get(), get()) } + viewModel { ThemeVM(get(), get(), get(), get(), get(), get()) } viewModel { LanguageVM(get(), get()) } viewModel { SplashVM() } viewModel { DemoVM(get(), get()) } diff --git a/model/src/commonMain/kotlin/com/velord/model/setting/SpecialTheme.kt b/model/src/commonMain/kotlin/com/velord/model/setting/SpecialTheme.kt new file mode 100644 index 00000000..6b1e1cc0 --- /dev/null +++ b/model/src/commonMain/kotlin/com/velord/model/setting/SpecialTheme.kt @@ -0,0 +1,40 @@ +package com.velord.model.setting + +enum class ThemeMode { + LIGHT, DARK, BOTH; + + fun isDark(currentUseDarkTheme: Boolean): Boolean = when(this) { + DARK -> true + LIGHT -> false + BOTH -> currentUseDarkTheme + } +} + +enum class SpecialTheme( + val mode: ThemeMode, + val isDefault: Boolean = false +) { + LIGHT(mode = ThemeMode.LIGHT, isDefault = true), + DARK(mode = ThemeMode.DARK, isDefault = true), + NEGATIVE_LIGHT(mode = ThemeMode.DARK), + OCEAN(mode = ThemeMode.BOTH); + + fun isEnabled(useDynamicColor: Boolean): Boolean = (useDynamicColor && isDefault.not()).not() + + fun getOppositeDefaultTheme(newUseDark: Boolean): SpecialTheme { + val newMode = if (newUseDark) ThemeMode.DARK else ThemeMode.LIGHT + if (this.mode == newMode || this.mode == ThemeMode.BOTH) return this + + return if (newUseDark) DEFAULT_DARK_SCHEME else DEFAULT_LIGHT_SCHEME + } + + companion object { + val DEFAULT_DARK_SCHEME: SpecialTheme = DARK + val DEFAULT_LIGHT_SCHEME: SpecialTheme = LIGHT + + fun getAvailableThemeRoster(useDarkTheme: Boolean): List { + val targetMode = if (useDarkTheme) ThemeMode.DARK else ThemeMode.LIGHT + return entries.filter { it.mode == targetMode || it.mode == ThemeMode.BOTH } + } + } +} \ No newline at end of file diff --git a/model/src/commonMain/kotlin/com/velord/model/setting/ThemeConfig.kt b/model/src/commonMain/kotlin/com/velord/model/setting/ThemeConfig.kt index 08e3278e..7c7029a1 100644 --- a/model/src/commonMain/kotlin/com/velord/model/setting/ThemeConfig.kt +++ b/model/src/commonMain/kotlin/com/velord/model/setting/ThemeConfig.kt @@ -2,12 +2,8 @@ package com.velord.model.setting import kotlinx.serialization.Serializable -enum class SpecialTheme( - val isDark: Boolean, - val default: Boolean -) { - LIGHT(false, true), - DARK(true, true); +enum class AppShapeStyle { + ROUNDED, CUT, SQUARE } @Serializable @@ -15,21 +11,18 @@ data class ThemeConfig( val abideToOs: Boolean, val useDarkTheme: Boolean, val useDynamicColor: Boolean, - val current: SpecialTheme + val current: SpecialTheme, + val shapeStyle: AppShapeStyle, ) { companion object { val DEFAULT = ThemeConfig( abideToOs = true, useDarkTheme = false, useDynamicColor = false, - current = SpecialTheme.LIGHT + current = SpecialTheme.LIGHT, + shapeStyle = AppShapeStyle.ROUNDED ) - fun getOppositeTheme(theme: SpecialTheme): SpecialTheme = when (theme) { - SpecialTheme.LIGHT -> SpecialTheme.DARK - SpecialTheme.DARK -> SpecialTheme.LIGHT - } - fun invoke( useDarkTheme: Boolean, useDynamicColor: Boolean @@ -39,12 +32,17 @@ data class ThemeConfig( abideToOs = true, useDarkTheme = useDarkTheme, useDynamicColor = useDynamicColor, - current = theme + current = theme, + shapeStyle = AppShapeStyle.ROUNDED ) } - private fun findDefaultTheme(isDark: Boolean): SpecialTheme = SpecialTheme.entries - .find { it.isDark == isDark && it.default } - ?: error("No default theme found") + private fun findDefaultTheme(isDark: Boolean): SpecialTheme { + val targetMode = if (isDark) ThemeMode.DARK else ThemeMode.LIGHT + return SpecialTheme.entries + .filter { it.isDefault } + .find { it.mode == targetMode || it.mode == ThemeMode.BOTH } + ?: error("No default theme found") + } } } diff --git a/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/SettingScreen.kt b/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/SettingScreen.kt index f710f76b..b9dc4ce3 100644 --- a/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/SettingScreen.kt +++ b/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/SettingScreen.kt @@ -1,40 +1,24 @@ package com.velord.ui.feature.setting -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.velord.core.resource.AppString import com.velord.core.resource.stringResource import com.velord.core.ui.compose.component.PlatformScreenHeader import com.velord.core.ui.compose.preview.PreviewCombined import com.velord.core.ui.compose.preview.previewLocalizationState -import com.velord.core.ui.util.LocalTheme import com.velord.model.setting.LanguagePreference +import com.velord.ui.feature.setting.component.LanguageSetting +import com.velord.ui.feature.setting.component.ThemeSetting import com.velord.ui.sharedviewmodel.LanguageUiAction import com.velord.ui.sharedviewmodel.LanguageUiState import com.velord.ui.sharedviewmodel.LanguageVM @@ -82,206 +66,14 @@ internal fun Content( onBackClick = onBackClick, ) - ThemeSettings(onThemeAction) - LanguageSettings( - languageUiState = languageUiState, - onLanguageAction = onLanguageAction, + ThemeSetting(onThemeAction) + LanguageSetting( + uiState = languageUiState, + onAction = onLanguageAction, ) } } } - -@Composable -private fun ThemeSettings(onThemeAction: (ThemeUiAction) -> Unit) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - .padding(horizontal = 8.dp) - .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.primaryContainer) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - AbideToOsSwitcher(onThemeAction) - DynamicThemeSwitcher(onThemeAction) - DarkThemeSwitcher(onThemeAction) - } -} - -@Composable -private fun LanguageSettings( - languageUiState: LanguageUiState, - onLanguageAction: (LanguageUiAction) -> Unit, -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - .padding(horizontal = 8.dp) - .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.primaryContainer) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(AppString.language), - style = MaterialTheme.typography.titleMedium, - ) - - LanguageOption( - title = stringResource(AppString.language_default), - isSelected = languageUiState.preference.isDefault, - onClick = { - onLanguageAction(LanguageUiAction.Select(LanguagePreference.DEFAULT)) - }, - ) - - LanguagePreference.entries - .filterNot { it.isDefault } - .filter { it.languageCode in languageUiState.localization.document.languageRoster } - .forEach { preference -> - LanguageOption( - title = stringResource( - resource = AppString.language_name, - language = preference.languageCode, - ), - isSelected = languageUiState.preference == preference, - onClick = { - onLanguageAction(LanguageUiAction.Select(preference)) - }, - ) - } - } -} - -@Composable -private fun LanguageOption( - title: String, - isSelected: Boolean, - onClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = isSelected, - onClick = onClick, - ) - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - RadioButton( - selected = isSelected, - onClick = null, - ) - Text(text = title) - } -} - -@Composable -private fun AbideToOsSwitcher(onThemeAction: (ThemeUiAction) -> Unit) { - val themeSwitcher = LocalTheme.current - - val isEnabled = themeSwitcher.isSystemOsSwitchAvailable - ThemeSwitcher( - title = stringResource(AppString.abide_to_os_theme), - isChecked = themeSwitcher.config.abideToOs, - isEnabled = isEnabled, - textWhenNotEnabled = stringResource(AppString.os_does_not_support_theme_switching), - onChange = { onThemeAction(ThemeUiAction.AbideToOsThemeSwitch) }, - ) -} - -@Composable -private fun DynamicThemeSwitcher(onThemeAction: (ThemeUiAction) -> Unit) { - SwitcherDivider() - - val themeSwitcher = LocalTheme.current - - val disabledText = StringBuilder() - val disableOsStr = stringResource(AppString.disable_os_theme_switcher_first) - if (themeSwitcher.isSystemDynamicColorAvailable.not()) { - val android11Str = stringResource(AppString.not_available_on_android_11) - disabledText.append(android11Str) - } - if (themeSwitcher.config.abideToOs) { - if (disabledText.isNotEmpty()) disabledText.append("\n") - disabledText.append(disableOsStr) - } - val isDynamicColorAvailable = themeSwitcher.isSystemDynamicColorAvailable - val isNotAbideToOs = themeSwitcher.config.abideToOs.not() - val isEnabled = isDynamicColorAvailable && isNotAbideToOs - ThemeSwitcher( - title = stringResource(AppString.use_system_dynamic_theme), - isChecked = themeSwitcher.config.useDynamicColor, - isEnabled = isEnabled, - textWhenNotEnabled = disabledText.toString(), - modifier = Modifier.padding(top = 8.dp), - onChange = { onThemeAction(ThemeUiAction.DynamicThemeSwitch) }, - ) -} - -@Composable -private fun DarkThemeSwitcher(onThemeAction: (ThemeUiAction) -> Unit) { - SwitcherDivider() - - val themeSwitcher = LocalTheme.current - - val isNotAvailableSystemOsSwitch = themeSwitcher.isSystemOsSwitchAvailable.not() - val isNotAbideToOs = themeSwitcher.config.abideToOs.not() - val isEnabled = isNotAvailableSystemOsSwitch || isNotAbideToOs - ThemeSwitcher( - title = stringResource(AppString.use_dark_theme), - isChecked = themeSwitcher.config.useDarkTheme, - isEnabled = isEnabled, - textWhenNotEnabled = stringResource(AppString.disable_os_theme_switcher_first), - modifier = Modifier.padding(top = 8.dp), - onChange = { onThemeAction(ThemeUiAction.DarkThemeSwitch) }, - ) -} - -@Composable -private fun ThemeSwitcher( - title: String, - isChecked: Boolean, - isEnabled: Boolean, - textWhenNotEnabled: String, - modifier: Modifier = Modifier, - onChange: () -> Unit, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text(text = title) - AnimatedVisibility(visible = isEnabled.not()) { - Text( - text = textWhenNotEnabled, - modifier = Modifier.animateContentSize( - animationSpec = tween(durationMillis = 800), - ), - color = MaterialTheme.colorScheme.error, - ) - } - Switch( - checked = isChecked, - onCheckedChange = { onChange() }, - enabled = isEnabled, - ) - } -} - -@Composable -private fun SwitcherDivider() { - HorizontalDivider( - modifier = Modifier.clip(MaterialTheme.shapes.large), - thickness = 2.5.dp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) -} - @PreviewCombined @Composable private fun Preview() { diff --git a/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/component/LanguageSetting.kt b/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/component/LanguageSetting.kt new file mode 100644 index 00000000..40df7eda --- /dev/null +++ b/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/component/LanguageSetting.kt @@ -0,0 +1,108 @@ +package com.velord.ui.feature.setting.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.velord.core.resource.AppString +import com.velord.core.resource.stringResource +import com.velord.core.ui.compose.preview.PreviewCombined +import com.velord.core.ui.compose.preview.previewLocalizationState +import com.velord.model.setting.LanguagePreference +import com.velord.ui.sharedviewmodel.LanguageUiAction +import com.velord.ui.sharedviewmodel.LanguageUiState + +@Composable +internal fun LanguageSetting( + uiState: LanguageUiState, + onAction: (LanguageUiAction) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + .padding(horizontal = 8.dp) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(AppString.language), + style = MaterialTheme.typography.titleMedium, + ) + + LanguageOption( + title = stringResource(AppString.language_default), + isSelected = uiState.preference.isDefault, + onClick = { + onAction(LanguageUiAction.Select(LanguagePreference.DEFAULT)) + }, + ) + + LanguagePreference.entries + .filterNot { it.isDefault } + .filter { it.languageCode in uiState.localization.document.languageRoster } + .forEach { preference -> + LanguageOption( + title = stringResource( + resource = AppString.language_name, + language = preference.languageCode, + ), + isSelected = uiState.preference == preference, + onClick = { + onAction(LanguageUiAction.Select(preference)) + }, + ) + } + } +} + +@Composable +private fun LanguageOption( + title: String, + isSelected: Boolean, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = onClick, + ) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RadioButton( + selected = isSelected, + onClick = null, + ) + Text(text = title) + } +} + + +@PreviewCombined +@Composable +private fun Preview() { + LanguageSetting( + uiState = LanguageUiState( + localization = previewLocalizationState, + preference = LanguagePreference.DEFAULT, + ), + onAction = {}, + ) +} diff --git a/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/component/ThemeSetting.kt b/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/component/ThemeSetting.kt new file mode 100644 index 00000000..e1a3cd2b --- /dev/null +++ b/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/component/ThemeSetting.kt @@ -0,0 +1,254 @@ +package com.velord.ui.feature.setting.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.velord.core.resource.AppString +import com.velord.core.resource.stringResource +import com.velord.core.ui.compose.preview.PreviewCombined +import com.velord.core.ui.util.LocalTheme +import com.velord.model.setting.AppShapeStyle +import com.velord.model.setting.SpecialTheme +import com.velord.ui.sharedviewmodel.ThemeUiAction + +private val SpecialTheme.titleRes get() = when (this) { + SpecialTheme.LIGHT -> AppString.theme_light + SpecialTheme.DARK -> AppString.theme_dark + SpecialTheme.NEGATIVE_LIGHT -> AppString.theme_negative_light + SpecialTheme.OCEAN -> AppString.theme_ocean +} + +private val AppShapeStyle.titleRes get() = when (this) { + AppShapeStyle.ROUNDED -> AppString.shape_rounded + AppShapeStyle.CUT -> AppString.shape_cut + AppShapeStyle.SQUARE -> AppString.shape_square +} + +@Composable +internal fun ThemeSetting(onAction: (ThemeUiAction) -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + .padding(horizontal = 8.dp) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(16.dp) + .animateContentSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + AbideToOsSwitcher(onAction) + DynamicThemeSwitcher(onAction) + DarkThemeSwitcher(onAction) + SpecialThemeRoster(onAction) + ShapeStyleRoster(onAction) + } +} + +@Composable +private fun AbideToOsSwitcher(onThemeAction: (ThemeUiAction) -> Unit) { + val themeSwitcher = LocalTheme.current + + val isEnabled = themeSwitcher.isSystemOsSwitchAvailable + ThemeSwitcher( + title = stringResource(AppString.abide_to_os_theme), + isChecked = themeSwitcher.config.abideToOs, + isEnabled = isEnabled, + textWhenNotEnabled = stringResource(AppString.os_does_not_support_theme_switching), + onChange = { onThemeAction(ThemeUiAction.AbideToOsThemeSwitch) }, + ) +} + +@Composable +private fun DynamicThemeSwitcher(onThemeAction: (ThemeUiAction) -> Unit) { + SwitcherDivider() + + val themeSwitcher = LocalTheme.current + + val disabledText = StringBuilder() + val disableOsStr = stringResource(AppString.disable_os_theme_switcher_first) + if (themeSwitcher.isSystemDynamicColorAvailable.not()) { + val android11Str = stringResource(AppString.not_available_on_android_11) + disabledText.append(android11Str) + } + if (themeSwitcher.config.abideToOs) { + if (disabledText.isNotEmpty()) disabledText.append("\n") + disabledText.append(disableOsStr) + } + val isDynamicColorAvailable = themeSwitcher.isSystemDynamicColorAvailable + val isNotAbideToOs = themeSwitcher.config.abideToOs.not() + val isEnabled = isDynamicColorAvailable && isNotAbideToOs + ThemeSwitcher( + title = stringResource(AppString.use_system_dynamic_theme), + isChecked = themeSwitcher.config.useDynamicColor, + isEnabled = isEnabled, + textWhenNotEnabled = disabledText.toString(), + modifier = Modifier.padding(top = 8.dp), + onChange = { onThemeAction(ThemeUiAction.DynamicThemeSwitch) }, + ) +} + +@Composable +private fun DarkThemeSwitcher(onThemeAction: (ThemeUiAction) -> Unit) { + SwitcherDivider() + + val themeSwitcher = LocalTheme.current + + val isNotAvailableSystemOsSwitch = themeSwitcher.isSystemOsSwitchAvailable.not() + val isNotAbideToOs = themeSwitcher.config.abideToOs.not() + val isEnabled = isNotAvailableSystemOsSwitch || isNotAbideToOs + ThemeSwitcher( + title = stringResource(AppString.use_dark_theme), + isChecked = themeSwitcher.config.useDarkTheme, + isEnabled = isEnabled, + textWhenNotEnabled = stringResource(AppString.disable_os_theme_switcher_first), + modifier = Modifier.padding(top = 8.dp), + onChange = { onThemeAction(ThemeUiAction.DarkThemeSwitch) }, + ) +} + +@Composable +private fun SpecialThemeRoster(onThemeAction: (ThemeUiAction) -> Unit) { + val themeSwitcher = LocalTheme.current + + val isNotAvailableSystemOsSwitch = themeSwitcher.isSystemOsSwitchAvailable.not() + val isNotAbideToOs = themeSwitcher.config.abideToOs.not() + val isEnabled = isNotAvailableSystemOsSwitch || isNotAbideToOs + if (isEnabled.not()) return + + val availableThemes = SpecialTheme.getAvailableThemeRoster(themeSwitcher.config.useDarkTheme) + availableThemes.forEach { theme -> + val isOptionEnabled = theme.isEnabled(themeSwitcher.config.useDynamicColor) + Column { + ThemeOption( + title = stringResource(theme.titleRes), + isSelected = themeSwitcher.config.current == theme, + isEnabled = isOptionEnabled, + onClick = { onThemeAction(ThemeUiAction.SpecialThemeSwitch(theme)) }, + ) + AnimatedVisibility(visible = isOptionEnabled.not()) { + Text( + text = stringResource(AppString.disable_dynamic_theme_first), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(bottom = 4.dp) + ) + } + } + } +} + +@Composable +private fun ShapeStyleRoster(onThemeAction: (ThemeUiAction) -> Unit) { + SwitcherDivider() + + Text( + text = stringResource(AppString.shape_style_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) + ) + + val themeSwitcher = LocalTheme.current + + AppShapeStyle.entries.forEach { style -> + ThemeOption( + title = stringResource(style.titleRes), + isSelected = themeSwitcher.config.shapeStyle == style, + isEnabled = true, + onClick = { onThemeAction(ThemeUiAction.ShapeStyleSwitch(style)) }, + ) + } +} + +@Composable +private fun ThemeOption( + title: String, + isSelected: Boolean, + isEnabled: Boolean, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + enabled = isEnabled, + onClick = onClick, + ) + .padding(vertical = 4.dp) + .alpha(if (isEnabled) 1f else 0.5f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RadioButton( + selected = isSelected, + enabled = isEnabled, + onClick = null, + ) + Text(text = title) + } +} + +@Composable +private fun ThemeSwitcher( + title: String, + isChecked: Boolean, + isEnabled: Boolean, + textWhenNotEnabled: String, + modifier: Modifier = Modifier, + onChange: () -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text(text = title) + AnimatedVisibility(visible = isEnabled.not()) { + Text( + text = textWhenNotEnabled, + modifier = Modifier.animateContentSize( + animationSpec = tween(durationMillis = 800), + ), + color = MaterialTheme.colorScheme.error, + ) + } + Switch( + checked = isChecked, + onCheckedChange = { onChange() }, + enabled = isEnabled, + ) + } +} + +@Composable +private fun SwitcherDivider() { + HorizontalDivider( + modifier = Modifier.clip(MaterialTheme.shapes.large), + thickness = 2.5.dp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) +} + +@PreviewCombined +@Composable +private fun Preview() { + ThemeSetting(onAction = {}) +} diff --git a/ui/feature-splash/src/commonMain/kotlin/com/velord/ui/feature/splash/SplashScreen.kt b/ui/feature-splash/src/commonMain/kotlin/com/velord/ui/feature/splash/SplashScreen.kt index b07d8ae5..03a70d10 100644 --- a/ui/feature-splash/src/commonMain/kotlin/com/velord/ui/feature/splash/SplashScreen.kt +++ b/ui/feature-splash/src/commonMain/kotlin/com/velord/ui/feature/splash/SplashScreen.kt @@ -42,7 +42,6 @@ import com.velord.core.resource.ic_launcher_foreground_dark import com.velord.core.resource.ic_launcher_foreground_light import com.velord.core.ui.compose.preview.PreviewCombined import com.velord.core.ui.util.LocalTheme -import com.velord.model.setting.SpecialTheme import org.jetbrains.compose.resources.painterResource private const val ANIMATION_TRANSITION_TO_CURRENT_THEME = 500 @@ -273,9 +272,11 @@ private fun getIconPainter(): Painter { Res.drawable.ic_launcher_foreground_dark } } else { - when (themeConfig.config.current) { - SpecialTheme.LIGHT -> Res.drawable.ic_launcher_foreground_light - SpecialTheme.DARK -> Res.drawable.ic_launcher_foreground_dark + // TODO: create dedicated icons for every theme + if (themeConfig.config.useDarkTheme) { + Res.drawable.ic_launcher_foreground_dark + } else { + Res.drawable.ic_launcher_foreground_light } } diff --git a/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/ThemeVM.kt b/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/ThemeVM.kt index e36076c7..124976b2 100644 --- a/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/ThemeVM.kt +++ b/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/ThemeVM.kt @@ -1,11 +1,15 @@ package com.velord.ui.sharedviewmodel +import com.velord.model.setting.AppShapeStyle import com.velord.model.setting.AppThemeConfig +import com.velord.model.setting.SpecialTheme import com.velord.model.setting.ThemeConfig import com.velord.usecase.setting.GetThemeConfigUC import com.velord.usecase.setting.SwitchAbideToOsThemeConfigUC import com.velord.usecase.setting.SwitchDarkThemeConfigUC import com.velord.usecase.setting.SwitchDynamicColorThemeConfigUC +import com.velord.usecase.setting.SwitchShapeStyleThemeConfigUC +import com.velord.usecase.setting.SwitchSpecialThemeConfigUC import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map @@ -26,6 +30,8 @@ sealed interface ThemeUiAction { data object AbideToOsThemeSwitch : ThemeUiAction data object DynamicThemeSwitch : ThemeUiAction data object DarkThemeSwitch : ThemeUiAction + data class SpecialThemeSwitch(val theme: SpecialTheme) : ThemeUiAction + data class ShapeStyleSwitch(val style: AppShapeStyle) : ThemeUiAction } class ThemeVM( @@ -33,6 +39,8 @@ class ThemeVM( private val switchDynamicColorThemeConfigUC: SwitchDynamicColorThemeConfigUC, private val switchAbideToOsThemeConfigUC: SwitchAbideToOsThemeConfigUC, private val switchDarkThemeConfigUC: SwitchDarkThemeConfigUC, + private val switchSpecialThemeConfigUC: SwitchSpecialThemeConfigUC, + private val switchShapeStyleThemeConfigUC: SwitchShapeStyleThemeConfigUC, ) : CoroutineScopeVM() { val uiStateFlow = MutableStateFlow(ThemeUiState.DEFAULT) @@ -48,10 +56,9 @@ class ThemeVM( } } - private fun onAbideToOsThemeSwitch() = launch { - uiStateFlow.value.appThemeConfig?.let { - switchAbideToOsThemeConfigUC.invoke(it.config) - } + private fun onAbideToOsThemeSwitch() { + val config = uiStateFlow.value.appThemeConfig?.config ?: return + launch { switchAbideToOsThemeConfigUC.invoke(config) } } private fun onDynamicThemeSwitch() { @@ -66,6 +73,17 @@ class ThemeVM( } } + private fun onSpecialThemeSwitch(theme: SpecialTheme) { + switchConfig { + switchSpecialThemeConfigUC.invoke(it, theme) + } + } + + private fun onShapeStyleSwitch(style: AppShapeStyle) { + val config = uiStateFlow.value.appThemeConfig?.config ?: return + launch { switchShapeStyleThemeConfigUC.invoke(config, style) } + } + private fun switchConfig(f: suspend (ThemeConfig) -> Unit) { val config = uiStateFlow.value.appThemeConfig?.config ?: return if (config.abideToOs) return @@ -90,6 +108,8 @@ class ThemeVM( is ThemeUiAction.AbideToOsThemeSwitch -> onAbideToOsThemeSwitch() is ThemeUiAction.DynamicThemeSwitch -> onDynamicThemeSwitch() is ThemeUiAction.DarkThemeSwitch -> onDarkThemeSwitch() + is ThemeUiAction.SpecialThemeSwitch -> onSpecialThemeSwitch(it.theme) + is ThemeUiAction.ShapeStyleSwitch -> onShapeStyleSwitch(it.style) } } }