From 43a8933c367ac8e575c9ed6a3df50e33eba37208 Mon Sep 17 00:00:00 2001 From: Anirudh Sevugan Date: Sun, 17 Aug 2025 18:01:20 -0500 Subject: [PATCH] Add files via upload --- simpliplay/src/constants.js | 12 +++ simpliplay/src/fileHandler.js | 110 +++++++++++++++++++ simpliplay/src/menuManager.js | 182 ++++++++++++++++++++++++++++++++ simpliplay/src/shortcuts.js | 46 ++++++++ simpliplay/src/updateChecker.js | 83 +++++++++++++++ simpliplay/src/windowManager.js | 63 +++++++++++ 6 files changed, 496 insertions(+) create mode 100644 simpliplay/src/constants.js create mode 100644 simpliplay/src/fileHandler.js create mode 100644 simpliplay/src/menuManager.js create mode 100644 simpliplay/src/shortcuts.js create mode 100644 simpliplay/src/updateChecker.js create mode 100644 simpliplay/src/windowManager.js diff --git a/simpliplay/src/constants.js b/simpliplay/src/constants.js new file mode 100644 index 0000000..5a83302 --- /dev/null +++ b/simpliplay/src/constants.js @@ -0,0 +1,12 @@ +// src/constants.js +const path = require('path'); +const os = require('os'); + +module.exports = { + APP_CONSTANTS: { + VERSION: '2.0.4.4', + GPU_ACCEL: 'enabled', + SNAPSHOTS_DIR: path.join(os.homedir(), 'simpliplay-snapshots'), + BAD_FILE_EXTENSIONS: ['.exe', '.bat', '.cmd', '.sh', '.msi', '.com', '.vbs', '.ps1', '.jar', '.scr'] + } +}; \ No newline at end of file diff --git a/simpliplay/src/fileHandler.js b/simpliplay/src/fileHandler.js new file mode 100644 index 0000000..825922c --- /dev/null +++ b/simpliplay/src/fileHandler.js @@ -0,0 +1,110 @@ +// src/fileHandler.js +const { dialog, shell, BrowserWindow } = require('electron'); +const fs = require('fs'); +const path = require('path'); +const { pathToFileURL } = require('url'); +const { APP_CONSTANTS } = require('./constants'); + +let hasOpenedFile = false; + +const takeSnapshot = async () => { + const mainWindow = BrowserWindow.getFocusedWindow(); + if (!mainWindow) return; + + try { + const image = await mainWindow.webContents.capturePage(); + const png = image.toPNG(); + + fs.mkdirSync(APP_CONSTANTS.SNAPSHOTS_DIR, { recursive: true }); + const filePath = path.join(APP_CONSTANTS.SNAPSHOTS_DIR, `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 openFile = (filePath) => { + const mainWindow = BrowserWindow.getFocusedWindow(); + if (!mainWindow) return; + + const fileURL = pathToFileURL(filePath).href; + + if (mainWindow.webContents.isLoading()) { + mainWindow.webContents.once("did-finish-load", () => { + mainWindow.webContents.send("play-media", fileURL); + }); + } else { + mainWindow.webContents.send("play-media", fileURL); + } +}; + +const openFileSafely = (filePath) => { + if (hasOpenedFile) return; + hasOpenedFile = true; + + const absPath = path.resolve(filePath); + if (isValidFileArg(absPath)) { + const winFileURL = pathToFileURL(absPath).href; + const mainWindow = BrowserWindow.getFocusedWindow(); + if (mainWindow?.webContents) { + mainWindow.webContents.send("play-media", winFileURL); + } + } + + setTimeout(() => { hasOpenedFile = false; }, 1000); +}; + +const isValidFileArg = (arg) => { + if (!arg || arg.startsWith('-') || arg.includes('electron')) return false; + + const resolvedPath = path.resolve(arg); + if (!fs.existsSync(resolvedPath)) return false; + + const ext = path.extname(resolvedPath).toLowerCase(); + return !APP_CONSTANTS.BAD_FILE_EXTENSIONS.includes(ext); +}; + +const handleFileOpen = () => { + const args = process.argv.slice(2); + const fileArg = args.find(isValidFileArg); + + if (fileArg) { + app.whenReady().then(() => { + 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); + }); + } + } +}; + +module.exports = { + takeSnapshot, + openFile, + openFileSafely, + isValidFileArg, + handleFileOpen +}; \ No newline at end of file diff --git a/simpliplay/src/menuManager.js b/simpliplay/src/menuManager.js new file mode 100644 index 0000000..4716cf9 --- /dev/null +++ b/simpliplay/src/menuManager.js @@ -0,0 +1,182 @@ +// src/menuManager.js +const { Menu, MenuItem, shell, dialog } = require('electron'); +const path = require('path'); +const { pathToFileURL } = require('url'); +const { getMainWindow } = require('./windowManager'); +const { checkForUpdate } = require('./updateChecker'); +const { APP_CONSTANTS } = require('./constants'); +const { takeSnapshot } = require('./fileHandler'); + +const loadedAddons = new Map(); + +const setupMenu = () => { + const template = [ + { + label: 'File', + submenu: [ + { + label: 'Take a Snapshot', + accelerator: 'CommandOrControl+Shift+S', + click: takeSnapshot + } + ] + }, + { + label: 'Add-ons', + submenu: [ + { + label: 'Load Add-on', + accelerator: 'CommandOrControl+Shift+A', + click: handleLoadAddon + }, + { 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' }, + { + label: 'Check for Updates', + accelerator: 'CommandOrControl+Shift+U', + click: () => checkForUpdate(APP_CONSTANTS.VERSION) + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => app.quit() + } + ] + } + ]; + + if (process.platform === 'darwin') { + template.unshift({ + label: 'SimpliPlay', + submenu: [ + { + label: 'Check for Updates', + accelerator: 'CommandOrControl+Shift+U', + click: () => checkForUpdate(APP_CONSTANTS.VERSION) + } + ] + }); + } + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); + return menu; +}; + +const setupContextMenu = (window) => { + 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: () => window.webContents.openDevTools() + })); + + window.webContents.on('context-menu', (event) => { + event.preventDefault(); + contextMenu.popup({ window }); + }); +}; + +const handleLoadAddon = async () => { + const mainWindow = getMainWindow(); + if (!mainWindow) return; + + 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) return; + + const filePath = result.filePaths[0]; + const fileName = path.basename(filePath); + const fileURL = pathToFileURL(filePath).href; + + if ([...loadedAddons.keys()].some(p => path.basename(p) === fileName)) { + await dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Could not load addon', + message: `An add-on named "${fileName}" has already been loaded.`, + buttons: ['OK'] + }); + return; + } + + if (!loadedAddons.has(filePath)) { + mainWindow.webContents.send('load-addon', fileURL); + addAddonToMenu(filePath, fileName, fileURL); + } +}; + +const addAddonToMenu = (filePath, fileName, fileURL) => { + const menu = Menu.getApplicationMenu(); + const addonsMenu = menu.items.find(item => item.label === 'Add-ons')?.submenu; + if (!addonsMenu) return; + + const addonItem = new MenuItem({ + label: fileName, + type: 'checkbox', + checked: true, + click: createAddonClickHandler(filePath, fileName, fileURL) + }); + + addonsMenu.append(addonItem); + loadedAddons.set(filePath, addonItem); +}; + +const createAddonClickHandler = (filePath, fileName, fileURL) => { + return async (menuItem) => { + const mainWindow = getMainWindow(); + if (!mainWindow) return; + + if (menuItem.checked) { + fs.access(filePath, (err) => { + if (err) { + handleAddonError(mainWindow, fileName); + menuItem.checked = false; + return; + } + mainWindow.webContents.send('load-addon', fileURL); + }); + } else { + mainWindow.webContents.send('unload-addon', fileURL); + } + }; +}; + +const handleAddonError = async (window, fileName) => { + await dialog.showMessageBox(window, { + type: 'error', + title: 'Could not load addon', + message: `The add-on "${fileName}" could not be found or doesn't exist anymore.`, + buttons: ['OK'] + }); +}; + +module.exports = { + setupMenu, + setupContextMenu, + loadedAddons +}; \ No newline at end of file diff --git a/simpliplay/src/shortcuts.js b/simpliplay/src/shortcuts.js new file mode 100644 index 0000000..ccbbdf0 --- /dev/null +++ b/simpliplay/src/shortcuts.js @@ -0,0 +1,46 @@ +// src/shortcuts.js +const { globalShortcut, dialog, app } = require('electron'); +const { getMainWindow } = require('./windowManager'); +const { takeSnapshot } = './fileHandler'; + +let didRegisterShortcuts = false; + +const setupShortcuts = () => { + if (didRegisterShortcuts) return; + + // Quit confirmation + globalShortcut.register('CommandOrControl+Q', () => { + const window = getMainWindow(); + if (!window) return; + + dialog.showMessageBox(window, { + type: 'question', + buttons: ['Cancel', 'Quit'], + defaultId: 1, + title: 'Quit?', + message: 'Are you sure you want to quit SimpliPlay?', + }).then(({ response }) => { + if (response === 1) app.quit(); + }); + }); + + // Snapshot shortcuts + ['CommandOrControl+Shift+S', 'CommandOrControl+S'].forEach(accelerator => { + globalShortcut.register(accelerator, () => { + const window = getMainWindow(); + if (window) takeSnapshot(); + }); + }); + + didRegisterShortcuts = true; +}; + +const unregisterShortcuts = () => { + didRegisterShortcuts = false; + globalShortcut.unregisterAll(); +}; + +module.exports = { + setupShortcuts, + unregisterShortcuts +}; \ No newline at end of file diff --git a/simpliplay/src/updateChecker.js b/simpliplay/src/updateChecker.js new file mode 100644 index 0000000..5b6119c --- /dev/null +++ b/simpliplay/src/updateChecker.js @@ -0,0 +1,83 @@ +const https = require('https'); +const { URL } = require('url'); +const { dialog, shell } = require('electron'); + +const latestReleaseUrl = 'https://github.com/A-Star100/simpliplay-desktop/releases/latest/'; + +function fetchRedirectLocation(url) { + return new Promise((resolve, reject) => { + https.get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + resolve(res.headers.location); + } else { + reject(new Error(`Expected redirect but got status code: ${res.statusCode}`)); + } + }).on('error', reject); + }); +} + +function normalizeVersion(version) { + return version.trim().replace(/[^\d\.]/g, ''); +} + +function compareVersions(v1, v2) { + const a = v1.split('.').map(Number); + const b = v2.split('.').map(Number); + const len = Math.max(a.length, b.length); + + for (let i = 0; i < len; i++) { + const num1 = a[i] || 0; + const num2 = b[i] || 0; + if (num1 > num2) return 1; + if (num1 < num2) return -1; + } + return 0; +} + +/** + * Checks for update and shows native dialog if update is available. + * @param {string} currentVersion + */ +async function checkForUpdate(currentVersion) { + try { + const redirectUrl = await fetchRedirectLocation(latestReleaseUrl); + + const urlObj = new URL(redirectUrl); + const parts = urlObj.pathname.split('/'); + + const releaseTag = parts[parts.length - 1]; + const versionMatch = releaseTag.match(/release-([\d\.]+)/); + if (!versionMatch) { + throw new Error(`Could not parse version from release tag: ${releaseTag}`); + } + const latestVersion = normalizeVersion(versionMatch[1]); + + const cmp = compareVersions(latestVersion, currentVersion); + + if (cmp > 0) { + const result = dialog.showMessageBoxSync({ + type: 'info', + buttons: ['Download', 'Later'], + defaultId: 0, + cancelId: 1, + title: 'Update Available', + message: `A new version (${latestVersion}) is available. Would you like to download it?`, + }); + + if (result === 0) { + shell.openExternal("https://simpliplay.netlify.app/#download-options"); + } + } else { + dialog.showMessageBoxSync({ + type: 'info', + buttons: ['OK'], + title: "You're up to date!", + message: `You are using the latest version (${currentVersion}).`, + }); + } + } catch (err) { + dialog.showErrorBox('Could not check for update.', err.message); + } +} + +module.exports = { checkForUpdate }; diff --git a/simpliplay/src/windowManager.js b/simpliplay/src/windowManager.js new file mode 100644 index 0000000..ca079b8 --- /dev/null +++ b/simpliplay/src/windowManager.js @@ -0,0 +1,63 @@ +// src/windowManager.js +const { BrowserWindow, Menu, dialog } = require('electron'); +const path = require('path'); +const { setupContextMenu } = require('./menuManager'); +const { APP_CONSTANTS } = require('./constants'); + +let mainWindow; + +const createWindow = (onReadyCallback) => { + if (!app.isReady()) { + app.whenReady().then(() => createWindow(onReadyCallback)); + return; + } + + if (mainWindow) mainWindow.close(); + + mainWindow = new BrowserWindow({ + width: 1920, + height: 1080, + webPreferences: { + preload: path.join(__dirname, '../preload.js'), + contextIsolation: true, + enableRemoteModule: false, + nodeIntegration: false, + sandbox: true, + }, + }); + + mainWindow.loadFile('index.html'); + setupWindowHandlers(onReadyCallback); + return mainWindow; +}; + +const setupWindowHandlers = (onReadyCallback) => { + if (!mainWindow) return; + + mainWindow.once('ready-to-show', () => { + if (APP_CONSTANTS.GPU_ACCEL === 'disabled') { + showGpuWarning(); + } + if (onReadyCallback) onReadyCallback(); + }); + + setupContextMenu(mainWindow); +}; + +const showGpuWarning = () => { + 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.", + }); +}; + +const getMainWindow = () => mainWindow; + +module.exports = { + createWindow, + setupWindowHandlers, + getMainWindow +}; \ No newline at end of file