diff --git a/Sources/FloatingMonitor/Configuration.swift b/Sources/FloatingMonitor/Configuration.swift index 0547cec..ceecf31 100644 --- a/Sources/FloatingMonitor/Configuration.swift +++ b/Sources/FloatingMonitor/Configuration.swift @@ -7,7 +7,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable { case ring // 圆环进度 case text // 纯文本百分比 case pie // 扇形小圆盘 - case compact // 紧凑横条 + 文字 var id: String { rawValue } @@ -17,7 +16,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable { case .ring: return "圆环进度" case .text: return "纯文本" case .pie: return "扇形圆盘" - case .compact: return "紧凑横条" } } @@ -27,7 +25,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable { case .ring: return "circle.dotted" case .text: return "textformat" case .pie: return "chart.pie" - case .compact: return "rectangle.compress.vertical" } } } @@ -65,6 +62,12 @@ final class Configuration: ObservableObject { @Published var swapStyle: GaugeDisplayStyle { didSet { save("swapStyle", swapStyle.rawValue) } } + @Published var cpuTemperatureStyle: GaugeDisplayStyle { + didSet { save("cpuTemperatureStyle", cpuTemperatureStyle.rawValue) } + } + @Published var gpuTemperatureStyle: GaugeDisplayStyle { + didSet { save("gpuTemperatureStyle", gpuTemperatureStyle.rawValue) } + } @Published var showCPU: Bool { didSet { save("showCPU", showCPU) } @@ -78,6 +81,15 @@ final class Configuration: ObservableObject { @Published var showSwap: Bool { didSet { save("showSwap", showSwap) } } + @Published var showTemperature: Bool { + didSet { save("showTemperature", showTemperature) } + } + @Published var showCPUTemperature: Bool { + didSet { save("showCPUTemperature", showCPUTemperature) } + } + @Published var showGPUTemperature: Bool { + didSet { save("showGPUTemperature", showGPUTemperature) } + } @Published var showTopProcesses: Bool { didSet { save("showTopProcesses", showTopProcesses) } } @@ -101,6 +113,13 @@ final class Configuration: ObservableObject { didSet { save("windowHeight", windowHeight) } } + @Published var gaugeAnimationEnabled: Bool { + didSet { save("gaugeAnimationEnabled", gaugeAnimationEnabled) } + } + @Published var textAnimationEnabled: Bool { + didSet { save("textAnimationEnabled", textAnimationEnabled) } + } + @Published var cpuPos: ModulePosition { didSet { save("cpuPos", cpuPos.rawValue) } } @@ -113,6 +132,15 @@ final class Configuration: ObservableObject { @Published var swapPos: ModulePosition { didSet { save("swapPos", swapPos.rawValue) } } + @Published var temperaturePos: ModulePosition { + didSet { save("temperaturePos", temperaturePos.rawValue) } + } + @Published var cpuTemperaturePos: ModulePosition { + didSet { save("cpuTemperaturePos", cpuTemperaturePos.rawValue) } + } + @Published var gpuTemperaturePos: ModulePosition { + didSet { save("gpuTemperaturePos", gpuTemperaturePos.rawValue) } + } @Published var processesPos: ModulePosition { didSet { save("processesPos", processesPos.rawValue) } } @@ -130,6 +158,15 @@ final class Configuration: ObservableObject { @Published var swapRow: Int { didSet { save("swapRow", swapRow) } } + @Published var temperatureRow: Int { + didSet { save("temperatureRow", temperatureRow) } + } + @Published var cpuTemperatureRow: Int { + didSet { save("cpuTemperatureRow", cpuTemperatureRow) } + } + @Published var gpuTemperatureRow: Int { + didSet { save("gpuTemperatureRow", gpuTemperatureRow) } + } @Published var processesRow: Int { didSet { save("processesRow", processesRow) } } @@ -141,11 +178,16 @@ final class Configuration: ObservableObject { gpuStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "gpuStyle") ?? "ring") ?? .ring memoryStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "memoryStyle") ?? "bar") ?? .bar swapStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "swapStyle") ?? "bar") ?? .bar + cpuTemperatureStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "cpuTemperatureStyle") ?? "text") ?? .text + gpuTemperatureStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "gpuTemperatureStyle") ?? "text") ?? .text showCPU = defaults.object(forKey: "showCPU") as? Bool ?? true showGPU = defaults.object(forKey: "showGPU") as? Bool ?? true showMemory = defaults.object(forKey: "showMemory") as? Bool ?? true showSwap = defaults.object(forKey: "showSwap") as? Bool ?? true + showTemperature = defaults.object(forKey: "showTemperature") as? Bool ?? true + showCPUTemperature = defaults.object(forKey: "showCPUTemperature") as? Bool ?? true + showGPUTemperature = defaults.object(forKey: "showGPUTemperature") as? Bool ?? true showTopProcesses = defaults.object(forKey: "showTopProcesses") as? Bool ?? true processCount = defaults.object(forKey: "processCount") as? Int ?? 5 @@ -155,16 +197,25 @@ final class Configuration: ObservableObject { windowWidth = defaults.object(forKey: "windowWidth") as? Double ?? 250 windowHeight = defaults.object(forKey: "windowHeight") as? Double ?? 300 + gaugeAnimationEnabled = defaults.object(forKey: "gaugeAnimationEnabled") as? Bool ?? true + textAnimationEnabled = defaults.object(forKey: "textAnimationEnabled") as? Bool ?? true + cpuPos = ModulePosition(rawValue: defaults.string(forKey: "cpuPos") ?? "full") ?? .full gpuPos = ModulePosition(rawValue: defaults.string(forKey: "gpuPos") ?? "full") ?? .full memoryPos = ModulePosition(rawValue: defaults.string(forKey: "memoryPos") ?? "full") ?? .full swapPos = ModulePosition(rawValue: defaults.string(forKey: "swapPos") ?? "full") ?? .full + temperaturePos = ModulePosition(rawValue: defaults.string(forKey: "temperaturePos") ?? "full") ?? .full + cpuTemperaturePos = ModulePosition(rawValue: defaults.string(forKey: "cpuTemperaturePos") ?? "left") ?? .left + gpuTemperaturePos = ModulePosition(rawValue: defaults.string(forKey: "gpuTemperaturePos") ?? "right") ?? .right processesPos = ModulePosition(rawValue: defaults.string(forKey: "processesPos") ?? "full") ?? .full cpuRow = defaults.object(forKey: "cpuRow") as? Int ?? 1 gpuRow = defaults.object(forKey: "gpuRow") as? Int ?? 2 memoryRow = defaults.object(forKey: "memoryRow") as? Int ?? 3 swapRow = defaults.object(forKey: "swapRow") as? Int ?? 4 + temperatureRow = defaults.object(forKey: "temperatureRow") as? Int ?? 5 + cpuTemperatureRow = defaults.object(forKey: "cpuTemperatureRow") as? Int ?? 5 + gpuTemperatureRow = defaults.object(forKey: "gpuTemperatureRow") as? Int ?? 5 processesRow = defaults.object(forKey: "processesRow") as? Int ?? 5 } diff --git a/Sources/FloatingMonitor/ContentView.swift b/Sources/FloatingMonitor/ContentView.swift index 2b0dae6..143c8f8 100644 --- a/Sources/FloatingMonitor/ContentView.swift +++ b/Sources/FloatingMonitor/ContentView.swift @@ -88,7 +88,9 @@ struct ContentView: View { title: "CPU", icon: "cpu", value: monitor.info.cpuUsage, unit: "%", detail: "\(monitor.info.cpuCores) 核心 · \(monitor.info.cpuModel)", - style: config.cpuStyle, fontSize: config.fontSize + style: config.cpuStyle, fontSize: config.fontSize, + animateGraphic: config.gaugeAnimationEnabled, + animateText: config.textAnimationEnabled ) } @@ -97,7 +99,9 @@ struct ContentView: View { title: "GPU", icon: "display", value: monitor.info.gpuUsage ?? 0, unit: "%", detail: monitor.info.gpuModel + (monitor.info.gpuUsage == nil ? " (N/A)" : ""), - style: config.gpuStyle, fontSize: config.fontSize + style: config.gpuStyle, fontSize: config.fontSize, + animateGraphic: config.gaugeAnimationEnabled, + animateText: config.textAnimationEnabled ) } @@ -106,7 +110,9 @@ struct ContentView: View { title: "内存", icon: "memorychip", value: monitor.info.memoryUsagePercent, unit: "%", detail: formatBytes(monitor.info.memoryUsed) + " / " + formatBytes(monitor.info.memoryTotal), - style: config.memoryStyle, fontSize: config.fontSize + style: config.memoryStyle, fontSize: config.fontSize, + animateGraphic: config.gaugeAnimationEnabled, + animateText: config.textAnimationEnabled ) } @@ -115,10 +121,46 @@ struct ContentView: View { title: "Swap", icon: "arrow.left.arrow.right", value: monitor.info.swapUsagePercent, unit: "%", detail: formatBytes(monitor.info.swapUsed) + " / " + formatBytes(monitor.info.swapTotal), - style: config.swapStyle, fontSize: config.fontSize + style: config.swapStyle, fontSize: config.fontSize, + animateGraphic: config.gaugeAnimationEnabled, + animateText: config.textAnimationEnabled ) } + private var cpuTemperatureGauge: some View { + let temp = monitor.info.cpuTemperature + let hasData = temp != nil + let v = temp ?? 0 + + return StyledGauge( + title: "CPU 温度", icon: "thermometer.medium", + value: v, unit: "°C", + detail: hasData ? "" : "N/A", + style: config.cpuTemperatureStyle, fontSize: config.fontSize, + maxValue: 120, + animateGraphic: config.gaugeAnimationEnabled, + animateText: config.textAnimationEnabled + ) + .opacity(hasData ? 1.0 : 0.5) + } + + private var gpuTemperatureGauge: some View { + let temp = monitor.info.gpuTemperature + let hasData = temp != nil + let v = temp ?? 0 + + return StyledGauge( + title: "GPU 温度", icon: "thermometer.low", + value: v, unit: "°C", + detail: hasData ? "" : "N/A", + style: config.gpuTemperatureStyle, fontSize: config.fontSize, + maxValue: 120, + animateGraphic: config.gaugeAnimationEnabled, + animateText: config.textAnimationEnabled + ) + .opacity(hasData ? 1.0 : 0.5) + } + private var processesSection: some View { VStack(spacing: 12) { ProcessListView( @@ -162,6 +204,14 @@ struct ContentView: View { items.append(Item(name: "Swap", row: config.swapRow, position: config.swapPos, view: AnyView(swapGauge))) } + if config.showCPUTemperature { + items.append(Item(name: "CPU温度", row: config.cpuTemperatureRow, position: config.cpuTemperaturePos, + view: AnyView(cpuTemperatureGauge))) + } + if config.showGPUTemperature { + items.append(Item(name: "GPU温度", row: config.gpuTemperatureRow, position: config.gpuTemperaturePos, + view: AnyView(gpuTemperatureGauge))) + } if config.showTopProcesses { items.append(Item(name: "进程", row: config.processesRow, position: config.processesPos, view: AnyView(processesSection))) diff --git a/Sources/FloatingMonitor/SMCSensor.swift b/Sources/FloatingMonitor/SMCSensor.swift new file mode 100644 index 0000000..40e5604 --- /dev/null +++ b/Sources/FloatingMonitor/SMCSensor.swift @@ -0,0 +1,259 @@ +import Foundation +import IOKit + +// MARK: - SMC 温度传感器(通过 AppleSMC + IOKit 读取 CPU / GPU 温度) + +// ── SMC 结构体定义(必须精确匹配 AppleSMC kext 的 SMCParamStruct)── + +private struct SMCVersion { + var major: UInt8 = 0 + var minor: UInt8 = 0 + var build: UInt8 = 0 + var reserved: UInt8 = 0 + var release: UInt16 = 0 +} + +private struct SMCPLimitData { + var version: UInt16 = 0 + var length: UInt16 = 0 + var cpuPLimit: UInt32 = 0 + var gpuPLimit: UInt32 = 0 + var memPLimit: UInt32 = 0 +} + +private struct SMCKeyInfoData { + var dataSize: UInt32 = 0 + var dataType: UInt32 = 0 + var dataAttributes: UInt8 = 0 +} + +private typealias SMCBytes = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 +) + +private struct SMCParamStruct { + var key: UInt32 = 0 + var vers = SMCVersion() + var pLimitData = SMCPLimitData() + var keyInfo = SMCKeyInfoData() + var padding: UInt16 = 0 // 关键!对齐填充 + var result: UInt8 = 0 + var status: UInt8 = 0 + var data8: UInt8 = 0 // 子 selector(5=读key, 9=查询key信息, 8=按索引取key) + var data32: UInt32 = 0 + var bytes: SMCBytes = ( + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 + ) +} + +// SMC 常量 +private let kSMCHandleYPCEvent: UInt32 = 2 +private let kSMCReadKey: UInt8 = 5 +private let kSMCGetKeyInfo: UInt8 = 9 +private let kSMCGetKeyFromIndex: UInt8 = 8 +private let kSMCSuccess: UInt8 = 0 + +/// 非 actor 工具类:封装 SMC 通信细节 +final class SMCSensor { + static let shared = SMCSensor() + + private var conn: io_connect_t = 0 + private var cpuKeys: [String] = [] + private var gpuKeys: [String] = [] + private var scanned = false + + private init() {} + + deinit { + if conn != 0 { IOServiceClose(conn) } + } + + // MARK: - 公开接口 + + func readCPUTemperature() -> Double? { + return readMax(keys: cpuKeys) + } + + func readGPUTemperature() -> Double? { + return readMax(keys: gpuKeys) + } + + // MARK: - 内部 + + private func readMax(keys: [String]) -> Double? { + guard openSMC() else { return nil } + if !scanned { scanKeys() } + + var maxTemp: Double? = nil + for key in keys { + if let temp = readKey(key) { + maxTemp = max(maxTemp ?? temp, temp) + } + } + return maxTemp + } + + private func openSMC() -> Bool { + if conn != 0 { return true } + + let service = IOServiceGetMatchingService( + kIOMainPortDefault, + IOServiceMatching("AppleSMC") + ) + guard service != 0 else { return false } + + let status = IOServiceOpen(service, mach_task_self_, 0, &conn) + IOObjectRelease(service) + + return status == KERN_SUCCESS && conn != 0 + } + + // MARK: - 扫描可用温度 key + + private func scanKeys() { + scanned = true + guard conn != 0 else { return } + + // 先获取 key 总数 + let totalKeys = readKeyCount() + guard totalKeys > 0 else { return } + + var cpu: [String] = [] + var gpu: [String] = [] + + for i in 0.. Int { + var input = SMCParamStruct() + input.key = fourCharCode("#KEY") + input.keyInfo.dataSize = 4 + input.keyInfo.dataType = fourCharCode("ui32") + input.data8 = kSMCReadKey + + guard let output = callSMC(&input), + output.result == kSMCSuccess else { return 0 } + + let raw = UInt32(output.bytes.0) << 24 + | UInt32(output.bytes.1) << 16 + | UInt32(output.bytes.2) << 8 + | UInt32(output.bytes.3) + return Int(raw) + } + + private func keyAtIndex(_ index: Int) -> String? { + var input = SMCParamStruct() + input.data8 = kSMCGetKeyFromIndex + input.data32 = UInt32(index) + + guard let output = callSMC(&input), + output.result == kSMCSuccess else { return nil } + + return charCodeToString(output.key) + } + + /// 查询 key 的类型和大小,返回 (dataType, dataSize) + private func keyInfo(_ keyName: String) -> (String, UInt32)? { + var input = SMCParamStruct() + input.key = fourCharCode(keyName) + input.data8 = kSMCGetKeyInfo + + guard let output = callSMC(&input), + output.result == kSMCSuccess else { return nil } + + return (charCodeToString(output.keyInfo.dataType), + output.keyInfo.dataSize) + } + + /// 读取 key 的值(自动适配 flt / sp78 类型) + private func readKey(_ keyName: String) -> Double? { + guard let (dataType, dataSize) = keyInfo(keyName) else { return nil } + + var input = SMCParamStruct() + input.key = fourCharCode(keyName) + input.keyInfo.dataSize = dataSize + input.keyInfo.dataType = fourCharCode(dataType) + input.data8 = kSMCReadKey + + guard let output = callSMC(&input), + output.result == kSMCSuccess else { return nil } + + switch dataType { + case "flt ": + // 32-bit IEEE 754 float(小端序) + let raw = UInt32(output.bytes.0) + | (UInt32(output.bytes.1) << 8) + | (UInt32(output.bytes.2) << 16) + | (UInt32(output.bytes.3) << 24) + return Double(Float(bitPattern: raw)) + + case "sp78": + // 16-bit signed fixed-point + let sign = (output.bytes.0 & 0x80) == 0 ? 1.0 : -1.0 + return sign * (Double(output.bytes.0 & 0x7F) + Double(output.bytes.1) / 256.0) + + default: + return nil + } + } + + /// 发送 SMCParamStruct 到 AppleSMC + private func callSMC(_ input: inout SMCParamStruct) -> SMCParamStruct? { + var output = SMCParamStruct() + let size = MemoryLayout.stride // 80 + var outputSize = size + + let result = IOConnectCallStructMethod( + conn, + kSMCHandleYPCEvent, + &input, size, + &output, &outputSize + ) + + guard result == KERN_SUCCESS else { return nil } + return output + } + + // MARK: - 工具 + + private func fourCharCode(_ string: String) -> UInt32 { + var result: UInt32 = 0 + for char in string.utf8 { + result = (result << 8) | UInt32(char) + } + return result + } + + private func charCodeToString(_ code: UInt32) -> String { + let c1 = UInt8((code >> 24) & 0xFF) + let c2 = UInt8((code >> 16) & 0xFF) + let c3 = UInt8((code >> 8) & 0xFF) + let c4 = UInt8(code & 0xFF) + return String(UnicodeScalar(c1)) + + String(UnicodeScalar(c2)) + + String(UnicodeScalar(c3)) + + String(UnicodeScalar(c4)) + } +} diff --git a/Sources/FloatingMonitor/SystemMonitor.swift b/Sources/FloatingMonitor/SystemMonitor.swift index e8d9a50..5035bc8 100644 --- a/Sources/FloatingMonitor/SystemMonitor.swift +++ b/Sources/FloatingMonitor/SystemMonitor.swift @@ -22,6 +22,9 @@ struct SystemInfo { var swapIns: UInt64 var swapOuts: UInt64 + var cpuTemperature: Double? // °C + var gpuTemperature: Double? // °C + var topCPUProcesses: [ProcessInfo] var topMemoryProcesses: [ProcessInfo] @@ -44,6 +47,7 @@ final class SystemMonitor: ObservableObject { memoryTotal: 0, memoryUsed: 0, memoryUsagePercent: 0, swapTotal: 0, swapUsed: 0, swapUsagePercent: 0, swapIns: 0, swapOuts: 0, + cpuTemperature: nil, gpuTemperature: nil, topCPUProcesses: [], topMemoryProcesses: [] ) @@ -90,6 +94,7 @@ final class SystemMonitor: ObservableObject { let gpu = fetchGPU() let mem = fetchMemory() let swap = fetchSwap() + let temp = fetchTemperature() let procs = fetchTopProcesses(count: topN) info = SystemInfo( @@ -106,6 +111,8 @@ final class SystemMonitor: ObservableObject { swapUsagePercent: swap.percent, swapIns: swap.ins, swapOuts: swap.outs, + cpuTemperature: temp.cpu, + gpuTemperature: temp.gpu, topCPUProcesses: procs.cpu, topMemoryProcesses: procs.mem ) @@ -266,6 +273,14 @@ final class SystemMonitor: ObservableObject { return (0, 0, 0, 0, 0) } + // MARK: - 温度 (SMC) + + private func fetchTemperature() -> (cpu: Double?, gpu: Double?) { + let cpu = SMCSensor.shared.readCPUTemperature() + let gpu = SMCSensor.shared.readGPUTemperature() + return (cpu, gpu) + } + // MARK: - 进程列表 private func fetchTopProcesses(count: Int) -> (cpu: [SystemInfo.ProcessInfo], mem: [SystemInfo.ProcessInfo]) { diff --git a/Sources/FloatingMonitor/Views/GaugeView.swift b/Sources/FloatingMonitor/Views/GaugeView.swift index 26fd686..fd39327 100644 --- a/Sources/FloatingMonitor/Views/GaugeView.swift +++ b/Sources/FloatingMonitor/Views/GaugeView.swift @@ -5,23 +5,46 @@ import SwiftUI struct StyledGauge: View { let title: String let icon: String - let value: Double // 0-100 - let unit: String // "%" / "GB" - let detail: String // 副标题(如 "3.2 / 16 GB") + let value: Double + let unit: String + let detail: String let style: GaugeDisplayStyle let fontSize: Double + var maxValue: Double = 100 + var animateGraphic: Bool = true + var animateText: Bool = true + + private var progress: Double { + maxValue > 0 ? min(value / maxValue, 1.0) : 0 + } - // 主题色(根据负载渐变) private var gaugeColor: Color { - if value < 40 { return .green } - if value < 70 { return .yellow } - if value < 85 { return .orange } + if progress < 0.4 { return .green } + if progress < 0.7 { return .yellow } + if progress < 0.85 { return .orange } return .red } + /// 文字动画:条件应用 contentTransition + @ViewBuilder + private func animatedNumber(_ text: Text) -> some View { + if animateText { + text.contentTransition(.numericText(value: value)) + } else { + text + } + } + + private var graphicAnim: Animation? { + animateGraphic ? .easeOut(duration: 0.2) : nil + } + + private var textAnim: Animation? { + animateText ? .easeOut(duration: 0.3) : nil + } + var body: some View { VStack(alignment: .leading, spacing: 6) { - // 标题行 HStack(spacing: 6) { Image(systemName: icon) .font(.system(size: fontSize)) @@ -32,18 +55,11 @@ struct StyledGauge: View { Spacer() } - // 根据样式渲染 switch style { - case .bar: - barView - case .ring: - ringView - case .text: - textView - case .pie: - pieView - case .compact: - compactView + case .bar: barView + case .ring: ringView + case .text: textView + case .pie: pieView } } } @@ -58,29 +74,26 @@ struct StyledGauge: View { .fill(Color.white.opacity(0.12)) .frame(height: 8) Capsule() - .fill( - LinearGradient( - colors: [gaugeColor.opacity(0.8), gaugeColor], - startPoint: .leading, - endPoint: .trailing - ) - ) - .frame(width: max(geo.size.width * CGFloat(value / 100), 4), height: 8) - .animation(.easeOut(duration: 0.2), value: value) + .fill(LinearGradient( + colors: [gaugeColor.opacity(0.8), gaugeColor], + startPoint: .leading, endPoint: .trailing)) + .frame(width: max(geo.size.width * CGFloat(progress), 4), height: 8) + .animation(graphicAnim, value: value) } } .frame(height: 8) HStack { - Text(String(format: "%.1f%@", value, unit)) + animatedNumber(Text(String(format: "%.1f%@", value, unit)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) - .foregroundColor(.primary) + .foregroundColor(.primary)) Spacer() Text(detail) .font(.system(size: fontSize - 2)) .foregroundColor(.secondary) .lineLimit(1) } + .animation(textAnim, value: value) } } @@ -92,33 +105,32 @@ struct StyledGauge: View { Circle() .stroke(Color.white.opacity(0.12), lineWidth: 5) .frame(width: 48, height: 48) - .animation(.easeOut(duration: 0.2), value: value) Circle() - .trim(from: 0, to: CGFloat(value / 100)) - .stroke( - AngularGradient( - colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)], - center: .center - ), - style: StrokeStyle(lineWidth: 5, lineCap: .round) - ) + .trim(from: 0, to: CGFloat(progress)) + .stroke(AngularGradient( + colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)], + center: .center), + style: StrokeStyle(lineWidth: 5, lineCap: .round)) .rotationEffect(.degrees(-90)) .frame(width: 48, height: 48) + .animation(graphicAnim, value: value) - Text(String(format: "%.0f", value)) + animatedNumber(Text(String(format: "%.0f", value)) .font(.system(size: fontSize - 1, weight: .bold, design: .monospaced)) - .foregroundColor(.primary) + .foregroundColor(.primary)) } + .animation(textAnim, value: value) VStack(alignment: .leading, spacing: 2) { - Text(String(format: "%.1f%@", value, unit)) + animatedNumber(Text(String(format: "%.1f%@", value, unit)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) - .foregroundColor(.primary) + .foregroundColor(.primary)) Text(detail) .font(.system(size: fontSize - 2)) .foregroundColor(.secondary) .lineLimit(1) } + .animation(textAnim, value: value) } } @@ -126,10 +138,10 @@ struct StyledGauge: View { private var textView: some View { HStack { - Text(String(format: "%.1f%@", value, unit)) + animatedNumber(Text(String(format: "%.1f%@", value, unit)) .font(.system(size: fontSize + 6, weight: .bold, design: .monospaced)) - .foregroundColor(gaugeColor) - .animation(.easeOut(duration: 0.2), value: value) + .foregroundColor(gaugeColor)) + .animation(textAnim, value: value) Spacer() Text(detail) .font(.system(size: fontSize - 2)) @@ -147,56 +159,28 @@ struct StyledGauge: View { .fill(Color.white.opacity(0.05)) .frame(width: 44, height: 44) - PieSlice(endAngle: .degrees(360 * value / 100)) + PieSlice(endAngle: .degrees(360 * progress)) .fill(gaugeColor.opacity(0.85)) .frame(width: 42, height: 42) .clipShape(Circle()) - .animation(.easeOut(duration: 0.2), value: value) + .animation(graphicAnim, value: value) - Text(String(format: "%.0f", value)) + animatedNumber(Text(String(format: "%.0f", value)) .font(.system(size: fontSize - 2, weight: .bold, design: .monospaced)) - .foregroundColor(.white) + .foregroundColor(.white)) } + .animation(textAnim, value: value) VStack(alignment: .leading, spacing: 2) { - Text(String(format: "%.1f%@", value, unit)) + animatedNumber(Text(String(format: "%.1f%@", value, unit)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) - .foregroundColor(.primary) - Text(detail) - .font(.system(size: fontSize - 2)) - .foregroundColor(.secondary) - .lineLimit(1) - } - } - } - - // MARK: - 紧凑横条 - - private var compactView: some View { - VStack(spacing: 4) { - GeometryReader { geo in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 4) - .fill(Color.white.opacity(0.1)) - .frame(height: 14) - RoundedRectangle(cornerRadius: 4) - .fill(gaugeColor) - .frame(width: max(geo.size.width * CGFloat(value / 100), 3), height: 14) - .animation(.easeOut(duration: 0.2), value: value) - } - } - .frame(height: 14) - - HStack { - Text(String(format: "%.1f%@", value, unit)) - .font(.system(size: fontSize, weight: .bold, design: .monospaced)) - .foregroundColor(.primary) - Spacer() + .foregroundColor(.primary)) Text(detail) .font(.system(size: fontSize - 2)) .foregroundColor(.secondary) .lineLimit(1) } + .animation(textAnim, value: value) } } } diff --git a/Sources/FloatingMonitor/Views/SettingsView.swift b/Sources/FloatingMonitor/Views/SettingsView.swift index 3c93c99..d04b5b2 100644 --- a/Sources/FloatingMonitor/Views/SettingsView.swift +++ b/Sources/FloatingMonitor/Views/SettingsView.swift @@ -56,7 +56,7 @@ struct SettingsView: View { .padding(.horizontal, 16) .padding(.bottom, 12) } - .frame(width: 340, height: 640) + .frame(width: 340, height: 680) .background(.ultraThinMaterial) } @@ -73,6 +73,8 @@ struct SettingsView: View { toggleRow("GPU", binding: $config.showGPU, icon: "display") toggleRow("内存", binding: $config.showMemory, icon: "memorychip") toggleRow("Swap", binding: $config.showSwap, icon: "arrow.left.arrow.right") + toggleRow("CPU 温度", binding: $config.showCPUTemperature, icon: "thermometer.medium") + toggleRow("GPU 温度", binding: $config.showGPUTemperature, icon: "thermometer.low") toggleRow("进程列表", binding: $config.showTopProcesses, icon: "list.bullet") } } @@ -105,7 +107,9 @@ struct SettingsView: View { styleRow("GPU", style: $config.gpuStyle, row: $config.gpuRow, pos: $config.gpuPos, icon: "display") styleRow("内存", style: $config.memoryStyle, row: $config.memoryRow, pos: $config.memoryPos, icon: "memorychip") styleRow("Swap", style: $config.swapStyle, row: $config.swapRow, pos: $config.swapPos, icon: "arrow.left.arrow.right") - styleRow("进程", style: .constant(.compact), row: $config.processesRow, pos: $config.processesPos, + styleRow("CPU温度", style: $config.cpuTemperatureStyle, row: $config.cpuTemperatureRow, pos: $config.cpuTemperaturePos, icon: "thermometer.medium") + styleRow("GPU温度", style: $config.gpuTemperatureStyle, row: $config.gpuTemperatureRow, pos: $config.gpuTemperaturePos, icon: "thermometer.low") + styleRow("进程", style: .constant(.bar), row: $config.processesRow, pos: $config.processesPos, icon: "list.bullet", styleDisabled: true) } } @@ -280,6 +284,26 @@ struct SettingsView: View { .foregroundColor(.secondary) .frame(width: 38, alignment: .trailing) } + + HStack { + Text("图形动画") + .font(.system(size: 12)) + Spacer() + Toggle("", isOn: $config.gaugeAnimationEnabled) + .toggleStyle(.switch) + .scaleEffect(0.7) + .frame(width: 38) + } + + HStack { + Text("文字动画") + .font(.system(size: 12)) + Spacer() + Toggle("", isOn: $config.textAnimationEnabled) + .toggleStyle(.switch) + .scaleEffect(0.7) + .frame(width: 38) + } } } diff --git a/Sources/Resources/AppIcon.icns b/Sources/Resources/AppIcon.icns index 8e2468c..58c933a 100644 Binary files a/Sources/Resources/AppIcon.icns and b/Sources/Resources/AppIcon.icns differ