Konto unten in der Seitenleiste — und eine Einstellung, die es nicht gab

Zwei Dinge aus deinen Einstellungs-Bildern.

**Das Konto sitzt jetzt unten in der Seitenleiste**, nicht als gleichrangiger
Eintrag zwischen den Mediatheken. Dort sucht man es, und von dort führt der
Weg in die Einstellungen und zum Abmelden. Auch das ist eine Systemstelle —
`tabViewSidebarBottomBar` —, also dieselbe Zeile, an der sie bei JellyTV
steht. Auf dem iPhone gibt es sie nicht; dort bleibt es beim eigenen Eintrag.

Der Buchstabe auf der Scheibe ist kein Platzhalter für ein Bild, das noch
kommt: Jellyfin gibt kein Profilbild heraus, solange keines hinterlegt ist,
und Plex kennt den Kontonamen am Server gar nicht. Bei Plex steht deshalb der
Servername oben und darunter nichts — statt zweimal dasselbe.

**Ton- und Untertitelsprache lassen sich jetzt einstellen.** Beim Durchsehen
deiner Bilder ist mir aufgefallen, dass es diese Vorliebe längst gibt: der
Player liest sie bei jeder Wiedergabe aus und wählt die passende Spur. Nur
äußern konnte man sie nirgends — sie stand in den Voreinstellungen und blieb
dort für immer leer. Eine Vorliebe, die niemand äußern kann, ist keine.

Fünfzehn Sprachen, keine vollständige Liste: ein Rad mit zweihundert
Einträgen, um „Deutsch" zu finden, wäre keine Hilfe. Was fehlt, kommt später
mit einem Suchfeld dazu.

Gebaut und übersetzt, aber **nicht angesehen**: der Simulator-Tunnel ist
mitten im Prüfen abgestürzt und ließ sich nicht wieder aufbauen. Dass es die
Systemstelle gibt, beweist der Compiler; wie sie aussieht, habe ich diesmal
nicht gesehen.

258 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-13 14:45:16 +02:00
co-authored by Claude Opus 5
parent 4a8500fc37
commit 3f6c6d8816
6 changed files with 433 additions and 96 deletions
+94
View File
@@ -0,0 +1,94 @@
import SwiftUI
import FlixyDesign
/// Wer angemeldet ist — unten in der Seitenleiste.
///
/// Der Platz ist nicht zufällig: dort sucht man sein Konto, weil es auf jedem
/// Schreibtisch und 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.
///
/// Auf dem iPhone gibt es diese Leiste nicht — dort gehört das Konto in die
/// Einstellungen selbst, und die haben ihren eigenen Eintrag.
struct AccountBar: View {
@Binding var selection: SidebarDestination
@Environment(FlixySession.self) private var session
@Environment(\.flixyAccent) private var accent
@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 {
Menu {
Button("account.settings", systemImage: "gearshape") {
selection = .settings
}
Divider()
Button("action.signOut", systemImage: "rectangle.portrait.and.arrow.right", role: .destructive) {
showsSignOutConfirmation = true
}
} 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)
Image(systemName: "chevron.up.chevron.down")
.font(.caption2)
.foregroundStyle(Color.flixyTextMuted)
}
.padding(.vertical, FlixySpacing.tight)
.contentShape(.rect)
}
.buttonStyle(.plain)
.accessibilityLabel(Text(title))
.confirmationDialog(
Text("settings.signOutConfirm"),
isPresented: $showsSignOutConfirmation,
titleVisibility: .visible
) {
Button("action.signOut", role: .destructive) { session.signOut() }
Button("action.cancel", role: .cancel) {}
} message: {
Text("settings.signOutExplanation")
}
}
/// 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)
}
}
+6
View File
@@ -218,6 +218,12 @@ struct RootView: View {
}
.tabViewStyle(.sidebarAdaptable)
.tabViewCustomization($customization)
// Das Konto sitzt unten in der Seitenleiste, nicht als Eintrag
// zwischen den Mediatheken. Dort sucht man es.
.tabViewSidebarBottomBar {
AccountBar(selection: $selection)
.padding(.horizontal, FlixySpacing.snug)
}
.miniPlayer(player, isVisible: playback.presentation == .mini)
}
+65
View File
@@ -0,0 +1,65 @@
import SwiftUI
import FlixyDesign
/// Welche Sprachen der Player von sich aus wählen soll.
///
/// Die Einstellung gab es schon — der Player liest sie bei jeder Wiedergabe
/// aus und wählt die passende Spur. Nur einstellen konnte man sie nirgends:
/// sie stand in den Voreinstellungen und blieb dort für immer leer. Eine
/// Vorliebe, die niemand äußern kann, ist keine.
struct LanguagesView: View {
@Environment(PlaybackSettings.self) private var settings
@Environment(\.flixyAccent) private var accent
/// Die Sprachen, die in Mediatheken tatsächlich vorkommen.
///
/// Keine vollständige Liste aller Sprachen der Welt: ein Auswahlrad mit
/// zweihundert Einträgen, um „Deutsch" zu finden, ist keine Hilfe. Was
/// fehlt, lässt sich später ergänzen — dann mit einem Suchfeld.
private static let codes = [
"de", "en", "fr", "es", "it", "nl", "pl", "pt", "ru", "sv", "da", "tr",
"ja", "ko", "zh",
]
var body: some View {
@Bindable var settings = settings
Form {
Section {
Picker("languages.audio", selection: $settings.preferredAudioLanguage) {
Text("languages.serverDefault").tag("")
ForEach(Self.codes, id: \.self) { code in
Text(name(of: code)).tag(code)
}
}
} header: {
Text("languages.audioHeader")
} footer: {
Text("languages.audioExplanation")
}
Section {
Picker("languages.subtitles", selection: $settings.preferredSubtitleLanguage) {
Text("languages.subtitlesOff").tag("")
ForEach(Self.codes, id: \.self) { code in
Text(name(of: code)).tag(code)
}
}
} header: {
Text("languages.subtitlesHeader")
} footer: {
Text("languages.subtitlesExplanation")
}
}
.tint(accent)
.scrollContentBackground(.hidden)
.background(Color.flixyCanvas)
.navigationTitle(Text("languages.title"))
.navigationBarTitleDisplayMode(.inline)
}
/// Der Name der Sprache in der Sprache des Geräts.
private func name(of code: String) -> String {
Locale.current.localizedString(forLanguageCode: code)?.capitalized ?? code.uppercased()
}
}
+6
View File
@@ -86,6 +86,12 @@ struct SettingsView: View {
Label("appearance.title", systemImage: "paintpalette")
}
NavigationLink {
LanguagesView()
} label: {
Label("languages.title", systemImage: "captions.bubble")
}
NavigationLink {
HomeLayoutView()
} label: {