From 885d66c4a48a0e618467a56903f2be9b7f29dc77 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sat, 7 Mar 2026 17:59:41 -0800 Subject: [PATCH] biome linting refactor --- biome.json | 42 +- components.json | 40 +- electron/electron-env.d.ts | 179 +- electron/ipc/handlers.ts | 907 +++--- electron/main.ts | 429 ++- electron/preload.ts | 204 +- electron/windows.ts | 246 +- package-lock.json | 2 +- package.json | 2 +- postcss.config.cjs | 10 +- src/App.css | 84 +- src/App.tsx | 94 +- src/components/launch/LaunchWindow.module.css | 145 +- src/components/launch/LaunchWindow.tsx | 446 +-- .../launch/SourceSelector.module.css | 100 +- src/components/launch/SourceSelector.tsx | 273 +- src/components/ui/accordion.tsx | 86 +- src/components/ui/audio-level-meter.tsx | 74 +- src/components/ui/button.tsx | 89 +- src/components/ui/card.tsx | 115 +- src/components/ui/content-clamp.tsx | 167 +- src/components/ui/dialog.tsx | 180 +- src/components/ui/dropdown-menu.tsx | 317 +- src/components/ui/input.tsx | 47 +- src/components/ui/item-content.tsx | 2 +- src/components/ui/label.tsx | 43 +- src/components/ui/popover.tsx | 90 +- src/components/ui/select.tsx | 255 +- src/components/ui/slider.tsx | 39 +- src/components/ui/sonner.tsx | 36 +- src/components/ui/switch.tsx | 50 +- src/components/ui/tabs.tsx | 82 +- src/components/ui/toggle-group.tsx | 93 +- src/components/ui/toggle.tsx | 70 +- .../video-editor/AddCustomFontDialog.tsx | 362 +-- .../video-editor/AnnotationOverlay.tsx | 398 +-- .../video-editor/AnnotationSettingsPanel.tsx | 1048 ++++--- src/components/video-editor/ArrowSvgs.tsx | 324 +- src/components/video-editor/CropControl.tsx | 402 +-- src/components/video-editor/ExportDialog.tsx | 494 ++- .../video-editor/FormatSelector.tsx | 128 +- .../video-editor/GifOptionsPanel.tsx | 199 +- .../video-editor/KeyboardShortcutsHelp.tsx | 117 +- .../video-editor/PlaybackControls.tsx | 159 +- src/components/video-editor/SettingsPanel.tsx | 1622 +++++----- .../video-editor/ShortcutsConfigDialog.tsx | 411 +-- src/components/video-editor/TutorialHelp.tsx | 262 +- src/components/video-editor/VideoEditor.tsx | 2734 +++++++++-------- src/components/video-editor/VideoPlayback.tsx | 1836 +++++------ src/components/video-editor/index.ts | 10 +- .../video-editor/projectPersistence.ts | 484 +-- src/components/video-editor/timeline/Item.tsx | 292 +- .../timeline/ItemGlass.module.css | 182 +- .../video-editor/timeline/KeyframeMarkers.tsx | 174 +- src/components/video-editor/timeline/Row.tsx | 62 +- .../video-editor/timeline/Subrow.tsx | 19 +- .../video-editor/timeline/TimelineEditor.tsx | 2391 +++++++------- .../video-editor/timeline/TimelineWrapper.tsx | 548 ++-- .../timeline/zoomSuggestionUtils.ts | 106 +- src/components/video-editor/types.ts | 186 +- .../video-editor/videoPlayback/focusUtils.ts | 80 +- .../video-editor/videoPlayback/index.ts | 16 +- .../video-editor/videoPlayback/layoutUtils.ts | 190 +- .../video-editor/videoPlayback/mathUtils.ts | 6 +- .../videoPlayback/overlayUtils.ts | 101 +- .../videoPlayback/videoEventHandlers.ts | 254 +- .../videoPlayback/zoomRegionUtils.ts | 38 +- .../videoPlayback/zoomTransform.ts | 98 +- src/contexts/ShortcutsContext.tsx | 101 +- src/hooks/useAudioLevelMeter.ts | 210 +- src/hooks/useMicrophoneDevices.ts | 161 +- src/hooks/useScreenRecorder.ts | 585 ++-- src/index.css | 283 +- src/lib/assetPath.ts | 45 +- src/lib/customFonts.ts | 359 +-- src/lib/exporter/annotationRenderer.ts | 591 ++-- src/lib/exporter/audioEncoder.ts | 342 +-- src/lib/exporter/frameRenderer.ts | 975 +++--- src/lib/exporter/gifExporter.ts | 443 +-- src/lib/exporter/index.ts | 49 +- src/lib/exporter/muxer.ts | 156 +- src/lib/exporter/streamingDecoder.ts | 715 ++--- src/lib/exporter/types.ts | 76 +- src/lib/exporter/videoDecoder.ts | 90 +- src/lib/exporter/videoExporter.ts | 603 ++-- src/lib/shortcuts.ts | 174 +- src/lib/utils.ts | 6 +- src/main.tsx | 20 +- src/utils/aspectRatioUtils.ts | 58 +- src/utils/platformUtils.ts | 58 +- src/vite-env.d.ts | 180 +- tailwind.config.cjs | 141 +- tsconfig.json | 60 +- tsconfig.node.json | 22 +- vite.config.ts | 114 +- vitest.config.ts | 26 +- 96 files changed, 14041 insertions(+), 13373 deletions(-) diff --git a/biome.json b/biome.json index 33c079a..c4c22f6 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, - "files": { "ignoreUnknown": false }, + "files": { "ignoreUnknown": false, "includes": ["**", "!**/*.css"] }, "formatter": { "enabled": true, "indentStyle": "tab", @@ -51,10 +51,10 @@ "useYield": "error" }, "style": { - "noNamespace": "error", + "noNamespace": "off", "useArrayLiterals": "error", "useAsConstAssertion": "error", - "useComponentExportOnlyModules": "warn" + "useComponentExportOnlyModules": "off" }, "suspicious": { "noAssignInExpressions": "error", @@ -69,8 +69,8 @@ "noDuplicateElseIf": "error", "noDuplicateObjectKeys": "error", "noDuplicateParameters": "error", - "noEmptyBlockStatements": "error", - "noExplicitAny": "error", + "noEmptyBlockStatements": "warn", + "noExplicitAny": "warn", "noExtraNonNullAssertion": "error", "noFallthroughSwitchClause": "error", "noFunctionAssign": "error", @@ -92,40 +92,10 @@ "useGetterReturn": "error" } }, - "includes": ["**", "**/dist", "**/.eslintrc.cjs", "**", "**/dist", "**/.eslintrc.cjs"] + "includes": ["**", "**/dist", "**/.eslintrc.cjs", "!**/*.css"] }, "javascript": { "formatter": { "quoteStyle": "double" } }, "overrides": [ - { - "includes": ["*.ts", "*.tsx", "*.mts", "*.cts"], - "linter": { - "rules": { - "complexity": { "noArguments": "error" }, - "correctness": { - "noConstAssign": "off", - "noGlobalObjectCalls": "off", - "noInvalidBuiltinInstantiation": "off", - "noInvalidConstructorSuper": "off", - "noSetterReturn": "off", - "noUndeclaredVariables": "off", - "noUnreachable": "off", - "noUnreachableSuper": "off" - }, - "style": { "useConst": "error" }, - "suspicious": { - "noDuplicateClassMembers": "off", - "noDuplicateObjectKeys": "off", - "noDuplicateParameters": "off", - "noFunctionAssign": "off", - "noImportAssign": "off", - "noRedeclare": "off", - "noUnsafeNegation": "off", - "noVar": "error", - "useGetterReturn": "off" - } - } - } - }, { "includes": ["*.ts", "*.tsx", "*.mts", "*.cts"], "linter": { diff --git a/components.json b/components.json index b4f4395..f6dc1d5 100644 --- a/components.json +++ b/components.json @@ -1,22 +1,22 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.cjs", - "css": "src/index.css", - "baseColor": "stone", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "registries": {} + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.cjs", + "css": "src/index.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} } diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 1f6885c..938cca0 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -1,71 +1,108 @@ -/// - -declare namespace NodeJS { - interface ProcessEnv { - /** - * The built directory structure - * - * ```tree - * ├─┬─┬ dist - * │ │ └── index.html - * │ │ - * │ ├─┬ dist-electron - * │ │ ├── main.js - * │ │ └── preload.js - * │ - * ``` - */ - APP_ROOT: string - /** /dist/ or /public/ */ - VITE_PUBLIC: string - } -} - -// Used in Renderer process, expose in `preload.ts` -interface Window { - electronAPI: { - getSources: (opts: Electron.SourcesOptions) => Promise - switchToEditor: () => Promise - openSourceSelector: () => Promise - selectSource: (source: any) => Promise - getSelectedSource: () => Promise - storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }> - getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }> - setRecordingState: (recording: boolean) => Promise - getCursorTelemetry: (videoPath?: string) => Promise<{ success: boolean; samples: CursorTelemetryPoint[]; message?: string; error?: string }> - onStopRecordingFromTray: (callback: () => void) => () => void - openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }> - saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }> - openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }> - setCurrentVideoPath: (path: string) => Promise<{ success: boolean }> - getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }> - clearCurrentVideoPath: () => Promise<{ success: boolean }> - saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean; error?: string }> - loadProjectFile: () => Promise<{ success: boolean; path?: string; project?: unknown; message?: string; canceled?: boolean; error?: string }> - loadCurrentProjectFile: () => Promise<{ success: boolean; path?: string; project?: unknown; message?: string; canceled?: boolean; error?: string }> - onMenuLoadProject: (callback: () => void) => () => void - onMenuSaveProject: (callback: () => void) => () => void - onMenuSaveProjectAs: (callback: () => void) => () => void - getPlatform: () => Promise - revealInFolder: (filePath: string) => Promise<{ success: boolean; error?: string; message?: string }>, - getShortcuts: () => Promise | null> - saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }> - hudOverlayHide: () => void; - hudOverlayClose: () => void; - setMicrophoneExpanded: (expanded: boolean) => void; - } -} - -interface ProcessedDesktopSource { - id: string - name: string - display_id: string - thumbnail: string | null - appIcon: string | null -} - -interface CursorTelemetryPoint { - timeMs: number - cx: number - cy: number -} +/// + +declare namespace NodeJS { + interface ProcessEnv { + /** + * The built directory structure + * + * ```tree + * ├─┬─┬ dist + * │ │ └── index.html + * │ │ + * │ ├─┬ dist-electron + * │ │ ├── main.js + * │ │ └── preload.js + * │ + * ``` + */ + APP_ROOT: string; + /** /dist/ or /public/ */ + VITE_PUBLIC: string; + } +} + +// Used in Renderer process, expose in `preload.ts` +interface Window { + electronAPI: { + getSources: (opts: Electron.SourcesOptions) => Promise; + switchToEditor: () => Promise; + openSourceSelector: () => Promise; + selectSource: (source: any) => Promise; + getSelectedSource: () => Promise; + storeRecordedVideo: ( + videoData: ArrayBuffer, + fileName: string, + ) => Promise<{ success: boolean; path?: string; message?: string }>; + getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>; + setRecordingState: (recording: boolean) => Promise; + getCursorTelemetry: (videoPath?: string) => Promise<{ + success: boolean; + samples: CursorTelemetryPoint[]; + message?: string; + error?: string; + }>; + onStopRecordingFromTray: (callback: () => void) => () => void; + openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>; + saveExportedVideo: ( + videoData: ArrayBuffer, + fileName: string, + ) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }>; + openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>; + setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>; + getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>; + clearCurrentVideoPath: () => Promise<{ success: boolean }>; + saveProjectFile: ( + projectData: unknown, + suggestedName?: string, + existingProjectPath?: string, + ) => Promise<{ + success: boolean; + path?: string; + message?: string; + canceled?: boolean; + error?: string; + }>; + loadProjectFile: () => Promise<{ + success: boolean; + path?: string; + project?: unknown; + message?: string; + canceled?: boolean; + error?: string; + }>; + loadCurrentProjectFile: () => Promise<{ + success: boolean; + path?: string; + project?: unknown; + message?: string; + canceled?: boolean; + error?: string; + }>; + onMenuLoadProject: (callback: () => void) => () => void; + onMenuSaveProject: (callback: () => void) => () => void; + onMenuSaveProjectAs: (callback: () => void) => () => void; + getPlatform: () => Promise; + revealInFolder: ( + filePath: string, + ) => Promise<{ success: boolean; error?: string; message?: string }>; + getShortcuts: () => Promise | null>; + saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>; + hudOverlayHide: () => void; + hudOverlayClose: () => void; + setMicrophoneExpanded: (expanded: boolean) => void; + }; +} + +interface ProcessedDesktopSource { + id: string; + name: string; + display_id: string; + thumbnail: string | null; + appIcon: string | null; +} + +interface CursorTelemetryPoint { + timeMs: number; + cx: number; + cy: number; +} diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 307d956..5f3727d 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -1,520 +1,541 @@ -import { ipcMain, desktopCapturer, BrowserWindow, shell, app, dialog, screen } from 'electron' +import fs from "node:fs/promises"; +import path from "node:path"; +import { app, BrowserWindow, desktopCapturer, dialog, ipcMain, screen, shell } from "electron"; +import { RECORDINGS_DIR } from "../main"; -import fs from 'node:fs/promises' -import path from 'node:path' -import { RECORDINGS_DIR } from '../main' - -const PROJECT_FILE_EXTENSION = 'openscreen' -const SHORTCUTS_FILE = path.join(app.getPath('userData'), 'shortcuts.json') +const PROJECT_FILE_EXTENSION = "openscreen"; +const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json"); type SelectedSource = { - name: string - [key: string]: unknown -} + name: string; + [key: string]: unknown; +}; -let selectedSource: SelectedSource | null = null -let currentVideoPath: string | null = null -let currentProjectPath: string | null = null +let selectedSource: SelectedSource | null = null; +let currentVideoPath: string | null = null; +let currentProjectPath: string | null = null; function normalizePath(filePath: string) { - return path.resolve(filePath) + return path.resolve(filePath); } function isTrustedProjectPath(filePath?: string | null) { - if (!filePath || !currentProjectPath) { - return false - } - return normalizePath(filePath) === normalizePath(currentProjectPath) + if (!filePath || !currentProjectPath) { + return false; + } + return normalizePath(filePath) === normalizePath(currentProjectPath); } -const CURSOR_TELEMETRY_VERSION = 1 -const CURSOR_SAMPLE_INTERVAL_MS = 100 -const MAX_CURSOR_SAMPLES = 60 * 60 * 10 // 1 hour @ 10Hz +const CURSOR_TELEMETRY_VERSION = 1; +const CURSOR_SAMPLE_INTERVAL_MS = 100; +const MAX_CURSOR_SAMPLES = 60 * 60 * 10; // 1 hour @ 10Hz interface CursorTelemetryPoint { - timeMs: number - cx: number - cy: number + timeMs: number; + cx: number; + cy: number; } -let cursorCaptureInterval: NodeJS.Timeout | null = null -let cursorCaptureStartTimeMs = 0 -let activeCursorSamples: CursorTelemetryPoint[] = [] -let pendingCursorSamples: CursorTelemetryPoint[] = [] +let cursorCaptureInterval: NodeJS.Timeout | null = null; +let cursorCaptureStartTimeMs = 0; +let activeCursorSamples: CursorTelemetryPoint[] = []; +let pendingCursorSamples: CursorTelemetryPoint[] = []; function clamp(value: number, min: number, max: number) { - return Math.min(max, Math.max(min, value)) + return Math.min(max, Math.max(min, value)); } function stopCursorCapture() { - if (cursorCaptureInterval) { - clearInterval(cursorCaptureInterval) - cursorCaptureInterval = null - } + if (cursorCaptureInterval) { + clearInterval(cursorCaptureInterval); + cursorCaptureInterval = null; + } } function sampleCursorPoint() { - const cursor = screen.getCursorScreenPoint() - const sourceDisplayId = Number(selectedSource?.display_id) - const sourceDisplay = Number.isFinite(sourceDisplayId) - ? screen.getAllDisplays().find((display) => display.id === sourceDisplayId) ?? null - : null - const display = sourceDisplay ?? screen.getDisplayNearestPoint(cursor) - const bounds = display.bounds - const width = Math.max(1, bounds.width) - const height = Math.max(1, bounds.height) + const cursor = screen.getCursorScreenPoint(); + const sourceDisplayId = Number(selectedSource?.display_id); + const sourceDisplay = Number.isFinite(sourceDisplayId) + ? (screen.getAllDisplays().find((display) => display.id === sourceDisplayId) ?? null) + : null; + const display = sourceDisplay ?? screen.getDisplayNearestPoint(cursor); + const bounds = display.bounds; + const width = Math.max(1, bounds.width); + const height = Math.max(1, bounds.height); - const cx = clamp((cursor.x - bounds.x) / width, 0, 1) - const cy = clamp((cursor.y - bounds.y) / height, 0, 1) + const cx = clamp((cursor.x - bounds.x) / width, 0, 1); + const cy = clamp((cursor.y - bounds.y) / height, 0, 1); - activeCursorSamples.push({ - timeMs: Math.max(0, Date.now() - cursorCaptureStartTimeMs), - cx, - cy, - }) + activeCursorSamples.push({ + timeMs: Math.max(0, Date.now() - cursorCaptureStartTimeMs), + cx, + cy, + }); - if (activeCursorSamples.length > MAX_CURSOR_SAMPLES) { - activeCursorSamples.shift() - } + if (activeCursorSamples.length > MAX_CURSOR_SAMPLES) { + activeCursorSamples.shift(); + } } export function registerIpcHandlers( - createEditorWindow: () => void, - createSourceSelectorWindow: () => BrowserWindow, - getMainWindow: () => BrowserWindow | null, - getSourceSelectorWindow: () => BrowserWindow | null, - onRecordingStateChange?: (recording: boolean, sourceName: string) => void + createEditorWindow: () => void, + createSourceSelectorWindow: () => BrowserWindow, + getMainWindow: () => BrowserWindow | null, + getSourceSelectorWindow: () => BrowserWindow | null, + onRecordingStateChange?: (recording: boolean, sourceName: string) => void, ) { - ipcMain.handle('get-sources', async (_, opts) => { - const sources = await desktopCapturer.getSources(opts) - return sources.map(source => ({ - id: source.id, - name: source.name, - display_id: source.display_id, - thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, - appIcon: source.appIcon ? source.appIcon.toDataURL() : null - })) - }) + ipcMain.handle("get-sources", async (_, opts) => { + const sources = await desktopCapturer.getSources(opts); + return sources.map((source) => ({ + id: source.id, + name: source.name, + display_id: source.display_id, + thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, + appIcon: source.appIcon ? source.appIcon.toDataURL() : null, + })); + }); - ipcMain.handle('select-source', (_, source: SelectedSource) => { - selectedSource = source - const sourceSelectorWin = getSourceSelectorWindow() - if (sourceSelectorWin) { - sourceSelectorWin.close() - } - return selectedSource - }) + ipcMain.handle("select-source", (_, source: SelectedSource) => { + selectedSource = source; + const sourceSelectorWin = getSourceSelectorWindow(); + if (sourceSelectorWin) { + sourceSelectorWin.close(); + } + return selectedSource; + }); - ipcMain.handle('get-selected-source', () => { - return selectedSource - }) + ipcMain.handle("get-selected-source", () => { + return selectedSource; + }); - ipcMain.handle('open-source-selector', () => { - const sourceSelectorWin = getSourceSelectorWindow() - if (sourceSelectorWin) { - sourceSelectorWin.focus() - return - } - createSourceSelectorWindow() - }) + ipcMain.handle("open-source-selector", () => { + const sourceSelectorWin = getSourceSelectorWindow(); + if (sourceSelectorWin) { + sourceSelectorWin.focus(); + return; + } + createSourceSelectorWindow(); + }); - ipcMain.handle('switch-to-editor', () => { - const mainWin = getMainWindow() - if (mainWin) { - mainWin.close() - } - createEditorWindow() - }) + ipcMain.handle("switch-to-editor", () => { + const mainWin = getMainWindow(); + if (mainWin) { + mainWin.close(); + } + createEditorWindow(); + }); + ipcMain.handle("store-recorded-video", async (_, videoData: ArrayBuffer, fileName: string) => { + try { + const videoPath = path.join(RECORDINGS_DIR, fileName); + await fs.writeFile(videoPath, Buffer.from(videoData)); + currentVideoPath = videoPath; + currentProjectPath = null; + const telemetryPath = `${videoPath}.cursor.json`; + if (pendingCursorSamples.length > 0) { + await fs.writeFile( + telemetryPath, + JSON.stringify( + { version: CURSOR_TELEMETRY_VERSION, samples: pendingCursorSamples }, + null, + 2, + ), + "utf-8", + ); + } + pendingCursorSamples = []; - ipcMain.handle('store-recorded-video', async (_, videoData: ArrayBuffer, fileName: string) => { - try { - const videoPath = path.join(RECORDINGS_DIR, fileName) - await fs.writeFile(videoPath, Buffer.from(videoData)) - currentVideoPath = videoPath; - currentProjectPath = null + return { + success: true, + path: videoPath, + message: "Video stored successfully", + }; + } catch (error) { + console.error("Failed to store video:", error); + return { + success: false, + message: "Failed to store video", + error: String(error), + }; + } + }); - const telemetryPath = `${videoPath}.cursor.json` - if (pendingCursorSamples.length > 0) { - await fs.writeFile( - telemetryPath, - JSON.stringify({ version: CURSOR_TELEMETRY_VERSION, samples: pendingCursorSamples }, null, 2), - 'utf-8' - ) - } - pendingCursorSamples = [] + ipcMain.handle("get-recorded-video-path", async () => { + try { + const files = await fs.readdir(RECORDINGS_DIR); + const videoFiles = files.filter((file) => file.endsWith(".webm")); - return { - success: true, - path: videoPath, - message: 'Video stored successfully' - } - } catch (error) { - console.error('Failed to store video:', error) - return { - success: false, - message: 'Failed to store video', - error: String(error) - } - } - }) + if (videoFiles.length === 0) { + return { success: false, message: "No recorded video found" }; + } + const latestVideo = videoFiles.sort().reverse()[0]; + const videoPath = path.join(RECORDINGS_DIR, latestVideo); + return { success: true, path: videoPath }; + } catch (error) { + console.error("Failed to get video path:", error); + return { success: false, message: "Failed to get video path", error: String(error) }; + } + }); - ipcMain.handle('get-recorded-video-path', async () => { - try { - const files = await fs.readdir(RECORDINGS_DIR) - const videoFiles = files.filter(file => file.endsWith('.webm')) - - if (videoFiles.length === 0) { - return { success: false, message: 'No recorded video found' } - } - - const latestVideo = videoFiles.sort().reverse()[0] - const videoPath = path.join(RECORDINGS_DIR, latestVideo) - - return { success: true, path: videoPath } - } catch (error) { - console.error('Failed to get video path:', error) - return { success: false, message: 'Failed to get video path', error: String(error) } - } - }) + ipcMain.handle("set-recording-state", (_, recording: boolean) => { + if (recording) { + stopCursorCapture(); + activeCursorSamples = []; + pendingCursorSamples = []; + cursorCaptureStartTimeMs = Date.now(); + sampleCursorPoint(); + cursorCaptureInterval = setInterval(sampleCursorPoint, CURSOR_SAMPLE_INTERVAL_MS); + } else { + stopCursorCapture(); + pendingCursorSamples = [...activeCursorSamples]; + activeCursorSamples = []; + } - ipcMain.handle('set-recording-state', (_, recording: boolean) => { - if (recording) { - stopCursorCapture() - activeCursorSamples = [] - pendingCursorSamples = [] - cursorCaptureStartTimeMs = Date.now() - sampleCursorPoint() - cursorCaptureInterval = setInterval(sampleCursorPoint, CURSOR_SAMPLE_INTERVAL_MS) - } else { - stopCursorCapture() - pendingCursorSamples = [...activeCursorSamples] - activeCursorSamples = [] - } + const source = selectedSource || { name: "Screen" }; + if (onRecordingStateChange) { + onRecordingStateChange(recording, source.name); + } + }); - const source = selectedSource || { name: 'Screen' } - if (onRecordingStateChange) { - onRecordingStateChange(recording, source.name) - } - }) + ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => { + const targetVideoPath = videoPath ?? currentVideoPath; + if (!targetVideoPath) { + return { success: true, samples: [] }; + } - ipcMain.handle('get-cursor-telemetry', async (_, videoPath?: string) => { - const targetVideoPath = videoPath ?? currentVideoPath - if (!targetVideoPath) { - return { success: true, samples: [] } - } + const telemetryPath = `${targetVideoPath}.cursor.json`; + try { + const content = await fs.readFile(telemetryPath, "utf-8"); + const parsed = JSON.parse(content); + const rawSamples = Array.isArray(parsed) + ? parsed + : Array.isArray(parsed?.samples) + ? parsed.samples + : []; - const telemetryPath = `${targetVideoPath}.cursor.json` - try { - const content = await fs.readFile(telemetryPath, 'utf-8') - const parsed = JSON.parse(content) - const rawSamples = Array.isArray(parsed) - ? parsed - : (Array.isArray(parsed?.samples) ? parsed.samples : []) + const samples: CursorTelemetryPoint[] = rawSamples + .filter((sample: unknown) => Boolean(sample && typeof sample === "object")) + .map((sample: unknown) => { + const point = sample as Partial; + return { + timeMs: + typeof point.timeMs === "number" && Number.isFinite(point.timeMs) + ? Math.max(0, point.timeMs) + : 0, + cx: + typeof point.cx === "number" && Number.isFinite(point.cx) + ? clamp(point.cx, 0, 1) + : 0.5, + cy: + typeof point.cy === "number" && Number.isFinite(point.cy) + ? clamp(point.cy, 0, 1) + : 0.5, + }; + }) + .sort((a: CursorTelemetryPoint, b: CursorTelemetryPoint) => a.timeMs - b.timeMs); - const samples: CursorTelemetryPoint[] = rawSamples - .filter((sample: unknown) => Boolean(sample && typeof sample === 'object')) - .map((sample: unknown) => { - const point = sample as Partial - return { - timeMs: typeof point.timeMs === 'number' && Number.isFinite(point.timeMs) ? Math.max(0, point.timeMs) : 0, - cx: typeof point.cx === 'number' && Number.isFinite(point.cx) ? clamp(point.cx, 0, 1) : 0.5, - cy: typeof point.cy === 'number' && Number.isFinite(point.cy) ? clamp(point.cy, 0, 1) : 0.5, - } - }) - .sort((a: CursorTelemetryPoint, b: CursorTelemetryPoint) => a.timeMs - b.timeMs) + return { success: true, samples }; + } catch (error) { + const nodeError = error as NodeJS.ErrnoException; + if (nodeError.code === "ENOENT") { + return { success: true, samples: [] }; + } + console.error("Failed to load cursor telemetry:", error); + return { + success: false, + message: "Failed to load cursor telemetry", + error: String(error), + samples: [], + }; + } + }); - return { success: true, samples } - } catch (error) { - const nodeError = error as NodeJS.ErrnoException - if (nodeError.code === 'ENOENT') { - return { success: true, samples: [] } - } - console.error('Failed to load cursor telemetry:', error) - return { success: false, message: 'Failed to load cursor telemetry', error: String(error), samples: [] } - } - }) + ipcMain.handle("open-external-url", async (_, url: string) => { + try { + await shell.openExternal(url); + return { success: true }; + } catch (error) { + console.error("Failed to open URL:", error); + return { success: false, error: String(error) }; + } + }); + // Return base path for assets so renderer can resolve file:// paths in production + ipcMain.handle("get-asset-base-path", () => { + try { + if (app.isPackaged) { + return path.join(process.resourcesPath, "assets"); + } + return path.join(app.getAppPath(), "public", "assets"); + } catch (err) { + console.error("Failed to resolve asset base path:", err); + return null; + } + }); - ipcMain.handle('open-external-url', async (_, url: string) => { - try { - await shell.openExternal(url) - return { success: true } - } catch (error) { - console.error('Failed to open URL:', error) - return { success: false, error: String(error) } - } - }) + ipcMain.handle("save-exported-video", async (_, videoData: ArrayBuffer, fileName: string) => { + try { + // Determine file type from extension + const isGif = fileName.toLowerCase().endsWith(".gif"); + const filters = isGif + ? [{ name: "GIF Image", extensions: ["gif"] }] + : [{ name: "MP4 Video", extensions: ["mp4"] }]; - // Return base path for assets so renderer can resolve file:// paths in production - ipcMain.handle('get-asset-base-path', () => { - try { - if (app.isPackaged) { - return path.join(process.resourcesPath, 'assets') - } - return path.join(app.getAppPath(), 'public', 'assets') - } catch (err) { - console.error('Failed to resolve asset base path:', err) - return null - } - }) + const result = await dialog.showSaveDialog({ + title: isGif ? "Save Exported GIF" : "Save Exported Video", + defaultPath: path.join(app.getPath("downloads"), fileName), + filters, + properties: ["createDirectory", "showOverwriteConfirmation"], + }); - ipcMain.handle('save-exported-video', async (_, videoData: ArrayBuffer, fileName: string) => { - try { - // Determine file type from extension - const isGif = fileName.toLowerCase().endsWith('.gif'); - const filters = isGif - ? [{ name: 'GIF Image', extensions: ['gif'] }] - : [{ name: 'MP4 Video', extensions: ['mp4'] }]; + if (result.canceled || !result.filePath) { + return { + success: false, + canceled: true, + message: "Export canceled", + }; + } - const result = await dialog.showSaveDialog({ - title: isGif ? 'Save Exported GIF' : 'Save Exported Video', - defaultPath: path.join(app.getPath('downloads'), fileName), - filters, - properties: ['createDirectory', 'showOverwriteConfirmation'] - }); + await fs.writeFile(result.filePath, Buffer.from(videoData)); - if (result.canceled || !result.filePath) { - return { - success: false, - canceled: true, - message: 'Export canceled' - }; - } + return { + success: true, + path: result.filePath, + message: "Video exported successfully", + }; + } catch (error) { + console.error("Failed to save exported video:", error); + return { + success: false, + message: "Failed to save exported video", + error: String(error), + }; + } + }); - await fs.writeFile(result.filePath, Buffer.from(videoData)); + ipcMain.handle("open-video-file-picker", async () => { + try { + const result = await dialog.showOpenDialog({ + title: "Select Video File", + defaultPath: RECORDINGS_DIR, + filters: [ + { name: "Video Files", extensions: ["webm", "mp4", "mov", "avi", "mkv"] }, + { name: "All Files", extensions: ["*"] }, + ], + properties: ["openFile"], + }); - return { - success: true, - path: result.filePath, - message: 'Video exported successfully' - }; - } catch (error) { - console.error('Failed to save exported video:', error) - return { - success: false, - message: 'Failed to save exported video', - error: String(error) - } - } - }) + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true }; + } - ipcMain.handle('open-video-file-picker', async () => { - try { - const result = await dialog.showOpenDialog({ - title: 'Select Video File', - defaultPath: RECORDINGS_DIR, - filters: [ - { name: 'Video Files', extensions: ['webm', 'mp4', 'mov', 'avi', 'mkv'] }, - { name: 'All Files', extensions: ['*'] } - ], - properties: ['openFile'] - }); + currentProjectPath = null; + return { + success: true, + path: result.filePaths[0], + }; + } catch (error) { + console.error("Failed to open file picker:", error); + return { + success: false, + message: "Failed to open file picker", + error: String(error), + }; + } + }); - if (result.canceled || result.filePaths.length === 0) { - return { success: false, canceled: true }; - } + ipcMain.handle("reveal-in-folder", async (_, filePath: string) => { + try { + // shell.showItemInFolder doesn't return a value, it throws on error + shell.showItemInFolder(filePath); + return { success: true }; + } catch (error) { + console.error(`Error revealing item in folder: ${filePath}`, error); + // Fallback to open the directory if revealing the item fails + // This might happen if the file was moved or deleted after export, + // or if the path is somehow invalid for showItemInFolder + try { + const openPathResult = await shell.openPath(path.dirname(filePath)); + if (openPathResult) { + // openPath returned an error message + return { success: false, error: openPathResult }; + } + return { success: true, message: "Could not reveal item, but opened directory." }; + } catch (openError) { + console.error(`Error opening directory: ${path.dirname(filePath)}`, openError); + return { success: false, error: String(error) }; + } + } + }); - currentProjectPath = null - return { - success: true, - path: result.filePaths[0] - }; - } catch (error) { - console.error('Failed to open file picker:', error); - return { - success: false, - message: 'Failed to open file picker', - error: String(error) - }; - } - }); + let currentVideoPath: string | null = null; + ipcMain.handle( + "save-project-file", + async (_, projectData: unknown, suggestedName?: string, existingProjectPath?: string) => { + try { + const trustedExistingProjectPath = isTrustedProjectPath(existingProjectPath) + ? existingProjectPath + : null; - ipcMain.handle('reveal-in-folder', async (_, filePath: string) => { - try { - // shell.showItemInFolder doesn't return a value, it throws on error - shell.showItemInFolder(filePath); - return { success: true }; - } catch (error) { - console.error(`Error revealing item in folder: ${filePath}`, error); - // Fallback to open the directory if revealing the item fails - // This might happen if the file was moved or deleted after export, - // or if the path is somehow invalid for showItemInFolder - try { - const openPathResult = await shell.openPath(path.dirname(filePath)); - if (openPathResult) { - // openPath returned an error message - return { success: false, error: openPathResult }; - } - return { success: true, message: 'Could not reveal item, but opened directory.' }; - } catch (openError) { - console.error(`Error opening directory: ${path.dirname(filePath)}`, openError); - return { success: false, error: String(error) }; - } - } - }); + if (trustedExistingProjectPath) { + await fs.writeFile( + trustedExistingProjectPath, + JSON.stringify(projectData, null, 2), + "utf-8", + ); + currentProjectPath = trustedExistingProjectPath; + return { + success: true, + path: trustedExistingProjectPath, + message: "Project saved successfully", + }; + } - let currentVideoPath: string | null = null; - ipcMain.handle('save-project-file', async (_, projectData: unknown, suggestedName?: string, existingProjectPath?: string) => { - try { - const trustedExistingProjectPath = isTrustedProjectPath(existingProjectPath) - ? existingProjectPath - : null + const safeName = (suggestedName || `project-${Date.now()}`).replace(/[^a-zA-Z0-9-_]/g, "_"); + const defaultName = safeName.endsWith(`.${PROJECT_FILE_EXTENSION}`) + ? safeName + : `${safeName}.${PROJECT_FILE_EXTENSION}`; - if (trustedExistingProjectPath) { - await fs.writeFile(trustedExistingProjectPath, JSON.stringify(projectData, null, 2), 'utf-8') - currentProjectPath = trustedExistingProjectPath - return { - success: true, - path: trustedExistingProjectPath, - message: 'Project saved successfully' - } - } + const result = await dialog.showSaveDialog({ + title: "Save OpenScreen Project", + defaultPath: path.join(RECORDINGS_DIR, defaultName), + filters: [ + { name: "OpenScreen Project", extensions: [PROJECT_FILE_EXTENSION] }, + { name: "JSON", extensions: ["json"] }, + ], + properties: ["createDirectory", "showOverwriteConfirmation"], + }); - const safeName = (suggestedName || `project-${Date.now()}`).replace(/[^a-zA-Z0-9-_]/g, '_') - const defaultName = safeName.endsWith(`.${PROJECT_FILE_EXTENSION}`) - ? safeName - : `${safeName}.${PROJECT_FILE_EXTENSION}` + if (result.canceled || !result.filePath) { + return { + success: false, + canceled: true, + message: "Save project canceled", + }; + } - const result = await dialog.showSaveDialog({ - title: 'Save OpenScreen Project', - defaultPath: path.join(RECORDINGS_DIR, defaultName), - filters: [ - { name: 'OpenScreen Project', extensions: [PROJECT_FILE_EXTENSION] }, - { name: 'JSON', extensions: ['json'] } - ], - properties: ['createDirectory', 'showOverwriteConfirmation'] - }) + await fs.writeFile(result.filePath, JSON.stringify(projectData, null, 2), "utf-8"); + currentProjectPath = result.filePath; - if (result.canceled || !result.filePath) { - return { - success: false, - canceled: true, - message: 'Save project canceled' - } - } + return { + success: true, + path: result.filePath, + message: "Project saved successfully", + }; + } catch (error) { + console.error("Failed to save project file:", error); + return { + success: false, + message: "Failed to save project file", + error: String(error), + }; + } + }, + ); - await fs.writeFile(result.filePath, JSON.stringify(projectData, null, 2), 'utf-8') - currentProjectPath = result.filePath + ipcMain.handle("load-project-file", async () => { + try { + const result = await dialog.showOpenDialog({ + title: "Open OpenScreen Project", + defaultPath: RECORDINGS_DIR, + filters: [ + { name: "OpenScreen Project", extensions: [PROJECT_FILE_EXTENSION] }, + { name: "JSON", extensions: ["json"] }, + { name: "All Files", extensions: ["*"] }, + ], + properties: ["openFile"], + }); - return { - success: true, - path: result.filePath, - message: 'Project saved successfully' - } - } catch (error) { - console.error('Failed to save project file:', error) - return { - success: false, - message: 'Failed to save project file', - error: String(error) - } - } - }) + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true, message: "Open project canceled" }; + } - ipcMain.handle('load-project-file', async () => { - try { - const result = await dialog.showOpenDialog({ - title: 'Open OpenScreen Project', - defaultPath: RECORDINGS_DIR, - filters: [ - { name: 'OpenScreen Project', extensions: [PROJECT_FILE_EXTENSION] }, - { name: 'JSON', extensions: ['json'] }, - { name: 'All Files', extensions: ['*'] } - ], - properties: ['openFile'] - }) + const filePath = result.filePaths[0]; + const content = await fs.readFile(filePath, "utf-8"); + const project = JSON.parse(content); + currentProjectPath = filePath; + if (project && typeof project === "object" && typeof project.videoPath === "string") { + currentVideoPath = project.videoPath; + } - if (result.canceled || result.filePaths.length === 0) { - return { success: false, canceled: true, message: 'Open project canceled' } - } + return { + success: true, + path: filePath, + project, + }; + } catch (error) { + console.error("Failed to load project file:", error); + return { + success: false, + message: "Failed to load project file", + error: String(error), + }; + } + }); - const filePath = result.filePaths[0] - const content = await fs.readFile(filePath, 'utf-8') - const project = JSON.parse(content) - currentProjectPath = filePath - if (project && typeof project === 'object' && typeof project.videoPath === 'string') { - currentVideoPath = project.videoPath - } + ipcMain.handle("load-current-project-file", async () => { + try { + if (!currentProjectPath) { + return { success: false, message: "No active project" }; + } - return { - success: true, - path: filePath, - project - } - } catch (error) { - console.error('Failed to load project file:', error) - return { - success: false, - message: 'Failed to load project file', - error: String(error) - } - } - }) + const content = await fs.readFile(currentProjectPath, "utf-8"); + const project = JSON.parse(content); + if (project && typeof project === "object" && typeof project.videoPath === "string") { + currentVideoPath = project.videoPath; + } + return { + success: true, + path: currentProjectPath, + project, + }; + } catch (error) { + console.error("Failed to load current project file:", error); + return { + success: false, + message: "Failed to load current project file", + error: String(error), + }; + } + }); + ipcMain.handle("set-current-video-path", (_, path: string) => { + currentVideoPath = path; + currentProjectPath = null; + return { success: true }; + }); - ipcMain.handle('load-current-project-file', async () => { - try { - if (!currentProjectPath) { - return { success: false, message: 'No active project' } - } + ipcMain.handle("get-current-video-path", () => { + return currentVideoPath ? { success: true, path: currentVideoPath } : { success: false }; + }); - const content = await fs.readFile(currentProjectPath, 'utf-8') - const project = JSON.parse(content) - if (project && typeof project === 'object' && typeof project.videoPath === 'string') { - currentVideoPath = project.videoPath - } - return { - success: true, - path: currentProjectPath, - project, - } - } catch (error) { - console.error('Failed to load current project file:', error) - return { - success: false, - message: 'Failed to load current project file', - error: String(error), - } - } - }) - ipcMain.handle('set-current-video-path', (_, path: string) => { - currentVideoPath = path; - currentProjectPath = null - return { success: true }; - }); + ipcMain.handle("clear-current-video-path", () => { + currentVideoPath = null; + return { success: true }; + }); - ipcMain.handle('get-current-video-path', () => { - return currentVideoPath ? { success: true, path: currentVideoPath } : { success: false }; - }); + ipcMain.handle("get-platform", () => { + return process.platform; + }); - ipcMain.handle('clear-current-video-path', () => { - currentVideoPath = null; - return { success: true }; - }); + ipcMain.handle("get-shortcuts", async () => { + try { + const data = await fs.readFile(SHORTCUTS_FILE, "utf-8"); + return JSON.parse(data); + } catch { + return null; + } + }); - ipcMain.handle('get-platform', () => { - return process.platform; - }); - - ipcMain.handle('get-shortcuts', async () => { - try { - const data = await fs.readFile(SHORTCUTS_FILE, 'utf-8'); - return JSON.parse(data); - } catch { - return null; - } - }); - - ipcMain.handle('save-shortcuts', async (_, shortcuts: unknown) => { - try { - await fs.writeFile(SHORTCUTS_FILE, JSON.stringify(shortcuts, null, 2), 'utf-8'); - return { success: true }; - } catch (error) { - console.error('Failed to save shortcuts:', error); - return { success: false, error: String(error) }; - } - }); + ipcMain.handle("save-shortcuts", async (_, shortcuts: unknown) => { + try { + await fs.writeFile(SHORTCUTS_FILE, JSON.stringify(shortcuts, null, 2), "utf-8"); + return { success: true }; + } catch (error) { + console.error("Failed to save shortcuts:", error); + return { success: false, error: String(error) }; + } + }); } diff --git a/electron/main.ts b/electron/main.ts index 32fb59a..cacdf6c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,31 +1,29 @@ -import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron' -import { fileURLToPath } from 'node:url' -import path from 'node:path' -import fs from 'node:fs/promises' -import { createHudOverlayWindow, createEditorWindow, createSourceSelectorWindow } from './windows' -import { registerIpcHandlers } from './ipc/handlers' +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron"; +import { registerIpcHandlers } from "./ipc/handlers"; +import { createEditorWindow, createHudOverlayWindow, createSourceSelectorWindow } from "./windows"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Use Screen & System Audio Recording permissions instead of CoreAudio Tap API on macOS. // CoreAudio Tap requires NSAudioCaptureUsageDescription in the parent app's Info.plist, // which doesn't work when running from a terminal/IDE during development, makes my life easier -if (process.platform === 'darwin') { - app.commandLine.appendSwitch('disable-features', 'MacCatapLoopbackAudioForScreenShare') +if (process.platform === "darwin") { + app.commandLine.appendSwitch("disable-features", "MacCatapLoopbackAudioForScreenShare"); } -export const RECORDINGS_DIR = path.join(app.getPath('userData'), 'recordings') - +export const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); async function ensureRecordingsDir() { - try { - await fs.mkdir(RECORDINGS_DIR, { recursive: true }) - console.log('RECORDINGS_DIR:', RECORDINGS_DIR) - console.log('User Data Path:', app.getPath('userData')) - } catch (error) { - console.error('Failed to create recordings directory:', error) - } + try { + await fs.mkdir(RECORDINGS_DIR, { recursive: true }); + console.log("RECORDINGS_DIR:", RECORDINGS_DIR); + console.log("User Data Path:", app.getPath("userData")); + } catch (error) { + console.error("Failed to create recordings directory:", error); + } } // The built directory structure @@ -37,249 +35,244 @@ async function ensureRecordingsDir() { // │ │ ├── main.js // │ │ └── preload.mjs // │ -process.env.APP_ROOT = path.join(__dirname, '..') +process.env.APP_ROOT = path.join(__dirname, ".."); // Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x -export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] -export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') -export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') +export const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; +export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); +export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); -process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST +process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL + ? path.join(process.env.APP_ROOT, "public") + : RENDERER_DIST; // Window references -let mainWindow: BrowserWindow | null = null -let sourceSelectorWindow: BrowserWindow | null = null -let tray: Tray | null = null -let selectedSourceName = '' +let mainWindow: BrowserWindow | null = null; +let sourceSelectorWindow: BrowserWindow | null = null; +let tray: Tray | null = null; +let selectedSourceName = ""; // Tray Icons -const defaultTrayIcon = getTrayIcon('openscreen.png'); -const recordingTrayIcon = getTrayIcon('rec-button.png'); +const defaultTrayIcon = getTrayIcon("openscreen.png"); +const recordingTrayIcon = getTrayIcon("rec-button.png"); function createWindow() { - mainWindow = createHudOverlayWindow() + mainWindow = createHudOverlayWindow(); } function isEditorWindow(window: BrowserWindow) { - return window.webContents.getURL().includes('windowType=editor') + return window.webContents.getURL().includes("windowType=editor"); } -function sendEditorMenuAction(channel: 'menu-load-project' | 'menu-save-project' | 'menu-save-project-as') { - let targetWindow = BrowserWindow.getFocusedWindow() ?? mainWindow +function sendEditorMenuAction( + channel: "menu-load-project" | "menu-save-project" | "menu-save-project-as", +) { + let targetWindow = BrowserWindow.getFocusedWindow() ?? mainWindow; - if (!targetWindow || targetWindow.isDestroyed() || !isEditorWindow(targetWindow)) { - createEditorWindowWrapper() - targetWindow = mainWindow - if (!targetWindow || targetWindow.isDestroyed()) return + if (!targetWindow || targetWindow.isDestroyed() || !isEditorWindow(targetWindow)) { + createEditorWindowWrapper(); + targetWindow = mainWindow; + if (!targetWindow || targetWindow.isDestroyed()) return; - targetWindow.webContents.once('did-finish-load', () => { - if (!targetWindow || targetWindow.isDestroyed()) return - targetWindow.webContents.send(channel) - }) - return - } + targetWindow.webContents.once("did-finish-load", () => { + if (!targetWindow || targetWindow.isDestroyed()) return; + targetWindow.webContents.send(channel); + }); + return; + } - targetWindow.webContents.send(channel) + targetWindow.webContents.send(channel); } function setupApplicationMenu() { - const isMac = process.platform === 'darwin' - const template: Electron.MenuItemConstructorOptions[] = [] + const isMac = process.platform === "darwin"; + const template: Electron.MenuItemConstructorOptions[] = []; - if (isMac) { - template.push({ - label: app.name, - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideOthers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' }, - ], - }) - } + if (isMac) { + template.push({ + label: app.name, + submenu: [ + { role: "about" }, + { type: "separator" }, + { role: "services" }, + { type: "separator" }, + { role: "hide" }, + { role: "hideOthers" }, + { role: "unhide" }, + { type: "separator" }, + { role: "quit" }, + ], + }); + } - template.push( - { - label: 'File', - submenu: [ - { - label: 'Load Project…', - accelerator: 'CmdOrCtrl+O', - click: () => sendEditorMenuAction('menu-load-project'), - }, - { - label: 'Save Project…', - accelerator: 'CmdOrCtrl+S', - click: () => sendEditorMenuAction('menu-save-project'), - }, - { - label: 'Save Project As…', - accelerator: 'CmdOrCtrl+Shift+S', - click: () => sendEditorMenuAction('menu-save-project-as'), - }, - ...(isMac ? [] : [{ type: 'separator' as const }, { role: 'quit' as const }]), - ], - }, - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'selectAll' }, - ], - }, - { - label: 'View', - submenu: [ - { role: 'reload' }, - { role: 'forceReload' }, - { role: 'toggleDevTools' }, - { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - ], - }, - { - label: 'Window', - submenu: isMac - ? [ - { role: 'minimize' }, - { role: 'zoom' }, - { type: 'separator' }, - { role: 'front' }, - ] - : [ - { role: 'minimize' }, - { role: 'close' }, - ], - }, - ) + template.push( + { + label: "File", + submenu: [ + { + label: "Load Project…", + accelerator: "CmdOrCtrl+O", + click: () => sendEditorMenuAction("menu-load-project"), + }, + { + label: "Save Project…", + accelerator: "CmdOrCtrl+S", + click: () => sendEditorMenuAction("menu-save-project"), + }, + { + label: "Save Project As…", + accelerator: "CmdOrCtrl+Shift+S", + click: () => sendEditorMenuAction("menu-save-project-as"), + }, + ...(isMac ? [] : [{ type: "separator" as const }, { role: "quit" as const }]), + ], + }, + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: isMac + ? [{ role: "minimize" }, { role: "zoom" }, { type: "separator" }, { role: "front" }] + : [{ role: "minimize" }, { role: "close" }], + }, + ); - const menu = Menu.buildFromTemplate(template) - Menu.setApplicationMenu(menu) + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); } function createTray() { - tray = new Tray(defaultTrayIcon); + tray = new Tray(defaultTrayIcon); } function getTrayIcon(filename: string) { - return nativeImage.createFromPath(path.join(process.env.VITE_PUBLIC || RENDERER_DIST, filename)).resize({ - width: 24, - height: 24, - quality: 'best' - }); + return nativeImage + .createFromPath(path.join(process.env.VITE_PUBLIC || RENDERER_DIST, filename)) + .resize({ + width: 24, + height: 24, + quality: "best", + }); } - function updateTrayMenu(recording: boolean = false) { - if (!tray) return; - const trayIcon = recording ? recordingTrayIcon : defaultTrayIcon; - const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "OpenScreen"; - const menuTemplate = recording - ? [ - { - label: "Stop Recording", - click: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send("stop-recording-from-tray"); - } - }, - }, - ] - : [ - { - label: "Open", - click: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.isMinimized() && mainWindow.restore(); - } else { - createWindow(); - } - }, - }, - { - label: "Quit", - click: () => { - app.quit(); - }, - }, - ]; - tray.setImage(trayIcon); - tray.setToolTip(trayToolTip); - tray.setContextMenu(Menu.buildFromTemplate(menuTemplate)); + if (!tray) return; + const trayIcon = recording ? recordingTrayIcon : defaultTrayIcon; + const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "OpenScreen"; + const menuTemplate = recording + ? [ + { + label: "Stop Recording", + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("stop-recording-from-tray"); + } + }, + }, + ] + : [ + { + label: "Open", + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.isMinimized() && mainWindow.restore(); + } else { + createWindow(); + } + }, + }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]; + tray.setImage(trayIcon); + tray.setToolTip(trayToolTip); + tray.setContextMenu(Menu.buildFromTemplate(menuTemplate)); } function createEditorWindowWrapper() { - if (mainWindow) { - mainWindow.close() - mainWindow = null - } - mainWindow = createEditorWindow() + if (mainWindow) { + mainWindow.close(); + mainWindow = null; + } + mainWindow = createEditorWindow(); } function createSourceSelectorWindowWrapper() { - sourceSelectorWindow = createSourceSelectorWindow() - sourceSelectorWindow.on('closed', () => { - sourceSelectorWindow = null - }) - return sourceSelectorWindow + sourceSelectorWindow = createSourceSelectorWindow(); + sourceSelectorWindow.on("closed", () => { + sourceSelectorWindow = null; + }); + return sourceSelectorWindow; } // On macOS, applications and their menu bar stay active until the user quits // explicitly with Cmd + Q. -app.on('window-all-closed', () => { - // Keep app running (macOS behavior) -}) - -app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) { - createWindow() - } -}) - +app.on("window-all-closed", () => { + // Keep app running (macOS behavior) +}); +app.on("activate", () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); // Register all IPC handlers when app is ready app.whenReady().then(async () => { - // Listen for HUD overlay quit event (macOS only) - const { ipcMain } = await import('electron'); - ipcMain.on('hud-overlay-close', () => { - app.quit(); - }); - createTray() - updateTrayMenu() - setupApplicationMenu() - // Ensure recordings directory exists - await ensureRecordingsDir() + // Listen for HUD overlay quit event (macOS only) + const { ipcMain } = await import("electron"); + ipcMain.on("hud-overlay-close", () => { + app.quit(); + }); + createTray(); + updateTrayMenu(); + setupApplicationMenu(); + // Ensure recordings directory exists + await ensureRecordingsDir(); - registerIpcHandlers( - createEditorWindowWrapper, - createSourceSelectorWindowWrapper, - () => mainWindow, - () => sourceSelectorWindow, - (recording: boolean, sourceName: string) => { - selectedSourceName = sourceName - if (!tray) createTray(); - updateTrayMenu(recording); - if (!recording) { - if (mainWindow) mainWindow.restore(); - } - } - ) - createWindow() -}) + registerIpcHandlers( + createEditorWindowWrapper, + createSourceSelectorWindowWrapper, + () => mainWindow, + () => sourceSelectorWindow, + (recording: boolean, sourceName: string) => { + selectedSourceName = sourceName; + if (!tray) createTray(); + updateTrayMenu(recording); + if (!recording) { + if (mainWindow) mainWindow.restore(); + } + }, + ); + createWindow(); +}); diff --git a/electron/preload.ts b/electron/preload.ts index 2dda463..4ccc07a 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,105 +1,105 @@ -import { contextBridge, ipcRenderer } from 'electron' +import { contextBridge, ipcRenderer } from "electron"; -contextBridge.exposeInMainWorld('electronAPI', { - hudOverlayHide: () => { - ipcRenderer.send('hud-overlay-hide'); - }, - hudOverlayClose: () => { - ipcRenderer.send('hud-overlay-close'); - }, - getAssetBasePath: async () => { - // ask main process for the correct base path (production vs dev) - return await ipcRenderer.invoke('get-asset-base-path') - }, - getSources: async (opts: Electron.SourcesOptions) => { - return await ipcRenderer.invoke('get-sources', opts) - }, - switchToEditor: () => { - return ipcRenderer.invoke('switch-to-editor') - }, - openSourceSelector: () => { - return ipcRenderer.invoke('open-source-selector') - }, - selectSource: (source: any) => { - return ipcRenderer.invoke('select-source', source) - }, - getSelectedSource: () => { - return ipcRenderer.invoke('get-selected-source') - }, +contextBridge.exposeInMainWorld("electronAPI", { + hudOverlayHide: () => { + ipcRenderer.send("hud-overlay-hide"); + }, + hudOverlayClose: () => { + ipcRenderer.send("hud-overlay-close"); + }, + getAssetBasePath: async () => { + // ask main process for the correct base path (production vs dev) + return await ipcRenderer.invoke("get-asset-base-path"); + }, + getSources: async (opts: Electron.SourcesOptions) => { + return await ipcRenderer.invoke("get-sources", opts); + }, + switchToEditor: () => { + return ipcRenderer.invoke("switch-to-editor"); + }, + openSourceSelector: () => { + return ipcRenderer.invoke("open-source-selector"); + }, + selectSource: (source: any) => { + return ipcRenderer.invoke("select-source", source); + }, + getSelectedSource: () => { + return ipcRenderer.invoke("get-selected-source"); + }, - storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => { - return ipcRenderer.invoke('store-recorded-video', videoData, fileName) - }, + storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => { + return ipcRenderer.invoke("store-recorded-video", videoData, fileName); + }, - getRecordedVideoPath: () => { - return ipcRenderer.invoke('get-recorded-video-path') - }, - setRecordingState: (recording: boolean) => { - return ipcRenderer.invoke('set-recording-state', recording) - }, - getCursorTelemetry: (videoPath?: string) => { - return ipcRenderer.invoke('get-cursor-telemetry', videoPath) - }, - onStopRecordingFromTray: (callback: () => void) => { - const listener = () => callback() - ipcRenderer.on('stop-recording-from-tray', listener) - return () => ipcRenderer.removeListener('stop-recording-from-tray', listener) - }, - openExternalUrl: (url: string) => { - return ipcRenderer.invoke('open-external-url', url) - }, - saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => { - return ipcRenderer.invoke('save-exported-video', videoData, fileName) - }, - openVideoFilePicker: () => { - return ipcRenderer.invoke('open-video-file-picker') - }, - setCurrentVideoPath: (path: string) => { - return ipcRenderer.invoke('set-current-video-path', path) - }, - getCurrentVideoPath: () => { - return ipcRenderer.invoke('get-current-video-path') - }, - clearCurrentVideoPath: () => { - return ipcRenderer.invoke('clear-current-video-path') - }, - saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => { - return ipcRenderer.invoke('save-project-file', projectData, suggestedName, existingProjectPath) - }, - loadProjectFile: () => { - return ipcRenderer.invoke('load-project-file') - }, - loadCurrentProjectFile: () => { - return ipcRenderer.invoke('load-current-project-file') - }, - onMenuLoadProject: (callback: () => void) => { - const listener = () => callback() - ipcRenderer.on('menu-load-project', listener) - return () => ipcRenderer.removeListener('menu-load-project', listener) - }, - onMenuSaveProject: (callback: () => void) => { - const listener = () => callback() - ipcRenderer.on('menu-save-project', listener) - return () => ipcRenderer.removeListener('menu-save-project', listener) - }, - onMenuSaveProjectAs: (callback: () => void) => { - const listener = () => callback() - ipcRenderer.on('menu-save-project-as', listener) - return () => ipcRenderer.removeListener('menu-save-project-as', listener) - }, - getPlatform: () => { - return ipcRenderer.invoke('get-platform') - }, - revealInFolder: (filePath: string) => { - return ipcRenderer.invoke('reveal-in-folder', filePath) - }, - getShortcuts: () => { - return ipcRenderer.invoke('get-shortcuts') - }, - saveShortcuts: (shortcuts: unknown) => { - return ipcRenderer.invoke('save-shortcuts', shortcuts) - }, - setMicrophoneExpanded: (expanded: boolean) => { - ipcRenderer.send('hud:setMicrophoneExpanded', expanded) - }, -}) + getRecordedVideoPath: () => { + return ipcRenderer.invoke("get-recorded-video-path"); + }, + setRecordingState: (recording: boolean) => { + return ipcRenderer.invoke("set-recording-state", recording); + }, + getCursorTelemetry: (videoPath?: string) => { + return ipcRenderer.invoke("get-cursor-telemetry", videoPath); + }, + onStopRecordingFromTray: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("stop-recording-from-tray", listener); + return () => ipcRenderer.removeListener("stop-recording-from-tray", listener); + }, + openExternalUrl: (url: string) => { + return ipcRenderer.invoke("open-external-url", url); + }, + saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => { + return ipcRenderer.invoke("save-exported-video", videoData, fileName); + }, + openVideoFilePicker: () => { + return ipcRenderer.invoke("open-video-file-picker"); + }, + setCurrentVideoPath: (path: string) => { + return ipcRenderer.invoke("set-current-video-path", path); + }, + getCurrentVideoPath: () => { + return ipcRenderer.invoke("get-current-video-path"); + }, + clearCurrentVideoPath: () => { + return ipcRenderer.invoke("clear-current-video-path"); + }, + saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => { + return ipcRenderer.invoke("save-project-file", projectData, suggestedName, existingProjectPath); + }, + loadProjectFile: () => { + return ipcRenderer.invoke("load-project-file"); + }, + loadCurrentProjectFile: () => { + return ipcRenderer.invoke("load-current-project-file"); + }, + onMenuLoadProject: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-load-project", listener); + return () => ipcRenderer.removeListener("menu-load-project", listener); + }, + onMenuSaveProject: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-save-project", listener); + return () => ipcRenderer.removeListener("menu-save-project", listener); + }, + onMenuSaveProjectAs: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-save-project-as", listener); + return () => ipcRenderer.removeListener("menu-save-project-as", listener); + }, + getPlatform: () => { + return ipcRenderer.invoke("get-platform"); + }, + revealInFolder: (filePath: string) => { + return ipcRenderer.invoke("reveal-in-folder", filePath); + }, + getShortcuts: () => { + return ipcRenderer.invoke("get-shortcuts"); + }, + saveShortcuts: (shortcuts: unknown) => { + return ipcRenderer.invoke("save-shortcuts", shortcuts); + }, + setMicrophoneExpanded: (expanded: boolean) => { + ipcRenderer.send("hud:setMicrophoneExpanded", expanded); + }, +}); diff --git a/electron/windows.ts b/electron/windows.ts index 7635626..77cb3a5 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -1,155 +1,151 @@ -import { BrowserWindow, screen } from 'electron' -import { ipcMain } from 'electron' -import path from 'node:path' -import { fileURLToPath } from 'node:url' +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { BrowserWindow, ipcMain, screen } from "electron"; -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const APP_ROOT = path.join(__dirname, '..') -const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] -const RENDERER_DIST = path.join(APP_ROOT, 'dist') +const APP_ROOT = path.join(__dirname, ".."); +const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; +const RENDERER_DIST = path.join(APP_ROOT, "dist"); let hudOverlayWindow: BrowserWindow | null = null; -ipcMain.on('hud-overlay-hide', () => { - if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) { - hudOverlayWindow.minimize(); - } +ipcMain.on("hud-overlay-hide", () => { + if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) { + hudOverlayWindow.minimize(); + } }); export function createHudOverlayWindow(): BrowserWindow { - const primaryDisplay = screen.getPrimaryDisplay(); - const { workArea } = primaryDisplay; + const primaryDisplay = screen.getPrimaryDisplay(); + const { workArea } = primaryDisplay; + const windowWidth = 500; + const windowHeight = 155; - const windowWidth = 500; - const windowHeight = 155; + const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2); + const y = Math.floor(workArea.y + workArea.height - windowHeight - 5); - 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, + x: x, + y: y, + frame: false, + transparent: true, + resizable: false, + alwaysOnTop: true, + skipTaskbar: true, + hasShadow: false, + webPreferences: { + preload: path.join(__dirname, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + backgroundThrottling: false, + }, + }); - const win = new BrowserWindow({ - width: windowWidth, - height: windowHeight, - minWidth: 500, - maxWidth: 500, - minHeight: 155, - maxHeight: 155, - x: x, - y: y, - frame: false, - transparent: true, - resizable: false, - alwaysOnTop: true, - skipTaskbar: true, - hasShadow: false, - webPreferences: { - preload: path.join(__dirname, 'preload.mjs'), - nodeIntegration: false, - contextIsolation: true, - backgroundThrottling: false, - }, - }) + win.webContents.on("did-finish-load", () => { + win?.webContents.send("main-process-message", new Date().toLocaleString()); + }); + hudOverlayWindow = win; - win.webContents.on('did-finish-load', () => { - win?.webContents.send('main-process-message', (new Date).toLocaleString()) - }) + win.on("closed", () => { + if (hudOverlayWindow === win) { + hudOverlayWindow = null; + } + }); - hudOverlayWindow = win; + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?windowType=hud-overlay"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "hud-overlay" }, + }); + } - win.on('closed', () => { - if (hudOverlayWindow === win) { - hudOverlayWindow = null; - } - }); - - - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL + '?windowType=hud-overlay') - } else { - win.loadFile(path.join(RENDERER_DIST, 'index.html'), { - query: { windowType: 'hud-overlay' } - }) - } - - return win + return win; } export function createEditorWindow(): BrowserWindow { - const isMac = process.platform === 'darwin'; + const isMac = process.platform === "darwin"; - const win = new BrowserWindow({ - width: 1200, - height: 800, - minWidth: 800, - minHeight: 600, - ...(isMac && { - titleBarStyle: 'hiddenInset', - trafficLightPosition: { x: 12, y: 12 }, - }), - transparent: false, - resizable: true, - alwaysOnTop: false, - skipTaskbar: false, - title: 'OpenScreen', - backgroundColor: '#000000', - webPreferences: { - preload: path.join(__dirname, 'preload.mjs'), - nodeIntegration: false, - contextIsolation: true, - webSecurity: false, - backgroundThrottling: false, - }, - }) + const win = new BrowserWindow({ + width: 1200, + height: 800, + minWidth: 800, + minHeight: 600, + ...(isMac && { + titleBarStyle: "hiddenInset", + trafficLightPosition: { x: 12, y: 12 }, + }), + transparent: false, + resizable: true, + alwaysOnTop: false, + skipTaskbar: false, + title: "OpenScreen", + backgroundColor: "#000000", + webPreferences: { + preload: path.join(__dirname, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + webSecurity: false, + backgroundThrottling: false, + }, + }); - // Maximize the window by default - win.maximize(); + // Maximize the window by default + win.maximize(); - win.webContents.on('did-finish-load', () => { - win?.webContents.send('main-process-message', (new Date).toLocaleString()) - }) + win.webContents.on("did-finish-load", () => { + win?.webContents.send("main-process-message", new Date().toLocaleString()); + }); - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL + '?windowType=editor') - } else { - win.loadFile(path.join(RENDERER_DIST, 'index.html'), { - query: { windowType: 'editor' } - }) - } + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?windowType=editor"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "editor" }, + }); + } - return win + return win; } export function createSourceSelectorWindow(): BrowserWindow { - const { width, height } = screen.getPrimaryDisplay().workAreaSize - - const win = new BrowserWindow({ - width: 620, - height: 420, - minHeight: 350, - maxHeight: 500, - x: Math.round((width - 620) / 2), - y: Math.round((height - 420) / 2), - frame: false, - resizable: false, - alwaysOnTop: true, - transparent: true, - backgroundColor: '#00000000', - webPreferences: { - preload: path.join(__dirname, 'preload.mjs'), - nodeIntegration: false, - contextIsolation: true, - }, - }) + const { width, height } = screen.getPrimaryDisplay().workAreaSize; - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL + '?windowType=source-selector') - } else { - win.loadFile(path.join(RENDERER_DIST, 'index.html'), { - query: { windowType: 'source-selector' } - }) - } + const win = new BrowserWindow({ + width: 620, + height: 420, + minHeight: 350, + maxHeight: 500, + x: Math.round((width - 620) / 2), + y: Math.round((height - 420) / 2), + frame: false, + resizable: false, + alwaysOnTop: true, + transparent: true, + backgroundColor: "#00000000", + webPreferences: { + preload: path.join(__dirname, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + }, + }); - return win + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?windowType=source-selector"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "source-selector" }, + }); + } + + return win; } diff --git a/package-lock.json b/package-lock.json index 7bf3c08..56e1fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "web-demuxer": "^4.0.0" }, "devDependencies": { - "@biomejs/biome": "2.3.13", + "@biomejs/biome": "^2.3.13", "@types/node": "^25.0.3", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", diff --git a/package.json b/package.json index 7ecb108..b3ccd98 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "web-demuxer": "^4.0.0" }, "devDependencies": { - "@biomejs/biome": "2.3.13", + "@biomejs/biome": "^2.3.13", "@types/node": "^25.0.3", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", diff --git a/postcss.config.cjs b/postcss.config.cjs index 96bb01e..e873f1a 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,6 +1,6 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/App.css b/src/App.css index fe59efc..df674c0 100644 --- a/src/App.css +++ b/src/App.css @@ -1,42 +1,42 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/src/App.tsx b/src/App.tsx index cc6742c..ee42296 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,47 +1,47 @@ -import { useEffect, useState } from "react"; -import { LaunchWindow } from "./components/launch/LaunchWindow"; -import { SourceSelector } from "./components/launch/SourceSelector"; -import VideoEditor from "./components/video-editor/VideoEditor"; -import { loadAllCustomFonts } from "./lib/customFonts"; -import { ShortcutsProvider } from "./contexts/ShortcutsContext"; -import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog"; - -export default function App() { - const [windowType, setWindowType] = useState(''); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const type = params.get('windowType') || ''; - setWindowType(type); - if (type === 'hud-overlay' || type === 'source-selector') { - document.body.style.background = 'transparent'; - document.documentElement.style.background = 'transparent'; - document.getElementById('root')?.style.setProperty('background', 'transparent'); - } - - // Load custom fonts on app initialization - loadAllCustomFonts().catch((error) => { - console.error('Failed to load custom fonts:', error); - }); - }, []); - - switch (windowType) { - case 'hud-overlay': - return ; - case 'source-selector': - return ; - case 'editor': - return ( - - - - - ); - default: - return ( -
-

Openscreen

-
- ); - } -} +import { useEffect, useState } from "react"; +import { LaunchWindow } from "./components/launch/LaunchWindow"; +import { SourceSelector } from "./components/launch/SourceSelector"; +import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog"; +import VideoEditor from "./components/video-editor/VideoEditor"; +import { ShortcutsProvider } from "./contexts/ShortcutsContext"; +import { loadAllCustomFonts } from "./lib/customFonts"; + +export default function App() { + const [windowType, setWindowType] = useState(""); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const type = params.get("windowType") || ""; + setWindowType(type); + if (type === "hud-overlay" || type === "source-selector") { + document.body.style.background = "transparent"; + document.documentElement.style.background = "transparent"; + document.getElementById("root")?.style.setProperty("background", "transparent"); + } + + // Load custom fonts on app initialization + loadAllCustomFonts().catch((error) => { + console.error("Failed to load custom fonts:", error); + }); + }, []); + + switch (windowType) { + case "hud-overlay": + return ; + case "source-selector": + return ; + case "editor": + return ( + + + + + ); + default: + return ( +
+

