Merge pull request #276 from gdmka/feat/open-with-voiceink-finder

Add Open with VoiceInk handling
This commit is contained in:
Prakash Joshi Pax 2025-09-05 10:44:46 +05:45 committed by GitHub
commit 8cc3b76972
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 139 additions and 1 deletions

View File

@ -49,4 +49,31 @@ class AppDelegate: NSObject, NSApplicationDelegate {
defaults.removeObject(forKey: "defaultPowerModeConfigV2")
defaults.removeObject(forKey: "isPowerModeEnabled")
}
// Keep in sync with AudioTranscribeView.supportedExtensions
private let supportedExtensions = ["wav", "mp3", "m4a", "aiff", "mp4", "mov", "aac", "flac", "caf"]
// Stash URL when app cold-starts to avoid spawning a new window/tab
var pendingOpenFileURL: URL?
func application(_ application: NSApplication, open urls: [URL]) {
guard let url = urls.first(where: { supportedExtensions.contains($0.pathExtension.lowercased()) }) else {
return
}
NSApp.activate(ignoringOtherApps: true)
if NSApp.windows.isEmpty {
// Cold start: do NOT create a window here to avoid extra window/tab.
// Defer to SwiftUIs WindowGroup-created ContentView and let it process this later.
pendingOpenFileURL = url
} else {
// Running: focus current window and route in-place to Transcribe Audio
NSApp.windows.first?.makeKeyAndOrderFront(nil)
NotificationCenter.default.post(name: .navigateToDestination, object: nil, userInfo: ["destination": "Transcribe Audio"])
DispatchQueue.main.async {
NotificationCenter.default.post(name: .openFileForTranscription, object: nil, userInfo: ["url": url])
}
}
}
}

View File

@ -18,5 +18,64 @@
<string>VoiceInk needs to interact with your browser to detect the current website for applying website-specific configurations.</string>
<key>NSScreenCaptureUsageDescription</key>
<string>VoiceInk needs screen recording access to understand context from your screen for improved transcription accuracy.</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Audio/Video File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.audio</string>
<string>public.movie</string>
</array>
<key>CFBundleTypeExtensions</key>
<array>
<string>wav</string>
<string>mp3</string>
<string>m4a</string>
<string>aiff</string>
<string>mp4</string>
<string>mov</string>
<string>aac</string>
<string>flac</string>
<string>caf</string>
</array>
</dict>
</array>
</dict>
</plist>
<!-- Somewhere near the existing keys -->
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Audio/Video File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.audio</string>
<string>public.movie</string>
</array>
<key>CFBundleTypeExtensions</key>
<array>
<string>wav</string>
<string>mp3</string>
<string>m4a</string>
<string>aiff</string>
<string>mp4</string>
<string>mov</string>
<string>aac</string>
<string>flac</string>
<string>caf</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -14,4 +14,5 @@ extension Notification.Name {
static let powerModeConfigurationApplied = Notification.Name("powerModeConfigurationApplied")
static let transcriptionCreated = Notification.Name("transcriptionCreated")
static let enhancementToggleChanged = Notification.Name("enhancementToggleChanged")
static let openFileForTranscription = Notification.Name("openFileForTranscription")
}

View File

@ -112,6 +112,12 @@ struct AudioTranscribeView: View {
Text(errorMessage)
}
}
.onReceive(NotificationCenter.default.publisher(for: .openFileForTranscription)) { notification in
if let url = notification.userInfo?["url"] as? URL {
// Do not auto-start; only select file for manual transcription
validateAndSetAudioFile(url)
}
}
}
private var dropZoneView: some View {
@ -381,4 +387,4 @@ struct AudioTranscribeView: View {
let seconds = Int(duration) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
}

View File

@ -164,6 +164,8 @@ struct ContentView: View {
@State private var hasLoadedData = false
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
@StateObject private var licenseViewModel = LicenseViewModel()
// Capture the hosting window to update tab/window title dynamically
@State private var hostingWindow: NSWindow?
private var isSetupComplete: Bool {
hasLoadedData &&
@ -189,9 +191,17 @@ struct ContentView: View {
}
.navigationSplitViewStyle(.balanced)
.frame(minWidth: 940, minHeight: 730)
// Resolve hosting NSWindow and set initial title
.background(
WindowTitleAccessor { window in
self.hostingWindow = window
self.hostingWindow?.title = selectedView.rawValue
}
)
.onAppear {
hasLoadedData = true
}
// inside ContentView body:
.onReceive(NotificationCenter.default.publisher(for: .navigateToDestination)) { notification in
print("ContentView: Received navigation notification")
if let destination = notification.userInfo?["destination"] as? String {
@ -215,6 +225,10 @@ struct ContentView: View {
case "Enhancement":
print("ContentView: Navigating to Enhancement")
selectedView = .enhancement
case "Transcribe Audio":
// Ensure we switch to the Transcribe Audio view in-place
print("ContentView: Navigating to Transcribe Audio")
selectedView = .transcribeAudio
default:
print("ContentView: No matching destination found for: \(destination)")
break
@ -223,6 +237,10 @@ struct ContentView: View {
print("ContentView: No destination in notification")
}
}
// Update the tab/window title whenever the active view changes
.onChange(of: selectedView) { newValue in
hostingWindow?.title = newValue.rawValue
}
}
@ViewBuilder
@ -259,3 +277,21 @@ struct ContentView: View {
}
}
}
struct WindowTitleAccessor: NSViewRepresentable {
var onResolve: (NSWindow?) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async { [weak view] in
onResolve(view?.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
DispatchQueue.main.async { [weak nsView] in
onResolve(nsView?.window)
}
}
}

View File

@ -114,6 +114,15 @@ struct VoiceInkApp: App {
if !UserDefaults.standard.bool(forKey: "IsTranscriptionCleanupEnabled") {
audioCleanupManager.startAutomaticCleanup(modelContext: container.mainContext)
}
// Process any pending open-file request now that the main ContentView is ready.
if let pendingURL = appDelegate.pendingOpenFileURL {
NotificationCenter.default.post(name: .navigateToDestination, object: nil, userInfo: ["destination": "Transcribe Audio"])
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
NotificationCenter.default.post(name: .openFileForTranscription, object: nil, userInfo: ["url": pendingURL])
}
appDelegate.pendingOpenFileURL = nil
}
}
.background(WindowAccessor { window in
WindowManager.shared.configureWindow(window)