Skip to content

Commit 55fa78b

Browse files
committed
feat: add manual update controls to settings
1 parent 96a8239 commit 55fa78b

7 files changed

Lines changed: 190 additions & 5 deletions

File tree

components/SettingsView.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1474,13 +1474,76 @@ requests"
14741474
const GeneralSettingsSection: React.FC<Pick<SectionProps, 'settings' | 'setCurrentSettings'>> = ({ settings, setCurrentSettings }) => {
14751475
const isOfflineRendererAvailable = typeof window !== 'undefined' && !!window.electronAPI?.renderPlantUML;
14761476
const offlineRendererMessage = 'Offline rendering requires the desktop application with a local Java runtime.';
1477+
const { addLog } = useLogger();
1478+
const [isManualCheckRunning, setIsManualCheckRunning] = useState(false);
1479+
const [manualCheckStatus, setManualCheckStatus] = useState<'idle' | 'success' | 'error'>('idle');
1480+
const [manualCheckMessage, setManualCheckMessage] = useState<string | null>(null);
1481+
const canManuallyCheckForUpdates = typeof window !== 'undefined' && !!window.electronAPI?.updaterCheckForUpdates;
1482+
1483+
const handleManualUpdateCheck = useCallback(async () => {
1484+
if (!window.electronAPI?.updaterCheckForUpdates) {
1485+
setManualCheckStatus('error');
1486+
setManualCheckMessage('Manual update checks are only available in the desktop application.');
1487+
return;
1488+
}
1489+
1490+
setIsManualCheckRunning(true);
1491+
setManualCheckStatus('idle');
1492+
setManualCheckMessage(null);
1493+
addLog('INFO', 'User action: Manual update check triggered.');
1494+
1495+
try {
1496+
const result = await window.electronAPI.updaterCheckForUpdates();
1497+
if (result?.success) {
1498+
if (result.updateAvailable) {
1499+
const label = result.version ?? result.releaseName ?? 'latest';
1500+
setManualCheckStatus('success');
1501+
setManualCheckMessage(`Update ${label} found. Downloading will begin automatically.`);
1502+
} else {
1503+
setManualCheckStatus('success');
1504+
setManualCheckMessage('You are running the latest version.');
1505+
}
1506+
} else {
1507+
setManualCheckStatus('error');
1508+
setManualCheckMessage(result?.error ?? 'Failed to check for updates.');
1509+
}
1510+
} catch (error) {
1511+
const message = error instanceof Error ? error.message : 'Failed to check for updates.';
1512+
setManualCheckStatus('error');
1513+
setManualCheckMessage(message);
1514+
} finally {
1515+
setIsManualCheckRunning(false);
1516+
}
1517+
}, [addLog]);
1518+
1519+
const effectiveManualCheckStatus = canManuallyCheckForUpdates ? manualCheckStatus : 'error';
1520+
const manualCheckMessageClass = effectiveManualCheckStatus === 'error'
1521+
? 'text-error'
1522+
: effectiveManualCheckStatus === 'success'
1523+
? 'text-success'
1524+
: 'text-text-secondary';
14771525
return (
14781526
<section className="pt-2 pb-6">
14791527
<h2 className="text-lg font-semibold text-text-main mb-4">General</h2>
14801528
<div className="space-y-6">
1481-
<SettingRow htmlFor="allowPrerelease" label="Receive Pre-releases" description="Get notified about new beta versions and test features early.">
1529+
<SettingRow htmlFor="allowPrerelease" label="Pre-release Updates" description="Allow DocForge to download and install beta releases when available.">
14821530
<ToggleSwitch id="allowPrerelease" checked={settings.allowPrerelease} onChange={(val) => setCurrentSettings(s => ({...s, allowPrerelease: val}))} />
14831531
</SettingRow>
1532+
<SettingRow htmlFor="autoCheckForUpdates" label="Automatic Update Checks" description="Check for new releases whenever DocForge starts.">
1533+
<ToggleSwitch id="autoCheckForUpdates" checked={settings.autoCheckForUpdates} onChange={(val) => setCurrentSettings(s => ({...s, autoCheckForUpdates: val}))} />
1534+
</SettingRow>
1535+
<SettingRow label="Check for Updates" description="Run an update check immediately.">
1536+
<div className="flex flex-col items-start md:items-end gap-2 w-full">
1537+
<Button variant="secondary" onClick={handleManualUpdateCheck} isLoading={isManualCheckRunning} disabled={isManualCheckRunning || !canManuallyCheckForUpdates}>
1538+
{isManualCheckRunning ? 'Checking…' : 'Check for Updates'}
1539+
</Button>
1540+
{(canManuallyCheckForUpdates ? manualCheckMessage : true) && (
1541+
<p className={`text-xs text-left md:text-right ${manualCheckMessageClass}`}>
1542+
{canManuallyCheckForUpdates ? manualCheckMessage : 'Manual update checks are only available in the desktop application.'}
1543+
</p>
1544+
)}
1545+
</div>
1546+
</SettingRow>
14841547
<SettingRow htmlFor="autoSaveLogs" label="Auto-save Logs" description="Automatically save all logs to a daily file on your computer for debugging.">
14851548
<ToggleSwitch id="autoSaveLogs" checked={settings.autoSaveLogs} onChange={(val) => setCurrentSettings(s => ({...s, autoSaveLogs: val}))} />
14861549
</SettingRow>

constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const DEFAULT_SETTINGS: Settings = {
2525
iconSet: 'heroicons',
2626
autoSaveLogs: false,
2727
allowPrerelease: false,
28+
autoCheckForUpdates: true,
2829
plantumlRendererMode: 'remote',
2930
uiScale: 100,
3031
documentTreeIndent: 16,

electron/database.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,24 @@ export const databaseService = {
472472
throw e;
473473
}
474474
},
475-
475+
476+
getSetting(key: string): unknown {
477+
try {
478+
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
479+
if (!row) {
480+
return undefined;
481+
}
482+
try {
483+
return JSON.parse(row.value);
484+
} catch {
485+
return row.value;
486+
}
487+
} catch (error) {
488+
console.error('DB Get Setting Error:', key, error);
489+
throw error;
490+
}
491+
},
492+
476493
run(sql: string, params: any[] = []): Database.RunResult {
477494
try {
478495
return db.prepare(sql).run(...(params || []));

electron/main.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,38 @@ log.catchErrors({
4646
console.log(`Log file will be written to: ${log.transports.file.getFile().path}`);
4747

4848
let mainWindow: BrowserWindow | null;
49+
let autoCheckEnabled = true;
50+
let pendingAutoUpdateCheck: NodeJS.Timeout | null = null;
51+
52+
const cancelScheduledAutoUpdateCheck = () => {
53+
if (pendingAutoUpdateCheck) {
54+
clearTimeout(pendingAutoUpdateCheck);
55+
pendingAutoUpdateCheck = null;
56+
}
57+
};
58+
59+
const scheduleAutoUpdateCheck = (delayMs = 3000) => {
60+
if (!autoCheckEnabled) {
61+
console.log('Automatic update checks are disabled; skipping schedule.');
62+
return;
63+
}
64+
65+
cancelScheduledAutoUpdateCheck();
66+
67+
pendingAutoUpdateCheck = setTimeout(async () => {
68+
pendingAutoUpdateCheck = null;
69+
if (!autoCheckEnabled) {
70+
console.log('Automatic update checks disabled before execution; skipping update check.');
71+
return;
72+
}
73+
74+
try {
75+
await autoUpdater.checkForUpdatesAndNotify();
76+
} catch (error) {
77+
console.error('Automatic update check failed:', error);
78+
}
79+
}, delayMs);
80+
};
4981

5082
const broadcastPythonEvent = (channel: string, payload: any) => {
5183
const targets = BrowserWindow.getAllWindows();
@@ -153,10 +185,25 @@ app.on('ready', () => {
153185
// The renderer process will detect the failure when it tries to communicate
154186
// via IPC and will display the fatal error screen. We still create the window.
155187
}
156-
188+
157189
createWindow();
158-
// Check for updates after window is created
159-
setTimeout(() => autoUpdater.checkForUpdatesAndNotify(), 3000);
190+
191+
try {
192+
const storedPreference = databaseService.getSetting('autoCheckForUpdates');
193+
if (typeof storedPreference === 'boolean') {
194+
autoCheckEnabled = storedPreference;
195+
} else if (typeof storedPreference !== 'undefined') {
196+
autoCheckEnabled = Boolean(storedPreference);
197+
}
198+
} catch (error) {
199+
console.error('Failed to read auto-update preference from settings:', error);
200+
}
201+
202+
if (autoCheckEnabled) {
203+
scheduleAutoUpdateCheck();
204+
} else {
205+
console.log('Automatic update checks are disabled via settings.');
206+
}
160207
});
161208

162209
app.on('window-all-closed', () => {
@@ -393,9 +440,38 @@ ipcMain.handle('app:get-log-path', () => log.transports.file.getFile().path);
393440
ipcMain.on('updater:set-allow-prerelease', (_, allow: boolean) => {
394441
autoUpdater.allowPrerelease = allow;
395442
});
443+
ipcMain.on('updater:set-auto-check-enabled', (_, enabled: boolean) => {
444+
autoCheckEnabled = enabled;
445+
if (enabled) {
446+
scheduleAutoUpdateCheck();
447+
} else {
448+
cancelScheduledAutoUpdateCheck();
449+
}
450+
});
396451
ipcMain.on('updater:quit-and-install', () => {
397452
autoUpdater.quitAndInstall();
398453
});
454+
ipcMain.handle('updater:check-now', async () => {
455+
try {
456+
const result = await autoUpdater.checkForUpdates();
457+
const updateInfo = result?.updateInfo;
458+
const version = updateInfo?.version ?? null;
459+
const releaseName = updateInfo?.releaseName ?? null;
460+
const currentVersion = app.getVersion();
461+
const updateAvailable = Boolean(version && version !== currentVersion);
462+
463+
return {
464+
success: true,
465+
updateAvailable,
466+
version,
467+
releaseName,
468+
};
469+
} catch (error) {
470+
const message = error instanceof Error ? error.message : String(error);
471+
console.error('Manual update check failed:', message);
472+
return { success: false, error: message };
473+
}
474+
});
399475

400476

401477
// Window Controls

electron/preload.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ type UpdateDownloadProgress = {
1313
bytesPerSecond: number;
1414
};
1515

16+
type ManualUpdateCheckResult = {
17+
success: boolean;
18+
updateAvailable?: boolean;
19+
version?: string | null;
20+
releaseName?: string | null;
21+
error?: string;
22+
};
23+
1624
contextBridge.exposeInMainWorld('electronAPI', {
1725
// --- Database ---
1826
dbQuery: (sql: string, params?: any[]) => ipcRenderer.invoke('db:query', sql, params),
@@ -44,6 +52,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
4452
getLogPath: () => ipcRenderer.invoke('app:get-log-path'),
4553
renderPlantUML: (diagram: string, format: 'svg' = 'svg') => ipcRenderer.invoke('plantuml:render-svg', diagram, format),
4654
updaterSetAllowPrerelease: (allow: boolean) => ipcRenderer.send('updater:set-allow-prerelease', allow),
55+
updaterSetAutoCheckEnabled: (enabled: boolean) => ipcRenderer.send('updater:set-auto-check-enabled', enabled),
56+
updaterCheckForUpdates: () => ipcRenderer.invoke('updater:check-now') as Promise<ManualUpdateCheckResult>,
4757
onUpdateAvailable: (callback: (info: UpdateAvailableInfo) => void) => {
4858
const handler = (_: IpcRendererEvent, info: UpdateAvailableInfo) => callback(info);
4959
ipcRenderer.on('update:available', handler);

hooks/useSettings.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ export const useSettings = () => {
6666
}
6767
}, [settings.allowPrerelease, loaded, addLog]);
6868

69+
useEffect(() => {
70+
if (loaded && isElectron && window.electronAPI?.updaterSetAutoCheckEnabled) {
71+
addLog('DEBUG', `Notifying main process: autoCheckForUpdates is ${settings.autoCheckForUpdates}`);
72+
window.electronAPI.updaterSetAutoCheckEnabled(settings.autoCheckForUpdates);
73+
}
74+
}, [settings.autoCheckForUpdates, loaded, addLog]);
75+
6976
const saveSettings = useCallback(async (newSettings: Settings) => {
7077
setSettings(newSettings);
7178
await repository.saveAllSettings(newSettings);

types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ declare global {
4040
format?: 'svg'
4141
) => Promise<{ success: boolean; svg?: string; error?: string; details?: string }>;
4242
updaterSetAllowPrerelease: (allow: boolean) => void;
43+
updaterSetAutoCheckEnabled?: (enabled: boolean) => void;
44+
updaterCheckForUpdates?: () => Promise<ManualUpdateCheckResult>;
4345
onUpdateAvailable?: (callback: (info: UpdateAvailableInfo) => void) => () => void;
4446
onUpdateDownloadProgress?: (callback: (progress: UpdateDownloadProgress) => void) => () => void;
4547
onUpdateDownloaded: (callback: (info: string | UpdateAvailableInfo) => void) => () => void;
@@ -94,6 +96,14 @@ export interface UpdateDownloadProgress {
9496
bytesPerSecond: number;
9597
}
9698

99+
export interface ManualUpdateCheckResult {
100+
success: boolean;
101+
updateAvailable?: boolean;
102+
version?: string | null;
103+
releaseName?: string | null;
104+
error?: string;
105+
}
106+
97107
export type NodeType = 'folder' | 'document';
98108
export type DocType = 'prompt' | 'source_code' | 'pdf' | 'image';
99109
export type ViewMode = 'edit' | 'preview' | 'split-vertical' | 'split-horizontal';
@@ -304,6 +314,7 @@ export interface Settings {
304314
iconSet: 'heroicons' | 'lucide' | 'feather' | 'tabler' | 'material';
305315
autoSaveLogs: boolean;
306316
allowPrerelease: boolean;
317+
autoCheckForUpdates: boolean;
307318
plantumlRendererMode: 'remote' | 'offline';
308319
uiScale: number;
309320
documentTreeIndent: number;

0 commit comments

Comments
 (0)