Files
ggbond268--mactools/Plugins/AppHotkey/Sources/AppHotkeyManagerView.swift
T
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

205 lines
7.5 KiB
Swift

import AppKit
import SwiftUI
import UniformTypeIdentifiers
import MacToolsPluginKit
// MARK: - Manager View
struct AppHotkeyManagerView: View {
@ObservedObject var store: AppHotkeyStore
let localization: PluginLocalization
let onUpdate: () -> Void
var onBeginRecording: ((UUID) -> Void)? = nil
var onEndRecording: ((UUID) -> Void)? = nil
var body: some View {
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.section) {
bindingSection
}
}
// MARK: Binding Section
private var bindingSection: some View {
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.sectionHeaderContent) {
HStack {
Label(localization.string("settings.section.bindings", defaultValue: "应用绑定"), systemImage: "keyboard")
.font(PluginSettingsTheme.Typography.sectionTitle)
.foregroundStyle(.secondary)
Spacer()
Button(action: addApp) {
Label(localization.string("settings.add", defaultValue: "添加"), systemImage: "plus")
.font(PluginSettingsTheme.Typography.controlLabel)
}
.buttonStyle(.bordered)
.controlSize(.small)
}
if store.entries.isEmpty {
emptyView
} else {
entryList
}
}
}
private var emptyView: some View {
HStack {
Spacer()
VStack(spacing: 8) {
Image(systemName: "keyboard")
.font(.system(size: PluginSettingsTheme.Size.emptyStateIcon))
.foregroundStyle(.secondary)
Text(localization.string("settings.empty", defaultValue: "点击「添加」选择应用并绑定快捷键"))
.font(PluginSettingsTheme.Typography.pageDescription)
.foregroundStyle(.secondary)
}
.padding(.vertical, PluginSettingsTheme.Spacing.pagePadding)
Spacer()
}
.pluginSettingsCardBackground(.host)
}
private var entryList: some View {
VStack(spacing: 0) {
ForEach(store.entries) { entry in
AppShortcutEntryRow(
entry: entry,
localization: localization,
onClearShortcut: {
store.updateShortcut(id: entry.id, shortcut: nil)
onUpdate()
},
onDelete: {
store.deleteEntry(id: entry.id)
onUpdate()
},
onBeginRecording: { onBeginRecording?(entry.id) },
onEndRecording: { onEndRecording?(entry.id) },
onRecord: { binding in
if let conflict = store.conflictEntry(for: binding, excludingID: entry.id) {
return .rejected(localization.format(
"settings.shortcutConflictFormat",
defaultValue: "与「%@」冲突",
conflict.displayName
))
}
store.updateShortcut(id: entry.id, shortcut: binding)
onUpdate()
return .accepted
}
)
if entry.id != store.entries.last?.id {
PluginSettingsListDivider()
}
}
}
.pluginSettingsCardBackground(.host)
}
// MARK: Actions
private func addApp() {
let panel = NSOpenPanel()
panel.title = localization.string("openPanel.title", defaultValue: "选择应用")
panel.message = localization.string("openPanel.message", defaultValue: "选择要绑定快捷键的应用")
panel.allowedContentTypes = [.application]
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
panel.directoryURL = URL(fileURLWithPath: "/Applications")
guard panel.runModal() == .OK, let url = panel.url else { return }
guard Bundle(url: url) != nil else { return }
let displayName = url.deletingPathExtension().lastPathComponent
let entry = AppShortcutEntry(bundleURL: url, displayName: displayName)
store.addEntry(entry)
onUpdate()
}
}
// MARK: - Entry Row
private struct AppShortcutEntryRow: View {
let entry: AppShortcutEntry
let localization: PluginLocalization
let onClearShortcut: () -> Void
let onDelete: () -> Void
let onBeginRecording: () -> Void
let onEndRecording: () -> Void
let onRecord: (ShortcutBinding) -> PluginShortcutRecordingResult
private var appIcon: NSImage {
guard let url = entry.bundleURL else {
return NSWorkspace.shared.icon(forFile: "/Applications")
}
return NSWorkspace.shared.icon(forFile: url.path(percentEncoded: false))
}
private var shortcutText: String {
ShortcutFormatter.displayString(for: entry.shortcut)
.replacingOccurrences(
of: "None",
with: localization.string("settings.shortcutUnset", defaultValue: "未设置")
)
}
private var subtitle: String {
guard let url = entry.bundleURL else {
return localization.string("settings.pathUnavailable", defaultValue: "应用路径不可用")
}
return url.path(percentEncoded: false)
}
var body: some View {
HStack(alignment: .center, spacing: PluginSettingsTheme.Spacing.rowContentControl) {
Image(nsImage: appIcon)
.resizable()
.frame(width: PluginSettingsTheme.Size.rowIcon, height: PluginSettingsTheme.Size.rowIcon)
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.rowTitleDescription) {
Text(entry.displayName)
.font(PluginSettingsTheme.Typography.emphasizedRowTitle)
.lineLimit(1)
Text(subtitle)
.font(PluginSettingsTheme.Typography.rowDescription)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
.frame(maxWidth: .infinity, alignment: .leading)
PluginShortcutRecorder(
title: localization.format(
"settings.shortcutRecorderTitleFormat",
defaultValue: "%@ 快捷键",
entry.displayName
),
displayText: shortcutText,
onRecord: onRecord,
onBeginRecording: onBeginRecording,
onEndRecording: onEndRecording
)
if entry.shortcut != nil {
Button(action: onClearShortcut) {
Image(systemName: "xmark.circle.fill")
.pluginSettingsRowIconStyle(.secondary)
}
.buttonStyle(.plain)
.help(localization.string("settings.clearShortcut", defaultValue: "清除快捷键"))
}
Button(action: onDelete) {
Image(systemName: "trash")
.pluginSettingsRowIconStyle(.red.opacity(0.8))
}
.buttonStyle(.plain)
.help(localization.string("settings.deleteBinding", defaultValue: "删除此绑定"))
}
.pluginSettingsListRowPadding()
}
}