Ein Angebot, das keiner annimmt, nimmt man zurück

„Intro überspringen" soll erscheinen, ohne dass man den Bildschirm
antippen muss — das war der Wunsch, und er war richtig. Nur blieb der
Knopf dann liegen, solange die Marke dauert: bei einem Abspann sechs
Minuten lang ein Knopf über dem Bild, auch nachdem die übrige Bedienung
längst weggeblendet war.

Jetzt hat er eine eigene Frist. Acht Sekunden von selbst, lang genug zum
Bemerken und Treffen; danach gehört er zur Bedienung wie alles andere
und kommt mit einem Antippen zurück. Am Ende des Films bleibt er stehen
— dort ist er die einzige Frage, die noch offen ist.

Im Web dasselbe, plus die Stelle, an der es im Stehen auseinanderfiel:
`markenstand` rechnet nur, während das Bild läuft. Wer pausiert im Intro
die Maus bewegte, bekam die Bedienung zurück und den Knopf nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-13 16:16:15 +02:00
co-authored by Claude Opus 5
parent fbd865e93b
commit 0186087051
2 changed files with 63 additions and 1 deletions
+46
View File
@@ -52,6 +52,16 @@ struct PlayerView: View {
@State private var trackPicker: TrackPickerSheet.Kind? @State private var trackPicker: TrackPickerSheet.Kind?
@State private var showsQualityChoice = false @State private var showsQualityChoice = false
@State private var hideTask: Task<Void, Never>? @State private var hideTask: Task<Void, Never>?
/// 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<Void, Never>?
@State private var scrubbing: Double? @State private var scrubbing: Double?
/// Welche Richtung gerade als Hinweis eingeblendet ist. /// Welche Richtung gerade als Hinweis eingeblendet ist.
@State private var seekHint: Double? @State private var seekHint: Double?
@@ -362,6 +372,9 @@ struct PlayerView: View {
.environment(settings) .environment(settings)
.environment(\.flaroAccent, accent) .environment(\.flaroAccent, accent)
} }
.onChange(of: model.activeMarker?.id, initial: true) { _, marke in
markenFristSetzen(marke)
}
.onChange(of: showsQualityChoice) { _, isShown in .onChange(of: showsQualityChoice) { _, isShown in
// Nach dem Schließen die Bedienelemente stehen lassen und die // Nach dem Schließen die Bedienelemente stehen lassen und die
// Ausblendfrist neu starten. Ohne das verschwänden sie sofort, // 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 // Über dem Schieberegler, damit sich beides nicht überdeckt, wenn
// die Bedienelemente eingeblendet sind. // die Bedienelemente eingeblendet sind.
.padding(.bottom, (showsControls ? 110 : FlaroSpacing.section) + safeArea.bottom) .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.activeMarker?.id)
.animation(.snappy, value: model.hasReachedEnd) .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. /// Die Abdeckung während des Umstellens.
+17 -1
View File
@@ -799,6 +799,8 @@ export function spieler(anfrage: Spieleranfrage): void {
* Klick mitten im Drücken zu verlieren. * Klick mitten im Drücken zu verlieren.
*/ */
let markenstandJetzt = ''; let markenstandJetzt = '';
let markenAb = 0;
let markeLaeuft = false;
function markenstand(jetzt: number): void { function markenstand(jetzt: number): void {
const drin = marken.find(m => jetzt >= m.startSeconds && jetzt < m.endSeconds); 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 weiter = abspann && naechste?.files[0] != null;
const kennung = `${drin?.startSeconds ?? ''}|${weiter}`; 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; if (!drin || kennung === markenstandJetzt) return;
markenstandJetzt = kennung; markenstandJetzt = kennung;
markenreihe.replaceChildren(); markenreihe.replaceChildren();
@@ -1294,6 +1307,9 @@ export function spieler(anfrage: Spieleranfrage): void {
let verstecker: number | undefined; let verstecker: number | undefined;
function zeigen(): void { function zeigen(): void {
schicht.classList.remove('pl-still'); 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(); verbergenPlanen();
} }
function verbergenPlanen(): void { function verbergenPlanen(): void {