wip
This commit is contained in:
parent
ad4483aa63
commit
2d3be2430f
Binary file not shown.
Binary file not shown.
@ -174,7 +174,10 @@
|
|||||||
},
|
},
|
||||||
"subpath" : "Yams"
|
"subpath" : "Yams"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"prebuilts" : [
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version" : 6
|
"version" : 7
|
||||||
}
|
}
|
||||||
@ -76,9 +76,7 @@
|
|||||||
5DCD48B72DFB44D600AC7FB6 /* Exceptions for "readeck" folder in "URLShare" target */ = {
|
5DCD48B72DFB44D600AC7FB6 /* Exceptions for "readeck" folder in "URLShare" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
Data/CoreData/CoreDataManager.swift,
|
|
||||||
Domain/Model/Bookmark.swift,
|
Domain/Model/Bookmark.swift,
|
||||||
readeck.xcdatamodeld,
|
|
||||||
);
|
);
|
||||||
target = 5D2B7FAE2DFA27A400EBDB2B /* URLShare */;
|
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>
|
<key>URLShare.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>readeck.xcscheme_^#shared#^_</key>
|
<key>readeck.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<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>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import Foundation
|
|||||||
protocol PAPI {
|
protocol PAPI {
|
||||||
var tokenProvider: TokenProvider { get }
|
var tokenProvider: TokenProvider { get }
|
||||||
func login(username: String, password: String) async throws -> UserDto
|
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 getBookmark(id: String) async throws -> BookmarkDetailDto
|
||||||
func getBookmarkArticle(id: String) async throws -> String
|
func getBookmarkArticle(id: String) async throws -> String
|
||||||
func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto
|
func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto
|
||||||
@ -131,21 +131,46 @@ class API: PAPI {
|
|||||||
return userDto
|
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 endpoint = "/api/bookmarks"
|
||||||
|
var queryParams: [String] = []
|
||||||
|
|
||||||
// Query-Parameter basierend auf State hinzufügen
|
// Query-Parameter basierend auf State hinzufügen
|
||||||
if let state = state {
|
if let state = state {
|
||||||
switch state {
|
switch state {
|
||||||
case .unread:
|
case .unread:
|
||||||
endpoint += "?is_archived=false&is_marked=false"
|
queryParams.append("is_archived=false")
|
||||||
|
queryParams.append("is_marked=false")
|
||||||
case .favorite:
|
case .favorite:
|
||||||
endpoint += "?is_marked=true"
|
queryParams.append("is_marked=true")
|
||||||
case .archived:
|
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(
|
return try await makeJSONRequest(
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
responseType: [BookmarkDto].self
|
responseType: [BookmarkDto].self
|
||||||
|
|||||||
@ -62,5 +62,3 @@ struct ImageResourceDto: Codable {
|
|||||||
let height: Int
|
let height: Int
|
||||||
let width: 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 Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
protocol PBookmarksRepository {
|
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 fetchBookmark(id: String) async throws -> BookmarkDetail
|
||||||
func fetchBookmarkArticle(id: String) async throws -> String
|
func fetchBookmarkArticle(id: String) async throws -> String
|
||||||
func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String
|
func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String
|
||||||
func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws
|
func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws
|
||||||
func deleteBookmark(id: String) async throws
|
func deleteBookmark(id: String) async throws
|
||||||
|
func syncBookmarks() async throws
|
||||||
}
|
}
|
||||||
|
|
||||||
class BookmarksRepository: PBookmarksRepository {
|
class BookmarksRepository: PBookmarksRepository {
|
||||||
private var api: PAPI
|
private var api: PAPI
|
||||||
|
|
||||||
|
private let coreDataManager = CoreDataManager.shared
|
||||||
|
|
||||||
init(api: PAPI) {
|
init(api: PAPI) {
|
||||||
self.api = api
|
self.api = api
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchBookmarks(state: BookmarkState? = nil) async throws -> [Bookmark] {
|
func syncBookmarks() async throws {
|
||||||
let bookmarkDtos = try await api.getBookmarks(state: state)
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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() }
|
return bookmarkDtos.map { $0.toDomain() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,8 @@ class GetBookmarksUseCase {
|
|||||||
self.repository = repository
|
self.repository = repository
|
||||||
}
|
}
|
||||||
|
|
||||||
func execute(state: BookmarkState? = nil) async throws -> [Bookmark] {
|
func execute(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil) async throws -> [Bookmark] {
|
||||||
let allBookmarks = try await repository.fetchBookmarks(state: state)
|
let allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset)
|
||||||
|
|
||||||
// Fallback-Filterung auf Client-Seite falls API keine Query-Parameter unterstützt
|
// Fallback-Filterung auf Client-Seite falls API keine Query-Parameter unterstützt
|
||||||
if let state = state {
|
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 SwiftUI
|
||||||
import SafariServices
|
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 {
|
struct BookmarkCardView: View {
|
||||||
let bookmark: Bookmark
|
let bookmark: Bookmark
|
||||||
let currentState: BookmarkState
|
let currentState: BookmarkState
|
||||||
|
|||||||
@ -10,49 +10,45 @@ struct BookmarksView: View {
|
|||||||
@State private var shareURL = ""
|
@State private var shareURL = ""
|
||||||
@State private var shareTitle = ""
|
@State private var shareTitle = ""
|
||||||
|
|
||||||
|
@FetchRequest(entity: BookmarkEntity.entity(), sortDescriptors: [])
|
||||||
|
private var bookmarks: FetchedResults<BookmarkEntity>
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ZStack {
|
ZStack {
|
||||||
if viewModel.isLoading && viewModel.bookmarks.isEmpty {
|
|
||||||
ProgressView("Lade \(state.displayName)...")
|
|
||||||
} else {
|
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.bookmarks, id: \.id) { bookmark in
|
ForEach(bookmarks) { bookmark in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
selectedBookmarkId = bookmark.id
|
selectedBookmarkId = bookmark.id
|
||||||
}) {
|
}) {
|
||||||
BookmarkCardView(
|
BookmarkEntityCardView(
|
||||||
bookmark: bookmark,
|
bookmark: bookmark,
|
||||||
currentState: state,
|
currentState: state,
|
||||||
onArchive: { bookmark in
|
onArchive: { bookmark in
|
||||||
Task {
|
Task {
|
||||||
await viewModel.toggleArchive(bookmark: bookmark)
|
//await viewModel.toggleArchive(bookmark: bookmark)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDelete: { bookmark in
|
onDelete: { bookmark in
|
||||||
Task {
|
Task {
|
||||||
await viewModel.deleteBookmark(bookmark: bookmark)
|
//await viewModel.deleteBookmark(bookmark: bookmark)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onToggleFavorite: { bookmark in
|
onToggleFavorite: { bookmark in
|
||||||
Task {
|
Task {
|
||||||
await viewModel.toggleFavorite(bookmark: bookmark)
|
//await viewModel.toggleFavorite(bookmark: bookmark)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
}
|
|
||||||
}
|
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.refreshable {
|
.refreshable {
|
||||||
await viewModel.refreshBookmarks()
|
await viewModel.refreshBookmarks()
|
||||||
}
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
if viewModel.bookmarks.isEmpty && !viewModel.isLoading {
|
if bookmarks.isEmpty {
|
||||||
ContentUnavailableView(
|
ContentUnavailableView(
|
||||||
"Keine Bookmarks",
|
"Keine Bookmarks",
|
||||||
systemImage: "bookmark",
|
systemImage: "bookmark",
|
||||||
@ -61,6 +57,7 @@ struct BookmarksView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FAB Button - nur bei "Ungelesen" anzeigen
|
// FAB Button - nur bei "Ungelesen" anzeigen
|
||||||
if state == .unread {
|
if state == .unread {
|
||||||
@ -106,6 +103,7 @@ struct BookmarksView: View {
|
|||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadBookmarks(state: state)
|
await viewModel.loadBookmarks(state: state)
|
||||||
|
await viewModel.syncBookmarks()
|
||||||
}
|
}
|
||||||
.onChange(of: showingAddBookmark) { oldValue, newValue in
|
.onChange(of: showingAddBookmark) { oldValue, newValue in
|
||||||
// Refresh bookmarks when sheet is dismissed
|
// Refresh bookmarks when sheet is dismissed
|
||||||
|
|||||||
@ -6,6 +6,7 @@ class BookmarksViewModel {
|
|||||||
private let getBooksmarksUseCase = DefaultUseCaseFactory.shared.makeGetBookmarksUseCase()
|
private let getBooksmarksUseCase = DefaultUseCaseFactory.shared.makeGetBookmarksUseCase()
|
||||||
private let updateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase()
|
private let updateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase()
|
||||||
private let deleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase()
|
private let deleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase()
|
||||||
|
private let syncBookmarksUseCase = DefaultUseCaseFactory.shared.makeSyncBookmarksUseCase()
|
||||||
|
|
||||||
var bookmarks: [Bookmark] = []
|
var bookmarks: [Bookmark] = []
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
@ -54,7 +55,7 @@ class BookmarksViewModel {
|
|||||||
currentState = state
|
currentState = state
|
||||||
|
|
||||||
do {
|
do {
|
||||||
bookmarks = try await getBooksmarksUseCase.execute(state: state)
|
bookmarks = try await getBooksmarksUseCase.execute(state: state, limit: 100, offset: 0)
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden der Bookmarks"
|
errorMessage = "Fehler beim Laden der Bookmarks"
|
||||||
bookmarks = []
|
bookmarks = []
|
||||||
@ -63,6 +64,14 @@ class BookmarksViewModel {
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncBookmarks() async {
|
||||||
|
do {
|
||||||
|
try await syncBookmarksUseCase.execute()
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Fehler beim Synchronisieren der Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func refreshBookmarks() async {
|
func refreshBookmarks() async {
|
||||||
await loadBookmarks(state: currentState)
|
await loadBookmarks(state: currentState)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ protocol UseCaseFactory {
|
|||||||
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase
|
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase
|
||||||
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
|
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
|
||||||
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase
|
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase
|
||||||
|
func makeSyncBookmarksUseCase() -> SyncBookmarksUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultUseCaseFactory: UseCaseFactory {
|
class DefaultUseCaseFactory: UseCaseFactory {
|
||||||
@ -17,6 +18,9 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
private lazy var api: PAPI = API(tokenProvider: tokenProvider)
|
private lazy var api: PAPI = API(tokenProvider: tokenProvider)
|
||||||
private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository)
|
private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository)
|
||||||
private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api)
|
private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api)
|
||||||
|
|
||||||
|
private lazy var bookmarkSyncRepository = BookmarkSyncRepository(api: api)
|
||||||
|
|
||||||
private let settingsRepository: PSettingsRepository = SettingsRepository()
|
private let settingsRepository: PSettingsRepository = SettingsRepository()
|
||||||
|
|
||||||
static let shared = DefaultUseCaseFactory()
|
static let shared = DefaultUseCaseFactory()
|
||||||
@ -63,4 +67,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase {
|
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase {
|
||||||
return CreateBookmarkUseCase(repository: bookmarksRepository)
|
return CreateBookmarkUseCase(repository: bookmarksRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeSyncBookmarksUseCase() -> SyncBookmarksUseCase {
|
||||||
|
return SyncBookmarksUseCase(bookmarkRepository: bookmarkSyncRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,12 +9,12 @@ import SwiftUI
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
struct readeckApp: App {
|
struct readeckApp: App {
|
||||||
let persistenceController = PersistenceController.shared
|
let persistenceController = CoreDataManager.shared
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MainTabView()
|
MainTabView()
|
||||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
.environment(\.managedObjectContext, persistenceController.persistentContainer.viewContext)
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
handleIncomingURL(url)
|
handleIncomingURL(url)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,51 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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="">
|
<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">
|
<entity name="BlaEntity" representedClassName="BlaEntity" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<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>
|
||||||
<entity name="SettingEntity" representedClassName="SettingEntity" syncable="YES" codeGenerationType="class">
|
<entity name="SettingEntity" representedClassName="SettingEntity" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user