WIP: add visible collect action

This commit is contained in:
Akshay Kolli
2026-06-30 03:17:40 -07:00
parent 870e284fbd
commit 582d317d28
4 changed files with 48 additions and 15 deletions

View File

@@ -16,7 +16,7 @@ The project is intentionally dependency-light: Swift Package Manager, AppKit, Ca
- SQLite persistence with bounded history, pinned-item retention, and encrypted app-managed payloads
- Search with independent token matching, structured filters such as `app:Safari`, `type:image`, `date:2026-06-30`, and optional local OCR for copied images
- Sort modes for recent, most used, images, links, text, files, audio, and pinned items
- Custom named collections for organizing clips from the card context menu or by dragging cards onto collection chips
- Custom named collections for organizing clips from the card Collect control, context menu, or by dragging cards onto collection chips
- Copy and paste actions with Accessibility permission fallback
- Image thumbnail cache with byte and file-count pruning
- Configurable history length, cache limit, polling profile, ignored apps, content kinds, launch-at-login, Dock/menu-bar presence, and clear-on-quit behavior, with card-level capture rules for ignoring a source app or content type

View File

@@ -38,7 +38,7 @@ Use this checklist before a release or after changes to panel, pasteboard, setti
5. Press `Esc` again and confirm the panel closes.
6. Reopen the panel, change sort segments, and confirm each segment updates results.
7. Right-click a card, choose Add to Collection > New Collection..., enter `Client Work`, and confirm a Client Work chip appears with the item count.
8. Right-click another card and confirm Add to Collection offers Client Work as a reusable destination.
8. Select another card and confirm its Collect button offers Client Work as a reusable destination.
9. Select the Client Work chip and confirm the rail filters to assigned items; quit and reopen ClipBored and confirm the assignment persists.
10. Double-click an item and confirm it attempts to paste or falls back to copy without creating a duplicate history entry.
11. Right-click a card, use Capture Rules to ignore its source app, copy from that app again, and confirm the new item is skipped.

View File

@@ -1134,6 +1134,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
cardViews.first?.debugCollectionMenuTitles ?? []
}
var debugFirstCardCollectActionMenuTitles: [String] {
cardViews.first?.debugCollectActionMenuTitles ?? []
}
var debugFirstCardCaptureRuleMenuTitles: [String] {
cardViews.first?.debugCaptureRuleMenuTitles ?? []
}
@@ -1788,6 +1792,10 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return collectionMenu.items.map { $0.isSeparatorItem ? "-" : $0.title }
}
var debugCollectActionMenuTitles: [String] {
collectionAssignmentMenu().items.map { $0.isSeparatorItem ? "-" : $0.title }
}
var debugCaptureRuleMenuTitles: [String] {
guard let rulesMenu = contextMenu().items.first(where: { $0.title == "Capture Rules" })?.submenu else {
return []
@@ -1859,6 +1867,12 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private func addCollectionMenu(to menu: NSMenu) {
let parent = NSMenuItem(title: "Add to Collection", action: nil, keyEquivalent: "")
let submenu = collectionAssignmentMenu()
menu.addItem(parent)
menu.setSubmenu(submenu, for: parent)
}
private func collectionAssignmentMenu() -> NSMenu {
let submenu = NSMenu(title: "Add to Collection")
submenu.autoenablesItems = false
@@ -1884,8 +1898,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
submenu.addItem(remove)
}
menu.addItem(parent)
menu.setSubmenu(submenu, for: parent)
return submenu
}
private func addCaptureRulesMenu(to menu: NSMenu) {
@@ -2017,6 +2030,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
cardActionButton("doc.on.doc", toolTip: "Copy", action: #selector(copyFromMenu)),
cardActionButton(itemIsPinned ? "pin.slash" : "pin", toolTip: pinTitle, action: #selector(togglePinFromMenu))
]
actionRailButtons.append(cardActionButton("plus", toolTip: "Collect", action: #selector(showCollectionMenuFromAction(_:))))
actionRailButtons.append(cardActionButton("square.stack.3d.up", toolTip: itemIsStacked ? "Remove from Stack" : "Add to Stack", action: #selector(toggleStackFromMenu)))
if canEditText {
actionRailButtons.append(cardActionButton("pencil", toolTip: "Edit", action: #selector(editTextFromMenu)))
@@ -2062,12 +2076,18 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
button.wantsLayer = true
let size = isPrimary ? layout.primaryActionButtonSize : layout.actionButtonSize
button.layer?.cornerRadius = size / 2
button.layer?.backgroundColor = isPrimary
? NSColor.controlAccentColor.cgColor
: NSColor.white.withAlphaComponent(0.08).cgColor
button.contentTintColor = isPrimary
? .white
: (toolTip == "Delete" ? NSColor.white.withAlphaComponent(0.48) : NSColor.white.withAlphaComponent(0.78))
if isPrimary {
button.layer?.backgroundColor = NSColor.controlAccentColor.cgColor
button.contentTintColor = .white
} else if toolTip == "Collect" {
button.layer?.backgroundColor = NSColor.systemGreen.withAlphaComponent(0.88).cgColor
button.contentTintColor = .white
} else {
button.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.08).cgColor
button.contentTintColor = toolTip == "Delete"
? NSColor.white.withAlphaComponent(0.48)
: NSColor.white.withAlphaComponent(0.78)
}
button.toolTip = toolTip
button.setAccessibilityLabel(toolTip)
button.translatesAutoresizingMaskIntoConstraints = false
@@ -2138,6 +2158,11 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
onTogglePin(index)
}
@objc private func showCollectionMenuFromAction(_ sender: NSButton) {
let menu = collectionAssignmentMenu()
menu.popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.maxY + 4), in: sender)
}
@objc private func assignToCollectionFromMenu(_ sender: NSMenuItem) {
guard let name = sender.representedObject as? String else { return }
onAssignCollection(index, name)

View File

@@ -215,8 +215,8 @@ final class ClipboardPanelViewTests: XCTestCase {
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))
drainMainQueue()
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Add to Stack", "Edit", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 182)
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Add to Stack", "Edit", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 210)
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
@@ -225,8 +225,8 @@ final class ClipboardPanelViewTests: XCTestCase {
fixture.viewModel.selectFirstItem()
XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file)
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Add to Stack", "Preview", "Open", "Reveal", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 238)
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Add to Stack", "Preview", "Open", "Reveal", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 266)
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
}
@@ -397,6 +397,10 @@ final class ClipboardPanelViewTests: XCTestCase {
fixture.view.debugFirstCardCollectionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
)
XCTAssertEqual(
fixture.view.debugFirstCardCollectActionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
)
XCTAssertEqual(
fixture.view.debugFirstCardCaptureRuleMenuTitles,
["Ignore Ghostty", "Ignore Text Items"]
@@ -433,7 +437,7 @@ final class ClipboardPanelViewTests: XCTestCase {
fixture.view.debugFirstCardMenuTitles,
["Paste", "Copy", "Remove from Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack", "Edit", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
)
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Remove from Stack", "Edit", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Remove from Stack", "Edit", "Delete"])
}
func testStackChipAppearsFiltersAndClearsWithStack() {
@@ -485,6 +489,10 @@ final class ClipboardPanelViewTests: XCTestCase {
fixture.view.debugFirstCardCollectionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "Client Work", "-", "New Collection..."]
)
XCTAssertEqual(
fixture.view.debugFirstCardCollectActionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "Client Work", "-", "New Collection..."]
)
}
func testBottomSafeInsetIsAppliedToPanelContent() {