import SwiftUI import HaloDesign import MediaKit /// 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: [LiveChannel] @Binding var selection: Set @Environment(HaloSession.self) private var session @Environment(\.haloAccent) 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")) .haloInlineTitle() .toolbar { ToolbarItem(placement: .haloLeading) { // „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: LiveChannel) -> 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: HaloSpacing.regular) { HaloArtwork( url: session.backend?.artworkURL(path: channel.artworkPath, width: 200, height: 120), aspectRatio: HaloAspectRatio.channelLogo, fallbackSymbol: "tv", contentMode: .fit ) .frame(width: 48, height: 29) if let number = channel.number { Text(number) .font(.system(.caption, design: .monospaced)) .foregroundStyle(Color.haloTextMuted) } Text(channel.title) .font(.haloBody) .foregroundStyle(Color.haloTextPrimary) Spacer() Image(systemName: isShown(channel) ? "checkmark.circle.fill" : "circle") .foregroundStyle(isShown(channel) ? accent : Color.haloTextMuted) } .contentShape(.rect) } .buttonStyle(.plain) } private func isShown(_ channel: LiveChannel) -> 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: LiveChannel, selection: Set) -> Bool { selection.isEmpty || selection.contains(channel.id) } static func apply(_ selection: Set, to channels: [LiveChannel]) -> [LiveChannel] { 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 { Set(stored.split(separator: "\n").map(String.init)) } static func encode(_ selection: Set) -> String { selection.sorted().joined(separator: "\n") } }