305 lines
9.8 KiB
Swift
305 lines
9.8 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) {
|
|
Toggle(isOn: $hotkeyManager.isPushToTalkEnabled) {
|
|
Text("Push-to-Talk")
|
|
.font(.subheadline.weight(.medium))
|
|
}
|
|
.toggleStyle(.switch)
|
|
|
|
if hotkeyManager.isPushToTalkEnabled {
|
|
pushToTalkOptions
|
|
}
|
|
}
|
|
}
|
|
|
|
private var pushToTalkOptions: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
PushToTalkKeySelector(selectedKey: $hotkeyManager.pushToTalkKey)
|
|
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "arrow.left.arrow.right.circle.fill")
|
|
.foregroundColor(.secondary)
|
|
.font(.system(size: 12))
|
|
Text("Click to switch")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 .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)
|
|
}
|
|
}
|
|
}
|