Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/desktop/electron-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ const config: Configuration = {
buildResources: join(pkg.resources, "build"),
},

// ASAR configuration for native modules
// ASAR configuration for native modules and external resources
asar: true,
asarUnpack: ["**/node_modules/node-pty/**/*"],
asarUnpack: [
"**/node_modules/node-pty/**/*",
// Sound files must be unpacked so external audio players (afplay, paplay, etc.) can access them
"**/resources/sounds/**/*",
],

files: [
"dist/**/*",
Expand Down
30 changes: 29 additions & 1 deletion apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
import { dirname, normalize, resolve } from "node:path";
import tailwindcss from "@tailwindcss/vite";
import reactPlugin from "@vitejs/plugin-react";
import { codeInspectorPlugin } from "code-inspector-plugin";
import { config } from "dotenv";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
import injectProcessEnvPlugin from "rollup-plugin-inject-process-env";
import type { Plugin } from "vite";
import tsconfigPathsPlugin from "vite-tsconfig-paths";
import { main, resources } from "./package.json";

Expand All @@ -22,9 +24,35 @@ const tsconfigPaths = tsconfigPathsPlugin({
projects: [resolve("tsconfig.json")],
});

/**
* Plugin to copy resources (like sounds) to the dist folder for preview mode.
* In preview mode, __dirname resolves relative to dist/main, so resources
* need to be at dist/resources/sounds for the main process to access them.
*
* Cleans the destination first to avoid stale files from previous builds.
*/
function copyResourcesPlugin(): Plugin {
return {
name: "copy-resources",
writeBundle() {
const srcDir = resolve(resources, "sounds");
const destDir = resolve(devPath, "resources/sounds");

if (existsSync(srcDir)) {
// Clean destination to avoid stale files
if (existsSync(destDir)) {
rmSync(destDir, { recursive: true });
}
mkdirSync(destDir, { recursive: true });
cpSync(srcDir, destDir, { recursive: true });
}
},
};
}

export default defineConfig({
main: {
plugins: [tsconfigPaths],
plugins: [tsconfigPaths, copyResourcesPlugin()],

build: {
rollupOptions: {
Expand Down
39 changes: 38 additions & 1 deletion apps/desktop/src/lib/electron-router-dom.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
import type { BrowserWindow } from "electron";
import { createElectronRouter } from "electron-router-dom";
import { PORTS } from "shared/constants";

export const { Router, registerRoute, settings } = createElectronRouter({
const electronRouter = createElectronRouter({
port: PORTS.VITE_DEV_SERVER,
types: {
ids: ["main", "about"],
},
});

export const { Router, settings } = electronRouter;

/** Window IDs defined in the router configuration */
type WindowId = "main" | "about";

/**
* Custom registerRoute that uses NODE_ENV instead of app.isPackaged.
* This allows `electron-vite preview` (bun start) to load built files
* instead of trying to connect to a dev server.
*
* - Development (NODE_ENV=development): loads from dev server
* - Preview/Production: loads from built HTML file
*/
export function registerRoute(props: {
id: WindowId;
browserWindow: BrowserWindow;
htmlFile: string;
query?: Record<string, string>;
}): void {
const isDev = process.env.NODE_ENV === "development";

if (isDev) {
// Development: use the library's default behavior (loads from dev server)
electronRouter.registerRoute(props);
} else {
// Preview or Production: load from file with hash routing
const windowId = props.id || "main";
let url = `/${windowId}`;
if (props.query) {
const query = new URLSearchParams(props.query).toString();
url = `${url}?${query}`;
}
props.browserWindow.loadFile(props.htmlFile, { hash: url });
}
}
37 changes: 7 additions & 30 deletions apps/desktop/src/lib/trpc/routers/ringtone/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { ChildProcess } from "node:child_process";
import { execFile } from "node:child_process";
import { existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { app } from "electron";
import { z } from "zod";
import {
getSoundPath,
getSoundsDirectory,
} from "../../../../main/lib/sound-paths";
import { publicProcedure, router } from "../..";

/**
Expand All @@ -17,31 +19,6 @@ let currentSession: {
} | null = null;
let nextSessionId = 0;

/**
* Gets the path to a ringtone sound file.
* In development, reads from src/resources. In production, reads from the bundled resources.
*/
function getRingtonePath(filename: string): string {
const isDev = !app.isPackaged;

if (isDev) {
return join(app.getAppPath(), "src/resources/sounds", filename);
}
return join(process.resourcesPath, "resources/sounds", filename);
}

/**
* Gets the directory containing ringtone files
*/
function getRingtonesDirectory(): string {
const isDev = !app.isPackaged;

if (isDev) {
return join(app.getAppPath(), "src/resources/sounds");
}
return join(process.resourcesPath, "resources/sounds");
}

/**
* Stops the currently playing sound and invalidates the session
*/
Expand Down Expand Up @@ -127,7 +104,7 @@ export const createRingtoneRouter = () => {
return { success: true as const };
}

const soundPath = getRingtonePath(input.filename);
const soundPath = getSoundPath(input.filename);
playSoundFile(soundPath);
return { success: true as const };
}),
Expand All @@ -144,7 +121,7 @@ export const createRingtoneRouter = () => {
* Get the list of available ringtone files from the sounds directory
*/
list: publicProcedure.query(() => {
const ringtonesDir = getRingtonesDirectory();
const ringtonesDir = getSoundsDirectory();
const files: string[] = [];

// Add ringtones from the sounds directory if it exists
Expand Down Expand Up @@ -172,6 +149,6 @@ export function playNotificationRingtone(filename: string): void {
return; // No sound for "none" option
}

const soundPath = getRingtonePath(filename);
const soundPath = getSoundPath(filename);
playSoundFile(soundPath);
}
18 changes: 2 additions & 16 deletions apps/desktop/src/main/lib/notification-sound.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { app } from "electron";
import {
DEFAULT_RINGTONE_ID,
getRingtoneFilename,
} from "../../shared/ringtones";
import { db } from "./db";

/**
* Gets the path to a ringtone sound file.
* In development, reads from src/resources. In production, reads from the bundled resources.
*/
function getRingtonePath(filename: string): string {
const isDev = !app.isPackaged;

if (isDev) {
return join(app.getAppPath(), "src/resources/sounds", filename);
}
return join(process.resourcesPath, "resources/sounds", filename);
}
import { getSoundPath } from "./sound-paths";

/**
* Gets the selected ringtone filename from the database.
Expand Down Expand Up @@ -83,6 +69,6 @@ export function playNotificationSound(): void {
return;
}

const soundPath = getRingtonePath(filename);
const soundPath = getSoundPath(filename);
playSoundFile(soundPath);
}
58 changes: 58 additions & 0 deletions apps/desktop/src/main/lib/sound-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { existsSync } from "node:fs";
import { join } from "node:path";
import { app } from "electron";

/**
* Gets the path to a ringtone sound file.
*
* Path resolution strategy:
* - Production (packaged .app): app.asar.unpacked/resources/sounds/
* - Development (NODE_ENV=development): src/resources/sounds/
* - Preview (electron-vite preview): dist/resources/sounds/ (relative to __dirname)
*
* Sound files are unpacked from asar so external audio players (afplay, etc.) can access them.
*/
export function getSoundPath(filename: string): string {
const dir = getSoundsDirectory();
return join(dir, filename);
}

/**
* Gets the directory containing ringtone sound files.
*
* In preview mode, uses __dirname (dist/main) to reliably resolve to dist/resources/sounds,
* avoiding dependency on app.getAppPath() or process.cwd() which may vary.
*/
export function getSoundsDirectory(): string {
if (app.isPackaged) {
// Production: unpacked from asar for external audio players
return join(process.resourcesPath, "app.asar.unpacked/resources/sounds");
}

const isDev = process.env.NODE_ENV === "development";

if (isDev) {
// Development: source files in project
return join(app.getAppPath(), "src/resources/sounds");
}

// Preview mode: __dirname is dist/main, so go up one level to dist/resources/sounds
// This is the most reliable path in preview since it's relative to the bundle location
const previewPath = join(__dirname, "../resources/sounds");
if (existsSync(previewPath)) {
return previewPath;
}

// Fallback: try source directory (in case sounds weren't copied to dist)
const srcPath = join(app.getAppPath(), "src/resources/sounds");
if (existsSync(srcPath)) {
console.warn(
"[sound-paths] Using src/resources/sounds as fallback - sounds may not have been copied to dist",
);
return srcPath;
}

// Return the expected preview path even if missing (will fail gracefully in playback)
console.warn(`[sound-paths] Sounds directory not found at: ${previewPath}`);
return previewPath;
}
Loading