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) } }