diff --git a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt index d232df24b3aec05b7b6c8ab85bb0ef96b6631017..b92286f58558e975a63d312bc67621fabf0faa44 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt @@ -52,6 +52,8 @@ import org.briarproject.bramble.util.OsUtils.isMac import org.briarproject.briar.attachment.AttachmentModule import org.briarproject.briar.desktop.attachment.media.ImageCompressor import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl +import org.briarproject.briar.desktop.settings.Settings +import org.briarproject.briar.desktop.settings.SettingsImpl import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.threading.BriarExecutorsImpl import org.briarproject.briar.desktop.threading.UiExecutor @@ -100,6 +102,10 @@ internal class DesktopModule( return DesktopDatabaseConfig(dbDir, keyDir) } + @Provides + @Singleton + fun provideSettings(settings: SettingsImpl): Settings = settings + @Provides @Singleton @EventExecutor diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt index 62c9ec49a3a463adefd6a420a27277ef51a79925..99e4c6ccbdce531df7b3e63a9e29ae1482026f17 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt @@ -229,7 +229,7 @@ fun ConversationList( } } - onMessageAddedToBottom.react { type -> + onMessageAddedToBottom.reactInCoroutine { type -> // scroll to bottom for new *outgoing* message or if scroll position was at last message before if (type == ConversationViewModel.MessageAddedType.OUTGOING || scrollState.isScrolledToPenultimate()) { scope.launch { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt new file mode 100644 index 0000000000000000000000000000000000000000..59fa618dda1bd33037100d2ce8c8d9103e954307 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt @@ -0,0 +1,30 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.settings + +import org.briarproject.briar.desktop.viewmodel.SingleStateEvent + +interface Settings { + + enum class Theme { AUTO, LIGHT, DARK } + + var theme: Theme + + val invalidateScreen: SingleStateEvent<Unit> +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..d70e0f4df32d61d44696094859d9d5dcb4d085cd --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt @@ -0,0 +1,43 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.settings + +import org.briarproject.briar.desktop.settings.Settings.Theme +import org.briarproject.briar.desktop.settings.Settings.Theme.AUTO +import org.briarproject.briar.desktop.viewmodel.SingleStateEvent +import java.util.prefs.Preferences +import javax.inject.Inject + +const val PREF_THEME = "theme" + +class SettingsImpl @Inject internal constructor() : Settings { + + // used for unencrypted settings, namely theme and language + private val prefs = Preferences.userNodeForPackage(this::class.java) + + override val invalidateScreen = SingleStateEvent<Unit>() + + override var theme: Theme + get() = Theme.valueOf(prefs.get(PREF_THEME, AUTO.name)) + set(value) { + prefs.put(PREF_THEME, value.name) + prefs.flush() // write preferences to disk + invalidateScreen.emit(Unit) + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt index 7d35ba60cb751f0d3ff0d5e9482685bcec99c970..3c3aec2b22b10f5bb049fb2c76a815b279bc25f7 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt @@ -19,6 +19,8 @@ package org.briarproject.briar.desktop.settings import androidx.compose.runtime.mutableStateOf +import org.briarproject.briar.desktop.settings.Settings.Theme.DARK +import org.briarproject.briar.desktop.settings.Settings.Theme.LIGHT import org.briarproject.briar.desktop.viewmodel.ViewModel import org.briarproject.briar.desktop.viewmodel.asState import javax.inject.Inject @@ -34,18 +36,21 @@ enum class SettingCategory { class SettingsViewModel @Inject -constructor() : ViewModel { +constructor( + private val settings: Settings, +) : ViewModel { private val _selectedSetting = mutableStateOf(SettingCategory.DISPLAY) val selectedSetting = _selectedSetting.asState() - private val _isDarkMode = mutableStateOf(true) + private val _isDarkMode = mutableStateOf(settings.theme == DARK) val isDarkMode = _isDarkMode.asState() fun selectSetting(selectedOption: SettingCategory) { _selectedSetting.value = selectedOption } - fun toggleTheme() { - _isDarkMode.value = !isDarkMode.value + fun toggleTheme() { // todo: set theme instead + settings.theme = if (settings.theme == DARK) LIGHT else DARK + _isDarkMode.value = settings.theme == DARK } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index 06cc0ba857e72aca9dcc0ad8c106ef3269912b88..659f8ed26344d2ae0a6c98644b79300517176ade 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -18,6 +18,7 @@ package org.briarproject.briar.desktop.ui +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -45,6 +46,9 @@ import org.briarproject.briar.desktop.DesktopFeatureFlags import org.briarproject.briar.desktop.expiration.ExpirationBanner import org.briarproject.briar.desktop.login.ErrorScreen import org.briarproject.briar.desktop.login.StartupScreen +import org.briarproject.briar.desktop.settings.Settings +import org.briarproject.briar.desktop.settings.Settings.Theme.AUTO +import org.briarproject.briar.desktop.settings.Settings.Theme.DARK import org.briarproject.briar.desktop.settings.SettingsViewModel import org.briarproject.briar.desktop.theme.BriarTheme import org.briarproject.briar.desktop.ui.Screen.EXPIRED @@ -85,6 +89,7 @@ constructor( private val lifecycleManager: LifecycleManager, private val eventBus: EventBus, private val viewModelProvider: ViewModelProvider, + private val settings: Settings, private val featureFlags: FeatureFlags, private val desktopFeatureFlags: DesktopFeatureFlags, ) : BriarUi, EventListener { @@ -110,6 +115,9 @@ constructor( @OptIn(ExperimentalComposeUiApi::class) @Composable override fun start(onClose: () -> Unit) { + // invalidate whole application window in case the theme or language setting is changed + settings.invalidateScreen.react {} + val title = i18n("main.title") val platformLocalization = object : PlatformLocalization { override val copy = i18n("copy") @@ -133,7 +141,9 @@ constructor( ) { var showAbout by remember { mutableStateOf(false) } val settingsViewModel: SettingsViewModel = viewModel() - BriarTheme(isDarkTheme = settingsViewModel.isDarkMode.value) { + val isDarkTheme = settings.theme == DARK || + (settings.theme == AUTO && isSystemInDarkTheme()) + BriarTheme(isDarkTheme) { Column(Modifier.fillMaxSize()) { ExpirationBanner { screenState = EXPIRED; stop() } when (screenState) { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt index 273dae37e6dd0d12b7f175f322933a26ca47e591..3a1cd50620aed8bc904a0703bbc9315f2061ba25 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt @@ -44,18 +44,32 @@ class SingleStateEvent<T : Any> { state.value = value } + fun reactAndReset(block: (T) -> Unit) { + val value = state.value + if (value != null) { + state.value = null + block(value) + } + } + + /** + * React to every new value of type [T] emitted through this event, + * by directly reading the state in the calling function. + * This can be used to invalidate a composable function. + * Make sure to not react to the same event on multiple places. + */ + @Composable + inline fun react(noinline block: (T) -> Unit) = reactAndReset(block) + /** - * React to every new value of type [T] emitted through this event. + * React to every new value of type [T] emitted through this event + * inside a LaunchedEffect. * Make sure to not react to the same event on multiple places. */ @Composable - fun react(block: (T) -> Unit) { + fun reactInCoroutine(block: (T) -> Unit) { LaunchedEffect(state.value) { - val value = state.value - if (value != null) { - block(value) - state.value = null - } + reactAndReset(block) } } } diff --git a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt index 99327752b145f42d799871f3a4e7e169ea94d8ed..4ff59a88ccdddddb3dc3ad82cbb905ff9781547f 100644 --- a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt +++ b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt @@ -52,6 +52,8 @@ import org.briarproject.briar.api.test.TestAvatarCreator import org.briarproject.briar.attachment.AttachmentModule import org.briarproject.briar.desktop.attachment.media.ImageCompressor import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl +import org.briarproject.briar.desktop.settings.Settings +import org.briarproject.briar.desktop.settings.SettingsImpl import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl import org.briarproject.briar.desktop.testdata.TestAvatarCreatorImpl @@ -105,6 +107,10 @@ internal class DesktopTestModule( return DesktopDatabaseConfig(dbDir, keyDir) } + @Provides + @Singleton + fun provideSettings(settings: SettingsImpl): Settings = settings + @Provides @Singleton @EventExecutor