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" } }