mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-21 12:51:47 +08:00
✨ feat(update): 下载进度到 100% 后由用户点击「重启应用更新」
- 取消 macOS 下载完成后自动安装/退出,Win/Mac 统一交互 - 进度弹窗在 100% 显示就绪提示,主按钮改为「重启应用更新」 - 关于页安装入口同步文案;点击后才执行安装脚本并重启 - 补充多语言与前端回归测试
This commit is contained in:
@@ -4201,8 +4201,8 @@ function App() {
|
||||
</Button>
|
||||
) : null,
|
||||
isLatestUpdateDownloaded ? (
|
||||
<Button key="install-direct" type="primary" icon={<DownloadOutlined />} onClick={handleInstallFromProgress}>
|
||||
{t('app.about.action.install_update')}
|
||||
<Button key="restart-to-update" type="primary" icon={<DownloadOutlined />} onClick={handleInstallFromProgress}>
|
||||
{t('app.about.action.restart_to_update')}
|
||||
</Button>
|
||||
) : null,
|
||||
].filter(Boolean);
|
||||
@@ -7961,8 +7961,8 @@ function App() {
|
||||
<Button key="open-install-directory" onClick={openDownloadedUpdateDirectory}>
|
||||
{t('app.about.action.open_install_directory')}
|
||||
</Button>,
|
||||
<Button key="install" type="primary" onClick={handleInstallFromProgress}>
|
||||
{t('app.about.action.install_update')}
|
||||
<Button key="restart" type="primary" onClick={handleInstallFromProgress}>
|
||||
{t('app.about.action.restart_to_update')}
|
||||
</Button>
|
||||
] : (updateDownloadProgress.status === 'error' ? [
|
||||
<Button key="close" onClick={hideUpdateDownloadProgress}>{t('common.close')}</Button>
|
||||
@@ -7974,10 +7974,20 @@ function App() {
|
||||
status={updateDownloadProgress.status === 'error' ? 'exception' : (updateDownloadProgress.status === 'done' ? 'success' : 'active')}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(16,24,40,0.55)' }}>
|
||||
{`${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`}
|
||||
{updateDownloadProgress.status === 'done'
|
||||
? t('app.about.download_progress.complete_hint')
|
||||
: `${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`}
|
||||
</div>
|
||||
{updateDownloadProgress.message ? (
|
||||
<div style={{ fontSize: 12, color: '#ff4d4f' }}>{updateDownloadProgress.message}</div>
|
||||
<div style={{
|
||||
fontSize: 12,
|
||||
color: updateDownloadProgress.status === 'error'
|
||||
? '#ff4d4f'
|
||||
: (darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(16,24,40,0.65)'),
|
||||
}}
|
||||
>
|
||||
{updateDownloadProgress.message}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('useAppUpdateManager', () => {
|
||||
expect(hook?.lastUpdateInfo?.downloaded).toBe(true);
|
||||
});
|
||||
|
||||
it('auto-installs a macOS update immediately after download when the backend reports auto relaunch support', async () => {
|
||||
it('keeps download at 100% ready-to-restart without auto-installing after download completes', async () => {
|
||||
backendApp.CheckForUpdates.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -193,8 +193,39 @@ describe('useAppUpdateManager', () => {
|
||||
});
|
||||
|
||||
expect(backendApp.DownloadUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(backendApp.InstallUpdateAndRestart).toHaveBeenCalledTimes(1);
|
||||
// 下载完成后不自动安装;用户需点击「重启应用更新」
|
||||
expect(backendApp.InstallUpdateAndRestart).not.toHaveBeenCalled();
|
||||
expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
|
||||
expect(hook?.updateDownloadProgress.status).toBe('done');
|
||||
expect(hook?.updateDownloadProgress.percent).toBe(100);
|
||||
expect(hook?.updateDownloadProgress.open).toBe(true);
|
||||
expect(hook?.lastUpdateInfo?.downloaded).toBe(true);
|
||||
});
|
||||
|
||||
it('installs and restarts only after the user confirms restart-to-update', async () => {
|
||||
backendApp.CheckForUpdates.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
hasUpdate: true,
|
||||
currentVersion: '0.8.1',
|
||||
latestVersion: '0.8.2',
|
||||
downloaded: true,
|
||||
assetSize: 2048,
|
||||
},
|
||||
});
|
||||
backendApp.InstallUpdateAndRestart.mockResolvedValue({ success: true });
|
||||
|
||||
renderHook();
|
||||
|
||||
await act(async () => {
|
||||
await hook?.checkForUpdates(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await hook?.handleInstallFromProgress();
|
||||
});
|
||||
|
||||
expect(backendApp.InstallUpdateAndRestart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('switches update channel and re-checks against the selected channel', async () => {
|
||||
|
||||
@@ -129,11 +129,6 @@ const normalizeAboutInfo = (value: unknown): AboutInfo => {
|
||||
};
|
||||
};
|
||||
|
||||
const shouldAutoInstallDownloadedUpdate = (resultData: UpdateDownloadResultData | null | undefined): boolean => {
|
||||
const platform = String(resultData?.platform || '').trim().toLowerCase();
|
||||
return platform === 'darwin' && resultData?.autoRelaunch !== false;
|
||||
};
|
||||
|
||||
export const useAppUpdateManager = ({
|
||||
runtimeBuildType,
|
||||
t,
|
||||
@@ -191,7 +186,7 @@ export const useAppUpdateManager = ({
|
||||
const localDownloaded = updateDownloadedVersionRef.current === buildUpdateKey(info);
|
||||
const hasDownloaded = Boolean(info.downloaded) || localDownloaded;
|
||||
return hasDownloaded
|
||||
? t('app.about.update_status.new_version_downloaded', { version: info.latestVersion })
|
||||
? t('app.about.update_status.new_version_ready_restart', { version: info.latestVersion })
|
||||
: t('app.about.update_status.new_version_not_downloaded', { version: info.latestVersion });
|
||||
}
|
||||
return t('app.about.update_status.latest', { version: info.currentVersion || t('common.unknown') });
|
||||
@@ -273,27 +268,22 @@ export const useAppUpdateManager = ({
|
||||
downloadPath: resultData?.downloadPath || prev.downloadPath || info.downloadPath,
|
||||
};
|
||||
});
|
||||
if (resultData?.downloadPath) {
|
||||
void message.success({ content: t('app.about.message.download_completed_with_path', { path: resultData.downloadPath }), duration: 5 });
|
||||
} else {
|
||||
void message.success({ content: t('app.about.message.download_completed'), duration: 2 });
|
||||
}
|
||||
// 与 Terminus/Codex 一致:下载到 100% 后停留在就绪态,由用户点击「重启应用更新」
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
open: true,
|
||||
status: 'done',
|
||||
percent: 100,
|
||||
downloaded: prev.total > 0 ? prev.total : (info.assetSize || prev.downloaded),
|
||||
message: t('app.about.download_progress.ready_to_restart'),
|
||||
}));
|
||||
void message.success({
|
||||
content: resultData?.downloadPath
|
||||
? t('app.about.message.download_ready_restart_with_path', { path: resultData.downloadPath })
|
||||
: t('app.about.message.download_ready_restart'),
|
||||
duration: 4,
|
||||
});
|
||||
setAboutUpdateStatus(formatAboutUpdateStatus({ ...info, channel: normalizeUpdateChannel(info.channel), downloaded: true }));
|
||||
|
||||
if (shouldAutoInstallDownloadedUpdate(resultData)) {
|
||||
let installRes: any = null;
|
||||
try {
|
||||
installRes = await (window as any).go?.app?.App?.InstallUpdateAndRestart?.();
|
||||
} catch (error: any) {
|
||||
installRes = { success: false, message: error?.message || t('common.unknown') };
|
||||
}
|
||||
if (!installRes?.success) {
|
||||
void message.error(t('app.about.message.install_failed_with_error', { error: installRes?.message || t('common.unknown') }));
|
||||
return;
|
||||
}
|
||||
updateInstallTriggeredVersionRef.current = targetKey || null;
|
||||
setUpdateDownloadProgress((prev) => ({ ...prev, open: false }));
|
||||
}
|
||||
} else {
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
@@ -335,14 +325,40 @@ export const useAppUpdateManager = ({
|
||||
if (!canInstall) {
|
||||
return;
|
||||
}
|
||||
const res = await (window as any).go.app.App.InstallUpdateAndRestart();
|
||||
// 点击后进入「正在应用并重启」态,再拉起安装脚本并退出
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
open: true,
|
||||
status: 'downloading',
|
||||
percent: 100,
|
||||
message: t('app.about.download_progress.applying_restart'),
|
||||
}));
|
||||
let res: any = null;
|
||||
try {
|
||||
res = await (window as any).go?.app?.App?.InstallUpdateAndRestart?.();
|
||||
} catch (error: any) {
|
||||
res = { success: false, message: error?.message || t('common.unknown') };
|
||||
}
|
||||
if (!res?.success) {
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
open: true,
|
||||
status: 'error',
|
||||
message: res?.message || t('common.unknown'),
|
||||
}));
|
||||
void message.error(t('app.about.message.install_failed_with_error', { error: res?.message || t('common.unknown') }));
|
||||
return;
|
||||
}
|
||||
updateInstallTriggeredVersionRef.current = lastUpdateKey || null;
|
||||
hideUpdateDownloadProgress();
|
||||
}, [hideUpdateDownloadProgress, lastUpdateInfo, lastUpdateKey, t, updateDownloadProgress.status]);
|
||||
// 后端会 Quit;此处保持弹窗文案,避免用户误以为失败
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
open: true,
|
||||
status: 'done',
|
||||
percent: 100,
|
||||
message: t('app.about.download_progress.restarting'),
|
||||
}));
|
||||
}, [lastUpdateInfo, lastUpdateKey, t, updateDownloadProgress.status]);
|
||||
|
||||
const openDownloadedUpdateDirectory = useCallback(async () => {
|
||||
const backendApp = (window as any).go?.app?.App;
|
||||
@@ -620,16 +636,31 @@ export const useAppUpdateManager = ({
|
||||
? event.percent
|
||||
: (total > 0 ? (downloaded / total) * 100 : 0);
|
||||
const percent = Math.max(0, Math.min(100, percentRaw));
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
open: prev.open,
|
||||
version: prev.version,
|
||||
key: prev.key,
|
||||
status: nextStatus,
|
||||
percent,
|
||||
downloaded,
|
||||
total,
|
||||
message: String(event.message || ''),
|
||||
}));
|
||||
setUpdateDownloadProgress((prev) => {
|
||||
// 用户已点「重启应用更新」时,不让 downloading 事件把 100% 就绪态打回中间态文案
|
||||
if (updateInstallTriggeredVersionRef.current && prev.key && updateInstallTriggeredVersionRef.current === prev.key) {
|
||||
return prev;
|
||||
}
|
||||
const eventMessage = String(event.message || '');
|
||||
let message = eventMessage;
|
||||
if (!message) {
|
||||
if (nextStatus === 'done') {
|
||||
message = t('app.about.download_progress.ready_to_restart');
|
||||
} else if (nextStatus === 'start' || nextStatus === 'downloading') {
|
||||
message = t('app.about.download_progress.downloading');
|
||||
}
|
||||
}
|
||||
return {
|
||||
open: prev.open || nextStatus === 'start' || nextStatus === 'downloading' || nextStatus === 'done' || nextStatus === 'error',
|
||||
version: prev.version,
|
||||
key: prev.key,
|
||||
status: nextStatus,
|
||||
percent: nextStatus === 'done' ? 100 : percent,
|
||||
downloaded: nextStatus === 'done' && total > 0 ? total : downloaded,
|
||||
total: total > 0 ? total : prev.total,
|
||||
message,
|
||||
};
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Wails API: EventsOn unavailable', e);
|
||||
@@ -637,7 +668,7 @@ export const useAppUpdateManager = ({
|
||||
return () => {
|
||||
if (offDownloadProgress) offDownloadProgress();
|
||||
};
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
return {
|
||||
aboutDisplayVersion,
|
||||
|
||||
Reference in New Issue
Block a user