V0.3
This commit is contained in:
@@ -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, l’application 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user