diff --git a/Flixy/Views/Navigation/AccountAvatar.swift b/Flixy/Views/Navigation/AccountAvatar.swift new file mode 100644 index 0000000..3b12821 --- /dev/null +++ b/Flixy/Views/Navigation/AccountAvatar.swift @@ -0,0 +1,32 @@ +import SwiftUI +import FlixyDesign + +/// Der erste Buchstabe auf einer Scheibe. +/// +/// **Kein Platzhalterbild.** Jellyfin gibt kein Profilbild heraus, ohne dass +/// eines hinterlegt ist, und am Konto führt die App heute keines mit. Ein +/// Buchstabe ist ehrlicher als eine graue Silhouette, die bei jedem gleich +/// aussieht — und man erkennt an ihm wenigstens, mit welchem Konto man +/// angemeldet ist. +/// +/// Stand bis eben in `AccountBar` und wird jetzt zweimal gebraucht: dort in +/// der Seitenleiste und auf dem iPhone oben rechts, wo seit dem Wegfall des +/// Einstellungen-Tabs der Weg dorthin liegt. +struct AccountAvatar: View { + let name: String + var diameter: CGFloat = 26 + + @Environment(\.flixyAccent) private var accent + + var body: some View { + Text(name.prefix(1).uppercased()) + // Mit der Scheibe wachsen, nicht fest: oben rechts auf dem + // Telefon ist er grösser als in der Leiste, und ein Buchstabe in + // Kleinschrift auf einer grossen Scheibe sieht aus wie ein + // Fehler. + .font(.system(size: diameter * 0.46, weight: .semibold)) + .foregroundStyle(Color.flixyCanvas) + .frame(width: diameter, height: diameter) + .background(accent, in: .circle) + } +} diff --git a/Flixy/Views/Navigation/AccountBar.swift b/Flixy/Views/Navigation/AccountBar.swift index f080075..f75f04a 100644 --- a/Flixy/Views/Navigation/AccountBar.swift +++ b/Flixy/Views/Navigation/AccountBar.swift @@ -23,7 +23,6 @@ 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 @@ -79,7 +78,7 @@ struct AccountBar: View { isExpanded.toggle() } label: { HStack(spacing: FlixySpacing.snug) { - avatar + AccountAvatar(name: title) VStack(alignment: .leading, spacing: 0) { Text(title) @@ -136,16 +135,4 @@ struct AccountBar: 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) - } } diff --git a/Flixy/Views/Navigation/RootView.swift b/Flixy/Views/Navigation/RootView.swift index bd69f23..2a65963 100644 --- a/Flixy/Views/Navigation/RootView.swift +++ b/Flixy/Views/Navigation/RootView.swift @@ -74,6 +74,12 @@ struct RootView: View { /// wäre er längst null. @State private var statusBarHeight: CGFloat = 0 + /// Ob die Einstellungen gerade offen sind. + /// + /// Nur auf dem iPhone: dort gibt es den Tab nicht mehr, und der Weg führt + /// über das Monogramm oben rechts. + @State private var showsSettings = false + var body: some View { Group { switch session.phase { @@ -186,7 +192,11 @@ struct RootView: View { // Auswahl, die auf der anderen Seite nicht vorkommt, zeigte sonst // einen leeren Bereich. switch selection { - case .library, .libraryHub, .liveTV: selection = .home + // `.settings` gehört dazu, seit es den Tab auf dem iPhone nicht + // mehr gibt: wer am iPad in den Einstellungen steht und das + // Fenster schmal zieht, stünde sonst vor einem Bereich, den die + // Leiste gar nicht führt. + case .library, .libraryHub, .liveTV, .settings: selection = .home default: break } } @@ -315,12 +325,19 @@ struct RootView: View { } .customizationID("flixy.tab.downloads") - Tab(value: SidebarDestination.settings) { - mediaStack { SettingsView() } - } label: { - tabLabel("sidebar.settings", symbol: "gearshape") + // **Nicht auf dem iPhone.** Dort führt der Weg über das + // Monogramm oben rechts — siehe `profileButton`. Einstellungen + // sind nichts, was man im Wechsel mit der Mediathek benutzt; ein + // Platz in der Leiste unten unterstellt genau das. In der + // Seitenleiste bleibt der Eintrag, dort ist Platz. + if sizeClass == .regular { + Tab(value: SidebarDestination.settings) { + mediaStack { SettingsView() } + } label: { + tabLabel("sidebar.settings", symbol: "gearshape") + } + .customizationID("flixy.tab.settings") } - .customizationID("flixy.tab.settings") // Die Suche nur dort, wo Platz für sie ist. // @@ -432,12 +449,30 @@ struct RootView: View { // Insel erreichen: Inhalt kam dort nie hin. Jetzt kommt er. .ignoresSafeArea(.container, edges: .top) } else { - tabs + tabs.overlay(alignment: .topTrailing) { profileButton } } #else tabs #endif } + // **Als Blatt, nicht als Bereich.** `SettingsView` ist ein Formular + // aus geschobenen Unterbildschirmen und braucht nur einen + // Navigationsstapel um sich — die Medienziele aus `mediaStack` ruft + // es nie auf. Und beim Schliessen steht man wieder dort, wo man war, + // statt in einem Bereich, den man erst wieder verlassen muss. + .sheet(isPresented: $showsSettings) { + NavigationStack { + SettingsView() + // Ein Blatt lässt sich nach unten wegziehen — aber nicht + // jeder weiss das, und wer in einem Unterbildschirm steht, + // zieht damit gleich alles weg. Ein Knopf sagt es. + .toolbar { + ToolbarItem(placement: .flixyTrailing) { + Button("action.close") { showsSettings = false } + } + } + } + } .miniPlayer(player, isVisible: playback.presentation == .mini) .onAppear { placement = storedPlacement } .onChange(of: placement) { _, new in @@ -450,6 +485,35 @@ struct RootView: View { .onChange(of: topInset, initial: true) { _, new in topChrome.inset = new } } + /// Der Weg in die Einstellungen auf dem iPhone. + /// + /// **Ein Überzug, kein Werkzeugleisten-Knopf.** Die Startseite schaltet + /// ihre Navigationsleiste ganz ab — sie war leer und belegte trotzdem + /// Höhe —, ein `ToolbarItem` erschiene dort also gar nicht. Hier liegt er + /// einmal über allen Bildschirmen statt dreimal in deren Leisten, und er + /// sitzt überall an derselben Stelle. + /// + /// Er blendet sich beim Scrollen aus wie die Insel auf dem iPad: über + /// einem Film, den man gerade sucht, hat ein Kontoknopf nichts verloren. + @ViewBuilder + private var profileButton: some View { + Button { + showsSettings = true + } label: { + AccountAvatar(name: session.accountName ?? session.serverName, diameter: 32) + .padding(FlixySpacing.snug) + // Damit er auch über einem hellen Aufmacher steht. + .background(.black.opacity(0.28), in: .circle) + } + .buttonStyle(.plain) + .padding(.trailing, FlixySpacing.regular) + .padding(.top, statusBarHeight + FlixySpacing.snug) + .opacity(topChrome.isRevealed ? 1 : 0) + .allowsHitTesting(topChrome.isRevealed) + .animation(.snappy(duration: 0.28), value: topChrome.isRevealed) + .accessibilityLabel(Text("sidebar.settings")) + } + /// Wie viel Platz die Leiste oben braucht. private static let topBarHeight: CGFloat = 52