Replace the Tags tab with a tag picker sheet
CI / build-and-deploy (pull_request) Successful in 31s
CI / build-and-deploy (push) Successful in 32s

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
This commit is contained in:
Krishna Kumar
2026-07-26 11:12:47 -05:00
co-authored by Claude Opus 5
parent 69c2bc7514
commit 8a777ffc83
7 changed files with 130 additions and 99 deletions
+22 -10
View File
@@ -65,6 +65,7 @@ struct BookmarksView: View {
LibraryFilter(name: "Everything", tag: nil)
]
@State private var librarySelection: LibraryFilter.ID?
@State private var showTags = false
private var layout: LibraryLayout { LibraryLayout(rawValue: layoutRaw) ?? .list }
private var otherLayout: LibraryLayout { layout == .cards ? .list : .cards }
@@ -78,8 +79,8 @@ struct BookmarksView: View {
LibraryTagStrip(
filters: $libraryFilters,
selection: $librarySelection,
available: availableTags,
onChange: applyTagFilter
onChange: applyTagFilter,
onBrowseTags: { showTags = true }
)
if layout == .cards {
@@ -192,6 +193,13 @@ struct BookmarksView: View {
.sheet(isPresented: $showAsk) {
AskView()
}
.sheet(isPresented: $showTags) {
TagsView(
viewModel: viewModel,
pinned: Set(libraryFilters.compactMap(\.tag)),
onPick: pinTag
)
}
.sheet(isPresented: $showAddBookmark) {
AddBookmarkView(viewModel: viewModel)
}
@@ -319,15 +327,19 @@ struct BookmarksView: View {
.animation(.spring(duration: 0.35), value: viewModel.bookmarks.isEmpty)
}
/// Tags of everything currently loaded, heaviest first, minus what's
/// already pinned as a tab.
private var availableTags: [String] {
let taken = Set(libraryFilters.compactMap(\.tag))
var counts: [String: Int] = [:]
for b in viewModel.bookmarks {
for tag in b.tagNames where !taken.contains(tag) { counts[tag, default: 0] += 1 }
/// Pin a tag as a filter tab and switch to it. Choosing one that's already
/// pinned selects that tab instead of adding a duplicate.
private func pinTag(_ tag: String) {
if let existing = libraryFilters.first(where: { $0.tag == tag }) {
librarySelection = existing.id
} else {
let new = LibraryFilter(name: tag, tag: tag)
withAnimation(.spring(duration: 0.35, bounce: 0.1)) {
libraryFilters.append(new)
librarySelection = new.id
}
}
return counts.sorted { ($0.value, $1.key) > ($1.value, $0.key) }.map(\.key)
applyTagFilter()
}
/// Tag tabs filter server-side through linkding's `#tag` search syntax