Add delete annotation feature with swipe gesture
Implemented ability to delete annotations via swipe-to-delete gesture in the annotations list view. Added close button with X icon to dismiss the annotations sheet. Changes: - Added DeleteAnnotationUseCase with repository integration - Extended API with DELETE endpoint for annotations - Implemented swipe-to-delete in AnnotationsListView - Added error handling and optimistic UI updates - Updated toolbar with close button (X icon)
This commit is contained in:
parent
7338db5fab
commit
460b05ef34
@ -95,6 +95,7 @@
|
||||
UI/Components/UnifiedLabelChip.swift,
|
||||
UI/Utils/NotificationNames.swift,
|
||||
Utils/Logger.swift,
|
||||
Utils/LogStore.swift,
|
||||
);
|
||||
target = 5D2B7FAE2DFA27A400EBDB2B /* URLShare */;
|
||||
};
|
||||
|
||||
@ -20,6 +20,7 @@ protocol PAPI {
|
||||
func getBookmarkLabels() async throws -> [BookmarkLabelDto]
|
||||
func getBookmarkAnnotations(bookmarkId: String) async throws -> [AnnotationDto]
|
||||
func createAnnotation(bookmarkId: String, color: String, startOffset: Int, endOffset: Int, startSelector: String, endSelector: String) async throws -> AnnotationDto
|
||||
func deleteAnnotation(bookmarkId: String, annotationId: String) async throws
|
||||
}
|
||||
|
||||
class API: PAPI {
|
||||
@ -486,6 +487,44 @@ class API: PAPI {
|
||||
logger.info("Successfully created annotation for bookmark: \(bookmarkId)")
|
||||
return result
|
||||
}
|
||||
|
||||
func deleteAnnotation(bookmarkId: String, annotationId: String) async throws {
|
||||
logger.info("Deleting annotation: \(annotationId) from bookmark: \(bookmarkId)")
|
||||
|
||||
let baseURL = await self.baseURL
|
||||
let fullEndpoint = "/api/bookmarks/\(bookmarkId)/annotations/\(annotationId)"
|
||||
|
||||
guard let url = URL(string: "\(baseURL)\(fullEndpoint)") else {
|
||||
logger.error("Invalid URL: \(baseURL)\(fullEndpoint)")
|
||||
throw APIError.invalidURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "DELETE"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
if let token = await tokenProvider.getToken() {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
logger.logNetworkRequest(method: "DELETE", url: url.absoluteString)
|
||||
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
logger.error("Invalid HTTP response for DELETE \(url.absoluteString)")
|
||||
throw APIError.invalidResponse
|
||||
}
|
||||
|
||||
guard 200...299 ~= httpResponse.statusCode else {
|
||||
handleUnauthorizedResponse(httpResponse.statusCode)
|
||||
logger.logNetworkError(method: "DELETE", url: url.absoluteString, error: APIError.serverError(httpResponse.statusCode))
|
||||
throw APIError.serverError(httpResponse.statusCode)
|
||||
}
|
||||
|
||||
logger.logNetworkRequest(method: "DELETE", url: url.absoluteString, statusCode: httpResponse.statusCode)
|
||||
logger.info("Successfully deleted annotation: \(annotationId)")
|
||||
}
|
||||
}
|
||||
|
||||
enum HTTPMethod: String {
|
||||
|
||||
@ -21,4 +21,8 @@ class AnnotationsRepository: PAnnotationsRepository {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAnnotation(bookmarkId: String, annotationId: String) async throws {
|
||||
try await api.deleteAnnotation(bookmarkId: bookmarkId, annotationId: annotationId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
protocol PAnnotationsRepository {
|
||||
func fetchAnnotations(bookmarkId: String) async throws -> [Annotation]
|
||||
func deleteAnnotation(bookmarkId: String, annotationId: String) async throws
|
||||
}
|
||||
|
||||
17
readeck/Domain/UseCase/DeleteAnnotationUseCase.swift
Normal file
17
readeck/Domain/UseCase/DeleteAnnotationUseCase.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
protocol PDeleteAnnotationUseCase {
|
||||
func execute(bookmarkId: String, annotationId: String) async throws
|
||||
}
|
||||
|
||||
class DeleteAnnotationUseCase: PDeleteAnnotationUseCase {
|
||||
private let repository: PAnnotationsRepository
|
||||
|
||||
init(repository: PAnnotationsRepository) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func execute(bookmarkId: String, annotationId: String) async throws {
|
||||
try await repository.deleteAnnotation(bookmarkId: bookmarkId, annotationId: annotationId)
|
||||
}
|
||||
}
|
||||
@ -64,6 +64,15 @@ struct AnnotationsListView: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
await viewModel.deleteAnnotation(bookmarkId: bookmarkId, annotationId: annotation.id)
|
||||
}
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .error:
|
||||
@ -74,8 +83,10 @@ struct AnnotationsListView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import Foundation
|
||||
@Observable
|
||||
class AnnotationsListViewModel {
|
||||
private let getAnnotationsUseCase: PGetBookmarkAnnotationsUseCase
|
||||
private let deleteAnnotationUseCase: PDeleteAnnotationUseCase
|
||||
|
||||
var annotations: [Annotation] = []
|
||||
var isLoading = false
|
||||
@ -11,6 +12,7 @@ class AnnotationsListViewModel {
|
||||
|
||||
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
||||
self.getAnnotationsUseCase = factory.makeGetBookmarkAnnotationsUseCase()
|
||||
self.deleteAnnotationUseCase = factory.makeDeleteAnnotationUseCase()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -26,4 +28,15 @@ class AnnotationsListViewModel {
|
||||
showErrorAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func deleteAnnotation(bookmarkId: String, annotationId: String) async {
|
||||
do {
|
||||
try await deleteAnnotationUseCase.execute(bookmarkId: bookmarkId, annotationId: annotationId)
|
||||
annotations.removeAll { $0.id == annotationId }
|
||||
} catch {
|
||||
errorMessage = "Failed to delete annotation"
|
||||
showErrorAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ protocol UseCaseFactory {
|
||||
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase
|
||||
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase
|
||||
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase
|
||||
func makeDeleteAnnotationUseCase() -> PDeleteAnnotationUseCase
|
||||
}
|
||||
|
||||
|
||||
@ -125,4 +126,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
||||
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase {
|
||||
return GetBookmarkAnnotationsUseCase(repository: annotationsRepository)
|
||||
}
|
||||
|
||||
func makeDeleteAnnotationUseCase() -> PDeleteAnnotationUseCase {
|
||||
return DeleteAnnotationUseCase(repository: annotationsRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,6 +92,10 @@ class MockUseCaseFactory: UseCaseFactory {
|
||||
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase {
|
||||
MockGetBookmarkAnnotationsUseCase()
|
||||
}
|
||||
|
||||
func makeDeleteAnnotationUseCase() -> PDeleteAnnotationUseCase {
|
||||
MockDeleteAnnotationUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -250,6 +254,12 @@ class MockGetBookmarkAnnotationsUseCase: PGetBookmarkAnnotationsUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
class MockDeleteAnnotationUseCase: PDeleteAnnotationUseCase {
|
||||
func execute(bookmarkId: String, annotationId: String) async throws {
|
||||
// Mock implementation - do nothing
|
||||
}
|
||||
}
|
||||
|
||||
extension Bookmark {
|
||||
static let mock: Bookmark = .init(
|
||||
id: "123", title: "title", url: "https://example.com", href: "https://example.com", description: "description", authors: ["Tom"], created: "", published: "", updated: "", siteName: "example.com", site: "https://example.com", readingTime: 2, wordCount: 20, hasArticle: true, isArchived: false, isDeleted: false, isMarked: true, labels: ["Test"], lang: "EN", loaded: false, readProgress: 0, documentType: "", state: 0, textDirection: "ltr", type: "", resources: .init(article: nil, icon: nil, image: nil, log: nil, props: nil, thumbnail: nil)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user