273 lines
9.7 KiB
Swift
273 lines
9.7 KiB
Swift
import SwiftUI
|
||
|
||
struct ContentView: View {
|
||
@ObservedObject var monitor: SystemMonitor
|
||
@ObservedObject private var config = Configuration.shared
|
||
|
||
weak var windowController: FloatingWindowController?
|
||
|
||
init(monitor: SystemMonitor, windowController: FloatingWindowController? = nil) {
|
||
self.monitor = monitor
|
||
self.windowController = windowController
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
// ── 顶部固定区域:图标 + Monitor + 时间 + 关闭按钮 ──
|
||
headerBar
|
||
|
||
Divider()
|
||
.background(Color.white.opacity(0.1))
|
||
|
||
// ── 可滚动内容 ──
|
||
ScrollView(.vertical, showsIndicators: false) {
|
||
VStack(spacing: 14) {
|
||
ForEach(Array(moduleRows.enumerated()), id: \.offset) { _, row in
|
||
row
|
||
}
|
||
}
|
||
.padding(.top, 6)
|
||
.padding(.horizontal, 16)
|
||
.padding(.bottom, 16)
|
||
}
|
||
}
|
||
.frame(minWidth: 240, idealWidth: 280, maxWidth: 400,
|
||
minHeight: 200, idealHeight: 480, maxHeight: 700)
|
||
.environment(\.colorScheme, (config.invertTextColor && !config.highlightMode) ? .dark : .light)
|
||
.onAppear {
|
||
monitor.interval = config.refreshInterval
|
||
monitor.topN = config.processCount
|
||
}
|
||
.onChange(of: config.processCount) { _, new in
|
||
monitor.topN = new
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: .refreshIntervalChanged)) { notification in
|
||
if let interval = notification.object as? Double {
|
||
monitor.interval = interval
|
||
}
|
||
}
|
||
.onReceive(monitor.$info) { _ in
|
||
windowController?.refreshAfterDataUpdate()
|
||
}
|
||
}
|
||
|
||
// MARK: - 顶部栏(精简版)
|
||
|
||
private var headerBar: some View {
|
||
HStack(spacing: 6) {
|
||
Image(systemName: "gauge.with.dots.needle.33percent")
|
||
.font(.system(size: 13, weight: .semibold))
|
||
.foregroundColor(.accentColor)
|
||
|
||
Text("Monitor")
|
||
.font(.system(size: config.fontSize, weight: .semibold))
|
||
.foregroundColor(config.highlightMode ? .accentColor : .primary.opacity(0.85))
|
||
|
||
Spacer()
|
||
|
||
Text(timeString)
|
||
.font(.system(size: 10, design: .monospaced))
|
||
.foregroundColor(config.highlightMode ? .accentColor : .secondary.opacity(0.5))
|
||
|
||
Button(action: { windowController?.hide() }) {
|
||
Image(systemName: "xmark")
|
||
.font(.system(size: 11, weight: .medium))
|
||
.foregroundColor(config.highlightMode ? .accentColor : .secondary.opacity(0.5))
|
||
}
|
||
.buttonStyle(.plain)
|
||
.help("隐藏窗口(点击菜单栏图标重新打开)")
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.vertical, 10)
|
||
}
|
||
|
||
// MARK: - 各指标
|
||
|
||
private var cpuGauge: some View {
|
||
StyledGauge(
|
||
title: "CPU", icon: "cpu",
|
||
value: monitor.info.cpuUsage, unit: "%",
|
||
detail: "\(monitor.info.cpuCores) 核心 · \(monitor.info.cpuModel)",
|
||
style: config.cpuStyle, fontSize: config.fontSize,
|
||
animateGraphic: config.gaugeAnimationEnabled,
|
||
animateText: config.textAnimationEnabled
|
||
)
|
||
}
|
||
|
||
private var gpuGauge: some View {
|
||
StyledGauge(
|
||
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,
|
||
animateGraphic: config.gaugeAnimationEnabled,
|
||
animateText: config.textAnimationEnabled
|
||
)
|
||
}
|
||
|
||
private var memoryGauge: some View {
|
||
StyledGauge(
|
||
title: "内存", icon: "memorychip",
|
||
value: monitor.info.memoryUsagePercent, unit: "%",
|
||
detail: formatBytes(monitor.info.memoryUsed) + " / " + formatBytes(monitor.info.memoryTotal),
|
||
style: config.memoryStyle, fontSize: config.fontSize,
|
||
animateGraphic: config.gaugeAnimationEnabled,
|
||
animateText: config.textAnimationEnabled
|
||
)
|
||
}
|
||
|
||
private var swapGauge: some View {
|
||
StyledGauge(
|
||
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,
|
||
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(
|
||
processes: monitor.info.topCPUProcesses,
|
||
title: "CPU 消耗 Top", icon: "bolt.horizontal",
|
||
isMemory: false, fontSize: config.fontSize
|
||
)
|
||
ProcessListView(
|
||
processes: monitor.info.topMemoryProcesses,
|
||
title: "内存消耗 Top", icon: "tray.full",
|
||
isMemory: true, fontSize: config.fontSize
|
||
)
|
||
}
|
||
}
|
||
|
||
// MARK: - 布局引擎(左/右/全宽排列)
|
||
|
||
/// 根据行号 + 列位生成布局
|
||
private var moduleRows: [AnyView] {
|
||
struct Item {
|
||
let name: String
|
||
let row: Int
|
||
let position: ModulePosition
|
||
let view: AnyView
|
||
}
|
||
|
||
var items: [Item] = []
|
||
if config.showCPU {
|
||
items.append(Item(name: "CPU", row: config.cpuRow, position: config.cpuPos,
|
||
view: AnyView(cpuGauge)))
|
||
}
|
||
if config.showGPU {
|
||
items.append(Item(name: "GPU", row: config.gpuRow, position: config.gpuPos,
|
||
view: AnyView(gpuGauge.opacity(monitor.info.gpuUsage == nil ? 0.5 : 1.0))))
|
||
}
|
||
if config.showMemory {
|
||
items.append(Item(name: "内存", row: config.memoryRow, position: config.memoryPos,
|
||
view: AnyView(memoryGauge)))
|
||
}
|
||
if config.showSwap {
|
||
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)))
|
||
}
|
||
|
||
// 按行号排序
|
||
items.sort { $0.row < $1.row }
|
||
|
||
// 同行 left+right 配对,full 独占
|
||
var rows: [AnyView] = []
|
||
var i = 0
|
||
while i < items.count {
|
||
let item = items[i]
|
||
switch item.position {
|
||
case .full:
|
||
rows.append(item.view)
|
||
i += 1
|
||
case .left:
|
||
// 找同行下一个 right
|
||
if let j = (i+1..<items.count).first(where: { items[$0].row == item.row && items[$0].position == .right }) {
|
||
rows.append(AnyView(HStack(alignment: .top, spacing: 12) {
|
||
item.view
|
||
items[j].view
|
||
}))
|
||
items.remove(at: j)
|
||
} else {
|
||
rows.append(item.view)
|
||
}
|
||
i += 1
|
||
case .right:
|
||
rows.append(item.view)
|
||
i += 1
|
||
}
|
||
}
|
||
return rows
|
||
}
|
||
|
||
// MARK: - 工具
|
||
|
||
private var timeString: String {
|
||
let f = DateFormatter()
|
||
f.dateFormat = "HH:mm:ss"
|
||
return f.string(from: Date())
|
||
}
|
||
|
||
private func formatBytes(_ bytes: UInt64) -> String {
|
||
if bytes >= 1_073_741_824 {
|
||
return String(format: "%.1f GB", Double(bytes) / 1_073_741_824)
|
||
} else if bytes >= 1_048_576 {
|
||
return String(format: "%.0f MB", Double(bytes) / 1_048_576)
|
||
} else if bytes >= 1_024 {
|
||
return String(format: "%.0f KB", Double(bytes) / 1_024)
|
||
} else {
|
||
return "\(bytes) B"
|
||
}
|
||
}
|
||
|
||
}
|