WIP: add collection-colored card headers

This commit is contained in:
Akshay Kolli
2026-06-30 03:36:36 -07:00
parent a2404ad4f9
commit 0b8e0d6be1
4 changed files with 136 additions and 31 deletions

View File

@@ -40,6 +40,56 @@ private struct ClipboardItemCardLayout: Equatable {
}
}
private enum ClipboardCollectionVisuals {
private static let customPalette: [NSColor] = [
NSColor(calibratedRed: 0.98, green: 0.30, blue: 0.32, alpha: 1),
NSColor(calibratedRed: 0.96, green: 0.64, blue: 0.00, alpha: 1),
NSColor(calibratedRed: 0.04, green: 0.47, blue: 0.95, alpha: 1),
NSColor(calibratedRed: 0.18, green: 0.72, blue: 0.34, alpha: 1),
NSColor(calibratedRed: 0.55, green: 0.35, blue: 0.88, alpha: 1),
NSColor(calibratedRed: 0.93, green: 0.12, blue: 0.34, alpha: 1),
NSColor(calibratedRed: 0.10, green: 0.62, blue: 0.72, alpha: 1),
NSColor(calibratedRed: 0.74, green: 0.42, blue: 0.16, alpha: 1)
]
static func color(for mode: ClipboardSortMode) -> NSColor {
switch mode {
case .mostRecent: return .secondaryLabelColor
case .mostUsed: return NSColor(calibratedRed: 0.58, green: 0.42, blue: 0.92, alpha: 1)
case .text: return NSColor(calibratedRed: 0.96, green: 0.64, blue: 0.00, alpha: 1)
case .links: return NSColor(calibratedRed: 0.02, green: 0.47, blue: 0.98, alpha: 1)
case .images: return NSColor(calibratedRed: 1.00, green: 0.22, blue: 0.25, alpha: 1)
case .audio: return NSColor(calibratedRed: 0.93, green: 0.12, blue: 0.34, alpha: 1)
case .files: return NSColor(calibratedRed: 0.11, green: 0.68, blue: 0.36, alpha: 1)
case .pinned: return NSColor(calibratedRed: 0.94, green: 0.12, blue: 0.48, alpha: 1)
}
}
static func color(forCollectionNamed name: String) -> NSColor {
switch name {
case "Useful Links":
return customPalette[0]
case "Important Notes":
return customPalette[1]
case "Code Snippets":
return customPalette[2]
case "Read Later":
return customPalette[3]
default:
return customPalette[stablePaletteIndex(for: name)]
}
}
private static func stablePaletteIndex(for name: String) -> Int {
var hash: UInt64 = 1_469_598_103_934_665_603
for scalar in name.lowercased().unicodeScalars {
hash ^= UInt64(scalar.value)
hash &*= 1_099_511_628_211
}
return Int(hash % UInt64(customPalette.count))
}
}
final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
private enum Metrics {
static let actionButtonSize: CGFloat = 30
@@ -522,31 +572,11 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
}
private func collectionColor(for mode: ClipboardSortMode) -> NSColor {
switch mode {
case .mostRecent: return .secondaryLabelColor
case .mostUsed: return NSColor(calibratedRed: 0.58, green: 0.42, blue: 0.92, alpha: 1)
case .text: return NSColor(calibratedRed: 0.96, green: 0.64, blue: 0.00, alpha: 1)
case .links: return NSColor(calibratedRed: 0.02, green: 0.47, blue: 0.98, alpha: 1)
case .images: return NSColor(calibratedRed: 1.00, green: 0.22, blue: 0.25, alpha: 1)
case .audio: return NSColor(calibratedRed: 0.93, green: 0.12, blue: 0.34, alpha: 1)
case .files: return NSColor(calibratedRed: 0.11, green: 0.68, blue: 0.36, alpha: 1)
case .pinned: return NSColor(calibratedRed: 0.94, green: 0.12, blue: 0.48, alpha: 1)
}
ClipboardCollectionVisuals.color(for: mode)
}
private func collectionColor(forCollectionNamed name: String) -> NSColor {
switch name {
case "Useful Links":
return NSColor(calibratedRed: 0.98, green: 0.30, blue: 0.32, alpha: 1)
case "Important Notes":
return NSColor(calibratedRed: 0.96, green: 0.64, blue: 0.00, alpha: 1)
case "Code Snippets":
return NSColor(calibratedRed: 0.04, green: 0.47, blue: 0.95, alpha: 1)
case "Read Later":
return NSColor(calibratedRed: 0.18, green: 0.72, blue: 0.34, alpha: 1)
default:
return NSColor(calibratedRed: 0.52, green: 0.42, blue: 0.86, alpha: 1)
}
ClipboardCollectionVisuals.color(forCollectionNamed: name)
}
private func applyCardDensity() {
@@ -639,7 +669,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
collectionNames: collectionNames,
isStacked: viewModel.isItemStacked(at: index),
stackCount: viewModel.stackCount,
canShowInClipboard: viewModel.canShowVisibleItemsInClipboard
canShowInClipboard: viewModel.canShowVisibleItemsInClipboard,
selectedCollectionName: viewModel.selectedCollectionName
)
card.onSelect = { [weak self] selected in
self?.viewModel.selectItem(at: selected)
@@ -1114,6 +1145,22 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
cardViews.map(\.debugHeaderBadgeSymbol)
}
var debugFirstCardHeaderTitle: String {
cardViews.first?.debugHeaderTitle ?? ""
}
var debugFirstCardHeaderSubtitle: String {
cardViews.first?.debugHeaderSubtitle ?? ""
}
var debugFirstCardHeaderColorHex: String {
cardViews.first?.debugHeaderColorHex ?? ""
}
var debugFirstCardFooterDetailText: String {
cardViews.first?.debugFooterDetailText ?? ""
}
var debugQuickPasteBadgeTexts: [String] {
cardViews.compactMap(\.debugQuickPasteBadgeText)
}
@@ -1199,6 +1246,12 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
return viewModel.collectionNames.compactMap { customCollectionButtons[$0]?.count }
}
var debugCustomCollectionColorHexes: [String: String] {
Dictionary(uniqueKeysWithValues: viewModel.collectionNames.map { name in
(name, ClipboardItemCardView.debugHex(ClipboardCollectionVisuals.color(forCollectionNamed: name)))
})
}
var debugStackChipIsVisible: Bool {
collectionStack.arrangedSubviews.contains(stackChip)
}
@@ -1643,6 +1696,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private let itemSourceAppName: String?
private let itemSourceAppBundleID: String?
private let itemCollectionName: String?
private let activeCollectionName: String?
private let activeCollectionColor: NSColor?
private let collectionNames: [String]
private let contentView = NSView()
private let footerDetailLabel = NSTextField(labelWithString: "")
@@ -1664,8 +1719,12 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
collectionNames: [String] = [],
isStacked: Bool = false,
stackCount: Int = 0,
canShowInClipboard: Bool = false
canShowInClipboard: Bool = false,
selectedCollectionName: String? = nil
) {
let normalizedItemCollection = ClipboardCollectionDefaults.normalizedName(item.collectionName)
let normalizedSelectedCollection = ClipboardCollectionDefaults.normalizedName(selectedCollectionName)
let activeCollection = normalizedSelectedCollection == normalizedItemCollection ? normalizedSelectedCollection : nil
self.index = index
self.itemID = item.id
self.layout = layout
@@ -1676,7 +1735,9 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
self.canShowInClipboard = canShowInClipboard
self.itemSourceAppName = Self.presentSourceText(item.sourceApp)
self.itemSourceAppBundleID = Self.presentSourceText(item.sourceAppBundleId)
self.itemCollectionName = ClipboardCollectionDefaults.normalizedName(item.collectionName)
self.itemCollectionName = normalizedItemCollection
self.activeCollectionName = activeCollection
self.activeCollectionColor = activeCollection.map(ClipboardCollectionVisuals.color(forCollectionNamed:))
self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) }
super.init(frame: .zero)
configure(item: item, thumbnail: thumbnail)
@@ -1806,6 +1867,9 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private(set) var debugPreviewSummary = ""
private(set) var debugPreviewStyle = ""
private(set) var debugHeaderBadgeSymbol = ""
private(set) var debugHeaderTitle = ""
private(set) var debugHeaderSubtitle = ""
private(set) var debugHeaderColorHex = ""
var debugMenuTitles: [String] {
contextMenu().items.map { $0.isSeparatorItem ? "-" : $0.title }
@@ -1852,9 +1916,21 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
quickPasteBadgeLabel?.stringValue
}
var debugFooterDetailText: String {
footerDetailLabel.stringValue
}
var debugItemID: UUID {
itemID
}
static func debugHex(_ color: NSColor) -> String {
let rgb = color.usingColorSpace(.deviceRGB) ?? color
let red = Int((rgb.redComponent * 255).rounded())
let green = Int((rgb.greenComponent * 255).rounded())
let blue = Int((rgb.blueComponent * 255).rounded())
return String(format: "#%02X%02X%02X", red, green, blue)
}
#endif
private func contextMenu() -> NSMenu {
@@ -2242,6 +2318,9 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
debugPreviewSummary = "\(titleText(for: item))|\(previewText(for: item))|\(detailMetricText(for: item))"
debugPreviewStyle = previewStyle(for: item, thumbnail: thumbnail)
debugHeaderBadgeSymbol = headerBadgeSymbol(for: item.kind)
debugHeaderTitle = headerTitle(for: item)
debugHeaderSubtitle = headerSubtitle(for: item)
debugHeaderColorHex = Self.debugHex(headerColor(for: item))
#endif
wantsLayer = true
@@ -2302,17 +2381,17 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private func headerView(for item: ClipboardItem) -> NSView {
let header = NSView()
header.wantsLayer = true
header.layer?.backgroundColor = accentColor(for: item.kind).cgColor
header.layer?.backgroundColor = headerColor(for: item).cgColor
header.heightAnchor.constraint(equalToConstant: layout.headerHeight).isActive = true
let kind = NSTextField(labelWithString: kindLabel(for: item.kind))
let kind = NSTextField(labelWithString: headerTitle(for: item))
kind.font = .systemFont(ofSize: layout.isCompact ? 15 : 16, weight: .bold)
kind.textColor = .white
kind.lineBreakMode = .byTruncatingTail
kind.maximumNumberOfLines = 1
kind.toolTip = kind.stringValue
let source = NSTextField(labelWithString: Self.relativeDateText(for: item.createdAt))
let source = NSTextField(labelWithString: headerSubtitle(for: item))
source.font = .systemFont(ofSize: layout.isCompact ? 10 : 11, weight: .regular)
source.textColor = NSColor.white.withAlphaComponent(0.72)
source.lineBreakMode = .byTruncatingTail
@@ -2382,6 +2461,20 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return header
}
private func headerTitle(for item: ClipboardItem) -> String {
activeCollectionName ?? kindLabel(for: item.kind)
}
private func headerSubtitle(for item: ClipboardItem) -> String {
let relativeDate = Self.relativeDateText(for: item.createdAt)
guard activeCollectionName != nil else { return relativeDate }
return "\(kindLabel(for: item.kind)) - \(relativeDate)"
}
private func headerColor(for item: ClipboardItem) -> NSColor {
activeCollectionColor ?? accentColor(for: item.kind)
}
private func quickPasteBadge() -> NSTextField? {
guard index < 9 else { return nil }
let label = NSTextField(labelWithString: "\(index + 1)")
@@ -2839,7 +2932,9 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
source.toolTip = source.stringValue
let detailText = detailMetricText(for: item)
if let collectionName = item.collectionName?.clipboardTrimmed, !collectionName.isEmpty {
if activeCollectionName == nil,
let collectionName = item.collectionName?.clipboardTrimmed,
!collectionName.isEmpty {
footerDetailLabel.stringValue = "\(collectionName) - \(detailText)"
} else {
footerDetailLabel.stringValue = detailText