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 {