Files
Flaro/Flixy/Views/LiveTV/ChannelSelectionSheet.swift
T
ScarriffleandClaude Opus 5 d54954b9c2 macOS: die App läuft jetzt auf dem Mac
Und dein Argument dafür war das beste: auf dem Mac kann ich selbst hinsehen.
Kein Simulator-Tunnel, der mitten im Prüfen abstürzt — `screencapture` auf das
Fenster der App, und zwar nur auf ihres, nicht auf den Bildschirm.

**Ein eigenes Ziel, nicht ein gemeinsames.** Die Info.plist der iOS-App ist
voller iOS-Angaben: Startbildschirm, erlaubte Ausrichtungen, Live Activities.
Die alle bedingt zu machen hieße, dass jede spätere Änderung an der iOS-Seite
über eine Stelle geht, die auch den Mac betrifft. Zwei Ziele sind ein paar
Zeilen mehr und eine Sorge weniger.

**Was den Unterschied ausmacht, steht an einer Stelle.** Ein neuer
`PlatformShims` in FlixyDesign übersetzt, was auf beiden Seiten verschieden
heißt: schmale Titelzeile, halbhohes Blatt, Eingabefelder ohne
Großschreibung, die Plätze in der Werkzeugleiste, die Zwischenablage. Zwanzig
Ansichten benutzen ihn, statt zwanzigmal `#if os(iOS)` zu tragen — sonst
wandert die Fallunterscheidung in jede neue Datei und wird beim übernächsten
Bildschirm vergessen.

Wirklich verschieden sind nur fünf Stellen: die Bildfläche (`UIView` gegen
`NSView`), der AirPlay-Knopf, die Ausrichtungssperre (auf dem Mac gibt es
nichts zu sperren), der App-Delegat für Hintergrund-Downloads und das
Anmeldefenster, das ein anderes Fenster als Anker braucht.

**Das Karussell blättert jetzt mit einem einrastenden Streifen** statt mit
`TabView(.page)` — den Seitenstil gibt es auf dem Mac nicht. Das ist nebenbei
die bessere Lösung: das verschachtelte TabView hatte die Eigenschaft, beim
Wechsel kurz zwei Seiten gleichzeitig zu zeigen.

**Was auf dem Mac fehlt und warum:** VLC. Das Binärpaket ist auf iOS
festgelegt; sein xcframework enthält zwar Mac-Teile, aber das ist eine eigene
Runde. Solange fällt die Wiedergabe dort auf den Systemplayer zurück — MKV und
DTS gehen also noch nicht. Live Activities gibt es auf dem Mac nicht, Bild-in-
Bild startet von Hand statt beim Verlassen der App.

Gesehen: die App startet, der Anmeldebildschirm steht, beide Wege sind da.
Angemeldet habe ich mich auf dem Mac **nicht** — das ginge nur, indem ich
einen Token in deinen Schlüsselbund schreibe, und dann fragt macOS dich beim
ersten Zugriff nach dem Passwort. Das machst du mit einem Klick selbst.

iOS baut unverändert, 263 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:45:13 +02:00

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"))
.flixyInlineTitle()
.toolbar {
ToolbarItem(placement: .flixyLeading) {
// „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")
}
}