Openscreen

+
+ ); + } +} diff --git a/src/components/launch/LaunchWindow.module.css b/src/components/launch/LaunchWindow.module.css index 33dbd22..248d41c 100644 --- a/src/components/launch/LaunchWindow.module.css +++ b/src/components/launch/LaunchWindow.module.css @@ -1,132 +1,133 @@ .electronDrag { - -webkit-app-region: drag; + -webkit-app-region: drag; } .electronNoDrag { - -webkit-app-region: no-drag; + -webkit-app-region: no-drag; } .hudBar { - isolation: isolate; - box-shadow: - 0 2px 16px rgba(0, 0, 0, 0.25), - 0 0 40px rgba(100, 80, 200, 0.08); + isolation: isolate; + box-shadow: + 0 2px 16px rgba(0, 0, 0, 0.25), + 0 0 40px rgba(100, 80, 200, 0.08); } /* Sub-pill group container */ .hudGroup { - display: flex; - align-items: center; - gap: 2px; - background: rgba(255, 255, 255, 0.05); - border-radius: 9999px; - padding: 4px 8px; - transition: background 0.15s ease; + display: flex; + align-items: center; + gap: 2px; + background: rgba(255, 255, 255, 0.05); + border-radius: 9999px; + padding: 4px 8px; + transition: background 0.15s ease; } .hudGroup:hover { - background: rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.08); } /* Icon button within groups */ .hudIconBtn { - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - border-radius: 9999px; - transition: all 0.15s ease; - cursor: pointer; - background: transparent; - border: none; - color: #fff; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border-radius: 9999px; + transition: all 0.15s ease; + cursor: pointer; + background: transparent; + border: none; + color: #fff; } .hudIconBtn:hover { - background: rgba(255, 255, 255, 0.1); - transform: scale(1.08); + background: rgba(255, 255, 255, 0.1); + transform: scale(1.08); } .hudIconBtn:active { - transform: scale(0.95); + transform: scale(0.95); } /* Active icon glow (green) for enabled audio toggles */ .hudIconActive { - filter: drop-shadow(0 0 4px rgba(74, 222, 128, 0.4)); + filter: drop-shadow(0 0 4px rgba(74, 222, 128, 0.4)); } /* Recording pulse animation on the record group */ @keyframes recordPulse { - 0%, 100% { - box-shadow: 0 0 8px rgba(239, 68, 68, 0.15); - } - 50% { - box-shadow: 0 0 16px rgba(239, 68, 68, 0.4); - } + 0%, + 100% { + box-shadow: 0 0 8px rgba(239, 68, 68, 0.15); + } + 50% { + box-shadow: 0 0 16px rgba(239, 68, 68, 0.4); + } } .recordingPulse { - animation: recordPulse 1.5s ease-in-out infinite; - background: rgba(239, 68, 68, 0.1) !important; + animation: recordPulse 1.5s ease-in-out infinite; + background: rgba(239, 68, 68, 0.1) !important; } /* Mic panel above the bar */ .micPanel { - background: linear-gradient(135deg, rgba(28, 28, 36, 0.97) 0%, rgba(18, 18, 26, 0.96) 100%); - backdrop-filter: blur(16px) saturate(140%); - -webkit-backdrop-filter: blur(16px) saturate(140%); - border: 1px solid rgba(80, 80, 120, 0.25); - border-radius: 16px; - box-shadow: - 0 2px 12px rgba(0, 0, 0, 0.2), - 0 0 30px rgba(100, 80, 200, 0.06); - animation: micPanelIn 0.15s ease-out; + background: linear-gradient(135deg, rgba(28, 28, 36, 0.97) 0%, rgba(18, 18, 26, 0.96) 100%); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); + border: 1px solid rgba(80, 80, 120, 0.25); + border-radius: 16px; + box-shadow: + 0 2px 12px rgba(0, 0, 0, 0.2), + 0 0 30px rgba(100, 80, 200, 0.06); + animation: micPanelIn 0.15s ease-out; } @keyframes micPanelIn { - from { - opacity: 0; - transform: translateY(4px); - } - to { - opacity: 1; - transform: translateY(0); - } + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } } /* Window control buttons */ .windowBtn { - display: flex; - align-items: center; - justify-content: center; - padding: 3px; - border-radius: 9999px; - transition: all 0.15s ease; - cursor: pointer; - background: transparent; - border: none; - opacity: 0.5; + display: flex; + align-items: center; + justify-content: center; + padding: 3px; + border-radius: 9999px; + transition: all 0.15s ease; + cursor: pointer; + background: transparent; + border: none; + opacity: 0.5; } .windowBtn:hover { - opacity: 0.9; - background: rgba(255, 255, 255, 0.08); + opacity: 0.9; + background: rgba(255, 255, 255, 0.08); } /* Folder button */ .folderButton { - cursor: pointer; - display: flex; - align-items: center; - gap: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; } .folderText { - color: #cbd5e1; - transition: text-decoration 0.15s; + color: #cbd5e1; + transition: text-decoration 0.15s; } .folderButton:hover .folderText { - text-decoration: underline; + text-decoration: underline; } diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 64b091c..1ad80ba 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,244 +1,256 @@ -import { useState, useEffect } from "react"; -import styles from "./LaunchWindow.module.css"; -import { useScreenRecorder } from "../../hooks/useScreenRecorder"; -import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices"; -import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter"; -import { AudioLevelMeter } from "../ui/audio-level-meter"; +import { useEffect, useState } from "react"; import { BsRecordCircle } from "react-icons/bs"; import { FaRegStopCircle } from "react-icons/fa"; -import { MdMonitor, MdMic, MdMicOff, MdVolumeUp, MdVolumeOff } from "react-icons/md"; -import { RxDragHandleDots2 } from "react-icons/rx"; import { FaFolderMinus } from "react-icons/fa6"; import { FiMinus, FiX } from "react-icons/fi"; - +import { MdMic, MdMicOff, MdMonitor, MdVolumeOff, MdVolumeUp } from "react-icons/md"; +import { RxDragHandleDots2 } from "react-icons/rx"; +import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter"; +import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices"; +import { useScreenRecorder } from "../../hooks/useScreenRecorder"; +import { AudioLevelMeter } from "../ui/audio-level-meter"; +import styles from "./LaunchWindow.module.css"; export function LaunchWindow() { - const { recording, toggleRecording, microphoneEnabled, setMicrophoneEnabled, microphoneDeviceId, setMicrophoneDeviceId, systemAudioEnabled, setSystemAudioEnabled } = useScreenRecorder(); - const [recordingStart, setRecordingStart] = useState(null); - const [elapsed, setElapsed] = useState(0); + const { + recording, + toggleRecording, + microphoneEnabled, + setMicrophoneEnabled, + microphoneDeviceId, + setMicrophoneDeviceId, + systemAudioEnabled, + setSystemAudioEnabled, + } = useScreenRecorder(); + const [recordingStart, setRecordingStart] = useState(null); + const [elapsed, setElapsed] = useState(0); - const showMicControls = microphoneEnabled && !recording; - const { devices, selectedDeviceId, setSelectedDeviceId } = useMicrophoneDevices(microphoneEnabled); - const { level } = useAudioLevelMeter({ - enabled: showMicControls, - deviceId: microphoneDeviceId, - }); + const showMicControls = microphoneEnabled && !recording; + const { devices, selectedDeviceId, setSelectedDeviceId } = + useMicrophoneDevices(microphoneEnabled); + const { level } = useAudioLevelMeter({ + enabled: showMicControls, + deviceId: microphoneDeviceId, + }); - useEffect(() => { - if (selectedDeviceId && selectedDeviceId !== 'default') { - setMicrophoneDeviceId(selectedDeviceId); - } - }, [selectedDeviceId, setMicrophoneDeviceId]); + useEffect(() => { + if (selectedDeviceId && selectedDeviceId !== "default") { + setMicrophoneDeviceId(selectedDeviceId); + } + }, [selectedDeviceId, setMicrophoneDeviceId]); - useEffect(() => { - let timer: NodeJS.Timeout | null = null; - if (recording) { - if (!recordingStart) setRecordingStart(Date.now()); - timer = setInterval(() => { - if (recordingStart) { - setElapsed(Math.floor((Date.now() - recordingStart) / 1000)); - } - }, 1000); - } else { - setRecordingStart(null); - setElapsed(0); - if (timer) clearInterval(timer); - } - return () => { - if (timer) clearInterval(timer); - }; - }, [recording, recordingStart]); + useEffect(() => { + let timer: NodeJS.Timeout | null = null; + if (recording) { + if (!recordingStart) setRecordingStart(Date.now()); + timer = setInterval(() => { + if (recordingStart) { + setElapsed(Math.floor((Date.now() - recordingStart) / 1000)); + } + }, 1000); + } else { + setRecordingStart(null); + setElapsed(0); + if (timer) clearInterval(timer); + } + return () => { + if (timer) clearInterval(timer); + }; + }, [recording, recordingStart]); - const formatTime = (seconds: number) => { - const m = Math.floor(seconds / 60).toString().padStart(2, '0'); - const s = (seconds % 60).toString().padStart(2, '0'); - return `${m}:${s}`; - }; - const [selectedSource, setSelectedSource] = useState("Screen"); - const [hasSelectedSource, setHasSelectedSource] = useState(false); + const formatTime = (seconds: number) => { + const m = Math.floor(seconds / 60) + .toString() + .padStart(2, "0"); + const s = (seconds % 60).toString().padStart(2, "0"); + return `${m}:${s}`; + }; + const [selectedSource, setSelectedSource] = useState("Screen"); + const [hasSelectedSource, setHasSelectedSource] = useState(false); - useEffect(() => { - const checkSelectedSource = async () => { - if (window.electronAPI) { - const source = await window.electronAPI.getSelectedSource(); - if (source) { - setSelectedSource(source.name); - setHasSelectedSource(true); - } else { - setSelectedSource("Screen"); - setHasSelectedSource(false); - } - } - }; + useEffect(() => { + const checkSelectedSource = async () => { + if (window.electronAPI) { + const source = await window.electronAPI.getSelectedSource(); + if (source) { + setSelectedSource(source.name); + setHasSelectedSource(true); + } else { + setSelectedSource("Screen"); + setHasSelectedSource(false); + } + } + }; - checkSelectedSource(); + checkSelectedSource(); - const interval = setInterval(checkSelectedSource, 500); - return () => clearInterval(interval); - }, []); + const interval = setInterval(checkSelectedSource, 500); + return () => clearInterval(interval); + }, []); - const openSourceSelector = () => { - if (window.electronAPI) { - window.electronAPI.openSourceSelector(); - } - }; + const openSourceSelector = () => { + if (window.electronAPI) { + window.electronAPI.openSourceSelector(); + } + }; - const openVideoFile = async () => { - const result = await window.electronAPI.openVideoFilePicker(); + const openVideoFile = async () => { + const result = await window.electronAPI.openVideoFilePicker(); - if (result.canceled) { - return; - } + if (result.canceled) { + return; + } - if (result.success && result.path) { - await window.electronAPI.setCurrentVideoPath(result.path); - await window.electronAPI.switchToEditor(); - } - }; + if (result.success && result.path) { + await window.electronAPI.setCurrentVideoPath(result.path); + await window.electronAPI.switchToEditor(); + } + }; - const sendHudOverlayHide = () => { - if (window.electronAPI && window.electronAPI.hudOverlayHide) { - window.electronAPI.hudOverlayHide(); - } - }; - const sendHudOverlayClose = () => { - if (window.electronAPI && window.electronAPI.hudOverlayClose) { - window.electronAPI.hudOverlayClose(); - } - }; + const sendHudOverlayHide = () => { + if (window.electronAPI && window.electronAPI.hudOverlayHide) { + window.electronAPI.hudOverlayHide(); + } + }; + const sendHudOverlayClose = () => { + if (window.electronAPI && window.electronAPI.hudOverlayClose) { + window.electronAPI.hudOverlayClose(); + } + }; - const toggleMicrophone = () => { - if (!recording) { - setMicrophoneEnabled(!microphoneEnabled); - } - }; + const toggleMicrophone = () => { + if (!recording) { + setMicrophoneEnabled(!microphoneEnabled); + } + }; - return ( -
-
- {/* Mic controls panel */} - {showMicControls && ( -
- - -
- )} + return ( +
+
+ {/* Mic controls panel */} + {showMicControls && ( +
+ + +
+ )} - {/* Main pill bar */} -
- {/* Drag handle */} -
- -
+ {/* Main pill bar */} +
+ {/* Drag handle */} +
+ +
- {/* Source selector */} - + {/* Source selector */} + - {/* Audio controls group */} -
- - -
+ {/* Audio controls group */} +
+ + +
- {/* Record/Stop group */} - + {/* Record/Stop group */} + - {/* Open file */} - + {/* Open file */} + - {/* Window controls */} -
- - -
-
-
-
- ); + {/* Window controls */} +
+ + +
+
+
+
+ ); } diff --git a/src/components/launch/SourceSelector.module.css b/src/components/launch/SourceSelector.module.css index 4b327fb..51239ac 100644 --- a/src/components/launch/SourceSelector.module.css +++ b/src/components/launch/SourceSelector.module.css @@ -1,93 +1,99 @@ - .glassContainer { - background: linear-gradient(135deg, rgba(28,28,34,0.92) 0%, rgba(18,18,22,0.88) 100%); - backdrop-filter: blur(20px) saturate(160%); - -webkit-backdrop-filter: blur(20px) saturate(160%); - border-radius: 14px; - box-shadow: 0 4px 16px 0 rgba(0,0,0,0.32), 0 1px 3px 0 rgba(0,0,0,0.18) inset; - border: 1px solid rgba(60,60,80,0.18); + background: linear-gradient(135deg, rgba(28, 28, 34, 0.92) 0%, rgba(18, 18, 22, 0.88) 100%); + backdrop-filter: blur(20px) saturate(160%); + -webkit-backdrop-filter: blur(20px) saturate(160%); + border-radius: 14px; + box-shadow: + 0 4px 16px 0 rgba(0, 0, 0, 0.32), + 0 1px 3px 0 rgba(0, 0, 0, 0.18) inset; + border: 1px solid rgba(60, 60, 80, 0.18); } .sourceCard { - border-radius: 12px; - background: linear-gradient(120deg, rgba(38,38,48,0.98) 0%, rgba(24,24,32,0.96) 100%); - border: 1px solid rgba(60,60,80,0.22); - box-shadow: 0 2px 8px 0 rgba(0,0,0,0.18); - transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease; - cursor: pointer; + border-radius: 12px; + background: linear-gradient(120deg, rgba(38, 38, 48, 0.98) 0%, rgba(24, 24, 32, 0.96) 100%); + border: 1px solid rgba(60, 60, 80, 0.22); + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.18); + transition: + box-shadow 0.2s ease, + border-color 0.2s ease, + transform 0.2s ease; + cursor: pointer; } .sourceCard:hover { - border-color: rgba(120,120,160,0.35); - transform: translateY(-1px); - box-shadow: 0 4px 12px 0 rgba(0,0,0,0.25); + border-color: rgba(120, 120, 160, 0.35); + transform: translateY(-1px); + box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.25); } .selected { - border: 2px solid #34B27B; - background: linear-gradient(120deg, rgba(52,178,123,0.08) 0%, rgba(38,38,48,0.98) 100%); - box-shadow: 0 0 12px rgba(52,178,123,0.15), 0 0 4px rgba(52,178,123,0.1); + border: 2px solid #34b27b; + background: linear-gradient(120deg, rgba(52, 178, 123, 0.08) 0%, rgba(38, 38, 48, 0.98) 100%); + box-shadow: + 0 0 12px rgba(52, 178, 123, 0.15), + 0 0 4px rgba(52, 178, 123, 0.1); } .selected:hover { - transform: translateY(0); + transform: translateY(0); } .icon { - width: 13px; - height: 13px; - color: #c7d2fe; + width: 13px; + height: 13px; + color: #c7d2fe; } .name { - font-size: 0.8rem; - color: #e4e4e7; - font-weight: 500; - letter-spacing: 0.01em; + font-size: 0.8rem; + color: #e4e4e7; + font-weight: 500; + letter-spacing: 0.01em; } .cardText { - color: #a1a1aa; - font-size: 0.75rem; + color: #a1a1aa; + font-size: 0.75rem; } /* Checkmark badge */ .checkBadge { - width: 18px; - height: 18px; - background: #34B27B; - border-radius: 9999px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 0 8px rgba(52,178,123,0.4); + width: 18px; + height: 18px; + background: #34b27b; + border-radius: 9999px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 8px rgba(52, 178, 123, 0.4); } /* scrollbar */ .sourceGridScroll { - scrollbar-width: thin; - scrollbar-color: rgba(52, 178, 123, 0.5) rgba(40, 40, 50, 0.6); + scrollbar-width: thin; + scrollbar-color: rgba(52, 178, 123, 0.5) rgba(40, 40, 50, 0.6); } .sourceGridScroll::-webkit-scrollbar { - width: 8px; + width: 8px; } .sourceGridScroll::-webkit-scrollbar-track { - background: rgba(30, 30, 38, 0.5); - border-radius: 4px; - margin: 4px 0; + background: rgba(30, 30, 38, 0.5); + border-radius: 4px; + margin: 4px 0; } .sourceGridScroll::-webkit-scrollbar-thumb { - background: rgba(80, 80, 100, 0.6); - border-radius: 4px; + background: rgba(80, 80, 100, 0.6); + border-radius: 4px; } .sourceGridScroll::-webkit-scrollbar-thumb:hover { - background: rgba(52, 178, 123, 0.6); + background: rgba(52, 178, 123, 0.6); } .sourceGridScroll::-webkit-scrollbar-thumb:active { - background: rgba(52, 178, 123, 0.8); + background: rgba(52, 178, 123, 0.8); } diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index 9056d2a..43feeaf 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -1,145 +1,158 @@ -import { useState, useEffect } from "react"; -import { Button } from "../ui/button"; +import { useEffect, useState } from "react"; import { MdCheck } from "react-icons/md"; +import { Button } from "../ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import styles from "./SourceSelector.module.css"; interface DesktopSource { - id: string; - name: string; - thumbnail: string | null; - display_id: string; - appIcon: string | null; + id: string; + name: string; + thumbnail: string | null; + display_id: string; + appIcon: string | null; } export function SourceSelector() { - const [sources, setSources] = useState([]); - const [selectedSource, setSelectedSource] = useState(null); - const [loading, setLoading] = useState(true); + const [sources, setSources] = useState([]); + const [selectedSource, setSelectedSource] = useState(null); + const [loading, setLoading] = useState(true); - useEffect(() => { - async function fetchSources() { - setLoading(true); - try { - const rawSources = await window.electronAPI.getSources({ - types: ['screen', 'window'], - thumbnailSize: { width: 320, height: 180 }, - fetchWindowIcons: true - }); - setSources( - rawSources.map(source => ({ - id: source.id, - name: - source.id.startsWith('window:') && source.name.includes(' — ') - ? source.name.split(' — ')[1] || source.name - : source.name, - thumbnail: source.thumbnail, - display_id: source.display_id, - appIcon: source.appIcon - })) - ); - } catch (error) { - console.error('Error loading sources:', error); - } finally { - setLoading(false); - } - } - fetchSources(); - }, []); + useEffect(() => { + async function fetchSources() { + setLoading(true); + try { + const rawSources = await window.electronAPI.getSources({ + types: ["screen", "window"], + thumbnailSize: { width: 320, height: 180 }, + fetchWindowIcons: true, + }); + setSources( + rawSources.map((source) => ({ + id: source.id, + name: + source.id.startsWith("window:") && source.name.includes(" — ") + ? source.name.split(" — ")[1] || source.name + : source.name, + thumbnail: source.thumbnail, + display_id: source.display_id, + appIcon: source.appIcon, + })), + ); + } catch (error) { + console.error("Error loading sources:", error); + } finally { + setLoading(false); + } + } + fetchSources(); + }, []); - const screenSources = sources.filter(s => s.id.startsWith('screen:')); - const windowSources = sources.filter(s => s.id.startsWith('window:')); + const screenSources = sources.filter((s) => s.id.startsWith("screen:")); + const windowSources = sources.filter((s) => s.id.startsWith("window:")); - const handleSourceSelect = (source: DesktopSource) => setSelectedSource(source); - const handleShare = async () => { - if (selectedSource) await window.electronAPI.selectSource(selectedSource); - }; + const handleSourceSelect = (source: DesktopSource) => setSelectedSource(source); + const handleShare = async () => { + if (selectedSource) await window.electronAPI.selectSource(selectedSource); + }; - if (loading) { - return ( -
-
-
-

Loading sources...

-
-
- ); - } + if (loading) { + return ( +
+
+
+

Loading sources...

+
+
+ ); + } - const renderSourceCard = (source: DesktopSource) => { - const isSelected = selectedSource?.id === source.id; - return ( -
handleSourceSelect(source)} - > -
- {source.name} - {isSelected && ( -
-
- -
-
- )} -
-
- {source.appIcon && ( - - )} -
{source.name}
-
-
- ); - }; + const renderSourceCard = (source: DesktopSource) => { + const isSelected = selectedSource?.id === source.id; + return ( +
handleSourceSelect(source)} + > +
+ {source.name} + {isSelected && ( +
+
+ +
+
+ )} +
+
+ {source.appIcon && ( + + )} +
{source.name}
+
+
+ ); + }; - return ( -
-
- - - Screens - Windows - -
- -
- {screenSources.map(renderSourceCard)} -
-
- -
- {windowSources.map(renderSourceCard)} -
-
-
-
-
-
- - -
-
- ); + return ( +
+
+ + + + Screens + + + Windows + + +
+ +
+ {screenSources.map(renderSourceCard)} +
+
+ +
+ {windowSources.map(renderSourceCard)} +
+
+
+
+
+
+ + +
+
+ ); } diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index 7c406b2..85336fd 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -1,55 +1,55 @@ -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDown } from "lucide-react" +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Accordion = AccordionPrimitive.Root +const Accordion = AccordionPrimitive.Root; const AccordionItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -AccordionItem.displayName = "AccordionItem" + +)); +AccordionItem.displayName = "AccordionItem"; const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - -)) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - -
{children}
-
-)) -AccordionContent.displayName = AccordionPrimitive.Content.displayName + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/components/ui/audio-level-meter.tsx b/src/components/ui/audio-level-meter.tsx index 9124b90..5b6a711 100644 --- a/src/components/ui/audio-level-meter.tsx +++ b/src/components/ui/audio-level-meter.tsx @@ -1,37 +1,37 @@ -interface AudioLevelMeterProps { - level: number; // 0-100 - className?: string; -} - -const bars = [ - { threshold: 10, height: '30%' }, - { threshold: 25, height: '45%' }, - { threshold: 45, height: '60%' }, - { threshold: 65, height: '75%' }, - { threshold: 85, height: '90%' }, -]; - -function getBarColor(level: number, threshold: number) { - if (!level || level < threshold) return 'bg-slate-700'; - if (threshold > 80) return 'bg-red-500'; - if (threshold > 60) return 'bg-yellow-500'; - if (threshold > 40) return 'bg-green-500'; - return 'bg-emerald-500'; -} - -export function AudioLevelMeter({ level, className = "" }: AudioLevelMeterProps) { - return ( -
- {bars.map((bar, index) => ( -
= bar.threshold ? bar.height : '15%', - opacity: level >= bar.threshold ? 1 : 0.4, - }} - /> - ))} -
- ); -} +interface AudioLevelMeterProps { + level: number; // 0-100 + className?: string; +} + +const bars = [ + { threshold: 10, height: "30%" }, + { threshold: 25, height: "45%" }, + { threshold: 45, height: "60%" }, + { threshold: 65, height: "75%" }, + { threshold: 85, height: "90%" }, +]; + +function getBarColor(level: number, threshold: number) { + if (!level || level < threshold) return "bg-slate-700"; + if (threshold > 80) return "bg-red-500"; + if (threshold > 60) return "bg-yellow-500"; + if (threshold > 40) return "bg-green-500"; + return "bg-emerald-500"; +} + +export function AudioLevelMeter({ level, className = "" }: AudioLevelMeterProps) { + return ( +
+ {bars.map((bar, index) => ( +
= bar.threshold ? bar.height : "15%", + opacity: level >= bar.threshold ? 1 : 0.4, + }} + /> + ))} +
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 65d4fcd..c460601 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,57 +1,50 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - return ( - - ) - } -) -Button.displayName = "Button" + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index cabfbfc..2935ed4 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,76 +1,55 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = "Card"; -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardHeader.displayName = "CardHeader"; -const CardTitle = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardTitle.displayName = "CardTitle" +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardTitle.displayName = "CardTitle"; -const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardDescription.displayName = "CardDescription" +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardDescription.displayName = "CardDescription"; -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardContent.displayName = "CardContent" +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardContent.displayName = "CardContent"; -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/src/components/ui/content-clamp.tsx b/src/components/ui/content-clamp.tsx index 1597f1b..c0600c1 100644 --- a/src/components/ui/content-clamp.tsx +++ b/src/components/ui/content-clamp.tsx @@ -1,86 +1,81 @@ -"use client" - -import * as React from "react" - -import { cn } from "@/lib/utils" -import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from "./popover" - -interface ContentClampProps extends React.HTMLAttributes { - children: React.ReactNode - truncateLength?: number -} - -function ContentClamp({ - children, - className, - truncateLength = 50, - ...props -}: ContentClampProps) { - const text = typeof children === "string" ? children : String(children ?? "") - const isTruncated = text.length > truncateLength - - const [open, setOpen] = React.useState(false) - const timeoutRef = React.useRef(null) - - const handleMouseEnter = () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = null - } - setOpen(true) - } - - const handleMouseLeave = () => { - timeoutRef.current = setTimeout(() => { - setOpen(false) - }, 100) - } - - React.useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - } - }, []) - - if (!isTruncated) { - return ( -
- {children} -
- ) - } - - const truncatedText = text.slice(0, truncateLength) + "..." - - return ( - - - e.preventDefault()} - {...props} - > - {truncatedText} - - - e.preventDefault()} - onClick={(e) => e.stopPropagation()} - > - - {children} - - - ) -} - -export { ContentClamp } +"use client"; + +import * as React from "react"; + +import { cn } from "@/lib/utils"; +import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from "./popover"; + +interface ContentClampProps extends React.HTMLAttributes { + children: React.ReactNode; + truncateLength?: number; +} + +function ContentClamp({ children, className, truncateLength = 50, ...props }: ContentClampProps) { + const text = typeof children === "string" ? children : String(children ?? ""); + const isTruncated = text.length > truncateLength; + + const [open, setOpen] = React.useState(false); + const timeoutRef = React.useRef(null); + + const handleMouseEnter = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + setOpen(true); + }; + + const handleMouseLeave = () => { + timeoutRef.current = setTimeout(() => { + setOpen(false); + }, 100); + }; + + React.useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + if (!isTruncated) { + return ( +
+ {children} +
+ ); + } + + const truncatedText = text.slice(0, truncateLength) + "..."; + + return ( + + + e.preventDefault()} + {...props} + > + {truncatedText} + + + e.preventDefault()} + onClick={(e) => e.stopPropagation()} + > + + {children} + + + ); +} + +export { ContentClamp }; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 213f877..0c3efbb 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,120 +1,102 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { - Dialog, - DialogPortal, - DialogOverlay, - DialogTrigger, - DialogClose, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index e804bca..c15187d 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -1,199 +1,186 @@ -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const DropdownMenu = DropdownMenuPrimitive.Root +const DropdownMenu = DropdownMenuPrimitive.Root; -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; -const DropdownMenuGroup = DropdownMenuPrimitive.Group +const DropdownMenuGroup = DropdownMenuPrimitive.Group; -const DropdownMenuPortal = DropdownMenuPrimitive.Portal +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; -const DropdownMenuSub = DropdownMenuPrimitive.Sub +const DropdownMenuSub = DropdownMenuPrimitive.Sub; -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - } + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } >(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)) -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName + + {children} + + +)); +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName + +)); +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - } + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } >(({ className, inset, ...props }, ref) => ( - svg]:size-4 [&>svg]:shrink-0", - inset && "pl-8", - className - )} - {...props} - /> -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className, + )} + {...props} + /> +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - } + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } >(({ className, inset, ...props }, ref) => ( - -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( - - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -} + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 1095513..68ac869 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,24 +1,23 @@ -import * as React from "react" -import { cn } from "@/lib/utils" - -export interface InputProps - extends React.InputHTMLAttributes {} - -const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" - -export { Input } +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface InputProps extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/src/components/ui/item-content.tsx b/src/components/ui/item-content.tsx index ebaae9d..b87d4f5 100644 --- a/src/components/ui/item-content.tsx +++ b/src/components/ui/item-content.tsx @@ -15,4 +15,4 @@ function ItemContent({ children, classes }: ItemContentProps) { ); } -export default ItemContent; \ No newline at end of file +export default ItemContent; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index f918cb2..fd6cbcc 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -1,23 +1,20 @@ -import * as React from "react" -import { cn } from "@/lib/utils" - -export interface LabelProps - extends React.LabelHTMLAttributes {} - -const Label = React.forwardRef( - ({ className, ...props }, ref) => { - return ( -