283 lines
11 KiB
Swift
283 lines
11 KiB
Swift
import SwiftUI
|
|
import MacToolsPluginKit
|
|
|
|
// MARK: - FanControlPresetManagerView
|
|
|
|
struct FanControlPresetManagerView: View {
|
|
@ObservedObject var presetStore: FanControlPresetStore
|
|
/// Live snapshot for showing actual hardware max RPM in sliders.
|
|
var fanSnapshot: FanSnapshot
|
|
var localization: PluginLocalization = PluginLocalization(bundle: .main)
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.section) {
|
|
builtInSection
|
|
customSection
|
|
}
|
|
}
|
|
|
|
// MARK: - Built-in Section
|
|
|
|
private var builtInSection: some View {
|
|
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.sectionHeaderContent) {
|
|
sectionHeader(
|
|
title: localization.string("settings.builtIn.title", defaultValue: "内置预设"),
|
|
icon: "lock"
|
|
)
|
|
|
|
VStack(spacing: 0) {
|
|
ForEach(FanControlPresetStore.builtInPresets) { preset in
|
|
BuiltInPresetRow(
|
|
preset: preset,
|
|
fanSnapshot: fanSnapshot,
|
|
localization: localization
|
|
)
|
|
if preset.id != FanControlPresetStore.builtInPresets.last?.id {
|
|
PluginSettingsListDivider()
|
|
}
|
|
}
|
|
}
|
|
.pluginSettingsCardBackground(.host)
|
|
}
|
|
}
|
|
|
|
// MARK: - Custom Section
|
|
|
|
private var customSection: some View {
|
|
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.sectionHeaderContent) {
|
|
HStack {
|
|
sectionHeader(
|
|
title: localization.string("settings.custom.title", defaultValue: "自定义预设"),
|
|
icon: "slider.horizontal.3"
|
|
)
|
|
Spacer()
|
|
Button(action: addPreset) {
|
|
Label(localization.string("settings.custom.add", defaultValue: "添加"), systemImage: "plus")
|
|
.font(PluginSettingsTheme.Typography.controlLabel)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.controlSize(.small)
|
|
}
|
|
|
|
if presetStore.customPresets.isEmpty {
|
|
emptyCustomPresetsView
|
|
} else {
|
|
VStack(spacing: 0) {
|
|
ForEach(presetStore.customPresets) { preset in
|
|
CustomPresetRow(
|
|
preset: preset,
|
|
fanSnapshot: fanSnapshot,
|
|
localization: localization,
|
|
onRename: { presetStore.renameCustomPreset(id: preset.id, newName: $0) },
|
|
onRPMChange: { presetStore.updateCustomPresetRPM(id: preset.id, rpm: $0) },
|
|
onDelete: { presetStore.deleteCustomPreset(id: preset.id) }
|
|
)
|
|
if preset.id != presetStore.customPresets.last?.id {
|
|
PluginSettingsListDivider()
|
|
}
|
|
}
|
|
}
|
|
.pluginSettingsCardBackground(.host)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var emptyCustomPresetsView: some View {
|
|
HStack {
|
|
Spacer()
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "slider.horizontal.3")
|
|
.font(.system(size: PluginSettingsTheme.Size.emptyStateIcon))
|
|
.foregroundStyle(.secondary)
|
|
Text(localization.string("settings.custom.empty", defaultValue: "点击「添加」创建自定义转速预设"))
|
|
.font(PluginSettingsTheme.Typography.pageDescription)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.vertical, PluginSettingsTheme.Spacing.pagePadding)
|
|
Spacer()
|
|
}
|
|
.pluginSettingsCardBackground(.host)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func sectionHeader(title: String, icon: String) -> some View {
|
|
Label(title, systemImage: icon)
|
|
.font(PluginSettingsTheme.Typography.sectionTitle)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
private func addPreset() {
|
|
_ = presetStore.addCustomPreset()
|
|
}
|
|
}
|
|
|
|
// MARK: - BuiltInPresetRow
|
|
|
|
private struct BuiltInPresetRow: View {
|
|
let preset: FanPreset
|
|
let fanSnapshot: FanSnapshot
|
|
let localization: PluginLocalization
|
|
|
|
var body: some View {
|
|
HStack(spacing: PluginSettingsTheme.Spacing.rowContentControl) {
|
|
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.rowTitleDescription) {
|
|
Text(preset.displayName(localization: localization))
|
|
.font(PluginSettingsTheme.Typography.emphasizedRowTitle)
|
|
Text(subtitle)
|
|
.font(PluginSettingsTheme.Typography.rowDescription)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
Text(localization.string("settings.builtIn.badge", defaultValue: "内置"))
|
|
.font(PluginSettingsTheme.Typography.statusBadge)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(
|
|
Capsule().fill(Color(nsColor: .quaternaryLabelColor).opacity(0.4))
|
|
)
|
|
}
|
|
.pluginSettingsListRowPadding()
|
|
}
|
|
|
|
private var subtitle: String {
|
|
switch preset.strategy {
|
|
case .auto:
|
|
return localization.string("settings.builtIn.auto.subtitle", defaultValue: "由 macOS 自动管理")
|
|
case .fullSpeed:
|
|
let max = fanSnapshot.globalMaxSpeed
|
|
return max > 0
|
|
? localization.format("preset.fullSpeed.subtitleWithRPM", defaultValue: "最高 %d RPM", max)
|
|
: localization.string("preset.fullSpeed.subtitle", defaultValue: "最高转速")
|
|
case .fixed(let rpm):
|
|
return "\(rpm) RPM"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - CustomPresetRow
|
|
|
|
private struct CustomPresetRow: View {
|
|
let preset: FanPreset
|
|
let fanSnapshot: FanSnapshot
|
|
let localization: PluginLocalization
|
|
let onRename: (String) -> Void
|
|
let onRPMChange: (Int) -> Void
|
|
let onDelete: () -> Void
|
|
|
|
@State private var nameText: String
|
|
@State private var sliderValue: Double
|
|
@FocusState private var isNameFocused: Bool
|
|
@State private var isNameHovered = false
|
|
|
|
init(
|
|
preset: FanPreset,
|
|
fanSnapshot: FanSnapshot,
|
|
localization: PluginLocalization = PluginLocalization(bundle: .main),
|
|
onRename: @escaping (String) -> Void,
|
|
onRPMChange: @escaping (Int) -> Void,
|
|
onDelete: @escaping () -> Void
|
|
) {
|
|
self.preset = preset
|
|
self.fanSnapshot = fanSnapshot
|
|
self.localization = localization
|
|
self.onRename = onRename
|
|
self.onRPMChange = onRPMChange
|
|
self.onDelete = onDelete
|
|
let rpm = { if case .fixed(let r) = preset.strategy { return r }; return FanRPMLimits.defaultCustomRPM }()
|
|
_nameText = State(initialValue: preset.name)
|
|
_sliderValue = State(initialValue: Double(rpm))
|
|
}
|
|
|
|
private var currentRPM: Int {
|
|
if case .fixed(let r) = preset.strategy { return r }
|
|
return FanRPMLimits.defaultCustomRPM
|
|
}
|
|
|
|
private func resignFocus() {
|
|
isNameFocused = false
|
|
NSApp.keyWindow?.makeFirstResponder(nil)
|
|
}
|
|
|
|
private var sliderMax: Double {
|
|
let max = fanSnapshot.globalMaxSpeed
|
|
return Double(max > 0 ? max : FanRPMLimits.fallbackMax)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.sectionHeaderContent) {
|
|
HStack(spacing: PluginSettingsTheme.Spacing.rowContentControl) {
|
|
TextField(localization.string("settings.custom.namePlaceholder", defaultValue: "预设名称"), text: $nameText)
|
|
.textFieldStyle(.plain)
|
|
.font(PluginSettingsTheme.Typography.emphasizedRowTitle)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 3)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
|
.fill(isNameFocused
|
|
? PluginSettingsTheme.Palette.nativeFieldBackground
|
|
: Color.clear)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
|
.strokeBorder(
|
|
isNameFocused
|
|
? Color(nsColor: .controlAccentColor)
|
|
: isNameHovered
|
|
? PluginSettingsTheme.Palette.nativeSeparator
|
|
: Color.clear,
|
|
lineWidth: 1
|
|
)
|
|
)
|
|
.frame(maxWidth: 100)
|
|
.onHover { isNameHovered = $0 }
|
|
.focused($isNameFocused)
|
|
.onSubmit { onRename(nameText) }
|
|
.onChange(of: nameText) { _, new in
|
|
onRename(new)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button(role: .destructive, action: onDelete) {
|
|
Image(systemName: "trash")
|
|
.foregroundStyle(.red)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help(localization.string("settings.custom.deleteHelp", defaultValue: "删除此预设"))
|
|
}
|
|
|
|
HStack(spacing: PluginSettingsTheme.Spacing.rowContentControl) {
|
|
Slider(
|
|
value: $sliderValue,
|
|
in: Double(FanRPMLimits.absoluteMin)...sliderMax,
|
|
step: 100
|
|
) {
|
|
EmptyView()
|
|
} onEditingChanged: { editing in
|
|
if editing { resignFocus() }
|
|
if !editing { onRPMChange(Int(sliderValue)) }
|
|
}
|
|
|
|
Text("\(Int(sliderValue)) RPM")
|
|
.font(PluginSettingsTheme.Typography.monospacedValue)
|
|
.foregroundStyle(.secondary)
|
|
.frame(width: 80, alignment: .trailing)
|
|
}
|
|
}
|
|
.pluginSettingsListRowPadding(interactive: true)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { resignFocus() }
|
|
// Sync external changes (e.g. from panel slider) back to local state
|
|
.onChange(of: preset.strategy) { _, newStrategy in
|
|
if case .fixed(let r) = newStrategy {
|
|
sliderValue = Double(r)
|
|
}
|
|
}
|
|
.onChange(of: preset.name) { _, newName in
|
|
if nameText != newName { nameText = newName }
|
|
}
|
|
}
|
|
}
|