feat: When in MySQL if loosing db connection contacts can still be saved
they will be synchronized with the db once connected again.
This commit is contained in:
+55
-24
@@ -555,13 +555,20 @@ export default function App() {
|
||||
if (toastTimer.current !== undefined) { window.clearTimeout(toastTimer.current); toastTimer.current = undefined; }
|
||||
advanceToast(); // skip to the next queued toast (or clear if none)
|
||||
}, [advanceToast]);
|
||||
// Status-bar message expanded into a popover (long errors — a TQSL or Club Log
|
||||
// failure — don't fit on one line, and truncating them with no way to read the
|
||||
// rest is useless).
|
||||
const [msgOpen, setMsgOpen] = useState(false);
|
||||
// Reset when the message goes away, or the NEXT one would pop open by itself.
|
||||
useEffect(() => { if (!error && !toast) setMsgOpen(false); }, [error, toast]);
|
||||
// Error banners auto-dismiss after a few seconds (longer than toasts since
|
||||
// they may be multi-line). The X button still closes them immediately.
|
||||
// they may be multi-line). The X button still closes them immediately. Do NOT
|
||||
// dismiss while the operator has it expanded and is reading it.
|
||||
useEffect(() => {
|
||||
if (!error) return;
|
||||
if (!error || msgOpen) return;
|
||||
const t = window.setTimeout(() => setError(''), 6000);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [error]);
|
||||
}, [error, msgOpen]);
|
||||
// True while the QSO recorder is capturing the current contact (set when we
|
||||
// leave the callsign field, cleared on log/cancel). Drives the REC badge.
|
||||
const [recording, setRecording] = useState(false);
|
||||
@@ -3358,25 +3365,8 @@ export default function App() {
|
||||
<Menubar menus={menus} onAction={handleMenu} />
|
||||
|
||||
<div className="relative flex items-center justify-center gap-2 font-mono">
|
||||
{/* Transient toast / error, in the empty band between the menu and
|
||||
the frequency (left of centre). Single-line + truncated. */}
|
||||
{(error || toast) && (
|
||||
<div className="absolute left-1 top-1/2 -translate-y-1/2 z-20 flex items-center max-w-[min(42vw,560px)] font-sans">
|
||||
{error ? (
|
||||
<div className="flex items-center gap-1.5 rounded-md border border-destructive/40 bg-destructive/10 text-destructive px-2.5 py-1 text-xs shadow min-w-0 animate-in fade-in">
|
||||
<AlertCircle className="size-3.5 shrink-0" />
|
||||
<span className="truncate" title={error}>{error}</span>
|
||||
<button className="shrink-0 hover:text-destructive/70" onClick={() => setError('')}><X className="size-3" /></button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 rounded-md border border-success-border bg-success-muted text-success-muted-foreground px-2.5 py-1 text-xs shadow min-w-0 animate-in fade-in">
|
||||
<Satellite className="size-3.5 shrink-0" />
|
||||
<span className="truncate" title={toast}>{toast}</span>
|
||||
<button className="shrink-0 text-success hover:text-success" onClick={dismissToast}><X className="size-3" /></button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Toasts and errors live in the STATUS BAR at the bottom now — the
|
||||
header band was too narrow and long messages were cut off. */}
|
||||
<div className="flex flex-col items-end leading-none">
|
||||
<span className="text-2xl font-semibold text-primary tracking-wide">{freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'}</span>
|
||||
{catState.split && rxFreqMhz && (
|
||||
@@ -4507,7 +4497,7 @@ export default function App() {
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<footer className="flex items-center gap-2 px-3 h-7 bg-card border-t border-border shrink-0">
|
||||
<footer className="relative flex items-center gap-2 px-3 h-7 bg-card border-t border-border shrink-0">
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
QSO count <strong className="text-foreground font-mono">{total.toLocaleString('en-US')}</strong>
|
||||
</span>
|
||||
@@ -4526,7 +4516,48 @@ export default function App() {
|
||||
disabled={!rotatorHeading.enabled}
|
||||
onClick={() => { setSettingsSection('rotator'); setShowSettings(true); }}
|
||||
/>
|
||||
<div className="flex-1" />
|
||||
{/* Toasts / errors: the status bar's free space is far wider than the
|
||||
header band they used to sit in. Still one line (the bar is 28px),
|
||||
but CLICK opens the full text wrapped — long messages (a TQSL or
|
||||
Club Log failure) are no longer cut off with no way to read them. */}
|
||||
<div className="flex-1 min-w-0 flex items-center gap-1 px-1">
|
||||
{(error || toast) && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMsgOpen((o) => !o)}
|
||||
title={t('msg.expand')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-[11px] min-w-0 max-w-full animate-in fade-in',
|
||||
error
|
||||
? 'border-destructive/40 bg-destructive/10 text-destructive hover:bg-destructive/20'
|
||||
: 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted/70',
|
||||
)}
|
||||
>
|
||||
{error ? <AlertCircle className="size-3.5 shrink-0" /> : <Satellite className="size-3.5 shrink-0" />}
|
||||
<span className="truncate">{error || toast}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => { setMsgOpen(false); if (error) setError(''); else dismissToast(); }}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
{msgOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setMsgOpen(false)} />
|
||||
<div className="absolute bottom-7 left-2 z-50 w-[min(70vw,760px)] rounded-lg border border-border bg-card shadow-xl p-3">
|
||||
<p className={cn('text-xs whitespace-pre-wrap break-words leading-relaxed',
|
||||
error ? 'text-destructive' : 'text-foreground')}>
|
||||
{error || toast}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{dbConn && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -46,6 +46,7 @@ const en: Dict = {
|
||||
'lang.choose': 'Choose your language', 'lang.chooseHint': 'You can change this later in Settings → General.',
|
||||
'lang.english': 'English', 'lang.french': 'Français',
|
||||
'settings.language': 'Language', 'settings.languageHint': 'Interface language.',
|
||||
'msg.expand': 'Click to read the full message',
|
||||
'offline.queued': 'Database unreachable — QSO saved locally, will sync automatically',
|
||||
'offline.tip': '{n} QSO(s) waiting — the database is unreachable',
|
||||
'offline.title': 'Offline — {n} QSO(s) waiting',
|
||||
@@ -264,6 +265,7 @@ const fr: Dict = {
|
||||
'lang.choose': 'Choisissez votre langue', 'lang.chooseHint': 'Modifiable plus tard dans Réglages → Général.',
|
||||
'lang.english': 'English', 'lang.french': 'Français',
|
||||
'settings.language': 'Langue', 'settings.languageHint': "Langue de l'interface.",
|
||||
'msg.expand': 'Cliquer pour lire le message en entier',
|
||||
'offline.queued': 'Base injoignable — QSO enregistré localement, synchro automatique',
|
||||
'offline.tip': '{n} QSO en attente — la base est injoignable',
|
||||
'offline.title': 'Hors ligne — {n} QSO en attente',
|
||||
|
||||
Reference in New Issue
Block a user