diff --git a/VoiceInk/AppDelegate.swift b/VoiceInk/AppDelegate.swift
index bf27f48..4bc530b 100644
--- a/VoiceInk/AppDelegate.swift
+++ b/VoiceInk/AppDelegate.swift
@@ -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 SwiftUI’s 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])
+ }
+ }
+ }
}
diff --git a/VoiceInk/Info.plist b/VoiceInk/Info.plist
index c29b98f..7584b1c 100644
--- a/VoiceInk/Info.plist
+++ b/VoiceInk/Info.plist
@@ -18,5 +18,64 @@
VoiceInk needs to interact with your browser to detect the current website for applying website-specific configurations.
NSScreenCaptureUsageDescription
VoiceInk needs screen recording access to understand context from your screen for improved transcription accuracy.
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeName
+ Audio/Video File
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ LSItemContentTypes
+
+ public.audio
+ public.movie
+
+ CFBundleTypeExtensions
+
+ wav
+ mp3
+ m4a
+ aiff
+ mp4
+ mov
+ aac
+ flac
+ caf
+
+
+
+
+
+
+CFBundleDocumentTypes
+
+
+ CFBundleTypeName
+ Audio/Video File
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ LSItemContentTypes
+
+ public.audio
+ public.movie
+
+ CFBundleTypeExtensions
+
+ wav
+ mp3
+ m4a
+ aiff
+ mp4
+ mov
+ aac
+ flac
+ caf
+
+
+
diff --git a/VoiceInk/Notifications/AppNotifications.swift b/VoiceInk/Notifications/AppNotifications.swift
index 0c1ae30..c23e055 100644
--- a/VoiceInk/Notifications/AppNotifications.swift
+++ b/VoiceInk/Notifications/AppNotifications.swift
@@ -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")
}
diff --git a/VoiceInk/Views/AudioTranscribeView.swift b/VoiceInk/Views/AudioTranscribeView.swift
index 9c4ef82..d73c8b8 100644
--- a/VoiceInk/Views/AudioTranscribeView.swift
+++ b/VoiceInk/Views/AudioTranscribeView.swift
@@ -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)
}
-}
+}
diff --git a/VoiceInk/Views/ContentView.swift b/VoiceInk/Views/ContentView.swift
index 3e91955..a610b49 100644
--- a/VoiceInk/Views/ContentView.swift
+++ b/VoiceInk/Views/ContentView.swift
@@ -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)
+ }
+ }
+}
diff --git a/VoiceInk/VoiceInk.swift b/VoiceInk/VoiceInk.swift
index 549a500..af22cfd 100644
--- a/VoiceInk/VoiceInk.swift
+++ b/VoiceInk/VoiceInk.swift
@@ -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)