From 9d72acce88daf2421cba380ed0d9307cf50ccc16 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 12 Oct 2026 23:03:33 +0200 Subject: [PATCH] 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. --- .../protonstream/settings/SettingsStore.kt | 13 ++++++++++ .../io/narl/protonstream/ui/AppViewModel.kt | 24 +++++++++++++++++-- ios/ProtonStream/App/AppModel.swift | 8 +++++-- ios/ProtonStream/App/SettingsStore.swift | 19 +++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/io/narl/protonstream/settings/SettingsStore.kt b/android/app/src/main/java/io/narl/protonstream/settings/SettingsStore.kt index b89c439..9794492 100644 --- a/android/app/src/main/java/io/narl/protonstream/settings/SettingsStore.kt +++ b/android/app/src/main/java/io/narl/protonstream/settings/SettingsStore.kt @@ -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" } } diff --git a/android/app/src/main/java/io/narl/protonstream/ui/AppViewModel.kt b/android/app/src/main/java/io/narl/protonstream/ui/AppViewModel.kt index 5a4c4c1..cb0ae37 100644 --- a/android/app/src/main/java/io/narl/protonstream/ui/AppViewModel.kt +++ b/android/app/src/main/java/io/narl/protonstream/ui/AppViewModel.kt @@ -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) { diff --git a/ios/ProtonStream/App/AppModel.swift b/ios/ProtonStream/App/AppModel.swift index 7197257..f0df162 100644 --- a/ios/ProtonStream/App/AppModel.swift +++ b/ios/ProtonStream/App/AppModel.swift @@ -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) } } diff --git a/ios/ProtonStream/App/SettingsStore.swift b/ios/ProtonStream/App/SettingsStore.swift index 26ca77d..8810147 100644 --- a/ios/ProtonStream/App/SettingsStore.swift +++ b/ios/ProtonStream/App/SettingsStore.swift @@ -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" } }