heynote/electron/main/index.ts

382 lines
12 KiB
TypeScript
Raw Normal View History

import { app, BrowserWindow, Tray, shell, ipcMain, Menu, nativeTheme, globalShortcut, nativeImage, screen } from 'electron'
2023-01-12 18:55:55 +01:00
import { release } from 'node:os'
import { join } from 'node:path'
import fs from "fs"
2023-03-06 01:30:37 +01:00
import { menu, getTrayMenu } from './menu'
import { WINDOW_CLOSE_EVENT, SETTINGS_CHANGE_EVENT } from '../constants';
import CONFIG from "../config"
import { isDev, isLinux, isMac, isWindows } from '../detect-platform';
2023-12-06 13:31:47 +01:00
import { initializeAutoUpdate, checkForUpdates } from './auto-update';
import { fixElectronCors } from './cors';
import { loadBuffer, contentSaved } from './buffer';
2023-03-06 01:30:37 +01:00
2023-01-12 18:55:55 +01:00
// The built directory structure
//
// ├─┬ dist-electron
// │ ├─┬ main
// │ │ └── index.js > Electron-Main
// │ └─┬ preload
// │ └── index.js > Preload-Scripts
// ├─┬ dist
// │ └── index.html > Electron-Renderer
//
process.env.DIST_ELECTRON = join(__dirname, '..')
process.env.DIST = join(process.env.DIST_ELECTRON, '../dist')
process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL
2023-01-15 10:53:09 +01:00
? join(process.env.DIST_ELECTRON, '../public')
: process.env.DIST
2023-01-12 18:55:55 +01:00
// Disable GPU Acceleration for Windows 7
if (release().startsWith('6.1')) app.disableHardwareAcceleration()
// Set application name for Windows 10+ notifications
if (isWindows) app.setAppUserModelId(app.getName())
2023-01-12 18:55:55 +01:00
if (!process.env.VITE_DEV_SERVER_URL && !app.requestSingleInstanceLock()) {
2023-01-15 10:53:09 +01:00
app.quit()
process.exit(0)
2023-01-12 18:55:55 +01:00
}
// Set custom application menu
Menu.setApplicationMenu(menu)
2023-01-12 18:55:55 +01:00
// Remove electron security warnings
// This warning only shows in development mode
// Read more on https://www.electronjs.org/docs/latest/tutorial/security
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
export let win: BrowserWindow | null = null
let tray: Tray | null = null;
2023-01-12 18:55:55 +01:00
// Here, you can also use other preload
const preload = join(__dirname, '../preload/index.js')
const url = process.env.VITE_DEV_SERVER_URL
const indexHtml = join(process.env.DIST, 'index.html')
2023-01-29 12:49:54 +01:00
let currentKeymap = CONFIG.get("settings.keymap")
2023-01-19 00:20:50 +01:00
2023-12-06 13:31:47 +01:00
// if this version is a beta version, set the release channel to beta
const isBetaVersion = app.getVersion().includes("beta")
if (isBetaVersion) {
CONFIG.set("settings.allowBetaVersions", true)
2023-12-06 13:31:47 +01:00
}
let forceQuit = false
export function setForceQuit() {
forceQuit = true
}
export function quit() {
setForceQuit()
app.quit()
}
2023-01-12 18:55:55 +01:00
async function createWindow() {
// read any stored window settings from config, or use defaults
let windowConfig = {
width: CONFIG.get("windowConfig.width", 900) as number,
height: CONFIG.get("windowConfig.height", 680) as number,
isMaximized: CONFIG.get("windowConfig.isMaximized", false) as boolean,
isFullScreen: CONFIG.get("windowConfig.isFullScreen", false) as boolean,
x: CONFIG.get("windowConfig.x"),
y: CONFIG.get("windowConfig.y"),
}
// windowConfig.x and windowConfig.y will be undefined when config file is missing, e.g. first time run
if (windowConfig.x !== undefined && windowConfig.y !== undefined) {
// check if window is outside of screen, or too large
const area = screen.getDisplayMatching({
x: windowConfig.x,
y: windowConfig.y,
width: windowConfig.width,
height: windowConfig.height,
}).workArea
if (windowConfig.width > area.width) {
windowConfig.width = area.width
}
if (windowConfig.height > area.height) {
windowConfig.height = area.height
}
if (windowConfig.x + windowConfig.width > area.width || windowConfig.y + windowConfig.height > area.height) {
// window is outside of screen, reset position
windowConfig.x = undefined
windowConfig.y = undefined
}
}
win = new BrowserWindow(Object.assign({
title: 'heynote',
icon: join(process.env.PUBLIC, 'favicon.ico'),
backgroundColor: nativeTheme.shouldUseDarkColors ? '#262B37' : '#FFFFFF',
2023-01-15 10:53:09 +01:00
//titleBarStyle: 'customButtonsOnHover',
autoHideMenuBar: true,
2023-01-15 10:53:09 +01:00
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: true,
2023-01-15 10:53:09 +01:00
},
}, windowConfig))
2023-01-19 02:47:57 +01:00
// maximize window if it was maximized last time
if (windowConfig.isMaximized) {
win.maximize()
}
if (windowConfig.isFullScreen) {
win.setFullScreen(true)
}
2023-01-15 10:53:09 +01:00
2023-01-19 00:20:50 +01:00
win.on("close", (event) => {
if (!forceQuit && CONFIG.get("settings.showInMenu")) {
event.preventDefault()
win.hide()
return
}
2023-01-19 00:20:50 +01:00
// Prevent the window from closing, and send a message to the renderer which will in turn
// send a message to the main process to save the current buffer and close the window.
if (!contentSaved) {
event.preventDefault()
win?.webContents.send(WINDOW_CLOSE_EVENT)
} else {
// save window config
Object.assign(windowConfig, {
isMaximized: win.isMaximized(),
isFullScreen: win.isFullScreen(),
}, win.getNormalBounds())
CONFIG.set("windowConfig", windowConfig)
2023-01-19 00:20:50 +01:00
}
})
win.on("hide", () => {
if (isWindows && CONFIG.get("settings.showInMenu")) {
win.setSkipTaskbar(true)
}
})
win.on("show", () => {
if (isWindows && CONFIG.get("settings.showInMenu")) {
win.setSkipTaskbar(false)
}
})
2023-03-09 09:46:21 +01:00
nativeTheme.themeSource = CONFIG.get("theme")
2023-01-15 10:53:09 +01:00
if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298
win.loadURL(url + '?dev=1')
2023-01-15 10:53:09 +01:00
// Open devTool if the app is not packaged
//win.webContents.openDevTools()
} else {
win.loadFile(indexHtml)
//win.webContents.openDevTools()
}
Add key bindings for inserting new blocks at the end/top of the buffer, as well as before the current block (#85) * Add functionality to insert new block after the last block - Update key bindings in `initial-content.ts` to include `Alt + Enter` for adding a new block after the last block. - Implement `getLastNoteBlock` function in `block.js` to retrieve the last block in the note. - Add `addNewBlockAfterLast` command in `commands.js` to handle the insertion of a new block after the last one. - Integrate `addNewBlockAfterLast` command into the keymap in `keymap.js`. * Add block insertion before/after current, before first and after last. Also, tests. - Added `getFirstNoteBlock` in `block.js` for accessing the first text block. - Implemented new functions in `commands.js` like `addNewBlockBeforeCurrent` and `addNewBlockBeforeFirst`. - Updated `keymap.js` with new key bindings to facilitate block creation. - Introduced `block-creation.spec.js` for testing the new block manipulation features. * Fix visual bug when inserting new block at the top * Update help text and Readme * Fix wrong cursor position after inserting new blocks at the top of the buffer, when the previous first block's delimiter is long (e.g. Markdown) * Make RegEx more generic * Fix import * Auto-generate the README.md and initial-content documentation - Add a documentation generator - Add an option to force the initial content to be erased with an env variable * Add more specific tests * Fix Mod key on Mac in test --------- Co-authored-by: Jonatan Heyman <jonatan@heyman.info>
2024-01-04 16:11:26 +01:00
2023-01-15 10:53:09 +01:00
// Test actively push message to the Electron-Renderer
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', new Date().toLocaleString())
})
// Make all links open with the browser, not with the application
Add key bindings for inserting new blocks at the end/top of the buffer, as well as before the current block (#85) * Add functionality to insert new block after the last block - Update key bindings in `initial-content.ts` to include `Alt + Enter` for adding a new block after the last block. - Implement `getLastNoteBlock` function in `block.js` to retrieve the last block in the note. - Add `addNewBlockAfterLast` command in `commands.js` to handle the insertion of a new block after the last one. - Integrate `addNewBlockAfterLast` command into the keymap in `keymap.js`. * Add block insertion before/after current, before first and after last. Also, tests. - Added `getFirstNoteBlock` in `block.js` for accessing the first text block. - Implemented new functions in `commands.js` like `addNewBlockBeforeCurrent` and `addNewBlockBeforeFirst`. - Updated `keymap.js` with new key bindings to facilitate block creation. - Introduced `block-creation.spec.js` for testing the new block manipulation features. * Fix visual bug when inserting new block at the top * Update help text and Readme * Fix wrong cursor position after inserting new blocks at the top of the buffer, when the previous first block's delimiter is long (e.g. Markdown) * Make RegEx more generic * Fix import * Auto-generate the README.md and initial-content documentation - Add a documentation generator - Add an option to force the initial content to be erased with an env variable * Add more specific tests * Fix Mod key on Mac in test --------- Co-authored-by: Jonatan Heyman <jonatan@heyman.info>
2024-01-04 16:11:26 +01:00
win.webContents.setWindowOpenHandler(({url}) => {
if (url.startsWith('https:') || url.startsWith('http:')) {
shell.openExternal(url)
}
Add key bindings for inserting new blocks at the end/top of the buffer, as well as before the current block (#85) * Add functionality to insert new block after the last block - Update key bindings in `initial-content.ts` to include `Alt + Enter` for adding a new block after the last block. - Implement `getLastNoteBlock` function in `block.js` to retrieve the last block in the note. - Add `addNewBlockAfterLast` command in `commands.js` to handle the insertion of a new block after the last one. - Integrate `addNewBlockAfterLast` command into the keymap in `keymap.js`. * Add block insertion before/after current, before first and after last. Also, tests. - Added `getFirstNoteBlock` in `block.js` for accessing the first text block. - Implemented new functions in `commands.js` like `addNewBlockBeforeCurrent` and `addNewBlockBeforeFirst`. - Updated `keymap.js` with new key bindings to facilitate block creation. - Introduced `block-creation.spec.js` for testing the new block manipulation features. * Fix visual bug when inserting new block at the top * Update help text and Readme * Fix wrong cursor position after inserting new blocks at the top of the buffer, when the previous first block's delimiter is long (e.g. Markdown) * Make RegEx more generic * Fix import * Auto-generate the README.md and initial-content documentation - Add a documentation generator - Add an option to force the initial content to be erased with an env variable * Add more specific tests * Fix Mod key on Mac in test --------- Co-authored-by: Jonatan Heyman <jonatan@heyman.info>
2024-01-04 16:11:26 +01:00
return {action: 'deny'}
2023-01-15 10:53:09 +01:00
})
fixElectronCors(win)
2023-01-12 18:55:55 +01:00
}
function createTray() {
let img
if (isMac) {
img = nativeImage.createFromPath(join(process.env.PUBLIC, "iconTemplate.png"))
} else if (isLinux) {
img = nativeImage.createFromPath(join(process.env.PUBLIC, 'favicon-linux.png'));
} else{
img = nativeImage.createFromPath(join(process.env.PUBLIC, 'favicon.ico'));
}
tray = new Tray(img);
tray.setToolTip("Heynote");
const menu = getTrayMenu(win)
if (isMac) {
// using tray.setContextMenu() on macOS will open the menu on left-click, so instead we'll
// manually bind the right-click event to open the menu
tray.addListener("right-click", () => {
tray?.popUpContextMenu(menu)
})
} else {
tray.setContextMenu(menu);
}
tray.addListener("click", () => {
win?.show()
})
}
2023-12-10 22:59:42 +01:00
function registerGlobalHotkey() {
globalShortcut.unregisterAll()
if (CONFIG.get("settings.enableGlobalHotkey")) {
try {
const ret = globalShortcut.register(CONFIG.get("settings.globalHotkey"), () => {
if (!win) {
return
}
if (win.isFocused()) {
if (isMac) {
2023-12-11 00:31:40 +01:00
// app.hide() only available on macOS
// We want to use app.hide() so that the menu bar also gets changed
2023-12-11 00:31:40 +01:00
app?.hide()
if (CONFIG.get("settings.alwaysOnTop")) {
// if alwaysOnTop is on, calling app.hide() won't hide the window
win.hide()
}
2023-12-11 00:31:40 +01:00
} else {
win.blur()
if (CONFIG.get("settings.showInMenu") || CONFIG.get("settings.alwaysOnTop")) {
// if we're using a tray icon, or alwaysOnTop is on, we want to completely hide the window
win.hide()
}
2023-12-11 00:31:40 +01:00
}
} else {
app.focus({steal: true})
if (win.isMinimized()) {
win.restore()
}
if (!win.isVisible()) {
win.show()
}
win.focus()
}
2023-12-10 22:59:42 +01:00
})
} catch (error) {
console.log("Could not register global hotkey:", error)
}
}
}
function registerShowInDock() {
// dock is only available on macOS
if (isMac) {
if (CONFIG.get("settings.showInDock")) {
app.dock.show().catch((error) => {
console.log("Could not show app in dock: ", error);
});
} else {
app.dock.hide();
}
}
}
function registerShowInMenu() {
if (CONFIG.get("settings.showInMenu")) {
createTray()
} else {
tray?.destroy()
}
}
function registerAlwaysOnTop() {
if (CONFIG.get("settings.alwaysOnTop")) {
const disableAlwaysOnTop = () => {
win.setAlwaysOnTop(true, "floating");
win.setVisibleOnAllWorkspaces(true, {visibleOnFullScreen: true});
win.setFullScreenable(false);
}
// if we're in fullscreen mode, we need to exit fullscreen before we can set alwaysOnTop
if (win.isFullScreen()) {
// on Mac setFullScreen happens asynchronously, so we need to wait for the event before we can disable alwaysOnTop
win.once("leave-full-screen", disableAlwaysOnTop)
win.setFullScreen(false)
} else {
disableAlwaysOnTop()
}
} else {
win.setAlwaysOnTop(false);
win.setVisibleOnAllWorkspaces(false);
win.setFullScreenable(true);
}
}
2023-03-06 01:30:37 +01:00
app.whenReady().then(createWindow).then(async () => {
initializeAutoUpdate(win)
2023-12-10 22:59:42 +01:00
registerGlobalHotkey()
registerShowInDock()
registerShowInMenu()
registerAlwaysOnTop()
2023-03-06 01:30:37 +01:00
})
2023-01-12 18:55:55 +01:00
app.on("before-quit", () => {
// if CMD+Q is pressed, we want to quit the app even if we're using a Menu/Tray icon
setForceQuit()
})
2023-01-12 18:55:55 +01:00
app.on('window-all-closed', () => {
2023-01-15 10:53:09 +01:00
win = null
if (!isMac) app.quit()
2023-01-12 18:55:55 +01:00
})
app.on('second-instance', () => {
2023-01-15 10:53:09 +01:00
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore()
win.focus()
}
2023-01-12 18:55:55 +01:00
})
app.on('activate', () => {
2023-01-15 10:53:09 +01:00
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
allWindows[0].focus()
} else {
createWindow()
}
2023-01-12 18:55:55 +01:00
})
ipcMain.handle('dark-mode:set', (event, mode) => {
2023-03-09 09:46:21 +01:00
CONFIG.set("theme", mode)
nativeTheme.themeSource = mode
2023-01-12 18:55:55 +01:00
})
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
2023-01-19 00:20:50 +01:00
// load buffer on app start
loadBuffer()
2023-01-19 00:20:50 +01:00
ipcMain.handle('settings:set', async (event, settings) => {
2023-12-06 11:56:10 +01:00
if (settings.keymap !== CONFIG.get("settings.keymap")) {
currentKeymap = settings.keymap
}
2023-12-10 22:59:42 +01:00
let globalHotkeyChanged = settings.enableGlobalHotkey !== CONFIG.get("settings.enableGlobalHotkey") || settings.globalHotkey !== CONFIG.get("settings.globalHotkey")
let showInDockChanged = settings.showInDock !== CONFIG.get("settings.showInDock");
let showInMenuChanged = settings.showInMenu !== CONFIG.get("settings.showInMenu");
let bufferPathChanged = settings.bufferPath !== CONFIG.get("settings.bufferPath");
let alwaysOnTopChanged = settings.alwaysOnTop !== CONFIG.get("settings.alwaysOnTop");
CONFIG.set("settings", settings)
2023-12-10 22:59:42 +01:00
win?.webContents.send(SETTINGS_CHANGE_EVENT, settings)
Add key bindings for inserting new blocks at the end/top of the buffer, as well as before the current block (#85) * Add functionality to insert new block after the last block - Update key bindings in `initial-content.ts` to include `Alt + Enter` for adding a new block after the last block. - Implement `getLastNoteBlock` function in `block.js` to retrieve the last block in the note. - Add `addNewBlockAfterLast` command in `commands.js` to handle the insertion of a new block after the last one. - Integrate `addNewBlockAfterLast` command into the keymap in `keymap.js`. * Add block insertion before/after current, before first and after last. Also, tests. - Added `getFirstNoteBlock` in `block.js` for accessing the first text block. - Implemented new functions in `commands.js` like `addNewBlockBeforeCurrent` and `addNewBlockBeforeFirst`. - Updated `keymap.js` with new key bindings to facilitate block creation. - Introduced `block-creation.spec.js` for testing the new block manipulation features. * Fix visual bug when inserting new block at the top * Update help text and Readme * Fix wrong cursor position after inserting new blocks at the top of the buffer, when the previous first block's delimiter is long (e.g. Markdown) * Make RegEx more generic * Fix import * Auto-generate the README.md and initial-content documentation - Add a documentation generator - Add an option to force the initial content to be erased with an env variable * Add more specific tests * Fix Mod key on Mac in test --------- Co-authored-by: Jonatan Heyman <jonatan@heyman.info>
2024-01-04 16:11:26 +01:00
2023-12-10 22:59:42 +01:00
if (globalHotkeyChanged) {
registerGlobalHotkey()
}
if (showInDockChanged) {
registerShowInDock()
}
if (showInMenuChanged) {
registerShowInMenu()
}
if (alwaysOnTopChanged) {
registerAlwaysOnTop()
}
if (bufferPathChanged) {
const buffer = loadBuffer()
if (buffer.exists()) {
win?.webContents.send("buffer-content:change", await buffer.load())
}
}
})