Files
Flaro/Flixy/Views/Settings/ServerSettingsView.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

152 lines
5.6 KiB
Swift

import SwiftUI
import FlixyDesign
import MediaKit
import PlexKit
/// Mit welchem Server gesprochen wird — und was er gerade anbietet.
struct ServerSettingsView: View {
@Environment(FlixySession.self) private var session
@Environment(\.flixyAccent) private var accent
var body: some View {
Form {
Section("settings.server") {
LabeledContent("settings.serverName", value: session.serverName)
// Welche Art von Server — bei zwei möglichen ist das keine
// Nebensache mehr, sondern die erste Frage bei jeder
// Rückmeldung.
LabeledContent("settings.serverKind", value: kindLabel)
if let connection = session.server?.connection {
LabeledContent("settings.connection", value: connectionLabel(connection))
}
if let address = session.serverAddress {
LabeledContent("settings.address", value: address)
.font(.flixyCaption)
.textSelection(.enabled)
}
if let accountName = session.accountName {
LabeledContent("settings.account", value: accountName)
}
}
Section {
ForEach(session.libraries) { library in
LibraryRow(library: library)
}
} header: {
Text("settings.libraries")
} footer: {
// Der Suchlauf stand einmal im Dreipunktmenü der Mediathek —
// einen Tipp neben „Filter" und „Ansicht wechseln". Er
// beschäftigt den Server minutenlang und gehört nicht in eine
// Reihe mit Dingen, die man im Vorbeigehen antippt.
if session.backend?.capabilities.contains(.libraryScan) == true {
Text("settings.scanExplanation")
}
}
}
.tint(accent)
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("settings.server"))
.flixyInlineTitle()
}
private var kindLabel: String {
switch session.kind {
case .plex: "Plex"
case .jellyfin: "Jellyfin"
case nil: "—"
}
}
/// Zeigt, über welchen Weg die Verbindung läuft — beim Suchen nach
/// Ruckeln ist das die erste Frage.
private func connectionLabel(_ connection: PlexConnection) -> String {
if connection.local { return String(localized: "settings.connectionLocal") }
if connection.relay { return String(localized: "settings.connectionRelay") }
return String(localized: "settings.connectionRemote")
}
}
/// Eine Mediathek in den Einstellungen, mit dem Suchlauf daneben.
struct LibraryRow: View {
let library: MediaLibrary
@Environment(FlixySession.self) private var session
@Environment(\.flixyAccent) private var accent
@State private var phase: Phase = .idle
private enum Phase { case idle, running, done }
var body: some View {
LabeledContent {
HStack(spacing: FlixySpacing.regular) {
// Nur, wenn es nicht dasselbe Wort ist: eine Mediathek namens
// „Filme" stand sonst als „Filme — Filme" da.
if library.title.localizedCaseInsensitiveCompare(library.kind.label) != .orderedSame {
Text(library.kind.label)
.foregroundStyle(Color.flixyTextSecondary)
}
if session.backend?.capabilities.contains(.libraryScan) == true {
Button {
Task { await scan() }
} label: {
symbol
}
.buttonStyle(.borderless)
.disabled(phase == .running)
.accessibilityLabel(Text("library.scan"))
}
}
} label: {
Text(library.title)
}
}
@ViewBuilder
private var symbol: some View {
switch phase {
case .idle:
Image(systemName: "arrow.triangle.2.circlepath")
case .running:
ProgressView().controlSize(.small)
case .done:
Image(systemName: "checkmark").foregroundStyle(accent)
}
}
/// Stößt einen Suchlauf auf dem Server an.
///
/// Der Server meldet nicht, wann er fertig ist. Deshalb steht hier kein
/// Fortschritt, sondern nur ein Haken dafür, dass der Auftrag angekommen
/// ist — ein Balken, der etwas anderes behauptet, wäre erfunden.
private func scan() async {
guard let backend = session.backend, phase != .running else { return }
phase = .running
try? await backend.refreshLibrary(library)
phase = .done
try? await Task.sleep(for: .seconds(4))
phase = .idle
}
}
extension MediaLibrary.Kind {
/// Wie diese Art von Mediathek heißt.
///
/// Übersetzt statt durchgereicht: die Server liefern „movies" und
/// „tvshows", und das großgeschrieben anzuzeigen war eine englische
/// Beschriftung in einer deutschen Oberfläche.
var label: String {
switch self {
case .movie: String(localized: "library.kind.movie")
case .show: String(localized: "library.kind.show")
case .music: String(localized: "library.kind.music")
case .photo: String(localized: "library.kind.photo")
case .liveTV: String(localized: "library.kind.liveTV")
case .other: String(localized: "library.kind.other")
}
}
}