mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
217 lines
6.7 KiB
Swift
217 lines
6.7 KiB
Swift
//
|
|
// ClipboardManager.swift
|
|
// DynamicIsland
|
|
//
|
|
// Created by GitHub Copilot on 17/07/25.
|
|
//
|
|
|
|
import AppKit
|
|
import SwiftUI
|
|
import Combine
|
|
import Foundation
|
|
|
|
// Clipboard item data structure
|
|
struct ClipboardItem: Identifiable, Codable {
|
|
let id = UUID()
|
|
let content: String
|
|
let type: ClipboardItemType
|
|
let timestamp: Date
|
|
let preview: String
|
|
|
|
init(content: String, type: ClipboardItemType) {
|
|
self.content = content
|
|
self.type = type
|
|
self.timestamp = Date()
|
|
self.preview = ClipboardItem.generatePreview(content: content, type: type)
|
|
}
|
|
|
|
static func generatePreview(content: String, type: ClipboardItemType) -> String {
|
|
switch type {
|
|
case .text:
|
|
return String(content.prefix(50))
|
|
case .url:
|
|
if let url = URL(string: content) {
|
|
return url.lastPathComponent.isEmpty ? url.host ?? content : url.lastPathComponent
|
|
}
|
|
return String(content.prefix(50))
|
|
case .file:
|
|
if let url = URL(string: content) {
|
|
return url.lastPathComponent
|
|
}
|
|
return "File"
|
|
case .image:
|
|
return "Image"
|
|
case .rtf:
|
|
// Remove RTF formatting for preview
|
|
let stripped = content.replacingOccurrences(of: "\\[^\\s]*\\s?", with: "", options: .regularExpression)
|
|
return String(stripped.prefix(50))
|
|
case .unknown:
|
|
return String(content.prefix(50))
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ClipboardItemType: String, CaseIterable, Codable {
|
|
case text = "text"
|
|
case url = "url"
|
|
case file = "file"
|
|
case image = "image"
|
|
case rtf = "rtf"
|
|
case unknown = "unknown"
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .text: return "doc.text"
|
|
case .url: return "link"
|
|
case .file: return "doc"
|
|
case .image: return "photo"
|
|
case .rtf: return "doc.richtext"
|
|
case .unknown: return "questionmark.circle"
|
|
}
|
|
}
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .text: return "Text"
|
|
case .url: return "URL"
|
|
case .file: return "File"
|
|
case .image: return "Image"
|
|
case .rtf: return "Rich Text"
|
|
case .unknown: return "Unknown"
|
|
}
|
|
}
|
|
}
|
|
|
|
class ClipboardManager: ObservableObject {
|
|
static let shared = ClipboardManager()
|
|
|
|
@Published var clipboardHistory: [ClipboardItem] = []
|
|
@Published var isMonitoring: Bool = false
|
|
|
|
private var timer: Timer?
|
|
private var lastChangeCount: Int = 0
|
|
private let maxHistoryItems = 3
|
|
|
|
private init() {
|
|
lastChangeCount = NSPasteboard.general.changeCount
|
|
loadHistoryFromDefaults()
|
|
}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
func startMonitoring() {
|
|
guard !isMonitoring else { return }
|
|
|
|
isMonitoring = true
|
|
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
|
|
self?.checkClipboard()
|
|
}
|
|
}
|
|
|
|
func stopMonitoring() {
|
|
isMonitoring = false
|
|
timer?.invalidate()
|
|
timer = nil
|
|
}
|
|
|
|
func copyToClipboard(_ item: ClipboardItem) {
|
|
NSPasteboard.general.clearContents()
|
|
NSPasteboard.general.setString(item.content, forType: .string)
|
|
}
|
|
|
|
func deleteItem(_ item: ClipboardItem) {
|
|
clipboardHistory.removeAll { $0.id == item.id }
|
|
saveHistoryToDefaults()
|
|
}
|
|
|
|
func clearHistory() {
|
|
clipboardHistory.removeAll()
|
|
saveHistoryToDefaults()
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
private func checkClipboard() {
|
|
let currentChangeCount = NSPasteboard.general.changeCount
|
|
|
|
guard currentChangeCount != lastChangeCount else { return }
|
|
lastChangeCount = currentChangeCount
|
|
|
|
guard let clipboardItem = getCurrentClipboardItem() else { return }
|
|
|
|
// Don't add duplicate items
|
|
if !clipboardHistory.contains(where: { $0.content == clipboardItem.content && $0.type == clipboardItem.type }) {
|
|
addToHistory(clipboardItem)
|
|
}
|
|
}
|
|
|
|
private func getCurrentClipboardItem() -> ClipboardItem? {
|
|
let pasteboard = NSPasteboard.general
|
|
|
|
// Check for URL first
|
|
if let url = pasteboard.string(forType: .URL) ?? pasteboard.string(forType: .fileURL) {
|
|
return ClipboardItem(content: url, type: .url)
|
|
}
|
|
|
|
// Check for file URLs
|
|
if let fileURLs = pasteboard.propertyList(forType: .fileURL) as? [String], !fileURLs.isEmpty {
|
|
return ClipboardItem(content: fileURLs.first!, type: .file)
|
|
}
|
|
|
|
// Check for images
|
|
if pasteboard.canReadItem(withDataConformingToTypes: [NSPasteboard.PasteboardType.tiff.rawValue, NSPasteboard.PasteboardType.png.rawValue]) {
|
|
return ClipboardItem(content: "Image data", type: .image)
|
|
}
|
|
|
|
// Check for RTF
|
|
if let rtfData = pasteboard.data(forType: .rtf),
|
|
let rtfString = NSAttributedString(rtf: rtfData, documentAttributes: nil)?.string {
|
|
return ClipboardItem(content: rtfString, type: .rtf)
|
|
}
|
|
|
|
// Check for plain text
|
|
if let string = pasteboard.string(forType: .string) {
|
|
// Determine if it's a URL
|
|
if string.hasPrefix("http://") || string.hasPrefix("https://") || string.hasPrefix("file://") {
|
|
return ClipboardItem(content: string, type: .url)
|
|
}
|
|
return ClipboardItem(content: string, type: .text)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func addToHistory(_ item: ClipboardItem) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
// Remove any existing items with the same content
|
|
self.clipboardHistory.removeAll { $0.content == item.content }
|
|
|
|
// Add to beginning of array
|
|
self.clipboardHistory.insert(item, at: 0)
|
|
|
|
// Keep only the most recent items
|
|
if self.clipboardHistory.count > self.maxHistoryItems {
|
|
self.clipboardHistory = Array(self.clipboardHistory.prefix(self.maxHistoryItems))
|
|
}
|
|
|
|
self.saveHistoryToDefaults()
|
|
}
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
private func saveHistoryToDefaults() {
|
|
if let encoded = try? JSONEncoder().encode(clipboardHistory) {
|
|
UserDefaults.standard.set(encoded, forKey: "ClipboardHistory")
|
|
}
|
|
}
|
|
|
|
private func loadHistoryFromDefaults() {
|
|
if let data = UserDefaults.standard.data(forKey: "ClipboardHistory"),
|
|
let history = try? JSONDecoder().decode([ClipboardItem].self, from: data) {
|
|
clipboardHistory = history
|
|
}
|
|
}
|
|
}
|