Monitor/Sources/FloatingMonitor/ContentView.swift
JOJO 4ab9a7b28c Initial: FloatingMonitor — macOS 悬浮系统监控
- 实时 CPU/GPU/内存/Swap 监控 + Top 进程
- 5 种计量器样式(圆环/条状/纯文本/扇形/紧凑)
- 半透明悬浮窗 + 菜单栏图标控制
- 独立设置窗口,每模块可指定行号+列位布局
- 窗口宽高/透明度/字体全局可调
2026-05-05 21:26:44 +08:00

228 lines
7.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)
.onAppear {
monitor.interval = config.refreshInterval
monitor.topN = config.processCount
applyOpacity()
}
.onChange(of: config.processCount) { _, new in
monitor.topN = new
}
.onChange(of: config.opacity) { _, _ in
applyOpacity()
}
.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(.primary.opacity(0.85))
Spacer()
Text(timeString)
.font(.system(size: 10, design: .monospaced))
.foregroundColor(.secondary.opacity(0.5))
Button(action: { windowController?.hide() }) {
Image(systemName: "xmark")
.font(.system(size: 11, weight: .medium))
.foregroundColor(.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
)
}
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
)
}
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
)
}
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
)
}
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.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"
}
}
private func applyOpacity() {
DispatchQueue.main.async {
windowController?.applyOpacity(config.opacity)
}
}
}