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:
co-authored by
Claude Opus 5
parent
fbd865e93b
commit
0186087051
@@ -52,6 +52,16 @@ struct PlayerView: View {
|
||||
@State private var trackPicker: TrackPickerSheet.Kind?
|
||||
@State private var showsQualityChoice = false
|
||||
@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?
|
||||
/// 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.
|
||||
|
||||
+17
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user