diff --git a/.build/arm64-apple-macosx/debug/index/db/v13/p4743--3eba9d/data.mdb b/.build/arm64-apple-macosx/debug/index/db/v13/p4743--3eba9d/data.mdb deleted file mode 100644 index aa89ad8..0000000 Binary files a/.build/arm64-apple-macosx/debug/index/db/v13/p4743--3eba9d/data.mdb and /dev/null differ diff --git a/.build/arm64-apple-macosx/debug/index/db/v13/p4743--3eba9d/lock.mdb b/.build/arm64-apple-macosx/debug/index/db/v13/p4743--3eba9d/lock.mdb deleted file mode 100644 index 493a42f..0000000 Binary files a/.build/arm64-apple-macosx/debug/index/db/v13/p4743--3eba9d/lock.mdb and /dev/null differ diff --git a/.build/workspace-state.json b/.build/workspace-state.json index aa952d4..d79d44d 100644 --- a/.build/workspace-state.json +++ b/.build/workspace-state.json @@ -174,7 +174,10 @@ }, "subpath" : "Yams" } + ], + "prebuilts" : [ + ] }, - "version" : 6 + "version" : 7 } \ No newline at end of file diff --git a/readeck.xcodeproj/project.pbxproj b/readeck.xcodeproj/project.pbxproj index 7a89528..d8ea7db 100644 --- a/readeck.xcodeproj/project.pbxproj +++ b/readeck.xcodeproj/project.pbxproj @@ -76,9 +76,7 @@ 5DCD48B72DFB44D600AC7FB6 /* Exceptions for "readeck" folder in "URLShare" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - Data/CoreData/CoreDataManager.swift, Domain/Model/Bookmark.swift, - readeck.xcdatamodeld, ); target = 5D2B7FAE2DFA27A400EBDB2B /* URLShare */; }; diff --git a/readeck.xcodeproj/xcshareddata/xcschemes/URLShare.xcscheme b/readeck.xcodeproj/xcshareddata/xcschemes/URLShare.xcscheme new file mode 100644 index 0000000..7f02c2d --- /dev/null +++ b/readeck.xcodeproj/xcshareddata/xcschemes/URLShare.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/readeck.xcodeproj/xcshareddata/xcschemes/readeck.xcscheme b/readeck.xcodeproj/xcshareddata/xcschemes/readeck.xcscheme new file mode 100644 index 0000000..4923848 --- /dev/null +++ b/readeck.xcodeproj/xcshareddata/xcschemes/readeck.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/readeck.xcodeproj/xcuserdata/ilyashallak.xcuserdatad/xcschemes/xcschememanagement.plist b/readeck.xcodeproj/xcuserdata/ilyashallak.xcuserdatad/xcschemes/xcschememanagement.plist index 6d0fa5f..17a7b0b 100644 --- a/readeck.xcodeproj/xcuserdata/ilyashallak.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/readeck.xcodeproj/xcuserdata/ilyashallak.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,12 +12,25 @@ URLShare.xcscheme_^#shared#^_ orderHint - 1 + 0 readeck.xcscheme_^#shared#^_ orderHint - 0 + 1 + + + SuppressBuildableAutocreation + + 5D2B7FAE2DFA27A400EBDB2B + + primary + + + 5D45F9C72DF858680048D5B8 + + primary + diff --git a/readeck/Data/API/API.swift b/readeck/Data/API/API.swift index 1225feb..7684fa8 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?) async throws -> [BookmarkDto] + func getBookmarks(state: BookmarkState?, updatedSince: Date?, limit: Int?, offset: Int?) async throws -> [BookmarkDto] func getBookmark(id: String) async throws -> BookmarkDetailDto func getBookmarkArticle(id: String) async throws -> String func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto @@ -131,21 +131,46 @@ class API: PAPI { return userDto } - func getBookmarks(state: BookmarkState? = nil) async throws -> [BookmarkDto] { + func getBookmarks(state: BookmarkState? = nil, updatedSince: Date? = nil, limit: Int? = nil, offset: Int? = nil) async throws -> [BookmarkDto] { var endpoint = "/api/bookmarks" + var queryParams: [String] = [] // Query-Parameter basierend auf State hinzufügen if let state = state { switch state { case .unread: - endpoint += "?is_archived=false&is_marked=false" + queryParams.append("is_archived=false") + queryParams.append("is_marked=false") case .favorite: - endpoint += "?is_marked=true" + queryParams.append("is_marked=true") case .archived: - endpoint += "?is_archived=true" + queryParams.append("is_archived=true") } } + if let updatedSince = updatedSince { + let dateFormatter = ISO8601DateFormatter() + let updatedSinceString = dateFormatter.string(from: updatedSince) + queryParams.append("updated_since=\(updatedSinceString)") + } + + /* + // Limit Parameter + if let limit = limit { + queryParams.append("limit=\(limit)") + } + + // Offset Parameter + if let offset = offset { + queryParams.append("offset=\(offset)") + } + + // Query-String zusammenbauen + if !queryParams.isEmpty { + endpoint += "?" + queryParams.joined(separator: "&") + } + */ + return try await makeJSONRequest( endpoint: endpoint, responseType: [BookmarkDto].self diff --git a/readeck/Data/API/DTOs/BookmarkDto.swift b/readeck/Data/API/DTOs/BookmarkDto.swift index 0da39ba..1593f3a 100644 --- a/readeck/Data/API/DTOs/BookmarkDto.swift +++ b/readeck/Data/API/DTOs/BookmarkDto.swift @@ -62,5 +62,3 @@ struct ImageResourceDto: Codable { let height: Int let width: Int } - - diff --git a/readeck/Data/Mappers/BookmarkEntityMapper.swift b/readeck/Data/Mappers/BookmarkEntityMapper.swift new file mode 100644 index 0000000..a13d681 --- /dev/null +++ b/readeck/Data/Mappers/BookmarkEntityMapper.swift @@ -0,0 +1,213 @@ +import Foundation +import CoreData + + +// MARK: - DTO -> Entity + +extension BookmarkDto { + func toEntity(context: NSManagedObjectContext) -> BookmarkEntity { + let entity = BookmarkEntity(context: context) + entity.title = self.title + entity.url = self.url + entity.authors = self.authors.first + entity.desc = self.description + entity.created = self.created + + entity.siteName = self.siteName + entity.site = self.site + entity.authors = self.authors.first // TODO: support multiple authors + entity.published = self.published + entity.created = self.created + entity.update = self.updated + entity.readingTime = Int16(self.readingTime ?? 0) + entity.readProgress = Int16(self.readProgress) + entity.wordCount = Int64(self.wordCount ?? 0) + entity.isArchived = self.isArchived + entity.isMarked = self.isMarked + entity.hasArticle = self.hasArticle + entity.loaded = self.loaded + entity.hasDeleted = self.isDeleted + entity.documentType = self.documentType + entity.href = self.href + entity.lang = self.lang + entity.textDirection = self.textDirection + entity.type = self.type + entity.state = Int16(self.state) + + // entity.resources = self.resources.toEntity(context: context) + + return entity + } +} + +extension BookmarkResourcesDto { + func toEntity(context: NSManagedObjectContext) -> BookmarkResourcesEntity { + let entity = BookmarkResourcesEntity(context: context) + + entity.article = self.article?.toEntity(context: context) + entity.icon = self.icon?.toEntity(context: context) + entity.image = self.image?.toEntity(context: context) + entity.log = self.log?.toEntity(context: context) + entity.props = self.props?.toEntity(context: context) + entity.thumbnail = self.thumbnail?.toEntity(context: context) + + return entity + } +} + +extension ImageResourceDto { + func toEntity(context: NSManagedObjectContext) -> ImageResourceEntity { + let entity = ImageResourceEntity(context: context) + entity.src = self.src + entity.width = Int64(self.width) + entity.height = Int64(self.height) + return entity + } +} + +extension ResourceDto { + func toEntity(context: NSManagedObjectContext) -> ResourceEntity { + let entity = ResourceEntity(context: context) + entity.src = self.src + return entity + } +} + +// ------------------------------------------------ + +// MARK: - BookmarkEntity to Domain Mapping +extension BookmarkEntity { + +} + +// MARK: - Domain to BookmarkEntity Mapping +extension Bookmark { + func toEntity(context: NSManagedObjectContext) -> BookmarkEntity { + let entity = BookmarkEntity(context: context) + entity.populateFrom(bookmark: self) + return entity + } + + func updateEntity(_ entity: BookmarkEntity) { + entity.populateFrom(bookmark: self) + } +} + +extension Resource { + func toEntity(context: NSManagedObjectContext) -> ResourceEntity { + let entity = ResourceEntity(context: context) + entity.populateFrom(resource: self) + return entity + } +} + +// MARK: - Private Helper Methods +private extension BookmarkEntity { + func populateFrom(bookmark: Bookmark) { + self.id = bookmark.id + self.title = bookmark.title + self.url = bookmark.url + self.desc = bookmark.description + self.siteName = bookmark.siteName + self.site = bookmark.site + self.authors = bookmark.authors.first // TODO: support multiple authors + self.published = bookmark.published + self.created = bookmark.created + self.update = bookmark.updated + self.readingTime = Int16(bookmark.readingTime ?? 0) + self.readProgress = Int16(bookmark.readProgress) + self.wordCount = Int64(bookmark.wordCount ?? 0) + self.isArchived = bookmark.isArchived + self.isMarked = bookmark.isMarked + self.hasArticle = bookmark.hasArticle + self.loaded = bookmark.loaded + self.hasDeleted = bookmark.isDeleted + self.documentType = bookmark.documentType + self.href = bookmark.href + self.lang = bookmark.lang + self.textDirection = bookmark.textDirection + self.type = bookmark.type + self.state = Int16(bookmark.state) + } +} + +// MARK: - BookmarkState Mapping +private extension BookmarkState { + static func fromRawValue(_ value: Int) -> BookmarkState { + switch value { + case 0: return .unread + case 1: return .favorite + case 2: return .archived + default: return .unread + } + } +} + +private extension BookmarkResourcesEntity { + func populateFrom(bookmarkResources: BookmarkResources) { + + } +} + +private extension ImageResourceEntity { + func populateFrom(imageResource: ImageResource) { + self.src = imageResource.src + self.height = Int64(imageResource.height) + self.width = Int64(imageResource.width) + } +} + +private extension ResourceEntity { + func populateFrom(resource: Resource) { + self.src = resource.src + } +} + +// MARK: - Date Conversion Helpers +private extension String { + func toDate() -> Date? { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.date(from: self) ?? + ISO8601DateFormatter().date(from: self) + } +} + +private extension Date { + func toISOString() -> String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.string(from: self) + } +} + +// MARK: - Array Mapping Extensions +extension Array where Element == BookmarkEntity { + func toDomain() -> [Bookmark] { + return [] // self.map { $0.toDomain() } + } +} + +extension Array where Element == Bookmark { + func toEntities(context: NSManagedObjectContext) -> [BookmarkEntity] { + return self.map { $0.toEntity(context: context) } + } +} +/* +extension BookmarkEntity { + func toDomain() -> Bookmark { + return Bookmark(id: id ?? "", title: title ?? "", url: url!, href: href ?? "", description: description, authors: [authors ?? ""], created: created ?? "", published: published, updated: update!, siteName: siteName ?? "", site: site!, readingTime: Int(readingTime), wordCount: Int(wordCount), hasArticle: hasArticle, isArchived: isArchived, isDeleted: isDeleted, isMarked: isMarked, labels: [], lang: lang, loaded: loaded, readProgress: Int(readProgress), documentType: documentType ?? "", state: Int(state), textDirection: textDirection ?? "", type: type ?? "", resources: resources.toDomain()) + ) + } +} + +extension BookmarkResourcesEntity { + func toDomain() -> BookmarkResources { + return BookmarkResources(article: ar, icon: <#T##ImageResource?#>, image: <#T##ImageResource?#>, log: <#T##Resource?#>, props: <#T##Resource?#>, thumbnail: <#T##ImageResource?#> + } +} + +extension ImageResourceEntity { + +} +*/ diff --git a/readeck/Data/Repository/BookmarkSyncRepository.swift b/readeck/Data/Repository/BookmarkSyncRepository.swift new file mode 100644 index 0000000..38ae536 --- /dev/null +++ b/readeck/Data/Repository/BookmarkSyncRepository.swift @@ -0,0 +1,103 @@ +import Foundation +import CoreData + +class BookmarkSyncRepository { + private let userDefaults = UserDefaults.standard + private let lastSyncTimestampKey = "lastBookmarkSyncTimestamp" + + private var api: PAPI + + private let coreDataManager = CoreDataManager.shared + + init(api: PAPI) { + self.api = api + } + + func resetBookmarks() async throws { + let context = coreDataManager.context + + try await context.perform { + // Fetch Request für alle BookmarkEntity Objekte + let fetchRequest: NSFetchRequest = BookmarkEntity.fetchRequest() + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + + do { + // Batch Delete ausführen + try context.execute(deleteRequest) + + // Context speichern + try context.save() + + // UserDefaults zurücksetzen + self.userDefaults.removeObject(forKey: self.lastSyncTimestampKey) + + print("Alle Bookmarks wurden erfolgreich gelöscht") + } catch { + print("Fehler beim Löschen der Bookmarks: \(error)") + throw error + } + } + } + + func syncBookmarks() async throws { + + try? await resetBookmarks() + + // Letzten Sync-Timestamp aus UserDefaults abrufen + let lastSyncTimestamp = userDefaults.double(forKey: lastSyncTimestampKey) + let updatedSince = lastSyncTimestamp > 0 ? Date(timeIntervalSince1970: lastSyncTimestamp) : nil + + // Bookmarks vom Server abrufen + let bookmarks = try await fetchBookmarks(updatedSince: updatedSince) + + // Batch-Insert durchführen + try await batchInsertBookmarks(bookmarks) + + // Aktuellen Timestamp speichern + userDefaults.set(Date().timeIntervalSince1970, forKey: lastSyncTimestampKey) + } + + private func fetchBookmarks(updatedSince: Date?) async throws -> [Bookmark] { + let bookmarks = try await api.getBookmarks(state: .unread, updatedSince: updatedSince, limit: nil, offset: nil) + return bookmarks.map { + $0.toDomain() + } + } + + private func batchInsertBookmarks(_ bookmarks: [Bookmark]) async throws { + let context = CoreDataManager.shared.context + + // Existierende URLs aus Core Data abrufen + let existingURLs = try await getExistingBookmarkURLs() + + // Nur neue Bookmarks filtern + let newBookmarks = bookmarks.filter { !existingURLs.contains($0.url) } + + // Batch-Insert + await context.perform { + for bookmark in newBookmarks { + newBookmarks.forEach { + _ = $0.toEntity(context: context) + } + } + + do { + try context.save() + } catch { + print("Fehler beim Speichern der Bookmarks: \(error)") + } + } + } + + private func getExistingBookmarkURLs() async throws -> Set { + let context = CoreDataManager.shared.context + let request: NSFetchRequest = BookmarkEntity.fetchRequest() + request.propertiesToFetch = ["url"] + request.resultType = .dictionaryResultType + + return try await context.perform { + let results = try context.fetch(request) as! [[String: Any]] + return Set(results.compactMap { $0["url"] as? String }) + } + } +} diff --git a/readeck/Data/Repository/BookmarksRepository.swift b/readeck/Data/Repository/BookmarksRepository.swift index 421f76d..c33044a 100644 --- a/readeck/Data/Repository/BookmarksRepository.swift +++ b/readeck/Data/Repository/BookmarksRepository.swift @@ -1,23 +1,32 @@ import Foundation +import CoreData protocol PBookmarksRepository { - func fetchBookmarks(state: BookmarkState?) async throws -> [Bookmark] + func fetchBookmarks(state: BookmarkState?, limit: Int?, offset: Int?) async throws -> [Bookmark] func fetchBookmark(id: String) async throws -> BookmarkDetail func fetchBookmarkArticle(id: String) async throws -> String func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws func deleteBookmark(id: String) async throws + func syncBookmarks() async throws } class BookmarksRepository: PBookmarksRepository { private var api: PAPI + + private let coreDataManager = CoreDataManager.shared init(api: PAPI) { self.api = api } - func fetchBookmarks(state: BookmarkState? = nil) async throws -> [Bookmark] { - let bookmarkDtos = try await api.getBookmarks(state: state) + func syncBookmarks() async throws { + + + } + + func fetchBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil) async throws -> [Bookmark] { + let bookmarkDtos = try await api.getBookmarks(state: state, updatedSince: nil, limit: limit, offset: offset) return bookmarkDtos.map { $0.toDomain() } } diff --git a/readeck/Domain/UseCase/GetBookmarksUseCase.swift b/readeck/Domain/UseCase/GetBookmarksUseCase.swift index 2602ae8..e22c718 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) async throws -> [Bookmark] { - let allBookmarks = try await repository.fetchBookmarks(state: state) + func execute(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil) async throws -> [Bookmark] { + let allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset) // Fallback-Filterung auf Client-Seite falls API keine Query-Parameter unterstützt if let state = state { diff --git a/readeck/Domain/UseCase/SyncBookmarksUseCase.swift b/readeck/Domain/UseCase/SyncBookmarksUseCase.swift index e69de29..adff5de 100644 --- a/readeck/Domain/UseCase/SyncBookmarksUseCase.swift +++ b/readeck/Domain/UseCase/SyncBookmarksUseCase.swift @@ -0,0 +1,19 @@ +// +// SyncBookmarksUseCase.swift +// readeck +// +// Created by Ilyas Hallak on 15.06.25. +// + +class SyncBookmarksUseCase { + + let bookmarkRepository: BookmarkSyncRepository + + init(bookmarkRepository: BookmarkSyncRepository) { + self.bookmarkRepository = bookmarkRepository + } + + func execute() async throws { + try await self.bookmarkRepository.syncBookmarks() + } +} diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift index 1340814..b4767e9 100644 --- a/readeck/UI/Bookmarks/BookmarkCardView.swift +++ b/readeck/UI/Bookmarks/BookmarkCardView.swift @@ -1,6 +1,199 @@ import SwiftUI import SafariServices +struct BookmarkEntityCardView: View { + let bookmark: BookmarkEntity + let currentState: BookmarkState + let onArchive: (BookmarkEntity) -> Void + let onDelete: (BookmarkEntity) -> Void + let onToggleFavorite: (BookmarkEntity) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + // Vorschaubild - verwende image oder thumbnail + AsyncImage(url: imageURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .fill(Color.gray.opacity(0.2)) + .overlay { + Image(systemName: "photo") + .foregroundColor(.gray) + } + } + .frame(height: 120) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + VStack(alignment: .leading, spacing: 4) { + // Titel + Text(bookmark.title ?? "") + .font(.headline) + .fontWeight(.semibold) + .lineLimit(2) + .multilineTextAlignment(.leading) + + // Meta-Info mit Datum + VStack(alignment: .leading, spacing: 4) { + HStack { + + // Veröffentlichungsdatum + if let publishedDate = formattedPublishedDate { + HStack { + Label(publishedDate, systemImage: "calendar") + Spacer() + } + + Spacer() // show spacer only if we have the published Date + } + + if bookmark.readingTime > 0 { + Label("\(bookmark.readingTime) min", systemImage: "clock") + } + } + + HStack { + if bookmark.siteName?.isEmpty == false { + Label(bookmark.siteName ?? "", systemImage: "globe") + } + } + HStack { + + Label("Original Seite öffnen", systemImage: "safari") + .onTapGesture { + SafariUtil.openInSafari(url: bookmark.url ?? "") + } + } + } + .font(.caption) + .foregroundColor(.secondary) + + // Progress Bar für Lesefortschritt + if bookmark.readProgress > 0 { + ProgressView(value: Double(bookmark.readProgress), total: 100) + .progressViewStyle(LinearProgressViewStyle()) + .frame(height: 4) + } + } + .padding(.horizontal, 12) + .padding(.bottom, 12) + } + .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) + Button { + onArchive(bookmark) + } label: { + if currentState == .archived { + Label("Wiederherstellen", systemImage: "tray.and.arrow.up") + } else { + Label("Archivieren", systemImage: "archivebox") + } + } + .tint(currentState == .archived ? .blue : .orange) + } + } + + // MARK: - Computed Properties + + private var formattedPublishedDate: String? { + guard let published = bookmark.published, !published.isEmpty else { + return nil + } + + // Prüfe auf Unix Epoch (1970-01-01) - bedeutet "kein Datum" + if published.contains("1970-01-01") { + return nil + } + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + + guard let date = formatter.date(from: published) else { + // Fallback ohne Millisekunden + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + guard let fallbackDate = formatter.date(from: published) else { + return nil + } + return formatDate(fallbackDate) + } + + return formatDate(date) + } + + private func formatDate(_ date: Date) -> String { + let now = Date() + let calendar = Calendar.current + + // Heute + if calendar.isDateInToday(date) { + let formatter = DateFormatter() + formatter.timeStyle = .short + return "Heute, \(formatter.string(from: date))" + } + + // Gestern + if calendar.isDateInYesterday(date) { + let formatter = DateFormatter() + formatter.timeStyle = .short + return "Gestern, \(formatter.string(from: date))" + } + + // Diese Woche + if calendar.isDate(date, equalTo: now, toGranularity: .weekOfYear) { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE, HH:mm" + return formatter.string(from: date) + } + + // Dieses Jahr + if calendar.isDate(date, equalTo: now, toGranularity: .year) { + let formatter = DateFormatter() + formatter.dateFormat = "d. MMM, HH:mm" + return formatter.string(from: date) + } + + // Andere Jahre + let formatter = DateFormatter() + formatter.dateFormat = "d. MMM yyyy" + return formatter.string(from: date) + } + + private var imageURL: URL? { + // Bevorzuge image, dann thumbnail, dann icon + if let imageUrl = bookmark.resources?.image?.src { + return URL(string: imageUrl) + } else if let thumbnailUrl = bookmark.resources?.thumbnail?.src { + return URL(string: thumbnailUrl) + } else if let iconUrl = bookmark.resources?.icon?.src { + return URL(string: iconUrl) + } + return nil + } +} + struct BookmarkCardView: View { let bookmark: Bookmark let currentState: BookmarkState diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift index 31c7438..c203cb4 100644 --- a/readeck/UI/Bookmarks/BookmarksView.swift +++ b/readeck/UI/Bookmarks/BookmarksView.swift @@ -10,54 +10,51 @@ struct BookmarksView: View { @State private var shareURL = "" @State private var shareTitle = "" + @FetchRequest(entity: BookmarkEntity.entity(), sortDescriptors: []) + private var bookmarks: FetchedResults + 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: { - selectedBookmarkId = bookmark.id - }) { - 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) - } + List { + ForEach(bookmarks) { bookmark in + Button(action: { + selectedBookmarkId = bookmark.id + }) { + BookmarkEntityCardView( + 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) + } + } + ) + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listStyle(.plain) + .refreshable { + await viewModel.refreshBookmarks() + } + .overlay { + if bookmarks.isEmpty { + ContentUnavailableView( + "Keine Bookmarks", + systemImage: "bookmark", + description: Text("Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden.") ) } - .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.") - ) } } } @@ -106,6 +103,7 @@ struct BookmarksView: View { } .task { await viewModel.loadBookmarks(state: state) + await viewModel.syncBookmarks() } .onChange(of: showingAddBookmark) { oldValue, newValue in // Refresh bookmarks when sheet is dismissed diff --git a/readeck/UI/Bookmarks/BookmarksViewModel.swift b/readeck/UI/Bookmarks/BookmarksViewModel.swift index 8adacae..74727f1 100644 --- a/readeck/UI/Bookmarks/BookmarksViewModel.swift +++ b/readeck/UI/Bookmarks/BookmarksViewModel.swift @@ -6,6 +6,7 @@ class BookmarksViewModel { private let getBooksmarksUseCase = DefaultUseCaseFactory.shared.makeGetBookmarksUseCase() private let updateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase() private let deleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase() + private let syncBookmarksUseCase = DefaultUseCaseFactory.shared.makeSyncBookmarksUseCase() var bookmarks: [Bookmark] = [] var isLoading = false @@ -54,14 +55,22 @@ class BookmarksViewModel { currentState = state do { - bookmarks = try await getBooksmarksUseCase.execute(state: state) + bookmarks = try await getBooksmarksUseCase.execute(state: state, limit: 100, offset: 0) } catch { errorMessage = "Fehler beim Laden der Bookmarks" - bookmarks = [] + bookmarks = [] } isLoading = false } + + func syncBookmarks() async { + do { + try await syncBookmarksUseCase.execute() + } catch { + errorMessage = "Fehler beim Synchronisieren der Bookmarks" + } + } @MainActor func refreshBookmarks() async { diff --git a/readeck/UI/DefaultUseCaseFactory.swift b/readeck/UI/DefaultUseCaseFactory.swift index 64f1044..bc9a755 100644 --- a/readeck/UI/DefaultUseCaseFactory.swift +++ b/readeck/UI/DefaultUseCaseFactory.swift @@ -10,6 +10,7 @@ protocol UseCaseFactory { func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase + func makeSyncBookmarksUseCase() -> SyncBookmarksUseCase } class DefaultUseCaseFactory: UseCaseFactory { @@ -17,6 +18,9 @@ class DefaultUseCaseFactory: UseCaseFactory { private lazy var api: PAPI = API(tokenProvider: tokenProvider) private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository) private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api) + + private lazy var bookmarkSyncRepository = BookmarkSyncRepository(api: api) + private let settingsRepository: PSettingsRepository = SettingsRepository() static let shared = DefaultUseCaseFactory() @@ -63,4 +67,8 @@ class DefaultUseCaseFactory: UseCaseFactory { func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase { return CreateBookmarkUseCase(repository: bookmarksRepository) } + + func makeSyncBookmarksUseCase() -> SyncBookmarksUseCase { + return SyncBookmarksUseCase(bookmarkRepository: bookmarkSyncRepository) + } } diff --git a/readeck/UI/readeckApp.swift b/readeck/UI/readeckApp.swift index 6c2dd3e..9151f62 100644 --- a/readeck/UI/readeckApp.swift +++ b/readeck/UI/readeckApp.swift @@ -9,12 +9,12 @@ import SwiftUI @main struct readeckApp: App { - let persistenceController = PersistenceController.shared + let persistenceController = CoreDataManager.shared var body: some Scene { WindowGroup { MainTabView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environment(\.managedObjectContext, persistenceController.persistentContainer.viewContext) .onOpenURL { url in handleIncomingURL(url) } diff --git a/readeck/readeck.xcdatamodeld/readeck.xcdatamodel/contents b/readeck/readeck.xcdatamodeld/readeck.xcdatamodel/contents index 22b2a9e..f3ee5dc 100644 --- a/readeck/readeck.xcdatamodeld/readeck.xcdatamodel/contents +++ b/readeck/readeck.xcdatamodeld/readeck.xcdatamodel/contents @@ -1,7 +1,51 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +