Update SettingsView

This commit is contained in:
Hariharan Mudaliar 2025-08-03 22:26:18 +05:30
commit 1b5925090a
4 changed files with 2177 additions and 92 deletions

View file

@ -14,6 +14,7 @@ import LottieUI
import Sparkle
import SwiftUI
import SwiftUIIntrospect
import UniformTypeIdentifiers
struct SettingsView: View {
@StateObject var extensionManager = DynamicIslandExtensionManager()
@ -1126,6 +1127,16 @@ struct Shortcuts: View {
Section {
KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen)
}
Section {
KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer)
} header: {
Text("Timer")
} footer: {
Text("Starts a 5-minute demo timer to test the timer live activity feature.")
.multilineTextAlignment(.trailing)
.foregroundStyle(.secondary)
.font(.caption)
}
}
.navigationTitle("Shortcuts")
}
@ -1178,62 +1189,318 @@ func warningBadge(_ text: String, _ description: String) -> some View {
}
struct TimerSettings: View {
@ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared
@Default(.enableTimerFeature) var enableTimerFeature
@AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 // 10 minutes default
@State private var customMinutes: Int = 10
@State private var customSeconds: Int = 0
var body: some View {
Form {
Section {
Defaults.Toggle("Enable timer feature", key: .enableTimerFeature)
.onChange(of: Defaults[.enableTimerFeature]) { _, newValue in
if !newValue {
// Stop any running timers when feature is disabled
TimerManager.shared.stopTimer()
if enableTimerFeature {
Toggle(
"Enable timer live activity",
isOn: $coordinator.timerLiveActivityEnabled
)
.animation(.easeInOut, value: coordinator.timerLiveActivityEnabled)
}
} header: {
Text("Timer Feature")
} footer: {
Text("Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view.")
}
if enableTimerFeature {
Section {
VStack(alignment: .leading, spacing: 16) {
Text("Custom Timer Duration")
.font(.headline)
HStack(spacing: 16) {
VStack {
Text("Minutes")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.secondary)
Picker(selection: $customMinutes, label: Text("Minutes")) {
ForEach(0...59, id: \.self) { minute in
Text("\(minute)").tag(minute)
}
}
.pickerStyle(.menu)
.frame(width: 80, height: 40)
}
VStack {
Text("Seconds")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.secondary)
Picker(selection: $customSeconds, label: Text("Seconds")) {
ForEach(0...59, id: \.self) { second in
Text("\(second)").tag(second)
}
}
.pickerStyle(.menu)
.frame(width: 80, height: 40)
}
}
HStack {
Text("Current custom timer: \(customTimerDisplayText)")
.foregroundStyle(.secondary)
Spacer()
Button("Update") {
customTimerDuration = Double(customMinutes * 60 + customSeconds)
}
.disabled(customMinutes == 0 && customSeconds == 0)
}
}
} header: {
Text("Timer")
} footer: {
if Defaults[.enableTimerFeature] {
Text("Timer tab will appear in the notch when enabled.")
} else {
Text("Enable to access timer functionality in the Dynamic Island.")
} header: {
Text("Custom Timer")
} footer: {
Text("Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface.")
}
Section {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Timer Sound")
.font(.system(size: 16, weight: .medium))
Spacer()
Button("Choose File") {
selectCustomTimerSound()
}
.buttonStyle(.bordered)
}
if let customTimerSoundPath = UserDefaults.standard.string(forKey: "customTimerSoundPath") {
Text("Custom: \(URL(fileURLWithPath: customTimerSoundPath).lastPathComponent)")
.font(.caption)
.foregroundColor(.secondary)
} else {
Text("Default: dynamic.m4a")
.font(.caption)
.foregroundColor(.secondary)
}
Button("Reset to Default") {
UserDefaults.standard.removeObject(forKey: "customTimerSoundPath")
}
.buttonStyle(.bordered)
.disabled(UserDefaults.standard.string(forKey: "customTimerSoundPath") == nil)
}
} header: {
Text("Timer Sound")
} footer: {
Text("Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF.")
}
}
}
.navigationTitle("Timer")
.onAppear {
let totalMinutes = Int(customTimerDuration) / 60
customMinutes = totalMinutes
customSeconds = Int(customTimerDuration) % 60
}
}
private var customTimerDisplayText: String {
let totalMinutes = Int(customTimerDuration) / 60
let seconds = Int(customTimerDuration) % 60
let hours = totalMinutes / 60
let minutes = totalMinutes % 60
if hours > 0 {
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))"
} else if minutes > 0 {
return "\(minutes):\(String(format: "%02d", seconds))"
} else {
return "\(seconds)s"
}
}
private func selectCustomTimerSound() {
let panel = NSOpenPanel()
panel.title = "Select Timer Sound"
panel.allowedContentTypes = [.audio]
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
panel.canChooseFiles = true
if panel.runModal() == .OK {
if let url = panel.url {
UserDefaults.standard.set(url.path, forKey: "customTimerSoundPath")
}
}
}
}
struct StatsSettings: View {
@ObservedObject var statsManager = StatsManager.shared
@Default(.enableStatsFeature) var enableStatsFeature
@Default(.showCpuGraph) var showCpuGraph
@Default(.showMemoryGraph) var showMemoryGraph
@Default(.showGpuGraph) var showGpuGraph
@Default(.showNetworkGraph) var showNetworkGraph
@Default(.showDiskGraph) var showDiskGraph
var enabledGraphsCount: Int {
[showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count
}
var body: some View {
Form {
Section {
Defaults.Toggle("Enable stats feature", key: .enableStatsFeature)
.onChange(of: Defaults[.enableStatsFeature]) { _, newValue in
if !newValue {
// Stop monitoring when feature is disabled
StatsManager.shared.stopMonitoring()
Defaults.Toggle("Enable system stats monitoring", key: .enableStatsFeature)
.onChange(of: enableStatsFeature) { _, newValue in
if newValue {
statsManager.startMonitoring()
} else {
statsManager.stopMonitoring()
}
}
} header: {
Text("System Monitoring")
Text("General")
} footer: {
if Defaults[.enableStatsFeature] {
Text("Stats tab will appear in the notch when enabled.")
} else {
Text("Enable to monitor CPU, Memory, GPU, Network and Disk usage.")
}
Text("When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery.")
.multilineTextAlignment(.trailing)
.foregroundStyle(.secondary)
.font(.caption)
}
if Defaults[.enableStatsFeature] {
if enableStatsFeature {
Section {
Defaults.Toggle("Show CPU graph", key: .showCpuGraph)
Defaults.Toggle("Show Memory graph", key: .showMemoryGraph)
Defaults.Toggle("Show GPU graph", key: .showGpuGraph)
Defaults.Toggle("Show Network graph", key: .showNetworkGraph)
Defaults.Toggle("Show Disk graph", key: .showDiskGraph)
Defaults.Toggle("CPU Usage", key: .showCpuGraph)
Defaults.Toggle("Memory Usage", key: .showMemoryGraph)
Defaults.Toggle("GPU Usage", key: .showGpuGraph)
Defaults.Toggle("Network Activity", key: .showNetworkGraph)
Defaults.Toggle("Disk I/O", key: .showDiskGraph)
} header: {
Text("Graph Visibility")
} footer: {
Text("Choose which performance graphs to display. 1-3 graphs use horizontal layout, 4+ graphs use compressed grid layout.")
if enabledGraphsCount >= 4 {
Text("With \(enabledGraphsCount) graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row.")
.multilineTextAlignment(.trailing)
.foregroundStyle(.secondary)
.font(.caption)
} else {
Text("Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds.")
.multilineTextAlignment(.trailing)
.foregroundStyle(.secondary)
.font(.caption)
}
}
Section {
HStack {
Text("Monitoring Status")
Spacer()
HStack(spacing: 4) {
Circle()
.fill(statsManager.isMonitoring ? .green : .red)
.frame(width: 8, height: 8)
Text(statsManager.isMonitoring ? "Active" : "Stopped")
.foregroundStyle(.secondary)
}
}
if statsManager.isMonitoring {
if showCpuGraph {
HStack {
Text("CPU Usage")
Spacer()
Text(statsManager.cpuUsageString)
.foregroundStyle(.secondary)
}
}
if showMemoryGraph {
HStack {
Text("Memory Usage")
Spacer()
Text(statsManager.memoryUsageString)
.foregroundStyle(.secondary)
}
}
if showGpuGraph {
HStack {
Text("GPU Usage")
Spacer()
Text(statsManager.gpuUsageString)
.foregroundStyle(.secondary)
}
}
if showNetworkGraph {
HStack {
Text("Network Download")
Spacer()
Text(String(format: "%.1f MB/s", statsManager.networkDownload))
.foregroundStyle(.secondary)
}
HStack {
Text("Network Upload")
Spacer()
Text(String(format: "%.1f MB/s", statsManager.networkUpload))
.foregroundStyle(.secondary)
}
}
if showDiskGraph {
HStack {
Text("Disk Read")
Spacer()
Text(String(format: "%.1f MB/s", statsManager.diskRead))
.foregroundStyle(.secondary)
}
HStack {
Text("Disk Write")
Spacer()
Text(String(format: "%.1f MB/s", statsManager.diskWrite))
.foregroundStyle(.secondary)
}
}
HStack {
Text("Last Updated")
Spacer()
Text(statsManager.lastUpdated, style: .relative)
.foregroundStyle(.secondary)
}
}
} header: {
Text("Live Performance Data")
}
Section {
HStack {
Button(statsManager.isMonitoring ? "Stop Monitoring" : "Start Monitoring") {
if statsManager.isMonitoring {
statsManager.stopMonitoring()
} else {
statsManager.startMonitoring()
}
}
.buttonStyle(.borderedProminent)
.foregroundColor(statsManager.isMonitoring ? .red : .blue)
Spacer()
Button("Clear Data") {
statsManager.clearHistory()
}
.buttonStyle(.bordered)
.disabled(statsManager.isMonitoring)
}
} header: {
Text("Controls")
}
}
}
@ -1242,55 +1509,128 @@ struct StatsSettings: View {
}
struct ClipboardSettings: View {
@ObservedObject var clipboardManager = ClipboardManager.shared
@Default(.enableClipboardManager) var enableClipboardManager
@Default(.clipboardHistorySize) var clipboardHistorySize
@Default(.showClipboardIcon) var showClipboardIcon
var body: some View {
Form {
Section {
Defaults.Toggle("Enable clipboard manager", key: .enableClipboardManager)
.onChange(of: Defaults[.enableClipboardManager]) { _, newValue in
if newValue {
ClipboardManager.shared.startMonitoring()
Defaults.Toggle("Enable Clipboard Manager", key: .enableClipboardManager)
.onChange(of: enableClipboardManager) { _, enabled in
if enabled {
clipboardManager.startMonitoring()
} else {
ClipboardManager.shared.stopMonitoring()
clipboardManager.stopMonitoring()
}
}
} header: {
Text("Clipboard Manager")
} footer: {
if Defaults[.enableClipboardManager] {
Text("Clipboard history will be tracked and accessible via the header button.")
} else {
Text("Enable to track clipboard history and access recent items.")
}
Text("Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history.")
}
if Defaults[.enableClipboardManager] {
if enableClipboardManager {
Section {
Defaults.Toggle("Show clipboard icon", key: .showClipboardIcon)
Defaults.Toggle("Show Clipboard Icon", key: .showClipboardIcon)
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("History size")
Spacer()
Text("\(Defaults[.clipboardHistorySize]) items")
.foregroundStyle(.secondary)
HStack {
Text("History Size")
Spacer()
Picker("History Size", selection: $clipboardHistorySize) {
Text("3 items").tag(3)
Text("5 items").tag(5)
Text("7 items").tag(7)
Text("10 items").tag(10)
}
Slider(
value: Binding(
get: { Double(Defaults[.clipboardHistorySize]) },
set: { Defaults[.clipboardHistorySize] = Int($0) }
),
in: 3...10,
step: 1
)
.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")
} footer: {
Text("Configure how many recent clipboard items to remember (minimum 3).")
Text("The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history.")
}
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"
}
}
}