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
|
- 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
|
- 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
|
- 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
|
- Copy and paste actions with Accessibility permission fallback
|
||||||
- Image thumbnail cache with byte and file-count pruning
|
- 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
|
- 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.
|
5. Press `Esc` again and confirm the panel closes.
|
||||||
6. Reopen the panel, change sort segments, and confirm each segment updates results.
|
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.
|
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.
|
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.
|
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.
|
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 ?? []
|
cardViews.first?.debugCollectionMenuTitles ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var debugFirstCardCollectActionMenuTitles: [String] {
|
||||||
|
cardViews.first?.debugCollectActionMenuTitles ?? []
|
||||||
|
}
|
||||||
|
|
||||||
var debugFirstCardCaptureRuleMenuTitles: [String] {
|
var debugFirstCardCaptureRuleMenuTitles: [String] {
|
||||||
cardViews.first?.debugCaptureRuleMenuTitles ?? []
|
cardViews.first?.debugCaptureRuleMenuTitles ?? []
|
||||||
}
|
}
|
||||||
@@ -1788,6 +1792,10 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
return collectionMenu.items.map { $0.isSeparatorItem ? "-" : $0.title }
|
return collectionMenu.items.map { $0.isSeparatorItem ? "-" : $0.title }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var debugCollectActionMenuTitles: [String] {
|
||||||
|
collectionAssignmentMenu().items.map { $0.isSeparatorItem ? "-" : $0.title }
|
||||||
|
}
|
||||||
|
|
||||||
var debugCaptureRuleMenuTitles: [String] {
|
var debugCaptureRuleMenuTitles: [String] {
|
||||||
guard let rulesMenu = contextMenu().items.first(where: { $0.title == "Capture Rules" })?.submenu else {
|
guard let rulesMenu = contextMenu().items.first(where: { $0.title == "Capture Rules" })?.submenu else {
|
||||||
return []
|
return []
|
||||||
@@ -1859,6 +1867,12 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
|
|
||||||
private func addCollectionMenu(to menu: NSMenu) {
|
private func addCollectionMenu(to menu: NSMenu) {
|
||||||
let parent = NSMenuItem(title: "Add to Collection", action: nil, keyEquivalent: "")
|
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")
|
let submenu = NSMenu(title: "Add to Collection")
|
||||||
submenu.autoenablesItems = false
|
submenu.autoenablesItems = false
|
||||||
|
|
||||||
@@ -1884,8 +1898,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
submenu.addItem(remove)
|
submenu.addItem(remove)
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.addItem(parent)
|
return submenu
|
||||||
menu.setSubmenu(submenu, for: parent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addCaptureRulesMenu(to menu: NSMenu) {
|
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("doc.on.doc", toolTip: "Copy", action: #selector(copyFromMenu)),
|
||||||
cardActionButton(itemIsPinned ? "pin.slash" : "pin", toolTip: pinTitle, action: #selector(togglePinFromMenu))
|
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)))
|
actionRailButtons.append(cardActionButton("square.stack.3d.up", toolTip: itemIsStacked ? "Remove from Stack" : "Add to Stack", action: #selector(toggleStackFromMenu)))
|
||||||
if canEditText {
|
if canEditText {
|
||||||
actionRailButtons.append(cardActionButton("pencil", toolTip: "Edit", action: #selector(editTextFromMenu)))
|
actionRailButtons.append(cardActionButton("pencil", toolTip: "Edit", action: #selector(editTextFromMenu)))
|
||||||
@@ -2062,12 +2076,18 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
button.wantsLayer = true
|
button.wantsLayer = true
|
||||||
let size = isPrimary ? layout.primaryActionButtonSize : layout.actionButtonSize
|
let size = isPrimary ? layout.primaryActionButtonSize : layout.actionButtonSize
|
||||||
button.layer?.cornerRadius = size / 2
|
button.layer?.cornerRadius = size / 2
|
||||||
button.layer?.backgroundColor = isPrimary
|
if isPrimary {
|
||||||
? NSColor.controlAccentColor.cgColor
|
button.layer?.backgroundColor = NSColor.controlAccentColor.cgColor
|
||||||
: NSColor.white.withAlphaComponent(0.08).cgColor
|
button.contentTintColor = .white
|
||||||
button.contentTintColor = isPrimary
|
} else if toolTip == "Collect" {
|
||||||
? .white
|
button.layer?.backgroundColor = NSColor.systemGreen.withAlphaComponent(0.88).cgColor
|
||||||
: (toolTip == "Delete" ? NSColor.white.withAlphaComponent(0.48) : NSColor.white.withAlphaComponent(0.78))
|
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.toolTip = toolTip
|
||||||
button.setAccessibilityLabel(toolTip)
|
button.setAccessibilityLabel(toolTip)
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -2138,6 +2158,11 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
onTogglePin(index)
|
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) {
|
@objc private func assignToCollectionFromMenu(_ sender: NSMenuItem) {
|
||||||
guard let name = sender.representedObject as? String else { return }
|
guard let name = sender.representedObject as? String else { return }
|
||||||
onAssignCollection(index, name)
|
onAssignCollection(index, name)
|
||||||
|
|||||||
@@ -215,8 +215,8 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))
|
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))
|
||||||
drainMainQueue()
|
drainMainQueue()
|
||||||
|
|
||||||
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Add to Stack", "Edit", "Delete"])
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Add to Stack", "Edit", "Delete"])
|
||||||
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 182)
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 210)
|
||||||
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
|
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
|
||||||
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
|
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
|
||||||
|
|
||||||
@@ -225,8 +225,8 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
|
|
||||||
fixture.viewModel.selectFirstItem()
|
fixture.viewModel.selectFirstItem()
|
||||||
XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file)
|
XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file)
|
||||||
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Add to Stack", "Preview", "Open", "Reveal", "Delete"])
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Add to Stack", "Preview", "Open", "Reveal", "Delete"])
|
||||||
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 238)
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 266)
|
||||||
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
|
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
|
||||||
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
|
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
|
||||||
}
|
}
|
||||||
@@ -397,6 +397,10 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
fixture.view.debugFirstCardCollectionMenuTitles,
|
fixture.view.debugFirstCardCollectionMenuTitles,
|
||||||
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
|
["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(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardCaptureRuleMenuTitles,
|
fixture.view.debugFirstCardCaptureRuleMenuTitles,
|
||||||
["Ignore Ghostty", "Ignore Text Items"]
|
["Ignore Ghostty", "Ignore Text Items"]
|
||||||
@@ -433,7 +437,7 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
fixture.view.debugFirstCardMenuTitles,
|
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"]
|
["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() {
|
func testStackChipAppearsFiltersAndClearsWithStack() {
|
||||||
@@ -485,6 +489,10 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
fixture.view.debugFirstCardCollectionMenuTitles,
|
fixture.view.debugFirstCardCollectionMenuTitles,
|
||||||
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "Client Work", "-", "New Collection..."]
|
["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() {
|
func testBottomSafeInsetIsAppliedToPanelContent() {
|
||||||
|
|||||||
Reference in New Issue
Block a user