diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df48987682..59e9c361ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -155,7 +155,7 @@ jobs: run: | VERSION="${{ steps.version.outputs.version }}" ARCH="${{ matrix.arch }}" - DMG_NAME="OpenScreen-Mac-${ARCH}-${VERSION}.dmg" + DMG_NAME="Marquee-Mac-${ARCH}-${VERSION}.dmg" RELEASE_DIR="release/${VERSION}" DMG_OUTPUT="${RELEASE_DIR}/${DMG_NAME}" STAGING="${RELEASE_DIR}/dmg-staging" @@ -168,7 +168,7 @@ jobs: hdiutil create \ -srcfolder "$STAGING" \ - -volname "OpenScreen" \ + -volname "Marquee" \ -fs HFS+ \ -fsargs "-c c=64,a=16,e=16" \ -format UDBZ \ @@ -346,7 +346,7 @@ jobs: PRERELEASE_FLAG: ${{ steps.release.outputs.prerelease_flag }} NOTES_START_TAG: ${{ steps.release.outputs.notes_start_tag }} run: | - TITLE="OpenScreen Creator Edition ${TAG#v}" + TITLE="Marquee ${TAG#v}" mapfile -t FILES < <(find artifacts -type f | sort) if [[ "${#FILES[@]}" -eq 0 ]]; then echo "::error::No installer artifacts were downloaded" diff --git a/AGENTS.md b/AGENTS.md index 5fbe4b9cc4..9000d3bb0e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -OpenScreen is a free, open-source screen recorder and video editor (Electron + React + TypeScript + Pixi.js) maintained as a continuation of the original v1.5.0 release. This file is the canonical guide for any AI coding agent working in this repo. +Marquee (formerly OpenScreen; the repository and the npm package keep the `openscreen` name) is a free, open-source screen recorder and video editor (Electron + React + TypeScript + Pixi.js) maintained as a continuation of the original v1.5.0 release. This file is the canonical guide for any AI coding agent working in this repo. ## Setup commands @@ -68,13 +68,13 @@ Unit/browser tests can't exercise real capture (native screen recording, a physi **Launch the app** - Normal: `npm run dev` — Vite serves the renderer and `vite-plugin-electron` opens the Electron window. The main process logs `Global shortcut registered: CommandOrControl+Shift+O` (shows the HUD) and `Record shortcut registered: ` (starts/stops a capture) when ready. The record shortcut is the first free one of Win+Shift+R, Ctrl+Alt+R, Win+Shift+X, Ctrl+Shift+Space, or the user's own from `app-settings.json`. -- The app is **tray-resident** on Windows/macOS: closing the HUD hides it, and a HUD hidden for 60 s (`OPENSCREEN_HUD_IDLE_RELEASE_MS` overrides) is destroyed and rebuilt on demand from persisted settings. With no window the process is still alive — quit from the tray menu or HUD **⋯ → Quit OpenScreen**. `--hidden` starts straight into the tray (used by Launch at login). +- The app is **tray-resident** on Windows/macOS: closing the HUD hides it, and a HUD hidden for 60 s (`OPENSCREEN_HUD_IDLE_RELEASE_MS` overrides) is destroyed and rebuilt on demand from persisted settings. With no window the process is still alive — quit from the tray menu or HUD **⋯ → Quit Marquee**. `--hidden` starts straight into the tray (used by Launch at login). - The app is single-instance: a lock dir at `%TEMP%/openscreen-single-instance-.lock` (macOS: `$TMPDIR`). If a stale Electron process holds it, a new launch quits silently (exit 0, no window). Kill leftover `electron` processes and delete that lock dir before relaunching. - **From a git worktree** (no `node_modules`/native binaries): junction/symlink `node_modules` from the main checkout (deps are usually identical — check `package-lock.json`), and copy the prebuilt native capture binaries from `electron/native/bin//` (gitignored — rebuilding needs the full VS/Xcode toolchain). Then `npm run dev` works normally. **Granting access** -- `request_access` resolves names against installed apps. A **dev build runs as `electron.exe`** (or `Electron.app`), *not* the installed `Openscreen` — grant **`electron.exe`** or the dev window stays masked in screenshots. Non-allowlisted windows are masked (solid rectangles); the screenshot note lists their process names to add. +- `request_access` resolves names against installed apps. A **dev build runs as `electron.exe`** (or `Electron.app`), *not* the installed `Marquee` — grant **`electron.exe`** or the dev window stays masked in screenshots. Non-allowlisted windows are masked (solid rectangles); the screenshot note lists their process names to add. **The HUD widget** (recording controller) @@ -87,7 +87,7 @@ Unit/browser tests can't exercise real capture (native screen recording, a physi **The tray icon** (bottom-right notification area) - Because the HUD skips the taskbar and can be minimized/hidden, the **system-tray icon is the reliable way to refocus the app**: **left-click or double-click reopens/focuses the HUD** (`showMainWindow`). Its icon swaps to a red dot while recording. -- **Right-click → context menu**: *Record Area…* / *Open* / *Quit* when idle, or ***Stop Recording*** while recording (mirrors the HUD's stop). Tooltip shows `OpenScreen — to record` or `Recording: `. Use this to stop a recording if the HUD isn't reachable. +- **Right-click → context menu**: *Record Area…* / *Open* / *Quit* when idle, or ***Stop Recording*** while recording (mirrors the HUD's stop). Tooltip shows `Marquee — to record` or `Recording: `. Use this to stop a recording if the HUD isn't reachable. **End-to-end flow (record → edit)** diff --git a/docs/decisions.md b/docs/decisions.md index 889405eae8..cb9d5b474f 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -393,6 +393,49 @@ GPU и ~31 с на процессоре. появится модель того же качества заметно меньшего размера или понадобится полностью офлайн-установка (тогда — отдельный пакет с моделью). +## D-029 — Приложение называется Marquee + +Дата: 2026-11-04. + +Контекст: продукт переименован в Marquee (лендинг и логотип — `site/`), а установщики, окна, трей, +уведомления и папки ещё назывались OpenScreen. + +Решение: всё, что видит пользователь, называется Marquee: `productName`, exe, файлы релиза +(`Marquee--Setup.exe`, `Marquee-Mac--.dmg`, `Marquee-Linux-…`), окна, +трей, уведомления, тексты всех 13 локалей, иконки (из `site/brand/app-icon-1024.png`). Прежним +остаётся то, что связывает новую версию с уже установленной и с данными пользователя: + +- `appId` `com.etiennelescot.openscreen`: по нему установщик заменяет установленный OpenScreen, + macOS сохраняет разрешения на запись экрана, Windows — группировку на панели задач, автозапуск + и уведомления. +- Папка данных. Electron называет её по имени приложения в момент первого + `app.getPath("userData")`; у OpenScreen это было имя пакета, `openscreen`. + `electron/appIdentity.ts` подключается в `main.ts` первым, задаёт имя и, если папки `Marquee` + ещё нет, а `openscreen` есть, оставляет приложение в старой: настройки, пресеты, токен Яндекс + Диска, записи и модели речи остаются на месте, ничего не копируется. +- Папка клипов по умолчанию — `Videos\Marquee`, но уже существующая `Videos\OpenScreen` + остаётся папкой по умолчанию. +- Замок одного экземпляра `openscreen-single-instance-.lock`: старый OpenScreen и Marquee + не работают одновременно и не делят горячую клавишу и трей. +- Внутренние имена: расширение проектов `.openscreen` (старые проекты открываются как раньше), + схема `openscreen-asr://`, ключи localStorage, переменные `OPENSCREEN_*`, нативные модули, папка + `OpenScreen Exports` на Яндекс Диске (к ней привязана публичная ссылка), идентичность в Microsoft + Store (имя зарезервировано в Partner Center), имя npm-пакета и репозитория. +- Nix-пакет: в меню и в описаниях модулей он Marquee, но имя пакета и команда `openscreen`, + опция `programs.openscreen` и файл `openscreen.desktop` прежние: на них ссылаются конфигурации, + а Wayland находит пункт меню для окна по имени пакета. На X11 пункт находится по + `startupWMClass`, а класс окна — имя приложения, поэтому там теперь `Marquee`. + +Альтернативы: переносить папку данных при первом запуске — запущенный или возвращённый OpenScreen +остался бы без настроек, а неудачный перенос пришлось бы разбирать; сменить `appId` — рядом со +старым приложением появилось бы второе, а на macOS разрешения пришлось бы выдавать заново. + +Последствия: у обновившихся пользователей папка данных так и называется `openscreen`, у новых — +`Marquee`. На macOS и Linux ключ, которым зашифрован токен Яндекс Диска, хранится под именем +приложения, поэтому токен придётся вставить заново; на Windows ключ лежит в той же папке данных. +Пересмотреть, если понадобится переименовать папку данных или расширение проектов (тогда — с +чтением старых). + ## Как добавлять решения Для каждого нового решения указывать дату, контекст, выбранный вариант, альтернативы, причину, diff --git a/electron-builder.json5 b/electron-builder.json5 index 421484d0ba..872a4b2ec2 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -1,13 +1,15 @@ // @see - https://www.electron.build/configuration/configuration { "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", + // Kept from OpenScreen: upgrades replace the old install, keep its macOS permissions and + // its Windows shortcuts and notifications (decision D-029). "appId": "com.etiennelescot.openscreen", "asar": true, // .node binaries can't be dlopen'd from inside an asar — must live unpacked. "asarUnpack": [ "**/*.node" ], - "productName": "OpenScreen", + "productName": "Marquee", // Copy the ONNX Runtime build transcription runs on into speech-runtime/ (idempotent). "beforePack": "scripts/before-pack.cjs", // Nothing from node_modules ships (see "files"), so there is nothing to rebuild for Electron. @@ -76,10 +78,10 @@ } ], "extendInfo": { - "NSAudioCaptureUsageDescription": "OpenScreen needs audio capture permission to record system audio.", - "NSMicrophoneUsageDescription": "OpenScreen needs microphone access to record voice audio.", - "NSCameraUsageDescription": "OpenScreen needs camera access to record webcam video.", - "NSScreenCaptureUsageDescription": "OpenScreen needs screen recording permission to detect and capture windows.", + "NSAudioCaptureUsageDescription": "Marquee needs audio capture permission to record system audio.", + "NSMicrophoneUsageDescription": "Marquee needs microphone access to record voice audio.", + "NSCameraUsageDescription": "Marquee needs camera access to record webcam video.", + "NSScreenCaptureUsageDescription": "Marquee needs screen recording permission to detect and capture windows.", "NSCameraUseContinuityCameraDeviceType": true } }, @@ -95,12 +97,12 @@ "category": "AudioVideo" }, "win": { - "executableName": "OpenScreen", + "executableName": "Marquee", "target": [ "nsis" ], "icon": "icons/icons/win/icon.ico", - "artifactName": "${productName}-Custom-${version}-Setup.${ext}", + "artifactName": "${productName}-${version}-Setup.${ext}", "extraResources": [ { "from": "electron/native/bin", diff --git a/electron/appIdentity.test.ts b/electron/appIdentity.test.ts new file mode 100644 index 0000000000..405dbf732f --- /dev/null +++ b/electron/appIdentity.test.ts @@ -0,0 +1,64 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const electron = vi.hoisted(() => ({ + appData: "", + customProfile: false, + setName: vi.fn(), + setPath: vi.fn(), +})); + +vi.mock("electron", () => ({ + app: { + setName: electron.setName, + setPath: electron.setPath, + getPath: () => electron.appData, + commandLine: { hasSwitch: () => electron.customProfile }, + }, +})); + +describe("app identity", () => { + let appData: string; + + beforeEach(() => { + appData = fs.mkdtempSync(path.join(os.tmpdir(), "marquee-identity-")); + electron.appData = appData; + electron.customProfile = false; + electron.setName.mockClear(); + electron.setPath.mockClear(); + vi.resetModules(); + }); + + afterEach(() => { + fs.rmSync(appData, { recursive: true, force: true }); + }); + + it("keeps an install upgraded from OpenScreen in its old data folder", async () => { + fs.mkdirSync(path.join(appData, "openscreen")); + await import("./appIdentity"); + expect(electron.setName).toHaveBeenCalledWith("Marquee"); + expect(electron.setPath).toHaveBeenCalledWith("userData", path.join(appData, "openscreen")); + }); + + it("uses its own folder once it has one", async () => { + fs.mkdirSync(path.join(appData, "openscreen")); + fs.mkdirSync(path.join(appData, "Marquee")); + await import("./appIdentity"); + expect(electron.setPath).not.toHaveBeenCalled(); + }); + + it("starts a fresh install in its own folder", async () => { + await import("./appIdentity"); + expect(electron.setName).toHaveBeenCalledWith("Marquee"); + expect(electron.setPath).not.toHaveBeenCalled(); + }); + + it("leaves a profile passed with --user-data-dir alone", async () => { + fs.mkdirSync(path.join(appData, "openscreen")); + electron.customProfile = true; + await import("./appIdentity"); + expect(electron.setPath).not.toHaveBeenCalled(); + }); +}); diff --git a/electron/appIdentity.ts b/electron/appIdentity.ts new file mode 100644 index 0000000000..04cbe24cb9 --- /dev/null +++ b/electron/appIdentity.ts @@ -0,0 +1,22 @@ +import fs from "node:fs"; +import path from "node:path"; +import { app } from "electron"; + +const APP_NAME = "Marquee"; +// OpenScreen's data folder, named after package.json "name". +const LEGACY_USER_DATA_NAME = "openscreen"; + +// Imported first by main.ts: Electron settles the userData folder, and creates it, on the +// first app.getPath("userData") call, so the name and the folder are set before anything asks. +app.setName(APP_NAME); + +// An install upgraded from OpenScreen keeps working in its old data folder: settings, presets, +// the Yandex Disk token, recordings and speech models all live there (decision D-029). A run +// with --user-data-dir (tests, headless checks) brings its own profile. +if (!app.commandLine.hasSwitch("user-data-dir")) { + const appData = app.getPath("appData"); + const legacyDir = path.join(appData, LEGACY_USER_DATA_NAME); + if (!fs.existsSync(path.join(appData, APP_NAME)) && fs.existsSync(legacyDir)) { + app.setPath("userData", legacyDir); + } +} diff --git a/electron/appSettings.test.ts b/electron/appSettings.test.ts new file mode 100644 index 0000000000..e34c418548 --- /dev/null +++ b/electron/appSettings.test.ts @@ -0,0 +1,33 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const videos = vi.hoisted(() => ({ dir: "" })); + +vi.mock("electron", () => ({ app: { getPath: () => videos.dir } })); + +import { AppSettingsStore } from "./appSettings"; + +describe("default clips folder", () => { + beforeEach(() => { + videos.dir = fs.mkdtempSync(path.join(os.tmpdir(), "marquee-videos-")); + }); + + afterEach(() => { + fs.rmSync(videos.dir, { recursive: true, force: true }); + }); + + it("is Videos\\Marquee for a new install", async () => { + const folder = await new AppSettingsStore(videos.dir).resolveSaveFolder(); + expect(folder).toBe(path.join(videos.dir, "Marquee")); + expect(fs.existsSync(folder)).toBe(true); + }); + + it("stays Videos\\OpenScreen when clips were saved there before the rename", async () => { + fs.mkdirSync(path.join(videos.dir, "OpenScreen")); + const folder = await new AppSettingsStore(videos.dir).resolveSaveFolder(); + expect(folder).toBe(path.join(videos.dir, "OpenScreen")); + expect(fs.existsSync(path.join(videos.dir, "Marquee"))).toBe(false); + }); +}); diff --git a/electron/appSettings.ts b/electron/appSettings.ts index 32a86510da..d3e6920b31 100644 --- a/electron/appSettings.ts +++ b/electron/appSettings.ts @@ -8,7 +8,9 @@ import { } from "../src/lib/appSettings"; const SETTINGS_FILE_NAME = "app-settings.json"; -const DEFAULT_SAVE_FOLDER_NAME = "OpenScreen"; +const DEFAULT_SAVE_FOLDER_NAME = "Marquee"; +// Where clips went before the rename; an existing folder stays the default (decision D-029). +const LEGACY_SAVE_FOLDER_NAME = "OpenScreen"; /** Main-process settings persisted next to the other user data files. */ export class AppSettingsStore { @@ -43,8 +45,14 @@ export class AppSettingsStore { return this.get(); } - getDefaultSaveFolder(): string { - return path.join(app.getPath("videos"), DEFAULT_SAVE_FOLDER_NAME); + async getDefaultSaveFolder(): Promise { + const videos = app.getPath("videos"); + const legacy = path.join(videos, LEGACY_SAVE_FOLDER_NAME); + const hasLegacy = await fs.stat(legacy).then( + (stats) => stats.isDirectory(), + () => false, + ); + return hasLegacy ? legacy : path.join(videos, DEFAULT_SAVE_FOLDER_NAME); } /** The folder clips are written to, created on demand. */ @@ -59,7 +67,7 @@ export class AppSettingsStore { } } - const fallback = this.getDefaultSaveFolder(); + const fallback = await this.getDefaultSaveFolder(); await fs.mkdir(fallback, { recursive: true }); return fallback; } diff --git a/electron/integrations/yandexDisk.test.ts b/electron/integrations/yandexDisk.test.ts index 5d6c9d2a68..2d6a70273a 100644 --- a/electron/integrations/yandexDisk.test.ts +++ b/electron/integrations/yandexDisk.test.ts @@ -147,13 +147,13 @@ describe("Yandex Disk uploads", () => { fs.rmSync(workDir, { recursive: true, force: true }); }); - it("refuses files OpenScreen did not create", async () => { + it("refuses files Marquee did not create", async () => { const stray = path.join(clipsDir, "stray.mp4"); fs.writeFileSync(stray, "video"); expect(await upload({ filePath: stray })).toEqual({ success: false, - error: "Only files created by OpenScreen can be uploaded.", + error: "Only files created by Marquee can be uploaded.", }); expect(statuses()).toHaveLength(0); }); diff --git a/electron/integrations/yandexDisk.ts b/electron/integrations/yandexDisk.ts index 829ae8917d..13a6d68c47 100644 --- a/electron/integrations/yandexDisk.ts +++ b/electron/integrations/yandexDisk.ts @@ -383,7 +383,7 @@ class YandexDiskService { this.approvedUploadPaths.has(this.normalizeApprovedPath(filePath)) || this.approvedUploadPaths.has(this.normalizeApprovedPath(resolvedFilePath)); if (!stats.isFile() || !isOpenScreenFile) { - return { success: false, error: "Only files created by OpenScreen can be uploaded." }; + return { success: false, error: "Only files created by Marquee can be uploaded." }; } const settings = await this.readSettings(); @@ -566,7 +566,7 @@ class YandexDiskService { fs.realpath(this.recordingsDirectory), ]); if (!isPathWithinDirectory(resolvedFilePath, resolvedRecordingsDirectory)) { - throw new Error("Refusing to delete a file outside the OpenScreen recordings folder."); + throw new Error("Refusing to delete a file outside the Marquee recordings folder."); } await fs.unlink(resolvedFilePath); this.broadcast({ diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 265cabf491..e8b5eac168 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -1488,7 +1488,7 @@ export function registerIpcHandlers( const detail = access.status === "missing-helper" ? "The cursor helper couldn't be found in this build, so the editable cursor can't be enabled. Rebuild the native helper (npm run build:native:mac) or switch the HUD cursor mode to system." - : "Allow OpenScreen under System Settings → Privacy & Security → Accessibility, then press record again to start the countdown."; + : "Allow Marquee under System Settings → Privacy & Security → Accessibility, then press record again to start the countdown."; const messageOptions = { type: "warning", buttons: ["Open Accessibility Settings", "Cancel"], @@ -1523,7 +1523,7 @@ export function registerIpcHandlers( cancelId: 1, message: "Screen Recording permission is required", detail: - "Allow OpenScreen in macOS System Settings, then come back and choose a screen or window.", + "Allow Marquee in macOS System Settings, then come back and choose a screen or window.", } satisfies Electron.MessageBoxOptions; const result = mainWin && !mainWin.isDestroyed() @@ -2944,7 +2944,7 @@ export function registerIpcHandlers( } // Validate extension and readability if (path.extname(filePath).toLowerCase() !== `.${PROJECT_FILE_EXTENSION}`) { - return { success: false, message: "Not an Openscreen project file" }; + return { success: false, message: "Not a Marquee project file" }; } const stats = await fs.stat(filePath).catch(() => null); if (!stats?.isFile()) { @@ -3095,7 +3095,7 @@ export function registerIpcHandlers( ) => { const { filePath, canceled } = await dialog.showSaveDialog({ title: "Save Diagnostic File", - defaultPath: `openscreen-diagnostic-${Date.now()}.json`, + defaultPath: `marquee-diagnostic-${Date.now()}.json`, filters: [{ name: "JSON", extensions: ["json"] }], }); diff --git a/electron/ipc/quickCapture.ts b/electron/ipc/quickCapture.ts index 4cec2406b1..2ddbee83fd 100644 --- a/electron/ipc/quickCapture.ts +++ b/electron/ipc/quickCapture.ts @@ -219,7 +219,7 @@ export function registerQuickCaptureIpcHandlers( // prepared ahead of time when the card asks for its thumbnail. const dragIcons = new Map(); const fallbackDragIcon = nativeImage - .createFromPath(path.join(process.env.VITE_PUBLIC ?? "", "openscreen.png")) + .createFromPath(path.join(process.env.VITE_PUBLIC ?? "", "app-icon.png")) .resize({ width: 64, height: 64 }); function rememberClip(filePath: string) { @@ -396,7 +396,7 @@ export function registerQuickCaptureIpcHandlers( async (_, sourcePath: string, title: unknown): Promise => { try { if (typeof sourcePath !== "string" || !isPathWithin(sourcePath, deps.recordingsDir)) { - return { success: false, error: "Only OpenScreen recordings can be saved as clips." }; + return { success: false, error: "Only Marquee recordings can be saved as clips." }; } const folder = await deps.settings.resolveSaveFolder(); const finalPath = await resolveUniqueClipPath(folder, title); @@ -556,7 +556,7 @@ export function registerQuickCaptureIpcHandlers( typeof screenVideoPath !== "string" || !isPathWithin(screenVideoPath, deps.recordingsDir) ) { - return { success: false, error: "Not an OpenScreen recording." }; + return { success: false, error: "Not a Marquee recording." }; } const clip = typeof clipPath === "string" ? resolveKnownClip(clipPath) : null; const clipSize = clip diff --git a/electron/main.ts b/electron/main.ts index 94f862ea8d..951639f550 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,3 +1,5 @@ +// First: names the app and picks its data folder before any module asks for userData. +import "./appIdentity"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -54,10 +56,9 @@ import { const __dirname = path.dirname(fileURLToPath(import.meta.url)); -app.setName("OpenScreen"); if (process.platform === "win32") { app.setAppUserModelId("com.etiennelescot.openscreen"); - process.title = "OpenScreen"; + process.title = app.name; } // Use Screen & System Audio Recording permissions instead of the CoreAudio Tap API on macOS. @@ -157,7 +158,7 @@ const isMac = process.platform === "darwin"; const trayIconSize = isMac ? 16 : 24; // Tray Icons -const defaultTrayIcon = getTrayIcon("openscreen.png", trayIconSize); +const defaultTrayIcon = getTrayIcon("app-icon.png", trayIconSize); const recordingTrayIcon = getTrayIcon("rec-button.png", trayIconSize); function attachHudLifecycle(win: BrowserWindow) { @@ -247,7 +248,7 @@ function showTrayHintOnce() { const shortcutLabel = formatAccelerator(activeQuickRecordShortcut, process.platform); new Notification({ - title: "OpenScreen is still running", + title: "Marquee is still running", body: shortcutLabel ? `It waits in the tray. Press ${shortcutLabel} to record, or right-click the tray icon to quit.` : "It waits in the tray. Right-click the tray icon to record or quit.", @@ -358,7 +359,7 @@ function setupApplicationMenu() { submenu: [ { role: "about", - label: mainT("common", "actions.about") || "About OpenScreen", + label: mainT("common", "actions.about") || "About Marquee", }, { type: "separator" }, { @@ -368,7 +369,7 @@ function setupApplicationMenu() { { type: "separator" }, { role: "hide", - label: mainT("common", "actions.hide") || "Hide OpenScreen", + label: mainT("common", "actions.hide") || "Hide Marquee", }, { role: "hideOthers", @@ -529,8 +530,8 @@ function updateTrayMenu(recording: boolean = false) { source: selectedSourceName, }) || `Recording: ${selectedSourceName}` : idleShortcutLabel - ? `OpenScreen — ${idleShortcutLabel} to record` - : "OpenScreen"; + ? `Marquee — ${idleShortcutLabel} to record` + : "Marquee"; const shortcutLabel = formatAccelerator(activeQuickRecordShortcut, process.platform); const shortcutHint = shortcutLabel ? ` (${shortcutLabel})` : ""; const menuTemplate: Electron.MenuItemConstructorOptions[] = recording @@ -829,7 +830,7 @@ appReady?.then(async () => { editorHasUnsavedChanges = false; if (Notification.isSupported() && (error || savedPath)) { const notification = new Notification({ - title: error ? "OpenScreen · Export failed" : "OpenScreen · Video saved", + title: error ? "Marquee · Export failed" : "Marquee · Video saved", body: error ?? savedPath ?? "Export complete", }); if (savedPath && !error) { diff --git a/electron/windows.ts b/electron/windows.ts index 6e6a135785..d727430eab 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -227,7 +227,7 @@ export function createEditorWindow(options: EditorLaunchOptions = {}): BrowserWi resizable: true, alwaysOnTop: false, skipTaskbar: false, - title: "OpenScreen", + title: "Marquee", backgroundColor: "#161618", show: false, // shown via ready-to-show to avoid white flash on first load webPreferences: { diff --git a/flake.nix b/flake.nix index 7b2d328a82..8ad0b79752 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "OpenScreen — desktop screen recorder with built-in editor"; + description = "Marquee — desktop screen recorder with built-in editor"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; @@ -104,7 +104,7 @@ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; shellHook = '' - echo "OpenScreen dev shell — node $(node --version), electron v$(electron --version 2>/dev/null | tr -d 'v')" + echo "Marquee dev shell — node $(node --version), electron v$(electron --version 2>/dev/null | tr -d 'v')" ''; }; } diff --git a/icons/icons/mac/icon.icns b/icons/icons/mac/icon.icns index 02de106cff..c391a9e48f 100644 Binary files a/icons/icons/mac/icon.icns and b/icons/icons/mac/icon.icns differ diff --git a/icons/icons/png/1024x1024.png b/icons/icons/png/1024x1024.png index 447f2d1473..5c8e1aa103 100644 Binary files a/icons/icons/png/1024x1024.png and b/icons/icons/png/1024x1024.png differ diff --git a/icons/icons/png/128x128.png b/icons/icons/png/128x128.png index a9d9acf24e..25e41e7568 100644 Binary files a/icons/icons/png/128x128.png and b/icons/icons/png/128x128.png differ diff --git a/icons/icons/png/16x16.png b/icons/icons/png/16x16.png index 4b8fe811ac..9d85341f56 100644 Binary files a/icons/icons/png/16x16.png and b/icons/icons/png/16x16.png differ diff --git a/icons/icons/png/24x24.png b/icons/icons/png/24x24.png index ebffa992c4..d5eb21afcf 100644 Binary files a/icons/icons/png/24x24.png and b/icons/icons/png/24x24.png differ diff --git a/icons/icons/png/256x256.png b/icons/icons/png/256x256.png index 727d064c60..8bde1f5964 100644 Binary files a/icons/icons/png/256x256.png and b/icons/icons/png/256x256.png differ diff --git a/icons/icons/png/32x32.png b/icons/icons/png/32x32.png index 3f04dd2100..f102a8b28c 100644 Binary files a/icons/icons/png/32x32.png and b/icons/icons/png/32x32.png differ diff --git a/icons/icons/png/48x48.png b/icons/icons/png/48x48.png index 4814b05257..f8fa493926 100644 Binary files a/icons/icons/png/48x48.png and b/icons/icons/png/48x48.png differ diff --git a/icons/icons/png/512x512.png b/icons/icons/png/512x512.png index 7ec2a0b4ca..99ea78e84f 100644 Binary files a/icons/icons/png/512x512.png and b/icons/icons/png/512x512.png differ diff --git a/icons/icons/png/64x64.png b/icons/icons/png/64x64.png index be64b4dbcf..4408d02680 100644 Binary files a/icons/icons/png/64x64.png and b/icons/icons/png/64x64.png differ diff --git a/icons/icons/win/icon.ico b/icons/icons/win/icon.ico index 57df136d6e..4d5de292bc 100644 Binary files a/icons/icons/win/icon.ico and b/icons/icons/win/icon.ico differ diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 9ed0a7d4f5..4a3518815d 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -1,4 +1,4 @@ -# Home Manager module for OpenScreen +# Home Manager module for Marquee # Usage in flake-based Home Manager config: # # inputs.openscreen.url = "github:EtienneLescot/openscreen"; @@ -20,13 +20,13 @@ let in { options.programs.openscreen = { - enable = lib.mkEnableOption "OpenScreen screen recorder"; + enable = lib.mkEnableOption "Marquee screen recorder"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.stdenv.hostPlatform.system}.openscreen; defaultText = lib.literalExpression "inputs.openscreen.packages.\${pkgs.stdenv.hostPlatform.system}.openscreen"; - description = "The OpenScreen package to use."; + description = "The Marquee package to use."; }; }; diff --git a/nix/module.nix b/nix/module.nix index 0cb7242ca7..d45b4832bd 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -1,4 +1,4 @@ -# NixOS module for OpenScreen +# NixOS module for Marquee # Usage in flake-based NixOS config: # # inputs.openscreen.url = "github:EtienneLescot/openscreen"; @@ -20,13 +20,13 @@ let in { options.programs.openscreen = { - enable = lib.mkEnableOption "OpenScreen screen recorder"; + enable = lib.mkEnableOption "Marquee screen recorder"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.stdenv.hostPlatform.system}.openscreen; defaultText = lib.literalExpression "inputs.openscreen.packages.\${pkgs.stdenv.hostPlatform.system}.openscreen"; - description = "The OpenScreen package to use."; + description = "The Marquee package to use."; }; }; diff --git a/nix/package.nix b/nix/package.nix index f30c7c7e41..3249953149 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -98,8 +98,10 @@ buildNpmPackage { desktopItems = [ (makeDesktopItem { + # The file stays openscreen.desktop: on Wayland, windows are matched by the package name. + # On X11 they are matched by startupWMClass, the app name set in electron/appIdentity.ts. name = "openscreen"; - desktopName = "OpenScreen"; + desktopName = "Marquee"; genericName = "Screen Recorder"; exec = "openscreen %U"; icon = "openscreen"; @@ -109,7 +111,7 @@ buildNpmPackage { "Video" "Recorder" ]; - startupWMClass = "Openscreen"; + startupWMClass = "Marquee"; terminal = false; }) ]; diff --git a/package.json b/package.json index f9fdc9b7e5..80a15b91b6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "openscreen", "private": true, "version": "1.9.0", - "description": "OpenScreen — персональный рекордер экрана и видеоредактор", + "description": "Marquee — персональный рекордер экрана и видеоредактор", "type": "module", "packageManager": "npm@10.9.4", "engines": { diff --git a/public/app-icon.png b/public/app-icon.png new file mode 100644 index 0000000000..8bde1f5964 Binary files /dev/null and b/public/app-icon.png differ diff --git a/public/rec-button.png b/public/rec-button.png index 5e6bbc4fbb..547c32e132 100644 Binary files a/public/rec-button.png and b/public/rec-button.png differ diff --git a/scripts/windows-installer.nsh b/scripts/windows-installer.nsh index 3632214e38..2add695815 100644 --- a/scripts/windows-installer.nsh +++ b/scripts/windows-installer.nsh @@ -47,7 +47,7 @@ Function TranscriptionPageCreate ${NSD_Check} $TranscriptionCheckbox ${EndIf} - ${NSD_CreateLabel} 12u 20u -12u 64u "Turns the speech in a recording into a text transcript and .srt subtitles, on this computer and without uploading anything. It takes 26 MB here; the speech model (250 to 560 MB) is downloaded the first time you transcribe.$\r$\n$\r$\nLeave it unchecked to keep OpenScreen smaller. To add it later, run this installer again." + ${NSD_CreateLabel} 12u 20u -12u 64u "Turns the speech in a recording into a text transcript and .srt subtitles, on this computer and without uploading anything. It takes 26 MB here; the speech model (250 to 560 MB) is downloaded the first time you transcribe.$\r$\n$\r$\nLeave it unchecked to keep Marquee smaller. To add it later, run this installer again." Pop $0 nsDialogs::Show diff --git a/site/brand/app-icon-1024.png b/site/brand/app-icon-1024.png new file mode 100644 index 0000000000..a6b7a09f0b Binary files /dev/null and b/site/brand/app-icon-1024.png differ diff --git a/src/App.tsx b/src/App.tsx index f098916cfb..6c8ff3e0ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -185,7 +185,7 @@ export default function App() { return (
-

