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

+32 -1
View File
@@ -229,7 +229,10 @@ struct LibraryView: View {
}
}
}
if browsing {
// Only over the whole library: under a filter, a shelf of
// titles the grid below is hiding reads as the filter not
// working.
if browsing && model.filter == .all {
ForEach(shelves) { shelf in
VStack(alignment: .leading, spacing: 10) {
Text(shelf.label).textStyle(.titleMedium).foregroundStyle(scheme.onSurface)
@@ -248,6 +251,20 @@ struct LibraryView: View {
Text("All titles").textStyle(.titleMedium).foregroundStyle(scheme.onSurface).padding(.top, 8)
}
}
if browsing {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(SettingsStore.filters, id: \.self) { filter in
FilterChip(label: filter.label, selected: filter == model.filter) { model.setFilter(filter) }
}
}
}
if tiles.isEmpty && model.filter != .all {
Text("Nothing in the library fits that filter.")
.textStyle(.bodyMedium)
.foregroundStyle(scheme.onSurfaceVariant)
}
}
// Posters, three across on a phone.
LazyVGrid(columns: [GridItem(.adaptive(minimum: 108), spacing: 12, alignment: .top)], spacing: 16) {
ForEach(tiles) { tile in
@@ -421,6 +438,20 @@ private struct LibrarySkeleton: View {
}
}
extension LibraryFilter {
/// The desktop's names for the filters, from `browse::Filter::label`.
var label: String {
switch self {
case .all: "All"
case .series: "Series"
case .films: "Films"
case .watching: "Watching"
case .unwatched: "Not started"
case .downloaded: "Downloaded"
}
}
}
/// The offer of posters and synopses, and its two answers.
private struct MetadataOfferCard: View {
let onOpenSettings: () -> Void