Add Keyboardshortcutlview

This commit is contained in:
Beingpax 2025-11-14 15:33:22 +05:45
parent fb69db2fe9
commit 35a08dce7b
3 changed files with 530 additions and 16 deletions

View File

@ -0,0 +1,467 @@
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)))))
}

View File

@ -3,7 +3,8 @@ import SwiftUI
struct MetricsContent: View {
let transcriptions: [Transcription]
let licenseState: LicenseViewModel.LicenseState
@State private var showKeyboardShortcuts = false
var body: some View {
Group {
if transcriptions.isEmpty {
@ -138,6 +139,7 @@ struct MetricsContent: View {
private var footerActionsView: some View {
HStack(spacing: 12) {
KeyboardShortcutsButton(showKeyboardShortcuts: $showKeyboardShortcuts)
CopySystemInfoButton()
FeedbackButton()
}
@ -280,6 +282,31 @@ private struct FeedbackButton: View {
}
}
private struct KeyboardShortcutsButton: View {
@Binding var showKeyboardShortcuts: Bool
var body: some View {
Button(action: {
showKeyboardShortcuts = true
}) {
HStack(spacing: 8) {
Image(systemName: "command")
.font(.system(size: 13, weight: .medium))
Text("Keyboard Shortcuts")
}
.font(.system(size: 13, weight: .medium))
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Capsule().fill(.thinMaterial))
}
.buttonStyle(.plain)
.popover(isPresented: $showKeyboardShortcuts, arrowEdge: .bottom) {
KeyboardShortcutsListView()
}
}
}
private struct CopySystemInfoButton: View {
@State private var isCopied: Bool = false

View File

@ -105,12 +105,18 @@ private struct ShortcutRow: View {
HStack(spacing: 8) {
Text(title)
.font(.system(size: 14, weight: .semibold))
.foregroundColor(.primary)
InfoTip(title: title, message: description, learnMoreURL: "https://tryvoiceink.com/docs/switching-enhancement-prompts")
}
Text(description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
Spacer(minLength: 0)
Spacer(minLength: 12)
if let isOn = isOn {
keyDisplayView(isActive: isOn.wrappedValue)
.onTapGesture {
@ -123,9 +129,11 @@ private struct ShortcutRow: View {
keyDisplayView()
}
}
.padding(.horizontal, 8)
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background(CardBackground(isSelected: false))
}
@ViewBuilder
private func keyDisplayView(isActive: Bool? = nil) -> some View {
HStack(spacing: 8) {
@ -142,21 +150,33 @@ private struct KeyChip: View {
var body: some View {
let active = isActive ?? true
Text(label)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.padding(.horizontal, 12)
.font(.system(size: 13, weight: .semibold, design: .rounded))
.foregroundColor(active ? .primary : .secondary)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(NSColor.controlBackgroundColor))
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(Color(NSColor.separatorColor).opacity(active ? 0.7 : 0.3), lineWidth: 1)
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color(NSColor.controlBackgroundColor).opacity(active ? 0.9 : 0.6),
Color(NSColor.controlBackgroundColor).opacity(active ? 0.7 : 0.5)
]),
startPoint: .top,
endPoint: .bottom
)
)
)
.foregroundColor(active ? .primary : .secondary)
.shadow(color: Color(NSColor.shadowColor).opacity(active ? 0.1 : 0), radius: 4, x: 0, y: 2)
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.strokeBorder(
Color(NSColor.separatorColor).opacity(active ? 0.4 : 0.2),
lineWidth: 1
)
)
.shadow(color: Color(NSColor.shadowColor).opacity(active ? 0.15 : 0.05), radius: 2, x: 0, y: 1)
.opacity(active ? 1.0 : 0.6)
}
}