saving & restoring settings with KStore
This commit is contained in:
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+55
-10
@@ -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 = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user