Drei Gründe, warum der Mac anders aussah, und keiner davon war Geschmack. **Die Schrift.** macOS rendert dieselben Textstile deutlich kleiner als iOS: `largeTitle` misst dort 26 Punkt statt 34, Fließtext 13 statt 17. Eine Oberfläche, die auf dem iPad stimmt, wirkt damit zusammengeschrumpft. Auf dem Mac stehen jetzt feste Größen — die von iOS. Das kostet dort Dynamic Type; vertretbar, weil macOS keine systemweite Textgröße kennt und man ein Fenster stattdessen größer ziehen kann. **Der Aufmacher wächst mit.** Das war der zweite, größere Teil: das Bild ist so breit wie das Fenster, die Schrift darauf war es nicht. Bei 1280 Punkt Breite wirkt ein 34-Punkt-Titel verloren, bei 834 auf dem iPad genau richtig. Titel, Schriftzug, Angaben und Abstände richten sich jetzt nach der Breite des Aufmachers — mit dem iPad als Bezug und einem Deckel, damit ein Fenster über die ganze Wand nicht in Plakatschrift endet. Die Beschreibung bekommt außerdem eine Zeilenbreite: ein Satz über 1280 Punkt ist unlesbar, das Auge verliert die Zeile. **Die Seitenleiste.** Sie richtet sich nach ihrem Inhalt, und bei Wörtern wie „Start" fällt sie auf eine Breite zusammen, in der schon der Kontoname darunter nicht mehr Platz hatte — daher das „S…". Die Beschriftungen haben jetzt eine Mindestbreite. **Die Formulare.** Auf dem Mac nahm SwiftUI den Spalten-Stil: Beschriftungen rechts ausgerichtet, keine abgesetzten Abschnitte. Jetzt derselbe gruppierte Stil wie auf iOS. Dazu: Scrollbalken sind weg. Der graue Streifen unter dem Aufmacher kam daher, dass in den Systemeinstellungen „Scrollbalken immer einblenden" steht — dagegen reicht `.hidden` nicht, `.never` schon. Und das Fenster öffnet mit 1280 × 820 statt in Notizzettelgröße, ohne graue Titelleiste. 263 Tests grün, iOS baut unverändert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
5.6 KiB
Swift
152 lines
5.6 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)
|
|
}
|
|
}
|
|
// Ohne Vorrang staucht SwiftUI den Text vor dem Zwischenraum:
|
|
// in einer schmalen Leiste stand vom Namen nur noch „S…" da.
|
|
.layoutPriority(1)
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
// 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)
|
|
}
|
|
}
|