fix(mac): updater progress UI and skip redundant download
Show update stage in the modal, forward electron-updater progress over IPC, and install immediately when the build is already cached. Rename window titles to TTRPG - *. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/app-window-icon.png" type="image/png" />
|
||||
<title>DnD Player — Control</title>
|
||||
<title>TTRPG - Control</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/app-window-icon.png" type="image/png" />
|
||||
<title>DnD Player — Editor</title>
|
||||
<title>TTRPG - Editor</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { ipcChannels, type UpdaterCheckResponse } from '../../shared/ipc/contracts';
|
||||
import {
|
||||
ipcChannels,
|
||||
type UpdaterCheckResponse,
|
||||
type UpdaterProgressEvent,
|
||||
} from '../../shared/ipc/contracts';
|
||||
import { EULA_CURRENT_VERSION } from '../../shared/license/eulaVersion';
|
||||
import type { LicenseSnapshot } from '../../shared/license/licenseSnapshot';
|
||||
import { PROJECT_ZIP_EXTENSION } from '../../shared/project/projectZipExtension';
|
||||
@@ -965,31 +969,76 @@ type CheckUpdatesModalProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function formatUpdaterStageLabel(
|
||||
t: (key: string, vars?: Record<string, string | number>) => string,
|
||||
ev: UpdaterProgressEvent | null,
|
||||
): string {
|
||||
if (!ev) return t('updates.stage.checking');
|
||||
const percentSuffix =
|
||||
ev.phase === 'downloading' && ev.percent !== undefined
|
||||
? t('updates.stagePercent', { percent: ev.percent })
|
||||
: '';
|
||||
switch (ev.phase) {
|
||||
case 'checking':
|
||||
return t('updates.stage.checking');
|
||||
case 'available':
|
||||
return t('updates.stage.available', { version: ev.version ?? '?' });
|
||||
case 'not-available':
|
||||
return t('updates.stage.not-available');
|
||||
case 'downloading':
|
||||
return t('updates.stage.downloading', { percent: percentSuffix });
|
||||
case 'installing':
|
||||
return t('updates.stage.installing');
|
||||
case 'error':
|
||||
return ev.message ? `${t('updates.stage.error')}: ${ev.message}` : t('updates.stage.error');
|
||||
default:
|
||||
return t('updates.stage.checking');
|
||||
}
|
||||
}
|
||||
|
||||
function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
|
||||
const { t } = useEditorI18n();
|
||||
const [phase, setPhase] = useState<'idle' | 'checking' | 'done'>('idle');
|
||||
const [res, setRes] = useState<UpdaterCheckResponse | null>(null);
|
||||
const [downloadBusy, setDownloadBusy] = useState(false);
|
||||
const [progress, setProgress] = useState<UpdaterProgressEvent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
startTransition(() => {
|
||||
setPhase('checking');
|
||||
setRes(null);
|
||||
setProgress({ phase: 'checking' });
|
||||
setDownloadBusy(false);
|
||||
});
|
||||
void getDndApi()
|
||||
.invoke(ipcChannels.updater.check, {})
|
||||
.then((r) => {
|
||||
setRes(r);
|
||||
setPhase('done');
|
||||
if (r.outcome === 'available') {
|
||||
setProgress({ phase: 'available', version: r.version });
|
||||
} else if (r.outcome === 'current') {
|
||||
setProgress({ phase: 'not-available', version: r.currentVersion });
|
||||
} else if (r.outcome === 'error') {
|
||||
setProgress({ phase: 'error', message: r.message });
|
||||
}
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
setRes({ outcome: 'error', message });
|
||||
setPhase('done');
|
||||
setProgress({ phase: 'error', message });
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
return getDndApi().on(ipcChannels.updater.progress, (ev) => {
|
||||
setProgress(ev);
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -1001,6 +1050,8 @@ function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const stageLine = t('updates.stageLine', { stage: formatUpdaterStageLabel(t, progress) });
|
||||
|
||||
const body =
|
||||
phase === 'checking' || res === null ? (
|
||||
<div className={styles.muted}>{t('updates.checking')}</div>
|
||||
@@ -1042,7 +1093,10 @@ function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.fieldGrid}>{body}</div>
|
||||
<div className={styles.fieldGrid}>
|
||||
{body}
|
||||
<div className={styles.muted}>{stageLine}</div>
|
||||
</div>
|
||||
<div className={styles.modalFooter}>
|
||||
{showUpdateIdle ? (
|
||||
<>
|
||||
@@ -1053,21 +1107,23 @@ function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
|
||||
variant="primary"
|
||||
disabled={downloadBusy}
|
||||
onClick={() => {
|
||||
if (res?.outcome !== 'available') return;
|
||||
setDownloadBusy(true);
|
||||
setProgress({ phase: 'downloading', version: res.version, percent: 0 });
|
||||
void getDndApi()
|
||||
.invoke(ipcChannels.updater.downloadAndRestart, {})
|
||||
.invoke(ipcChannels.updater.downloadAndRestart, { version: res.version })
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
setDownloadBusy(false);
|
||||
setRes({ outcome: 'error', message: r.message });
|
||||
setProgress({ phase: 'error', message: r.message });
|
||||
}
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
setDownloadBusy(false);
|
||||
setRes({
|
||||
outcome: 'error',
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
setRes({ outcome: 'error', message });
|
||||
setProgress({ phase: 'error', message });
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -1076,7 +1132,7 @@ function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
|
||||
</>
|
||||
) : showUpdateBusy ? (
|
||||
<Button variant="primary" disabled>
|
||||
{t('updates.downloading')}
|
||||
{progress?.phase === 'installing' ? t('updates.stage.installing') : t('updates.downloading')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" disabled={downloadBusy} onClick={onClose}>
|
||||
|
||||
@@ -130,6 +130,14 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'updates.noLicense': 'Нужна активная лицензия.',
|
||||
'updates.download': 'Обновить',
|
||||
'updates.downloading': 'Загрузка…',
|
||||
'updates.stageLine': 'Этап: {stage}',
|
||||
'updates.stage.checking': 'проверка обновлений',
|
||||
'updates.stage.available': 'доступна версия {version}',
|
||||
'updates.stage.not-available': 'актуальная версия',
|
||||
'updates.stage.downloading': 'загрузка{percent}',
|
||||
'updates.stage.installing': 'установка и перезапуск',
|
||||
'updates.stage.error': 'ошибка',
|
||||
'updates.stagePercent': ' ({percent}%)',
|
||||
|
||||
'projectMenu.home': 'Начальный экран',
|
||||
'projectMenu.import': 'Импорт',
|
||||
@@ -359,6 +367,14 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'updates.noLicense': 'An active license is required.',
|
||||
'updates.download': 'Update',
|
||||
'updates.downloading': 'Downloading…',
|
||||
'updates.stageLine': 'Stage: {stage}',
|
||||
'updates.stage.checking': 'checking for updates',
|
||||
'updates.stage.available': 'version {version} available',
|
||||
'updates.stage.not-available': 'up to date',
|
||||
'updates.stage.downloading': 'downloading{percent}',
|
||||
'updates.stage.installing': 'installing and restarting',
|
||||
'updates.stage.error': 'error',
|
||||
'updates.stagePercent': ' ({percent}%)',
|
||||
|
||||
'projectMenu.home': 'Home',
|
||||
'projectMenu.import': 'Import',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/app-window-icon.png" type="image/png" />
|
||||
<title>DnD Player — Presentation</title>
|
||||
<title>TTRPG - Presentation</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user