添加温度显示(CPU/GPU独立模块,SMC传感器),拆分温度配置,去掉紧凑横条,添加图形/文字动画开关,数值动画优化

This commit is contained in:
JOJO 2026-05-06 10:37:58 +08:00
parent ee96706ab1
commit f749771135
7 changed files with 474 additions and 91 deletions

View File

@ -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
}

View File

@ -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)))

View File

@ -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 // selector5=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..<totalKeys {
guard let keyName = keyAtIndex(i),
let (dataType, _) = keyInfo(keyName) else { continue }
// sp78 key
let isTempType = (dataType == "flt " || dataType == "sp78")
guard isTempType else { continue }
if keyName.hasPrefix("Tp") {
cpu.append(keyName)
} else if keyName.hasPrefix("Tg") {
gpu.append(keyName)
}
}
if !cpu.isEmpty { cpuKeys = cpu }
if !gpu.isEmpty { gpuKeys = gpu }
}
// MARK: - SMC
private func readKeyCount() -> 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<SMCParamStruct>.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))
}
}

View File

@ -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]) {

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

Binary file not shown.