Ein Kreisel ist kein Pausenknopf
Während der Player auf Daten wartete, stand im mittleren Knopf das Abspieldreieck und der Ladekreis lag dahinter. Das las sich als „angehalten": man drückte auf Abspielen, und es geschah nichts — es lief ja bereits, es kam nur nichts an. Jetzt sitzt der Kreisel **im** Knopf, dort wo sonst das Symbol steht. Der freistehende Kreisel in der Bildmitte bleibt für den Fall, dass die Bedienelemente ausgeblendet sind. Zwei übereinander gibt es nicht mehr. `togglePlayPause` zählt das Warten jetzt zum Laufen. Vorher führte ein Tipp in diesem Moment zu `play()` auf etwas, das schon spielte. Dazu auf dem Mac, wie im Browser auch: - Ein Klick ins Bild hält an, statt erst die Bedienelemente zu holen. Die kommen dort von allein, sobald sich der Zeiger regt. - Leertaste hält an, Pfeil links und rechts springen. Mit dem Finger bleibt es beim Alten: ohne Zeiger gibt es keine Bewegung ohne Berührung, und ein Tipp, der sofort anhält, nähme den einzigen Weg zu Scrubber, Tonspur und Qualität. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3af6c3112f
commit
9a4b6b1e06
@@ -3205,6 +3205,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"player.buffering" : {
|
||||||
|
"comment" : "Sprachausgabe für den Abspielknopf, solange auf Daten gewartet wird",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Lädt …"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Loading…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"player.close" : {
|
"player.close" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -523,9 +523,14 @@ final class PlayerModel {
|
|||||||
programmeEndsAt = match.endsAt
|
programmeEndsAt = match.endsAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Anhalten oder weiterlaufen lassen.
|
||||||
|
///
|
||||||
|
/// Das Warten auf Daten zählt hier zum Laufen. Ohne `isBuffering` in
|
||||||
|
/// dieser Bedingung führte ein Tipp in genau dem Moment zu `play()` auf
|
||||||
|
/// etwas, das bereits spielte — und sichtbar geschah nichts.
|
||||||
func togglePlayPause() {
|
func togglePlayPause() {
|
||||||
guard let engine else { return }
|
guard let engine else { return }
|
||||||
if isPlaying { engine.pause() } else { engine.play() }
|
if isPlaying || isBuffering { engine.pause() } else { engine.play() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(to time: Duration) {
|
func seek(to time: Duration) {
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ struct PlayerView: View {
|
|||||||
/// Welche Richtung gerade als Hinweis eingeblendet ist.
|
/// Welche Richtung gerade als Hinweis eingeblendet ist.
|
||||||
@State private var seekHint: Double?
|
@State private var seekHint: Double?
|
||||||
@State private var hintTask: Task<Void, Never>?
|
@State private var hintTask: Task<Void, Never>?
|
||||||
|
/// Ob die Tastatur bei diesem Bildschirm ankommt. Nur auf dem Mac gesetzt.
|
||||||
|
@FocusState private var keyboardFocus: Bool
|
||||||
|
/// Wann der Zeiger zuletzt die Bedienelemente geholt hat.
|
||||||
|
@State private var lastPointerReveal: Date = .distantPast
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -68,7 +72,12 @@ struct PlayerView: View {
|
|||||||
case .playing:
|
case .playing:
|
||||||
// Wartet auf Daten: ohne Anzeige sieht das aus wie ein
|
// Wartet auf Daten: ohne Anzeige sieht das aus wie ein
|
||||||
// Standbild, und man weiß nicht, ob noch etwas kommt.
|
// Standbild, und man weiß nicht, ob noch etwas kommt.
|
||||||
if model.isBuffering {
|
//
|
||||||
|
// Nur, solange die Bedienelemente aus sind. Sind sie an,
|
||||||
|
// steht der Kreisel **im** Abspielknopf — dort, wo sonst
|
||||||
|
// das Symbol ist. Vorher lag er dahinter und das Dreieck
|
||||||
|
// davor: es sah pausiert aus, war es aber nicht.
|
||||||
|
if model.isBuffering && !showsControls {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.tint(.white)
|
.tint(.white)
|
||||||
.scaleEffect(1.4)
|
.scaleEffect(1.4)
|
||||||
@@ -109,9 +118,42 @@ struct PlayerView: View {
|
|||||||
.persistentSystemOverlays(showsControls ? .automatic : .hidden)
|
.persistentSystemOverlays(showsControls ? .automatic : .hidden)
|
||||||
.landscapeWhileVisible(settings.locksLandscape)
|
.landscapeWhileVisible(settings.locksLandscape)
|
||||||
.contentShape(.rect)
|
.contentShape(.rect)
|
||||||
// Für alles außerhalb der Sprungflächen: Tippen zeigt und verbirgt
|
// Für alles außerhalb der Sprungflächen.
|
||||||
// die Bedienelemente wie bisher.
|
.onTapGesture { primaryTap() }
|
||||||
.onTapGesture { toggleControls() }
|
// **Tastatur und Zeiger — nur auf dem Mac.**
|
||||||
|
//
|
||||||
|
// Dort ist beides selbstverständlich: Leertaste hält an, die Pfeile
|
||||||
|
// springen, und die Bedienelemente kommen von allein, sobald sich der
|
||||||
|
// Zeiger regt. Auf dem iPhone gibt es keines von beidem, und
|
||||||
|
// `focusable` legte dort nur einen Fokusrahmen über den Film.
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
.focusable()
|
||||||
|
.focusEffectDisabled()
|
||||||
|
.focused($keyboardFocus)
|
||||||
|
// Von Hand gesetzt, weil sonst niemand den Fokus hat: der Player
|
||||||
|
// besteht aus Knöpfen, und solange keiner angeklickt wurde, ginge
|
||||||
|
// jeder Tastendruck ins Leere.
|
||||||
|
.onAppear { keyboardFocus = true }
|
||||||
|
.onKeyPress(.space) {
|
||||||
|
model.togglePlayPause()
|
||||||
|
revealControls()
|
||||||
|
return .handled
|
||||||
|
}
|
||||||
|
.onKeyPress(.leftArrow) {
|
||||||
|
seek(by: -Double(settings.rewindSeconds))
|
||||||
|
revealControls()
|
||||||
|
return .handled
|
||||||
|
}
|
||||||
|
.onKeyPress(.rightArrow) {
|
||||||
|
seek(by: Double(settings.forwardSeconds))
|
||||||
|
revealControls()
|
||||||
|
return .handled
|
||||||
|
}
|
||||||
|
.onContinuousHover { phase in
|
||||||
|
guard case .active = phase else { return }
|
||||||
|
pointerMoved()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
.sheet(item: $trackPicker) { kind in
|
.sheet(item: $trackPicker) { kind in
|
||||||
TrackPickerSheet(model: model, kind: kind)
|
TrackPickerSheet(model: model, kind: kind)
|
||||||
}
|
}
|
||||||
@@ -288,10 +330,27 @@ struct PlayerView: View {
|
|||||||
model.togglePlayPause()
|
model.togglePlayPause()
|
||||||
scheduleControlsHide()
|
scheduleControlsHide()
|
||||||
} label: {
|
} label: {
|
||||||
|
ZStack {
|
||||||
|
// Solange auf Daten gewartet wird, steht hier der Kreisel
|
||||||
|
// — kein Dreieck. Ein Dreieck heißt „angehalten, tipp
|
||||||
|
// mich", und beides stimmte nicht: es lief bereits, und
|
||||||
|
// der Tipp bewirkte nichts.
|
||||||
|
if model.isBuffering {
|
||||||
|
ProgressView()
|
||||||
|
.tint(.white)
|
||||||
|
.scaleEffect(1.4)
|
||||||
|
} else {
|
||||||
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
|
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
|
||||||
.font(.system(size: 52))
|
.font(.system(size: 52))
|
||||||
}
|
}
|
||||||
.accessibilityLabel(Text(model.isPlaying ? "player.pause" : "player.play"))
|
}
|
||||||
|
// Feste Größe, damit die beiden Knöpfe daneben stehen
|
||||||
|
// bleiben. Sie wanderten bisher bei jedem Wechsel ein Stück,
|
||||||
|
// weil Dreieck und Doppelbalken verschieden breit sind.
|
||||||
|
.frame(width: 64, height: 64)
|
||||||
|
}
|
||||||
|
.accessibilityLabel(Text(playPauseLabel(for: model)))
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: model.isBuffering)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
model.skip(by: .seconds(settings.forwardSeconds))
|
model.skip(by: .seconds(settings.forwardSeconds))
|
||||||
@@ -304,6 +363,15 @@ struct PlayerView: View {
|
|||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Was die Sprachausgabe zu dem Knopf sagt.
|
||||||
|
///
|
||||||
|
/// Beim Warten weder „Abspielen" noch „Pause": beides wäre eine Auskunft
|
||||||
|
/// über einen Zustand, in dem der Player gerade nicht ist.
|
||||||
|
private func playPauseLabel(for model: PlayerModel) -> LocalizedStringKey {
|
||||||
|
if model.isBuffering { return "player.buffering" }
|
||||||
|
return model.isPlaying ? "player.pause" : "player.play"
|
||||||
|
}
|
||||||
|
|
||||||
private func scrubber(for model: PlayerModel) -> some View {
|
private func scrubber(for model: PlayerModel) -> some View {
|
||||||
VStack(spacing: FlixySpacing.tight) {
|
VStack(spacing: FlixySpacing.tight) {
|
||||||
Slider(
|
Slider(
|
||||||
@@ -499,7 +567,7 @@ struct PlayerView: View {
|
|||||||
// Das doppelte zuerst: sonst gewinnt das einfache und man käme nie
|
// Das doppelte zuerst: sonst gewinnt das einfache und man käme nie
|
||||||
// zum Springen.
|
// zum Springen.
|
||||||
.onTapGesture(count: 2) { seek(by: seconds) }
|
.onTapGesture(count: 2) { seek(by: seconds) }
|
||||||
.onTapGesture { toggleControls() }
|
.onTapGesture { primaryTap() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func seek(by seconds: Double) {
|
private func seek(by seconds: Double) {
|
||||||
@@ -523,14 +591,57 @@ struct PlayerView: View {
|
|||||||
|
|
||||||
// MARK: - Ein- und Ausblenden
|
// MARK: - Ein- und Ausblenden
|
||||||
|
|
||||||
|
/// Was ein einfacher Klick oder Tipp ins Bild auslöst.
|
||||||
|
///
|
||||||
|
/// **Auf dem Mac: anhalten.** Dort liegt ein Zeiger auf dem Bild, und die
|
||||||
|
/// Bedienelemente kommen von selbst, sobald er sich bewegt — ein Klick
|
||||||
|
/// muss sie nicht erst holen. Erst aufdecken und dann den Pausenknopf
|
||||||
|
/// suchen sind zwei Handgriffe für eine Sache.
|
||||||
|
///
|
||||||
|
/// **Mit dem Finger nicht.** Dort gibt es keine Bewegung ohne Berührung:
|
||||||
|
/// ein Tipp, der sofort anhält, nähme den einzigen Weg zu Scrubber,
|
||||||
|
/// Tonspur und Qualität.
|
||||||
|
private func primaryTap() {
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
model.togglePlayPause()
|
||||||
|
revealControls()
|
||||||
|
#else
|
||||||
|
toggleControls()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private func toggleControls() {
|
private func toggleControls() {
|
||||||
withAnimation(.easeInOut(duration: 0.2)) { showsControls.toggle() }
|
if showsControls {
|
||||||
// Beim Aufdecken nachsehen, was inzwischen läuft — genau dann will
|
withAnimation(.easeInOut(duration: 0.2)) { showsControls = false }
|
||||||
// man es wissen.
|
} else {
|
||||||
if showsControls, model.isLive {
|
revealControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zeigt die Bedienelemente und setzt die Ausblendfrist neu.
|
||||||
|
private func revealControls() {
|
||||||
|
if !showsControls {
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) { showsControls = true }
|
||||||
|
// Beim Aufdecken nachsehen, was inzwischen läuft — genau dann
|
||||||
|
// will man es wissen.
|
||||||
|
if model.isLive {
|
||||||
Task { await model.refreshLiveProgrammeIfNeeded() }
|
Task { await model.refreshLiveProgrammeIfNeeded() }
|
||||||
}
|
}
|
||||||
if showsControls { scheduleControlsHide() }
|
}
|
||||||
|
scheduleControlsHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Der Zeiger hat sich bewegt.
|
||||||
|
///
|
||||||
|
/// Gedrosselt, weil die Meldung bei jeder Bewegung kommt — bis zu
|
||||||
|
/// hundertmal je Sekunde. Die Ausblendfrist jedes Mal neu aufzusetzen
|
||||||
|
/// hieße, hundertmal je Sekunde eine Aufgabe abzubrechen und eine neue zu
|
||||||
|
/// starten. Fünfmal reicht vollkommen.
|
||||||
|
private func pointerMoved() {
|
||||||
|
let now = Date.now
|
||||||
|
guard !showsControls || now.timeIntervalSince(lastPointerReveal) > 0.2 else { return }
|
||||||
|
lastPointerReveal = now
|
||||||
|
revealControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blendet die Bedienelemente nach kurzer Ruhe aus — aber nur, solange
|
/// Blendet die Bedienelemente nach kurzer Ruhe aus — aber nur, solange
|
||||||
|
|||||||
+11
-2
@@ -341,19 +341,28 @@ img { display: block; }
|
|||||||
|
|
||||||
/* Während geladen wird, treten die Transportknöpfe zurück: sie tun ohnehin
|
/* Während geladen wird, treten die Transportknöpfe zurück: sie tun ohnehin
|
||||||
nichts, und der Kreisel lag vorher genau hinter dem größten von ihnen. */
|
nichts, und der Kreisel lag vorher genau hinter dem größten von ihnen. */
|
||||||
#player.loading .pl-center { opacity: 0; pointer-events: none; }
|
/* Während des ersten Ladens haben Vor- und Zurückspulen keine Wirkung — es
|
||||||
|
gibt noch keine Zeitachse. Der mittlere Knopf bleibt stehen: er trägt
|
||||||
|
jetzt selbst den Kreisel. */
|
||||||
|
#player.loading .pl-center > :not(.pl-play) { opacity: 0; pointer-events: none; }
|
||||||
.pl-center { transition: opacity .2s ease; }
|
.pl-center { transition: opacity .2s ease; }
|
||||||
|
|
||||||
.pl-busy {
|
.pl-busy {
|
||||||
position: absolute; display: none; pointer-events: none;
|
position: absolute; display: none; pointer-events: none;
|
||||||
}
|
}
|
||||||
.pl-busy.on { display: block; }
|
.pl-busy.on { display: block; }
|
||||||
.pl-busy i {
|
.pl-busy i, .pl-wait {
|
||||||
display: block; width: 44px; height: 44px; border-radius: 999px;
|
display: block; width: 44px; height: 44px; border-radius: 999px;
|
||||||
border: 3px solid rgba(255,255,255,.2); border-top-color: var(--accent);
|
border: 3px solid rgba(255,255,255,.2); border-top-color: var(--accent);
|
||||||
animation: spin .8s linear infinite;
|
animation: spin .8s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Feste Größe für den mittleren Knopf, damit die beiden daneben stehen
|
||||||
|
bleiben: der Kreisel ist schmaler als das Symbol, das er vertritt, und
|
||||||
|
ohne diese Zeile rückten Zurück- und Vorspulen bei jedem Wechsel
|
||||||
|
zusammen. */
|
||||||
|
.pl-round.pl-play { width: 68px; height: 68px; }
|
||||||
|
|
||||||
.pl-error {
|
.pl-error {
|
||||||
position: absolute; max-width: 460px; padding: var(--s-section);
|
position: absolute; max-width: 460px; padding: var(--s-section);
|
||||||
background: var(--elevated); border: 1px solid rgba(229,72,77,.35);
|
background: var(--elevated); border: 1px solid rgba(229,72,77,.35);
|
||||||
|
|||||||
+42
-10
@@ -76,10 +76,16 @@ export function playerOeffnen(item: Item, beimSchliessen?: () => void): void {
|
|||||||
|
|
||||||
/* ---------- Fehler sichtbar machen ---------- */
|
/* ---------- Fehler sichtbar machen ---------- */
|
||||||
let hls: HlsTyp | null = null;
|
let hls: HlsTyp | null = null;
|
||||||
|
/** Ob gerade auf Daten gewartet wird — am Anfang immer. */
|
||||||
|
let wartet = true;
|
||||||
|
|
||||||
const scheitern = (satz: string, hinweis?: string, adresse?: string): void => {
|
const scheitern = (satz: string, hinweis?: string, adresse?: string): void => {
|
||||||
ebene.classList.remove('loading');
|
ebene.classList.remove('loading');
|
||||||
el('pl-busy').classList.remove('on');
|
el('pl-busy').classList.remove('on');
|
||||||
|
// Sonst dreht sich der Kreisel im Knopf hinter dem Fehlerkasten weiter,
|
||||||
|
// als käme noch etwas.
|
||||||
|
wartet = false;
|
||||||
|
malen();
|
||||||
ebene.querySelector('.pl-error')?.remove();
|
ebene.querySelector('.pl-error')?.remove();
|
||||||
const kasten = document.createElement('div');
|
const kasten = document.createElement('div');
|
||||||
kasten.className = 'pl-error';
|
kasten.className = 'pl-error';
|
||||||
@@ -105,7 +111,21 @@ export function playerOeffnen(item: Item, beimSchliessen?: () => void): void {
|
|||||||
clearTimeout(ausblenden);
|
clearTimeout(ausblenden);
|
||||||
// Solange etwas steht, bleibt die Bedienung: sie auszublenden, während
|
// Solange etwas steht, bleibt die Bedienung: sie auszublenden, während
|
||||||
// niemand spielt, versteckt den einzigen Knopf, der weiterhilft.
|
// niemand spielt, versteckt den einzigen Knopf, der weiterhilft.
|
||||||
if (!v.paused) ausblenden = setTimeout(() => chrome.classList.remove('on'), 3000);
|
if (!v.paused) {
|
||||||
|
ausblenden = setTimeout(() => {
|
||||||
|
chrome.classList.remove('on');
|
||||||
|
kreisel();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
kreisel();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Der freistehende Kreisel — nur, wenn die Bedienung aus ist.
|
||||||
|
*
|
||||||
|
* Sonst stünden zwei übereinander: einer in der Mitte des Bildes und einer
|
||||||
|
* im Abspielknopf, der genau davor sitzt. Genau so sah es vorher aus. */
|
||||||
|
const kreisel = (): void => {
|
||||||
|
el('pl-busy').classList.toggle('on', wartet && !chrome.classList.contains('on'));
|
||||||
};
|
};
|
||||||
ebene.addEventListener('mousemove', zeigen);
|
ebene.addEventListener('mousemove', zeigen);
|
||||||
ebene.addEventListener('touchstart', zeigen, { passive: true });
|
ebene.addEventListener('touchstart', zeigen, { passive: true });
|
||||||
@@ -117,16 +137,28 @@ export function playerOeffnen(item: Item, beimSchliessen?: () => void): void {
|
|||||||
el('pl-fwd').onclick = () => { v.currentTime = Math.min(v.duration || 1e9, v.currentTime + P.skipForward); zeigen(); };
|
el('pl-fwd').onclick = () => { v.currentTime = Math.min(v.duration || 1e9, v.currentTime + P.skipForward); zeigen(); };
|
||||||
v.addEventListener('click', umschalten);
|
v.addEventListener('click', umschalten);
|
||||||
|
|
||||||
|
/** Zeichnet den mittleren Knopf: Kreisel, Dreieck oder Doppelbalken.
|
||||||
|
*
|
||||||
|
* Solange auf Daten gewartet wird, steht dort der Kreisel — **kein**
|
||||||
|
* Dreieck. Ein Dreieck heißt „angehalten, drück mich", und beides stimmte
|
||||||
|
* nicht: es lief bereits, und der Klick bewirkte nichts. */
|
||||||
const malen = (): void => {
|
const malen = (): void => {
|
||||||
el('pl-toggle').innerHTML = icon(v.paused ? 'play' : 'pause', 52);
|
const knopf = el('pl-toggle');
|
||||||
el('pl-toggle').setAttribute('aria-label', v.paused ? 'Abspielen' : 'Anhalten');
|
knopf.innerHTML = wartet ? '<i class="pl-wait"></i>' : icon(v.paused ? 'play' : 'pause', 52);
|
||||||
zeigen();
|
knopf.setAttribute('aria-label', wartet ? 'Lädt' : v.paused ? 'Abspielen' : 'Anhalten');
|
||||||
};
|
};
|
||||||
v.addEventListener('play', malen);
|
|
||||||
v.addEventListener('pause', malen);
|
const warten = (an: boolean): void => {
|
||||||
v.addEventListener('waiting', () => el('pl-busy').classList.add('on'));
|
wartet = an;
|
||||||
|
malen();
|
||||||
|
kreisel();
|
||||||
|
};
|
||||||
|
|
||||||
|
v.addEventListener('play', () => { malen(); zeigen(); });
|
||||||
|
v.addEventListener('pause', () => { warten(false); zeigen(); });
|
||||||
|
v.addEventListener('waiting', () => warten(true));
|
||||||
v.addEventListener('playing', () => {
|
v.addEventListener('playing', () => {
|
||||||
el('pl-busy').classList.remove('on');
|
warten(false);
|
||||||
ebene.classList.remove('loading');
|
ebene.classList.remove('loading');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,7 +234,7 @@ export function playerOeffnen(item: Item, beimSchliessen?: () => void): void {
|
|||||||
lauf.stelle = v.currentTime || 0;
|
lauf.stelle = v.currentTime || 0;
|
||||||
ebene.querySelector('.pl-error')?.remove();
|
ebene.querySelector('.pl-error')?.remove();
|
||||||
ebene.classList.add('loading');
|
ebene.classList.add('loading');
|
||||||
el('pl-busy').classList.add('on');
|
warten(true);
|
||||||
menueZu();
|
menueZu();
|
||||||
void strom();
|
void strom();
|
||||||
};
|
};
|
||||||
@@ -411,7 +443,7 @@ export function playerOeffnen(item: Item, beimSchliessen?: () => void): void {
|
|||||||
zeigen();
|
zeigen();
|
||||||
tonMalen();
|
tonMalen();
|
||||||
ebene.classList.add('loading');
|
ebene.classList.add('loading');
|
||||||
el('pl-busy').classList.add('on');
|
warten(true);
|
||||||
void strom();
|
void strom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-12
@@ -348,19 +348,26 @@ img { display: block; }
|
|||||||
|
|
||||||
/* Während geladen wird, treten die Transportknöpfe zurück: sie tun ohnehin
|
/* Während geladen wird, treten die Transportknöpfe zurück: sie tun ohnehin
|
||||||
nichts, und der Kreisel lag vorher genau hinter dem größten von ihnen. */
|
nichts, und der Kreisel lag vorher genau hinter dem größten von ihnen. */
|
||||||
#player.loading .pl-center { opacity: 0; pointer-events: none; }
|
/* Während des ersten Ladens haben Vor- und Zurückspulen keine Wirkung — es
|
||||||
|
gibt noch keine Zeitachse. Der mittlere Knopf bleibt stehen: er trägt
|
||||||
|
jetzt selbst den Kreisel. */
|
||||||
|
#player.loading .pl-center > :not(.pl-play) { opacity: 0; pointer-events: none; }
|
||||||
.pl-center { transition: opacity .2s ease; }
|
.pl-center { transition: opacity .2s ease; }
|
||||||
|
|
||||||
.pl-busy {
|
.pl-busy {
|
||||||
position: absolute; display: none; pointer-events: none;
|
position: absolute; display: none; pointer-events: none;
|
||||||
}
|
}
|
||||||
.pl-busy.on { display: block; }
|
.pl-busy.on { display: block; }
|
||||||
.pl-busy i {
|
.pl-busy i, .pl-wait {
|
||||||
display: block; width: 44px; height: 44px; border-radius: 999px;
|
display: block; width: 44px; height: 44px; border-radius: 999px;
|
||||||
border: 3px solid rgba(255,255,255,.2); border-top-color: var(--accent);
|
border: 3px solid rgba(255,255,255,.2); border-top-color: var(--accent);
|
||||||
animation: spin .8s linear infinite;
|
animation: spin .8s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Feste Größe für den mittleren Knopf, damit die beiden daneben stehen
|
||||||
|
bleiben: der Kreisel ist schmaler als das Symbol, das er vertritt. */
|
||||||
|
.pl-round.pl-play { width: 68px; height: 68px; }
|
||||||
|
|
||||||
.pl-error {
|
.pl-error {
|
||||||
position: absolute; max-width: 460px; padding: var(--s-section);
|
position: absolute; max-width: 460px; padding: var(--s-section);
|
||||||
background: var(--elevated); border: 1px solid rgba(229,72,77,.35);
|
background: var(--elevated); border: 1px solid rgba(229,72,77,.35);
|
||||||
@@ -1624,9 +1631,16 @@ function openPlayer(item) {
|
|||||||
const chrome = layer.querySelector('.pl-chrome');
|
const chrome = layer.querySelector('.pl-chrome');
|
||||||
const el2 = (id) => layer.querySelector('#' + id);
|
const el2 = (id) => layer.querySelector('#' + id);
|
||||||
|
|
||||||
|
/* Ob gerade auf Daten gewartet wird — am Anfang immer. */
|
||||||
|
let wartet = true;
|
||||||
|
|
||||||
const fail = (satz, hinweis, adresse) => {
|
const fail = (satz, hinweis, adresse) => {
|
||||||
layer.classList.remove('loading');
|
layer.classList.remove('loading');
|
||||||
el2('pl-busy').classList.remove('on');
|
el2('pl-busy').classList.remove('on');
|
||||||
|
// Sonst dreht sich der Kreisel im Knopf hinter dem Fehlerkasten weiter,
|
||||||
|
// als käme noch etwas.
|
||||||
|
wartet = false;
|
||||||
|
paint();
|
||||||
layer.querySelector('.pl-error')?.remove();
|
layer.querySelector('.pl-error')?.remove();
|
||||||
const box = document.createElement('div');
|
const box = document.createElement('div');
|
||||||
box.className = 'pl-error';
|
box.className = 'pl-error';
|
||||||
@@ -1644,7 +1658,17 @@ function openPlayer(item) {
|
|||||||
clearTimeout(hideTimer);
|
clearTimeout(hideTimer);
|
||||||
// Solange etwas steht, bleibt die Bedienung: sie auszublenden, während
|
// Solange etwas steht, bleibt die Bedienung: sie auszublenden, während
|
||||||
// niemand spielt, versteckt den einzigen Knopf, der weiterhilft.
|
// niemand spielt, versteckt den einzigen Knopf, der weiterhilft.
|
||||||
if (!v.paused) hideTimer = setTimeout(() => chrome.classList.remove('on'), 3000);
|
if (!v.paused) {
|
||||||
|
hideTimer = setTimeout(() => { chrome.classList.remove('on'); spin(); }, 3000);
|
||||||
|
}
|
||||||
|
spin();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Der freistehende Kreisel — nur, wenn die Bedienung aus ist. Sonst
|
||||||
|
stünden zwei übereinander: einer in der Mitte des Bildes und einer im
|
||||||
|
Abspielknopf, der genau davor sitzt. */
|
||||||
|
const spin = () => {
|
||||||
|
el2('pl-busy').classList.toggle('on', wartet && !chrome.classList.contains('on'));
|
||||||
};
|
};
|
||||||
layer.addEventListener('mousemove', show);
|
layer.addEventListener('mousemove', show);
|
||||||
layer.addEventListener('touchstart', show, { passive: true });
|
layer.addEventListener('touchstart', show, { passive: true });
|
||||||
@@ -1656,16 +1680,24 @@ function openPlayer(item) {
|
|||||||
el2('pl-fwd').onclick = () => { v.currentTime = Math.min(v.duration || 1e9, v.currentTime + P.skipForward); show(); };
|
el2('pl-fwd').onclick = () => { v.currentTime = Math.min(v.duration || 1e9, v.currentTime + P.skipForward); show(); };
|
||||||
v.addEventListener('click', toggle);
|
v.addEventListener('click', toggle);
|
||||||
|
|
||||||
|
/* Zeichnet den mittleren Knopf: Kreisel, Dreieck oder Doppelbalken.
|
||||||
|
|
||||||
|
Solange auf Daten gewartet wird, steht dort der Kreisel — **kein**
|
||||||
|
Dreieck. Ein Dreieck heißt „angehalten, drück mich", und beides stimmte
|
||||||
|
nicht: es lief bereits, und der Klick bewirkte nichts. */
|
||||||
const paint = () => {
|
const paint = () => {
|
||||||
el2('pl-toggle').innerHTML = icon(v.paused ? 'play' : 'pause', 52);
|
const knopf = el2('pl-toggle');
|
||||||
el2('pl-toggle').setAttribute('aria-label', v.paused ? 'Abspielen' : 'Anhalten');
|
knopf.innerHTML = wartet ? '<i class="pl-wait"></i>' : icon(v.paused ? 'play' : 'pause', 52);
|
||||||
show();
|
knopf.setAttribute('aria-label', wartet ? 'Lädt' : v.paused ? 'Abspielen' : 'Anhalten');
|
||||||
};
|
};
|
||||||
v.addEventListener('play', paint);
|
|
||||||
v.addEventListener('pause', paint);
|
const waiting = (an) => { wartet = an; paint(); spin(); };
|
||||||
v.addEventListener('waiting', () => el2('pl-busy').classList.add('on'));
|
|
||||||
|
v.addEventListener('play', () => { paint(); show(); });
|
||||||
|
v.addEventListener('pause', () => { waiting(false); show(); });
|
||||||
|
v.addEventListener('waiting', () => waiting(true));
|
||||||
v.addEventListener('playing', () => {
|
v.addEventListener('playing', () => {
|
||||||
el2('pl-busy').classList.remove('on');
|
waiting(false);
|
||||||
layer.classList.remove('loading');
|
layer.classList.remove('loading');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1814,7 +1846,7 @@ function openPlayer(item) {
|
|||||||
lauf.stelle = v.currentTime || 0;
|
lauf.stelle = v.currentTime || 0;
|
||||||
layer.querySelector('.pl-error')?.remove();
|
layer.querySelector('.pl-error')?.remove();
|
||||||
layer.classList.add('loading');
|
layer.classList.add('loading');
|
||||||
el2('pl-busy').classList.add('on');
|
waiting(true);
|
||||||
closeMenu();
|
closeMenu();
|
||||||
startStream(item, v, fail, lauf);
|
startStream(item, v, fail, lauf);
|
||||||
};
|
};
|
||||||
@@ -1890,7 +1922,7 @@ function openPlayer(item) {
|
|||||||
|
|
||||||
show();
|
show();
|
||||||
layer.classList.add('loading');
|
layer.classList.add('loading');
|
||||||
el2('pl-busy').classList.add('on');
|
waiting(true);
|
||||||
startStream(item, v, fail, lauf);
|
startStream(item, v, fail, lauf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user