@@ -195,19 +229,25 @@ export function KeyboardShortcutsDialog({
))}
-
{t("keyboardShortcuts.panTimeline")}
+
+ {t("keyboardShortcuts.panTimeline")}
+
{scrollLabels.pan}
-
{t("keyboardShortcuts.zoomTimeline")}
+
+ {t("keyboardShortcuts.zoomTimeline")}
+
{scrollLabels.zoom}
-
{t("keyboardShortcuts.cycleAnnotations")}
+
+ {t("keyboardShortcuts.cycleAnnotations")}
+
{t("keyboardShortcuts.tab")}
@@ -237,11 +277,7 @@ export function TutorialHelp() {
return (
-
+
{t("tutorial.howTrimmingWorks")}
@@ -260,8 +296,11 @@ export function TutorialHelp() {
{t("tutorial.descriptionP1")}
- {t("tutorial.descriptionRemove")} .{" "}
- {t("tutorial.descriptionP3")}
+
+ {" "}
+ {t("tutorial.descriptionRemove")}
+
+ . {t("tutorial.descriptionP3")}
{/* Visual Illustration */}
@@ -340,12 +379,20 @@ export function TutorialHelp() {
{/* Steps */}
-
{t("tutorial.addTrimStep")}
-
{t("tutorial.addTrimDesc")}
+
+ {t("tutorial.addTrimStep")}
+
+
+ {t("tutorial.addTrimDesc")}
+
-
{t("tutorial.adjustStep")}
-
{t("tutorial.adjustDesc")}
+
+ {t("tutorial.adjustStep")}
+
+
+ {t("tutorial.adjustDesc")}
+
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index f18b931f7..e2a9c1973 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -155,11 +155,11 @@ import { SettingsPanel } from "./SettingsPanel";
import { getDevOpenRecordingConfig, getSmokeExportConfig } from "./smokeExportConfig";
import { createSmokeExportProgressSampler } from "./smokeExportProgress";
import {
+ AboutDialog,
APP_HEADER_ICON_BUTTON_CLASS,
- DiscordLinkButton,
FeedbackDialog,
openExternalLink,
- RECORDLY_ISSUES_URL,
+ VYBECLIP_ISSUES_URL,
} from "./TutorialHelp";
import TimelineEditor, { type TimelineEditorHandle } from "./timeline/TimelineEditor";
import {
@@ -1036,16 +1036,37 @@ export default function VideoEditor() {
}
}, [handleSaveEditorPreset, presetNameDraft]);
+ const discardExportedTempWithWarning = useCallback((tempFilePath: string) => {
+ // Best-effort cleanup — main-process also reaps stale temp files on
+ // before-quit, but we still log failures (both rejected promises and
+ // resolved { success: false } results) so an orphaned multi-GB temp
+ // file is discoverable instead of silently consuming disk space.
+ void window.electronAPI.discardExportedTemp
+ ?.(tempFilePath)
+ ?.then((result) => {
+ if (!result?.success) {
+ console.warn(
+ `Failed to discard exported temp file at ${tempFilePath}:`,
+ result?.error ?? "unknown error",
+ );
+ }
+ })
+ .catch((discardError) => {
+ console.warn(
+ `Failed to discard exported temp file at ${tempFilePath}:`,
+ discardError,
+ );
+ });
+ }, []);
+
const clearPendingExportSave = useCallback(() => {
const pending = pendingExportSaveRef.current;
pendingExportSaveRef.current = null;
setHasPendingExportSave(false);
if (pending?.tempFilePath && typeof window !== "undefined") {
- // Best-effort cleanup — main-process also reaps stale temp files on
- // before-quit, so we ignore failures here.
- void window.electronAPI.discardExportedTemp?.(pending.tempFilePath);
+ discardExportedTempWithWarning(pending.tempFilePath);
}
- }, []);
+ }, [discardExportedTempWithWarning]);
const refreshProjectLibrary = useCallback(async () => {
try {
@@ -1411,7 +1432,7 @@ export default function VideoEditor() {
const pending = pendingExportSaveRef.current;
pendingExportSaveRef.current = null;
if (pending?.tempFilePath && typeof window !== "undefined") {
- void window.electronAPI.discardExportedTemp?.(pending.tempFilePath);
+ discardExportedTempWithWarning(pending.tempFilePath);
}
if (pendingTelemetryRetryTimeoutRef.current !== null) {
window.clearTimeout(pendingTelemetryRetryTimeoutRef.current);
@@ -1709,9 +1730,15 @@ export default function VideoEditor() {
currentProjectPath?.split(/[\\/]/).pop() ??
currentSourcePath?.split(/[\\/]/).pop() ??
"";
- const withoutExtension = fileName.replace(/\.recordly$/i, "").replace(/\.[^.]+$/, "");
+ const withoutExtension = fileName
+ .replace(/\.(vybeclip|recordly|openscreen)$/i, "")
+ .replace(/\.[^.]+$/, "");
return withoutExtension || t("editor.project.untitled", "Untitled");
}, [currentProjectPath, currentSourcePath, t]);
+ const projectFileExtension = useMemo(() => {
+ const legacyExtension = currentProjectPath?.match(/\.(recordly|openscreen)$/i)?.[1];
+ return legacyExtension ? `.${legacyExtension.toLowerCase()}` : ".vybeclip";
+ }, [currentProjectPath]);
useEffect(() => {
if (!isEditingProjectName) {
@@ -3398,7 +3425,9 @@ export default function VideoEditor() {
const handlePreviewSkipBack = useCallback(() => {
const currentMs = timelinePlayheadTime * 1000;
const keyframes = timelineRef.current?.keyframes ?? [];
- const previous = [...keyframes].reverse().find((keyframe) => keyframe.time < currentMs - 50);
+ const previous = [...keyframes]
+ .reverse()
+ .find((keyframe) => keyframe.time < currentMs - 50);
handleSeek(previous ? previous.time / 1000 : Math.max(0, timelinePlayheadTime - 5));
}, [handleSeek, timelinePlayheadTime]);
@@ -3406,9 +3435,7 @@ export default function VideoEditor() {
const currentMs = timelinePlayheadTime * 1000;
const keyframes = timelineRef.current?.keyframes ?? [];
const next = keyframes.find((keyframe) => keyframe.time > currentMs + 50);
- handleSeek(
- next ? next.time / 1000 : Math.min(timelineDuration, timelinePlayheadTime + 5),
- );
+ handleSeek(next ? next.time / 1000 : Math.min(timelineDuration, timelinePlayheadTime + 5));
}, [handleSeek, timelineDuration, timelinePlayheadTime]);
const handleSelectZoom = useCallback((id: string | null) => {
@@ -5065,7 +5092,7 @@ export default function VideoEditor() {
const openLightningIssues = useCallback(async () => {
await openExternalLink(
- RECORDLY_ISSUES_URL,
+ VYBECLIP_ISSUES_URL,
t("editor.feedback.openFailed", "Failed to open link."),
);
}, [t]);
@@ -5215,10 +5242,7 @@ export default function VideoEditor() {
volume={
audio.shouldMutePreviewVideo || audio.isCurrentClipMuted
? 0
- : Math.max(
- 0,
- Math.min(1, previewVolume * audio.embeddedSourcePreviewGain),
- )
+ : Math.max(0, Math.min(1, previewVolume * audio.embeddedSourcePreviewGain))
}
suspendRendering={suspendRendering}
/>
@@ -5251,7 +5275,7 @@ export default function VideoEditor() {
{t(
"editor.nativeCaptureUnavailable.description",
- "Your device does not support native capture. This could be for a variety of reasons we haven’t figured out yet. This doesn’t break Recordly, but it does make cursor smoothing impossible.",
+ "Your device does not support native capture. This could be for a variety of reasons we haven’t figured out yet. This doesn’t break VybeClip, but it does make cursor smoothing impossible.",
)}
@@ -5317,8 +5341,8 @@ export default function VideoEditor() {
>
-
+
- .recordly
+ {projectFileExtension}
) : (
@@ -5395,7 +5419,7 @@ export default function VideoEditor() {
{projectDisplayName}
- .recordly
+ {projectFileExtension}
)}
@@ -5736,7 +5760,9 @@ export default function VideoEditor() {
onGifLoopChange={setGifLoop}
gifSizePreset={gifSizePreset}
onGifSizePresetChange={setGifSizePreset}
- showCaptionSidecarOption={hasCaptionsForSidecar && exportFormat === "mp4"}
+ showCaptionSidecarOption={
+ hasCaptionsForSidecar && exportFormat === "mp4"
+ }
includeCaptionSidecar={includeCaptionSidecar}
onIncludeCaptionSidecarChange={setIncludeCaptionSidecar}
mp4OutputDimensions={mp4OutputDimensions}
diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx
index 488bc3a9d..a0397f834 100644
--- a/src/components/video-editor/VideoPlayback.tsx
+++ b/src/components/video-editor/VideoPlayback.tsx
@@ -525,6 +525,8 @@ const VideoPlayback = forwardRef
(
height: number;
} | null>(null);
const captionBoxRef = useRef(null);
+ const captionClipPathFrameRef = useRef(null);
+ const captionClipPathSignatureRef = useRef(null);
const currentTimeRef = useRef(0);
const zoomRegionsRef = useRef([]);
const selectedZoomIdRef = useRef(null);
@@ -736,10 +738,20 @@ const VideoPlayback = forwardRef(
captionBox.style.clipPath = "";
captionBox.style.removeProperty("-webkit-clip-path");
}
+ captionClipPathSignatureRef.current = null;
+ return;
+ }
+
+ // Coalesce recomputation triggers that land within the same animation
+ // frame (e.g. rapid caption-layout updates during active editing) by
+ // only ever scheduling one pending RAF at a time.
+ if (captionClipPathFrameRef.current !== null) {
return;
}
const frame = requestAnimationFrame(() => {
+ captionClipPathFrameRef.current = null;
+
const width = captionBox.offsetWidth;
const height = captionBox.offsetHeight;
if (width <= 0 || height <= 0) {
@@ -751,19 +763,36 @@ const VideoPlayback = forwardRef(
overlayRef.current?.clientWidth || 960,
autoCaptionSettings.maxWidth,
);
+ const radius = getCaptionScaledRadius(autoCaptionSettings.boxRadius, fontSize);
+
+ // Skip recomputing (and rewriting) the clip-path when the geometry
+ // that actually determines the squircle path hasn't changed since
+ // the last computation, avoiding redundant path string generation
+ // and style writes on every caption-layout change.
+ const signature = `${width}x${height}x${radius}`;
+ if (captionClipPathSignatureRef.current === signature) {
+ return;
+ }
+ captionClipPathSignatureRef.current = signature;
const squirclePath = getSquircleSvgPath({
x: 0,
y: 0,
width,
height,
- radius: getCaptionScaledRadius(autoCaptionSettings.boxRadius, fontSize),
+ radius,
});
captionBox.style.clipPath = `path('${squirclePath}')`;
captionBox.style.setProperty("-webkit-clip-path", `path('${squirclePath}')`);
});
+ captionClipPathFrameRef.current = frame;
- return () => cancelAnimationFrame(frame);
+ return () => {
+ if (captionClipPathFrameRef.current === frame) {
+ cancelAnimationFrame(frame);
+ captionClipPathFrameRef.current = null;
+ }
+ };
}, [activeCaptionLayout, autoCaptionSettings]);
const motionBlurStateRef = useRef(createMotionBlurState());
const webcamEnabled = webcam?.enabled ?? false;
diff --git a/src/components/video-editor/videoPlayback/zoomRegionUtils.ts b/src/components/video-editor/videoPlayback/zoomRegionUtils.ts
index 8e1cc6bf5..b167fc66b 100644
--- a/src/components/video-editor/videoPlayback/zoomRegionUtils.ts
+++ b/src/components/video-editor/videoPlayback/zoomRegionUtils.ts
@@ -203,7 +203,7 @@ function getConnectedRegionTransition(connectedPairs: ConnectedRegionPair[], tim
for (const pair of connectedPairs) {
const { currentRegion, nextRegion, transitionStart, transitionEnd } = pair;
- if (timeMs < transitionStart || timeMs > transitionEnd) {
+ if (timeMs <= transitionStart || timeMs >= transitionEnd) {
continue;
}
@@ -250,6 +250,11 @@ export function findDominantRegion(
const connectedPairs = options.connectZooms ? getConnectedRegionPairs(regions) : [];
if (options.connectZooms) {
+ const connectedTransition = getConnectedRegionTransition(connectedPairs, timeMs);
+ if (connectedTransition) {
+ return connectedTransition;
+ }
+
const connectedHold = getConnectedRegionHold(timeMs, connectedPairs);
if (connectedHold) {
return { ...connectedHold, transition: null };
diff --git a/src/components/vybe/VybeStudioPreview.tsx b/src/components/vybe/VybeStudioPreview.tsx
new file mode 100644
index 000000000..5bd459916
--- /dev/null
+++ b/src/components/vybe/VybeStudioPreview.tsx
@@ -0,0 +1,510 @@
+import {
+ ArrowClockwiseIcon,
+ ArrowSquareOutIcon,
+ CameraIcon,
+ CaretDownIcon,
+ CircleNotchIcon,
+ ExportIcon,
+ FilmSlateIcon,
+ FolderOpenIcon,
+ MicrophoneIcon,
+ MonitorIcon,
+ PlusIcon,
+ RecordIcon,
+ SlidersHorizontalIcon,
+ SparkleIcon,
+ UploadSimpleIcon,
+ WarningCircleIcon,
+ WaveformIcon,
+} from "@phosphor-icons/react";
+import { useCallback, useEffect, useState } from "react";
+import { toast } from "sonner";
+import { VybeClipLogo } from "../brand/VybeClipLogo";
+import type { ProjectLibraryEntry } from "../video-editor/ProjectBrowserDialog";
+import { toFileUrl } from "../video-editor/projectPersistence";
+import {
+ getStudioPermissionState,
+ importStudioVideo,
+ openRecentStudioProject,
+ openStudioProject,
+ type StudioPermissionState,
+ startStudioRecording,
+} from "./studioActions";
+
+const inspectorRows = [
+ ["Format", "16:9 Desktop"],
+ ["Canvas Style", "Clean Demo"],
+ ["Pointer", "Soft Highlight"],
+ ["Captions", "Smart Draft"],
+];
+
+function formatProjectDate(updatedAt: number) {
+ return new Intl.DateTimeFormat(undefined, {
+ month: "short",
+ day: "numeric",
+ year:
+ new Date(updatedAt).getFullYear() === new Date().getFullYear() ? undefined : "numeric",
+ }).format(updatedAt);
+}
+
+export function VybeStudioPreview() {
+ const isDesktop = Boolean(window.electronAPI);
+ const [projects, setProjects] = useState([]);
+ const [projectsLoading, setProjectsLoading] = useState(isDesktop);
+ const [pendingAction, setPendingAction] = useState(null);
+ const [permissions, setPermissions] = useState(null);
+ const [permissionsLoading, setPermissionsLoading] = useState(isDesktop);
+
+ const refreshProjects = useCallback(async () => {
+ if (!window.electronAPI) return;
+
+ setProjectsLoading(true);
+ try {
+ const result = await window.electronAPI.listProjectFiles();
+ if (!result.success) {
+ throw new Error(result.error || "Unable to load projects");
+ }
+ setProjects(result.entries);
+ } catch (error) {
+ console.error("Failed to load VybeClip projects:", error);
+ toast.error("Could not load your projects");
+ } finally {
+ setProjectsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void refreshProjects();
+ }, [refreshProjects]);
+
+ const refreshPermissions = useCallback(async () => {
+ if (!window.electronAPI) return;
+
+ setPermissionsLoading(true);
+ try {
+ setPermissions(await getStudioPermissionState(window.electronAPI));
+ } catch (error) {
+ console.error("Failed to load VybeClip permission status:", error);
+ } finally {
+ setPermissionsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void refreshPermissions();
+ }, [refreshPermissions]);
+
+ const runAction = useCallback(
+ async (actionName: string, action: () => Promise) => {
+ if (!window.electronAPI || pendingAction) return;
+ setPendingAction(actionName);
+ try {
+ await action();
+ } catch (error) {
+ console.error(`VybeClip ${actionName} action failed:`, error);
+ toast.error(
+ error instanceof Error ? error.message : "The action could not be completed",
+ );
+ } finally {
+ setPendingAction(null);
+ }
+ },
+ [pendingAction],
+ );
+
+ const handleRecord = useCallback(() => {
+ void runAction("record", async () => {
+ try {
+ await startStudioRecording(window.electronAPI);
+ } finally {
+ await refreshPermissions();
+ }
+ });
+ }, [refreshPermissions, runAction]);
+
+ const handleImport = useCallback(() => {
+ void runAction("import", async () => {
+ await importStudioVideo(window.electronAPI);
+ });
+ }, [runAction]);
+
+ const handleOpenProject = useCallback(() => {
+ void runAction("open-project", async () => {
+ await openStudioProject(window.electronAPI);
+ });
+ }, [runAction]);
+
+ const handleOpenRecentProject = useCallback(
+ (projectPath: string) => {
+ void runAction("open-project", async () => {
+ await openRecentStudioProject(window.electronAPI, projectPath);
+ });
+ },
+ [runAction],
+ );
+
+ const handleOpenProjectsFolder = useCallback(() => {
+ void runAction("projects-folder", async () => {
+ const result = await window.electronAPI.openProjectsDirectory();
+ if (!result.success) {
+ throw new Error(result.message || "Could not open the projects folder");
+ }
+ });
+ }, [runAction]);
+
+ const recentProjects = projects.slice(0, 5);
+ const actionsDisabled = !isDesktop || pendingAction !== null;
+ const permissionIssue =
+ permissions?.platform === "darwin" && !permissions.screenRecordingGranted
+ ? "screen"
+ : permissions?.platform === "darwin" && !permissions.accessibilityGranted
+ ? "accessibility"
+ : null;
+
+ const handleOpenPermissionSettings = useCallback(() => {
+ if (!permissionIssue) return;
+ void runAction("permissions", async () => {
+ if (permissionIssue === "screen") {
+ await window.electronAPI.openScreenRecordingPreferences();
+ } else {
+ await window.electronAPI.openAccessibilityPreferences();
+ }
+ });
+ }, [permissionIssue, runAction]);
+
+ return (
+
+
+ {permissionIssue && (
+
+
+
+
+
+ {permissionIssue === "screen"
+ ? "Screen Recording permission is required"
+ : "Accessibility permission is required for cursor tracking"}
+
+
+ Enable VybeClip in macOS Privacy & Security, then reopen the app.
+
+
+
+
+
void refreshPermissions()}
+ disabled={permissionsLoading}
+ className="grid h-7 w-7 place-items-center rounded-md border border-white/[0.08] text-[#C6C6D0] transition hover:bg-white/[0.05] disabled:opacity-45"
+ aria-label="Refresh permission status"
+ >
+
+
+
+ Open Settings
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ New Capture
+
+
+ Record a screen, window, camera and audio.
+
+
+
+
+
+
+
+
+
+
+
+ Timeline
+
+
+ 00:00
+
+ New project
+
+
+
+
+
+
+
+
+
+
+ Scene Inspector
+
+
+
+
+
+
+
+ {inspectorRows.map(([label, value]) => (
+
+
+ {label}
+
+
+ {value}
+
+
+
+ ))}
+
+
+
+ {pendingAction === "open-project" ? (
+
+ ) : (
+
+ )}
+ Open Existing Project
+
+
+
+
+ );
+}
diff --git a/src/components/vybe/studioActions.test.ts b/src/components/vybe/studioActions.test.ts
new file mode 100644
index 000000000..a58e7626e
--- /dev/null
+++ b/src/components/vybe/studioActions.test.ts
@@ -0,0 +1,111 @@
+import { describe, expect, it, vi } from "vitest";
+import {
+ importStudioVideo,
+ openRecentStudioProject,
+ openStudioProject,
+ startStudioRecording,
+} from "./studioActions";
+
+function createApi() {
+ return {
+ showRecordingControls: vi.fn().mockResolvedValue({ success: true }),
+ openSourceSelector: vi.fn().mockResolvedValue(undefined),
+ getPlatform: vi.fn().mockResolvedValue("darwin"),
+ getScreenRecordingPermissionStatus: vi
+ .fn()
+ .mockResolvedValue({ success: true, status: "granted" }),
+ getAccessibilityPermissionStatus: vi
+ .fn()
+ .mockResolvedValue({ success: true, trusted: true, prompted: false }),
+ openScreenRecordingPreferences: vi.fn().mockResolvedValue({ success: true }),
+ openAccessibilityPreferences: vi.fn().mockResolvedValue({ success: true }),
+ openVideoFilePicker: vi.fn().mockResolvedValue({ success: true, path: "/tmp/capture.mp4" }),
+ setCurrentVideoPath: vi.fn().mockResolvedValue({ success: true }),
+ switchToEditor: vi.fn().mockResolvedValue(undefined),
+ loadProjectFile: vi.fn().mockResolvedValue({ success: true, path: "/tmp/demo.vybeclip" }),
+ openProjectFileAtPath: vi
+ .fn()
+ .mockResolvedValue({ success: true, path: "/tmp/demo.vybeclip" }),
+ };
+}
+
+describe("VybeClip Studio actions", () => {
+ it("opens recording controls before the source selector", async () => {
+ const api = createApi();
+
+ await startStudioRecording(api);
+
+ expect(api.showRecordingControls).toHaveBeenCalledOnce();
+ expect(api.openSourceSelector).toHaveBeenCalledOnce();
+ expect(api.showRecordingControls.mock.invocationCallOrder[0]).toBeLessThan(
+ api.openSourceSelector.mock.invocationCallOrder[0],
+ );
+ });
+
+ it("opens Screen Recording settings instead of the HUD when permission is missing", async () => {
+ const api = createApi();
+ api.getScreenRecordingPermissionStatus.mockResolvedValue({
+ success: true,
+ status: "denied",
+ });
+
+ await expect(startStudioRecording(api)).rejects.toThrow("Enable Screen Recording");
+ expect(api.openScreenRecordingPreferences).toHaveBeenCalledOnce();
+ expect(api.showRecordingControls).not.toHaveBeenCalled();
+ });
+
+ it("opens Accessibility settings instead of the HUD when permission is missing", async () => {
+ const api = createApi();
+ api.getAccessibilityPermissionStatus.mockResolvedValue({
+ success: true,
+ trusted: false,
+ prompted: false,
+ });
+
+ await expect(startStudioRecording(api)).rejects.toThrow("Enable Accessibility");
+ expect(api.openAccessibilityPreferences).toHaveBeenCalledOnce();
+ expect(api.showRecordingControls).not.toHaveBeenCalled();
+ });
+
+ it("does not require macOS permissions on other platforms", async () => {
+ const api = createApi();
+ api.getPlatform.mockResolvedValue("win32");
+
+ await startStudioRecording(api);
+ expect(api.getScreenRecordingPermissionStatus).not.toHaveBeenCalled();
+ expect(api.showRecordingControls).toHaveBeenCalledOnce();
+ });
+
+ it("imports a selected video before opening the editor", async () => {
+ const api = createApi();
+
+ await expect(importStudioVideo(api)).resolves.toBe(true);
+ expect(api.setCurrentVideoPath).toHaveBeenCalledWith("/tmp/capture.mp4");
+ expect(api.switchToEditor).toHaveBeenCalledOnce();
+ });
+
+ it("does not open the editor when video import is cancelled", async () => {
+ const api = createApi();
+ api.openVideoFilePicker.mockResolvedValue({ success: false, canceled: true });
+
+ await expect(importStudioVideo(api)).resolves.toBe(false);
+ expect(api.setCurrentVideoPath).not.toHaveBeenCalled();
+ expect(api.switchToEditor).not.toHaveBeenCalled();
+ });
+
+ it("opens a project selected by the file picker", async () => {
+ const api = createApi();
+
+ await expect(openStudioProject(api)).resolves.toBe(true);
+ expect(api.loadProjectFile).toHaveBeenCalledOnce();
+ expect(api.switchToEditor).toHaveBeenCalledOnce();
+ });
+
+ it("opens a recent project by path", async () => {
+ const api = createApi();
+
+ await expect(openRecentStudioProject(api, "/tmp/recent.vybeclip")).resolves.toBe(true);
+ expect(api.openProjectFileAtPath).toHaveBeenCalledWith("/tmp/recent.vybeclip");
+ expect(api.switchToEditor).toHaveBeenCalledOnce();
+ });
+});
diff --git a/src/components/vybe/studioActions.ts b/src/components/vybe/studioActions.ts
new file mode 100644
index 000000000..6554514f4
--- /dev/null
+++ b/src/components/vybe/studioActions.ts
@@ -0,0 +1,98 @@
+type StudioElectronApi = Pick<
+ Window["electronAPI"],
+ | "loadProjectFile"
+ | "getAccessibilityPermissionStatus"
+ | "getPlatform"
+ | "getScreenRecordingPermissionStatus"
+ | "openAccessibilityPreferences"
+ | "openProjectFileAtPath"
+ | "openScreenRecordingPreferences"
+ | "openSourceSelector"
+ | "openVideoFilePicker"
+ | "setCurrentVideoPath"
+ | "showRecordingControls"
+ | "switchToEditor"
+>;
+
+export type StudioPermissionState = {
+ platform: string;
+ screenRecordingGranted: boolean;
+ accessibilityGranted: boolean;
+};
+
+export async function getStudioPermissionState(
+ api: StudioElectronApi,
+): Promise {
+ const platform = await api.getPlatform();
+ if (platform !== "darwin") {
+ return {
+ platform,
+ screenRecordingGranted: true,
+ accessibilityGranted: true,
+ };
+ }
+
+ const [screenRecording, accessibility] = await Promise.all([
+ api.getScreenRecordingPermissionStatus(),
+ api.getAccessibilityPermissionStatus(),
+ ]);
+
+ return {
+ platform,
+ screenRecordingGranted: screenRecording.success && screenRecording.status === "granted",
+ accessibilityGranted: accessibility.success && accessibility.trusted,
+ };
+}
+
+export async function startStudioRecording(api: StudioElectronApi) {
+ const permissions = await getStudioPermissionState(api);
+ if (!permissions.screenRecordingGranted) {
+ await api.openScreenRecordingPreferences();
+ throw new Error("Enable Screen Recording in System Settings, then reopen VybeClip");
+ }
+ if (!permissions.accessibilityGranted) {
+ await api.openAccessibilityPreferences();
+ throw new Error("Enable Accessibility in System Settings, then reopen VybeClip");
+ }
+
+ const result = await api.showRecordingControls();
+ if (!result.success) {
+ throw new Error("Could not open recording controls");
+ }
+
+ await api.openSourceSelector();
+}
+
+export async function importStudioVideo(api: StudioElectronApi) {
+ const result = await api.openVideoFilePicker();
+ if (result.canceled) return false;
+ if (!result.success || !result.path) {
+ throw new Error("Could not open that video");
+ }
+
+ await api.setCurrentVideoPath(result.path);
+ await api.switchToEditor();
+ return true;
+}
+
+export async function openStudioProject(api: StudioElectronApi) {
+ const result = await api.loadProjectFile();
+ if (result.canceled) return false;
+ if (!result.success) {
+ throw new Error(result.message || "Could not open that project");
+ }
+
+ await api.switchToEditor();
+ return true;
+}
+
+export async function openRecentStudioProject(api: StudioElectronApi, projectPath: string) {
+ const result = await api.openProjectFileAtPath(projectPath);
+ if (result.canceled) return false;
+ if (!result.success) {
+ throw new Error(result.message || "Could not open that project");
+ }
+
+ await api.switchToEditor();
+ return true;
+}
diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts
index c5cd70056..fcaedb440 100644
--- a/src/hooks/useScreenRecorder.ts
+++ b/src/hooks/useScreenRecorder.ts
@@ -499,8 +499,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
await window.electronAPI.openScreenRecordingPreferences();
alert(
options.startup
- ? "Recordly needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen Recordly."
- : "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
+ ? "VybeClip needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen VybeClip."
+ : "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen VybeClip before recording.",
);
return false;
}
@@ -522,8 +522,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
await window.electronAPI.openAccessibilityPreferences();
alert(
options.startup
- ? "Recordly also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen Recordly."
- : "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
+ ? "VybeClip also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen VybeClip."
+ : "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen VybeClip before recording.",
);
return false;
diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json
index 98bb2c7aa..125afee1f 100644
--- a/src/i18n/locales/en/common.json
+++ b/src/i18n/locales/en/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Recordly Editor",
+ "name": "VybeClip",
+ "editorTitle": "VybeClip Editor",
"subtitle": "Screen recording and editing",
"language": "Language",
"manageRecordings": "Open recordings folder"
diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json
index 902522306..79c0067cb 100644
--- a/src/i18n/locales/en/editor.json
+++ b/src/i18n/locales/en/editor.json
@@ -115,7 +115,7 @@
},
"nativeCaptureUnavailable": {
"title": "Nothing’s broken, but we won’t be able to render an animated cursor overlay.",
- "description": "Your device does not support native capture. This could be for a variety of reasons we haven’t figured out yet. This doesn’t break Recordly, but it does make cursor smoothing impossible.",
+ "description": "Your device does not support native capture. This could be for a variety of reasons we haven’t figured out yet. This doesn’t break VybeClip, but it does make cursor smoothing impossible.",
"confirm": "Okay"
},
"exportStatus": {
diff --git a/src/i18n/locales/en/extensions.json b/src/i18n/locales/en/extensions.json
index b1be4045e..90b798ebe 100644
--- a/src/i18n/locales/en/extensions.json
+++ b/src/i18n/locales/en/extensions.json
@@ -20,7 +20,10 @@
"status": {
"enabled": "Enabled",
"disabled": "Disabled",
- "installed": "Installed"
+ "installed": "Installed",
+ "comingSoon": "Coming soon",
+ "comingSoonFull": "Installing is coming soon",
+ "installDisabledHint": "Marketplace installs are disabled during the beta"
},
"detail": {
"by": "By {{author}}",
@@ -46,6 +49,9 @@
"count": "{{count}} extension",
"countPlural": "{{count}} extensions"
},
+ "browse": {
+ "installDisabledNotice": "Installing extensions from the marketplace is temporarily disabled during the testing phase. You can still browse what's available — installing is coming soon. Locally bundled extensions keep working normally."
+ },
"toast": {
"installedAndEnabled": "Extension installed and enabled",
"uninstalled": "Uninstalled {{name}}",
diff --git a/src/i18n/locales/en/launch.json b/src/i18n/locales/en/launch.json
index 9dfcbe038..27468c7ed 100644
--- a/src/i18n/locales/en/launch.json
+++ b/src/i18n/locales/en/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "Checking for updates...",
"downloadingTitle": "Downloading update...",
"errorTitle": "Update check failed. Click to retry.",
- "upToDateTitle": "Recordly {{version}} is up to date.",
- "availableTitle": "Recordly {{version}} is available.",
+ "upToDateTitle": "VybeClip {{version}} is up to date.",
+ "availableTitle": "VybeClip {{version}} is available.",
"availableGenericTitle": "An update is available."
}
},
@@ -67,10 +67,10 @@
"share": "Share"
},
"permissions": {
- "screenRecordingNeeded": "Recordly needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen Recordly.",
- "screenRecordingMissing": "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
- "accessibilityNeeded": "Recordly also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen Recordly.",
- "accessibilityMissing": "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
+ "screenRecordingNeeded": "VybeClip needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen VybeClip.",
+ "screenRecordingMissing": "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen VybeClip before recording.",
+ "accessibilityNeeded": "VybeClip also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen VybeClip.",
+ "accessibilityMissing": "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen VybeClip before recording.",
"selectSource": "Please select a source to record",
"systemAudioUnavailable": "System audio is not available for this source. Recording will continue without system audio.",
"microphoneDenied": "Microphone access was denied. Recording will continue without microphone audio.",
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json
index dc24cd97a..a67253403 100644
--- a/src/i18n/locales/en/settings.json
+++ b/src/i18n/locales/en/settings.json
@@ -85,7 +85,7 @@
"connectedZoomDuration": "Connected Zoom Duration",
"connectedZoomEasing": "Connected Pan Curve",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "Glide",
"smooth": "Smooth",
"snappy": "Snappy",
diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json
index f74c23773..6bcb7d04a 100644
--- a/src/i18n/locales/es/common.json
+++ b/src/i18n/locales/es/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Editor de Recordly",
+ "name": "VybeClip",
+ "editorTitle": "Editor de VybeClip",
"subtitle": "Grabación de pantalla y edición",
"language": "Idioma",
"manageRecordings": "Abrir carpeta de grabaciones"
diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json
index 40373f1a6..ef204f93f 100644
--- a/src/i18n/locales/es/editor.json
+++ b/src/i18n/locales/es/editor.json
@@ -115,7 +115,7 @@
},
"nativeCaptureUnavailable": {
"title": "Nada está roto, pero no podremos renderizar una superposición de cursor animada.",
- "description": "Tu dispositivo no es compatible con la captura nativa. Esto puede deberse a varias razones que todavía no hemos identificado. Recordly seguirá funcionando, pero hará imposible el suavizado del cursor.",
+ "description": "Tu dispositivo no es compatible con la captura nativa. Esto puede deberse a varias razones que todavía no hemos identificado. VybeClip seguirá funcionando, pero hará imposible el suavizado del cursor.",
"confirm": "Entendido"
},
"exportStatus": {
diff --git a/src/i18n/locales/es/launch.json b/src/i18n/locales/es/launch.json
index a458844a2..5502c7433 100644
--- a/src/i18n/locales/es/launch.json
+++ b/src/i18n/locales/es/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "Buscando actualizaciones...",
"downloadingTitle": "Descargando actualización...",
"errorTitle": "La comprobación de actualizaciones falló. Haz clic para reintentar.",
- "upToDateTitle": "Recordly {{version}} está actualizado.",
- "availableTitle": "Recordly {{version}} está disponible.",
+ "upToDateTitle": "VybeClip {{version}} está actualizado.",
+ "availableTitle": "VybeClip {{version}} está disponible.",
"availableGenericTitle": "Hay una actualización disponible."
}
},
@@ -67,10 +67,10 @@
"share": "Compartir"
},
"permissions": {
- "screenRecordingNeeded": "Recordly necesita permiso de grabación de pantalla antes de comenzar. Se ha abierto Configuración del sistema. Después de habilitarlo, cierra y vuelve a abrir Recordly.",
- "screenRecordingMissing": "El permiso de grabación de pantalla aún falta. Se ha abierto Configuración del sistema nuevamente. Habilítalo, luego cierra y vuelve a abrir Recordly antes de grabar.",
- "accessibilityNeeded": "Recordly también necesita permiso de accesibilidad para el seguimiento del cursor. Se ha abierto Configuración del sistema. Después de habilitarlo, cierra y vuelve a abrir Recordly.",
- "accessibilityMissing": "El permiso de accesibilidad aún falta. Se ha abierto Configuración del sistema nuevamente. Habilítalo, luego cierra y vuelve a abrir Recordly antes de grabar.",
+ "screenRecordingNeeded": "VybeClip necesita permiso de grabación de pantalla antes de comenzar. Se ha abierto Configuración del sistema. Después de habilitarlo, cierra y vuelve a abrir VybeClip.",
+ "screenRecordingMissing": "El permiso de grabación de pantalla aún falta. Se ha abierto Configuración del sistema nuevamente. Habilítalo, luego cierra y vuelve a abrir VybeClip antes de grabar.",
+ "accessibilityNeeded": "VybeClip también necesita permiso de accesibilidad para el seguimiento del cursor. Se ha abierto Configuración del sistema. Después de habilitarlo, cierra y vuelve a abrir VybeClip.",
+ "accessibilityMissing": "El permiso de accesibilidad aún falta. Se ha abierto Configuración del sistema nuevamente. Habilítalo, luego cierra y vuelve a abrir VybeClip antes de grabar.",
"selectSource": "Por favor selecciona una fuente para grabar",
"systemAudioUnavailable": "El audio del sistema no está disponible para esta fuente. La grabación continuará sin audio del sistema.",
"microphoneDenied": "Se denegó el acceso al micrófono. La grabación continuará sin audio del micrófono.",
diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json
index 66a05e89c..1d3d36d01 100644
--- a/src/i18n/locales/es/settings.json
+++ b/src/i18n/locales/es/settings.json
@@ -66,7 +66,7 @@
"connectedZoomDuration": "Duración entre zooms conectados",
"connectedZoomEasing": "Curva del paneo conectado",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "Glide",
"smooth": "Suave",
"snappy": "Rápida",
diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json
index 38daaa826..37af554cf 100644
--- a/src/i18n/locales/fr/common.json
+++ b/src/i18n/locales/fr/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Recordly Editor",
+ "name": "VybeClip",
+ "editorTitle": "VybeClip Editor",
"subtitle": "Enregistrement et édition d’écran",
"language": "Langue",
"manageRecordings": "Ouvrir le dossier des enregistrements"
diff --git a/src/i18n/locales/fr/editor.json b/src/i18n/locales/fr/editor.json
index cb4b4d13f..58cb4c746 100644
--- a/src/i18n/locales/fr/editor.json
+++ b/src/i18n/locales/fr/editor.json
@@ -115,7 +115,7 @@
},
"nativeCaptureUnavailable": {
"title": "Rien n'est cassé, mais nous ne pourrons pas afficher une superposition animée du curseur.",
- "description": "Votre appareil ne prend pas en charge la capture native. Cela peut arriver pour plusieurs raisons que nous n'avons pas encore identifiées. Recordly continuera de fonctionner, mais le lissage du curseur sera impossible.",
+ "description": "Votre appareil ne prend pas en charge la capture native. Cela peut arriver pour plusieurs raisons que nous n'avons pas encore identifiées. VybeClip continuera de fonctionner, mais le lissage du curseur sera impossible.",
"confirm": "D'accord"
},
"exportStatus": {
diff --git a/src/i18n/locales/fr/launch.json b/src/i18n/locales/fr/launch.json
index df2e43b2b..b20ffe021 100644
--- a/src/i18n/locales/fr/launch.json
+++ b/src/i18n/locales/fr/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "Vérification des mises à jour...",
"downloadingTitle": "Téléchargement de la mise à jour...",
"errorTitle": "Échec de la vérification des mises à jour. Cliquez pour réessayer.",
- "upToDateTitle": "Recordly {{version}} est à jour.",
- "availableTitle": "Recordly {{version}} est disponible.",
+ "upToDateTitle": "VybeClip {{version}} est à jour.",
+ "availableTitle": "VybeClip {{version}} est disponible.",
"availableGenericTitle": "Une mise à jour est disponible."
}
},
@@ -67,10 +67,10 @@
"share": "Partager"
},
"permissions": {
- "screenRecordingNeeded": "Recordly nécessite l’autorisation d’enregistrement d’écran avant de commencer. Les paramètres système ont été ouverts. Après l’avoir activée, quittez et relancez Recordly.",
- "screenRecordingMissing": "L’autorisation d’enregistrement d’écran est toujours manquante. Les paramètres système ont été ouverts à nouveau. Activez-la, puis quittez et relancez Recordly avant d’enregistrer.",
- "accessibilityNeeded": "Recordly nécessite également l’autorisation d’accessibilité pour le suivi du curseur. Les paramètres système ont été ouverts. Après l’avoir activée, quittez et relancez Recordly.",
- "accessibilityMissing": "L’autorisation d’accessibilité est toujours manquante. Les paramètres système ont été ouverts à nouveau. Activez-la, puis quittez et relancez Recordly avant d’enregistrer.",
+ "screenRecordingNeeded": "VybeClip nécessite l’autorisation d’enregistrement d’écran avant de commencer. Les paramètres système ont été ouverts. Après l’avoir activée, quittez et relancez VybeClip.",
+ "screenRecordingMissing": "L’autorisation d’enregistrement d’écran est toujours manquante. Les paramètres système ont été ouverts à nouveau. Activez-la, puis quittez et relancez VybeClip avant d’enregistrer.",
+ "accessibilityNeeded": "VybeClip nécessite également l’autorisation d’accessibilité pour le suivi du curseur. Les paramètres système ont été ouverts. Après l’avoir activée, quittez et relancez VybeClip.",
+ "accessibilityMissing": "L’autorisation d’accessibilité est toujours manquante. Les paramètres système ont été ouverts à nouveau. Activez-la, puis quittez et relancez VybeClip avant d’enregistrer.",
"selectSource": "Veuillez sélectionner une source à enregistrer",
"systemAudioUnavailable": "L’audio système n’est pas disponible pour cette source. L’enregistrement continuera sans audio système.",
"microphoneDenied": "L’accès au microphone a été refusé. L’enregistrement continuera sans audio du microphone.",
diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json
index 200ecaa02..4f4369106 100644
--- a/src/i18n/locales/fr/settings.json
+++ b/src/i18n/locales/fr/settings.json
@@ -66,7 +66,7 @@
"connectedZoomDuration": "Durée du zoom connecté",
"connectedZoomEasing": "Courbe du pan connecté",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "Glide",
"smooth": "Lisse",
"snappy": "Rapide",
diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json
index 9c270d7e8..a09c6ea01 100644
--- a/src/i18n/locales/it/common.json
+++ b/src/i18n/locales/it/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Editor Recordly",
+ "name": "VybeClip",
+ "editorTitle": "Editor VybeClip",
"subtitle": "Registrazione e modifica dello schermo",
"language": "Lingua",
"manageRecordings": "Apri cartella registrazioni"
diff --git a/src/i18n/locales/it/editor.json b/src/i18n/locales/it/editor.json
index b23d247da..44c5a785e 100644
--- a/src/i18n/locales/it/editor.json
+++ b/src/i18n/locales/it/editor.json
@@ -115,7 +115,7 @@
},
"nativeCaptureUnavailable": {
"title": "Niente è rotto, ma non sarà possibile renderizzare un overlay del cursore animato.",
- "description": "Il tuo dispositivo non supporta la cattura nativa. Le cause possono essere varie e non ancora identificate. Recordly funziona comunque, ma non è possibile applicare lo smoothing del cursore.",
+ "description": "Il tuo dispositivo non supporta la cattura nativa. Le cause possono essere varie e non ancora identificate. VybeClip funziona comunque, ma non è possibile applicare lo smoothing del cursore.",
"confirm": "Ok"
},
"exportStatus": {
diff --git a/src/i18n/locales/it/launch.json b/src/i18n/locales/it/launch.json
index c27abc3ad..2fe3165f3 100644
--- a/src/i18n/locales/it/launch.json
+++ b/src/i18n/locales/it/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "Verifica aggiornamenti in corso...",
"downloadingTitle": "Download dell'aggiornamento in corso...",
"errorTitle": "Verifica aggiornamenti non riuscita. Clicca per riprovare.",
- "upToDateTitle": "Recordly {{version}} è aggiornato.",
- "availableTitle": "Recordly {{version}} è disponibile.",
+ "upToDateTitle": "VybeClip {{version}} è aggiornato.",
+ "availableTitle": "VybeClip {{version}} è disponibile.",
"availableGenericTitle": "È disponibile un aggiornamento."
}
},
@@ -67,10 +67,10 @@
"share": "Condividi"
},
"permissions": {
- "screenRecordingNeeded": "Recordly necessita del permesso di Registrazione schermo prima di iniziare. Sono state aperte le Impostazioni di sistema. Dopo averlo abilitato, esci e riapri Recordly.",
- "screenRecordingMissing": "Manca ancora il permesso di Registrazione schermo. Le Impostazioni di sistema sono state riaperte. Abilitalo, poi esci e riapri Recordly prima di registrare.",
- "accessibilityNeeded": "Recordly necessita anche del permesso di Accessibilità per il tracciamento del cursore. Sono state aperte le Impostazioni di sistema. Dopo averlo abilitato, esci e riapri Recordly.",
- "accessibilityMissing": "Manca ancora il permesso di Accessibilità. Le Impostazioni di sistema sono state riaperte. Abilitalo, poi esci e riapri Recordly prima di registrare.",
+ "screenRecordingNeeded": "VybeClip necessita del permesso di Registrazione schermo prima di iniziare. Sono state aperte le Impostazioni di sistema. Dopo averlo abilitato, esci e riapri VybeClip.",
+ "screenRecordingMissing": "Manca ancora il permesso di Registrazione schermo. Le Impostazioni di sistema sono state riaperte. Abilitalo, poi esci e riapri VybeClip prima di registrare.",
+ "accessibilityNeeded": "VybeClip necessita anche del permesso di Accessibilità per il tracciamento del cursore. Sono state aperte le Impostazioni di sistema. Dopo averlo abilitato, esci e riapri VybeClip.",
+ "accessibilityMissing": "Manca ancora il permesso di Accessibilità. Le Impostazioni di sistema sono state riaperte. Abilitalo, poi esci e riapri VybeClip prima di registrare.",
"selectSource": "Seleziona una sorgente da registrare",
"systemAudioUnavailable": "L'audio di sistema non è disponibile per questa sorgente. La registrazione continuerà senza audio di sistema.",
"microphoneDenied": "L'accesso al microfono è stato negato. La registrazione continuerà senza audio del microfono.",
diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json
index d9b9a1a18..0263a93ce 100644
--- a/src/i18n/locales/it/settings.json
+++ b/src/i18n/locales/it/settings.json
@@ -85,7 +85,7 @@
"connectedZoomDuration": "Durata zoom connessi",
"connectedZoomEasing": "Curva pan connesso",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "Glide",
"smooth": "Fluido",
"snappy": "Rapido",
diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json
index f036318e0..f6644e136 100644
--- a/src/i18n/locales/ko/common.json
+++ b/src/i18n/locales/ko/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Recordly 편집기",
+ "name": "VybeClip",
+ "editorTitle": "VybeClip 편집기",
"subtitle": "화면 녹화 및 편집",
"language": "언어",
"manageRecordings": "녹화 폴더 열기"
diff --git a/src/i18n/locales/ko/editor.json b/src/i18n/locales/ko/editor.json
index 49678dee1..affcc6e63 100644
--- a/src/i18n/locales/ko/editor.json
+++ b/src/i18n/locales/ko/editor.json
@@ -116,7 +116,7 @@
},
"nativeCaptureUnavailable": {
"title": "문제가 생긴 것은 아니지만, 애니메이션 커서 오버레이를 렌더링할 수 없습니다.",
- "description": "이 장치는 네이티브 캡처를 지원하지 않습니다. 아직 확인하지 못한 여러 이유가 있을 수 있습니다. Recordly는 계속 작동하지만 커서 스무딩은 사용할 수 없습니다.",
+ "description": "이 장치는 네이티브 캡처를 지원하지 않습니다. 아직 확인하지 못한 여러 이유가 있을 수 있습니다. VybeClip는 계속 작동하지만 커서 스무딩은 사용할 수 없습니다.",
"confirm": "확인"
},
"exportStatus": {
diff --git a/src/i18n/locales/ko/launch.json b/src/i18n/locales/ko/launch.json
index 345000399..2c47bb0a0 100644
--- a/src/i18n/locales/ko/launch.json
+++ b/src/i18n/locales/ko/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "업데이트를 확인하는 중...",
"downloadingTitle": "업데이트를 다운로드하는 중...",
"errorTitle": "업데이트 확인에 실패했습니다. 클릭하여 다시 시도하세요.",
- "upToDateTitle": "Recordly {{version}}은(는) 최신 버전입니다.",
- "availableTitle": "Recordly {{version}}을(를) 사용할 수 있습니다.",
+ "upToDateTitle": "VybeClip {{version}}은(는) 최신 버전입니다.",
+ "availableTitle": "VybeClip {{version}}을(를) 사용할 수 있습니다.",
"availableGenericTitle": "업데이트를 사용할 수 있습니다."
}
},
@@ -67,10 +67,10 @@
"share": "공유"
},
"permissions": {
- "screenRecordingNeeded": "녹화를 시작하기 전에 Recordly에 화면 녹화 권한이 필요합니다. 시스템 설정을 열었습니다. 권한을 허용한 뒤 Recordly를 종료하고 다시 실행해 주세요.",
- "screenRecordingMissing": "화면 녹화 권한이 아직 없습니다. 시스템 설정을 다시 열었습니다. 권한을 허용한 뒤 Recordly를 종료하고 다시 실행한 후 녹화를 시작해 주세요.",
- "accessibilityNeeded": "커서 추적을 위해 Recordly에 손쉬운 사용 권한도 필요합니다. 시스템 설정을 열었습니다. 권한을 허용한 뒤 Recordly를 종료하고 다시 실행해 주세요.",
- "accessibilityMissing": "손쉬운 사용 권한이 아직 없습니다. 시스템 설정을 다시 열었습니다. 권한을 허용한 뒤 Recordly를 종료하고 다시 실행한 후 녹화를 시작해 주세요.",
+ "screenRecordingNeeded": "녹화를 시작하기 전에 VybeClip에 화면 녹화 권한이 필요합니다. 시스템 설정을 열었습니다. 권한을 허용한 뒤 VybeClip를 종료하고 다시 실행해 주세요.",
+ "screenRecordingMissing": "화면 녹화 권한이 아직 없습니다. 시스템 설정을 다시 열었습니다. 권한을 허용한 뒤 VybeClip를 종료하고 다시 실행한 후 녹화를 시작해 주세요.",
+ "accessibilityNeeded": "커서 추적을 위해 VybeClip에 손쉬운 사용 권한도 필요합니다. 시스템 설정을 열었습니다. 권한을 허용한 뒤 VybeClip를 종료하고 다시 실행해 주세요.",
+ "accessibilityMissing": "손쉬운 사용 권한이 아직 없습니다. 시스템 설정을 다시 열었습니다. 권한을 허용한 뒤 VybeClip를 종료하고 다시 실행한 후 녹화를 시작해 주세요.",
"selectSource": "녹화할 소스를 선택해 주세요",
"systemAudioUnavailable": "이 소스에서는 시스템 오디오를 사용할 수 없습니다. 시스템 오디오 없이 녹화를 계속합니다.",
"microphoneDenied": "마이크 접근이 거부되었습니다. 마이크 오디오 없이 녹화를 계속합니다.",
diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json
index c528a98fb..326fcba90 100644
--- a/src/i18n/locales/ko/settings.json
+++ b/src/i18n/locales/ko/settings.json
@@ -66,7 +66,7 @@
"connectedZoomDuration": "연결 확대 시간",
"connectedZoomEasing": "연결 이동 커브",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "Glide",
"smooth": "Smooth",
"snappy": "Snappy",
diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json
index 0e7c2f33f..2bcb133c5 100644
--- a/src/i18n/locales/nl/common.json
+++ b/src/i18n/locales/nl/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Recordly Editor",
+ "name": "VybeClip",
+ "editorTitle": "VybeClip Editor",
"subtitle": "Schermopname en bewerking",
"language": "Taal",
"manageRecordings": "Opnamemap openen"
diff --git a/src/i18n/locales/nl/editor.json b/src/i18n/locales/nl/editor.json
index d58b9e525..8a638dc5f 100644
--- a/src/i18n/locales/nl/editor.json
+++ b/src/i18n/locales/nl/editor.json
@@ -116,7 +116,7 @@
},
"nativeCaptureUnavailable": {
"title": "Er is niets kapot, maar we kunnen geen geanimeerde cursor-overlay renderen.",
- "description": "Je apparaat ondersteunt geen native capture. Dit kan verschillende oorzaken hebben die we nog niet hebben achterhaald. Recordly blijft werken, maar cursor smoothing is dan niet mogelijk.",
+ "description": "Je apparaat ondersteunt geen native capture. Dit kan verschillende oorzaken hebben die we nog niet hebben achterhaald. VybeClip blijft werken, maar cursor smoothing is dan niet mogelijk.",
"confirm": "Oké"
},
"exportStatus": {
diff --git a/src/i18n/locales/nl/launch.json b/src/i18n/locales/nl/launch.json
index d3d5870c9..867a46116 100644
--- a/src/i18n/locales/nl/launch.json
+++ b/src/i18n/locales/nl/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "Controleren op updates...",
"downloadingTitle": "Update downloaden...",
"errorTitle": "Updatecontrole mislukt. Klik om opnieuw te proberen.",
- "upToDateTitle": "Recordly {{version}} is up-to-date.",
- "availableTitle": "Recordly {{version}} is beschikbaar.",
+ "upToDateTitle": "VybeClip {{version}} is up-to-date.",
+ "availableTitle": "VybeClip {{version}} is beschikbaar.",
"availableGenericTitle": "Er is een update beschikbaar."
}
},
@@ -67,10 +67,10 @@
"share": "Delen"
},
"permissions": {
- "screenRecordingNeeded": "Recordly heeft toestemming voor schermopname nodig voordat je begint. Systeeminstellingen zijn geopend. Schakel het in, sluit Recordly en open het opnieuw.",
- "screenRecordingMissing": "Toestemming voor schermopname ontbreekt nog. Systeeminstellingen zijn opnieuw geopend. Schakel het in, sluit Recordly en open het opnieuw voordat je opneemt.",
- "accessibilityNeeded": "Recordly heeft ook toegankelijkheidstoestemming nodig voor cursortracking. Systeeminstellingen zijn geopend. Schakel het in, sluit Recordly en open het opnieuw.",
- "accessibilityMissing": "Toegankelijkheidstoestemming ontbreekt nog. Systeeminstellingen zijn opnieuw geopend. Schakel het in, sluit Recordly en open het opnieuw voordat je opneemt.",
+ "screenRecordingNeeded": "VybeClip heeft toestemming voor schermopname nodig voordat je begint. Systeeminstellingen zijn geopend. Schakel het in, sluit VybeClip en open het opnieuw.",
+ "screenRecordingMissing": "Toestemming voor schermopname ontbreekt nog. Systeeminstellingen zijn opnieuw geopend. Schakel het in, sluit VybeClip en open het opnieuw voordat je opneemt.",
+ "accessibilityNeeded": "VybeClip heeft ook toegankelijkheidstoestemming nodig voor cursortracking. Systeeminstellingen zijn geopend. Schakel het in, sluit VybeClip en open het opnieuw.",
+ "accessibilityMissing": "Toegankelijkheidstoestemming ontbreekt nog. Systeeminstellingen zijn opnieuw geopend. Schakel het in, sluit VybeClip en open het opnieuw voordat je opneemt.",
"selectSource": "Selecteer een bron om op te nemen",
"systemAudioUnavailable": "Systeemaudio is niet beschikbaar voor deze bron. De opname gaat verder zonder systeemaudio.",
"microphoneDenied": "Microfoontoegang is geweigerd. De opname gaat verder zonder microfoonaudio.",
diff --git a/src/i18n/locales/nl/settings.json b/src/i18n/locales/nl/settings.json
index d88e4278c..977c73ce8 100644
--- a/src/i18n/locales/nl/settings.json
+++ b/src/i18n/locales/nl/settings.json
@@ -66,7 +66,7 @@
"connectedZoomDuration": "Verbonden zoomduur",
"connectedZoomEasing": "Verbonden pancurve",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "Glijden",
"smooth": "Vloeiend",
"snappy": "Pittig",
diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json
index 97a2ea1d9..40e2862dd 100644
--- a/src/i18n/locales/pt-BR/common.json
+++ b/src/i18n/locales/pt-BR/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Editor do Recordly",
+ "name": "VybeClip",
+ "editorTitle": "Editor do VybeClip",
"subtitle": "Gravação e edição de tela",
"language": "Idioma",
"manageRecordings": "Abrir pasta de gravações"
diff --git a/src/i18n/locales/pt-BR/editor.json b/src/i18n/locales/pt-BR/editor.json
index 702939fd1..9eb2c1b23 100644
--- a/src/i18n/locales/pt-BR/editor.json
+++ b/src/i18n/locales/pt-BR/editor.json
@@ -115,7 +115,7 @@
},
"nativeCaptureUnavailable": {
"title": "Nada está quebrado, mas não poderemos renderizar uma sobreposição animada do cursor.",
- "description": "Seu dispositivo não oferece suporte à captura nativa. Isso pode acontecer por vários motivos que ainda não identificamos. O Recordly continuará funcionando, mas a suavização do cursor ficará indisponível.",
+ "description": "Seu dispositivo não oferece suporte à captura nativa. Isso pode acontecer por vários motivos que ainda não identificamos. O VybeClip continuará funcionando, mas a suavização do cursor ficará indisponível.",
"confirm": "Entendi"
},
"exportStatus": {
diff --git a/src/i18n/locales/pt-BR/launch.json b/src/i18n/locales/pt-BR/launch.json
index 8d19ac7db..0734dd83b 100644
--- a/src/i18n/locales/pt-BR/launch.json
+++ b/src/i18n/locales/pt-BR/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "Verificando atualizações...",
"downloadingTitle": "Baixando atualização...",
"errorTitle": "Falha ao verificar atualizações. Clique para tentar novamente.",
- "upToDateTitle": "Recordly {{version}} está atualizado.",
- "availableTitle": "Recordly {{version}} está disponível.",
+ "upToDateTitle": "VybeClip {{version}} está atualizado.",
+ "availableTitle": "VybeClip {{version}} está disponível.",
"availableGenericTitle": "Há uma atualização disponível."
}
},
@@ -67,10 +67,10 @@
"share": "Compartilhar"
},
"permissions": {
- "screenRecordingNeeded": "O Recordly precisa da permissão de Gravação de Tela antes de iniciar. Ajustes do Sistema foram abertos. Depois de ativar, feche e reabra o Recordly.",
- "screenRecordingMissing": "A permissão de Gravação de Tela ainda está ausente. Ajustes do Sistema foram abertos novamente. Ative e depois feche e reabra o Recordly antes de gravar.",
- "accessibilityNeeded": "O Recordly também precisa da permissão de Acessibilidade para rastrear o cursor. Ajustes do Sistema foram abertos. Depois de ativar, feche e reabra o Recordly.",
- "accessibilityMissing": "A permissão de Acessibilidade ainda está ausente. Ajustes do Sistema foram abertos novamente. Ative e depois feche e reabra o Recordly antes de gravar.",
+ "screenRecordingNeeded": "O VybeClip precisa da permissão de Gravação de Tela antes de iniciar. Ajustes do Sistema foram abertos. Depois de ativar, feche e reabra o VybeClip.",
+ "screenRecordingMissing": "A permissão de Gravação de Tela ainda está ausente. Ajustes do Sistema foram abertos novamente. Ative e depois feche e reabra o VybeClip antes de gravar.",
+ "accessibilityNeeded": "O VybeClip também precisa da permissão de Acessibilidade para rastrear o cursor. Ajustes do Sistema foram abertos. Depois de ativar, feche e reabra o VybeClip.",
+ "accessibilityMissing": "A permissão de Acessibilidade ainda está ausente. Ajustes do Sistema foram abertos novamente. Ative e depois feche e reabra o VybeClip antes de gravar.",
"selectSource": "Selecione uma fonte para gravar",
"systemAudioUnavailable": "O áudio do sistema não está disponível para esta fonte. A gravação continuará sem áudio do sistema.",
"microphoneDenied": "O acesso ao microfone foi negado. A gravação continuará sem áudio do microfone.",
diff --git a/src/i18n/locales/pt-BR/settings.json b/src/i18n/locales/pt-BR/settings.json
index a34e6e4d2..594ae0e9c 100644
--- a/src/i18n/locales/pt-BR/settings.json
+++ b/src/i18n/locales/pt-BR/settings.json
@@ -66,7 +66,7 @@
"connectedZoomDuration": "Duração do zoom conectado",
"connectedZoomEasing": "Curva de pan conectado",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "Glide",
"smooth": "Smooth",
"snappy": "Snappy",
diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json
index 3946d72b9..aa24faf84 100644
--- a/src/i18n/locales/ru/common.json
+++ b/src/i18n/locales/ru/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Recordly Editor",
+ "name": "VybeClip",
+ "editorTitle": "VybeClip Editor",
"subtitle": "Запись экрана и редактирование видео",
"language": "Язык",
"manageRecordings": "Открыть папку с записями"
diff --git a/src/i18n/locales/ru/launch.json b/src/i18n/locales/ru/launch.json
index 65bd57b8c..452ada2f9 100644
--- a/src/i18n/locales/ru/launch.json
+++ b/src/i18n/locales/ru/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "Поиск обновлений...",
"downloadingTitle": "Загрузка обновления...",
"errorTitle": "Ошибка при проверке обновления. Нажмите, чтобы повторить",
- "upToDateTitle": "У вас актуальная версия Recordly {{version}}.",
- "availableTitle": "Доступна новая версия Recordly {{version}}.",
+ "upToDateTitle": "У вас актуальная версия VybeClip {{version}}.",
+ "availableTitle": "Доступна новая версия VybeClip {{version}}.",
"availableGenericTitle": "Доступно обновление."
}
},
@@ -67,10 +67,10 @@
"share": "Поделиться"
},
"permissions": {
- "screenRecordingNeeded": "Recordly нужно разрешение на запись экрана. Мы открыли настройки – включите доступ и перезапустите приложение.",
- "screenRecordingMissing": "Доступа к записи экрана всё ещё нет. Мы снова открыли настройки. Разрешите доступ и перезапустите Recordly.",
- "accessibilityNeeded": "Разрешите Recordly использовать универсальный доступ, чтобы отслеживать курсор. Мы открыли настройки – включите его и перезапустите приложение.",
- "accessibilityMissing": "Разрешения для универсального доступа всё ещё нет. Включите его в настройках и перезапустите Recordly.",
+ "screenRecordingNeeded": "VybeClip нужно разрешение на запись экрана. Мы открыли настройки – включите доступ и перезапустите приложение.",
+ "screenRecordingMissing": "Доступа к записи экрана всё ещё нет. Мы снова открыли настройки. Разрешите доступ и перезапустите VybeClip.",
+ "accessibilityNeeded": "Разрешите VybeClip использовать универсальный доступ, чтобы отслеживать курсор. Мы открыли настройки – включите его и перезапустите приложение.",
+ "accessibilityMissing": "Разрешения для универсального доступа всё ещё нет. Включите его в настройках и перезапустите VybeClip.",
"selectSource": "Выберите источник записи",
"systemAudioUnavailable": "Системные звуки недоступны для этого источника. Запись продолжится без их захвата.",
"microphoneDenied": "Нет доступа к микрофону. Запись продолжится без вашего голоса.",
diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json
index 687f8383b..76579ec3b 100644
--- a/src/i18n/locales/ru/settings.json
+++ b/src/i18n/locales/ru/settings.json
@@ -85,7 +85,7 @@
"connectedZoomDuration": "Длительность интервала",
"connectedZoomEasing": "Кривая панорамирования",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "Плавно",
"smooth": "Мягко",
"snappy": "Резко",
@@ -216,4 +216,4 @@
"mixedLabel": "Источник",
"deleteRegion": "Удалить аудио"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json
index b35ca3d8c..9cec83f09 100644
--- a/src/i18n/locales/zh-CN/common.json
+++ b/src/i18n/locales/zh-CN/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Recordly 编辑器",
+ "name": "VybeClip",
+ "editorTitle": "VybeClip 编辑器",
"subtitle": "屏幕录制与编辑",
"language": "语言",
"manageRecordings": "打开录制文件夹"
diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json
index 1bf3a6084..309ee044b 100644
--- a/src/i18n/locales/zh-CN/editor.json
+++ b/src/i18n/locales/zh-CN/editor.json
@@ -115,7 +115,7 @@
},
"nativeCaptureUnavailable": {
"title": "没有出错,但我们无法渲染动画光标叠加层。",
- "description": "你的设备不支持原生捕获。这可能是由我们尚未确定的多种原因造成的。Recordly 仍可继续运行,但无法进行光标平滑处理。",
+ "description": "你的设备不支持原生捕获。这可能是由我们尚未确定的多种原因造成的。VybeClip 仍可继续运行,但无法进行光标平滑处理。",
"confirm": "好的"
},
"exportStatus": {
diff --git a/src/i18n/locales/zh-CN/launch.json b/src/i18n/locales/zh-CN/launch.json
index 164c02afe..7ebc5a2f8 100644
--- a/src/i18n/locales/zh-CN/launch.json
+++ b/src/i18n/locales/zh-CN/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "正在检查更新...",
"downloadingTitle": "正在下载更新...",
"errorTitle": "检查更新失败。点击重试。",
- "upToDateTitle": "Recordly {{version}} 已是最新版本。",
- "availableTitle": "Recordly {{version}} 可更新。",
+ "upToDateTitle": "VybeClip {{version}} 已是最新版本。",
+ "availableTitle": "VybeClip {{version}} 可更新。",
"availableGenericTitle": "有可用更新。"
}
},
@@ -67,10 +67,10 @@
"share": "共享"
},
"permissions": {
- "screenRecordingNeeded": "Recordly 需要屏幕录制权限才能开始。系统设置已打开。启用后请退出并重新打开 Recordly。",
- "screenRecordingMissing": "屏幕录制权限仍然缺失。系统设置已再次打开。请启用权限,然后退出并重新打开 Recordly。",
- "accessibilityNeeded": "Recordly 还需要辅助功能权限以跟踪光标。系统设置已打开。启用后请退出并重新打开 Recordly。",
- "accessibilityMissing": "辅助功能权限仍然缺失。系统设置已再次打开。请启用权限,然后退出并重新打开 Recordly。",
+ "screenRecordingNeeded": "VybeClip 需要屏幕录制权限才能开始。系统设置已打开。启用后请退出并重新打开 VybeClip。",
+ "screenRecordingMissing": "屏幕录制权限仍然缺失。系统设置已再次打开。请启用权限,然后退出并重新打开 VybeClip。",
+ "accessibilityNeeded": "VybeClip 还需要辅助功能权限以跟踪光标。系统设置已打开。启用后请退出并重新打开 VybeClip。",
+ "accessibilityMissing": "辅助功能权限仍然缺失。系统设置已再次打开。请启用权限,然后退出并重新打开 VybeClip。",
"selectSource": "请选择要录制的源",
"systemAudioUnavailable": "此源不支持系统音频。将继续录制但不包含系统音频。",
"microphoneDenied": "麦克风访问被拒绝。将继续录制但不包含麦克风音频。",
diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json
index a68d2a04f..0cc5d3ab6 100644
--- a/src/i18n/locales/zh-CN/settings.json
+++ b/src/i18n/locales/zh-CN/settings.json
@@ -80,7 +80,7 @@
"connectedZoomDuration": "连接缩放时长",
"connectedZoomEasing": "连接平移曲线",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "滑行",
"smooth": "平滑",
"snappy": "利落",
diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json
index c5fa86d10..db5080bc8 100644
--- a/src/i18n/locales/zh-TW/common.json
+++ b/src/i18n/locales/zh-TW/common.json
@@ -1,7 +1,7 @@
{
"app": {
- "name": "Recordly",
- "editorTitle": "Recordly 編輯器",
+ "name": "VybeClip",
+ "editorTitle": "VybeClip 編輯器",
"subtitle": "螢幕錄製與編輯",
"language": "語言",
"manageRecordings": "打開錄製影像文件夾"
diff --git a/src/i18n/locales/zh-TW/editor.json b/src/i18n/locales/zh-TW/editor.json
index 6f2b0f823..049768c5a 100644
--- a/src/i18n/locales/zh-TW/editor.json
+++ b/src/i18n/locales/zh-TW/editor.json
@@ -115,7 +115,7 @@
},
"nativeCaptureUnavailable": {
"title": "沒有出錯,但我們無法轉譯動畫游標覆蓋層。",
- "description": "你的裝置不支援原生擷取。這可能是由我們尚未釐清的多種原因造成的。Recordly 仍可繼續運作,但無法進行游標平滑處理。",
+ "description": "你的裝置不支援原生擷取。這可能是由我們尚未釐清的多種原因造成的。VybeClip 仍可繼續運作,但無法進行游標平滑處理。",
"confirm": "好的"
},
"exportStatus": {
diff --git a/src/i18n/locales/zh-TW/launch.json b/src/i18n/locales/zh-TW/launch.json
index 12ec494d0..4baaff259 100644
--- a/src/i18n/locales/zh-TW/launch.json
+++ b/src/i18n/locales/zh-TW/launch.json
@@ -50,8 +50,8 @@
"checkingTitle": "正在檢查更新...",
"downloadingTitle": "正在下載更新...",
"errorTitle": "檢查更新失敗,點一下重試。",
- "upToDateTitle": "Recordly {{version}} 已是最新版本。",
- "availableTitle": "Recordly {{version}} 已推出新版本。",
+ "upToDateTitle": "VybeClip {{version}} 已是最新版本。",
+ "availableTitle": "VybeClip {{version}} 已推出新版本。",
"availableGenericTitle": "有可用更新。"
}
},
@@ -67,10 +67,10 @@
"share": "分享"
},
"permissions": {
- "screenRecordingNeeded": "Recordly 需要「螢幕錄製」權限才能開始。已開啟系統設定。啟用後,請結束並重新開啟 Recordly。",
- "screenRecordingMissing": "仍未取得「螢幕錄製」權限。已再次開啟系統設定。請先啟用,再結束並重新開啟 Recordly 後再錄製。",
- "accessibilityNeeded": "Recordly 也需要「輔助使用」權限以追蹤游標。已開啟系統設定。啟用後,請結束並重新開啟 Recordly。",
- "accessibilityMissing": "仍未取得「輔助使用」權限。已再次開啟系統設定。請先啟用,再結束並重新開啟 Recordly 後再錄製。",
+ "screenRecordingNeeded": "VybeClip 需要「螢幕錄製」權限才能開始。已開啟系統設定。啟用後,請結束並重新開啟 VybeClip。",
+ "screenRecordingMissing": "仍未取得「螢幕錄製」權限。已再次開啟系統設定。請先啟用,再結束並重新開啟 VybeClip 後再錄製。",
+ "accessibilityNeeded": "VybeClip 也需要「輔助使用」權限以追蹤游標。已開啟系統設定。啟用後,請結束並重新開啟 VybeClip。",
+ "accessibilityMissing": "仍未取得「輔助使用」權限。已再次開啟系統設定。請先啟用,再結束並重新開啟 VybeClip 後再錄製。",
"selectSource": "請選擇要錄製的來源",
"systemAudioUnavailable": "此來源不支援系統音訊,將在沒有系統音訊的情況下繼續錄製。",
"microphoneDenied": "麥克風存取遭拒,將在沒有麥克風音訊的情況下繼續錄製。",
diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json
index db0ea7e04..c156b3178 100644
--- a/src/i18n/locales/zh-TW/settings.json
+++ b/src/i18n/locales/zh-TW/settings.json
@@ -66,7 +66,7 @@
"connectedZoomDuration": "連接縮放時間",
"connectedZoomEasing": "連接平移曲線",
"zoomEasingOptions": {
- "recordly": "Recordly",
+ "recordly": "VybeClip",
"glide": "滑行",
"smooth": "平滑",
"snappy": "俐落",
diff --git a/src/index.css b/src/index.css
index a5ae2368b..650135316 100644
--- a/src/index.css
+++ b/src/index.css
@@ -30,27 +30,27 @@
:root {
--app-font-sans: "SF Pro Display", "SF Pro Text", Helvetica, sans-serif;
- --brand-accent: #2563eb;
- --brand-accent-rgb: 37, 99, 235;
+ --brand-accent: #b6410f;
+ --brand-accent-rgb: 182, 65, 15;
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
- --primary: 221 83% 53%;
+ --primary: 19 85% 39%;
--primary-foreground: 210 40% 98%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
- --accent: 214 100% 97%;
- --accent-foreground: 221 83% 40%;
+ --accent: 21 51% 91%;
+ --accent-foreground: 19 85% 31%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
- --ring: 221 83% 53%;
+ --ring: 19 85% 39%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
@@ -59,11 +59,11 @@
--radius: 0.5rem;
/* Editor surface colors — light mode */
- --editor-bg: 0 0% 100%;
- --editor-header: 0 0% 97%;
- --editor-panel: 0 0% 98%;
- --editor-surface: 0 0% 97%;
- --editor-surface-alt: 0 0% 95%;
+ --editor-bg: 32 28% 97%;
+ --editor-header: 21 39% 94%;
+ --editor-panel: 21 34% 96%;
+ --editor-surface: 21 28% 94%;
+ --editor-surface-alt: 21 22% 91%;
--editor-dialog: 0 0% 100%;
--editor-dialog-alt: 0 0% 98%;
--editor-timeline: 220 13% 91%;
@@ -72,53 +72,53 @@
/* Slider track colors — light mode */
--slider-track: 0 0% 0% / 0.12;
- --slider-thumb: 0 0% 10%;
+ --slider-thumb: 19 85% 39%;
--slider-thumb-shadow: 0 0% 0% / 0.22;
--slider-glow: 0 0% 0% / 0.35;
}
.dark {
- --background: 20 14.3% 4.1%;
- --foreground: 60 9.1% 97.8%;
- --card: 20 14.3% 4.1%;
- --card-foreground: 60 9.1% 97.8%;
- --popover: 20 14.3% 4.1%;
- --popover-foreground: 60 9.1% 97.8%;
- --primary: 221 83% 53%;
+ --background: 200 33% 3%;
+ --foreground: 240 10% 80%;
+ --card: 202 35% 6%;
+ --card-foreground: 240 10% 80%;
+ --popover: 202 35% 6%;
+ --popover-foreground: 240 10% 80%;
+ --primary: 19 85% 39%;
--primary-foreground: 210 40% 98%;
- --secondary: 12 6.5% 15.1%;
- --secondary-foreground: 60 9.1% 97.8%;
- --muted: 12 6.5% 15.1%;
- --muted-foreground: 24 5.4% 63.9%;
- --accent: 217 33% 17%;
- --accent-foreground: 210 40% 98%;
+ --secondary: 202 48% 10%;
+ --secondary-foreground: 240 10% 80%;
+ --muted: 202 35% 12%;
+ --muted-foreground: 240 7% 62%;
+ --accent: 197 72% 23%;
+ --accent-foreground: 21 51% 88%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
- --border: 12 6.5% 15.1%;
- --input: 12 6.5% 15.1%;
- --ring: 221 83% 53%;
- --chart-1: 220 70% 50%;
- --chart-2: 160 60% 45%;
- --chart-3: 30 80% 55%;
- --chart-4: 280 65% 60%;
- --chart-5: 340 75% 55%;
+ --border: 202 35% 16%;
+ --input: 202 35% 16%;
+ --ring: 19 85% 39%;
+ --chart-1: 19 85% 39%;
+ --chart-2: 197 72% 23%;
+ --chart-3: 21 51% 67%;
+ --chart-4: 21 48% 53%;
+ --chart-5: 240 10% 80%;
/* Editor surface colors — dark mode */
- --editor-bg: 240 6% 7%;
- --editor-header: 240 5% 8.5%;
- --editor-panel: 240 5% 9%;
- --editor-surface: 240 4% 10%;
- --editor-surface-alt: 240 5% 11%;
- --editor-dialog: 240 10% 3%;
- --editor-dialog-alt: 237 9% 5%;
- --editor-timeline: 240 6% 7%;
- --editor-row: 240 4% 10%;
- --editor-subrow: 240 7% 15%;
+ --editor-bg: 200 33% 3%;
+ --editor-header: 202 35% 6%;
+ --editor-panel: 202 35% 6%;
+ --editor-surface: 202 48% 10%;
+ --editor-surface-alt: 197 55% 13%;
+ --editor-dialog: 200 33% 3%;
+ --editor-dialog-alt: 202 35% 6%;
+ --editor-timeline: 202 35% 6%;
+ --editor-row: 202 48% 10%;
+ --editor-subrow: 197 45% 16%;
/* Slider track colors — dark mode */
--slider-track: 0 0% 100% / 0.12;
- --slider-thumb: 0 0% 100%;
- --slider-thumb-shadow: 0 0% 100% / 0.32;
- --slider-glow: 0 0% 100% / 0.5;
+ --slider-thumb: 21 51% 67%;
+ --slider-thumb-shadow: 21 51% 67% / 0.32;
+ --slider-glow: 19 85% 39% / 0.5;
}
}
@@ -141,6 +141,18 @@
font: inherit;
}
+ .electron-drag {
+ -webkit-app-region: drag;
+ }
+
+ .electron-no-drag {
+ -webkit-app-region: no-drag;
+ }
+
+ html[data-platform="macos"] .vybe-studio-header {
+ padding-left: 5.25rem;
+ }
+
body .font-mono,
body kbd {
font-family: var(--app-font-sans);
@@ -171,11 +183,11 @@
border-radius: inherit;
background: linear-gradient(
90deg,
- rgba(37, 99, 235, 0) 0%,
- rgba(37, 99, 235, 0.35) 18%,
- rgba(96, 165, 250, 0.92) 52%,
- rgba(37, 99, 235, 0.3) 82%,
- rgba(37, 99, 235, 0) 100%
+ rgba(182, 65, 15, 0) 0%,
+ rgba(192, 117, 77, 0.28) 18%,
+ rgba(213, 157, 128, 0.92) 52%,
+ rgba(182, 65, 15, 0.3) 82%,
+ rgba(182, 65, 15, 0) 100%
);
animation: indeterminate-progress 1.15s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@@ -268,6 +280,3 @@
transform: translateX(270%);
}
}
-
-
-
diff --git a/src/lib/exporter/exportSavePolicy.ts b/src/lib/exporter/exportSavePolicy.ts
index c48516ed3..9c9a7ce45 100644
--- a/src/lib/exporter/exportSavePolicy.ts
+++ b/src/lib/exporter/exportSavePolicy.ts
@@ -39,8 +39,8 @@ export function describeBlockedInMemoryExportSave({
}): string {
const normalizedExtension = normalizeExportExtension(extension) || "export";
if (isExportTooLargeForInMemorySave(blobSize)) {
- return `The ${normalizedExtension.toUpperCase()} export is too large to save through the legacy in-memory path. Please retry the export so Recordly can save it through the temp-file streaming path.`;
+ return `The ${normalizedExtension.toUpperCase()} export is too large to save through the legacy in-memory path. Please retry the export so VybeClip can save it through the temp-file streaming path.`;
}
- return `The ${normalizedExtension.toUpperCase()} export could not be saved through the temp-file streaming path, and Recordly will not fall back to the legacy in-memory path for MP4 exports. Please retry the export.`;
+ return `The ${normalizedExtension.toUpperCase()} export could not be saved through the temp-file streaming path, and VybeClip will not fall back to the legacy in-memory path for MP4 exports. Please retry the export.`;
}
diff --git a/src/lib/exporter/gifExporter.ts b/src/lib/exporter/gifExporter.ts
index 2630e5926..ee931f0d0 100644
--- a/src/lib/exporter/gifExporter.ts
+++ b/src/lib/exporter/gifExporter.ts
@@ -316,7 +316,7 @@ export class GifExporter {
}
// Render the GIF
- const blob = await new Promise((resolve, _reject) => {
+ const blob = await new Promise((resolve, reject) => {
this.gif!.on("finished", (blob: Blob) => {
resolve(blob);
});
@@ -336,7 +336,24 @@ export class GifExporter {
}
});
- // gif.js doesn't have a typed 'error' event, but we can catch errors in the try/catch
+ // gif.js's TypeScript definitions don't declare an 'error' event, but the
+ // underlying EventEmitter can still emit one (e.g. worker errors), and
+ // gif.js can also emit 'abort'. Wire both up so failures reject the
+ // promise instead of leaving the export hanging forever.
+ this.gif!.on("abort", () => {
+ reject(new Error("GIF rendering was aborted"));
+ });
+ (this.gif as unknown as { on(event: "error", listener: (err: unknown) => void): void }).on(
+ "error",
+ (err: unknown) => {
+ reject(
+ new Error(
+ `GIF rendering failed: ${err instanceof Error ? err.message : String(err)}`,
+ ),
+ );
+ },
+ );
+
this.gif!.render();
});
diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts
index 5b65882b8..b335d8b23 100644
--- a/src/lib/exporter/modernVideoExporter.ts
+++ b/src/lib/exporter/modernVideoExporter.ts
@@ -1,3 +1,4 @@
+import { toast } from "sonner";
import type {
AnnotationRegion,
AudioRegion,
@@ -287,6 +288,7 @@ type NativeStaticLayoutZoomSample = {
};
const NATIVE_EXPORT_ENGINE_NAME = "Breeze";
+const NATIVE_STATIC_LAYOUT_FALLBACK_TOAST_ID = "native-static-layout-export-fallback";
const READABLE_SOURCE_RETRY_ERROR_TOKENS = [
"readavpacket",
"get_media_info",
@@ -2603,6 +2605,10 @@ export class ModernVideoExporter {
console.warn("[VideoExporter] Native static layout export failed; falling back", error);
this.nativeStaticLayoutSkipReason = "native-static-runtime-failed";
this.nativeStaticLayoutSkipReasons = [this.nativeStaticLayoutSkipReason];
+ toast.warning(
+ "Hardware-accelerated export isn't available right now, so this export will use the standard (slower) path instead.",
+ { id: NATIVE_STATIC_LAYOUT_FALLBACK_TOAST_ID, duration: 8000 },
+ );
restoreEncoderState();
return null;
} finally {
diff --git a/src/lib/extensions/extensionHost.ts b/src/lib/extensions/extensionHost.ts
index d0683e7e3..af8c16239 100644
--- a/src/lib/extensions/extensionHost.ts
+++ b/src/lib/extensions/extensionHost.ts
@@ -26,7 +26,28 @@ import type {
RenderHookPhase,
} from "./types";
-const EXTENSION_SETTINGS_STORAGE_KEY = "recordly.extension-settings.v1";
+// Legacy (pre-namespacing) storage key. All extensions' settings used to be
+// stored together, in plaintext, under this single key — which meant any
+// extension could read every other extension's settings via raw
+// `window.localStorage` access (extensions run via dynamic import() in the
+// same main world, so raw localStorage is directly reachable, bypassing the
+// host API's per-extension scoping entirely). Kept around only so
+// `migrateLegacySharedSettingsStore()` can copy already-installed extensions'
+// settings forward into their own namespaced keys.
+const LEGACY_SHARED_EXTENSION_SETTINGS_STORAGE_KEY = "recordly.extension-settings.v1";
+
+// Each extension now gets its own namespaced localStorage key, e.g.
+// "recordly.extension-settings.v1:my-extension-id". This prevents one
+// extension from reading another's persisted settings via raw localStorage
+// access — the host API already scoped access correctly, but raw
+// `window.localStorage` access did not.
+const EXTENSION_SETTINGS_STORAGE_KEY_PREFIX = "recordly.extension-settings.v1:";
+
+const LEGACY_SETTINGS_MIGRATION_FLAG_KEY = "recordly.extension-settings.v1.migrated";
+
+function getNamespacedExtensionSettingsKey(extensionId: string): string {
+ return `${EXTENSION_SETTINGS_STORAGE_KEY_PREFIX}${extensionId}`;
+}
// ---------------------------------------------------------------------------
// Security: Hide electronAPI from extension code
@@ -144,7 +165,6 @@ export class ExtensionHost {
Set<(settingId: string, value: unknown) => void>
>();
private listeners = new Set<() => void>();
- private fullSettingsStore: Record> | null = null;
private persistTimeout: ReturnType | null = null;
private iconPathCache = new Map();
@@ -552,13 +572,63 @@ export class ExtensionHost {
}
}
- private readPersistedSettingsStore(): Record> {
+ /**
+ * One-time, best-effort migration: copies each extension's settings out of
+ * the old shared plaintext key into its own namespaced key, so
+ * already-installed extensions don't silently lose their settings when
+ * this per-extension scoping was introduced. Safe to call multiple times;
+ * it no-ops once a flag has been recorded, and it never overwrites a
+ * namespaced key that already has data.
+ */
+ private migrateLegacySharedSettingsStore(): void {
+ if (typeof window === "undefined" || !window.localStorage) {
+ return;
+ }
+
+ try {
+ if (window.localStorage.getItem(LEGACY_SETTINGS_MIGRATION_FLAG_KEY)) {
+ return;
+ }
+
+ const raw = window.localStorage.getItem(LEGACY_SHARED_EXTENSION_SETTINGS_STORAGE_KEY);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
+ for (const [extensionId, settings] of Object.entries(
+ parsed as Record,
+ )) {
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
+ continue;
+ }
+
+ const namespacedKey = getNamespacedExtensionSettingsKey(extensionId);
+ // Don't clobber anything already migrated/written under the new key.
+ if (window.localStorage.getItem(namespacedKey) != null) {
+ continue;
+ }
+
+ window.localStorage.setItem(namespacedKey, JSON.stringify(settings));
+ }
+ }
+ }
+
+ // Leave the legacy key in place (in case of bugs in this migration) but
+ // record that migration ran, so we don't redo it on every load.
+ window.localStorage.setItem(LEGACY_SETTINGS_MIGRATION_FLAG_KEY, "1");
+ } catch {
+ // Best-effort — if anything goes wrong, just skip migration.
+ }
+ }
+
+ private readPersistedExtensionSettings(extensionId: string): Record {
if (typeof window === "undefined" || !window.localStorage) {
return {};
}
+ this.migrateLegacySharedSettingsStore();
+
try {
- const raw = window.localStorage.getItem(EXTENSION_SETTINGS_STORAGE_KEY);
+ const raw = window.localStorage.getItem(getNamespacedExtensionSettingsKey(extensionId));
if (!raw) {
return {};
}
@@ -568,63 +638,51 @@ export class ExtensionHost {
return {};
}
- return parsed as Record>;
+ return parsed as Record;
} catch {
return {};
}
}
- private writePersistedSettingsStore(store: Record>): void {
+ private writePersistedExtensionSettings(
+ extensionId: string,
+ settings: Record,
+ ): void {
if (typeof window === "undefined" || !window.localStorage) {
return;
}
try {
- window.localStorage.setItem(EXTENSION_SETTINGS_STORAGE_KEY, JSON.stringify(store));
+ const key = getNamespacedExtensionSettingsKey(extensionId);
+ if (Object.keys(settings).length === 0) {
+ window.localStorage.removeItem(key);
+ } else {
+ window.localStorage.setItem(key, JSON.stringify(settings));
+ }
} catch {
// Ignore storage quota / privacy mode failures.
}
}
- private getFullSettingsStore(): Record> {
- if (this.fullSettingsStore) {
- return this.fullSettingsStore;
- }
- this.fullSettingsStore = this.readPersistedSettingsStore();
- return this.fullSettingsStore;
- }
-
private ensureExtensionSettingsLoaded(extensionId: string): void {
if (this.extensionSettings.has(extensionId)) {
return;
}
- const store = this.getFullSettingsStore();
- const persisted = store[extensionId];
- const normalized =
- persisted && typeof persisted === "object" && !Array.isArray(persisted)
- ? { ...persisted }
- : {};
-
- this.extensionSettings.set(extensionId, normalized);
+ const persisted = this.readPersistedExtensionSettings(extensionId);
+ this.extensionSettings.set(extensionId, { ...persisted });
}
private persistExtensionSettings(extensionId: string): void {
- const store = this.getFullSettingsStore();
- const settings = this.extensionSettings.get(extensionId) ?? {};
-
- if (Object.keys(settings).length === 0) {
- delete store[extensionId];
- } else {
- store[extensionId] = { ...settings };
- }
-
// Debounce the actual write to localStorage to avoid blocking the UI thread during rapid changes
if (this.persistTimeout) {
clearTimeout(this.persistTimeout);
}
this.persistTimeout = setTimeout(() => {
- this.writePersistedSettingsStore(store);
+ this.writePersistedExtensionSettings(
+ extensionId,
+ this.extensionSettings.get(extensionId) ?? {},
+ );
this.persistTimeout = null;
}, 500);
}
@@ -634,7 +692,9 @@ export class ExtensionHost {
clearTimeout(this.persistTimeout);
this.persistTimeout = null;
}
- this.writePersistedSettingsStore(this.getFullSettingsStore());
+ for (const [extensionId, settings] of this.extensionSettings.entries()) {
+ this.writePersistedExtensionSettings(extensionId, settings);
+ }
}
/**