294 lines
8.1 KiB
TypeScript
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
|
|
}
|