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 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+