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)