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 nach oben aufklappt**, kein Systemmenü. Der Unterschied /// ist nicht nur Geschmack: ein Menü legt sich als eigene Ebene über alles und /// springt dorthin, wo Platz ist. Die Karte bleibt, wo sie ist, wächst nach /// oben und lässt die Zeile mit dem Namen die ganze Zeit stehen — man sieht /// also weiter, um wessen Konto es geht. /// /// 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 { VStack(spacing: 0) { if isExpanded { action("account.settings", symbol: "gearshape") { collapse() selection = .settings } separator action( "action.signOut", symbol: "rectangle.portrait.and.arrow.right", isDestructive: true ) { collapse() showsSignOutConfirmation = true } separator } accountRow } .glassEffect(.regular, in: .rect(cornerRadius: FlixyRadius.sheet)) .animation(.snappy(duration: 0.25), value: isExpanded) .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 { withAnimation(.snappy(duration: 0.25)) { 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, wenn zugeklappt: dorthin wächst die Karte. Image(systemName: isExpanded ? "chevron.down" : "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) } private func collapse() { withAnimation(.snappy(duration: 0.25)) { isExpanded = false } } }