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 fontSize: FontSize? = nil
|
||||
var hasFinishedSetup: Bool = false
|
||||
|
||||
var isLoggedIn: Bool {
|
||||
token != nil && !token!.isEmpty
|
||||
@ -24,10 +25,15 @@ protocol PSettingsRepository {
|
||||
func loadSettings() async throws -> Settings?
|
||||
func clearSettings() 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 {
|
||||
private let coreDataManager = CoreDataManager.shared
|
||||
private let userDefault = UserDefaults.standard
|
||||
|
||||
func saveSettings(_ settings: Settings) async throws {
|
||||
let context = coreDataManager.context
|
||||
@ -141,11 +147,47 @@ class SettingsRepository: PSettingsRepository {
|
||||
if let settingEntity = settingEntities.first {
|
||||
settingEntity.token = token
|
||||
} else {
|
||||
// Fallback: Neue Einstellung erstellen (sollte normalerweise nicht passieren)
|
||||
let settingEntity = SettingEntity(context: context)
|
||||
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()
|
||||
continuation.resume()
|
||||
} 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 {
|
||||
try await settingsRepository.clearSettings()
|
||||
cachedSettings = nil
|
||||
try await settingsRepository.saveToken("")
|
||||
cachedSettings?.token = ""
|
||||
} 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 {
|
||||
try await settingsRepository.saveSettings(
|
||||
.init(
|
||||
|
||||
@ -5,6 +5,7 @@ struct BookmarkDetailView: View {
|
||||
let bookmarkId: String
|
||||
@State private var viewModel = BookmarkDetailViewModel()
|
||||
@State private var webViewHeight: CGFloat = 300
|
||||
@State private var showingFontSettings = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@ -51,6 +52,29 @@ struct BookmarkDetailView: View {
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
||||
await viewModel.loadArticleContent(id: bookmarkId)
|
||||
|
||||
@ -10,6 +10,7 @@ protocol UseCaseFactory {
|
||||
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase
|
||||
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
|
||||
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase
|
||||
func makeLogoutUseCase() -> LogoutUseCase
|
||||
}
|
||||
|
||||
class DefaultUseCaseFactory: UseCaseFactory {
|
||||
@ -51,6 +52,10 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
||||
return UpdateBookmarkUseCase(repository: bookmarksRepository)
|
||||
}
|
||||
|
||||
func makeLogoutUseCase() -> LogoutUseCase {
|
||||
return LogoutUseCase()
|
||||
}
|
||||
|
||||
// Nicht mehr nötig - Token wird automatisch geladen
|
||||
func refreshConfiguration() async {
|
||||
// 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
|
||||
|
||||
struct SettingsView: View {
|
||||
@State private var viewModel = SettingsViewModel()
|
||||
|
||||
@State var selectedTheme: Theme = .system
|
||||
@State var selectedFontSize: FontSize = .medium
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
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()
|
||||
}
|
||||
}
|
||||
SettingsContainerView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,9 +4,11 @@ import SwiftUI
|
||||
|
||||
@Observable
|
||||
class SettingsViewModel {
|
||||
private let loginUseCase: LoginUseCase
|
||||
private let _loginUseCase: LoginUseCase
|
||||
private let saveSettingsUseCase: SaveSettingsUseCase
|
||||
private let loadSettingsUseCase: LoadSettingsUseCase
|
||||
private let logoutUseCase: LogoutUseCase
|
||||
private let settingsRepository: SettingsRepository
|
||||
|
||||
// MARK: - Server Settings
|
||||
var endpoint = ""
|
||||
@ -37,56 +39,17 @@ class SettingsViewModel {
|
||||
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
|
||||
self.loginUseCase = factory.makeLoginUseCase()
|
||||
self._loginUseCase = factory.makeLoginUseCase()
|
||||
self.saveSettingsUseCase = factory.makeSaveSettingsUseCase()
|
||||
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
||||
self.logoutUseCase = factory.makeLogoutUseCase()
|
||||
self.settingsRepository = SettingsRepository()
|
||||
}
|
||||
|
||||
var isSetupMode: Bool {
|
||||
!settingsRepository.hasFinishedSetup
|
||||
}
|
||||
|
||||
@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
|
||||
func saveSettings() async {
|
||||
isSaving = true
|
||||
@ -145,11 +97,17 @@ class SettingsViewModel {
|
||||
successMessage = nil
|
||||
|
||||
do {
|
||||
let user = try await loginUseCase.execute(username: username, password: password)
|
||||
let user = try await _loginUseCase.execute(username: username, password: password)
|
||||
|
||||
isLoggedIn = true
|
||||
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)
|
||||
await DefaultUseCaseFactory.shared.refreshConfiguration()
|
||||
|
||||
@ -164,15 +122,23 @@ class SettingsViewModel {
|
||||
@MainActor
|
||||
func logout() async {
|
||||
do {
|
||||
// Hier könntest du eine Logout-UseCase hinzufügen
|
||||
// try await logoutUseCase.execute()
|
||||
try await logoutUseCase.execute()
|
||||
isLoggedIn = false
|
||||
successMessage = "Abgemeldet"
|
||||
|
||||
// Notification senden, dass sich der Setup-Status geändert hat
|
||||
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Abmelden"
|
||||
}
|
||||
}
|
||||
|
||||
func clearMessages() {
|
||||
errorMessage = nil
|
||||
successMessage = nil
|
||||
}
|
||||
|
||||
var canSave: Bool {
|
||||
!endpoint.isEmpty && !username.isEmpty && !password.isEmpty
|
||||
}
|
||||
@ -180,6 +146,11 @@ class SettingsViewModel {
|
||||
var canLogin: Bool {
|
||||
!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 {
|
||||
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
|
||||
struct readeckApp: App {
|
||||
let persistenceController = PersistenceController.shared
|
||||
|
||||
@State private var hasFinishedSetup = false
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MainTabView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.onOpenURL { url in
|
||||
handleIncomingURL(url)
|
||||
}
|
||||
.onAppear {
|
||||
#if DEBUG
|
||||
NFX.sharedInstance().start()
|
||||
#endif
|
||||
Group {
|
||||
if hasFinishedSetup {
|
||||
MainTabView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
} else {
|
||||
SettingsContainerView()
|
||||
}
|
||||
}
|
||||
.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) {
|
||||
guard url.scheme == "readeck",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user