This commit is contained in:
2026-08-10 22:06:49 +02:00
parent 2a5d9bb0fd
commit 8bff7982ff
83 changed files with 17975 additions and 4824 deletions
+142
View File
@@ -0,0 +1,142 @@
// =====================================================================
// Bandeau « une mise à jour est disponible ».
//
// Discret : une bande fine en haut de l'application, jamais une modale
// qui interrompt le travail. On peut reporter — la proposition revient
// au prochain lancement.
// =====================================================================
import { useCallback, useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { Download, RefreshCw, Sparkles, X } from 'lucide-react'
import { AvailableUpdate, checkForUpdate } from '../lib/updater'
import { useFeedback } from '../lib/feedback'
import { Button, IconButton } from './ui'
/** Nouvelle vérification toutes les 2 heures pour les sessions qui durent. */
const RECHECK_MS = 2 * 60 * 60 * 1000
export default function UpdateBanner(): JSX.Element | null {
const { toast } = useFeedback()
const [update, setUpdate] = useState<AvailableUpdate | null>(null)
const [dismissed, setDismissed] = useState(false)
const [installing, setInstalling] = useState(false)
const [progress, setProgress] = useState<number | null>(null)
const look = useCallback(async () => {
const found = await checkForUpdate()
if (found) setUpdate(found)
}, [])
useEffect(() => {
// Au démarrage, mais sans retarder l'affichage de l'application.
const first = setTimeout(look, 4000)
const timer = setInterval(look, RECHECK_MS)
return () => {
clearTimeout(first)
clearInterval(timer)
}
}, [look])
if (!update || dismissed) return null
const install = async (): Promise<void> => {
setInstalling(true)
setProgress(0)
try {
// Au succès l'application redémarre : rien à faire de plus ici.
await update.install((pct) => setProgress(pct))
} catch (e) {
setInstalling(false)
setProgress(null)
toast(
`La mise à jour a échoué : ${(e as Error).message}. Réessaie plus tard ou préviens l'administrateur.`,
'error'
)
}
}
return (
<AnimatePresence>
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="shrink-0 overflow-hidden border-b border-accent/25 bg-accent/10"
>
<div className="flex items-center gap-3 px-4 py-2">
<Sparkles size={15} className="shrink-0 text-accent" />
<span className="min-w-0 flex-1 text-xs text-ink">
<span className="font-medium">Version {update.version} disponible.</span>{' '}
{installing ? (
<span className="text-muted">
{progress === null || progress === 0
? 'Téléchargement…'
: progress < 100
? `Téléchargement ${progress} %…`
: 'Installation, lapplication va redémarrer…'}
</span>
) : (
<span className="text-muted">
{update.notes?.split('\n')[0] || "L'application se relancera toute seule."}
</span>
)}
</span>
{installing ? (
<div className="h-1 w-32 shrink-0 overflow-hidden rounded-full bg-accent/20">
<div
className={`h-full rounded-full bg-accent transition-[width] duration-300 ${
progress === null ? 'w-1/3 animate-pulse' : ''
}`}
style={progress === null ? undefined : { width: `${progress}%` }}
/>
</div>
) : (
<>
<Button size="sm" variant="primary" icon={<Download size={13} />} onClick={install}>
Installer et redémarrer
</Button>
<IconButton
size="sm"
label="Plus tard"
icon={<X size={15} />}
onClick={() => setDismissed(true)}
/>
</>
)}
</div>
</motion.div>
</AnimatePresence>
)
}
/** Ligne « Version X · Rechercher une mise à jour », pour l'écran d'aide. */
export function UpdateCheckRow({ version }: { version: string | null }): JSX.Element {
const { toast } = useFeedback()
const [busy, setBusy] = useState(false)
const look = async (): Promise<void> => {
setBusy(true)
const found = await checkForUpdate()
setBusy(false)
toast(
found
? `Version ${found.version} disponible — le bandeau apparaît en haut de l'écran.`
: 'Tu es déjà à jour.',
found ? 'info' : 'success'
)
}
return (
<div className="flex items-center justify-between gap-4 border-t border-border pt-3 text-sm">
<span className="text-muted">
Version installée <span className="font-medium text-ink">{version ?? '—'}</span>
</span>
<Button size="sm" icon={<RefreshCw size={13} />} loading={busy} onClick={look}>
Rechercher une mise à jour
</Button>
</div>
)
}