244 lines
6.8 KiB
TypeScript
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();
|
|
}
|
|
});
|