Theme-Wechsel, Reconnect-Fehler

This commit is contained in:
2026-03-23 17:15:04 +01:00
parent c4a4833bec
commit bcb6a93dd5
@@ -0,0 +1,348 @@
import SwiftUI
struct DisplayOptionsView: View {
let profile: ServerProfile
@Environment(ServerProfileStore.self) private var profileStore
@Environment(\.dismiss) private var dismiss
// Local state mirrors profile values, saved on every change
@State private var appTheme: String = "system"
@State private var accentThemeRaw: String = AccentTheme.ocean.rawValue
@State private var appTextColor: Color = .primary
@State private var hasCustomTextColor: Bool = false
@State private var appBgColor: Color = Color(.systemBackground)
@State private var hasCustomBgColor: Bool = false
@State private var editorFontSize: Double = 16
@State private var readerFontSize: Double = 16
@State private var showResetConfirm = false
@State private var showResetAppearanceConfirm = false
@AppStorage("displayOptionsInfoSeen") private var infoSeen = false
@State private var showInfoAlert = false
private var selectedTheme: AccentTheme {
AccentTheme(rawValue: accentThemeRaw) ?? .ocean
}
var body: some View {
Form {
hintSection
appearanceSection
editorSection
readerSection
resetSection
previewSection
}
.navigationTitle(L("display.title"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(L("display.reset.all"), role: .destructive) {
showResetConfirm = true
}
.foregroundStyle(.red)
}
}
.confirmationDialog(L("display.reset.all.confirm"),
isPresented: $showResetConfirm,
titleVisibility: .visible) {
Button(L("display.reset.all.button"), role: .destructive) { resetAll() }
Button(L("common.cancel"), role: .cancel) {}
}
.confirmationDialog(L("display.reset.appearance.confirm"),
isPresented: $showResetAppearanceConfirm,
titleVisibility: .visible) {
Button(L("display.reset.all.button"), role: .destructive) { resetAppearance() }
Button(L("common.cancel"), role: .cancel) {}
}
.onAppear {
loadFromProfile()
if !infoSeen {
showInfoAlert = true
infoSeen = true
}
}
.alert(L("display.info.title"), isPresented: $showInfoAlert) {
Button(L("common.ok"), role: .cancel) {}
} message: {
Text(L("display.info.message"))
}
.onChange(of: appTheme) { persist() }
.onChange(of: accentThemeRaw) { persist() }
.onChange(of: appTextColor) { persist() }
.onChange(of: hasCustomTextColor) { persist() }
.onChange(of: appBgColor) { persist() }
.onChange(of: hasCustomBgColor) { persist() }
.onChange(of: editorFontSize) { persist() }
.onChange(of: readerFontSize) { persist() }
}
// MARK: - Sections
private var hintSection: some View {
Section {
HStack(alignment: .top, spacing: 10) {
Image(systemName: "info.circle.fill")
.foregroundStyle(.blue)
.padding(.top, 2)
Text(String(format: L("display.hint"), profile.name))
.font(.footnote)
.foregroundStyle(.secondary)
}
.listRowBackground(Color(.secondarySystemBackground))
}
}
private var appearanceSection: some View {
Section(L("settings.appearance")) {
// Light / Dark / System picker
Picker(L("settings.appearance.theme"), selection: $appTheme) {
Text(L("settings.appearance.theme.system")).tag("system")
Text(L("settings.appearance.theme.light")).tag("light")
Text(L("settings.appearance.theme.dark")).tag("dark")
}
.pickerStyle(.segmented)
// Accent colour swatches
VStack(alignment: .leading, spacing: 10) {
Text(L("settings.appearance.accent"))
.font(.subheadline)
.foregroundStyle(.secondary)
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: 8), spacing: 10) {
ForEach(AccentTheme.allCases) { theme in
Button {
accentThemeRaw = theme.rawValue
} label: {
ZStack {
Circle()
.fill(theme.shelfColor)
.frame(width: 32, height: 32)
if selectedTheme == theme {
Circle()
.strokeBorder(.white, lineWidth: 2)
.frame(width: 32, height: 32)
Image(systemName: "checkmark")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(.white)
}
}
}
.buttonStyle(.plain)
.accessibilityLabel(theme.displayName)
.accessibilityAddTraits(selectedTheme == theme ? .isSelected : [])
}
}
}
.padding(.vertical, 4)
// App text colour
colorRow(
label: L("display.app.textcolor"),
color: $appTextColor,
hasCustom: $hasCustomTextColor,
resetColor: .primary
)
// App background colour
colorRow(
label: L("display.app.bgcolor"),
color: $appBgColor,
hasCustom: $hasCustomBgColor,
resetColor: Color(.systemBackground)
)
}
}
private var editorSection: some View {
Section(L("display.editor")) {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(L("display.editor.fontsize"))
Spacer()
Text(String(format: L("display.fontsize.pt"), editorFontSize))
.foregroundStyle(.secondary)
.monospacedDigit()
}
Slider(value: $editorFontSize, in: 10...28, step: 1)
.tint(.accentColor)
}
.padding(.vertical, 4)
}
}
private var readerSection: some View {
Section(L("display.reader")) {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(L("display.reader.fontsize"))
Spacer()
Text(String(format: L("display.fontsize.pt"), readerFontSize))
.foregroundStyle(.secondary)
.monospacedDigit()
}
Slider(value: $readerFontSize, in: 10...28, step: 1)
.tint(.accentColor)
}
.padding(.vertical, 4)
}
}
private func colorRow(label: String, color: Binding<Color>, hasCustom: Binding<Bool>, resetColor: Color) -> some View {
HStack {
if hasCustom.wrappedValue {
ColorPicker(label, selection: color, supportsOpacity: false)
Button {
hasCustom.wrappedValue = false
color.wrappedValue = resetColor
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
} else {
Text(label)
Spacer()
Text(L("display.color.system"))
.foregroundStyle(.secondary)
.font(.footnote)
Button {
hasCustom.wrappedValue = true
} label: {
Image(systemName: "plus.circle.fill")
.foregroundStyle(Color.accentColor)
}
.buttonStyle(.plain)
}
}
}
private var resetSection: some View {
Section {
Button(role: .destructive) {
showResetAppearanceConfirm = true
} label: {
HStack {
Spacer()
Text(L("display.reset.appearance"))
Spacer()
}
}
}
}
private var previewSection: some View {
Section(L("display.preview")) {
previewCard
.listRowInsets(.init(top: 12, leading: 12, bottom: 12, trailing: 12))
}
}
// MARK: - Preview Card
private var previewCard: some View {
let bgColor: Color = hasCustomBgColor ? appBgColor : Color(.systemBackground)
let textColor: Color = hasCustomTextColor ? appTextColor : Color(.label)
let fontSize = CGFloat(readerFontSize)
let editorSize = CGFloat(editorFontSize)
return VStack(alignment: .leading, spacing: 10) {
// App / Reader preview
VStack(alignment: .leading, spacing: 6) {
Label(L("display.preview.reader"), systemImage: "doc.text")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.padding(.bottom, 2)
Text("Sample Heading")
.font(.system(size: fontSize + 4, weight: .bold))
.foregroundStyle(textColor)
Text("This is a preview of how your pages will look in the reader.")
.font(.system(size: fontSize))
.foregroundStyle(textColor)
.lineSpacing(fontSize * 0.3)
Text("let x = 42")
.font(.system(size: fontSize - 2, design: .monospaced))
.foregroundStyle(textColor.opacity(0.85))
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(textColor.opacity(0.08), in: RoundedRectangle(cornerRadius: 4))
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(bgColor, in: RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(Color(.separator), lineWidth: 0.5))
// Editor preview
VStack(alignment: .leading, spacing: 6) {
Label(L("display.preview.editor"), systemImage: "pencil")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.padding(.bottom, 2)
Text("## My Page\n\nStart writing your **Markdown** here…")
.font(.system(size: editorSize, design: .monospaced))
.foregroundStyle(Color(.label))
.lineSpacing(editorSize * 0.2)
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.systemBackground), in: RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(Color(.separator), lineWidth: 0.5))
}
}
// MARK: - Helpers
private func loadFromProfile() {
let p = profileStore.profiles.first { $0.id == profile.id } ?? profile
appTheme = p.appTheme
accentThemeRaw = p.accentTheme
editorFontSize = p.editorFontSize
readerFontSize = p.readerFontSize
if let hex = p.appTextColor, let c = Color(hex: hex) {
appTextColor = c
hasCustomTextColor = true
}
if let hex = p.appBackgroundColor, let c = Color(hex: hex) {
appBgColor = c
hasCustomBgColor = true
}
}
private func persist() {
let textHex = hasCustomTextColor ? appTextColor.toHexString() : nil
let bgHex = hasCustomBgColor ? appBgColor.toHexString() : nil
profileStore.updateDisplayOptions(
for: profile,
editorFontSize: editorFontSize,
readerFontSize: readerFontSize,
appTextColor: textHex,
appBackgroundColor: bgHex,
appTheme: appTheme,
accentTheme: accentThemeRaw
)
}
private func resetAppearance() {
appTheme = "system"
accentThemeRaw = AccentTheme.ocean.rawValue
hasCustomTextColor = false
hasCustomBgColor = false
appTextColor = .primary
appBgColor = Color(.systemBackground)
persist()
}
private func resetAll() {
appTheme = "system"
accentThemeRaw = AccentTheme.ocean.rawValue
hasCustomTextColor = false
hasCustomBgColor = false
appTextColor = .primary
appBgColor = Color(.systemBackground)
editorFontSize = 16
readerFontSize = 16
persist()
}
}