Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Copy/App/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ final class AppCoordinator {
let isDemoMode: Bool
/// `UserDefaults` key the status-menu toggle flips; read at launch to enter demo mode.
static let demoModeKey = "demoMode"
private(set) lazy var shelfViewModel = ShelfViewModel(store: store, pinboardStore: pinboardStore, settings: settings)
private(set) lazy var linkFetcher = LinkMetadataFetcher(store: store)
private(set) lazy var shelfViewModel = ShelfViewModel(store: store, pinboardStore: pinboardStore, settings: settings, linkFetcher: linkFetcher)
private(set) var linkFetcher: LinkMetadataFetcher
private(set) lazy var ocrController = OCRController(store: store)
private(set) lazy var archiveController = ArchiveController(store: store, pinboardStore: pinboardStore)

Expand Down
57 changes: 25 additions & 32 deletions Copy/Shelf/ItemCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -444,36 +444,21 @@ struct ItemCardView: View {
case .text, .richText:
textBody
case .link:
if let linkTitle = item.linkTitle {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
LinkFaviconView(item: item, store: store)
Text(URL(string: item.plainText ?? "")?.host ?? "Link")
.font(Tokens.cardSubtitle)
.lineLimit(1)
}
Text(linkTitle)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 5) {
LinkFaviconView(item: item, store: store)
Text(item.linkTitle ?? URL(string: item.plainText ?? "")?.host ?? "Link")
.font(.system(size: 13, weight: .semibold))
.lineLimit(bodyLineLimit(standard: 2, compact: 1))
.multilineTextAlignment(.leading)
Text(String((item.plainText ?? "").prefix(1_500)))
.font(Tokens.bodyMono)
.foregroundStyle(.secondary)
.lineLimit(bodyLineLimit(standard: 2, compact: 1))
}
} else {
VStack(alignment: .leading, spacing: 4) {
Image(systemName: "link")
.font(.system(size: 14))
.foregroundStyle(.secondary)
Text(URL(string: item.plainText ?? "")?.host ?? "Link")
.font(Tokens.cardSubtitle)
.lineLimit(1)
Text(String((item.plainText ?? "").prefix(1_500)))
.font(Tokens.bodyMono)
.foregroundStyle(.secondary)
.lineLimit(bodyLineLimit(standard: 5, compact: 3))
}
// Give the URL the remaining body lines while keeping the footer visible.
Text(String((item.plainText ?? "").prefix(1_500)))
.font(Tokens.bodyMono)
.foregroundStyle(.secondary)
.lineLimit(bodyLineLimit(standard: 8, compact: 3))
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
}
case .image:
imageBody
Expand Down Expand Up @@ -550,12 +535,20 @@ struct LinkFaviconView: View {
}
}
.frame(width: 16, height: 16)
.onAppear {
if image == nil {
image = FaviconCache.shared.cached(for: item)
if image == nil {
FaviconCache.shared.favicon(for: item, store: store) { image = $0 }
}
.onAppear(perform: loadImage)
// Metadata persistence updates the item title after writing its favicon. The
// view may already be mounted by then, so `onAppear` alone would leave its
// earlier nil lookup stuck until the app relaunched.
.onChange(of: item.linkTitle) { _, _ in loadImage() }
}

private func loadImage() {
guard image == nil else { return }
image = FaviconCache.shared.cached(for: item)
if image == nil {
FaviconCache.shared.favicon(for: item, store: store) { loaded in
// An older miss must not overwrite a newer successful lookup.
if let loaded { image = loaded }
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion Copy/Shelf/ShelfRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,13 @@ private struct ShelfItemsRow: View {
// The LazyHStack only builds a card once it scrolls into view,
// so this fires as the user nears the oldest card and widens the
// fetch window. Without it the shelf stopped at the first page.
.onAppear { viewModel.loadMoreIfNeeded(at: index) }
.onAppear {
viewModel.loadMoreIfNeeded(at: index)
viewModel.fetchLinkPreviewIfNeeded(for: item)
}
.onChange(of: viewModel.settings.fetchLinkPreviews) { _, enabled in
if enabled { viewModel.fetchLinkPreviewIfNeeded(for: item) }
}
.popover(isPresented: Binding(
get: { viewModel.isSelected(item) && item.uuid == viewModel.selection.primary && viewModel.previewShown },
set: { viewModel.previewShown = $0 }
Expand Down
8 changes: 7 additions & 1 deletion Copy/Shelf/ShelfViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ final class ShelfViewModel {
/// `viewModel.settings.compactShelf` re-renders on toggle without any extra
/// change-hook plumbing (the pattern `AppCoordinator` uses for AppKit-side settings
/// like `hideDuringScreenSharing` doesn't apply here since this is pure SwiftUI).
private let linkFetcher: LinkMetadataFetcher
let settings: SettingsStore

var items: [ClipItem] = []
Expand Down Expand Up @@ -159,10 +160,11 @@ final class ShelfViewModel {
/// every item on the board, so there's nothing to page there.
@ObservationIgnored private var isPaged = true

init(store: ItemStore, pinboardStore: PinboardStore, settings: SettingsStore) {
init(store: ItemStore, pinboardStore: PinboardStore, settings: SettingsStore, linkFetcher: LinkMetadataFetcher) {
self.store = store
self.pinboardStore = pinboardStore
self.settings = settings
self.linkFetcher = linkFetcher
pinboardsToken = pinboardStore.observeAll(
onError: { NSLog("Copy: pinboard observation failed: \($0)") },
onChange: { [weak self] in self?.pinboards = $0 })
Expand All @@ -174,6 +176,10 @@ final class ShelfViewModel {
return items.first(where: { $0.uuid == uuid })
}

func fetchLinkPreviewIfNeeded(for item: ClipItem) {
linkFetcher.fetchIfNeeded(for: item, enabled: settings.fetchLinkPreviews)
}

func isSelected(_ item: ClipItem) -> Bool {
selection.selected.contains(item.uuid)
}
Expand Down