Openscreen

+

Marquee

); diff --git a/src/components/launch/LaunchWindow.test.tsx b/src/components/launch/LaunchWindow.test.tsx index e458d7f02d..97ac39fd82 100644 --- a/src/components/launch/LaunchWindow.test.tsx +++ b/src/components/launch/LaunchWindow.test.tsx @@ -163,12 +163,12 @@ vi.mock("@/contexts/I18nContext", () => ({ language: "Language", "systemLanguagePrompt.title": "Use your system language?", "systemLanguagePrompt.description": - "We detected English as your system language. Do you want to switch OpenScreen to English?", + "We detected English as your system language. Do you want to switch Marquee to English?", "systemLanguagePrompt.keepDefault": "Keep current language", "systemLanguagePrompt.switch": "Switch to English", "softwareEncoderFallback.title": "Switched to software encoding", "softwareEncoderFallback.description": - "The default GPU encoder failed to start, so OpenScreen fell back to software H.264 encoding. Recording should continue as normal, but CPU usage may be higher.", + "The default GPU encoder failed to start, so Marquee fell back to software H.264 encoding. Recording should continue as normal, but CPU usage may be higher.", "softwareEncoderFallback.dismiss": "Got it", "softwareEncoderFallback.dontShowAgain": "Don't show again", }; diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index b4cb2f2bf1..61d6bdd4ec 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -74,7 +74,7 @@ const STACK_BOTTOM = 24; const MIN_WIDTH = 220; const PREVIEW_CLIP: ReadyClip = { - path: "C:\\Users\\you\\Videos\\OpenScreen\\Checkout flow.mp4", + path: "C:\\Users\\you\\Videos\\Marquee\\Checkout flow.mp4", title: "Checkout flow", sizeBytes: 6_400_000, width: 1280, diff --git a/src/components/launch/SettingsPanel.test.tsx b/src/components/launch/SettingsPanel.test.tsx index 26b54e3e76..3d40cf6029 100644 --- a/src/components/launch/SettingsPanel.test.tsx +++ b/src/components/launch/SettingsPanel.test.tsx @@ -146,7 +146,7 @@ describe("SettingsPanel", () => { fireEvent.click(screen.getByRole("button", { name: "Recording presets" })); fireEvent.click(screen.getByRole("button", { name: "Open Studio" })); fireEvent.click(screen.getByRole("button", { name: "Edit a video file…" })); - fireEvent.click(screen.getByRole("button", { name: "Quit OpenScreen" })); + fireEvent.click(screen.getByRole("button", { name: "Quit Marquee" })); expect(props.onOpenYandexDisk).toHaveBeenCalledTimes(1); expect(props.onOpenPresets).toHaveBeenCalledTimes(1); diff --git a/src/components/launch/SettingsPanel.tsx b/src/components/launch/SettingsPanel.tsx index 16c93ba8f4..5c599cbf15 100644 --- a/src/components/launch/SettingsPanel.tsx +++ b/src/components/launch/SettingsPanel.tsx @@ -216,7 +216,7 @@ export function SettingsPanel({ diff --git a/src/components/launch/SourceSelector.test.tsx b/src/components/launch/SourceSelector.test.tsx index 82172e1cea..b4a69fdcbd 100644 --- a/src/components/launch/SourceSelector.test.tsx +++ b/src/components/launch/SourceSelector.test.tsx @@ -18,10 +18,10 @@ vi.mock("@/contexts/I18nContext", () => ({ if (key === "sourceSelector.loading") return "Loading sources..."; if (key === "sourceSelector.emptyTitle") return "No screens or windows found"; if (key === "sourceSelector.emptyDescription") { - return "If you just granted screen recording permission, reload this picker. On macOS you may need to reopen OpenScreen."; + return "If you just granted screen recording permission, reload this picker. On macOS you may need to reopen Marquee."; } if (key === "sourceSelector.loadFailedDescription") { - return "OpenScreen could not load capture sources. Reload this picker and try again."; + return "Marquee could not load capture sources. Reload this picker and try again."; } if (key === "sourceSelector.screens") return `Screens (${vars?.count ?? "0"})`; if (key === "sourceSelector.windows") return `Windows (${vars?.count ?? "0"})`; diff --git a/src/components/launch/YandexDiskSettingsPanel.tsx b/src/components/launch/YandexDiskSettingsPanel.tsx index 808d04c9da..0c0e43420e 100644 --- a/src/components/launch/YandexDiskSettingsPanel.tsx +++ b/src/components/launch/YandexDiskSettingsPanel.tsx @@ -22,7 +22,7 @@ const COPY = { title: "Yandex Disk", connected: "Connected", disconnected: "Not connected", - description: "Publish finished videos manually to your shared OpenScreen folder.", + description: "Publish finished videos manually to your shared Yandex Disk folder.", folder: "Folder in your Yandex Disk root", openFolder: "Open shared folder", deleteLocal: "Delete the local recording after upload", @@ -42,7 +42,7 @@ const COPY = { title: "Яндекс Диск", connected: "Подключён", disconnected: "Не подключён", - description: "Публикует готовые видео вручную в вашу общую папку OpenScreen.", + description: "Публикует готовые видео вручную в вашу общую папку на Яндекс Диске.", folder: "Папка в корне Яндекс Диска", openFolder: "Открыть общую папку", deleteLocal: "Удалять локальную запись после загрузки", diff --git a/src/components/video-editor/EditorEmptyState.tsx b/src/components/video-editor/EditorEmptyState.tsx index 22ecdd2457..5f55bc199d 100644 --- a/src/components/video-editor/EditorEmptyState.tsx +++ b/src/components/video-editor/EditorEmptyState.tsx @@ -149,7 +149,7 @@ export function EditorEmptyState({
{/* Logo */}