Compare commits
No commits in common. "2d3be2430f27da7b4e16102397c0adff769da19d" and "789d581705df5f98d173e8c5a25f29cf32a9b3c6" have entirely different histories.
2d3be2430f
...
789d581705
Binary file not shown.
Binary file not shown.
@ -174,10 +174,7 @@
|
|||||||
},
|
},
|
||||||
"subpath" : "Yams"
|
"subpath" : "Yams"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"prebuilts" : [
|
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version" : 7
|
"version" : 6
|
||||||
}
|
}
|
||||||
@ -76,7 +76,9 @@
|
|||||||
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 */;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,97 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
<?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,25 +12,12 @@
|
|||||||
<key>URLShare.xcscheme_^#shared#^_</key>
|
<key>URLShare.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>readeck.xcscheme_^#shared#^_</key>
|
<key>readeck.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>0</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?, updatedSince: Date?, limit: Int?, offset: Int?) async throws -> [BookmarkDto]
|
func getBookmarks(state: BookmarkState?) 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,46 +131,21 @@ class API: PAPI {
|
|||||||
return userDto
|
return userDto
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBookmarks(state: BookmarkState? = nil, updatedSince: Date? = nil, limit: Int? = nil, offset: Int? = nil) async throws -> [BookmarkDto] {
|
func getBookmarks(state: BookmarkState? = 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:
|
||||||
queryParams.append("is_archived=false")
|
endpoint += "?is_archived=false&is_marked=false"
|
||||||
queryParams.append("is_marked=false")
|
|
||||||
case .favorite:
|
case .favorite:
|
||||||
queryParams.append("is_marked=true")
|
endpoint += "?is_marked=true"
|
||||||
case .archived:
|
case .archived:
|
||||||
queryParams.append("is_archived=true")
|
endpoint += "?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,3 +62,5 @@ struct ImageResourceDto: Codable {
|
|||||||
let height: Int
|
let height: Int
|
||||||
let width: Int
|
let width: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,213 +0,0 @@
|
|||||||
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 {
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
57
readeck/Data/Persistence.swift
Normal file
57
readeck/Data/Persistence.swift
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// Persistence.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 10.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
struct PersistenceController {
|
||||||
|
static let shared = PersistenceController()
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
static let preview: PersistenceController = {
|
||||||
|
let result = PersistenceController(inMemory: true)
|
||||||
|
let viewContext = result.container.viewContext
|
||||||
|
for _ in 0..<10 {
|
||||||
|
let newItem = Item(context: viewContext)
|
||||||
|
newItem.timestamp = Date()
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try viewContext.save()
|
||||||
|
} catch {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
let nsError = error as NSError
|
||||||
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}()
|
||||||
|
|
||||||
|
let container: NSPersistentContainer
|
||||||
|
|
||||||
|
init(inMemory: Bool = false) {
|
||||||
|
container = NSPersistentContainer(name: "readeck")
|
||||||
|
if inMemory {
|
||||||
|
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
||||||
|
}
|
||||||
|
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
||||||
|
if let error = error as NSError? {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Typical reasons for an error here include:
|
||||||
|
* The parent directory does not exist, cannot be created, or disallows writing.
|
||||||
|
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
||||||
|
* The device is out of space.
|
||||||
|
* The store could not be migrated to the current model version.
|
||||||
|
Check the error message to determine what the actual problem was.
|
||||||
|
*/
|
||||||
|
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,103 +0,0 @@
|
|||||||
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,32 +1,23 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
|
||||||
|
|
||||||
protocol PBookmarksRepository {
|
protocol PBookmarksRepository {
|
||||||
func fetchBookmarks(state: BookmarkState?, limit: Int?, offset: Int?) async throws -> [Bookmark]
|
func fetchBookmarks(state: BookmarkState?) 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 syncBookmarks() async throws {
|
func fetchBookmarks(state: BookmarkState? = nil) async throws -> [Bookmark] {
|
||||||
|
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, limit: Int? = nil, offset: Int? = nil) async throws -> [Bookmark] {
|
func execute(state: BookmarkState? = nil) async throws -> [Bookmark] {
|
||||||
let allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset)
|
let allBookmarks = try await repository.fetchBookmarks(state: state)
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
//
|
|
||||||
// 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,199 +1,6 @@
|
|||||||
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,51 +10,54 @@ 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 {
|
||||||
List {
|
if viewModel.isLoading && viewModel.bookmarks.isEmpty {
|
||||||
ForEach(bookmarks) { bookmark in
|
ProgressView("Lade \(state.displayName)...")
|
||||||
Button(action: {
|
} else {
|
||||||
selectedBookmarkId = bookmark.id
|
List {
|
||||||
}) {
|
ForEach(viewModel.bookmarks, id: \.id) { bookmark in
|
||||||
BookmarkEntityCardView(
|
Button(action: {
|
||||||
bookmark: bookmark,
|
selectedBookmarkId = bookmark.id
|
||||||
currentState: state,
|
}) {
|
||||||
onArchive: { bookmark in
|
BookmarkCardView(
|
||||||
Task {
|
bookmark: bookmark,
|
||||||
//await viewModel.toggleArchive(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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
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.")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,7 +106,6 @@ 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,7 +6,6 @@ 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
|
||||||
@ -55,22 +54,14 @@ class BookmarksViewModel {
|
|||||||
currentState = state
|
currentState = state
|
||||||
|
|
||||||
do {
|
do {
|
||||||
bookmarks = try await getBooksmarksUseCase.execute(state: state, limit: 100, offset: 0)
|
bookmarks = try await getBooksmarksUseCase.execute(state: state)
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden der Bookmarks"
|
errorMessage = "Fehler beim Laden der Bookmarks"
|
||||||
bookmarks = []
|
bookmarks = []
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
88
readeck/UI/ContentView.swift
Normal file
88
readeck/UI/ContentView.swift
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// ContentView.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 10.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
|
||||||
|
@FetchRequest(
|
||||||
|
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
|
||||||
|
animation: .default)
|
||||||
|
private var items: FetchedResults<Item>
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
ForEach(items) { item in
|
||||||
|
NavigationLink {
|
||||||
|
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
|
||||||
|
} label: {
|
||||||
|
Text(item.timestamp!, formatter: itemFormatter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete(perform: deleteItems)
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
#if os(iOS)
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
ToolbarItem {
|
||||||
|
Button(action: addItem) {
|
||||||
|
Label("Add Item", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text("Select an item")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addItem() {
|
||||||
|
withAnimation {
|
||||||
|
let newItem = Item(context: viewContext)
|
||||||
|
newItem.timestamp = Date()
|
||||||
|
|
||||||
|
do {
|
||||||
|
try viewContext.save()
|
||||||
|
} catch {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
let nsError = error as NSError
|
||||||
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteItems(offsets: IndexSet) {
|
||||||
|
withAnimation {
|
||||||
|
offsets.map { items[$0] }.forEach(viewContext.delete)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try viewContext.save()
|
||||||
|
} catch {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
let nsError = error as NSError
|
||||||
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let itemFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .short
|
||||||
|
formatter.timeStyle = .medium
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||||
|
}
|
||||||
@ -10,7 +10,6 @@ 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 {
|
||||||
@ -18,9 +17,6 @@ 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()
|
||||||
@ -67,8 +63,4 @@ 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 = CoreDataManager.shared
|
let persistenceController = PersistenceController.shared
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MainTabView()
|
MainTabView()
|
||||||
.environment(\.managedObjectContext, persistenceController.persistentContainer.viewContext)
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
handleIncomingURL(url)
|
handleIncomingURL(url)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,51 +1,7 @@
|
|||||||
<?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="BlaEntity" representedClassName="BlaEntity" syncable="YES" codeGenerationType="class">
|
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="timestamp" optional="YES" attributeType="Date" 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