Files
bsky-desktop/src/app/main.js
2024-12-20 02:29:02 -08:00

519 lines
16 KiB
JavaScript

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();
});