205 lines
7.5 KiB
Swift
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()
|
|
}
|
|
}
|