From 34431af1dce09c6887d21160880b9f9b78451d92 Mon Sep 17 00:00:00 2001 From: Anirudh Sevugan Date: Sun, 17 Aug 2025 17:39:28 -0500 Subject: [PATCH] refactor main.js --- simpliplay/main.js | 613 ++++++++++++++++++--------------------------- 1 file changed, 237 insertions(+), 376 deletions(-) diff --git a/simpliplay/main.js b/simpliplay/main.js index 27ddcdc..be5392f 100644 --- a/simpliplay/main.js +++ b/simpliplay/main.js @@ -1,88 +1,45 @@ -const { app, BrowserWindow, Menu, MenuItem, shell, dialog, globalShortcut } = require('electron'); +const { app, BrowserWindow, Menu, MenuItem, shell, dialog, globalShortcut, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const { pathToFileURL } = require("url"); -const { checkForUpdate } = require('./updateChecker'); -let gpuAccel = ""; -let didRegisterShortcuts = false; -let version = "2.0.4.4" - -if (process.platform === 'darwin') { - if (process.argv.includes('--use-gl')) { - app.commandLine.appendSwitch('disable-features', 'Metal'); - app.commandLine.appendSwitch('use-gl', 'desktop'); - } -} - -// random change just to make sure that snapcraft releases fixed version on new channel to delete arm64 versions +// Main application window let mainWindow; + +// State management +let didRegisterShortcuts = false; +const loadedAddons = new Map(); +let gpuAccelStatus = "enabled"; +const appVersion = "2.0.4.4"; + +// Security and Platform Configuration +const isDarwin = process.platform === 'darwin'; +const isSingleInstance = app.requestSingleInstanceLock(); +const snapshotsDir = path.join(os.homedir(), 'simpliplay-snapshots'); + +// Initialize IPC communication +ipcMain.handle('get-gpu-status', () => gpuAccelStatus); + if (process.argv.includes('--disable-gpu')) { app.disableHardwareAcceleration(); - gpuAccel = "disabled"; + gpuAccelStatus = "disabled"; } -// Handle file opening from Finder or File Explorer -app.on('open-file', (event, filePath) => { - event.preventDefault(); - openFile(filePath); -}); +if (isDarwin && process.argv.includes('--use-gl')) { + app.commandLine.appendSwitch('disable-features', 'Metal'); + app.commandLine.appendSwitch('use-gl', 'desktop'); +} -const openFile = (filePath) => { - app.whenReady().then(() => { - const fileURL = pathToFileURL(filePath).href; +// --- Window Management - if (mainWindow) { - if (mainWindow.webContents.isLoading()) { - mainWindow.webContents.once("did-finish-load", () => { - mainWindow.webContents.send("play-media", fileURL); - }); - } else { - mainWindow.webContents.send("play-media", fileURL); - } - } else { - createWindow(() => { - mainWindow.webContents.send("play-media", fileURL); - }); - } - }); -}; - - -const takeSnapshot = async () => { - if (!mainWindow) return; - - try { - const image = await mainWindow.webContents.capturePage(); - const png = image.toPNG(); - const snapshotsDir = path.join(os.homedir(), 'simpliplay-snapshots'); - fs.mkdirSync(snapshotsDir, { recursive: true }); - const filePath = path.join(snapshotsDir, `snapshot-${Date.now()}.png`); - fs.writeFileSync(filePath, png); - - const { response } = await dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Snapshot Saved', - message: `Snapshot saved to:\n${filePath}`, - buttons: ['OK', 'Open File'], - defaultId: 0, - }); - - if (response === 1) shell.openPath(filePath); - } catch (error) { - dialog.showErrorBox("Snapshot Error", `Failed to capture snapshot: ${error.message}`); - } -}; - -const createWindow = (onReadyCallback) => { - if (!app.isReady()) { - app.whenReady().then(() => createWindow(onReadyCallback)); +// Factory function to create a new browser window +const createMainWindow = () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.focus(); return; } - if (mainWindow) mainWindow.close(); - mainWindow = new BrowserWindow({ width: 1920, height: 1080, @@ -90,54 +47,190 @@ const createWindow = (onReadyCallback) => { preload: path.join(__dirname, "preload.js"), contextIsolation: true, enableRemoteModule: false, - nodeIntegration: false, // Keep this false for security + nodeIntegration: false, sandbox: true, }, }); mainWindow.loadFile("index.html"); - if (process.platform === 'darwin') { - if (process.argv.includes('--use-gl')) { - mainWindow.webContents.on('did-finish-load', () => { - mainWindow.webContents.executeJavaScript("navigator.userAgent").then(ua => { - console.log("User Agent:", ua); - }); + mainWindow.on("focus", setupShortcuts); + mainWindow.on("blur", unregisterShortcuts); + mainWindow.on("closed", () => { + mainWindow = null; + unregisterShortcuts(); + }); - mainWindow.webContents.executeJavaScript("chrome.loadTimes ? chrome.loadTimes() : {}").then(loadTimes => { - console.log("GPU Info (legacy):", loadTimes); - }); + mainWindow.webContents.once("did-finish-load", () => { + if (gpuAccelStatus === "disabled") { + dialog.showMessageBox(mainWindow, { + type: 'warning', + title: 'Warning!', + message: "Disabling GPU acceleration greatly decreases performance and is not recommended.", }); } - } - - mainWindow.once("ready-to-show", () => { - - if (gpuAccel === "disabled") { - dialog.showMessageBox(mainWindow, { - type: 'warning', - buttons: ['Ok'], - defaultId: 0, - title: 'Warning!', - message: "Disabling GPU acceleration greatly decreases performance and is not recommended, but if you're curious, I don't wanna stop you.", - }); - } - - if (onReadyCallback) onReadyCallback(); }); + setupMenu(); setupContextMenu(); }; +// --- File Handling -// Set up context menu (prevents errors if `mainWindow` is undefined) -const setupContextMenu = () => { +const handleOpenFile = (filePath) => { + const fileURL = pathToFileURL(filePath).href; + + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("play-media", fileURL); + } else { + createMainWindow(); + app.once('ready', () => { + mainWindow.webContents.once('did-finish-load', () => { + mainWindow.webContents.send("play-media", fileURL); + }); + }); + } +}; + +const isValidFileArg = (arg) => { + if (!arg || arg.startsWith('-') || arg.includes('electron')) return false; + const resolvedPath = path.resolve(arg); + if (!fs.existsSync(resolvedPath)) return false; + + const badExtensions = ['.exe', '.bat', '.cmd', '.sh', '.msi', '.com', '.vbs', '.ps1', '.jar', '.scr']; + const ext = path.extname(resolvedPath).toLowerCase(); + return !badExtensions.includes(ext); +}; + +// --- Features and Utilities + +const takeSnapshot = async () => { if (!mainWindow) return; - const contextMenu = new Menu(); - contextMenu.append(new MenuItem({ label: 'Take a Snapshot', click: takeSnapshot })); - contextMenu.append(new MenuItem({ type: 'separator' })); - contextMenu.append(new MenuItem({ label: 'Inspect', click: () => mainWindow.webContents.openDevTools() })); + try { + const image = await mainWindow.webContents.capturePage(); + const png = image.toPNG(); + + fs.mkdirSync(snapshotsDir, { recursive: true }); + const filePath = path.join(snapshotsDir, `snapshot-${Date.now()}.png`); + fs.writeFileSync(filePath, png); + + const result = await dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Snapshot Saved', + message: `Snapshot saved to:\n${filePath}`, + buttons: ['OK', 'Open Folder'], + defaultId: 0, + }); + + if (result.response === 1) { + shell.openPath(snapshotsDir); + } + } catch (error) { + dialog.showErrorBox("Snapshot Error", `Failed to capture snapshot: ${error.message}`); + } +}; + +// --- Menu and Shortcuts + +const setupMenu = () => { + const isMac = process.platform === 'darwin'; + const template = [ + ...(isMac ? [{ + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { label: 'Check for Updates', accelerator: 'CmdOrCtrl+Shift+U', click: () => checkForUpdate(appVersion) }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }] : []), + { + label: 'File', + submenu: [ + { label: 'Take a Snapshot', accelerator: 'CmdOrCtrl+Shift+S', click: takeSnapshot }, + { type: 'separator' }, + isMac ? { role: 'close' } : { role: 'quit' } + ] + }, + { + label: 'Add-ons', + submenu: [ + { + label: 'Load Add-on...', + accelerator: 'CmdOrCtrl+Shift+A', + click: async () => { + const result = await dialog.showOpenDialog(mainWindow, { + title: 'Load Add-on', + filters: [{ name: 'JavaScript Files', extensions: ['js'] }], // Changed from .simpliplay to .js + properties: ['openFile'], + }); + + if (!result.canceled && result.filePaths.length > 0) { + const filePath = result.filePaths[0]; + const fileName = path.basename(filePath); + + if (loadedAddons.has(filePath)) { + dialog.showErrorBox('Could not load addon', `An add-on named "${fileName}" has already been loaded.`); + return; + } + + const addonMenuItem = { + label: fileName, + type: 'checkbox', + checked: true, + click: (menuItem) => { + if (menuItem.checked) { + mainWindow.webContents.send('load-addon', pathToFileURL(filePath).href); + } else { + mainWindow.webContents.send('unload-addon', pathToFileURL(filePath).href); + } + } + }; + + Menu.getApplicationMenu().items.find(item => item.label === 'Add-ons').submenu.append(addonMenuItem); + loadedAddons.set(filePath, addonMenuItem); + mainWindow.webContents.send('load-addon', pathToFileURL(filePath).href); + } + } + }, + { type: 'separator' }, + ], + }, + { + label: 'Help', + submenu: [ + { label: 'Source Code', click: () => shell.openExternal('https://github.com/A-Star100/simpliplay-desktop') }, + { label: 'Website', click: () => shell.openExternal('https://simpliplay.netlify.app') }, + { label: 'Help Center', click: () => shell.openExternal('https://simpliplay.netlify.app/help') }, + { type: 'separator' }, + ...(isMac ? [] : [{ + label: 'Check for Updates', + accelerator: 'CmdOrCtrl+Shift+U', + click: () => checkForUpdate(appVersion) + }]), + ] + }, + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +}; + +const setupContextMenu = () => { + if (!mainWindow) return; + const contextMenu = Menu.buildFromTemplate([ + { label: 'Take a Snapshot', click: takeSnapshot }, + { type: 'separator' }, + { label: 'Inspect', click: () => mainWindow.webContents.openDevTools() } + ]); mainWindow.webContents.on('context-menu', (event) => { event.preventDefault(); @@ -145,186 +238,12 @@ const setupContextMenu = () => { }); }; - -// Set up application menu -const setupMenu = () => { - const menu = Menu.getApplicationMenu(); - if (!menu) return; - - const fileMenu = menu.items.find(item => item.label === 'File'); - if (fileMenu && !fileMenu.submenu.items.some(item => item.label === 'Take a Snapshot')) { - fileMenu.submenu.append(new MenuItem({ label: 'Take a Snapshot', accelerator: 'CommandOrControl+Shift+S', click: takeSnapshot })); - } - - const appMenu = menu.items.find(item => item.label === 'SimpliPlay'); - -if (appMenu && !appMenu.submenu.items.some(item => item.label === 'Check for Updates')) { - const submenu = appMenu.submenu; - const separatorIndex = submenu.items.findIndex(item => item.type === 'separator'); - - const updateMenuItem = new MenuItem({ - label: 'Check for Updates', - accelerator: 'CommandOrControl+Shift+U', - click: () => checkForUpdate(version) - }); - - if (separatorIndex === -1) { - // No separator found — just append - submenu.append(updateMenuItem); - } else { - // Insert right before the separator - submenu.insert(separatorIndex, updateMenuItem); - } -} - - - const helpMenu = menu.items.find(item => item.label === 'Help'); - if (helpMenu) { - const addMenuItem = (label, url) => { - if (!helpMenu.submenu.items.some(item => item.label === label)) { - helpMenu.submenu.append(new MenuItem({ label, click: () => shell.openExternal(url) })); - } - }; - - addMenuItem('Source Code', 'https://github.com/A-Star100/simpliplay-desktop'); - addMenuItem('Website', 'https://simpliplay.netlify.app'); - addMenuItem('Help Center', 'https://simpliplay.netlify.app/help'); - - // Check for Updates - if (!helpMenu.submenu.items.some(item => item.label === 'Check for Updates')) { - helpMenu.submenu.append( - new MenuItem({ - label: 'Check for Updates', - accelerator: 'CommandOrControl+Shift+U', - click: () => checkForUpdate(version) - }) - ); - } - - if (!helpMenu.submenu.items.some(item => item.label === 'Quit')) { - helpMenu.submenu.append(new MenuItem({ type: 'separator' })); - helpMenu.submenu.append(new MenuItem({ label: 'Quit', click: () => app.quit() })); - } - } - -const loadedAddons = new Map(); // key: addon filepath, value: MenuItem - -const newMenuItems = menu ? [...menu.items] : []; - -let addonsMenu; - -// Check if Add-ons menu already exists -const existingAddonsMenuItem = newMenuItems.find(item => item.label === 'Add-ons'); - -if (existingAddonsMenuItem) { - addonsMenu = existingAddonsMenuItem.submenu; -} else { - addonsMenu = new Menu(); - - // "Load Add-on" menu item - addonsMenu.append(new MenuItem({ - label: 'Load Add-on', - accelerator: 'CommandOrControl+Shift+A', - click: async () => { - const result = await dialog.showOpenDialog(mainWindow, { - title: 'Load Add-on', - filters: [{ name: 'JavaScript Files', extensions: ['simpliplay'] }], - properties: ['openFile'], - }); - - if (!result.canceled && result.filePaths.length > 0) { - const filePath = result.filePaths[0]; - const fileName = path.basename(filePath); - const fileURL = pathToFileURL(filePath).href; - - // Check if an addon with the same filename is already loaded - const alreadyLoaded = [...loadedAddons.keys()].some( - loadedPath => path.basename(loadedPath) === fileName - ); - - if (alreadyLoaded) { - await dialog.showMessageBox(mainWindow, { - type: 'error', - title: 'Could not load addon', - message: `An add-on named "${fileName}" has already been loaded before.`, - buttons: ['OK'] - }); - return; - } - - if (!loadedAddons.has(filePath)) { - mainWindow.webContents.send('load-addon', fileURL); - - const addonMenuItem = new MenuItem({ - label: fileName, - type: 'checkbox', - checked: true, - click: (menuItem) => { - if (menuItem.checked) { - fs.access(filePath, (err) => { - if (!err) { - mainWindow.webContents.send('load-addon', fileURL); - } else { - dialog.showMessageBox(mainWindow, { - type: 'error', - title: 'Could not load addon', - message: `The add-on "${fileName}" could not be found or doesn't exist anymore.`, - buttons: ['OK'] - }).then(() => { - // Delay unchecking to ensure dialog closes first - setTimeout(() => { - menuItem.checked = false; - }, 100); - }); - } - }); - - } else { - mainWindow.webContents.send('unload-addon', fileURL); - } - } - }); - - if (!addonsMenu.items.some(item => item.type === 'separator')) { - addonsMenu.append(new MenuItem({ type: 'separator' })); - } - - addonsMenu.append(addonMenuItem); - loadedAddons.set(filePath, addonMenuItem); - - // Rebuild the menu after adding the new addon item - Menu.setApplicationMenu(Menu.buildFromTemplate(newMenuItems)); - } - } - } - })); - - // Add the Add-ons menu only once here: - newMenuItems.push(new MenuItem({ label: 'Add-ons', submenu: addonsMenu })); - - // Set the application menu after adding Add-ons menu - Menu.setApplicationMenu(Menu.buildFromTemplate(newMenuItems)); -} - - -// Re-apply the full menu if you add newMenuItems outside of the if above -//Menu.setApplicationMenu(Menu.buildFromTemplate(newMenuItems)); - - - //Menu.setApplicationMenu(menu); -}; - const setupShortcuts = () => { -if (didRegisterShortcuts === false) { + if (didRegisterShortcuts) return; globalShortcut.register('CommandOrControl+Q', () => { - const focusedWindow = BrowserWindow.getFocusedWindow(); // Get the currently focused window - - if (!focusedWindow) return; // Do nothing if no window is focused - - dialog.showMessageBox(focusedWindow, { + dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { type: 'question', buttons: ['Cancel', 'Quit'], - defaultId: 1, title: 'Quit?', message: 'Are you sure you want to quit SimpliPlay?', }).then(({ response }) => { @@ -332,110 +251,52 @@ if (didRegisterShortcuts === false) { }); }); - globalShortcut.register('CommandOrControl+Shift+S', () => { - - const focusedWindow = BrowserWindow.getFocusedWindow(); // Get the currently focused window - - if (!focusedWindow) return; // Do nothing if no window is focused - - takeSnapshot(); - }); - - globalShortcut.register('CommandOrControl+S', () => { - - const focusedWindow = BrowserWindow.getFocusedWindow(); // Get the currently focused window - - if (!focusedWindow) return; // Do nothing if no window is focused - - takeSnapshot(); - }); - - // globalShortcut.register('CommandOrControl+Shift+S', takeSnapshot); + globalShortcut.register('CommandOrControl+Shift+S', takeSnapshot); + globalShortcut.register('CommandOrControl+S', takeSnapshot); didRegisterShortcuts = true; -} }; -function unregisterShortcuts() { +const unregisterShortcuts = () => { + globalShortcut.unregisterAll(); didRegisterShortcuts = false; - globalShortcut.unregisterAll(); - console.log("Shortcuts unregistered"); -} +}; -app.whenReady().then(() => { - createWindow(); - setupMenu(); +// --- Application Lifecycle - - mainWindow?.on("focus", () => { - if (!didRegisterShortcuts) setupShortcuts(); - }); - - mainWindow?.on("blur", unregisterShortcuts); - - // Store but delay opening - const args = process.argv.slice(2); - const fileArg = args.find(isValidFileArg); - - if (fileArg) { - app.whenReady().then(() => { - if (mainWindow) openFileSafely(fileArg); - }); - } - - app.on("open-file", (event, filePath) => { - event.preventDefault(); - openFileSafely(filePath); - }); - - if (["win32", "linux"].includes(process.platform)) { - if (!app.requestSingleInstanceLock()) { - app.quit(); - } else { - app.on("second-instance", (event, argv) => { - const fileArg = argv.find(isValidFileArg); - if (fileArg) openFileSafely(fileArg); - }); - } - } -}); - -let hasOpenedFile = false; - -function openFileSafely(filePath) { - if (!hasOpenedFile) { - hasOpenedFile = true; - - const absPath = path.resolve(filePath); // ensure absolute path - - if (mainWindow?.webContents) { - const winFileURL = pathToFileURL(filePath).href; // ✅ Convert and encode file path - mainWindow.webContents.send("play-media", winFileURL); - } - - setTimeout(() => (hasOpenedFile = false), 1000); - } -} - -function isValidFileArg(arg) { - if (!arg || arg.startsWith('-') || arg.includes('electron')) return false; - - const resolvedPath = path.resolve(arg); - if (!fs.existsSync(resolvedPath)) return false; - - // Reject known executable/script extensions - const badExtensions = ['.exe', '.bat', '.cmd', '.sh', '.msi', '.com', '.vbs', '.ps1', '.jar', '.scr']; - const ext = path.extname(resolvedPath).toLowerCase(); - - return !badExtensions.includes(ext); -} - -app.on("window-all-closed", () => { - globalShortcut.unregisterAll(); +if (!isSingleInstance && !isDarwin) { app.quit(); - /* once bug fixed replace above with: - if (process.platform !== 'darwin') app.quit() */ -}); +} else { + app.on('second-instance', (event, argv, workingDirectory) => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + const fileArg = argv.find(isValidFileArg); + if (fileArg) handleOpenFile(fileArg); + } + }); -app.on("will-quit", () => { - globalShortcut.unregisterAll(); -}); + app.on('open-file', (event, filePath) => { + event.preventDefault(); + handleOpenFile(filePath); + }); + + app.on('ready', () => { + createMainWindow(); + const fileArg = process.argv.slice(1).find(isValidFileArg); + if (fileArg) handleOpenFile(fileArg); + }); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createMainWindow(); + } + }); + + app.on("window-all-closed", () => { + if (!isDarwin) app.quit(); + }); + + app.on("will-quit", () => { + unregisterShortcuts(); + }); +}