From 8ead8d1454d4e59e8bff32fdb7e749850fc3b2af Mon Sep 17 00:00:00 2001 From: Naomi Chopra Date: Fri, 4 Jul 2025 14:22:28 +0530 Subject: [PATCH] chore: enable audio downloading --- apps/desktop/src/main/core/app-manager.ts | 35 +---- apps/desktop/src/main/core/window-manager.ts | 29 ++-- .../src/main/managers/recording-manager.ts | 6 - .../src/main/managers/service-manager.ts | 37 ++++-- .../desktop/src/main/services/auto-updater.ts | 3 +- .../components/TranscriptionsList.tsx | 55 +++++--- .../src/services/transcription-service.ts | 93 ++++++++++++- .../src/trpc/routers/transcriptions.ts | 114 +++++++++++++++- apps/desktop/src/utils/audio-converter.ts | 77 +++++++++++ apps/desktop/src/utils/audio-file-cleanup.ts | 124 ++++++++++++++++++ 10 files changed, 494 insertions(+), 79 deletions(-) create mode 100644 apps/desktop/src/utils/audio-converter.ts create mode 100644 apps/desktop/src/utils/audio-file-cleanup.ts diff --git a/apps/desktop/src/main/core/app-manager.ts b/apps/desktop/src/main/core/app-manager.ts index a0e1adf..9a776e4 100644 --- a/apps/desktop/src/main/core/app-manager.ts +++ b/apps/desktop/src/main/core/app-manager.ts @@ -1,16 +1,9 @@ -import { - app, - systemPreferences, - BrowserWindow, - globalShortcut, -} from "electron"; +import { app, systemPreferences, globalShortcut } from "electron"; import { initializeDatabase } from "../../db/config"; import { logger } from "../logger"; import { WindowManager } from "./window-manager"; import { setupApplicationMenu } from "../menu"; import { ServiceManager } from "../managers/service-manager"; -import { createIPCHandler } from "electron-trpc-experimental/main"; -import { router } from "../../trpc/router"; import { EventHandlers } from "./event-handlers"; export class AppManager { @@ -20,9 +13,6 @@ export class AppManager { constructor() { this.windowManager = new WindowManager(); this.serviceManager = ServiceManager.createInstance(); - this.windowManager.setMainWindowCreatedCallback( - this.onMainWindowCreated.bind(this), - ); } async initialize(): Promise { @@ -30,7 +20,7 @@ export class AppManager { await this.initializeDatabase(); await this.requestPermissions(); - await this.serviceManager.initialize(this.windowManager); + await this.serviceManager.initialize(); this.exposeGlobalServices(); await this.setupWindows(); await this.setupMenu(); @@ -80,28 +70,13 @@ export class AppManager { private async setupWindows(): Promise { this.windowManager.createWidgetWindow(); this.windowManager.createOrShowMainWindow(); - this.setupTRPCHandler(); + // tRPC handler is now set up in WindowManager when windows are created if (process.platform === "darwin" && app.dock) { app.dock.show(); } } - private setupTRPCHandler(): Promise { - const windows = this.windowManager - .getAllWindows() - .filter((w): w is BrowserWindow => w !== null); - createIPCHandler({ router, windows }); - return Promise.resolve(); - } - - updateTRPCHandler(): void { - const windows = this.windowManager - .getAllWindows() - .filter((w): w is BrowserWindow => w !== null); - createIPCHandler({ router, windows }); - } - private async setupMenu(): Promise { setupApplicationMenu( () => this.windowManager.createOrShowMainWindow(), @@ -151,10 +126,6 @@ export class AppManager { return this.serviceManager.getAutoUpdaterService(); } - private onMainWindowCreated(window: BrowserWindow): void { - this.updateTRPCHandler(); - } - async cleanup(): Promise { globalShortcut.unregisterAll(); await this.serviceManager.cleanup(); diff --git a/apps/desktop/src/main/core/window-manager.ts b/apps/desktop/src/main/core/window-manager.ts index 6ea2db7..e82e819 100644 --- a/apps/desktop/src/main/core/window-manager.ts +++ b/apps/desktop/src/main/core/window-manager.ts @@ -1,6 +1,7 @@ import { BrowserWindow, screen, systemPreferences } from "electron"; import path from "node:path"; import { logger } from "../logger"; +import { ServiceManager } from "../managers/service-manager"; declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; declare const MAIN_WINDOW_VITE_NAME: string; @@ -11,7 +12,6 @@ export class WindowManager { private widgetWindow: BrowserWindow | null = null; private currentWindowDisplayId: number | null = null; private activeSpaceChangeSubscriptionId: number | null = null; - private onMainWindowCreated?: (window: BrowserWindow) => void; createOrShowMainWindow(): void { if (this.mainWindow && !this.mainWindow.isDestroyed()) { @@ -43,12 +43,15 @@ export class WindowManager { } this.mainWindow.on("closed", () => { + ServiceManager.getInstance()! + .getTRPCHandler()! + .detachWindow(this.mainWindow!); this.mainWindow = null; }); - if (this.onMainWindowCreated) { - this.onMainWindowCreated(this.mainWindow); - } + ServiceManager.getInstance()! + .getTRPCHandler()! + .attachWindow(this.mainWindow!); } createWidgetWindow(): void { @@ -89,6 +92,13 @@ export class WindowManager { ); } + this.widgetWindow.on("closed", () => { + ServiceManager.getInstance()! + .getTRPCHandler()! + .detachWindow(this.widgetWindow!); + this.widgetWindow = null; + }); + if (process.platform === "darwin") { this.widgetWindow.setAlwaysOnTop(true, "floating", 1); this.widgetWindow.setVisibleOnAllWorkspaces(true, { @@ -97,6 +107,11 @@ export class WindowManager { this.widgetWindow.setHiddenInMissionControl(true); this.setupDisplayChangeNotifications(); } + + // Update tRPC handler with new window + ServiceManager.getInstance()! + .getTRPCHandler()! + .attachWindow(this.widgetWindow!); } private setupDisplayChangeNotifications(): void { @@ -159,12 +174,6 @@ export class WindowManager { return [this.mainWindow, this.widgetWindow]; } - setMainWindowCreatedCallback( - callback: (window: BrowserWindow) => void, - ): void { - this.onMainWindowCreated = callback; - } - openAllDevTools(): void { const windows = this.getAllWindows().filter( (window): window is BrowserWindow => diff --git a/apps/desktop/src/main/managers/recording-manager.ts b/apps/desktop/src/main/managers/recording-manager.ts index 0b0bd10..1bd23ba 100644 --- a/apps/desktop/src/main/managers/recording-manager.ts +++ b/apps/desktop/src/main/managers/recording-manager.ts @@ -4,7 +4,6 @@ import { logger, logPerformance } from "../logger"; import { ServiceManager } from "./service-manager"; import { appContextStore } from "../../stores/app-context"; import type { RecordingState, RecordingStatus } from "../../types/recording"; -import { WindowManager } from "../core/window-manager"; /** * Manages recording state and coordinates audio recording across the application @@ -14,17 +13,12 @@ export class RecordingManager extends EventEmitter { private currentSessionId: string | null = null; private recordingState: RecordingState = "idle"; private lastError: string | undefined; - private windowManager: WindowManager | null = null; constructor(private serviceManager: ServiceManager) { super(); this.setupIPCHandlers(); } - public setWindowManager(windowManager: WindowManager): void { - this.windowManager = windowManager; - } - private setState(newState: RecordingState, error?: string): void { const oldState = this.recordingState; this.recordingState = newState; diff --git a/apps/desktop/src/main/managers/service-manager.ts b/apps/desktop/src/main/managers/service-manager.ts index b2022de..004ea62 100644 --- a/apps/desktop/src/main/managers/service-manager.ts +++ b/apps/desktop/src/main/managers/service-manager.ts @@ -4,9 +4,11 @@ import { TranscriptionService } from "../../services/transcription-service"; import { SettingsService } from "../../services/settings-service"; import { SwiftIOBridge } from "../../services/platform/swift-bridge-service"; import { AutoUpdaterService } from "../services/auto-updater"; -import { WindowManager } from "../core/window-manager"; import { RecordingManager } from "./recording-manager"; import { VADService } from "../../services/vad-service"; +import { createIPCHandler } from "electron-trpc-experimental/main"; +import { router } from "../../trpc/router"; +import { BrowserWindow } from "electron"; /** * Manages service initialization and lifecycle @@ -23,8 +25,9 @@ export class ServiceManager { private swiftIOBridge: SwiftIOBridge | null = null; private autoUpdaterService: AutoUpdaterService | null = null; private recordingManager: RecordingManager | null = null; + private trpcHandler: ReturnType | null = null; - async initialize(windowManager: WindowManager): Promise { + async initialize(): Promise { if (this.isInitialized) { logger.main.warn( "ServiceManager is already initialized, skipping initialization", @@ -38,8 +41,9 @@ export class ServiceManager { this.initializePlatformServices(); await this.initializeVADService(); await this.initializeAIServices(); - this.initializeRecordingManager(windowManager); - this.initializeAutoUpdater(windowManager); + this.initializeRecordingManager(); + this.initializeAutoUpdater(); + this.initializeTRPCHandler(); this.isInitialized = true; logger.main.info("Services initialized successfully"); @@ -125,14 +129,19 @@ export class ServiceManager { } } - private initializeRecordingManager(windowManager: WindowManager): void { + private initializeRecordingManager(): void { this.recordingManager = new RecordingManager(this); - this.recordingManager.setWindowManager(windowManager); logger.main.info("Recording manager initialized"); } - private initializeAutoUpdater(windowManager: WindowManager): void { - this.autoUpdaterService = new AutoUpdaterService(windowManager); + private initializeAutoUpdater(): void { + this.autoUpdaterService = new AutoUpdaterService(); + } + + private initializeTRPCHandler(): void { + // Initialize with empty windows array, windows will be added later + this.trpcHandler = createIPCHandler({ router, windows: [] }); + logger.main.info("tRPC handler initialized"); } // Getters for other managers to access services @@ -217,6 +226,18 @@ export class ServiceManager { return this.vadService; } + getTRPCHandler(): ReturnType | null { + if (!this.isInitialized) { + throw new Error( + "ServiceManager not initialized. Call initialize() first.", + ); + } + if (!this.trpcHandler) { + throw new Error("TRPCHandler failed to initialize"); + } + return this.trpcHandler; + } + async cleanup(): Promise { if (this.recordingManager) { logger.main.info("Cleaning up recording manager..."); diff --git a/apps/desktop/src/main/services/auto-updater.ts b/apps/desktop/src/main/services/auto-updater.ts index e17313c..fa543b3 100644 --- a/apps/desktop/src/main/services/auto-updater.ts +++ b/apps/desktop/src/main/services/auto-updater.ts @@ -1,10 +1,9 @@ import { app } from "electron"; import { EventEmitter } from "events"; import { logger } from "../logger"; -import { WindowManager } from "../core/window-manager"; export class AutoUpdaterService extends EventEmitter { - constructor(private windowManager: WindowManager) { + constructor() { super(); } diff --git a/apps/desktop/src/renderer/main/pages/transcriptions/components/TranscriptionsList.tsx b/apps/desktop/src/renderer/main/pages/transcriptions/components/TranscriptionsList.tsx index 8f9e8f6..4521fe4 100644 --- a/apps/desktop/src/renderer/main/pages/transcriptions/components/TranscriptionsList.tsx +++ b/apps/desktop/src/renderer/main/pages/transcriptions/components/TranscriptionsList.tsx @@ -19,6 +19,7 @@ import { FileText, Search, MoreHorizontal, + FileAudio, } from "lucide-react"; import { format } from "date-fns"; import { Input } from "@/components/ui/input"; @@ -33,6 +34,7 @@ import { export const TranscriptionsList: React.FC = () => { const [searchTerm, setSearchTerm] = useState(""); + const [openDropdownId, setOpenDropdownId] = useState(null); // tRPC React Query hooks const transcriptionsQuery = api.transcriptions.getTranscriptions.useQuery( @@ -72,6 +74,13 @@ export const TranscriptionsList: React.FC = () => { }, }); + const downloadAudioMutation = + api.transcriptions.downloadAudioFile.useMutation({ + onError: (error) => { + console.error("Error downloading audio:", error); + }, + }); + const transcriptions = transcriptionsQuery.data || []; const totalCount = transcriptionsCountQuery.data || 0; const loading = @@ -95,15 +104,19 @@ export const TranscriptionsList: React.FC = () => { console.log("Playing audio:", audioFile); }; - const handleDownload = (transcription: Transcription) => { - // Create and download a text file with the transcription - const element = document.createElement("a"); - const file = new Blob([transcription.text], { type: "text/plain" }); - element.href = URL.createObjectURL(file); - element.download = `transcription-${transcription.id}.txt`; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); + const handleDownloadAudio = async (transcriptionId: number) => { + console.log("Downloading audio:", transcriptionId); + // Close dropdown first + setOpenDropdownId(null); + + // Small delay to ensure dropdown closes before system dialog opens + setTimeout(async () => { + try { + await downloadAudioMutation.mutateAsync({ transcriptionId }); + } catch (error) { + console.error("Failed to download audio:", error); + } + }, 0); }; const getTitle = (text: string) => { @@ -227,7 +240,12 @@ export const TranscriptionsList: React.FC = () => { )} - + + setOpenDropdownId(open ? transcription.id : null) + } + >