@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,136 +1,152 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import SwiftUI
|
|
3
3
|
import Combine
|
|
4
|
+
import AVFoundation
|
|
4
5
|
import RecordingsLib
|
|
5
6
|
|
|
6
|
-
/// Which main pane the canvas shows.
|
|
7
|
-
enum Pane: Equatable {
|
|
8
|
-
case record // the recording workspace (default / first screen)
|
|
9
|
-
case library // browse past recordings
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/// Sidebar filter selection for the library.
|
|
13
|
-
enum LibraryFilter: Hashable {
|
|
14
|
-
case all
|
|
15
|
-
case project(String) // project id
|
|
16
|
-
case noProject
|
|
17
|
-
case mode(String) // "raw" | "enhanced"
|
|
18
|
-
case thisMachine
|
|
19
|
-
case machine(String)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
7
|
/// Observable application state for the full macOS app. Bridges the live `RecordingEngine`,
|
|
23
|
-
///
|
|
8
|
+
/// global cleanup preferences, voice shortcuts, and the `recordings` CLI Store.
|
|
24
9
|
@MainActor
|
|
25
10
|
final class RecordingsStore: ObservableObject {
|
|
26
11
|
let engine: RecordingEngine
|
|
27
|
-
let
|
|
12
|
+
let preferences: ProjectStore
|
|
28
13
|
let voiceShortcuts: VoiceShortcuts
|
|
29
14
|
|
|
30
|
-
@Published var
|
|
31
|
-
@Published var library: [Recording] = []
|
|
15
|
+
@Published var library: [Recording] = [] { didSet { updateVisibleRecordings() } }
|
|
32
16
|
@Published var selection: String?
|
|
33
|
-
@Published var
|
|
34
|
-
@Published var
|
|
17
|
+
@Published var searchText: String = "" { didSet { updateVisibleRecordings() } }
|
|
18
|
+
@Published private(set) var visibleRecordings: [Recording] = []
|
|
19
|
+
@Published var showMenuBar = UserDefaults.standard.object(forKey: "recordingsShowMenuBar") as? Bool ?? true {
|
|
20
|
+
didSet { UserDefaults.standard.set(showMenuBar, forKey: "recordingsShowMenuBar") }
|
|
21
|
+
}
|
|
35
22
|
@Published var isLoadingLibrary = false
|
|
36
23
|
@Published var loadError: String?
|
|
37
24
|
@Published var operationError: String?
|
|
38
25
|
|
|
39
|
-
@Published var
|
|
40
|
-
|
|
26
|
+
@Published private(set) var isPlaying = false
|
|
27
|
+
@Published private(set) var playbackTime: Double = 0
|
|
28
|
+
@Published private(set) var playbackProgress: Double = 0
|
|
29
|
+
@Published private(set) var playbackRecordingID: String?
|
|
30
|
+
@Published private(set) var hasPlayback = false
|
|
31
|
+
private var player: AVAudioPlayer?
|
|
32
|
+
private var playbackTimer: Timer?
|
|
33
|
+
|
|
34
|
+
func beginRecording() {
|
|
35
|
+
guard !isVisualPreview else { return }
|
|
36
|
+
clearPlayback()
|
|
37
|
+
engine.startRecording()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func prepareLatestPlayback() {
|
|
41
|
+
guard !engine.captureIsActive, let path = engine.latestAudioPath,
|
|
42
|
+
FileManager.default.fileExists(atPath: path) else { return }
|
|
43
|
+
preparePlayback(path: path, id: nil, play: false)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func canPlay(_ recording: Recording) -> Bool {
|
|
47
|
+
recording.audioPath.map { FileManager.default.fileExists(atPath: $0) } ?? false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func play(_ recording: Recording) {
|
|
51
|
+
guard !engine.captureIsActive else { return }
|
|
52
|
+
if playbackRecordingID == recording.id { toggleLatestPlayback(); return }
|
|
53
|
+
guard let path = recording.audioPath else { return }
|
|
54
|
+
preparePlayback(path: path, id: recording.id, play: true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func preparePlayback(path: String, id: String?, play: Bool) {
|
|
58
|
+
clearPlayback()
|
|
59
|
+
do {
|
|
60
|
+
player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
|
61
|
+
player?.prepareToPlay()
|
|
62
|
+
hasPlayback = true
|
|
63
|
+
playbackRecordingID = id
|
|
64
|
+
playbackTime = player?.duration ?? 0
|
|
65
|
+
if play { toggleLatestPlayback() }
|
|
66
|
+
} catch { operationError = "This recording’s audio could not be opened." }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func toggleLatestPlayback() {
|
|
70
|
+
guard let player else { return }
|
|
71
|
+
if isPlaying {
|
|
72
|
+
player.pause(); isPlaying = false; playbackTimer?.invalidate(); return
|
|
73
|
+
}
|
|
74
|
+
guard player.play() else { operationError = "Audio playback could not start."; return }
|
|
75
|
+
isPlaying = true
|
|
76
|
+
playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
|
|
77
|
+
Task { @MainActor [weak self] in
|
|
78
|
+
guard let self, let player = self.player else { return }
|
|
79
|
+
self.playbackTime = player.currentTime
|
|
80
|
+
self.playbackProgress = player.duration > 0 ? player.currentTime / player.duration : 0
|
|
81
|
+
if !player.isPlaying { self.clearPlayback() }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func clearPlayback() {
|
|
87
|
+
playbackTimer?.invalidate(); playbackTimer = nil
|
|
88
|
+
player?.stop(); player = nil
|
|
89
|
+
isPlaying = false; hasPlayback = false
|
|
90
|
+
playbackRecordingID = nil; playbackTime = 0; playbackProgress = 0
|
|
91
|
+
if !engine.captureIsActive { engine.recordingDuration = 0 }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
var isVisualPreview = false
|
|
41
95
|
private let home: String
|
|
42
96
|
private var cancellables = Set<AnyCancellable>()
|
|
97
|
+
private var connectionRevision = 0
|
|
43
98
|
|
|
44
99
|
init(engine: RecordingEngine = RecordingEngine(),
|
|
45
|
-
|
|
100
|
+
preferences: ProjectStore = ProjectStore(),
|
|
46
101
|
voiceShortcuts: VoiceShortcuts = VoiceShortcuts()) {
|
|
47
102
|
self.engine = engine
|
|
48
|
-
self.
|
|
103
|
+
self.preferences = preferences
|
|
49
104
|
self.voiceShortcuts = voiceShortcuts
|
|
50
105
|
self.home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
51
|
-
|
|
52
|
-
engine.
|
|
106
|
+
engine.projectStore = nil
|
|
107
|
+
engine.globalRecordingPreferences = preferences
|
|
53
108
|
engine.voiceShortcuts = voiceShortcuts
|
|
54
109
|
|
|
55
110
|
// Re-publish the wrapped ObservableObjects so views that observe only this store
|
|
56
|
-
// refresh on live recording changes (timer, live text) and
|
|
111
|
+
// refresh on live recording changes (timer, live text) and preference edits.
|
|
57
112
|
engine.objectWillChange
|
|
58
113
|
.sink { [weak self] in self?.objectWillChange.send() }
|
|
59
114
|
.store(in: &cancellables)
|
|
60
115
|
engine.$persistedRecordingRevision
|
|
61
116
|
.dropFirst()
|
|
62
|
-
.sink { [weak self] _ in self?.loadLibrary()
|
|
117
|
+
.sink { [weak self] _ in self?.loadLibrary(); self?.prepareLatestPlayback()
|
|
118
|
+
if UserDefaults.standard.bool(forKey: "recordingsNotificationSound") { NSSound(named: "Pop")?.play() } }
|
|
119
|
+
.store(in: &cancellables)
|
|
120
|
+
engine.$isRecording.dropFirst().filter { $0 }
|
|
121
|
+
.sink { [weak self] _ in self?.clearPlayback() }
|
|
63
122
|
.store(in: &cancellables)
|
|
64
|
-
|
|
123
|
+
preferences.objectWillChange
|
|
65
124
|
.sink { [weak self] in self?.objectWillChange.send() }
|
|
66
125
|
.store(in: &cancellables)
|
|
67
126
|
voiceShortcuts.objectWillChange
|
|
68
127
|
.sink { [weak self] in self?.objectWillChange.send() }
|
|
69
128
|
.store(in: &cancellables)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
try await projectStore.reconcileWithCanonicalStore(home: home)
|
|
80
|
-
self.operationError = nil
|
|
81
|
-
} catch {
|
|
82
|
-
self.operationError = projectStore.persistenceError
|
|
83
|
-
?? (error as? RecordingsCLI.Failure)?.message
|
|
84
|
-
?? error.localizedDescription
|
|
129
|
+
NotificationCenter.default.publisher(for: ServiceAPIConfiguration.didChangeNotification)
|
|
130
|
+
.receive(on: RunLoop.main)
|
|
131
|
+
.sink { [weak self] _ in
|
|
132
|
+
guard let self else { return }
|
|
133
|
+
self.connectionRevision += 1
|
|
134
|
+
self.library = []
|
|
135
|
+
self.selection = nil
|
|
136
|
+
self.loadError = nil
|
|
137
|
+
self.loadLibrary()
|
|
85
138
|
}
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// MARK: - Derived collections
|
|
90
|
-
|
|
91
|
-
var projects: [RecProject] { projectStore.settings.projects }
|
|
92
|
-
|
|
93
|
-
var machines: [String] {
|
|
94
|
-
Set(library.compactMap { $0.machineId }.filter { !$0.isEmpty })
|
|
95
|
-
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
var lastUpdated: Date? { library.compactMap(\.createdDate).max() }
|
|
99
|
-
|
|
100
|
-
var visibleRecordings: [Recording] {
|
|
101
|
-
library.filter { matchesFilter($0) && matchesSearch($0) }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private func matchesFilter(_ r: Recording) -> Bool {
|
|
105
|
-
switch filter {
|
|
106
|
-
case .all: return true
|
|
107
|
-
case .project(let id): return r.projectId == id
|
|
108
|
-
case .noProject: return r.projectId == nil
|
|
109
|
-
case .mode(let m): return r.processingMode == m
|
|
110
|
-
case .thisMachine: return r.machineId == localMachineID
|
|
111
|
-
case .machine(let m): return r.machineId == m
|
|
112
|
-
}
|
|
139
|
+
.store(in: &cancellables)
|
|
113
140
|
}
|
|
114
141
|
|
|
115
|
-
|
|
142
|
+
// Recording ticks and transcript deltas must not rescan the entire history.
|
|
143
|
+
private func updateVisibleRecordings() {
|
|
116
144
|
let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
117
|
-
guard !q.isEmpty else { return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
func count(for filter: LibraryFilter) -> Int {
|
|
124
|
-
library.filter {
|
|
125
|
-
switch filter {
|
|
126
|
-
case .all: return true
|
|
127
|
-
case .project(let id): return $0.projectId == id
|
|
128
|
-
case .noProject: return $0.projectId == nil
|
|
129
|
-
case .mode(let m): return $0.processingMode == m
|
|
130
|
-
case .thisMachine: return $0.machineId == localMachineID
|
|
131
|
-
case .machine(let m): return $0.machineId == m
|
|
132
|
-
}
|
|
133
|
-
}.count
|
|
145
|
+
guard !q.isEmpty else { visibleRecordings = library; return }
|
|
146
|
+
visibleRecordings = library.filter { recording in
|
|
147
|
+
recording.displayText.localizedCaseInsensitiveContains(q) ||
|
|
148
|
+
recording.tags.contains { $0.localizedCaseInsensitiveContains(q) }
|
|
149
|
+
}
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
var selectedRecording: Recording? {
|
|
@@ -138,40 +154,38 @@ final class RecordingsStore: ObservableObject {
|
|
|
138
154
|
return library.first { $0.id == id }
|
|
139
155
|
}
|
|
140
156
|
|
|
141
|
-
func projectName(_ id: String?) -> String? {
|
|
142
|
-
guard let id else { return nil }
|
|
143
|
-
return projects.first { $0.id == id }?.name
|
|
144
|
-
}
|
|
145
|
-
|
|
146
157
|
// MARK: - Library loading
|
|
147
158
|
|
|
148
159
|
private var reloadRequestedDuringLoad = false
|
|
149
160
|
|
|
150
161
|
func loadLibrary() {
|
|
162
|
+
guard !isVisualPreview else { return }
|
|
151
163
|
guard !isLoadingLibrary else { reloadRequestedDuringLoad = true; return }
|
|
152
164
|
isLoadingLibrary = true
|
|
153
165
|
let home = home
|
|
166
|
+
let revision = connectionRevision
|
|
154
167
|
Task.detached(priority: .userInitiated) {
|
|
155
|
-
let result: Result<
|
|
168
|
+
let result: Result<[Recording], Error>
|
|
156
169
|
do {
|
|
157
170
|
let recs = try RecordingsCLI.listAll(home: home)
|
|
158
|
-
|
|
159
|
-
result = .success((recs, stats))
|
|
171
|
+
result = .success(recs)
|
|
160
172
|
} catch {
|
|
161
173
|
result = .failure(error)
|
|
162
174
|
}
|
|
163
175
|
await MainActor.run {
|
|
164
176
|
self.isLoadingLibrary = false
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
// A response from the previous endpoint must not repopulate this library.
|
|
178
|
+
if revision == self.connectionRevision {
|
|
179
|
+
switch result {
|
|
180
|
+
case .success(let recs):
|
|
181
|
+
self.library = recs
|
|
182
|
+
self.loadError = nil
|
|
183
|
+
if let selection = self.selection, !recs.contains(where: { $0.id == selection }) {
|
|
184
|
+
self.selection = nil
|
|
185
|
+
}
|
|
186
|
+
case .failure(let error):
|
|
187
|
+
self.loadError = (error as? RecordingsCLI.Failure)?.message ?? error.localizedDescription
|
|
172
188
|
}
|
|
173
|
-
case .failure(let error):
|
|
174
|
-
self.loadError = (error as? RecordingsCLI.Failure)?.message ?? error.localizedDescription
|
|
175
189
|
}
|
|
176
190
|
if self.reloadRequestedDuringLoad {
|
|
177
191
|
self.reloadRequestedDuringLoad = false
|
|
@@ -182,6 +196,7 @@ final class RecordingsStore: ObservableObject {
|
|
|
182
196
|
}
|
|
183
197
|
|
|
184
198
|
func delete(id: String) {
|
|
199
|
+
if isVisualPreview { library.removeAll { $0.id == id }; selection = nil; return }
|
|
185
200
|
let home = home
|
|
186
201
|
Task.detached(priority: .userInitiated) {
|
|
187
202
|
let result: Result<Void, Error>
|
|
@@ -195,7 +210,7 @@ final class RecordingsStore: ObservableObject {
|
|
|
195
210
|
switch result {
|
|
196
211
|
case .success:
|
|
197
212
|
self.library.removeAll { $0.id == id }
|
|
198
|
-
if self.selection == id { self.selection =
|
|
213
|
+
if self.selection == id { self.selection = nil }
|
|
199
214
|
case .failure(let error):
|
|
200
215
|
self.operationError = (error as? RecordingsCLI.Failure)?.message ?? error.localizedDescription
|
|
201
216
|
}
|
|
@@ -1,87 +1,99 @@
|
|
|
1
|
-
import RecordingsLib
|
|
2
1
|
import SwiftUI
|
|
2
|
+
import AppKit
|
|
3
|
+
import RecordingsLib
|
|
3
4
|
|
|
4
|
-
/// Design tokens and Liquid Glass helpers for the Recordings app. Mirrors the Hasna Notes
|
|
5
|
-
/// visual system (narrow colored Liquid-Glass sidebar + one continuous canvas, hairline
|
|
6
|
-
/// dividers, rounded type) with a Recordings identity: an "infinity violet→indigo" sidebar.
|
|
7
5
|
enum Theme {
|
|
8
|
-
|
|
9
|
-
static let
|
|
10
|
-
static let recordRed = Color(red: 0.92, green: 0.26, blue: 0.30)
|
|
11
|
-
|
|
6
|
+
static let accent = Color.primary
|
|
7
|
+
static let recordRed = Color(red: 1, green: 0.24, blue: 0.25)
|
|
12
8
|
static let cornerLarge: CGFloat = 22
|
|
13
9
|
static let cornerMedium: CGFloat = 14
|
|
14
10
|
static let cornerSmall: CGFloat = 9
|
|
11
|
+
static func canvas(_ scheme: ColorScheme) -> Color { Color(NSColor.windowBackgroundColor) }
|
|
12
|
+
static func clock(_ seconds: Double) -> String {
|
|
13
|
+
let total = max(0, Int(seconds))
|
|
14
|
+
return String(format: "%02d:%02d", total / 60, total % 60)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
/// A single native backdrop per window; controls share lightweight glass highlights.
|
|
19
|
+
struct FrostedBackground: View {
|
|
20
|
+
var radius: CGFloat = 16
|
|
21
|
+
var cool = false
|
|
22
|
+
var body: some View { RecordingGlassBackground(radius: radius) }
|
|
23
|
+
}
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
struct GlassInset: View {
|
|
26
|
+
var radius: CGFloat = 8
|
|
27
|
+
var body: some View {
|
|
28
|
+
RoundedRectangle(cornerRadius: radius)
|
|
29
|
+
.fill(LinearGradient(colors: [.white.opacity(0.26), .white.opacity(0.06)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
|
30
|
+
.overlay(RoundedRectangle(cornerRadius: radius).strokeBorder(.white.opacity(0.45), lineWidth: 0.5))
|
|
31
|
+
.shadow(color: .black.opacity(0.04), radius: 2, y: 1)
|
|
22
32
|
}
|
|
33
|
+
}
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
? [Color(red: 0.16, green: 0.09, blue: 0.34),
|
|
29
|
-
Color(red: 0.20, green: 0.12, blue: 0.44),
|
|
30
|
-
Color(red: 0.28, green: 0.12, blue: 0.46)]
|
|
31
|
-
: [Color(red: 0.30, green: 0.18, blue: 0.66),
|
|
32
|
-
Color(red: 0.28, green: 0.20, blue: 0.76),
|
|
33
|
-
Color(red: 0.44, green: 0.18, blue: 0.70)]
|
|
34
|
-
return LinearGradient(colors: colors, startPoint: .top, endPoint: .bottom)
|
|
35
|
+
struct GlassButtonStyle: ButtonStyle {
|
|
36
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
37
|
+
configuration.label.padding(6).background(GlassInset())
|
|
38
|
+
.opacity(configuration.isPressed ? 0.65 : 1)
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
struct GlassCircle: View {
|
|
43
|
+
let symbol: String
|
|
44
|
+
var size: CGFloat = 44
|
|
45
|
+
var red = false
|
|
46
|
+
var progress: Double? = nil
|
|
47
|
+
var body: some View {
|
|
48
|
+
ZStack {
|
|
49
|
+
Circle().fill(.white.opacity(0.20)).padding(-3)
|
|
50
|
+
Circle()
|
|
51
|
+
.fill(LinearGradient(colors: red
|
|
52
|
+
? [Color(red: 1, green: 0.38, blue: 0.38), Color(red: 0.94, green: 0.18, blue: 0.19)]
|
|
53
|
+
: [.white.opacity(0.58), .white.opacity(0.06), .white.opacity(0.25)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
|
54
|
+
.shadow(color: red ? .red.opacity(0.26) : .black.opacity(0.10), radius: red ? 22 : 9, y: 4)
|
|
55
|
+
Circle().strokeBorder(.white.opacity(red ? 0.8 : 0.95), lineWidth: 1.2)
|
|
56
|
+
Circle().strokeBorder(.black.opacity(red ? 0.05 : 0.07), lineWidth: 1).padding(-2)
|
|
57
|
+
if let progress {
|
|
58
|
+
Circle().trim(from: 0, to: min(1, max(0, progress)))
|
|
59
|
+
.stroke(.gray.opacity(0.72), style: StrokeStyle(lineWidth: 4, lineCap: .round))
|
|
60
|
+
.rotationEffect(.degrees(-90)).padding(-5)
|
|
61
|
+
}
|
|
62
|
+
Image(systemName: symbol)
|
|
63
|
+
.font(.system(size: size * (symbol == "mic.fill" ? 0.34 : size > 100 ? 0.34 : 0.46), weight: .regular))
|
|
64
|
+
.foregroundStyle(red ? .white : Theme.accent)
|
|
65
|
+
.shadow(color: .white.opacity(0.85), radius: 0, y: 1)
|
|
66
|
+
}
|
|
67
|
+
.frame(width: size, height: size)
|
|
44
68
|
}
|
|
45
69
|
}
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
let
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
switch ChromeSurface.forReducedTransparency(reduceTransparency) {
|
|
56
|
-
case .opaque:
|
|
57
|
-
// Reduce Transparency: opaque system background, never a translucent material.
|
|
58
|
-
content
|
|
59
|
-
.background(
|
|
60
|
-
Color(NSColor.windowBackgroundColor),
|
|
61
|
-
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
|
62
|
-
)
|
|
63
|
-
.overlay(
|
|
64
|
-
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
|
65
|
-
.strokeBorder(.separator, lineWidth: 1)
|
|
66
|
-
)
|
|
67
|
-
case .liquidGlass:
|
|
68
|
-
content.glassEffect(makeGlass(), in: .rect(cornerRadius: cornerRadius))
|
|
69
|
-
}
|
|
71
|
+
struct GlassIconButton: View {
|
|
72
|
+
let symbol: String
|
|
73
|
+
let label: String
|
|
74
|
+
var size: CGFloat = 28
|
|
75
|
+
let action: () -> Void
|
|
76
|
+
var body: some View {
|
|
77
|
+
Button(action: action) { GlassCircle(symbol: symbol, size: size) }
|
|
78
|
+
.buttonStyle(.plain).help(label).accessibilityLabel(label)
|
|
70
79
|
}
|
|
80
|
+
}
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if interactive { glass = glass.interactive() }
|
|
76
|
-
return glass
|
|
82
|
+
extension View {
|
|
83
|
+
func glassSurface(cornerRadius: CGFloat, tint: Color? = nil, interactive: Bool = false) -> some View {
|
|
84
|
+
background(FrostedBackground(radius: cornerRadius))
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
extension Date {
|
|
81
|
-
/// Compact relative description for list rows, e.g. "2h ago".
|
|
82
89
|
var relativeDescription: String {
|
|
83
|
-
let f = RelativeDateTimeFormatter()
|
|
84
|
-
f.unitsStyle = .abbreviated
|
|
90
|
+
let f = RelativeDateTimeFormatter(); f.unitsStyle = .abbreviated
|
|
85
91
|
return f.localizedString(for: self, relativeTo: Date())
|
|
86
92
|
}
|
|
93
|
+
var recordingDateLabel: String {
|
|
94
|
+
let time = formatted(date: .omitted, time: .shortened)
|
|
95
|
+
if Calendar.current.isDateInToday(self) { return "Today, \(time)" }
|
|
96
|
+
if Calendar.current.isDateInYesterday(self) { return "Yesterday, \(time)" }
|
|
97
|
+
return formatted(.dateTime.month(.abbreviated).day().year())
|
|
98
|
+
}
|
|
87
99
|
}
|
|
@@ -8,6 +8,7 @@ let package = Package(
|
|
|
8
8
|
.macOS(.v26)
|
|
9
9
|
],
|
|
10
10
|
products: [
|
|
11
|
+
.library(name: "RecordingsLib", targets: ["RecordingsLib"]),
|
|
11
12
|
.executable(name: "App", targets: ["App"]),
|
|
12
13
|
.library(name: "RecordingsUpdateProtocol", targets: ["RecordingsUpdateProtocol"]),
|
|
13
14
|
.executable(name: "recordings-update-broker", targets: ["RecordingsUpdateBroker"]),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Dispatch
|
|
2
|
+
|
|
3
|
+
/// Bridge synchronous, potentially blocking APIs without occupying a cooperative worker.
|
|
4
|
+
/// The caller still owns cancellation and generation checks: submitted work drains to its
|
|
5
|
+
/// existing API deadline and returns its exact result even if its awaiting task is cancelled.
|
|
6
|
+
/// Operations must not depend on Swift task-local values or task cancellation state.
|
|
7
|
+
enum BlockingOperation {
|
|
8
|
+
private static let queue = DispatchQueue(
|
|
9
|
+
label: "md.recordings.blocking-operations",
|
|
10
|
+
attributes: .concurrent
|
|
11
|
+
)
|
|
12
|
+
static func run<Value: Sendable>(
|
|
13
|
+
qos: DispatchQoS.QoSClass = .userInitiated,
|
|
14
|
+
_ operation: @escaping @Sendable () -> Value
|
|
15
|
+
) async -> Value {
|
|
16
|
+
await withCheckedContinuation { continuation in
|
|
17
|
+
queue.async(qos: DispatchQoS(qosClass: qos, relativePriority: 0)) {
|
|
18
|
+
continuation.resume(returning: operation())
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func runThrowing<Value: Sendable>(
|
|
24
|
+
qos: DispatchQoS.QoSClass = .userInitiated,
|
|
25
|
+
_ operation: @escaping @Sendable () throws -> Value
|
|
26
|
+
) async throws -> Value {
|
|
27
|
+
try await run(qos: qos) { Result(catching: operation) }.get()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Keeps pause and metering on the audio-delivery queue, without publishing every PCM chunk.
|
|
4
|
+
final class CaptureMonitor: @unchecked Sendable {
|
|
5
|
+
private let lock = NSLock()
|
|
6
|
+
private var paused = false
|
|
7
|
+
private var bytes = 0
|
|
8
|
+
private var level: Double = 0
|
|
9
|
+
|
|
10
|
+
func setPaused(_ value: Bool) { lock.withLock { paused = value; if value { level = 0 } } }
|
|
11
|
+
|
|
12
|
+
func admit(_ data: Data) -> Bool {
|
|
13
|
+
lock.withLock {
|
|
14
|
+
guard !paused else { return false }
|
|
15
|
+
bytes += data.count
|
|
16
|
+
var sum: Double = 0
|
|
17
|
+
var count = 0
|
|
18
|
+
data.withUnsafeBytes { raw in
|
|
19
|
+
for offset in stride(from: 0, to: max(0, raw.count - 1), by: 2) {
|
|
20
|
+
let value = Double(Int16(bitPattern: UInt16(raw[offset]) | UInt16(raw[offset + 1]) << 8)) / 32768
|
|
21
|
+
sum += value * value
|
|
22
|
+
count += 1
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
level = count > 0 ? min(1, sqrt(sum / Double(count)) * 5) : 0
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func snapshot() -> (duration: Double, level: Double) {
|
|
31
|
+
lock.withLock { (Double(bytes) / 48_000, level) }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public enum RecentPasteDeliveryStatus: String, Codable, Sendable {
|
|
36
|
+
case confirmed
|
|
37
|
+
case unconfirmed
|
|
38
|
+
case notDelivered
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public struct RecentPaste: Identifiable, Sendable {
|
|
42
|
+
public let id = UUID()
|
|
43
|
+
public let text: String
|
|
44
|
+
public let bundleIdentifier: String?
|
|
45
|
+
public let appName: String
|
|
46
|
+
public let location: String
|
|
47
|
+
public let timestamp = Date()
|
|
48
|
+
public let status: String
|
|
49
|
+
public let verified: Bool
|
|
50
|
+
public let captureID: String?
|
|
51
|
+
public let deliveryStatus: RecentPasteDeliveryStatus
|
|
52
|
+
|
|
53
|
+
public init(text: String, bundleIdentifier: String?, appName: String, location: String,
|
|
54
|
+
status: String, verified: Bool, captureID: String? = nil,
|
|
55
|
+
deliveryStatus: RecentPasteDeliveryStatus? = nil) {
|
|
56
|
+
self.text = text
|
|
57
|
+
self.bundleIdentifier = bundleIdentifier
|
|
58
|
+
self.appName = appName
|
|
59
|
+
self.location = location
|
|
60
|
+
self.status = status
|
|
61
|
+
self.verified = verified
|
|
62
|
+
self.captureID = captureID
|
|
63
|
+
self.deliveryStatus = deliveryStatus ?? (verified ? .confirmed : .unconfirmed)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
|
10
10
|
<true/>
|
|
11
11
|
<key>CFBundleName</key>
|
|
12
|
-
<string>Recordings</string>
|
|
12
|
+
<string>Hasna Recordings</string>
|
|
13
13
|
<key>CFBundleDisplayName</key>
|
|
14
14
|
<string>Hasna Recordings</string>
|
|
15
15
|
<key>CFBundleExecutable</key>
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
<key>CFBundleIdentifier</key>
|
|
18
18
|
<string>com.hasna.recordings</string>
|
|
19
19
|
<key>CFBundleVersion</key>
|
|
20
|
-
<string>0.
|
|
20
|
+
<string>0.5.0</string>
|
|
21
21
|
<key>CFBundlePackageType</key>
|
|
22
22
|
<string>APPL</string>
|
|
23
23
|
<key>CFBundleShortVersionString</key>
|
|
24
|
-
<string>0.
|
|
24
|
+
<string>0.5.0</string>
|
|
25
25
|
<key>LSMinimumSystemVersion</key>
|
|
26
26
|
<string>26.0</string>
|
|
27
27
|
<key>NSMicrophoneUsageDescription</key>
|