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:
Krishna Kumar
2026-05-21 11:51:54 -05:00
parent b7ef5c2215
commit 1b40f50aab
11 changed files with 269 additions and 20 deletions

View 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
}
}
}
}