diff --git a/src/components/AppUpdater.tsx b/src/components/AppUpdater.tsx index 27d8d581..5db23032 100644 --- a/src/components/AppUpdater.tsx +++ b/src/components/AppUpdater.tsx @@ -4,7 +4,7 @@ import { open } from '@tauri-apps/plugin-shell'; import { listen } from '@tauri-apps/api/event'; import { dirname } from '@tauri-apps/api/path'; import { invoke } from '@tauri-apps/api/core'; -import { ArrowUpCircle, ChevronDown, Download, FolderOpen, X } from 'lucide-react'; +import { ArrowUpCircle, CheckCircle2, ChevronDown, Download, FolderOpen, RefreshCw, ShieldCheck, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { version as currentVersion } from '../../package.json'; import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform'; @@ -103,7 +103,10 @@ export default function AppUpdater() { const [dlProgress, setDlProgress] = useState({ bytes: 0, total: 0 }); const [dlPath, setDlPath] = useState(''); const [dlError, setDlError] = useState(''); + const [countdown, setCountdown] = useState(null); const unlistenRef = useRef<(() => void) | null>(null); + const countdownRef = useRef(null); + const relaunchFnRef = useRef<(() => Promise) | null>(null); const fetchRelease = async (preview = false) => { try { @@ -152,7 +155,10 @@ export default function AppUpdater() { // Clean up download listener when component unmounts useEffect(() => { - return () => { unlistenRef.current?.(); }; + return () => { + unlistenRef.current?.(); + if (countdownRef.current) window.clearInterval(countdownRef.current); + }; }, []); if (!release || dismissed) return null; @@ -169,6 +175,31 @@ export default function AppUpdater() { setDismissed(true); }; + const startRestartCountdown = (seconds: number) => { + let remaining = seconds; + setCountdown(remaining); + countdownRef.current = window.setInterval(() => { + remaining -= 1; + if (remaining <= 0) { + if (countdownRef.current) window.clearInterval(countdownRef.current); + countdownRef.current = null; + setCountdown(null); + relaunchFnRef.current?.(); + } else { + setCountdown(remaining); + } + }, 1000); + }; + + const handleRestartNow = async () => { + if (countdownRef.current) { + window.clearInterval(countdownRef.current); + countdownRef.current = null; + } + setCountdown(null); + await relaunchFnRef.current?.(); + }; + const handleDownload = async () => { // On macOS: use the Tauri Updater plugin — downloads .app.tar.gz, verifies // the minisign signature against the bundled pubkey, replaces the .app, and @@ -179,6 +210,8 @@ export default function AppUpdater() { setDlError(''); try { const { check } = await import('@tauri-apps/plugin-updater'); + const { relaunch } = await import('@tauri-apps/plugin-process'); + relaunchFnRef.current = relaunch; const update = await check(); if (!update) { setDlError(t('common.updaterErrorMsg')); @@ -196,9 +229,12 @@ export default function AppUpdater() { setDlProgress({ bytes: downloaded, total }); } else if (event.event === 'Finished') { setDlState('done'); + // downloadAndInstall replaces the .app in place but does not exit + // the running process. Give the user a 3s countdown (with a manual + // "Restart now" button) before auto-relaunch. + startRestartCountdown(3); } }); - // downloadAndInstall replaces the .app and relaunches automatically on macOS. } catch (e) { setDlError(String(e)); setDlState('error'); @@ -316,12 +352,25 @@ export default function AppUpdater() { ) : useTauriUpdater ? ( <> {dlState === 'idle' && ( -
- +
+
+ {t('common.updaterMacReadyTitle', { defaultValue: 'Ready to install' })} +
+
{t('common.updaterMacReady', { - defaultValue: 'Downloads, verifies and installs automatically. The app will restart when done.', + defaultValue: 'The update downloads, verifies and applies in place — no DMG needed. The app restarts automatically when done.', })} - +
+
+ + + {t('common.updaterTrustNotarized', { defaultValue: 'Notarized by Apple' })} + + + + {t('common.updaterTrustSignature', { defaultValue: 'Signature verified' })} + +
)} {dlState === 'downloading' && ( @@ -338,8 +387,14 @@ export default function AppUpdater() { )} {dlState === 'done' && (
+
- {t('common.updaterMacDone', { defaultValue: 'Installed. Restarting…' })} + {t('common.updaterMacDoneTitle', { defaultValue: 'Update installed' })} +
+
+ {countdown !== null + ? t('common.updaterRestartingIn', { defaultValue: 'Restarting in {{n}}s…', n: countdown }) + : t('common.updaterRestarting', { defaultValue: 'Restarting…' })}
)} @@ -394,27 +449,55 @@ export default function AppUpdater() {
{/* end update-modal-body */} - {/* Footer buttons */} + {/* Footer buttons — state-dependent to avoid redundant/jumping buttons */}
- -
- - {showInstallBtn && dlState === 'idle' && ( - + {dlState === 'idle' && ( + <> + +
+ + {showInstallBtn && ( + + )} + + )} + {dlState === 'downloading' &&
} + {dlState === 'done' && useTauriUpdater && ( + <> +
+ + + )} + {dlState === 'done' && !useTauriUpdater && ( + <> +
+ + )} {dlState === 'error' && ( - + <> +
+ + + )}
diff --git a/src/locales/de.ts b/src/locales/de.ts index 72103ad3..73c6fd8a 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -407,6 +407,15 @@ export const deTranslation = { updaterInstallHint: 'Psysonic schließen und das Installationsprogramm manuell ausführen.', updaterAurHint: 'Update über AUR installieren:', updaterErrorMsg: 'Download fehlgeschlagen', + updaterInstallNow: 'Jetzt installieren', + updaterMacReadyTitle: 'Bereit zur Installation', + updaterMacReady: 'Das Update wird heruntergeladen, verifiziert und direkt übernommen — keine DMG nötig. Die App startet anschließend automatisch neu.', + updaterTrustNotarized: 'Von Apple beglaubigt', + updaterTrustSignature: 'Signatur verifiziert', + updaterMacDoneTitle: 'Update installiert', + updaterRestartingIn: 'Neustart in {{n}}s …', + updaterRestarting: 'Neustart läuft …', + updaterRestartNow: 'Jetzt neu starten', updaterRetryBtn: 'Erneut versuchen', durationHoursMinutes: '{{hours}} Std. {{minutes}} Min.', durationMinutesOnly: '{{minutes}} Min.', diff --git a/src/locales/en.ts b/src/locales/en.ts index 21f85ae6..c5ffa378 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -409,6 +409,15 @@ export const enTranslation = { updaterAurHint: 'Install the update via AUR:', updaterErrorMsg: 'Download failed', updaterRetryBtn: 'Retry', + updaterInstallNow: 'Install now', + updaterMacReadyTitle: 'Ready to install', + updaterMacReady: 'The update downloads, verifies and applies in place — no DMG needed. The app restarts automatically when done.', + updaterTrustNotarized: 'Notarized by Apple', + updaterTrustSignature: 'Signature verified', + updaterMacDoneTitle: 'Update installed', + updaterRestartingIn: 'Restarting in {{n}}s…', + updaterRestarting: 'Restarting…', + updaterRestartNow: 'Restart now', durationHoursMinutes: '{{hours}}h {{minutes}}m', durationMinutesOnly: '{{minutes}}m', updaterOpenGitHub: 'Open on GitHub', diff --git a/src/styles/layout.css b/src/styles/layout.css index 40265d7e..1d3f9357 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -769,6 +769,53 @@ gap: var(--space-2); font-size: 12px; } +/* macOS Tauri Updater — idle state info block */ +.update-modal-mac-info { + display: flex; + flex-direction: column; + gap: var(--space-2); +} +.update-modal-mac-info-main { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} +.update-modal-mac-info-sub { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.45; +} +.update-modal-trust-badges { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + margin-top: var(--space-1); +} +.update-modal-trust-badge { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: var(--positive, var(--accent)); + background: var(--bg-glass); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: 3px 8px; +} +.update-modal-trust-badge svg { + flex-shrink: 0; +} +/* macOS Tauri Updater — done state (after install, before/during restart) */ +.update-modal-done-icon { + align-self: center; + color: var(--positive, var(--accent)); + margin-bottom: var(--space-1); +} +.update-modal-done-countdown { + font-size: 12px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} /* AUR hint */ .update-modal-aur { display: flex;