feat: Add bookmark actions (archive, favorite, delete)
- Add PATCH API endpoint for updating bookmarks with toggle functions - Add DELETE API endpoint for permanent bookmark deletion - Implement UpdateBookmarkUseCase with convenience methods for common actions - Implement DeleteBookmarkUseCase for permanent bookmark removal - Create BookmarkUpdateRequest domain model with builder pattern - Extend BookmarkCardView with action menu and confirmation dialog - Add context-sensitive actions based on current bookmark state - Implement optimistic updates in BookmarksViewModel - Add error handling and recovery for failed operations - Enhance UI with badges, progress indicators, and action buttons
This commit is contained in:
parent
c8368f0a70
commit
cd265730d3
@ -13,6 +13,8 @@ protocol PAPI {
|
||||
func getBookmarks(state: BookmarkState?) async throws -> [BookmarkDto]
|
||||
func getBookmark(id: String) async throws -> BookmarkDetailDto
|
||||
func getBookmarkArticle(id: String) async throws -> String
|
||||
func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws
|
||||
func deleteBookmark(id: String) async throws
|
||||
}
|
||||
|
||||
class API: PAPI {
|
||||
@ -162,12 +164,76 @@ class API: PAPI {
|
||||
endpoint: "/api/bookmarks/\(id)/article"
|
||||
)
|
||||
}
|
||||
|
||||
func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws {
|
||||
let requestData = try JSONEncoder().encode(updateRequest)
|
||||
|
||||
// PATCH Request ohne Response-Body erwarten
|
||||
let baseURL = await self.baseURL
|
||||
let fullEndpoint = "/api/bookmarks/\(id)"
|
||||
|
||||
guard let url = URL(string: "\(baseURL)\(fullEndpoint)") else {
|
||||
throw APIError.invalidURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PATCH"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
if let token = await tokenProvider.getToken() {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
request.httpBody = requestData
|
||||
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw APIError.invalidResponse
|
||||
}
|
||||
|
||||
guard 200...299 ~= httpResponse.statusCode else {
|
||||
print("Server Error: \(httpResponse.statusCode)")
|
||||
throw APIError.serverError(httpResponse.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteBookmark(id: String) async throws {
|
||||
// DELETE Request ohne Response-Body erwarten
|
||||
let baseURL = await self.baseURL
|
||||
let fullEndpoint = "/api/bookmarks/\(id)"
|
||||
|
||||
guard let url = URL(string: "\(baseURL)\(fullEndpoint)") else {
|
||||
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")
|
||||
}
|
||||
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw APIError.invalidResponse
|
||||
}
|
||||
|
||||
guard 200...299 ~= httpResponse.statusCode else {
|
||||
print("Server Error: \(httpResponse.statusCode)")
|
||||
throw APIError.serverError(httpResponse.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum HTTPMethod: String {
|
||||
case GET = "GET"
|
||||
case POST = "POST"
|
||||
case PUT = "PUT"
|
||||
case PATCH = "PATCH"
|
||||
case DELETE = "DELETE"
|
||||
}
|
||||
|
||||
|
||||
25
readeck/Data/API/DTOs/UpdateBookmarkRequestDto.swift
Normal file
25
readeck/Data/API/DTOs/UpdateBookmarkRequestDto.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
struct UpdateBookmarkRequestDto: Codable {
|
||||
let addLabels: [String]?
|
||||
let isArchived: Bool?
|
||||
let isDeleted: Bool?
|
||||
let isMarked: Bool?
|
||||
let labels: [String]?
|
||||
let readAnchor: String?
|
||||
let readProgress: Int?
|
||||
let removeLabels: [String]?
|
||||
let title: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case addLabels = "add_labels"
|
||||
case isArchived = "is_archived"
|
||||
case isDeleted = "is_deleted"
|
||||
case isMarked = "is_marked"
|
||||
case labels
|
||||
case readAnchor = "read_anchor"
|
||||
case readProgress = "read_progress"
|
||||
case removeLabels = "remove_labels"
|
||||
case title
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,9 @@ protocol PBookmarksRepository {
|
||||
func fetchBookmarks(state: BookmarkState?) async throws -> [Bookmark]
|
||||
func fetchBookmark(id: String) async throws -> BookmarkDetail
|
||||
func fetchBookmarkArticle(id: String) async throws -> String
|
||||
func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws
|
||||
func deleteBookmark(id: String) async throws
|
||||
func addBookmark(bookmark: Bookmark) async throws
|
||||
func removeBookmark(id: String) async throws
|
||||
}
|
||||
|
||||
class BookmarksRepository: PBookmarksRepository {
|
||||
@ -49,8 +50,24 @@ class BookmarksRepository: PBookmarksRepository {
|
||||
// Implement logic to add a bookmark if needed
|
||||
}
|
||||
|
||||
func removeBookmark(id: String) async throws {
|
||||
// Implement logic to remove a bookmark if needed
|
||||
func deleteBookmark(id: String) async throws {
|
||||
try await api.deleteBookmark(id: id)
|
||||
}
|
||||
|
||||
func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws {
|
||||
let dto = UpdateBookmarkRequestDto(
|
||||
addLabels: updateRequest.addLabels,
|
||||
isArchived: updateRequest.isArchived,
|
||||
isDeleted: updateRequest.isDeleted,
|
||||
isMarked: updateRequest.isMarked,
|
||||
labels: updateRequest.labels,
|
||||
readAnchor: updateRequest.readAnchor,
|
||||
readProgress: updateRequest.readProgress,
|
||||
removeLabels: updateRequest.removeLabels,
|
||||
title: updateRequest.title
|
||||
)
|
||||
|
||||
try await api.updateBookmark(id: id, updateRequest: dto)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ protocol UseCaseFactory {
|
||||
func makeGetBookmarkArticleUseCase() -> GetBookmarkArticleUseCase
|
||||
func makeSaveSettingsUseCase() -> SaveSettingsUseCase
|
||||
func makeLoadSettingsUseCase() -> LoadSettingsUseCase
|
||||
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase
|
||||
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
|
||||
}
|
||||
|
||||
class DefaultUseCaseFactory: UseCaseFactory {
|
||||
@ -42,9 +44,17 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
||||
func makeLoadSettingsUseCase() -> LoadSettingsUseCase {
|
||||
LoadSettingsUseCase(authRepository: authRepository)
|
||||
}
|
||||
|
||||
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase {
|
||||
return UpdateBookmarkUseCase(repository: bookmarksRepository)
|
||||
}
|
||||
|
||||
// Nicht mehr nötig - Token wird automatisch geladen
|
||||
func refreshConfiguration() async {
|
||||
// Optional: Cache löschen falls nötig
|
||||
}
|
||||
|
||||
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase {
|
||||
return DeleteBookmarkUseCase(repository: bookmarksRepository)
|
||||
}
|
||||
}
|
||||
|
||||
62
readeck/Domain/Model/BookmarkUpdateRequest.swift
Normal file
62
readeck/Domain/Model/BookmarkUpdateRequest.swift
Normal file
@ -0,0 +1,62 @@
|
||||
import Foundation
|
||||
|
||||
struct BookmarkUpdateRequest {
|
||||
let addLabels: [String]?
|
||||
let isArchived: Bool?
|
||||
let isDeleted: Bool?
|
||||
let isMarked: Bool?
|
||||
let labels: [String]?
|
||||
let readAnchor: String?
|
||||
let readProgress: Int?
|
||||
let removeLabels: [String]?
|
||||
let title: String?
|
||||
|
||||
init(
|
||||
addLabels: [String]? = nil,
|
||||
isArchived: Bool? = nil,
|
||||
isDeleted: Bool? = nil,
|
||||
isMarked: Bool? = nil,
|
||||
labels: [String]? = nil,
|
||||
readAnchor: String? = nil,
|
||||
readProgress: Int? = nil,
|
||||
removeLabels: [String]? = nil,
|
||||
title: String? = nil
|
||||
) {
|
||||
self.addLabels = addLabels
|
||||
self.isArchived = isArchived
|
||||
self.isDeleted = isDeleted
|
||||
self.isMarked = isMarked
|
||||
self.labels = labels
|
||||
self.readAnchor = readAnchor
|
||||
self.readProgress = readProgress
|
||||
self.removeLabels = removeLabels
|
||||
self.title = title
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience Initializers für häufige Aktionen
|
||||
extension BookmarkUpdateRequest {
|
||||
static func archive(_ isArchived: Bool) -> BookmarkUpdateRequest {
|
||||
return BookmarkUpdateRequest(isArchived: isArchived)
|
||||
}
|
||||
|
||||
static func favorite(_ isMarked: Bool) -> BookmarkUpdateRequest {
|
||||
return BookmarkUpdateRequest(isMarked: isMarked)
|
||||
}
|
||||
|
||||
static func delete(_ isDeleted: Bool) -> BookmarkUpdateRequest {
|
||||
return BookmarkUpdateRequest(isDeleted: isDeleted)
|
||||
}
|
||||
|
||||
static func updateProgress(_ progress: Int, anchor: String? = nil) -> BookmarkUpdateRequest {
|
||||
return BookmarkUpdateRequest(readAnchor: anchor, readProgress: progress)
|
||||
}
|
||||
|
||||
static func updateTitle(_ title: String) -> BookmarkUpdateRequest {
|
||||
return BookmarkUpdateRequest(title: title)
|
||||
}
|
||||
|
||||
static func updateLabels(_ labels: [String]) -> BookmarkUpdateRequest {
|
||||
return BookmarkUpdateRequest(labels: labels)
|
||||
}
|
||||
}
|
||||
13
readeck/Domain/UseCase/DeleteBookmarkUseCase.swift
Normal file
13
readeck/Domain/UseCase/DeleteBookmarkUseCase.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
class DeleteBookmarkUseCase {
|
||||
private let repository: PBookmarksRepository
|
||||
|
||||
init(repository: PBookmarksRepository) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func execute(bookmarkId: String) async throws {
|
||||
try await repository.deleteBookmark(id: bookmarkId)
|
||||
}
|
||||
}
|
||||
44
readeck/Domain/UseCase/UpdateBookmarkUseCase.swift
Normal file
44
readeck/Domain/UseCase/UpdateBookmarkUseCase.swift
Normal file
@ -0,0 +1,44 @@
|
||||
import Foundation
|
||||
|
||||
class UpdateBookmarkUseCase {
|
||||
private let repository: PBookmarksRepository
|
||||
|
||||
init(repository: PBookmarksRepository) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func execute(bookmarkId: String, updateRequest: BookmarkUpdateRequest) async throws {
|
||||
try await repository.updateBookmark(id: bookmarkId, updateRequest: updateRequest)
|
||||
}
|
||||
|
||||
// Convenience methods für häufige Aktionen
|
||||
func toggleArchive(bookmarkId: String, isArchived: Bool) async throws {
|
||||
let request = BookmarkUpdateRequest.archive(isArchived)
|
||||
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||
}
|
||||
|
||||
func toggleFavorite(bookmarkId: String, isMarked: Bool) async throws {
|
||||
let request = BookmarkUpdateRequest.favorite(isMarked)
|
||||
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||
}
|
||||
|
||||
func markAsDeleted(bookmarkId: String) async throws {
|
||||
let request = BookmarkUpdateRequest.delete(true)
|
||||
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||
}
|
||||
|
||||
func updateReadProgress(bookmarkId: String, progress: Int, anchor: String? = nil) async throws {
|
||||
let request = BookmarkUpdateRequest.updateProgress(progress, anchor: anchor)
|
||||
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||
}
|
||||
|
||||
func updateTitle(bookmarkId: String, title: String) async throws {
|
||||
let request = BookmarkUpdateRequest.updateTitle(title)
|
||||
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||
}
|
||||
|
||||
func updateLabels(bookmarkId: String, labels: [String]) async throws {
|
||||
let request = BookmarkUpdateRequest.updateLabels(labels)
|
||||
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,12 @@ import SwiftUI
|
||||
|
||||
struct BookmarkCardView: View {
|
||||
let bookmark: Bookmark
|
||||
let currentState: BookmarkState
|
||||
let onArchive: (Bookmark) -> Void
|
||||
let onDelete: (Bookmark) -> Void
|
||||
let onToggleFavorite: (Bookmark) -> Void
|
||||
|
||||
@State private var showingActionSheet = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
@ -22,18 +28,33 @@ struct BookmarkCardView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Status-Badges
|
||||
// Status-Badges und Action-Button
|
||||
HStack {
|
||||
if bookmark.isMarked {
|
||||
Badge(text: "Markiert", color: .blue)
|
||||
}
|
||||
if bookmark.isArchived {
|
||||
Badge(text: "Archiviert", color: .gray)
|
||||
}
|
||||
if bookmark.hasArticle {
|
||||
Badge(text: "Artikel", color: .green)
|
||||
HStack(spacing: 4) {
|
||||
if bookmark.isMarked {
|
||||
Badge(text: "Markiert", color: .blue)
|
||||
}
|
||||
if bookmark.isArchived {
|
||||
Badge(text: "Archiviert", color: .gray)
|
||||
}
|
||||
if bookmark.hasArticle {
|
||||
Badge(text: "Artikel", color: .green)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Action Menu Button
|
||||
Button(action: {
|
||||
showingActionSheet = true
|
||||
}) {
|
||||
Image(systemName: "ellipsis")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(8)
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
// Titel
|
||||
@ -80,6 +101,36 @@ struct BookmarkCardView: View {
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1)
|
||||
.confirmationDialog("Bookmark Aktionen", isPresented: $showingActionSheet) {
|
||||
actionButtons
|
||||
}
|
||||
}
|
||||
|
||||
private var actionButtons: some View {
|
||||
Group {
|
||||
// Favorit Toggle
|
||||
Button(bookmark.isMarked ? "Favorit entfernen" : "Als Favorit markieren") {
|
||||
onToggleFavorite(bookmark)
|
||||
}
|
||||
|
||||
// Archivieren/Dearchivieren basierend auf aktuellem State
|
||||
if currentState == .archived {
|
||||
Button("Aus Archiv entfernen") {
|
||||
onArchive(bookmark)
|
||||
}
|
||||
} else {
|
||||
Button("Archivieren") {
|
||||
onArchive(bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
// Permanent löschen (immer verfügbar)
|
||||
Button("Permanent löschen", role: .destructive) {
|
||||
onDelete(bookmark)
|
||||
}
|
||||
|
||||
Button("Abbrechen", role: .cancel) { }
|
||||
}
|
||||
}
|
||||
|
||||
private var imageURL: URL? {
|
||||
|
||||
@ -7,24 +7,48 @@ struct BookmarksView: View {
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
|
||||
List(viewModel.bookmarks, id: \.id) { bookmark in
|
||||
NavigationLink(destination: BookmarkDetailView(bookmarkId: bookmark.id)) {
|
||||
BookmarkCardView(bookmark: bookmark)
|
||||
if viewModel.isLoading && viewModel.bookmarks.isEmpty {
|
||||
ProgressView("Lade \(state.displayName)...")
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(viewModel.bookmarks, id: \.id) { bookmark in
|
||||
NavigationLink(destination: BookmarkDetailView(bookmarkId: bookmark.id)) {
|
||||
BookmarkCardView(
|
||||
bookmark: bookmark,
|
||||
currentState: state,
|
||||
onArchive: { bookmark in
|
||||
Task {
|
||||
await viewModel.toggleArchive(bookmark: bookmark)
|
||||
}
|
||||
},
|
||||
onDelete: { bookmark in
|
||||
Task {
|
||||
await viewModel.deleteBookmark(bookmark: bookmark)
|
||||
}
|
||||
},
|
||||
onToggleFavorite: { bookmark in
|
||||
Task {
|
||||
await viewModel.toggleFavorite(bookmark: bookmark)
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.loadBookmarks()
|
||||
await viewModel.refreshBookmarks()
|
||||
}
|
||||
.overlay {
|
||||
if viewModel.bookmarks.isEmpty && !viewModel.isLoading {
|
||||
ContentUnavailableView(
|
||||
"Keine Bookmarks",
|
||||
systemImage: "bookmark",
|
||||
description: Text("Es wurden noch keine Bookmarks gespeichert.")
|
||||
description: Text("Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden.")
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -32,7 +56,9 @@ struct BookmarksView: View {
|
||||
}
|
||||
.navigationTitle(state.displayName)
|
||||
.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||
Button("OK", role: .cancel) { }
|
||||
Button("OK", role: .cancel) {
|
||||
viewModel.errorMessage = nil
|
||||
}
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "")
|
||||
}
|
||||
@ -42,26 +68,3 @@ struct BookmarksView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unterkomponente für die Darstellung eines einzelnen Bookmarks
|
||||
private struct BookmarkRow: View {
|
||||
let bookmark: Bookmark
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: BookmarkDetailView(bookmarkId: bookmark.id)) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(bookmark.title)
|
||||
.font(.headline)
|
||||
|
||||
Text(bookmark.url)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(bookmark.created)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ import Foundation
|
||||
@Observable
|
||||
class BookmarksViewModel {
|
||||
private let getBooksmarksUseCase = DefaultUseCaseFactory.shared.makeGetBookmarksUseCase()
|
||||
private let updateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase()
|
||||
private let deleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase()
|
||||
|
||||
var bookmarks: [Bookmark] = []
|
||||
var isLoading = false
|
||||
@ -34,4 +36,52 @@ class BookmarksViewModel {
|
||||
func refreshBookmarks() async {
|
||||
await loadBookmarks(state: currentState)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func toggleArchive(bookmark: Bookmark) async {
|
||||
do {
|
||||
try await updateBookmarkUseCase.toggleArchive(
|
||||
bookmarkId: bookmark.id,
|
||||
isArchived: !bookmark.isArchived
|
||||
)
|
||||
|
||||
// Liste aktualisieren
|
||||
await loadBookmarks(state: currentState)
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Archivieren des Bookmarks"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func toggleFavorite(bookmark: Bookmark) async {
|
||||
do {
|
||||
try await updateBookmarkUseCase.toggleFavorite(
|
||||
bookmarkId: bookmark.id,
|
||||
isMarked: !bookmark.isMarked
|
||||
)
|
||||
|
||||
// Liste aktualisieren
|
||||
await loadBookmarks(state: currentState)
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Markieren des Bookmarks"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func deleteBookmark(bookmark: Bookmark) async {
|
||||
do {
|
||||
// Echtes Löschen über API statt nur als gelöscht markieren
|
||||
try await deleteBookmarkUseCase.execute(bookmarkId: bookmark.id)
|
||||
|
||||
// Lokal aus der Liste entfernen (optimistische Update)
|
||||
bookmarks.removeAll { $0.id == bookmark.id }
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Löschen des Bookmarks"
|
||||
// Bei Fehler die Liste neu laden, um konsistenten Zustand zu haben
|
||||
await loadBookmarks(state: currentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user