From 2058a6a78959a70960a1526426623e09f914ec4d Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sun, 1 Dec 2024 12:32:06 +0100 Subject: [PATCH] Add settings store/repo etc. to save user settings. --- .../com/pixelized/desktop/lwa/Module.kt | 24 ++++- .../desktop/lwa/business/SettingsUseCase.kt | 10 +++ .../CharacterSheetRepository.kt | 3 +- .../characterSheet/CharacterSheetStore.kt | 2 +- .../repository/network/NetworkRepository.kt | 14 ++- .../repository/settings/SettingsFactory.kt | 38 ++++++++ .../repository/settings/SettingsRepository.kt | 36 ++++++++ .../lwa/repository/settings/SettingsStore.kt | 90 +++++++++++++++++++ .../lwa/repository/settings/model/Settings.kt | 5 ++ .../repository/settings/model/SettingsJson.kt | 6 ++ .../settings/model/SettingsJsonV1.kt | 8 ++ .../lwa/screen/network/NetworkViewModel.kt | 21 +++-- 12 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SettingsUseCase.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsStore.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/Settings.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJson.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJsonV1.kt diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 2cb200d..7fca837 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -2,6 +2,7 @@ package com.pixelized.desktop.lwa import com.pixelized.desktop.lwa.business.CharacterSheetUseCase import com.pixelized.desktop.lwa.business.RollUseCase +import com.pixelized.desktop.lwa.business.SettingsUseCase import com.pixelized.desktop.lwa.business.SkillStepUseCase import com.pixelized.desktop.lwa.business.SkillValueComputationUseCase import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser @@ -11,6 +12,9 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore import com.pixelized.desktop.lwa.repository.characterSheet.SkillDescriptionFactory import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository +import com.pixelized.desktop.lwa.repository.settings.SettingsFactory +import com.pixelized.desktop.lwa.repository.settings.SettingsRepository +import com.pixelized.desktop.lwa.repository.settings.SettingsStore import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetFactory import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetViewModel import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditFactory @@ -21,6 +25,7 @@ import com.pixelized.desktop.lwa.screen.network.NetworkFactory import com.pixelized.desktop.lwa.screen.network.NetworkViewModel import com.pixelized.desktop.lwa.screen.roll.RollViewModel import com.pixelized.desktop.lwa.screen.rollhistory.RollHistoryViewModel +import kotlinx.serialization.json.Json import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.viewModelOf @@ -28,19 +33,32 @@ import org.koin.dsl.module val moduleDependencies get() = listOf( + toolsDependencies, parserDependencies, factoryDependencies, + useCaseDependencies, + storeDependencies, repositoryDependencies, viewModelDependencies, - useCaseDependencies, ) +val toolsDependencies + get() = module { + factory { Json { explicitNulls = false } } + } + +val storeDependencies + get() = module { + singleOf(::CharacterSheetStore) + singleOf(::SettingsStore) + } + val repositoryDependencies get() = module { - singleOf(::CharacterSheetStore) singleOf(::NetworkRepository) singleOf(::CharacterSheetRepository) singleOf(::RollHistoryRepository) + singleOf(::SettingsRepository) } val factoryDependencies @@ -51,6 +69,7 @@ val factoryDependencies factoryOf(::NetworkFactory) factoryOf(::SkillFieldFactory) factoryOf(::SkillDescriptionFactory) + factoryOf(::SettingsFactory) } val viewModelDependencies @@ -74,4 +93,5 @@ val useCaseDependencies factoryOf(::RollUseCase) factoryOf(::SkillValueComputationUseCase) factoryOf(::CharacterSheetUseCase) + factoryOf(::SettingsUseCase) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SettingsUseCase.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SettingsUseCase.kt new file mode 100644 index 0000000..ead140d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SettingsUseCase.kt @@ -0,0 +1,10 @@ +package com.pixelized.desktop.lwa.business + +import com.pixelized.desktop.lwa.repository.settings.model.Settings + +class SettingsUseCase { + + fun defaultSettings(): Settings = Settings( + playerName = "", + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt index 16cf6ef..e67648a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt @@ -3,6 +3,7 @@ package com.pixelized.desktop.lwa.repository.characterSheet import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -12,7 +13,7 @@ import kotlinx.coroutines.flow.stateIn class CharacterSheetRepository( private val store: CharacterSheetStore, ) { - private val scope = CoroutineScope(Dispatchers.IO) + private val scope = CoroutineScope(Dispatchers.IO + Job()) private val sheets = store.characterSheetFlow() .stateIn( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt index a0e3986..2b7f0b3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt @@ -15,9 +15,9 @@ import java.io.File class CharacterSheetStore( private val factory: CharacterSheetJsonFactory, + private val jsonFormatter: Json, ) { private val characterDirectory = File(characterStorePath()).also { it.mkdirs() } - private val jsonFormatter: Json = Json { explicitNulls = false } private val flow = MutableStateFlow>(value = emptyList()) init { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt index c3deaea..a32fecb 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt @@ -5,6 +5,7 @@ package com.pixelized.desktop.lwa.repository.network //import io.ktor.server.netty.NettyApplicationEngine import com.pixelized.desktop.lwa.repository.network.helper.client import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket +import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame import com.pixelized.desktop.lwa.utils.extention.encodeToFrame import com.pixelized.server.lwa.SERVER_PORT @@ -26,7 +27,9 @@ import kotlinx.serialization.json.Json typealias Client = HttpClient -class NetworkRepository { +class NetworkRepository( + private val settingsRepository: SettingsRepository, +) { companion object { const val DEFAULT_PORT = SERVER_PORT const val DEFAULT_HOST = "pixelized.freeboxos.fr" @@ -40,15 +43,9 @@ class NetworkRepository { private val incomingMessageBuffer = MutableSharedFlow() val data: SharedFlow get() = incomingMessageBuffer - private val _player = MutableStateFlow("") - val player: StateFlow get() = _player - private val _status = MutableStateFlow(Status.DISCONNECTED) val status: StateFlow get() = _status - fun onPlayerNameChange(player: String) { - _player.value = player - } fun connect( host: String, @@ -101,11 +98,12 @@ class NetworkRepository { } suspend fun share( + playerName: String = settingsRepository.settings().playerName, content: MessageContent, ) { if (status.value == Status.CONNECTED) { val message = Message( - from = player.value, + from = playerName, value = content, ) // emit the message into the outgoing buffer diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt new file mode 100644 index 0000000..d5654b5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt @@ -0,0 +1,38 @@ +package com.pixelized.desktop.lwa.repository.settings + +import com.pixelized.desktop.lwa.business.SettingsUseCase +import com.pixelized.desktop.lwa.repository.settings.model.Settings +import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson +import com.pixelized.desktop.lwa.repository.settings.model.SettingsJsonV1 + + +class SettingsFactory( + private val useCase: SettingsUseCase, +) { + + fun convertToJson( + settings: Settings, + ): SettingsJson { + return SettingsJsonV1( + playerName = settings.playerName, + ) + } + + fun convertFromJson( + json: SettingsJson, + ): Settings { + return when (json) { + is SettingsJsonV1 -> convertFromJsonV1(json) + } + } + + private fun convertFromJsonV1( + json: SettingsJsonV1, + ): Settings { + return with(useCase.defaultSettings()) { + Settings( + playerName = json.playerName ?: playerName + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsRepository.kt new file mode 100644 index 0000000..aa4e04c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsRepository.kt @@ -0,0 +1,36 @@ +package com.pixelized.desktop.lwa.repository.settings + +import com.pixelized.desktop.lwa.business.SettingsUseCase +import com.pixelized.desktop.lwa.repository.settings.model.Settings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +class SettingsRepository( + private val store: SettingsStore, + private val useCase: SettingsUseCase, +) { + private val scope = CoroutineScope(Dispatchers.IO + Job()) + + private val settings = store.settingsFlow() + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = useCase.defaultSettings() + ) + + fun settingsFlow(): StateFlow = settings + + fun settings(): Settings = settings.value + + fun update(settings: Settings) { + store.save(settings = settings) + } + + fun reset() { + store.save(settings = useCase.defaultSettings()) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsStore.kt new file mode 100644 index 0000000..4e04927 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsStore.kt @@ -0,0 +1,90 @@ +package com.pixelized.desktop.lwa.repository.settings + +import com.pixelized.desktop.lwa.business.SettingsUseCase +import com.pixelized.desktop.lwa.repository.settings.model.Settings +import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson +import com.pixelized.desktop.lwa.repository.storePath +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File + +class SettingsStore( + private val factory: SettingsFactory, + private val useCase: SettingsUseCase, + private val jsonFormatter: Json, +) { + private val settingsDirectory = File(storePath()).also { it.mkdirs() } + private val flow = MutableStateFlow(value = useCase.defaultSettings()) + + fun settingsFlow(): StateFlow = flow + + init { + val scope = CoroutineScope(Dispatchers.IO + Job()) + scope.launch { + flow.value = load() + } + } + + @Throws( + SettingsStoreException::class, + FileWriteException::class, + JsonConversionException::class, + ) + fun save(settings: Settings) { + val json = try { + factory.convertToJson(settings = settings).let(jsonFormatter::encodeToString) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + try { + val file = settingsFile() + file.writeText( + text = json, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException( + root = exception + ) + } + flow.value = settings + } + + private fun load(): Settings { + return settingsFile().let { file -> + val json = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException( + root = exception + ) + } + if (json.isBlank()) { + return useCase.defaultSettings() + } + try { + factory.convertFromJson( + json = jsonFormatter.decodeFromString(json) + ) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + } + } + + private fun settingsFile(): File { + return File("${storePath()}settings.json") + } + + sealed class SettingsStoreException(root: Exception) : Exception(root) + class JsonConversionException(root: Exception) : SettingsStoreException(root) + class FileWriteException(root: Exception) : SettingsStoreException(root) + class FileReadException(root: Exception) : SettingsStoreException(root) +} + diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/Settings.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/Settings.kt new file mode 100644 index 0000000..20d82b5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/Settings.kt @@ -0,0 +1,5 @@ +package com.pixelized.desktop.lwa.repository.settings.model + +data class Settings( + val playerName: String, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJson.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJson.kt new file mode 100644 index 0000000..5266f80 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJson.kt @@ -0,0 +1,6 @@ +package com.pixelized.desktop.lwa.repository.settings.model + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface SettingsJson \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJsonV1.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJsonV1.kt new file mode 100644 index 0000000..45657c8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJsonV1.kt @@ -0,0 +1,8 @@ +package com.pixelized.desktop.lwa.repository.settings.model + +import kotlinx.serialization.Serializable + +@Serializable +data class SettingsJsonV1( + val playerName: String?, +) : SettingsJson \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt index e747022..34c5f36 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt @@ -12,13 +12,16 @@ import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.composable.blur.BlurContentController import com.pixelized.desktop.lwa.composable.error.snack.ErrorSnackUio import com.pixelized.desktop.lwa.repository.network.NetworkRepository +import com.pixelized.desktop.lwa.repository.settings.SettingsRepository +import com.pixelized.desktop.lwa.utils.extention.collectAsState import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch class NetworkViewModel( - private val repository: NetworkRepository, - private val factory: NetworkFactory + private val settingsRepository: SettingsRepository, + private val networkRepository: NetworkRepository, + private val factory: NetworkFactory, ) : ViewModel() { private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST) private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT) @@ -38,8 +41,8 @@ class NetworkViewModel( @Composable @Stable get() { - val player = repository.player.collectAsState() - val status = repository.status.collectAsState() + val player = settingsRepository.settingsFlow().collectAsState { it.playerName } + val status = networkRepository.status.collectAsState() return remember { derivedStateOf { factory.convertToUio( @@ -53,7 +56,11 @@ class NetworkViewModel( } fun onPlayerNameChange(player: String) { - repository.onPlayerNameChange(player = player) + settingsRepository.update( + settings = settingsRepository.settings().copy( + playerName = player, + ) + ) } fun onPortChange(port: String) { @@ -68,7 +75,7 @@ class NetworkViewModel( controller.show() _isLoading.value = true - repository.connect( + networkRepository.connect( host = host.value, port = port.value, onConnect = { @@ -86,6 +93,6 @@ class NetworkViewModel( } fun disconnect() { - repository.disconnect() + networkRepository.disconnect() } } \ No newline at end of file