Aufnehmen, was noch gar nicht läuft

Der dritte Modus neben Raster und Senderliste — und der einzige, den es
nicht überall gibt. Bei Plex fehlt schlicht der Schalter dorthin, denn
supportsRecordings fragt die eigene Fähigkeit ab.

Vormerken sitzt im Blatt, das eine angetippte Sendung ohnehin schon
öffnet, statt in einem zweiten Kontextmenü daneben. Trägt die Sendung
bereits einen Serien-Timer, steht dort das statt der Knöpfe: zweimal
vormerken ist kein Wunsch, sondern ein Versehen.

Aufnahmen führen über die Detailseite statt unmittelbar in den Player.
Eine Aufnahme ist auf dem Server ein gewöhnlicher Eintrag, und erst dort
steht, in welchen Fassungen sie vorliegt — von der Liste aus zu starten
hieße, das zu raten.

Zwölf Schlüssel in DE und EN. Die Übersetzungsdatei ist dabei auf das
Format zurückgeschrieben worden, das auch im letzten Commit steht;
inhaltlich ist sie HEAD plus diese zwölf, nachgerechnet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-14 06:26:34 +02:00
co-authored by Claude Opus 5
parent eebe9280cb
commit 982ae83e76
4 changed files with 614 additions and 59 deletions
+69 -13
View File
@@ -20,12 +20,16 @@ struct LiveTVView: View {
@State private var isTuning: String?
/// Warum das Einschalten nicht geklappt hat — mit jedem versuchten Weg.
@State private var tuneFailure: TuneFailure?
/// Raster oder Senderliste.
/// Raster, Senderliste oder Aufnahmen.
///
/// Auf dem Handy sind beide Ansichten berechtigt: das Raster zum Stöbern,
/// die Liste zum schnellen Umschalten. Die Wahl bleibt erhalten, weil
/// niemand sie zweimal treffen will.
@AppStorage("liveTV.showsGuide") private var showsGuide = true
/// Alle drei sind berechtigt: das Raster zum Stöbern, die Liste zum
/// schnellen Umschalten, die Aufnahmen für das, was schon da ist. Die
/// Wahl bleibt erhalten, weil niemand sie zweimal treffen will.
///
/// Neuer Schlüssel statt des alten Wahrheitswerts: aus „zeigt das Raster"
/// ist eine Wahl aus dreien geworden, und die beiden ineinander zu
/// überführen wäre mehr Aufwand, als einmal beim Raster zu beginnen.
@AppStorage("liveTV.mode") private var mode: LiveTVMode = .guide
/// Welche Sender gezeigt werden. Leer heißt alle.
@AppStorage("liveTV.selection") private var storedSelection = ""
@State private var showsSelection = false
@@ -61,7 +65,7 @@ struct LiveTVView: View {
Button("action.retry") { Task { await load() } }
}
case .available where channels.isEmpty:
case .available where channels.isEmpty && mode != .recordings:
// Empfänger gefunden, aber keine Sender. Ein anderer
// Zustand als „nicht eingerichtet", und die Unterscheidung
// zählt: sie sagt, ob es am Server liegt oder an der App.
@@ -77,7 +81,8 @@ struct LiveTVView: View {
}
case .available:
if showsGuide {
switch mode {
case .guide:
ProgramGuide(
channels: shownChannels,
programs: programs,
@@ -85,10 +90,20 @@ struct LiveTVView: View {
artwork: { channel in
session.backend?.artworkURL(path: channel.artworkPath, width: 120, height: 120)
},
onTune: { channel, program in Task { await tune(channel, program: program) } }
onTune: { channel, program in Task { await tune(channel, program: program) } },
// Beide nur, wenn der Server aufnehmen kann. Sonst
// `nil`, und die Knöpfe erscheinen gar nicht erst.
onRecord: session.supportsRecordings
? { program in Task { await record(program, series: false) } }
: nil,
onRecordSeries: session.supportsRecordings
? { program in Task { await record(program, series: true) } }
: nil
)
} else {
case .channels:
channelList
case .recordings:
RecordingsView()
}
}
}
@@ -106,16 +121,27 @@ struct LiveTVView: View {
.accessibilityLabel(Text("liveTV.selection"))
}
ToolbarItem(placement: .flixyTrailing) {
Picker("liveTV.mode", selection: $showsGuide) {
Image(systemName: "calendar.day.timeline.left").tag(true)
Image(systemName: "list.bullet").tag(false)
Picker("liveTV.mode", selection: $mode) {
Image(systemName: "calendar.day.timeline.left").tag(LiveTVMode.guide)
Image(systemName: "list.bullet").tag(LiveTVMode.channels)
// Der dritte Schalter fehlt schlicht, wo der Server
// nicht aufnehmen kann — bei Plex etwa.
if session.supportsRecordings {
Image(systemName: "recordingtape").tag(LiveTVMode.recordings)
}
}
.pickerStyle(.segmented)
.labelsHidden()
}
}
}
.task { await load() }
.task {
// Ein gespeicherter Modus, den dieser Server nicht kennt, führte
// sonst auf einen Schalter, den es nicht gibt — und damit auf
// einen leeren Bildschirm ohne Weg zurück.
if mode == .recordings, !session.supportsRecordings { mode = .guide }
await load()
}
.refreshable { await load() }
.sheet(isPresented: $showsSelection) {
ChannelSelectionSheet(
@@ -264,6 +290,36 @@ struct LiveTVView: View {
subscriptionKey: result.subscriptionKey
)
}
/// Merkt eine Sendung vor.
///
/// Ohne Rückmeldung bei Erfolg: der Knopf verschwindet, sobald die
/// Sendung einen Timer trägt, und das ist die Antwort. Eine Meldung
/// obendrauf wäre eine zweite.
private func record(_ program: LiveProgram, series: Bool) async {
guard let recorder = session.recorder else { return }
do {
if series {
try await recorder.scheduleSeriesTimer(for: program)
} else {
try await recorder.scheduleTimer(for: program)
}
await load()
} catch {
tuneFailure = TuneFailure(
channel: program.title,
reason: ErrorText.describe(error),
trace: []
)
}
}
}
/// Was Live-TV gerade zeigt.
enum LiveTVMode: String {
case guide
case channels
case recordings
}
/// Eine Senderzeile mit dem, was gerade und als Nächstes läuft.