Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5069354df3 | |||
| ee69df9222 | |||
| a235a0c50b |
+11
-5
@@ -13,11 +13,17 @@
|
|||||||
},
|
},
|
||||||
"npmRebuild": true,
|
"npmRebuild": true,
|
||||||
"buildDependenciesFromSource": true,
|
"buildDependenciesFromSource": true,
|
||||||
"compression": "normal",
|
"compression": "normal",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release/${version}"
|
"output": "release/${version}"
|
||||||
},
|
},
|
||||||
"files": [
|
"publish": [
|
||||||
|
{
|
||||||
|
"provider": "generic",
|
||||||
|
"url": "https://gittea.softs.business/huanld/openscreen/raw/branch/release-assets%2Flatest"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"dist-electron",
|
"dist-electron",
|
||||||
"!*.png",
|
"!*.png",
|
||||||
|
|||||||
Vendored
+8
@@ -24,6 +24,14 @@ declare namespace NodeJS {
|
|||||||
// Used in Renderer process, expose in `preload.ts`
|
// Used in Renderer process, expose in `preload.ts`
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: {
|
electronAPI: {
|
||||||
|
updates: {
|
||||||
|
getStatus: () => Promise<import("../src/lib/updateStatus").UpdateStatus>;
|
||||||
|
check: () => Promise<import("../src/lib/updateStatus").UpdateCheckResult>;
|
||||||
|
install: () => Promise<import("../src/lib/updateStatus").UpdateCheckResult>;
|
||||||
|
onStatus: (
|
||||||
|
callback: (status: import("../src/lib/updateStatus").UpdateStatus) => void,
|
||||||
|
) => () => void;
|
||||||
|
};
|
||||||
invokeNativeBridge: <TData = unknown>(
|
invokeNativeBridge: <TData = unknown>(
|
||||||
request: import("../src/native/contracts").NativeBridgeRequest,
|
request: import("../src/native/contracts").NativeBridgeRequest,
|
||||||
) => Promise<import("../src/native/contracts").NativeBridgeResponse<TData>>;
|
) => Promise<import("../src/native/contracts").NativeBridgeResponse<TData>>;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "electron";
|
} from "electron";
|
||||||
import { mainT, setMainLocale } from "./i18n";
|
import { mainT, setMainLocale } from "./i18n";
|
||||||
import { getSelectedDesktopSource, registerIpcHandlers } from "./ipc/handlers";
|
import { getSelectedDesktopSource, registerIpcHandlers } from "./ipc/handlers";
|
||||||
|
import { initializeAutoUpdates } from "./updater";
|
||||||
import {
|
import {
|
||||||
createCountdownOverlayWindow,
|
createCountdownOverlayWindow,
|
||||||
createEditorWindow,
|
createEditorWindow,
|
||||||
@@ -515,6 +516,7 @@ app.whenReady().then(async () => {
|
|||||||
createTray();
|
createTray();
|
||||||
updateTrayMenu();
|
updateTrayMenu();
|
||||||
setupApplicationMenu();
|
setupApplicationMenu();
|
||||||
|
initializeAutoUpdates();
|
||||||
// Ensure recordings directory exists
|
// Ensure recordings directory exists
|
||||||
await ensureRecordingsDir();
|
await ensureRecordingsDir();
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,27 @@ const assetBaseUrl = assetBaseUrlArg ? assetBaseUrlArg.slice(ASSET_BASE_URL_ARG_
|
|||||||
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
|
updates: {
|
||||||
|
getStatus: () => {
|
||||||
|
return ipcRenderer.invoke("updates:get-status");
|
||||||
|
},
|
||||||
|
check: () => {
|
||||||
|
return ipcRenderer.invoke("updates:check");
|
||||||
|
},
|
||||||
|
install: () => {
|
||||||
|
return ipcRenderer.invoke("updates:install");
|
||||||
|
},
|
||||||
|
onStatus: (callback: (status: import("../src/lib/updateStatus").UpdateStatus) => void) => {
|
||||||
|
const listener = (
|
||||||
|
_event: Electron.IpcRendererEvent,
|
||||||
|
status: import("../src/lib/updateStatus").UpdateStatus,
|
||||||
|
) => {
|
||||||
|
callback(status);
|
||||||
|
};
|
||||||
|
ipcRenderer.on("updates:status", listener);
|
||||||
|
return () => ipcRenderer.removeListener("updates:status", listener);
|
||||||
|
},
|
||||||
|
},
|
||||||
invokeNativeBridge: <TData>(request: NativeBridgeRequest) => {
|
invokeNativeBridge: <TData>(request: NativeBridgeRequest) => {
|
||||||
return ipcRenderer.invoke(NATIVE_BRIDGE_CHANNEL, request) as Promise<TData>;
|
return ipcRenderer.invoke(NATIVE_BRIDGE_CHANNEL, request) as Promise<TData>;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { app, BrowserWindow, ipcMain } from "electron";
|
||||||
|
import { autoUpdater, type ProgressInfo, type UpdateInfo } from "electron-updater";
|
||||||
|
import type { UpdateCheckResult, UpdateStatus } from "../src/lib/updateStatus";
|
||||||
|
|
||||||
|
const DEFAULT_UPDATE_FEED_URL =
|
||||||
|
"https://gittea.softs.business/huanld/openscreen/raw/branch/release-assets%2Flatest";
|
||||||
|
const AUTO_CHECK_DELAY_MS = 10_000;
|
||||||
|
const AUTO_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
let status: UpdateStatus = createStatus("idle");
|
||||||
|
let handlersRegistered = false;
|
||||||
|
let initialized = false;
|
||||||
|
let checkInFlight: Promise<UpdateCheckResult> | null = null;
|
||||||
|
|
||||||
|
function createStatus(
|
||||||
|
phase: UpdateStatus["phase"],
|
||||||
|
patch: Partial<UpdateStatus> = {},
|
||||||
|
): UpdateStatus {
|
||||||
|
return {
|
||||||
|
phase,
|
||||||
|
currentVersion: app.getVersion(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
...patch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReleaseNotes(releaseNotes: UpdateInfo["releaseNotes"]): string | undefined {
|
||||||
|
if (typeof releaseNotes === "string") {
|
||||||
|
return releaseNotes;
|
||||||
|
}
|
||||||
|
if (Array.isArray(releaseNotes)) {
|
||||||
|
return releaseNotes
|
||||||
|
.map((note) => note.note)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n");
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(next: UpdateStatus) {
|
||||||
|
status = next;
|
||||||
|
for (const window of BrowserWindow.getAllWindows()) {
|
||||||
|
if (!window.isDestroyed()) {
|
||||||
|
window.webContents.send("updates:status", status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusFromInfo(phase: UpdateStatus["phase"], info: UpdateInfo): UpdateStatus {
|
||||||
|
return createStatus(phase, {
|
||||||
|
version: info.version,
|
||||||
|
releaseName: info.releaseName ?? undefined,
|
||||||
|
releaseNotes: normalizeReleaseNotes(info.releaseNotes),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates(): Promise<UpdateCheckResult> {
|
||||||
|
if (!initialized) {
|
||||||
|
updateStatus(
|
||||||
|
createStatus("unsupported", {
|
||||||
|
error: "Update service is not initialized.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return { success: false, status, error: status.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app.isPackaged && process.env.OPENSCREEN_ALLOW_DEV_UPDATE_CHECK !== "1") {
|
||||||
|
updateStatus(
|
||||||
|
createStatus("unsupported", {
|
||||||
|
error: "Update checks only run in packaged builds.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return { success: false, status, error: status.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkInFlight) {
|
||||||
|
return checkInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(createStatus("checking"));
|
||||||
|
checkInFlight = autoUpdater
|
||||||
|
.checkForUpdates()
|
||||||
|
.then(() => ({ success: true, status }))
|
||||||
|
.catch((error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
updateStatus(createStatus("error", { error: message }));
|
||||||
|
return { success: false, status, error: message };
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
checkInFlight = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return checkInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerUpdateIpcHandlers() {
|
||||||
|
if (handlersRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlersRegistered = true;
|
||||||
|
ipcMain.handle("updates:get-status", () => status);
|
||||||
|
ipcMain.handle("updates:check", () => checkForUpdates());
|
||||||
|
ipcMain.handle("updates:install", () => {
|
||||||
|
if (status.phase !== "downloaded") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status,
|
||||||
|
error: "No downloaded update is ready to install.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setImmediate(() => autoUpdater.quitAndInstall(false, true));
|
||||||
|
return { success: true, status };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeAutoUpdates() {
|
||||||
|
registerUpdateIpcHandlers();
|
||||||
|
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
autoUpdater.autoDownload = true;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
autoUpdater.logger = console;
|
||||||
|
|
||||||
|
const feedUrl = process.env.OPENSCREEN_UPDATE_FEED_URL?.trim() || DEFAULT_UPDATE_FEED_URL;
|
||||||
|
const updateToken = process.env.OPENSCREEN_UPDATE_TOKEN?.trim();
|
||||||
|
if (updateToken) {
|
||||||
|
autoUpdater.requestHeaders = {
|
||||||
|
Authorization: `token ${updateToken}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
autoUpdater.setFeedURL({
|
||||||
|
provider: "generic",
|
||||||
|
url: feedUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("checking-for-update", () => {
|
||||||
|
updateStatus(createStatus("checking"));
|
||||||
|
});
|
||||||
|
autoUpdater.on("update-available", (info) => {
|
||||||
|
updateStatus(statusFromInfo("available", info));
|
||||||
|
});
|
||||||
|
autoUpdater.on("update-not-available", (info) => {
|
||||||
|
updateStatus(statusFromInfo("not-available", info));
|
||||||
|
});
|
||||||
|
autoUpdater.on("download-progress", (progress: ProgressInfo) => {
|
||||||
|
updateStatus(
|
||||||
|
createStatus("downloading", {
|
||||||
|
version: status.version,
|
||||||
|
releaseName: status.releaseName,
|
||||||
|
releaseNotes: status.releaseNotes,
|
||||||
|
percent: progress.percent,
|
||||||
|
bytesPerSecond: progress.bytesPerSecond,
|
||||||
|
transferred: progress.transferred,
|
||||||
|
total: progress.total,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
|
updateStatus(statusFromInfo("downloaded", info));
|
||||||
|
});
|
||||||
|
autoUpdater.on("error", (error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
updateStatus(createStatus("error", { error: message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
void checkForUpdates();
|
||||||
|
}, AUTO_CHECK_DELAY_MS);
|
||||||
|
setInterval(() => {
|
||||||
|
void checkForUpdates();
|
||||||
|
}, AUTO_CHECK_INTERVAL_MS).unref();
|
||||||
|
}
|
||||||
Generated
+85
-10
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "openscreen",
|
"name": "openscreen",
|
||||||
"version": "1.4.10",
|
"version": "1.4.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "openscreen",
|
"name": "openscreen",
|
||||||
"version": "1.4.10",
|
"version": "1.4.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fix-webm-duration/fix": "^1.0.1",
|
"@fix-webm-duration/fix": "^1.0.1",
|
||||||
"@pixi/filter-drop-shadow": "^5.2.0",
|
"@pixi/filter-drop-shadow": "^5.2.0",
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dnd-timeline": "^2.4.0",
|
"dnd-timeline": "^2.4.0",
|
||||||
|
"electron-updater": "^6.8.3",
|
||||||
"emoji-picker-react": "^4.18.0",
|
"emoji-picker-react": "^4.18.0",
|
||||||
"fix-webm-duration": "^1.0.6",
|
"fix-webm-duration": "^1.0.6",
|
||||||
"gif.js": "^0.2.0",
|
"gif.js": "^0.2.0",
|
||||||
@@ -4625,7 +4626,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
@@ -4959,7 +4959,6 @@
|
|||||||
"version": "9.5.1",
|
"version": "9.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
|
||||||
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
|
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@@ -5504,7 +5503,6 @@
|
|||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -6015,6 +6013,69 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-updater": {
|
||||||
|
"version": "6.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
|
||||||
|
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"builder-util-runtime": "9.5.1",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lazy-val": "^1.0.5",
|
||||||
|
"lodash.escaperegexp": "^4.1.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"semver": "~7.7.3",
|
||||||
|
"tiny-typed-emitter": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/fs-extra": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/jsonfile": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-winstaller": {
|
"node_modules/electron-winstaller": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
||||||
@@ -6874,7 +6935,6 @@
|
|||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/gsap": {
|
"node_modules/gsap": {
|
||||||
@@ -7287,7 +7347,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -7419,7 +7478,6 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
@@ -7586,6 +7644,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.escaperegexp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/log-update": {
|
"node_modules/log-update": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
|
||||||
@@ -7991,7 +8062,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mz": {
|
"node_modules/mz": {
|
||||||
@@ -9394,7 +9464,6 @@
|
|||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=11.0.0"
|
"node": ">=11.0.0"
|
||||||
@@ -10099,6 +10168,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-typed-emitter": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "openscreen",
|
"name": "openscreen",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.10",
|
"version": "1.4.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "npm@10.9.4",
|
"packageManager": "npm@10.9.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dnd-timeline": "^2.4.0",
|
"dnd-timeline": "^2.4.0",
|
||||||
|
"electron-updater": "^6.8.3",
|
||||||
"emoji-picker-react": "^4.18.0",
|
"emoji-picker-react": "^4.18.0",
|
||||||
"fix-webm-duration": "^1.0.6",
|
"fix-webm-duration": "^1.0.6",
|
||||||
"gif.js": "^0.2.0",
|
"gif.js": "^0.2.0",
|
||||||
|
|||||||
+70
-1
@@ -1,4 +1,5 @@
|
|||||||
import { lazy, Suspense, useEffect, useState } from "react";
|
import { lazy, Suspense, useEffect, useRef, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { CountdownOverlay } from "./components/launch/CountdownOverlay.tsx";
|
import { CountdownOverlay } from "./components/launch/CountdownOverlay.tsx";
|
||||||
import { LaunchWindow } from "./components/launch/LaunchWindow";
|
import { LaunchWindow } from "./components/launch/LaunchWindow";
|
||||||
import { SourceSelector } from "./components/launch/SourceSelector";
|
import { SourceSelector } from "./components/launch/SourceSelector";
|
||||||
@@ -6,6 +7,7 @@ import { Toaster } from "./components/ui/sonner";
|
|||||||
import { TooltipProvider } from "./components/ui/tooltip";
|
import { TooltipProvider } from "./components/ui/tooltip";
|
||||||
import { ShortcutsProvider } from "./contexts/ShortcutsContext";
|
import { ShortcutsProvider } from "./contexts/ShortcutsContext";
|
||||||
import { loadAllCustomFonts } from "./lib/customFonts";
|
import { loadAllCustomFonts } from "./lib/customFonts";
|
||||||
|
import type { UpdateStatus } from "./lib/updateStatus";
|
||||||
|
|
||||||
const VideoEditor = lazy(() => import("./components/video-editor/VideoEditor"));
|
const VideoEditor = lazy(() => import("./components/video-editor/VideoEditor"));
|
||||||
const ShortcutsConfigDialog = lazy(() =>
|
const ShortcutsConfigDialog = lazy(() =>
|
||||||
@@ -79,11 +81,78 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{content}
|
{content}
|
||||||
|
<UpdateNotifier
|
||||||
|
enabled={
|
||||||
|
hasElectronBridge &&
|
||||||
|
windowType !== "hud-overlay" &&
|
||||||
|
windowType !== "source-selector" &&
|
||||||
|
windowType !== "countdown-overlay"
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Toaster theme="dark" className="pointer-events-auto" />
|
<Toaster theme="dark" className="pointer-events-auto" />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UpdateNotifier({ enabled }: { enabled: boolean }) {
|
||||||
|
const lastPhaseRef = useRef<UpdateStatus["phase"]>("idle");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !window.electronAPI?.updates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyStatus = (status: UpdateStatus) => {
|
||||||
|
const version = status.version ? ` ${status.version}` : "";
|
||||||
|
if (status.phase === "available") {
|
||||||
|
toast.loading(`Downloading OpenScreen${version} update...`, {
|
||||||
|
id: "openscreen-update",
|
||||||
|
duration: Number.POSITIVE_INFINITY,
|
||||||
|
});
|
||||||
|
} else if (status.phase === "downloading") {
|
||||||
|
const percent = typeof status.percent === "number" ? ` ${Math.round(status.percent)}%` : "";
|
||||||
|
toast.loading(`Downloading OpenScreen${version} update${percent}...`, {
|
||||||
|
id: "openscreen-update",
|
||||||
|
duration: Number.POSITIVE_INFINITY,
|
||||||
|
});
|
||||||
|
} else if (status.phase === "downloaded") {
|
||||||
|
toast.success(`OpenScreen${version} is ready to install.`, {
|
||||||
|
id: "openscreen-update",
|
||||||
|
duration: Number.POSITIVE_INFINITY,
|
||||||
|
action: {
|
||||||
|
label: "Restart",
|
||||||
|
onClick: () => {
|
||||||
|
void window.electronAPI.updates.install();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
status.phase === "error" &&
|
||||||
|
(lastPhaseRef.current === "available" ||
|
||||||
|
lastPhaseRef.current === "downloading" ||
|
||||||
|
lastPhaseRef.current === "downloaded")
|
||||||
|
) {
|
||||||
|
toast.error(status.error || "OpenScreen update failed.", {
|
||||||
|
id: "openscreen-update",
|
||||||
|
});
|
||||||
|
} else if (status.phase === "not-available" || status.phase === "unsupported") {
|
||||||
|
toast.dismiss("openscreen-update");
|
||||||
|
}
|
||||||
|
lastPhaseRef.current = status.phase;
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = window.electronAPI.updates.onStatus(applyStatus);
|
||||||
|
void window.electronAPI.updates
|
||||||
|
.getStatus()
|
||||||
|
.then(applyStatus)
|
||||||
|
.catch(() => undefined);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function BrowserDevFallback() {
|
function BrowserDevFallback() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen items-center justify-center bg-[#08090b] px-6 text-slate-100">
|
<div className="flex h-screen w-screen items-center justify-center bg-[#08090b] px-6 text-slate-100">
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ describe("buildGuideVideoAnnotations", () => {
|
|||||||
startMs: 1200,
|
startMs: 1200,
|
||||||
content: "1. Click Settings.",
|
content: "1. Click Settings.",
|
||||||
});
|
});
|
||||||
|
expect(annotations[0]?.endMs).toBe(3200);
|
||||||
expect(annotations[0]?.position.x).toBeGreaterThan(20);
|
expect(annotations[0]?.position.x).toBeGreaterThan(20);
|
||||||
|
expect(annotations[1]?.endMs).toBe(3200);
|
||||||
|
expect(annotations[1]?.position.x).toBeGreaterThan((annotations[0]?.position.x ?? 0) + 34);
|
||||||
|
expect(annotations[1]?.position.y).toBeCloseTo(30.5);
|
||||||
expect(annotations[1]).toMatchObject({
|
expect(annotations[1]).toMatchObject({
|
||||||
id: "guide-video-2",
|
id: "guide-video-2",
|
||||||
type: "magnifier",
|
type: "magnifier",
|
||||||
@@ -79,10 +83,13 @@ describe("buildGuideVideoAnnotations", () => {
|
|||||||
expect(annotations[2]).toMatchObject({
|
expect(annotations[2]).toMatchObject({
|
||||||
id: "guide-video-3",
|
id: "guide-video-3",
|
||||||
type: "figure",
|
type: "figure",
|
||||||
|
endMs: 3200,
|
||||||
figureData: {
|
figureData: {
|
||||||
|
arrowDirection: "left",
|
||||||
color: "#34B27B",
|
color: "#34B27B",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
expect(annotations[2]?.position.x).toBeGreaterThan(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an empty list when no draft exists", () => {
|
it("returns an empty list when no draft exists", () => {
|
||||||
@@ -97,7 +104,7 @@ describe("buildGuideVideoAnnotations", () => {
|
|||||||
expect(annotations).toEqual([]);
|
expect(annotations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates 0.3x speed regions for one second at each guide point", () => {
|
it("creates 0.3x speed regions for two seconds at each guide point", () => {
|
||||||
let id = 1;
|
let id = 1;
|
||||||
const speedRegions = buildGuideVideoSpeedRegions(createSession(), {
|
const speedRegions = buildGuideVideoSpeedRegions(createSession(), {
|
||||||
nextId: () => `guide-speed-${id++}`,
|
nextId: () => `guide-speed-${id++}`,
|
||||||
@@ -107,7 +114,7 @@ describe("buildGuideVideoAnnotations", () => {
|
|||||||
{
|
{
|
||||||
id: "guide-speed-1",
|
id: "guide-speed-1",
|
||||||
startMs: 1200,
|
startMs: 1200,
|
||||||
endMs: 2200,
|
endMs: 3200,
|
||||||
speed: 0.3,
|
speed: 0.3,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ export interface BuildGuideVideoAnnotationsOptions {
|
|||||||
defaultDurationMs?: number;
|
defaultDurationMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_STEP_DURATION_MS = 3200;
|
const DEFAULT_STEP_DURATION_MS = 2000;
|
||||||
const DEFAULT_STEP_SLOW_MOTION_DURATION_MS = 1000;
|
const DEFAULT_STEP_SLOW_MOTION_DURATION_MS = 2000;
|
||||||
const DEFAULT_STEP_SLOW_MOTION_SPEED = 0.3;
|
const DEFAULT_STEP_SLOW_MOTION_SPEED = 0.3;
|
||||||
const CAPTION_WIDTH = 34;
|
const CAPTION_WIDTH = 34;
|
||||||
const CAPTION_HEIGHT = 13;
|
const CAPTION_HEIGHT = 13;
|
||||||
const MAGNIFIER_SIZE = 18;
|
const MAGNIFIER_SIZE = 18;
|
||||||
const ARROW_SIZE = 10;
|
const ARROW_SIZE = 10;
|
||||||
|
const ANNOTATION_GAP = 2;
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
@@ -58,15 +59,19 @@ function getCaptionPosition(candidate: GuideStepCandidate | undefined) {
|
|||||||
|
|
||||||
function getArrowDirection(
|
function getArrowDirection(
|
||||||
candidate: GuideStepCandidate | undefined,
|
candidate: GuideStepCandidate | undefined,
|
||||||
captionPosition: { x: number; y: number },
|
originPosition: { x: number; y: number },
|
||||||
|
originSize: { width: number; height: number } = {
|
||||||
|
width: CAPTION_WIDTH,
|
||||||
|
height: CAPTION_HEIGHT,
|
||||||
|
},
|
||||||
): ArrowDirection {
|
): ArrowDirection {
|
||||||
const target = candidate?.position;
|
const target = candidate?.position;
|
||||||
if (!target) return "right";
|
if (!target) return "right";
|
||||||
|
|
||||||
const captionCenterX = captionPosition.x + CAPTION_WIDTH / 2;
|
const originCenterX = originPosition.x + originSize.width / 2;
|
||||||
const captionCenterY = captionPosition.y + CAPTION_HEIGHT / 2;
|
const originCenterY = originPosition.y + originSize.height / 2;
|
||||||
const dx = target.normalizedX * 100 - captionCenterX;
|
const dx = target.normalizedX * 100 - originCenterX;
|
||||||
const dy = target.normalizedY * 100 - captionCenterY;
|
const dy = target.normalizedY * 100 - originCenterY;
|
||||||
const horizontal = dx > 8 ? "right" : dx < -8 ? "left" : "";
|
const horizontal = dx > 8 ? "right" : dx < -8 ? "left" : "";
|
||||||
const vertical = dy > 8 ? "down" : dy < -8 ? "up" : "";
|
const vertical = dy > 8 ? "down" : dy < -8 ? "up" : "";
|
||||||
|
|
||||||
@@ -74,6 +79,40 @@ function getArrowDirection(
|
|||||||
return (horizontal || vertical || "right") as ArrowDirection;
|
return (horizontal || vertical || "right") as ArrowDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMagnifierPosition(captionPosition: { x: number; y: number }) {
|
||||||
|
const canPlaceRight = captionPosition.x + CAPTION_WIDTH + ANNOTATION_GAP + MAGNIFIER_SIZE <= 98;
|
||||||
|
const x = canPlaceRight
|
||||||
|
? captionPosition.x + CAPTION_WIDTH + ANNOTATION_GAP
|
||||||
|
: captionPosition.x - MAGNIFIER_SIZE - ANNOTATION_GAP;
|
||||||
|
const y = captionPosition.y + (CAPTION_HEIGHT - MAGNIFIER_SIZE) / 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: clamp(x, 2, 100 - MAGNIFIER_SIZE - 2),
|
||||||
|
y: clamp(y, 2, 100 - MAGNIFIER_SIZE - 2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrowPosition(
|
||||||
|
position: NonNullable<GuideStepCandidate["position"]>,
|
||||||
|
originPosition: { x: number; y: number },
|
||||||
|
originSize: { width: number; height: number },
|
||||||
|
) {
|
||||||
|
const targetX = position.normalizedX * 100;
|
||||||
|
const targetY = position.normalizedY * 100;
|
||||||
|
const originCenterX = originPosition.x + originSize.width / 2;
|
||||||
|
const originCenterY = originPosition.y + originSize.height / 2;
|
||||||
|
const distance = Math.hypot(targetX - originCenterX, targetY - originCenterY);
|
||||||
|
const targetOffset = Math.min(18, Math.max(10, distance * 0.35));
|
||||||
|
const ratio = distance > 0 ? Math.max(0, (distance - targetOffset) / distance) : 0;
|
||||||
|
const arrowCenterX = originCenterX + (targetX - originCenterX) * ratio;
|
||||||
|
const arrowCenterY = originCenterY + (targetY - originCenterY) * ratio;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: clamp(arrowCenterX - ARROW_SIZE / 2, 0, 100 - ARROW_SIZE),
|
||||||
|
y: clamp(arrowCenterY - ARROW_SIZE / 2, 0, 100 - ARROW_SIZE),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildCaption(step: GeneratedGuideStep) {
|
function buildCaption(step: GeneratedGuideStep) {
|
||||||
const instruction = step.instruction.trim();
|
const instruction = step.instruction.trim();
|
||||||
const title = step.title.trim();
|
const title = step.title.trim();
|
||||||
@@ -101,7 +140,6 @@ export function buildGuideVideoAnnotations(
|
|||||||
const startMs = Math.max(0, Math.round(candidate?.timeMs ?? index * durationMs));
|
const startMs = Math.max(0, Math.round(candidate?.timeMs ?? index * durationMs));
|
||||||
const endMs = Math.max(startMs + 750, startMs + durationMs);
|
const endMs = Math.max(startMs + 750, startMs + durationMs);
|
||||||
const captionPosition = getCaptionPosition(candidate);
|
const captionPosition = getCaptionPosition(candidate);
|
||||||
const arrowDirection = getArrowDirection(candidate, captionPosition);
|
|
||||||
|
|
||||||
annotations.push({
|
annotations.push({
|
||||||
id: options.nextId(),
|
id: options.nextId(),
|
||||||
@@ -124,24 +162,23 @@ export function buildGuideVideoAnnotations(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (candidate?.position) {
|
if (candidate?.position) {
|
||||||
|
const magnifierPosition = getMagnifierPosition(captionPosition);
|
||||||
|
const arrowPosition = getArrowPosition(candidate.position, magnifierPosition, {
|
||||||
|
width: MAGNIFIER_SIZE,
|
||||||
|
height: MAGNIFIER_SIZE,
|
||||||
|
});
|
||||||
|
const arrowDirection = getArrowDirection(candidate, arrowPosition, {
|
||||||
|
width: ARROW_SIZE,
|
||||||
|
height: ARROW_SIZE,
|
||||||
|
});
|
||||||
|
|
||||||
annotations.push({
|
annotations.push({
|
||||||
id: options.nextId(),
|
id: options.nextId(),
|
||||||
startMs,
|
startMs,
|
||||||
endMs,
|
endMs,
|
||||||
type: "magnifier",
|
type: "magnifier",
|
||||||
content: buildCaption(step),
|
content: buildCaption(step),
|
||||||
position: {
|
position: magnifierPosition,
|
||||||
x: clamp(
|
|
||||||
candidate.position.normalizedX * 100 - MAGNIFIER_SIZE / 2,
|
|
||||||
0,
|
|
||||||
100 - MAGNIFIER_SIZE,
|
|
||||||
),
|
|
||||||
y: clamp(
|
|
||||||
candidate.position.normalizedY * 100 - MAGNIFIER_SIZE / 2,
|
|
||||||
0,
|
|
||||||
100 - MAGNIFIER_SIZE,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
size: { width: MAGNIFIER_SIZE, height: MAGNIFIER_SIZE },
|
size: { width: MAGNIFIER_SIZE, height: MAGNIFIER_SIZE },
|
||||||
style: { ...DEFAULT_ANNOTATION_STYLE },
|
style: { ...DEFAULT_ANNOTATION_STYLE },
|
||||||
zIndex: options.nextZIndex(),
|
zIndex: options.nextZIndex(),
|
||||||
@@ -160,10 +197,7 @@ export function buildGuideVideoAnnotations(
|
|||||||
endMs,
|
endMs,
|
||||||
type: "figure",
|
type: "figure",
|
||||||
content: "",
|
content: "",
|
||||||
position: {
|
position: arrowPosition,
|
||||||
x: clamp(candidate.position.normalizedX * 100 - ARROW_SIZE / 2, 0, 100 - ARROW_SIZE),
|
|
||||||
y: clamp(candidate.position.normalizedY * 100 - ARROW_SIZE / 2, 0, 100 - ARROW_SIZE),
|
|
||||||
},
|
|
||||||
size: { width: ARROW_SIZE, height: ARROW_SIZE },
|
size: { width: ARROW_SIZE, height: ARROW_SIZE },
|
||||||
style: { ...DEFAULT_ANNOTATION_STYLE },
|
style: { ...DEFAULT_ANNOTATION_STYLE },
|
||||||
zIndex: options.nextZIndex(),
|
zIndex: options.nextZIndex(),
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export type UpdateStatusPhase =
|
||||||
|
| "idle"
|
||||||
|
| "checking"
|
||||||
|
| "available"
|
||||||
|
| "not-available"
|
||||||
|
| "downloading"
|
||||||
|
| "downloaded"
|
||||||
|
| "error"
|
||||||
|
| "unsupported";
|
||||||
|
|
||||||
|
export interface UpdateStatus {
|
||||||
|
phase: UpdateStatusPhase;
|
||||||
|
currentVersion: string;
|
||||||
|
version?: string;
|
||||||
|
releaseName?: string;
|
||||||
|
releaseNotes?: string;
|
||||||
|
percent?: number;
|
||||||
|
bytesPerSecond?: number;
|
||||||
|
transferred?: number;
|
||||||
|
total?: number;
|
||||||
|
error?: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateCheckResult {
|
||||||
|
success: boolean;
|
||||||
|
status: UpdateStatus;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user