Skip to content
Closed

Ai gf #315

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3d680e8
Merge feat(export): allow re-saving exported video on dialog cancel (…
siddharthvaddem Mar 22, 2026
4a29906
lang support
siddharthvaddem Mar 22, 2026
7aca8b8
move project settings to top
siddharthvaddem Mar 22, 2026
cbbe2d7
movable camera pip
siddharthvaddem Mar 22, 2026
a8bb0e8
improved vertical split gated behind 9:16
siddharthvaddem Mar 22, 2026
5d561ff
feat: add fullscreen video player
ateendra24 Mar 22, 2026
eae3f11
feat: Implement `PlaybackControls` component and add i18n files for c…
ateendra24 Mar 22, 2026
e35780b
Merge pull request #244 from ateendra24/fix-issue-226
siddharthvaddem Mar 22, 2026
0a5e57c
feat: add webcam source selector with stable HUD layout
EtienneLescot Mar 27, 2026
317089d
fix: restore flex layout to ensure HUD renders in transparent Electro…
EtienneLescot Mar 27, 2026
e72851d
fix: use fixed positioning for HUD and device selectors to avoid h-fu…
EtienneLescot Mar 27, 2026
a9222c9
fix: equalize badge heights and reduce window to 160px
EtienneLescot Mar 27, 2026
fed5a44
fix: enforce identical badge height with h-[36px] on both selectors
EtienneLescot Mar 27, 2026
eade280
fix: address PR review comments
EtienneLescot Mar 27, 2026
9762448
fix: address coderabbit comments (loading state + keyboard access)
EtienneLescot Mar 27, 2026
9817c85
fix: address coderabbit review (concurrent stream, collapsed label, u…
EtienneLescot Mar 27, 2026
baec9a7
fix: focusable element when webcam expanded with no devices, add erro…
EtienneLescot Mar 27, 2026
2f36160
version bump
siddharthvaddem Apr 2, 2026
846cf71
fix: prevent double-finalize race condition in restartRecording on Wi…
abres33 Apr 3, 2026
3061c14
Merge pull request #249 from EtienneLescot/feat/webcam-selector-optim…
siddharthvaddem Apr 3, 2026
b101820
Merge pull request #293 from abres33/fix/restart-recording-windows
siddharthvaddem Apr 3, 2026
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
1 change: 1 addition & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ interface Window {
setMicrophoneExpanded: (expanded: boolean) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => () => void;
setLocale: (locale: string) => Promise<void>;
};
}

Expand Down
59 changes: 59 additions & 0 deletions electron/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Lightweight i18n for the Electron main process.
// Imports the same JSON translation files used by the renderer.

import commonEn from "../src/i18n/locales/en/common.json";
import dialogsEn from "../src/i18n/locales/en/dialogs.json";
import commonEs from "../src/i18n/locales/es/common.json";
import dialogsEs from "../src/i18n/locales/es/dialogs.json";
import commonZh from "../src/i18n/locales/zh-CN/common.json";
import dialogsZh from "../src/i18n/locales/zh-CN/dialogs.json";

type Locale = "en" | "zh-CN" | "es";
type Namespace = "common" | "dialogs";
type MessageMap = Record<string, unknown>;

const messages: Record<Locale, Record<Namespace, MessageMap>> = {
en: { common: commonEn, dialogs: dialogsEn },
"zh-CN": { common: commonZh, dialogs: dialogsZh },
es: { common: commonEs, dialogs: dialogsEs },
};

let currentLocale: Locale = "en";

export function setMainLocale(locale: string) {
if (locale === "en" || locale === "zh-CN" || locale === "es") {
currentLocale = locale;
}
}

export function getMainLocale(): Locale {
return currentLocale;
}

function getMessageValue(obj: unknown, dotPath: string): string | undefined {
const keys = dotPath.split(".");
let current: unknown = obj;
for (const key of keys) {
if (current == null || typeof current !== "object") return undefined;
current = (current as Record<string, unknown>)[key];
}
return typeof current === "string" ? current : undefined;
}

function interpolate(str: string, vars?: Record<string, string | number>): string {
if (!vars) return str;
return str.replace(/\{\{(\w+)\}\}/g, (_, key: string) => String(vars[key] ?? `{{${key}}}`));
}

export function mainT(
namespace: Namespace,
key: string,
vars?: Record<string, string | number>,
): string {
const value =
getMessageValue(messages[currentLocale]?.[namespace], key) ??
getMessageValue(messages.en?.[namespace], key);

if (value == null) return `${namespace}.${key}`;
return interpolate(value, vars);
}
34 changes: 23 additions & 11 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type RecordingSession,
type StoreRecordedSessionInput,
} from "../../src/lib/recordingSession";
import { mainT } from "../i18n";
import { RECORDINGS_DIR } from "../main";

