Files
crossdesk/apps/ios/CrossDeskMobile/Input/RemoteShortcuts.swift
T

157 lines
8.7 KiB
Swift

import Foundation
import Combine
struct RemoteShortcut: Codable, Identifiable, Equatable {
let id: String
let name: String
let platform: String
let group: String
let key: UInt
let modifiers: [UInt]
var service = false
var custom = false
var title: String { custom ? name : NSLocalizedString(name, comment: "") }
var chord: String {
(modifiers.map { Self.modifierName($0, platform: platform) } +
[Self.keyName(key, platform: platform)]).joined(separator: " + ")
}
static let platforms = ["windows", "macos", "linux"]
static let modifierKeys: [UInt] = [17, 16, 18, 91]
static let keys: [UInt] = Array(65...90) + Array(48...57) + Array(112...123) +
[27, 9, 13, 32, 8, 46, 45, 36, 35, 33, 34, 37, 38, 39, 40]
static func modifierName(_ key: UInt, platform: String) -> String {
switch key {
case 17: return "Ctrl"
case 16: return "Shift"
case 18: return platform == "macos" ? "Option" : "Alt"
case 91: return platform == "macos" ? "Cmd" : platform == "linux" ? "Super" : "Win"
default: return ""
}
}
static func keyName(_ key: UInt, platform: String) -> String {
if (65...90).contains(key) || (48...57).contains(key) { return String(UnicodeScalar(UInt32(key))!) }
if (112...123).contains(key) { return "F\(key - 111)" }
switch key {
case 8: return platform == "macos" ? "Delete ⌫" : "Backspace"
case 9: return "Tab"
case 13: return "Enter"
case 27: return "Esc"
case 32: return "Space"
case 33: return "PageUp"
case 34: return "PageDown"
case 35: return "End"
case 36: return "Home"
case 37: return "←"
case 38: return "↑"
case 39: return "→"
case 40: return "↓"
case 45: return "Insert"
case 46: return platform == "macos" ? "Delete ⌦" : "Delete"
default: return ""
}
}
static func platformName(_ platform: String) -> String {
switch platform {
case "windows": return "Windows"
case "macos": return "macOS"
case "linux": return "Linux"
default: return NSLocalizedString("未知", comment: "")
}
}
var valid: Bool {
Self.platforms.contains(platform) && id.hasPrefix("custom-") &&
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && name.count <= 64 &&
Self.keys.contains(key) && modifiers.count <= 4 &&
modifiers.allSatisfy(Self.modifierKeys.contains) &&
!(platform == "windows" && key == 46 && modifiers.contains(17) && modifiers.contains(18))
}
}
final class RemoteShortcutStore: ObservableObject {
@Published private(set) var revision = 0
private let defaults: UserDefaults
private let prefix = "remote_shortcuts_v1."
init(defaults: UserDefaults = .standard) { self.defaults = defaults }
var desktop: String {
get { let value = defaults.string(forKey: prefix + "linux_desktop") ?? ""; return ["gnome", "kde"].contains(value) ? value : "" }
set { defaults.set(["gnome", "kde"].contains(newValue) ? newValue : "", forKey: prefix + "linux_desktop"); revision &+= 1 }
}
var custom: [RemoteShortcut] {
guard let data = defaults.data(forKey: prefix + "custom"),
let decoded = try? JSONDecoder().decode([RemoteShortcut].self, from: data) else { return [] }
var seen = Set<String>()
return Array(decoded.filter { $0.valid && $0.custom && !$0.service && seen.insert($0.id).inserted }.prefix(16))
}
func items(platform: String) -> [RemoteShortcut] {
func item(_ id: String, _ name: String, _ group: String, _ key: UInt, _ modifiers: UInt...) -> RemoteShortcut {
RemoteShortcut(id: id, name: name, platform: platform, group: group, key: key, modifiers: modifiers)
}
var result: [RemoteShortcut] = []
if RemoteShortcut.platforms.contains(platform) {
let primary: UInt = platform == "macos" ? 91 : 17
let names = ["复制", "粘贴", "剪切", "全选", "撤销", "保存"]
let ids = ["copy", "paste", "cut", "all", "undo", "save"]
let keys: [UInt] = [67, 86, 88, 65, 90, 83]
for index in ids.indices { result.append(item(ids[index], names[index], "common", keys[index], primary)) }
if platform == "linux" {
result += [item("term-copy", "终端复制", "common", 67, 17, 16), item("term-paste", "终端粘贴", "common", 86, 17, 16)]
}
}
let keys: [UInt] = [27, 9, 13, 8, 46, 36, 35, 33, 34, 37, 38, 39, 40] + Array(112...123)
for key in keys {
result.append(item(key == 27 ? "esc" : key == 9 ? "tab" : "key-\(key)", RemoteShortcut.keyName(key, platform: platform), "keys", key))
}
if platform == "windows" {
result += [item("switch", "切换窗口", "system", 9, 18), item("desktop", "显示桌面", "system", 68, 91), item("task", "任务管理器", "system", 27, 17, 16)]
result.append(RemoteShortcut(id: "sas", name: "安全选项", platform: platform, group: "system", key: 46, modifiers: [17, 18], service: true))
} else if platform == "macos" {
result += [item("switch", "切换应用", "system", 9, 91), item("search", "聚焦搜索", "system", 32, 91), item("forcequit", "强制退出面板", "system", 27, 18, 91)]
} else if platform == "linux" && !desktop.isEmpty {
result += [item("switch", desktop == "gnome" ? "切换应用" : "切换窗口", "system", 9, desktop == "gnome" ? 91 : 18), item("run", "运行窗口", "system", 113, 18)]
result.append(desktop == "gnome" ? item("apps", "应用列表", "system", 65, 91) : item("window-menu", "窗口菜单", "system", 114, 18))
}
return result + custom.filter { $0.platform == platform }
}
func pins(platform: String) -> [String] {
let values = defaults.stringArray(forKey: prefix + "pins_" + platform) ?? ["esc", "tab", "copy", "paste"]
let available = Set(items(platform: platform).map(\.id))
var seen = Set<String>()
return Array(values.filter { available.contains($0) && seen.insert($0).inserted }.prefix(8))
}
func pinned(platform: String) -> [RemoteShortcut] {
let all = items(platform: platform)
return pins(platform: platform).compactMap { id in all.first { $0.id == id } }
}
func setPins(_ values: [String], platform: String) {
defaults.set(Array(values.prefix(8)), forKey: prefix + "pins_" + platform); revision &+= 1
}
func remove(_ id: String) {
write(custom.filter { $0.id != id })
for platform in RemoteShortcut.platforms { setPins(pins(platform: platform).filter { $0 != id }, platform: platform) }
}
func save(id: String?, name: String, platform: String, key: UInt, modifiers: [UInt], pin: Bool) -> String? {
let name = name.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = RemoteShortcut.modifierKeys.filter(modifiers.contains)
let item = RemoteShortcut(id: id ?? "custom-" + UUID().uuidString, name: name, platform: platform, group: "mine", key: key, modifiers: normalized, custom: true)
if platform == "windows" && key == 46 && normalized.contains(17) && normalized.contains(18) { return NSLocalizedString("请使用系统预设中的安全选项", comment: "") }
guard item.valid else { return NSLocalizedString("请输入有效的快捷键名称", comment: "") }
var values = custom
if values.contains(where: { $0.id != id && $0.platform == platform && $0.key == key && Set($0.modifiers) == Set(normalized) }) { return NSLocalizedString("已有相同组合的快捷键", comment: "") }
if !values.contains(where: { $0.id == id }) && values.count >= 16 { return NSLocalizedString("最多保存 16 个自定义快捷键", comment: "") }
if pin && !pins(platform: platform).contains(item.id) && pins(platform: platform).count >= 8 { return NSLocalizedString("最多固定 8 个快捷键", comment: "") }
values.removeAll { $0.id == id }; values.append(item); write(values)
for target in RemoteShortcut.platforms {
var selected = pins(platform: target); let previous = selected.firstIndex(of: item.id)
selected.removeAll { $0 == item.id }
if target == platform && pin { selected.insert(item.id, at: min(previous ?? selected.count, selected.count)) }
setPins(selected, platform: target)
}
return nil
}
private func write(_ values: [RemoteShortcut]) {
guard let data = try? JSONEncoder().encode(values) else { return }
defaults.set(data, forKey: prefix + "custom"); revision &+= 1
}
}