Working Text Clipboard

This commit is contained in:
Hariharan Mudaliar 2025-07-17 20:21:49 +05:30
commit 4890e6fc28
8 changed files with 631 additions and 4 deletions

View file

@ -0,0 +1,190 @@
//
// ClipboardHistoryPopover.swift
// DynamicIsland
//
// Created by GitHub Copilot on 17/07/25.
//
import SwiftUI
struct ClipboardHistoryPopover: View {
@ObservedObject var clipboardManager = ClipboardManager.shared
@Binding var isPresented: Bool
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Header
HStack {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.white)
.font(.system(size: 16, weight: .medium))
Text("Clipboard History")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
Spacer()
Button(action: {
clipboardManager.clearHistory()
}) {
Image(systemName: "trash")
.foregroundColor(.red)
.font(.system(size: 12))
}
.buttonStyle(PlainButtonStyle())
.disabled(clipboardManager.clipboardHistory.isEmpty)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
Divider()
.background(Color.gray.opacity(0.3))
// Content
if clipboardManager.clipboardHistory.isEmpty {
EmptyClipboardView()
} else {
ClipboardItemsList()
}
}
.frame(width: 280)
.frame(maxHeight: 200)
.background(Color.black.opacity(0.95))
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
.shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 5)
}
@ViewBuilder
private func EmptyClipboardView() -> some View {
VStack(spacing: 8) {
Image(systemName: "clipboard")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("No clipboard history")
.font(.system(size: 12))
.foregroundColor(.gray)
Text("Copy something to get started")
.font(.system(size: 10))
.foregroundColor(.gray.opacity(0.7))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.vertical, 20)
}
@ViewBuilder
private func ClipboardItemsList() -> some View {
ScrollView {
LazyVStack(spacing: 1) {
ForEach(clipboardManager.clipboardHistory) { item in
ClipboardItemRow(item: item) {
isPresented = false
}
}
}
}
.frame(maxHeight: 150)
}
}
struct ClipboardItemRow: View {
let item: ClipboardItem
let onCopy: () -> Void
@ObservedObject var clipboardManager = ClipboardManager.shared
@State private var isHovering = false
var body: some View {
HStack(alignment: .top, spacing: 8) {
// Type icon
Image(systemName: item.type.icon)
.font(.system(size: 12))
.foregroundColor(.blue)
.frame(width: 16)
// Content
VStack(alignment: .leading, spacing: 2) {
Text(item.preview)
.font(.system(size: 11))
.foregroundColor(.white)
.lineLimit(2)
.multilineTextAlignment(.leading)
HStack {
Text(item.type.displayName)
.font(.system(size: 9))
.foregroundColor(.gray)
Spacer()
Text(timeAgoString(from: item.timestamp))
.font(.system(size: 9))
.foregroundColor(.gray)
}
}
Spacer()
// Action buttons (shown on hover)
if isHovering {
HStack(spacing: 4) {
Button(action: {
clipboardManager.copyToClipboard(item)
onCopy()
}) {
Image(systemName: "doc.on.doc")
.font(.system(size: 10))
.foregroundColor(.green)
}
.buttonStyle(PlainButtonStyle())
Button(action: {
clipboardManager.deleteItem(item)
}) {
Image(systemName: "trash")
.font(.system(size: 10))
.foregroundColor(.red)
}
.buttonStyle(PlainButtonStyle())
}
}
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(isHovering ? Color.white.opacity(0.1) : Color.clear)
.onHover { hovering in
withAnimation(.easeInOut(duration: 0.2)) {
isHovering = hovering
}
}
.onTapGesture {
clipboardManager.copyToClipboard(item)
onCopy()
}
}
private func timeAgoString(from date: Date) -> String {
let interval = Date().timeIntervalSince(date)
if interval < 60 {
return "Just now"
} else if interval < 3600 {
let minutes = Int(interval / 60)
return "\(minutes)m ago"
} else if interval < 86400 {
let hours = Int(interval / 3600)
return "\(hours)h ago"
} else {
let days = Int(interval / 86400)
return "\(days)d ago"
}
}
}
#Preview {
ClipboardHistoryPopover(isPresented: .constant(true))
.padding()
.background(Color.gray)
}

View file

