diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt index ee7fc41..a2bf224 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt @@ -4,7 +4,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.FadeTransition import dev.meloda.overseerr.screens.main.MainScreen @@ -24,12 +26,13 @@ internal fun App() = KoinContext { } val settingsController: SettingsController = koinInject() + val settings by settingsController.settings.collectAsStateWithLifecycle() LaunchedEffect(true) { settingsController.loadAppSettings() } - AppTheme { + AppTheme(themeMode = settings.themeMode) { Surface(modifier = Modifier.fillMaxSize()) { Navigator(MainScreen()) { navigator -> FadeTransition(navigator) diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/presentation/LoginScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/presentation/LoginScreen.kt index aae103f..5c981f6 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/presentation/LoginScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/presentation/LoginScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -32,7 +33,7 @@ class LoginScreen : Screen { override fun Content() { val navigator = LocalNavigator.currentOrThrow val viewModel: LoginViewModel = koinViewModel() - val screenState: LoginScreenState by viewModel.screenState.collectAsState() + val screenState: LoginScreenState by viewModel.screenState.collectAsStateWithLifecycle() var loginValue by rememberSaveable { mutableStateOf(screenState.login) diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt index b6e9c11..d650a1e 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt @@ -5,25 +5,61 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.meloda.overseerr.screens.login.presentation.LoginScreen import dev.meloda.overseerr.screens.requests.presentation.RequestsScreen import dev.meloda.overseerr.screens.url.presentation.UrlScreen +import dev.meloda.overseerr.settings.SettingsController +import dev.meloda.overseerr.settings.model.ThemeMode +import kotlinx.coroutines.launch +import org.koin.compose.koinInject class MainScreen : Screen { @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { + val coroutineScope = rememberCoroutineScope() + val navigator = LocalNavigator.currentOrThrow + val settingsController: SettingsController = koinInject() + val settings by settingsController.settings.collectAsStateWithLifecycle() + Scaffold( topBar = { - TopAppBar(title = { Text(text = "Main screen") }) + TopAppBar( + title = { Text(text = "Main screen") }, + actions = { + TextButton( + onClick = { + val newThemeMode = ThemeMode.entries.getOrElse( + ThemeMode.entries.indexOf(settings.themeMode) + 1 + ) { ThemeMode.System } + + settingsController.updateThemeMode(newThemeMode) + coroutineScope.launch { + settingsController.saveAppSettings() + } + } + ) { + Text( + text = when (settings.themeMode) { + ThemeMode.System -> "System" + ThemeMode.Dark -> "Dark" + ThemeMode.Light -> "Light" + } + ) + } + } + ) } ) { padding -> Row( diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt index c7e81f7..2c591aa 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -43,7 +44,7 @@ class RequestsScreen : Screen { override fun Content() { val navigator = LocalNavigator.currentOrThrow val viewModel: RequestsViewModel = koinViewModel() - val screenState: RequestsScreenState by viewModel.screenState.collectAsState() + val screenState: RequestsScreenState by viewModel.screenState.collectAsStateWithLifecycle() val hazeState = remember { HazeState() } val hazeStyle = HazeMaterials.ultraThin() diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt index 3ba946c..f25e643 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -27,7 +28,7 @@ class UrlScreen : Screen { override fun Content() { val navigator = LocalNavigator.currentOrThrow val viewModel: UrlViewModel = koinViewModel() - val screenState by viewModel.screenState.collectAsState() + val screenState by viewModel.screenState.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/SettingsController.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/SettingsController.kt index cc9a8a1..f748a2d 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/SettingsController.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/SettingsController.kt @@ -2,6 +2,7 @@ package dev.meloda.overseerr.settings import dev.meloda.overseerr.ext.setValue import dev.meloda.overseerr.settings.model.AppSettings +import dev.meloda.overseerr.settings.model.ThemeMode import io.github.xxfast.kstore.KStore import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -9,8 +10,11 @@ import kotlinx.coroutines.flow.StateFlow interface SettingsController { val settings: StateFlow + suspend fun saveAppSettings() suspend fun updateAppSettings(update: (AppSettings) -> AppSettings) suspend fun loadAppSettings(): AppSettings + + fun updateThemeMode(newThemeMode: ThemeMode) } class SettingsControllerImpl( @@ -19,6 +23,10 @@ class SettingsControllerImpl( override val settings = MutableStateFlow(AppSettings.EMPTY) + override suspend fun saveAppSettings() { + store.set(settings.value) + } + override suspend fun updateAppSettings(update: (AppSettings) -> AppSettings) { store.set(update(settings.value)) } @@ -28,4 +36,8 @@ class SettingsControllerImpl( settings.setValue { loadedSettings } return loadedSettings } + + override fun updateThemeMode(newThemeMode: ThemeMode) { + settings.setValue { old -> old.copy(themeMode = newThemeMode) } + } } diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt index a08951a..26f916c 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt @@ -5,7 +5,8 @@ import kotlinx.serialization.Serializable @Serializable data class AppSettings( val url: String = "", - val plexToken: String = "" + val plexToken: String = "", + val themeMode: ThemeMode = ThemeMode.System ) { companion object { val EMPTY: AppSettings = AppSettings() diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/ThemeMode.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/ThemeMode.kt new file mode 100644 index 0000000..6997bda --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/ThemeMode.kt @@ -0,0 +1,5 @@ +package dev.meloda.overseerr.settings.model + +enum class ThemeMode { + System, Dark, Light +} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt index f879d2a..d449c22 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt @@ -6,15 +6,22 @@ import androidx.compose.material3.Surface import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.* +import dev.meloda.overseerr.settings.model.ThemeMode internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } @Composable internal fun AppTheme( + themeMode: ThemeMode = ThemeMode.System, content: @Composable () -> Unit ) { val systemIsDark = isSystemInDarkTheme() - val isDarkState = remember { mutableStateOf(systemIsDark) } + val isDarkState = remember(themeMode, systemIsDark) { + mutableStateOf( + if (themeMode == ThemeMode.System) systemIsDark + else themeMode == ThemeMode.Dark + ) + } CompositionLocalProvider( LocalThemeIsDark provides isDarkState ) {