288 lines
10 KiB
JavaScript
288 lines
10 KiB
JavaScript
const { app, shell, session, dialog, ipcMain, BrowserWindow } = require('electron');
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
app.enableSandbox();
|
|
|
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
|
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
|
|
app.quit();
|
|
}
|
|
|
|
// Reloading and dev tools shortcuts
|
|
const { isPackaged } = app;
|
|
const isDev = process.env.ELECTRON_DEBUG === "1" || !isPackaged;
|
|
if (isDev) {
|
|
require('electron-debug')({ showDevTools: false });
|
|
}
|
|
|
|
// @TODO: let user apply this setting somewhere in the UI (togglable)
|
|
// (Note: it would be better to use REG.EXE to apply the change, rather than a .reg file)
|
|
// This registry modification changes the right click > Edit option for images in Windows Explorer
|
|
const reg_contents = `Windows Registry Editor Version 5.00
|
|
|
|
[HKEY_CLASSES_ROOT\\SystemFileAssociations\\image\\shell\\edit\\command]
|
|
@="\\"${process.argv[0].replace(/\\/g, "\\\\")}\\" ${isPackaged ? "" : '\\".\\" '}\\"%1\\""
|
|
`; // oof that's a lot of escaping \\
|
|
//// \\\\
|
|
// /\ /\ /\ /\ /\ /\ /\ \\
|
|
// //\\ //\\ //\\ //\\ //\\ //\\ //\\ \\
|
|
// || || || || || || || \\
|
|
//\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\
|
|
const reg_file_path = path.join(
|
|
isPackaged ? path.dirname(process.argv[0]) : ".",
|
|
`set-jspaint${isPackaged ? "" : "-DEV-MODE"}-as-default-image-editor.reg`
|
|
);
|
|
if (process.platform == "win32" && isPackaged) {
|
|
fs.writeFile(reg_file_path, reg_contents, (err) => {
|
|
if (err) {
|
|
return console.error(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
// In case of XSS holes, don't give the page free reign over the filesystem!
|
|
// Only allow allow access to files explicitly opened by the user.
|
|
const allowed_file_paths = [];
|
|
|
|
let initial_file_path;
|
|
if (process.argv.length >= 2) {
|
|
// in production, "path/to/jspaint.exe" "maybe/a/file.png"
|
|
// in development, "path/to/electron.exe" "." "maybe/a/file.png"
|
|
const initial_file_path = process.argv[isPackaged ? 1 : 2];
|
|
allowed_file_paths.push(initial_file_path);
|
|
}
|
|
|
|
// Keep a global reference of the window object, if you don't, the window will
|
|
// be closed automatically when the JavaScript object is garbage collected.
|
|
// @TODO: It's been several electron versions. I doubt this is still necessary. (It was from a boilerplate.)
|
|
let mainWindow;
|
|
|
|
const createWindow = () => {
|
|
// Create the browser window.
|
|
mainWindow = new BrowserWindow({
|
|
useContentSize: true,
|
|
autoHideMenuBar: true, // it adds height for a native menu bar unless we hide it here
|
|
// setMenu(null) below is too late; it's already decided on the size by then
|
|
width: 800,
|
|
height: 600,
|
|
minWidth: 260,
|
|
minHeight: 360,
|
|
icon: path.join(__dirname, "../images/icons",
|
|
process.platform === "win32" ?
|
|
"jspaint.ico" :
|
|
process.platform === "darwin" ?
|
|
"jspaint.icns" :
|
|
"48x48.png"
|
|
),
|
|
title: "JS Paint",
|
|
webPreferences: {
|
|
preload: path.join(__dirname, "/electron-injected.js"),
|
|
contextIsolation: false,
|
|
},
|
|
});
|
|
|
|
// @TODO: maybe use the native menu for the "Modern" theme, or a "Native" theme
|
|
mainWindow.setMenu(null);
|
|
|
|
// and load the index.html of the app.
|
|
mainWindow.loadURL(`file://${__dirname}/../index.html`);
|
|
|
|
// Emitted when the window is closed.
|
|
mainWindow.on('closed', () => {
|
|
// Dereference the window object, usually you would store windows
|
|
// in an array if your app supports multi windows, this is the time
|
|
// when you should delete the corresponding element.
|
|
mainWindow = null;
|
|
});
|
|
|
|
// Emitted before the window is closed.
|
|
mainWindow.on('close', (event) => {
|
|
// Don't need to check mainWindow.isDocumentEdited(),
|
|
// because the (un)edited state is handled by the renderer process, in are_you_sure().
|
|
// Note: if the web contents are not responding, this will make the app harder to close.
|
|
// Similarly, if there's an error, the app will be harder to close (perhaps worse as it's less likely to show a Not Responding dialog).
|
|
// And this also prevents it from closing with Ctrl+C in the terminal, which is arguably a feature.
|
|
mainWindow.webContents.send('close-window-prompt');
|
|
event.preventDefault();
|
|
});
|
|
|
|
// Open links without target=_blank externally.
|
|
mainWindow.webContents.on('will-navigate', (e, url) => {
|
|
// check that the URL is not part of the app
|
|
if (!url.includes("file://")) {
|
|
e.preventDefault();
|
|
shell.openExternal(url);
|
|
}
|
|
});
|
|
// Open links with target=_blank externally.
|
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
// check that the URL is not part of the app
|
|
if (!url.includes("file://")) {
|
|
shell.openExternal(url);
|
|
}
|
|
return { action: "deny" };
|
|
});
|
|
|
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
callback({
|
|
responseHeaders: {
|
|
...details.responseHeaders,
|
|
// connect-src needs data: for loading from localStorage,
|
|
// and maybe blob: for loading from IndexedDB in the future.
|
|
// (It uses fetch().)
|
|
// Note: this should mirror the CSP in index.html, except maybe for firebase stuff.
|
|
"Content-Security-Policy": [`
|
|
default-src 'self';
|
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
|
img-src 'self' data: blob: http: https:;
|
|
font-src 'self' https://fonts.gstatic.com;
|
|
connect-src * data: blob:;
|
|
`],
|
|
}
|
|
})
|
|
});
|
|
|
|
ipcMain.on("get-env-info", (event) => {
|
|
event.returnValue = {
|
|
isDev,
|
|
isMacOS: process.platform === "darwin",
|
|
initialFilePath: initial_file_path,
|
|
};
|
|
});
|
|
ipcMain.on("set-represented-filename", (event, filePath) => {
|
|
if (allowed_file_paths.includes(filePath)) {
|
|
mainWindow.setRepresentedFilename(filePath);
|
|
}
|
|
});
|
|
ipcMain.on("set-document-edited", (event, isEdited) => {
|
|
mainWindow.setDocumentEdited(isEdited);
|
|
});
|
|
ipcMain.handle("show-save-dialog", async (event, options) => {
|
|
const { filePath, canceled } = await dialog.showSaveDialog(mainWindow, {
|
|
title: options.title,
|
|
// defaultPath: options.defaultPath,
|
|
defaultPath: options.defaultPath || path.basename(options.defaultFileName),
|
|
filters: options.filters,
|
|
});
|
|
const fileName = path.basename(filePath);
|
|
allowed_file_paths.push(filePath);
|
|
return { filePath, fileName, canceled };
|
|
});
|
|
ipcMain.handle("show-open-dialog", async (event, options) => {
|
|
const { filePaths, canceled } = await dialog.showOpenDialog(mainWindow, {
|
|
title: options.title,
|
|
defaultPath: options.defaultPath,
|
|
filters: options.filters,
|
|
properties: options.properties,
|
|
});
|
|
allowed_file_paths.push(...filePaths);
|
|
return { filePaths, canceled };
|
|
});
|
|
ipcMain.handle("write-file", async (event, file_path, data) => {
|
|
if (!allowed_file_paths.includes(file_path)) {
|
|
return { responseCode: "ACCESS_DENIED" };
|
|
}
|
|
// make sure data is an ArrayBuffer, so you can't use an options object for (unknown) evil reasons
|
|
if (data instanceof ArrayBuffer) {
|
|
try {
|
|
await fs.promises.writeFile(file_path, Buffer.from(data));
|
|
} catch (error) {
|
|
return { responseCode: "WRITE_FAILED", error };
|
|
}
|
|
return { responseCode: "SUCCESS" };
|
|
} else {
|
|
return { responseCode: "INVALID_DATA" };
|
|
}
|
|
});
|
|
ipcMain.handle("read-file", async (event, file_path) => {
|
|
if (!allowed_file_paths.includes(file_path)) {
|
|
return { responseCode: "ACCESS_DENIED" };
|
|
}
|
|
try {
|
|
const buffer = await fs.promises.readFile(file_path);
|
|
return { responseCode: "SUCCESS", data: new Uint8Array(buffer), fileName: path.basename(file_path) };
|
|
} catch (error) {
|
|
return { responseCode: "READ_FAILED", error };
|
|
}
|
|
});
|
|
ipcMain.handle("set-wallpaper", async (event, data) => {
|
|
const image_path = path.join(app.getPath("userData"), "bg.png"); // Note: used without escaping
|
|
if (!(data instanceof ArrayBuffer)) {
|
|
return { responseCode: "INVALID_DATA" };
|
|
}
|
|
data = new Uint8Array(data);
|
|
const png_magic_bytes = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
|
for (let i = 0; i < png_magic_bytes.length; i++) {
|
|
if (data[i] !== png_magic_bytes[i]) {
|
|
console.log("Found bytes:", data.slice(0, png_magic_bytes.length), "but expected:", png_magic_bytes);
|
|
return { responseCode: "INVALID_PNG_DATA" };
|
|
}
|
|
}
|
|
try {
|
|
await fs.promises.writeFile(image_path, Buffer.from(data));
|
|
} catch (error) {
|
|
return { responseCode: "WRITE_TEMP_PNG_FAILED", error };
|
|
}
|
|
|
|
// The wallpaper module actually has support for Xfce, but it's not general enough.
|
|
const bash_for_xfce = `xfconf-query -c xfce4-desktop -l | grep last-image | while read path; do xfconf-query -c xfce4-desktop -p $path -s '${image_path}'; done`;
|
|
const { lookpath } = require("lookpath");
|
|
if (await lookpath("xfconf-query") && await lookpath("grep")) {
|
|
const exec = require("util").promisify(require('child_process').exec);
|
|
try {
|
|
await exec(bash_for_xfce);
|
|
} catch (error) {
|
|
console.error("Error setting wallpaper for Xfce:", error);
|
|
return { responseCode: "XFCONF_FAILED", error };
|
|
}
|
|
return { responseCode: "SUCCESS" };
|
|
} else {
|
|
// Note: { scale: "center" } is only supported on macOS.
|
|
// I worked around this by providing an image with a transparent margin on other platforms,
|
|
// in setWallpaperCentered.
|
|
return new Promise((resolve, reject) => {
|
|
require("wallpaper").set(image_path, { scale: "center" }, error => {
|
|
if (error) {
|
|
resolve({ responseCode: "SET_WALLPAPER_FAILED", error });
|
|
} else {
|
|
resolve({ responseCode: "SUCCESS" });
|
|
}
|
|
});
|
|
});
|
|
// Newer promise-based wallpaper API that I can't import:
|
|
// try {
|
|
// await setWallpaper(image_path, { scale: "center" });
|
|
// } catch (error) {
|
|
// return { responseCode: "SET_WALLPAPER_FAILED", error };
|
|
// }
|
|
// return { responseCode: "SUCCESS" };
|
|
}
|
|
});
|
|
};
|
|
|
|
// This method will be called when Electron has finished
|
|
// initialization and is ready to create browser windows.
|
|
// Some APIs can only be used after this event occurs.
|
|
app.on('ready', createWindow);
|
|
|
|
// Quit when all windows are closed.
|
|
app.on('window-all-closed', () => {
|
|
// On OS X it is common for applications and their menu bar
|
|
// to stay active until the user quits explicitly with Cmd + Q
|
|
if (process.platform !== 'darwin') {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
// On OS X it's common to re-create a window in the app when the
|
|
// dock icon is clicked and there are no other windows open.
|
|
if (mainWindow === null) {
|
|
createWindow();
|
|
}
|
|
});
|
|
|
|
// In this file you can include the rest of your app's specific main process
|
|
// code. You can also put them in separate files and import them here.
|