feat: add auto-save/load character to local storage (Closes #28)
Use multiplatform-settings for cross-platform persistence. Character auto-loads from storage on launch (falls back to EXAMPLE_CHARACTER). Auto-saves on every change with 500ms debounce via viewModelScope. Uses DataLoader.serialize/deserialize for JSON conversion. Works on all platform targets (Android, Desktop, Web, iOS). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,37 @@
|
|||||||
package org.shahondin1624.viewmodel
|
package org.shahondin1624.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.shahondin1624.lib.functions.DataLoader
|
||||||
import org.shahondin1624.model.EXAMPLE_CHARACTER
|
import org.shahondin1624.model.EXAMPLE_CHARACTER
|
||||||
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel holding the current character as observable state.
|
* ViewModel holding the current character as observable state.
|
||||||
* Foundation for all editing stories (6.2-6.6) and interactive features.
|
* Auto-saves to local storage on changes (debounced 500ms) and
|
||||||
|
* auto-loads from storage on launch, falling back to EXAMPLE_CHARACTER.
|
||||||
*/
|
*/
|
||||||
class CharacterViewModel : ViewModel() {
|
class CharacterViewModel : ViewModel() {
|
||||||
|
|
||||||
private val _character = MutableStateFlow(EXAMPLE_CHARACTER)
|
private val settings = Settings()
|
||||||
|
|
||||||
|
private val _character = MutableStateFlow(loadCharacter())
|
||||||
|
|
||||||
/** Observable character state. */
|
/** Observable character state. */
|
||||||
val character: StateFlow<ShadowrunCharacter> = _character.asStateFlow()
|
val character: StateFlow<ShadowrunCharacter> = _character.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
startAutoSave()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the current character entirely.
|
* Replace the current character entirely.
|
||||||
*/
|
*/
|
||||||
@@ -31,4 +45,60 @@ class CharacterViewModel : ViewModel() {
|
|||||||
fun updateCharacter(transform: (ShadowrunCharacter) -> ShadowrunCharacter) {
|
fun updateCharacter(transform: (ShadowrunCharacter) -> ShadowrunCharacter) {
|
||||||
_character.value = transform(_character.value)
|
_character.value = transform(_character.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current character immediately.
|
||||||
|
*/
|
||||||
|
fun saveNow() {
|
||||||
|
saveCharacter(_character.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load character from local storage, falling back to EXAMPLE_CHARACTER.
|
||||||
|
*/
|
||||||
|
private fun loadCharacter(): ShadowrunCharacter {
|
||||||
|
return try {
|
||||||
|
val json = settings.getStringOrNull(STORAGE_KEY)
|
||||||
|
if (json != null) {
|
||||||
|
DataLoader.deserialize(json)
|
||||||
|
} else {
|
||||||
|
EXAMPLE_CHARACTER
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If deserialization fails (e.g., schema change), fall back to example
|
||||||
|
EXAMPLE_CHARACTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save character to local storage as JSON.
|
||||||
|
*/
|
||||||
|
private fun saveCharacter(character: ShadowrunCharacter) {
|
||||||
|
try {
|
||||||
|
val json = DataLoader.serialize(character)
|
||||||
|
settings.putString(STORAGE_KEY, json)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Silently ignore save failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start auto-saving on character changes with 500ms debounce.
|
||||||
|
* Drops the initial emission (load) to avoid unnecessary save.
|
||||||
|
*/
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
private fun startAutoSave() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_character
|
||||||
|
.drop(1) // Skip initial value (already saved or loaded)
|
||||||
|
.debounce(500)
|
||||||
|
.collect { character ->
|
||||||
|
saveCharacter(character)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STORAGE_KEY = "shadowrun_character"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user