Fix tap-to-open on cards, and move the list to the library design
Two changes. Tapping a card did nothing. LibraryCard carried its own press-scale effect via onLongPressGesture(minimumDuration: 0), and a zero-duration long press fires immediately and swallows the tap the host attaches. It was invisible in the prototype because nothing was listening for taps there. LibraryCard is now purely presentational and the grid wraps it in a Button with the existing RowPressStyle, which gets the same scale without competing for the gesture. The list now speaks the same design as the cards: paper ground, the color chip where the favicon was, serif quoted title, monospaced domain/date, serif-italic AI summary, monospaced tag chips. Everything the old row carried is still there — unread state, excerpt, tags, podcast affordance, reading progress — along with all swipe actions, since it is still a List. BookmarkListRow takes a style rather than being rewritten, because the Tags and Search screens use the same row and should not be silently restyled by a change aimed at the bookmarks screen. The tag filter strip moved up to BookmarksView. Both layouts speak the same language now, so the strip belongs to the screen rather than to one mode — which also retires the rule that leaving cards mode had to clear the tag filter to stop it becoming invisible. The tag chip strip fades at its trailing edge; without it a tag clipped mid-word reads as broken text rather than as something scrollable. Verified in both layouts and both color schemes against the live server. Full suite passes (16 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
This commit is contained in:
co-authored by
Claude Opus 5
parent
99add346fd
commit
3c7a783b6c
@@ -1,13 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The library presentation of `viewModel.bookmarks`: color cards on paper,
|
||||
/// over a browser-tab strip of tag filters. Drops into BookmarksView's content
|
||||
/// area in place of the List, and carries the same actions — tap to open, long
|
||||
/// press for the full menu.
|
||||
/// The library's card presentation of `viewModel.bookmarks`. Drops into
|
||||
/// BookmarksView's content area in place of the List and carries the same
|
||||
/// actions — tap to open, long press for the full menu. The filter strip above
|
||||
/// it belongs to BookmarksView, since both layouts share it.
|
||||
struct LibraryGridView: View {
|
||||
@Bindable var viewModel: BookmarksViewModel
|
||||
@Binding var filters: [LibraryFilter]
|
||||
@Binding var selection: LibraryFilter.ID?
|
||||
let onOpen: (Bookmark) -> Void
|
||||
let onEdit: (Bookmark) -> Void
|
||||
|
||||
@@ -19,45 +17,25 @@ struct LibraryGridView: View {
|
||||
viewModel.bookmarks.map(LibraryItem.init(bookmark:))
|
||||
}
|
||||
|
||||
/// Tags of everything currently loaded, heaviest first, minus what's
|
||||
/// already pinned as a tab.
|
||||
private var availableTags: [String] {
|
||||
let taken = Set(filters.compactMap(\.tag))
|
||||
var counts: [String: Int] = [:]
|
||||
for b in viewModel.bookmarks where !b.tagNames.isEmpty {
|
||||
for tag in b.tagNames where !taken.contains(tag) { counts[tag, default: 0] += 1 }
|
||||
}
|
||||
return counts.sorted { ($0.value, $1.key) > ($1.value, $0.key) }.map(\.key)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
LibraryTagStrip(
|
||||
filters: $filters,
|
||||
selection: $selection,
|
||||
available: availableTags,
|
||||
onChange: applyFilter
|
||||
)
|
||||
|
||||
ScrollView {
|
||||
LazyVGrid(
|
||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
|
||||
spacing: 8
|
||||
) {
|
||||
ForEach(items) { item in
|
||||
card(item)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.top, 16)
|
||||
.padding(.bottom, 40)
|
||||
|
||||
if viewModel.isLoadingMore {
|
||||
ProgressView().padding(.bottom, 28)
|
||||
ScrollView {
|
||||
LazyVGrid(
|
||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
|
||||
spacing: 8
|
||||
) {
|
||||
ForEach(items) { item in
|
||||
card(item)
|
||||
}
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.top, 16)
|
||||
.padding(.bottom, 40)
|
||||
|
||||
if viewModel.isLoadingMore {
|
||||
ProgressView().padding(.bottom, 28)
|
||||
}
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.background(Paper.sheet)
|
||||
.podcastSheets(
|
||||
viewModel: viewModel,
|
||||
@@ -71,39 +49,31 @@ struct LibraryGridView: View {
|
||||
// The grid renders LibraryItems, but every action needs the Bookmark it
|
||||
// came from. Ids are linkding's, so this is a direct lookup.
|
||||
if let bookmark = viewModel.bookmarks.first(where: { $0.id == item.id }) {
|
||||
LibraryCard(item: item)
|
||||
.contentShape(.rect)
|
||||
.onTapGesture { onOpen(bookmark) }
|
||||
.contextMenu {
|
||||
bookmarkMenuItems(
|
||||
bookmark: bookmark,
|
||||
viewModel: viewModel,
|
||||
openURL: openURL,
|
||||
onOpen: { onOpen(bookmark) },
|
||||
onEdit: { onEdit(bookmark) },
|
||||
onPodcast: {
|
||||
launchPodcast(
|
||||
for: bookmark,
|
||||
viewModel: viewModel,
|
||||
showFullPlayer: $showFullPlayer,
|
||||
episodePicker: $episodePicker
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
.onAppear { maybeLoadMore(bookmark) }
|
||||
Button { onOpen(bookmark) } label: {
|
||||
LibraryCard(item: item)
|
||||
}
|
||||
.buttonStyle(RowPressStyle())
|
||||
.contextMenu {
|
||||
bookmarkMenuItems(
|
||||
bookmark: bookmark,
|
||||
viewModel: viewModel,
|
||||
openURL: openURL,
|
||||
onOpen: { onOpen(bookmark) },
|
||||
onEdit: { onEdit(bookmark) },
|
||||
onPodcast: {
|
||||
launchPodcast(
|
||||
for: bookmark,
|
||||
viewModel: viewModel,
|
||||
showFullPlayer: $showFullPlayer,
|
||||
episodePicker: $episodePicker
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
.onAppear { maybeLoadMore(bookmark) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Tag tabs filter server-side through linkding's `#tag` search syntax —
|
||||
/// filtering the loaded page client-side would only ever search the most
|
||||
/// recent 50 of 600+ bookmarks and quietly look empty.
|
||||
private func applyFilter() {
|
||||
let tag = filters.first { $0.id == selection }?.tag
|
||||
viewModel.searchQuery = tag.map { "#\($0)" } ?? ""
|
||||
Task { await viewModel.search() }
|
||||
}
|
||||
|
||||
private func maybeLoadMore(_ bookmark: Bookmark) {
|
||||
guard let last = viewModel.bookmarks.last, last.id == bookmark.id,
|
||||
viewModel.nextPageUrl != nil, !viewModel.isLoadingMore else { return }
|
||||
|
||||
Reference in New Issue
Block a user