diff --git a/readeck/Assets.xcassets/green2.colorset/Contents.json b/readeck/Assets.xcassets/green2.colorset/Contents.json new file mode 100644 index 0000000..f5a0250 --- /dev/null +++ b/readeck/Assets.xcassets/green2.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x5B", + "green" : "0x4D", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x5B", + "green" : "0x4D", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/readeck/Data/API/API.swift b/readeck/Data/API/API.swift index d77d1b5..ccb37ba 100644 --- a/readeck/Data/API/API.swift +++ b/readeck/Data/API/API.swift @@ -10,13 +10,14 @@ import Foundation protocol PAPI { var tokenProvider: TokenProvider { get } func login(endpoint: String, username: String, password: String) async throws -> UserDto - func getBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?) async throws -> BookmarksPageDto + func getBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?, tag: String?) async throws -> BookmarksPageDto func getBookmark(id: String) async throws -> BookmarkDetailDto func getBookmarkArticle(id: String) async throws -> String func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws func deleteBookmark(id: String) async throws func searchBookmarks(search: String) async throws -> BookmarksPageDto + func getBookmarkLabels() async throws -> [BookmarkLabelDto] } class API: PAPI { @@ -180,12 +181,12 @@ class API: PAPI { return try JSONDecoder().decode(UserDto.self, from: data) } - func getBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil) async throws -> BookmarksPageDto { + func getBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil, tag: String? = nil) async throws -> BookmarksPageDto { var endpoint = "/api/bookmarks" var queryItems: [URLQueryItem] = [] // Query-Parameter basierend auf State hinzufügen - if let state = state { + if let state { switch state { case .unread: queryItems.append(URLQueryItem(name: "is_archived", value: "false")) @@ -199,24 +200,28 @@ class API: PAPI { } } - if let limit = limit { + if let limit { queryItems.append(URLQueryItem(name: "limit", value: "\(limit)")) } - if let offset = offset { + if let offset { queryItems.append(URLQueryItem(name: "offset", value: "\(offset)")) } - if let search = search { + if let search { queryItems.append(URLQueryItem(name: "search", value: search)) } // type-Parameter als Array von BookmarkType - if let type = type, !type.isEmpty { + if let type, !type.isEmpty { for t in type { queryItems.append(URLQueryItem(name: "type", value: t.rawValue)) } } + if let tag { + queryItems.append(URLQueryItem(name: "labels", value: tag)) + } + if !queryItems.isEmpty { let queryString = queryItems.map { "\($0.name)=\($0.value ?? "")" }.joined(separator: "&") endpoint += "?\(queryString)" @@ -350,6 +355,13 @@ class API: PAPI { links: links ) } + + func getBookmarkLabels() async throws -> [BookmarkLabelDto] { + return try await makeJSONRequest( + endpoint: "/api/bookmarks/labels", + responseType: [BookmarkLabelDto].self + ) + } } enum HTTPMethod: String { diff --git a/readeck/Data/API/DTOs/BookmarkLabelDto.swift b/readeck/Data/API/DTOs/BookmarkLabelDto.swift new file mode 100644 index 0000000..160ee0e --- /dev/null +++ b/readeck/Data/API/DTOs/BookmarkLabelDto.swift @@ -0,0 +1,18 @@ +import Foundation + +struct BookmarkLabelDto: Codable, Identifiable { + var id: String { get { href } } + let name: String + let count: Int + let href: String + + enum CodingKeys: String, CodingKey { + case name, count, href + } + + init(name: String, count: Int, href: String) { + self.name = name + self.count = count + self.href = href + } +} diff --git a/readeck/Data/Mappers/BookmarkMapper.swift b/readeck/Data/Mappers/BookmarkMapper.swift index 6bb2db3..8bee83e 100644 --- a/readeck/Data/Mappers/BookmarkMapper.swift +++ b/readeck/Data/Mappers/BookmarkMapper.swift @@ -71,3 +71,9 @@ extension ImageResourceDto { return ImageResource(src: src, height: height, width: width) } } + +extension BookmarkLabelDto { + func toDomain() -> BookmarkLabel { + return BookmarkLabel(name: self.name, count: self.count, href: self.href) + } +} diff --git a/readeck/Data/Repository/BookmarksRepository.swift b/readeck/Data/Repository/BookmarksRepository.swift index d5b02a7..b52bbde 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?, type: [BookmarkType]?) async throws -> BookmarksPage + func fetchBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?, tag: String?) async throws -> BookmarksPage func fetchBookmark(id: String) async throws -> BookmarkDetail func fetchBookmarkArticle(id: String) async throws -> String func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String @@ -17,8 +17,8 @@ class BookmarksRepository: PBookmarksRepository { self.api = api } - 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) + func fetchBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil, tag: String? = nil) async throws -> BookmarksPage { + let bookmarkDtos = try await api.getBookmarks(state: state, limit: limit, offset: offset, search: search, type: type, tag: tag) return bookmarkDtos.toDomain() } diff --git a/readeck/Data/Repository/LabelsRepository.swift b/readeck/Data/Repository/LabelsRepository.swift new file mode 100644 index 0000000..cb39dad --- /dev/null +++ b/readeck/Data/Repository/LabelsRepository.swift @@ -0,0 +1,14 @@ +import Foundation + +class LabelsRepository: PLabelsRepository { + private let api: PAPI + + init(api: PAPI) { + self.api = api + } + + func getLabels() async throws -> [BookmarkLabel] { + let dtos = try await api.getBookmarkLabels() + return dtos.map { $0.toDomain() } + } +} diff --git a/readeck/Domain/Model/BookmarkDetail.swift b/readeck/Domain/Model/BookmarkDetail.swift index 37c37fb..c2ecb67 100644 --- a/readeck/Domain/Model/BookmarkDetail.swift +++ b/readeck/Domain/Model/BookmarkDetail.swift @@ -17,6 +17,7 @@ struct BookmarkDetail { let labels: [String] let thumbnailUrl: String let imageUrl: String + var content: String? } extension BookmarkDetail { diff --git a/readeck/Domain/Model/BookmarkLabel.swift b/readeck/Domain/Model/BookmarkLabel.swift new file mode 100644 index 0000000..86b7d3a --- /dev/null +++ b/readeck/Domain/Model/BookmarkLabel.swift @@ -0,0 +1,15 @@ +import Foundation + +struct BookmarkLabel: Identifiable, Equatable, Hashable { + let id: String // kann href oder name sein, je nach Backend + let name: String + let count: Int + let href: String + + init(name: String, count: Int, href: String) { + self.name = name + self.count = count + self.href = href + self.id = href // oder name, je nach Backend-Eindeutigkeit + } +} diff --git a/readeck/Domain/Protocols/PLabelsRepository.swift b/readeck/Domain/Protocols/PLabelsRepository.swift new file mode 100644 index 0000000..e172af9 --- /dev/null +++ b/readeck/Domain/Protocols/PLabelsRepository.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol PLabelsRepository { + func getLabels() async throws -> [BookmarkLabel] +} diff --git a/readeck/Domain/UseCase/GetBookmarksUseCase.swift b/readeck/Domain/UseCase/GetBookmarksUseCase.swift index 6681fb0..027b259 100644 --- a/readeck/Domain/UseCase/GetBookmarksUseCase.swift +++ b/readeck/Domain/UseCase/GetBookmarksUseCase.swift @@ -7,8 +7,8 @@ class GetBookmarksUseCase { self.repository = repository } - 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) + func execute(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil, tag: String? = nil) async throws -> BookmarksPage { + var allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset, search: search, type: type, tag: tag) if let state = state { allBookmarks.bookmarks = allBookmarks.bookmarks.filter { bookmark in diff --git a/readeck/Domain/UseCase/GetLabelsUseCase.swift b/readeck/Domain/UseCase/GetLabelsUseCase.swift new file mode 100644 index 0000000..663fc65 --- /dev/null +++ b/readeck/Domain/UseCase/GetLabelsUseCase.swift @@ -0,0 +1,13 @@ +import Foundation + +class GetLabelsUseCase { + private let labelsRepository: PLabelsRepository + + init(labelsRepository: PLabelsRepository) { + self.labelsRepository = labelsRepository + } + + func execute() async throws -> [BookmarkLabel] { + return try await labelsRepository.getLabels() + } +} diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift index afcad23..3bba474 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift @@ -30,6 +30,7 @@ struct BookmarkLabelsView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { + Button("Abbrechen") { dismiss() } @@ -172,4 +173,4 @@ struct LabelChip: View { #Preview { BookmarkLabelsView(bookmarkId: "test-id", initialLabels: ["wichtig", "arbeit", "persönlich"]) -} \ No newline at end of file +} diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift index db554b6..32e7ebb 100644 --- a/readeck/UI/Bookmarks/BookmarksView.swift +++ b/readeck/UI/Bookmarks/BookmarksView.swift @@ -15,9 +15,17 @@ struct BookmarksView: View { let state: BookmarkState let type: [BookmarkType] - @Binding var selectedBookmark: Bookmark? - + let tag: String? + + // MARK: Initializer + init(state: BookmarkState, type: [BookmarkType], selectedBookmark: Binding, tag: String? = nil) { + self.state = state + self.type = type + self._selectedBookmark = selectedBookmark + self.tag = tag + } + // MARK: Environments @Environment(\.horizontalSizeClass) var horizontalSizeClass @@ -147,7 +155,7 @@ struct BookmarksView: View { }*/ .onAppear { Task { - await viewModel.loadBookmarks(state: state, type: type) + await viewModel.loadBookmarks(state: state, type: type, tag: tag) } } .onChange(of: showingAddBookmark) { oldValue, newValue in diff --git a/readeck/UI/Bookmarks/BookmarksViewModel.swift b/readeck/UI/Bookmarks/BookmarksViewModel.swift index 0af2ef6..092bf97 100644 --- a/readeck/UI/Bookmarks/BookmarksViewModel.swift +++ b/readeck/UI/Bookmarks/BookmarksViewModel.swift @@ -13,11 +13,13 @@ class BookmarksViewModel { var errorMessage: String? var currentState: BookmarkState = .unread var currentType = [BookmarkType.article] + var currentTag: String? = nil var showingAddBookmarkFromShare = false var shareURL = "" var shareTitle = "" + private var cancellables = Set() private var limit = 20 private var offset = 0 @@ -74,11 +76,12 @@ class BookmarksViewModel { } @MainActor - func loadBookmarks(state: BookmarkState = .unread, type: [BookmarkType] = [.article]) async { + func loadBookmarks(state: BookmarkState = .unread, type: [BookmarkType] = [.article], tag: String? = nil) async { isLoading = true errorMessage = nil currentState = state currentType = type + currentTag = tag offset = 0 // Offset zurücksetzen hasMoreData = true // Pagination zurücksetzen @@ -89,10 +92,11 @@ class BookmarksViewModel { limit: limit, offset: offset, search: searchQuery, - type: type + type: type, + tag: tag ) bookmarks = newBookmarks - hasMoreData = newBookmarks.bookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind + hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages // Prüfen, ob weitere Daten verfügbar sind } catch { errorMessage = "Fehler beim Laden der Bookmarks" bookmarks = nil @@ -114,9 +118,10 @@ class BookmarksViewModel { state: currentState, limit: limit, offset: offset, - type: currentType) - bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks) // Neue Bookmarks hinzufügen - hasMoreData = newBookmarks.bookmarks.count == limit // Prüfen, + type: currentType, + tag: currentTag) + bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks) + hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages } catch { errorMessage = "Fehler beim Nachladen der Bookmarks" } diff --git a/readeck/UI/DefaultUseCaseFactory.swift b/readeck/UI/DefaultUseCaseFactory.swift index 2a346f4..480fb87 100644 --- a/readeck/UI/DefaultUseCaseFactory.swift +++ b/readeck/UI/DefaultUseCaseFactory.swift @@ -15,6 +15,8 @@ protocol UseCaseFactory { func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase func makeAddLabelsToBookmarkUseCase() -> AddLabelsToBookmarkUseCase func makeRemoveLabelsFromBookmarkUseCase() -> RemoveLabelsFromBookmarkUseCase + func makeGetLabelsUseCase() -> GetLabelsUseCase + func makeAddTextToSpeechQueueUseCase() -> AddTextToSpeechQueueUseCase } class DefaultUseCaseFactory: UseCaseFactory { @@ -88,4 +90,14 @@ class DefaultUseCaseFactory: UseCaseFactory { func makeRemoveLabelsFromBookmarkUseCase() -> RemoveLabelsFromBookmarkUseCase { return RemoveLabelsFromBookmarkUseCase(repository: bookmarksRepository) } + + func makeGetLabelsUseCase() -> GetLabelsUseCase { + let api = API(tokenProvider: CoreDataTokenProvider()) + let labelsRepository = LabelsRepository(api: api) + return GetLabelsUseCase(labelsRepository: labelsRepository) + } + + func makeAddTextToSpeechQueueUseCase() -> AddTextToSpeechQueueUseCase { + return AddTextToSpeechQueueUseCase() + } } diff --git a/readeck/UI/Labels/LabelsView.swift b/readeck/UI/Labels/LabelsView.swift new file mode 100644 index 0000000..5be0621 --- /dev/null +++ b/readeck/UI/Labels/LabelsView.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct LabelsView: View { + @State var viewModel = LabelsViewModel() + @State private var selectedTag: String? = nil + @State private var selectedBookmark: Bookmark? = nil + + var body: some View { + VStack(alignment: .leading) { + if viewModel.isLoading { + ProgressView() + } else if let errorMessage = viewModel.errorMessage { + Text("Fehler: \(errorMessage)") + .foregroundColor(.red) + } else { + List { + ForEach(viewModel.labels, id: \.href) { label in + + NavigationLink { + BookmarksView(state: .all, type: [], selectedBookmark: .constant(nil), tag: label.name) + .navigationTitle("\(label.name) (\(label.count))") + } label: { + HStack { + Text(label.name) + Spacer() + Text("\(label.count)") + .foregroundColor(.secondary) + } + } + } + } + } + } + .onAppear { + Task { + await viewModel.loadLabels() + } + } + } +} diff --git a/readeck/UI/Labels/LabelsViewModel.swift b/readeck/UI/Labels/LabelsViewModel.swift new file mode 100644 index 0000000..1f2c188 --- /dev/null +++ b/readeck/UI/Labels/LabelsViewModel.swift @@ -0,0 +1,23 @@ +import Foundation +import Observation + +@Observable +class LabelsViewModel { + private let getLabelsUseCase = DefaultUseCaseFactory.shared.makeGetLabelsUseCase() + + var labels: [BookmarkLabel] = [] + var isLoading = false + var errorMessage: String? = nil + + @MainActor + func loadLabels() async { + isLoading = true + errorMessage = nil + do { + labels = try await getLabelsUseCase.execute() + } catch { + errorMessage = "Fehler beim Laden der Labels" + } + isLoading = false + } +} diff --git a/readeck/UI/Menu/PadSidebarView.swift b/readeck/UI/Menu/PadSidebarView.swift index bd87179..ceca7cd 100644 --- a/readeck/UI/Menu/PadSidebarView.swift +++ b/readeck/UI/Menu/PadSidebarView.swift @@ -41,7 +41,7 @@ struct PadSidebarView: View { .listRowBackground(Color(R.color.menu_sidebar_bg)) } - } + } } .listRowBackground(Color(R.color.menu_sidebar_bg)) .background(Color(R.color.menu_sidebar_bg)) @@ -86,7 +86,7 @@ struct PadSidebarView: View { case .pictures: BookmarksView(state: .all, type: [.photo], selectedBookmark: $selectedBookmark) case .tags: - Text("Tags") + LabelsView() } } .navigationTitle(selectedTab.label) diff --git a/readeck/UI/Settings/SettingsServerView.swift b/readeck/UI/Settings/SettingsServerView.swift index 3bac051..d7a3bde 100644 --- a/readeck/UI/Settings/SettingsServerView.swift +++ b/readeck/UI/Settings/SettingsServerView.swift @@ -126,7 +126,7 @@ struct SettingsServerView: View { Button("Debug-Anmeldung") { viewModel.username = "admin" viewModel.password = "Diggah123" - viewModel.endpoint = "https://keep.mnk.any64.de" + viewModel.endpoint = "https://readeck.mnk.any64.de" } .font(.caption) .foregroundColor(.secondary)