Settings Refactor: Unified card design for server and general settings, centralized SectionHeader component, setup mode now only shows server card, automatic view switching after login/logout, consistent button styles and spacing, removed code duplication.
This commit is contained in:
parent
0f66b15b01
commit
cc08b2cc1b
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug",
|
||||||
|
"program": "${workspaceFolder}/<executable file>",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ struct Settings {
|
|||||||
|
|
||||||
var fontFamily: FontFamily? = nil
|
var fontFamily: FontFamily? = nil
|
||||||
var fontSize: FontSize? = nil
|
var fontSize: FontSize? = nil
|
||||||
|
var hasFinishedSetup: Bool = false
|
||||||
|
|
||||||
var isLoggedIn: Bool {
|
var isLoggedIn: Bool {
|
||||||
token != nil && !token!.isEmpty
|
token != nil && !token!.isEmpty
|
||||||
@ -24,10 +25,15 @@ protocol PSettingsRepository {
|
|||||||
func loadSettings() async throws -> Settings?
|
func loadSettings() async throws -> Settings?
|
||||||
func clearSettings() async throws
|
func clearSettings() async throws
|
||||||
func saveToken(_ token: String) async throws
|
func saveToken(_ token: String) async throws
|
||||||
|
func saveUsername(_ username: String) async throws
|
||||||
|
func savePassword(_ password: String) async throws
|
||||||
|
func saveHasFinishedSetup(_ hasFinishedSetup: Bool) async throws
|
||||||
|
var hasFinishedSetup: Bool { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsRepository: PSettingsRepository {
|
class SettingsRepository: PSettingsRepository {
|
||||||
private let coreDataManager = CoreDataManager.shared
|
private let coreDataManager = CoreDataManager.shared
|
||||||
|
private let userDefault = UserDefaults.standard
|
||||||
|
|
||||||
func saveSettings(_ settings: Settings) async throws {
|
func saveSettings(_ settings: Settings) async throws {
|
||||||
let context = coreDataManager.context
|
let context = coreDataManager.context
|
||||||
@ -141,11 +147,47 @@ class SettingsRepository: PSettingsRepository {
|
|||||||
if let settingEntity = settingEntities.first {
|
if let settingEntity = settingEntities.first {
|
||||||
settingEntity.token = token
|
settingEntity.token = token
|
||||||
} else {
|
} else {
|
||||||
// Fallback: Neue Einstellung erstellen (sollte normalerweise nicht passieren)
|
|
||||||
let settingEntity = SettingEntity(context: context)
|
let settingEntity = SettingEntity(context: context)
|
||||||
settingEntity.token = token
|
settingEntity.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try context.save()
|
||||||
|
|
||||||
|
// Wenn ein Token gespeichert wird, Setup als abgeschlossen markieren
|
||||||
|
if !token.isEmpty {
|
||||||
|
self.hasFinishedSetup = true
|
||||||
|
// Notification senden, dass sich der Setup-Status geändert hat
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.resume()
|
||||||
|
} catch {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveUsername(_ username: String) async throws {
|
||||||
|
let context = coreDataManager.context
|
||||||
|
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
context.perform {
|
||||||
|
do {
|
||||||
|
let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest()
|
||||||
|
fetchRequest.fetchLimit = 1
|
||||||
|
|
||||||
|
let settingEntities = try context.fetch(fetchRequest)
|
||||||
|
|
||||||
|
if let settingEntity = settingEntities.first {
|
||||||
|
settingEntity.username = username
|
||||||
|
} else {
|
||||||
|
let settingEntity = SettingEntity(context: context)
|
||||||
|
settingEntity.username = username
|
||||||
|
}
|
||||||
|
|
||||||
try context.save()
|
try context.save()
|
||||||
continuation.resume()
|
continuation.resume()
|
||||||
} catch {
|
} catch {
|
||||||
@ -154,4 +196,51 @@ class SettingsRepository: PSettingsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func savePassword(_ password: String) async throws {
|
||||||
|
let context = coreDataManager.context
|
||||||
|
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
context.perform {
|
||||||
|
do {
|
||||||
|
let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest()
|
||||||
|
fetchRequest.fetchLimit = 1
|
||||||
|
|
||||||
|
let settingEntities = try context.fetch(fetchRequest)
|
||||||
|
|
||||||
|
if let settingEntity = settingEntities.first {
|
||||||
|
settingEntity.password = password
|
||||||
|
} else {
|
||||||
|
let settingEntity = SettingEntity(context: context)
|
||||||
|
settingEntity.password = password
|
||||||
|
}
|
||||||
|
|
||||||
|
try context.save()
|
||||||
|
continuation.resume()
|
||||||
|
} catch {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveHasFinishedSetup(_ hasFinishedSetup: Bool) async throws {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
self.hasFinishedSetup = hasFinishedSetup
|
||||||
|
// Notification senden, dass sich der Setup-Status geändert hat
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||||
|
}
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasFinishedSetup: Bool {
|
||||||
|
get {
|
||||||
|
return userDefault.value(forKey: "hasFinishedSetup") as? Bool ?? false
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
userDefault.set(newValue, forKey: "hasFinishedSetup")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,12 +34,13 @@ class TokenManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearToken() async {
|
func clearToken() async throws {
|
||||||
do {
|
do {
|
||||||
try await settingsRepository.clearSettings()
|
try await settingsRepository.saveToken("")
|
||||||
cachedSettings = nil
|
cachedSettings?.token = ""
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to clear settings: \(error)")
|
print("Failed to clear token: \(error)")
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
43
readeck/Domain/UseCase/LogoutUseCase.swift
Normal file
43
readeck/Domain/UseCase/LogoutUseCase.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// LogoutUseCase.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 29.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol LogoutUseCaseProtocol {
|
||||||
|
func execute() async throws
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogoutUseCase: LogoutUseCaseProtocol {
|
||||||
|
private let settingsRepository: SettingsRepository
|
||||||
|
private let tokenManager: TokenManager
|
||||||
|
|
||||||
|
init(
|
||||||
|
settingsRepository: SettingsRepository = SettingsRepository(),
|
||||||
|
tokenManager: TokenManager = TokenManager.shared
|
||||||
|
) {
|
||||||
|
self.settingsRepository = settingsRepository
|
||||||
|
self.tokenManager = tokenManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute() async throws {
|
||||||
|
// Clear the token
|
||||||
|
try await tokenManager.clearToken()
|
||||||
|
|
||||||
|
// Reset hasFinishedSetup to false
|
||||||
|
try await settingsRepository.saveHasFinishedSetup(false)
|
||||||
|
|
||||||
|
// Clear user session data
|
||||||
|
try await settingsRepository.saveToken("")
|
||||||
|
try await settingsRepository.saveUsername("")
|
||||||
|
try await settingsRepository.savePassword("")
|
||||||
|
|
||||||
|
// Note: We keep the endpoint for potential re-login
|
||||||
|
// but clear the authentication data
|
||||||
|
|
||||||
|
print("LogoutUseCase: User logged out successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,17 @@ class SaveSettingsUseCase {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func execute(endpoint: String, username: String, password: String, hasFinishedSetup: Bool) async throws {
|
||||||
|
try await settingsRepository.saveSettings(
|
||||||
|
.init(
|
||||||
|
endpoint: endpoint,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
hasFinishedSetup: hasFinishedSetup
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func execute(token: String) async throws {
|
func execute(token: String) async throws {
|
||||||
try await settingsRepository.saveSettings(
|
try await settingsRepository.saveSettings(
|
||||||
.init(
|
.init(
|
||||||
|
|||||||
@ -5,6 +5,7 @@ struct BookmarkDetailView: View {
|
|||||||
let bookmarkId: String
|
let bookmarkId: String
|
||||||
@State private var viewModel = BookmarkDetailViewModel()
|
@State private var viewModel = BookmarkDetailViewModel()
|
||||||
@State private var webViewHeight: CGFloat = 300
|
@State private var webViewHeight: CGFloat = 300
|
||||||
|
@State private var showingFontSettings = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@ -51,6 +52,29 @@ struct BookmarkDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button(action: {
|
||||||
|
showingFontSettings = true
|
||||||
|
}) {
|
||||||
|
Image(systemName: "textformat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingFontSettings) {
|
||||||
|
NavigationView {
|
||||||
|
FontSettingsView()
|
||||||
|
.navigationTitle("Schrift-Einstellungen")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Fertig") {
|
||||||
|
showingFontSettings = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
||||||
await viewModel.loadArticleContent(id: bookmarkId)
|
await viewModel.loadArticleContent(id: bookmarkId)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ protocol UseCaseFactory {
|
|||||||
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase
|
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase
|
||||||
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
|
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
|
||||||
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase
|
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase
|
||||||
|
func makeLogoutUseCase() -> LogoutUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultUseCaseFactory: UseCaseFactory {
|
class DefaultUseCaseFactory: UseCaseFactory {
|
||||||
@ -51,6 +52,10 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
return UpdateBookmarkUseCase(repository: bookmarksRepository)
|
return UpdateBookmarkUseCase(repository: bookmarksRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeLogoutUseCase() -> LogoutUseCase {
|
||||||
|
return LogoutUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
// Nicht mehr nötig - Token wird automatisch geladen
|
// Nicht mehr nötig - Token wird automatisch geladen
|
||||||
func refreshConfiguration() async {
|
func refreshConfiguration() async {
|
||||||
// Optional: Cache löschen falls nötig
|
// Optional: Cache löschen falls nötig
|
||||||
|
|||||||
18
readeck/UI/Extension/UIDeviceExtension.swift
Normal file
18
readeck/UI/Extension/UIDeviceExtension.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// UIDeviceExtension.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 29.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIDevice {
|
||||||
|
static var isPad: Bool {
|
||||||
|
return UIDevice.current.userInterfaceIdiom == .pad
|
||||||
|
}
|
||||||
|
|
||||||
|
static var isPhone: Bool {
|
||||||
|
return UIDevice.current.userInterfaceIdiom == .phone
|
||||||
|
}
|
||||||
|
}
|
||||||
104
readeck/UI/Settings/FontSettingsView.swift
Normal file
104
readeck/UI/Settings/FontSettingsView.swift
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
//
|
||||||
|
// FontSettingsView.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 29.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FontSettingsView: View {
|
||||||
|
@State private var viewModel = FontSettingsViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
// Header
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "textformat")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
|
Text("Schrift")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Font Family Picker
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||||
|
Text("Schriftart")
|
||||||
|
.font(.headline)
|
||||||
|
Picker("Schriftart", selection: $viewModel.selectedFontFamily) {
|
||||||
|
ForEach(FontFamily.allCases, id: \.self) { family in
|
||||||
|
Text(family.displayName).tag(family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(MenuPickerStyle())
|
||||||
|
.onChange(of: viewModel.selectedFontFamily) {
|
||||||
|
Task {
|
||||||
|
await viewModel.saveFontSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
|
||||||
|
// Font Size Picker
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Schriftgröße")
|
||||||
|
.font(.headline)
|
||||||
|
Picker("Schriftgröße", selection: $viewModel.selectedFontSize) {
|
||||||
|
ForEach(FontSize.allCases, id: \.self) { size in
|
||||||
|
Text(size.displayName).tag(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(SegmentedPickerStyle())
|
||||||
|
.onChange(of: viewModel.selectedFontSize) {
|
||||||
|
Task {
|
||||||
|
await viewModel.saveFontSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(4)
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||||
|
.task {
|
||||||
|
await viewModel.loadFontSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
FontSettingsView()
|
||||||
|
}
|
||||||
147
readeck/UI/Settings/FontSettingsViewModel.swift
Normal file
147
readeck/UI/Settings/FontSettingsViewModel.swift
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
//
|
||||||
|
// FontSettingsViewModel.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 29.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
class FontSettingsViewModel {
|
||||||
|
private let saveSettingsUseCase: SaveSettingsUseCase
|
||||||
|
private let loadSettingsUseCase: LoadSettingsUseCase
|
||||||
|
|
||||||
|
// MARK: - Font Settings
|
||||||
|
var selectedFontFamily: FontFamily = .system
|
||||||
|
var selectedFontSize: FontSize = .medium
|
||||||
|
|
||||||
|
// MARK: - Messages
|
||||||
|
var errorMessage: String?
|
||||||
|
var successMessage: String?
|
||||||
|
|
||||||
|
// 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
|
||||||
|
self.saveSettingsUseCase = factory.makeSaveSettingsUseCase()
|
||||||
|
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func loadFontSettings() async {
|
||||||
|
do {
|
||||||
|
if let settings = try await loadSettingsUseCase.execute() {
|
||||||
|
selectedFontFamily = settings.fontFamily ?? .system
|
||||||
|
selectedFontSize = settings.fontSize ?? .medium
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Fehler beim Laden der Schrift-Einstellungen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func saveFontSettings() async {
|
||||||
|
do {
|
||||||
|
try await saveSettingsUseCase.execute(
|
||||||
|
selectedFontFamily: selectedFontFamily,
|
||||||
|
selectedFontSize: selectedFontSize
|
||||||
|
)
|
||||||
|
successMessage = "Schrift-Einstellungen gespeichert"
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Fehler beim Speichern der Schrift-Einstellungen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearMessages() {
|
||||||
|
errorMessage = nil
|
||||||
|
successMessage = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Font Enums (moved from SettingsViewModel)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
readeck/UI/Settings/SectionHeader.swift
Normal file
17
readeck/UI/Settings/SectionHeader.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SectionHeader: View {
|
||||||
|
let title: String
|
||||||
|
let icon: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text(title)
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
readeck/UI/Settings/SettingsContainerView.swift
Normal file
52
readeck/UI/Settings/SettingsContainerView.swift
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// SettingsContainerView.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 29.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsContainerView: View {
|
||||||
|
@State private var viewModel = SettingsViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(spacing: 20) {
|
||||||
|
// Server-Card immer anzeigen
|
||||||
|
SettingsServerView(viewModel: viewModel)
|
||||||
|
.cardStyle()
|
||||||
|
|
||||||
|
// Allgemeine Einstellungen nur im normalen Modus anzeigen
|
||||||
|
if !viewModel.isSetupMode {
|
||||||
|
SettingsGeneralView(viewModel: viewModel)
|
||||||
|
.cardStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGroupedBackground))
|
||||||
|
}
|
||||||
|
.navigationTitle("Einstellungen")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
await viewModel.loadSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card Modifier für einheitlichen Look
|
||||||
|
extension View {
|
||||||
|
func cardStyle() -> some View {
|
||||||
|
self
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||||
|
.shadow(color: Color.black.opacity(0.06), radius: 4, x: 0, y: 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SettingsContainerView()
|
||||||
|
}
|
||||||
166
readeck/UI/Settings/SettingsGeneralView.swift
Normal file
166
readeck/UI/Settings/SettingsGeneralView.swift
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
//
|
||||||
|
// SettingsGeneralView.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 29.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
// SectionHeader wird jetzt zentral importiert
|
||||||
|
|
||||||
|
struct SettingsGeneralView: View {
|
||||||
|
@State var viewModel: SettingsViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
SectionHeader(title: "Allgemeine Einstellungen", icon: "gear")
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Theme")
|
||||||
|
.font(.headline)
|
||||||
|
Picker("Theme", selection: $viewModel.selectedTheme) {
|
||||||
|
ForEach(Theme.allCases, id: \.self) { theme in
|
||||||
|
Text(theme.displayName).tag(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font Settings
|
||||||
|
FontSettingsView()
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
|
||||||
|
// Sync Settings
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Sync-Einstellungen")
|
||||||
|
.font(.headline)
|
||||||
|
Toggle("Automatischer Sync", isOn: $viewModel.autoSyncEnabled)
|
||||||
|
.toggleStyle(SwitchToggleStyle())
|
||||||
|
if viewModel.autoSyncEnabled {
|
||||||
|
HStack {
|
||||||
|
Text("Sync-Intervall")
|
||||||
|
Spacer()
|
||||||
|
Stepper("\(viewModel.syncInterval) Minuten", value: $viewModel.syncInterval, in: 1...60)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading Settings
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Leseeinstellungen")
|
||||||
|
.font(.headline)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data Management
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Datenmanagement")
|
||||||
|
.font(.headline)
|
||||||
|
Button(role: .destructive) {
|
||||||
|
Task {
|
||||||
|
// await viewModel.clearCache()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text("Cache leeren")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(role: .destructive) {
|
||||||
|
Task {
|
||||||
|
// await viewModel.resetSettings()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "arrow.clockwise")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text("Einstellungen zurücksetzen")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Info
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Über die App")
|
||||||
|
.font(.headline)
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "info.circle")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Version \(viewModel.appVersion)")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "person.crop.circle")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Entwickler: \(viewModel.developerName)")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "globe")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Link("Website", destination: URL(string: "https://example.com")!)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Button
|
||||||
|
Button(action: {
|
||||||
|
Task {
|
||||||
|
await viewModel.saveSettings()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
if viewModel.isSaving {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
}
|
||||||
|
Text(viewModel.isSaving ? "Speichere..." : "Einstellungen speichern")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.background(viewModel.canSave ? Color.accentColor : Color.gray)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.disabled(!viewModel.canSave || viewModel.isSaving)
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
if let successMessage = viewModel.successMessage {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text(successMessage)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let errorMessage = viewModel.errorMessage {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text(errorMessage)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SettingsGeneralView(viewModel: SettingsViewModel())
|
||||||
|
}
|
||||||
225
readeck/UI/Settings/SettingsServerView.swift
Normal file
225
readeck/UI/Settings/SettingsServerView.swift
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
//
|
||||||
|
// SettingsServerView.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 29.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
// SectionHeader wird jetzt zentral importiert
|
||||||
|
|
||||||
|
struct SettingsServerView: View {
|
||||||
|
@State var viewModel = SettingsViewModel()
|
||||||
|
@State private var isTesting: Bool = false
|
||||||
|
@State private var connectionTestSuccess: Bool = false
|
||||||
|
@State private var showingLogoutAlert = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
SectionHeader(title: viewModel.isSetupMode ? "Server-Einstellungen" : "Server-Verbindung", icon: "server.rack")
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
|
Text(viewModel.isSetupMode ?
|
||||||
|
"Geben Sie Ihre Readeck-Server-Details ein, um zu beginnen." :
|
||||||
|
"Ihre aktuelle Server-Verbindung und Anmeldedaten.")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
|
||||||
|
// Form
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("Server-Endpunkt")
|
||||||
|
.font(.headline)
|
||||||
|
TextField("https://readeck.example.com", text: $viewModel.endpoint)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.keyboardType(.URL)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(!viewModel.isSetupMode)
|
||||||
|
.onChange(of: viewModel.endpoint) {
|
||||||
|
if viewModel.isSetupMode {
|
||||||
|
viewModel.clearMessages()
|
||||||
|
connectionTestSuccess = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("Benutzername")
|
||||||
|
.font(.headline)
|
||||||
|
TextField("Ihr Benutzername", text: $viewModel.username)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(!viewModel.isSetupMode)
|
||||||
|
.onChange(of: viewModel.username) {
|
||||||
|
if viewModel.isSetupMode {
|
||||||
|
viewModel.clearMessages()
|
||||||
|
connectionTestSuccess = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("Passwort")
|
||||||
|
.font(.headline)
|
||||||
|
SecureField("Ihr Passwort", text: $viewModel.password)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.disabled(!viewModel.isSetupMode)
|
||||||
|
.onChange(of: viewModel.password) {
|
||||||
|
if viewModel.isSetupMode {
|
||||||
|
viewModel.clearMessages()
|
||||||
|
connectionTestSuccess = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection Status
|
||||||
|
if viewModel.isLoggedIn {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text("Erfolgreich angemeldet")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
if let errorMessage = viewModel.errorMessage {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text(errorMessage)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let successMessage = viewModel.successMessage {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text(successMessage)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
if viewModel.isSetupMode {
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Button(action: {
|
||||||
|
Task {
|
||||||
|
await testConnection()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
if isTesting {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
}
|
||||||
|
Text(isTesting ? "Teste Verbindung..." : "Verbindung testen")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.background(viewModel.canLogin ? Color.accentColor : Color.gray)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.disabled(!viewModel.canLogin || isTesting || viewModel.isLoading)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
Task {
|
||||||
|
await viewModel.login()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
if viewModel.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
}
|
||||||
|
Text(viewModel.isLoading ? "Anmelde..." : (viewModel.isLoggedIn ? "Erneut anmelden" : "Anmelden"))
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.background((viewModel.canLogin && connectionTestSuccess) ? Color.blue : Color.gray)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.disabled(!viewModel.canLogin || !connectionTestSuccess || viewModel.isLoading || isTesting)
|
||||||
|
|
||||||
|
Button("Debug-Anmeldung") {
|
||||||
|
viewModel.username = "admin"
|
||||||
|
viewModel.password = "Diggah123"
|
||||||
|
viewModel.endpoint = "https://readeck.mnk.any64.de"
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(action: {
|
||||||
|
showingLogoutAlert = true
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "rectangle.portrait.and.arrow.right")
|
||||||
|
Text("Abmelden")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.background(Color.red)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Abmelden", isPresented: $showingLogoutAlert) {
|
||||||
|
Button("Abbrechen", role: .cancel) { }
|
||||||
|
Button("Abmelden", role: .destructive) {
|
||||||
|
Task {
|
||||||
|
await viewModel.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("Möchten Sie sich wirklich abmelden? Dies wird alle Ihre Anmeldedaten löschen und Sie zur Einrichtung zurückführen.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func testConnection() async {
|
||||||
|
guard viewModel.canLogin else {
|
||||||
|
viewModel.errorMessage = "Bitte füllen Sie alle Felder aus."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isTesting = true
|
||||||
|
viewModel.clearMessages()
|
||||||
|
connectionTestSuccess = false
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Test login without saving settings
|
||||||
|
let _ = try await viewModel.loginUseCase.execute(
|
||||||
|
username: viewModel.username.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
password: viewModel.password
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we get here, the test was successful
|
||||||
|
connectionTestSuccess = true
|
||||||
|
viewModel.successMessage = "Verbindung erfolgreich getestet! ✓"
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
connectionTestSuccess = false
|
||||||
|
viewModel.errorMessage = "Verbindungstest fehlgeschlagen: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
|
||||||
|
isTesting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SettingsServerView(viewModel: SettingsViewModel())
|
||||||
|
}
|
||||||
@ -1,232 +1,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@State private var viewModel = SettingsViewModel()
|
|
||||||
|
|
||||||
@State var selectedTheme: Theme = .system
|
|
||||||
@State var selectedFontSize: FontSize = .medium
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
SettingsContainerView()
|
||||||
Form {
|
|
||||||
Section("Server-Einstellungen") {
|
|
||||||
TextField("Endpoint URL", text: $viewModel.endpoint)
|
|
||||||
.textContentType(.URL)
|
|
||||||
.keyboardType(.URL)
|
|
||||||
.autocapitalization(.none)
|
|
||||||
|
|
||||||
TextField("Benutzername", text: $viewModel.username)
|
|
||||||
.textContentType(.username)
|
|
||||||
.autocapitalization(.none)
|
|
||||||
|
|
||||||
SecureField("Passwort", text: $viewModel.password)
|
|
||||||
.textContentType(.password)
|
|
||||||
|
|
||||||
Button {
|
|
||||||
Task {
|
|
||||||
await viewModel.login()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
if viewModel.isLoading {
|
|
||||||
ProgressView()
|
|
||||||
.scaleEffect(0.8)
|
|
||||||
}
|
|
||||||
Text(viewModel.isLoggedIn ? "Erneut anmelden" : "Anmelden")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled(!viewModel.canLogin || viewModel.isLoading)
|
|
||||||
|
|
||||||
Button("Debug-Anmeldung") {
|
|
||||||
viewModel.username = "admin"
|
|
||||||
viewModel.password = "Diggah123"
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.isLoggedIn {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundColor(.green)
|
|
||||||
Text("Erfolgreich angemeldet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
.onChange(of: viewModel.selectedFontFamily) {
|
|
||||||
Task {
|
|
||||||
await viewModel.saveFontSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Picker("Schriftgröße", selection: $viewModel.selectedFontSize) {
|
|
||||||
ForEach(FontSize.allCases, id: \.self) { size in
|
|
||||||
Text(size.displayName).tag(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(SegmentedPickerStyle())
|
|
||||||
.onChange(of: viewModel.selectedFontSize) {
|
|
||||||
Task {
|
|
||||||
await viewModel.saveFontSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundColor(.green)
|
|
||||||
Text(successMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("Einstellungen")
|
|
||||||
.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
|
||||||
Button("OK", role: .cancel) {
|
|
||||||
viewModel.errorMessage = nil
|
|
||||||
}
|
|
||||||
} message: {
|
|
||||||
Text(viewModel.errorMessage ?? "")
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
await viewModel.loadSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import SwiftUI
|
|||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
class SettingsViewModel {
|
class SettingsViewModel {
|
||||||
private let loginUseCase: LoginUseCase
|
private let _loginUseCase: LoginUseCase
|
||||||
private let saveSettingsUseCase: SaveSettingsUseCase
|
private let saveSettingsUseCase: SaveSettingsUseCase
|
||||||
private let loadSettingsUseCase: LoadSettingsUseCase
|
private let loadSettingsUseCase: LoadSettingsUseCase
|
||||||
|
private let logoutUseCase: LogoutUseCase
|
||||||
|
private let settingsRepository: SettingsRepository
|
||||||
|
|
||||||
// MARK: - Server Settings
|
// MARK: - Server Settings
|
||||||
var endpoint = ""
|
var endpoint = ""
|
||||||
@ -37,56 +39,17 @@ class SettingsViewModel {
|
|||||||
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()
|
||||||
self.saveSettingsUseCase = factory.makeSaveSettingsUseCase()
|
self.saveSettingsUseCase = factory.makeSaveSettingsUseCase()
|
||||||
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
||||||
|
self.logoutUseCase = factory.makeLogoutUseCase()
|
||||||
|
self.settingsRepository = SettingsRepository()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSetupMode: Bool {
|
||||||
|
!settingsRepository.hasFinishedSetup
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -103,17 +66,6 @@ class SettingsViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func saveFontSettings() async {
|
|
||||||
do {
|
|
||||||
try await saveSettingsUseCase.execute(
|
|
||||||
selectedFontFamily: selectedFontFamily, selectedFontSize: selectedFontSize
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
errorMessage = "Fehler beim Speichern der Font-Einstellungen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func saveSettings() async {
|
func saveSettings() async {
|
||||||
isSaving = true
|
isSaving = true
|
||||||
@ -145,11 +97,17 @@ class SettingsViewModel {
|
|||||||
successMessage = nil
|
successMessage = nil
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let user = try await loginUseCase.execute(username: username, password: password)
|
let user = try await _loginUseCase.execute(username: username, password: password)
|
||||||
|
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
successMessage = "Erfolgreich angemeldet"
|
successMessage = "Erfolgreich angemeldet"
|
||||||
|
|
||||||
|
// Setup als abgeschlossen markieren
|
||||||
|
try await settingsRepository.saveHasFinishedSetup(true)
|
||||||
|
|
||||||
|
// Notification senden, dass sich der Setup-Status geändert hat
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||||
|
|
||||||
// Factory-Konfiguration aktualisieren (Token wird automatisch gespeichert)
|
// Factory-Konfiguration aktualisieren (Token wird automatisch gespeichert)
|
||||||
await DefaultUseCaseFactory.shared.refreshConfiguration()
|
await DefaultUseCaseFactory.shared.refreshConfiguration()
|
||||||
|
|
||||||
@ -164,15 +122,23 @@ class SettingsViewModel {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func logout() async {
|
func logout() async {
|
||||||
do {
|
do {
|
||||||
// Hier könntest du eine Logout-UseCase hinzufügen
|
try await logoutUseCase.execute()
|
||||||
// try await logoutUseCase.execute()
|
|
||||||
isLoggedIn = false
|
isLoggedIn = false
|
||||||
successMessage = "Abgemeldet"
|
successMessage = "Abgemeldet"
|
||||||
|
|
||||||
|
// Notification senden, dass sich der Setup-Status geändert hat
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Abmelden"
|
errorMessage = "Fehler beim Abmelden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearMessages() {
|
||||||
|
errorMessage = nil
|
||||||
|
successMessage = nil
|
||||||
|
}
|
||||||
|
|
||||||
var canSave: Bool {
|
var canSave: Bool {
|
||||||
!endpoint.isEmpty && !username.isEmpty && !password.isEmpty
|
!endpoint.isEmpty && !username.isEmpty && !password.isEmpty
|
||||||
}
|
}
|
||||||
@ -180,6 +146,11 @@ class SettingsViewModel {
|
|||||||
var canLogin: Bool {
|
var canLogin: Bool {
|
||||||
!username.isEmpty && !password.isEmpty
|
!username.isEmpty && !password.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose loginUseCase for testing purposes
|
||||||
|
var loginUseCase: LoginUseCase {
|
||||||
|
return _loginUseCase
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -196,49 +167,3 @@ enum Theme: String, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -142,15 +142,3 @@ struct NavigationSplitViewContainer: View {
|
|||||||
#Preview {
|
#Preview {
|
||||||
MainTabView()
|
MainTabView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
extension UIDevice {
|
|
||||||
static var isPad: Bool {
|
|
||||||
return UIDevice.current.userInterfaceIdiom == .pad
|
|
||||||
}
|
|
||||||
|
|
||||||
static var isPhone: Bool {
|
|
||||||
return UIDevice.current.userInterfaceIdiom == .phone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -11,21 +11,37 @@ import netfox
|
|||||||
@main
|
@main
|
||||||
struct readeckApp: App {
|
struct readeckApp: App {
|
||||||
let persistenceController = PersistenceController.shared
|
let persistenceController = PersistenceController.shared
|
||||||
|
@State private var hasFinishedSetup = false
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MainTabView()
|
Group {
|
||||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
if hasFinishedSetup {
|
||||||
.onOpenURL { url in
|
MainTabView()
|
||||||
handleIncomingURL(url)
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
}
|
} else {
|
||||||
.onAppear {
|
SettingsContainerView()
|
||||||
#if DEBUG
|
|
||||||
NFX.sharedInstance().start()
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.onOpenURL { url in
|
||||||
|
handleIncomingURL(url)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
#if DEBUG
|
||||||
|
NFX.sharedInstance().start()
|
||||||
|
#endif
|
||||||
|
loadSetupStatus()
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("SetupStatusChanged"))) { _ in
|
||||||
|
loadSetupStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadSetupStatus() {
|
||||||
|
let settingsRepository = SettingsRepository()
|
||||||
|
hasFinishedSetup = settingsRepository.hasFinishedSetup
|
||||||
|
}
|
||||||
|
|
||||||
private func handleIncomingURL(_ url: URL) {
|
private func handleIncomingURL(_ url: URL) {
|
||||||
guard url.scheme == "readeck",
|
guard url.scheme == "readeck",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user