feat(updater): self-update via GitHub Releases with launch check and settings UI
Phase 3 of the 0.1.0 release prep: - tauri-plugin-updater + tauri-plugin-process wired into the builder, with updater:default / process:allow-restart capabilities - bundle.createUpdaterArtifacts: true; endpoint points at the GitHub releases latest.json, pubkey baked into tauri.conf.json - store: update status state machine (check / download progress / install) with a quiet launch check (prod only) that stays silent on network errors - UI: dismissible update toast with download progress, plus an Updates group in Settings > General showing current version and manual check/install
This commit is contained in:
@@ -178,6 +178,12 @@ export function SettingsModal() {
|
||||
const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase);
|
||||
const getOrphanedThumbnailsInfo = useGalleryStore((state) => state.getOrphanedThumbnailsInfo);
|
||||
const cleanupOrphanedThumbnails = useGalleryStore((state) => state.cleanupOrphanedThumbnails);
|
||||
const appVersion = useGalleryStore((state) => state.appVersion);
|
||||
const updateStatus = useGalleryStore((state) => state.updateStatus);
|
||||
const updateVersion = useGalleryStore((state) => state.updateVersion);
|
||||
const updateError = useGalleryStore((state) => state.updateError);
|
||||
const checkForUpdates = useGalleryStore((state) => state.checkForUpdates);
|
||||
const installUpdate = useGalleryStore((state) => state.installUpdate);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingsOpen) return;
|
||||
@@ -578,6 +584,47 @@ export function SettingsModal() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8 space-y-9">
|
||||
<SettingsGroup title="Updates">
|
||||
<SettingsItem
|
||||
label={
|
||||
<span className="inline-flex items-center gap-2.5">
|
||||
<span>Phokus {appVersion ? `v${appVersion}` : "—"}</span>
|
||||
{updateStatus === "available" || updateStatus === "downloading" || updateStatus === "installing" ? (
|
||||
<StatusPill tone="busy">v{updateVersion} available</StatusPill>
|
||||
) : updateStatus === "upToDate" ? (
|
||||
<StatusPill tone="ready">Up to date</StatusPill>
|
||||
) : null}
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
updateStatus === "error" ? (
|
||||
<span className="text-amber-300/90">Update check failed: {updateError}</span>
|
||||
) : updateStatus === "downloading" || updateStatus === "installing" ? (
|
||||
"Downloading update — the app will restart when it finishes."
|
||||
) : (
|
||||
"Updates are checked quietly at launch and installed only when you choose."
|
||||
)
|
||||
}
|
||||
>
|
||||
{updateStatus === "available" ? (
|
||||
<button
|
||||
className="rounded-md border border-emerald-400/35 bg-emerald-500/15 px-3 py-1.5 text-xs text-emerald-200 transition-colors hover:bg-emerald-500/25"
|
||||
onClick={() => void installUpdate()}
|
||||
>
|
||||
Install & restart
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
|
||||
onClick={() => void checkForUpdates()}
|
||||
disabled={updateStatus === "checking" || updateStatus === "downloading" || updateStatus === "installing"}
|
||||
>
|
||||
{updateStatus === "checking" ? "Checking..." : "Check for updates"}
|
||||
</button>
|
||||
)}
|
||||
</SettingsItem>
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup title="Storage & notifications">
|
||||
<SettingsItem
|
||||
label="App data folder"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useGalleryStore } from "../store";
|
||||
|
||||
export function UpdateToast() {
|
||||
const updateStatus = useGalleryStore((s) => s.updateStatus);
|
||||
const updateVersion = useGalleryStore((s) => s.updateVersion);
|
||||
const updateProgress = useGalleryStore((s) => s.updateProgress);
|
||||
const updateDismissed = useGalleryStore((s) => s.updateDismissed);
|
||||
const installUpdate = useGalleryStore((s) => s.installUpdate);
|
||||
const dismissUpdate = useGalleryStore((s) => s.dismissUpdate);
|
||||
|
||||
const visible =
|
||||
!updateDismissed &&
|
||||
(updateStatus === "available" || updateStatus === "downloading" || updateStatus === "installing");
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 12 }}
|
||||
transition={{ duration: 0.18 }}
|
||||
className="fixed bottom-4 right-4 z-50 w-80 rounded-lg border border-white/10 bg-gray-900 p-4 shadow-xl"
|
||||
>
|
||||
{updateStatus === "available" ? (
|
||||
<>
|
||||
<p className="text-sm font-medium text-white">Update available</p>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Phokus v{updateVersion} is ready to download and install.
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<button
|
||||
className="rounded-md border border-emerald-400/35 bg-emerald-500/15 px-3 py-1.5 text-xs text-emerald-200 transition-colors hover:bg-emerald-500/25"
|
||||
onClick={() => void installUpdate()}
|
||||
>
|
||||
Install & restart
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md border border-transparent px-3 py-1.5 text-xs text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-gray-200"
|
||||
onClick={dismissUpdate}
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm font-medium text-white">
|
||||
{updateStatus === "installing" ? "Installing update..." : "Downloading update..."}
|
||||
</p>
|
||||
<div className="mt-3 h-1 overflow-hidden rounded-full bg-white/10">
|
||||
<div
|
||||
className={`h-full rounded-full bg-emerald-400/80 transition-[width] duration-200 ${
|
||||
updateProgress === null ? "w-full animate-pulse" : ""
|
||||
}`}
|
||||
style={updateProgress !== null ? { width: `${Math.round(updateProgress * 100)}%` } : undefined}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-600">The app will restart when it finishes.</p>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user