diff --git a/readeck/Data/API/API.swift b/readeck/Data/API/API.swift index 70bc9a1..ba720c3 100644 --- a/readeck/Data/API/API.swift +++ b/readeck/Data/API/API.swift @@ -10,7 +10,7 @@ import Foundation protocol PAPI { var tokenProvider: TokenProvider { get } func login(username: String, password: String) async throws -> UserDto - func getBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?) async throws -> [BookmarkDto] + func getBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?) async throws -> BookmarksPageDto func getBookmark(id: String) async throws -> BookmarkDetailDto func getBookmarkArticle(id: String) async throws -> String func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto @@ -36,6 +36,46 @@ class API: PAPI { return url } } + + private func makeJSONRequestWithHeaders( + endpoint: String, + method: HTTPMethod = .GET, + body: Data? = nil, + responseType: T.Type + ) async throws -> (T, HTTPURLResponse) { + let baseURL = await self.baseURL + let fullEndpoint = endpoint.hasPrefix("/api") ? endpoint : "/api\(endpoint)" + + guard let url = URL(string: "\(baseURL)\(fullEndpoint)") else { + throw APIError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + if let token = await tokenProvider.getToken() { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + if let body = body { + request.httpBody = body + } + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.invalidResponse + } + + guard 200...299 ~= httpResponse.statusCode else { + print("Server Error: \(httpResponse.statusCode) - \(String(data: data, encoding: .utf8) ?? "No response body")") + throw APIError.serverError(httpResponse.statusCode) + } + + let decoded = try JSONDecoder().decode(T.self, from: data) + return (decoded, httpResponse) + } // Separate Methode für JSON-Requests private func makeJSONRequest( @@ -131,7 +171,8 @@ class API: PAPI { return userDto } - func getBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [BookmarkDto] { + // Angepasste getBookmarks-Methode mit Header-Auslesen + func getBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil) async throws -> BookmarksPageDto { var endpoint = "/api/bookmarks" var queryItems: [URLQueryItem] = [] @@ -145,6 +186,8 @@ class API: PAPI { queryItems.append(URLQueryItem(name: "is_marked", value: "true")) case .archived: queryItems.append(URLQueryItem(name: "is_archived", value: "true")) + case .all: + break } } @@ -159,15 +202,37 @@ class API: PAPI { queryItems.append(URLQueryItem(name: "search", value: search)) } + // type-Parameter als Array von BookmarkType + if let type = type, !type.isEmpty { + for t in type { + queryItems.append(URLQueryItem(name: "type", value: t.rawValue)) + } + } + if !queryItems.isEmpty { let queryString = queryItems.map { "\($0.name)=\($0.value ?? "")" }.joined(separator: "&") endpoint += "?\(queryString)" } - return try await makeJSONRequest( + let (bookmarks, response) = try await makeJSONRequestWithHeaders( endpoint: endpoint, responseType: [BookmarkDto].self ) + + // Header auslesen + let currentPage = response.value(forHTTPHeaderField: "Current-Page").flatMap { Int($0) } + let totalCount = response.value(forHTTPHeaderField: "Total-Count").flatMap { Int($0) } + let totalPages = response.value(forHTTPHeaderField: "Total-Pages").flatMap { Int($0) } + let linksHeader = response.value(forHTTPHeaderField: "Link") + let links = linksHeader?.components(separatedBy: ",") + + return BookmarksPageDto( + bookmarks: bookmarks, + currentPage: currentPage, + totalCount: totalCount, + totalPages: totalPages, + links: links + ) } func getBookmark(id: String) async throws -> BookmarkDetailDto { diff --git a/readeck/Data/API/DTOs/CreateBookmarkResponseDto.swift b/readeck/Data/API/DTOs/CreateBookmarkResponseDto.swift new file mode 100644 index 0000000..498f3c2 --- /dev/null +++ b/readeck/Data/API/DTOs/CreateBookmarkResponseDto.swift @@ -0,0 +1,6 @@ +import Foundation + +struct CreateBookmarkResponseDto: Codable { + let message: String + let status: Int +} diff --git a/readeck/Data/API/DTOs/UserDto.swift b/readeck/Data/API/DTOs/UserDto.swift new file mode 100644 index 0000000..d8eb66b --- /dev/null +++ b/readeck/Data/API/DTOs/UserDto.swift @@ -0,0 +1,11 @@ +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/Data/Mappers/BookmarkMapper.swift b/readeck/Data/Mappers/BookmarkMapper.swift index 9efe225..6bb2db3 100644 --- a/readeck/Data/Mappers/BookmarkMapper.swift +++ b/readeck/Data/Mappers/BookmarkMapper.swift @@ -1,5 +1,17 @@ import Foundation +extension BookmarksPageDto { + func toDomain() -> BookmarksPage { + return BookmarksPage( + bookmarks: bookmarks.map { $0.toDomain() }, + currentPage: currentPage, + totalCount: totalCount, + totalPages: totalPages, + links: links + ) + } +} + // MARK: - BookmarkDto to Domain Mapping extension BookmarkDto { func toDomain() -> Bookmark { @@ -58,4 +70,4 @@ extension ImageResourceDto { func toDomain() -> ImageResource { return ImageResource(src: src, height: height, width: width) } -} \ No newline at end of file +} diff --git a/readeck/Data/Repository/BookmarksRepository.swift b/readeck/Data/Repository/BookmarksRepository.swift index 80e0080..8b448e5 100644 --- a/readeck/Data/Repository/BookmarksRepository.swift +++ b/readeck/Data/Repository/BookmarksRepository.swift @@ -1,7 +1,7 @@ import Foundation protocol PBookmarksRepository { - func fetchBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?) async throws -> [Bookmark] + func fetchBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?) async throws -> BookmarksPage func fetchBookmark(id: String) async throws -> BookmarkDetail func fetchBookmarkArticle(id: String) async throws -> String func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String @@ -16,9 +16,9 @@ class BookmarksRepository: PBookmarksRepository { self.api = api } - func fetchBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [Bookmark] { - let bookmarkDtos = try await api.getBookmarks(state: state, limit: limit, offset: offset, search: search) - return bookmarkDtos.map { $0.toDomain() } + func fetchBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil) async throws -> BookmarksPage { + let bookmarkDtos = try await api.getBookmarks(state: state, limit: limit, offset: offset, search: search, type: type) + return bookmarkDtos.toDomain() } func fetchBookmark(id: String) async throws -> BookmarkDetail { diff --git a/readeck/Domain/Model/Bookmark.swift b/readeck/Domain/Model/Bookmark.swift index 49088ee..f008481 100644 --- a/readeck/Domain/Model/Bookmark.swift +++ b/readeck/Domain/Model/Bookmark.swift @@ -1,5 +1,13 @@ import Foundation +struct BookmarksPage { + var bookmarks: [Bookmark] + let currentPage: Int? + let totalCount: Int? + let totalPages: Int? + let links: [String]? +} + struct Bookmark { let id: String let title: String diff --git a/readeck/Domain/Model/BookmarkType.swift b/readeck/Domain/Model/BookmarkType.swift new file mode 100644 index 0000000..717913d --- /dev/null +++ b/readeck/Domain/Model/BookmarkType.swift @@ -0,0 +1,7 @@ +import Foundation + +public enum BookmarkType: String, CaseIterable, Codable { + case article + case photo + case video +} \ No newline at end of file diff --git a/readeck/Domain/UseCase/GetBookmarksUseCase.swift b/readeck/Domain/UseCase/GetBookmarksUseCase.swift index 3b8c553..6681fb0 100644 --- a/readeck/Domain/UseCase/GetBookmarksUseCase.swift +++ b/readeck/Domain/UseCase/GetBookmarksUseCase.swift @@ -7,13 +7,14 @@ class GetBookmarksUseCase { self.repository = repository } - func execute(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [Bookmark] { - let allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset, search: search) + func execute(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil) async throws -> BookmarksPage { + var allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset, search: search, type: type) - // Fallback-Filterung auf Client-Seite falls API keine Query-Parameter unterstützt if let state = state { - return allBookmarks.filter { bookmark in + allBookmarks.bookmarks = allBookmarks.bookmarks.filter { bookmark in switch state { + case .all: + return true case .unread: return !bookmark.isArchived && !bookmark.isMarked case .favorite: diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift index 1340814..f7182f6 100644 --- a/readeck/UI/Bookmarks/BookmarkCardView.swift +++ b/readeck/UI/Bookmarks/BookmarkCardView.swift @@ -82,22 +82,11 @@ struct BookmarkCardView: View { .background(Color(.systemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) .shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1) - // Swipe Actions hinzufügen .swipeActions(edge: .trailing, allowsFullSwipe: true) { - // Löschen (ganz rechts) Button("Löschen", role: .destructive) { onDelete(bookmark) } .tint(.red) - - // Favorit (rechts) - Button { - onToggleFavorite(bookmark) - } label: { - Label(bookmark.isMarked ? "Entfernen" : "Favorit", - systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill") - } - .tint(bookmark.isMarked ? .gray : .pink) } .swipeActions(edge: .leading, allowsFullSwipe: true) { // Archivieren (links) @@ -111,6 +100,14 @@ struct BookmarkCardView: View { } } .tint(currentState == .archived ? .blue : .orange) + + Button { + onToggleFavorite(bookmark) + } label: { + Label(bookmark.isMarked ? "Entfernen" : "Favorit", + systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill") + } + .tint(bookmark.isMarked ? .gray : .pink) } } @@ -121,7 +118,6 @@ struct BookmarkCardView: View { return nil } - // Prüfe auf Unix Epoch (1970-01-01) - bedeutet "kein Datum" if published.contains("1970-01-01") { return nil } diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift index 1b0dbd2..3f28b87 100644 --- a/readeck/UI/Bookmarks/BookmarksView.swift +++ b/readeck/UI/Bookmarks/BookmarksView.swift @@ -1,154 +1,164 @@ -import Foundation import Combine +import Foundation import SwiftUI struct BookmarksView: View { + + // MARK: States + @State private var viewModel = BookmarksViewModel() @State private var showingAddBookmark = false @State private var selectedBookmarkId: String? - let state: BookmarkState - - @Binding var selectedBookmark: Bookmark? - @State private var showingAddBookmarkFromShare = false @State private var shareURL = "" @State private var shareTitle = "" + let state: BookmarkState + let type: [BookmarkType] + + @Binding var selectedBookmark: Bookmark? + + // MARK: Environments + @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass - + var body: some View { - NavigationStack { - ZStack { - if viewModel.isLoading && viewModel.bookmarks.isEmpty { - ProgressView("Lade \(state.displayName)...") - } else { - List { - ForEach(viewModel.bookmarks, id: \.id) { bookmark in - Button(action: { - if UIDevice.isPhone { - selectedBookmarkId = bookmark.id - } else { - if selectedBookmark?.id == bookmark.id { - // Optional: Deselect, um erneutes Auswählen zu ermöglichen - selectedBookmark = nil - DispatchQueue.main.async { - selectedBookmark = bookmark - } - } else { + ZStack { + if viewModel.isLoading && viewModel.bookmarks?.bookmarks.isEmpty == true { + ProgressView("Lade \(state.displayName)...") + } else { + List { + ForEach(viewModel.bookmarks?.bookmarks ?? [], id: \.id) { bookmark in + Button(action: { + if UIDevice.isPhone { + selectedBookmarkId = bookmark.id + } else { + if selectedBookmark?.id == bookmark.id { + selectedBookmark = nil + DispatchQueue.main.async { selectedBookmark = bookmark } - } - }) { - BookmarkCardView( - bookmark: bookmark, - currentState: state, - onArchive: { bookmark in - Task { - await viewModel.toggleArchive(bookmark: bookmark) - } - }, - onDelete: { bookmark in - Task { - await viewModel.deleteBookmark(bookmark: bookmark) - } - }, - onToggleFavorite: { bookmark in - Task { - await viewModel.toggleFavorite(bookmark: bookmark) - } - } - ) - .onAppear { - if bookmark.id == viewModel.bookmarks.last?.id { - Task { - await viewModel.loadMoreBookmarks() - } - } + } else { + selectedBookmark = bookmark } } - .buttonStyle(PlainButtonStyle()) - .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - } - } - .listStyle(.plain) - .refreshable { - await viewModel.refreshBookmarks() - } - .overlay { - if viewModel.bookmarks.isEmpty && !viewModel.isLoading { - ContentUnavailableView( - "Keine Bookmarks", - systemImage: "bookmark", - description: Text("Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden.") + }) { + BookmarkCardView( + bookmark: bookmark, + currentState: state, + onArchive: { bookmark in + Task { + await viewModel.toggleArchive(bookmark: bookmark) + } + }, + onDelete: { bookmark in + Task { + await viewModel.deleteBookmark(bookmark: bookmark) + } + }, + onToggleFavorite: { bookmark in + Task { + await viewModel.toggleFavorite(bookmark: bookmark) + } + } ) - } - } - - } - - // FAB Button - nur bei "Ungelesen" anzeigen - if state == .unread { - VStack { - Spacer() - HStack { - Spacer() - - Button(action: { - showingAddBookmark = true - }) { - Image(systemName: "plus") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.white) - .frame(width: 56, height: 56) - .background(Color.accentColor) - .clipShape(Circle()) - .shadow(color: .black.opacity(0.25), radius: 6, x: 0, y: 3) + .onAppear { + if bookmark.id == viewModel.bookmarks?.bookmarks.last?.id { + Task { + await viewModel.loadMoreBookmarks() + } + } } - .padding(.trailing, 20) - .padding(.bottom, 20) } + .buttonStyle(PlainButtonStyle()) + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) } } - } - .navigationTitle(state.displayName) - .navigationDestination(item: Binding( - get: { selectedBookmarkId }, - set: { selectedBookmarkId = $0 } - )) { bookmarkId in - BookmarkDetailView(bookmarkId: bookmarkId) - } - .sheet(isPresented: $showingAddBookmark) { - AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle) - } - .sheet(isPresented: $viewModel.showingAddBookmarkFromShare, content: { - AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle) - }) - /*.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) { - Button("OK", role: .cancel) { - viewModel.errorMessage = nil + .listStyle(.plain) + .refreshable { + await viewModel.refreshBookmarks() } - } message: { - Text(viewModel.errorMessage ?? "") - }*/ - .onAppear { - Task { - await viewModel.loadBookmarks(state: state) + .overlay { + if viewModel.bookmarks?.bookmarks.isEmpty == true && !viewModel.isLoading { + ContentUnavailableView( + "Keine Bookmarks", + systemImage: "bookmark", + description: Text( + "Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden." + ) + ) + } } + .searchable( + text: $viewModel.searchQuery, placement: .automatic, prompt: "Search...") } - .onChange(of: showingAddBookmark) { oldValue, newValue in - // Refresh bookmarks when sheet is dismissed - if oldValue && !newValue { - Task { - await viewModel.loadBookmarks(state: state) + + // FAB Button - nur bei "Ungelesen" anzeigen + if state == .unread { + VStack { + Spacer() + HStack { + Spacer() + + Button(action: { + showingAddBookmark = true + }) { + Image(systemName: "plus") + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(width: 56, height: 56) + .background(Color.accentColor) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.25), radius: 6, x: 0, y: 3) + } + .padding(.trailing, 20) + .padding(.bottom, 20) } } } } - .searchable(text: $viewModel.searchQuery, placement: .automatic, prompt: "Search...") + .navigationTitle(state.displayName) + .navigationDestination( + item: Binding( + get: { selectedBookmarkId }, + set: { selectedBookmarkId = $0 } + ) + ) { bookmarkId in + BookmarkDetailView(bookmarkId: bookmarkId) + } + .sheet(isPresented: $showingAddBookmark) { + AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle) + } + .sheet( + isPresented: $viewModel.showingAddBookmarkFromShare, + content: { + AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle) + } + ) + /*.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) { + Button("OK", role: .cancel) { + viewModel.errorMessage = nil + } + } message: { + Text(viewModel.errorMessage ?? "") + }*/ + .onAppear { + Task { + await viewModel.loadBookmarks(state: state) + } + } + .onChange(of: showingAddBookmark) { oldValue, newValue in + // Refresh bookmarks when sheet is dismissed + if oldValue && !newValue { + Task { + await viewModel.loadBookmarks(state: state) + } + } + } } } diff --git a/readeck/UI/Bookmarks/BookmarksViewModel.swift b/readeck/UI/Bookmarks/BookmarksViewModel.swift index 3d974ad..6564346 100644 --- a/readeck/UI/Bookmarks/BookmarksViewModel.swift +++ b/readeck/UI/Bookmarks/BookmarksViewModel.swift @@ -8,29 +8,27 @@ class BookmarksViewModel { private let updateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase() private let deleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase() - var bookmarks: [Bookmark] = [] + var bookmarks: BookmarksPage? var isLoading = false var errorMessage: String? var currentState: BookmarkState = .unread + var type = [BookmarkType.article] var showingAddBookmarkFromShare = false var shareURL = "" var shareTitle = "" private var cancellables = Set() - - // Pagination-Variablen private var limit = 20 private var offset = 0 private var hasMoreData = true + private var searchWorkItem: DispatchWorkItem? var searchQuery: String = "" { didSet { throttleSearch() } } - - private var searchWorkItem: DispatchWorkItem? init() { setupNotificationObserver() @@ -88,13 +86,14 @@ class BookmarksViewModel { state: state, limit: limit, offset: offset, - search: searchQuery // Suche integrieren + search: searchQuery, + type: type ) bookmarks = newBookmarks - hasMoreData = newBookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind + hasMoreData = newBookmarks.bookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind } catch { errorMessage = "Fehler beim Laden der Bookmarks" - bookmarks = [] + bookmarks = nil } isLoading = false @@ -110,8 +109,8 @@ class BookmarksViewModel { do { offset += limit // Offset erhöhen let newBookmarks = try await getBooksmarksUseCase.execute(state: currentState, limit: limit, offset: offset) - bookmarks.append(contentsOf: newBookmarks) // Neue Bookmarks hinzufügen - hasMoreData = newBookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind + bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks) // Neue Bookmarks hinzufügen + hasMoreData = newBookmarks.bookmarks.count == limit // Prüfen, } catch { errorMessage = "Fehler beim Nachladen der Bookmarks" } @@ -163,7 +162,7 @@ class BookmarksViewModel { try await deleteBookmarkUseCase.execute(bookmarkId: bookmark.id) // Lokal aus der Liste entfernen (optimistische Update) - bookmarks.removeAll { $0.id == bookmark.id } + bookmarks?.bookmarks.removeAll { $0.id == bookmark.id } } catch { errorMessage = "Fehler beim Löschen des Bookmarks" diff --git a/readeck/UI/Bookmarks/File.swift b/readeck/UI/Bookmarks/File.swift deleted file mode 100644 index d05637f..0000000 --- a/readeck/UI/Bookmarks/File.swift +++ /dev/null @@ -1,7 +0,0 @@ -// -// File.swift -// readeck -// -// Created by Ilyas Hallak on 25.06.25. -// - diff --git a/readeck/UI/Menu/BookmarkState.swift b/readeck/UI/Menu/BookmarkState.swift index 7e01529..85b7873 100644 --- a/readeck/UI/Menu/BookmarkState.swift +++ b/readeck/UI/Menu/BookmarkState.swift @@ -1,10 +1,20 @@ +// +// BookmarkState.swift +// readeck +// +// Created by Ilyas Hallak on 01.07.25. +// + enum BookmarkState: String, CaseIterable { + case all = "all" case unread = "unread" case favorite = "favorite" case archived = "archived" var displayName: String { switch self { + case .all: + return "Alle" case .unread: return "Ungelesen" case .favorite: @@ -16,6 +26,8 @@ enum BookmarkState: String, CaseIterable { var systemImage: String { switch self { + case .all: + return "list.bullet" case .unread: return "house" case .favorite: @@ -24,4 +36,4 @@ enum BookmarkState: String, CaseIterable { return "archivebox" } } -} \ No newline at end of file +} diff --git a/readeck/UI/Menu/PadSidebarView.swift b/readeck/UI/Menu/PadSidebarView.swift index 879ae6a..249b64d 100644 --- a/readeck/UI/Menu/PadSidebarView.swift +++ b/readeck/UI/Menu/PadSidebarView.swift @@ -1,3 +1,12 @@ +// +// PadSidebarView.swift +// readeck +// +// Created by Ilyas Hallak on 01.07.25. +// + +import SwiftUI + struct PadSidebarView: View { @State private var selectedTab: SidebarTab = .unread @State private var selectedBookmark: Bookmark? @@ -14,12 +23,14 @@ struct PadSidebarView: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) - if tab == .article { - Spacer() + if tab == .archived { + Spacer(minLength: 20) } if tab == .pictures { + Spacer(minLength: 30) Divider() + Spacer() } } .listRowBackground(selectedTab == tab ? Color.accentColor.opacity(0.15) : Color.clear) @@ -46,21 +57,21 @@ struct PadSidebarView: View { } content: { switch selectedTab { case .all: - Text("All") + BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark) case .unread: - BookmarksView(state: .unread, selectedBookmark: $selectedBookmark) + BookmarksView(state: .unread, type: [.article], selectedBookmark: $selectedBookmark) case .favorite: - BookmarksView(state: .favorite, selectedBookmark: $selectedBookmark) + BookmarksView(state: .favorite, type: [.article], selectedBookmark: $selectedBookmark) case .archived: - BookmarksView(state: .archived, selectedBookmark: $selectedBookmark) + BookmarksView(state: .archived, type: [.article], selectedBookmark: $selectedBookmark) case .settings: SettingsView() case .article: - Text("Artikel") + BookmarksView(state: .all, type: [.article], selectedBookmark: $selectedBookmark) case .videos: - Text("Videos") + BookmarksView(state: .all, type: [.video], selectedBookmark: $selectedBookmark) case .pictures: - Text("Pictures") + BookmarksView(state: .all, type: [.photo], selectedBookmark: $selectedBookmark) case .tags: Text("Tags") } @@ -73,4 +84,4 @@ struct PadSidebarView: View { } } } -} \ No newline at end of file +} diff --git a/readeck/UI/Menu/PhoneTabView.swift b/readeck/UI/Menu/PhoneTabView.swift index 71a3aac..d496de5 100644 --- a/readeck/UI/Menu/PhoneTabView.swift +++ b/readeck/UI/Menu/PhoneTabView.swift @@ -1,19 +1,36 @@ +// +// PhoneTabView.swift +// readeck +// +// Created by Ilyas Hallak on 01.07.25. +// + +import SwiftUI + struct PhoneTabView: View { var body: some View { TabView { + NavigationStack { - BookmarksView(state: .unread, selectedBookmark: .constant(nil)) + BookmarksView(state: .unread, type: [.article, .video, .photo], selectedBookmark: .constant(nil)) + } + .tabItem { + Label("Alle", systemImage: "list.bullet") + } + + NavigationStack { + BookmarksView(state: .unread, type: [.article], selectedBookmark: .constant(nil)) } .tabItem { Label("Ungelesen", systemImage: "house") } - BookmarksView(state: .favorite, selectedBookmark: .constant(nil)) + BookmarksView(state: .favorite, type: [.article], selectedBookmark: .constant(nil)) .tabItem { Label("Favoriten", systemImage: "heart") } - BookmarksView(state: .archived, selectedBookmark: .constant(nil)) + BookmarksView(state: .archived, type: [.article], selectedBookmark: .constant(nil)) .tabItem { Label("Archiv", systemImage: "archivebox") } diff --git a/readeck/UI/Menu/SidebarTab.swift b/readeck/UI/Menu/SidebarTab.swift index cd2b735..424470b 100644 --- a/readeck/UI/Menu/SidebarTab.swift +++ b/readeck/UI/Menu/SidebarTab.swift @@ -1,3 +1,10 @@ +// +// SidebarTab.swift +// readeck +// +// Created by Ilyas Hallak on 01.07.25. +// + enum SidebarTab: Hashable, CaseIterable, Identifiable { case all, unread, favorite, archived, settings, article, videos, pictures, tags @@ -30,4 +37,4 @@ enum SidebarTab: Hashable, CaseIterable, Identifiable { case .tags: return "tag" } } -} \ No newline at end of file +} diff --git a/readeck/UI/Menu/TabView.swift b/readeck/UI/Menu/TabView.swift index bba377d..5f3a612 100644 --- a/readeck/UI/Menu/TabView.swift +++ b/readeck/UI/Menu/TabView.swift @@ -1,34 +1,6 @@ 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? @@ -42,162 +14,13 @@ struct MainTabView: View { var body: some View { if UIDevice.isPhone { - PhoneView() + PhoneTabView() } 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/SettingsContainerView.swift b/readeck/UI/Settings/SettingsContainerView.swift index 4698336..4983fa2 100644 --- a/readeck/UI/Settings/SettingsContainerView.swift +++ b/readeck/UI/Settings/SettingsContainerView.swift @@ -8,31 +8,23 @@ 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)) + ScrollView { + LazyVStack(spacing: 20) { + SettingsServerView() + .cardStyle() + + FontSettingsView() + .cardStyle() + + SettingsGeneralView() + .cardStyle() } - .navigationTitle("Einstellungen") - .navigationBarTitleDisplayMode(.large) - } - .task { - await viewModel.loadSettings() + .padding() + .background(Color(.systemGroupedBackground)) } + .navigationTitle("Einstellungen") + .navigationBarTitleDisplayMode(.large) } } diff --git a/readeck/UI/Settings/SettingsGeneralView.swift b/readeck/UI/Settings/SettingsGeneralView.swift index 0b06e44..5fbd9ae 100644 --- a/readeck/UI/Settings/SettingsGeneralView.swift +++ b/readeck/UI/Settings/SettingsGeneralView.swift @@ -9,7 +9,7 @@ import SwiftUI // SectionHeader wird jetzt zentral importiert struct SettingsGeneralView: View { - @State var viewModel: SettingsViewModel + @State private var viewModel = SettingsGeneralViewModel() var body: some View { VStack(spacing: 20) { @@ -28,10 +28,6 @@ struct SettingsGeneralView: View { .pickerStyle(.segmented) } - // Font Settings - FontSettingsView() - .padding(.vertical, 4) - // Sync Settings VStack(alignment: .leading, spacing: 12) { Text("Sync-Einstellungen") @@ -118,26 +114,19 @@ struct SettingsGeneralView: View { // Save Button Button(action: { Task { - await viewModel.saveSettings() + await viewModel.saveGeneralSettings() } }) { HStack { - if viewModel.isSaving { - ProgressView() - .scaleEffect(0.8) - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - } - Text(viewModel.isSaving ? "Speichere..." : "Einstellungen speichern") + Text("Einstellungen speichern") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding() - .background(viewModel.canSave ? Color.accentColor : Color.gray) + .background(Color.accentColor) .foregroundColor(.white) .cornerRadius(10) } - .disabled(!viewModel.canSave || viewModel.isSaving) - // Messages if let successMessage = viewModel.successMessage { HStack { @@ -158,9 +147,27 @@ struct SettingsGeneralView: View { } } } + .task { + await viewModel.loadGeneralSettings() + } } } +enum Theme: String, CaseIterable { + case system = "system" + case light = "light" + case dark = "dark" + + var displayName: String { + switch self { + case .system: return "System" + case .light: return "Hell" + case .dark: return "Dunkel" + } + } +} + + #Preview { - SettingsGeneralView(viewModel: SettingsViewModel()) + SettingsGeneralView() } diff --git a/readeck/UI/Settings/SettingsGeneralViewModel.swift b/readeck/UI/Settings/SettingsGeneralViewModel.swift index 9a06fb9..fef1987 100644 --- a/readeck/UI/Settings/SettingsGeneralViewModel.swift +++ b/readeck/UI/Settings/SettingsGeneralViewModel.swift @@ -37,13 +37,13 @@ class SettingsGeneralViewModel { 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" + autoSyncEnabled = false // settings.autoSyncEnabled + // syncInterval = settings.syncInterval + // enableReaderMode = settings.enableReaderMode + // openExternalLinksInApp = settings.openExternalLinksInApp + // autoMarkAsRead = settings.autoMarkAsRead + appVersion = "1.0.0" + developerName = "Ilyas Hallak" } } catch { errorMessage = "Fehler beim Laden der Einstellungen" @@ -53,14 +53,17 @@ class SettingsGeneralViewModel { @MainActor func saveGeneralSettings() async { do { - try await saveSettingsUseCase.execute( + + // TODO: add save general settings here + /*try await saveSettingsUseCase.execute( + token: "", selectedTheme: selectedTheme, autoSyncEnabled: autoSyncEnabled, syncInterval: syncInterval, enableReaderMode: enableReaderMode, openExternalLinksInApp: openExternalLinksInApp, autoMarkAsRead: autoMarkAsRead - ) + )*/ successMessage = "Einstellungen gespeichert" } catch { errorMessage = "Fehler beim Speichern der Einstellungen" diff --git a/readeck/UI/Settings/SettingsServerView.swift b/readeck/UI/Settings/SettingsServerView.swift index 6560e8a..859d024 100644 --- a/readeck/UI/Settings/SettingsServerView.swift +++ b/readeck/UI/Settings/SettingsServerView.swift @@ -9,7 +9,7 @@ import SwiftUI // SectionHeader wird jetzt zentral importiert struct SettingsServerView: View { - @State var viewModel = SettingsViewModel() + @State private var viewModel = SettingsServerViewModel() @State private var isTesting: Bool = false @State private var connectionTestSuccess: Bool = false @State private var showingLogoutAlert = false @@ -188,38 +188,18 @@ struct SettingsServerView: View { } message: { Text("Möchten Sie sich wirklich abmelden? Dies wird alle Ihre Anmeldedaten löschen und Sie zur Einrichtung zurückführen.") } + .task { + await viewModel.loadServerSettings() + } } 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)" - } - + connectionTestSuccess = await viewModel.testConnection() isTesting = false } } #Preview { - SettingsServerView(viewModel: SettingsViewModel()) + SettingsServerView() } diff --git a/readeck/UI/Settings/SettingsServerViewModel.swift b/readeck/UI/Settings/SettingsServerViewModel.swift index d6ff429..209874b 100644 --- a/readeck/UI/Settings/SettingsServerViewModel.swift +++ b/readeck/UI/Settings/SettingsServerViewModel.swift @@ -61,6 +61,34 @@ class SettingsServerViewModel { } } + @MainActor + func testConnection() async -> Bool { + guard canLogin else { + errorMessage = "Bitte füllen Sie alle Felder aus." + return false + } + + clearMessages() + + do { + // Test login without saving settings + let _ = try await loginUseCase.execute( + username: username.trimmingCharacters(in: .whitespacesAndNewlines), + password: password + ) + + + successMessage = "Verbindung erfolgreich getestet! ✓" + + return true + + } catch { + errorMessage = "Verbindungstest fehlgeschlagen: \(error.localizedDescription)" + } + + return false + } + @MainActor func login() async { isLoading = true @@ -103,4 +131,4 @@ class SettingsServerViewModel { var canLogin: Bool { !username.isEmpty && !password.isEmpty } -} \ No newline at end of file +} diff --git a/readeck/UI/Settings/SettingsViewModel.swift b/readeck/UI/Settings/SettingsViewModel.swift deleted file mode 100644 index 8eddc4b..0000000 --- a/readeck/UI/Settings/SettingsViewModel.swift +++ /dev/null @@ -1,169 +0,0 @@ -import Foundation -import Observation -import SwiftUI - -@Observable -class SettingsViewModel { - 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 = "" - var username = "" - var password = "" - var isLoading = false - var isSaving = false - var isLoggedIn = false - - // MARK: - UI Settings - var selectedTheme: Theme = .system - - // MARK: - Sync Settings - var autoSyncEnabled: Bool = true - var syncInterval: Int = 15 - - // MARK: - Reading Settings - var enableReaderMode: Bool = false - var openExternalLinksInApp: Bool = true - var autoMarkAsRead: Bool = false - - // MARK: - App Info - var appVersion: String = "1.0.0" - var developerName: String = "Your Name" - - - // MARK: - Messages - var errorMessage: String? - var successMessage: String? - - init() { - let factory = DefaultUseCaseFactory.shared - 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 - func loadSettings() 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 saveSettings() async { - isSaving = true - errorMessage = nil - successMessage = nil - - do { - try await saveSettingsUseCase.execute( - endpoint: endpoint, - username: username, - password: password - ) - successMessage = "Einstellungen gespeichert" - - // Factory-Konfiguration aktualisieren - await DefaultUseCaseFactory.shared.refreshConfiguration() - - } catch { - errorMessage = "Fehler beim Speichern der Einstellungen" - } - - isSaving = false - } - - @MainActor - func login() async { - isLoading = true - errorMessage = nil - successMessage = nil - - do { - 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() - - } catch { - errorMessage = "Anmeldung fehlgeschlagen" - isLoggedIn = false - } - - isLoading = false - } - - @MainActor - func logout() async { - do { - 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 - } - - var canLogin: Bool { - !username.isEmpty && !password.isEmpty - } - - // Expose loginUseCase for testing purposes - var loginUseCase: LoginUseCase { - return _loginUseCase - } -} - - -enum Theme: String, CaseIterable { - case system = "system" - case light = "light" - case dark = "dark" - - var displayName: String { - switch self { - case .system: return "System" - case .light: return "Hell" - case .dark: return "Dunkel" - } - } -} diff --git a/readeck/UI/readeckApp.swift b/readeck/UI/readeckApp.swift index 1b319b9..0cd5712 100644 --- a/readeck/UI/readeckApp.swift +++ b/readeck/UI/readeckApp.swift @@ -20,7 +20,8 @@ struct readeckApp: App { MainTabView() .environment(\.managedObjectContext, persistenceController.container.viewContext) } else { - SettingsContainerView() + SettingsServerView() + .padding() } } .onOpenURL { url in