Folo/apps/main/src/window.ts

294 lines
8.1 KiB
TypeScript

import path from "node:path"
import { fileURLToPath } from "node:url"
import { is } from "@electron-toolkit/utils"
import { callWindowExpose } from "@follow/shared/bridge"
import type { BrowserWindowConstructorOptions } from "electron"
import { BrowserWindow, screen, shell } from "electron"
import { isDev, isMacOS, isWindows, isWindows11 } from "./env"
import { getIconPath } from "./helper"
import { store } from "./lib/store"
import { logger } from "./logger"
import { cancelPollingUpdateUnreadCount, pollingUpdateUnreadCount } from "./tipc/dock"
const windows = {
settingWindow: null as BrowserWindow | null,
mainWindow: null as BrowserWindow | null,
}
globalThis["windows"] = windows
const { platform } = process
const __dirname = fileURLToPath(new URL(".", import.meta.url))
export function createWindow(
options: {
extraPath?: string
height: number
width: number
} & BrowserWindowConstructorOptions,
) {
const { extraPath, height, width, ...configs } = options
const baseWindowConfig: Electron.BrowserWindowConstructorOptions = {
width,
height,
show: false,
resizable: configs?.resizable ?? true,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
webviewTag: true,
webSecurity: !isDev,
},
}
switch (platform) {
case "darwin": {
Object.assign(baseWindowConfig, {
titleBarStyle: "hiddenInset",
trafficLightPosition: {
x: 18,
y: 18,
},
vibrancy: "under-window",
visualEffectState: "active",
transparent: true,
} as Electron.BrowserWindowConstructorOptions)
break
}
case "win32": {
Object.assign(baseWindowConfig, {
icon: getIconPath(),
titleBarStyle: "hidden",
// Electron material bug, comment this for now
// backgroundMaterial: isWindows11 ? "mica" : undefined,
frame: true,
} as Electron.BrowserWindowConstructorOptions)
break
}
default: {
baseWindowConfig.icon = getIconPath()
}
}
// Create the browser window.
const window = new BrowserWindow({
...baseWindowConfig,
...configs,
})
window.on("leave-html-full-screen", () => {
function refreshBound(timeout = 0) {
setTimeout(() => {
// FIXME: workaround for theme bug in full screen mode
const size = window?.getSize()
window?.setSize(size[0] + 1, size[1] + 1)
window?.setSize(size[0], size[1])
}, timeout)
}
// To solve the vibrancy losing issue when leaving full screen mode
// @see https://github.com/toeverything/AFFiNE/blob/280e24934a27557529479a70ab38c4f5fc65cb00/packages/frontend/electron/src/main/windows-manager/main-window.ts:L157
refreshBound()
refreshBound(1000)
})
window.on("ready-to-show", () => {
window?.show()
})
window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: "deny" }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
window.loadURL(process.env["ELECTRON_RENDERER_URL"] + (options?.extraPath || ""))
logger.log(process.env["ELECTRON_RENDERER_URL"] + (options?.extraPath || ""))
} else {
const openPath = path.resolve(__dirname, "../renderer/index.html")
window.loadFile(openPath, {
hash: options?.extraPath,
})
logger.log(openPath, {
hash: options?.extraPath,
})
}
if (isWindows) {
// Change the default font-family and font-size of the devtools.
// Make it consistent with Chrome on Windows, instead of SimSun.
// ref: [[Feature Request]: Add possibility to change DevTools font · Issue #42055 · electron/electron](https://github.com/electron/electron/issues/42055)
window.webContents.on("devtools-opened", () => {
// source-code-font: For code such as Elements panel
// monospace-font: For sidebar such as Event Listener Panel
const css = `
:root {
--source-code-font-family: consolas;
--source-code-font-size: 13px;
--monospace-font-family: consolas;
--monospace-font-size: 13px;
}`
window.webContents.devToolsWebContents?.executeJavaScript(`
const overriddenStyle = document.createElement('style');
overriddenStyle.innerHTML = '${css.replaceAll("\n", " ")}';
document.body.append(overriddenStyle);
document.body.classList.remove('platform-windows');
`)
})
}
return window
}
export const windowStateStoreKey = "windowState"
export const createMainWindow = () => {
const windowState = store.get(windowStateStoreKey) as {
height: number
width: number
x: number
y: number
} | null
const primaryDisplay = screen.getPrimaryDisplay()
const { workArea } = primaryDisplay
const maxWidth = workArea.width
const maxHeight = workArea.height
const width = Math.min(windowState?.width || 1200, maxWidth)
const height = Math.min(windowState?.height || 900, maxHeight)
const ensureInBounds = (value: number, min: number, max: number): number => {
return Math.max(min, Math.min(value, max))
}
const x =
windowState?.x !== undefined
? ensureInBounds(windowState.x, workArea.x, workArea.x + workArea.width - width)
: undefined
const y =
windowState?.y !== undefined
? ensureInBounds(windowState.y, workArea.y, workArea.y + workArea.height - height)
: undefined
const window = createWindow({
width: windowState?.width || 1200,
height: windowState?.height || 900,
x,
y,
minWidth: Math.min(1024, maxWidth),
minHeight: Math.min(500, maxHeight),
})
window.on("close", () => {
if (isWindows11) {
const windowStoreKey = Symbol.for("maximized")
if (window[windowStoreKey]) {
const stored = window[windowStoreKey]
store.set(windowStateStoreKey, {
width: stored.size[0],
height: stored.size[1],
x: stored.position[0],
y: stored.position[1],
})
return
}
}
const bounds = window.getBounds()
store.set(windowStateStoreKey, {
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
})
})
windows.mainWindow = window
window.on("close", (event) => {
if (isMacOS) {
event.preventDefault()
if (window.isFullScreen()) {
window.once("leave-full-screen", () => {
window.hide()
})
window.setFullScreen(false)
} else {
window.hide()
}
const caller = callWindowExpose(window)
caller.onWindowClose()
} else {
windows.mainWindow = null
}
})
window.on("show", () => {
cancelPollingUpdateUnreadCount()
const caller = callWindowExpose(window)
caller.onWindowShow()
})
window.on("hide", async () => {
const caller = callWindowExpose(window)
const settings = await caller.getUISettings()
if (settings.showDockBadge) {
pollingUpdateUnreadCount()
}
})
return window
}
export const createSettingWindow = (path?: string) => {
// We need to open the setting modal in the main window when the main window exists,
// if we open a new window then the state between the two windows will be out of sync.
if (windows.mainWindow && windows.mainWindow.isVisible()) {
windows.mainWindow.show()
callWindowExpose(windows.mainWindow).showSetting(path)
return
}
if (windows.settingWindow) {
windows.settingWindow.show()
return
}
const window = createWindow({
extraPath: `#settings/${path || ""}`,
width: 700,
height: 600,
resizable: false,
})
windows.settingWindow = window
window.on("closed", () => {
windows.settingWindow = null
})
}
export const getMainWindow = () => windows.mainWindow
export const getMainWindowOrCreate = () => {
if (!windows.mainWindow) {
createMainWindow()
}
return windows.mainWindow
}
export const destroyMainWindow = () => {
windows.mainWindow?.destroy()
windows.mainWindow = null
}