feat(core,app,android,ios): filter the library on every client, including by download

The library filter moves from the desktop into pstr_core::browse and
gains a Downloaded filter: titles with at least one episode kept on the
device. The bridge's arrangement() takes the filter. Android and iOS get
a row of filter chips above the grid. The choice is remembered per
device, and shelves stay hidden under a filter, as on the desktop.
This commit is contained in:
nvrl committed 2026-10-13 15:36:29 +02:00
1 parent fdf6b0359b
commit eb67df0aa0
11 files changed
+292 -88

No files matched your search

+12 -2
View File
@@ -47,6 +47,7 @@ final class AppModel {
/// when the grid falls back to `titles` as they are.
var arrangement: ArrangementRecord?
/// As the viewer last left them.
var filter: LibraryFilter = SettingsStore().libraryFilter
var sort: LibrarySort = SettingsStore().librarySort
var grouped = SettingsStore().libraryGrouped
var shares: [ShareRecord] = []
@@ -118,6 +119,13 @@ final class AppModel {
Task { await reloadLibrary(query) }
}
/// Narrow the grid to some titles, and keep the choice for the next launch.
func setFilter(_ filter: LibraryFilter) {
self.filter = filter
SettingsStore().libraryFilter = filter
Task { await reloadLibrary(query) }
}
func search(_ query: String) {
self.query = query
searchTask?.cancel()
@@ -787,6 +795,7 @@ final class AppModel {
/// `reload`, for a caller that goes on to read what it loaded.
func reloadNow() async {
let query = query.trimmingCharacters(in: .whitespaces).isEmpty ? nil : self.query
let filter = filter
let sort = sort
let grouped = grouped
do {
@@ -798,7 +807,7 @@ final class AppModel {
return Reloaded(
shares: try engine.shares(),
titles: try engine.library(search: query),
arrangement: try engine.arrangement(search: query, sort: sort, grouped: grouped),
arrangement: try engine.arrangement(search: query, filter: filter, sort: sort, grouped: grouped),
offline: try engine.offlineFiles(),
metadataSettings: try engine.metadataSettings(),
storage: try engine.storageUsage()
@@ -819,11 +828,12 @@ final class AppModel {
private func reloadLibrary(_ query: String) async {
let search = query.trimmingCharacters(in: .whitespaces).isEmpty ? nil : query
let filter = filter
let sort = sort
let grouped = grouped
do {
let (titles, arrangement) = try await run { engine in
(try engine.library(search: search), try engine.arrangement(search: search, sort: sort, grouped: grouped))
(try engine.library(search: search), try engine.arrangement(search: search, filter: filter, sort: sort, grouped: grouped))
}
self.titles = titles
self.arrangement = arrangement
+11
View File
@@ -51,6 +51,15 @@ struct SettingsStore {
nonmutating set { defaults.set(String(describing: newValue), forKey: Key.librarySort) }
}
/// Which titles the library last showed.
var libraryFilter: LibraryFilter {
get {
let stored = defaults.string(forKey: Key.libraryFilter)
return Self.filters.first { String(describing: $0) == stored } ?? .all
}
nonmutating set { defaults.set(String(describing: newValue), forKey: Key.libraryFilter) }
}
/// The library's offer of posters and synopses was turned down.
var metadataOfferDismissed: Bool {
get { bool(Key.metadataOfferDismissed, default: false) }
@@ -63,6 +72,7 @@ struct SettingsStore {
nonmutating set { defaults.set(newValue, forKey: Key.libraryGrouped) }
}
static let filters: [LibraryFilter] = [.all, .series, .films, .watching, .unwatched, .downloaded]
private static let sorts: [LibrarySort] = [.name, .recent, .added, .release, .rating, .popularity]
private func bool(_ key: String, default value: Bool) -> Bool {
@@ -75,6 +85,7 @@ struct SettingsStore {
static let hardwareDecoding = "hardware_decoding"
static let cacheBudgetGib = "cache_budget_gib"
static let librarySort = "library_sort"
static let libraryFilter = "library_filter"
static let libraryGrouped = "library_grouped"
static let metadataOfferDismissed = "metadata_offer_dismissed"
}