From 7df56687c7cf4990766aa03e9c3cdcdab62901f3 Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Wed, 2 Jul 2025 16:25:23 +0200 Subject: [PATCH] Refactor UI navigation and settings management - Split TabView and Sidebar logic into PhoneTabView, PadSidebarView, SidebarTab, and BookmarkState for better device adaptation - Remove old SettingsViewModel, introduce SettingsGeneralViewModel and SettingsServerViewModel for modular settings - Update BookmarksView and BookmarksViewModel for new paginated and filtered data model - Clean up and modularize settings UI (SettingsGeneralView, SettingsServerView, FontSettingsView) - Remove obsolete files (old TabView, File.swift, SettingsViewModel, etc.) - Add BookmarksPageDto and update related data flow - Various UI/UX improvements and code cleanup BREAKING: Settings and navigation structure refactored, old settings logic removed --- readeck/Data/API/DTOs/BookmarksPageDto.swift | 14 ++ .../Data/DTOs/CreateBookmarkResponseDto.swift | 6 - readeck/Data/DTOs/UserDto.swift | 11 - readeck/UI/Menu/BookmarkState.swift | 27 +++ readeck/UI/Menu/PadSidebarView.swift | 76 +++++++ readeck/UI/Menu/PhoneTabView.swift | 28 +++ readeck/UI/Menu/SidebarTab.swift | 33 +++ readeck/UI/Menu/TabView.swift | 203 ++++++++++++++++++ .../Settings/SettingsGeneralViewModel.swift | 74 +++++++ .../UI/Settings/SettingsServerViewModel.swift | 106 +++++++++ readeck/UI/TabView.swift | 144 ------------- 11 files changed, 561 insertions(+), 161 deletions(-) create mode 100644 readeck/Data/API/DTOs/BookmarksPageDto.swift delete mode 100644 readeck/Data/DTOs/CreateBookmarkResponseDto.swift delete mode 100644 readeck/Data/DTOs/UserDto.swift create mode 100644 readeck/UI/Menu/BookmarkState.swift create mode 100644 readeck/UI/Menu/PadSidebarView.swift create mode 100644 readeck/UI/Menu/PhoneTabView.swift create mode 100644 readeck/UI/Menu/SidebarTab.swift create mode 100644 readeck/UI/Menu/TabView.swift create mode 100644 readeck/UI/Settings/SettingsGeneralViewModel.swift create mode 100644 readeck/UI/Settings/SettingsServerViewModel.swift delete mode 100644 readeck/UI/TabView.swift diff --git a/readeck/Data/API/DTOs/BookmarksPageDto.swift b/readeck/Data/API/DTOs/BookmarksPageDto.swift new file mode 100644 index 0000000..e64bd37 --- /dev/null +++ b/readeck/Data/API/DTOs/BookmarksPageDto.swift @@ -0,0 +1,14 @@ +// +// BookmarksPageDto.swift +// readeck +// +// Created by Ilyas Hallak on 01.07.25. +// + +struct BookmarksPageDto { + let bookmarks: [BookmarkDto] + let currentPage: Int? + let totalCount: Int? + let totalPages: Int? + let links: [String]? +} diff --git a/readeck/Data/DTOs/CreateBookmarkResponseDto.swift b/readeck/Data/DTOs/CreateBookmarkResponseDto.swift deleted file mode 100644 index 498f3c2..0000000 --- a/readeck/Data/DTOs/CreateBookmarkResponseDto.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -struct CreateBookmarkResponseDto: Codable { - let message: String - let status: Int -} diff --git a/readeck/Data/DTOs/UserDto.swift b/readeck/Data/DTOs/UserDto.swift deleted file mode 100644 index d8eb66b..0000000 --- a/readeck/Data/DTOs/UserDto.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -struct UserDto: Codable { - let id: String - let token: String - - enum CodingKeys: String, CodingKey { - case id - case token - } -} \ No newline at end of file diff --git a/readeck/UI/Menu/BookmarkState.swift b/readeck/UI/Menu/BookmarkState.swift new file mode 100644 index 0000000..7e01529 --- /dev/null +++ b/readeck/UI/Menu/BookmarkState.swift @@ -0,0 +1,27 @@ +enum BookmarkState: String, CaseIterable { + case unread = "unread" + case favorite = "favorite" + case archived = "archived" + + var displayName: String { + switch self { + case .unread: + return "Ungelesen" + case .favorite: + return "Favoriten" + case .archived: + return "Archiv" + } + } + + var systemImage: String { + switch self { + case .unread: + return "house" + case .favorite: + return "heart" + case .archived: + return "archivebox" + } + } +} \ No newline at end of file diff --git a/readeck/UI/Menu/PadSidebarView.swift b/readeck/UI/Menu/PadSidebarView.swift new file mode 100644 index 0000000..879ae6a --- /dev/null +++ b/readeck/UI/Menu/PadSidebarView.swift @@ -0,0 +1,76 @@ +struct PadSidebarView: View { + @State private var selectedTab: SidebarTab = .unread + @State private var selectedBookmark: Bookmark? + + var body: some View { + NavigationSplitView { + List { + ForEach(SidebarTab.allCases.filter { $0 != .settings }, id: \.self) { tab in + Button(action: { + selectedTab = tab + }) { + Label(tab.label, systemImage: tab.systemImage) + .foregroundColor(selectedTab == tab ? .accentColor : .primary) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + + if tab == .article { + Spacer() + } + + if tab == .pictures { + Divider() + } + } + .listRowBackground(selectedTab == tab ? Color.accentColor.opacity(0.15) : Color.clear) + } + } + .listStyle(.sidebar) + .safeAreaInset(edge: .bottom, alignment: .center) { + VStack(spacing: 0) { + Divider() + Button(action: { + selectedTab = .settings + }) { + Label(SidebarTab.settings.label, systemImage: SidebarTab.settings.systemImage) + .foregroundColor(selectedTab == .settings ? .accentColor : .primary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 12) + .contentShape(Rectangle()) + } + .listRowBackground(selectedTab == .settings ? Color.accentColor.opacity(0.15) : Color.clear) + } + .padding(.horizontal, 12) + .background(Color(.systemGroupedBackground)) + } + } content: { + switch selectedTab { + case .all: + Text("All") + case .unread: + BookmarksView(state: .unread, selectedBookmark: $selectedBookmark) + case .favorite: + BookmarksView(state: .favorite, selectedBookmark: $selectedBookmark) + case .archived: + BookmarksView(state: .archived, selectedBookmark: $selectedBookmark) + case .settings: + SettingsView() + case .article: + Text("Artikel") + case .videos: + Text("Videos") + case .pictures: + Text("Pictures") + case .tags: + Text("Tags") + } + } detail: { + if let bookmark = selectedBookmark, selectedTab != .settings { + BookmarkDetailView(bookmarkId: bookmark.id) + } else { + Text(selectedTab == .settings ? "" : "Select a bookmark") + .foregroundColor(.gray) + } + } + } +} \ No newline at end of file diff --git a/readeck/UI/Menu/PhoneTabView.swift b/readeck/UI/Menu/PhoneTabView.swift new file mode 100644 index 0000000..71a3aac --- /dev/null +++ b/readeck/UI/Menu/PhoneTabView.swift @@ -0,0 +1,28 @@ +struct PhoneTabView: View { + var body: some View { + TabView { + NavigationStack { + BookmarksView(state: .unread, selectedBookmark: .constant(nil)) + } + .tabItem { + Label("Ungelesen", systemImage: "house") + } + + BookmarksView(state: .favorite, selectedBookmark: .constant(nil)) + .tabItem { + Label("Favoriten", systemImage: "heart") + } + + BookmarksView(state: .archived, selectedBookmark: .constant(nil)) + .tabItem { + Label("Archiv", systemImage: "archivebox") + } + + SettingsView() + .tabItem { + Label("Settings", systemImage: "gear") + } + } + .accentColor(.accentColor) + } +} diff --git a/readeck/UI/Menu/SidebarTab.swift b/readeck/UI/Menu/SidebarTab.swift new file mode 100644 index 0000000..cd2b735 --- /dev/null +++ b/readeck/UI/Menu/SidebarTab.swift @@ -0,0 +1,33 @@ +enum SidebarTab: Hashable, CaseIterable, Identifiable { + case all, unread, favorite, archived, settings, article, videos, pictures, tags + + var id: Self { self } + + var label: String { + switch self { + case .all: return "Alle" + case .unread: return "Ungelesen" + case .favorite: return "Favoriten" + case .archived: return "Archiv" + case .settings: return "Einstellungen" + case .article: return "Artikel" + case .videos: return "Videos" + case .pictures: return "Bilder" + case .tags: return "Tags" + } + } + + var systemImage: String { + switch self { + case .unread: return "house" + case .favorite: return "heart" + case .archived: return "archivebox" + case .settings: return "gear" + case .all: return "list.bullet" + case .article: return "doc.plaintext" + case .videos: return "film" + case .pictures: return "photo" + case .tags: return "tag" + } + } +} \ No newline at end of file diff --git a/readeck/UI/Menu/TabView.swift b/readeck/UI/Menu/TabView.swift new file mode 100644 index 0000000..bba377d --- /dev/null +++ b/readeck/UI/Menu/TabView.swift @@ -0,0 +1,203 @@ +import SwiftUI +import Foundation + +enum BookmarkState: String, CaseIterable { + case unread = "unread" + case favorite = "favorite" + case archived = "archived" + + var displayName: String { + switch self { + case .unread: + return "Ungelesen" + case .favorite: + return "Favoriten" + case .archived: + return "Archiv" + } + } + + var systemImage: String { + switch self { + case .unread: + return "house" + case .favorite: + return "heart" + case .archived: + return "archivebox" + } + } +} + +struct MainTabView: View { + @State private var selectedTab: SidebarTab = .unread + @State var selectedBookmark: Bookmark? + + // sizeClass + @Environment(\.horizontalSizeClass) + var horizontalSizeClass + + @Environment(\.verticalSizeClass) + var verticalSizeClass + + var body: some View { + if UIDevice.isPhone { + PhoneView() + } else { + PadSidebarView() + } + } +} + +// Sidebar Tabs +enum SidebarTab: Hashable, CaseIterable, Identifiable { + case all, unread, favorite, archived, settings, article, videos, pictures, tags + + var id: Self { self } + + var label: String { + switch self { + case .all: return "Alle" + case .unread: return "Ungelesen" + case .favorite: return "Favoriten" + case .archived: return "Archiv" + case .settings: return "Einstellungen" + case .article: return "Artikel" + case .videos: return "Videos" + case .pictures: return "Bilder" + case .tags: return "Tags" + } + } + + var systemImage: String { + switch self { + case .unread: return "house" + case .favorite: return "heart" + case .archived: return "archivebox" + case .settings: return "gear" + case .all: return "list.bullet" + case .article: return "doc.plaintext" + case .videos: return "film" + case .pictures: return "photo" + case .tags: return "tag" + } + } +} + +struct PadSidebarView: View { + @State private var selectedTab: SidebarTab = .unread + @State private var selectedBookmark: Bookmark? + + var body: some View { + NavigationSplitView { + List { + ForEach(SidebarTab.allCases.filter { $0 != .settings }, id: \.self) { tab in + Button(action: { + selectedTab = tab + }) { + Label(tab.label, systemImage: tab.systemImage) + .foregroundColor(selectedTab == tab ? .accentColor : .primary) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + + if tab == .article { + Spacer() + } + + if tab == .pictures { + Divider() + } + } + .listRowBackground(selectedTab == tab ? Color.accentColor.opacity(0.15) : Color.clear) + } + } + .listStyle(.sidebar) + .safeAreaInset(edge: .bottom, alignment: .center) { + VStack(spacing: 0) { + Divider() + Button(action: { + selectedTab = .settings + }) { + Label(SidebarTab.settings.label, systemImage: SidebarTab.settings.systemImage) + .foregroundColor(selectedTab == .settings ? .accentColor : .primary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 12) + .contentShape(Rectangle()) + } + .listRowBackground(selectedTab == .settings ? Color.accentColor.opacity(0.15) : Color.clear) + } + .padding(.horizontal, 12) + .background(Color(.systemGroupedBackground)) + } + } content: { + switch selectedTab { + case .all: + Text("All") + case .unread: + BookmarksView(state: .unread, selectedBookmark: $selectedBookmark) + case .favorite: + BookmarksView(state: .favorite, selectedBookmark: $selectedBookmark) + case .archived: + BookmarksView(state: .archived, selectedBookmark: $selectedBookmark) + case .settings: + SettingsView() + case .article: + Text("Artikel") + case .videos: + Text("Videos") + case .pictures: + Text("Pictures") + case .tags: + Text("Tags") + } + } detail: { + if let bookmark = selectedBookmark, selectedTab != .settings { + BookmarkDetailView(bookmarkId: bookmark.id) + } else { + Text(selectedTab == .settings ? "" : "Select a bookmark") + .foregroundColor(.gray) + } + } + } +} + +// iPhone: TabView bleibt wie gehabt +extension MainTabView { + @ViewBuilder + fileprivate func PhoneView() -> some View { + TabView { + NavigationStack { + BookmarksView(state: .unread, selectedBookmark: .constant(nil)) + } + .tabItem { + Label("Ungelesen", systemImage: "house") + } + + NavigationView { + BookmarksView(state: .favorite, selectedBookmark: .constant(nil)) + .tabItem { + Label("Favoriten", systemImage: "heart") + } + } + + NavigationView { + BookmarksView(state: .archived, selectedBookmark: .constant(nil)) + .tabItem { + Label("Archiv", systemImage: "archivebox") + } + } + + NavigationView { + SettingsView() + .tabItem { + Label("Settings", systemImage: "gear") + } + } + } + .accentColor(.accentColor) + } +} + +#Preview { + MainTabView() +} diff --git a/readeck/UI/Settings/SettingsGeneralViewModel.swift b/readeck/UI/Settings/SettingsGeneralViewModel.swift new file mode 100644 index 0000000..9a06fb9 --- /dev/null +++ b/readeck/UI/Settings/SettingsGeneralViewModel.swift @@ -0,0 +1,74 @@ +import Foundation +import Observation +import SwiftUI + +@Observable +class SettingsGeneralViewModel { + private let saveSettingsUseCase: SaveSettingsUseCase + private let loadSettingsUseCase: LoadSettingsUseCase + + // MARK: - UI Settings + var selectedTheme: Theme = .system + // MARK: - Sync Settings + var autoSyncEnabled: Bool = true + var syncInterval: Int = 15 + // MARK: - Reading Settings + var enableReaderMode: Bool = false + var openExternalLinksInApp: Bool = true + var autoMarkAsRead: Bool = false + // MARK: - App Info + var appVersion: String = "1.0.0" + var developerName: String = "Your Name" + // MARK: - Messages + var errorMessage: String? + var successMessage: String? + // MARK: - Data Management (Platzhalter) + // func clearCache() async {} + // func resetSettings() async {} + + init() { + let factory = DefaultUseCaseFactory.shared + self.saveSettingsUseCase = factory.makeSaveSettingsUseCase() + self.loadSettingsUseCase = factory.makeLoadSettingsUseCase() + } + + @MainActor + func loadGeneralSettings() async { + do { + if let settings = try await loadSettingsUseCase.execute() { + selectedTheme = .system // settings.theme ?? .system + autoSyncEnabled = settings.autoSyncEnabled + syncInterval = settings.syncInterval + enableReaderMode = settings.enableReaderMode + openExternalLinksInApp = settings.openExternalLinksInApp + autoMarkAsRead = settings.autoMarkAsRead + appVersion = settings.appVersion ?? "1.0.0" + developerName = settings.developerName ?? "Your Name" + } + } catch { + errorMessage = "Fehler beim Laden der Einstellungen" + } + } + + @MainActor + func saveGeneralSettings() async { + do { + try await saveSettingsUseCase.execute( + selectedTheme: selectedTheme, + autoSyncEnabled: autoSyncEnabled, + syncInterval: syncInterval, + enableReaderMode: enableReaderMode, + openExternalLinksInApp: openExternalLinksInApp, + autoMarkAsRead: autoMarkAsRead + ) + successMessage = "Einstellungen gespeichert" + } catch { + errorMessage = "Fehler beim Speichern der Einstellungen" + } + } + + func clearMessages() { + errorMessage = nil + successMessage = nil + } +} diff --git a/readeck/UI/Settings/SettingsServerViewModel.swift b/readeck/UI/Settings/SettingsServerViewModel.swift new file mode 100644 index 0000000..d6ff429 --- /dev/null +++ b/readeck/UI/Settings/SettingsServerViewModel.swift @@ -0,0 +1,106 @@ +import Foundation +import Observation +import SwiftUI + +@Observable +class SettingsServerViewModel { + private let loginUseCase: LoginUseCase + private let logoutUseCase: LogoutUseCase + private let saveSettingsUseCase: SaveSettingsUseCase + private let loadSettingsUseCase: LoadSettingsUseCase + private let settingsRepository: SettingsRepository + + // MARK: - Server Settings + var endpoint = "" + var username = "" + var password = "" + var isLoading = false + var isLoggedIn = false + // MARK: - Messages + var errorMessage: String? + var successMessage: String? + + init() { + let factory = DefaultUseCaseFactory.shared + self.loginUseCase = factory.makeLoginUseCase() + self.logoutUseCase = factory.makeLogoutUseCase() + self.saveSettingsUseCase = factory.makeSaveSettingsUseCase() + self.loadSettingsUseCase = factory.makeLoadSettingsUseCase() + self.settingsRepository = SettingsRepository() + } + + var isSetupMode: Bool { + !settingsRepository.hasFinishedSetup + } + + @MainActor + func loadServerSettings() async { + do { + if let settings = try await loadSettingsUseCase.execute() { + endpoint = settings.endpoint ?? "" + username = settings.username ?? "" + password = settings.password ?? "" + isLoggedIn = settings.isLoggedIn + } + } catch { + errorMessage = "Fehler beim Laden der Einstellungen" + } + } + + @MainActor + func saveServerSettings() async { + do { + try await saveSettingsUseCase.execute( + endpoint: endpoint, + username: username, + password: password + ) + successMessage = "Server-Einstellungen gespeichert" + } catch { + errorMessage = "Fehler beim Speichern der Server-Einstellungen" + } + } + + @MainActor + func login() async { + isLoading = true + errorMessage = nil + successMessage = nil + do { + let _ = try await loginUseCase.execute(username: username, password: password) + isLoggedIn = true + successMessage = "Erfolgreich angemeldet" + try await settingsRepository.saveHasFinishedSetup(true) + NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil) + await DefaultUseCaseFactory.shared.refreshConfiguration() + } catch { + errorMessage = "Anmeldung fehlgeschlagen" + isLoggedIn = false + } + isLoading = false + } + + @MainActor + func logout() async { + do { + try await logoutUseCase.execute() + isLoggedIn = false + successMessage = "Abgemeldet" + 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 + } + var canLogin: Bool { + !username.isEmpty && !password.isEmpty + } +} \ No newline at end of file diff --git a/readeck/UI/TabView.swift b/readeck/UI/TabView.swift deleted file mode 100644 index 0315e01..0000000 --- a/readeck/UI/TabView.swift +++ /dev/null @@ -1,144 +0,0 @@ -import SwiftUI -import Foundation - -enum BookmarkState: String, CaseIterable { - case unread = "unread" - case favorite = "favorite" - case archived = "archived" - - var displayName: String { - switch self { - case .unread: - return "Ungelesen" - case .favorite: - return "Favoriten" - case .archived: - return "Archiv" - } - } - - var systemImage: String { - switch self { - case .unread: - return "house" - case .favorite: - return "heart" - case .archived: - return "archivebox" - } - } -} - -struct MainTabView: View { - @State private var selectedTab: String = "Ungelesen" - - // sizeClass - @Environment(\.horizontalSizeClass) - var horizontalSizeClass - - @Environment(\.verticalSizeClass) - var verticalSizeClass - - @State var selectedBookmark: Bookmark? - - var body: some View { - if UIDevice.isPhone { - PhoneView() - } else { - PadView() - } - } - - @ViewBuilder - private func PhoneView() -> some View { - TabView(selection: $selectedTab) { - BookmarksView(state: .unread, selectedBookmark: .constant(nil)) - .tabItem { - Label("Ungelesen", systemImage: "house") - } - .tag("Ungelesen") - - BookmarksView(state: .favorite, selectedBookmark: .constant(nil)) - .tabItem { - Label("Favoriten", systemImage: "heart") - } - .tag("Favoriten") - - BookmarksView(state: .archived, selectedBookmark: .constant(nil)) - .tabItem { - Label("Archiv", systemImage: "archivebox") - } - .tag("Archiv") - - SettingsView() - .tabItem { - Label("Settings", systemImage: "gear") - } - .tag("Settings") - } - .accentColor(.accentColor) - } - - @ViewBuilder - private func PadView() -> some View { - TabView(selection: $selectedTab) { - // Ungelesen Tab - NavigationSplitView { - BookmarksView(state: .unread, selectedBookmark: $selectedBookmark) - } detail: { - if let selectedBookmark = selectedBookmark { - BookmarkDetailView(bookmarkId: selectedBookmark.id) - } else { - Text("Select a bookmark") - .foregroundColor(.gray) - } - } - .tabItem { - Label("Unread", systemImage: "house") - } - .tag("Unread") - - NavigationSplitViewContainer(state: .favorite, selectedBookmark: $selectedBookmark) - .tabItem { - Label("Favoriten", systemImage: "heart") - } - .tag("Favorite") - - NavigationSplitViewContainer(state: .archived, selectedBookmark: $selectedBookmark) - .tabItem { - Label("Archive", systemImage: "archivebox") - } - .tag("Archive") - - SettingsView() - .tabItem { - Label("Settings", systemImage: "gear") - } - .tag("Settings") - } - .accentColor(.accentColor) - } -} - -// Container für NavigationSplitView -struct NavigationSplitViewContainer: View { - let state: BookmarkState - @Binding var selectedBookmark: Bookmark? - - var body: some View { - NavigationSplitView { - BookmarksView(state: state, selectedBookmark: $selectedBookmark) - } detail: { - if let selectedBookmark = selectedBookmark { - BookmarkDetailView(bookmarkId: selectedBookmark.id) - } else { - Text("Select a bookmark") - .foregroundColor(.gray) - } - } - } -} - -#Preview { - MainTabView() -}