mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
Fix experimental update channel and toast
This commit is contained in:
+2
-3
@@ -15,7 +15,6 @@ import { loadAllCustomFonts } from "./lib/customFonts";
|
||||
export default function App() {
|
||||
const [windowType, setWindowType] = useState("");
|
||||
const { t } = useI18n();
|
||||
const isMacOS = /mac/i.test(navigator.platform);
|
||||
const appIconSrc = "/app-icons/recordly-128.png";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,7 +27,7 @@ export default function App() {
|
||||
type === "hud-overlay" ||
|
||||
type === "source-selector" ||
|
||||
type === "countdown" ||
|
||||
(type === "update-toast" && isMacOS)
|
||||
type === "update-toast"
|
||||
) {
|
||||
document.body.style.background = "transparent";
|
||||
document.documentElement.style.background = "transparent";
|
||||
@@ -49,7 +48,7 @@ export default function App() {
|
||||
loadAllCustomFonts().catch((error) => {
|
||||
console.error("Failed to load custom fonts:", error);
|
||||
});
|
||||
}, [isMacOS]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title =
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
.window {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--background));
|
||||
box-shadow: none;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 7px;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.iconError {
|
||||
background: hsl(var(--destructive) / 0.14);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.headingRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 19px;
|
||||
}
|
||||
|
||||
.headingRow h1 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.content > p {
|
||||
margin: 4px 0 0;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.version,
|
||||
.preview {
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: hsl(var(--primary) / 0.12);
|
||||
color: hsl(var(--primary));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.primaryButton,
|
||||
.secondaryButton {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 140ms ease,
|
||||
border-color 140ms ease,
|
||||
transform 140ms ease;
|
||||
}
|
||||
|
||||
.primaryButton:active,
|
||||
.secondaryButton:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.primaryButton {
|
||||
border: 1px solid hsl(var(--primary));
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.primaryButton:hover {
|
||||
background: hsl(var(--primary) / 0.9);
|
||||
}
|
||||
|
||||
.secondaryButton {
|
||||
border: 1px solid hsl(var(--input));
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.secondaryButton:hover {
|
||||
background: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
}
|
||||
|
||||
.progressBlock {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.progressTrack {
|
||||
height: 5px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.progressFill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: hsl(var(--primary));
|
||||
transition: width 220ms ease;
|
||||
}
|
||||
|
||||
.progressMeta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 6px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.progressMeta strong {
|
||||
color: hsl(var(--foreground));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 900ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import {
|
||||
WarningCircle as AlertCircle,
|
||||
DownloadSimple as Download,
|
||||
Spinner as LoaderCircle,
|
||||
Rocket,
|
||||
ArrowClockwiseIcon,
|
||||
CheckCircleIcon,
|
||||
DownloadSimpleIcon,
|
||||
WarningCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "@/contexts/I18nContext";
|
||||
import styles from "./UpdateToastWindow.module.css";
|
||||
|
||||
type UpdateToastPayload = {
|
||||
version: string;
|
||||
@@ -12,385 +14,186 @@ type UpdateToastPayload = {
|
||||
phase: "available" | "downloading" | "ready" | "error";
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
isExperimental?: boolean;
|
||||
progressPercent?: number;
|
||||
transferredBytes?: number;
|
||||
totalBytes?: number;
|
||||
remainingBytes?: number;
|
||||
bytesPerSecond?: number;
|
||||
primaryAction?: "install-and-restart" | "retry-check";
|
||||
};
|
||||
|
||||
const DEFAULT_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000;
|
||||
const REMINDER_OPTIONS = [
|
||||
{ label: "1 hour", value: 1 * 60 * 60 * 1000 },
|
||||
{ label: "3 hours", value: 3 * 60 * 60 * 1000 },
|
||||
{ label: "Tomorrow", value: 24 * 60 * 60 * 1000 },
|
||||
{ label: "3 days", value: 3 * 24 * 60 * 60 * 1000 },
|
||||
];
|
||||
|
||||
function formatBytes(value: number | undefined) {
|
||||
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const megabytes = value / (1024 * 1024);
|
||||
if (megabytes >= 1024) {
|
||||
return `${(megabytes / 1024).toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
return `${megabytes.toFixed(megabytes >= 100 ? 0 : 1)} MB`;
|
||||
return megabytes >= 1024
|
||||
? `${(megabytes / 1024).toFixed(1)} GB`
|
||||
: `${megabytes.toFixed(megabytes >= 100 ? 0 : 1)} MB`;
|
||||
}
|
||||
|
||||
function getToastTitle(payload: UpdateToastPayload) {
|
||||
if (payload.isPreview) {
|
||||
return "Update Prompt Preview";
|
||||
}
|
||||
type Translate = ReturnType<typeof useI18n>["t"];
|
||||
|
||||
function getTitle(payload: UpdateToastPayload, t: Translate) {
|
||||
switch (payload.phase) {
|
||||
case "available":
|
||||
return `Recordly ${payload.version} is available`;
|
||||
return payload.isExperimental
|
||||
? t(
|
||||
"launch.updateToast.experimentalAvailableTitle",
|
||||
"Experimental update available",
|
||||
)
|
||||
: t("launch.updateToast.availableTitle", "Update available");
|
||||
case "downloading":
|
||||
return `Installing Recordly ${payload.version}`;
|
||||
return t("launch.updateToast.downloadingTitle", "Downloading your update");
|
||||
case "ready":
|
||||
return `Recordly ${payload.version} is ready`;
|
||||
return t("launch.updateToast.readyTitle", "Ready to restart");
|
||||
case "error":
|
||||
return payload.primaryAction === "retry-check"
|
||||
? "Could not check for updates"
|
||||
: `Recordly ${payload.version} needs attention`;
|
||||
? t("launch.updateToast.checkErrorTitle", "Couldn’t check for updates")
|
||||
: t("launch.updateToast.downloadErrorTitle", "Couldn’t download the update");
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimaryButtonLabel(payload: UpdateToastPayload) {
|
||||
return payload.primaryAction === "retry-check" ? "Try Again" : "Install & Restart";
|
||||
function getDetail(payload: UpdateToastPayload, t: Translate) {
|
||||
if (payload.phase === "available" && payload.isExperimental) {
|
||||
return t(
|
||||
"launch.updateToast.experimentalDescription",
|
||||
"You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.",
|
||||
);
|
||||
}
|
||||
|
||||
return payload.detail;
|
||||
}
|
||||
|
||||
function getPhaseIcon(payload: UpdateToastPayload) {
|
||||
function getPrimaryLabel(payload: UpdateToastPayload, t: Translate) {
|
||||
if (payload.primaryAction === "retry-check") {
|
||||
return t("launch.updateToast.tryAgain", "Try again");
|
||||
}
|
||||
return payload.phase === "ready"
|
||||
? t("launch.updateToast.restartToUpdate", "Restart to update")
|
||||
: t("launch.updateToast.updateNow", "Update now");
|
||||
}
|
||||
|
||||
function PhaseIcon({ payload }: { payload: UpdateToastPayload }) {
|
||||
switch (payload.phase) {
|
||||
case "available":
|
||||
return <Download size={20} />;
|
||||
return <DownloadSimpleIcon size={20} weight="bold" />;
|
||||
case "downloading":
|
||||
return <LoaderCircle size={20} className="animate-spin" />;
|
||||
return <ArrowClockwiseIcon size={20} weight="bold" className={styles.spin} />;
|
||||
case "ready":
|
||||
return <Rocket size={20} />;
|
||||
return <CheckCircleIcon size={20} weight="fill" />;
|
||||
case "error":
|
||||
return <AlertCircle size={20} />;
|
||||
return <WarningCircleIcon size={20} weight="fill" />;
|
||||
}
|
||||
}
|
||||
|
||||
export function UpdateToastWindow() {
|
||||
const [payload, setPayload] = useState<UpdateToastPayload | null>(null);
|
||||
const [reminderDelayMs, setReminderDelayMs] = useState(DEFAULT_REMINDER_DELAY_MS);
|
||||
const { t } = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
void window.electronAPI.getCurrentUpdateToastPayload().then((nextPayload) => {
|
||||
if (mounted) {
|
||||
setPayload(nextPayload);
|
||||
}
|
||||
});
|
||||
|
||||
pollTimer = setInterval(() => {
|
||||
const refresh = () => {
|
||||
void window.electronAPI.getCurrentUpdateToastPayload().then((nextPayload) => {
|
||||
if (mounted) {
|
||||
setPayload(nextPayload);
|
||||
}
|
||||
if (mounted) setPayload(nextPayload);
|
||||
});
|
||||
}, 750);
|
||||
};
|
||||
|
||||
const dispose = window.electronAPI.onUpdateToastStateChanged((nextPayload) => {
|
||||
setPayload(nextPayload);
|
||||
});
|
||||
refresh();
|
||||
const pollTimer = setInterval(refresh, 750);
|
||||
const dispose = window.electronAPI.onUpdateToastStateChanged(setPayload);
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
}
|
||||
clearInterval(pollTimer);
|
||||
dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReminderDelayMs(payload.delayMs || DEFAULT_REMINDER_DELAY_MS);
|
||||
}, [payload]);
|
||||
|
||||
const normalizedProgress = Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round(payload?.progressPercent ?? 0)),
|
||||
);
|
||||
const downloadedLabel = formatBytes(payload?.transferredBytes);
|
||||
const totalLabel = formatBytes(payload?.totalBytes);
|
||||
const remainingLabel = formatBytes(payload?.remainingBytes);
|
||||
const speedLabel = formatBytes(payload?.bytesPerSecond);
|
||||
const phaseStats: Array<{ label: string; value: string }> = [];
|
||||
if (payload?.phase === "downloading") {
|
||||
if (downloadedLabel && totalLabel) {
|
||||
phaseStats.push({ label: "Downloaded", value: `${downloadedLabel} / ${totalLabel}` });
|
||||
} else if (downloadedLabel) {
|
||||
phaseStats.push({ label: "Downloaded", value: downloadedLabel });
|
||||
}
|
||||
if (remainingLabel) {
|
||||
phaseStats.push({ label: "Left", value: remainingLabel });
|
||||
}
|
||||
if (speedLabel) {
|
||||
phaseStats.push({ label: "Speed", value: `${speedLabel}/s` });
|
||||
}
|
||||
if (!payload) {
|
||||
return <div className={styles.window} />;
|
||||
}
|
||||
|
||||
const isMacOS = /mac/i.test(navigator.platform);
|
||||
const wrapperStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: 10,
|
||||
boxSizing: "border-box",
|
||||
background: isMacOS ? "transparent" : "#0b1220",
|
||||
} as const;
|
||||
const cardStyle = {
|
||||
width: "100%",
|
||||
maxWidth: 440,
|
||||
display: "flex",
|
||||
gap: 14,
|
||||
alignItems: "flex-start",
|
||||
padding: "18px 18px 16px",
|
||||
borderRadius: 24,
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(12, 19, 34, 0.98) 0%, rgba(10, 17, 30, 0.98) 100%)",
|
||||
border: "1px solid rgba(37, 99, 235, 0.24)",
|
||||
boxShadow: "0 20px 48px rgba(2, 6, 23, 0.5), inset 0 1px 0 rgba(148, 163, 184, 0.08)",
|
||||
color: "#ffffff",
|
||||
fontFamily: "var(--app-font-sans)",
|
||||
} as const;
|
||||
const iconBoxStyle = {
|
||||
width: 42,
|
||||
height: 42,
|
||||
minWidth: 42,
|
||||
borderRadius: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "rgba(37, 99, 235, 0.16)",
|
||||
color: "#60a5fa",
|
||||
boxShadow: "inset 0 0 0 1px rgba(37, 99, 235, 0.18)",
|
||||
} as const;
|
||||
const titleStyle = {
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.25,
|
||||
margin: 0,
|
||||
color: "#f8fafc",
|
||||
} as const;
|
||||
const secondaryTextStyle = {
|
||||
color: "rgba(226, 232, 240, 0.78)",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.5,
|
||||
margin: "6px 0 0 0",
|
||||
} as const;
|
||||
const subtleButtonStyle = {
|
||||
height: 38,
|
||||
borderRadius: 12,
|
||||
padding: "0 14px",
|
||||
border: "1px solid rgba(148, 163, 184, 0.16)",
|
||||
background: "rgba(15, 23, 42, 0.72)",
|
||||
color: "#e2e8f0",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
transition: "all 0.15s ease",
|
||||
} as const;
|
||||
const primaryButtonStyle = {
|
||||
...subtleButtonStyle,
|
||||
border: "none",
|
||||
background: "linear-gradient(180deg, #3b82f6 0%, #2563eb 100%)",
|
||||
color: "#ffffff",
|
||||
boxShadow: "0 12px 24px rgba(37, 99, 235, 0.26)",
|
||||
} as const;
|
||||
const selectStyle = {
|
||||
height: 38,
|
||||
borderRadius: 12,
|
||||
padding: "0 34px 0 12px",
|
||||
border: "1px solid rgba(37, 99, 235, 0.22)",
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(18, 29, 51, 0.96) 0%, rgba(12, 22, 42, 0.96) 100%)",
|
||||
color: "#dbeafe",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
outline: "none",
|
||||
boxShadow: "inset 0 0 0 1px rgba(37, 99, 235, 0.06)",
|
||||
cursor: "pointer",
|
||||
} as const;
|
||||
const progress = Math.max(0, Math.min(100, Math.round(payload.progressPercent ?? 0)));
|
||||
const transferred = formatBytes(payload.transferredBytes);
|
||||
const total = formatBytes(payload.totalBytes);
|
||||
const speed = formatBytes(payload.bytesPerSecond);
|
||||
const progressDetail = [
|
||||
transferred && total ? `${transferred} of ${total}` : transferred,
|
||||
speed ? `${speed}/s` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
const handlePrimaryAction = async () => {
|
||||
if (!payload || payload.phase === "downloading") {
|
||||
return;
|
||||
}
|
||||
if (payload.phase === "downloading") return;
|
||||
|
||||
if (payload.primaryAction === "retry-check") {
|
||||
await window.electronAPI.checkForAppUpdates();
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.phase === "ready") {
|
||||
await window.electronAPI.installDownloadedUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
await window.electronAPI.downloadAvailableUpdate(true);
|
||||
};
|
||||
|
||||
const handleLater = async () => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleNotNow = async () => {
|
||||
if (payload.isPreview) {
|
||||
await window.electronAPI.dismissUpdateToast();
|
||||
return;
|
||||
}
|
||||
|
||||
await window.electronAPI.deferDownloadedUpdate(reminderDelayMs);
|
||||
await window.electronAPI.deferDownloadedUpdate(payload.delayMs);
|
||||
};
|
||||
|
||||
if (!payload) {
|
||||
return <div style={wrapperStyle} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<div style={cardStyle}>
|
||||
<div style={iconBoxStyle}>{getPhaseIcon(payload)}</div>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<p style={titleStyle}>{getToastTitle(payload)}</p>
|
||||
<div className={`${styles.window} launch-theme`}>
|
||||
<section className={styles.card} aria-live="polite" aria-label="Recordly update">
|
||||
<div className={`${styles.icon} ${payload.phase === "error" ? styles.iconError : ""}`}>
|
||||
<PhaseIcon payload={payload} />
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.headingRow}>
|
||||
<h1>{getTitle(payload, t)}</h1>
|
||||
<span className={styles.version}>v{payload.version.replace(/^v/, "")}</span>
|
||||
{payload.isExperimental ? (
|
||||
<span className={styles.preview}>
|
||||
{t("launch.updateToast.experimentalBadge", "Experimental")}
|
||||
</span>
|
||||
) : null}
|
||||
{payload.isPreview ? (
|
||||
<span
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
padding: "2px 8px",
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.18em",
|
||||
textTransform: "uppercase",
|
||||
color: "#93c5fd",
|
||||
background: "rgba(37, 99, 235, 0.14)",
|
||||
border: "1px solid rgba(37, 99, 235, 0.18)",
|
||||
}}
|
||||
>
|
||||
Dev
|
||||
<span className={styles.preview}>
|
||||
{t("launch.updateToast.previewBadge", "Preview")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p style={secondaryTextStyle}>{payload.detail}</p>
|
||||
<p>{getDetail(payload, t)}</p>
|
||||
|
||||
{payload.phase === "downloading" ? (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
height: 10,
|
||||
overflow: "hidden",
|
||||
borderRadius: 999,
|
||||
background: "rgba(148, 163, 184, 0.14)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${normalizedProgress}%`,
|
||||
borderRadius: 999,
|
||||
background:
|
||||
"linear-gradient(90deg, #60a5fa 0%, #2563eb 45%, #1d4ed8 100%)",
|
||||
boxShadow: "0 0 22px rgba(37, 99, 235, 0.38)",
|
||||
}}
|
||||
/>
|
||||
<div className={styles.progressBlock}>
|
||||
<div className={styles.progressTrack}>
|
||||
<div className={styles.progressFill} style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
color: "#dbeafe",
|
||||
}}
|
||||
>
|
||||
{normalizedProgress}% complete
|
||||
</span>
|
||||
{phaseStats.map((stat) => (
|
||||
<span
|
||||
key={stat.label}
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
color: "rgba(191, 219, 254, 0.9)",
|
||||
background: "rgba(37, 99, 235, 0.12)",
|
||||
borderRadius: 999,
|
||||
padding: "4px 8px",
|
||||
border: "1px solid rgba(37, 99, 235, 0.16)",
|
||||
}}
|
||||
>
|
||||
{stat.label}: {stat.value}
|
||||
</span>
|
||||
))}
|
||||
<div className={styles.progressMeta}>
|
||||
<strong>{progress}%</strong>
|
||||
{progressDetail ? <span>{progressDetail}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 10,
|
||||
marginTop: 14,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{payload.phase !== "downloading" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePrimaryAction}
|
||||
style={primaryButtonStyle}
|
||||
>
|
||||
{getPrimaryButtonLabel(payload)}
|
||||
</button>
|
||||
<select
|
||||
value={String(reminderDelayMs)}
|
||||
onChange={(event) => {
|
||||
setReminderDelayMs(Number.parseInt(event.target.value, 10));
|
||||
}}
|
||||
style={selectStyle}
|
||||
>
|
||||
{REMINDER_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLater}
|
||||
style={subtleButtonStyle}
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.actions}>
|
||||
<button type="button" className={styles.secondaryButton} onClick={handleNotNow}>
|
||||
{t("launch.updateToast.notNow", "Not now")}
|
||||
</button>
|
||||
<button type="button" className={styles.primaryButton} onClick={handlePrimaryAction}>
|
||||
{getPrimaryLabel(payload, t)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2566,7 +2566,7 @@ export function SettingsPanel({
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground/70">
|
||||
{tSettings(
|
||||
"updates.experimentalDescription",
|
||||
"Receive first-line test builds published as prereleases. These may be less stable.",
|
||||
"This is the front line of user testing - highly experimental so expect bugs",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "Der Zugriff auf das Mikrofon wurde verweigert. Die Aufzeichnung wird ohne Mikrofonton fortgesetzt.",
|
||||
"failedToStart": "Die Aufzeichnung konnte nicht gestartet werden: {{error}}",
|
||||
"failedToStartGeneric": "Die Aufnahme konnte nicht gestartet werden"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "Update verfügbar",
|
||||
"experimentalAvailableTitle": "Experimentelles Update verfügbar",
|
||||
"experimentalDescription": "Sie haben experimentelle Updates aktiviert und können daher das neueste Update von Recordly testen, bevor es allgemein verfügbar ist.",
|
||||
"downloadingTitle": "Update wird heruntergeladen",
|
||||
"readyTitle": "Bereit zum Neustart",
|
||||
"checkErrorTitle": "Updates konnten nicht geprüft werden",
|
||||
"downloadErrorTitle": "Das Update konnte nicht heruntergeladen werden",
|
||||
"experimentalBadge": "Experimentell",
|
||||
"previewBadge": "Vorschau",
|
||||
"notNow": "Nicht jetzt",
|
||||
"updateNow": "Jetzt aktualisieren",
|
||||
"restartToUpdate": "Zum Aktualisieren neu starten",
|
||||
"tryAgain": "Erneut versuchen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "Quellmikrofon",
|
||||
"mixedLabel": "Quelle",
|
||||
"deleteRegion": "Audio löschen"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "Sie haben experimentelle Updates aktiviert und können daher das neueste Update von Recordly testen, bevor es allgemein verfügbar ist.",
|
||||
"title": "Updates",
|
||||
"experimental": "Experimentelle Updates",
|
||||
"saveFailed": "Update-Kanal konnte nicht geändert werden."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "Microphone access was denied. Recording will continue without microphone audio.",
|
||||
"failedToStart": "Failed to start recording: {{error}}",
|
||||
"failedToStartGeneric": "Failed to start recording"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "Update available",
|
||||
"experimentalAvailableTitle": "Experimental update available",
|
||||
"experimentalDescription": "You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.",
|
||||
"downloadingTitle": "Downloading your update",
|
||||
"readyTitle": "Ready to restart",
|
||||
"checkErrorTitle": "Couldn't check for updates",
|
||||
"downloadErrorTitle": "Couldn't download the update",
|
||||
"experimentalBadge": "Experimental",
|
||||
"previewBadge": "Preview",
|
||||
"notNow": "Not now",
|
||||
"updateNow": "Update now",
|
||||
"restartToUpdate": "Restart to update",
|
||||
"tryAgain": "Try again"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"updates": {
|
||||
"title": "Updates",
|
||||
"experimental": "Experimental updates",
|
||||
"experimentalDescription": "Receive first-line test builds published as prereleases. These may be less stable.",
|
||||
"experimentalDescription": "You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.",
|
||||
"saveFailed": "Failed to change the update channel."
|
||||
},
|
||||
"zoom": {
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "Se denegó el acceso al micrófono. La grabación continuará sin audio del micrófono.",
|
||||
"failedToStart": "Error al iniciar la grabación: {{error}}",
|
||||
"failedToStartGeneric": "Error al iniciar la grabación"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "Actualización disponible",
|
||||
"experimentalAvailableTitle": "Actualización experimental disponible",
|
||||
"experimentalDescription": "Has activado las actualizaciones experimentales, así que puedes probar la actualización más reciente de Recordly antes de que esté disponible para todos.",
|
||||
"downloadingTitle": "Descargando la actualización",
|
||||
"readyTitle": "Listo para reiniciar",
|
||||
"checkErrorTitle": "No se pudieron buscar actualizaciones",
|
||||
"downloadErrorTitle": "No se pudo descargar la actualización",
|
||||
"experimentalBadge": "Experimental",
|
||||
"previewBadge": "Vista previa",
|
||||
"notNow": "Ahora no",
|
||||
"updateNow": "Actualizar ahora",
|
||||
"restartToUpdate": "Reiniciar para actualizar",
|
||||
"tryAgain": "Intentar de nuevo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "Micrófono",
|
||||
"mixedLabel": "Fuente",
|
||||
"deleteRegion": "Eliminar audio"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "Has activado las actualizaciones experimentales, así que puedes probar la actualización más reciente de Recordly antes de que esté disponible para todos.",
|
||||
"title": "Actualizaciones",
|
||||
"experimental": "Actualizaciones experimentales",
|
||||
"saveFailed": "No se pudo cambiar el canal de actualización."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "L’accès au microphone a été refusé. L’enregistrement continuera sans audio du microphone.",
|
||||
"failedToStart": "Échec du démarrage de l’enregistrement : {{error}}",
|
||||
"failedToStartGeneric": "Échec du démarrage de l’enregistrement"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "Mise à jour disponible",
|
||||
"experimentalAvailableTitle": "Mise à jour expérimentale disponible",
|
||||
"experimentalDescription": "Vous avez activé les mises à jour expérimentales, vous pouvez donc tester la dernière mise à jour de Recordly avant sa disponibilité générale.",
|
||||
"downloadingTitle": "Téléchargement de la mise à jour",
|
||||
"readyTitle": "Prêt à redémarrer",
|
||||
"checkErrorTitle": "Impossible de vérifier les mises à jour",
|
||||
"downloadErrorTitle": "Impossible de télécharger la mise à jour",
|
||||
"experimentalBadge": "Expérimental",
|
||||
"previewBadge": "Aperçu",
|
||||
"notNow": "Plus tard",
|
||||
"updateNow": "Mettre à jour",
|
||||
"restartToUpdate": "Redémarrer pour mettre à jour",
|
||||
"tryAgain": "Réessayer"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "Microphone",
|
||||
"mixedLabel": "Source",
|
||||
"deleteRegion": "Supprimer la zone audio"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "Vous avez activé les mises à jour expérimentales, vous pouvez donc tester la dernière mise à jour de Recordly avant sa disponibilité générale.",
|
||||
"title": "Mises à jour",
|
||||
"experimental": "Mises à jour expérimentales",
|
||||
"saveFailed": "Impossible de changer le canal de mise à jour."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "L'accesso al microfono è stato negato. La registrazione continuerà senza audio del microfono.",
|
||||
"failedToStart": "Avvio della registrazione non riuscito: {{error}}",
|
||||
"failedToStartGeneric": "Avvio della registrazione non riuscito"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "Aggiornamento disponibile",
|
||||
"experimentalAvailableTitle": "Aggiornamento sperimentale disponibile",
|
||||
"experimentalDescription": "Hai attivato gli aggiornamenti sperimentali, quindi puoi scegliere di provare l'ultimo aggiornamento di Recordly prima che sia disponibile per tutti.",
|
||||
"downloadingTitle": "Download dell'aggiornamento",
|
||||
"readyTitle": "Pronto per il riavvio",
|
||||
"checkErrorTitle": "Impossibile verificare gli aggiornamenti",
|
||||
"downloadErrorTitle": "Impossibile scaricare l'aggiornamento",
|
||||
"experimentalBadge": "Sperimentale",
|
||||
"previewBadge": "Anteprima",
|
||||
"notNow": "Non ora",
|
||||
"updateNow": "Aggiorna ora",
|
||||
"restartToUpdate": "Riavvia per aggiornare",
|
||||
"tryAgain": "Riprova"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "Sorgente microfono",
|
||||
"mixedLabel": "Sorgente",
|
||||
"deleteRegion": "Elimina audio"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "Hai attivato gli aggiornamenti sperimentali, quindi puoi scegliere di provare l'ultimo aggiornamento di Recordly prima che sia disponibile per tutti.",
|
||||
"title": "Aggiornamenti",
|
||||
"experimental": "Aggiornamenti sperimentali",
|
||||
"saveFailed": "Impossibile cambiare il canale di aggiornamento."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "마이크 접근이 거부되었습니다. 마이크 오디오 없이 녹화를 계속합니다.",
|
||||
"failedToStart": "녹화 시작에 실패했습니다: {{error}}",
|
||||
"failedToStartGeneric": "녹화 시작에 실패했습니다"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "업데이트 사용 가능",
|
||||
"experimentalAvailableTitle": "실험적 업데이트 사용 가능",
|
||||
"experimentalDescription": "실험적 업데이트를 선택했으므로 Recordly의 최신 업데이트가 널리 제공되기 전에 먼저 테스트해 볼 수 있습니다.",
|
||||
"downloadingTitle": "업데이트 다운로드 중",
|
||||
"readyTitle": "다시 시작할 준비 완료",
|
||||
"checkErrorTitle": "업데이트를 확인할 수 없습니다",
|
||||
"downloadErrorTitle": "업데이트를 다운로드할 수 없습니다",
|
||||
"experimentalBadge": "실험적",
|
||||
"previewBadge": "미리보기",
|
||||
"notNow": "나중에",
|
||||
"updateNow": "지금 업데이트",
|
||||
"restartToUpdate": "다시 시작하여 업데이트",
|
||||
"tryAgain": "다시 시도"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "마이크 소스",
|
||||
"mixedLabel": "소스",
|
||||
"deleteRegion": "오디오 삭제"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "실험적 업데이트를 선택했으므로 Recordly의 최신 업데이트가 널리 제공되기 전에 먼저 테스트해 볼 수 있습니다.",
|
||||
"title": "업데이트",
|
||||
"experimental": "실험적 업데이트",
|
||||
"saveFailed": "업데이트 채널을 변경하지 못했습니다."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "Microfoontoegang is geweigerd. De opname gaat verder zonder microfoonaudio.",
|
||||
"failedToStart": "Opname starten mislukt: {{error}}",
|
||||
"failedToStartGeneric": "Opname starten mislukt"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "Update beschikbaar",
|
||||
"experimentalAvailableTitle": "Experimentele update beschikbaar",
|
||||
"experimentalDescription": "Je hebt experimentele updates ingeschakeld, dus je kunt de nieuwste update van Recordly testen voordat die breed beschikbaar is.",
|
||||
"downloadingTitle": "Update downloaden",
|
||||
"readyTitle": "Klaar om opnieuw te starten",
|
||||
"checkErrorTitle": "Kan niet controleren op updates",
|
||||
"downloadErrorTitle": "Kan de update niet downloaden",
|
||||
"experimentalBadge": "Experimenteel",
|
||||
"previewBadge": "Preview",
|
||||
"notNow": "Niet nu",
|
||||
"updateNow": "Nu updaten",
|
||||
"restartToUpdate": "Opnieuw starten om te updaten",
|
||||
"tryAgain": "Opnieuw proberen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "Microfoon",
|
||||
"mixedLabel": "Bron",
|
||||
"deleteRegion": "Audio verwijderen"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "Je hebt experimentele updates ingeschakeld, dus je kunt de nieuwste update van Recordly testen voordat die breed beschikbaar is.",
|
||||
"title": "Updates",
|
||||
"experimental": "Experimentele updates",
|
||||
"saveFailed": "Kan het updatekanaal niet wijzigen."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "O acesso ao microfone foi negado. A gravação continuará sem áudio do microfone.",
|
||||
"failedToStart": "Falha ao iniciar gravação: {{error}}",
|
||||
"failedToStartGeneric": "Falha ao iniciar gravação"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "Atualização disponível",
|
||||
"experimentalAvailableTitle": "Atualização experimental disponível",
|
||||
"experimentalDescription": "Você ativou as atualizações experimentais, então pode escolher testar a atualização mais recente do Recordly antes que ela fique amplamente disponível.",
|
||||
"downloadingTitle": "Baixando sua atualização",
|
||||
"readyTitle": "Pronto para reiniciar",
|
||||
"checkErrorTitle": "Não foi possível verificar atualizações",
|
||||
"downloadErrorTitle": "Não foi possível baixar a atualização",
|
||||
"experimentalBadge": "Experimental",
|
||||
"previewBadge": "Prévia",
|
||||
"notNow": "Agora não",
|
||||
"updateNow": "Atualizar agora",
|
||||
"restartToUpdate": "Reiniciar para atualizar",
|
||||
"tryAgain": "Tentar novamente"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "Microfone",
|
||||
"mixedLabel": "Fonte",
|
||||
"deleteRegion": "Excluir áudio"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "Você ativou as atualizações experimentais, então pode escolher testar a atualização mais recente do Recordly antes que ela fique amplamente disponível.",
|
||||
"title": "Atualizações",
|
||||
"experimental": "Atualizações experimentais",
|
||||
"saveFailed": "Falha ao alterar o canal de atualização."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "Нет доступа к микрофону. Запись продолжится без вашего голоса.",
|
||||
"failedToStart": "Не удалось начать запись: {{error}}",
|
||||
"failedToStartGeneric": "Не удалось начать запись"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "Доступно обновление",
|
||||
"experimentalAvailableTitle": "Доступно экспериментальное обновление",
|
||||
"experimentalDescription": "Вы включили экспериментальные обновления, поэтому можете протестировать последнее обновление Recordly до его широкого выпуска.",
|
||||
"downloadingTitle": "Загрузка обновления",
|
||||
"readyTitle": "Готово к перезапуску",
|
||||
"checkErrorTitle": "Не удалось проверить обновления",
|
||||
"downloadErrorTitle": "Не удалось загрузить обновление",
|
||||
"experimentalBadge": "Экспериментальное",
|
||||
"previewBadge": "Предпросмотр",
|
||||
"notNow": "Не сейчас",
|
||||
"updateNow": "Обновить сейчас",
|
||||
"restartToUpdate": "Перезапустить для обновления",
|
||||
"tryAgain": "Повторить"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "Микрофон",
|
||||
"mixedLabel": "Источник",
|
||||
"deleteRegion": "Удалить аудио"
|
||||
},
|
||||
"updates": {
|
||||
"title": "Обновления",
|
||||
"experimental": "Экспериментальные обновления",
|
||||
"saveFailed": "Не удалось изменить канал обновлений.",
|
||||
"experimentalDescription": "Вы включили экспериментальные обновления, поэтому можете протестировать последнее обновление Recordly до его широкого выпуска."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "麦克风访问被拒绝。将继续录制但不包含麦克风音频。",
|
||||
"failedToStart": "录制启动失败:{{error}}",
|
||||
"failedToStartGeneric": "录制启动失败"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "有可用更新",
|
||||
"experimentalAvailableTitle": "有可用的实验性更新",
|
||||
"experimentalDescription": "你已选择接收实验性更新,因此可以在 Recordly 最新更新广泛发布之前选择先行测试。",
|
||||
"downloadingTitle": "正在下载更新",
|
||||
"readyTitle": "已准备好重新启动",
|
||||
"checkErrorTitle": "无法检查更新",
|
||||
"downloadErrorTitle": "无法下载更新",
|
||||
"experimentalBadge": "实验性",
|
||||
"previewBadge": "预览",
|
||||
"notNow": "暂不",
|
||||
"updateNow": "立即更新",
|
||||
"restartToUpdate": "重新启动以更新",
|
||||
"tryAgain": "重试"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "麦克风",
|
||||
"mixedLabel": "来源",
|
||||
"deleteRegion": "删除音频"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "你已选择接收实验性更新,因此可以在 Recordly 最新更新广泛发布之前选择先行测试。",
|
||||
"title": "更新",
|
||||
"experimental": "实验性更新",
|
||||
"saveFailed": "无法更改更新频道。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,20 @@
|
||||
"microphoneDenied": "麥克風存取遭拒,將在沒有麥克風音訊的情況下繼續錄製。",
|
||||
"failedToStart": "無法開始錄製:{{error}}",
|
||||
"failedToStartGeneric": "無法開始錄製"
|
||||
},
|
||||
"updateToast": {
|
||||
"availableTitle": "有可用更新",
|
||||
"experimentalAvailableTitle": "有可用的實驗性更新",
|
||||
"experimentalDescription": "你已選擇接收實驗性更新,因此可以在 Recordly 最新更新廣泛推出之前選擇先行測試。",
|
||||
"downloadingTitle": "正在下載更新",
|
||||
"readyTitle": "已準備好重新啟動",
|
||||
"checkErrorTitle": "無法檢查更新",
|
||||
"downloadErrorTitle": "無法下載更新",
|
||||
"experimentalBadge": "實驗性",
|
||||
"previewBadge": "預覽",
|
||||
"notNow": "暫不",
|
||||
"updateNow": "立即更新",
|
||||
"restartToUpdate": "重新啟動以更新",
|
||||
"tryAgain": "重試"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,5 +254,11 @@
|
||||
"micLabel": "麥克風源",
|
||||
"mixedLabel": "源",
|
||||
"deleteRegion": "刪除音訊"
|
||||
},
|
||||
"updates": {
|
||||
"experimentalDescription": "你已選擇接收實驗性更新,因此可以在 Recordly 最新更新廣泛推出之前選擇先行測試。",
|
||||
"title": "更新",
|
||||
"experimental": "實驗性更新",
|
||||
"saveFailed": "無法變更更新頻道。"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user