const { app, BrowserWindow, BrowserView, globalShortcut, ipcMain, Tray, Menu, protocol, session } = require("electron"); const electronremote = require("@electron/remote/main"); //const asar = require('@electron/asar'); const windowStateKeeper = require("electron-window-state"); const { setupTitlebar, attachTitlebarToWindow } = require("./titlebar/main"); const openAboutWindow = require("./about-window/src/index").default; const badge = require('./badge'); const contextMenu = require('./context-menu'); const autoUpdater = require('./utils/auto-update'); //const loadCRX = require('./utils/loadCRX'); const log4js = require("log4js"); const path = require("path"); const fs = require("fs"); const os = require("os"); require('v8-compile-cache'); const packageJson = require(path.join(__dirname, '..', '..', 'package.json')); // isUpdaing: global.isUpdating = false; // App Info: global.appInfo = { name: app.getName(), version: app.getVersion(), license: packageJson.license, deeplink: 'bsky' } // Paths: global.paths = { app_root: app.getAppPath(), app: path.join(app.getAppPath(), 'src'), data: os.platform() === 'win32' ? path.join(os.homedir(), 'AppData', 'Roaming', global.appInfo.name) : os.platform() === 'darwin' ? path.join(os.homedir(), 'Library', 'Application Support', global.appInfo.name) : path.join(os.homedir(), '.config', global.appInfo.name), home: os.homedir(), temp: path.join(os.tmpdir(), global.appInfo.name), }; global.paths.updateDir = path.join(global.paths.data, 'update'); // URLs: global.urls = { main: 'https://bsky.app' }; // Settings urls: global.settings = { general: `${global.urls.main}/settings` }; global.settings.account = `${global.settings.general}/account`; global.settings.appearance = `${global.settings.general}/appearance`; global.settings.privacy = `${global.settings.general}/privacy-and-security`; // Badge options: const badgeOptions = { fontColor: '#FFFFFF', // The font color font: '62px Microsoft Yahei', // The font and its size. You shouldn't have to change this at all color: '#FF0000', // The background color radius: 48, // The radius for the badge circle. You shouldn't have to change this at all useSystemAccentTheme: true, // Use the system accent color for the badge updateBadgeEvent: 'ui:badgeCount', // The IPC event name to listen on badgeDescription: 'Unread Notifications', // The badge description invokeType: 'send', // The IPC event type max: 9, // The maximum integer allowed for the badge. Anything above this will have "+" added to the end of it. fit: false, // Useful for multi-digit numbers. For single digits keep this set to false additionalFunc: (count) => { // An additional function to run whenever the IPC event fires. It has a count parameter which is the number that the badge was set to. //console.log(`Received ${count} new notifications!`); }, }; /* Logging */ const logFileName = 'BSKY-DESKTOP'; const logFile = path.join(global.paths.data, `${logFileName}.log`); log4js.configure({ appenders: { stdout: { type: "stdout" }, bskydesktop: { type: "file", filename: `${logFile}`, backups: 5, } }, categories: { default: { appenders: ["stdout", "bskydesktop"], level: "debug" } } }); const logger = log4js.getLogger("bskydesktop"); logger.level = fs.existsSync(path.join(global.paths.data, '.dev')) || fs.existsSync(path.join(global.paths.data, '.debug')) ? "debug" : "info"; // if logfile already exists, rename it unles the lock file is present if (fs.existsSync(logFile) && !fs.existsSync(path.join(global.paths.data, 'lockfile'))) { const stats = fs.statSync(logFile); const mtime = new Date(stats.mtime); const today = new Date(); // If the log file is from a different day or the secconds is more than 5, rename it if (mtime.getDate() !== today.getDate() || mtime.getSeconds() + 5 < today.getSeconds()) { fs.renameSync(logFile, path.join(global.paths.data, `${logFileName}.${mtime.toISOString().split('T')[0]}.log`)); }; }; logger.log("Starting Bsky Desktop"); // Create data directory if it does not exist: if (!fs.existsSync(global.paths.data)) { logger.info("Creating Data Directory"); fs.mkdirSync(global.paths.data, { recursive: true }); }; // Create temp directory if it does not exist: if (!fs.existsSync(global.paths.temp)) { logger.info("Creating Temp Directory"); fs.mkdirSync(global.paths.temp, { recursive: true }); }; // Create update directory if it does not exist: if (!fs.existsSync(global.paths.updateDir)) { logger.info("Creating Update Directory"); fs.mkdirSync(global.paths.updateDir, { recursive: true }); }; // improve performance on linux? if (process.platform !== "win32" && process.platform !== "darwin") { logger.log("Disabling Hardware Acceleration and Transparent Visuals"); app.commandLine.appendSwitch("disable-transparent-visuals"); app.commandLine.appendSwitch("disable-gpu"); app.disableHardwareAcceleration(); } // setup the titlebar main process: setupTitlebar(); // Disable reload and F5 if not in dev mode: if (process.env.NODE_ENV !== 'development') { app.on('browser-window-focus', function () { globalShortcut.register("CommandOrControl+R", () => { //console.log("CommandOrControl+R is pressed: Shortcut Disabled"); }); globalShortcut.register("F5", () => { //console.log("F5 is pressed: Shortcut Disabled"); }); }); app.on('browser-window-blur', function () { globalShortcut.unregister('CommandOrControl+R'); globalShortcut.unregister('F5'); }); }; // create main window function createWindow() { logger.log("Creating windowStateKeeper"); const mainWindowState = windowStateKeeper({ defaultWidth: 1340, defaultHeight: 800, fullScreen: false, maximize: true, }); logger.log("Creating splash screen"); const splash = (global.splash = new BrowserWindow({ width: 400, height: 400, frame: false, show: false, icon: path.join(global.paths.app, 'ui', 'images', 'logo.png'), alwaysOnTop: true, skipTaskbar: true, webPreferences: { nodeIntegration: true, contextIsolation: false } })); splash.loadFile(path.join(global.paths.app, 'ui', 'splash.html')); logger.log("Creating Main Window"); const mainWindow = (global.mainWindow = new BrowserWindow({ x: mainWindowState.x, y: mainWindowState.y, width: mainWindowState.width, height: mainWindowState.height, minWidth: 800, minHeight: 600, frame: false, show: false, icon: path.join(global.paths.app, 'ui', 'images', 'logo.png'), titleBarStyle: 'hidden', titleBarOverlay: true, webPreferences: { nodeIntegration: true, contextIsolation: true, preload: path.join(global.paths.app, 'ui', 'preload-titlebar.js'), } })); mainWindowState.manage(mainWindow); mainWindow.loadFile(path.join(global.paths.app, 'ui', 'titlebar.html')); mainWindow.hide(); const PageView = (global.PageView = new BrowserView({ webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(global.paths.app, 'ui', 'preload.js'), session: global.session, }, })); mainWindow.setBrowserView(PageView); PageView.webContents.loadURL(global.urls.main); PageView.setBounds({ x: 0, y: 30, width: mainWindow.getBounds().width, height: mainWindow.getBounds().height - 30, }); mainWindow.on("resize", () => { PageView.setBounds({ x: 0, y: 30, width: mainWindow.getBounds().width, height: mainWindow.getBounds().height - 30, }); }); // Context Menu: contextMenu({ labels: { showSaveImage: 'Download Image', showSaveVideo: 'Download Video', showSaveAudio: 'Download Audio', showCopyLink: 'Copy Link', showCopyImage: 'Copy Image', showInspectElement: 'Inspect Element' }, showSelectAll: false, showSaveImage: true, showSaveVideo: true, showSaveAudio: true, showCopyLink: true, showCopyImage: false, showInspectElement: !app.isPackaged, window: PageView }); // Badge count: (use mainWindow as that shows the badge on the taskbar) new badge(mainWindow, badgeOptions); logger.log("Main Window Created, Showing splashscreen"); splash.show(); //mainWindow.show(); logger.log("Attaching Titlebar to Main Window"); attachTitlebarToWindow(mainWindow); // DevTools: //mainWindow.webContents.openDevTools(); //PageView.webContents.openDevTools(); //splash.webContents.openDevTools(); logger.log("Initializing @electron/remote"); electronremote.initialize(); electronremote.enable(mainWindow.webContents); electronremote.enable(PageView.webContents); // PageView Events: PageView.webContents.on('did-finish-load', () => { if (!global.isUpdating) { // Show the main window mainWindow.show(); // Hide the splash screen splash.destroy(); }; }); PageView.webContents.setWindowOpenHandler(({ url }) => { new BrowserWindow({ show: true, autoHideMenuBar: true }).loadURL(url); return { action: 'deny' }; }); // Log PageView navigations: /*PageView.webContents.on('will-navigate', (event, url) => { logger.log(`Navigating to: ${url}`); }); PageView.webContents.on('did-navigate-in-page', (event, url) => { logger.log(`Navigated to: ${url}`); });*/ }; function showAboutWindow() { openAboutWindow({ icon_path: path.join(global.paths.app, 'ui', 'images', 'bsky-logo.svg'), package_json_dir: global.paths.app_root, product_name: global.appInfo.name, //open_devtools: process.env.NODE_ENV !== 'production', use_version_info: [ ['Application Version', `${global.appInfo.version}`], ], license: `MIT, GPL-2.0, GPL-3.0, ${global.appInfo.license}`, }); }; function createTray() { logger.log("Creating Tray"); const tray = new Tray(path.join(global.paths.app, 'ui', 'images', 'logo.png')); tray.setToolTip('Bsky Desktop'); tray.setContextMenu(Menu.buildFromTemplate([ { label: global.appInfo.name, enabled: false }, { type: 'separator' }, { label: 'About', click() { showAboutWindow(); } }, { label: 'Quit', role: 'quit', click() { app.quit(); } } ])); tray.on('click', () => { if (mainWindow.isVisible()) { mainWindow.hide(); } else { mainWindow.show(); } }); }; // Handle deeplinks: function handleDeeplink(commandLine) { logger.debug(commandLine); let uri; try { // Extract the last element in the array uri = commandLine.pop(); if (uri.startsWith(`${global.appInfo.deeplink}://`)) { logger.debug(`[DEEPLINK] Found URI: ${uri}`); uri = uri.split('/'); } else { uri = ["none"]; } } catch (error) { uri = ["none"]; } logger.debug(`[DEEPLINK] Parsing URI: ${uri.join('/')}`); switch (uri[2]) { case "about": logger.log("[DEEPLINK] Show About Window"); showAboutWindow(); break; case "settings": switch (uri[3]) { case "general": logger.log("[DEEPLINK] Open General Settings"); global.PageView.webContents.send('ui:openSettings', 'general'); break; case "account": logger.log("[DEEPLINK] Open Account Settings"); global.PageView.webContents.send('ui:openSettings', 'account'); break; case "appearance": logger.log("[DEEPLINK] Open Appearance Settings"); global.PageView.webContents.send('ui:openSettings', 'appearance'); break; case "privacy": logger.log("[DEEPLINK] Open Privacy Settings"); global.PageView.webContents.send('ui:openSettings', 'privacy-and-security'); break; default: logger.warn("[DEEPLINK] Unknown settings command"); break; }; break; case "notiftest": global.PageView.webContents.send('ui:notif', { title: 'Updater', message: 'Update downloaded', options: { position: 'topRight', timeout: 5000, layout: 2, color: 'blue' } }); break; default: if (uri[0] !== 'none') { logger.warn("[DEEPLINK] Unknown command"); } break; }; }; // Hanle ui: protocol, protocol.registerSchemesAsPrivileged([ { scheme: 'ui', privileges: { secure: true, supportFetchAPI: true, bypassCSP: true } } ]); // Main App Events: app.whenReady().then(() => { logger.log("App Ready, Ensuring singleInstanceLock and registering deeplink"); const gotTheLock = app.requestSingleInstanceLock(); if (gotTheLock) { logger.log("SingleInstanceLock Acquired, Registering deeplink"); if (process.defaultApp) { if (process.argv.length >= 2) { app.setAsDefaultProtocolClient(global.appInfo.deeplink, process.execPath, [path.resolve(process.argv[1])]) } } else { app.setAsDefaultProtocolClient(global.appInfo.deeplink) }; if (!process.defaultApp && process.argv.length >= 2) { logger.log("Handling deeplink from commandline"); handleDeeplink(process.argv); }; app.on('second-instance', (event, commandLine, workingDirectory) => { logger.log("Second Instance Detected, handling"); handleDeeplink(commandLine); /*if (global.mainWindow) { if (global.mainWindow.isMinimized()) global.mainWindow.restore(); global.mainWindow.focus(); };*/ }); // Create persistent session for the app: const ses = session.fromPath(path.join(global.paths.data, 'session'), { cache: true, partition: 'persist:bsky', allowRunningInsecureContent: false, contextIsolation: true, enableRemoteModule: true, nodeIntegration: false, sandbox: true, spellcheck: true, webSecurity: true, worldSafeExecuteJavaScript: true, preload: path.join(global.paths.app, 'ui', 'preload.js'), }); // Set UserAgent: ses.setUserAgent(`Mozilla/5.0 bsky-desktop/${global.appInfo.version} (Electron:${process.versions.electron};) Chrome:${process.versions.chrome};`); // Handle ui: protocol, ses.protocol.handle('ui', (req) => { // Log the incoming request URL for debugging //console.log('Request URL:', req.url); // Construct the correct file path const pathToMedia = path.join(__dirname, '..', 'ui', req.url.substring(5)); //console.log('Path to media:', pathToMedia); // Log the resolved path // Determine MIME type based on the file extension const mimeType = req.url.endsWith('.css') ? 'text/css' : req.url.endsWith('.js') ? 'text/javascript' : req.url.endsWith('.png') ? 'image/png' : req.url.endsWith('.svg') ? 'image/svg+xml' : req.url.endsWith('.html') ? 'text/html' : 'application/octet-stream'; // Default binary mime type try { // Attempt to read the file synchronously const media = fs.readFileSync(pathToMedia); // Log success and return the response //console.log('File found and served successfully'); return new Response(media, { // Pass the Buffer (binary data) as the body status: 200, headers: { 'Content-Type': mimeType, 'Access-Control-Allow-Origin': '*' } }); } catch (error) { // Log the error if file is not found console.error('Error reading file:', error); return new Response('File not found', { // Send plain text error if file not found status: 404, headers: { 'Content-Type': 'text/plain' } }); } }); // Set session: global.session = ses; // Initialize the updater: logger.log("Initializing Updater"); autoUpdater(); // Handle ipc for render: ipcMain.on('close-app', (event, arg) => { app.quit(); }); createWindow(); createTray(); } else { logger.log("Failed to get singleInstanceLock, Quitting"); app.quit(); }; }); app.on('open-url', (event, url) => { event.preventDefault(); handleDeeplink([url]); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); }; }); app.on('will-quit', () => { globalShortcut.unregisterAll(); }); app.on('before-quit', () => { logger.log('[Before Quit] Shutting down logger and quitting'); log4js.shutdown(); });