Monitor/Sources/FloatingMonitor/ContentView.swift

270 lines
9.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}
}
// 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"
}
}
}