Add bookmark labels functionality
- Add BookmarkLabel model and DTO - Create LabelsRepository and PLabelsRepository protocol - Add GetLabelsUseCase for fetching labels - Update BookmarkMapper to handle labels - Add LabelsView and LabelsViewModel for UI - Update BookmarksView and BookmarkLabelsView to display labels - Add green2 color asset for labels - Update API and repository layers to support labels
This commit is contained in:
parent
d2e8228903
commit
3e6db364b5
38
readeck/Assets.xcassets/green2.colorset/Contents.json
Normal file
38
readeck/Assets.xcassets/green2.colorset/Contents.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
18
readeck/Data/API/DTOs/BookmarkLabelDto.swift
Normal file
18
readeck/Data/API/DTOs/BookmarkLabelDto.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
14
readeck/Data/Repository/LabelsRepository.swift
Normal file
14
readeck/Data/Repository/LabelsRepository.swift
Normal file
@ -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() }
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ struct BookmarkDetail {
|
||||
let labels: [String]
|
||||
let thumbnailUrl: String
|
||||
let imageUrl: String
|
||||
var content: String?
|
||||
}
|
||||
|
||||
extension BookmarkDetail {
|
||||
|
||||
15
readeck/Domain/Model/BookmarkLabel.swift
Normal file
15
readeck/Domain/Model/BookmarkLabel.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
5
readeck/Domain/Protocols/PLabelsRepository.swift
Normal file
5
readeck/Domain/Protocols/PLabelsRepository.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
protocol PLabelsRepository {
|
||||
func getLabels() async throws -> [BookmarkLabel]
|
||||
}
|
||||
@ -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
|
||||
|
||||
13
readeck/Domain/UseCase/GetLabelsUseCase.swift
Normal file
13
readeck/Domain/UseCase/GetLabelsUseCase.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,7 @@ struct BookmarkLabelsView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
|
||||
Button("Abbrechen") {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@ -15,8 +15,16 @@ 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<Bookmark?>, tag: String? = nil) {
|
||||
self.state = state
|
||||
self.type = type
|
||||
self._selectedBookmark = selectedBookmark
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
// MARK: Environments
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<AnyCancellable>()
|
||||
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"
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
40
readeck/UI/Labels/LabelsView.swift
Normal file
40
readeck/UI/Labels/LabelsView.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
readeck/UI/Labels/LabelsViewModel.swift
Normal file
23
readeck/UI/Labels/LabelsViewModel.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -86,7 +86,7 @@ struct PadSidebarView: View {
|
||||
case .pictures:
|
||||
BookmarksView(state: .all, type: [.photo], selectedBookmark: $selectedBookmark)
|
||||
case .tags:
|
||||
Text("Tags")
|
||||
LabelsView()
|
||||
}
|
||||
}
|
||||
.navigationTitle(selectedTab.label)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user