The tab bar drops from five to four; Tags now opens as a sheet from the + in the library's filter strip, which is the control that was already about adding a tag. Picking a tag pins it as a filter tab rather than pushing to a separate per-tag list. That made TagBookmarksView redundant — the filter tab shows the same thing, in whichever layout you're already using — so it's gone. This also fixes what the + could reach. The old inline menu listed tags found on the loaded page, so it offered 21 of 96 tags, and once a tag filter was active it collapsed to just the tags co-occurring with that one — pinning a second unrelated tag was impossible. The sheet sources names from the tags endpoint via a new BookmarksViewModel.loadAllTags(), and counts still come from loaded bookmarks, so a tag we haven't paged in shows no number rather than a wrong one. The sheet is searchable, marks already-pinned tags with a check, and picking a pinned tag selects that tab instead of duplicating it. Verified by driving the UI: the tab bar is now Bookmarks/Sources/ Podcasts/Search, + opens the sheet with 42 rows where the old menu had 21, and picking "ai" pinned an "ai" tab. Suite passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
67 lines
2.4 KiB
Swift
67 lines
2.4 KiB
Swift
import Foundation
|
|
import AppIntents
|
|
|
|
/// Which top-level tab the app is showing. Used so an intent can switch tabs.
|
|
enum AppTab: Hashable {
|
|
case bookmarks, sources, podcasts, search
|
|
}
|
|
|
|
/// Bridges App Intents (which run in the main app process, since there is no
|
|
/// separate AppIntents extension target) to the live SwiftUI scene.
|
|
///
|
|
/// Intents mutate this shared, observable singleton; `MainContainer` and
|
|
/// `SearchView` observe it and react (present a browser, switch to the search
|
|
/// tab, etc.). This is the standard "intent → in-app navigation" pattern when
|
|
/// perform() runs in-process.
|
|
@MainActor
|
|
@Observable
|
|
final class IntentRouter {
|
|
static let shared = IntentRouter()
|
|
private init() {}
|
|
|
|
/// A bookmark URL an intent asked to open in the in-app browser.
|
|
var openBookmarkURL: String?
|
|
|
|
/// A query an intent asked the app to search for.
|
|
var searchRequest: String?
|
|
|
|
func openBookmark(url: String) { openBookmarkURL = url }
|
|
func search(_ query: String) { searchRequest = query }
|
|
}
|
|
|
|
/// Errors surfaced to Siri / Shortcuts when an intent can't run.
|
|
enum MarksIntentError: Error, CustomLocalizedStringResourceConvertible {
|
|
case notConfigured
|
|
case invalidURL
|
|
|
|
var localizedStringResource: LocalizedStringResource {
|
|
switch self {
|
|
case .notConfigured: "Open Marks and connect to your Linkding server first."
|
|
case .invalidURL: "That doesn't look like a valid link."
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Shared plumbing for the intents: build an API client from saved config and
|
|
/// keep the widgets in sync after mutations.
|
|
enum MarksIntent {
|
|
/// Builds a `LinkdingAPI` from the saved server config, or throws a
|
|
/// user-facing error if the app has never been connected.
|
|
static func api() throws -> LinkdingAPI {
|
|
guard let config = ServerConfig.load() else { throw MarksIntentError.notConfigured }
|
|
return LinkdingAPI(config: config)
|
|
}
|
|
|
|
/// Prepend a freshly-saved bookmark into the widget cache so the Recent
|
|
/// widget reflects it immediately (mirrors `BookmarksViewModel.load`).
|
|
static func refreshWidgets(adding b: Bookmark) {
|
|
var items = WidgetDataStore.loadBookmarks()
|
|
items.removeAll { $0.url == b.url }
|
|
items.insert(
|
|
WidgetBookmark(url: b.url, title: b.displayTitle, domain: b.domain, faviconUrl: b.faviconUrl),
|
|
at: 0
|
|
)
|
|
WidgetDataStore.saveBookmarks(Array(items.prefix(20)))
|
|
}
|
|
}
|