From 2bc93abe24c2e930e351d61af93a29b6751ca1fe Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Sat, 14 Jun 2025 23:34:44 +0200 Subject: [PATCH] Add Settings view with server config and font customization - Add SettingsView with server login, theme selection, and font preview - Implement SettingsViewModel with @Observable for state management - Add font family and size selection with live preview - Include sync settings, reading preferences, and data management options --- readeck/UI/Settings/SettingsView.swift | 169 +++++++++++++++++--- readeck/UI/Settings/SettingsViewModel.swift | 132 ++++++++++++++- 2 files changed, 278 insertions(+), 23 deletions(-) diff --git a/readeck/UI/Settings/SettingsView.swift b/readeck/UI/Settings/SettingsView.swift index 28e4187..726d135 100644 --- a/readeck/UI/Settings/SettingsView.swift +++ b/readeck/UI/Settings/SettingsView.swift @@ -3,6 +3,9 @@ import SwiftUI struct SettingsView: View { @State private var viewModel = SettingsViewModel() + @State var selectedTheme: Theme = .system + @State var selectedFontSize: FontSize = .medium + var body: some View { NavigationView { Form { @@ -18,27 +21,8 @@ struct SettingsView: View { SecureField("Passwort", text: $viewModel.password) .textContentType(.password) - } - - Section { - Button { - Task { - await viewModel.saveSettings() - } - } label: { - HStack { - if viewModel.isSaving { - ProgressView() - .scaleEffect(0.8) - } - Text("Einstellungen speichern") - } - } - .disabled(!viewModel.canSave || viewModel.isSaving) - } - - Section("Anmeldung") { - Button { + + Button { Task { await viewModel.login() } @@ -62,6 +46,149 @@ struct SettingsView: View { } } + Section { + Button { + Task { + await viewModel.saveSettings() + } + } label: { + HStack { + if viewModel.isSaving { + ProgressView() + .scaleEffect(0.8) + } + Text("Einstellungen speichern") + } + } + .disabled(!viewModel.canSave || viewModel.isSaving) + } + + + Section("Erscheinungsbild") { + Picker("Theme", selection: $viewModel.selectedTheme) { + ForEach(Theme.allCases, id: \.self) { theme in + Text(theme.displayName).tag(theme) + } + } + + // Font Settings with Preview + VStack(alignment: .leading, spacing: 12) { + Text("Schrift-Einstellungen") + .font(.headline) + + HStack { + VStack(alignment: .leading, spacing: 8) { + Picker("Schriftart", selection: $viewModel.selectedFontFamily) { + ForEach(FontFamily.allCases, id: \.self) { family in + Text(family.displayName).tag(family) + } + } + .pickerStyle(MenuPickerStyle()) + + Picker("Schriftgröße", selection: $viewModel.selectedFontSize) { + ForEach(FontSize.allCases, id: \.self) { size in + Text(size.displayName).tag(size) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + + Spacer() + } + + // Font Preview + VStack(alignment: .leading, spacing: 8) { + Text("Vorschau") + .font(.caption) + .foregroundColor(.secondary) + + VStack(alignment: .leading, spacing: 6) { + Text("readeck Bookmark Title") + .font(viewModel.previewTitleFont) + .fontWeight(.semibold) + .lineLimit(1) + + Text("This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog.") + .font(viewModel.previewBodyFont) + .lineLimit(3) + + Text("12 min • Today • example.com") + .font(viewModel.previewCaptionFont) + .foregroundColor(.secondary) + } + .padding(12) + .background(Color(.systemGray6)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + .padding(.vertical, 8) + } + + Section("Sync-Einstellungen") { + Toggle("Automatischer Sync", isOn: $viewModel.autoSyncEnabled) + .toggleStyle(SwitchToggleStyle()) + + if viewModel.autoSyncEnabled { + Stepper("Sync-Intervall: \(viewModel.syncInterval) Minuten", value: $viewModel.syncInterval, in: 1...60) + .padding(.vertical, 8) + } + } + + Section("Leseeinstellungen") { + Toggle("Safari Reader Modus", isOn: $viewModel.enableReaderMode) + .toggleStyle(SwitchToggleStyle()) + + Toggle("Externe Links in In-App Safari öffnen", isOn: $viewModel.openExternalLinksInApp) + .toggleStyle(SwitchToggleStyle()) + + Toggle("Artikel automatisch als gelesen markieren", isOn: $viewModel.autoMarkAsRead) + .toggleStyle(SwitchToggleStyle()) + } + + Section("Datenmanagement") { + Button(role: .destructive) { + Task { + // await viewModel.clearCache() + } + } label: { + HStack { + Image(systemName: "trash") + .foregroundColor(.red) + Text("Cache leeren") + } + } + + Button(role: .destructive) { + Task { + // await viewModel.resetSettings() + } + } label: { + HStack { + Image(systemName: "arrow.clockwise") + .foregroundColor(.red) + Text("Einstellungen zurücksetzen") + } + } + } + + Section("Über die App") { + HStack { + Image(systemName: "info.circle") + Text("Version \(viewModel.appVersion)") + } + + HStack { + Image(systemName: "person.crop.circle") + Text("Entwickler: \(viewModel.developerName)") + } + + HStack { + Image(systemName: "globe") + Link("Website", destination: URL(string: "https://example.com")!) + } + } + + // Success/Error Messages if let successMessage = viewModel.successMessage { Section { diff --git a/readeck/UI/Settings/SettingsViewModel.swift b/readeck/UI/Settings/SettingsViewModel.swift index 887ff22..698c4d0 100644 --- a/readeck/UI/Settings/SettingsViewModel.swift +++ b/readeck/UI/Settings/SettingsViewModel.swift @@ -1,4 +1,6 @@ import Foundation +import Observation +import SwiftUI @Observable class SettingsViewModel { @@ -6,14 +8,79 @@ class SettingsViewModel { private let saveSettingsUseCase: SaveSettingsUseCase private let loadSettingsUseCase: LoadSettingsUseCase + // MARK: - Server Settings var endpoint = "" var username = "" var password = "" var isLoading = false var isSaving = false var isLoggedIn = false - var errorMessage: String? - var successMessage: String? + + // MARK: - UI Settings + var selectedTheme: Theme = .system + + // MARK: - Sync Settings + var autoSyncEnabled: Bool = true + var syncInterval: Int = 15 + + // MARK: - Reading Settings + var enableReaderMode: Bool = false + var openExternalLinksInApp: Bool = true + var autoMarkAsRead: Bool = false + + // MARK: - App Info + var appVersion: String = "1.0.0" + var developerName: String = "Your Name" + + + // MARK: - Messages + var errorMessage: String? + var successMessage: String? + + // MARK: - Font Settings + var selectedFontFamily: FontFamily = .system + var selectedFontSize: FontSize = .medium + + // MARK: - Computed Font Properties for Preview + var previewTitleFont: Font { + switch selectedFontFamily { + case .system: + return selectedFontSize.systemFont.weight(.semibold) + case .serif: + return Font.custom("Times New Roman", size: selectedFontSize.size).weight(.semibold) + case .sansSerif: + return Font.custom("Helvetica Neue", size: selectedFontSize.size).weight(.semibold) + case .monospace: + return Font.custom("Menlo", size: selectedFontSize.size).weight(.semibold) + } + } + + var previewBodyFont: Font { + switch selectedFontFamily { + case .system: + return selectedFontSize.systemFont + case .serif: + return Font.custom("Times New Roman", size: selectedFontSize.size) + case .sansSerif: + return Font.custom("Helvetica Neue", size: selectedFontSize.size) + case .monospace: + return Font.custom("Menlo", size: selectedFontSize.size) + } + } + + var previewCaptionFont: Font { + let captionSize = selectedFontSize.size * 0.85 + switch selectedFontFamily { + case .system: + return Font.system(size: captionSize) + case .serif: + return Font.custom("Times New Roman", size: captionSize) + case .sansSerif: + return Font.custom("Helvetica Neue", size: captionSize) + case .monospace: + return Font.custom("Menlo", size: captionSize) + } + } init() { let factory = DefaultUseCaseFactory.shared @@ -102,3 +169,64 @@ class SettingsViewModel { !username.isEmpty && !password.isEmpty } } + + +enum Theme: String, CaseIterable { + case system = "system" + case light = "light" + case dark = "dark" + + var displayName: String { + switch self { + case .system: return "System" + case .light: return "Hell" + case .dark: return "Dunkel" + } + } +} + +// MARK: - Font Enums +enum FontFamily: String, CaseIterable { + case system = "system" + case serif = "serif" + case sansSerif = "sansSerif" + case monospace = "monospace" + + var displayName: String { + switch self { + case .system: return "System" + case .serif: return "Serif" + case .sansSerif: return "Sans Serif" + case .monospace: return "Monospace" + } + } +} + +enum FontSize: String, CaseIterable { + case small = "small" + case medium = "medium" + case large = "large" + case extraLarge = "extraLarge" + + var displayName: String { + switch self { + case .small: return "S" + case .medium: return "M" + case .large: return "L" + case .extraLarge: return "XL" + } + } + + var size: CGFloat { + switch self { + case .small: return 14 + case .medium: return 16 + case .large: return 18 + case .extraLarge: return 20 + } + } + + var systemFont: Font { + return Font.system(size: size) + } +}