wip
This commit is contained in:
parent
ad4483aa63
commit
2d3be2430f
Binary file not shown.
Binary file not shown.
@ -174,7 +174,10 @@
|
||||
},
|
||||
"subpath" : "Yams"
|
||||
}
|
||||
],
|
||||
"prebuilts" : [
|
||||
|
||||
]
|
||||
},
|
||||
"version" : 6
|
||||
"version" : 7
|
||||
}
|
||||
@ -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 */;
|
||||
};
|
||||
|
||||
97
readeck.xcodeproj/xcshareddata/xcschemes/URLShare.xcscheme
Normal file
97
readeck.xcodeproj/xcshareddata/xcschemes/URLShare.xcscheme
Normal file
@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5D2B7FAE2DFA27A400EBDB2B"
|
||||
BuildableName = "URLShare.appex"
|
||||
BlueprintName = "URLShare"
|
||||
ReferencedContainer = "container:readeck.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5D45F9C72DF858680048D5B8"
|
||||
BuildableName = "readeck.app"
|
||||
BlueprintName = "readeck"
|
||||
ReferencedContainer = "container:readeck.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5D45F9C72DF858680048D5B8"
|
||||
BuildableName = "readeck.app"
|
||||
BlueprintName = "readeck"
|
||||
ReferencedContainer = "container:readeck.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5D45F9C72DF858680048D5B8"
|
||||
BuildableName = "readeck.app"
|
||||
BlueprintName = "readeck"
|
||||
ReferencedContainer = "container:readeck.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
78
readeck.xcodeproj/xcshareddata/xcschemes/readeck.xcscheme
Normal file
78
readeck.xcodeproj/xcshareddata/xcschemes/readeck.xcscheme
Normal file
@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5D45F9C72DF858680048D5B8"
|
||||
BuildableName = "readeck.app"
|
||||
BlueprintName = "readeck"
|
||||
ReferencedContainer = "container:readeck.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5D45F9C72DF858680048D5B8"
|
||||
BuildableName = "readeck.app"
|
||||
BlueprintName = "readeck"
|
||||
ReferencedContainer = "container:readeck.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5D45F9C72DF858680048D5B8"
|
||||
BuildableName = "readeck.app"
|
||||
BlueprintName = "readeck"
|
||||
ReferencedContainer = "container:readeck.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@ -12,12 +12,25 @@
|
||||
<key>URLShare.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>readeck.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>5D2B7FAE2DFA27A400EBDB2B</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>5D45F9C72DF858680048D5B8</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -62,5 +62,3 @@ struct ImageResourceDto: Codable {
|
||||
let height: Int
|
||||
let width: Int
|
||||
}
|
||||
|
||||
|
||||
|
||||
213
readeck/Data/Mappers/BookmarkEntityMapper.swift
Normal file
213
readeck/Data/Mappers/BookmarkEntityMapper.swift
Normal file
@ -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 {
|
||||
|
||||
}
|
||||
*/
|
||||
103
readeck/Data/Repository/BookmarkSyncRepository.swift
Normal file
103
readeck/Data/Repository/BookmarkSyncRepository.swift
Normal file
@ -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<NSFetchRequestResult> = 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<String> {
|
||||
let context = CoreDataManager.shared.context
|
||||
let request: NSFetchRequest<BookmarkEntity> = 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() }
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -10,54 +10,51 @@ struct BookmarksView: View {
|
||||
@State private var shareURL = ""
|
||||
@State private var shareTitle = ""
|
||||
|
||||
@FetchRequest(entity: BookmarkEntity.entity(), sortDescriptors: [])
|
||||
private var bookmarks: FetchedResults<BookmarkEntity>
|
||||
|
||||
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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -1,7 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24F74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<entity name="BlaEntity" representedClassName="BlaEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<entity name="BookmarkEntity" representedClassName="BookmarkEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="authors" optional="YES" attributeType="String"/>
|
||||
<attribute name="created" optional="YES" attributeType="String"/>
|
||||
<attribute name="desc" optional="YES" attributeType="String"/>
|
||||
<attribute name="documentType" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasArticle" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasDeleted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="href" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="isArchived" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="isMarked" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="lang" optional="YES" attributeType="String"/>
|
||||
<attribute name="loaded" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="published" optional="YES" attributeType="String"/>
|
||||
<attribute name="readingTime" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="readProgress" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="site" optional="YES" attributeType="String"/>
|
||||
<attribute name="siteName" optional="YES" attributeType="String"/>
|
||||
<attribute name="state" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="textDirection" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<attribute name="type" optional="YES" attributeType="String"/>
|
||||
<attribute name="update" optional="YES" attributeType="String"/>
|
||||
<attribute name="url" optional="YES" attributeType="String"/>
|
||||
<attribute name="wordCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="resources" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BookmarkResourcesEntity"/>
|
||||
</entity>
|
||||
<entity name="BookmarkResourcesEntity" representedClassName="BookmarkResourcesEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<relationship name="article" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ResourceEntity"/>
|
||||
<relationship name="icon" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ImageResourceEntity"/>
|
||||
<relationship name="image" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ImageResourceEntity"/>
|
||||
<relationship name="log" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ResourceEntity"/>
|
||||
<relationship name="props" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ResourceEntity"/>
|
||||
<relationship name="thumbnail" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ImageResourceEntity"/>
|
||||
</entity>
|
||||
<entity name="ImageResourceEntity" representedClassName="ImageResourceEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="height" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="src" optional="YES" attributeType="String"/>
|
||||
<attribute name="width" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<entity name="ResourceEntity" representedClassName="ResourceEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="src" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="SettingEntity" representedClassName="SettingEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user