feat(android,ios): open the library the way it was last sorted

Sort order and franchise folding reset on every launch on both phones.
They are kept with the front end's own settings now, and Android reads
them before the first grid is drawn, off the main thread.
This commit is contained in:
nvrl committed 2026-10-12 23:06:24 +02:00
1 parent d154002109
commit 9d72acce88
4 files changed
+60 -4

No files matched your search

@@ -1,6 +1,7 @@
package io.narl.protonstream.settings
import android.content.Context
import androidx.core.content.edit
/**
* The settings that are Android's alone.
@@ -35,6 +36,16 @@ class SettingsStore(context: Context) {
get() = preferences.getInt(KEY_CACHE_BUDGET_GIB, DEFAULT_CACHE_BUDGET_GIB)
set(value) { preferences.edit().putInt(KEY_CACHE_BUDGET_GIB, value).apply() }
/** How the library was last sorted, as a `LibrarySort` name; null for the default. */
var librarySort: String?
get() = preferences.getString(KEY_LIBRARY_SORT, null)
set(value) { preferences.edit { putString(KEY_LIBRARY_SORT, value) } }
/** Whether the library folds each franchise into one tile. */
var libraryGrouped: Boolean
get() = preferences.getBoolean(KEY_LIBRARY_GROUPED, true)
set(value) { preferences.edit { putBoolean(KEY_LIBRARY_GROUPED, value) } }
companion object {
/** The desktop's range, in the steps a phone's storage is bought in. */
val CACHE_BUDGET_CHOICES = listOf(1, 2, 4, 8, 16)
@@ -49,5 +60,7 @@ class SettingsStore(context: Context) {
private const val KEY_BACKGROUND_AUDIO = "background_audio"
private const val KEY_HARDWARE_DECODING = "hardware_decoding"
private const val KEY_CACHE_BUDGET_GIB = "cache_budget_gib"
private const val KEY_LIBRARY_SORT = "library_sort"
private const val KEY_LIBRARY_GROUPED = "library_grouped"
}
}
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.withContext
import io.narl.protonstream.native.NativeRuntime
import io.narl.protonstream.settings.SettingsStore
import io.narl.protonstream.ui.theme.AppearanceState
import uniffi.pstr_android.ArrangementRecord
import uniffi.pstr_android.LibrarySort
@@ -123,7 +124,17 @@ class AppViewModel(context: Context, private val workManager: WorkManager) : Vie
it.copy(message = "Certificate verification is unavailable; Proton requests will fail")
}
}
reload()
// The arrangement first, so the first grid is drawn the way the viewer
// left it rather than A–Z and then re-sorted under them.
viewModelScope.launch {
val (sort, grouped) = withContext(Dispatchers.IO) {
SettingsStore(appContext).run {
librarySort?.let { name -> LibrarySort.entries.firstOrNull { it.name == name } } to libraryGrouped
}
}
mutableState.update { it.copy(sort = sort ?: it.sort, grouped = grouped) }
reload()
}
catchUp()
viewModelScope.launch {
val account = runCatching { withContext(Dispatchers.IO) { NativeRuntime.engine().accountState() } }
@@ -153,9 +164,18 @@ class AppViewModel(context: Context, private val workManager: WorkManager) : Vie
}
/** Reorder the grid, or fold or unfold its franchises. */
/** Sort and fold the library, and keep both for the next launch. */
fun arrange(sort: LibrarySort, grouped: Boolean) {
mutableState.update { it.copy(sort = sort, grouped = grouped) }
viewModelScope.launch { reloadLibrary(mutableState.value.query) }
viewModelScope.launch {
withContext(Dispatchers.IO) {
SettingsStore(appContext).apply {
librarySort = sort.name
libraryGrouped = grouped
}
}
reloadLibrary(mutableState.value.query)
}
}
fun search(query: String) {
+6 -2
View File
@@ -44,8 +44,9 @@ final class AppModel {
/// `grouped`, and the shelves above it. Nil until the bridge has answered,
/// when the grid falls back to `titles` as they are.
var arrangement: ArrangementRecord?
var sort: LibrarySort = .name
var grouped = true
/// As the viewer last left them.
var sort: LibrarySort = SettingsStore().librarySort
var grouped = SettingsStore().libraryGrouped
var shares: [ShareRecord] = []
var offline: [OfflineRecord] = []
var metadataSettings = MetadataSettingsRecord(enabled: false, provider: .aniList, language: "en", ready: true)
@@ -105,6 +106,9 @@ final class AppModel {
func arrange(_ sort: LibrarySort, _ grouped: Bool) {
self.sort = sort
self.grouped = grouped
let settings = SettingsStore()
settings.librarySort = sort
settings.libraryGrouped = grouped
Task { await reloadLibrary(query) }
}
+19
View File
@@ -42,6 +42,23 @@ struct SettingsStore {
static func gibToBytes(_ gib: Int) -> UInt64 { UInt64(gib) * 1024 * 1024 * 1024 }
/// How the library was last sorted; `.name` until the viewer chooses.
var librarySort: LibrarySort {
get {
let stored = defaults.string(forKey: Key.librarySort)
return Self.sorts.first { String(describing: $0) == stored } ?? .name
}
nonmutating set { defaults.set(String(describing: newValue), forKey: Key.librarySort) }
}
/// Whether the library folds each franchise into one tile.
var libraryGrouped: Bool {
get { bool(Key.libraryGrouped, default: true) }
nonmutating set { defaults.set(newValue, forKey: Key.libraryGrouped) }
}
private static let sorts: [LibrarySort] = [.name, .recent, .added, .release, .rating, .popularity]
private func bool(_ key: String, default value: Bool) -> Bool {
defaults.object(forKey: key) as? Bool ?? value
}
@@ -51,5 +68,7 @@ struct SettingsStore {
static let backgroundAudio = "background_audio"
static let hardwareDecoding = "hardware_decoding"
static let cacheBudgetGib = "cache_budget_gib"
static let librarySort = "library_sort"
static let libraryGrouped = "library_grouped"
}
}