new: persist preferences across sessions
This commit is contained in:
@@ -3,7 +3,27 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// Adding custom `dependsOn` edges below disables the automatic default hierarchy, so re-apply it
|
||||
// explicitly to keep the standard commonMain/appleMain/iosMain/webMain wiring.
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
androidLibrary {
|
||||
namespace = "fr.ajaury.gwenedeg.preferences"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
// Persistence via DataStore for every target that has an artifact for it (Android, iOS,
|
||||
// Desktop). Web (JS/Wasm) has no DataStore artifact, so it keeps commonMain's in-memory
|
||||
// implementation.
|
||||
val dataStoreMain by creating {
|
||||
dependsOn(commonMain.get())
|
||||
dependencies {
|
||||
implementation(libs.androidx.datastore.preferences.core)
|
||||
implementation(libs.okio)
|
||||
}
|
||||
}
|
||||
androidMain.get().dependsOn(dataStoreMain)
|
||||
jvmMain.get().dependsOn(dataStoreMain)
|
||||
iosMain.get().dependsOn(dataStoreMain)
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.ajaury.gwenedeg.preferences.di
|
||||
|
||||
import android.content.Context
|
||||
import fr.ajaury.gwenedeg.preferences.data.DATA_STORE_FILE_NAME
|
||||
import fr.ajaury.gwenedeg.preferences.data.DataStorePreferencesRepository
|
||||
import fr.ajaury.gwenedeg.preferences.data.createPreferencesDataStore
|
||||
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
actual val preferencesModule: Module = module {
|
||||
single<PreferencesRepository> {
|
||||
val context = get<Context>()
|
||||
DataStorePreferencesRepository(
|
||||
createPreferencesDataStore {
|
||||
context.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -8,22 +8,26 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class PreferencesRepositoryImpl : PreferencesRepository {
|
||||
/**
|
||||
* In-memory [PreferencesRepository] used on platforms without a DataStore artifact (web). Values do
|
||||
* not survive an app restart / page reload.
|
||||
*/
|
||||
internal class InMemoryPreferencesRepository : PreferencesRepository {
|
||||
private val subtitleState = MutableStateFlow(SubtitlePreferences())
|
||||
override val subtitlePreferences: StateFlow<SubtitlePreferences> = subtitleState.asStateFlow()
|
||||
|
||||
private val playbackState = MutableStateFlow(PlaybackPreferences())
|
||||
override val playbackPreferences: StateFlow<PlaybackPreferences> = playbackState.asStateFlow()
|
||||
|
||||
override fun setShowTranscription(enabled: Boolean) {
|
||||
override suspend fun setShowTranscription(enabled: Boolean) {
|
||||
subtitleState.update { it.copy(showTranscription = enabled) }
|
||||
}
|
||||
|
||||
override fun setShowTranslation(enabled: Boolean) {
|
||||
override suspend fun setShowTranslation(enabled: Boolean) {
|
||||
subtitleState.update { it.copy(showTranslation = enabled) }
|
||||
}
|
||||
|
||||
override fun setPlaybackSpeed(speed: Float) {
|
||||
override suspend fun setPlaybackSpeed(speed: Float) {
|
||||
playbackState.update { it.copy(speed = speed) }
|
||||
}
|
||||
}
|
||||
+5
-8
@@ -1,12 +1,9 @@
|
||||
package fr.ajaury.gwenedeg.preferences.di
|
||||
|
||||
import fr.ajaury.gwenedeg.preferences.data.PreferencesRepositoryImpl
|
||||
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.bind
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val preferencesModule: Module = module {
|
||||
singleOf(::PreferencesRepositoryImpl) { bind<PreferencesRepository>() }
|
||||
}
|
||||
/**
|
||||
* Binds [fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository]. Persisted with DataStore on
|
||||
* every target that ships it (Android, iOS, Desktop); web falls back to an in-memory implementation.
|
||||
*/
|
||||
expect val preferencesModule: Module
|
||||
|
||||
+6
-6
@@ -2,16 +2,16 @@ package fr.ajaury.gwenedeg.preferences.domain
|
||||
|
||||
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
|
||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PreferencesRepository {
|
||||
val subtitlePreferences: StateFlow<SubtitlePreferences>
|
||||
val subtitlePreferences: Flow<SubtitlePreferences>
|
||||
|
||||
val playbackPreferences: StateFlow<PlaybackPreferences>
|
||||
val playbackPreferences: Flow<PlaybackPreferences>
|
||||
|
||||
fun setShowTranscription(enabled: Boolean)
|
||||
suspend fun setShowTranscription(enabled: Boolean)
|
||||
|
||||
fun setShowTranslation(enabled: Boolean)
|
||||
suspend fun setShowTranslation(enabled: Boolean)
|
||||
|
||||
fun setPlaybackSpeed(speed: Float)
|
||||
suspend fun setPlaybackSpeed(speed: Float)
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package fr.ajaury.gwenedeg.preferences.data
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
|
||||
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
|
||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* [PreferencesRepository] persisting to a Preferences [DataStore]. Reads are exposed as hot
|
||||
* [StateFlow]s seeded with the defaults; writes are fire-and-forget on a background scope.
|
||||
*/
|
||||
internal class DataStorePreferencesRepository(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) : PreferencesRepository {
|
||||
override val subtitlePreferences: Flow<SubtitlePreferences> =
|
||||
dataStore.data
|
||||
.map { it.toSubtitlePreferences() }
|
||||
|
||||
override val playbackPreferences: Flow<PlaybackPreferences> =
|
||||
dataStore.data
|
||||
.map { it.toPlaybackPreferences() }
|
||||
|
||||
override suspend fun setShowTranscription(enabled: Boolean) = edit { it[ShowTranscriptionKey] = enabled }
|
||||
|
||||
override suspend fun setShowTranslation(enabled: Boolean) = edit { it[ShowTranslationKey] = enabled }
|
||||
|
||||
override suspend fun setPlaybackSpeed(speed: Float) = edit { it[PlaybackSpeedKey] = speed }
|
||||
|
||||
private suspend fun edit(transform: (MutablePreferences) -> Unit) {
|
||||
dataStore.edit(transform)
|
||||
}
|
||||
|
||||
private fun Preferences.toSubtitlePreferences(): SubtitlePreferences {
|
||||
val defaults = SubtitlePreferences()
|
||||
return SubtitlePreferences(
|
||||
showTranscription = this[ShowTranscriptionKey] ?: defaults.showTranscription,
|
||||
showTranslation = this[ShowTranslationKey] ?: defaults.showTranslation,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Preferences.toPlaybackPreferences(): PlaybackPreferences =
|
||||
PlaybackPreferences(
|
||||
speed = this[PlaybackSpeedKey] ?: PlaybackPreferences.DEFAULT_SPEED,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val ShowTranscriptionKey = booleanPreferencesKey("show_transcription")
|
||||
val ShowTranslationKey = booleanPreferencesKey("show_translation")
|
||||
val PlaybackSpeedKey = floatPreferencesKey("playback_speed")
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package fr.ajaury.gwenedeg.preferences.data
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import okio.Path.Companion.toPath
|
||||
|
||||
internal const val DATA_STORE_FILE_NAME = "gwenedeg.preferences_pb"
|
||||
|
||||
/** Builds a Preferences [DataStore] backed by the file at [producePath]. */
|
||||
internal fun createPreferencesDataStore(producePath: () -> String): DataStore<Preferences> =
|
||||
PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() })
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
@file:OptIn(ExperimentalForeignApi::class)
|
||||
|
||||
package fr.ajaury.gwenedeg.preferences.di
|
||||
|
||||
import fr.ajaury.gwenedeg.preferences.data.DATA_STORE_FILE_NAME
|
||||
import fr.ajaury.gwenedeg.preferences.data.DataStorePreferencesRepository
|
||||
import fr.ajaury.gwenedeg.preferences.data.createPreferencesDataStore
|
||||
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import platform.Foundation.NSDocumentDirectory
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
|
||||
actual val preferencesModule: Module = module {
|
||||
single<PreferencesRepository> {
|
||||
DataStorePreferencesRepository(
|
||||
createPreferencesDataStore {
|
||||
val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
|
||||
directory = NSDocumentDirectory,
|
||||
inDomain = NSUserDomainMask,
|
||||
appropriateForURL = null,
|
||||
create = false,
|
||||
error = null,
|
||||
)
|
||||
val path = requireNotNull(documentDirectory?.path) { "Missing documents directory" }
|
||||
"$path/$DATA_STORE_FILE_NAME"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.ajaury.gwenedeg.preferences.di
|
||||
|
||||
import fr.ajaury.gwenedeg.preferences.data.DATA_STORE_FILE_NAME
|
||||
import fr.ajaury.gwenedeg.preferences.data.DataStorePreferencesRepository
|
||||
import fr.ajaury.gwenedeg.preferences.data.createPreferencesDataStore
|
||||
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import java.io.File
|
||||
|
||||
actual val preferencesModule: Module = module {
|
||||
single<PreferencesRepository> {
|
||||
DataStorePreferencesRepository(
|
||||
createPreferencesDataStore {
|
||||
val directory = File(System.getProperty("user.home"), ".gwenedeg").apply { mkdirs() }
|
||||
File(directory, DATA_STORE_FILE_NAME).absolutePath
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package fr.ajaury.gwenedeg.preferences.di
|
||||
|
||||
import fr.ajaury.gwenedeg.preferences.data.InMemoryPreferencesRepository
|
||||
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.bind
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
actual val preferencesModule: Module = module {
|
||||
singleOf(::InMemoryPreferencesRepository) { bind<PreferencesRepository>() }
|
||||
}
|
||||
@@ -19,6 +19,8 @@ material3 = "1.11.0-alpha07"
|
||||
koin = "4.2.1"
|
||||
kermit = "2.1.0"
|
||||
vorbisspi = "1.0.3.3"
|
||||
datastore = "1.1.1"
|
||||
okio = "3.9.1"
|
||||
|
||||
[libraries]
|
||||
# Gradle plugin artifacts, used by build-logic convention plugins only
|
||||
@@ -28,6 +30,8 @@ compose-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin",
|
||||
composeCompiler-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
|
||||
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
|
||||
androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastore" }
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
||||
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
|
||||
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
|
||||
|
||||
+14
-6
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
@@ -77,8 +78,9 @@ class PlayerViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyCurrentSpeed() {
|
||||
playbackRepository.setSpeed(preferencesRepository.playbackPreferences.value.speed)
|
||||
private suspend fun applyCurrentSpeed() {
|
||||
val playbackPreferences = preferencesRepository.playbackPreferences.firstOrNull() ?: return
|
||||
playbackRepository.setSpeed(playbackPreferences.speed)
|
||||
}
|
||||
|
||||
fun performMainPlayAction() {
|
||||
@@ -111,16 +113,22 @@ class PlayerViewModel(
|
||||
PlaybackPreferences.MIN_SPEED,
|
||||
PlaybackPreferences.MAX_SPEED,
|
||||
)
|
||||
preferencesRepository.setPlaybackSpeed(clampedSpeed)
|
||||
playbackRepository.setSpeed(clampedSpeed)
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setPlaybackSpeed(clampedSpeed)
|
||||
playbackRepository.setSpeed(clampedSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
fun setShowTranscription(enabled: Boolean) {
|
||||
preferencesRepository.setShowTranscription(enabled)
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setShowTranscription(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun setShowTranslation(enabled: Boolean) {
|
||||
preferencesRepository.setShowTranslation(enabled)
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setShowTranslation(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
|
||||
Reference in New Issue
Block a user