diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 0c6eb8b19..ac29d45af 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -245,6 +245,17 @@ interface Window { canceled?: boolean; error?: string; }>; + getPathForFile: (file: File) => string; + loadProjectFileFromPath: (filePath: string) => Promise<{ + success: boolean; + path?: string; + project?: unknown; + message?: string; + canceled?: boolean; + error?: string; + }>; + onMenuNewProject: (callback: () => void) => () => void; + onMenuImportVideo: (callback: () => void) => () => void; onMenuLoadProject: (callback: () => void) => () => void; onMenuSaveProject: (callback: () => void) => () => void; onMenuSaveProjectAs: (callback: () => void) => () => void; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 2c01ca28b..15a6539a7 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -48,7 +48,17 @@ const PROJECT_FILE_EXTENSION = "openscreen"; export const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json"); const RECORDING_FILE_PREFIX = "recording-"; const RECORDING_SESSION_SUFFIX = ".session.json"; -const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set([".webm", ".mp4", ".mov", ".avi", ".mkv"]); +const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set([ + ".webm", + ".mp4", + ".mov", + ".avi", + ".mkv", + ".m4v", + ".wmv", + ".flv", + ".ts", +]); const PREVIEW_AUDIO_DIR = path.join(app.getPath("userData"), "preview-audio"); const nativeMacCaptureEvents = new EventEmitter(); @@ -1430,10 +1440,10 @@ export function registerIpcHandlers( }); ipcMain.handle("switch-to-editor", () => { - const mainWin = getMainWindow(); - if (mainWin) { - mainWin.close(); - } + // createEditorWindow is createEditorWindowWrapper — it already closes + // the current mainWindow (the HUD) before opening the editor. Closing + // it here too causes a double-close which leaves ghost transparent + // windows and makes the HUD shadow compound on each cycle. createEditorWindow(); }); @@ -1453,16 +1463,19 @@ export function registerIpcHandlers( return; } - if (!overlayWindow.isVisible()) { - overlayWindow.showInactive(); - } - + // Wait for the first frame to be painted before showing the window. + // Showing before ready-to-show produces a black rectangle flash because + // Chromium hasn't rendered any pixels yet. if (overlayWindow.webContents.isLoading()) { await new Promise((resolve) => { - overlayWindow.webContents.once("did-finish-load", () => resolve()); + overlayWindow.once("ready-to-show", resolve); }); } + if (!overlayWindow.isVisible()) { + overlayWindow.showInactive(); + } + overlayWindow.webContents.send("countdown-overlay-value", value, runId); }); @@ -2430,7 +2443,7 @@ export function registerIpcHandlers( filters: [ { name: mainT("dialogs", "fileDialogs.videoFiles"), - extensions: ["webm", "mp4", "mov", "avi", "mkv"], + extensions: ["webm", "mp4", "mov", "avi", "mkv", "m4v", "wmv", "flv", "ts"], }, { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, ], @@ -2658,6 +2671,51 @@ export function registerIpcHandlers( } } + ipcMain.handle("load-project-file-from-path", async (_event, filePath: string) => { + return loadProjectFileFromPath(filePath); + }); + + async function loadProjectFileFromPath(filePath: string): Promise { + try { + if (!filePath || typeof filePath !== "string") { + return { success: false, message: "Invalid file path" }; + } + // Validate extension and readability + if (path.extname(filePath).toLowerCase() !== `.${PROJECT_FILE_EXTENSION}`) { + return { success: false, message: "Not an Openscreen project file" }; + } + const stats = await fs.stat(filePath).catch(() => null); + if (!stats?.isFile()) { + return { success: false, message: "File not found" }; + } + const content = await fs.readFile(filePath, "utf-8"); + const project = JSON.parse(content); + currentProjectPath = filePath; + + // Approve session paths; tolerate failures (e.g. video moved outside + // trusted dirs) so the project still loads and the renderer can surface + // a "video not found" error rather than a generic load failure. + let session: import("../../src/lib/recordingSession").RecordingSession | null = null; + try { + session = await getApprovedProjectSession(project, filePath); + } catch (sessionError) { + console.warn( + "[loadProjectFileFromPath] Could not approve session paths, proceeding without session:", + sessionError, + ); + } + setCurrentRecordingSessionState(session); + return { success: true, path: filePath, project }; + } catch (error) { + console.error("Failed to load project file from path:", error); + return { + success: false, + message: "Failed to load project file", + error: String(error), + }; + } + } + ipcMain.handle("load-current-project-file", async () => { return loadCurrentProjectFile(); }); @@ -2740,6 +2798,8 @@ export function registerIpcHandlers( function clearCurrentVideoPath(): ProjectPathResult { currentVideoPath = null; + currentProjectPath = null; + setCurrentRecordingSessionState(null); return { success: true }; } @@ -2814,6 +2874,7 @@ export function registerIpcHandlers( saveProjectFile, loadProjectFile, loadCurrentProjectFile, + loadProjectFileFromPath, setCurrentVideoPath, getCurrentVideoPathResult, clearCurrentVideoPath, diff --git a/electron/ipc/nativeBridge.ts b/electron/ipc/nativeBridge.ts index 7f7b24b51..425f93e1a 100644 --- a/electron/ipc/nativeBridge.ts +++ b/electron/ipc/nativeBridge.ts @@ -27,6 +27,7 @@ export interface NativeBridgeContext { ) => Promise; loadProjectFile: () => Promise; loadCurrentProjectFile: () => Promise; + loadProjectFileFromPath: (path: string) => Promise; setCurrentVideoPath: (path: string) => ProjectPathResult | Promise; getCurrentVideoPathResult: () => ProjectPathResult; clearCurrentVideoPath: () => ProjectPathResult; @@ -100,6 +101,7 @@ export function registerNativeBridgeHandlers(context: NativeBridgeContext) { saveProjectFile: context.saveProjectFile, loadProjectFile: context.loadProjectFile, loadCurrentProjectFile: context.loadCurrentProjectFile, + loadProjectFileFromPath: context.loadProjectFileFromPath, setCurrentVideoPath: context.setCurrentVideoPath, getCurrentVideoPathResult: context.getCurrentVideoPathResult, clearCurrentVideoPath: context.clearCurrentVideoPath, @@ -168,6 +170,11 @@ export function registerNativeBridgeHandlers(context: NativeBridgeContext) { requestId, await projectService.loadCurrentProjectFile(), ); + case "loadProjectFileFromPath": + return createSuccessResponse( + requestId, + await projectService.loadProjectFileFromPath(request.payload.path), + ); case "setCurrentVideoPath": return createSuccessResponse( requestId, diff --git a/electron/main.ts b/electron/main.ts index d668285f3..14255d5b3 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -114,7 +114,7 @@ function isEditorWindow(window: BrowserWindow) { } function sendEditorMenuAction( - channel: "menu-load-project" | "menu-save-project" | "menu-save-project-as", + channel: "menu-load-project" | "menu-save-project" | "menu-save-project-as" | "menu-new-project", ) { let targetWindow = BrowserWindow.getFocusedWindow() ?? mainWindow; @@ -173,6 +173,12 @@ function setupApplicationMenu() { { label: mainT("common", "actions.file") || "File", submenu: [ + { + label: mainT("dialogs", "unsavedChanges.newProject") || "New Project", + accelerator: "CmdOrCtrl+N", + click: () => sendEditorMenuAction("menu-new-project"), + }, + { type: "separator" as const }, { label: mainT("dialogs", "unsavedChanges.loadProject") || "Load Project…", accelerator: "CmdOrCtrl+O", diff --git a/electron/native-bridge/services/projectService.ts b/electron/native-bridge/services/projectService.ts index 965b4fb70..9e96aa22d 100644 --- a/electron/native-bridge/services/projectService.ts +++ b/electron/native-bridge/services/projectService.ts @@ -16,6 +16,7 @@ interface ProjectServiceOptions { ) => Promise; loadProjectFile: () => Promise; loadCurrentProjectFile: () => Promise; + loadProjectFileFromPath: (path: string) => Promise; setCurrentVideoPath: (path: string) => ProjectPathResult | Promise; getCurrentVideoPathResult: () => ProjectPathResult; clearCurrentVideoPath: () => ProjectPathResult; @@ -60,6 +61,12 @@ export class ProjectService { return result; } + async loadProjectFileFromPath(path: string) { + const result = await this.options.loadProjectFileFromPath(path); + this.getCurrentContext(); + return result; + } + async setCurrentVideoPath(path: string) { const result = await this.options.setCurrentVideoPath(path); this.getCurrentContext(); diff --git a/electron/preload.ts b/electron/preload.ts index 60916ffe0..a89d296ee 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from "electron"; +import { contextBridge, ipcRenderer, webUtils } from "electron"; import type { NativeMacRecordingRequest } from "../src/lib/nativeMacRecording"; import type { NativeWindowsRecordingRequest } from "../src/lib/nativeWindowsRecording"; import type { RecordingSession, StoreRecordedSessionInput } from "../src/lib/recordingSession"; @@ -173,9 +173,29 @@ contextBridge.exposeInMainWorld("electronAPI", { loadProjectFile: () => { return ipcRenderer.invoke("load-project-file"); }, + loadProjectFileFromPath: (filePath: string) => { + return ipcRenderer.invoke("load-project-file-from-path", filePath); + }, + getPathForFile: (file: File) => { + try { + return webUtils.getPathForFile(file); + } catch { + return ""; + } + }, loadCurrentProjectFile: () => { return ipcRenderer.invoke("load-current-project-file"); }, + onMenuNewProject: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-new-project", listener); + return () => ipcRenderer.removeListener("menu-new-project", listener); + }, + onMenuImportVideo: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("menu-import-video", listener); + return () => ipcRenderer.removeListener("menu-import-video", listener); + }, onMenuLoadProject: (callback: () => void) => { const listener = () => callback(); ipcRenderer.on("menu-load-project", listener); diff --git a/electron/windows.ts b/electron/windows.ts index 3a7350edf..5d34fe80c 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -74,7 +74,7 @@ export function createHudOverlayWindow(): BrowserWindow { alwaysOnTop: true, skipTaskbar: true, hasShadow: false, - show: !HEADLESS, + show: false, // shown via ready-to-show to avoid black rectangle flash webPreferences: { preload: path.join(__dirname, "preload.mjs"), additionalArguments: [ASSET_BASE_URL_ARG], @@ -91,6 +91,12 @@ export function createHudOverlayWindow(): BrowserWindow { win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } + // Show only once content is painted — prevents the black rectangle flash + // that appears when a transparent window is shown before its first paint. + win.once("ready-to-show", () => { + if (!HEADLESS) win.show(); + }); + win.webContents.on("did-finish-load", () => { win?.webContents.send("main-process-message", new Date().toLocaleString()); }); @@ -135,8 +141,8 @@ export function createEditorWindow(): BrowserWindow { alwaysOnTop: false, skipTaskbar: false, title: "OpenScreen", - backgroundColor: "#000000", - show: !HEADLESS, + backgroundColor: "#09090b", + show: false, // shown via ready-to-show to avoid white flash on first load webPreferences: { preload: path.join(__dirname, "preload.mjs"), additionalArguments: [ASSET_BASE_URL_ARG], @@ -150,6 +156,19 @@ export function createEditorWindow(): BrowserWindow { // Maximize the window by default win.maximize(); + // Show only once content is painted — prevents white flash on cold Vite start. + win.once("ready-to-show", () => { + if (!HEADLESS) win.show(); + }); + + // Inject dark background before any React paint so the sub-titlebar area + // never flashes white even on the very first cold Vite load. + win.webContents.on("dom-ready", () => { + win.webContents.insertCSS("html, body, #root { background: #09090b !important; }").catch(() => { + // Best-effort cosmetic; ignore if the page is mid-teardown. + }); + }); + win.webContents.on("did-finish-load", () => { win?.webContents.send("main-process-message", new Date().toLocaleString()); }); diff --git a/index.html b/index.html index ce1c274aa..510c15ddd 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,12 @@ - + - +
diff --git a/src/App.tsx b/src/App.tsx index 6f737b9b0..6c36aa8c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { LaunchWindow } from "./components/launch/LaunchWindow"; import { SourceSelector } from "./components/launch/SourceSelector"; import { Toaster } from "./components/ui/sonner"; import { TooltipProvider } from "./components/ui/tooltip"; +import { useScopedT } from "./contexts/I18nContext"; import { ShortcutsProvider } from "./contexts/ShortcutsContext"; import { loadAllCustomFonts } from "./lib/customFonts"; @@ -18,6 +19,7 @@ export default function App() { const [windowType, setWindowType] = useState( () => new URLSearchParams(window.location.search).get("windowType") || "", ); + const tEditor = useScopedT("editor"); useEffect(() => { const type = new URLSearchParams(window.location.search).get("windowType") || ""; @@ -64,7 +66,35 @@ export default function App() { case "editor": return ( - }> + + + + + + {tEditor("loadingEditor")} + + } + > diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index bea0e0167..bba5f494c 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,4 +1,4 @@ -import { Check, ChevronDown, Columns3, Languages, Rows3 } from "lucide-react"; +import { Check, ChevronDown, Clapperboard, Columns3, Languages, Rows3 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { BsPauseCircle, BsPlayCircle, BsRecordCircle } from "react-icons/bs"; @@ -344,29 +344,6 @@ export function LaunchWindow() { } }; - const openVideoFile = async () => { - const result = await window.electronAPI.openVideoFilePicker(); - - if (result.canceled) { - return; - } - - if (result.success && result.path) { - const setVideoPathResult = await nativeBridgeClient.project.setCurrentVideoPath(result.path); - if (!setVideoPathResult.success) { - console.error("Failed to set current video path:", setVideoPathResult); - return; - } - await window.electronAPI.switchToEditor(); - } - }; - - const openProjectFile = async () => { - const result = await nativeBridgeClient.project.loadProjectFile(); - if (result.canceled || !result.success) return; - await window.electronAPI.switchToEditor(); - }; - const sendHudOverlayHide = () => { if (window.electronAPI && window.electronAPI.hudOverlayHide) { window.electronAPI.hudOverlayHide(); @@ -807,29 +784,15 @@ export function LaunchWindow() { )} {!recording && ( - <> - {/* Open video file */} - - - - - {/* Open project */} - - - - + + + )} {/* Right sidebar controls */} diff --git a/src/components/launch/SourceSelector.test.tsx b/src/components/launch/SourceSelector.test.tsx index b40c46db1..82172e1ce 100644 --- a/src/components/launch/SourceSelector.test.tsx +++ b/src/components/launch/SourceSelector.test.tsx @@ -1,3 +1,4 @@ +import "@testing-library/jest-dom"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { SourceSelector } from "./SourceSelector"; diff --git a/src/components/video-editor/EditorEmptyState.tsx b/src/components/video-editor/EditorEmptyState.tsx new file mode 100644 index 000000000..511323abe --- /dev/null +++ b/src/components/video-editor/EditorEmptyState.tsx @@ -0,0 +1,202 @@ +import { AlertCircle, Film, FolderOpen, Upload, X } from "lucide-react"; +import { useCallback, useRef, useState } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { useScopedT } from "@/contexts/I18nContext"; +import { nativeBridgeClient } from "@/native"; + +interface EditorEmptyStateProps { + onVideoImported: (videoPath: string) => void; + /** Called with the loaded project data — handles both button click and drag-drop */ + onProjectOpened: (project: unknown, path: string | null) => void; +} + +type DropError = "unsupported-format" | "load-failed" | null; + +export function EditorEmptyState({ onVideoImported, onProjectOpened }: EditorEmptyStateProps) { + const te = useScopedT("editor"); + const tc = useScopedT("common"); + const [isDraggingOver, setIsDraggingOver] = useState(false); + const [dropError, setDropError] = useState(null); + // Freeze the last non-null error type so dialog content doesn't snap to the + // else-branch during the closing animation (same pattern as UnsavedChangesDialog). + const lastDropErrorRef = useRef>("unsupported-format"); + if (dropError !== null) { + lastDropErrorRef.current = dropError; + } + + const handleImportVideo = useCallback(async () => { + const result = await window.electronAPI.openVideoFilePicker(); + if (result.canceled || !result.success || !result.path) return; + + const setResult = await nativeBridgeClient.project.setCurrentVideoPath(result.path); + if (!setResult.success) return; + + onVideoImported(result.path); + }, [onVideoImported]); + + const handleLoadProject = useCallback(async () => { + const result = await nativeBridgeClient.project.loadProjectFile(); + if (result.canceled || !result.success || !result.project) return; + onProjectOpened(result.project, result.path ?? null); + }, [onProjectOpened]); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + if (e.dataTransfer.items.length > 0) { + setIsDraggingOver(true); + } + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + setIsDraggingOver(false); + } + }, []); + + const handleDrop = useCallback( + async (e: React.DragEvent) => { + e.preventDefault(); + setIsDraggingOver(false); + + const files = Array.from(e.dataTransfer.files); + if (files.length === 0) return; + + const projectFile = files.find((f) => f.name.endsWith(".openscreen")); + if (!projectFile) { + setDropError("unsupported-format"); + return; + } + + // Use Electron's webUtils.getPathForFile — File.path was removed in Electron 32+ + let filePath: string; + try { + filePath = window.electronAPI.getPathForFile(projectFile); + } catch { + setDropError("load-failed"); + return; + } + if (!filePath) { + setDropError("load-failed"); + return; + } + + let result: Awaited>; + try { + result = await window.electronAPI.loadProjectFileFromPath(filePath); + } catch { + setDropError("load-failed"); + return; + } + if (!result.success || !result.project) { + setDropError("load-failed"); + return; + } + + onProjectOpened(result.project, result.path ?? null); + }, + [onProjectOpened], + ); + + return ( +
+ {/* Drop overlay */} + {isDraggingOver && ( +
+ +

{te("emptyState.dropOverlay")}

+
+ )} + + {/* Drop error dialog */} + !open && setDropError(null)}> + + +
+ + + {lastDropErrorRef.current === "unsupported-format" + ? te("emptyState.dropErrors.unsupportedFormatTitle") + : te("emptyState.dropErrors.couldNotOpenTitle")} + +
+
+ +
+
+ +
+

+ {lastDropErrorRef.current === "unsupported-format" + ? te("emptyState.dropErrors.unsupportedFormatMessage") + : te("emptyState.dropErrors.couldNotOpenMessage")} +

+
+ + +
+
+ +
+ {/* Logo */} + + +
+

{te("emptyState.title")}

+

+ {te("emptyState.description")} +

+
+ + {/* Actions */} +
+ + +
+ +
+

{te("emptyState.supportedFormats")}

+
+ + {te("emptyState.dragDropHint")} +
+
+
+
+ ); +} diff --git a/src/components/video-editor/UnsavedChangesDialog.tsx b/src/components/video-editor/UnsavedChangesDialog.tsx index 902b1427b..f7423b620 100644 --- a/src/components/video-editor/UnsavedChangesDialog.tsx +++ b/src/components/video-editor/UnsavedChangesDialog.tsx @@ -10,6 +10,7 @@ import { useScopedT } from "@/contexts/I18nContext"; interface UnsavedChangesDialogProps { isOpen: boolean; + variant?: "close" | "newProject" | "loadProject"; onSaveAndClose: () => void; onDiscardAndClose: () => void; onCancel: () => void; @@ -17,6 +18,7 @@ interface UnsavedChangesDialogProps { export function UnsavedChangesDialog({ isOpen, + variant = "close", onSaveAndClose, onDiscardAndClose, onCancel, @@ -24,6 +26,25 @@ export function UnsavedChangesDialog({ const td = useScopedT("dialogs"); const tc = useScopedT("common"); + const detail = + variant === "newProject" + ? td("unsavedChanges.detailNewProject") + : variant === "loadProject" + ? td("unsavedChanges.detailLoadProject") + : td("unsavedChanges.detail"); + const saveLabel = + variant === "newProject" + ? td("unsavedChanges.saveAndNewProject") + : variant === "loadProject" + ? td("unsavedChanges.saveAndLoadProject") + : td("unsavedChanges.saveAndClose"); + const discardLabel = + variant === "newProject" + ? td("unsavedChanges.discardAndNewProject") + : variant === "loadProject" + ? td("unsavedChanges.discardAndLoadProject") + : td("unsavedChanges.discardAndClose"); + return ( !open && onCancel()}> @@ -42,9 +63,7 @@ export function UnsavedChangesDialog({

{td("unsavedChanges.message")}

- - {td("unsavedChanges.detail")} - + {detail}
-
- - {/* Top section: preview and contextual settings */} - -
-
-
- {/* Video preview */} -
-
- updateState({ webcamPosition: pos })} - onWebcamPositionDragEnd={commitState} - onDurationChange={setDuration} - onTimeUpdate={setCurrentTime} - currentTime={currentTime} - onPlayStateChange={setIsPlaying} - onError={setError} - wallpaper={wallpaper} - zoomRegions={zoomRegions} - selectedZoomId={selectedZoomId} - onSelectZoom={handleSelectZoom} - onZoomFocusChange={handleZoomFocusChange} - onZoomFocusDragEnd={commitState} - isPlaying={isPlaying} - showShadow={shadowIntensity > 0} - shadowIntensity={shadowIntensity} - showBlur={showBlur} - motionBlurAmount={motionBlurAmount} - borderRadius={borderRadius} - padding={padding} - cropRegion={cropRegion} - cursorRecordingData={cursorRecordingData} - trimRegions={trimRegions} - speedRegions={speedRegions} - annotationRegions={annotationOnlyRegions} - selectedAnnotationId={selectedAnnotationId} - onSelectAnnotation={handleSelectAnnotation} - onAnnotationPositionChange={handleAnnotationPositionChange} - onAnnotationSizeChange={handleAnnotationSizeChange} - blurRegions={blurRegions} - selectedBlurId={selectedBlurId} - onSelectBlur={handleSelectBlur} - onBlurPositionChange={handleAnnotationPositionChange} - onBlurSizeChange={handleAnnotationSizeChange} - onBlurDataChange={handleBlurDataPreviewChange} - onBlurDataCommit={commitState} - cursorTelemetry={cursorTelemetry} - cursorClickTimestamps={cursorClickTimestamps} - showCursor={effectiveShowCursor} - cursorSize={cursorSize} - cursorSmoothing={cursorSmoothing} - cursorMotionBlur={cursorMotionBlur} - cursorClickBounce={cursorClickBounce} - cursorClipToBounds={cursorClipToBounds} - isPreviewingZoom={isPreviewingZoom} - /> + {/* Empty state — shown when no video is loaded */} + {!videoPath && ( +
+ { + setVideoPath(toFileUrl(path)); + setVideoSourcePath(path); + setWebcamVideoPath(null); + setWebcamVideoSourcePath(null); + }} + onProjectOpened={async (project, path) => { + const restored = await applyLoadedProject(project, path); + if (!restored) { + toast.error(t("project.invalidFormat")); + } + }} + /> +
+ )} + + {videoPath && ( +
+ + {/* Top section: preview and contextual settings */} + +
+
+
+ {/* Video preview */} +
+
+ updateState({ webcamPosition: pos })} + onWebcamPositionDragEnd={commitState} + onDurationChange={setDuration} + onTimeUpdate={setCurrentTime} + currentTime={currentTime} + onPlayStateChange={setIsPlaying} + onError={setError} + wallpaper={wallpaper} + zoomRegions={zoomRegions} + selectedZoomId={selectedZoomId} + onSelectZoom={handleSelectZoom} + onZoomFocusChange={handleZoomFocusChange} + onZoomFocusDragEnd={commitState} + isPlaying={isPlaying} + showShadow={shadowIntensity > 0} + shadowIntensity={shadowIntensity} + showBlur={showBlur} + motionBlurAmount={motionBlurAmount} + borderRadius={borderRadius} + padding={padding} + cropRegion={cropRegion} + cursorRecordingData={cursorRecordingData} + trimRegions={trimRegions} + speedRegions={speedRegions} + annotationRegions={annotationOnlyRegions} + selectedAnnotationId={selectedAnnotationId} + onSelectAnnotation={handleSelectAnnotation} + onAnnotationPositionChange={handleAnnotationPositionChange} + onAnnotationSizeChange={handleAnnotationSizeChange} + blurRegions={blurRegions} + selectedBlurId={selectedBlurId} + onSelectBlur={handleSelectBlur} + onBlurPositionChange={handleAnnotationPositionChange} + onBlurSizeChange={handleAnnotationSizeChange} + onBlurDataChange={handleBlurDataPreviewChange} + onBlurDataCommit={commitState} + cursorTelemetry={cursorTelemetry} + cursorClickTimestamps={cursorClickTimestamps} + showCursor={effectiveShowCursor} + cursorSize={cursorSize} + cursorSmoothing={cursorSmoothing} + cursorMotionBlur={cursorMotionBlur} + cursorClickBounce={cursorClickBounce} + cursorClipToBounds={cursorClipToBounds} + isPreviewingZoom={isPreviewingZoom} + /> +
-
- {/* Playback controls */} -
-
- + {/* Playback controls */} +
+
+ +
-
-
- pushState({ wallpaper: w })} - selectedZoomDepth={ - selectedZoomId ? zoomRegions.find((z) => z.id === selectedZoomId)?.depth : null - } - onZoomDepthChange={(depth) => selectedZoomId && handleZoomDepthChange(depth)} - selectedZoomCustomScale={ - selectedZoomId - ? (zoomRegions.find((z) => z.id === selectedZoomId)?.customScale ?? null) - : null - } - onZoomCustomScaleChange={handleZoomCustomScaleChange} - onZoomCustomScaleCommit={handleZoomCustomScaleCommit} - onZoomPreviewStart={() => setIsPreviewingZoom(true)} - onZoomPreviewEnd={() => setIsPreviewingZoom(false)} - selectedZoomFocusMode={ - selectedZoomId - ? (zoomRegions.find((z) => z.id === selectedZoomId)?.focusMode ?? "manual") - : null - } - onZoomFocusModeChange={(mode) => - selectedZoomId && handleZoomFocusModeChange(mode) - } - selectedZoomFocus={ - selectedZoomId - ? (zoomRegions.find((z) => z.id === selectedZoomId)?.focus ?? null) - : null - } - onZoomFocusCoordinateChange={(focus) => - selectedZoomId && handleZoomFocusChange(selectedZoomId, focus) - } - onZoomFocusCoordinateCommit={commitState} - hasCursorTelemetry={cursorTelemetry.length > 0} - selectedZoomId={selectedZoomId} +
+ pushState({ wallpaper: w })} + selectedZoomDepth={ + selectedZoomId + ? zoomRegions.find((z) => z.id === selectedZoomId)?.depth + : null + } + onZoomDepthChange={(depth) => selectedZoomId && handleZoomDepthChange(depth)} + selectedZoomCustomScale={ + selectedZoomId + ? (zoomRegions.find((z) => z.id === selectedZoomId)?.customScale ?? null) + : null + } + onZoomCustomScaleChange={handleZoomCustomScaleChange} + onZoomCustomScaleCommit={handleZoomCustomScaleCommit} + onZoomPreviewStart={() => setIsPreviewingZoom(true)} + onZoomPreviewEnd={() => setIsPreviewingZoom(false)} + selectedZoomFocusMode={ + selectedZoomId + ? (zoomRegions.find((z) => z.id === selectedZoomId)?.focusMode ?? "manual") + : null + } + onZoomFocusModeChange={(mode) => + selectedZoomId && handleZoomFocusModeChange(mode) + } + selectedZoomFocus={ + selectedZoomId + ? (zoomRegions.find((z) => z.id === selectedZoomId)?.focus ?? null) + : null + } + onZoomFocusCoordinateChange={(focus) => + selectedZoomId && handleZoomFocusChange(selectedZoomId, focus) + } + onZoomFocusCoordinateCommit={commitState} + hasCursorTelemetry={cursorTelemetry.length > 0} + selectedZoomId={selectedZoomId} + onZoomDelete={handleZoomDelete} + selectedZoomRotationPreset={ + selectedZoomId + ? (zoomRegions.find((z) => z.id === selectedZoomId)?.rotationPreset ?? null) + : null + } + onZoomRotationPresetChange={handleZoomRotationPresetChange} + selectedTrimId={selectedTrimId} + onTrimDelete={handleTrimDelete} + shadowIntensity={shadowIntensity} + onShadowChange={(v) => updateState({ shadowIntensity: v })} + onShadowCommit={commitState} + showBlur={showBlur} + onBlurChange={(v) => pushState({ showBlur: v })} + showTrimWaveform={showTrimWaveform} + onTrimWaveformChange={(v) => pushState({ showTrimWaveform: v })} + motionBlurAmount={motionBlurAmount} + onMotionBlurChange={(v) => updateState({ motionBlurAmount: v })} + onMotionBlurCommit={commitState} + borderRadius={borderRadius} + onBorderRadiusChange={(v) => updateState({ borderRadius: v })} + onBorderRadiusCommit={commitState} + padding={padding} + onPaddingChange={(v) => updateState({ padding: v })} + onPaddingCommit={commitState} + cropRegion={cropRegion} + onCropChange={(r) => pushState({ cropRegion: r })} + aspectRatio={aspectRatio} + hasWebcam={Boolean(webcamVideoPath)} + webcamLayoutPreset={webcamLayoutPreset} + onWebcamLayoutPresetChange={(preset) => + pushState({ + webcamLayoutPreset: preset, + webcamPosition: preset === "picture-in-picture" ? webcamPosition : null, + }) + } + webcamMaskShape={webcamMaskShape} + onWebcamMaskShapeChange={(shape) => pushState({ webcamMaskShape: shape })} + webcamSizePreset={webcamSizePreset} + onWebcamSizePresetChange={(v) => updateState({ webcamSizePreset: v })} + onWebcamSizePresetCommit={commitState} + videoElement={videoPlaybackRef.current?.video || null} + exportQuality={exportQuality} + onExportQualityChange={setExportQuality} + exportFormat={exportFormat} + onExportFormatChange={setExportFormat} + gifFrameRate={gifFrameRate} + onGifFrameRateChange={setGifFrameRate} + gifLoop={gifLoop} + onGifLoopChange={setGifLoop} + gifSizePreset={gifSizePreset} + onGifSizePresetChange={setGifSizePreset} + gifOutputDimensions={calculateOutputDimensions( + calculateEffectiveSourceDimensions( + videoPlaybackRef.current?.video?.videoWidth || + DEFAULT_SOURCE_DIMENSIONS.width, + videoPlaybackRef.current?.video?.videoHeight || + DEFAULT_SOURCE_DIMENSIONS.height, + cropRegion, + ).width, + calculateEffectiveSourceDimensions( + videoPlaybackRef.current?.video?.videoWidth || + DEFAULT_SOURCE_DIMENSIONS.width, + videoPlaybackRef.current?.video?.videoHeight || + DEFAULT_SOURCE_DIMENSIONS.height, + cropRegion, + ).height, + gifSizePreset, + GIF_SIZE_PRESETS, + aspectRatio === "native" + ? getNativeAspectRatioValue( + videoPlaybackRef.current?.video?.videoWidth || + DEFAULT_SOURCE_DIMENSIONS.width, + videoPlaybackRef.current?.video?.videoHeight || + DEFAULT_SOURCE_DIMENSIONS.height, + cropRegion, + ) + : getAspectRatioValue(aspectRatio), + )} + onExport={handleOpenExportDialog} + onExportPanelOpen={() => { + setSelectedZoomId(null); + setSelectedTrimId(null); + setSelectedSpeedId(null); + }} + selectedAnnotationId={selectedAnnotationId} + annotationRegions={annotationOnlyRegions} + onAnnotationContentChange={handleAnnotationContentChange} + onAnnotationTypeChange={handleAnnotationTypeChange} + onAnnotationStyleChange={handleAnnotationStyleChange} + onAnnotationFigureDataChange={handleAnnotationFigureDataChange} + onAnnotationDuplicate={handleAnnotationDuplicate} + onAnnotationDelete={handleAnnotationDelete} + selectedBlurId={selectedBlurId} + blurRegions={blurRegions} + onBlurDataChange={handleBlurDataPanelChange} + onBlurDataCommit={commitState} + onBlurDelete={handleAnnotationDelete} + selectedSpeedId={selectedSpeedId} + selectedSpeedValue={ + selectedSpeedId + ? (speedRegions.find((r) => r.id === selectedSpeedId)?.speed ?? null) + : null + } + onSpeedChange={handleSpeedChange} + onSpeedDelete={handleSpeedDelete} + unsavedExport={unsavedExport} + onSaveUnsavedExport={handleSaveUnsavedExport} + onSaveDiagnostic={handleSaveDiagnostic} + showCursor={showCursor} + onShowCursorChange={setShowCursor} + cursorSize={cursorSize} + onCursorSizeChange={setCursorSize} + cursorSmoothing={cursorSmoothing} + onCursorSmoothingChange={setCursorSmoothing} + cursorMotionBlur={cursorMotionBlur} + onCursorMotionBlurChange={setCursorMotionBlur} + cursorClickBounce={cursorClickBounce} + onCursorClickBounceChange={setCursorClickBounce} + cursorClipToBounds={cursorClipToBounds} + onCursorClipToBoundsChange={setCursorClipToBounds} + hasCursorData={ + cursorTelemetry.length > 0 || + hasNativeCursorRecordingData(cursorRecordingData) + } + showCursorSettings={showCursorSettings} + /> +
+
+ + + +
+
+ + {/* Full-width timeline */} + +
+ z.id === selectedZoomId)?.rotationPreset ?? null) - : null - } - onZoomRotationPresetChange={handleZoomRotationPresetChange} - selectedTrimId={selectedTrimId} + selectedZoomId={selectedZoomId} + onSelectZoom={handleSelectZoom} + trimRegions={trimRegions} + onTrimAdded={handleTrimAdded} + onTrimSpanChange={handleTrimSpanChange} onTrimDelete={handleTrimDelete} - shadowIntensity={shadowIntensity} - onShadowChange={(v) => updateState({ shadowIntensity: v })} - onShadowCommit={commitState} - showBlur={showBlur} - onBlurChange={(v) => pushState({ showBlur: v })} - showTrimWaveform={showTrimWaveform} - onTrimWaveformChange={(v) => pushState({ showTrimWaveform: v })} - motionBlurAmount={motionBlurAmount} - onMotionBlurChange={(v) => updateState({ motionBlurAmount: v })} - onMotionBlurCommit={commitState} - borderRadius={borderRadius} - onBorderRadiusChange={(v) => updateState({ borderRadius: v })} - onBorderRadiusCommit={commitState} - padding={padding} - onPaddingChange={(v) => updateState({ padding: v })} - onPaddingCommit={commitState} - cropRegion={cropRegion} - onCropChange={(r) => pushState({ cropRegion: r })} - aspectRatio={aspectRatio} - hasWebcam={Boolean(webcamVideoPath)} - webcamLayoutPreset={webcamLayoutPreset} - onWebcamLayoutPresetChange={(preset) => - pushState({ - webcamLayoutPreset: preset, - webcamPosition: preset === "picture-in-picture" ? webcamPosition : null, - }) - } - webcamMaskShape={webcamMaskShape} - onWebcamMaskShapeChange={(shape) => pushState({ webcamMaskShape: shape })} - webcamSizePreset={webcamSizePreset} - onWebcamSizePresetChange={(v) => updateState({ webcamSizePreset: v })} - onWebcamSizePresetCommit={commitState} - videoElement={videoPlaybackRef.current?.video || null} - exportQuality={exportQuality} - onExportQualityChange={setExportQuality} - exportFormat={exportFormat} - onExportFormatChange={setExportFormat} - gifFrameRate={gifFrameRate} - onGifFrameRateChange={setGifFrameRate} - gifLoop={gifLoop} - onGifLoopChange={setGifLoop} - gifSizePreset={gifSizePreset} - onGifSizePresetChange={setGifSizePreset} - gifOutputDimensions={calculateOutputDimensions( - calculateEffectiveSourceDimensions( - videoPlaybackRef.current?.video?.videoWidth || - DEFAULT_SOURCE_DIMENSIONS.width, - videoPlaybackRef.current?.video?.videoHeight || - DEFAULT_SOURCE_DIMENSIONS.height, - cropRegion, - ).width, - calculateEffectiveSourceDimensions( - videoPlaybackRef.current?.video?.videoWidth || - DEFAULT_SOURCE_DIMENSIONS.width, - videoPlaybackRef.current?.video?.videoHeight || - DEFAULT_SOURCE_DIMENSIONS.height, - cropRegion, - ).height, - gifSizePreset, - GIF_SIZE_PRESETS, - aspectRatio === "native" - ? getNativeAspectRatioValue( - videoPlaybackRef.current?.video?.videoWidth || - DEFAULT_SOURCE_DIMENSIONS.width, - videoPlaybackRef.current?.video?.videoHeight || - DEFAULT_SOURCE_DIMENSIONS.height, - cropRegion, - ) - : getAspectRatioValue(aspectRatio), - )} - onExport={handleOpenExportDialog} - onExportPanelOpen={() => { - setSelectedZoomId(null); - setSelectedTrimId(null); - setSelectedSpeedId(null); - }} - selectedAnnotationId={selectedAnnotationId} + selectedTrimId={selectedTrimId} + onSelectTrim={handleSelectTrim} + speedRegions={speedRegions} + onSpeedAdded={handleSpeedAdded} + onSpeedSpanChange={handleSpeedSpanChange} + onSpeedDelete={handleSpeedDelete} + selectedSpeedId={selectedSpeedId} + onSelectSpeed={handleSelectSpeed} annotationRegions={annotationOnlyRegions} - onAnnotationContentChange={handleAnnotationContentChange} - onAnnotationTypeChange={handleAnnotationTypeChange} - onAnnotationStyleChange={handleAnnotationStyleChange} - onAnnotationFigureDataChange={handleAnnotationFigureDataChange} - onAnnotationDuplicate={handleAnnotationDuplicate} + onAnnotationAdded={handleAnnotationAdded} + onAnnotationSpanChange={handleAnnotationSpanChange} onAnnotationDelete={handleAnnotationDelete} - selectedBlurId={selectedBlurId} + selectedAnnotationId={selectedAnnotationId} + onSelectAnnotation={handleSelectAnnotation} blurRegions={blurRegions} - onBlurDataChange={handleBlurDataPanelChange} - onBlurDataCommit={commitState} + onBlurAdded={handleBlurAdded} + onBlurSpanChange={handleAnnotationSpanChange} onBlurDelete={handleAnnotationDelete} - selectedSpeedId={selectedSpeedId} - selectedSpeedValue={ - selectedSpeedId - ? (speedRegions.find((r) => r.id === selectedSpeedId)?.speed ?? null) - : null - } - onSpeedChange={handleSpeedChange} - onSpeedDelete={handleSpeedDelete} - unsavedExport={unsavedExport} - onSaveUnsavedExport={handleSaveUnsavedExport} - onSaveDiagnostic={handleSaveDiagnostic} - showCursor={showCursor} - onShowCursorChange={setShowCursor} - cursorSize={cursorSize} - onCursorSizeChange={setCursorSize} - cursorSmoothing={cursorSmoothing} - onCursorSmoothingChange={setCursorSmoothing} - cursorMotionBlur={cursorMotionBlur} - onCursorMotionBlurChange={setCursorMotionBlur} - cursorClickBounce={cursorClickBounce} - onCursorClickBounceChange={setCursorClickBounce} - cursorClipToBounds={cursorClipToBounds} - onCursorClipToBoundsChange={setCursorClipToBounds} - hasCursorData={ - cursorTelemetry.length > 0 || hasNativeCursorRecordingData(cursorRecordingData) + selectedBlurId={selectedBlurId} + onSelectBlur={handleSelectBlur} + aspectRatio={aspectRatio} + onAspectRatioChange={(ar) => + pushState({ + aspectRatio: ar, + webcamLayoutPreset: + (isPortraitAspectRatio(ar) && webcamLayoutPreset === "dual-frame") || + (!isPortraitAspectRatio(ar) && webcamLayoutPreset === "vertical-stack") + ? "picture-in-picture" + : webcamLayoutPreset, + }) } - showCursorSettings={showCursorSettings} + videoUrl={videoPath ?? undefined} + showTrimWaveform={showTrimWaveform} />
-
-
- - -
-
- - {/* Full-width timeline */} - -
- - pushState({ - aspectRatio: ar, - webcamLayoutPreset: - (isPortraitAspectRatio(ar) && webcamLayoutPreset === "dual-frame") || - (!isPortraitAspectRatio(ar) && webcamLayoutPreset === "vertical-stack") - ? "picture-in-picture" - : webcamLayoutPreset, - }) - } - videoUrl={videoPath ?? undefined} - showTrimWaveform={showTrimWaveform} - /> -
-
-
-
+ + +
+ )} + + setConfirmDialogVariant(null)} + />
); } diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 3b5a93ec5..9f7d8a17d 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -60,13 +60,13 @@ import { type CursorTelemetryPoint, computeRotation3DContainScale, DEFAULT_ROTATION_3D, + getZoomScale, isRotation3DIdentity, lerpRotation3D, rotation3DPerspective, type SpeedRegion, type TrimRegion, ZOOM_DEPTH_SCALES, - type ZoomDepth, type ZoomFocus, type ZoomRegion, } from "./types"; @@ -84,7 +84,7 @@ import { PixiCursorOverlay, preloadCursorAssets, } from "./videoPlayback/cursorRenderer"; -import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focusUtils"; +import { clampFocusToScale } from "./videoPlayback/focusUtils"; import { layoutVideoContent as layoutVideoContentUtil } from "./videoPlayback/layoutUtils"; import { clamp01 } from "./videoPlayback/mathUtils"; import { updateOverlayIndicator } from "./videoPlayback/overlayUtils"; @@ -477,8 +477,19 @@ const VideoPlayback = forwardRef( [onDurationChange, syncResolvedDuration], ); - const clampFocusToStage = useCallback((focus: ZoomFocus, depth: ZoomDepth) => { - return clampFocusToStageUtil(focus, depth, stageSizeRef.current); + // IMPORTANT: must use clampFocusToScale(focus, getZoomScale(region)) here, + // NOT clampFocusToStage(focus, region.depth). + // + // region.depth is the preset slot (1×/2×/4×) and ignores customScale entirely. + // getZoomScale(region) returns customScale when set, falling back to the preset + // depth scale — so drag-to-reposition respects the actual zoom level the user + // configured, not the preset bucket it sits in. + // + // This was previously broken (invisible drag boundaries near canvas edges) and + // has been fixed twice. If you're refactoring this drag handler, keep this call + // as clampFocusForRegion(focus, region) — do not switch it back to region.depth. + const clampFocusForRegion = useCallback((focus: ZoomFocus, region: ZoomRegion) => { + return clampFocusToScale(focus, getZoomScale(region)); }, []); const updateOverlayForRegion = useCallback( @@ -672,7 +683,7 @@ const VideoPlayback = forwardRef( cx: clamp01(localX / stageWidth), cy: clamp01(localY / stageHeight), }; - const clampedFocus = clampFocusToStage(unclampedFocus, region.depth); + const clampedFocus = clampFocusForRegion(unclampedFocus, region); onZoomFocusChange(region.id, clampedFocus); updateOverlayForRegion({ ...region, focus: clampedFocus }, clampedFocus); diff --git a/src/hooks/useEditorHistory.ts b/src/hooks/useEditorHistory.ts index bfa1a4bed..25b6e21a1 100644 --- a/src/hooks/useEditorHistory.ts +++ b/src/hooks/useEditorHistory.ts @@ -130,6 +130,11 @@ export function useEditorHistory(initial: EditorState = INITIAL_EDITOR_STATE) { dirtyRef.current = false; }, []); + const resetState = useCallback((newInitial: EditorState = INITIAL_EDITOR_STATE) => { + setHistory({ past: [], present: newInitial, future: [] }); + dirtyRef.current = false; + }, []); + return { state: history.present, pushState, @@ -137,6 +142,7 @@ export function useEditorHistory(initial: EditorState = INITIAL_EDITOR_STATE) { commitState, undo, redo, + resetState, canUndo: history.past.length > 0, canRedo: history.future.length > 0, }; diff --git a/src/i18n/locales/ar/dialogs.json b/src/i18n/locales/ar/dialogs.json index 2263f600b..568c542cf 100644 --- a/src/i18n/locales/ar/dialogs.json +++ b/src/i18n/locales/ar/dialogs.json @@ -51,6 +51,12 @@ "detail": "هل تريد حفظ مشروعك قبل الإغلاق؟", "saveAndClose": "حفظ وإغلاق", "discardAndClose": "تجاهل وإغلاق", + "detailNewProject": "هل تريد حفظ مشروعك قبل إنشاء مشروع جديد؟", + "saveAndNewProject": "حفظ وإنشاء مشروع جديد", + "discardAndNewProject": "تجاهل وإنشاء مشروع جديد", + "detailLoadProject": "هل تريد حفظ مشروعك قبل تحميل مشروع آخر؟", + "saveAndLoadProject": "حفظ وتحميل مشروع", + "discardAndLoadProject": "تجاهل وتحميل مشروع", "loadProject": "تحميل مشروع...", "saveProject": "حفظ المشروع...", "saveProjectAs": "حفظ المشروع باسم..." diff --git a/src/i18n/locales/ar/editor.json b/src/i18n/locales/ar/editor.json index 1eec625b5..b3e122280 100644 --- a/src/i18n/locales/ar/editor.json +++ b/src/i18n/locales/ar/editor.json @@ -6,6 +6,7 @@ "confirm": "تأكيد" }, "loadingVideo": "جاري تحميل الفيديو...", + "loadingEditor": "جارٍ تحميل المحرر...", "errors": { "noVideoLoaded": "لم يتم تحميل أي فيديو", "videoNotReady": "الفيديو غير جاهز", @@ -42,5 +43,20 @@ "cameraNotFound": "لم يتم العثور على كاميرا.", "permissionDenied": "تم رفض إذن التسجيل. يرجى السماح بتسجيل الشاشة.", "accessibilityAllowAndRetry": "اسمح بوصول تسهيلات الاستخدام لـ OpenScreen، ثم اضغط على التسجيل مرة أخرى لبدء العد التنازلي." + }, + "emptyState": { + "title": "لا يوجد مشروع مفتوح", + "description": "استورد مقطع فيديو للبدء في التحرير، أو حمّل مشروع OpenScreen موجود.", + "importVideoButton": "استيراد ملف فيديو...", + "loadProjectButton": "تحميل مشروع...", + "supportedFormats": "الصيغ المدعومة: MP4، MOV، WebM، MKV، AVI، M4V، WMV", + "dragDropHint": "أو اسحب وأفلت ملف مشروع .openscreen هنا", + "dropOverlay": "أفلت ملف المشروع لفتحه", + "dropErrors": { + "unsupportedFormatTitle": "تنسيق غير مدعوم", + "unsupportedFormatMessage": "يمكن إسقاط ملفات مشروع .openscreen فقط هنا. لاستيراد مقطع فيديو، استخدم زر \"استيراد ملف فيديو...\" بدلاً من ذلك.", + "couldNotOpenTitle": "تعذّر فتح الملف", + "couldNotOpenMessage": "تعذّر فتح ملف المشروع. ربما تم نقل الفيديو المرجعي أو حذفه." + } } } diff --git a/src/i18n/locales/ar/settings.json b/src/i18n/locales/ar/settings.json index 60fd62759..9ddc2ba7d 100644 --- a/src/i18n/locales/ar/settings.json +++ b/src/i18n/locales/ar/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "حفظ المشروع", - "load": "تحميل المشروع" + "load": "تحميل المشروع", + "new": "مشروع جديد" }, "export": { "videoButton": "تصدير الفيديو", diff --git a/src/i18n/locales/en/dialogs.json b/src/i18n/locales/en/dialogs.json index a84b5fda8..f4d8d4e59 100644 --- a/src/i18n/locales/en/dialogs.json +++ b/src/i18n/locales/en/dialogs.json @@ -52,6 +52,14 @@ "detail": "Do you want to save your project before closing?", "saveAndClose": "Save & Close", "discardAndClose": "Discard & Close", + "detailNewProject": "Do you want to save your project before creating a new one?", + "saveAndNewProject": "Save & New Project", + "discardAndNewProject": "Discard & New Project", + "detailLoadProject": "Do you want to save your project before loading another one?", + "saveAndLoadProject": "Save & Load Project", + "discardAndLoadProject": "Discard & Load Project", + "newProject": "New Project", + "importVideo": "Import Video File…", "loadProject": "Load Project…", "saveProject": "Save Project…", "saveProjectAs": "Save Project As…" diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json index aad37003f..ebd9a5d5f 100644 --- a/src/i18n/locales/en/editor.json +++ b/src/i18n/locales/en/editor.json @@ -6,6 +6,7 @@ "confirm": "Confirm" }, "loadingVideo": "Loading video...", + "loadingEditor": "Loading editor...", "errors": { "noVideoLoaded": "No video loaded", "videoNotReady": "Video not ready", @@ -42,5 +43,20 @@ "cameraNotFound": "Camera not found.", "permissionDenied": "Recording permission denied. Please allow screen recording.", "accessibilityAllowAndRetry": "Allow Accessibility access for OpenScreen, then press record again to start the countdown." + }, + "emptyState": { + "title": "No project open", + "description": "Import a video to start editing, or load an existing OpenScreen project.", + "importVideoButton": "Import Video File…", + "loadProjectButton": "Load Project…", + "supportedFormats": "Supported formats: MP4, MOV, WebM, MKV, AVI, M4V, WMV", + "dragDropHint": "or drag & drop a .openscreen project file here", + "dropOverlay": "Drop project file to open", + "dropErrors": { + "unsupportedFormatTitle": "Unsupported Format", + "unsupportedFormatMessage": "Only .openscreen project files can be dropped here. To import a video file, use the \"Import Video File…\" button on this screen.", + "couldNotOpenTitle": "Could Not Open File", + "couldNotOpenMessage": "The project file could not be opened. The video it references may have been moved or deleted." + } } } diff --git a/src/i18n/locales/en/launch.json b/src/i18n/locales/en/launch.json index 1d7548f26..0509752ee 100644 --- a/src/i18n/locales/en/launch.json +++ b/src/i18n/locales/en/launch.json @@ -9,7 +9,8 @@ "openVideoFile": "Open video file", "openProject": "Open project", "useVerticalTray": "Use vertical tray", - "useHorizontalTray": "Use horizontal tray" + "useHorizontalTray": "Use horizontal tray", + "openStudio": "Open Studio" }, "audio": { "enableSystemAudio": "Enable system audio", diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 2d0555391..02d44c658 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "Save Project", - "load": "Load Project" + "load": "Load Project", + "new": "New Project" }, "export": { "videoButton": "Export Video", diff --git a/src/i18n/locales/es/dialogs.json b/src/i18n/locales/es/dialogs.json index f8a5e63ff..0b9090f14 100644 --- a/src/i18n/locales/es/dialogs.json +++ b/src/i18n/locales/es/dialogs.json @@ -51,6 +51,12 @@ "detail": "¿Deseas guardar tu proyecto antes de cerrar?", "saveAndClose": "Guardar y cerrar", "discardAndClose": "Descartar y cerrar", + "detailNewProject": "¿Deseas guardar tu proyecto antes de crear uno nuevo?", + "saveAndNewProject": "Guardar y nuevo proyecto", + "discardAndNewProject": "Descartar y nuevo proyecto", + "detailLoadProject": "¿Deseas guardar tu proyecto antes de cargar otro?", + "saveAndLoadProject": "Guardar y cargar proyecto", + "discardAndLoadProject": "Descartar y cargar proyecto", "loadProject": "Cargar proyecto…", "saveProject": "Guardar proyecto…", "saveProjectAs": "Guardar proyecto como…" diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json index 27e0caeda..16a2c8547 100644 --- a/src/i18n/locales/es/editor.json +++ b/src/i18n/locales/es/editor.json @@ -37,10 +37,26 @@ "accessibilityAllowAndRetry": "Permite el acceso de accesibilidad para OpenScreen y luego pulsa grabar de nuevo para iniciar la cuenta atrás." }, "loadingVideo": "Cargando video...", + "loadingEditor": "Cargando editor...", "newRecording": { "title": "Volver a la grabadora", "description": "Tu sesión actual ha sido guardada.", "cancel": "Cancelar", "confirm": "Confirmar" + }, + "emptyState": { + "title": "No hay proyecto abierto", + "description": "Importa un video para empezar a editar o carga un proyecto de OpenScreen existente.", + "importVideoButton": "Importar archivo de video…", + "loadProjectButton": "Cargar proyecto…", + "supportedFormats": "Formatos compatibles: MP4, MOV, WebM, MKV, AVI, M4V, WMV", + "dragDropHint": "o arrastra y suelta un archivo .openscreen aquí", + "dropOverlay": "Suelta el archivo de proyecto para abrirlo", + "dropErrors": { + "unsupportedFormatTitle": "Formato no compatible", + "unsupportedFormatMessage": "Solo se pueden soltar aquí archivos de proyecto .openscreen. Para importar un video, usa el botón \"Importar archivo de video...\" en su lugar.", + "couldNotOpenTitle": "No se pudo abrir el archivo", + "couldNotOpenMessage": "No se pudo abrir el archivo de proyecto. El video al que hace referencia puede haber sido movido o eliminado." + } } } diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 25f557d0f..21295bf80 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "Guardar proyecto", - "load": "Cargar proyecto" + "load": "Cargar proyecto", + "new": "Nuevo proyecto" }, "export": { "videoButton": "Exportar video", diff --git a/src/i18n/locales/fr/dialogs.json b/src/i18n/locales/fr/dialogs.json index dbaae385f..e67e18637 100644 --- a/src/i18n/locales/fr/dialogs.json +++ b/src/i18n/locales/fr/dialogs.json @@ -51,6 +51,12 @@ "detail": "Voulez-vous enregistrer votre projet avant de fermer ?", "saveAndClose": "Enregistrer et fermer", "discardAndClose": "Ignorer et fermer", + "detailNewProject": "Voulez-vous enregistrer votre projet avant d'en créer un nouveau ?", + "saveAndNewProject": "Enregistrer et nouveau projet", + "discardAndNewProject": "Ignorer et nouveau projet", + "detailLoadProject": "Voulez-vous enregistrer votre projet avant d'en charger un autre ?", + "saveAndLoadProject": "Enregistrer et charger un projet", + "discardAndLoadProject": "Ignorer et charger un projet", "loadProject": "Charger un projet…", "saveProject": "Enregistrer le projet…", "saveProjectAs": "Enregistrer le projet sous…" diff --git a/src/i18n/locales/fr/editor.json b/src/i18n/locales/fr/editor.json index 7195aedff..4eb57a9cc 100644 --- a/src/i18n/locales/fr/editor.json +++ b/src/i18n/locales/fr/editor.json @@ -42,5 +42,21 @@ "permissionDenied": "Permission d'enregistrement refusée. Veuillez autoriser l'enregistrement d'écran.", "accessibilityAllowAndRetry": "Autorisez l'accès Accessibilité pour OpenScreen, puis appuyez de nouveau sur enregistrer pour lancer le compte à rebours." }, - "loadingVideo": "Chargement de la vidéo..." + "loadingVideo": "Chargement de la vidéo...", + "loadingEditor": "Chargement de l'éditeur...", + "emptyState": { + "title": "Aucun projet ouvert", + "description": "Importez une vidéo pour commencer à éditer, ou chargez un projet OpenScreen existant.", + "importVideoButton": "Importer un fichier vidéo…", + "loadProjectButton": "Charger un projet…", + "supportedFormats": "Formats pris en charge : MP4, MOV, WebM, MKV, AVI, M4V, WMV", + "dragDropHint": "ou glissez-déposez un fichier .openscreen ici", + "dropOverlay": "Déposez le fichier de projet pour l'ouvrir", + "dropErrors": { + "unsupportedFormatTitle": "Format non pris en charge", + "unsupportedFormatMessage": "Seuls les fichiers .openscreen peuvent être déposés ici. Pour importer une vidéo, utilisez plutôt le bouton \"Importer un fichier vidéo...\".", + "couldNotOpenTitle": "Impossible d'ouvrir le fichier", + "couldNotOpenMessage": "Le fichier de projet n'a pas pu être ouvert. La vidéo qu'il référence a peut-être été déplacée ou supprimée." + } + } } diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index fe4b8f677..f5224afdd 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "Enregistrer le projet", - "load": "Charger un projet" + "load": "Charger un projet", + "new": "Nouveau projet" }, "export": { "videoButton": "Exporter la vidéo", diff --git a/src/i18n/locales/ja-JP/dialogs.json b/src/i18n/locales/ja-JP/dialogs.json index a59cde7ce..e523ce4d9 100644 --- a/src/i18n/locales/ja-JP/dialogs.json +++ b/src/i18n/locales/ja-JP/dialogs.json @@ -52,6 +52,12 @@ "detail": "閉じる前にプロジェクトを保存しますか?", "saveAndClose": "保存して閉じる", "discardAndClose": "破棄して閉じる", + "detailNewProject": "新しいプロジェクトを作成する前に保存しますか?", + "saveAndNewProject": "保存して新規プロジェクト", + "discardAndNewProject": "破棄して新規プロジェクト", + "detailLoadProject": "別のプロジェクトを読み込む前にプロジェクトを保存しますか?", + "saveAndLoadProject": "保存してプロジェクトを読み込む", + "discardAndLoadProject": "破棄してプロジェクトを読み込む", "loadProject": "プロジェクトを読み込む…", "saveProject": "プロジェクトを保存…", "saveProjectAs": "プロジェクトを名前を付けて保存…" diff --git a/src/i18n/locales/ja-JP/editor.json b/src/i18n/locales/ja-JP/editor.json index d37e132c2..5151d1054 100644 --- a/src/i18n/locales/ja-JP/editor.json +++ b/src/i18n/locales/ja-JP/editor.json @@ -6,6 +6,7 @@ "confirm": "確認" }, "loadingVideo": "動画を読み込み中...", + "loadingEditor": "エディターを読み込み中...", "errors": { "noVideoLoaded": "動画が読み込まれていません", "videoNotReady": "動画の準備ができていません", @@ -42,5 +43,20 @@ "cameraDisconnected": "ウェブカメラが切断されました。", "cameraNotFound": "カメラが見つかりません。", "accessibilityAllowAndRetry": "OpenScreenにアクセシビリティアクセスを許可してから、もう一度録画を押してカウントダウンを開始してください。" + }, + "emptyState": { + "title": "プロジェクトが開かれていません", + "description": "動画をインポートして編集を開始するか、既存の OpenScreen プロジェクトを読み込んでください。", + "importVideoButton": "動画ファイルをインポート…", + "loadProjectButton": "プロジェクトを読み込む…", + "supportedFormats": "対応フォーマット:MP4、MOV、WebM、MKV、AVI、M4V、WMV", + "dragDropHint": ".openscreen プロジェクトファイルをここにドラッグ&ドロップ", + "dropOverlay": "プロジェクトファイルをドロップして開く", + "dropErrors": { + "unsupportedFormatTitle": "非対応フォーマット", + "unsupportedFormatMessage": "ここにドロップできるのは .openscreen プロジェクトファイルのみです。動画をインポートするには「動画ファイルをインポート...」ボタンをご使用ください。", + "couldNotOpenTitle": "ファイルを開けませんでした", + "couldNotOpenMessage": "プロジェクトファイルを開けませんでした。参照している動画が移動または削除された可能性があります。" + } } } diff --git a/src/i18n/locales/ja-JP/settings.json b/src/i18n/locales/ja-JP/settings.json index d0c5ce5e0..efecf27e2 100644 --- a/src/i18n/locales/ja-JP/settings.json +++ b/src/i18n/locales/ja-JP/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "プロジェクトを保存", - "load": "プロジェクトを読み込む" + "load": "プロジェクトを読み込む", + "new": "新規プロジェクト" }, "export": { "videoButton": "動画をエクスポート", diff --git a/src/i18n/locales/ko-KR/dialogs.json b/src/i18n/locales/ko-KR/dialogs.json index 3093cdfd2..6bff11dc9 100644 --- a/src/i18n/locales/ko-KR/dialogs.json +++ b/src/i18n/locales/ko-KR/dialogs.json @@ -51,6 +51,12 @@ "detail": "닫기 전에 프로젝트를 저장하시겠습니까?", "saveAndClose": "저장 후 닫기", "discardAndClose": "저장하지 않고 닫기", + "detailNewProject": "새 프로젝트를 만들기 전에 저장하시겠습니까?", + "saveAndNewProject": "저장 후 새 프로젝트", + "discardAndNewProject": "저장하지 않고 새 프로젝트", + "detailLoadProject": "다른 프로젝트를 불러오기 전에 저장하시겠습니까?", + "saveAndLoadProject": "저장 후 프로젝트 불러오기", + "discardAndLoadProject": "저장하지 않고 프로젝트 불러오기", "loadProject": "프로젝트 불러오기...", "saveProject": "프로젝트 저장...", "saveProjectAs": "다른 이름으로 프로젝트 저장..." diff --git a/src/i18n/locales/ko-KR/editor.json b/src/i18n/locales/ko-KR/editor.json index 13c8bfd2d..23990c386 100644 --- a/src/i18n/locales/ko-KR/editor.json +++ b/src/i18n/locales/ko-KR/editor.json @@ -6,6 +6,7 @@ "confirm": "확인" }, "loadingVideo": "비디오 로드 중...", + "loadingEditor": "편집기 로드 중...", "errors": { "noVideoLoaded": "불러온 비디오가 없습니다", "videoNotReady": "비디오가 준비되지 않았습니다", @@ -42,5 +43,20 @@ "cameraDisconnected": "웹캠 연결이 끊어졌습니다.", "cameraNotFound": "카메라를 찾을 수 없습니다.", "accessibilityAllowAndRetry": "OpenScreen의 손쉬운 사용 접근을 허용한 다음, 카운트다운을 시작하려면 다시 녹화를 누르세요." + }, + "emptyState": { + "title": "열린 프로젝트 없음", + "description": "동영상을 가져와 편집을 시작하거나 기존 OpenScreen 프로젝트를 불러오세요.", + "importVideoButton": "동영상 파일 가져오기…", + "loadProjectButton": "프로젝트 불러오기…", + "supportedFormats": "지원 형식: MP4, MOV, WebM, MKV, AVI, M4V, WMV", + "dragDropHint": ".openscreen 프로젝트 파일을 여기에 드래그 앤 드롭", + "dropOverlay": "프로젝트 파일을 드롭하여 열기", + "dropErrors": { + "unsupportedFormatTitle": "지원되지 않는 형식", + "unsupportedFormatMessage": ".openscreen 프로젝트 파일만 여기에 드롭할 수 있습니다. 동영상을 가져오려면 \"동영상 파일 가져오기...\" 버튼을 사용하세요.", + "couldNotOpenTitle": "파일을 열 수 없음", + "couldNotOpenMessage": "프로젝트 파일을 열 수 없습니다. 참조된 동영상이 이동되었거나 삭제되었을 수 있습니다." + } } } diff --git a/src/i18n/locales/ko-KR/settings.json b/src/i18n/locales/ko-KR/settings.json index b42257375..5921ca3e2 100644 --- a/src/i18n/locales/ko-KR/settings.json +++ b/src/i18n/locales/ko-KR/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "프로젝트 저장", - "load": "프로젝트 불러오기" + "load": "프로젝트 불러오기", + "new": "새 프로젝트" }, "export": { "videoButton": "비디오 내보내기", diff --git a/src/i18n/locales/ru/dialogs.json b/src/i18n/locales/ru/dialogs.json index 40b4113ab..8d1f0aa2a 100644 --- a/src/i18n/locales/ru/dialogs.json +++ b/src/i18n/locales/ru/dialogs.json @@ -51,6 +51,12 @@ "detail": "Хотите сохранить проект перед закрытием?", "saveAndClose": "Сохранить и закрыть", "discardAndClose": "Отменить и закрыть", + "detailNewProject": "Хотите сохранить проект перед созданием нового?", + "saveAndNewProject": "Сохранить и новый проект", + "discardAndNewProject": "Отменить и новый проект", + "detailLoadProject": "Хотите сохранить проект перед загрузкой другого?", + "saveAndLoadProject": "Сохранить и загрузить проект", + "discardAndLoadProject": "Отменить и загрузить проект", "loadProject": "Загрузить проект…", "saveProject": "Сохранить проект…", "saveProjectAs": "Сохранить проект как…" diff --git a/src/i18n/locales/ru/editor.json b/src/i18n/locales/ru/editor.json index 5452124f4..ff0c80b8b 100644 --- a/src/i18n/locales/ru/editor.json +++ b/src/i18n/locales/ru/editor.json @@ -6,6 +6,7 @@ "confirm": "Подтвердить" }, "loadingVideo": "Загрузка видео...", + "loadingEditor": "Загрузка редактора...", "errors": { "noVideoLoaded": "Видео не загружено", "videoNotReady": "Видео не готово", @@ -42,5 +43,20 @@ "cameraNotFound": "Камера не найдена.", "permissionDenied": "Разрешение на запись запрещено. Пожалуйста, разрешите запись экрана.", "accessibilityAllowAndRetry": "Разрешите OpenScreen доступ к Универсальному доступу, затем снова нажмите запись, чтобы начать обратный отсчет." + }, + "emptyState": { + "title": "Нет открытых проектов", + "description": "Импортируйте видео для начала редактирования или загрузите существующий проект OpenScreen.", + "importVideoButton": "Импортировать видеофайл…", + "loadProjectButton": "Загрузить проект…", + "supportedFormats": "Поддерживаемые форматы: MP4, MOV, WebM, MKV, AVI, M4V, WMV", + "dragDropHint": "или перетащите файл проекта .openscreen сюда", + "dropOverlay": "Перетащите файл проекта для открытия", + "dropErrors": { + "unsupportedFormatTitle": "Неподдерживаемый формат", + "unsupportedFormatMessage": "Сюда можно перетаскивать только файлы проекта .openscreen. Для импорта видео используйте кнопку «Импортировать видеофайл...».", + "couldNotOpenTitle": "Не удалось открыть файл", + "couldNotOpenMessage": "Не удалось открыть файл проекта. Видео, на которое он ссылается, возможно, было перемещено или удалено." + } } } diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index 43cdb4d96..e08684490 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "Сохранить проект", - "load": "Загрузить проект" + "load": "Загрузить проект", + "new": "Новый проект" }, "export": { "videoButton": "Экспорт видео", diff --git a/src/i18n/locales/tr/dialogs.json b/src/i18n/locales/tr/dialogs.json index 9fab50dfb..1b62e7072 100644 --- a/src/i18n/locales/tr/dialogs.json +++ b/src/i18n/locales/tr/dialogs.json @@ -51,6 +51,12 @@ "detail": "Kapatmadan önce projenizi kaydetmek ister misiniz?", "saveAndClose": "Kaydet ve Kapat", "discardAndClose": "Kaydetmeden Kapat", + "detailNewProject": "Yeni proje oluşturmadan önce kaydetmek ister misiniz?", + "saveAndNewProject": "Kaydet ve Yeni Proje", + "discardAndNewProject": "Kaydetmeden Yeni Proje", + "detailLoadProject": "Başka bir proje yüklemeden önce kaydetmek ister misiniz?", + "saveAndLoadProject": "Kaydet ve Proje Yükle", + "discardAndLoadProject": "Kaydetmeden Proje Yükle", "loadProject": "Proje Yükle…", "saveProject": "Proje Kaydet…", "saveProjectAs": "Farklı Kaydet…" diff --git a/src/i18n/locales/tr/editor.json b/src/i18n/locales/tr/editor.json index b50630a93..de45a180f 100644 --- a/src/i18n/locales/tr/editor.json +++ b/src/i18n/locales/tr/editor.json @@ -37,10 +37,26 @@ "accessibilityAllowAndRetry": "OpenScreen için Erişilebilirlik erişimine izin verin, ardından geri sayımı başlatmak için tekrar kayda basın." }, "loadingVideo": "Video yükleniyor...", + "loadingEditor": "Editör yükleniyor...", "newRecording": { "title": "Kaydediciye Dön", "description": "Mevcut oturumunuz kaydedildi.", "cancel": "İptal", "confirm": "Onayla" + }, + "emptyState": { + "title": "Açık proje yok", + "description": "Düzenlemeye başlamak için bir video içe aktarın veya mevcut bir OpenScreen projesi yükleyin.", + "importVideoButton": "Video Dosyası İçe Aktar…", + "loadProjectButton": "Proje Yükle…", + "supportedFormats": "Desteklenen formatlar: MP4, MOV, WebM, MKV, AVI, M4V, WMV", + "dragDropHint": "veya bir .openscreen proje dosyasını buraya sürükleyip bırakın", + "dropOverlay": "Açmak için proje dosyasını bırakın", + "dropErrors": { + "unsupportedFormatTitle": "Desteklenmeyen Format", + "unsupportedFormatMessage": "Buraya yalnızca .openscreen proje dosyaları bırakılabilir. Video içe aktarmak için \"Video Dosyası İçe Aktar...\" düğmesini kullanın.", + "couldNotOpenTitle": "Dosya Açılamadı", + "couldNotOpenMessage": "Proje dosyası açılamadı. Başvurulan video taşınmış veya silinmiş olabilir." + } } } diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json index d4970ea08..7155a0842 100644 --- a/src/i18n/locales/tr/settings.json +++ b/src/i18n/locales/tr/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "Projeyi Kaydet", - "load": "Proje Yükle" + "load": "Proje Yükle", + "new": "Yeni Proje" }, "export": { "videoButton": "Videoyu Dışa Aktar", diff --git a/src/i18n/locales/vi/dialogs.json b/src/i18n/locales/vi/dialogs.json index c94dbaa14..b7216ad1d 100644 --- a/src/i18n/locales/vi/dialogs.json +++ b/src/i18n/locales/vi/dialogs.json @@ -51,6 +51,12 @@ "detail": "Bạn có muốn lưu dự án của mình trước khi đóng không?", "saveAndClose": "Lưu & Đóng", "discardAndClose": "Bỏ qua & Đóng", + "detailNewProject": "Bạn có muốn lưu dự án trước khi tạo dự án mới không?", + "saveAndNewProject": "Lưu & Dự án mới", + "discardAndNewProject": "Bỏ qua & Dự án mới", + "detailLoadProject": "Bạn có muốn lưu dự án của mình trước khi tải dự án khác không?", + "saveAndLoadProject": "Lưu & Tải dự án", + "discardAndLoadProject": "Bỏ qua & Tải dự án", "loadProject": "Tải dự án…", "saveProject": "Lưu dự án…", "saveProjectAs": "Lưu dự án thành…" diff --git a/src/i18n/locales/vi/editor.json b/src/i18n/locales/vi/editor.json index 03e909f41..1875bb559 100644 --- a/src/i18n/locales/vi/editor.json +++ b/src/i18n/locales/vi/editor.json @@ -6,6 +6,7 @@ "confirm": "Xác nhận" }, "loadingVideo": "Đang tải video...", + "loadingEditor": "Đang tải trình chỉnh sửa...", "errors": { "noVideoLoaded": "Chưa tải video nào", "videoNotReady": "Video chưa sẵn sàng", @@ -42,5 +43,20 @@ "cameraNotFound": "Không tìm thấy máy ảnh.", "permissionDenied": "Quyền ghi hình bị từ chối. Vui lòng cho phép ghi màn hình.", "accessibilityAllowAndRetry": "Cho phép OpenScreen truy cập Trợ năng, sau đó nhấn ghi lại để bắt đầu đếm ngược." + }, + "emptyState": { + "title": "Không có dự án nào được mở", + "description": "Nhập video để bắt đầu chỉnh sửa hoặc tải một dự án OpenScreen hiện có.", + "importVideoButton": "Nhập tệp video…", + "loadProjectButton": "Tải dự án…", + "supportedFormats": "Định dạng được hỗ trợ: MP4, MOV, WebM, MKV, AVI, M4V, WMV", + "dragDropHint": "hoặc kéo và thả tệp dự án .openscreen vào đây", + "dropOverlay": "Thả tệp dự án để mở", + "dropErrors": { + "unsupportedFormatTitle": "Định dạng không được hỗ trợ", + "unsupportedFormatMessage": "Chỉ có thể thả các tệp dự án .openscreen vào đây. Để nhập video, hãy sử dụng nút \"Nhập tệp video...\" thay thế.", + "couldNotOpenTitle": "Không thể mở tệp", + "couldNotOpenMessage": "Không thể mở tệp dự án. Video mà nó tham chiếu có thể đã bị di chuyển hoặc xóa." + } } } diff --git a/src/i18n/locales/vi/settings.json b/src/i18n/locales/vi/settings.json index d0a242e55..60139ccb6 100644 --- a/src/i18n/locales/vi/settings.json +++ b/src/i18n/locales/vi/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "Lưu dự án", - "load": "Tải dự án" + "load": "Tải dự án", + "new": "Dự án mới" }, "export": { "videoButton": "Xuất Video", diff --git a/src/i18n/locales/zh-CN/dialogs.json b/src/i18n/locales/zh-CN/dialogs.json index 0385b36f3..246ea4859 100644 --- a/src/i18n/locales/zh-CN/dialogs.json +++ b/src/i18n/locales/zh-CN/dialogs.json @@ -51,6 +51,12 @@ "detail": "是否在关闭前保存项目?", "saveAndClose": "保存并关闭", "discardAndClose": "放弃并关闭", + "detailNewProject": "是否在创建新项目前保存当前项目?", + "saveAndNewProject": "保存并新建项目", + "discardAndNewProject": "放弃并新建项目", + "detailLoadProject": "是否在加载其他项目前保存当前项目?", + "saveAndLoadProject": "保存并加载项目", + "discardAndLoadProject": "放弃并加载项目", "loadProject": "加载项目…", "saveProject": "保存项目…", "saveProjectAs": "项目另存为…" diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json index 56a36f8d7..d11f1dd95 100644 --- a/src/i18n/locales/zh-CN/editor.json +++ b/src/i18n/locales/zh-CN/editor.json @@ -6,6 +6,7 @@ "confirm": "确认" }, "loadingVideo": "正在加载视频...", + "loadingEditor": "正在加载编辑器...", "errors": { "noVideoLoaded": "未加载视频", "videoNotReady": "视频未就绪", @@ -42,5 +43,20 @@ "cameraNotFound": "未找到摄像头。", "permissionDenied": "录屏权限被拒绝。请允许屏幕录制。", "accessibilityAllowAndRetry": "允许 OpenScreen 使用辅助功能权限,然后再次按录制以开始倒计时。" + }, + "emptyState": { + "title": "未打开任何项目", + "description": "导入视频开始编辑,或加载已有的 OpenScreen 项目。", + "importVideoButton": "导入视频文件…", + "loadProjectButton": "加载项目…", + "supportedFormats": "支持的格式:MP4、MOV、WebM、MKV、AVI、M4V、WMV", + "dragDropHint": "或将 .openscreen 项目文件拖放到此处", + "dropOverlay": "将项目文件拖放至此以打开", + "dropErrors": { + "unsupportedFormatTitle": "不支持的格式", + "unsupportedFormatMessage": "此处只能拖放 .openscreen 项目文件。要导入视频,请使用\"导入视频文件...\"按钮。", + "couldNotOpenTitle": "无法打开文件", + "couldNotOpenMessage": "无法打开项目文件。它引用的视频可能已被移动或删除。" + } } } diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 7322ba2c9..9455bf581 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -98,7 +98,8 @@ }, "project": { "save": "保存项目", - "load": "加载项目" + "load": "加载项目", + "new": "新建项目" }, "export": { "videoButton": "导出视频", diff --git a/src/i18n/locales/zh-TW/dialogs.json b/src/i18n/locales/zh-TW/dialogs.json index b582aba8d..f5ba7f9a4 100644 --- a/src/i18n/locales/zh-TW/dialogs.json +++ b/src/i18n/locales/zh-TW/dialogs.json @@ -51,6 +51,12 @@ "detail": "是否在關閉前儲存專案?", "saveAndClose": "儲存並關閉", "discardAndClose": "捨棄並關閉", + "detailNewProject": "是否在建立新專案前儲存目前的專案?", + "saveAndNewProject": "儲存並建立新專案", + "discardAndNewProject": "捨棄並建立新專案", + "detailLoadProject": "是否在載入其他專案前儲存目前的專案?", + "saveAndLoadProject": "儲存並載入專案", + "discardAndLoadProject": "捨棄並載入專案", "loadProject": "載入專案…", "saveProject": "儲存專案…", "saveProjectAs": "專案另存新檔…" diff --git a/src/i18n/locales/zh-TW/editor.json b/src/i18n/locales/zh-TW/editor.json index d4ad23f15..131518713 100644 --- a/src/i18n/locales/zh-TW/editor.json +++ b/src/i18n/locales/zh-TW/editor.json @@ -6,6 +6,7 @@ "confirm": "確認" }, "loadingVideo": "正在載入影片...", + "loadingEditor": "正在載入編輯器...", "errors": { "noVideoLoaded": "未載入影片", "videoNotReady": "影片未就緒", @@ -42,5 +43,20 @@ "cameraDisconnected": "網路攝影機已中斷連線。", "cameraNotFound": "找不到攝影機。", "accessibilityAllowAndRetry": "允許 OpenScreen 使用輔助使用權限,然後再次按下錄製以開始倒數。" + }, + "emptyState": { + "title": "未開啟任何專案", + "description": "匯入影片以開始編輯,或載入現有的 OpenScreen 專案。", + "importVideoButton": "匯入影片檔案…", + "loadProjectButton": "載入專案…", + "supportedFormats": "支援的格式:MP4、MOV、WebM、MKV、AVI、M4V、WMV", + "dragDropHint": "或將 .openscreen 專案檔案拖放至此", + "dropOverlay": "將專案檔案拖放至此以開啟", + "dropErrors": { + "unsupportedFormatTitle": "不支援的格式", + "unsupportedFormatMessage": "此處只能拖放 .openscreen 專案檔案。要匯入影片,請使用「匯入影片檔案...」按鈕。", + "couldNotOpenTitle": "無法開啟檔案", + "couldNotOpenMessage": "無法開啟專案檔案。它所參照的影片可能已被移動或刪除。" + } } } diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 582e2e86e..44058e7b3 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -99,7 +99,8 @@ }, "project": { "save": "儲存專案", - "load": "載入專案" + "load": "載入專案", + "new": "新增專案" }, "export": { "videoButton": "匯出影片", diff --git a/src/native/client.ts b/src/native/client.ts index 3f53ce483..9ff60d357 100644 --- a/src/native/client.ts +++ b/src/native/client.ts @@ -94,6 +94,12 @@ export const nativeBridgeClient = { domain: "project", action: "loadCurrentProjectFile", }), + loadProjectFileFromPath: (path: string) => + requireNativeBridgeData({ + domain: "project", + action: "loadProjectFileFromPath", + payload: { path }, + }), setCurrentVideoPath: (path: string) => requireNativeBridgeData({ domain: "project", diff --git a/src/native/contracts.ts b/src/native/contracts.ts index 6836095ac..77afa6f48 100644 --- a/src/native/contracts.ts +++ b/src/native/contracts.ts @@ -174,6 +174,12 @@ export type NativeBridgeRequest = payload?: EmptyPayload; requestId?: string; } + | { + domain: "project"; + action: "loadProjectFileFromPath"; + payload: { path: string }; + requestId?: string; + } | { domain: "project"; action: "setCurrentVideoPath";