Drei Sachen aus dem Feldtest. Der Griff zum Größerziehen war wirkungslos, und zwar durch meinen Fehler: die Drehung lag auf dem ganzen Überzug statt auf dem Symbol. Bei zwei der vier Ecken kippte damit die gesamte Trefferfläche weg, und man griff ins Leere. Jetzt sitzt der Griff über `overlay(alignment:)`, nur der Pfeil dreht sich, und das Ziehen hat Vorrang vor dem Verschieben der Kachel — sonst gewinnt das Verschieben, das am selben Ort hängt. Senderlogos sind Schriftzüge und fast nie quadratisch. Formatfüllend in einem Quadrat fehlten links und rechts genau die Teile, an denen man den Sender erkennt. Sie bekommen jetzt eine breitere Fläche und werden eingepasst statt beschnitten — `FlixyArtwork` kann beides, und welches gilt, entscheidet der Aufrufer. Und im Player steht bei Live-TV, was gerade läuft. Der Name reist mit dem Einschalten mit, denn nachzuschlagen ist er nicht — ein Sender hat keine Metadaten in der Mediathek. Beim Aufdecken der Bedienelemente wird er nachgeholt, aber nur, wenn die alte Sendung wirklich abgelaufen ist: sonst stünde derselbe Name den ganzen Abend, und das wäre derselbe Fehler, der eben erst im Programm behoben wurde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
122 lines
4.5 KiB
Swift
122 lines
4.5 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import PlexKit
|
|
|
|
/// Welche Sender überhaupt angezeigt werden.
|
|
///
|
|
/// Der Empfänger liefert alles, was die Senderliste hergibt — bei einer
|
|
/// Swisscom-Liste sind das mehr als zwanzig, und die meisten sieht man nie an.
|
|
/// Jeden davon zu zeichnen und sein Programm zu holen kostet Zeit und macht
|
|
/// die Ansicht unübersichtlich.
|
|
struct ChannelSelectionSheet: View {
|
|
let channels: [PlexChannel]
|
|
@Binding var selection: Set<String>
|
|
|
|
@Environment(FlixySession.self) private var session
|
|
@Environment(\.flixyAccent) private var accent
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
Section {
|
|
ForEach(channels) { channel in
|
|
row(for: channel)
|
|
}
|
|
} footer: {
|
|
Text("liveTV.selection.hint")
|
|
}
|
|
}
|
|
.navigationTitle(Text("liveTV.selection"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
// „Alle" statt „keine": eine leere Auswahl bedeutet
|
|
// ohnehin alle, und ein Knopf, der scheinbar nichts tut,
|
|
// wäre verwirrend.
|
|
Button("liveTV.selection.all") { selection.removeAll() }
|
|
.disabled(selection.isEmpty)
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("action.done") { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func row(for channel: PlexChannel) -> some View {
|
|
Button {
|
|
// Beim ersten Antippen aus „alle" eine echte Auswahl machen:
|
|
// sonst hakt man einen Sender an und alle anderen bleiben
|
|
// trotzdem sichtbar.
|
|
if selection.isEmpty {
|
|
selection = Set(channels.map(\.id)).subtracting([channel.id])
|
|
} else if selection.contains(channel.id) {
|
|
selection.remove(channel.id)
|
|
} else {
|
|
selection.insert(channel.id)
|
|
}
|
|
} label: {
|
|
HStack(spacing: FlixySpacing.regular) {
|
|
FlixyArtwork(
|
|
url: session.server?.artworkURL(path: channel.thumb, width: 200, height: 120),
|
|
aspectRatio: FlixyAspectRatio.channelLogo,
|
|
fallbackSymbol: "tv",
|
|
contentMode: .fit
|
|
)
|
|
.frame(width: 48, height: 29)
|
|
|
|
if let number = channel.channelIdentifier {
|
|
Text(number)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundStyle(Color.flixyTextMuted)
|
|
}
|
|
|
|
Text(channel.title)
|
|
.font(.flixyBody)
|
|
.foregroundStyle(Color.flixyTextPrimary)
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: isShown(channel) ? "checkmark.circle.fill" : "circle")
|
|
.foregroundStyle(isShown(channel) ? accent : Color.flixyTextMuted)
|
|
}
|
|
.contentShape(.rect)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
private func isShown(_ channel: PlexChannel) -> Bool {
|
|
ChannelSelection.shows(channel, selection: selection)
|
|
}
|
|
}
|
|
|
|
/// Die Regel, nach der ausgewählt wird.
|
|
///
|
|
/// Eigener Typ, weil sie an zwei Stellen gilt — in der Auswahl und in der
|
|
/// Anzeige — und beide dasselbe meinen müssen. Eine **leere** Auswahl bedeutet
|
|
/// „alle": so sieht man beim ersten Öffnen sein Programm, ohne vorher etwas
|
|
/// angehakt zu haben.
|
|
enum ChannelSelection {
|
|
static func shows(_ channel: PlexChannel, selection: Set<String>) -> Bool {
|
|
selection.isEmpty || selection.contains(channel.id)
|
|
}
|
|
|
|
static func apply(_ selection: Set<String>, to channels: [PlexChannel]) -> [PlexChannel] {
|
|
guard !selection.isEmpty else { return channels }
|
|
let kept = channels.filter { selection.contains($0.id) }
|
|
// Wählt jemand Sender ab, die es nicht mehr gibt, bliebe sonst eine
|
|
// leere Liste stehen, ohne dass er es ändern könnte.
|
|
return kept.isEmpty ? channels : kept
|
|
}
|
|
|
|
/// Die Auswahl als Text, wie sie in den Einstellungen liegt.
|
|
static func decode(_ stored: String) -> Set<String> {
|
|
Set(stored.split(separator: "\n").map(String.init))
|
|
}
|
|
|
|
static func encode(_ selection: Set<String>) -> String {
|
|
selection.sorted().joined(separator: "\n")
|
|
}
|
|
}
|