saving & restoring settings with KStore

This commit is contained in:
2024-08-05 05:41:22 +03:00
parent 3b65d44f2f
commit e0543fd2bf
12 changed files with 220 additions and 14 deletions
+3
View File
@@ -72,6 +72,8 @@ kotlin {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(libs.kstore)
implementation(libs.kstore.file)
}
commonTest.dependencies {
@@ -92,6 +94,7 @@ kotlin {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
implementation(libs.ktor.client.okhttp)
implementation(libs.appdirs)
}
iosMain.dependencies {
@@ -8,8 +8,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
class AppActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appDir = filesDir.path
enableEdgeToEdge()
setContent { App() }
}
@@ -17,4 +19,6 @@ class AppActivity : ComponentActivity() {
@Preview
@Composable
fun AppPreview() { App() }
fun AppPreview() {
App()
}
@@ -3,15 +3,27 @@ package dev.meloda.overseerr
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.FadeTransition
import dev.meloda.overseerr.screens.main.MainScreen
import dev.meloda.overseerr.settings.SettingsController
import dev.meloda.overseerr.theme.AppTheme
import org.koin.compose.KoinContext
import org.koin.compose.koinInject
var appDir: String = ""
@Composable
internal fun App() = KoinContext {
val settingsController: SettingsController = koinInject()
LaunchedEffect(true) {
settingsController.loadAppSettings()
}
AppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Navigator(MainScreen()) { navigator ->
@@ -4,6 +4,7 @@ import dev.meloda.overseerr.model.Platform
import dev.meloda.overseerr.network.di.networkModule
import dev.meloda.overseerr.screens.login.di.loginModule
import dev.meloda.overseerr.screens.url.di.urlModule
import dev.meloda.overseerr.settings.di.settingsModule
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@@ -11,6 +12,7 @@ val appModule = module {
singleOf(::Platform)
includes(
settingsModule,
networkModule,
loginModule,
urlModule
@@ -1,11 +1,58 @@
package dev.meloda.overseerr.screens.url
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.meloda.overseerr.ext.setValue
import dev.meloda.overseerr.screens.url.model.UrlScreenState
import dev.meloda.overseerr.settings.SettingsController
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
interface UrlViewModel {
val screenState: StateFlow<UrlScreenState>
fun onUrlInputChanged(newText: String)
fun onLoadButtonClicked()
fun onSaveButtonClicked()
fun onTestButtonClicked()
}
class UrlViewModelImpl : ViewModel(), UrlViewModel {
class UrlViewModelImpl(
private val settingsController: SettingsController
) : ViewModel(), UrlViewModel {
override val screenState = MutableStateFlow(UrlScreenState.EMPTY)
init {
settingsController.settings
.onEach { settings -> screenState.setValue { old -> old.copy(url = settings.url) } }
.launchIn(viewModelScope)
}
override fun onUrlInputChanged(newText: String) {
screenState.setValue { old -> old.copy(url = newText) }
}
override fun onLoadButtonClicked() {
viewModelScope.launch {
val settings = settingsController.loadAppSettings()
screenState.setValue { old -> old.copy(url = settings.url) }
}
}
override fun onSaveButtonClicked() {
viewModelScope.launch {
settingsController.updateAppSettings { settings ->
settings.copy(url = screenState.value.url)
}
}
}
override fun onTestButtonClicked() {
}
}
@@ -0,0 +1,13 @@
package dev.meloda.overseerr.screens.url.model
data class UrlScreenState(
val url: String,
val isWrongUrlError: Boolean
) {
companion object {
val EMPTY: UrlScreenState = UrlScreenState(
url = "",
isWrongUrlError = false
)
}
}
@@ -1,19 +1,26 @@
package dev.meloda.overseerr.screens.url.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.meloda.overseerr.screens.login.presentation.LoginScreen
import dev.meloda.overseerr.screens.url.UrlViewModel
import dev.meloda.overseerr.screens.url.UrlViewModelImpl
import dev.meloda.overseerr.settings.SettingsController
import org.koin.compose.koinInject
class UrlScreen : Screen {
@@ -21,7 +28,11 @@ class UrlScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel: UrlViewModel = viewModel { UrlViewModelImpl() }
val settingsController: SettingsController = koinInject()
val viewModel: UrlViewModel = viewModel { UrlViewModelImpl(settingsController) }
val screenState by viewModel.screenState.collectAsState()
Scaffold(
modifier = Modifier.fillMaxSize(),
@@ -39,18 +50,52 @@ class UrlScreen : Screen {
)
}
) { padding ->
Box(
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
.padding(bottom = padding.calculateBottomPadding())
.padding(padding)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier.fillMaxWidth(),
value = screenState.url,
onValueChange = viewModel::onUrlInputChanged,
placeholder = { Text(text = "Url") },
isError = screenState.isWrongUrlError,
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Uri
)
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(text = "Input url screen")
Button(
onClick = { navigator.push(LoginScreen()) }
onClick = viewModel::onLoadButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Text(text = "Next")
Text(text = "Load")
}
Button(
onClick = viewModel::onSaveButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Text(text = "Save")
}
Button(
onClick = viewModel::onTestButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Text(text = "Test")
}
}
}
}
@@ -0,0 +1,34 @@
package dev.meloda.overseerr.settings
import dev.meloda.overseerr.ext.setValue
import dev.meloda.overseerr.settings.model.AppSettings
import io.github.xxfast.kstore.KStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
interface SettingsController {
val settings: StateFlow<AppSettings>
suspend fun updateAppSettings(update: (AppSettings) -> AppSettings)
suspend fun loadAppSettings(): AppSettings
}
class SettingsControllerImpl(
private val store: KStore<AppSettings>
) : SettingsController {
override val settings = MutableStateFlow(AppSettings.EMPTY)
override suspend fun updateAppSettings(update: (AppSettings) -> AppSettings) = withContext(Dispatchers.IO) {
store.set(update(settings.value))
}
override suspend fun loadAppSettings(): AppSettings = withContext(Dispatchers.IO) {
val loadedSettings = store.get() ?: AppSettings.EMPTY
settings.setValue { loadedSettings }
loadedSettings
}
}
@@ -0,0 +1,18 @@
package dev.meloda.overseerr.settings.di
import dev.meloda.overseerr.appDir
import dev.meloda.overseerr.settings.SettingsController
import dev.meloda.overseerr.settings.SettingsControllerImpl
import dev.meloda.overseerr.settings.model.AppSettings
import io.github.xxfast.kstore.file.storeOf
import okio.Path.Companion.toPath
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
val settingsModule = module {
single {
storeOf<AppSettings>(file = "$appDir/app_settings.json".toPath())
}
singleOf(::SettingsControllerImpl) bind SettingsController::class
}
@@ -0,0 +1,14 @@
package dev.meloda.overseerr.settings.model
import kotlinx.serialization.Serializable
@Serializable
data class AppSettings(
val url: String
) {
companion object {
val EMPTY: AppSettings = AppSettings(
url = ""
)
}
}
+10
View File
@@ -3,11 +3,21 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import dev.meloda.overseerr.App
import dev.meloda.overseerr.appDir
import dev.meloda.overseerr.di.appModule
import net.harawata.appdirs.AppDirsFactory
import org.koin.core.context.startKoin
import java.awt.Dimension
import java.io.File
fun main() = application {
appDir = AppDirsFactory.getInstance()
.getUserDataDir("Overseerr-KMP", "1.0.0", "dev.meloda")
println("appDir: $appDir")
File(appDir).mkdirs()
startKoin {
modules(appModule)
}
+5 -1
View File
@@ -14,6 +14,8 @@ multiplatformSettings = "1.1.1"
koin = "4.0.0-RC1"
viewmodel-compose = "2.8.0"
haze = "0.7.3"
kstore = "0.8.0"
appdirs = "1.2.2"
[libraries]
@@ -39,7 +41,9 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" }
[plugins]