new: persist preferences across sessions
This commit is contained in:
@@ -3,7 +3,27 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
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 {
|
androidLibrary {
|
||||||
namespace = "fr.ajaury.gwenedeg.preferences"
|
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.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
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())
|
private val subtitleState = MutableStateFlow(SubtitlePreferences())
|
||||||
override val subtitlePreferences: StateFlow<SubtitlePreferences> = subtitleState.asStateFlow()
|
override val subtitlePreferences: StateFlow<SubtitlePreferences> = subtitleState.asStateFlow()
|
||||||
|
|
||||||
private val playbackState = MutableStateFlow(PlaybackPreferences())
|
private val playbackState = MutableStateFlow(PlaybackPreferences())
|
||||||
override val playbackPreferences: StateFlow<PlaybackPreferences> = playbackState.asStateFlow()
|
override val playbackPreferences: StateFlow<PlaybackPreferences> = playbackState.asStateFlow()
|
||||||
|
|
||||||
override fun setShowTranscription(enabled: Boolean) {
|
override suspend fun setShowTranscription(enabled: Boolean) {
|
||||||
subtitleState.update { it.copy(showTranscription = enabled) }
|
subtitleState.update { it.copy(showTranscription = enabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setShowTranslation(enabled: Boolean) {
|
override suspend fun setShowTranslation(enabled: Boolean) {
|
||||||
subtitleState.update { it.copy(showTranslation = enabled) }
|
subtitleState.update { it.copy(showTranslation = enabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setPlaybackSpeed(speed: Float) {
|
override suspend fun setPlaybackSpeed(speed: Float) {
|
||||||
playbackState.update { it.copy(speed = speed) }
|
playbackState.update { it.copy(speed = speed) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-8
@@ -1,12 +1,9 @@
|
|||||||
package fr.ajaury.gwenedeg.preferences.di
|
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.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.PlaybackPreferences
|
||||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface PreferencesRepository {
|
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"
|
koin = "4.2.1"
|
||||||
kermit = "2.1.0"
|
kermit = "2.1.0"
|
||||||
vorbisspi = "1.0.3.3"
|
vorbisspi = "1.0.3.3"
|
||||||
|
datastore = "1.1.1"
|
||||||
|
okio = "3.9.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Gradle plugin artifacts, used by build-logic convention plugins only
|
# 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" }
|
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-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-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
||||||
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", 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" }
|
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.StateFlow
|
||||||
import kotlinx.coroutines.flow.WhileSubscribed
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -77,8 +78,9 @@ class PlayerViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyCurrentSpeed() {
|
private suspend fun applyCurrentSpeed() {
|
||||||
playbackRepository.setSpeed(preferencesRepository.playbackPreferences.value.speed)
|
val playbackPreferences = preferencesRepository.playbackPreferences.firstOrNull() ?: return
|
||||||
|
playbackRepository.setSpeed(playbackPreferences.speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performMainPlayAction() {
|
fun performMainPlayAction() {
|
||||||
@@ -111,16 +113,22 @@ class PlayerViewModel(
|
|||||||
PlaybackPreferences.MIN_SPEED,
|
PlaybackPreferences.MIN_SPEED,
|
||||||
PlaybackPreferences.MAX_SPEED,
|
PlaybackPreferences.MAX_SPEED,
|
||||||
)
|
)
|
||||||
preferencesRepository.setPlaybackSpeed(clampedSpeed)
|
viewModelScope.launch {
|
||||||
playbackRepository.setSpeed(clampedSpeed)
|
preferencesRepository.setPlaybackSpeed(clampedSpeed)
|
||||||
|
playbackRepository.setSpeed(clampedSpeed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShowTranscription(enabled: Boolean) {
|
fun setShowTranscription(enabled: Boolean) {
|
||||||
preferencesRepository.setShowTranscription(enabled)
|
viewModelScope.launch {
|
||||||
|
preferencesRepository.setShowTranscription(enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShowTranslation(enabled: Boolean) {
|
fun setShowTranslation(enabled: Boolean) {
|
||||||
preferencesRepository.setShowTranslation(enabled)
|
viewModelScope.launch {
|
||||||
|
preferencesRepository.setShowTranslation(enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
|||||||
Reference in New Issue
Block a user