vOOice/VoiceInk/Views/RecordView.swift

319 lines
10 KiB
Swift

import SwiftUI
import KeyboardShortcuts
import AppKit
struct RecordView: View {
@EnvironmentObject var whisperState: WhisperState
@EnvironmentObject var hotkeyManager: HotkeyManager
@Environment(\.colorScheme) private var colorScheme
@ObservedObject private var mediaController = MediaController.shared
private var hasShortcutSet: Bool {
KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil
}
var body: some View {
ScrollView(showsIndicators: false) {
mainContent
}
.background(Color(.controlBackgroundColor).opacity(0.5))
}
private var mainContent: some View {
VStack(spacing: 48) {
heroSection
controlsSection
}
.padding(32)
}
private var heroSection: some View {
VStack(spacing: 20) {
appIconView
titleSection
}
}
private var appIconView: some View {
ZStack {
Circle()
.fill(Color.accentColor.opacity(0.15))
.frame(width: 160, height: 160)
.blur(radius: 30)
if let image = NSImage(named: "AppIcon") {
Image(nsImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 120, height: 120)
.cornerRadius(30)
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(.white.opacity(0.2), lineWidth: 1)
)
.shadow(color: .accentColor.opacity(0.3), radius: 20)
}
}
}
private var titleSection: some View {
VStack(spacing: 8) {
Text("VOICEINK")
.font(.system(size: 42, weight: .bold))
if whisperState.currentModel != nil {
Text("Powered by Whisper AI")
.font(.system(size: 15))
.foregroundColor(.secondary)
}
}
}
private var controlsSection: some View {
VStack(spacing: 32) {
compactControlsCard
instructionsCard
}
}
private var compactControlsCard: some View {
HStack(spacing: 32) {
shortcutSection
if hasShortcutSet {
Divider()
.frame(height: 40)
pushToTalkSection
Divider()
.frame(height: 40)
// Settings section
VStack(alignment: .leading, spacing: 12) {
Toggle(isOn: $whisperState.isAutoCopyEnabled) {
HStack {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.secondary)
Text("Auto-copy to clipboard")
.font(.subheadline.weight(.medium))
}
}
.toggleStyle(.switch)
Toggle(isOn: .init(
get: { SoundManager.shared.isEnabled },
set: { SoundManager.shared.isEnabled = $0 }
)) {
HStack {
Image(systemName: "speaker.wave.2")
.foregroundColor(.secondary)
Text("Sound feedback")
.font(.subheadline.weight(.medium))
}
}
.toggleStyle(.switch)
Toggle(isOn: $mediaController.isMediaPauseEnabled) {
HStack {
Image(systemName: "play.slash")
.foregroundColor(.secondary)
Text("Pause media during recording")
.font(.subheadline.weight(.medium))
}
}
.toggleStyle(.switch)
.help("Automatically pause music playback when recording starts and resume when recording stops")
}
}
}
.padding(24)
}
private var shortcutSection: some View {
VStack(spacing: 12) {
if hasShortcutSet {
if let shortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) {
KeyboardShortcutView(shortcut: shortcut)
.scaleEffect(1.2)
}
} else {
Image(systemName: "keyboard.badge.exclamationmark")
.font(.system(size: 28))
.foregroundColor(.orange)
}
Button(action: {
NotificationCenter.default.post(
name: .navigateToDestination,
object: nil,
userInfo: ["destination": "Settings"]
)
}) {
Text(hasShortcutSet ? "Change" : "Set Shortcut")
.font(.subheadline.weight(.medium))
.foregroundColor(.accentColor)
}
.buttonStyle(.plain)
}
}
private var pushToTalkSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Push-to-Talk")
.font(.subheadline.weight(.medium))
if hotkeyManager.isPushToTalkEnabled {
SelectableKeyCapView(
text: getKeySymbol(for: hotkeyManager.pushToTalkKey),
subtext: getKeyText(for: hotkeyManager.pushToTalkKey),
isSelected: true
)
}
}
}
}
private func getKeySymbol(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return ""
case .leftOption: return ""
case .leftControl: return ""
case .fn: return "Fn"
case .rightCommand: return ""
case .rightShift: return ""
}
}
private func getKeyText(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return "Right Option"
case .leftOption: return "Left Option"
case .leftControl: return "Left Control"
case .fn: return "Function"
case .rightCommand: return "Right Command"
case .rightShift: return "Right Shift"
}
}
private var instructionsCard: some View {
VStack(alignment: .leading, spacing: 28) {
Text("How it works")
.font(.title3.weight(.bold))
VStack(alignment: .leading, spacing: 24) {
ForEach(getInstructions(), id: \.title) { instruction in
InstructionRow(instruction: instruction)
}
Divider()
.padding(.vertical, 4)
afterRecordingSection
}
}
.padding(28)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.windowBackgroundColor).opacity(0.4))
)
}
private var afterRecordingSection: some View {
VStack(alignment: .leading, spacing: 16) {
Text("After recording")
.font(.headline)
VStack(alignment: .leading, spacing: 12) {
InfoRow(icon: "doc.on.clipboard", text: "Copied to clipboard")
InfoRow(icon: "text.cursor", text: "Pasted at cursor position")
}
}
}
private func getInstructions() -> [(icon: String, title: String, description: String)] {
let keyName: String
switch hotkeyManager.pushToTalkKey {
case .rightOption:
keyName = "right Option (⌥)"
case .leftOption:
keyName = "left Option (⌥)"
case .leftControl:
keyName = "left Control (⌃)"
case .fn:
keyName = "Fn"
case .rightCommand:
keyName = "right Command (⌘)"
case .rightShift:
keyName = "right Shift (⇧)"
}
let activateDescription = hotkeyManager.isPushToTalkEnabled ?
"Hold the \(keyName) key" :
"Press your configured shortcut"
let finishDescription = hotkeyManager.isPushToTalkEnabled ?
"Release the \(keyName) key to stop and process" :
"Press the shortcut again to stop"
return [
(
icon: "mic.circle.fill",
title: "Start Recording",
description: activateDescription
),
(
icon: "waveform",
title: "Speak Clearly",
description: "Talk into your microphone naturally"
),
(
icon: "stop.circle.fill",
title: "Finish Up",
description: finishDescription
)
]
}
}
// Simplified InstructionRow
struct InstructionRow: View {
let instruction: (icon: String, title: String, description: String)
var body: some View {
HStack(alignment: .top, spacing: 16) {
Image(systemName: instruction.icon)
.font(.system(size: 20))
.foregroundColor(.accentColor)
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
Text(instruction.title)
.font(.subheadline.weight(.medium))
Text(instruction.description)
.font(.subheadline)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
// Simplified InfoRow
struct InfoRow: View {
let icon: String
let text: String
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.system(size: 14))
.foregroundColor(.secondary)
Text(text)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}