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
This commit is contained in:
parent
42f6c0dc92
commit
2bc93abe24
@ -3,6 +3,9 @@ import SwiftUI
|
|||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@State private var viewModel = SettingsViewModel()
|
@State private var viewModel = SettingsViewModel()
|
||||||
|
|
||||||
|
@State var selectedTheme: Theme = .system
|
||||||
|
@State var selectedFontSize: FontSize = .medium
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
Form {
|
Form {
|
||||||
@ -18,26 +21,7 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
SecureField("Passwort", text: $viewModel.password)
|
SecureField("Passwort", text: $viewModel.password)
|
||||||
.textContentType(.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 {
|
Task {
|
||||||
await viewModel.login()
|
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
|
// Success/Error Messages
|
||||||
if let successMessage = viewModel.successMessage {
|
if let successMessage = viewModel.successMessage {
|
||||||
Section {
|
Section {
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
class SettingsViewModel {
|
class SettingsViewModel {
|
||||||
@ -6,15 +8,80 @@ class SettingsViewModel {
|
|||||||
private let saveSettingsUseCase: SaveSettingsUseCase
|
private let saveSettingsUseCase: SaveSettingsUseCase
|
||||||
private let loadSettingsUseCase: LoadSettingsUseCase
|
private let loadSettingsUseCase: LoadSettingsUseCase
|
||||||
|
|
||||||
|
// MARK: - Server Settings
|
||||||
var endpoint = ""
|
var endpoint = ""
|
||||||
var username = ""
|
var username = ""
|
||||||
var password = ""
|
var password = ""
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
var isSaving = false
|
var isSaving = false
|
||||||
var isLoggedIn = false
|
var isLoggedIn = false
|
||||||
|
|
||||||
|
// 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 errorMessage: String?
|
||||||
var successMessage: 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() {
|
init() {
|
||||||
let factory = DefaultUseCaseFactory.shared
|
let factory = DefaultUseCaseFactory.shared
|
||||||
self.loginUseCase = factory.makeLoginUseCase()
|
self.loginUseCase = factory.makeLoginUseCase()
|
||||||
@ -102,3 +169,64 @@ class SettingsViewModel {
|
|||||||
!username.isEmpty && !password.isEmpty
|
!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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user