mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-28 16:55:36 +00:00
merge: Windows update UX fixes (#windows-fixes)
This commit is contained in:
Vendored
+10
@@ -50,6 +50,14 @@ interface UpdateToastState {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
}
|
||||
|
||||
interface UpdateStatusSummary {
|
||||
status: "idle" | "checking" | "up-to-date" | "available" | "downloading" | "ready" | "error";
|
||||
currentVersion: string;
|
||||
availableVersion: string | null;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
@@ -303,7 +311,9 @@ interface Window {
|
||||
dismissUpdateToast: () => Promise<{ success: boolean }>;
|
||||
skipUpdateVersion: () => Promise<{ success: boolean; message?: string }>;
|
||||
getCurrentUpdateToastPayload: () => Promise<UpdateToastState | null>;
|
||||
getUpdateStatusSummary: () => Promise<UpdateStatusSummary>;
|
||||
previewUpdateToast: () => Promise<{ success: boolean }>;
|
||||
checkForAppUpdates: () => Promise<{ success: boolean; logPath: string }>;
|
||||
onUpdateToastStateChanged: (callback: (payload: UpdateToastState | null) => void) => () => void;
|
||||
onUpdateReadyToast: (callback: (payload: {
|
||||
version: string;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ipcMain,
|
||||
Menu,
|
||||
nativeImage,
|
||||
Notification,
|
||||
session,
|
||||
systemPreferences,
|
||||
Tray,
|
||||
@@ -26,11 +27,14 @@ import {
|
||||
downloadAvailableUpdate,
|
||||
deferUpdateReminder,
|
||||
getCurrentUpdateToastPayload,
|
||||
getUpdaterLogPath,
|
||||
getUpdateStatusSummary,
|
||||
installDownloadedUpdateNow,
|
||||
previewUpdateToast,
|
||||
skipAvailableUpdateVersion,
|
||||
setupAutoUpdates,
|
||||
} from "./updater";
|
||||
import type { UpdateToastPayload } from "./updater";
|
||||
import {
|
||||
createEditorWindow,
|
||||
createHudOverlayWindow,
|
||||
@@ -84,6 +88,8 @@ let tray: Tray | null = null;
|
||||
let selectedSourceName = "";
|
||||
let editorHasUnsavedChanges = false;
|
||||
let isForceClosing = false;
|
||||
let activeUpdateNotification: Notification | null = null;
|
||||
let activeUpdateNotificationKey: string | null = null;
|
||||
const hasSingleInstanceLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!hasSingleInstanceLock) {
|
||||
@@ -317,7 +323,99 @@ function syncDockIcon() {
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdateNotificationTitle(payload: UpdateToastPayload) {
|
||||
switch (payload.phase) {
|
||||
case "available":
|
||||
return `Recordly ${payload.version} is available`;
|
||||
case "downloading":
|
||||
return `Downloading Recordly ${payload.version}`;
|
||||
case "ready":
|
||||
return `Recordly ${payload.version} is ready`;
|
||||
case "error":
|
||||
return `Recordly ${payload.version} needs attention`;
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdateNotificationBody(payload: UpdateToastPayload) {
|
||||
switch (payload.phase) {
|
||||
case "available":
|
||||
return "Click to download the update.";
|
||||
case "downloading":
|
||||
return "Recordly is downloading the update in the foreground.";
|
||||
case "ready":
|
||||
return "Click to install the downloaded update.";
|
||||
case "error":
|
||||
return "Click to retry checking for updates.";
|
||||
}
|
||||
}
|
||||
|
||||
function clearActiveUpdateNotification() {
|
||||
if (activeUpdateNotification) {
|
||||
activeUpdateNotification.close();
|
||||
activeUpdateNotification = null;
|
||||
}
|
||||
activeUpdateNotificationKey = null;
|
||||
}
|
||||
|
||||
function sendUpdateToastToWindows(channel: "update-toast-state", payload: unknown) {
|
||||
if (process.platform !== "darwin") {
|
||||
if (!payload) {
|
||||
clearActiveUpdateNotification();
|
||||
return true;
|
||||
}
|
||||
|
||||
const updatePayload = payload as UpdateToastPayload;
|
||||
if (updatePayload.phase === "downloading") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Notification.isSupported()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const notificationKey = [updatePayload.phase, updatePayload.version, updatePayload.detail].join(":");
|
||||
if (activeUpdateNotificationKey === notificationKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
clearActiveUpdateNotification();
|
||||
const notification = new Notification({
|
||||
title: getUpdateNotificationTitle(updatePayload),
|
||||
body: getUpdateNotificationBody(updatePayload),
|
||||
icon: getAppImage("app-icons/recordly-128.png"),
|
||||
silent: false,
|
||||
});
|
||||
|
||||
notification.on("click", () => {
|
||||
focusOrCreateMainWindow();
|
||||
switch (updatePayload.phase) {
|
||||
case "available":
|
||||
void downloadAvailableUpdate(sendUpdateToastToWindows);
|
||||
break;
|
||||
case "ready":
|
||||
installDownloadedUpdateNow(sendUpdateToastToWindows);
|
||||
break;
|
||||
case "error":
|
||||
void checkForAppUpdates(getUpdateDialogWindow, { manual: true });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
notification.on("close", () => {
|
||||
if (activeUpdateNotification === notification) {
|
||||
activeUpdateNotification = null;
|
||||
activeUpdateNotificationKey = null;
|
||||
}
|
||||
});
|
||||
|
||||
notification.show();
|
||||
activeUpdateNotification = notification;
|
||||
activeUpdateNotificationKey = notificationKey;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
const existingWindow = getUpdateToastWindow();
|
||||
if (!existingWindow) {
|
||||
@@ -382,10 +480,19 @@ ipcMain.handle("get-current-update-toast-payload", () => {
|
||||
return getCurrentUpdateToastPayload();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-update-status-summary", () => {
|
||||
return getUpdateStatusSummary();
|
||||
});
|
||||
|
||||
ipcMain.handle("preview-update-toast", () => {
|
||||
return { success: previewUpdateToast(sendUpdateToastToWindows) };
|
||||
});
|
||||
|
||||
ipcMain.handle("check-for-app-updates", async () => {
|
||||
await checkForAppUpdates(getUpdateDialogWindow, { manual: true });
|
||||
return { success: true, logPath: getUpdaterLogPath() };
|
||||
});
|
||||
|
||||
function updateTrayMenu(recording: boolean = false) {
|
||||
if (!tray) return;
|
||||
const trayIcon = recording ? getRecordingTrayIcon() : getDefaultTrayIcon();
|
||||
@@ -506,6 +613,10 @@ app.on("second-instance", () => {
|
||||
|
||||
// Register all IPC handlers when app is ready
|
||||
app.whenReady().then(async () => {
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId("dev.recordly.app");
|
||||
}
|
||||
|
||||
session.defaultSession.setPermissionCheckHandler((_webContents, permission) => {
|
||||
const allowed = ["media", "audioCapture", "microphone", "camera", "videoCapture"];
|
||||
return allowed.includes(permission);
|
||||
|
||||
Binary file not shown.
@@ -5,17 +5,17 @@
|
||||
"helpers": {
|
||||
"wgc-capture": {
|
||||
"binaryName": "wgc-capture.exe",
|
||||
"binarySha256": "5f364b07b016c597288f90a9b61c581e6559d6d32b79ffadceb37859bbea8dd2",
|
||||
"binarySha256": "013164c0a1391d334e5aa2fe0ff2e47b507407e36c7e44e7333a70a84b944643",
|
||||
"sourceDir": "electron/native/wgc-capture",
|
||||
"sourceFingerprint": "9e9bce082266ca5968cf5f0b535b469d47c4ec3a775a93726171c0dfdbcdaa44",
|
||||
"updatedAt": "2026-03-28T05:48:46.587Z"
|
||||
"updatedAt": "2026-03-29T02:15:34.516Z"
|
||||
},
|
||||
"cursor-monitor": {
|
||||
"binaryName": "cursor-monitor.exe",
|
||||
"binarySha256": "916ed064c12a3b861d7721df8b9209d10ec0fdb8078564a2ba3ea8cf4f4354c4",
|
||||
"binarySha256": "b0732abc06998a40c3e95078465ad750a6169901944571c39cfd7996effe39c0",
|
||||
"sourceDir": "electron/native/cursor-monitor",
|
||||
"sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e",
|
||||
"updatedAt": "2026-03-28T05:17:17.697Z"
|
||||
"updatedAt": "2026-03-29T02:15:38.286Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -265,9 +265,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getCurrentUpdateToastPayload: () => {
|
||||
return ipcRenderer.invoke("get-current-update-toast-payload");
|
||||
},
|
||||
getUpdateStatusSummary: () => {
|
||||
return ipcRenderer.invoke("get-update-status-summary");
|
||||
},
|
||||
previewUpdateToast: () => {
|
||||
return ipcRenderer.invoke("preview-update-toast");
|
||||
},
|
||||
checkForAppUpdates: () => {
|
||||
return ipcRenderer.invoke("check-for-app-updates");
|
||||
},
|
||||
onUpdateToastStateChanged: (
|
||||
callback: (payload: {
|
||||
version: string;
|
||||
@@ -276,6 +282,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
} | null) => void,
|
||||
) => {
|
||||
const listener = (
|
||||
@@ -288,6 +295,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
}
|
||||
| null,
|
||||
) => callback(payload);
|
||||
|
||||
+180
-1
@@ -1,17 +1,41 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { app, BrowserWindow, dialog } from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import type { MessageBoxOptions, MessageBoxReturnValue } from "electron";
|
||||
import { USER_DATA_PATH } from "./appPaths";
|
||||
|
||||
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||
export const UPDATE_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000;
|
||||
const DISMISSED_READY_REMINDER_DELAY_MS = 5 * 60 * 1000;
|
||||
const AUTO_UPDATES_DISABLED = process.env.RECORDLY_DISABLE_AUTO_UPDATES === "1";
|
||||
const AUTO_UPDATE_ERROR_TOASTS_DISABLED =
|
||||
process.env.RECORDLY_DISABLE_AUTO_UPDATE_ERROR_TOASTS === "1";
|
||||
const UPDATE_FEED_URL_OVERRIDE = process.env.RECORDLY_UPDATE_FEED_URL?.trim() ?? "";
|
||||
const UPDATER_LOG_PATH =
|
||||
process.env.RECORDLY_UPDATER_LOG_PATH?.trim() || path.join(USER_DATA_PATH, "updater.log");
|
||||
const DEV_UPDATE_PREVIEW_VERSION = "9.9.9";
|
||||
const DEV_UPDATE_PREVIEW_PROGRESS_STEP_MS = 300;
|
||||
const DEV_UPDATE_PREVIEW_PROGRESS_INCREMENT = 20;
|
||||
|
||||
export type UpdateToastPhase = "available" | "downloading" | "ready" | "error";
|
||||
|
||||
export type UpdateStatusKind =
|
||||
| "idle"
|
||||
| "checking"
|
||||
| "up-to-date"
|
||||
| "available"
|
||||
| "downloading"
|
||||
| "ready"
|
||||
| "error";
|
||||
|
||||
export interface UpdateStatusSummary {
|
||||
status: UpdateStatusKind;
|
||||
currentVersion: string;
|
||||
availableVersion: string | null;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface UpdateToastPayload {
|
||||
version: string;
|
||||
detail: string;
|
||||
@@ -19,6 +43,7 @@ export interface UpdateToastPayload {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
}
|
||||
|
||||
type UpdateToastSender = (
|
||||
@@ -38,6 +63,72 @@ let pendingDownloadedVersion: string | null = null;
|
||||
let downloadInProgress = false;
|
||||
let downloadToastDismissed = false;
|
||||
let skippedVersion: string | null = null;
|
||||
let updateCheckErrorHandled = false;
|
||||
let activeUpdateToastSender: UpdateToastSender | undefined;
|
||||
let updateStatusSummary: UpdateStatusSummary = {
|
||||
status: "idle",
|
||||
currentVersion: app.getVersion(),
|
||||
availableVersion: null,
|
||||
};
|
||||
|
||||
function setUpdateStatusSummary(summary: Partial<UpdateStatusSummary>) {
|
||||
updateStatusSummary = {
|
||||
...updateStatusSummary,
|
||||
currentVersion: app.getVersion(),
|
||||
...summary,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeError(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
return error.stack || `${error.name}: ${error.message}`;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function writeUpdaterLog(message: string, detail?: unknown) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(UPDATER_LOG_PATH), { recursive: true });
|
||||
const suffix = detail === undefined ? "" : ` ${summarizeError(detail)}`;
|
||||
fs.appendFileSync(
|
||||
UPDATER_LOG_PATH,
|
||||
`${new Date().toISOString()} ${message}${suffix}\n`,
|
||||
"utf8",
|
||||
);
|
||||
} catch (logError) {
|
||||
console.error("Failed to write updater log:", logError);
|
||||
}
|
||||
}
|
||||
|
||||
function createAutoCheckErrorToastPayload(): UpdateToastPayload {
|
||||
return {
|
||||
version: app.getVersion(),
|
||||
phase: "error",
|
||||
detail:
|
||||
"Recordly could not check for updates automatically. Retry now, or inspect updater.log in your user data folder.",
|
||||
delayMs: UPDATE_REMINDER_DELAY_MS,
|
||||
primaryAction: "retry-check",
|
||||
};
|
||||
}
|
||||
|
||||
function shouldSurfaceAutomaticCheckErrors() {
|
||||
return !AUTO_UPDATE_ERROR_TOASTS_DISABLED;
|
||||
}
|
||||
|
||||
function configureUpdateFeed() {
|
||||
if (!UPDATE_FEED_URL_OVERRIDE) {
|
||||
writeUpdaterLog("Using published GitHub update feed.");
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "generic",
|
||||
url: UPDATE_FEED_URL_OVERRIDE,
|
||||
channel: "latest",
|
||||
});
|
||||
writeUpdaterLog(`Using overridden update feed: ${UPDATE_FEED_URL_OVERRIDE}`);
|
||||
}
|
||||
|
||||
function canUseAutoUpdates() {
|
||||
return !AUTO_UPDATES_DISABLED && app.isPackaged && !process.mas;
|
||||
@@ -92,6 +183,7 @@ function createAvailableUpdateToastPayload(version: string): UpdateToastPayload
|
||||
phase: "available",
|
||||
detail: "A new version is available. Download it now, or wait and we will remind you again in 3 hours.",
|
||||
delayMs: UPDATE_REMINDER_DELAY_MS,
|
||||
primaryAction: "download-update",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +210,7 @@ function createDownloadedUpdateToastPayload(version: string): UpdateToastPayload
|
||||
phase: "ready",
|
||||
detail: "Install now to restart into the new version, or wait and we will remind you again in 3 hours.",
|
||||
delayMs: UPDATE_REMINDER_DELAY_MS,
|
||||
primaryAction: "install-update",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +220,7 @@ function createUpdateErrorToastPayload(version: string, error: unknown): UpdateT
|
||||
phase: "error",
|
||||
detail: `The update download failed. ${String(error)}`,
|
||||
delayMs: UPDATE_REMINDER_DELAY_MS,
|
||||
primaryAction: "download-update",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +244,14 @@ export function getCurrentUpdateToastPayload() {
|
||||
return currentToastPayload;
|
||||
}
|
||||
|
||||
export function getUpdaterLogPath() {
|
||||
return UPDATER_LOG_PATH;
|
||||
}
|
||||
|
||||
export function getUpdateStatusSummary() {
|
||||
return updateStatusSummary;
|
||||
}
|
||||
|
||||
async function showNoUpdatesDialog(getMainWindow: () => BrowserWindow | null) {
|
||||
await showMessageBox(getMainWindow, {
|
||||
type: "info",
|
||||
@@ -267,6 +369,8 @@ export function installDownloadedUpdateNow(sendToRenderer?: UpdateToastSender) {
|
||||
clearDeferredReminderTimer();
|
||||
downloadToastDismissed = false;
|
||||
clearVisibleUpdateToast(sendToRenderer);
|
||||
setUpdateStatusSummary({ status: "ready", availableVersion: pendingDownloadedVersion });
|
||||
writeUpdaterLog("Installing downloaded update.");
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
@@ -290,13 +394,26 @@ export async function downloadAvailableUpdate(sendToRenderer?: UpdateToastSender
|
||||
clearDeferredReminderTimer();
|
||||
downloadInProgress = true;
|
||||
downloadToastDismissed = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "downloading",
|
||||
availableVersion,
|
||||
detail: `Downloading Recordly ${availableVersion}`,
|
||||
});
|
||||
emitUpdateToastState(sendToRenderer, createDownloadingUpdateToastPayload(availableVersion, 0));
|
||||
writeUpdaterLog(`Starting update download for ${availableVersion}.`);
|
||||
|
||||
try {
|
||||
await autoUpdater.downloadUpdate();
|
||||
writeUpdaterLog(`Update download requested for ${availableVersion}.`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
downloadInProgress = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "error",
|
||||
availableVersion,
|
||||
detail: String(error),
|
||||
});
|
||||
writeUpdaterLog(`Update download failed for ${availableVersion}.`, error);
|
||||
emitUpdateToastState(
|
||||
sendToRenderer,
|
||||
createUpdateErrorToastPayload(availableVersion, error),
|
||||
@@ -464,6 +581,9 @@ export async function checkForAppUpdates(
|
||||
options?: { manual?: boolean },
|
||||
) {
|
||||
if (!canUseAutoUpdates()) {
|
||||
writeUpdaterLog(
|
||||
`Skipped update check because auto-updates are unavailable. packaged=${app.isPackaged} mas=${process.mas ? "yes" : "no"} disabled=${AUTO_UPDATES_DISABLED ? "yes" : "no"}`,
|
||||
);
|
||||
if (options?.manual) {
|
||||
await showMessageBox(getMainWindow, {
|
||||
type: "info",
|
||||
@@ -478,6 +598,7 @@ export async function checkForAppUpdates(
|
||||
}
|
||||
|
||||
if (updateCheckInProgress) {
|
||||
writeUpdaterLog("Skipped update check because a previous check is still running.");
|
||||
if (options?.manual) {
|
||||
await showMessageBox(getMainWindow, {
|
||||
type: "info",
|
||||
@@ -490,16 +611,28 @@ export async function checkForAppUpdates(
|
||||
|
||||
manualCheckRequested = Boolean(options?.manual);
|
||||
updateCheckInProgress = true;
|
||||
updateCheckErrorHandled = false;
|
||||
setUpdateStatusSummary({ status: "checking", detail: "Checking for updates..." });
|
||||
writeUpdaterLog(`Starting ${manualCheckRequested ? "manual" : "automatic"} update check.`);
|
||||
|
||||
try {
|
||||
await autoUpdater.checkForUpdates();
|
||||
writeUpdaterLog("Update check request completed.");
|
||||
} catch (error) {
|
||||
updateCheckInProgress = false;
|
||||
const shouldReport = manualCheckRequested;
|
||||
manualCheckRequested = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "error",
|
||||
availableVersion,
|
||||
detail: String(error),
|
||||
});
|
||||
writeUpdaterLog("Update check failed.", error);
|
||||
console.error("Auto-update check failed:", error);
|
||||
if (shouldReport) {
|
||||
if (shouldReport && !updateCheckErrorHandled) {
|
||||
await showUpdateErrorDialog(getMainWindow, error);
|
||||
} else if (!updateCheckErrorHandled && shouldSurfaceAutomaticCheckErrors()) {
|
||||
emitUpdateToastState(activeUpdateToastSender, createAutoCheckErrorToastPayload());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,19 +646,34 @@ export function setupAutoUpdates(
|
||||
}
|
||||
|
||||
if (!canUseAutoUpdates()) {
|
||||
setUpdateStatusSummary({ status: "idle", availableVersion: null, detail: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
updaterInitialized = true;
|
||||
activeUpdateToastSender = sendToRenderer;
|
||||
configureUpdateFeed();
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
writeUpdaterLog(`Updater initialized. logPath=${UPDATER_LOG_PATH}`);
|
||||
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
setUpdateStatusSummary({ status: "checking", availableVersion: null, detail: "Checking for updates..." });
|
||||
writeUpdaterLog("electron-updater emitted checking-for-update.");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
writeUpdaterLog(`Update available: version=${info.version}`);
|
||||
updateCheckInProgress = false;
|
||||
availableVersion = info.version;
|
||||
pendingDownloadedVersion = null;
|
||||
downloadInProgress = false;
|
||||
downloadToastDismissed = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "available",
|
||||
availableVersion: info.version,
|
||||
detail: `Recordly ${info.version} is available.`,
|
||||
});
|
||||
if (skippedVersion === info.version) {
|
||||
manualCheckRequested = false;
|
||||
return;
|
||||
@@ -544,11 +692,17 @@ export function setupAutoUpdates(
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
writeUpdaterLog("No update available.");
|
||||
updateCheckInProgress = false;
|
||||
availableVersion = null;
|
||||
pendingDownloadedVersion = null;
|
||||
downloadInProgress = false;
|
||||
downloadToastDismissed = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "up-to-date",
|
||||
availableVersion: null,
|
||||
detail: `Recordly ${app.getVersion()} is up to date.`,
|
||||
});
|
||||
clearVisibleUpdateToast(sendToRenderer);
|
||||
const shouldReport = manualCheckRequested;
|
||||
manualCheckRequested = false;
|
||||
@@ -563,6 +717,14 @@ export function setupAutoUpdates(
|
||||
}
|
||||
|
||||
downloadInProgress = true;
|
||||
setUpdateStatusSummary({
|
||||
status: "downloading",
|
||||
availableVersion,
|
||||
detail: `Downloading Recordly ${availableVersion}`,
|
||||
});
|
||||
writeUpdaterLog(
|
||||
`Download progress for ${availableVersion}: ${progress.percent.toFixed(1)}%`,
|
||||
);
|
||||
if (downloadToastDismissed) {
|
||||
return;
|
||||
}
|
||||
@@ -577,6 +739,15 @@ export function setupAutoUpdates(
|
||||
updateCheckInProgress = false;
|
||||
const shouldReport = manualCheckRequested;
|
||||
manualCheckRequested = false;
|
||||
if (!downloadInProgress) {
|
||||
updateCheckErrorHandled = true;
|
||||
}
|
||||
setUpdateStatusSummary({
|
||||
status: "error",
|
||||
availableVersion,
|
||||
detail: String(error),
|
||||
});
|
||||
writeUpdaterLog("electron-updater emitted error.", error);
|
||||
console.error("Auto-updater error:", error);
|
||||
if (downloadInProgress && availableVersion) {
|
||||
downloadInProgress = false;
|
||||
@@ -588,10 +759,13 @@ export function setupAutoUpdates(
|
||||
}
|
||||
if (shouldReport) {
|
||||
void showUpdateErrorDialog(getMainWindow, error);
|
||||
} else if (shouldSurfaceAutomaticCheckErrors()) {
|
||||
emitUpdateToastState(sendToRenderer, createAutoCheckErrorToastPayload());
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
writeUpdaterLog(`Update downloaded: version=${info.version}`);
|
||||
updateCheckInProgress = false;
|
||||
manualCheckRequested = false;
|
||||
downloadInProgress = false;
|
||||
@@ -601,6 +775,11 @@ export function setupAutoUpdates(
|
||||
}
|
||||
availableVersion = info.version;
|
||||
pendingDownloadedVersion = info.version;
|
||||
setUpdateStatusSummary({
|
||||
status: "ready",
|
||||
availableVersion: info.version,
|
||||
detail: `Recordly ${info.version} is ready to install.`,
|
||||
});
|
||||
clearDeferredReminderTimer();
|
||||
|
||||
if (emitUpdateToastState(sendToRenderer, createDownloadedUpdateToastPayload(info.version))) {
|
||||
|
||||
+14
-4
@@ -326,6 +326,11 @@ export function getHudOverlayWindow(): BrowserWindow | null {
|
||||
|
||||
export function createUpdateToastWindow(): BrowserWindow {
|
||||
const initialBounds = getUpdateToastBounds();
|
||||
const parentWindow =
|
||||
process.platform === "darwin" && hudOverlayWindow && !hudOverlayWindow.isDestroyed()
|
||||
? hudOverlayWindow
|
||||
: undefined;
|
||||
const useTransparentToastWindow = process.platform !== "win32";
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: initialBounds.width,
|
||||
@@ -333,15 +338,15 @@ export function createUpdateToastWindow(): BrowserWindow {
|
||||
x: initialBounds.x,
|
||||
y: initialBounds.y,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
transparent: useTransparentToastWindow,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
hasShadow: false,
|
||||
show: false,
|
||||
focusable: true,
|
||||
...(hudOverlayWindow && !hudOverlayWindow.isDestroyed() ? { parent: hudOverlayWindow } : {}),
|
||||
backgroundColor: "#00000000",
|
||||
...(parentWindow ? { parent: parentWindow } : {}),
|
||||
backgroundColor: useTransparentToastWindow ? "#00000000" : "#101418",
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.mjs"),
|
||||
nodeIntegration: false,
|
||||
@@ -382,7 +387,12 @@ export function showUpdateToastWindow(): BrowserWindow {
|
||||
const win = getUpdateToastWindow() ?? createUpdateToastWindow();
|
||||
positionUpdateToastWindow();
|
||||
if (!win.isVisible()) {
|
||||
win.showInactive();
|
||||
if (process.platform === "win32") {
|
||||
win.show();
|
||||
win.moveTop();
|
||||
} else {
|
||||
win.showInactive();
|
||||
}
|
||||
} else {
|
||||
win.moveTop();
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,13 +17,14 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const type = params.get("windowType") || "";
|
||||
const isMacOS = /mac/i.test(navigator.platform);
|
||||
setWindowType(type);
|
||||
|
||||
if (
|
||||
type === "hud-overlay" ||
|
||||
type === "source-selector" ||
|
||||
type === "countdown" ||
|
||||
type === "update-toast"
|
||||
(type === "update-toast" && isMacOS)
|
||||
) {
|
||||
document.body.style.background = "transparent";
|
||||
document.documentElement.style.background = "transparent";
|
||||
|
||||
@@ -35,6 +35,66 @@
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.updateBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 11px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
transition: all 0.15s ease;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.updateBadge:disabled {
|
||||
opacity: 0.72;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.updateBadgeQuiet {
|
||||
color: #a5b4c7;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.updateBadgeQuiet:hover:not(:disabled) {
|
||||
color: #d7dee8;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.updateBadgeHot {
|
||||
color: #f8fbff;
|
||||
border-color: rgba(125, 211, 252, 0.24);
|
||||
background: linear-gradient(180deg, rgba(125, 211, 252, 0.12), rgba(125, 211, 252, 0.04));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.updateBadgeHot:hover:not(:disabled) {
|
||||
color: #ffffff;
|
||||
border-color: rgba(125, 211, 252, 0.36);
|
||||
background: linear-gradient(180deg, rgba(125, 211, 252, 0.17), rgba(125, 211, 252, 0.07));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.updateBadgeSpin {
|
||||
animation: updateBadgeSpin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes updateBadgeSpin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.barState {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
AppWindow,
|
||||
ArrowUpCircle,
|
||||
ChevronUp,
|
||||
CheckCircle2,
|
||||
Eye,
|
||||
EyeOff,
|
||||
FolderOpen,
|
||||
@@ -12,6 +14,7 @@ import {
|
||||
MoreVertical,
|
||||
Pause,
|
||||
Play,
|
||||
RefreshCw,
|
||||
Square,
|
||||
Timer,
|
||||
Video,
|
||||
@@ -185,6 +188,17 @@ export function LaunchWindow() {
|
||||
const [hideHudFromCapture, setHideHudFromCapture] = useState(true);
|
||||
const [platform, setPlatform] = useState<string | null>(null);
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null);
|
||||
const [updateStatus, setUpdateStatus] = useState<{
|
||||
status: "idle" | "checking" | "up-to-date" | "available" | "downloading" | "ready" | "error";
|
||||
currentVersion: string;
|
||||
availableVersion: string | null;
|
||||
detail?: string;
|
||||
}>({
|
||||
status: "idle",
|
||||
currentVersion: "",
|
||||
availableVersion: null,
|
||||
});
|
||||
const [updateActionPending, setUpdateActionPending] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const hudContentRef = useRef<HTMLDivElement>(null);
|
||||
const hudBarRef = useRef<HTMLDivElement>(null);
|
||||
@@ -372,6 +386,31 @@ export function LaunchWindow() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const refreshUpdateStatus = async () => {
|
||||
try {
|
||||
const summary = await window.electronAPI.getUpdateStatusSummary();
|
||||
if (mounted) {
|
||||
setUpdateStatus(summary);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load update status summary:", error);
|
||||
}
|
||||
};
|
||||
|
||||
void refreshUpdateStatus();
|
||||
const pollTimer = window.setInterval(() => {
|
||||
void refreshUpdateStatus();
|
||||
}, 2500);
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
window.clearInterval(pollTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const loadVersion = async () => {
|
||||
@@ -619,6 +658,74 @@ export function LaunchWindow() {
|
||||
toggleDropdown("webcam");
|
||||
};
|
||||
|
||||
const updateButtonLabel =
|
||||
updateStatus.status === "up-to-date"
|
||||
? t("recording.update.updated")
|
||||
: t("recording.update.update");
|
||||
const updateButtonTitle = (() => {
|
||||
switch (updateStatus.status) {
|
||||
case "up-to-date":
|
||||
return t("recording.update.upToDateTitle", "Recordly {{version}} is up to date.", {
|
||||
version: updateStatus.currentVersion,
|
||||
});
|
||||
case "available":
|
||||
case "ready":
|
||||
return updateStatus.availableVersion
|
||||
? t("recording.update.availableTitle", "Recordly {{version}} is available.", {
|
||||
version: updateStatus.availableVersion,
|
||||
})
|
||||
: t("recording.update.availableGenericTitle");
|
||||
case "downloading":
|
||||
return updateStatus.detail ?? t("recording.update.downloadingTitle");
|
||||
case "checking":
|
||||
return t("recording.update.checkingTitle");
|
||||
case "error":
|
||||
return updateStatus.detail ?? t("recording.update.errorTitle");
|
||||
default:
|
||||
return t("recording.update.idleTitle");
|
||||
}
|
||||
})();
|
||||
const updateButtonClassName = `${styles.updateBadge} ${updateStatus.status === "up-to-date" ? styles.updateBadgeQuiet : styles.updateBadgeHot} ${styles.electronNoDrag}`;
|
||||
const updateButtonIcon = (() => {
|
||||
switch (updateStatus.status) {
|
||||
case "up-to-date":
|
||||
return <CheckCircle2 size={14} />;
|
||||
case "checking":
|
||||
case "downloading":
|
||||
return <RefreshCw size={14} className={styles.updateBadgeSpin} />;
|
||||
default:
|
||||
return <ArrowUpCircle size={14} />;
|
||||
}
|
||||
})();
|
||||
|
||||
const handleUpdateButtonClick = async () => {
|
||||
if (updateActionPending || updateStatus.status === "downloading") {
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdateActionPending(true);
|
||||
try {
|
||||
switch (updateStatus.status) {
|
||||
case "available":
|
||||
await window.electronAPI.downloadAvailableUpdate();
|
||||
break;
|
||||
case "ready":
|
||||
await window.electronAPI.installDownloadedUpdate();
|
||||
break;
|
||||
default:
|
||||
await window.electronAPI.checkForAppUpdates();
|
||||
break;
|
||||
}
|
||||
|
||||
const summary = await window.electronAPI.getUpdateStatusSummary();
|
||||
setUpdateStatus(summary);
|
||||
} catch (error) {
|
||||
console.error("Failed to handle update button action:", error);
|
||||
} finally {
|
||||
setUpdateActionPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const recordingControls = (
|
||||
<>
|
||||
<div className="flex items-center gap-[5px]">
|
||||
@@ -1054,6 +1161,19 @@ export function LaunchWindow() {
|
||||
<RxDragHandleDots2 size={14} className="text-[#6b6b78]" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleUpdateButtonClick();
|
||||
}}
|
||||
className={updateButtonClassName}
|
||||
title={updateButtonTitle}
|
||||
disabled={updateActionPending}
|
||||
>
|
||||
{updateButtonIcon}
|
||||
<span>{updateButtonLabel}</span>
|
||||
</button>
|
||||
|
||||
<div className={styles.barStateViewport}>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<motion.div
|
||||
|
||||
@@ -8,6 +8,7 @@ type UpdateToastPayload = {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
};
|
||||
|
||||
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;
|
||||
@@ -34,16 +35,16 @@ function getToastTitle(payload: UpdateToastPayload) {
|
||||
}
|
||||
}
|
||||
|
||||
function getIcon(payload: UpdateToastPayload) {
|
||||
switch (payload.phase) {
|
||||
case "available":
|
||||
return <Download className="h-5 w-5" />;
|
||||
case "downloading":
|
||||
return <LoaderCircle className="h-5 w-5 animate-spin" />;
|
||||
case "ready":
|
||||
return <Rocket className="h-5 w-5" />;
|
||||
case "error":
|
||||
return <AlertCircle className="h-5 w-5" />;
|
||||
function getPrimaryActionLabel(payload: UpdateToastPayload) {
|
||||
switch (payload.primaryAction) {
|
||||
case "download-update":
|
||||
return "Download Update";
|
||||
case "install-update":
|
||||
return "Install Update";
|
||||
case "retry-check":
|
||||
return "Retry Check";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +63,7 @@ export function UpdateToastWindow() {
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
void window.electronAPI.getCurrentUpdateToastPayload().then((nextPayload) => {
|
||||
if (mounted) {
|
||||
@@ -69,12 +71,25 @@ export function UpdateToastWindow() {
|
||||
}
|
||||
});
|
||||
|
||||
pollTimer = setInterval(() => {
|
||||
void window.electronAPI.getCurrentUpdateToastPayload().then((nextPayload) => {
|
||||
if (!mounted || !nextPayload) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPayload((currentPayload) => currentPayload ?? nextPayload);
|
||||
});
|
||||
}, 750);
|
||||
|
||||
const dispose = window.electronAPI.onUpdateToastStateChanged((nextPayload) => {
|
||||
setPayload(nextPayload);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
}
|
||||
dispose();
|
||||
};
|
||||
}, []);
|
||||
@@ -88,11 +103,104 @@ export function UpdateToastWindow() {
|
||||
};
|
||||
}, [payload?.phase, payload?.version, payload?.progressPercent]);
|
||||
|
||||
const cardStyle = {
|
||||
background: "#0d1117",
|
||||
border: "1px solid rgba(125, 211, 252, 0.22)",
|
||||
boxShadow: "0 24px 48px rgba(0, 0, 0, 0.45)",
|
||||
borderRadius: 24,
|
||||
padding: 16,
|
||||
color: "#ffffff",
|
||||
width: "100%",
|
||||
maxWidth: 404,
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
alignItems: "flex-start",
|
||||
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
} as const;
|
||||
const wrapperStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: 8,
|
||||
boxSizing: "border-box",
|
||||
background: "transparent",
|
||||
} as const;
|
||||
const secondaryTextStyle = {
|
||||
color: "rgba(255, 255, 255, 0.74)",
|
||||
fontSize: 14,
|
||||
lineHeight: 1.45,
|
||||
margin: "4px 0 0 0",
|
||||
} as const;
|
||||
const titleStyle = {
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
margin: 0,
|
||||
color: "#ffffff",
|
||||
} as const;
|
||||
const iconBoxStyle = {
|
||||
width: 40,
|
||||
height: 40,
|
||||
minWidth: 40,
|
||||
borderRadius: 16,
|
||||
background: "rgba(125, 211, 252, 0.15)",
|
||||
color: "#7dd3fc",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 2,
|
||||
} as const;
|
||||
const rowStyle = {
|
||||
display: "flex",
|
||||
flexWrap: "wrap" as const,
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
} as const;
|
||||
const subtleButtonStyle = {
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
color: "rgba(255, 255, 255, 0.92)",
|
||||
borderRadius: 12,
|
||||
padding: "8px 12px",
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
} as const;
|
||||
const primaryButtonStyle = {
|
||||
...subtleButtonStyle,
|
||||
background: "#7dd3fc",
|
||||
color: "#031a2c",
|
||||
border: "none",
|
||||
} as const;
|
||||
const ghostButtonStyle = {
|
||||
...subtleButtonStyle,
|
||||
background: "transparent",
|
||||
color: "rgba(255, 255, 255, 0.72)",
|
||||
border: "1px solid rgba(125, 211, 252, 0.16)",
|
||||
} as const;
|
||||
|
||||
if (!payload) {
|
||||
return <div className="h-full w-full bg-transparent" />;
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<div style={{ ...cardStyle, alignItems: "center" }}>
|
||||
<div style={iconBoxStyle}>
|
||||
<LoaderCircle size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p style={titleStyle}>Checking for updates</p>
|
||||
<p style={secondaryTextStyle}>
|
||||
Waiting for updater state from the main process.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedProgress = Math.max(0, Math.min(100, Math.round(payload.progressPercent ?? 0)));
|
||||
const primaryActionLabel = getPrimaryActionLabel(payload);
|
||||
const swipeThreshold = 96;
|
||||
const handleSwipeDismiss = async () => {
|
||||
setDragOffsetX(0);
|
||||
@@ -104,11 +212,28 @@ export function UpdateToastWindow() {
|
||||
await window.electronAPI.dismissUpdateToast();
|
||||
};
|
||||
|
||||
const handlePrimaryAction = async () => {
|
||||
switch (payload.primaryAction) {
|
||||
case "download-update":
|
||||
await window.electronAPI.downloadAvailableUpdate();
|
||||
return;
|
||||
case "install-update":
|
||||
await window.electronAPI.installDownloadedUpdate();
|
||||
return;
|
||||
case "retry-check":
|
||||
await window.electronAPI.checkForAppUpdates();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-transparent p-2">
|
||||
<div style={wrapperStyle}>
|
||||
<div
|
||||
className="pointer-events-auto flex w-full max-w-[404px] items-start gap-3 rounded-[24px] border border-sky-300/20 bg-[#0d1117]/95 p-4 text-white shadow-2xl shadow-black/45 backdrop-blur-xl transition-transform duration-150 ease-out select-none"
|
||||
className="pointer-events-auto select-none"
|
||||
style={{
|
||||
...cardStyle,
|
||||
transform: `translateX(${dragOffsetX}px) rotate(${dragOffsetX / 30}deg)`,
|
||||
opacity: Math.max(0.35, 1 - Math.min(1, Math.abs(dragOffsetX) / 180)),
|
||||
}}
|
||||
@@ -160,54 +285,42 @@ export function UpdateToastWindow() {
|
||||
setDragOffsetX(0);
|
||||
}}
|
||||
>
|
||||
<div className="mt-0.5 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-2xl bg-sky-400/15 text-sky-300">
|
||||
{getIcon(payload)}
|
||||
<div style={iconBoxStyle}>
|
||||
{payload.phase === "available" ? <Download size={20} /> : null}
|
||||
{payload.phase === "downloading" ? <LoaderCircle size={20} className="animate-spin" /> : null}
|
||||
{payload.phase === "ready" ? <Rocket size={20} /> : null}
|
||||
{payload.phase === "error" ? <AlertCircle size={20} /> : null}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-semibold tracking-tight">{getToastTitle(payload)}</p>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<p style={titleStyle}>{getToastTitle(payload)}</p>
|
||||
{payload.isPreview ? (
|
||||
<span className="rounded-full border border-sky-300/20 bg-sky-400/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.18em] text-sky-200">
|
||||
<span style={{ borderRadius: 999, border: "1px solid rgba(125, 211, 252, 0.2)", background: "rgba(125, 211, 252, 0.1)", padding: "2px 8px", fontSize: 10, fontWeight: 700, letterSpacing: "0.18em", textTransform: "uppercase", color: "#bae6fd" }}>
|
||||
Dev
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-5 text-white/70">{payload.detail}</p>
|
||||
<p style={secondaryTextStyle}>{payload.detail}</p>
|
||||
|
||||
{payload.phase === "downloading" ? (
|
||||
<div className="mt-3">
|
||||
<div className="h-2 overflow-hidden rounded-full bg-white/10">
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ height: 8, overflow: "hidden", borderRadius: 999, background: "rgba(255, 255, 255, 0.1)" }}>
|
||||
<div
|
||||
className="h-full rounded-full bg-sky-300 transition-[width] duration-300"
|
||||
style={{ width: `${normalizedProgress}%` }}
|
||||
style={{ height: "100%", borderRadius: 999, background: "#7dd3fc", width: `${normalizedProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs font-medium text-sky-100/85">{normalizedProgress}% downloaded</p>
|
||||
<p style={{ margin: "8px 0 0 0", color: "rgba(224, 242, 254, 0.9)", fontSize: 12, fontWeight: 600 }}>{normalizedProgress}% downloaded</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{payload.phase === "available" || payload.phase === "error" ? (
|
||||
<div style={rowStyle}>
|
||||
{primaryActionLabel ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await window.electronAPI.downloadAvailableUpdate();
|
||||
}}
|
||||
className="rounded-xl bg-sky-400 px-3 py-2 text-xs font-semibold text-[#031a2c] transition-colors hover:bg-sky-300"
|
||||
onClick={handlePrimaryAction}
|
||||
style={primaryButtonStyle}
|
||||
>
|
||||
Download Update
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{payload.phase === "ready" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await window.electronAPI.installDownloadedUpdate();
|
||||
}}
|
||||
className="rounded-xl bg-sky-400 px-3 py-2 text-xs font-semibold text-[#031a2c] transition-colors hover:bg-sky-300"
|
||||
>
|
||||
Install Update
|
||||
{primaryActionLabel}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
@@ -217,7 +330,7 @@ export function UpdateToastWindow() {
|
||||
onClick={async () => {
|
||||
await window.electronAPI.dismissUpdateToast();
|
||||
}}
|
||||
className="rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs font-medium text-white/85 transition-colors hover:bg-white/10"
|
||||
style={subtleButtonStyle}
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
@@ -234,7 +347,7 @@ export function UpdateToastWindow() {
|
||||
|
||||
await window.electronAPI.deferDownloadedUpdate(payload.delayMs);
|
||||
}}
|
||||
className="rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs font-medium text-white/85 transition-colors hover:bg-white/10"
|
||||
style={subtleButtonStyle}
|
||||
>
|
||||
Later ({formatDelayHours(payload.delayMs)})
|
||||
</button>
|
||||
@@ -251,7 +364,7 @@ export function UpdateToastWindow() {
|
||||
|
||||
await window.electronAPI.deferDownloadedUpdate(THREE_DAYS_MS);
|
||||
}}
|
||||
className="rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs font-medium text-white/85 transition-colors hover:bg-white/10"
|
||||
style={subtleButtonStyle}
|
||||
>
|
||||
Later (3 days)
|
||||
</button>
|
||||
@@ -263,7 +376,7 @@ export function UpdateToastWindow() {
|
||||
onClick={async () => {
|
||||
await window.electronAPI.skipUpdateVersion();
|
||||
}}
|
||||
className="rounded-xl border border-sky-300/15 bg-transparent px-3 py-2 text-xs font-medium text-white/65 transition-colors hover:bg-white/5 hover:text-white"
|
||||
style={ghostButtonStyle}
|
||||
>
|
||||
Skip This Version
|
||||
</button>
|
||||
|
||||
@@ -37,7 +37,18 @@
|
||||
"pause": "Pause",
|
||||
"stop": "Stop",
|
||||
"cancel": "Cancel",
|
||||
"more": "More"
|
||||
"more": "More",
|
||||
"update": {
|
||||
"update": "Update",
|
||||
"updated": "Updated",
|
||||
"idleTitle": "Check for updates.",
|
||||
"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.",
|
||||
"availableGenericTitle": "An update is available."
|
||||
}
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "Loading sources...",
|
||||
|
||||
@@ -37,7 +37,18 @@
|
||||
"pause": "Pausa",
|
||||
"stop": "Detener",
|
||||
"cancel": "Cancelar",
|
||||
"more": "Más"
|
||||
"more": "Más",
|
||||
"update": {
|
||||
"update": "Actualizar",
|
||||
"updated": "Actualizado",
|
||||
"idleTitle": "Buscar actualizaciones.",
|
||||
"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.",
|
||||
"availableGenericTitle": "Hay una actualización disponible."
|
||||
}
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "Cargando fuentes...",
|
||||
|
||||
@@ -37,7 +37,18 @@
|
||||
"pause": "暂停",
|
||||
"stop": "停止",
|
||||
"cancel": "取消",
|
||||
"more": "更多"
|
||||
"more": "更多",
|
||||
"update": {
|
||||
"update": "更新",
|
||||
"updated": "已是最新",
|
||||
"idleTitle": "检查更新。",
|
||||
"checkingTitle": "正在检查更新...",
|
||||
"downloadingTitle": "正在下载更新...",
|
||||
"errorTitle": "检查更新失败。点击重试。",
|
||||
"upToDateTitle": "Recordly {{version}} 已是最新版本。",
|
||||
"availableTitle": "Recordly {{version}} 可更新。",
|
||||
"availableGenericTitle": "有可用更新。"
|
||||
}
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "正在加载源...",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--brand-accent: #2563eb;
|
||||
--brand-accent-rgb: 37, 99, 235;
|
||||
|
||||
Reference in New Issue
Block a user