const PROJECT_FILE_EXTENSION = "openscreen";
Expand Down Expand Up @@ -472,11 +473,13 @@ export function registerIpcHandlers(
// Determine file type from extension
const isGif = fileName.toLowerCase().endsWith(".gif");
const filters = isGif
? [{ name: "GIF Image", extensions: ["gif"] }]
: [{ name: "MP4 Video", extensions: ["mp4"] }];
? [{ name: mainT("dialogs", "fileDialogs.gifImage"), extensions: ["gif"] }]
: [{ name: mainT("dialogs", "fileDialogs.mp4Video"), extensions: ["mp4"] }];

const result = await dialog.showSaveDialog({
title: isGif ? "Save Exported GIF" : "Save Exported Video",
title: isGif
? mainT("dialogs", "fileDialogs.saveGif")
: mainT("dialogs", "fileDialogs.saveVideo"),
defaultPath: path.join(app.getPath("downloads"), fileName),
filters,
properties: ["createDirectory", "showOverwriteConfirmation"],
Expand Down Expand Up @@ -510,11 +513,14 @@ export function registerIpcHandlers(
ipcMain.handle("open-video-file-picker", async () => {
try {
const result = await dialog.showOpenDialog({
title: "Select Video File",
title: mainT("dialogs", "fileDialogs.selectVideo"),
defaultPath: RECORDINGS_DIR,
filters: [
{ name: "Video Files", extensions: ["webm", "mp4", "mov", "avi", "mkv"] },
{ name: "All Files", extensions: ["*"] },
{
name: mainT("dialogs", "fileDialogs.videoFiles"),
extensions: ["webm", "mp4", "mov", "avi", "mkv"],
},
{ name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] },
],
properties: ["openFile"],
});
Expand Down Expand Up @@ -590,10 +596,13 @@ export function registerIpcHandlers(
: `${safeName}.${PROJECT_FILE_EXTENSION}`;

const result = await dialog.showSaveDialog({
title: "Save OpenScreen Project",
title: mainT("dialogs", "fileDialogs.saveProject"),
defaultPath: path.join(RECORDINGS_DIR, defaultName),
filters: [
{ name: "OpenScreen Project", extensions: [PROJECT_FILE_EXTENSION] },
{
name: mainT("dialogs", "fileDialogs.openscreenProject"),
extensions: [PROJECT_FILE_EXTENSION],
},
{ name: "JSON", extensions: ["json"] },
],
properties: ["createDirectory", "showOverwriteConfirmation"],
Expand Down Expand Up @@ -629,12 +638,15 @@ export function registerIpcHandlers(
ipcMain.handle("load-project-file", async () => {
try {
const result = await dialog.showOpenDialog({
title: "Open OpenScreen Project",
title: mainT("dialogs", "fileDialogs.openProject"),
defaultPath: RECORDINGS_DIR,
filters: [
{ name: "OpenScreen Project", extensions: [PROJECT_FILE_EXTENSION] },
{
name: mainT("dialogs", "fileDialogs.openscreenProject"),
extensions: [PROJECT_FILE_EXTENSION],
},
{ name: "JSON", extensions: ["json"] },
{ name: "All Files", extensions: ["*"] },
{ name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] },
],
properties: ["openFile"],
});
Expand Down
39 changes: 25 additions & 14 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
systemPreferences,
Tray,
} from "electron";
import { mainT, setMainLocale } from "./i18n";
import { registerIpcHandlers } from "./ipc/handlers";
import { createEditorWindow, createHudOverlayWindow, createSourceSelectorWindow } from "./windows";

Expand Down Expand Up @@ -130,28 +131,28 @@ function setupApplicationMenu() {

template.push(
{
label: "File",
label: mainT("common", "actions.file") || "File",
submenu: [
{
label: "Load Project…",
label: mainT("dialogs", "unsavedChanges.loadProject") || "Load Project…",
accelerator: "CmdOrCtrl+O",
click: () => sendEditorMenuAction("menu-load-project"),
},
{
label: "Save Project…",
label: mainT("dialogs", "unsavedChanges.saveProject") || "Save Project…",
accelerator: "CmdOrCtrl+S",
click: () => sendEditorMenuAction("menu-save-project"),
},
{
label: "Save Project As…",
label: mainT("dialogs", "unsavedChanges.saveProjectAs") || "Save Project As…",
accelerator: "CmdOrCtrl+Shift+S",
click: () => sendEditorMenuAction("menu-save-project-as"),
},
...(isMac ? [] : [{ type: "separator" as const }, { role: "quit" as const }]),
],
},
{
label: "Edit",
label: mainT("common", "actions.edit") || "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
Expand All @@ -163,7 +164,7 @@ function setupApplicationMenu() {
],
},
{
label: "View",
label: mainT("common", "actions.view") || "View",
submenu: [
{ role: "reload" },
{ role: "forceReload" },
Expand All @@ -177,7 +178,7 @@ function setupApplicationMenu() {
],
},
{
label: "Window",
label: mainT("common", "actions.window") || "Window",
submenu: isMac
? [{ role: "minimize" }, { role: "zoom" }, { type: "separator" }, { role: "front" }]
: [{ role: "minimize" }, { role: "close" }],
Expand Down Expand Up @@ -215,7 +216,7 @@ function updateTrayMenu(recording: boolean = false) {
const menuTemplate = recording
? [
{
label: "Stop Recording",
label: mainT("common", "actions.stopRecording") || "Stop Recording",
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("stop-recording-from-tray");
Expand All @@ -225,13 +226,13 @@ function updateTrayMenu(recording: boolean = false) {
]
: [
{
label: "Open",
label: mainT("common", "actions.open") || "Open",
click: () => {
showMainWindow();
},
},
{
label: "Quit",
label: mainT("common", "actions.quit") || "Quit",
click: () => {
app.quit();
},
Expand Down Expand Up @@ -281,12 +282,16 @@ function createEditorWindowWrapper() {

const choice = dialog.showMessageBoxSync(mainWindow!, {
type: "warning",
buttons: ["Save & Close", "Discard & Close", "Cancel"],
buttons: [
mainT("dialogs", "unsavedChanges.saveAndClose"),
mainT("dialogs", "unsavedChanges.discardAndClose"),
mainT("common", "actions.cancel"),
],
defaultId: 0,
cancelId: 2,
title: "Unsaved Changes",
message: "You have unsaved changes.",
detail: "Do you want to save your project before closing?",
title: mainT("dialogs", "unsavedChanges.title"),
message: mainT("dialogs", "unsavedChanges.message"),
detail: mainT("dialogs", "unsavedChanges.detail"),
});

const windowToClose = mainWindow;
Expand Down Expand Up @@ -354,6 +359,12 @@ app.whenReady().then(async () => {
ipcMain.on("hud-overlay-close", () => {
app.quit();
});
ipcMain.handle("set-locale", (_, locale: string) => {
setMainLocale(locale);
setupApplicationMenu();
updateTrayMenu();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve tray recording mode when locale changes

The set-locale IPC handler rebuilds the tray menu with updateTrayMenu() but does not pass the current recording state, so it falls back to recording = false. If a user changes language while actively recording (the launch HUD allows this), the tray menu is reset to the idle actions and loses the Stop Recording entry until another recording-state update arrives.

Useful? React with 👍 / 👎.

});

createTray();
updateTrayMenu();
setupApplicationMenu();
Expand Down
3 changes: 3 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
saveShortcuts: (shortcuts: unknown) => {
return ipcRenderer.invoke("save-shortcuts", shortcuts);
},
setLocale: (locale: string) => {
return ipcRenderer.invoke("set-locale", locale);
},
setMicrophoneExpanded: (expanded: boolean) => {
ipcRenderer.send("hud:setMicrophoneExpanded", expanded);
},
Expand Down
12 changes: 6 additions & 6 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ export function createHudOverlayWindow(): BrowserWindow {
const primaryDisplay = screen.getPrimaryDisplay();
const { workArea } = primaryDisplay;

const windowWidth = 500;
const windowHeight = 155;
const windowWidth = 600;
const windowHeight = 160;

const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2);
const y = Math.floor(workArea.y + workArea.height - windowHeight - 5);

const win = new BrowserWindow({
width: windowWidth,
height: windowHeight,
minWidth: 500,
maxWidth: 500,
minHeight: 155,
maxHeight: 155,
minWidth: 600,
maxWidth: 600,
minHeight: 160,
maxHeight: 160,
x: x,
y: y,
frame: false,
Expand Down
Loading
Loading