WIP: add video clip support
This commit is contained in:
@@ -11,6 +11,7 @@ enum ClipboardItemKind: Int {
|
||||
case audio
|
||||
case color
|
||||
case code
|
||||
case video
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
@@ -24,6 +25,7 @@ enum ClipboardItemKind: Int {
|
||||
case .audio: return "audio"
|
||||
case .color: return "color"
|
||||
case .code: return "code"
|
||||
case .video: return "video"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +33,7 @@ enum ClipboardItemKind: Int {
|
||||
extension ClipboardItemKind {
|
||||
var canOpen: Bool {
|
||||
switch self {
|
||||
case .url, .file, .image, .pdf, .audio:
|
||||
case .url, .file, .image, .pdf, .audio, .video:
|
||||
return true
|
||||
case .text, .richText, .unknown, .color, .code:
|
||||
return false
|
||||
@@ -40,7 +42,7 @@ extension ClipboardItemKind {
|
||||
|
||||
var canReveal: Bool {
|
||||
switch self {
|
||||
case .file, .image, .pdf, .audio:
|
||||
case .file, .image, .pdf, .audio, .video:
|
||||
return true
|
||||
case .text, .richText, .unknown, .url, .color, .code:
|
||||
return false
|
||||
@@ -49,7 +51,7 @@ extension ClipboardItemKind {
|
||||
|
||||
var hasManagedCacheReference: Bool {
|
||||
switch self {
|
||||
case .url, .image, .pdf, .audio, .richText:
|
||||
case .url, .image, .pdf, .audio, .richText, .video:
|
||||
return true
|
||||
case .text, .file, .unknown, .color, .code:
|
||||
return false
|
||||
@@ -68,8 +70,9 @@ enum ClipboardSortMode: Int {
|
||||
case audio
|
||||
case colors
|
||||
case code
|
||||
case videos
|
||||
|
||||
static let allCases: [ClipboardSortMode] = [.mostRecent, .mostUsed, .text, .links, .images, .colors, .audio, .files, .pinned, .code]
|
||||
static let allCases: [ClipboardSortMode] = [.mostRecent, .mostUsed, .text, .links, .images, .colors, .audio, .videos, .files, .pinned, .code]
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
@@ -83,6 +86,7 @@ enum ClipboardSortMode: Int {
|
||||
case .audio: return "Audio"
|
||||
case .colors: return "Colors"
|
||||
case .code: return "Code"
|
||||
case .videos: return "Videos"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +160,7 @@ struct ClipboardItem {
|
||||
case .audio: return "audio sound"
|
||||
case .color: return "color swatch hex"
|
||||
case .code: return "code snippet source programming"
|
||||
case .video: return "video movie mp4 quicktime"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
62
sources/clipbored/models/VideoPayload.swift
Normal file
62
sources/clipbored/models/VideoPayload.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
enum VideoPayload {
|
||||
static let pasteboardTypes: [NSPasteboard.PasteboardType] = [
|
||||
NSPasteboard.PasteboardType(rawValue: UTType.mpeg4Movie.identifier),
|
||||
NSPasteboard.PasteboardType(rawValue: UTType.quickTimeMovie.identifier),
|
||||
NSPasteboard.PasteboardType(rawValue: UTType.movie.identifier),
|
||||
NSPasteboard.PasteboardType(rawValue: UTType.video.identifier),
|
||||
NSPasteboard.PasteboardType(rawValue: "com.apple.m4v-video")
|
||||
]
|
||||
|
||||
static func data(from pasteboard: NSPasteboard) -> (data: Data, type: NSPasteboard.PasteboardType)? {
|
||||
for type in pasteboardTypes {
|
||||
if let data = pasteboard.data(forType: type), !data.isEmpty {
|
||||
return (data, type)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func fileExtension(for type: NSPasteboard.PasteboardType) -> String {
|
||||
switch type.rawValue {
|
||||
case UTType.mpeg4Movie.identifier:
|
||||
return "mp4"
|
||||
case UTType.quickTimeMovie.identifier, UTType.movie.identifier, UTType.video.identifier:
|
||||
return "mov"
|
||||
case "com.apple.m4v-video":
|
||||
return "m4v"
|
||||
default:
|
||||
return "mov"
|
||||
}
|
||||
}
|
||||
|
||||
static func pasteboardType(forPath path: String) -> NSPasteboard.PasteboardType {
|
||||
switch URL(fileURLWithPath: path).pathExtension.lowercased() {
|
||||
case "mp4":
|
||||
return NSPasteboard.PasteboardType(rawValue: UTType.mpeg4Movie.identifier)
|
||||
case "m4v":
|
||||
return NSPasteboard.PasteboardType(rawValue: "com.apple.m4v-video")
|
||||
case "mov", "qt":
|
||||
return NSPasteboard.PasteboardType(rawValue: UTType.quickTimeMovie.identifier)
|
||||
default:
|
||||
return NSPasteboard.PasteboardType(rawValue: UTType.movie.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
static func displayTitle(byteCount: Int) -> String {
|
||||
"Video (\(ByteCountFormatter.string(fromByteCount: Int64(byteCount), countStyle: .file)))"
|
||||
}
|
||||
|
||||
static func fileExtension(from path: String) -> String {
|
||||
let value = URL(fileURLWithPath: path).pathExtension.clipboardTrimmed
|
||||
return value.isEmpty ? "mov" : value.lowercased()
|
||||
}
|
||||
|
||||
static func kindText(from path: String) -> String {
|
||||
let value = fileExtension(from: path)
|
||||
return value.isEmpty ? "Video" : value.uppercased()
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,10 @@ final class ClipboardCacheService {
|
||||
cacheAttachment(data, id: id, fileExtension: "sound")
|
||||
}
|
||||
|
||||
func cacheVideo(_ data: Data, id: UUID, fileExtension: String) -> String? {
|
||||
cacheAttachment(data, id: id, fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
func cacheRichText(_ data: Data, id: UUID) -> String? {
|
||||
cacheAttachment(data, id: id, fileExtension: "rtf")
|
||||
}
|
||||
@@ -105,7 +109,7 @@ final class ClipboardCacheService {
|
||||
case .file:
|
||||
return filePreviewThumbnail(for: item.payload)
|
||||
|
||||
case .text, .unknown, .audio, .richText, .color, .code:
|
||||
case .text, .unknown, .audio, .richText, .color, .code, .video:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -174,6 +178,9 @@ final class ClipboardCacheService {
|
||||
case .audio:
|
||||
guard let data = data(for: item.payload) else { return nil }
|
||||
return writeTemporaryCopy(data: data, id: item.id, fileExtension: "sound")
|
||||
case .video:
|
||||
guard let data = data(for: item.payload) else { return nil }
|
||||
return writeTemporaryCopy(data: data, id: item.id, fileExtension: VideoPayload.fileExtension(from: item.payload))
|
||||
case .richText:
|
||||
guard let data = data(for: item.payload) else { return nil }
|
||||
return writeTemporaryCopy(data: data, id: item.id, fileExtension: "rtf")
|
||||
@@ -197,7 +204,7 @@ final class ClipboardCacheService {
|
||||
case .url:
|
||||
guard let data = webLocationData(for: item.payload) else { return nil }
|
||||
return writeTemporaryCopy(data: data, id: item.id, fileExtension: "webloc")
|
||||
case .image, .pdf, .audio, .richText:
|
||||
case .image, .pdf, .audio, .richText, .video:
|
||||
return temporaryReadableURL(for: item)
|
||||
}
|
||||
}
|
||||
@@ -212,7 +219,7 @@ final class ClipboardCacheService {
|
||||
if let thumbnailPath = item.thumbnailPath {
|
||||
_ = self.data(for: thumbnailPath)
|
||||
}
|
||||
if (item.kind == .pdf || item.kind == .audio || item.kind == .richText), self.isManagedAttachment(path: item.payload) {
|
||||
if (item.kind == .pdf || item.kind == .audio || item.kind == .richText || item.kind == .video), self.isManagedAttachment(path: item.payload) {
|
||||
_ = self.data(for: item.payload)
|
||||
}
|
||||
}
|
||||
@@ -230,7 +237,7 @@ final class ClipboardCacheService {
|
||||
try? self.fileManager.removeItem(atPath: path)
|
||||
self.thumbnailCache.removeObject(forKey: NSString(string: path))
|
||||
}
|
||||
if (item.kind == .pdf || item.kind == .audio || item.kind == .richText), self.isManagedAttachment(path: item.payload) {
|
||||
if (item.kind == .pdf || item.kind == .audio || item.kind == .richText || item.kind == .video), self.isManagedAttachment(path: item.payload) {
|
||||
try? self.fileManager.removeItem(atPath: item.payload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +222,15 @@ final class ClipboardMonitorService {
|
||||
return pdfItem
|
||||
}
|
||||
|
||||
if isIgnored(.video), hasVideo(on: pasteboard) {
|
||||
reportReadFailureStatus(ignoredKindMessage(.video))
|
||||
return nil
|
||||
}
|
||||
|
||||
if let videoItem = itemFromVideo(pasteboard, sourceApp: source.name, sourceBundleId: source.bundleId) {
|
||||
return videoItem
|
||||
}
|
||||
|
||||
if isIgnored(.audio), hasAudio(on: pasteboard) {
|
||||
reportReadFailureStatus(ignoredKindMessage(.audio))
|
||||
return nil
|
||||
@@ -440,6 +449,10 @@ final class ClipboardMonitorService {
|
||||
pasteboard.data(forType: .sound) != nil
|
||||
}
|
||||
|
||||
private func hasVideo(on pasteboard: NSPasteboard) -> Bool {
|
||||
VideoPayload.data(from: pasteboard) != nil
|
||||
}
|
||||
|
||||
private func hasColor(on pasteboard: NSPasteboard) -> Bool {
|
||||
NSColor(from: pasteboard) != nil
|
||||
}
|
||||
@@ -511,6 +524,32 @@ final class ClipboardMonitorService {
|
||||
)
|
||||
}
|
||||
|
||||
private func itemFromVideo(_ pasteboard: NSPasteboard, sourceApp: String?, sourceBundleId: String?) -> ClipboardItem? {
|
||||
guard let video = VideoPayload.data(from: pasteboard) else { return nil }
|
||||
let id = UUID()
|
||||
let hash = store.hashString(video.data.base64EncodedString())
|
||||
guard let path = cacheService.cacheVideo(video.data, id: id, fileExtension: VideoPayload.fileExtension(for: video.type)) else {
|
||||
reportReadFailureStatus("Failed to cache video for clipboard history.")
|
||||
return nil
|
||||
}
|
||||
|
||||
return ClipboardItem(
|
||||
id: id,
|
||||
kind: .video,
|
||||
displayText: VideoPayload.displayTitle(byteCount: video.data.count),
|
||||
payload: path,
|
||||
payloadHash: hash,
|
||||
createdAt: Date(),
|
||||
lastUsedAt: Date(),
|
||||
useCount: 1,
|
||||
sourceApp: sourceApp,
|
||||
imagePath: nil,
|
||||
thumbnailPath: nil,
|
||||
isPinned: false,
|
||||
sourceAppBundleId: sourceBundleId
|
||||
)
|
||||
}
|
||||
|
||||
private func itemFromColor(_ pasteboard: NSPasteboard, sourceApp: String?, sourceBundleId: String?) -> ClipboardItem? {
|
||||
guard let color = NSColor(from: pasteboard) else { return nil }
|
||||
guard let hex = ColorPayload.hexString(from: color) else {
|
||||
|
||||
@@ -130,6 +130,13 @@ final class PasteActionService {
|
||||
pasteboardItem.setString(dragLabel(for: item), forType: .string)
|
||||
return [pasteboardItem]
|
||||
|
||||
case .video:
|
||||
guard let data = cacheService.data(for: item.payload) else { return [] }
|
||||
let pasteboardItem = NSPasteboardItem()
|
||||
pasteboardItem.setData(data, forType: VideoPayload.pasteboardType(forPath: item.payload))
|
||||
pasteboardItem.setString(dragLabel(for: item), forType: .string)
|
||||
return [pasteboardItem]
|
||||
|
||||
case .color:
|
||||
guard let color = ColorPayload.color(from: item.payload) else { return [] }
|
||||
return [color]
|
||||
@@ -188,6 +195,10 @@ final class PasteActionService {
|
||||
guard let data = cacheService.data(for: item.payload) else { return false }
|
||||
board.clearContents()
|
||||
didWrite = board.setData(data, forType: .sound)
|
||||
case .video:
|
||||
guard let data = cacheService.data(for: item.payload) else { return false }
|
||||
board.clearContents()
|
||||
didWrite = board.setData(data, forType: VideoPayload.pasteboardType(forPath: item.payload))
|
||||
case .color:
|
||||
guard let color = ColorPayload.color(from: item.payload) else { return false }
|
||||
board.clearContents()
|
||||
@@ -248,7 +259,7 @@ final class PasteActionService {
|
||||
return nonEmptyPlainText(richTextFallbackPlainString(for: item))
|
||||
case .image:
|
||||
return nonEmptyPlainText(item.ocrText) ?? nonEmptyPlainText(item.displayText)
|
||||
case .pdf, .audio:
|
||||
case .pdf, .audio, .video:
|
||||
return nonEmptyPlainText(item.displayText)
|
||||
case .color:
|
||||
return nonEmptyPlainText(ColorPayload.displayHex(from: item.payload)) ?? nonEmptyPlainText(item.displayText)
|
||||
|
||||
@@ -64,6 +64,7 @@ private enum ClipboardCollectionVisuals {
|
||||
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)
|
||||
case .code: return NSColor(calibratedRed: 0.25, green: 0.38, blue: 0.78, alpha: 1)
|
||||
case .videos: return NSColor(calibratedRed: 0.43, green: 0.32, blue: 0.94, alpha: 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,6 +645,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
case .files: return "Files"
|
||||
case .pinned: return "Pinned"
|
||||
case .code: return "Code"
|
||||
case .videos: return "Videos"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,6 +661,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
case .files: return "doc.fill"
|
||||
case .pinned: return "pin.fill"
|
||||
case .code: return "chevron.left.forwardslash.chevron.right"
|
||||
case .videos: return "film"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,6 +1162,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
return ("No text clips yet", "Copied text and rich text appear here.")
|
||||
case .code:
|
||||
return ("No code snippets yet", "Copied code snippets appear here.")
|
||||
case .videos:
|
||||
return ("No videos yet", "Copied movie and video clips appear here.")
|
||||
case .files:
|
||||
return ("No files yet", "Copied files and PDFs appear here.")
|
||||
case .audio:
|
||||
@@ -1968,7 +1973,7 @@ private enum ClipboardItemDragPasteboard {
|
||||
.sound,
|
||||
.rtf,
|
||||
.color
|
||||
]
|
||||
] + VideoPayload.pasteboardTypes
|
||||
}
|
||||
|
||||
private func shelfSearchText(from event: NSEvent) -> String? {
|
||||
@@ -3100,7 +3105,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
|
||||
private var canOpen: Bool {
|
||||
switch itemKind {
|
||||
case .url, .file, .image, .pdf, .audio:
|
||||
case .url, .file, .image, .pdf, .audio, .video:
|
||||
return true
|
||||
case .text, .richText, .unknown, .color, .code:
|
||||
return false
|
||||
@@ -3109,7 +3114,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
|
||||
private var canPreview: Bool {
|
||||
switch itemKind {
|
||||
case .text, .url, .image, .richText, .file, .pdf, .audio, .unknown, .color, .code:
|
||||
case .text, .url, .image, .richText, .file, .pdf, .audio, .unknown, .color, .code, .video:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -3120,7 +3125,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
|
||||
private var canPlainText: Bool {
|
||||
switch itemKind {
|
||||
case .url, .image, .richText, .file, .pdf, .audio, .color:
|
||||
case .url, .image, .richText, .file, .pdf, .audio, .color, .video:
|
||||
return true
|
||||
case .text, .unknown, .code:
|
||||
return false
|
||||
@@ -3129,7 +3134,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
|
||||
private var canReveal: Bool {
|
||||
switch itemKind {
|
||||
case .file, .image, .pdf, .audio:
|
||||
case .file, .image, .pdf, .audio, .video:
|
||||
return true
|
||||
case .text, .richText, .url, .unknown, .color, .code:
|
||||
return false
|
||||
@@ -3710,6 +3715,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
return filePreviewView(for: item, thumbnail: thumbnail)
|
||||
case .audio:
|
||||
return audioPreviewView(for: item)
|
||||
case .video:
|
||||
return videoPreviewView(for: item)
|
||||
case .color:
|
||||
return colorPreviewView(for: item)
|
||||
case .code:
|
||||
@@ -4149,6 +4156,78 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
return container
|
||||
}
|
||||
|
||||
private func videoPreviewView(for item: ClipboardItem) -> NSView {
|
||||
let container = NSView()
|
||||
container.wantsLayer = true
|
||||
container.layer?.backgroundColor = accentColor(for: item.kind).withAlphaComponent(0.10).cgColor
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let frame = NSView()
|
||||
frame.wantsLayer = true
|
||||
frame.layer?.cornerRadius = 14
|
||||
frame.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.78).cgColor
|
||||
frame.layer?.borderWidth = 1
|
||||
frame.layer?.borderColor = NSColor.white.withAlphaComponent(0.18).cgColor
|
||||
frame.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let film = headerIcon("film", color: .white)
|
||||
film.translatesAutoresizingMaskIntoConstraints = false
|
||||
let play = headerIcon("play.fill", color: .white)
|
||||
play.translatesAutoresizingMaskIntoConstraints = false
|
||||
frame.addSubview(film)
|
||||
frame.addSubview(play)
|
||||
|
||||
let extensionPill = capsuleLabel(VideoPayload.kindText(from: item.payload), color: accentColor(for: item.kind))
|
||||
extensionPill.translatesAutoresizingMaskIntoConstraints = false
|
||||
frame.addSubview(extensionPill)
|
||||
|
||||
let title = NSTextField(labelWithString: titleText(for: item))
|
||||
title.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||
title.textColor = .labelColor
|
||||
title.maximumNumberOfLines = 1
|
||||
title.lineBreakMode = .byTruncatingTail
|
||||
title.toolTip = title.stringValue
|
||||
|
||||
let detail = NSTextField(labelWithString: previewText(for: item))
|
||||
detail.font = .systemFont(ofSize: 12)
|
||||
detail.textColor = .secondaryLabelColor
|
||||
detail.maximumNumberOfLines = 1
|
||||
detail.lineBreakMode = .byTruncatingTail
|
||||
detail.toolTip = detail.stringValue
|
||||
|
||||
let labels = NSStackView(views: [title, detail])
|
||||
labels.orientation = .vertical
|
||||
labels.alignment = .leading
|
||||
labels.spacing = 3
|
||||
labels.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
container.addSubview(frame)
|
||||
container.addSubview(labels)
|
||||
NSLayoutConstraint.activate([
|
||||
frame.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
|
||||
frame.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
|
||||
frame.topAnchor.constraint(equalTo: container.topAnchor, constant: layout.isCompact ? 14 : 16),
|
||||
frame.heightAnchor.constraint(equalToConstant: layout.isCompact ? 82 : 92),
|
||||
film.centerXAnchor.constraint(equalTo: frame.centerXAnchor),
|
||||
film.centerYAnchor.constraint(equalTo: frame.centerYAnchor),
|
||||
film.widthAnchor.constraint(equalToConstant: 38),
|
||||
film.heightAnchor.constraint(equalToConstant: 38),
|
||||
play.centerXAnchor.constraint(equalTo: frame.centerXAnchor, constant: 1),
|
||||
play.centerYAnchor.constraint(equalTo: frame.centerYAnchor),
|
||||
play.widthAnchor.constraint(equalToConstant: 16),
|
||||
play.heightAnchor.constraint(equalToConstant: 16),
|
||||
extensionPill.trailingAnchor.constraint(equalTo: frame.trailingAnchor, constant: -10),
|
||||
extensionPill.bottomAnchor.constraint(equalTo: frame.bottomAnchor, constant: -10),
|
||||
labels.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
|
||||
labels.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
|
||||
labels.topAnchor.constraint(equalTo: frame.bottomAnchor, constant: 10),
|
||||
labels.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -8),
|
||||
title.widthAnchor.constraint(equalTo: labels.widthAnchor),
|
||||
detail.widthAnchor.constraint(equalTo: labels.widthAnchor)
|
||||
])
|
||||
return container
|
||||
}
|
||||
|
||||
private func colorPreviewView(for item: ClipboardItem) -> NSView {
|
||||
let swatchColor = ColorPayload.color(from: item.payload) ?? accentColor(for: item.kind)
|
||||
let textColor = ColorPayload.contrastingTextColor(for: swatchColor)
|
||||
@@ -4363,6 +4442,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
return thumbnail == nil ? "file-preview" : "file-media-preview"
|
||||
case .audio:
|
||||
return "audio-preview"
|
||||
case .video:
|
||||
return "video-preview"
|
||||
case .color:
|
||||
return "color-preview"
|
||||
case .code:
|
||||
@@ -4542,6 +4623,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
return fileTitle(for: item, fallback: "PDF document")
|
||||
case .audio:
|
||||
return audioTitle(for: item)
|
||||
case .video:
|
||||
return videoTitle(for: item)
|
||||
case .image:
|
||||
return imageTitle(for: item)
|
||||
case .color:
|
||||
@@ -4576,6 +4659,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
return fileLocationText(from: item.payload, fallback: "PDF document")
|
||||
case .audio:
|
||||
return "Sound clip"
|
||||
case .video:
|
||||
return "Video clip"
|
||||
case .color:
|
||||
return ColorPayload.componentSummary(from: item.payload)
|
||||
case .code:
|
||||
@@ -4611,6 +4696,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
return "PDF"
|
||||
case .audio:
|
||||
return "Audio"
|
||||
case .video:
|
||||
return VideoPayload.kindText(from: item.payload)
|
||||
case .color:
|
||||
return "Color"
|
||||
case .code:
|
||||
@@ -4702,6 +4789,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
return "Audio"
|
||||
}
|
||||
|
||||
private func videoTitle(for item: ClipboardItem) -> String {
|
||||
let display = firstUsefulLine(item.displayText)
|
||||
if !display.isEmpty, !looksInternal(display), display.lowercased() != "video" {
|
||||
return display
|
||||
}
|
||||
return "Video"
|
||||
}
|
||||
|
||||
private func webComponents(from value: String) -> URLComponents? {
|
||||
let trimmed = value.clipboardTrimmed
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
@@ -4929,6 +5024,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
return NSColor(calibratedRed: 0.55, green: 0.35, blue: 0.88, alpha: 1)
|
||||
case .audio:
|
||||
return NSColor(calibratedRed: 0.93, green: 0.12, blue: 0.34, alpha: 1)
|
||||
case .video:
|
||||
return NSColor(calibratedRed: 0.43, green: 0.32, blue: 0.94, alpha: 1)
|
||||
case .color:
|
||||
return NSColor(calibratedRed: 0.00, green: 0.65, blue: 0.74, alpha: 1)
|
||||
case .code:
|
||||
@@ -4947,6 +5044,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
case .file: return "doc"
|
||||
case .pdf: return "doc.text.fill"
|
||||
case .audio: return "music.note"
|
||||
case .video: return "film"
|
||||
case .color: return "paintpalette"
|
||||
case .code: return "chevron.left.forwardslash.chevron.right"
|
||||
case .unknown: return "questionmark"
|
||||
@@ -5020,6 +5118,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
case .unknown: return "Unknown"
|
||||
case .pdf: return "PDF"
|
||||
case .audio: return "Audio"
|
||||
case .video: return "Video"
|
||||
case .color: return "Color"
|
||||
case .code: return "Code"
|
||||
}
|
||||
|
||||
@@ -454,6 +454,9 @@ final class ClipboardPanelViewModel {
|
||||
case .audio:
|
||||
guard let url = cacheService.temporaryReadableURL(for: item) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
case .video:
|
||||
guard let url = cacheService.temporaryReadableURL(for: item) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
case .color:
|
||||
break
|
||||
default:
|
||||
@@ -477,6 +480,9 @@ final class ClipboardPanelViewModel {
|
||||
case .audio:
|
||||
guard let url = cacheService.temporaryReadableURL(for: item) else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||
case .video:
|
||||
guard let url = cacheService.temporaryReadableURL(for: item) else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||
case .color:
|
||||
break
|
||||
default:
|
||||
@@ -792,6 +798,12 @@ final class ClipboardPanelViewModel {
|
||||
.sorted(by: sortByUsage)
|
||||
.map(\.1)
|
||||
|
||||
case .videos:
|
||||
return collectionFiltered
|
||||
.filter { $0.1.kind == .video }
|
||||
.sorted(by: sortByUsage)
|
||||
.map(\.1)
|
||||
|
||||
case .colors:
|
||||
return collectionFiltered
|
||||
.filter { $0.1.kind == .color }
|
||||
@@ -1016,6 +1028,8 @@ final class ClipboardPanelViewModel {
|
||||
return [.pdf]
|
||||
case "audio", "sound", "music":
|
||||
return [.audio]
|
||||
case "video", "videos", "movie", "movies", "mp4", "quicktime", "mov":
|
||||
return [.video]
|
||||
case "color", "colors", "swatch", "swatches", "hex":
|
||||
return [.color]
|
||||
case "unknown", "item":
|
||||
|
||||
@@ -172,6 +172,7 @@ final class SettingsWindowController: NSObject, NSTextFieldDelegate, NSTextViewD
|
||||
kindCheckbox("Images", .image),
|
||||
kindCheckbox("Colors", .color),
|
||||
kindCheckbox("Audio", .audio),
|
||||
kindCheckbox("Videos", .video),
|
||||
kindCheckbox("Rich text", .richText),
|
||||
kindCheckbox("PDFs", .pdf),
|
||||
kindCheckbox("Files", .file)
|
||||
@@ -201,7 +202,7 @@ final class SettingsWindowController: NSObject, NSTextFieldDelegate, NSTextViewD
|
||||
}
|
||||
|
||||
private func privacySettingsView() -> NSView {
|
||||
let storageLabel = caption("Clipboard history is stored locally in Application Support. Text, image cache files, audio clips, and PDF attachments are encrypted with Keychain when available, or an owner-only local fallback key if needed.")
|
||||
let storageLabel = caption("Clipboard history is stored locally in Application Support. Text, image cache files, audio clips, video clips, and PDF attachments are encrypted with Keychain when available, or an owner-only local fallback key if needed.")
|
||||
let permissionHelpLabel = caption("Clipboard history capture works without this permission. Grant Accessibility to paste selected items into the previous app.")
|
||||
configureCheckbox(clearHistoryOnQuitButton, title: "Clear history on quit", action: #selector(clearHistoryOnQuitChanged))
|
||||
configureStatusLabel(accessibilityStatusLabel)
|
||||
|
||||
Reference in New Issue
Block a user