From 0a394b9b3b570fd2440b37e0fb1bb9c2e0cb48d9 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 11 Oct 2026 20:56:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=80=9EWartet"=20f=C3=BCr=20immer,=20obwohl?= =?UTF-8?q?=20der=20Server=20l=C3=A4ngst=20fertig=20war?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Eintrag ging nach einer 202 zurück auf „wartet" und rief danach `resume`. Das nimmt aber nur auf, was **angehalten oder gescheitert** ist — ein wartender lief in die Schutzklausel und kam nie wieder heraus. Im Protokoll packte ffmpeg um, in der App stand „wartet", und dazwischen lag nichts. Jetzt wird die Warteschlange abgearbeitet statt ein einzelner Eintrag aufgenommen; „wartet" steht darin. Dazu zwei Dinge, um die du gebeten hast: **Das Protokoll liest sich wie eine Kommandozeile.** Älteste Zeile oben, neueste unten, die Ansicht steht am Ende. Mitgeschoben wird nur, wenn man unten steht — wer hochgescrollt hat, liest etwas, und ihn bei jeder neuen Zeile ans Ende zu reissen wäre, als nähme man ihm die Seite aus der Hand. **Ein Tipp auf eine Kachel führt auf die Detailseite**, am Telefon wie am iPad wie am Mac. Nur in „Weiterschauen" spielt er ab: dort steht der Titel, *weil* man ihn angefangen hat. Vorher war es umgekehrt, mit dem Argument „wer eine Kachel antippt, weiss schon, was er sehen will". Im Gebrauch stimmte das nicht — wer nachsehen wollte, wie viele Staffeln eine Serie hat, bekam den Film. Ein Umweg von einem Schritt ist ärgerlich; eine Wiedergabe, die man nicht wollte, ist schlimmer. Co-Authored-By: Claude Opus 5 --- Flaro/Views/Components/QuickPlayTile.swift | 44 +++++++++---------- Flaro/Views/Home/MediaRow.swift | 9 ++-- Flaro/Views/Search/SearchView.swift | 2 +- .../Engine/DownloadCoordinator.swift | 8 +++- web/app/src/views/einstellungen/protokolle.ts | 23 +++++++--- 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/Flaro/Views/Components/QuickPlayTile.swift b/Flaro/Views/Components/QuickPlayTile.swift index 29d287e..d5a05fe 100644 --- a/Flaro/Views/Components/QuickPlayTile.swift +++ b/Flaro/Views/Components/QuickPlayTile.swift @@ -17,14 +17,18 @@ import MediaKit /// und wäre für VoiceOver gar nicht mehr da. struct QuickPlayTile: View { let item: MediaItem - /// Ob ein Tipp auf die Detailseite führt, statt abzuspielen. + /// Ob ein Tipp sofort abspielt, statt auf die Detailseite zu führen. /// - /// **Für die Suche.** Dort weiß man gerade *nicht*, was man sehen will — - /// sonst hätte man nicht gesucht. Ein Tipp, der sofort abspielt, ist auf - /// der Startseite richtig (die Kachel stand schon da, man kennt sie) und - /// in einer Trefferliste falsch: der erste Treffer ist eine Vermutung des - /// Servers, keine Entscheidung des Nutzers. - var opensDetails: Bool = false + /// **Nur beim Weiterschauen.** Dort ist die Absicht eindeutig: der Titel + /// steht in dieser Reihe, *weil* man ihn angefangen hat. + /// + /// **Überall sonst führt der Tipp auf die Seite.** Vorher war es + /// umgekehrt, mit dem Argument „wer eine Kachel antippt, weiss meistens + /// schon, was er sehen will". Im Gebrauch stimmte das nicht: wer nur + /// nachsehen will, wie viele Staffeln eine Serie hat, bekam den Film. + /// Ein Umweg von einem Schritt ist ärgerlich; eine Wiedergabe, die man + /// nicht wollte, ist schlimmer. + var startsPlayback: Bool = false @ViewBuilder let card: () -> Card @Environment(FlaroSession.self) private var session @@ -69,8 +73,9 @@ struct QuickPlayTile: View { /// /// Drei Gründe, derselbe Ausgang: am Mac ist der Klick seit jeher der Weg /// auf die Seite (zum Starten gibt es den Knopf beim Überfahren), eine - /// Sammlung hat nichts abzuspielen, und in der Suche ist es gewollt. - private var tapOpensDetails: Bool { isMac || opensDetails || !isPlayable } + /// Sammlung hat nichts abzuspielen, und überall ausser beim + /// Weiterschauen ist die Seite das Ziel. + private var tapOpensDetails: Bool { isMac || !startsPlayback || !isPlayable } var body: some View { kachel @@ -99,11 +104,9 @@ struct QuickPlayTile: View { // Kachel steht. .contextMenu { if isPlayable { - // **Am Telefon und am iPad führt der Weg zur Detailseite - // hierher.** Antippen spielt dort ab; wer stattdessen - // nachsehen will, was das überhaupt ist, drückt lang. Am Mac - // ist es umgekehrt: dort öffnet der Klick die Seite, und zum - // Starten gibt es den Knopf beim Überfahren. + // **Nur dort, wo der Tipp abspielt.** Sonst führt er + // schon auf die Seite, und ein zweiter Weg dorthin im Menü + // wäre ein Eintrag, der nichts Neues anbietet. if !tapOpensDetails { Button("hero.details", systemImage: "info.circle") { push(route) } Divider() @@ -199,14 +202,11 @@ struct QuickPlayTile: View { /// Die Kachel selbst — und was ein Antippen bedeutet. /// - /// **Am Telefon und am iPad spielt es ab.** Wer eine Kachel antippt, weiss - /// meistens schon, was er sehen will; der Umweg über die Detailseite - /// kostete zwei Schritte für eine Absicht. Die Seite ist weiter da — über - /// langes Drücken, wo auch „gesehen" und „entfernen" stehen. - /// - /// **Am Mac bleibt der Klick die Detailseite.** Dort gibt es den - /// Abspielknopf beim Überfahren, und langes Drücken ist keine übliche - /// Geste — ein Rechtsklick ist der Weg ins Menü, nicht in den Film. + /// **Der Tipp führt auf die Detailseite**, am Telefon wie am iPad wie am + /// Mac. Nur in „Weiterschauen" spielt er ab: dort steht der Titel, + /// *weil* man ihn angefangen hat. Abspielen ist sonst einen Schritt + /// weiter — über den Knopf auf der Seite, über langes Drücken, oder am + /// Mac über den Knopf beim Überfahren. /// /// Ein `NavigationLink` schiebt selbst; ein Knopf braucht dafür den Weg /// aus der Umgebung. Deshalb zwei Fassungen und nicht eine mit `if` im diff --git a/Flaro/Views/Home/MediaRow.swift b/Flaro/Views/Home/MediaRow.swift index cc503f8..59de4f8 100644 --- a/Flaro/Views/Home/MediaRow.swift +++ b/Flaro/Views/Home/MediaRow.swift @@ -5,8 +5,11 @@ import MediaKit /// Eine waagerechte Reihe von Titeln. struct MediaRow: View { let row: MediaRowGroup - /// Wird durchgereicht — siehe `QuickPlayTile`. - var opensDetails: Bool = false + /// Ob ein Tipp sofort abspielt — siehe `QuickPlayTile`. + /// + /// **Die Reihe weiss es, nicht die Kachel.** „Weiterschauen" ist eine + /// Eigenschaft der Reihe; eine Kachel kennt nur ihren Titel. + private var startsPlayback: Bool { row.isContinueWatching } @Environment(FlaroSession.self) private var session @@ -32,7 +35,7 @@ struct MediaRow: View { ScrollView(.horizontal) { LazyHStack(spacing: FlaroSpacing.regular) { ForEach(row.items) { item in - QuickPlayTile(item: item, opensDetails: opensDetails) { card(for: item) } + QuickPlayTile(item: item, startsPlayback: startsPlayback) { card(for: item) } } } .padding(.horizontal, FlaroSpacing.screenMargin) diff --git a/Flaro/Views/Search/SearchView.swift b/Flaro/Views/Search/SearchView.swift index 87882b1..f667d12 100644 --- a/Flaro/Views/Search/SearchView.swift +++ b/Flaro/Views/Search/SearchView.swift @@ -48,7 +48,7 @@ struct SearchView: View { // weiß aber noch nicht, was er sehen will; der erste // Treffer ist eine Vermutung des Servers. Starten geht // weiter, über langes Drücken. - MediaRow(row: hub, opensDetails: true) + MediaRow(row: hub) } } .padding(.vertical, FlaroSpacing.loose) diff --git a/Packages/FlaroKit/Sources/FlaroDownloads/Engine/DownloadCoordinator.swift b/Packages/FlaroKit/Sources/FlaroDownloads/Engine/DownloadCoordinator.swift index 3fce4ec..40e1fd1 100644 --- a/Packages/FlaroKit/Sources/FlaroDownloads/Engine/DownloadCoordinator.swift +++ b/Packages/FlaroKit/Sources/FlaroDownloads/Engine/DownloadCoordinator.swift @@ -648,10 +648,14 @@ public final class DownloadCoordinator { item.state = .waiting save() + // **Nicht `resume`.** Das nimmt nur auf, was angehalten oder + // gescheitert ist — ein wartender Eintrag lief dort in die + // Schutzklausel und blieb für immer auf „wartet" stehen, während der + // Server längst fertig gepackt hatte. Die Warteschlange abzuarbeiten + // ist das, was hier gebraucht wird: `.waiting` steht darin. Task { [weak self] in try? await Task.sleep(for: Self.wartezeit) - guard let self else { return } - await self.resume(item) + await self?.startNextIfPossible() } } diff --git a/web/app/src/views/einstellungen/protokolle.ts b/web/app/src/views/einstellungen/protokolle.ts index bbc840a..e5247db 100644 --- a/web/app/src/views/einstellungen/protokolle.ts +++ b/web/app/src/views/einstellungen/protokolle.ts @@ -10,9 +10,14 @@ import { abschnitt } from './teile'; * was gerade schiefgeht, brauchte drei Handgriffe für eine Antwort, die auf * der ersten Seite hätte stehen können. * - * **Jetzt steht es sofort da und läuft mit.** Neueste Zeile oben, damit man - * zum Mitlesen nicht scrollen muss; die älteren Tage stehen darunter als - * Kacheln zum Herunterladen. + * **Jetzt steht es sofort da und läuft mit.** Wie eine Kommandozeile: + * älteste Zeile oben, neueste unten, und die Ansicht steht am Ende. Das ist + * die Richtung, in der ein Logbuch gelesen wird — man sieht, was zuletzt + * geschah, und scrollt nach oben, um zu sehen, was davor war. + * + * **Nur mitgeschoben, wenn man unten steht.** Wer hochgescrollt hat, liest + * etwas; ihn bei jeder neuen Zeile ans Ende zu reissen wäre, als würde man + * ihm die Seite aus der Hand nehmen. */ /** Wie viele Zeilen anfangs geholt werden. */ @@ -30,10 +35,13 @@ export function protokollbereich(): HTMLElement { liste.innerHTML = '
'; bereich.appendChild(liste); + // **Über der Liste, nicht darunter.** „Weiter zurück" holt ältere Zeilen, + // und die erscheinen oben — ein Knopf dafür am unteren Ende zeigte in die + // falsche Richtung. const mehr = document.createElement('button'); mehr.className = 'btn hidden'; mehr.textContent = 'Weiter zurück'; - bereich.appendChild(mehr); + bereich.insertBefore(mehr, liste); const archiv = document.createElement('div'); archiv.className = 'protokolltage'; @@ -90,7 +98,11 @@ export function protokollbereich(): HTMLElement { } function zeichnen(roh: string): void { - const zeilenListe = roh.split('\n').filter(z => z.trim().length > 0).reverse(); + const zeilenListe = roh.split('\n').filter(z => z.trim().length > 0); + + // **Vor dem Neuzeichnen merken, ob man unten stand.** Danach ist die + // Höhe eine andere, und die Frage liesse sich nicht mehr beantworten. + const amEnde = liste.scrollHeight - liste.scrollTop - liste.clientHeight < 40; liste.innerHTML = ''; if (zeilenListe.length === 0) { @@ -99,6 +111,7 @@ export function protokollbereich(): HTMLElement { } zeilenListe.forEach(z => liste.appendChild(protokollzeile(z))); + if (amEnde) liste.scrollTop = liste.scrollHeight; } async function tageLaden(): Promise {