diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index e148944..3869f54 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -58,15 +58,27 @@ }, "3 items" : { + }, + "5 colors" : { + }, "5 items" : { }, "7 items" : { + }, + "10 colors" : { + }, "10 items" : { + }, + "15 colors" : { + + }, + "20 colors" : { + }, "About" : { "localizations" : { @@ -283,6 +295,12 @@ }, "Clear clipboard history removes recent copies. Clear pinned items removes your favorites. Both actions are permanent." : { + }, + "Clear Color History" : { + + }, + "Clear color history removes all picked colors. Start color picking begins screen color capture mode." : { + }, "Clear Data" : { @@ -316,12 +334,21 @@ }, "Color Formats" : { + }, + "Color History" : { + }, "Color Picked!" : { }, "Color Picker" : { + }, + "Color Picker feature is disabled" : { + + }, + "Color Picker Panel:" : { + }, "ColorPicker Disabled" : { @@ -524,6 +551,9 @@ }, "Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view." : { + }, + "Enable screen color picking functionality. Use Cmd+Shift+P to quickly access the color picker." : { + }, "Enable sneak peek" : { @@ -914,12 +944,18 @@ }, "Opens the clipboard history panel. Default is Cmd+Shift+V (similar to Windows+V on PC). Only works when clipboard feature is enabled." : { + }, + "Opens the color picker panel for screen color capture. Default is Cmd+Shift+P. Only works when color picker feature is enabled." : { + }, "Opens the detailed system performance monitor panel. Default is Cmd+Shift+S. Only works when stats feature is enabled." : { }, "Panel mode shows clipboard in a floating window near the notch. Popover mode shows clipboard as a dropdown attached to the clipboard button." : { + }, + "Panel mode shows color picker in a floating window. Popover mode shows color picker as a dropdown attached to the color picker button." : { + }, "Pause" : { @@ -945,6 +981,9 @@ }, "Picked Color" : { + }, + "Picking Status" : { + }, "Pin items from history to save them here" : { @@ -963,6 +1002,9 @@ }, "Popover mode shows clipboard as a dropdown attached to the clipboard button. Panel mode shows clipboard in a floating window near the notch." : { + }, + "Popover mode shows color picker as a dropdown attached to the color picker button. Panel mode shows color picker in a floating window." : { + }, "Progress bar" : { "localizations" : { @@ -976,6 +1018,9 @@ }, "Progressbar style" : { + }, + "Quick Actions" : { + }, "Quit" : { "localizations" : { @@ -1000,6 +1045,9 @@ } } } + }, + "Ready" : { + }, "Recent Colors" : { @@ -1113,6 +1161,9 @@ }, "Show Color History" : { + }, + "Show Color Picker Panel" : { + }, "Show cool face animation while inactivity" : { "extractionState" : "stale", @@ -1172,6 +1223,9 @@ }, "Start" : { + }, + "Start Color Picking" : { + }, "Start Demo Timer:" : { @@ -1205,6 +1259,9 @@ }, "Stats Panel:" : { + }, + "Status & Actions" : { + }, "Stop" : { diff --git a/DynamicIsland/components/Clipboard/ClipboardPanel.swift b/DynamicIsland/components/Clipboard/ClipboardPanel.swift index 905e148..f7c0bbc 100644 --- a/DynamicIsland/components/Clipboard/ClipboardPanel.swift +++ b/DynamicIsland/components/Clipboard/ClipboardPanel.swift @@ -157,58 +157,44 @@ struct ClipboardPanelView: View { } var body: some View { - ZStack { - VStack(spacing: 0) { - // Header with tabs - ClipboardPanelHeader( - selectedTab: $selectedTab, - searchText: $searchText, - onClose: onClose + VStack(spacing: 0) { + // Header with tabs + ClipboardPanelHeader( + selectedTab: $selectedTab, + searchText: $searchText, + onClose: onClose + ) + + Divider() + .background(Color.gray.opacity(0.3)) + + // Content + if filteredItems.isEmpty { + ClipboardPanelEmptyState( + hasSearch: !searchText.isEmpty, + isHistoryTab: selectedTab == .history ) - - Divider() - .background(Color.gray.opacity(0.3)) - - // Content - if filteredItems.isEmpty { - ClipboardPanelEmptyState( - hasSearch: !searchText.isEmpty, - isHistoryTab: selectedTab == .history - ) - } else { - ScrollView { - LazyVStack(spacing: 1) { - ForEach(filteredItems) { item in - ClipboardPanelItemRow( - item: item, - isHovered: hoveredItemId == item.id, - isPinned: clipboardManager.pinnedItems.contains(where: { $0.id == item.id }) - ) { hoverId in - hoveredItemId = hoverId - } + } else { + ScrollView { + LazyVStack(spacing: 1) { + ForEach(filteredItems) { item in + ClipboardPanelItemRow( + item: item, + isHovered: hoveredItemId == item.id, + isPinned: clipboardManager.pinnedItems.contains(where: { $0.id == item.id }) + ) { hoverId in + hoveredItemId = hoverId } } - .padding(.vertical, 8) } + .padding(.vertical, 8) } } - .frame(width: 320, height: 400) - .background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) - .cornerRadius(12) - .shadow(color: .black.opacity(0.4), radius: 20, x: 0, y: 10) - - // Close button positioned in top-left corner - VStack { - HStack { - NativeStyleCloseButton(action: onClose) - .padding(.leading, 8) - .padding(.top, 8) - - Spacer() - } - Spacer() - } } + .frame(width: 320, height: 400) + .background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) + .cornerRadius(12) + .shadow(color: .black.opacity(0.4), radius: 20, x: 0, y: 10) } } @@ -223,6 +209,9 @@ struct ClipboardPanelHeader: View { VStack(spacing: 8) { // Title and close button HStack { + // Close button + NativeStyleCloseButton(action: onClose) + Image(systemName: "doc.on.clipboard") .foregroundColor(.primary) .font(.system(size: 16, weight: .medium)) @@ -336,6 +325,7 @@ struct ClipboardPanelItemRow: View { let isPinned: Bool let onHover: (UUID?) -> Void @ObservedObject var clipboardManager = ClipboardManager.shared + @State private var justCopied = false var body: some View { HStack(alignment: .top, spacing: 12) { @@ -388,10 +378,20 @@ struct ClipboardPanelItemRow: View { // Copy button Button(action: { clipboardManager.copyToClipboard(item) + + withAnimation(.easeInOut(duration: 0.2)) { + justCopied = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + withAnimation(.easeInOut(duration: 0.2)) { + justCopied = false + } + } }) { - Image(systemName: "doc.on.doc") + Image(systemName: justCopied ? "checkmark.circle.fill" : "doc.on.doc") .font(.system(size: 11)) - .foregroundColor(.green) + .foregroundColor(justCopied ? .green : .green) } .buttonStyle(PlainButtonStyle()) diff --git a/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift b/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift index 90a6f86..c00b1a2 100644 --- a/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift +++ b/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift @@ -179,30 +179,16 @@ struct ColorPickerPanelView: View { } var body: some View { - ZStack { - VStack(spacing: 0) { - headerSection - - Divider() - .background(Color.gray.opacity(0.3)) - - contentSection - } - .background(ColorPickerVisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) - .cornerRadius(12) + VStack(spacing: 0) { + headerSection - // Close button positioned in top-left corner - VStack { - HStack { - NativeStyleCloseButton(action: onClose) - .padding(.leading, 8) - .padding(.top, 8) - - Spacer() - } - Spacer() - } + Divider() + .background(Color.gray.opacity(0.3)) + + contentSection } + .background(ColorPickerVisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) + .cornerRadius(12) .alert("Delete Color", isPresented: $showingDeleteAlert) { Button("Cancel", role: .cancel) { } Button("Delete", role: .destructive) { @@ -219,6 +205,9 @@ struct ColorPickerPanelView: View { VStack(spacing: 12) { // Title and controls HStack { + // Close button + NativeStyleCloseButton(action: onClose) + HStack(spacing: 8) { Image(systemName: "eyedropper.halffull") .font(.system(size: 16, weight: .medium)) diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index f715fef..6f0a2ec 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -16,6 +16,7 @@ struct DynamicIslandHeader: View { @ObservedObject var clipboardManager = ClipboardManager.shared @StateObject var tvm = TrayDrop.shared @State private var showClipboardPopover = false + @State private var showColorPickerPopover = false var body: some View { HStack(spacing: 0) { @@ -95,7 +96,12 @@ struct DynamicIslandHeader: View { // ColorPicker button if Defaults[.enableColorPickerFeature] { Button(action: { - ColorPickerPanelManager.shared.toggleColorPickerPanel() + switch Defaults[.colorPickerDisplayMode] { + case .panel: + ColorPickerPanelManager.shared.toggleColorPickerPanel() + case .popover: + showColorPickerPopover.toggle() + } }) { Capsule() .fill(.black) @@ -108,6 +114,9 @@ struct DynamicIslandHeader: View { } } .buttonStyle(PlainButtonStyle()) + .popover(isPresented: $showColorPickerPopover, arrowEdge: .bottom) { + ColorPickerPopover() + } } if Defaults[.settingsIconInNotch] { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 0471beb..2ae087a 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -59,6 +59,9 @@ struct SettingsView: View { NavigationLink(value: "Clipboard") { Label("Clipboard", systemImage: "clipboard") } + NavigationLink(value: "ColorPicker") { + Label("Color Picker", systemImage: "eyedropper") + } if extensionManager.installedExtensions .contains(where: { $0.bundleIdentifier == downloadManagerExtension }) { NavigationLink(value: "Downloads") { @@ -102,6 +105,8 @@ struct SettingsView: View { StatsSettings() case "Clipboard": ClipboardSettings() + case "ColorPicker": + ColorPickerSettings() case "Downloads": Downloads() case "Shelf": @@ -1116,6 +1121,7 @@ struct Shortcuts: View { @Default(.enableClipboardManager) var enableClipboardManager @Default(.enableShortcuts) var enableShortcuts @Default(.enableStatsFeature) var enableStatsFeature + @Default(.enableColorPickerFeature) var enableColorPickerFeature var body: some View { Form { @@ -1228,6 +1234,29 @@ struct Shortcuts: View { .foregroundStyle(.secondary) .font(.caption) } + + Section { + HStack { + VStack(alignment: .leading) { + KeyboardShortcuts.Recorder("Color Picker Panel:", name: .colorPickerPanel) + .disabled(!enableShortcuts || !enableColorPickerFeature) + if !enableColorPickerFeature { + Text("Color Picker feature is disabled") + .font(.caption) + .foregroundStyle(.secondary) + .padding(.top, 2) + } + } + Spacer() + } + } header: { + Text("Color Picker") + } footer: { + Text("Opens the color picker panel for screen color capture. Default is Cmd+Shift+P. Only works when color picker feature is enabled.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } } else { Section { VStack(alignment: .leading, spacing: 8) { @@ -1795,6 +1824,113 @@ struct ClipboardSettings: View { } } +struct ColorPickerSettings: View { + @ObservedObject var colorPickerManager = ColorPickerManager.shared + @Default(.enableColorPickerFeature) var enableColorPickerFeature + @Default(.showColorFormats) var showColorFormats + @Default(.colorPickerDisplayMode) var colorPickerDisplayMode + @Default(.colorHistorySize) var colorHistorySize + @Default(.showColorPickerIcon) var showColorPickerIcon + + var body: some View { + Form { + Section { + Defaults.Toggle("Enable Color Picker", key: .enableColorPickerFeature) + } header: { + Text("Color Picker") + } footer: { + Text("Enable screen color picking functionality. Use Cmd+Shift+P to quickly access the color picker.") + } + + if enableColorPickerFeature { + Section { + Defaults.Toggle("Show Color Picker Icon", key: .showColorPickerIcon) + + HStack { + Text("Display Mode") + Spacer() + Picker("Display Mode", selection: $colorPickerDisplayMode) { + ForEach(ColorPickerDisplayMode.allCases, id: \.self) { mode in + Text(mode.displayName).tag(mode) + } + } + .pickerStyle(.menu) + .frame(width: 100) + } + + HStack { + Text("History Size") + Spacer() + Picker("History Size", selection: $colorHistorySize) { + Text("5 colors").tag(5) + Text("10 colors").tag(10) + Text("15 colors").tag(15) + Text("20 colors").tag(20) + } + .pickerStyle(.menu) + .frame(width: 100) + } + + Defaults.Toggle("Show All Color Formats", key: .showColorFormats) + + } header: { + Text("Settings") + } footer: { + switch colorPickerDisplayMode { + case .popover: + Text("Popover mode shows color picker as a dropdown attached to the color picker button. Panel mode shows color picker in a floating window.") + case .panel: + Text("Panel mode shows color picker in a floating window. Popover mode shows color picker as a dropdown attached to the color picker button.") + } + } + + Section { + HStack { + Text("Color History") + Spacer() + Text("\(colorPickerManager.colorHistory.count)") + .foregroundColor(.secondary) + } + + HStack { + Text("Picking Status") + Spacer() + Text(colorPickerManager.isPickingColor ? "Active" : "Ready") + .foregroundColor(colorPickerManager.isPickingColor ? .green : .secondary) + } + + Button("Show Color Picker Panel") { + ColorPickerPanelManager.shared.showColorPickerPanel() + } + .disabled(!enableColorPickerFeature) + + } header: { + Text("Status & Actions") + } + + Section { + Button("Clear Color History") { + colorPickerManager.clearHistory() + } + .foregroundColor(.red) + .disabled(colorPickerManager.colorHistory.isEmpty) + + Button("Start Color Picking") { + colorPickerManager.startColorPicking() + } + .disabled(!enableColorPickerFeature || colorPickerManager.isPickingColor) + + } header: { + Text("Quick Actions") + } footer: { + Text("Clear color history removes all picked colors. Start color picking begins screen color capture mode.") + } + } + } + .navigationTitle("Color Picker") + } +} + #Preview { HUD() } diff --git a/DynamicIsland/components/Tabs/TabSelectionView.swift b/DynamicIsland/components/Tabs/TabSelectionView.swift index 7035c05..7bb3027 100644 --- a/DynamicIsland/components/Tabs/TabSelectionView.swift +++ b/DynamicIsland/components/Tabs/TabSelectionView.swift @@ -22,29 +22,28 @@ struct TabSelectionView: View { @Default(.enableColorPickerFeature) var enableColorPickerFeature @Namespace var animation - var availableTabs: [TabModel] { - var tabs = [ - TabModel(label: "Home", icon: "house.fill", view: .home), - TabModel(label: "Shelf", icon: "tray.fill", view: .shelf) - ] + private var tabs: [TabModel] { + var tabsArray: [TabModel] = [] - if enableTimerFeature { - tabs.append(TabModel(label: "Timer", icon: "timer", view: .timer)) + tabsArray.append(TabModel(label: "Home", icon: "house.fill", view: .home)) + + tabsArray.append(TabModel(label: "Shelf", icon: "tray.fill", view: .shelf)) + + // Timer tab only shown when timer feature is enabled + if Defaults[.enableTimerFeature] { + tabsArray.append(TabModel(label: "Timer", icon: "timer", view: .timer)) } - if enableStatsFeature { - tabs.append(TabModel(label: "Stats", icon: "chart.line.uptrend.xyaxis", view: .stats)) + // Stats tab only shown when stats feature is enabled + if Defaults[.enableStatsFeature] { + tabsArray.append(TabModel(label: "Stats", icon: "chart.xyaxis.line", view: .stats)) } - if enableColorPickerFeature { - tabs.append(TabModel(label: "ColorPicker", icon: "eyedropper", view: .colorPicker)) - } - - return tabs + return tabsArray } var body: some View { HStack(spacing: 0) { - ForEach(availableTabs) { tab in + ForEach(tabs) { tab in TabButton(label: tab.label, icon: tab.icon, selected: coordinator.currentView == tab.view) { withAnimation(.smooth) { coordinator.currentView = tab.view diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 92dd270..7aa3525 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -49,6 +49,25 @@ enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable } } +enum ColorPickerDisplayMode: String, CaseIterable, Codable, Defaults.Serializable { + case popover = "popover" // Traditional popover attached to button + case panel = "panel" // Floating panel near notch + + var displayName: String { + switch self { + case .popover: return "Popover" + case .panel: return "Panel" + } + } + + var description: String { + switch self { + case .popover: return "Shows color picker as a dropdown attached to the color picker button" + case .panel: return "Shows color picker in a floating panel near the notch" + } + } +} + enum HideNotchOption: String, Defaults.Serializable { case always case nowPlayingOnly @@ -189,6 +208,9 @@ extension Defaults.Keys { // MARK: ColorPicker Feature static let enableColorPickerFeature = Key("enableColorPickerFeature", default: true) static let showColorFormats = Key("showColorFormats", default: true) + static let colorPickerDisplayMode = Key("colorPickerDisplayMode", default: .panel) + static let colorHistorySize = Key("colorHistorySize", default: 10) + static let showColorPickerIcon = Key("showColorPickerIcon", default: true) // MARK: Clipboard Feature static let enableClipboardManager = Key("enableClipboardManager", default: true)