Theme-Wechsel, Reconnect-Fehler
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user