@hasna/recordings 0.3.13 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -43
- package/bun.lock +37 -38
- package/contracts/v1/fixtures.json +1206 -0
- package/dist/cli/index.js +1909 -619
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/contracts/hosted-v1.d.ts +105 -0
- package/dist/contracts/hosted-v1.d.ts.map +1 -0
- package/dist/contracts/hosted-v1.js +41 -0
- package/dist/contracts/stream-v1.d.ts +117 -0
- package/dist/contracts/stream-v1.d.ts.map +1 -0
- package/dist/contracts/stream-v1.js +35 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +6 -0
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/hosted/index.d.ts +58 -0
- package/dist/hosted/index.d.ts.map +1 -0
- package/dist/hosted/index.js +266 -0
- package/dist/hosted/transport.d.ts +36 -0
- package/dist/hosted/transport.d.ts.map +1 -0
- package/dist/hosted-v1-aavn7ktb.js +4114 -0
- package/dist/hosted-v1-gdr9extc.js +84 -0
- package/dist/http/client.d.ts +183 -22
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2879 -2288
- package/dist/lib/audio-artifact-storage.d.ts +98 -0
- package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +132 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/persistence-probe.d.ts +56 -17
- package/dist/lib/persistence-probe.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1713 -459
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +904 -1
- package/dist/sdk/resolve.d.ts +53 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +6 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud-config.d.ts +12 -0
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +20 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +590 -287
- package/dist/server/migrate-command.d.ts +1 -1
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +21 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1358 -874
- package/dist/store.d.ts +12 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/hosted-sdk.md +71 -0
- package/docs/wire-contracts.md +24 -0
- package/package.json +33 -11
- package/packaging/macos/build_release_pkg.sh +3 -3
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -2
- package/scripts/build_companion_cli.sh +7 -1
- package/scripts/ci-linux-suite.ts +23 -13
- package/scripts/install_macos_app.sh +27 -24
- package/scripts/macos_artifact.ts +60 -49
- package/scripts/migrate.ts +4 -20
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +36 -4
- package/scripts/native-core-receipt.py +171 -0
- package/scripts/native_fs_guard.ts +2 -0
- package/scripts/release-suite-gate.ts +227 -150
- package/scripts/resolve_tailscale_cli.sh +24 -3
- package/scripts/smoke_macos_app.sh +21 -21
- package/src/native/Recordings/App/ContentView.swift +53 -68
- package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
- package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
- package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
- package/src/native/Recordings/App/RecordingsApp.swift +185 -9
- package/src/native/Recordings/App/RecordingsListView.swift +60 -112
- package/src/native/Recordings/App/RecordingsStore.swift +124 -109
- package/src/native/Recordings/App/Theme.swift +73 -61
- package/src/native/Recordings/Package.swift +1 -0
- package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
- package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
- package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
- package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
- package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
- package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
- package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
- package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
- package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
- package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
- package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
- package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
- package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
- package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
- package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
- package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +8 -7
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
- package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
- package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
- package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
- package/dist/__tests__/preload.d.ts +0 -2
- package/dist/__tests__/preload.d.ts.map +0 -1
- package/src/native/Recordings/App/SidebarView.swift +0 -212
|
@@ -1,468 +1,66 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
import RecordingsLib
|
|
3
|
-
@preconcurrency import KeyboardShortcuts
|
|
4
3
|
|
|
5
|
-
/// The Recordings workspace — the app's first screen. One Liquid-Glass hero drives the whole
|
|
6
|
-
/// flow: speak, and the app types it, answers a question, or edits the selection. There is no
|
|
7
|
-
/// mode selector; the phase (idle → listening → finalizing → processing → ready/error) is the
|
|
8
|
-
/// only state the page renders.
|
|
9
4
|
struct RecordWorkspaceView: View {
|
|
10
5
|
@ObservedObject var store: RecordingsStore
|
|
11
|
-
@Environment(\.colorScheme) private var colorScheme
|
|
12
|
-
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
13
|
-
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
|
|
14
|
-
@Namespace private var glass
|
|
15
|
-
|
|
16
6
|
private var engine: RecordingEngine { store.engine }
|
|
17
|
-
private var
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
GlassEffectContainer(spacing: 12) {
|
|
23
|
-
hero
|
|
24
|
-
.frame(maxWidth: 560)
|
|
25
|
-
}
|
|
26
|
-
.padding(.top, 28)
|
|
27
|
-
|
|
28
|
-
if let reply = engine.conversationReply {
|
|
29
|
-
replyCard(reply)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
activeProjectRow
|
|
33
|
-
|
|
34
|
-
if let synchronizationError = store.projectStore.synchronizationError {
|
|
35
|
-
projectSynchronizationWarning(synchronizationError)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if !engine.recentTranscriptions.isEmpty {
|
|
39
|
-
recentStrip
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
Spacer(minLength: 12)
|
|
43
|
-
}
|
|
44
|
-
.frame(maxWidth: .infinity)
|
|
45
|
-
.padding(.horizontal, 28)
|
|
46
|
-
.padding(.bottom, 24)
|
|
47
|
-
}
|
|
48
|
-
.animation(reduceMotion ? nil : .smooth(duration: 0.28), value: phase)
|
|
49
|
-
.onChange(of: engine.isTranscribing) { wasTranscribing, isTranscribing in
|
|
50
|
-
// Refresh promptly when the foreground path finishes. RecordingsStore also
|
|
51
|
-
// observes confirmed persistence so async saves still refresh after this view
|
|
52
|
-
// has been unmounted, without treating failed saves as completed recordings.
|
|
53
|
-
if wasTranscribing && !isTranscribing { store.loadLibrary() }
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// MARK: - Hero (single morphing glass surface)
|
|
58
|
-
|
|
59
|
-
@ViewBuilder
|
|
60
|
-
private var hero: some View {
|
|
61
|
-
// The hidden template is the tallest phase layout (listening, with the full
|
|
62
|
-
// six-line live-text reservation). Every phase renders inside that fixed envelope,
|
|
63
|
-
// so streaming live text and phase transitions can never shift the content below
|
|
64
|
-
// the hero. The template scales with Dynamic Type because it uses the real fonts.
|
|
65
|
-
let content = ZStack {
|
|
66
|
-
heroSizingTemplate
|
|
67
|
-
.hidden()
|
|
68
|
-
.accessibilityHidden(true)
|
|
69
|
-
VStack(spacing: 14) { heroContent }
|
|
70
|
-
}
|
|
71
|
-
.frame(maxWidth: .infinity, minHeight: 208)
|
|
72
|
-
.padding(26)
|
|
73
|
-
switch ChromeSurface.forReducedTransparency(reduceTransparency) {
|
|
74
|
-
case .opaque:
|
|
75
|
-
// Reduce Transparency: a fully opaque system background — never a material,
|
|
76
|
-
// which still composites the desktop through the window.
|
|
77
|
-
content
|
|
78
|
-
.background(
|
|
79
|
-
Color(NSColor.windowBackgroundColor),
|
|
80
|
-
in: .rect(cornerRadius: Theme.cornerLarge)
|
|
81
|
-
)
|
|
82
|
-
.overlay(
|
|
83
|
-
RoundedRectangle(cornerRadius: Theme.cornerLarge, style: .continuous)
|
|
84
|
-
.strokeBorder(.separator, lineWidth: 1)
|
|
85
|
-
)
|
|
86
|
-
case .liquidGlass:
|
|
87
|
-
content
|
|
88
|
-
.glassEffect(heroGlass, in: .rect(cornerRadius: Theme.cornerLarge))
|
|
89
|
-
.glassEffectID("record-hero", in: glass)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private var heroGlass: Glass {
|
|
94
|
-
switch phase {
|
|
95
|
-
case .idle: return .regular.interactive()
|
|
96
|
-
case .listening: return .regular.tint(Theme.recordRed.opacity(0.18))
|
|
97
|
-
case .finalizing, .processing: return .regular.tint(.orange.opacity(0.12))
|
|
98
|
-
case .ready: return .regular.tint(.green.opacity(0.10))
|
|
99
|
-
case .failed: return .regular.tint(.orange.opacity(0.16))
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
@ViewBuilder
|
|
104
|
-
private var heroContent: some View {
|
|
105
|
-
switch phase {
|
|
106
|
-
case .idle:
|
|
107
|
-
idleContent
|
|
108
|
-
case .listening:
|
|
109
|
-
listeningContent
|
|
110
|
-
case .finalizing:
|
|
111
|
-
busyContent(label: "Finishing up…", detail: "Capturing the last words", showsLiveText: true)
|
|
112
|
-
case .processing(let label):
|
|
113
|
-
busyContent(
|
|
114
|
-
label: label,
|
|
115
|
-
detail: nil,
|
|
116
|
-
showsLiveText: false,
|
|
117
|
-
showsCancel: engine.canCancelIntentDelivery
|
|
118
|
-
)
|
|
119
|
-
case .ready(let summary):
|
|
120
|
-
readyContent(summary: summary)
|
|
121
|
-
case .failed(let message):
|
|
122
|
-
failedContent(message: message)
|
|
123
|
-
}
|
|
7
|
+
private var busy: Bool { !engine.canStartRecording && !engine.captureIsActive }
|
|
8
|
+
private var symbol: String {
|
|
9
|
+
if engine.captureIsActive { return engine.isPaused ? "play.fill" : "stop.fill" }
|
|
10
|
+
if busy { return "ellipsis" }
|
|
11
|
+
return store.hasPlayback ? (store.isPlaying ? "pause.fill" : "play.fill") : "mic.fill"
|
|
124
12
|
}
|
|
125
13
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
VStack(spacing: 14) {
|
|
130
|
-
HStack(spacing: 12) {
|
|
131
|
-
Image(systemName: "waveform").font(.largeTitle)
|
|
132
|
-
Text("0:00:00")
|
|
133
|
-
.font(.system(size: 34, weight: .semibold, design: .rounded).monospacedDigit())
|
|
134
|
-
}
|
|
135
|
-
liveTextReservation
|
|
136
|
-
HStack(spacing: 12) {
|
|
137
|
-
Button {} label: { Label("Discard", systemImage: "xmark") }
|
|
138
|
-
.buttonStyle(.glass)
|
|
139
|
-
Button {} label: { Label("Stop & Transcribe", systemImage: "stop.fill") }
|
|
140
|
-
.buttonStyle(.glassProminent)
|
|
141
|
-
}
|
|
142
|
-
.controlSize(.large)
|
|
143
|
-
.disabled(true)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/// Reserves exactly six lines at the live-text font so the region cannot grow as words
|
|
148
|
-
/// stream in.
|
|
149
|
-
private var liveTextReservation: some View {
|
|
150
|
-
Text(String(repeating: "M\n", count: 5) + "M")
|
|
151
|
-
.font(.system(.title3, design: .rounded))
|
|
152
|
-
.lineLimit(6)
|
|
153
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// MARK: Idle
|
|
157
|
-
|
|
158
|
-
private var idleContent: some View {
|
|
159
|
-
let presentation = RecordingStartControlPresentation(
|
|
160
|
-
kind: .record,
|
|
161
|
-
canStartRecording: engine.canStartRecording
|
|
162
|
-
)
|
|
163
|
-
return VStack(spacing: 14) {
|
|
14
|
+
var body: some View {
|
|
15
|
+
VStack(spacing: 0) {
|
|
16
|
+
Spacer().frame(height: 16)
|
|
164
17
|
Button {
|
|
165
|
-
engine.
|
|
18
|
+
if engine.isPaused { engine.togglePause() }
|
|
19
|
+
else if engine.captureIsActive { engine.stopAndTranscribe() }
|
|
20
|
+
else if store.hasPlayback { store.toggleLatestPlayback() }
|
|
21
|
+
else { store.beginRecording() }
|
|
166
22
|
} label: {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Text(idleHint).font(.callout).foregroundStyle(.secondary)
|
|
171
|
-
}
|
|
172
|
-
.frame(maxWidth: .infinity).padding(.vertical, 14).contentShape(.rect)
|
|
23
|
+
GlassCircle(symbol: symbol, size: 88,
|
|
24
|
+
red: engine.captureIsActive && !engine.isPaused,
|
|
25
|
+
progress: store.hasPlayback && !engine.captureIsActive ? store.playbackProgress : nil)
|
|
173
26
|
}
|
|
174
27
|
.buttonStyle(.plain)
|
|
28
|
+
.disabled(busy)
|
|
29
|
+
.accessibilityLabel(engine.captureIsActive ? (engine.isPaused ? "Resume recording" : "Stop and transcribe") : (store.hasPlayback ? "Play recording" : "Start recording"))
|
|
175
30
|
.keyboardShortcut(.defaultAction)
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.
|
|
181
|
-
|
|
182
|
-
.multilineTextAlignment(.center)
|
|
183
|
-
|
|
184
|
-
if engine.statusMessage != "Ready" {
|
|
185
|
-
Text(engine.statusMessage)
|
|
186
|
-
.font(.caption)
|
|
187
|
-
.foregroundStyle(Color.orange)
|
|
188
|
-
.multilineTextAlignment(.center)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
.tint(Theme.accent)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private var idleHint: String {
|
|
195
|
-
if let shortcut = KeyboardShortcuts.getShortcut(for: .toggleRecording) {
|
|
196
|
-
return "Click, or hold \(shortcut.description)"
|
|
197
|
-
}
|
|
198
|
-
return "Click to start"
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// MARK: Listening
|
|
202
|
-
|
|
203
|
-
private var listeningContent: some View {
|
|
204
|
-
VStack(spacing: 14) {
|
|
205
|
-
HStack(spacing: 12) {
|
|
206
|
-
Image(systemName: "waveform")
|
|
207
|
-
.symbolEffect(.variableColor.iterative, isActive: !reduceMotion)
|
|
208
|
-
.foregroundStyle(Theme.recordRed).font(.largeTitle)
|
|
209
|
-
.accessibilityHidden(true)
|
|
210
|
-
Text(fmt(engine.recordingDuration))
|
|
211
|
-
.font(.system(size: 34, weight: .semibold, design: .rounded).monospacedDigit())
|
|
212
|
-
.contentTransition(reduceMotion ? .identity : .numericText())
|
|
213
|
-
.accessibilityLabel("Recording, \(fmt(engine.recordingDuration))")
|
|
214
|
-
}
|
|
215
|
-
liveText(placeholder: "Listening…")
|
|
216
|
-
HStack(spacing: 12) {
|
|
217
|
-
Button(role: .cancel) { engine.cancelRecording() } label: {
|
|
218
|
-
Label("Discard", systemImage: "xmark")
|
|
219
|
-
}
|
|
220
|
-
.buttonStyle(.glass)
|
|
221
|
-
.keyboardShortcut(.cancelAction)
|
|
222
|
-
.accessibilityLabel("Discard recording")
|
|
223
|
-
Button { engine.stopAndTranscribe() } label: {
|
|
224
|
-
Label("Stop & Transcribe", systemImage: "stop.fill")
|
|
225
|
-
}
|
|
226
|
-
.buttonStyle(.glassProminent).tint(Theme.recordRed)
|
|
227
|
-
.keyboardShortcut(.defaultAction)
|
|
228
|
-
.accessibilityLabel("Stop and transcribe")
|
|
229
|
-
}
|
|
230
|
-
.controlSize(.large)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// MARK: Finalizing / Processing
|
|
235
|
-
|
|
236
|
-
private func busyContent(
|
|
237
|
-
label: String,
|
|
238
|
-
detail: String?,
|
|
239
|
-
showsLiveText: Bool,
|
|
240
|
-
showsCancel: Bool = false
|
|
241
|
-
) -> some View {
|
|
242
|
-
VStack(spacing: 14) {
|
|
243
|
-
HStack(spacing: 10) {
|
|
244
|
-
ProgressView().controlSize(.large)
|
|
245
|
-
Text(label).font(.system(.title2, design: .rounded)).foregroundStyle(.secondary)
|
|
246
|
-
}
|
|
247
|
-
.accessibilityElement(children: .combine)
|
|
248
|
-
if let detail {
|
|
249
|
-
Text(detail).font(.caption).foregroundStyle(.tertiary)
|
|
250
|
-
}
|
|
251
|
-
if showsLiveText {
|
|
252
|
-
liveText(placeholder: "Finishing up…")
|
|
253
|
-
}
|
|
254
|
-
if showsCancel {
|
|
255
|
-
Button(role: .cancel) {
|
|
256
|
-
engine.cancelIntentProcessing()
|
|
257
|
-
} label: {
|
|
258
|
-
Label("Cancel", systemImage: "xmark")
|
|
259
|
-
}
|
|
260
|
-
.buttonStyle(.glass)
|
|
261
|
-
.controlSize(.large)
|
|
262
|
-
.keyboardShortcut(.cancelAction)
|
|
263
|
-
.help("Stop waiting — the transcript stays in Recent")
|
|
264
|
-
.accessibilityLabel("Cancel and keep the transcript")
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// MARK: Ready
|
|
270
|
-
|
|
271
|
-
private func readyContent(summary: String) -> some View {
|
|
272
|
-
let presentation = RecordingStartControlPresentation(
|
|
273
|
-
kind: .recordAgain,
|
|
274
|
-
canStartRecording: engine.canStartRecording
|
|
275
|
-
)
|
|
276
|
-
return VStack(spacing: 14) {
|
|
277
|
-
Image(systemName: "checkmark.circle.fill")
|
|
278
|
-
.font(.system(size: 40, weight: .semibold))
|
|
279
|
-
.foregroundStyle(.green)
|
|
280
|
-
.accessibilityHidden(true)
|
|
281
|
-
Text(summary)
|
|
282
|
-
.font(.system(.title3, design: .rounded))
|
|
283
|
-
.multilineTextAlignment(.center)
|
|
284
|
-
Button {
|
|
285
|
-
engine.startRecording()
|
|
286
|
-
} label: {
|
|
287
|
-
Label(presentation.title, systemImage: "mic.fill")
|
|
288
|
-
}
|
|
289
|
-
.buttonStyle(.glass)
|
|
290
|
-
.controlSize(.large)
|
|
291
|
-
.keyboardShortcut(.defaultAction)
|
|
292
|
-
.disabled(!presentation.isEnabled)
|
|
293
|
-
.accessibilityLabel(presentation.accessibilityLabel)
|
|
294
|
-
}
|
|
295
|
-
.accessibilityElement(children: .contain)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// MARK: Error
|
|
299
|
-
|
|
300
|
-
private func failedContent(message: String) -> some View {
|
|
301
|
-
let presentation = RecordingStartControlPresentation(
|
|
302
|
-
kind: .tryAgain,
|
|
303
|
-
canStartRecording: engine.canStartRecording
|
|
304
|
-
)
|
|
305
|
-
return VStack(spacing: 14) {
|
|
306
|
-
Image(systemName: "exclamationmark.triangle.fill")
|
|
307
|
-
.font(.system(size: 36, weight: .semibold))
|
|
308
|
-
.foregroundStyle(.orange)
|
|
309
|
-
.accessibilityHidden(true)
|
|
310
|
-
Text(message)
|
|
311
|
-
.font(.system(.title3, design: .rounded))
|
|
312
|
-
.multilineTextAlignment(.center)
|
|
313
|
-
Button {
|
|
314
|
-
engine.startRecording()
|
|
315
|
-
} label: {
|
|
316
|
-
Label(presentation.title, systemImage: "mic.fill")
|
|
317
|
-
}
|
|
318
|
-
.buttonStyle(.glass)
|
|
319
|
-
.controlSize(.large)
|
|
320
|
-
.keyboardShortcut(.defaultAction)
|
|
321
|
-
.disabled(!presentation.isEnabled)
|
|
322
|
-
.accessibilityLabel(presentation.accessibilityLabel)
|
|
323
|
-
}
|
|
324
|
-
.accessibilityElement(children: .contain)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/// Live text renders inside a fixed six-line reservation: the region's size never
|
|
328
|
-
/// depends on how much has been transcribed, so nothing below it can shift.
|
|
329
|
-
private func liveText(placeholder: String) -> some View {
|
|
330
|
-
ZStack(alignment: .topLeading) {
|
|
331
|
-
liveTextReservation
|
|
332
|
-
.hidden()
|
|
333
|
-
.accessibilityHidden(true)
|
|
334
|
-
if !engine.liveTranscriptionText.isEmpty {
|
|
335
|
-
Text(engine.liveTranscriptionText)
|
|
336
|
-
.font(.system(.title3, design: .rounded))
|
|
337
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
338
|
-
.lineLimit(6).contentTransition(.opacity)
|
|
339
|
-
} else {
|
|
340
|
-
Text(placeholder).font(.callout).foregroundStyle(.secondary)
|
|
341
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// MARK: - Conversation reply
|
|
347
|
-
|
|
348
|
-
/// Content surface, deliberately not glass: answers must stay highly readable.
|
|
349
|
-
private func replyCard(_ reply: ConversationReply) -> some View {
|
|
350
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
351
|
-
HStack {
|
|
352
|
-
Label("You asked", systemImage: "questionmark.bubble")
|
|
353
|
-
.font(.caption.weight(.semibold)).foregroundStyle(.secondary)
|
|
354
|
-
Spacer()
|
|
355
|
-
Button {
|
|
356
|
-
engine.copyToClipboard(reply.answer)
|
|
357
|
-
} label: {
|
|
358
|
-
Label("Copy", systemImage: "doc.on.doc")
|
|
31
|
+
.contextMenu {
|
|
32
|
+
if engine.captureIsActive {
|
|
33
|
+
Button(engine.isPaused ? "Resume recording" : "Pause recording") { engine.togglePause() }.disabled(!engine.isRecording)
|
|
34
|
+
Button("Discard recording") { engine.cancelRecording() }
|
|
35
|
+
} else if store.hasPlayback {
|
|
36
|
+
Button("New recording") { store.clearPlayback(); store.beginRecording() }
|
|
359
37
|
}
|
|
360
|
-
.buttonStyle(.borderless)
|
|
361
|
-
.help("Copy the answer")
|
|
362
|
-
.accessibilityLabel("Copy answer")
|
|
363
38
|
}
|
|
364
|
-
Text(
|
|
365
|
-
.font(.
|
|
366
|
-
.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
.frame(maxWidth: 560)
|
|
376
|
-
.accessibilityElement(children: .contain)
|
|
377
|
-
.accessibilityLabel("Answer: \(reply.answer)")
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
@ViewBuilder
|
|
381
|
-
private var activeProjectRow: some View {
|
|
382
|
-
if !store.projects.isEmpty {
|
|
383
|
-
HStack(spacing: 8) {
|
|
384
|
-
Image(systemName: "folder.fill")
|
|
385
|
-
.foregroundStyle(store.projectStore.activeProject != nil ? AnyShapeStyle(.tint) : AnyShapeStyle(.secondary))
|
|
386
|
-
Menu {
|
|
387
|
-
Button("None") { selectProject(nil) }
|
|
388
|
-
Divider()
|
|
389
|
-
ForEach(store.projects) { project in
|
|
390
|
-
Button {
|
|
391
|
-
selectProject(project.id)
|
|
392
|
-
} label: {
|
|
393
|
-
if project.id == store.projectStore.settings.activeProjectId {
|
|
394
|
-
Label(project.name, systemImage: "checkmark")
|
|
395
|
-
} else { Text(project.name) }
|
|
396
|
-
}
|
|
39
|
+
Text(Theme.clock(store.hasPlayback && !engine.captureIsActive ? store.playbackTime : engine.recordingDuration))
|
|
40
|
+
.font(.system(size: 17, weight: .regular)).monospacedDigit()
|
|
41
|
+
.foregroundStyle(.secondary).shadow(color: .white.opacity(0.85), radius: 0, y: 1)
|
|
42
|
+
.padding(.top, 12)
|
|
43
|
+
if busy {
|
|
44
|
+
HStack(spacing: 6) {
|
|
45
|
+
ProgressView().controlSize(.mini)
|
|
46
|
+
Text(engine.statusMessage).lineLimit(1)
|
|
47
|
+
if engine.canCancelIntentDelivery {
|
|
48
|
+
Button { engine.cancelIntentProcessing() } label: { Image(systemName: "xmark.circle") }
|
|
49
|
+
.buttonStyle(.plain).accessibilityLabel("Cancel processing")
|
|
397
50
|
}
|
|
398
|
-
} label: {
|
|
399
|
-
Text(store.projectStore.activeProject?.name ?? "No project")
|
|
400
51
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
Text(
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
try store.projectStore.setActive(id)
|
|
415
|
-
} catch {
|
|
416
|
-
store.operationError = store.projectStore.persistenceError ?? error.localizedDescription
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private func projectSynchronizationWarning(_ message: String) -> some View {
|
|
421
|
-
HStack(spacing: 8) {
|
|
422
|
-
Image(systemName: "exclamationmark.triangle.fill")
|
|
423
|
-
.foregroundStyle(.orange)
|
|
424
|
-
Text(message)
|
|
425
|
-
.font(.caption)
|
|
426
|
-
.foregroundStyle(.secondary)
|
|
427
|
-
.lineLimit(2)
|
|
428
|
-
Spacer()
|
|
429
|
-
Button("Retry") { store.reconcileProjects() }
|
|
430
|
-
.buttonStyle(.borderless)
|
|
431
|
-
.disabled(store.projectStore.isSynchronizingProjects)
|
|
432
|
-
}
|
|
433
|
-
.frame(maxWidth: 560)
|
|
434
|
-
.accessibilityElement(children: .combine)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// MARK: - Recent strip
|
|
438
|
-
|
|
439
|
-
private var recentStrip: some View {
|
|
440
|
-
let items = Array(engine.recentTranscriptions.prefix(3))
|
|
441
|
-
return VStack(alignment: .leading, spacing: 6) {
|
|
442
|
-
Text("JUST NOW").font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
|
|
443
|
-
ForEach(Array(items.enumerated()), id: \.element.id) { i, item in
|
|
444
|
-
HStack(alignment: .top, spacing: 8) {
|
|
445
|
-
Image(systemName: "text.alignleft").font(.caption).foregroundStyle(.tertiary).padding(.top, 2)
|
|
446
|
-
Text(item.displayText).font(.callout).lineLimit(2)
|
|
447
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
448
|
-
Button {
|
|
449
|
-
engine.pasteIntoFrontApp(item.displayText)
|
|
450
|
-
} label: { Image(systemName: "arrow.up.right.square") }
|
|
451
|
-
.buttonStyle(.plain).foregroundStyle(.secondary).help("Paste into front app")
|
|
452
|
-
.accessibilityLabel("Paste transcript into front app")
|
|
453
|
-
}
|
|
454
|
-
.padding(.vertical, 4)
|
|
455
|
-
if i < items.count - 1 { Divider().opacity(0.3) }
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
.frame(maxWidth: 560)
|
|
459
|
-
.padding(14)
|
|
460
|
-
.background(.quaternary.opacity(0.5), in: .rect(cornerRadius: Theme.cornerMedium))
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
private func fmt(_ t: TimeInterval) -> String {
|
|
464
|
-
let total = Int(t)
|
|
465
|
-
let h = total / 3600, m = (total % 3600) / 60, s = total % 60
|
|
466
|
-
return h > 0 ? String(format: "%d:%02d:%02d", h, m, s) : String(format: "%d:%02d", m, s)
|
|
52
|
+
.font(.system(size: 11)).foregroundStyle(.secondary).padding(.top, 13)
|
|
53
|
+
} else if let blocked = engine.blockedReason {
|
|
54
|
+
Text(blocked).font(.system(size: 10)).foregroundStyle(.orange).lineLimit(2)
|
|
55
|
+
.multilineTextAlignment(.center).padding(.horizontal, 20).padding(.top, 8)
|
|
56
|
+
} else if case .failed(let message) = engine.flowPhase {
|
|
57
|
+
Text(message).font(.system(size: 10)).foregroundStyle(.orange).lineLimit(2)
|
|
58
|
+
.multilineTextAlignment(.center).padding(.horizontal, 20).padding(.top, 8)
|
|
59
|
+
} else if engine.canCancelIntentDelivery {
|
|
60
|
+
Button("Cancel") { engine.cancelIntentProcessing() }.font(.caption)
|
|
61
|
+
}
|
|
62
|
+
Spacer(minLength: 0)
|
|
63
|
+
}
|
|
64
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
467
65
|
}
|
|
468
66
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import RecordingsLib
|
|
4
|
+
|
|
5
|
+
struct RecorderSettingsView: View {
|
|
6
|
+
@ObservedObject var store: RecordingsStore
|
|
7
|
+
let close: () -> Void
|
|
8
|
+
let advanced: () -> Void
|
|
9
|
+
@State private var showsAudioInput = false
|
|
10
|
+
@AppStorage("recordingsNotificationSound") private var notificationSound = false
|
|
11
|
+
|
|
12
|
+
var body: some View {
|
|
13
|
+
VStack(spacing: 0) {
|
|
14
|
+
HStack {
|
|
15
|
+
Button(action: close) { Image(systemName: "chevron.left").font(.system(size: 12)) }
|
|
16
|
+
.buttonStyle(GlassButtonStyle()).accessibilityLabel("Back to recorder")
|
|
17
|
+
Text("Settings").font(.system(size: 13, weight: .semibold))
|
|
18
|
+
Spacer()
|
|
19
|
+
}.frame(height: 18)
|
|
20
|
+
Spacer().frame(height: 18)
|
|
21
|
+
HStack {
|
|
22
|
+
Text("Audio Input")
|
|
23
|
+
Spacer()
|
|
24
|
+
Button { showsAudioInput.toggle() } label: {
|
|
25
|
+
HStack {
|
|
26
|
+
Image(systemName: "mic").font(.system(size: 14))
|
|
27
|
+
Text(AVCaptureDevice.default(for: .audio)?.localizedName ?? "System microphone").lineLimit(1)
|
|
28
|
+
Spacer(); Image(systemName: "chevron.down").font(.system(size: 12))
|
|
29
|
+
}.padding(.horizontal, 10).frame(width: 230, height: 30).background(field)
|
|
30
|
+
}.buttonStyle(.plain)
|
|
31
|
+
.popover(isPresented: $showsAudioInput) {
|
|
32
|
+
VStack(alignment: .leading, spacing: 14) {
|
|
33
|
+
Label("System default microphone", systemImage: "checkmark")
|
|
34
|
+
Button("Sound Settings…") {
|
|
35
|
+
if let url = URL(string: "x-apple.systempreferences:com.apple.Sound-Settings.extension") { NSWorkspace.shared.open(url) }
|
|
36
|
+
}
|
|
37
|
+
}.font(.system(size: 13)).padding(12).background(FrostedBackground())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
Spacer().frame(height: 16)
|
|
42
|
+
HStack {
|
|
43
|
+
Text("Audio Quality")
|
|
44
|
+
Spacer()
|
|
45
|
+
HStack { Text("High (24 kHz PCM)"); Spacer() }
|
|
46
|
+
.padding(.horizontal, 10).frame(width: 230, height: 30).background(field)
|
|
47
|
+
.help("Uncompressed audio at the native transcription sample rate.")
|
|
48
|
+
}
|
|
49
|
+
Spacer().frame(height: 20)
|
|
50
|
+
settingToggle("Auto-paste transcriptions", isOn: Binding(get: { store.engine.autoPasteEnabled }, set: { store.engine.autoPasteEnabled = $0 }))
|
|
51
|
+
Spacer().frame(height: 18)
|
|
52
|
+
settingToggle("Play notification sound", isOn: $notificationSound)
|
|
53
|
+
Spacer().frame(height: 18)
|
|
54
|
+
settingToggle("Show in menu bar", isOn: $store.showMenuBar)
|
|
55
|
+
Spacer().frame(height: 16)
|
|
56
|
+
HStack {
|
|
57
|
+
Text("Recordings save automatically.").foregroundStyle(.secondary)
|
|
58
|
+
Spacer()
|
|
59
|
+
Button("API & Advanced…", action: advanced).buttonStyle(.plain)
|
|
60
|
+
}.font(.system(size: 11))
|
|
61
|
+
Spacer(minLength: 0)
|
|
62
|
+
}
|
|
63
|
+
.font(.system(size: 13)).toggleStyle(.switch).controlSize(.small)
|
|
64
|
+
.padding(.horizontal, 16).padding(.top, 8)
|
|
65
|
+
.frame(width: 420, height: 314)
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private var field: some View {
|
|
70
|
+
GlassInset()
|
|
71
|
+
}
|
|
72
|
+
private func settingToggle(_ label: String, isOn: Binding<Bool>) -> some View {
|
|
73
|
+
HStack { Text(label); Spacer(); Toggle(label, isOn: isOn).labelsHidden() }.frame(height: 26)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -26,7 +26,7 @@ struct RecordingDetailView: View {
|
|
|
26
26
|
VStack(alignment: .leading, spacing: 0) {
|
|
27
27
|
HStack(spacing: 10) {
|
|
28
28
|
Text(rec.createdDate.map { dateLabel($0) } ?? "Recording")
|
|
29
|
-
.font(.system(
|
|
29
|
+
.font(.system(size: 13, weight: .semibold))
|
|
30
30
|
Spacer()
|
|
31
31
|
toolbar(rec)
|
|
32
32
|
}
|
|
@@ -35,7 +35,7 @@ struct RecordingDetailView: View {
|
|
|
35
35
|
|
|
36
36
|
ScrollView {
|
|
37
37
|
Text(rec.displayText.isEmpty ? "No transcript" : rec.displayText)
|
|
38
|
-
.font(.system(
|
|
38
|
+
.font(.system(size: 13))
|
|
39
39
|
.textSelection(.enabled)
|
|
40
40
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
41
41
|
.padding(.horizontal, 18).padding(.top, 14)
|
|
@@ -80,7 +80,7 @@ struct RecordingDetailView: View {
|
|
|
80
80
|
|
|
81
81
|
private func iconButton(_ name: String, help: String, action: @escaping () -> Void) -> some View {
|
|
82
82
|
Button(action: action) { Image(systemName: name).frame(width: 24, height: 24) }
|
|
83
|
-
.buttonStyle(
|
|
83
|
+
.buttonStyle(GlassButtonStyle()).help(help)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
private func metadata(_ rec: Recording) -> some View {
|
|
@@ -88,7 +88,6 @@ struct RecordingDetailView: View {
|
|
|
88
88
|
if rec.durationMs > 0 { metaItem("clock", rec.durationLabel) }
|
|
89
89
|
if let model = rec.modelUsed { metaItem("cpu", model) }
|
|
90
90
|
if let lang = rec.language, !lang.isEmpty { metaItem("globe", lang) }
|
|
91
|
-
if let name = store.projectName(rec.projectId) { metaItem("folder", name) }
|
|
92
91
|
if let machine = rec.machineId, !machine.isEmpty { metaItem("desktopcomputer", machine) }
|
|
93
92
|
if !rec.tags.isEmpty { metaItem("number", rec.tags.joined(separator: ", ")) }
|
|
94
93
|
Spacer()
|