main.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import { app, BrowserWindow, dialog, Event, Input, ipcMain, Menu, shell, WebContentsView } from 'electron'
  2. import { fileURLToPath } from 'node:url'
  3. import path from 'node:path'
  4. import fs from 'node:fs'
  5. import { initUpdater } from './updater'
  6. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  7. // The built directory structure
  8. //
  9. // ├─┬─┬ dist
  10. // │ │ └── index.html
  11. // │ │
  12. // │ ├─┬ dist-electron
  13. // │ │ ├── main.js
  14. // │ │ └── preload.mjs
  15. // │
  16. process.env.APP_ROOT = path.join(__dirname, '..')
  17. // 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
  18. export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
  19. export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
  20. export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
  21. process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
  22. type InputPassword = {
  23. username: string;
  24. password: string;
  25. }
  26. let mainWindow: BrowserWindow | null
  27. let childView: WebContentsView | null
  28. let isSideOpen = true
  29. let childViewAspectRatio = 0;
  30. let childViewInputPassword: InputPassword | undefined;
  31. const SIDE_WIDTH = 250
  32. const EXPAND_VIEW_SIZE = 40
  33. const LOADING_VIEW_WIDTH = 150
  34. const LOADING_VIEW_HEIGHT = 60
  35. function loadWindowPage(window: BrowserWindow, subPath: string) {
  36. if (VITE_DEV_SERVER_URL) {
  37. window.loadURL(VITE_DEV_SERVER_URL + "#" + subPath)
  38. } else {
  39. window.loadFile(path.join(RENDERER_DIST, 'index.html'), {
  40. hash: subPath,
  41. })
  42. }
  43. }
  44. function loadViewUrl(view: WebContentsView, subPath: string) {
  45. if (!mainWindow) {
  46. return
  47. }
  48. if (VITE_DEV_SERVER_URL) {
  49. view.webContents.loadURL(VITE_DEV_SERVER_URL + "#" + subPath)
  50. } else {
  51. view.webContents.loadFile(path.join(RENDERER_DIST, 'index.html'), {
  52. hash: subPath,
  53. })
  54. }
  55. }
  56. Menu.setApplicationMenu(null)
  57. function createWindow() {
  58. mainWindow = new BrowserWindow({
  59. icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'),
  60. webPreferences: {
  61. preload: path.join(__dirname, 'preload.mjs'),
  62. contextIsolation: true,
  63. devTools: true,
  64. allowRunningInsecureContent: true,
  65. },
  66. width: 1200,
  67. height: 800,
  68. })
  69. childView = new WebContentsView({
  70. webPreferences: {
  71. allowRunningInsecureContent: true,
  72. },
  73. })
  74. const expandButtonView = new WebContentsView({
  75. webPreferences: {
  76. preload: path.join(__dirname, 'preload.mjs'),
  77. contextIsolation: true,
  78. },
  79. })
  80. const loadingView = new BrowserWindow({
  81. skipTaskbar: true,
  82. width: LOADING_VIEW_WIDTH,
  83. height: LOADING_VIEW_HEIGHT,
  84. parent: mainWindow,
  85. thickFrame: true,
  86. titleBarStyle: 'hidden',
  87. webPreferences: {
  88. preload: path.join(__dirname, 'preload.mjs'),
  89. contextIsolation: true,
  90. },
  91. })
  92. mainWindow.contentView.addChildView(childView)
  93. mainWindow.contentView.addChildView(expandButtonView)
  94. expandButtonView.setVisible(false)
  95. expandButtonView.setBackgroundColor('#00000000')
  96. childView.webContents.on('did-start-loading', () => {
  97. loadingView.show()
  98. });
  99. childView.webContents.on('did-stop-loading', () => {
  100. loadingView.hide()
  101. });
  102. childView.webContents.on('did-fail-load', (_, errorCode, errorDescription) => {
  103. loadingView.hide()
  104. loadViewUrl(childView!, '/error?code=' + errorCode + '&message=' + errorDescription);
  105. });
  106. function updateChildWindowBounds() {
  107. const bounds = mainWindow!.getBounds();
  108. const contentBounds = mainWindow!.getContentBounds();
  109. const contentWidth = contentBounds.width;
  110. const contentHeight = contentBounds.height;
  111. expandButtonView.setBounds({
  112. x: 0,
  113. y: contentHeight - 100,
  114. width: EXPAND_VIEW_SIZE,
  115. height: EXPAND_VIEW_SIZE
  116. })
  117. loadingView.setBounds({
  118. x: bounds.x + (bounds.width - LOADING_VIEW_WIDTH) / 2,
  119. y: bounds.y + 30,
  120. width: LOADING_VIEW_WIDTH,
  121. height: LOADING_VIEW_HEIGHT
  122. })
  123. const availableX = isSideOpen ? SIDE_WIDTH : 0;
  124. const availableWidth = contentWidth - availableX;
  125. const availableHeight = contentHeight;
  126. if (childViewAspectRatio) {
  127. const availableRatio = availableWidth / availableHeight;
  128. let childWidth, childHeight;
  129. if (availableRatio > childViewAspectRatio) {
  130. childHeight = availableHeight;
  131. childWidth = childHeight * childViewAspectRatio;
  132. } else {
  133. childWidth = availableWidth;
  134. childHeight = childWidth / childViewAspectRatio;
  135. }
  136. const childX = availableX + (availableWidth - childWidth) / 2;
  137. const childY = (availableHeight - childHeight) / 2;
  138. childView!.setBounds({
  139. x: Math.round(childX),
  140. y: Math.round(childY),
  141. width: Math.round(childWidth),
  142. height: Math.round(childHeight)
  143. });
  144. } else {
  145. childView!.setBounds({
  146. x: availableX,
  147. y: 0,
  148. width: availableWidth,
  149. height: availableHeight
  150. });
  151. }
  152. }
  153. loadWindowPage(mainWindow, '/');
  154. loadWindowPage(loadingView, '/loading');
  155. loadViewUrl(childView, '/hello');
  156. loadViewUrl(expandButtonView, '/expand');
  157. updateChildWindowBounds();
  158. initUpdater(mainWindow);
  159. mainWindow.on('resize', () => {
  160. updateChildWindowBounds()
  161. })
  162. mainWindow.webContents.on('did-finish-load', () => {
  163. mainWindow?.webContents.send('main-process-message', (new Date).toLocaleString())
  164. })
  165. function handleWindowFullScreenKeys(window: BrowserWindow | WebContentsView, event: Event, input: Input) {
  166. if (input.key === 'F11' && input.type === 'keyDown') {
  167. event.preventDefault();
  168. mainWindow?.setFullScreen(!mainWindow?.isFullScreen())
  169. } else if (input.key === 'F12' && input.type === 'keyDown') {
  170. event.preventDefault();
  171. if (window === mainWindow) {
  172. toggleDevTools()
  173. } else if (window === childView) {
  174. childView?.webContents.toggleDevTools()
  175. }
  176. } else if (input.key === 'F5' && input.type === 'keyDown') {
  177. event.preventDefault();
  178. if (window === mainWindow) {
  179. if (input.control)
  180. mainWindow?.webContents.reloadIgnoringCache()
  181. else
  182. mainWindow?.webContents.reload()
  183. } else if (window === childView) {
  184. if (input.control)
  185. childView?.webContents.reloadIgnoringCache()
  186. else
  187. childView?.webContents.reload()
  188. }
  189. }
  190. }
  191. function toggleDevTools() {
  192. if (mainWindow) {
  193. if (mainWindow.webContents.isDevToolsOpened()) {
  194. mainWindow.webContents.closeDevTools()
  195. } else {
  196. mainWindow.webContents.openDevTools({
  197. mode: 'detach',
  198. })
  199. }
  200. }
  201. }
  202. // 添加F11全屏切换功能
  203. mainWindow.webContents.on('before-input-event', (event, input) => {
  204. handleWindowFullScreenKeys(mainWindow!, event, input)
  205. })
  206. childView.webContents.on('before-input-event', (event, input) => {
  207. handleWindowFullScreenKeys(childView!, event, input)
  208. })
  209. childView.webContents.on('did-finish-load', () => {
  210. if (childViewInputPassword) {
  211. const script = `
  212. (function() {
  213. function tryFill() {
  214. const passwordInputs = document.querySelectorAll('input[type="password"]');
  215. if (passwordInputs.length === 0) return false;
  216. const allInputs = Array.from(document.querySelectorAll('input'));
  217. const usernameInput = allInputs.find(el => {
  218. const type = el.getAttribute('type');
  219. return !type || type === 'text' || type === 'email';
  220. });
  221. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
  222. if (usernameInput) {
  223. nativeInputValueSetter.call(usernameInput, ${JSON.stringify(childViewInputPassword.username)});
  224. usernameInput.dispatchEvent(new Event('input', { bubbles: true }));
  225. usernameInput.dispatchEvent(new Event('change', { bubbles: true }));
  226. }
  227. const pwdInput = passwordInputs[0];
  228. nativeInputValueSetter.call(pwdInput, ${JSON.stringify(childViewInputPassword.password)});
  229. pwdInput.dispatchEvent(new Event('input', { bubbles: true }));
  230. pwdInput.dispatchEvent(new Event('change', { bubbles: true }));
  231. return true;
  232. }
  233. let attempts = 0;
  234. const timer = setInterval(() => {
  235. if (tryFill() || ++attempts >= 10) clearInterval(timer);
  236. }, 500);
  237. })();
  238. `;
  239. childView!.webContents.executeJavaScript(script).catch(() => {});
  240. }
  241. });
  242. // 处理退出应用事件
  243. ipcMain.on('exit-app', () => {
  244. app.quit()
  245. })
  246. // 处理全屏切换事件
  247. ipcMain.on('toggle-fullscreen', (_event, isFullScreen: boolean) => {
  248. if (mainWindow) {
  249. if (isFullScreen) {
  250. mainWindow.setFullScreen(true)
  251. } else {
  252. mainWindow.setFullScreen(false)
  253. }
  254. }
  255. })
  256. ipcMain.on('toggle-dev-tools', toggleDevTools)
  257. // 加载子页URL
  258. ipcMain.on('load-child-url', (_event, url: string, aspectRatio: number, inputPassword?: InputPassword) => {
  259. if (childView)
  260. childView.webContents.loadURL(url)
  261. childViewInputPassword = inputPassword;
  262. childViewAspectRatio = aspectRatio
  263. updateChildWindowBounds()
  264. })
  265. // 子页侧边栏开关
  266. ipcMain.on('toggle-child-side', (_event, value: boolean) => {
  267. isSideOpen = value
  268. expandButtonView.setVisible(!isSideOpen)
  269. mainWindow?.webContents.send('main-side-state-changed', isSideOpen)
  270. updateChildWindowBounds()
  271. })
  272. ipcMain.on('open-in-system', (_event, url: string) => {
  273. shell.openExternal(url)
  274. })
  275. ipcMain.on('refresh-child-view', () => {
  276. childView?.webContents.reloadIgnoringCache()
  277. })
  278. // 处理获取应用路径事件
  279. ipcMain.handle('get-app-path', () => {
  280. return app.getAppPath()
  281. })
  282. // 处理打开窗口事件
  283. ipcMain.on('open-window', (_event, url: string) => {
  284. const newWin = new BrowserWindow({
  285. icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'),
  286. webPreferences: {
  287. preload: path.join(__dirname, 'preload.mjs'),
  288. contextIsolation: true,
  289. allowRunningInsecureContent: true,
  290. },
  291. fullscreenable: true,
  292. maximizable: true,
  293. width: 1200,
  294. height: 800,
  295. })
  296. newWin.loadURL(url)
  297. newWin.maximize();
  298. // 添加F11全屏切换功能
  299. newWin.webContents.on('before-input-event', (event, input) => {
  300. if (input.key === 'F11' && input.type === 'keyDown') {
  301. event.preventDefault();
  302. newWin?.setFullScreen(!newWin?.fullScreen)
  303. } else if (input.key === 'F12' && input.type === 'keyDown') {
  304. event.preventDefault();
  305. newWin?.webContents.toggleDevTools();
  306. }
  307. })
  308. })
  309. // 处理加载apps.json事件
  310. ipcMain.handle('load-apps-json', async () => {
  311. const appPath = process.cwd()
  312. const appsJsonPath = path.join(appPath, 'apps.json')
  313. try {
  314. if (fs.existsSync(appsJsonPath)) {
  315. const data = fs.readFileSync(appsJsonPath, 'utf8')
  316. return JSON.parse(data)
  317. } else {
  318. // 开发环境下回退到public目录
  319. const devAppsJsonPath = path.join(process.env.VITE_PUBLIC || '', 'apps.json')
  320. if (fs.existsSync(devAppsJsonPath)) {
  321. const data = fs.readFileSync(devAppsJsonPath, 'utf8')
  322. return JSON.parse(data)
  323. }
  324. throw new Error('apps.json not found')
  325. }
  326. } catch (error) {
  327. console.error('Error loading apps.json:', error)
  328. throw error
  329. }
  330. })
  331. // 处理加载默认apps.json事件
  332. ipcMain.handle('load-default-apps-json', async () => {
  333. const devAppsJsonPath = path.join(process.env.VITE_PUBLIC || '', 'apps.json')
  334. if (fs.existsSync(devAppsJsonPath)) {
  335. const data = fs.readFileSync(devAppsJsonPath, 'utf8')
  336. return JSON.parse(data)
  337. }
  338. throw new Error('apps.json not found')
  339. })
  340. // 处理显示配置窗口事件
  341. ipcMain.on('show-config', () => {
  342. const configWindow = new BrowserWindow({
  343. icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'),
  344. webPreferences: {
  345. preload: path.join(__dirname, 'preload.mjs'),
  346. contextIsolation: true,
  347. allowRunningInsecureContent: true,
  348. },
  349. title: '列表配置',
  350. parent: mainWindow || undefined,
  351. skipTaskbar: true,
  352. minimizable: false,
  353. maximizable: false,
  354. modal: true,
  355. width: 800,
  356. height: 600,
  357. })
  358. loadWindowPage(configWindow, '/config')
  359. });
  360. // 处理保存apps.json事件
  361. ipcMain.on('save-apps-json', (_event, appsJson: string) => {
  362. const appPath = process.cwd()
  363. const appsJsonPath = path.join(appPath, 'apps.json')
  364. try {
  365. fs.writeFileSync(appsJsonPath, appsJson)
  366. mainWindow?.webContents.send('main-config-changed')
  367. } catch (error) {
  368. console.error('Error saving apps.json:', error)
  369. throw error
  370. }
  371. })
  372. // 处理显示关于窗口事件
  373. ipcMain.handle('clear-cache', async () => {
  374. if (mainWindow) {
  375. const res = await dialog.showMessageBox(mainWindow, {
  376. title: '提示',
  377. message: '是否要清除缓存?',
  378. type: 'question',
  379. buttons: ['取消', '确定'],
  380. defaultId: 0,
  381. noLink: true,
  382. });
  383. if (res.response === 0)
  384. return
  385. await mainWindow.webContents.session.clearCache()
  386. if (childView) {
  387. await childView.webContents.session.clearCache()
  388. childView.webContents.reloadIgnoringCache();
  389. }
  390. }
  391. })
  392. ipcMain.on('show-about', () => {
  393. const aboutWindow = new BrowserWindow({
  394. icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'),
  395. webPreferences: {
  396. preload: path.join(__dirname, 'preload.mjs'),
  397. contextIsolation: true,
  398. allowRunningInsecureContent: true,
  399. },
  400. parent: mainWindow || undefined,
  401. title: '关于程序',
  402. skipTaskbar: true,
  403. maximizable: false,
  404. minimizable: false,
  405. modal: true,
  406. width: 450,
  407. height: 470,
  408. })
  409. loadWindowPage(aboutWindow, '/about')
  410. });
  411. }
  412. // Quit when all mainWindowdows are closed, except on macOS. There, it's common
  413. // for applications and their menu bar to stay active until the user quits
  414. // explicitly with Cmd + Q.
  415. app.on('window-all-closed', () => {
  416. if (process.platform !== 'darwin') {
  417. app.quit()
  418. mainWindow = null
  419. }
  420. })
  421. app.on('activate', () => {
  422. // On OS X it's common to re-create a mainWindowdow in the app when the
  423. // dock icon is clicked and there are no other mainWindowdows open.
  424. if (BrowserWindow.getAllWindows().length === 0) {
  425. createWindow()
  426. }
  427. })
  428. app.whenReady().then(createWindow)