WIP: add visible collect action
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user