fix: Bug when opening another SQLite
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
@@ -620,17 +621,15 @@ func (a *App) startup(ctx context.Context) {
|
||||
// Windows reinstall. It lives OUTSIDE the DB since we must know the path
|
||||
// before opening it.
|
||||
if custom := readDBPointer(dataDir); custom != "" {
|
||||
// Portability guard: a pointer that is merely ANOTHER folder's default DB
|
||||
// location ("…/<other>/data/opslog.db") means the portable folder was
|
||||
// renamed or copied — its config.json still points at the original. Ignore
|
||||
// it and use THIS folder's own data (and clear the stale pointer so it
|
||||
// stops happening). A genuine custom location — another drive, a different
|
||||
// filename — is NOT default-style, so it's still honoured.
|
||||
stale := strings.EqualFold(filepath.Base(custom), "opslog.db") &&
|
||||
strings.EqualFold(filepath.Base(filepath.Dir(custom)), "data") &&
|
||||
!strings.EqualFold(filepath.Clean(filepath.Dir(custom)), filepath.Clean(dataDir))
|
||||
if stale {
|
||||
fmt.Printf("OpsLog: ignoring stale DB pointer %q (folder moved) — using %s\n", custom, a.dbPath)
|
||||
// Honour any chosen database that still exists on disk. Only fall back to
|
||||
// this folder's own default when the pointed-to file is genuinely GONE —
|
||||
// the portable folder was copied to a machine without the original drive,
|
||||
// or the path was deleted — and clear the now-dangling pointer so it stops
|
||||
// trying. (The old heuristic guessed "folder moved" from the file name and
|
||||
// wrongly discarded valid logbooks named …/data/opslog.db, sending the user
|
||||
// back to the default every launch — the save appeared not to stick.)
|
||||
if _, err := os.Stat(custom); err != nil {
|
||||
fmt.Printf("OpsLog: chosen database %q not found (%v) — using %s\n", custom, err, a.dbPath)
|
||||
_ = writeDBPointer(dataDir, "")
|
||||
} else {
|
||||
a.dbPath = custom
|
||||
@@ -1527,6 +1526,26 @@ func (a *App) QuitApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// RestartApp relaunches OpsLog and closes this instance, so a database change
|
||||
// (the DB pointer is read once at startup) applies without the user manually
|
||||
// reopening the app. The new process opens the SQLite files while this one is
|
||||
// closing; SQLite's busy_timeout(5000) covers that brief overlap.
|
||||
func (a *App) RestartApp() error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate executable: %w", err)
|
||||
}
|
||||
cmd := exec.Command(exe)
|
||||
cmd.Dir = filepath.Dir(exe)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("relaunch OpsLog: %w", err)
|
||||
}
|
||||
if a.ctx != nil {
|
||||
wruntime.Quit(a.ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reloadLookupProviders rebuilds the lookup chain from current settings.
|
||||
// Called at startup and after the user saves new credentials.
|
||||
//
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Copy, Plus, Star, StarOff, Trash2,
|
||||
ChevronDown, ChevronRight,
|
||||
User, Database, Radio, Cog, Server, Award, Antenna as AntennaIcon,
|
||||
Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power,
|
||||
Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power, Check,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
|
||||
@@ -26,9 +26,8 @@ import {
|
||||
ConnectClusterServer, DisconnectClusterServer,
|
||||
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
|
||||
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
|
||||
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, QuitApp, CreateDatabase,
|
||||
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, RestartApp, CreateDatabase,
|
||||
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
|
||||
GetDataDir,
|
||||
GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
|
||||
GetTelemetryEnabled, SetTelemetryEnabled,
|
||||
GetLiveStatusEnabled, SetLiveStatusEnabled,
|
||||
@@ -988,7 +987,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const [mysqlMsg, setMysqlMsg] = useState('');
|
||||
const [restartMsg, setRestartMsg] = useState(''); // backend switch / save → "restart to apply"
|
||||
const [backendStatus, setBackendStatus] = useState<{ active: string; fallback: boolean; error: string } | null>(null);
|
||||
const [dataDir, setDataDir] = useState('');
|
||||
|
||||
const [clusterServers, setClusterServers] = useState<ClusterServer[]>([]);
|
||||
const [clusterAutoConnect, setClusterAutoConnectState] = useState(false);
|
||||
@@ -1081,7 +1079,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
try { setDbSettings(await GetDatabaseSettings() as any); } catch {}
|
||||
try { setMysqlCfg(await GetMySQLSettings() as any); } catch {}
|
||||
try { setBackendStatus(await GetDBBackendStatus() as any); } catch {}
|
||||
try { setDataDir(await GetDataDir()); } catch {}
|
||||
try {
|
||||
const locs: any = await ListTQSLStationLocations();
|
||||
setStationLocations((locs ?? []).map((l: any) => l.name).filter(Boolean));
|
||||
@@ -3506,13 +3503,17 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
|
||||
function DatabasePanel() {
|
||||
async function refreshDb() { try { setDbSettings(await GetDatabaseSettings() as any); } catch {} }
|
||||
async function refreshBackend() { try { setBackendStatus(await GetDBBackendStatus() as any); } catch {} }
|
||||
// The chosen file is persisted (config.json) the moment it's picked; dbMsg
|
||||
// holds the pending path so the banner can offer a one-click restart to load
|
||||
// it (the DB pointer is only read at startup).
|
||||
async function openExisting() {
|
||||
try {
|
||||
const p = await PickOpenDatabase();
|
||||
if (!p) return;
|
||||
await OpenDatabase(p);
|
||||
await refreshDb();
|
||||
setDbMsg(`Database set to:\n${p}\nRestart OpsLog to apply.`);
|
||||
setDbMsg(p);
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
async function saveCopy() {
|
||||
@@ -3521,7 +3522,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
if (!p) return;
|
||||
await MoveDatabase(p);
|
||||
await refreshDb();
|
||||
setDbMsg(`A copy was saved to:\n${p}\nand selected. Restart OpsLog to apply.`);
|
||||
setDbMsg(p);
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
async function createNew() {
|
||||
@@ -3530,29 +3531,45 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
if (!p) return;
|
||||
await CreateDatabase(p);
|
||||
await refreshDb();
|
||||
setDbMsg(`New empty logbook created at:\n${p}\nand selected. Restart OpsLog to apply.`);
|
||||
setDbMsg(p);
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
async function resetDefault() {
|
||||
try {
|
||||
await ResetDatabaseToDefault();
|
||||
await refreshDb();
|
||||
setDbMsg('Database reset to the default location. Restart OpsLog to apply.');
|
||||
setDbMsg(dbSettings.default_path || '');
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
// Switching the logbook backend applies immediately (no restart): the local
|
||||
// SQLite file always stays the config store; only the QSO logbook moves.
|
||||
function useLocalLogbook() {
|
||||
SaveMySQLSettings({ ...mysqlCfg, enabled: false } as any)
|
||||
.then(async () => { setMysqlField({ enabled: false }); setRestartMsg(t('db.switchedSqlite')); await refreshBackend(); })
|
||||
.catch((e: any) => setErr(String(e?.message ?? e)));
|
||||
}
|
||||
function connectMysql() {
|
||||
SaveMySQLSettings(mysqlCfg as any)
|
||||
.then(async () => { setRestartMsg(t('db.switchedMysql')); await refreshBackend(); })
|
||||
.catch((e: any) => setErr(String(e?.message ?? e)));
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title={t('sec.database')}
|
||||
/>
|
||||
{/* Backend selector for the ACTIVE PROFILE's logbook. Each profile can
|
||||
target its own database; choosing here and Save switches the live
|
||||
logbook immediately (no restart). */}
|
||||
<SectionHeader title={t('sec.database')} />
|
||||
|
||||
{/* Logbook backend: local SQLite file (solo) or shared MySQL (multi-op).
|
||||
Switching is instant — no restart. Only changing the local SQLite
|
||||
FILE needs a restart (it also stores this operator's settings). */}
|
||||
<div className="grid grid-cols-[130px_1fr] gap-2 items-center max-w-2xl mb-1">
|
||||
<Label className="text-sm">Backend</Label>
|
||||
<Label className="text-sm">{t('db.backend')}</Label>
|
||||
<Select
|
||||
value={mysqlCfg.enabled ? 'mysql' : 'sqlite'}
|
||||
onValueChange={(v) => setMysqlField({ enabled: v === 'mysql' })}
|
||||
onValueChange={(v) => {
|
||||
const enabled = v === 'mysql';
|
||||
setMysqlField({ enabled });
|
||||
setRestartMsg('');
|
||||
if (!enabled) useLocalLogbook(); // switching to local applies at once
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-72"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -3561,50 +3578,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground max-w-2xl mb-3">
|
||||
{t('db.profileHint')}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground max-w-2xl mb-3">{t('db.profileHint')}</p>
|
||||
|
||||
{/* Save (always visible) applies the active profile's DB target live. */}
|
||||
<div className="max-w-2xl mb-4 flex items-center gap-3">
|
||||
<Button size="sm" className="h-8"
|
||||
onClick={() => {
|
||||
SaveMySQLSettings(mysqlCfg as any)
|
||||
.then(() => setRestartMsg(mysqlCfg.enabled
|
||||
? t('db.switchedMysql')
|
||||
: t('db.switchedSqlite')))
|
||||
.catch((e: any) => setErr(String(e?.message ?? e)));
|
||||
}}>
|
||||
{t('db.saveSwitch')}
|
||||
</Button>
|
||||
{restartMsg && <span className="text-[11px] text-success">{restartMsg}</span>}
|
||||
</div>
|
||||
|
||||
{/* Active-backend status: confirms what OpsLog actually opened at launch. */}
|
||||
{/* Compact active-backend confirmation / MySQL-fallback warning. */}
|
||||
{backendStatus && (
|
||||
<div className="max-w-2xl mb-4">
|
||||
{backendStatus.fallback ? (
|
||||
<div className="text-xs bg-warning-muted border border-warning-border text-warning-muted-foreground rounded-md px-3 py-2">
|
||||
backendStatus.fallback ? (
|
||||
<div className="max-w-2xl mb-4 text-xs bg-warning-muted border border-warning-border text-warning-muted-foreground rounded-md px-3 py-2">
|
||||
{t('db.fallback')}
|
||||
<div className="font-mono text-[10px] mt-1 break-all">{backendStatus.error}</div>
|
||||
</div>
|
||||
) : backendStatus.active === 'mysql' ? (
|
||||
<div className="text-xs bg-muted/40 border border-border rounded-md px-3 py-2">
|
||||
<strong>{t('db.logbook')}</strong> {t('db.mysqlConnected')}
|
||||
<span className="mx-1.5 text-muted-foreground">·</span>
|
||||
<strong>{t('db.config')}</strong> {t('db.localSqlite')}
|
||||
<div className="text-[10px] text-muted-foreground mt-1">
|
||||
{t('db.mysqlNote')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs bg-muted/40 border border-border rounded-md px-3 py-2">
|
||||
{t('db.activeBackend')} <strong className="uppercase">{backendStatus.active}</strong>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w-2xl mb-4 text-[11px] text-muted-foreground">
|
||||
{t('db.activeBackend')} <strong className="uppercase text-foreground">{backendStatus.active}</strong>
|
||||
{backendStatus.active === 'mysql' && <span> · {t('db.configLocal')}</span>}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* SQLite: local logbook file management */}
|
||||
{!mysqlCfg.enabled && (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<div className="space-y-1">
|
||||
@@ -3620,26 +3611,35 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button size="sm" onClick={createNew}><Plus className="size-3.5" /> {t('db.newDb')}</Button>
|
||||
<Button variant="outline" size="sm" onClick={openExisting}>{t('db.openExisting')}</Button>
|
||||
<Button variant="outline" size="sm" onClick={saveCopy}>{t('db.saveCopy')}</Button>
|
||||
<Button variant="outline" size="sm" onClick={openExisting}><FolderOpen className="size-3.5" /> {t('db.openExisting')}</Button>
|
||||
<Button variant="outline" size="sm" onClick={saveCopy}><Copy className="size-3.5" /> {t('db.saveCopy')}</Button>
|
||||
{dbSettings.is_custom && <Button variant="ghost" size="sm" onClick={resetDefault}>{t('db.resetDefault')}</Button>}
|
||||
</div>
|
||||
|
||||
{dbMsg && (
|
||||
<div className="text-xs bg-success-muted border border-success-border text-success-muted-foreground rounded-md px-3 py-2 flex items-center justify-between gap-3 whitespace-pre-line">
|
||||
<span>{dbMsg}</span>
|
||||
<Button size="sm" variant="destructive" className="shrink-0" onClick={() => QuitApp()}>{t('db.quitNow')}</Button>
|
||||
<div className="text-xs bg-success-muted border border-success-border text-success-muted-foreground rounded-md px-3 py-3 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Check className="size-4 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium">{t('db.savedRestart')}</div>
|
||||
<div className="font-mono text-[10px] mt-1 break-all opacity-90">{dbMsg}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pl-6">
|
||||
<Button size="sm" onClick={() => { RestartApp().catch((e: any) => setErr(String(e?.message ?? e))); }}>
|
||||
<Power className="size-3.5" /> {t('db.restartNow')}
|
||||
</Button>
|
||||
<span className="text-[10px] opacity-80">{t('db.restartHint')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shared MySQL database (multi-operator) */}
|
||||
{/* MySQL: shared logbook connection (multi-operator) */}
|
||||
{mysqlCfg.enabled && (
|
||||
<div className="space-y-3 max-w-2xl">
|
||||
<div className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{t('db.mysqlHint')}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground leading-relaxed">{t('db.mysqlHint')}</div>
|
||||
<div className="grid grid-cols-[130px_1fr] gap-2 items-center">
|
||||
<Label className="text-sm">{t('db.host')}</Label>
|
||||
<Input className="h-8" placeholder="192.168.1.10 or db.example.com" value={mysqlCfg.host} onChange={(e) => setMysqlField({ host: e.target.value })} />
|
||||
@@ -3652,28 +3652,18 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Label className="text-sm">{t('es.password')}</Label>
|
||||
<Input type="password" className="h-8" value={mysqlCfg.password} onChange={(e) => setMysqlField({ password: e.target.value })} />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Button variant="outline" size="sm" className="h-8"
|
||||
onClick={() => { setMysqlMsg(t('db.testing')); TestMySQLConnection(mysqlCfg as any).then(() => setMysqlMsg(t('db.connectedReady'))).catch((e: any) => setMysqlMsg(t('db.failed') + String(e?.message ?? e))); }}>
|
||||
{t('db.testCreate')}
|
||||
</Button>
|
||||
<Button size="sm" className="h-8" onClick={connectMysql}>{t('db.connectUse')}</Button>
|
||||
<span className="text-[11px] text-muted-foreground">{mysqlMsg}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data location */}
|
||||
<div className="border-t border-border/60 mt-6 pt-5 space-y-3 max-w-2xl">
|
||||
<div>
|
||||
<div className="text-sm font-medium">{t('db.dataLocation')}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('db.currentDataDir')}</Label>
|
||||
<div className="font-mono text-xs bg-muted/40 border border-border rounded-md px-3 py-2 break-all">
|
||||
{dataDir || '—'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{restartMsg && <div className="max-w-2xl mt-3 text-[11px] text-success">{restartMsg}</div>}
|
||||
|
||||
{/* Backup settings, merged into this Database section. */}
|
||||
<div className="border-t border-border/60 mt-6 pt-5">
|
||||
|
||||
@@ -149,6 +149,8 @@ const en: Dict = {
|
||||
// Database panel
|
||||
'db.optSqlite': 'SQLite — local file (solo)', 'db.optMysql': 'MySQL — shared server (multi-operator)', 'db.profileHint': 'This is the logbook for the active profile. Different profiles can point at different databases — switching profile switches the logbook.',
|
||||
'db.saveSwitch': 'Save & switch logbook', 'db.switchedMysql': 'Logbook switched to MySQL ✓', 'db.switchedSqlite': 'Logbook switched to local SQLite ✓',
|
||||
'db.backend': 'Backend', 'db.configLocal': 'settings stay in the local SQLite file', 'db.connectUse': 'Connect & use',
|
||||
'db.savedRestart': 'Saved. Restart OpsLog to open this logbook:', 'db.restartNow': 'Restart OpsLog', 'db.restartHint': '(reopens automatically)',
|
||||
'db.current': 'Current database', 'db.customLoc': '(custom location)', 'db.default': '(default)', 'db.defaultLabel': 'Default:',
|
||||
'db.newDb': 'New database…', 'db.openExisting': 'Open existing…', 'db.saveCopy': 'Save a copy & switch…', 'db.resetDefault': 'Reset to default', 'db.quitNow': 'Quit now',
|
||||
'db.host': 'Host', 'db.port': 'Port', 'db.database': 'Database', 'db.user': 'User', 'db.testCreate': 'Test & create database', 'db.testing': 'Testing…', 'db.connectedReady': 'Connected — database ready ✓', 'db.failed': 'Failed: ',
|
||||
@@ -334,6 +336,8 @@ const fr: Dict = {
|
||||
'prof.configId': 'ID de configuration', 'prof.description': 'Description', 'prof.new': 'Nouveau', 'prof.newTitle': 'Créer un nouveau profil vierge', 'prof.dupTitle': 'Cloner le profil sélectionné (garde tous ses champs)', 'prof.setActive': 'Activer', 'prof.setActiveTitle': 'Activer le profil sélectionné — les nouveaux QSO utiliseront ses champs MY_*', 'prof.deleteTitle': 'Supprimer le profil sélectionné', 'prof.cantDeleteLast': 'Impossible de supprimer le dernier profil', 'prof.activeSuffix': ' (actif)', 'prof.viewingNote': 'Tu consultes {name}. Le profil actif est {active} — ses valeurs sont inscrites sur les nouveaux QSO. Clique « Activer » pour basculer.',
|
||||
'db.optSqlite': 'SQLite — fichier local (solo)', 'db.optMysql': 'MySQL — serveur partagé (multi-opérateur)', 'db.profileHint': 'Ceci est le journal du profil actif. Des profils différents peuvent pointer vers des bases différentes — changer de profil change le journal.',
|
||||
'db.saveSwitch': 'Enregistrer & basculer le journal', 'db.switchedMysql': 'Journal basculé vers MySQL ✓', 'db.switchedSqlite': 'Journal basculé vers SQLite local ✓',
|
||||
'db.backend': 'Base de données', 'db.configLocal': 'les réglages restent dans le fichier SQLite local', 'db.connectUse': 'Connecter et utiliser',
|
||||
'db.savedRestart': 'Enregistré. Redémarrez OpsLog pour ouvrir cette base :', 'db.restartNow': 'Redémarrer OpsLog', 'db.restartHint': '(réouverture automatique)',
|
||||
'db.current': 'Base actuelle', 'db.customLoc': '(emplacement personnalisé)', 'db.default': '(par défaut)', 'db.defaultLabel': 'Par défaut :',
|
||||
'db.newDb': 'Nouvelle base…', 'db.openExisting': 'Ouvrir existante…', 'db.saveCopy': 'Enregistrer une copie & basculer…', 'db.resetDefault': 'Réinitialiser par défaut', 'db.quitNow': 'Quitter maintenant',
|
||||
'db.host': 'Hôte', 'db.port': 'Port', 'db.database': 'Base', 'db.user': 'Utilisateur', 'db.testCreate': 'Tester & créer la base', 'db.testing': 'Test…', 'db.connectedReady': 'Connecté — base prête ✓', 'db.failed': 'Échec : ',
|
||||
|
||||
Vendored
+2
@@ -552,6 +552,8 @@ export function ResetAwardDefs():Promise<Array<award.Def>>;
|
||||
|
||||
export function ResetDatabaseToDefault():Promise<void>;
|
||||
|
||||
export function RestartApp():Promise<void>;
|
||||
|
||||
export function RestartQSORecorder():Promise<void>;
|
||||
|
||||
export function RotatorGoTo(arg1:number,arg2:number):Promise<void>;
|
||||
|
||||
@@ -1066,6 +1066,10 @@ export function ResetDatabaseToDefault() {
|
||||
return window['go']['main']['App']['ResetDatabaseToDefault']();
|
||||
}
|
||||
|
||||
export function RestartApp() {
|
||||
return window['go']['main']['App']['RestartApp']();
|
||||
}
|
||||
|
||||
export function RestartQSORecorder() {
|
||||
return window['go']['main']['App']['RestartQSORecorder']();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user