Der Simulator läuft wieder. Ich habe alles durchgeklickt, was ich dir in den letzten beiden Runden ungeprüft gegeben habe. Es war nicht in Ordnung. **Zwei Navigationsleisten übereinander.** Auf dem iPhone hat die Leiste unten fünf Plätze; alles darüber schiebt das System in ein „Mehr"-Verzeichnis — und dessen Einträge bekamen zwei Zurück-Pfeile, einen vom System und einen von uns. Statt das zu flicken, gibt es jetzt die **Bibliothek**, die du ohnehin wolltest: auf dem iPhone ein Eintrag, hinter dem alle Mediatheken stehen, auf dem iPad weiter jede einzeln in der Seitenleiste. Damit bleibt die Leiste bei fünf Einträgen, und das „Mehr" entfällt. **Kacheln in der Mediathek öffneten nichts.** Kein Fehler, keine Meldung — Antippen führte einfach ins Leere. Der Grund: in einem Stapel dürfen nicht beide Arten von Verknüpfung nebeneinander vorkommen. Sobald ein Bildschirm über eine **Ansichts**-Verknüpfung geschoben wurde, fanden die **Wert**-Verknüpfungen darin ihr Ziel nicht mehr. Jetzt ist alles wertbasiert, und das Ziel für Detailseiten steht **einmal je Stapel** an dessen Wurzel statt in jedem Bildschirm einzeln. **Der Lupenknopf in der Kopfzeile zeigte auf ein Ziel, das es nie gab.** Kein Bildschirm hat je ein `SearchRoute`-Ziel angemeldet — der Knopf tat seit jeher nichts. Er ist weg; die Suche hat ihren eigenen Platz in der Leiste. **Der Download-Dialog zeigte Schlüssel statt Namen** — „quality.hd" statt „720p". Die drei Übersetzungen fehlten schlicht. **Die Kontokarte wuchs aus dem Bild heraus.** Die Leiste unten in der Seitenleiste hat eine feste Höhe; meine aufklappende Karte schob die Zeile mit dem Namen nach unten hinaus. Jetzt hängt sie als Popover darüber — die Zeile bleibt stehen, wie bei JellyTV. Verkleinerte Downloads bei Jellyfin laufen: Stufen erscheinen, die Adresse stimmt, die Datei kommt an. Dass die Attrappe nur 28 kB liefert und die App das als „keine Videodatei" zurückweist, ist richtig so — die Prüfung tut, was sie soll. Zwei Gruben in der Attrappe nebenbei gestopft: sie kannte `/stream.mp4` nicht und meldete ihr Testbild als 360p, womit es gar keine kleineren Stufen geben konnte. 263 Tests grün. Diesmal alles angesehen: iPhone und iPad, Einstellungen, Bibliothek, Detailseite, Download-Dialog, Kontokarte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
5.4 KiB
Swift
149 lines
5.4 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
|
|
/// Wer angemeldet ist — unten in der Seitenleiste.
|
|
///
|
|
/// Der Platz ist nicht zufällig: dort sucht man sein Konto, weil es in jeder
|
|
/// App der letzte Eintrag ist. Und von dort führt der Weg in die
|
|
/// Einstellungen, statt dass sie als gleichrangiger Eintrag zwischen den
|
|
/// Mediatheken stehen.
|
|
///
|
|
/// **Eine Karte, die über der Zeile aufgeht**, kein Systemmenü: mit denselben
|
|
/// Abständen und derselben Schrift wie der Rest der App, statt der grauen
|
|
/// Standardliste.
|
|
///
|
|
/// Sie hängt als Popover an der Zeile und wächst nicht in der Leiste selbst.
|
|
/// Das war der erste Versuch, und er ging schief: die Leiste unten hat eine
|
|
/// feste Höhe, die Karte wuchs nach unten aus dem Bild heraus, und die Zeile
|
|
/// mit dem Namen verschwand dabei.
|
|
///
|
|
/// Auf dem iPhone gibt es diese Leiste nicht; dort haben die Einstellungen
|
|
/// ihren eigenen Eintrag.
|
|
struct AccountBar: View {
|
|
@Binding var selection: SidebarDestination
|
|
|
|
@Environment(FlixySession.self) private var session
|
|
@Environment(\.flixyAccent) private var accent
|
|
@State private var isExpanded = false
|
|
@State private var showsSignOutConfirmation = false
|
|
|
|
private var title: String {
|
|
session.accountName ?? session.serverName
|
|
}
|
|
|
|
/// Der Server steht nur dann darunter, wenn oben schon der Kontoname
|
|
/// steht — sonst stünde zweimal dasselbe.
|
|
private var subtitle: String? {
|
|
session.accountName == nil ? nil : session.serverName
|
|
}
|
|
|
|
var body: some View {
|
|
accountRow
|
|
.popover(isPresented: $isExpanded, arrowEdge: .bottom) {
|
|
VStack(spacing: 0) {
|
|
action("account.settings", symbol: "gearshape") {
|
|
isExpanded = false
|
|
selection = .settings
|
|
}
|
|
separator
|
|
action(
|
|
"action.signOut",
|
|
symbol: "rectangle.portrait.and.arrow.right",
|
|
isDestructive: true
|
|
) {
|
|
isExpanded = false
|
|
showsSignOutConfirmation = true
|
|
}
|
|
}
|
|
.frame(minWidth: 240)
|
|
.background(Color.flixySurface)
|
|
// Auf dem iPhone gäbe es diese Leiste gar nicht; die Angabe
|
|
// steht trotzdem da, damit aus dem Popover dort kein
|
|
// bildschirmfüllendes Blatt wird.
|
|
.presentationCompactAdaptation(.popover)
|
|
}
|
|
.confirmationDialog(
|
|
Text("settings.signOutConfirm"),
|
|
isPresented: $showsSignOutConfirmation,
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("action.signOut", role: .destructive) { session.signOut() }
|
|
Button("action.cancel", role: .cancel) {}
|
|
} message: {
|
|
Text("settings.signOutExplanation")
|
|
}
|
|
}
|
|
|
|
private var accountRow: some View {
|
|
Button {
|
|
isExpanded.toggle()
|
|
} label: {
|
|
HStack(spacing: FlixySpacing.snug) {
|
|
avatar
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(title)
|
|
.font(.flixyCardTitle)
|
|
.foregroundStyle(Color.flixyTextPrimary)
|
|
.lineLimit(1)
|
|
|
|
if let subtitle {
|
|
Text(subtitle)
|
|
.font(.flixyCaption)
|
|
.foregroundStyle(Color.flixyTextMuted)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
|
|
Spacer(minLength: FlixySpacing.snug)
|
|
|
|
// Zeigt nach oben: dorthin geht die Karte auf.
|
|
Image(systemName: "chevron.up")
|
|
.font(.caption2.weight(.semibold))
|
|
.foregroundStyle(Color.flixyTextMuted)
|
|
}
|
|
.padding(.horizontal, FlixySpacing.regular)
|
|
.padding(.vertical, FlixySpacing.snug)
|
|
.contentShape(.rect)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(Text(title))
|
|
.accessibilityHint(Text("account.hint"))
|
|
}
|
|
|
|
private func action(
|
|
_ title: LocalizedStringKey,
|
|
symbol: String,
|
|
isDestructive: Bool = false,
|
|
perform: @escaping () -> Void
|
|
) -> some View {
|
|
Button(action: perform) {
|
|
Label(title, systemImage: symbol)
|
|
.font(.flixyBody)
|
|
.foregroundStyle(isDestructive ? Color.flixyDestructive : Color.flixyTextPrimary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, FlixySpacing.regular)
|
|
.padding(.vertical, FlixySpacing.regular)
|
|
.contentShape(.rect)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
private var separator: some View {
|
|
Divider().overlay(Color.flixySeparator)
|
|
}
|
|
|
|
/// Der erste Buchstabe auf einer Scheibe.
|
|
///
|
|
/// Jellyfin gibt kein Profilbild heraus, ohne dass eines hinterlegt ist,
|
|
/// und Plex kennt den Kontonamen am Server gar nicht. Ein Buchstabe ist
|
|
/// ehrlicher als ein graues Platzhalterbild.
|
|
private var avatar: some View {
|
|
Text(title.prefix(1).uppercased())
|
|
.font(.flixyCaption.weight(.semibold))
|
|
.foregroundStyle(Color.flixyCanvas)
|
|
.frame(width: 26, height: 26)
|
|
.background(accent, in: .circle)
|
|
}
|
|
}
|