This commit is contained in:
@@ -32,7 +32,7 @@ final class ClipboardCacheService {
|
||||
try? fileManager.createDirectory(at: attachmentDirectory, withIntermediateDirectories: true)
|
||||
hardenDirectory(imageDirectory)
|
||||
hardenDirectory(attachmentDirectory)
|
||||
clearTemporaryPreviews()
|
||||
clearTemporaryPreviews(wait: true)
|
||||
}
|
||||
|
||||
func cacheImage(_ image: NSImage, id: UUID) -> (full: String, thumb: String)? {
|
||||
|
||||
@@ -77,27 +77,19 @@ final class PasteActionService {
|
||||
}
|
||||
|
||||
func pastePlainText(_ item: ClipboardItem, targetApp: NSRunningApplication?) -> PasteActionResult {
|
||||
guard writePlainTextToPasteboard(item) else {
|
||||
guard let text = plainText(for: item), writePlainTextToPasteboard(text) else {
|
||||
return .failed("Could not write plain text to clipboard.")
|
||||
}
|
||||
|
||||
guard let targetApp,
|
||||
!targetApp.isTerminated else {
|
||||
return .copiedPlainText
|
||||
return completePlainTextPaste(targetApp: targetApp)
|
||||
}
|
||||
|
||||
func pastePlainText(_ value: String, targetApp: NSRunningApplication?) -> PasteActionResult {
|
||||
guard writePlainTextToPasteboard(value) else {
|
||||
return .failed("Could not write plain text to clipboard.")
|
||||
}
|
||||
|
||||
guard accessibilityPermissionProvider() else {
|
||||
return .copiedPlainTextNeedsPermission
|
||||
}
|
||||
|
||||
guard targetActivator(targetApp) else {
|
||||
return .copiedPlainText
|
||||
}
|
||||
|
||||
keyboardPasteScheduler { [weak self] in
|
||||
self?.pasteViaKeyboard()
|
||||
}
|
||||
return .pastedPlainText
|
||||
return completePlainTextPaste(targetApp: targetApp)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -110,6 +102,11 @@ final class PasteActionService {
|
||||
writePlainTextToPasteboard(item) ? .copiedPlainText : .failed("Could not write plain text to clipboard.")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func copyPlainText(_ value: String) -> PasteActionResult {
|
||||
writePlainTextToPasteboard(value) ? .copiedPlainText : .failed("Could not write plain text to clipboard.")
|
||||
}
|
||||
|
||||
func pasteboardWriters(for item: ClipboardItem) -> [NSPasteboardWriting] {
|
||||
switch item.kind {
|
||||
case .image:
|
||||
@@ -269,6 +266,12 @@ final class PasteActionService {
|
||||
@discardableResult
|
||||
func writePlainTextToPasteboard(_ item: ClipboardItem) -> Bool {
|
||||
guard let text = plainText(for: item) else { return false }
|
||||
return writePlainTextToPasteboard(text)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func writePlainTextToPasteboard(_ text: String) -> Bool {
|
||||
guard !text.clipboardTrimmed.isEmpty else { return false }
|
||||
let board = NSPasteboard.general
|
||||
board.clearContents()
|
||||
let didWrite = board.setString(text, forType: .string)
|
||||
@@ -278,6 +281,26 @@ final class PasteActionService {
|
||||
return didWrite
|
||||
}
|
||||
|
||||
private func completePlainTextPaste(targetApp: NSRunningApplication?) -> PasteActionResult {
|
||||
guard let targetApp,
|
||||
!targetApp.isTerminated else {
|
||||
return .copiedPlainText
|
||||
}
|
||||
|
||||
guard accessibilityPermissionProvider() else {
|
||||
return .copiedPlainTextNeedsPermission
|
||||
}
|
||||
|
||||
guard targetActivator(targetApp) else {
|
||||
return .copiedPlainText
|
||||
}
|
||||
|
||||
keyboardPasteScheduler { [weak self] in
|
||||
self?.pasteViaKeyboard()
|
||||
}
|
||||
return .pastedPlainText
|
||||
}
|
||||
|
||||
private func stringPasteboardItem(_ value: String) -> NSPasteboardItem {
|
||||
let pasteboardItem = NSPasteboardItem()
|
||||
pasteboardItem.setString(value, forType: .string)
|
||||
|
||||
@@ -585,6 +585,12 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
stackChip.onCopyStackNext = { [weak self] in
|
||||
self?.viewModel.copyNextStackItem()
|
||||
}
|
||||
stackChip.onPasteStackText = { [weak self] in
|
||||
self?.viewModel.pasteStackAsText()
|
||||
}
|
||||
stackChip.onCopyStackText = { [weak self] in
|
||||
self?.viewModel.copyStackAsText()
|
||||
}
|
||||
stackChip.onClearStack = { [weak self] in
|
||||
self?.viewModel.clearStack()
|
||||
}
|
||||
@@ -806,6 +812,12 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
card.onCopyStackNext = { [weak self] in
|
||||
self?.viewModel.copyNextStackItem()
|
||||
}
|
||||
card.onPasteStackText = { [weak self] in
|
||||
self?.viewModel.pasteStackAsText()
|
||||
}
|
||||
card.onCopyStackText = { [weak self] in
|
||||
self?.viewModel.copyStackAsText()
|
||||
}
|
||||
card.onClearStack = { [weak self] in
|
||||
self?.viewModel.clearStack()
|
||||
}
|
||||
@@ -2141,6 +2153,8 @@ private final class CollectionChipView: NSView {
|
||||
var onAddVisibleToStack: (() -> Void)?
|
||||
var onPasteStackNext: (() -> Void)?
|
||||
var onCopyStackNext: (() -> Void)?
|
||||
var onPasteStackText: (() -> Void)?
|
||||
var onCopyStackText: (() -> Void)?
|
||||
var onClearStack: (() -> Void)?
|
||||
var onEdit: (() -> Void)?
|
||||
var onDelete: (() -> Void)?
|
||||
@@ -2348,6 +2362,8 @@ private final class CollectionChipView: NSView {
|
||||
onAddVisibleToStack != nil
|
||||
|| onPasteStackNext != nil
|
||||
|| onCopyStackNext != nil
|
||||
|| onPasteStackText != nil
|
||||
|| onCopyStackText != nil
|
||||
|| onClearStack != nil
|
||||
|| onEdit != nil
|
||||
|| onDelete != nil
|
||||
@@ -2371,6 +2387,16 @@ private final class CollectionChipView: NSView {
|
||||
item.target = self
|
||||
menu.addItem(item)
|
||||
}
|
||||
if onPasteStackText != nil {
|
||||
let item = NSMenuItem(title: "Paste Stack as Text", action: #selector(pasteStackTextFromMenu), keyEquivalent: "")
|
||||
item.target = self
|
||||
menu.addItem(item)
|
||||
}
|
||||
if onCopyStackText != nil {
|
||||
let item = NSMenuItem(title: "Copy Stack as Text", action: #selector(copyStackTextFromMenu), keyEquivalent: "")
|
||||
item.target = self
|
||||
menu.addItem(item)
|
||||
}
|
||||
if onClearStack != nil {
|
||||
let item = NSMenuItem(title: "Clear Stack", action: #selector(clearStackFromMenu), keyEquivalent: "")
|
||||
item.target = self
|
||||
@@ -2407,6 +2433,14 @@ private final class CollectionChipView: NSView {
|
||||
onCopyStackNext?()
|
||||
}
|
||||
|
||||
@objc private func pasteStackTextFromMenu() {
|
||||
onPasteStackText?()
|
||||
}
|
||||
|
||||
@objc private func copyStackTextFromMenu() {
|
||||
onCopyStackText?()
|
||||
}
|
||||
|
||||
@objc private func clearStackFromMenu() {
|
||||
onClearStack?()
|
||||
}
|
||||
@@ -2590,6 +2624,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
var onAddVisibleToStack: () -> Void = {}
|
||||
var onPasteStackNext: () -> Void = {}
|
||||
var onCopyStackNext: () -> Void = {}
|
||||
var onPasteStackText: () -> Void = {}
|
||||
var onCopyStackText: () -> Void = {}
|
||||
var onClearStack: () -> Void = {}
|
||||
var onShowInClipboard: (Int) -> Void = { _ in }
|
||||
var onRename: (Int) -> Void = { _ in }
|
||||
@@ -2972,6 +3008,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
if stackCount > 0 {
|
||||
addMenuItem("Paste Stack Next", action: #selector(pasteStackNextFromMenu), to: menu)
|
||||
addMenuItem("Copy Stack Next", action: #selector(copyStackNextFromMenu), to: menu)
|
||||
addMenuItem("Paste Stack as Text", action: #selector(pasteStackTextFromMenu), to: menu)
|
||||
addMenuItem("Copy Stack as Text", action: #selector(copyStackTextFromMenu), to: menu)
|
||||
addMenuItem("Clear Stack", action: #selector(clearStackFromMenu), to: menu)
|
||||
}
|
||||
if canEditText {
|
||||
@@ -3395,6 +3433,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
onCopyStackNext()
|
||||
}
|
||||
|
||||
@objc private func pasteStackTextFromMenu() {
|
||||
onPasteStackText()
|
||||
}
|
||||
|
||||
@objc private func copyStackTextFromMenu() {
|
||||
onCopyStackText()
|
||||
}
|
||||
|
||||
@objc private func clearStackFromMenu() {
|
||||
onClearStack()
|
||||
}
|
||||
|
||||
@@ -373,6 +373,29 @@ final class ClipboardPanelViewModel {
|
||||
handleStackActionResult(result, item: item)
|
||||
}
|
||||
|
||||
func copyStackAsText() {
|
||||
guard let package = stackPlainTextPackage() else {
|
||||
statusMessage = "Stack has no text to copy"
|
||||
return
|
||||
}
|
||||
|
||||
let result = pasteService.copyPlainText(package.text)
|
||||
handleStackPlainTextActionResult(result, items: package.items)
|
||||
}
|
||||
|
||||
func pasteStackAsText() {
|
||||
guard let package = stackPlainTextPackage() else {
|
||||
statusMessage = "Stack has no text to paste"
|
||||
return
|
||||
}
|
||||
|
||||
let result = pasteService.pastePlainText(package.text, targetApp: targetApplicationProvider())
|
||||
if case .pastedPlainText = result {
|
||||
willPasteToTarget()
|
||||
}
|
||||
handleStackPlainTextActionResult(result, items: package.items)
|
||||
}
|
||||
|
||||
func pasteboardWriters(forItemAt index: Int) -> [NSPasteboardWriting] {
|
||||
guard index >= 0 && index < visibleItems.count else { return [] }
|
||||
return pasteService.pasteboardWriters(for: visibleItems[index])
|
||||
@@ -659,6 +682,20 @@ final class ClipboardPanelViewModel {
|
||||
return items.first { $0.id == id }
|
||||
}
|
||||
|
||||
private func stackPlainTextPackage() -> (text: String, items: [ClipboardItem])? {
|
||||
pruneStackItems()
|
||||
let pairs: [(item: ClipboardItem, text: String)] = stackItemIDs.compactMap { id in
|
||||
guard let item = items.first(where: { $0.id == id }),
|
||||
let text = pasteService.plainText(for: item)?.clipboardTrimmed,
|
||||
!text.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return (item, text)
|
||||
}
|
||||
guard !pairs.isEmpty else { return nil }
|
||||
return (pairs.map(\.text).joined(separator: "\n\n"), pairs.map(\.item))
|
||||
}
|
||||
|
||||
private func handleStackActionResult(_ result: PasteActionService.PasteActionResult, item: ClipboardItem) {
|
||||
if case .failed(let message) = result {
|
||||
statusMessage = message
|
||||
@@ -681,6 +718,34 @@ final class ClipboardPanelViewModel {
|
||||
settings.setPasteStatus(message: statusMessage)
|
||||
}
|
||||
|
||||
private func handleStackPlainTextActionResult(_ result: PasteActionService.PasteActionResult, items: [ClipboardItem]) {
|
||||
if case .failed(let message) = result {
|
||||
statusMessage = message
|
||||
return
|
||||
}
|
||||
|
||||
for item in items {
|
||||
store.markUsed(item.id)
|
||||
consumeStackItem(item.id)
|
||||
}
|
||||
if stackItemIDs.isEmpty {
|
||||
isStackFilterSelected = false
|
||||
}
|
||||
selectedItemID = items.first?.id
|
||||
let noun = items.count == 1 ? "clip" : "clips"
|
||||
switch result {
|
||||
case .pastedPlainText:
|
||||
statusMessage = "Pasted \(items.count) Stack \(noun) as Text"
|
||||
case .copiedPlainTextNeedsPermission:
|
||||
statusMessage = "Copied \(items.count) Stack \(noun) as Text. Grant Accessibility access to paste automatically."
|
||||
case .copiedPlainText:
|
||||
statusMessage = "Copied \(items.count) Stack \(noun) as Text"
|
||||
default:
|
||||
statusMessage = result.message
|
||||
}
|
||||
settings.setPasteStatus(message: statusMessage)
|
||||
}
|
||||
|
||||
private func consumeStackItem(_ id: UUID) {
|
||||
guard let index = stackItemIDs.firstIndex(of: id) else { return }
|
||||
stackItemIDs.remove(at: index)
|
||||
|
||||
Reference in New Issue
Block a user