diff --git a/Flaro/Views/Player/PlayerView.swift b/Flaro/Views/Player/PlayerView.swift index d3eb375..39901b3 100644 --- a/Flaro/Views/Player/PlayerView.swift +++ b/Flaro/Views/Player/PlayerView.swift @@ -52,6 +52,16 @@ 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. + /// + /// **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? @State private var scrubbing: Double? /// Welche Richtung gerade als Hinweis eingeblendet ist. @State private var seekHint: Double? @@ -362,6 +372,9 @@ struct PlayerView: View { .environment(settings) .environment(\.flaroAccent, accent) } + .onChange(of: model.activeMarker?.id, initial: true) { _, marke in + markenFristSetzen(marke) + } .onChange(of: showsQualityChoice) { _, isShown in // Nach dem Schließen die Bedienelemente stehen lassen und die // Ausblendfrist neu starten. Ohne das verschwänden sie sofort, @@ -790,9 +803,42 @@ struct PlayerView: View { // Über dem Schieberegler, damit sich beides nicht überdeckt, wenn // die Bedienelemente eingeblendet sind. .padding(.bottom, (showsControls ? 110 : FlaroSpacing.section) + safeArea.bottom) + // **Am Ende bleibt es stehen, sonst nicht.** + // + // Ist der Film aus, ist der Knopf die einzige Frage, die noch + // offen ist — der darf warten. Mitten in einer Marke ist er ein + // Angebot, und ein Angebot, das nicht angenommen wird, nimmt + // man zurück. + .opacity(markenSichtbar(model) ? 1 : 0) + .allowsHitTesting(markenSichtbar(model)) } .animation(.snappy, value: model.activeMarker?.id) .animation(.snappy, value: model.hasReachedEnd) + .animation(.easeIn(duration: 0.28), value: zeigtMarken) + } + + 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 + } } /// Die Abdeckung während des Umstellens. diff --git a/web/app/src/spieler.ts b/web/app/src/spieler.ts index df60642..9038943 100644 --- a/web/app/src/spieler.ts +++ b/web/app/src/spieler.ts @@ -799,6 +799,8 @@ export function spieler(anfrage: Spieleranfrage): void { * Klick mitten im Drücken zu verlieren. */ let markenstandJetzt = ''; + let markenAb = 0; + let markeLaeuft = false; function markenstand(jetzt: number): void { const drin = marken.find(m => jetzt >= m.startSeconds && jetzt < m.endSeconds); @@ -806,7 +808,18 @@ export function spieler(anfrage: Spieleranfrage): void { const weiter = abspann && naechste?.files[0] != null; const kennung = `${drin?.startSeconds ?? ''}|${weiter}`; - markenreihe.classList.toggle('hidden', !drin); + if (kennung !== markenstandJetzt) markenAb = performance.now(); + + // **Eine Marke dauert Minuten, ein Angebot nicht.** Die Knöpfe kommen + // von selbst, ohne dass man den Film antippen muss — aber ein Abspann + // läuft sechs Minuten, und so lange gehört kein Knopf über das Bild. + // Acht Sekunden von allein, danach nur noch zusammen mit der übrigen + // Bedienung: wer später doch überspringen will, bewegt die Maus. + const frisch = performance.now() - markenAb < 8000; + markeLaeuft = drin != null; + markenreihe.classList.toggle( + 'hidden', !drin || (!frisch && schicht.classList.contains('pl-still'))); + if (!drin || kennung === markenstandJetzt) return; markenstandJetzt = kennung; markenreihe.replaceChildren(); @@ -1294,6 +1307,9 @@ export function spieler(anfrage: Spieleranfrage): void { let verstecker: number | undefined; function zeigen(): void { 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. + if (markeLaeuft) markenreihe.classList.remove('hidden'); verbergenPlanen(); } function verbergenPlanen(): void {