Skip to content
Snippets Groups Projects
Verified Commit 480050be authored by Mikolai Gütschow's avatar Mikolai Gütschow
Browse files

make theme setting persistent

parent c9061977
No related branches found
No related tags found
1 merge request!140Settings persistence and allow changing language
...@@ -52,6 +52,8 @@ import org.briarproject.bramble.util.OsUtils.isMac ...@@ -52,6 +52,8 @@ import org.briarproject.bramble.util.OsUtils.isMac
import org.briarproject.briar.attachment.AttachmentModule import org.briarproject.briar.attachment.AttachmentModule
import org.briarproject.briar.desktop.attachment.media.ImageCompressor import org.briarproject.briar.desktop.attachment.media.ImageCompressor
import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl 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.BriarExecutors
import org.briarproject.briar.desktop.threading.BriarExecutorsImpl import org.briarproject.briar.desktop.threading.BriarExecutorsImpl
import org.briarproject.briar.desktop.threading.UiExecutor import org.briarproject.briar.desktop.threading.UiExecutor
...@@ -100,6 +102,10 @@ internal class DesktopModule( ...@@ -100,6 +102,10 @@ internal class DesktopModule(
return DesktopDatabaseConfig(dbDir, keyDir) return DesktopDatabaseConfig(dbDir, keyDir)
} }
@Provides
@Singleton
fun provideSettings(settings: SettingsImpl): Settings = settings
@Provides @Provides
@Singleton @Singleton
@EventExecutor @EventExecutor
......
...@@ -229,7 +229,7 @@ fun ConversationList( ...@@ -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 // scroll to bottom for new *outgoing* message or if scroll position was at last message before
if (type == ConversationViewModel.MessageAddedType.OUTGOING || scrollState.isScrolledToPenultimate()) { if (type == ConversationViewModel.MessageAddedType.OUTGOING || scrollState.isScrolledToPenultimate()) {
scope.launch { scope.launch {
......
/*
* 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>
}
/*
* 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)
}
}
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
package org.briarproject.briar.desktop.settings package org.briarproject.briar.desktop.settings
import androidx.compose.runtime.mutableStateOf 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.ViewModel
import org.briarproject.briar.desktop.viewmodel.asState import org.briarproject.briar.desktop.viewmodel.asState
import javax.inject.Inject import javax.inject.Inject
...@@ -34,18 +36,21 @@ enum class SettingCategory { ...@@ -34,18 +36,21 @@ enum class SettingCategory {
class SettingsViewModel class SettingsViewModel
@Inject @Inject
constructor() : ViewModel { constructor(
private val settings: Settings,
) : ViewModel {
private val _selectedSetting = mutableStateOf(SettingCategory.DISPLAY) private val _selectedSetting = mutableStateOf(SettingCategory.DISPLAY)
val selectedSetting = _selectedSetting.asState() val selectedSetting = _selectedSetting.asState()
private val _isDarkMode = mutableStateOf(true) private val _isDarkMode = mutableStateOf(settings.theme == DARK)
val isDarkMode = _isDarkMode.asState() val isDarkMode = _isDarkMode.asState()
fun selectSetting(selectedOption: SettingCategory) { fun selectSetting(selectedOption: SettingCategory) {
_selectedSetting.value = selectedOption _selectedSetting.value = selectedOption
} }
fun toggleTheme() { fun toggleTheme() { // todo: set theme instead
_isDarkMode.value = !isDarkMode.value settings.theme = if (settings.theme == DARK) LIGHT else DARK
_isDarkMode.value = settings.theme == DARK
} }
} }
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
package org.briarproject.briar.desktop.ui package org.briarproject.briar.desktop.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
...@@ -45,6 +46,9 @@ import org.briarproject.briar.desktop.DesktopFeatureFlags ...@@ -45,6 +46,9 @@ import org.briarproject.briar.desktop.DesktopFeatureFlags
import org.briarproject.briar.desktop.expiration.ExpirationBanner import org.briarproject.briar.desktop.expiration.ExpirationBanner
import org.briarproject.briar.desktop.login.ErrorScreen import org.briarproject.briar.desktop.login.ErrorScreen
import org.briarproject.briar.desktop.login.StartupScreen 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.settings.SettingsViewModel
import org.briarproject.briar.desktop.theme.BriarTheme import org.briarproject.briar.desktop.theme.BriarTheme
import org.briarproject.briar.desktop.ui.Screen.EXPIRED import org.briarproject.briar.desktop.ui.Screen.EXPIRED
...@@ -85,6 +89,7 @@ constructor( ...@@ -85,6 +89,7 @@ constructor(
private val lifecycleManager: LifecycleManager, private val lifecycleManager: LifecycleManager,
private val eventBus: EventBus, private val eventBus: EventBus,
private val viewModelProvider: ViewModelProvider, private val viewModelProvider: ViewModelProvider,
private val settings: Settings,
private val featureFlags: FeatureFlags, private val featureFlags: FeatureFlags,
private val desktopFeatureFlags: DesktopFeatureFlags, private val desktopFeatureFlags: DesktopFeatureFlags,
) : BriarUi, EventListener { ) : BriarUi, EventListener {
...@@ -110,6 +115,9 @@ constructor( ...@@ -110,6 +115,9 @@ constructor(
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
override fun start(onClose: () -> Unit) { 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 title = i18n("main.title")
val platformLocalization = object : PlatformLocalization { val platformLocalization = object : PlatformLocalization {
override val copy = i18n("copy") override val copy = i18n("copy")
...@@ -133,7 +141,9 @@ constructor( ...@@ -133,7 +141,9 @@ constructor(
) { ) {
var showAbout by remember { mutableStateOf(false) } var showAbout by remember { mutableStateOf(false) }
val settingsViewModel: SettingsViewModel = viewModel() val settingsViewModel: SettingsViewModel = viewModel()
BriarTheme(isDarkTheme = settingsViewModel.isDarkMode.value) { val isDarkTheme = settings.theme == DARK ||
(settings.theme == AUTO && isSystemInDarkTheme())
BriarTheme(isDarkTheme) {
Column(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize()) {
ExpirationBanner { screenState = EXPIRED; stop() } ExpirationBanner { screenState = EXPIRED; stop() }
when (screenState) { when (screenState) {
......
...@@ -44,18 +44,32 @@ class SingleStateEvent<T : Any> { ...@@ -44,18 +44,32 @@ class SingleStateEvent<T : Any> {
state.value = value 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. * 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. * Make sure to not react to the same event on multiple places.
*/ */
@Composable @Composable
fun react(block: (T) -> Unit) { inline fun react(noinline block: (T) -> Unit) = reactAndReset(block)
/**
* 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 reactInCoroutine(block: (T) -> Unit) {
LaunchedEffect(state.value) { LaunchedEffect(state.value) {
val value = state.value reactAndReset(block)
if (value != null) {
block(value)
state.value = null
}
} }
} }
} }
...@@ -52,6 +52,8 @@ import org.briarproject.briar.api.test.TestAvatarCreator ...@@ -52,6 +52,8 @@ import org.briarproject.briar.api.test.TestAvatarCreator
import org.briarproject.briar.attachment.AttachmentModule import org.briarproject.briar.attachment.AttachmentModule
import org.briarproject.briar.desktop.attachment.media.ImageCompressor import org.briarproject.briar.desktop.attachment.media.ImageCompressor
import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl 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.DeterministicTestDataCreator
import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl
import org.briarproject.briar.desktop.testdata.TestAvatarCreatorImpl import org.briarproject.briar.desktop.testdata.TestAvatarCreatorImpl
...@@ -105,6 +107,10 @@ internal class DesktopTestModule( ...@@ -105,6 +107,10 @@ internal class DesktopTestModule(
return DesktopDatabaseConfig(dbDir, keyDir) return DesktopDatabaseConfig(dbDir, keyDir)
} }
@Provides
@Singleton
fun provideSettings(settings: SettingsImpl): Settings = settings
@Provides @Provides
@Singleton @Singleton
@EventExecutor @EventExecutor
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment