Ein Knopf an jeder Folge, und die Suche spielt nicht mehr sofort ab
**Staffelansicht: Download je Folge.** Für den Fall, den du beschrieben hast — Staffel halb gesehen, Rest nachholen. Ein Tipp öffnet dieselbe Qualitätsauswahl wie auf der Detailseite. Der Knopf steht **neben** dem Link, nicht darin: ein `Button` im Label eines `NavigationLink` löst auf iOS nie aus, und `.plain` lässt das lautlos geschehen. Dieselbe Falle steht in `QuickPlayTile` schon beschrieben. Als Geschwister in einer Reihe teilt das Layout den Platz auf, und der Titel kann gar nicht erst unter dem Knopf verschwinden. Die Kinderliste trägt keine Dateiangaben — ohne Nachladen wäre die Qualitätsliste leer und die Prüfung auf freien Speicher übersprungen. `DownloadButton` holt sie deshalb beim ersten Tipp nach (`loadsDetails`), mit Kreisel und fester Breite, damit die Zeile nicht springt. Nicht beim Erscheinen: zwanzig Zeilen wären zwanzig Anfragen für einen Knopf, den man meistens nicht drückt. Nur bei Folgen. Für eine ganze Staffel gibt es den Knopf oben schon. **Suche: Antippen führt auf die Seite.** Die Suche baut dieselbe Reihe wie die Startseite und war damit auch dieselbe Geste. Wer sucht, weiß aber noch nicht, was er sehen will — der erste Treffer ist eine Vermutung des Servers. Abspielen steht weiter im Menü unter langem Drücken. Dabei aufgefallen: `SearchView` hinterlegte `flixyItemChanged` nicht, anders als Startseite und Mediathek. „Als gesehen markieren" wirkte dort am Server, aber die Kachel lud nie neu — es sah aus, als wäre nichts passiert. Im Simulator geprüft: Knopf an jeder Folge, Auswahl mit echten Größen („Original · HD · 906,9 MB"), Suchtreffer öffnet die Detailseite. iOS und tvOS bauen, 489 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e86c0a6e95
commit
ad556c9034
@@ -12,6 +12,19 @@ import PlexKit
|
||||
struct DownloadButton: View {
|
||||
let item: MediaItem
|
||||
var mediaIndex: Int = 0
|
||||
/// Ob die Dateiangaben erst noch geholt werden müssen.
|
||||
///
|
||||
/// **Für Zeilen, die nur eine Zusammenfassung tragen.** Die Folgenliste
|
||||
/// einer Staffel kommt ohne Fassungen und ohne Dateigrößen — mit so einem
|
||||
/// Titel wäre die Qualitätsliste leer und die erwartete Größe null, was
|
||||
/// die Prüfung auf freien Speicherplatz stillschweigend überspringt.
|
||||
/// `DownloadService.downloadAll` holt jede Folge aus genau diesem Grund
|
||||
/// einzeln nach.
|
||||
///
|
||||
/// Geholt wird erst beim ersten Tipp: zwanzig Zeilen beim Erscheinen
|
||||
/// nachzuladen wären zwanzig Anfragen für einen Knopf, den man meistens
|
||||
/// nicht drückt.
|
||||
var loadsDetails: Bool = false
|
||||
|
||||
@Environment(FlixySession.self) private var session
|
||||
@Environment(DownloadCoordinator.self) private var downloads
|
||||
@@ -19,20 +32,40 @@ struct DownloadButton: View {
|
||||
|
||||
@State private var refusal: DownloadRefusal?
|
||||
@State private var showsQualityChoice = false
|
||||
/// Der nachgeholte Titel — gilt ab dann statt `item`.
|
||||
@State private var detailed: MediaItem?
|
||||
@State private var isLoadingDetails = false
|
||||
|
||||
private var existing: DownloadItem? {
|
||||
downloads.items.first { $0.ratingKey == item.id }
|
||||
}
|
||||
|
||||
/// Der Titel, aus dem Fassungen und Größen gelesen werden.
|
||||
///
|
||||
/// Die Kennung ist in beiden dieselbe — nur die Dateiangaben fehlen dem
|
||||
/// einen.
|
||||
private var source: MediaItem { detailed ?? item }
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
act()
|
||||
} label: {
|
||||
Label(label, systemImage: symbol)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.flixyCardTitle)
|
||||
.padding(FlixySpacing.snug)
|
||||
Group {
|
||||
if isLoadingDetails {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Label(label, systemImage: symbol)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.flixyCardTitle)
|
||||
}
|
||||
}
|
||||
// Feste Größe, damit der Knopf beim Nachladen nicht springt: der
|
||||
// Kreisel ist schmaler als das Symbol, und eine Zeile, die während
|
||||
// des Wartens die Breite wechselt, sieht aus wie ein Fehler.
|
||||
.frame(width: 22, height: 22)
|
||||
.padding(FlixySpacing.snug)
|
||||
}
|
||||
.disabled(isLoadingDetails)
|
||||
.buttonStyle(.bordered)
|
||||
.tint(existing?.state == .completed ? accent : Color.flixyTextSecondary)
|
||||
.accessibilityLabel(Text(label))
|
||||
@@ -63,7 +96,7 @@ struct DownloadButton: View {
|
||||
case .completed: "checkmark.circle.fill"
|
||||
case .downloading, .queued, .waiting: "stop.circle"
|
||||
case .paused: "play.circle"
|
||||
case .failed: "exclamationmark.arrow.trianglehead.2.clockwise.rotate.90"
|
||||
case .failed: continues ? "play.circle" : "exclamationmark.arrow.trianglehead.2.clockwise.rotate.90"
|
||||
case nil: "arrow.down.circle"
|
||||
}
|
||||
}
|
||||
@@ -73,17 +106,28 @@ struct DownloadButton: View {
|
||||
case .completed: "downloads.available"
|
||||
case .downloading, .queued, .waiting: "downloads.cancel"
|
||||
case .paused: "downloads.resume"
|
||||
case .failed: "action.retry"
|
||||
case .failed: continues ? "downloads.resume" : "action.retry"
|
||||
case nil: "downloads.download"
|
||||
}
|
||||
}
|
||||
|
||||
/// Ob ein Tipp dort weitermacht, wo es aufgehört hat.
|
||||
///
|
||||
/// Ein gescheiterter Download hat seinen Wiederaufnahme-Datensatz nur,
|
||||
/// wenn ihn das Netz unterbrochen hat — hat der Server abgelehnt, fängt
|
||||
/// es von vorn an, und dann heißt der Knopf auch so.
|
||||
private var continues: Bool { existing?.resumeData != nil }
|
||||
|
||||
private func act() {
|
||||
guard let existing else {
|
||||
// Vor dem Laden fragen. Der Unterschied zwischen einem
|
||||
// 40-GB-Remux und 2 GB in 720p ist auf einem Telefon keine
|
||||
// Nebensache — und hinterher lässt er sich nicht mehr ändern.
|
||||
showsQualityChoice = true
|
||||
guard loadsDetails, detailed == nil else {
|
||||
showsQualityChoice = true
|
||||
return
|
||||
}
|
||||
Task { await loadDetailsThenAsk() }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,17 +144,30 @@ struct DownloadButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Holt die Dateiangaben nach und fragt dann nach der Qualität.
|
||||
///
|
||||
/// Scheitert das Nachholen, wird trotzdem gefragt — dann steht nur
|
||||
/// „Original" zur Wahl, und die Ablehnung kommt mit einer Begründung aus
|
||||
/// `DownloadService`. Das ist besser als ein Knopf, der nach einem Tipp
|
||||
/// kommentarlos nichts tut.
|
||||
private func loadDetailsThenAsk() async {
|
||||
isLoadingDetails = true
|
||||
detailed = try? await session.backend?.item(id: item.id)
|
||||
isLoadingDetails = false
|
||||
showsQualityChoice = true
|
||||
}
|
||||
|
||||
private func enqueue(quality: DownloadQuality) {
|
||||
let service = DownloadService(backend: session.backend, coordinator: downloads)
|
||||
do {
|
||||
_ = try service.download(item, mediaIndex: mediaIndex, quality: quality)
|
||||
_ = try service.download(source, mediaIndex: mediaIndex, quality: quality)
|
||||
} catch {
|
||||
refusal = error
|
||||
}
|
||||
}
|
||||
|
||||
private var currentMedia: MediaVersion? {
|
||||
item.versions.indices.contains(mediaIndex) ? item.versions[mediaIndex] : item.preferredVersion
|
||||
source.versions.indices.contains(mediaIndex) ? source.versions[mediaIndex] : source.preferredVersion
|
||||
}
|
||||
|
||||
/// Nur Stufen, die unter der Auflösung der Datei liegen.
|
||||
|
||||
Reference in New Issue
Block a user