Add settings store/repo etc. to save user settings.

This commit is contained in:
Thomas Andres Gomez 2024-12-01 12:32:06 +01:00
parent 992c79a100
commit 2058a6a789
12 changed files with 238 additions and 19 deletions

View file

@ -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)
}

View file

@ -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 = "",
)
}

View file

@ -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(

View file

@ -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<List<CharacterSheet>>(value = emptyList())
init {

View file

@ -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<Message>()
val data: SharedFlow<Message> get() = incomingMessageBuffer
private val _player = MutableStateFlow("")
val player: StateFlow<String> get() = _player
private val _status = MutableStateFlow(Status.DISCONNECTED)
val status: StateFlow<Status> 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

View file

@ -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
)
}
}
}

View file

@ -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> = settings
fun settings(): Settings = settings.value
fun update(settings: Settings) {
store.save(settings = settings)
}
fun reset() {
store.save(settings = useCase.defaultSettings())
}
}

View file

@ -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<Settings> = 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<SettingsJson>(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)
}

View file

@ -0,0 +1,5 @@
package com.pixelized.desktop.lwa.repository.settings.model
data class Settings(
val playerName: String,
)

View file

@ -0,0 +1,6 @@
package com.pixelized.desktop.lwa.repository.settings.model
import kotlinx.serialization.Serializable
@Serializable
sealed interface SettingsJson

View file

@ -0,0 +1,8 @@
package com.pixelized.desktop.lwa.repository.settings.model
import kotlinx.serialization.Serializable
@Serializable
data class SettingsJsonV1(
val playerName: String?,
) : SettingsJson

View file

@ -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()
}
}