From 2f12d13422c0359562663d93202fe7ce80c3c949 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Fri, 7 Feb 2025 21:53:49 +0100 Subject: [PATCH] Saveable/Reactive Preferences --- app/src/processing/app/Preferences.kt | 69 ++++++++++++------- .../app/contrib/ui/ContributionManager.kt | 9 +-- app/src/processing/app/ui/theme/Theme.kt | 2 +- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index 43b55a917..5c0751d87 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -2,9 +2,13 @@ package processing.app import androidx.compose.runtime.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.launch import java.io.File import java.io.InputStream +import java.io.OutputStream import java.nio.file.* import java.util.Properties @@ -15,35 +19,58 @@ const val DEFAULTS_FILE_NAME = "defaults.txt" fun PlatformStart(){ Platform.inst ?: Platform.init() } +class ReactiveProperties: Properties() { + val _stateMap = mutableStateMapOf() + override fun setProperty(key: String, value: String) { + super.setProperty(key, value) + _stateMap[key] = value + } + + override fun getProperty(key: String): String? { + return _stateMap[key] ?: super.getProperty(key) + } + + operator fun get(key: String): String? = getProperty(key) +} +val LocalPreferences = compositionLocalOf { error("No preferences provided") } +@OptIn(FlowPreview::class) @Composable -fun loadPreferences(): Properties{ +fun PreferencesProvider(content: @Composable () -> Unit){ PlatformStart() val settingsFolder = Platform.getSettingsFolder() val preferencesFile = settingsFolder.resolve(PREFERENCES_FILE_NAME) - if(!preferencesFile.exists()){ - preferencesFile.createNewFile() - } - watchFile(preferencesFile) - var update by remember { mutableStateOf(System.currentTimeMillis()) } + val update = watchFile(preferencesFile) + val properties = remember(preferencesFile, update) { ReactiveProperties().apply { + load((ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME)?: InputStream.nullInputStream()).reader(Charsets.UTF_8)) + load(preferencesFile.inputStream().reader(Charsets.UTF_8)) + }} - // TODO: Make observable when preferences change - // TODO: Save to file when preferences change - class ObservableProperties : Properties() { - override fun setProperty(key: String, value: String): Any? { - update = System.currentTimeMillis() - return super.setProperty(key, value) - } + val initialState = remember(properties) { properties._stateMap.toMap() } + + LaunchedEffect(properties) { + snapshotFlow { properties._stateMap.toMap() } + .dropWhile { it == initialState } + .debounce(1000) + .collect { + preferencesFile.outputStream().use { output -> + output.write( + properties.entries + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.key.toString() }) + .joinToString("\n") { (key, value) -> "$key=$value" } + .toByteArray() + ) + } + } } - return ObservableProperties().apply { - load(ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME) ?: InputStream.nullInputStream()) - load(preferencesFile.inputStream()) + CompositionLocalProvider(LocalPreferences provides properties){ + content() } + } - @Composable fun watchFile(file: File): Any? { val scope = rememberCoroutineScope() @@ -72,12 +99,4 @@ fun watchFile(file: File): Any? { } } return event -} -val LocalPreferences = compositionLocalOf { error("No preferences provided") } -@Composable -fun PreferencesProvider(content: @Composable () -> Unit){ - val preferences = loadPreferences() - CompositionLocalProvider(LocalPreferences provides preferences){ - content() - } } \ No newline at end of file diff --git a/app/src/processing/app/contrib/ui/ContributionManager.kt b/app/src/processing/app/contrib/ui/ContributionManager.kt index 2ad472159..4d21227a4 100644 --- a/app/src/processing/app/contrib/ui/ContributionManager.kt +++ b/app/src/processing/app/contrib/ui/ContributionManager.kt @@ -22,8 +22,9 @@ import androidx.compose.ui.window.application import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import kotlinx.serialization.Serializable +import processing.app.LocalPreferences import processing.app.Platform -import processing.app.loadPreferences +import processing.app.ReactiveProperties import java.net.URL import java.util.* import javax.swing.JFrame @@ -106,7 +107,7 @@ fun contributionsManager(){ var localContributions by remember { mutableStateOf(listOf()) } var error by remember { mutableStateOf(null) } - val preferences = loadPreferences() + val preferences = LocalPreferences.current LaunchedEffect(preferences){ try { @@ -284,9 +285,9 @@ fun contributionsManager(){ } -fun loadContributionProperties(preferences: Properties): List>{ +fun loadContributionProperties(preferences: ReactiveProperties): List>{ val result = mutableListOf>() - val sketchBook = Path(preferences.getProperty("sketchbook.path.four", Platform.getDefaultSketchbookFolder().path)) + val sketchBook = Path(preferences.getProperty("sketchbook.path.four") ?: Platform.getDefaultSketchbookFolder().path) sketchBook.forEachDirectoryEntry{ contributionsFolder -> if(!contributionsFolder.isDirectory()) return@forEachDirectoryEntry val typeName = contributionsFolder.fileName.toString() diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt index 735d8e5b2..82428db7f 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -16,7 +16,7 @@ import java.util.Properties class Theme(themeFile: String? = "") : Properties() { init { load(ClassLoader.getSystemResourceAsStream("theme.txt")) - load(ClassLoader.getSystemResourceAsStream(themeFile) ?: InputStream.nullInputStream()) + load(ClassLoader.getSystemResourceAsStream(themeFile ?: "") ?: InputStream.nullInputStream()) } fun getColor(key: String): Color { return Color(getProperty(key).toColorInt())