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

297 lines
10 KiB
Swift

import SwiftUI
struct SettingsView: View {
@ObservedObject var config = Configuration.shared
@ObservedObject var monitor: SystemMonitor
var body: some View {
VStack(spacing: 0) {
//
HStack {
Text("显示设置")
.font(.system(size: 14, weight: .semibold))
Spacer()
Button(action: { closeWindow() }) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 16))
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
.padding(.horizontal, 16)
.padding(.top, 16)
.padding(.bottom, 12)
Divider().background(Color.white.opacity(0.1))
ScrollView {
VStack(spacing: 16) {
// /
toggleGroup
Divider().background(Color.white.opacity(0.08))
//
styleGroup
Divider().background(Color.white.opacity(0.08))
//
processGroup
Divider().background(Color.white.opacity(0.08))
//
otherGroup
}
.padding(16)
}
//
HStack {
Spacer()
Button("完成") { closeWindow() }
.keyboardShortcut(.return)
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
}
.frame(width: 340, height: 640)
.background(.ultraThinMaterial)
}
// MARK: - /
private var toggleGroup: some View {
VStack(alignment: .leading, spacing: 8) {
Text("显示内容")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.secondary)
.padding(.leading, 4)
toggleRow("CPU", binding: $config.showCPU, icon: "cpu")
toggleRow("GPU", binding: $config.showGPU, icon: "display")
toggleRow("内存", binding: $config.showMemory, icon: "memorychip")
toggleRow("Swap", binding: $config.showSwap, icon: "arrow.left.arrow.right")
toggleRow("进程列表", binding: $config.showTopProcesses, icon: "list.bullet")
}
}
private func toggleRow(_ label: String, binding: Binding<Bool>, icon: String) -> some View {
HStack {
Image(systemName: icon)
.frame(width: 18)
.foregroundColor(binding.wrappedValue ? .accentColor : .secondary.opacity(0.5))
Text(label)
.font(.system(size: 12))
Spacer()
Toggle("", isOn: binding)
.toggleStyle(.switch)
.scaleEffect(0.7)
.frame(width: 38)
}
}
// MARK: -
private var styleGroup: some View {
VStack(alignment: .leading, spacing: 8) {
Text("样式 · 行号 · 列位")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.secondary)
.padding(.leading, 4)
styleRow("CPU", style: $config.cpuStyle, row: $config.cpuRow, pos: $config.cpuPos, icon: "cpu")
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,
icon: "list.bullet", styleDisabled: true)
}
}
private func styleRow(_ label: String, style: Binding<GaugeDisplayStyle>,
row: Binding<Int>, pos: Binding<ModulePosition>, icon: String,
styleDisabled: Bool = false) -> some View {
HStack(spacing: 6) {
Image(systemName: icon)
.frame(width: 14)
.foregroundColor(.secondary)
Text(label)
.font(.system(size: 11))
.frame(width: 26, alignment: .leading)
// stepper
Stepper("", value: row, in: 1...10)
.labelsHidden()
.scaleEffect(0.7)
.frame(width: 24)
Text("\(row.wrappedValue)")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.secondary)
.frame(width: 14, alignment: .center)
Spacer()
if styleDisabled {
Text("")
.foregroundColor(.secondary.opacity(0.4))
.frame(width: 64)
} else {
Picker("", selection: style) {
ForEach(GaugeDisplayStyle.allCases) { s in
Text(s.displayName).tag(s)
}
}
.pickerStyle(.menu)
.frame(width: 76)
}
Picker("", selection: pos) {
ForEach(ModulePosition.allCases) { p in
Text(p.displayName).tag(p)
}
}
.pickerStyle(.segmented)
.frame(width: 90)
}
}
// MARK: -
private var processGroup: some View {
VStack(alignment: .leading, spacing: 6) {
Text("进程列表")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.secondary)
.padding(.leading, 4)
HStack {
Text("显示数量")
.font(.system(size: 12))
Spacer()
Picker("", selection: $config.processCount) {
ForEach([3, 5, 8, 10], id: \.self) { n in
Text("\(n)").tag(n)
}
}
.pickerStyle(.segmented)
.frame(width: 160)
}
}
}
// MARK: -
private var otherGroup: some View {
VStack(alignment: .leading, spacing: 8) {
Text("其他")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.secondary)
.padding(.leading, 4)
HStack {
Text("刷新间隔")
.font(.system(size: 12))
Spacer()
Picker("", selection: Binding<Double>(
get: { config.refreshInterval },
set: { newValue in
config.refreshInterval = newValue
NotificationCenter.default.post(
name: .refreshIntervalChanged, object: newValue
)
}
)) {
Text("0.5s").tag(0.5)
Text("1.0s").tag(1.0)
Text("1.5s").tag(1.5)
Text("2.0s").tag(2.0)
Text("3.0s").tag(3.0)
}
.pickerStyle(.segmented)
.frame(width: 180)
}
HStack {
Text("透明度")
.font(.system(size: 12))
.frame(width: 52, alignment: .leading)
Slider(value: $config.opacity, in: 0.5...1.0, step: 0.05) {
Text("")
} onEditingChanged: { editing in
if !editing { updateWindowOpacity() }
}
.frame(width: 130)
Text(String(format: "%.0f%%", config.opacity * 100))
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.secondary)
.frame(width: 38, alignment: .trailing)
}
HStack {
Text("字体大小")
.font(.system(size: 12))
.frame(width: 52, alignment: .leading)
Slider(value: $config.fontSize, in: 9...16, step: 1) {
Text("")
}
.frame(width: 130)
Text(String(format: "%.0f", config.fontSize))
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.secondary)
.frame(width: 38, alignment: .trailing)
}
HStack {
Text("窗口宽度")
.font(.system(size: 12))
.frame(width: 52, alignment: .leading)
Slider(value: $config.windowWidth, in: 240...500) {
Text("")
} onEditingChanged: { editing in
if !editing { applyWindowSize() }
}
.frame(width: 130)
Text(String(format: "%.0f", config.windowWidth))
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.secondary)
.frame(width: 38, alignment: .trailing)
}
HStack {
Text("窗口高度")
.font(.system(size: 12))
.frame(width: 52, alignment: .leading)
Slider(value: $config.windowHeight, in: 200...700) {
Text("")
} onEditingChanged: { editing in
if !editing { applyWindowSize() }
}
.frame(width: 130)
Text(String(format: "%.0f", config.windowHeight))
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.secondary)
.frame(width: 38, alignment: .trailing)
}
}
}
private func applyWindowSize() {
NotificationCenter.default.post(
name: .resizeMonitorWindow,
object: NSSize(width: config.windowWidth, height: config.windowHeight)
)
}
private func updateWindowOpacity() {
if let window = NSApplication.shared.windows.first(where: { $0 is FloatingWindow }) {
window.alphaValue = config.opacity
}
}
private func closeWindow() {
NSApp.keyWindow?.close()
}
}