@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
|
@@ -4,6 +4,12 @@ import Darwin
|
|
|
4
4
|
import SwiftUI
|
|
5
5
|
import KeyboardShortcuts
|
|
6
6
|
import RecordingsLib
|
|
7
|
+
import Combine
|
|
8
|
+
|
|
9
|
+
private final class RecorderPanel: NSPanel {
|
|
10
|
+
override var canBecomeKey: Bool { true }
|
|
11
|
+
override var canBecomeMain: Bool { true }
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
private final class PermissionRequestResultBox: @unchecked Sendable {
|
|
9
15
|
private let lock = NSLock()
|
|
@@ -63,7 +69,14 @@ final class RecordingsAppState: ObservableObject {
|
|
|
63
69
|
private let runtimeSmokeOutputPath: String?
|
|
64
70
|
private let runtimeSmokeAcknowledgementPath: String?
|
|
65
71
|
private let runtimeSmokeCompletionPath: String?
|
|
72
|
+
@Published var page: RecorderPage = .recorder
|
|
66
73
|
private var mainWindow: NSWindow?
|
|
74
|
+
private var settingsWindowController: SettingsWindowController?
|
|
75
|
+
private var historyWindow: NSWindow?
|
|
76
|
+
private var transcriptionBar: NSPanel?
|
|
77
|
+
private var recentPastesWindow: NSWindow?
|
|
78
|
+
private var subscriptions = Set<AnyCancellable>()
|
|
79
|
+
private var compactSettingsWindow: NSWindow?
|
|
67
80
|
private(set) var windowCreationCount = 0
|
|
68
81
|
private(set) var windowActivationCount = 0
|
|
69
82
|
|
|
@@ -77,8 +90,28 @@ final class RecordingsAppState: ObservableObject {
|
|
|
77
90
|
runtimeSmokeCompletionPath = plan.runtimeSmokeCompletionPath
|
|
78
91
|
runtimeSmokeProbe = plan.runtimeSmokeMode == "normal" ? RuntimeSmokeProbe() : nil
|
|
79
92
|
if plan.installsGlobalHandlers {
|
|
80
|
-
let store
|
|
93
|
+
let store: RecordingsStore
|
|
94
|
+
#if DEBUG
|
|
95
|
+
if CommandLine.arguments.contains("--visual-preview") {
|
|
96
|
+
let home = NSTemporaryDirectory() + "recordings-design-preview"
|
|
97
|
+
store = RecordingsStore(engine: RecordingEngine(homePath: home, installsGlobalHandlers: false),
|
|
98
|
+
preferences: ProjectStore(filePath: home + "/projects.json"), voiceShortcuts: VoiceShortcuts(homePath: home))
|
|
99
|
+
store.isVisualPreview = true
|
|
100
|
+
let labels = ["Interview notes", "Project idea", "Quick thought", "Meeting recap", "Voice memo"]
|
|
101
|
+
let durations = [737000, 343000, 68000, 1691000, 156000]
|
|
102
|
+
store.library = labels.enumerated().map { i, name in
|
|
103
|
+
Recording(id: "visual-preview-\(i)", audioPath: ProcessInfo.processInfo.environment["RECORDINGS_VISUAL_PREVIEW_AUDIO"], rawText: name, durationMs: durations[i],
|
|
104
|
+
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(Double(-i * 86400))))
|
|
105
|
+
}
|
|
106
|
+
if CommandLine.arguments.contains("--visual-preview-recording") {
|
|
107
|
+
store.engine.configureVisualPreview()
|
|
108
|
+
}
|
|
109
|
+
} else { store = RecordingsStore() }
|
|
110
|
+
#else
|
|
111
|
+
store = RecordingsStore()
|
|
112
|
+
#endif
|
|
81
113
|
self.store = store
|
|
114
|
+
store.$showMenuBar.sink { [weak self] _ in self?.objectWillChange.send() }.store(in: &subscriptions)
|
|
82
115
|
if plan.declaresMainWindow {
|
|
83
116
|
Task { @MainActor [weak self] in
|
|
84
117
|
self?.openRecordings()
|
|
@@ -90,6 +123,8 @@ final class RecordingsAppState: ObservableObject {
|
|
|
90
123
|
}
|
|
91
124
|
|
|
92
125
|
func openRecordings() {
|
|
126
|
+
page = .recorder
|
|
127
|
+
resizeDesktop()
|
|
93
128
|
// Bar launches never create the workspace window — on any path, including the
|
|
94
129
|
// reopen handler and the runtime smoke. Keyed on declaresWindow (NOT on
|
|
95
130
|
// declaresMainWindow, which excludes every runtime smoke): a full build's
|
|
@@ -99,12 +134,133 @@ final class RecordingsAppState: ObservableObject {
|
|
|
99
134
|
// deterministically on every build.
|
|
100
135
|
guard declaresWindow else { return }
|
|
101
136
|
if let store {
|
|
102
|
-
showWindow(contentView: NSHostingView(rootView: ContentView(store: store)))
|
|
137
|
+
showWindow(contentView: NSHostingView(rootView: ContentView(store: store, state: self, windowAction: performWindowAction)))
|
|
103
138
|
} else if runtimeSmokeMode == "normal" {
|
|
104
139
|
showWindow(contentView: NSHostingView(rootView: Text("Recordings runtime smoke")))
|
|
105
140
|
}
|
|
106
141
|
}
|
|
107
142
|
|
|
143
|
+
private func showPage(_ destination: RecorderPage) {
|
|
144
|
+
if mainWindow == nil { openRecordings() }
|
|
145
|
+
page = destination
|
|
146
|
+
resizeDesktop()
|
|
147
|
+
activate(mainWindow)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private func resizeDesktop() {
|
|
151
|
+
guard let mainWindow else { return }
|
|
152
|
+
mainWindow.contentMinSize = page.size
|
|
153
|
+
mainWindow.contentMaxSize = page.size
|
|
154
|
+
mainWindow.setContentSize(page.size)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private func performWindowAction(_ kind: NSWindow.ButtonType) {
|
|
158
|
+
switch kind {
|
|
159
|
+
case .closeButton: mainWindow?.performClose(nil)
|
|
160
|
+
case .miniaturizeButton: mainWindow?.miniaturize(nil)
|
|
161
|
+
case .zoomButton: mainWindow?.toggleFullScreen(nil)
|
|
162
|
+
default: break
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func openBar() {
|
|
167
|
+
guard let store else { return }
|
|
168
|
+
if transcriptionBar == nil {
|
|
169
|
+
let screen = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 800)
|
|
170
|
+
let width = min(820, screen.width - 48) + 16
|
|
171
|
+
let panel = RecorderPanel(contentRect: NSRect(x: screen.midX - width / 2, y: screen.maxY - 84, width: width, height: 76),
|
|
172
|
+
styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
|
|
173
|
+
panel.title = "Hasna Recordings — Live Transcript"
|
|
174
|
+
panel.isOpaque = false; panel.backgroundColor = .clear
|
|
175
|
+
panel.level = .floating; panel.hasShadow = true
|
|
176
|
+
panel.isMovableByWindowBackground = true
|
|
177
|
+
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
178
|
+
panel.isReleasedWhenClosed = false
|
|
179
|
+
panel.contentView = NSHostingView(rootView: MenuBarStatusView(store: store, openRecordings: openRecordings,
|
|
180
|
+
openSettings: openSettings, openHistory: openHistory, openRecent: openRecent,
|
|
181
|
+
closeBar: { [weak self] in self?.transcriptionBar?.orderOut(nil) }, barOnly: barOnly))
|
|
182
|
+
transcriptionBar = panel
|
|
183
|
+
}
|
|
184
|
+
transcriptionBar?.makeKeyAndOrderFront(nil)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func openRecent() {
|
|
188
|
+
guard let store else { return }
|
|
189
|
+
let screen = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 800)
|
|
190
|
+
let width = min(760, screen.width - 48)
|
|
191
|
+
if recentPastesWindow == nil {
|
|
192
|
+
recentPastesWindow = makePanel(title: "Recent pastes", width: width, height: 350,
|
|
193
|
+
content: NSHostingView(rootView: VStack(spacing: 0) {
|
|
194
|
+
PanelPointer().fill(Color(red: 0.94, green: 0.95, blue: 0.97))
|
|
195
|
+
.frame(width: 26, height: 12).offset(x: width * 0.34)
|
|
196
|
+
RecentPastesView(store: store, close: { [weak self] in self?.recentPastesWindow?.close() })
|
|
197
|
+
.frame(width: width, height: 338)
|
|
198
|
+
}))
|
|
199
|
+
recentPastesWindow?.level = .floating
|
|
200
|
+
}
|
|
201
|
+
let barBottom = transcriptionBar?.frame.minY ?? (screen.maxY - 84)
|
|
202
|
+
recentPastesWindow?.setFrameOrigin(NSPoint(x: screen.midX - width / 2, y: barBottom - 356))
|
|
203
|
+
activate(recentPastesWindow)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
func openHistory() {
|
|
207
|
+
guard let store else { return }
|
|
208
|
+
store.selection = nil
|
|
209
|
+
store.searchText = ""
|
|
210
|
+
if !barOnly { showPage(.history); return }
|
|
211
|
+
store.searchText = ""
|
|
212
|
+
if historyWindow == nil {
|
|
213
|
+
historyWindow = makePanel(title: "Recordings", width: 500, height: 354,
|
|
214
|
+
content: NSHostingView(rootView: RecordingsListView(store: store, close: { [weak self] in self?.historyWindow?.close() }).background(FrostedBackground())))
|
|
215
|
+
}
|
|
216
|
+
store.loadLibrary()
|
|
217
|
+
activate(historyWindow)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func openSettings() {
|
|
221
|
+
guard let store else { return }
|
|
222
|
+
if !barOnly { showPage(.settings); return }
|
|
223
|
+
if compactSettingsWindow == nil {
|
|
224
|
+
compactSettingsWindow = makePanel(title: "Hasna Recordings Settings", width: 420, height: 314,
|
|
225
|
+
content: NSHostingView(rootView: RecorderSettingsView(store: store,
|
|
226
|
+
close: { [weak self] in self?.compactSettingsWindow?.close() }, advanced: openAdvancedSettings).background(FrostedBackground())))
|
|
227
|
+
}
|
|
228
|
+
activate(compactSettingsWindow)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private func makePanel(title: String, width: CGFloat, height: CGFloat, content: NSView) -> NSWindow {
|
|
232
|
+
let panel = RecorderPanel(contentRect: NSRect(x: 0, y: 0, width: width, height: height),
|
|
233
|
+
styleMask: [.borderless, .resizable], backing: .buffered, defer: false)
|
|
234
|
+
panel.title = title
|
|
235
|
+
panel.isReleasedWhenClosed = false
|
|
236
|
+
panel.isMovableByWindowBackground = true
|
|
237
|
+
panel.isOpaque = false; panel.backgroundColor = .clear; panel.hasShadow = true
|
|
238
|
+
panel.appearance = NSAppearance(named: .aqua)
|
|
239
|
+
panel.contentView = content
|
|
240
|
+
panel.center()
|
|
241
|
+
return panel
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private func activate(_ window: NSWindow?) {
|
|
245
|
+
NSApplication.shared.activate()
|
|
246
|
+
window?.makeKeyAndOrderFront(nil)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func openAdvancedSettings() {
|
|
250
|
+
guard let store, !store.isVisualPreview else { return }
|
|
251
|
+
if !barOnly { showPage(.advanced); return }
|
|
252
|
+
if settingsWindowController == nil {
|
|
253
|
+
settingsWindowController = SettingsWindowController {
|
|
254
|
+
NSHostingView(rootView: SettingsView(
|
|
255
|
+
engine: store.engine,
|
|
256
|
+
shortcuts: store.voiceShortcuts,
|
|
257
|
+
preferences: store.preferences
|
|
258
|
+
))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
settingsWindowController?.show()
|
|
262
|
+
}
|
|
263
|
+
|
|
108
264
|
private func showWindow(contentView: NSView) {
|
|
109
265
|
windowActivationCount += 1
|
|
110
266
|
NSApplication.shared.setActivationPolicy(.regular)
|
|
@@ -116,22 +272,31 @@ final class RecordingsAppState: ObservableObject {
|
|
|
116
272
|
}
|
|
117
273
|
|
|
118
274
|
let window = NSWindow(
|
|
119
|
-
contentRect: NSRect(x: 0, y: 0, width:
|
|
275
|
+
contentRect: NSRect(x: 0, y: 0, width: 224, height: 244),
|
|
120
276
|
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
|
121
277
|
backing: .buffered,
|
|
122
278
|
defer: false
|
|
123
279
|
)
|
|
124
|
-
window.title = "Recordings"
|
|
280
|
+
window.title = "Hasna Recordings"
|
|
125
281
|
window.titleVisibility = .hidden
|
|
126
282
|
window.titlebarAppearsTransparent = true
|
|
283
|
+
window.isOpaque = false
|
|
284
|
+
window.backgroundColor = .clear
|
|
285
|
+
window.isMovableByWindowBackground = true
|
|
286
|
+
window.appearance = NSAppearance(named: .aqua)
|
|
127
287
|
window.isReleasedWhenClosed = false
|
|
128
288
|
window.contentView = contentView
|
|
289
|
+
window.contentMinSize = NSSize(width: 224, height: 244)
|
|
290
|
+
window.contentMaxSize = NSSize(width: 224, height: 244)
|
|
129
291
|
window.center()
|
|
130
292
|
mainWindow = window
|
|
131
293
|
windowCreationCount += 1
|
|
132
294
|
NSApplication.shared.activate()
|
|
133
295
|
NSRunningApplication.current.activate(options: [.activateAllWindows])
|
|
134
296
|
window.makeKeyAndOrderFront(nil)
|
|
297
|
+
for kind in [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton] {
|
|
298
|
+
window.standardWindowButton(kind)?.isHidden = true
|
|
299
|
+
}
|
|
135
300
|
}
|
|
136
301
|
|
|
137
302
|
func startRuntimeSmokeIfNeeded() {
|
|
@@ -351,6 +516,10 @@ struct RecordingsApp: App {
|
|
|
351
516
|
MenuBarStatusView(
|
|
352
517
|
store: store,
|
|
353
518
|
openRecordings: state.openRecordings,
|
|
519
|
+
openSettings: state.openSettings,
|
|
520
|
+
openHistory: state.openHistory,
|
|
521
|
+
openRecent: state.openRecent,
|
|
522
|
+
openBar: state.openBar,
|
|
354
523
|
barOnly: state.barOnly
|
|
355
524
|
)
|
|
356
525
|
} else if state.runtimeSmokeProbe != nil {
|
|
@@ -370,23 +539,30 @@ struct RecordingsApp: App {
|
|
|
370
539
|
|
|
371
540
|
Settings {
|
|
372
541
|
if let store = state.store {
|
|
373
|
-
SettingsView(engine: store.engine, shortcuts: store.voiceShortcuts,
|
|
542
|
+
SettingsView(engine: store.engine, shortcuts: store.voiceShortcuts, preferences: store.preferences)
|
|
374
543
|
} else {
|
|
375
544
|
EmptyView()
|
|
376
545
|
}
|
|
377
546
|
}
|
|
378
547
|
.commands {
|
|
379
548
|
if let store = state.store {
|
|
549
|
+
CommandGroup(replacing: .appSettings) {
|
|
550
|
+
Button("Settings…", action: state.openSettings)
|
|
551
|
+
.keyboardShortcut(",", modifiers: .command)
|
|
552
|
+
}
|
|
380
553
|
CommandGroup(replacing: .newItem) {
|
|
381
554
|
Button("New Recording") {
|
|
382
|
-
|
|
383
|
-
store.
|
|
555
|
+
self.state.openRecordings()
|
|
556
|
+
store.beginRecording()
|
|
384
557
|
}
|
|
385
558
|
.keyboardShortcut("n", modifiers: .command)
|
|
559
|
+
.disabled(!store.engine.canStartRecording)
|
|
386
560
|
}
|
|
387
561
|
CommandGroup(after: .toolbar) {
|
|
388
|
-
Button("Recordings
|
|
562
|
+
Button("Recordings") { state.openHistory() }
|
|
389
563
|
.keyboardShortcut("l", modifiers: .command)
|
|
564
|
+
Button("Show Transcription Bar", action: state.openBar)
|
|
565
|
+
.keyboardShortcut("b", modifiers: [.command, .shift])
|
|
390
566
|
}
|
|
391
567
|
}
|
|
392
568
|
}
|
|
@@ -394,7 +570,7 @@ struct RecordingsApp: App {
|
|
|
394
570
|
|
|
395
571
|
private var menuBarInsertion: Binding<Bool> {
|
|
396
572
|
Binding(
|
|
397
|
-
get: { state.declaresMenuBar && (state.store
|
|
573
|
+
get: { state.declaresMenuBar && (state.store?.showMenuBar == true || state.runtimeSmokeProbe != nil) },
|
|
398
574
|
set: { _ in }
|
|
399
575
|
)
|
|
400
576
|
}
|
|
@@ -1,130 +1,78 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
import RecordingsLib
|
|
3
3
|
|
|
4
|
-
/// The recordings library list: a search field, then a flat list of transcripts on the white
|
|
5
|
-
/// canvas, separated by hairline dividers. Selecting a row shows it in the detail pane.
|
|
6
4
|
struct RecordingsListView: View {
|
|
7
5
|
@ObservedObject var store: RecordingsStore
|
|
6
|
+
var close: () -> Void = {}
|
|
7
|
+
@State private var searching = false
|
|
8
8
|
|
|
9
9
|
var body: some View {
|
|
10
|
-
VStack(spacing:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Image(systemName: "magnifyingglass").foregroundStyle(.secondary)
|
|
20
|
-
TextField("Search transcripts", text: $store.searchText)
|
|
21
|
-
.textFieldStyle(.plain)
|
|
22
|
-
.font(.system(.body, design: .rounded))
|
|
23
|
-
if !store.searchText.isEmpty {
|
|
24
|
-
Button { store.searchText = "" } label: {
|
|
25
|
-
Image(systemName: "xmark.circle.fill").foregroundStyle(.tertiary)
|
|
26
|
-
}
|
|
27
|
-
.buttonStyle(.plain)
|
|
10
|
+
VStack(spacing: 10) {
|
|
11
|
+
HStack {
|
|
12
|
+
Button(action: close) { Image(systemName: "chevron.left") }.help("Back to recorder").accessibilityLabel("Back to recorder")
|
|
13
|
+
Text("Recordings").font(.system(size: 13, weight: .semibold))
|
|
14
|
+
Spacer()
|
|
15
|
+
Button { searching.toggle() } label: { Image(systemName: "magnifyingglass") }
|
|
16
|
+
.help("Search transcripts").accessibilityLabel("Search transcripts")
|
|
17
|
+
Button { store.loadLibrary() } label: { Image(systemName: "arrow.clockwise") }
|
|
18
|
+
.help("Refresh recordings").accessibilityLabel("Refresh recordings")
|
|
28
19
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@ViewBuilder
|
|
34
|
-
private var list: some View {
|
|
35
|
-
if store.isLoadingLibrary && store.library.isEmpty {
|
|
36
|
-
centered { ProgressView() }
|
|
37
|
-
} else if let error = store.loadError, store.library.isEmpty {
|
|
38
|
-
centered {
|
|
39
|
-
VStack(spacing: 8) {
|
|
40
|
-
Image(systemName: "exclamationmark.triangle").font(.largeTitle).foregroundStyle(.orange)
|
|
41
|
-
Text(error).font(.callout).foregroundStyle(.secondary).multilineTextAlignment(.center)
|
|
42
|
-
Button("Retry") { store.loadLibrary() }
|
|
43
|
-
}
|
|
44
|
-
.padding(24)
|
|
20
|
+
.buttonStyle(GlassButtonStyle()).foregroundStyle(Theme.accent)
|
|
21
|
+
.padding(.horizontal, 2)
|
|
22
|
+
if searching && store.selectedRecording == nil {
|
|
23
|
+
TextField("Search transcripts", text: $store.searchText).textFieldStyle(.plain).padding(7).background(GlassInset()).padding(.horizontal, 12)
|
|
45
24
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Text(emptyMessage).foregroundStyle(.secondary).multilineTextAlignment(.center)
|
|
51
|
-
Button("Record") { store.pane = .record }.buttonStyle(.borderless)
|
|
25
|
+
if store.selectedRecording != nil {
|
|
26
|
+
HStack {
|
|
27
|
+
Button { store.selection = nil } label: { Label("All recordings", systemImage: "chevron.left") }.buttonStyle(GlassButtonStyle())
|
|
28
|
+
Spacer()
|
|
52
29
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
30
|
+
RecordingDetailView(store: store)
|
|
31
|
+
} else if store.isLoadingLibrary && store.library.isEmpty {
|
|
32
|
+
Spacer(); ProgressView(); Spacer()
|
|
33
|
+
} else if let error = store.loadError, store.library.isEmpty {
|
|
34
|
+
Spacer()
|
|
35
|
+
Text(error.contains("HASNA_RECORDINGS") ? "Connect your Recordings API in Settings to see your history." : error)
|
|
36
|
+
.font(.callout).foregroundStyle(.secondary).multilineTextAlignment(.center).padding()
|
|
37
|
+
Button("Retry") { store.loadLibrary() }; Spacer()
|
|
38
|
+
} else if store.visibleRecordings.isEmpty {
|
|
39
|
+
Spacer(); Text(store.searchText.isEmpty ? "No recordings yet" : "No matching recordings").foregroundStyle(.secondary); Spacer()
|
|
40
|
+
} else {
|
|
41
|
+
ScrollView {
|
|
42
|
+
LazyVStack(spacing: 2) {
|
|
43
|
+
ForEach(store.visibleRecordings) { rec in
|
|
44
|
+
HStack(spacing: 12) {
|
|
45
|
+
GlassIconButton(symbol: store.playbackRecordingID == rec.id && store.isPlaying ? "pause.fill" : "play.fill", label: "Play recording", size: 28) { store.play(rec) }
|
|
46
|
+
.disabled(!store.canPlay(rec) || store.engine.captureIsActive)
|
|
47
|
+
.help(store.canPlay(rec) ? "Play recording" : "Audio is not stored on this Mac")
|
|
48
|
+
Button { store.selection = rec.id } label: {
|
|
49
|
+
VStack(alignment: .leading, spacing: 5) {
|
|
50
|
+
Text(rec.snippet).font(.system(size: 13)).lineLimit(1)
|
|
51
|
+
Text(rec.createdDate?.recordingDateLabel ?? "Recording").font(.system(size: 11)).foregroundStyle(.secondary)
|
|
52
|
+
}
|
|
53
|
+
.frame(maxWidth: .infinity, alignment: .leading).contentShape(Rectangle())
|
|
54
|
+
}.buttonStyle(.plain)
|
|
55
|
+
Text(Theme.clock(rec.durationSeconds)).font(.system(size: 12)).monospacedDigit().foregroundStyle(.secondary)
|
|
56
|
+
Menu {
|
|
57
|
+
Button("Open transcript") { store.selection = rec.id }
|
|
58
|
+
Button("Copy transcript") { NSPasteboard.general.clearContents(); NSPasteboard.general.setString(rec.displayText, forType: .string) }
|
|
59
|
+
Button("Delete recording", role: .destructive) { store.delete(id: rec.id) }
|
|
60
|
+
} label: { Image(systemName: "ellipsis").font(.system(size: 13)) }
|
|
61
|
+
.menuStyle(.borderlessButton).menuIndicator(.hidden).frame(width: 28)
|
|
71
62
|
}
|
|
72
|
-
|
|
63
|
+
.padding(.horizontal, 10).padding(.vertical, 8)
|
|
64
|
+
.background(GlassInset(radius: 10))
|
|
65
|
+
}
|
|
73
66
|
}
|
|
74
67
|
}
|
|
75
68
|
}
|
|
76
69
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
private var emptyMessage: String {
|
|
80
|
-
if !store.searchText.isEmpty { return "No matches for “\(store.searchText)”" }
|
|
81
|
-
switch store.filter {
|
|
82
|
-
case .project(let id):
|
|
83
|
-
return "No recordings in \(store.projectName(id) ?? "this project") yet"
|
|
84
|
-
case .noProject: return "No unassigned recordings"
|
|
85
|
-
case .mode(let m): return "No \(m) recordings yet"
|
|
86
|
-
case .thisMachine, .machine: return "No recordings from this machine"
|
|
87
|
-
case .all: return "No recordings yet"
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
@ViewBuilder
|
|
92
|
-
private func centered<Content: View>(@ViewBuilder _ content: () -> Content) -> some View {
|
|
93
|
-
VStack { Spacer(); content(); Spacer() }.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private struct RecordingRow: View {
|
|
98
|
-
let rec: Recording
|
|
99
|
-
let projectName: String?
|
|
100
|
-
let selected: Bool
|
|
70
|
+
.padding(12)
|
|
101
71
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if let date = rec.createdDate {
|
|
108
|
-
Text(date.relativeDescription).font(.caption2).foregroundStyle(.secondary)
|
|
109
|
-
}
|
|
110
|
-
if rec.durationMs > 0 {
|
|
111
|
-
Text("·").font(.caption2).foregroundStyle(.secondary)
|
|
112
|
-
Text(rec.durationLabel).font(.caption2).foregroundStyle(.secondary)
|
|
113
|
-
}
|
|
114
|
-
if rec.isEnhanced {
|
|
115
|
-
Text("·").font(.caption2).foregroundStyle(.secondary)
|
|
116
|
-
Label("Enhanced", systemImage: "wand.and.stars")
|
|
117
|
-
.labelStyle(.titleAndIcon).font(.caption2).foregroundStyle(Theme.accent.opacity(0.9))
|
|
118
|
-
}
|
|
119
|
-
if let projectName {
|
|
120
|
-
Text("·").font(.caption2).foregroundStyle(.secondary)
|
|
121
|
-
Label(projectName, systemImage: "folder").labelStyle(.titleAndIcon)
|
|
122
|
-
.font(.caption2).foregroundStyle(.secondary)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
.padding(.horizontal, 16).padding(.vertical, 11)
|
|
127
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
128
|
-
.background { if selected { Theme.accent.opacity(0.12) } }
|
|
72
|
+
.frame(minWidth: 500, minHeight: 354)
|
|
73
|
+
.onAppear { store.loadLibrary() }
|
|
74
|
+
.alert("Recordings Error", isPresented: Binding(get: { store.operationError != nil }, set: { if !$0 { store.operationError = nil } })) {
|
|
75
|
+
Button("OK") { store.operationError = nil }
|
|
76
|
+
} message: { Text(store.operationError ?? "The operation failed.") }
|
|
129
77
|
}
|
|
130
78
|
}
|