Every font size in the library design multiplied by 1.5 — card title and stamp, list title, meta, excerpt and tags, filter tabs, and the prototype's header. Geometry had to follow, or the larger type would have broken the layout it sits in: - The card grid drops from three columns to two. At 16.5pt in a 95pt column a card gets about nine characters per line and every title truncates; the text size is what sets the column count. - Filter tabs grow 30pt -> 42pt tall, the card gains padding and a slightly squarer aspect, and the list's color chip, unread dot and tag chips scale with the text they sit beside. Verified in both layouts against the live server. Suite passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
86 lines
3.0 KiB
Swift
86 lines
3.0 KiB
Swift
import SwiftUI
|
|
|
|
/// 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
|
|
let onOpen: (Bookmark) -> Void
|
|
let onEdit: (Bookmark) -> Void
|
|
|
|
@Environment(\.openURL) private var openURL
|
|
@State private var showFullPlayer = false
|
|
@State private var episodePicker: Bookmark?
|
|
|
|
private var items: [LibraryItem] {
|
|
viewModel.bookmarks.map(LibraryItem.init(bookmark:))
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
LazyVGrid(
|
|
// Two columns, not three: at the scaled-up type a third
|
|
// column leaves ~9 characters per line and every title
|
|
// truncates. The text size sets the column count.
|
|
columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: 2),
|
|
spacing: 10
|
|
) {
|
|
ForEach(items) { item in
|
|
card(item)
|
|
}
|
|
}
|
|
.padding(.horizontal, 18)
|
|
.padding(.top, 16)
|
|
.padding(.bottom, 40)
|
|
|
|
if viewModel.isLoadingMore {
|
|
ProgressView().padding(.bottom, 28)
|
|
}
|
|
}
|
|
.scrollBounceBehavior(.basedOnSize)
|
|
.background(Paper.sheet)
|
|
.podcastSheets(
|
|
viewModel: viewModel,
|
|
showFullPlayer: $showFullPlayer,
|
|
episodePicker: $episodePicker
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func card(_ item: LibraryItem) -> some 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 }) {
|
|
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) }
|
|
}
|
|
}
|
|
|
|
private func maybeLoadMore(_ bookmark: Bookmark) {
|
|
guard let last = viewModel.bookmarks.last, last.id == bookmark.id,
|
|
viewModel.nextPageUrl != nil, !viewModel.isLoadingMore else { return }
|
|
Task { await viewModel.loadMore() }
|
|
}
|
|
}
|