// // NotchStatsView.swift // DynamicIsland // // Stats tab view for system performance monitoring // Created by Hariharan Mudaliar import SwiftUI import Defaults // Graph data protocol for unified interface protocol GraphData { var title: String { get } var color: Color { get } var icon: String { get } var type: GraphType { get } } enum GraphType { case single case dual } // Single value graph data struct SingleGraphData: GraphData { let title: String let value: String let data: [Double] let color: Color let icon: String 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 var availableGraphs: [GraphData] { var graphs: [GraphData] = [] if showCpuGraph { 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" )) } 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: String(format: "↓%.1f MB/s", statsManager.networkDownload), negativeValue: String(format: "↑%.1f MB/s", statsManager.networkUpload), positiveData: statsManager.networkDownloadHistory, negativeData: statsManager.networkUploadHistory, positiveColor: .orange, negativeColor: .red, color: .orange, icon: "network" )) } if showDiskGraph { graphs.append(DualGraphData( title: "Disk", positiveValue: String(format: "R %.1f MB/s", statsManager.diskRead), negativeValue: String(format: "W %.1f MB/s", statsManager.diskWrite), positiveData: statsManager.diskReadHistory, negativeData: statsManager.diskWriteHistory, positiveColor: .cyan, negativeColor: .yellow, color: .cyan, icon: "internaldrive" )) } return graphs } var gridColumns: [GridItem] { let graphCount = availableGraphs.count if graphCount <= 3 { // Horizontal layout for 1-3 graphs return Array(repeating: GridItem(.flexible(), spacing: 12), count: graphCount) } else { // Compressed grid layout for 4+ graphs return Array(repeating: GridItem(.flexible(), spacing: 8), count: graphCount) } } var body: some View { VStack(spacing: 0) { if !enableStatsFeature { // Disabled state VStack(spacing: 12) { Image(systemName: "chart.line.uptrend.xyaxis") .font(.system(size: 40)) .foregroundColor(.secondary) Text("Stats Disabled") .font(.headline) .foregroundColor(.primary) Text("Enable stats monitoring in Settings to view system performance data.") .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) } .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 dynamic layout ZStack(alignment: .topTrailing) { VStack(spacing: 12) { // Dynamic Stats Grid LazyVGrid(columns: gridColumns, spacing: 12) { ForEach(0.. 0 ? data.map { $0 / maxValue } : data Path { path in guard !normalizedData.isEmpty else { return } let stepX = geometry.size.width / CGFloat(normalizedData.count - 1) for (index, value) in normalizedData.enumerated() { let x = CGFloat(index) * stepX let y = geometry.size.height * (1 - CGFloat(value)) if index == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } } } .stroke(color, lineWidth: 2) // Gradient fill Path { path in guard !normalizedData.isEmpty else { return } let stepX = geometry.size.width / CGFloat(normalizedData.count - 1) path.move(to: CGPoint(x: 0, y: geometry.size.height)) for (index, value) in normalizedData.enumerated() { let x = CGFloat(index) * stepX let y = geometry.size.height * (1 - CGFloat(value)) path.addLine(to: CGPoint(x: x, y: y)) } path.addLine(to: CGPoint(x: geometry.size.width, y: geometry.size.height)) path.closeSubpath() } .fill( LinearGradient( gradient: Gradient(colors: [color.opacity(0.3), color.opacity(0.1)]), startPoint: .top, endPoint: .bottom ) ) } } } 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() .frame(width: 400, height: 300) .background(Color.black) }