- 菜单栏:CPU/GPU/内存多样式、网速两行、温度、风扇 - 面板:概览仪表盘、进程、散热、防休眠,液态玻璃与浅色/深色主题 - 采集层:CPU/内存/网络/GPU/磁盘/电池/进程/SMC 温度与风扇,按界面分级采样 - root 辅助工具:风扇调速、合盖不睡眠,断开时自动恢复 - XcodeGen 工程、单元测试、截图模式
145 lines
5.5 KiB
Swift
145 lines
5.5 KiB
Swift
import Metrics
|
|
import SwiftUI
|
|
|
|
struct KeepAwakePage: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
PageScroll {
|
|
HeroCard()
|
|
HStack(alignment: .top, spacing: DS.Space.s3) {
|
|
VStack(spacing: DS.Space.s3) {
|
|
ModeCard()
|
|
DurationCard()
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
LidCard().frame(maxWidth: .infinity)
|
|
}
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct HeroCard: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
let keepAwake = model.keepAwake
|
|
|
|
Card {
|
|
HStack(spacing: DS.Space.s3) {
|
|
RoundedRectangle(cornerRadius: DS.Radius.md)
|
|
.fill(keepAwake.isActive ? DS.Palette.primary : DS.Palette.track)
|
|
.frame(width: DS.Space.s12 - DS.Space.s2, height: DS.Space.s12 - DS.Space.s2)
|
|
.overlay {
|
|
Image(systemName: keepAwake.isActive ? "cup.and.saucer.fill" : "cup.and.saucer")
|
|
.font(.system(size: DS.TextSize.lg.rawValue, weight: .medium))
|
|
.foregroundStyle(keepAwake.isActive ? DS.Palette.onPrimary : DS.Palette.textSecondary)
|
|
}
|
|
VStack(alignment: .leading, spacing: DS.Space.s1 / 2) {
|
|
Text("防休眠")
|
|
.dsFont(.lg, weight: .semibold)
|
|
.foregroundStyle(DS.Palette.textPrimary)
|
|
StatusLine()
|
|
}
|
|
Spacer()
|
|
DSToggle(isOn: Binding(get: { keepAwake.isActive },
|
|
set: { value in Task { await keepAwake.setActive(value) } }),
|
|
label: "防休眠")
|
|
}
|
|
|
|
if let notice = keepAwake.notice {
|
|
InfoBanner(icon: keepAwake.noticeIsError ? "exclamationmark.triangle.fill" : "info.circle.fill",
|
|
text: notice,
|
|
tone: keepAwake.noticeIsError ? .error : .primary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct StatusLine: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
let keepAwake = model.keepAwake
|
|
TimelineView(.periodic(from: .now, by: 30)) { context in
|
|
Text(text(now: context.date))
|
|
.dsFont(.sm)
|
|
.foregroundStyle(keepAwake.isActive ? DS.Palette.primary : DS.Palette.textSecondary)
|
|
.monospacedDigit()
|
|
}
|
|
}
|
|
|
|
private func text(now: Date) -> String {
|
|
let keepAwake = model.keepAwake
|
|
guard keepAwake.isActive else { return "未开启,Mac 按系统设置休眠" }
|
|
var parts = [keepAwake.mode.title]
|
|
if keepAwake.lidClosedActive { parts.append("合盖运行") }
|
|
if let end = keepAwake.endDate {
|
|
let minutes = max(1, Int(ceil(end.timeIntervalSince(now) / 60)))
|
|
parts.append("剩余 \(Format.duration(minutes: minutes))")
|
|
} else {
|
|
parts.append("不限时")
|
|
}
|
|
return parts.joined(separator: " · ")
|
|
}
|
|
}
|
|
|
|
private struct ModeCard: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
Card {
|
|
Text("模式").dsFont(.sm, weight: .semibold).foregroundStyle(DS.Palette.textPrimary)
|
|
ForEach(Array(KeepAwakeController.Mode.allCases.enumerated()), id: \.element) { index, mode in
|
|
if index > 0 { HairlineDivider() }
|
|
RadioRow(title: mode.title, subtitle: mode.subtitle, isSelected: model.keepAwake.mode == mode) {
|
|
model.keepAwake.setMode(mode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct LidCard: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
let keepAwake = model.keepAwake
|
|
|
|
Card {
|
|
SettingRow(title: "合盖后继续运行",
|
|
subtitle: "合上屏幕时 Mac 不进入睡眠,下载、渲染、远程连接不中断。") {
|
|
DSToggle(isOn: Binding(get: { keepAwake.lidClosedRequested },
|
|
set: { value in Task { await keepAwake.setLidClosed(value) } }),
|
|
label: "合盖后继续运行")
|
|
.disabled(!model.helper.isReady)
|
|
}
|
|
|
|
if !model.helper.isReady {
|
|
HelperRequiredBanner(text: "合盖运行需要修改系统睡眠设置,需安装辅助工具(管理员授权一次)。")
|
|
}
|
|
|
|
InfoBanner(icon: "exclamationmark.triangle",
|
|
text: "合盖运行时散热变差,请勿放入包中。使用电池且电量低于 \(model.settings.lidModeBatteryFloor)% 时会自动关闭。",
|
|
tone: .warning)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct DurationCard: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
Card {
|
|
Text("持续时间").dsFont(.sm, weight: .semibold).foregroundStyle(DS.Palette.textPrimary)
|
|
SegmentedControl(selection: Binding(get: { model.keepAwake.duration },
|
|
set: { model.keepAwake.setDuration($0) }),
|
|
options: KeepAwakeController.Duration.allCases.map { ($0, $0.title) })
|
|
Text("到时后自动恢复系统默认的睡眠行为。")
|
|
.dsFont(.xs)
|
|
.foregroundStyle(DS.Palette.textTertiary)
|
|
}
|
|
}
|
|
}
|