Add edit bookmark, offline cache, and unread filter
- Edit bookmark: full PATCH via swipe action or long-press context menu; EditBookmarkView covers URL, title, description, tags, and unread toggle - Offline cache: bookmarks persisted to ApplicationSupport JSON and shown instantly on launch; cache is per-server, skipped when unread filter is on - Unread filter: toolbar toggle sends ?unread=true to the API; title updates to "Unread"; no stale-cache flash when toggling - Extract normalizedURL and parsedTags into String+Helpers, removing three copies of URL normalization and two of tag parsing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
109
Marks/Views/EditBookmarkView.swift
Normal file
109
Marks/Views/EditBookmarkView.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EditBookmarkView: View {
|
||||
@Bindable var viewModel: BookmarksViewModel
|
||||
let bookmark: Bookmark
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var url: String
|
||||
@State private var title: String
|
||||
@State private var description: String
|
||||
@State private var tagsText: String
|
||||
@State private var unread: Bool
|
||||
@State private var isSaving = false
|
||||
@State private var error: String?
|
||||
|
||||
init(viewModel: BookmarksViewModel, bookmark: Bookmark) {
|
||||
self.viewModel = viewModel
|
||||
self.bookmark = bookmark
|
||||
_url = State(initialValue: bookmark.url)
|
||||
_title = State(initialValue: bookmark.title)
|
||||
_description = State(initialValue: bookmark.description)
|
||||
_tagsText = State(initialValue: bookmark.tagNames.joined(separator: ", "))
|
||||
_unread = State(initialValue: bookmark.unread)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("URL") {
|
||||
TextField("https://", text: $url)
|
||||
.keyboardType(.URL)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
|
||||
Section("Title") {
|
||||
TextField("Optional", text: $title)
|
||||
}
|
||||
|
||||
Section("Description") {
|
||||
TextField("Optional", text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField("comma separated", text: $tagsText)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
} header: {
|
||||
Text("Tags")
|
||||
} footer: {
|
||||
Text("Separate tags with commas")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Mark as unread", isOn: $unread)
|
||||
}
|
||||
|
||||
if let error {
|
||||
Section {
|
||||
Text(error)
|
||||
.foregroundStyle(.red)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Edit Bookmark")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { dismiss() }
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") { save() }
|
||||
.disabled(url.trimmingCharacters(in: .whitespaces).isEmpty || isSaving)
|
||||
.overlay {
|
||||
if isSaving { ProgressView().scaleEffect(0.7) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
let urlString = url.normalizedURL
|
||||
guard URL(string: urlString) != nil else {
|
||||
error = "Invalid URL"
|
||||
return
|
||||
}
|
||||
let tags = tagsText.parsedTags
|
||||
var updated = bookmark
|
||||
updated.url = urlString
|
||||
updated.title = title.trimmingCharacters(in: .whitespaces)
|
||||
updated.description = description.trimmingCharacters(in: .whitespaces)
|
||||
updated.tagNames = tags
|
||||
updated.unread = unread
|
||||
isSaving = true
|
||||
error = nil
|
||||
Task {
|
||||
do {
|
||||
try await viewModel.updateBookmark(updated)
|
||||
dismiss()
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user