@ -13,7 +13,10 @@ struct DynamicIslandHeader: View {
@EnvironmentObject var webcamManager: WebcamManager
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject var clipboardManager = ClipboardManager.shared
@StateObject var tvm = TrayDrop.shared
@State private var showClipboardPopover = false
var body: some View {
HStack(spacing: 0) {
HStack {
@ -61,6 +64,41 @@ struct DynamicIslandHeader: View {
}
.buttonStyle(PlainButtonStyle())
}
if Defaults[.enableClipboardManager] {
Button(action: {
showClipboardPopover.toggle()
}) {
Capsule()
.fill(.black)
.frame(width: 30, height: 30)
.overlay {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.white)
.padding()
.imageScale(.medium)
}
}
.buttonStyle(PlainButtonStyle())
.popover(isPresented: $showClipboardPopover, arrowEdge: .bottom) {
ClipboardHistoryPopover(isPresented: $showClipboardPopover)
.onAppear {
vm.isClipboardPopoverActive = true
}
.onDisappear {
vm.isClipboardPopoverActive = false
}
}
.onChange(of: showClipboardPopover) { isOpen in
vm.isClipboardPopoverActive = isOpen
}
.onAppear {
if Defaults[.enableClipboardManager] && !clipboardManager.isMonitoring {
clipboardManager.startMonitoring()
}
}
}
if Defaults[.settingsIconInNotch] {
Button(action: {
SettingsWindowController.shared.showWindow()

View file

@ -63,6 +63,9 @@ struct SettingsView: View {
NavigationLink(value: "Stats") {
Label("Stats", systemImage: "chart.line.uptrend.xyaxis")
}
NavigationLink(value: "Clipboard") {
Label("Clipboard", systemImage: "doc.on.clipboard")
}
NavigationLink(value: "Shortcuts") {
Label("Shortcuts", systemImage: "keyboard")
}
@ -97,6 +100,8 @@ struct SettingsView: View {
Shelf()
case "Stats":
Stats()
case "Clipboard":
ClipboardSettings()
case "Shortcuts":
Shortcuts()
case "Extensions":
@ -1300,6 +1305,126 @@ func warningBadge(_ text: String, _ description: String) -> some View {
}
}
struct ClipboardSettings: View {
@ObservedObject var clipboardManager = ClipboardManager.shared
@Default(.enableClipboardManager) var enableClipboardManager
@Default(.clipboardHistorySize) var clipboardHistorySize
var body: some View {
Form {
Section {
Defaults.Toggle("Enable Clipboard Manager", key: .enableClipboardManager)
.onChange(of: enableClipboardManager) { enabled in
if enabled {
clipboardManager.startMonitoring()
} else {
clipboardManager.stopMonitoring()
}
}
} header: {
Text("Clipboard Manager")
} footer: {
Text("Monitor clipboard changes and keep a history of recent copies. The clipboard button will appear in the header next to the settings gear.")
}
if enableClipboardManager {
Section {
HStack {
Text("History Size")
Spacer()
Picker("History Size", selection: $clipboardHistorySize) {
Text("3 items").tag(3)
Text("5 items").tag(5)
Text("10 items").tag(10)
}
.pickerStyle(.menu)
.frame(width: 100)
}
HStack {
Text("Current Items")
Spacer()
Text("\(clipboardManager.clipboardHistory.count)")
.foregroundColor(.secondary)
}
HStack {
Text("Monitoring Status")
Spacer()
Text(clipboardManager.isMonitoring ? "Active" : "Stopped")
.foregroundColor(clipboardManager.isMonitoring ? .green : .secondary)
}
} header: {
Text("Settings")
}
Section {
Button("Clear Clipboard History") {
clipboardManager.clearHistory()
}
.foregroundColor(.red)
.disabled(clipboardManager.clipboardHistory.isEmpty)
} header: {
Text("Actions")
} footer: {
Text("This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header.")
}
if !clipboardManager.clipboardHistory.isEmpty {
Section {
ForEach(clipboardManager.clipboardHistory) { item in
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: item.type.icon)
.foregroundColor(.blue)
.frame(width: 16)
Text(item.type.displayName)
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text(timeAgoString(from: item.timestamp))
.font(.caption2)
.foregroundColor(.secondary)
}
Text(item.preview)
.font(.system(.body, design: .monospaced))
.lineLimit(2)
}
.padding(.vertical, 2)
}
} header: {
Text("Current History")
}
}
}
}
.formStyle(.grouped)
.navigationTitle("Clipboard")
.onAppear {
if enableClipboardManager && !clipboardManager.isMonitoring {
clipboardManager.startMonitoring()
}
}
}
private func timeAgoString(from date: Date) -> String {
let interval = Date().timeIntervalSince(date)
if interval < 60 {
return "Just now"
} else if interval < 3600 {
let minutes = Int(interval / 60)
return "\(minutes)m ago"
} else if interval < 86400 {
let hours = Int(interval / 3600)
return "\(hours)h ago"
} else {
let days = Int(interval / 86400)
return "\(days)d ago"
}
}
}
#Preview {
HUD()
}