diff --git a/Flaro/Views/Player/PlayerView.swift b/Flaro/Views/Player/PlayerView.swift index 441e614..d282469 100644 --- a/Flaro/Views/Player/PlayerView.swift +++ b/Flaro/Views/Player/PlayerView.swift @@ -52,16 +52,15 @@ struct PlayerView: View { @State private var trackPicker: TrackPickerSheet.Kind? @State private var showsQualityChoice = false @State private var hideTask: Task? - /// Ob die Marken-Knöpfe gerade von sich aus liegen. + /// Ob die Marken-Knöpfe inzwischen an der Bedienung hängen. /// - /// **Eine Marke dauert Minuten, ein Angebot nicht.** „Intro - /// überspringen" soll erscheinen, ohne dass man den Bildschirm - /// antippen muss — aber ein Abspann läuft sechs Minuten, und so lange - /// gehört kein Knopf über das Bild. Also hat er eine eigene Frist: - /// einmal von selbst, danach nur noch zusammen mit der übrigen - /// Bedienung. - @State private var zeigtMarken = false - @State private var markenTask: Task? + /// **Erst für sich, dann mit der Bedienung.** Eine neue Marke zeigt + /// ihre Knöpfe von selbst — sonst müsste man erst das Bild antippen, + /// um das Intro überspringen zu können, und dann hätte man die + /// Bedienung ohnehin. Sobald man sie aber einmal eingeblendet hat, + /// gehören die Knöpfe dazu und gehen mit ihr. Keine Frist, kein + /// Zeitgeber: der Zuschauer sagt selbst, wann er genug gesehen hat. + @State private var markenGebunden = false @State private var scrubbing: Double? /// Welche Richtung gerade als Hinweis eingeblendet ist. @State private var seekHint: Double? @@ -372,8 +371,16 @@ struct PlayerView: View { .environment(settings) .environment(\.flaroAccent, accent) } - .onChange(of: model.activeMarker?.id, initial: true) { _, marke in - markenFristSetzen(marke) + // Eine neue Marke ist ein neues Angebot: sie steht wieder für sich, + // bis die Bedienung das nächste Mal auftaucht. + .onChange(of: model.activeMarker?.id, initial: true) { _, _ in + markenGebunden = false + } + // **Der Übergang zählt, nicht der Zustand.** Beim Start liegt die + // Bedienung ohnehin da; das als „einmal gezeigt" zu werten hiesse, + // dem Intro seinen Knopf zu nehmen, bevor es ihn je hatte. + .onChange(of: showsControls) { vorher, jetzt in + if !vorher && jetzt { markenGebunden = true } } .onChange(of: showsQualityChoice) { _, isShown in // Nach dem Schließen die Bedienelemente stehen lassen und die @@ -814,43 +821,17 @@ struct PlayerView: View { } .animation(.snappy, value: model.activeMarker?.id) .animation(.snappy, value: model.hasReachedEnd) - .animation(.easeIn(duration: 0.28), value: zeigtMarken) + .animation(.easeIn(duration: 0.28), value: markenGebunden) } - /// **Dieselbe Frist für Vorspann und Abspann.** + /// **Dieselbe Regel für Vorspann und Abspann, Web wie App.** /// - /// Der Abspann hatte kurz eine Ausnahme: die Knöpfe blieben dort - /// stehen, solange er lief. Das war die Antwort auf „beim Abspann - /// kommt kein Knopf" — und die Frage war falsch gestellt: es kam - /// keiner, weil dem Staffelfinale die Marke fehlte, nicht weil er zu - /// früh ging. Die Ausnahme löste also nichts und liess dafür einen - /// Knopf sechs Minuten über dem Bild stehen. - /// - /// Am Ende des Films bleiben sie trotzdem: dort ist „Nächste Folge" - /// die einzige Frage, die noch offen ist, und es gibt kein Bild mehr, - /// das sie verdecken könnten. + /// Sichtbar, solange die Knöpfe noch für sich stehen; danach genau + /// dann, wenn die Bedienung da ist. Am Ende des Films bleiben sie + /// trotzdem: dort ist „Nächste Folge" die einzige noch offene Frage, + /// und es gibt kein Bild mehr, das sie verdecken könnten. private func markenSichtbar(_ model: PlayerModel) -> Bool { - model.hasReachedEnd || showsControls || zeigtMarken - } - - /// Eine neue Marke zeigt ihre Knöpfe von selbst — für acht Sekunden. - /// - /// Lang genug, um sie zu bemerken und zu treffen, kurz genug, dass sie - /// nicht das halbe Intro überdecken. Wer später doch überspringen will, - /// tippt einmal: dann liegt die Bedienung ohnehin wieder da, und die - /// Knöpfe mit ihr. - private func markenFristSetzen(_ marke: Int?) { - markenTask?.cancel() - guard marke != nil else { - zeigtMarken = false - return - } - zeigtMarken = true - markenTask = Task { - try? await Task.sleep(for: .seconds(8)) - guard !Task.isCancelled else { return } - zeigtMarken = false - } + model.hasReachedEnd || showsControls || !markenGebunden } /// Die Abdeckung während des Umstellens. diff --git a/web/app/src/spieler.ts b/web/app/src/spieler.ts index 9a71991..d9312e5 100644 --- a/web/app/src/spieler.ts +++ b/web/app/src/spieler.ts @@ -381,8 +381,8 @@ export function spieler(anfrage: Spieleranfrage): void { // // Dort stand er, und damit verschwand er mit ihr — am Fernseher sieht man // die Leiste gar nicht, und ein Knopf, von dem niemand weiss, dass es ihn - // gibt, ist keiner. Als eigene Schicht über dem Bild bleibt er sichtbar, - // solange die Marke läuft. + // gibt, ist keiner. Als eigene Schicht über dem Bild erscheint er von + // selbst — und hängt sich an die Bedienung, sobald die einmal da war. // **Zwei Knöpfe, nicht einer.** Im Abspann einer Serie gibt es zwei // vernünftige Antworten — weiterschauen oder den Abspann überspringen — // und welche gemeint ist, weiss nur der Zuschauer. @@ -659,7 +659,8 @@ export function spieler(anfrage: Spieleranfrage): void { // Dort stand er, und damit verschwand er mit ihr — am Fernseher sieht man // die Leiste gar nicht, und ein Knopf, von dem niemand weiss, dass es ihn // gibt, ist keiner. Jetzt liegt er als eigene Schicht über dem Bild, - // rechts unten, und bleibt sichtbar, solange die Marke läuft. + // rechts unten, und erscheint von selbst — bis man die Bedienung einmal + // eingeblendet hat, danach kommt und geht er mit ihr. bedienung.append(zeitleiste, unten); // **Beide Nachbarn gleich zu Beginn holen.** Zwei kleine Anfragen, einmal @@ -841,7 +842,15 @@ export function spieler(anfrage: Spieleranfrage): void { * Klick mitten im Drücken zu verlieren. */ let markenstandJetzt = ''; - let markenAb = 0; + /** Ob die Marken-Knöpfe inzwischen an der Bedienung hängen. + * + * **Erst für sich, dann mit der Bedienung.** Eine neue Marke zeigt ihre + * Knöpfe von selbst — sonst müsste man erst das Bild anfassen, um das + * Intro überspringen zu können, und dann läge die Bedienung ohnehin da. + * Sobald man sie aber einmal eingeblendet hat, gehören die Knöpfe dazu + * und gehen mit ihr. Keine Frist, kein Zeitgeber: der Zuschauer sagt + * selbst, wann er genug gesehen hat. */ + let markenGebunden = false; let markeLaeuft = false; function markenstand(jetzt: number): void { @@ -850,19 +859,14 @@ export function spieler(anfrage: Spieleranfrage): void { const weiter = abspann && naechste?.files[0] != null; const kennung = `${drin?.startSeconds ?? ''}|${weiter}`; - if (kennung !== markenstandJetzt) markenAb = performance.now(); + // Eine neue Marke ist ein neues Angebot: sie steht wieder für sich, + // bis die Bedienung das nächste Mal auftaucht. + if (kennung !== markenstandJetzt) markenGebunden = false; - // **Ein Intro ist ein Angebot, ein Abspann eine Frage.** - // - // „Intro überspringen" kommt von selbst und geht nach acht Sekunden - // wieder: wer es nicht genommen hat, will zusehen, und dann gehört - // kein Knopf über das Bild. Im Abspann ist es umgekehrt. „Nächste - // Folge" ist das, wofür man dort überhaupt hinsieht — der bleibt - // stehen, bis die Folge zu Ende ist. - const bleibt = abspann || performance.now() - markenAb < 8000; markeLaeuft = drin != null; markenreihe.classList.toggle( - 'hidden', !drin || (!bleibt && schicht.classList.contains('pl-still'))); + 'hidden', + !drin || (markenGebunden && schicht.classList.contains('pl-still'))); if (!drin || kennung === markenstandJetzt) return; markenstandJetzt = kennung; @@ -1350,6 +1354,11 @@ export function spieler(anfrage: Spieleranfrage): void { let verstecker: number | undefined; function zeigen(): void { + // **Der Übergang zählt, nicht der Zustand.** `zeigen` läuft bei jeder + // Mausbewegung, auch wenn die Bedienung längst da ist. Das jedes Mal + // als „einmal gezeigt" zu werten hiesse, dem Intro seinen Knopf zu + // nehmen, bevor es ihn je hatte. + if (schicht.classList.contains('pl-still')) markenGebunden = true; schicht.classList.remove('pl-still'); // Im Stehen rechnet `markenstand` nicht weiter: wer pausiert im Intro // die Maus bewegt, bekäme die Bedienung zurück und den Knopf nicht.