添加温度显示(CPU/GPU独立模块,SMC传感器),拆分温度配置,去掉紧凑横条,添加图形/文字动画开关,数值动画优化
This commit is contained in:
parent
ee96706ab1
commit
f749771135
@ -7,7 +7,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
|
|||||||
case ring // 圆环进度
|
case ring // 圆环进度
|
||||||
case text // 纯文本百分比
|
case text // 纯文本百分比
|
||||||
case pie // 扇形小圆盘
|
case pie // 扇形小圆盘
|
||||||
case compact // 紧凑横条 + 文字
|
|
||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
@ -17,7 +16,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
|
|||||||
case .ring: return "圆环进度"
|
case .ring: return "圆环进度"
|
||||||
case .text: return "纯文本"
|
case .text: return "纯文本"
|
||||||
case .pie: return "扇形圆盘"
|
case .pie: return "扇形圆盘"
|
||||||
case .compact: return "紧凑横条"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +25,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
|
|||||||
case .ring: return "circle.dotted"
|
case .ring: return "circle.dotted"
|
||||||
case .text: return "textformat"
|
case .text: return "textformat"
|
||||||
case .pie: return "chart.pie"
|
case .pie: return "chart.pie"
|
||||||
case .compact: return "rectangle.compress.vertical"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,6 +62,12 @@ final class Configuration: ObservableObject {
|
|||||||
@Published var swapStyle: GaugeDisplayStyle {
|
@Published var swapStyle: GaugeDisplayStyle {
|
||||||
didSet { save("swapStyle", swapStyle.rawValue) }
|
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 {
|
@Published var showCPU: Bool {
|
||||||
didSet { save("showCPU", showCPU) }
|
didSet { save("showCPU", showCPU) }
|
||||||
@ -78,6 +81,15 @@ final class Configuration: ObservableObject {
|
|||||||
@Published var showSwap: Bool {
|
@Published var showSwap: Bool {
|
||||||
didSet { save("showSwap", showSwap) }
|
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 {
|
@Published var showTopProcesses: Bool {
|
||||||
didSet { save("showTopProcesses", showTopProcesses) }
|
didSet { save("showTopProcesses", showTopProcesses) }
|
||||||
}
|
}
|
||||||
@ -101,6 +113,13 @@ final class Configuration: ObservableObject {
|
|||||||
didSet { save("windowHeight", windowHeight) }
|
didSet { save("windowHeight", windowHeight) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var gaugeAnimationEnabled: Bool {
|
||||||
|
didSet { save("gaugeAnimationEnabled", gaugeAnimationEnabled) }
|
||||||
|
}
|
||||||
|
@Published var textAnimationEnabled: Bool {
|
||||||
|
didSet { save("textAnimationEnabled", textAnimationEnabled) }
|
||||||
|
}
|
||||||
|
|
||||||
@Published var cpuPos: ModulePosition {
|
@Published var cpuPos: ModulePosition {
|
||||||
didSet { save("cpuPos", cpuPos.rawValue) }
|
didSet { save("cpuPos", cpuPos.rawValue) }
|
||||||
}
|
}
|
||||||
@ -113,6 +132,15 @@ final class Configuration: ObservableObject {
|
|||||||
@Published var swapPos: ModulePosition {
|
@Published var swapPos: ModulePosition {
|
||||||
didSet { save("swapPos", swapPos.rawValue) }
|
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 {
|
@Published var processesPos: ModulePosition {
|
||||||
didSet { save("processesPos", processesPos.rawValue) }
|
didSet { save("processesPos", processesPos.rawValue) }
|
||||||
}
|
}
|
||||||
@ -130,6 +158,15 @@ final class Configuration: ObservableObject {
|
|||||||
@Published var swapRow: Int {
|
@Published var swapRow: Int {
|
||||||
didSet { save("swapRow", swapRow) }
|
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 {
|
@Published var processesRow: Int {
|
||||||
didSet { save("processesRow", processesRow) }
|
didSet { save("processesRow", processesRow) }
|
||||||
}
|
}
|
||||||
@ -141,11 +178,16 @@ final class Configuration: ObservableObject {
|
|||||||
gpuStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "gpuStyle") ?? "ring") ?? .ring
|
gpuStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "gpuStyle") ?? "ring") ?? .ring
|
||||||
memoryStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "memoryStyle") ?? "bar") ?? .bar
|
memoryStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "memoryStyle") ?? "bar") ?? .bar
|
||||||
swapStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "swapStyle") ?? "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
|
showCPU = defaults.object(forKey: "showCPU") as? Bool ?? true
|
||||||
showGPU = defaults.object(forKey: "showGPU") as? Bool ?? true
|
showGPU = defaults.object(forKey: "showGPU") as? Bool ?? true
|
||||||
showMemory = defaults.object(forKey: "showMemory") as? Bool ?? true
|
showMemory = defaults.object(forKey: "showMemory") as? Bool ?? true
|
||||||
showSwap = defaults.object(forKey: "showSwap") 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
|
showTopProcesses = defaults.object(forKey: "showTopProcesses") as? Bool ?? true
|
||||||
|
|
||||||
processCount = defaults.object(forKey: "processCount") as? Int ?? 5
|
processCount = defaults.object(forKey: "processCount") as? Int ?? 5
|
||||||
@ -155,16 +197,25 @@ final class Configuration: ObservableObject {
|
|||||||
windowWidth = defaults.object(forKey: "windowWidth") as? Double ?? 250
|
windowWidth = defaults.object(forKey: "windowWidth") as? Double ?? 250
|
||||||
windowHeight = defaults.object(forKey: "windowHeight") as? Double ?? 300
|
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
|
cpuPos = ModulePosition(rawValue: defaults.string(forKey: "cpuPos") ?? "full") ?? .full
|
||||||
gpuPos = ModulePosition(rawValue: defaults.string(forKey: "gpuPos") ?? "full") ?? .full
|
gpuPos = ModulePosition(rawValue: defaults.string(forKey: "gpuPos") ?? "full") ?? .full
|
||||||
memoryPos = ModulePosition(rawValue: defaults.string(forKey: "memoryPos") ?? "full") ?? .full
|
memoryPos = ModulePosition(rawValue: defaults.string(forKey: "memoryPos") ?? "full") ?? .full
|
||||||
swapPos = ModulePosition(rawValue: defaults.string(forKey: "swapPos") ?? "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
|
processesPos = ModulePosition(rawValue: defaults.string(forKey: "processesPos") ?? "full") ?? .full
|
||||||
|
|
||||||
cpuRow = defaults.object(forKey: "cpuRow") as? Int ?? 1
|
cpuRow = defaults.object(forKey: "cpuRow") as? Int ?? 1
|
||||||
gpuRow = defaults.object(forKey: "gpuRow") as? Int ?? 2
|
gpuRow = defaults.object(forKey: "gpuRow") as? Int ?? 2
|
||||||
memoryRow = defaults.object(forKey: "memoryRow") as? Int ?? 3
|
memoryRow = defaults.object(forKey: "memoryRow") as? Int ?? 3
|
||||||
swapRow = defaults.object(forKey: "swapRow") as? Int ?? 4
|
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
|
processesRow = defaults.object(forKey: "processesRow") as? Int ?? 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -88,7 +88,9 @@ struct ContentView: View {
|
|||||||
title: "CPU", icon: "cpu",
|
title: "CPU", icon: "cpu",
|
||||||
value: monitor.info.cpuUsage, unit: "%",
|
value: monitor.info.cpuUsage, unit: "%",
|
||||||
detail: "\(monitor.info.cpuCores) 核心 · \(monitor.info.cpuModel)",
|
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",
|
title: "GPU", icon: "display",
|
||||||
value: monitor.info.gpuUsage ?? 0, unit: "%",
|
value: monitor.info.gpuUsage ?? 0, unit: "%",
|
||||||
detail: monitor.info.gpuModel + (monitor.info.gpuUsage == nil ? " (N/A)" : ""),
|
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",
|
title: "内存", icon: "memorychip",
|
||||||
value: monitor.info.memoryUsagePercent, unit: "%",
|
value: monitor.info.memoryUsagePercent, unit: "%",
|
||||||
detail: formatBytes(monitor.info.memoryUsed) + " / " + formatBytes(monitor.info.memoryTotal),
|
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",
|
title: "Swap", icon: "arrow.left.arrow.right",
|
||||||
value: monitor.info.swapUsagePercent, unit: "%",
|
value: monitor.info.swapUsagePercent, unit: "%",
|
||||||
detail: formatBytes(monitor.info.swapUsed) + " / " + formatBytes(monitor.info.swapTotal),
|
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 {
|
private var processesSection: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
ProcessListView(
|
ProcessListView(
|
||||||
@ -162,6 +204,14 @@ struct ContentView: View {
|
|||||||
items.append(Item(name: "Swap", row: config.swapRow, position: config.swapPos,
|
items.append(Item(name: "Swap", row: config.swapRow, position: config.swapPos,
|
||||||
view: AnyView(swapGauge)))
|
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 {
|
if config.showTopProcesses {
|
||||||
items.append(Item(name: "进程", row: config.processesRow, position: config.processesPos,
|
items.append(Item(name: "进程", row: config.processesRow, position: config.processesPos,
|
||||||
view: AnyView(processesSection)))
|
view: AnyView(processesSection)))
|
||||||
|
|||||||
259
Sources/FloatingMonitor/SMCSensor.swift
Normal file
259
Sources/FloatingMonitor/SMCSensor.swift
Normal 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 // 子 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..<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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,9 @@ struct SystemInfo {
|
|||||||
var swapIns: UInt64
|
var swapIns: UInt64
|
||||||
var swapOuts: UInt64
|
var swapOuts: UInt64
|
||||||
|
|
||||||
|
var cpuTemperature: Double? // °C
|
||||||
|
var gpuTemperature: Double? // °C
|
||||||
|
|
||||||
var topCPUProcesses: [ProcessInfo]
|
var topCPUProcesses: [ProcessInfo]
|
||||||
var topMemoryProcesses: [ProcessInfo]
|
var topMemoryProcesses: [ProcessInfo]
|
||||||
|
|
||||||
@ -44,6 +47,7 @@ final class SystemMonitor: ObservableObject {
|
|||||||
memoryTotal: 0, memoryUsed: 0, memoryUsagePercent: 0,
|
memoryTotal: 0, memoryUsed: 0, memoryUsagePercent: 0,
|
||||||
swapTotal: 0, swapUsed: 0, swapUsagePercent: 0,
|
swapTotal: 0, swapUsed: 0, swapUsagePercent: 0,
|
||||||
swapIns: 0, swapOuts: 0,
|
swapIns: 0, swapOuts: 0,
|
||||||
|
cpuTemperature: nil, gpuTemperature: nil,
|
||||||
topCPUProcesses: [], topMemoryProcesses: []
|
topCPUProcesses: [], topMemoryProcesses: []
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -90,6 +94,7 @@ final class SystemMonitor: ObservableObject {
|
|||||||
let gpu = fetchGPU()
|
let gpu = fetchGPU()
|
||||||
let mem = fetchMemory()
|
let mem = fetchMemory()
|
||||||
let swap = fetchSwap()
|
let swap = fetchSwap()
|
||||||
|
let temp = fetchTemperature()
|
||||||
let procs = fetchTopProcesses(count: topN)
|
let procs = fetchTopProcesses(count: topN)
|
||||||
|
|
||||||
info = SystemInfo(
|
info = SystemInfo(
|
||||||
@ -106,6 +111,8 @@ final class SystemMonitor: ObservableObject {
|
|||||||
swapUsagePercent: swap.percent,
|
swapUsagePercent: swap.percent,
|
||||||
swapIns: swap.ins,
|
swapIns: swap.ins,
|
||||||
swapOuts: swap.outs,
|
swapOuts: swap.outs,
|
||||||
|
cpuTemperature: temp.cpu,
|
||||||
|
gpuTemperature: temp.gpu,
|
||||||
topCPUProcesses: procs.cpu,
|
topCPUProcesses: procs.cpu,
|
||||||
topMemoryProcesses: procs.mem
|
topMemoryProcesses: procs.mem
|
||||||
)
|
)
|
||||||
@ -266,6 +273,14 @@ final class SystemMonitor: ObservableObject {
|
|||||||
return (0, 0, 0, 0, 0)
|
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: - 进程列表
|
// MARK: - 进程列表
|
||||||
|
|
||||||
private func fetchTopProcesses(count: Int) -> (cpu: [SystemInfo.ProcessInfo], mem: [SystemInfo.ProcessInfo]) {
|
private func fetchTopProcesses(count: Int) -> (cpu: [SystemInfo.ProcessInfo], mem: [SystemInfo.ProcessInfo]) {
|
||||||
|
|||||||
@ -5,23 +5,46 @@ import SwiftUI
|
|||||||
struct StyledGauge: View {
|
struct StyledGauge: View {
|
||||||
let title: String
|
let title: String
|
||||||
let icon: String
|
let icon: String
|
||||||
let value: Double // 0-100
|
let value: Double
|
||||||
let unit: String // "%" / "GB"
|
let unit: String
|
||||||
let detail: String // 副标题(如 "3.2 / 16 GB")
|
let detail: String
|
||||||
let style: GaugeDisplayStyle
|
let style: GaugeDisplayStyle
|
||||||
let fontSize: Double
|
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 {
|
private var gaugeColor: Color {
|
||||||
if value < 40 { return .green }
|
if progress < 0.4 { return .green }
|
||||||
if value < 70 { return .yellow }
|
if progress < 0.7 { return .yellow }
|
||||||
if value < 85 { return .orange }
|
if progress < 0.85 { return .orange }
|
||||||
return .red
|
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 {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
// 标题行
|
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.system(size: fontSize))
|
.font(.system(size: fontSize))
|
||||||
@ -32,18 +55,11 @@ struct StyledGauge: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据样式渲染
|
|
||||||
switch style {
|
switch style {
|
||||||
case .bar:
|
case .bar: barView
|
||||||
barView
|
case .ring: ringView
|
||||||
case .ring:
|
case .text: textView
|
||||||
ringView
|
case .pie: pieView
|
||||||
case .text:
|
|
||||||
textView
|
|
||||||
case .pie:
|
|
||||||
pieView
|
|
||||||
case .compact:
|
|
||||||
compactView
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,29 +74,26 @@ struct StyledGauge: View {
|
|||||||
.fill(Color.white.opacity(0.12))
|
.fill(Color.white.opacity(0.12))
|
||||||
.frame(height: 8)
|
.frame(height: 8)
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(
|
.fill(LinearGradient(
|
||||||
LinearGradient(
|
colors: [gaugeColor.opacity(0.8), gaugeColor],
|
||||||
colors: [gaugeColor.opacity(0.8), gaugeColor],
|
startPoint: .leading, endPoint: .trailing))
|
||||||
startPoint: .leading,
|
.frame(width: max(geo.size.width * CGFloat(progress), 4), height: 8)
|
||||||
endPoint: .trailing
|
.animation(graphicAnim, value: value)
|
||||||
)
|
|
||||||
)
|
|
||||||
.frame(width: max(geo.size.width * CGFloat(value / 100), 4), height: 8)
|
|
||||||
.animation(.easeOut(duration: 0.2), value: value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: 8)
|
.frame(height: 8)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(String(format: "%.1f%@", value, unit))
|
animatedNumber(Text(String(format: "%.1f%@", value, unit))
|
||||||
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
|
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(detail)
|
Text(detail)
|
||||||
.font(.system(size: fontSize - 2))
|
.font(.system(size: fontSize - 2))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
.animation(textAnim, value: value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,33 +105,32 @@ struct StyledGauge: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.stroke(Color.white.opacity(0.12), lineWidth: 5)
|
.stroke(Color.white.opacity(0.12), lineWidth: 5)
|
||||||
.frame(width: 48, height: 48)
|
.frame(width: 48, height: 48)
|
||||||
.animation(.easeOut(duration: 0.2), value: value)
|
|
||||||
Circle()
|
Circle()
|
||||||
.trim(from: 0, to: CGFloat(value / 100))
|
.trim(from: 0, to: CGFloat(progress))
|
||||||
.stroke(
|
.stroke(AngularGradient(
|
||||||
AngularGradient(
|
colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)],
|
||||||
colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)],
|
center: .center),
|
||||||
center: .center
|
style: StrokeStyle(lineWidth: 5, lineCap: .round))
|
||||||
),
|
|
||||||
style: StrokeStyle(lineWidth: 5, lineCap: .round)
|
|
||||||
)
|
|
||||||
.rotationEffect(.degrees(-90))
|
.rotationEffect(.degrees(-90))
|
||||||
.frame(width: 48, height: 48)
|
.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))
|
.font(.system(size: fontSize - 1, weight: .bold, design: .monospaced))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary))
|
||||||
}
|
}
|
||||||
|
.animation(textAnim, value: value)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
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))
|
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary))
|
||||||
Text(detail)
|
Text(detail)
|
||||||
.font(.system(size: fontSize - 2))
|
.font(.system(size: fontSize - 2))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
.animation(textAnim, value: value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,10 +138,10 @@ struct StyledGauge: View {
|
|||||||
|
|
||||||
private var textView: some View {
|
private var textView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text(String(format: "%.1f%@", value, unit))
|
animatedNumber(Text(String(format: "%.1f%@", value, unit))
|
||||||
.font(.system(size: fontSize + 6, weight: .bold, design: .monospaced))
|
.font(.system(size: fontSize + 6, weight: .bold, design: .monospaced))
|
||||||
.foregroundColor(gaugeColor)
|
.foregroundColor(gaugeColor))
|
||||||
.animation(.easeOut(duration: 0.2), value: value)
|
.animation(textAnim, value: value)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(detail)
|
Text(detail)
|
||||||
.font(.system(size: fontSize - 2))
|
.font(.system(size: fontSize - 2))
|
||||||
@ -147,56 +159,28 @@ struct StyledGauge: View {
|
|||||||
.fill(Color.white.opacity(0.05))
|
.fill(Color.white.opacity(0.05))
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
|
|
||||||
PieSlice(endAngle: .degrees(360 * value / 100))
|
PieSlice(endAngle: .degrees(360 * progress))
|
||||||
.fill(gaugeColor.opacity(0.85))
|
.fill(gaugeColor.opacity(0.85))
|
||||||
.frame(width: 42, height: 42)
|
.frame(width: 42, height: 42)
|
||||||
.clipShape(Circle())
|
.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))
|
.font(.system(size: fontSize - 2, weight: .bold, design: .monospaced))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white))
|
||||||
}
|
}
|
||||||
|
.animation(textAnim, value: value)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
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))
|
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
|
||||||
.foregroundColor(.primary)
|
.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()
|
|
||||||
Text(detail)
|
Text(detail)
|
||||||
.font(.system(size: fontSize - 2))
|
.font(.system(size: fontSize - 2))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
.animation(textAnim, value: value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ struct SettingsView: View {
|
|||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.bottom, 12)
|
.padding(.bottom, 12)
|
||||||
}
|
}
|
||||||
.frame(width: 340, height: 640)
|
.frame(width: 340, height: 680)
|
||||||
.background(.ultraThinMaterial)
|
.background(.ultraThinMaterial)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +73,8 @@ struct SettingsView: View {
|
|||||||
toggleRow("GPU", binding: $config.showGPU, icon: "display")
|
toggleRow("GPU", binding: $config.showGPU, icon: "display")
|
||||||
toggleRow("内存", binding: $config.showMemory, icon: "memorychip")
|
toggleRow("内存", binding: $config.showMemory, icon: "memorychip")
|
||||||
toggleRow("Swap", binding: $config.showSwap, icon: "arrow.left.arrow.right")
|
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")
|
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("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("内存", 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("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)
|
icon: "list.bullet", styleDisabled: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,6 +284,26 @@ struct SettingsView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.frame(width: 38, alignment: .trailing)
|
.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.
Loading…
Reference in New Issue
Block a user