main.ts 16 KB

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