kv-clearnup/electron/main.ts
2026-02-02 08:33:46 +07:00

244 lines
6.8 KiB
TypeScript

import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron';
import fs from 'fs';
import path from 'path';
import { spawn, ChildProcess } from 'child_process';
import { scanDirectory, getDeepDiveSummary, getDiskUsage, findHeavyFolders } from './features/scanner.js';
import { startEnforcement } from './features/enforcer.js';
import { disableAutoUpdates, ignoreUpdate } from './features/updater.js';
import { clearCaches, emptyTrash, cleanupDocker, cleanupTmp, purgePath, cleanupXcode, cleanupTurnkey } from './features/cleaner.js';
// In CJS, __dirname is globally defined. We rely on esbuild providing it.
// To stop TypeScript from complaining about missing type in 'module' mode:
declare const __dirname: string;
declare const __filename: string;
let mainWindow: BrowserWindow | null = null;
let backendProcess: ChildProcess | null = null;
let tray: Tray | null = null;
const startBackend = () => {
if (process.env.NODE_ENV === 'development') {
console.log('Development mode: Backend should be running via start-go.sh');
return;
}
const backendPath = path.join(process.resourcesPath, 'backend');
console.log('Starting backend from:', backendPath);
try {
backendProcess = spawn(backendPath, [], {
stdio: 'inherit'
});
backendProcess.on('error', (err) => {
console.error('Failed to start backend:', err);
});
backendProcess.on('exit', (code, signal) => {
console.log(`Backend exited with code ${code} and signal ${signal}`);
});
} catch (error) {
console.error('Error spawning backend:', error);
}
};
function createTray() {
const iconPath = path.join(__dirname, '../dist/tray/tray-iconTemplate.png');
// Use Template image for automatic Light/Dark mode adjustment.
// Fallback to color if needed, but Template is best for menu bar.
// Check if dist/tray exists, if not try public/tray (dev mode)
let finalIconPath = iconPath;
if (!fs.existsSync(iconPath)) {
finalIconPath = path.join(__dirname, '../public/tray/tray-iconTemplate.png');
}
const image = nativeImage.createFromPath(finalIconPath);
tray = new Tray(image.resize({ width: 16, height: 16 }));
tray.setToolTip('Antigravity Cleaner');
updateTrayMenu('Initializing...');
}
let isDockVisible = true;
function updateTrayMenu(statusText: string) {
if (!tray) return;
const contextMenu = Menu.buildFromTemplate([
{ label: `Storage: ${statusText}`, enabled: false },
{ type: 'separator' },
{
label: 'Open Dashboard', click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
}
},
{
label: 'Free Up Storage', click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
// Optionally send IPC to trigger scan?
}
}
},
{ type: 'separator' },
{
label: 'Show Dock Icon',
type: 'checkbox',
checked: isDockVisible,
click: (menuItem) => {
isDockVisible = menuItem.checked;
if (isDockVisible) {
app.dock.show();
} else {
app.dock.hide();
}
}
},
{ type: 'separator' },
{ label: 'Quit', click: () => app.quit() }
]);
tray.setContextMenu(contextMenu);
// Do NOT reset Title if we are just updating the menu structure,
// but statusText drives the menu so we keep it.
tray.setTitle(statusText);
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
backgroundColor: '#FFFFFF', // Helps prevent white flash
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
nodeIntegration: true,
contextIsolation: true,
},
});
const isDev = process.env.NODE_ENV === 'development';
const port = process.env.PORT || 5173;
if (isDev) {
mainWindow.loadURL(`http://localhost:${port}`);
// mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
// IPC Handlers
ipcMain.handle('scan-directory', async (event, path) => {
return scanDirectory(path);
});
ipcMain.handle('deep-dive-scan', async () => {
return getDeepDiveSummary();
});
ipcMain.handle('get-disk-usage', async () => {
return getDiskUsage();
});
ipcMain.handle('deepest-scan', async (event, targetPath) => {
// Default to Documents if no path provided, or allow passing path
const target = targetPath || path.join(app.getPath('home'), 'Documents');
return findHeavyFolders(target);
});
ipcMain.handle('disable-updates', async () => {
return disableAutoUpdates();
});
ipcMain.handle('clean-system', async () => {
return clearCaches(); // and others
});
ipcMain.handle('cleanup-docker', async () => {
return cleanupDocker();
});
ipcMain.handle('cleanup-tmp', async () => {
return cleanupTmp();
});
ipcMain.handle('cleanup-xcode', async () => {
return cleanupXcode();
});
ipcMain.handle('cleanup-turnkey', async () => {
return cleanupTurnkey();
});
ipcMain.handle('purge-path', async (event, targetPath) => {
// Security check: ensure path is within user home or projects?
// For now, allow it but in production we need safeguards.
// Also import purgePath from cleaner.ts (need to update imports)
// const { purgePath } = await import('./features/cleaner.js'); // Already imported above
return purgePath(targetPath);
});
// Start background enforcer (example: monitoring home projects)
// startEnforcement('/Users/khoa.vo/Projects'); // TODO: Make configurable
// IPC for Tray
ipcMain.handle('update-tray-title', (event, title) => {
if (tray) {
tray.setTitle(title);
updateTrayMenu(title); // Update menu with new status too if needed
}
});
ipcMain.handle('get-app-icon', async (event, appPath) => {
try {
const icon = await app.getFileIcon(appPath, { size: 'normal' });
return icon.toDataURL();
} catch (e) {
console.error('Failed to get icon for:', appPath, e);
return '';
return '';
}
});
ipcMain.handle('update-tray-icon', (event, dataUrl) => {
if (tray && dataUrl) {
const image = nativeImage.createFromDataURL(dataUrl);
// Resize to tray standard (16x16 or 22x22 usually, let's keep it crisp)
tray.setImage(image.resize({ width: 22, height: 22 }));
}
});
createWindow();
createTray(); // Initialize Tray
startBackend();
});
app.on('will-quit', () => {
if (backendProcess) {
console.log('Killing backend process...');
backendProcess.kill();
backendProcess = null;
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});