„Wartet" für immer, obwohl der Server längst fertig war
Der Eintrag ging nach einer 202 zurück auf „wartet" und rief danach `resume`. Das nimmt aber nur auf, was **angehalten oder gescheitert** ist — ein wartender lief in die Schutzklausel und kam nie wieder heraus. Im Protokoll packte ffmpeg um, in der App stand „wartet", und dazwischen lag nichts. Jetzt wird die Warteschlange abgearbeitet statt ein einzelner Eintrag aufgenommen; „wartet" steht darin. Dazu zwei Dinge, um die du gebeten hast: **Das Protokoll liest sich wie eine Kommandozeile.** Älteste Zeile oben, neueste unten, die Ansicht steht am Ende. Mitgeschoben wird nur, wenn man unten steht — wer hochgescrollt hat, liest etwas, und ihn bei jeder neuen Zeile ans Ende zu reissen wäre, als nähme man ihm die Seite aus der Hand. **Ein Tipp auf eine Kachel führt auf die Detailseite**, am Telefon wie am iPad wie am Mac. Nur in „Weiterschauen" spielt er ab: dort steht der Titel, *weil* man ihn angefangen hat. Vorher war es umgekehrt, mit dem Argument „wer eine Kachel antippt, weiss schon, was er sehen will". Im Gebrauch stimmte das nicht — wer nachsehen wollte, wie viele Staffeln eine Serie hat, bekam den Film. Ein Umweg von einem Schritt ist ärgerlich; eine Wiedergabe, die man nicht wollte, ist schlimmer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ce5dc66047
commit
0a394b9b3b
@@ -17,14 +17,18 @@ import MediaKit
|
|||||||
/// und wäre für VoiceOver gar nicht mehr da.
|
/// und wäre für VoiceOver gar nicht mehr da.
|
||||||
struct QuickPlayTile<Card: View>: View {
|
struct QuickPlayTile<Card: View>: View {
|
||||||
let item: MediaItem
|
let item: MediaItem
|
||||||
/// Ob ein Tipp auf die Detailseite führt, statt abzuspielen.
|
/// Ob ein Tipp sofort abspielt, statt auf die Detailseite zu führen.
|
||||||
///
|
///
|
||||||
/// **Für die Suche.** Dort weiß man gerade *nicht*, was man sehen will —
|
/// **Nur beim Weiterschauen.** Dort ist die Absicht eindeutig: der Titel
|
||||||
/// sonst hätte man nicht gesucht. Ein Tipp, der sofort abspielt, ist auf
|
/// steht in dieser Reihe, *weil* man ihn angefangen hat.
|
||||||
/// der Startseite richtig (die Kachel stand schon da, man kennt sie) und
|
///
|
||||||
/// in einer Trefferliste falsch: der erste Treffer ist eine Vermutung des
|
/// **Überall sonst führt der Tipp auf die Seite.** Vorher war es
|
||||||
/// Servers, keine Entscheidung des Nutzers.
|
/// umgekehrt, mit dem Argument „wer eine Kachel antippt, weiss meistens
|
||||||
var opensDetails: Bool = false
|
/// schon, was er sehen will". Im Gebrauch stimmte das nicht: wer nur
|
||||||
|
/// nachsehen will, wie viele Staffeln eine Serie hat, bekam den Film.
|
||||||
|
/// Ein Umweg von einem Schritt ist ärgerlich; eine Wiedergabe, die man
|
||||||
|
/// nicht wollte, ist schlimmer.
|
||||||
|
var startsPlayback: Bool = false
|
||||||
@ViewBuilder let card: () -> Card
|
@ViewBuilder let card: () -> Card
|
||||||
|
|
||||||
@Environment(FlaroSession.self) private var session
|
@Environment(FlaroSession.self) private var session
|
||||||
@@ -69,8 +73,9 @@ struct QuickPlayTile<Card: View>: View {
|
|||||||
///
|
///
|
||||||
/// Drei Gründe, derselbe Ausgang: am Mac ist der Klick seit jeher der Weg
|
/// Drei Gründe, derselbe Ausgang: am Mac ist der Klick seit jeher der Weg
|
||||||
/// auf die Seite (zum Starten gibt es den Knopf beim Überfahren), eine
|
/// auf die Seite (zum Starten gibt es den Knopf beim Überfahren), eine
|
||||||
/// Sammlung hat nichts abzuspielen, und in der Suche ist es gewollt.
|
/// Sammlung hat nichts abzuspielen, und überall ausser beim
|
||||||
private var tapOpensDetails: Bool { isMac || opensDetails || !isPlayable }
|
/// Weiterschauen ist die Seite das Ziel.
|
||||||
|
private var tapOpensDetails: Bool { isMac || !startsPlayback || !isPlayable }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
kachel
|
kachel
|
||||||
@@ -99,11 +104,9 @@ struct QuickPlayTile<Card: View>: View {
|
|||||||
// Kachel steht.
|
// Kachel steht.
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
if isPlayable {
|
if isPlayable {
|
||||||
// **Am Telefon und am iPad führt der Weg zur Detailseite
|
// **Nur dort, wo der Tipp abspielt.** Sonst führt er
|
||||||
// hierher.** Antippen spielt dort ab; wer stattdessen
|
// schon auf die Seite, und ein zweiter Weg dorthin im Menü
|
||||||
// nachsehen will, was das überhaupt ist, drückt lang. Am Mac
|
// wäre ein Eintrag, der nichts Neues anbietet.
|
||||||
// ist es umgekehrt: dort öffnet der Klick die Seite, und zum
|
|
||||||
// Starten gibt es den Knopf beim Überfahren.
|
|
||||||
if !tapOpensDetails {
|
if !tapOpensDetails {
|
||||||
Button("hero.details", systemImage: "info.circle") { push(route) }
|
Button("hero.details", systemImage: "info.circle") { push(route) }
|
||||||
Divider()
|
Divider()
|
||||||
@@ -199,14 +202,11 @@ struct QuickPlayTile<Card: View>: View {
|
|||||||
|
|
||||||
/// Die Kachel selbst — und was ein Antippen bedeutet.
|
/// Die Kachel selbst — und was ein Antippen bedeutet.
|
||||||
///
|
///
|
||||||
/// **Am Telefon und am iPad spielt es ab.** Wer eine Kachel antippt, weiss
|
/// **Der Tipp führt auf die Detailseite**, am Telefon wie am iPad wie am
|
||||||
/// meistens schon, was er sehen will; der Umweg über die Detailseite
|
/// Mac. Nur in „Weiterschauen" spielt er ab: dort steht der Titel,
|
||||||
/// kostete zwei Schritte für eine Absicht. Die Seite ist weiter da — über
|
/// *weil* man ihn angefangen hat. Abspielen ist sonst einen Schritt
|
||||||
/// langes Drücken, wo auch „gesehen" und „entfernen" stehen.
|
/// weiter — über den Knopf auf der Seite, über langes Drücken, oder am
|
||||||
///
|
/// Mac über den Knopf beim Überfahren.
|
||||||
/// **Am Mac bleibt der Klick die Detailseite.** Dort gibt es den
|
|
||||||
/// Abspielknopf beim Überfahren, und langes Drücken ist keine übliche
|
|
||||||
/// Geste — ein Rechtsklick ist der Weg ins Menü, nicht in den Film.
|
|
||||||
///
|
///
|
||||||
/// Ein `NavigationLink` schiebt selbst; ein Knopf braucht dafür den Weg
|
/// Ein `NavigationLink` schiebt selbst; ein Knopf braucht dafür den Weg
|
||||||
/// aus der Umgebung. Deshalb zwei Fassungen und nicht eine mit `if` im
|
/// aus der Umgebung. Deshalb zwei Fassungen und nicht eine mit `if` im
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import MediaKit
|
|||||||
/// Eine waagerechte Reihe von Titeln.
|
/// Eine waagerechte Reihe von Titeln.
|
||||||
struct MediaRow: View {
|
struct MediaRow: View {
|
||||||
let row: MediaRowGroup
|
let row: MediaRowGroup
|
||||||
/// Wird durchgereicht — siehe `QuickPlayTile`.
|
/// Ob ein Tipp sofort abspielt — siehe `QuickPlayTile`.
|
||||||
var opensDetails: Bool = false
|
///
|
||||||
|
/// **Die Reihe weiss es, nicht die Kachel.** „Weiterschauen" ist eine
|
||||||
|
/// Eigenschaft der Reihe; eine Kachel kennt nur ihren Titel.
|
||||||
|
private var startsPlayback: Bool { row.isContinueWatching }
|
||||||
|
|
||||||
@Environment(FlaroSession.self) private var session
|
@Environment(FlaroSession.self) private var session
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ struct MediaRow: View {
|
|||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
LazyHStack(spacing: FlaroSpacing.regular) {
|
LazyHStack(spacing: FlaroSpacing.regular) {
|
||||||
ForEach(row.items) { item in
|
ForEach(row.items) { item in
|
||||||
QuickPlayTile(item: item, opensDetails: opensDetails) { card(for: item) }
|
QuickPlayTile(item: item, startsPlayback: startsPlayback) { card(for: item) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ struct SearchView: View {
|
|||||||
// weiß aber noch nicht, was er sehen will; der erste
|
// weiß aber noch nicht, was er sehen will; der erste
|
||||||
// Treffer ist eine Vermutung des Servers. Starten geht
|
// Treffer ist eine Vermutung des Servers. Starten geht
|
||||||
// weiter, über langes Drücken.
|
// weiter, über langes Drücken.
|
||||||
MediaRow(row: hub, opensDetails: true)
|
MediaRow(row: hub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, FlaroSpacing.loose)
|
.padding(.vertical, FlaroSpacing.loose)
|
||||||
|
|||||||
@@ -648,10 +648,14 @@ public final class DownloadCoordinator {
|
|||||||
item.state = .waiting
|
item.state = .waiting
|
||||||
save()
|
save()
|
||||||
|
|
||||||
|
// **Nicht `resume`.** Das nimmt nur auf, was angehalten oder
|
||||||
|
// gescheitert ist — ein wartender Eintrag lief dort in die
|
||||||
|
// Schutzklausel und blieb für immer auf „wartet" stehen, während der
|
||||||
|
// Server längst fertig gepackt hatte. Die Warteschlange abzuarbeiten
|
||||||
|
// ist das, was hier gebraucht wird: `.waiting` steht darin.
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
try? await Task.sleep(for: Self.wartezeit)
|
try? await Task.sleep(for: Self.wartezeit)
|
||||||
guard let self else { return }
|
await self?.startNextIfPossible()
|
||||||
await self.resume(item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,14 @@ import { abschnitt } from './teile';
|
|||||||
* was gerade schiefgeht, brauchte drei Handgriffe für eine Antwort, die auf
|
* was gerade schiefgeht, brauchte drei Handgriffe für eine Antwort, die auf
|
||||||
* der ersten Seite hätte stehen können.
|
* der ersten Seite hätte stehen können.
|
||||||
*
|
*
|
||||||
* **Jetzt steht es sofort da und läuft mit.** Neueste Zeile oben, damit man
|
* **Jetzt steht es sofort da und läuft mit.** Wie eine Kommandozeile:
|
||||||
* zum Mitlesen nicht scrollen muss; die älteren Tage stehen darunter als
|
* älteste Zeile oben, neueste unten, und die Ansicht steht am Ende. Das ist
|
||||||
* Kacheln zum Herunterladen.
|
* die Richtung, in der ein Logbuch gelesen wird — man sieht, was zuletzt
|
||||||
|
* geschah, und scrollt nach oben, um zu sehen, was davor war.
|
||||||
|
*
|
||||||
|
* **Nur mitgeschoben, wenn man unten steht.** Wer hochgescrollt hat, liest
|
||||||
|
* etwas; ihn bei jeder neuen Zeile ans Ende zu reissen wäre, als würde man
|
||||||
|
* ihm die Seite aus der Hand nehmen.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Wie viele Zeilen anfangs geholt werden. */
|
/** Wie viele Zeilen anfangs geholt werden. */
|
||||||
@@ -30,10 +35,13 @@ export function protokollbereich(): HTMLElement {
|
|||||||
liste.innerHTML = '<div class="spinner"></div>';
|
liste.innerHTML = '<div class="spinner"></div>';
|
||||||
bereich.appendChild(liste);
|
bereich.appendChild(liste);
|
||||||
|
|
||||||
|
// **Über der Liste, nicht darunter.** „Weiter zurück" holt ältere Zeilen,
|
||||||
|
// und die erscheinen oben — ein Knopf dafür am unteren Ende zeigte in die
|
||||||
|
// falsche Richtung.
|
||||||
const mehr = document.createElement('button');
|
const mehr = document.createElement('button');
|
||||||
mehr.className = 'btn hidden';
|
mehr.className = 'btn hidden';
|
||||||
mehr.textContent = 'Weiter zurück';
|
mehr.textContent = 'Weiter zurück';
|
||||||
bereich.appendChild(mehr);
|
bereich.insertBefore(mehr, liste);
|
||||||
|
|
||||||
const archiv = document.createElement('div');
|
const archiv = document.createElement('div');
|
||||||
archiv.className = 'protokolltage';
|
archiv.className = 'protokolltage';
|
||||||
@@ -90,7 +98,11 @@ export function protokollbereich(): HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function zeichnen(roh: string): void {
|
function zeichnen(roh: string): void {
|
||||||
const zeilenListe = roh.split('\n').filter(z => z.trim().length > 0).reverse();
|
const zeilenListe = roh.split('\n').filter(z => z.trim().length > 0);
|
||||||
|
|
||||||
|
// **Vor dem Neuzeichnen merken, ob man unten stand.** Danach ist die
|
||||||
|
// Höhe eine andere, und die Frage liesse sich nicht mehr beantworten.
|
||||||
|
const amEnde = liste.scrollHeight - liste.scrollTop - liste.clientHeight < 40;
|
||||||
|
|
||||||
liste.innerHTML = '';
|
liste.innerHTML = '';
|
||||||
if (zeilenListe.length === 0) {
|
if (zeilenListe.length === 0) {
|
||||||
@@ -99,6 +111,7 @@ export function protokollbereich(): HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
zeilenListe.forEach(z => liste.appendChild(protokollzeile(z)));
|
zeilenListe.forEach(z => liste.appendChild(protokollzeile(z)));
|
||||||
|
if (amEnde) liste.scrollTop = liste.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tageLaden(): Promise<void> {
|
async function tageLaden(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user