vOOice/VoiceInk/Views/KeyboardShortcutsListView.swift
2025-11-14 15:33:22 +05:45

468 lines
18 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import SwiftData
import KeyboardShortcuts
struct KeyboardShortcutsListView: View {
@EnvironmentObject private var hotkeyManager: HotkeyManager
@ObservedObject private var shortcutSettings = EnhancementShortcutSettings.shared
@State private var customCancelShortcut: KeyboardShortcuts.Shortcut?
@State private var pasteOriginalShortcut: KeyboardShortcuts.Shortcut?
@State private var pasteEnhancedShortcut: KeyboardShortcuts.Shortcut?
@State private var retryShortcut: KeyboardShortcuts.Shortcut?
@State private var toggleHotkey1: KeyboardShortcuts.Shortcut?
@State private var toggleHotkey2: KeyboardShortcuts.Shortcut?
var body: some View {
VStack(spacing: 0) {
// Header
VStack(alignment: .leading, spacing: 6) {
Text("Keyboard Shortcuts")
.font(.system(size: 24, weight: .bold))
.foregroundColor(.primary)
Text("Quick reference for all VoiceInk shortcuts")
.font(.system(size: 13, weight: .medium))
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 28)
.padding(.vertical, 24)
.background(
Rectangle()
.fill(Color(NSColor.controlBackgroundColor).opacity(0.5))
)
Divider()
.overlay(Color(NSColor.separatorColor).opacity(0.5))
// Content
ScrollView {
LazyVGrid(columns: [
GridItem(.flexible(), spacing: 14),
GridItem(.flexible(), spacing: 14)
], spacing: 14) {
// Recording Hotkeys
if hotkeyManager.selectedHotkey1 != .none {
ShortcutCard(
icon: "mic.fill",
iconColor: .blue,
title: "Toggle Recording",
subtitle: "Hotkey 1"
) {
if hotkeyManager.selectedHotkey1 == .custom, let shortcut = toggleHotkey1 {
KeyboardShortcutBadge(shortcut: shortcut)
} else {
HotkeyBadge(text: hotkeyManager.selectedHotkey1.displayName)
}
}
}
if hotkeyManager.selectedHotkey2 != .none {
ShortcutCard(
icon: "mic.fill",
iconColor: .purple,
title: "Toggle Recording",
subtitle: "Hotkey 2"
) {
if hotkeyManager.selectedHotkey2 == .custom, let shortcut = toggleHotkey2 {
KeyboardShortcutBadge(shortcut: shortcut)
} else {
HotkeyBadge(text: hotkeyManager.selectedHotkey2.displayName)
}
}
}
if hotkeyManager.isMiddleClickToggleEnabled {
ShortcutCard(
icon: "cursorarrow.click.2",
iconColor: .green,
title: "Middle-Click",
subtitle: "Hold to record"
) {
MouseBadge()
}
}
// Transcription Actions
ShortcutCard(
icon: "doc.text.fill",
iconColor: .orange,
title: "Paste Last Transcription (Orig.)",
subtitle: "Paste most recent original transcription"
) {
if let shortcut = pasteOriginalShortcut {
KeyboardShortcutBadge(shortcut: shortcut)
} else {
NotSetBadge()
}
}
ShortcutCard(
icon: "wand.and.stars",
iconColor: .pink,
title: "Paste Last Transcription (Enh.)",
subtitle: "Paste enhanced or original if unavailable"
) {
if let shortcut = pasteEnhancedShortcut {
KeyboardShortcutBadge(shortcut: shortcut)
} else {
NotSetBadge()
}
}
ShortcutCard(
icon: "arrow.clockwise",
iconColor: .blue,
title: "Retry Last Transcription",
subtitle: "Redo the last transcription"
) {
if let shortcut = retryShortcut {
KeyboardShortcutBadge(shortcut: shortcut)
} else {
NotSetBadge()
}
}
// Recording Session Shortcuts
ShortcutCard(
icon: "escape",
iconColor: .red,
title: "Dismiss Recorder",
subtitle: customCancelShortcut != nil ? "Custom shortcut or default: Double ESC" : "Default: Double ESC"
) {
if let cancelShortcut = customCancelShortcut {
KeyboardShortcutBadge(shortcut: cancelShortcut)
} else {
StaticKeysBadge(keys: ["", ""])
}
}
ShortcutCard(
icon: "wand.and.stars",
iconColor: .purple,
title: "Toggle Enhancement",
subtitle: shortcutSettings.isToggleEnhancementShortcutEnabled ? "Enable/disable AI enhancement" : "Disabled in settings"
) {
StaticKeysBadge(keys: ["", "E"], isEnabled: shortcutSettings.isToggleEnhancementShortcutEnabled)
}
ShortcutCard(
icon: "wand.and.stars",
iconColor: .orange,
title: "Switch Enhancement Prompt",
subtitle: "Use ⌘1⌘0 (Command)"
) {
StaticKeysBadge(keys: ["", "10"])
}
ShortcutCard(
icon: "sparkles.square.fill.on.square",
iconColor: Color(red: 1.0, green: 0.8, blue: 0.0),
title: "Switch Power Mode",
subtitle: "Use ⌥1⌥0 (Option)"
) {
StaticKeysBadge(keys: ["", "10"])
}
}
.padding(.horizontal, 26)
.padding(.vertical, 26)
}
.background(
LinearGradient(
gradient: Gradient(colors: [
Color(NSColor.windowBackgroundColor),
Color(NSColor.controlBackgroundColor).opacity(0.3)
]),
startPoint: .top,
endPoint: .bottom
)
)
}
.frame(width: 820, height: 600)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.onAppear {
loadShortcuts()
}
.onReceive(Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()) { _ in
// Poll for shortcut changes every 0.5 seconds
loadShortcuts()
}
}
private func loadShortcuts() {
customCancelShortcut = KeyboardShortcuts.getShortcut(for: .cancelRecorder)
pasteOriginalShortcut = KeyboardShortcuts.getShortcut(for: .pasteLastTranscription)
pasteEnhancedShortcut = KeyboardShortcuts.getShortcut(for: .pasteLastEnhancement)
retryShortcut = KeyboardShortcuts.getShortcut(for: .retryLastTranscription)
toggleHotkey1 = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder)
toggleHotkey2 = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder2)
}
}
// MARK: - Shortcut Card
private struct ShortcutCard<Content: View>: View {
let title: String
let subtitle: String
let shortcutView: Content
init(icon: String = "", iconColor: Color = .clear, title: String, subtitle: String, @ViewBuilder shortcutView: () -> Content) {
self.title = title
self.subtitle = subtitle
self.shortcutView = shortcutView()
}
var body: some View {
HStack(alignment: .center, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 14, weight: .semibold))
.foregroundColor(.primary)
.lineLimit(1)
Text(subtitle)
.font(.system(size: 12, weight: .regular))
.foregroundColor(.secondary.opacity(0.8))
.lineLimit(1)
}
Spacer(minLength: 12)
shortcutView
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 18)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color(NSColor.controlBackgroundColor).opacity(0.7),
Color(NSColor.controlBackgroundColor).opacity(0.5)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(
LinearGradient(
gradient: Gradient(colors: [
Color(NSColor.separatorColor).opacity(0.5),
Color(NSColor.separatorColor).opacity(0.3)
]),
startPoint: .top,
endPoint: .bottom
),
lineWidth: 1.5
)
)
.shadow(color: Color(NSColor.shadowColor).opacity(0.2), radius: 4, x: 0, y: 2)
}
}
// MARK: - Badge Components
private struct KeyboardShortcutBadge: View {
let shortcut: KeyboardShortcuts.Shortcut
var body: some View {
HStack(spacing: 4) {
ForEach(shortcutComponents, id: \.self) { component in
KeyBadge(text: component)
}
}
}
private var shortcutComponents: [String] {
var components: [String] = []
if shortcut.modifiers.contains(.command) { components.append("") }
if shortcut.modifiers.contains(.option) { components.append("") }
if shortcut.modifiers.contains(.shift) { components.append("") }
if shortcut.modifiers.contains(.control) { components.append("") }
if let key = shortcut.key {
components.append(keyToString(key))
}
return components
}
private func keyToString(_ key: KeyboardShortcuts.Key) -> String {
switch key {
case .space: return "Space"
case .return: return ""
case .escape: return ""
case .a: return "A"
case .b: return "B"
case .c: return "C"
case .d: return "D"
case .e: return "E"
case .f: return "F"
case .g: return "G"
case .h: return "H"
case .i: return "I"
case .j: return "J"
case .k: return "K"
case .l: return "L"
case .m: return "M"
case .n: return "N"
case .o: return "O"
case .p: return "P"
case .q: return "Q"
case .r: return "R"
case .s: return "S"
case .t: return "T"
case .u: return "U"
case .v: return "V"
case .w: return "W"
case .x: return "X"
case .y: return "Y"
case .z: return "Z"
case .zero: return "0"
case .one: return "1"
case .two: return "2"
case .three: return "3"
case .four: return "4"
case .five: return "5"
case .six: return "6"
case .seven: return "7"
case .eight: return "8"
case .nine: return "9"
default: return String(key.rawValue).uppercased()
}
}
}
private struct StaticKeysBadge: View {
let keys: [String]
var isEnabled: Bool = true
var body: some View {
HStack(spacing: 4) {
ForEach(keys, id: \.self) { key in
KeyBadge(text: key, isEnabled: isEnabled)
}
}
}
}
private struct KeyBadge: View {
let text: String
var isEnabled: Bool = true
var body: some View {
Text(text)
.font(.system(size: 13, weight: .semibold, design: .rounded))
.foregroundColor(isEnabled ? .primary : .secondary)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color(NSColor.controlBackgroundColor).opacity(isEnabled ? 0.9 : 0.6),
Color(NSColor.controlBackgroundColor).opacity(isEnabled ? 0.7 : 0.5)
]),
startPoint: .top,
endPoint: .bottom
)
)
)
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.strokeBorder(
Color(NSColor.separatorColor).opacity(isEnabled ? 0.4 : 0.2),
lineWidth: 1
)
)
.shadow(color: Color(NSColor.shadowColor).opacity(isEnabled ? 0.15 : 0.05), radius: 2, x: 0, y: 1)
.opacity(isEnabled ? 1.0 : 0.6)
}
}
private struct HotkeyBadge: View {
let text: String
var body: some View {
Text(text)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.primary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color(NSColor.controlBackgroundColor).opacity(0.9),
Color(NSColor.controlBackgroundColor).opacity(0.7)
]),
startPoint: .top,
endPoint: .bottom
)
)
)
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.strokeBorder(Color(NSColor.separatorColor).opacity(0.4), lineWidth: 1)
)
.shadow(color: Color(NSColor.shadowColor).opacity(0.15), radius: 2, x: 0, y: 1)
}
}
private struct MouseBadge: View {
var body: some View {
HStack(spacing: 6) {
Image(systemName: "button.vertical.right.press.fill")
.font(.system(size: 11))
.foregroundColor(.primary)
Text("Middle Button")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.primary)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color(NSColor.controlBackgroundColor).opacity(0.9),
Color(NSColor.controlBackgroundColor).opacity(0.7)
]),
startPoint: .top,
endPoint: .bottom
)
)
)
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.strokeBorder(Color(NSColor.separatorColor).opacity(0.4), lineWidth: 1)
)
.shadow(color: Color(NSColor.shadowColor).opacity(0.15), radius: 2, x: 0, y: 1)
}
}
private struct NotSetBadge: View {
var body: some View {
Text("Not Set")
.font(.system(size: 12, weight: .medium))
.foregroundColor(.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(Color(NSColor.controlBackgroundColor).opacity(0.3))
)
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.strokeBorder(Color(NSColor.separatorColor).opacity(0.25), lineWidth: 1)
)
}
}
#Preview {
KeyboardShortcutsListView()
.environmentObject(HotkeyManager(whisperState: WhisperState(modelContext: try! ModelContext(ModelContainer(for: Transcription.self)))))
}