diff --git a/.gitignore b/.gitignore index 4a248a2..306064c 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,6 @@ boringnotch + meteo + airpods + traduzione modificabile segmentedhud.swift.ref gemini-apireference.md -ScreenshotApp-main \ No newline at end of file +ScreenshotApp-main +stats +boring.notch \ No newline at end of file diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index a3fce63..636f001 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -124,13 +124,13 @@ struct ContentView: View { } // Only close if mouse leaves and the notch is open and no popovers are active - if !hovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive { + if !hovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !vm.isStatsPopoverActive { // Add slight delay for stats tab to prevent accidental closure during layout changes let delay: Double = (coordinator.currentView == .stats) ? 0.2 : 0.0 DispatchQueue.main.asyncAfter(deadline: .now() + delay) { // Double-check conditions after delay - if !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive { + if !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !vm.isStatsPopoverActive { vm.close() } } @@ -180,7 +180,14 @@ struct ContentView: View { } .onChange(of: vm.isBatteryPopoverActive) { _, newPopoverState in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if !newPopoverState && !isHovering && vm.notchState == .open { + if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isStatsPopoverActive { + vm.close() + } + } + } + .onChange(of: vm.isStatsPopoverActive) { _, newPopoverState in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive { vm.close() } } @@ -188,7 +195,7 @@ struct ContentView: View { .onChange(of: vm.shouldRecheckHover) { _, _ in // Recheck hover state when popovers are closed DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !isHovering { + if vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !vm.isStatsPopoverActive && !isHovering { vm.close() } } diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 91c901f..c5828dd 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -41,10 +41,11 @@ "%lld colors" : { }, - "%lld%%" : { - + "%lld processes" : { + "comment" : "A label at the bottom of the ranked processes popover showing the total number of processes being monitored.", + "isCommentAutoGenerated" : true }, - "•" : { + "%lld%%" : { }, "• Bug fixes" : { @@ -332,6 +333,10 @@ }, "Click 'Pick Color' or use Cmd+Shift+P to start picking colors" : { + }, + "Click for details" : { + "comment" : "A hint text displayed below the graph in the Stats Card, indicating that tapping it will reveal more details.", + "isCommentAutoGenerated" : true }, "Clipboard" : { @@ -562,9 +567,6 @@ }, "Enable global keyboard shortcuts above to customize your shortcuts." : { - }, - "Enable graph visibility in Settings → Stats to view performance data." : { - }, "Enable haptics" : { "extractionState" : "stale", @@ -749,9 +751,6 @@ }, "LinkedIn" : { - }, - "Live" : { - }, "Live Monitoring" : { @@ -941,9 +940,6 @@ }, "No favorites" : { - }, - "No Graphs Enabled" : { - }, "No results" : { @@ -968,9 +964,6 @@ }, "Notch Height" : { - }, - "Off" : { - }, "OK" : { @@ -1058,6 +1051,10 @@ }, "Picking Status" : { + }, + "PID: %d" : { + "comment" : "A small label displaying the process ID of a particular app. The argument is the process ID of the app.", + "isCommentAutoGenerated" : true }, "Pin items from history to save them here" : { @@ -1140,6 +1137,10 @@ }, "Recording Status" : { + }, + "Refresh" : { + "comment" : "A button to refresh the displayed data.", + "isCommentAutoGenerated" : true }, "Release name" : { "localizations" : { @@ -1461,6 +1462,10 @@ }, "Toggle the Dynamic Island open or closed from anywhere." : { + }, + "Top Processes by %@" : { + "comment" : "A heading that announces the top processes by a specific metric. The text inside the parentheses is replaced with the actual metric name.", + "isCommentAutoGenerated" : true }, "Transform your notch truly yours" : { @@ -1546,5 +1551,5 @@ } }, - "version" : "1.0" + "version" : "1.1" } \ No newline at end of file diff --git a/DynamicIsland/components/Notch/NotchStatsView.swift b/DynamicIsland/components/Notch/NotchStatsView.swift index 94e81e5..6ee65bc 100644 --- a/DynamicIsland/components/Notch/NotchStatsView.swift +++ b/DynamicIsland/components/Notch/NotchStatsView.swift @@ -2,8 +2,8 @@ // NotchStatsView.swift // DynamicIsland // -// Stats tab view for system performance monitoring -// Created by Hariharan Mudaliar +// Adapted from boring.notch StatsView +// Stats tab view for system performance monitoring with clickable process popovers import SwiftUI import Defaults @@ -31,160 +31,146 @@ struct SingleGraphData: GraphData { let type: GraphType = .single } -// Dual value graph data (for network/disk) -struct DualGraphData: GraphData { - let title: String - let positiveValue: String - let negativeValue: String - let positiveData: [Double] - let negativeData: [Double] - let positiveColor: Color - let negativeColor: Color - let color: Color // Primary color for the component - let icon: String - let type: GraphType = .dual -} - struct NotchStatsView: 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 + @State private var showingCPUPopover = false + @State private var showingMemoryPopover = false + @State private var showingGPUPopover = false + @State private var isHoveringCPUPopover = false + @State private var isHoveringMemoryPopover = false + @State private var isHoveringGPUPopover = false + @EnvironmentObject var vm: DynamicIslandViewModel var availableGraphs: [GraphData] { var graphs: [GraphData] = [] - if showCpuGraph { - graphs.append(SingleGraphData( - title: "CPU", - value: statsManager.cpuUsageString, - data: statsManager.cpuHistory, - color: .blue, - icon: "cpu" - )) - } + // Only CPU, Memory, and GPU as in boring.notch - no network/disk + graphs.append(SingleGraphData( + title: "CPU", + value: statsManager.cpuUsageString, + data: statsManager.cpuHistory, + color: .blue, + icon: "cpu" + )) - if showMemoryGraph { - graphs.append(SingleGraphData( - title: "Memory", - value: statsManager.memoryUsageString, - data: statsManager.memoryHistory, - color: .green, - icon: "memorychip" - )) - } + graphs.append(SingleGraphData( + title: "Memory", + value: statsManager.memoryUsageString, + data: statsManager.memoryHistory, + color: .green, + icon: "memorychip" + )) - if showGpuGraph { - graphs.append(SingleGraphData( - title: "GPU", - value: statsManager.gpuUsageString, - data: statsManager.gpuHistory, - color: .purple, - icon: "display" - )) - } - - if showNetworkGraph { - graphs.append(DualGraphData( - title: "Network", - positiveValue: "↓" + statsManager.networkDownloadString, - negativeValue: "↑" + statsManager.networkUploadString, - positiveData: statsManager.networkDownloadHistory, - negativeData: statsManager.networkUploadHistory, - positiveColor: .orange, - negativeColor: .red, - color: .orange, - icon: "network" - )) - } - - if showDiskGraph { - graphs.append(DualGraphData( - title: "Disk", - positiveValue: "R " + statsManager.diskReadString, - negativeValue: "W " + statsManager.diskWriteString, - positiveData: statsManager.diskReadHistory, - negativeData: statsManager.diskWriteHistory, - positiveColor: .cyan, - negativeColor: .yellow, - color: .cyan, - icon: "internaldrive" - )) - } + graphs.append(SingleGraphData( + title: "GPU", + value: statsManager.gpuUsageString, + data: statsManager.gpuHistory, + color: .purple, + icon: "display" + )) return graphs } - // Smart grid layout system for different graph counts + // Restored original 3-graph layout from boring.notch @ViewBuilder var statsGridLayout: some View { - let graphCount = availableGraphs.count - - if graphCount <= 3 { - // 1-3 graphs: Single row with equal spacing - LazyVGrid( - columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: graphCount), - spacing: 12 - ) { - ForEach(0.. Binding { + switch graphData.title { + case "CPU": + return $showingCPUPopover + case "Memory": + return $showingMemoryPopover + case "GPU": + return $showingGPUPopover + default: + return .constant(false) + } + } + + private func rankingTypeForGraph(_ graphData: GraphData) -> ProcessRankingType { + switch graphData.title { + case "CPU": + return .cpu + case "Memory": + return .memory + case "GPU": + return .gpu + default: + return .cpu + } + } + // Helper function to create graph views using unified component @ViewBuilder func graphView(for graphData: GraphData) -> some View { @@ -212,88 +198,22 @@ struct NotchStatsView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() - } else if availableGraphs.isEmpty { - // No graphs enabled state - VStack(spacing: 12) { - Image(systemName: "chart.xyaxis.line") - .font(.system(size: 40)) - .foregroundColor(.secondary) - - Text("No Graphs Enabled") - .font(.headline) - .foregroundColor(.primary) - - Text("Enable graph visibility in Settings → Stats to view performance data.") - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding() } else { - // Stats content with improved animation coordination - ZStack(alignment: .topTrailing) { - VStack(spacing: 12) { - // Simplified consistent layout - statsGridLayout - } - .padding(16) - .animation(.easeInOut(duration: 0.4), value: availableGraphs.count) - .transition(.asymmetric( - insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)), - removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)) - )) - - // Live indicator and controls in top-right corner - HStack(spacing: 8) { - // Control buttons - HStack(spacing: 4) { - if statsManager.isMonitoring { - Button("Stop") { - statsManager.stopMonitoring() - } - .buttonStyle(.bordered) - .foregroundColor(.red) - .controlSize(.mini) - } else { - Button("Start") { - statsManager.startMonitoring() - } - .buttonStyle(.borderedProminent) - .controlSize(.mini) - } - - Button("Clear") { - statsManager.clearHistory() - } - .buttonStyle(.bordered) - .disabled(statsManager.isMonitoring) - .controlSize(.mini) - } - .font(.caption2) - - // Live indicator - HStack(spacing: 4) { - Circle() - .fill(statsManager.isMonitoring ? .green : .red) - .frame(width: 6, height: 6) - - Text(statsManager.isMonitoring ? "Live" : "Off") - .font(.caption2) - .foregroundColor(.secondary) - } - } - .padding(.top, 8) - .padding(.trailing, 12) + // Stats content - restored to original boring.notch 3-graph layout + VStack(spacing: 8) { + statsGridLayout } + .padding(12) + .animation(.easeInOut(duration: 0.4), value: availableGraphs.count) + .transition(.asymmetric( + insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)), + removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)) + )) } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(NSColor.windowBackgroundColor).opacity(0.1)) - .clipShape(RoundedRectangle(cornerRadius: 12)) .onAppear { - if enableStatsFeature && !statsManager.isMonitoring { + if enableStatsFeature && Defaults[.autoStartStatsMonitoring] && !statsManager.isMonitoring { statsManager.startMonitoring() } } @@ -302,75 +222,105 @@ struct NotchStatsView: View { } .animation(.easeInOut(duration: 0.4), value: enableStatsFeature) .animation(.easeInOut(duration: 0.4), value: availableGraphs.count) + .onChange(of: showingCPUPopover) { _, _ in + updateStatsPopoverState() + } + .onChange(of: showingMemoryPopover) { _, _ in + updateStatsPopoverState() + } + .onChange(of: showingGPUPopover) { _, _ in + updateStatsPopoverState() + } + .onChange(of: isHoveringCPUPopover) { _, _ in + updateStatsPopoverState() + } + .onChange(of: isHoveringMemoryPopover) { _, _ in + updateStatsPopoverState() + } + .onChange(of: isHoveringGPUPopover) { _, _ in + updateStatsPopoverState() + } + } + + private func updateStatsPopoverState() { + // Use the same logic as battery popover: active only when shown AND hovered + let newState = (showingCPUPopover && isHoveringCPUPopover) || + (showingMemoryPopover && isHoveringMemoryPopover) || + (showingGPUPopover && isHoveringGPUPopover) + if vm.isStatsPopoverActive != newState { + vm.isStatsPopoverActive = newState + #if DEBUG + print("📊 Stats popover state updated: \(newState)") + print(" CPU: shown=\(showingCPUPopover), hovering=\(isHoveringCPUPopover)") + print(" Memory: shown=\(showingMemoryPopover), hovering=\(isHoveringMemoryPopover)") + print(" GPU: shown=\(showingGPUPopover), hovering=\(isHoveringGPUPopover)") + #endif + } } } -// Unified Stats Card Component - handles both single and dual data types +// Unified Stats Card Component - now clickable for popovers, matches boring.notch sizing struct UnifiedStatsCard: View { let graphData: GraphData + @State private var isHovered = false var body: some View { - VStack(spacing: 6) { - // Header - consistent across all card types + VStack(spacing: 3) { + // Header with title on left and percentage on right - matches boring.notch layout HStack(spacing: 4) { - Image(systemName: graphData.icon) - .foregroundColor(graphData.color) - .font(.caption2) - - Text(graphData.title) - .font(.caption2) - .fontWeight(.medium) - .foregroundColor(.secondary) + // Title and icon on left + HStack(spacing: 3) { + Image(systemName: graphData.icon) + .foregroundColor(graphData.color) + .font(.caption) + + Text(graphData.title) + .font(.caption) + .fontWeight(.medium) + .foregroundColor(.secondary) + } Spacer() - } - - // Values section - adapts based on graph type - Group { + + // Percentage value on right if let singleData = graphData as? SingleGraphData { Text(singleData.value) - .font(.title3) + .font(.caption) .fontWeight(.bold) .foregroundColor(.primary) - } else if let dualData = graphData as? DualGraphData { - HStack(spacing: 6) { - Text(dualData.positiveValue) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(dualData.positiveColor) - - Text("•") - .font(.caption2) - .foregroundColor(.secondary) - - Text(dualData.negativeValue) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(dualData.negativeColor) - } } } - .frame(height: 22) // Fixed height for consistent card sizing - // Graph section - adapts based on graph type + // Graph section - full height since no separate value section, matches boring.notch Group { if let singleData = graphData as? SingleGraphData { MiniGraph(data: singleData.data, color: singleData.color) - } else if let dualData = graphData as? DualGraphData { - DualQuadrantGraph( - positiveData: dualData.positiveData, - negativeData: dualData.negativeData, - positiveColor: dualData.positiveColor, - negativeColor: dualData.negativeColor - ) } } - .frame(height: 50) // Fixed height for consistent card sizing + .frame(height: 36) // Matches boring.notch exactly - reduced from 50px + + // Click hint + Text("Click for details") + .font(.caption2) + .foregroundColor(.secondary) + .opacity(isHovered ? 1.0 : 0.0) + .animation(.easeInOut(duration: 0.2), value: isHovered) } - .padding(10) - .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding(8) // Reduced padding to match boring.notch + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(NSColor.controlBackgroundColor).opacity(0.7)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(graphData.color.opacity(isHovered ? 0.5 : 0.2), lineWidth: 1) + ) + ) + .scaleEffect(isHovered ? 1.02 : 1.0) + .animation(.easeInOut(duration: 0.2), value: isHovered) .frame(maxWidth: .infinity, maxHeight: .infinity) + .onHover { hovering in + isHovered = hovering + } } } @@ -429,122 +379,7 @@ struct MiniGraph: View { } } -struct DualQuadrantGraph: View { - let positiveData: [Double] - let negativeData: [Double] - let positiveColor: Color - let negativeColor: Color - - var body: some View { - GeometryReader { geometry in - let maxPositive = positiveData.max() ?? 1.0 - let maxNegative = negativeData.max() ?? 1.0 - let maxValue = max(maxPositive, maxNegative) - - let normalizedPositive = maxValue > 0 ? positiveData.map { $0 / maxValue } : positiveData - let normalizedNegative = maxValue > 0 ? negativeData.map { $0 / maxValue } : negativeData - - let centerY = geometry.size.height / 2 - - ZStack { - // Center dividing line - Path { path in - path.move(to: CGPoint(x: 0, y: centerY)) - path.addLine(to: CGPoint(x: geometry.size.width, y: centerY)) - } - .stroke(Color.secondary.opacity(0.3), lineWidth: 1) - - // Positive quadrant (upper half) - Path { path in - guard !normalizedPositive.isEmpty else { return } - - let stepX = geometry.size.width / CGFloat(normalizedPositive.count - 1) - - for (index, value) in normalizedPositive.enumerated() { - let x = CGFloat(index) * stepX - let y = centerY - (centerY * CGFloat(value)) // Above center - - if index == 0 { - path.move(to: CGPoint(x: x, y: y)) - } else { - path.addLine(to: CGPoint(x: x, y: y)) - } - } - } - .stroke(positiveColor, lineWidth: 2) - - // Positive fill - Path { path in - guard !normalizedPositive.isEmpty else { return } - - let stepX = geometry.size.width / CGFloat(normalizedPositive.count - 1) - - path.move(to: CGPoint(x: 0, y: centerY)) - - for (index, value) in normalizedPositive.enumerated() { - let x = CGFloat(index) * stepX - let y = centerY - (centerY * CGFloat(value)) - path.addLine(to: CGPoint(x: x, y: y)) - } - - path.addLine(to: CGPoint(x: geometry.size.width, y: centerY)) - path.closeSubpath() - } - .fill( - LinearGradient( - gradient: Gradient(colors: [positiveColor.opacity(0.3), positiveColor.opacity(0.1)]), - startPoint: .top, - endPoint: .center - ) - ) - - // Negative quadrant (lower half) - Path { path in - guard !normalizedNegative.isEmpty else { return } - - let stepX = geometry.size.width / CGFloat(normalizedNegative.count - 1) - - for (index, value) in normalizedNegative.enumerated() { - let x = CGFloat(index) * stepX - let y = centerY + (centerY * CGFloat(value)) // Below center - - if index == 0 { - path.move(to: CGPoint(x: x, y: y)) - } else { - path.addLine(to: CGPoint(x: x, y: y)) - } - } - } - .stroke(negativeColor, lineWidth: 2) - - // Negative fill - Path { path in - guard !normalizedNegative.isEmpty else { return } - - let stepX = geometry.size.width / CGFloat(normalizedNegative.count - 1) - - path.move(to: CGPoint(x: 0, y: centerY)) - - for (index, value) in normalizedNegative.enumerated() { - let x = CGFloat(index) * stepX - let y = centerY + (centerY * CGFloat(value)) - path.addLine(to: CGPoint(x: x, y: y)) - } - - path.addLine(to: CGPoint(x: geometry.size.width, y: centerY)) - path.closeSubpath() - } - .fill( - LinearGradient( - gradient: Gradient(colors: [negativeColor.opacity(0.3), negativeColor.opacity(0.1)]), - startPoint: .bottom, - endPoint: .center - ) - ) - } - } - } -} + #Preview { NotchStatsView() diff --git a/DynamicIsland/managers/StatsManager.swift b/DynamicIsland/managers/StatsManager.swift index deb6fb6..4391cdf 100644 --- a/DynamicIsland/managers/StatsManager.swift +++ b/DynamicIsland/managers/StatsManager.swift @@ -11,6 +11,7 @@ import SwiftUI import IOKit import IOKit.ps import Darwin +import AppKit //import Network class StatsManager: ObservableObject { @@ -403,4 +404,61 @@ class StatsManager: ObservableObject { diskReadHistory = Array(repeating: 0.0, count: maxHistoryPoints) diskWriteHistory = Array(repeating: 0.0, count: maxHistoryPoints) } + + // MARK: - Process Monitoring Methods + func getProcessesRankedByCPU() -> [ProcessStats] { + return getRunningProcesses().sorted { $0.cpuUsage > $1.cpuUsage } + } + + func getProcessesRankedByMemory() -> [ProcessStats] { + return getRunningProcesses().sorted { $0.memoryUsage > $1.memoryUsage } + } + + func getProcessesRankedByGPU() -> [ProcessStats] { + // For now, rank by CPU usage as GPU per-process stats are complex to obtain + return getRunningProcesses().sorted { $0.cpuUsage > $1.cpuUsage } + } + + private func getRunningProcesses() -> [ProcessStats] { + var processes: [ProcessStats] = [] + + // Get all running applications from NSWorkspace + let runningApps = NSWorkspace.shared.runningApplications + + for app in runningApps { + // Skip system processes and processes without bundle identifier + guard let bundleId = app.bundleIdentifier, + !bundleId.hasPrefix("com.apple.") || bundleId == "com.apple.finder" else { + continue + } + + let pid = app.processIdentifier + var cpuUsage: Double = 0.0 + var memoryUsage: UInt64 = 0 + + // Get CPU and memory usage using proc_pidinfo + var taskInfo = proc_taskinfo() + let result = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, Int32(MemoryLayout.size)) + + if result == MemoryLayout.size { + // Calculate CPU usage (basic approximation) + cpuUsage = Double(taskInfo.pti_total_user + taskInfo.pti_total_system) / 1000000.0 + + // Get memory usage + memoryUsage = taskInfo.pti_resident_size + } + + let processStats = ProcessStats( + pid: pid, + name: app.localizedName ?? bundleId, + cpuUsage: cpuUsage, + memoryUsage: memoryUsage, + icon: app.icon + ) + + processes.append(processStats) + } + + return processes + } } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index b865bde..c30515d 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -298,6 +298,7 @@ extension Defaults.Keys { // MARK: Stats Feature static let enableStatsFeature = Key("enableStatsFeature", default: false) + static let autoStartStatsMonitoring = Key("autoStartStatsMonitoring", default: true) static let showCpuGraph = Key("showCpuGraph", default: true) static let showMemoryGraph = Key("showMemoryGraph", default: true) static let showGpuGraph = Key("showGpuGraph", default: true) diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index 85fe418..54e1aec 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -31,6 +31,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject { @Published var isBatteryPopoverActive: Bool = false @Published var isClipboardPopoverActive: Bool = false @Published var isColorPickerPopoverActive: Bool = false + @Published var isStatsPopoverActive: Bool = false @Published var shouldRecheckHover: Bool = false let webcamManager = WebcamManager.shared