app.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
  1. import * as base from './base.js';
  2. import * as electron from 'electron';
  3. import * as fs from 'fs';
  4. import * as os from 'os';
  5. import * as path from 'path';
  6. import * as process from 'process';
  7. import * as url from 'url';
  8. const app = {};
  9. app.Application = class {
  10. constructor() {
  11. this._views = new app.ViewCollection(this);
  12. this._configuration = new app.ConfigurationService();
  13. this._menu = new app.MenuService(this._views);
  14. this._openQueue = [];
  15. this._package = {};
  16. }
  17. async start() {
  18. const dirname = path.dirname(url.fileURLToPath(import.meta.url));
  19. const packageFile = path.join(path.dirname(dirname), 'package.json');
  20. const packageContent = fs.readFileSync(packageFile, 'utf-8');
  21. this._package = JSON.parse(packageContent);
  22. electron.app.setAppUserModelId('com.lutzroeder.netron');
  23. electron.app.allowRendererProcessReuse = true;
  24. if (!electron.app.requestSingleInstanceLock()) {
  25. electron.app.quit();
  26. return;
  27. }
  28. electron.app.on('second-instance', (event, commandLine, workingDirectory) => {
  29. const currentDirectory = process.cwd();
  30. process.chdir(workingDirectory);
  31. const open = this._parseCommandLine(commandLine);
  32. process.chdir(currentDirectory);
  33. if (!open && !this._views.empty) {
  34. const view = this._views.first();
  35. if (view) {
  36. view.restore();
  37. }
  38. }
  39. });
  40. electron.ipcMain.on('get-environment', (event) => {
  41. event.returnValue = this.environment;
  42. });
  43. electron.ipcMain.on('get-configuration', (event, obj) => {
  44. event.returnValue = this._configuration.has(obj.name) ? this._configuration.get(obj.name) : undefined;
  45. });
  46. electron.ipcMain.on('set-configuration', (event, obj) => {
  47. this._configuration.set(obj.name, obj.value);
  48. this._configuration.save();
  49. event.returnValue = null;
  50. });
  51. electron.ipcMain.on('delete-configuration', (event, obj) => {
  52. this._configuration.delete(obj.name);
  53. this._configuration.save();
  54. event.returnValue = null;
  55. });
  56. electron.ipcMain.on('drop-paths', (event, data) => {
  57. const paths = data.paths.filter((path) => {
  58. if (fs.existsSync(path)) {
  59. const stat = fs.statSync(path);
  60. return stat.isFile() || stat.isDirectory();
  61. }
  62. return false;
  63. });
  64. this._dropPaths(event.sender, paths);
  65. event.returnValue = null;
  66. });
  67. electron.ipcMain.on('show-save-dialog', async (event, options) => {
  68. const owner = event.sender.getOwnerBrowserWindow();
  69. const argument = {};
  70. try {
  71. const { filePath, canceled } = await electron.dialog.showSaveDialog(owner, options);
  72. argument.filePath = filePath;
  73. argument.canceled = canceled;
  74. } catch (error) {
  75. argument.error = error.message;
  76. }
  77. event.sender.send('show-save-dialog-complete', argument);
  78. });
  79. electron.ipcMain.on('execute', async (event, data) => {
  80. const owner = event.sender.getOwnerBrowserWindow();
  81. const argument = {};
  82. try {
  83. argument.value = await this.execute(data.name, data.value || null, owner);
  84. } catch (error) {
  85. argument.error = error.message;
  86. }
  87. event.sender.send('execute-complete', argument);
  88. });
  89. electron.app.on('will-finish-launching', () => {
  90. electron.app.on('open-file', (event, path) => {
  91. this._openPath(path);
  92. });
  93. });
  94. electron.app.on('ready', () => {
  95. this._ready();
  96. });
  97. electron.app.on('window-all-closed', () => {
  98. if (process.platform !== 'darwin') {
  99. electron.app.quit();
  100. }
  101. });
  102. electron.app.on('will-quit', () => {
  103. this._configuration.save();
  104. });
  105. this._parseCommandLine(process.argv);
  106. await this._checkForUpdates();
  107. }
  108. get environment() {
  109. this._environment = this._environment || {
  110. packaged: electron.app.isPackaged,
  111. name: this._package.productName,
  112. version: this._package.version,
  113. date: this._package.date,
  114. repository: `https://github.com/${this._package.repository}`,
  115. platform: process.platform,
  116. separator: path.sep,
  117. titlebar: true // process.platform === 'darwin'
  118. };
  119. return this._environment;
  120. }
  121. _parseCommandLine(argv) {
  122. let open = false;
  123. if (argv.length > 1) {
  124. for (const arg of argv.slice(1)) {
  125. const dirname = path.dirname(url.fileURLToPath(import.meta.url));
  126. if (!arg.startsWith('-') && arg !== path.dirname(dirname)) {
  127. const extension = path.extname(arg).toLowerCase();
  128. if (extension !== '' && extension !== '.js' && fs.existsSync(arg)) {
  129. const stat = fs.statSync(arg);
  130. if (stat.isFile() || stat.isDirectory()) {
  131. this._openPath(arg);
  132. open = true;
  133. }
  134. }
  135. }
  136. }
  137. }
  138. return open;
  139. }
  140. _ready() {
  141. this._configuration.open();
  142. if (this._openQueue) {
  143. const queue = this._openQueue;
  144. this._openQueue = null;
  145. while (queue.length > 0) {
  146. const file = queue.shift();
  147. this._openPath(file);
  148. }
  149. }
  150. if (this._views.empty) {
  151. this._views.openView();
  152. }
  153. this._updateMenu();
  154. this._views.on('active-view-changed', () => {
  155. this._menu.update();
  156. });
  157. this._views.on('active-view-updated', () => {
  158. this._menu.update();
  159. });
  160. }
  161. _open(path) {
  162. let paths = path ? [path] : [];
  163. if (paths.length === 0) {
  164. const extensions = new base.Metadata().extensions;
  165. const options = {
  166. properties: ['openFile'],
  167. filters: [{ name: 'All Model Files', extensions }]
  168. };
  169. const owner = electron.BrowserWindow.getFocusedWindow();
  170. paths = electron.dialog.showOpenDialogSync(owner, options);
  171. }
  172. if (Array.isArray(paths) && paths.length > 0) {
  173. for (const path of paths) {
  174. this._openPath(path);
  175. }
  176. }
  177. }
  178. _openPath(path) {
  179. if (this._openQueue) {
  180. this._openQueue.push(path);
  181. return;
  182. }
  183. if (path && path.length > 0) {
  184. const exists = fs.existsSync(path);
  185. if (exists) {
  186. const stat = fs.statSync(path);
  187. if (stat.isFile() || stat.isDirectory()) {
  188. const views = Array.from(this._views.views);
  189. // find existing view for this file
  190. let view = views.find((view) => view.match(path));
  191. // find empty welcome window
  192. if (!view) {
  193. view = views.find((view) => view.match(null));
  194. }
  195. // create new window
  196. if (!view) {
  197. view = this._views.openView();
  198. }
  199. view.open(path);
  200. }
  201. }
  202. this._updateRecents(exists ? path : undefined);
  203. }
  204. }
  205. _dropPaths(sender, paths) {
  206. const window = sender.getOwnerBrowserWindow();
  207. let view = this._views.get(window);
  208. for (const path of paths) {
  209. if (view) {
  210. view.open(path);
  211. this._updateRecents(path);
  212. view = null;
  213. } else {
  214. this._openPath(path);
  215. }
  216. }
  217. }
  218. async _export() {
  219. const view = this._views.activeView;
  220. if (view && view.path) {
  221. let defaultPath = 'Untitled';
  222. const file = view.path;
  223. const lastIndex = file.lastIndexOf('.');
  224. if (lastIndex !== -1) {
  225. defaultPath = file.substring(0, lastIndex);
  226. }
  227. const owner = electron.BrowserWindow.getFocusedWindow();
  228. const options = {
  229. title: 'Export',
  230. defaultPath,
  231. buttonLabel: 'Export',
  232. filters: [
  233. { name: 'PNG', extensions: ['png'] },
  234. { name: 'SVG', extensions: ['svg'] }
  235. ]
  236. };
  237. const { filePath, canceled } = await electron.dialog.showSaveDialog(owner, options);
  238. if (filePath && !canceled) {
  239. view.execute('export', { 'file': filePath });
  240. }
  241. }
  242. }
  243. async execute(command, value, window) {
  244. switch (command) {
  245. case 'open': this._open(value); break;
  246. case 'export': await this._export(); break;
  247. case 'close': window.close(); break;
  248. case 'quit': electron.app.quit(); break;
  249. case 'reload': this._reload(); break;
  250. case 'report-issue': electron.shell.openExternal(`https://github.com/${this._package.repository}/issues/new`); break;
  251. case 'about': this._about(); break;
  252. default: {
  253. const view = this._views.get(window) || this._views.activeView;
  254. if (view) {
  255. view.execute(command, value || {});
  256. }
  257. this._menu.update();
  258. }
  259. }
  260. }
  261. _reload() {
  262. const view = this._views.activeView;
  263. if (view && view.path) {
  264. view.open(view.path);
  265. this._updateRecents(view.path);
  266. }
  267. }
  268. async _checkForUpdates() {
  269. if (!electron.app.isPackaged) {
  270. return;
  271. }
  272. const updater = await import('electron-updater');
  273. const autoUpdater = updater.default.autoUpdater;
  274. if (autoUpdater.app && autoUpdater.app.appUpdateConfigPath && !fs.existsSync(autoUpdater.app.appUpdateConfigPath)) {
  275. return;
  276. }
  277. const promise = autoUpdater.checkForUpdates();
  278. if (promise) {
  279. promise.catch((error) => {
  280. /* eslint-disable no-console */
  281. console.log(error.message);
  282. /* eslint-enable no-console */
  283. });
  284. }
  285. }
  286. _about() {
  287. let view = this._views.activeView;
  288. if (!view) {
  289. view = this._views.openView();
  290. }
  291. view.execute('about');
  292. }
  293. _updateRecents(path) {
  294. let updated = false;
  295. let recents = this._configuration.has('recents') ? this._configuration.get('recents') : [];
  296. if (path && (recents.length === 0 || recents[0] !== path)) {
  297. recents = recents.filter((recent) => path !== recent);
  298. recents.unshift(path);
  299. updated = true;
  300. }
  301. const value = [];
  302. for (const recent of recents) {
  303. if (value.length >= 9) {
  304. updated = true;
  305. break;
  306. }
  307. if (!fs.existsSync(recent)) {
  308. updated = true;
  309. continue;
  310. }
  311. const stat = fs.statSync(recent);
  312. if (!stat.isFile() && !stat.isDirectory()) {
  313. updated = true;
  314. continue;
  315. }
  316. value.push(recent);
  317. }
  318. if (updated) {
  319. this._configuration.set('recents', value);
  320. this._updateMenu();
  321. }
  322. }
  323. _updateMenu() {
  324. let recents = [];
  325. if (this._configuration.has('recents')) {
  326. const value = this._configuration.get('recents');
  327. recents = value.map((recent) => app.Application.location(recent));
  328. }
  329. if (this.environment.titlebar && recents.length > 0) {
  330. for (const view of this._views.views) {
  331. view.execute('recents', recents);
  332. }
  333. }
  334. const darwin = process.platform === 'darwin';
  335. if (!this.environment.titlebar || darwin) {
  336. const menuRecentsTemplate = [];
  337. for (let i = 0; i < recents.length; i++) {
  338. const recent = recents[i];
  339. menuRecentsTemplate.push({
  340. path: recent.path,
  341. label: recent.label,
  342. accelerator: (darwin ? 'Cmd+' : 'Ctrl+') + (i + 1).toString(),
  343. click: (item) => this._openPath(item.path)
  344. });
  345. }
  346. const menuTemplate = [];
  347. if (darwin) {
  348. menuTemplate.unshift({
  349. label: electron.app.name,
  350. submenu: [
  351. {
  352. label: `About ${electron.app.name}`,
  353. click: () => /* this.execute('about', null) */ this._about()
  354. },
  355. { type: 'separator' },
  356. { role: 'hide' },
  357. { role: 'hideothers' },
  358. { role: 'unhide' },
  359. { type: 'separator' },
  360. { role: 'quit' }
  361. ]
  362. });
  363. }
  364. menuTemplate.push({
  365. label: '&File',
  366. submenu: [
  367. {
  368. label: '&Open...',
  369. accelerator: 'CmdOrCtrl+O',
  370. click: () => this._open(null)
  371. },
  372. {
  373. label: 'Open &Recent',
  374. submenu: menuRecentsTemplate
  375. },
  376. { type: 'separator' },
  377. {
  378. id: 'file.export',
  379. label: '&Export...',
  380. accelerator: 'CmdOrCtrl+Shift+E',
  381. click: async () => await this.execute('export', null)
  382. },
  383. { type: 'separator' },
  384. { role: 'close' },
  385. ]
  386. });
  387. if (!darwin) {
  388. menuTemplate.slice(-1)[0].submenu.push(
  389. { type: 'separator' },
  390. { role: 'quit' }
  391. );
  392. }
  393. if (darwin) {
  394. electron.systemPreferences.setUserDefault('NSDisabledDictationMenuItem', 'boolean', true);
  395. electron.systemPreferences.setUserDefault('NSDisabledCharacterPaletteMenuItem', 'boolean', true);
  396. }
  397. menuTemplate.push({
  398. label: '&Edit',
  399. submenu: [
  400. {
  401. id: 'edit.cut',
  402. label: 'Cu&t',
  403. accelerator: 'CmdOrCtrl+X',
  404. click: async () => await this.execute('cut', null),
  405. },
  406. {
  407. id: 'edit.copy',
  408. label: '&Copy',
  409. accelerator: 'CmdOrCtrl+C',
  410. click: async () => await this.execute('copy', null),
  411. },
  412. {
  413. id: 'edit.paste',
  414. label: '&Paste',
  415. accelerator: 'CmdOrCtrl+V',
  416. click: async () => await this.execute('paste', null),
  417. },
  418. {
  419. id: 'edit.select-all',
  420. label: 'Select &All',
  421. accelerator: 'CmdOrCtrl+A',
  422. click: async () => await this.execute('selectall', null),
  423. },
  424. { type: 'separator' },
  425. {
  426. id: 'edit.find',
  427. label: '&Find...',
  428. accelerator: 'CmdOrCtrl+F',
  429. click: async () => await this.execute('find', null),
  430. }
  431. ]
  432. });
  433. const viewTemplate = {
  434. label: '&View',
  435. submenu: [
  436. {
  437. id: 'view.toggle-attributes',
  438. accelerator: 'CmdOrCtrl+D',
  439. click: async () => await this.execute('toggle', 'attributes'),
  440. },
  441. {
  442. id: 'view.toggle-weights',
  443. accelerator: 'CmdOrCtrl+I',
  444. click: async () => await this.execute('toggle', 'weights'),
  445. },
  446. {
  447. id: 'view.toggle-names',
  448. accelerator: 'CmdOrCtrl+U',
  449. click: async () => await this.execute('toggle', 'names'),
  450. },
  451. {
  452. id: 'view.toggle-direction',
  453. accelerator: 'CmdOrCtrl+K',
  454. click: async () => await this.execute('toggle', 'direction')
  455. },
  456. {
  457. id: 'view.toggle-mousewheel',
  458. accelerator: 'CmdOrCtrl+M',
  459. click: async () => await this.execute('toggle', 'mousewheel'),
  460. },
  461. { type: 'separator' },
  462. {
  463. id: 'view.reload',
  464. label: '&Reload',
  465. accelerator: darwin ? 'Cmd+R' : 'F5',
  466. click: async () => await this._reload(),
  467. },
  468. { type: 'separator' },
  469. {
  470. id: 'view.reset-zoom',
  471. label: 'Actual &Size',
  472. accelerator: 'Shift+Backspace',
  473. click: async () => await this.execute('reset-zoom', null),
  474. },
  475. {
  476. id: 'view.zoom-in',
  477. label: 'Zoom &In',
  478. accelerator: 'Shift+Up',
  479. click: async () => await this.execute('zoom-in', null),
  480. },
  481. {
  482. id: 'view.zoom-out',
  483. label: 'Zoom &Out',
  484. accelerator: 'Shift+Down',
  485. click: async () => await this.execute('zoom-out', null),
  486. },
  487. { type: 'separator' },
  488. {
  489. id: 'view.show-properties',
  490. label: '&Properties...',
  491. accelerator: 'CmdOrCtrl+Enter',
  492. click: async () => await this.execute('show-properties', null),
  493. }
  494. ]
  495. };
  496. if (!electron.app.isPackaged) {
  497. viewTemplate.submenu.push({ type: 'separator' });
  498. viewTemplate.submenu.push({ role: 'toggledevtools' });
  499. }
  500. menuTemplate.push(viewTemplate);
  501. if (darwin) {
  502. menuTemplate.push({
  503. role: 'window',
  504. submenu: [
  505. { role: 'minimize' },
  506. { role: 'zoom' },
  507. { type: 'separator' },
  508. { role: 'front' }
  509. ]
  510. });
  511. }
  512. const helpSubmenu = [
  513. {
  514. label: 'Report &Issue',
  515. click: async () => await this.execute('report-issue', null)
  516. }
  517. ];
  518. if (!darwin) {
  519. helpSubmenu.push({ type: 'separator' });
  520. helpSubmenu.push({
  521. label: `&About ${electron.app.name}`,
  522. click: async () => await this.execute('about', null)
  523. });
  524. }
  525. menuTemplate.push({
  526. role: 'help',
  527. submenu: helpSubmenu
  528. });
  529. const commandTable = new Map();
  530. commandTable.set('file.export', {
  531. enabled: (view) => view && view.path ? true : false
  532. });
  533. commandTable.set('edit.cut', {
  534. enabled: (view) => view && view.path ? true : false
  535. });
  536. commandTable.set('edit.copy', {
  537. enabled: (view) => view && (view.path || view.get('can-copy')) ? true : false
  538. });
  539. commandTable.set('edit.paste', {
  540. enabled: (view) => view && view.path ? true : false
  541. });
  542. commandTable.set('edit.select-all', {
  543. enabled: (view) => view && view.path ? true : false
  544. });
  545. commandTable.set('edit.find', {
  546. enabled: (view) => view && view.path ? true : false
  547. });
  548. commandTable.set('view.toggle-attributes', {
  549. enabled: (view) => view && view.path ? true : false,
  550. label: (view) => !view || view.get('attributes') ? 'Hide &Attributes' : 'Show &Attributes'
  551. });
  552. commandTable.set('view.toggle-weights', {
  553. enabled: (view) => view && view.path ? true : false,
  554. label: (view) => !view || view.get('weights') ? 'Hide &Weights' : 'Show &Weights'
  555. });
  556. commandTable.set('view.toggle-names', {
  557. enabled: (view) => view && view.path ? true : false,
  558. label: (view) => !view || view.get('names') ? 'Hide &Names' : 'Show &Names'
  559. });
  560. commandTable.set('view.toggle-direction', {
  561. enabled: (view) => view && view.path ? true : false,
  562. label: (view) => !view || view.get('direction') === 'vertical' ? 'Show &Horizontal' : 'Show &Vertical'
  563. });
  564. commandTable.set('view.toggle-mousewheel', {
  565. enabled: (view) => view && view.path ? true : false,
  566. label: (view) => !view || view.get('mousewheel') === 'scroll' ? '&Mouse Wheel: Zoom' : '&Mouse Wheel: Scroll'
  567. });
  568. commandTable.set('view.reload', {
  569. enabled: (view) => view && view.path ? true : false
  570. });
  571. commandTable.set('view.reset-zoom', {
  572. enabled: (view) => view && view.path ? true : false
  573. });
  574. commandTable.set('view.zoom-in', {
  575. enabled: (view) => view && view.path ? true : false
  576. });
  577. commandTable.set('view.zoom-out', {
  578. enabled: (view) => view && view.path ? true : false
  579. });
  580. commandTable.set('view.show-properties', {
  581. enabled: (view) => view && view.path ? true : false
  582. });
  583. this._menu.build(menuTemplate, commandTable);
  584. this._menu.update();
  585. }
  586. }
  587. static location(path) {
  588. if (process.platform !== 'win32') {
  589. const homeDir = os.homedir();
  590. if (path.startsWith(homeDir)) {
  591. return { path, label: `~${path.substring(homeDir.length)}` };
  592. }
  593. }
  594. return { path, label: path };
  595. }
  596. };
  597. app.View = class {
  598. constructor(owner) {
  599. this._owner = owner;
  600. this._ready = false;
  601. this._path = null;
  602. this._properties = new Map();
  603. this._dispatch = [];
  604. const dirname = path.dirname(url.fileURLToPath(import.meta.url));
  605. const size = electron.screen.getPrimaryDisplay().workAreaSize;
  606. const options = {
  607. show: false,
  608. title: electron.app.name,
  609. backgroundColor: electron.nativeTheme.shouldUseDarkColors ? '#1e1e1e' : '#ececec',
  610. icon: electron.nativeImage.createFromPath(path.join(dirname, 'icon.png')),
  611. minWidth: 600,
  612. minHeight: 600,
  613. width: size.width > 1024 ? 1024 : size.width,
  614. height: size.height > 768 ? 768 : size.height,
  615. webPreferences: {
  616. preload: path.join(dirname, 'electron.mjs'),
  617. nodeIntegration: true
  618. }
  619. };
  620. if (owner.application.environment.titlebar) {
  621. options.frame = false;
  622. options.thickFrame = true;
  623. options.titleBarStyle = 'hiddenInset';
  624. }
  625. if (!this._owner.empty && app.View._position && app.View._position.length === 2) {
  626. options.x = app.View._position[0] + 30;
  627. options.y = app.View._position[1] + 30;
  628. if (options.x + options.width > size.width) {
  629. options.x = 0;
  630. }
  631. if (options.y + options.height > size.height) {
  632. options.y = 0;
  633. }
  634. }
  635. this._window = new electron.BrowserWindow(options);
  636. app.View._position = this._window.getPosition();
  637. this._window.on('close', () => this._owner.closeView(this));
  638. this._window.on('focus', () => this.emit('activated'));
  639. this._window.on('blur', () => this.emit('deactivated'));
  640. this._window.on('minimize', () => this.state());
  641. this._window.on('restore', () => this.state());
  642. this._window.on('maximize', () => this.state());
  643. this._window.on('unmaximize', () => this.state());
  644. this._window.on('enter-full-screen', () => this.state('enter-full-screen'));
  645. this._window.on('leave-full-screen', () => this.state('leave-full-screen'));
  646. this._window.webContents.on('did-finish-load', () => {
  647. this._didFinishLoad = true;
  648. });
  649. this._window.webContents.setWindowOpenHandler((detail) => {
  650. const url = detail.url;
  651. if (url.startsWith('http://') || url.startsWith('https://')) {
  652. electron.shell.openExternal(url);
  653. }
  654. return { action: 'deny' };
  655. });
  656. this._window.once('ready-to-show', () => {
  657. this._window.show();
  658. });
  659. if (owner.application.environment.titlebar && process.platform !== 'darwin') {
  660. this._window.removeMenu();
  661. }
  662. this._loadURL();
  663. }
  664. get window() {
  665. return this._window;
  666. }
  667. get path() {
  668. return this._path;
  669. }
  670. open(path) {
  671. this._openPath = path;
  672. const location = app.Application.location(path);
  673. if (this._didFinishLoad) {
  674. this._window.webContents.send('open', location);
  675. } else {
  676. this._window.webContents.on('did-finish-load', () => {
  677. this._window.webContents.send('open', location);
  678. });
  679. this._loadURL();
  680. }
  681. }
  682. _loadURL() {
  683. const dirname = path.dirname(url.fileURLToPath(import.meta.url));
  684. const pathname = path.join(dirname, 'index.html');
  685. let content = fs.readFileSync(pathname, 'utf-8');
  686. content = content.replace(/<\s*script[^>]*>[\s\S]*?(<\s*\/script[^>]*>|$)/ig, '');
  687. const data = `data:text/html;charset=utf-8,${encodeURIComponent(content)}`;
  688. const options = {
  689. baseURLForDataURL: url.pathToFileURL(pathname).toString()
  690. };
  691. this._window.loadURL(data, options);
  692. }
  693. restore() {
  694. if (this._window) {
  695. if (this._window.isMinimized()) {
  696. this._window.restore();
  697. }
  698. this._window.show();
  699. }
  700. }
  701. match(path) {
  702. if (this._openPath) {
  703. return this._openPath === path;
  704. }
  705. return this._path === path;
  706. }
  707. execute(command, data) {
  708. if (this._dispatch) {
  709. this._dispatch.push({ command, data });
  710. } else if (this._window && this._window.webContents) {
  711. const window = this._window;
  712. const contents = window.webContents;
  713. switch (command) {
  714. case 'toggle-developer-tools':
  715. if (contents.isDevToolsOpened()) {
  716. contents.closeDevTools();
  717. } else {
  718. contents.openDevTools();
  719. }
  720. break;
  721. case 'fullscreen':
  722. window.setFullScreen(!window.isFullScreen());
  723. break;
  724. default:
  725. contents.send(command, data);
  726. break;
  727. }
  728. }
  729. }
  730. update(data) {
  731. for (const [name, value] of Object.entries(data)) {
  732. switch (name) {
  733. case 'path': {
  734. if (value) {
  735. this._path = value;
  736. const location = app.Application.location(this._path);
  737. const title = process.platform === 'darwin' ? location.label : `${location.label} - ${electron.app.name}`;
  738. this._window.setTitle(title);
  739. this._window.focus();
  740. }
  741. delete this._openPath;
  742. break;
  743. }
  744. default: {
  745. this._properties.set(name, value);
  746. }
  747. }
  748. }
  749. this.emit('updated');
  750. }
  751. get(name) {
  752. return this._properties.get(name);
  753. }
  754. on(event, callback) {
  755. this._events = this._events || {};
  756. this._events[event] = this._events[event] || [];
  757. this._events[event].push(callback);
  758. }
  759. emit(event, data) {
  760. if (this._events && this._events[event]) {
  761. for (const callback of this._events[event]) {
  762. callback(this, data);
  763. }
  764. }
  765. }
  766. state(event) {
  767. let fullscreen = false;
  768. switch (event) {
  769. case 'enter-full-screen': fullscreen = true; break;
  770. case 'leave-full-screen': fullscreen = false; break;
  771. default: fullscreen = this._window.isFullScreen(); break;
  772. }
  773. this.execute('window-state', {
  774. minimized: this._window.isMinimized(),
  775. maximized: this._window.isMaximized(),
  776. fullscreen
  777. });
  778. if (this._dispatch) {
  779. const dispatch = this._dispatch;
  780. delete this._dispatch;
  781. for (const obj of dispatch) {
  782. this.execute(obj.command, obj.data);
  783. }
  784. }
  785. }
  786. };
  787. app.ViewCollection = class {
  788. constructor(application) {
  789. this._application = application;
  790. this._views = new Map();
  791. electron.ipcMain.on('window-close', (event) => {
  792. const window = event.sender.getOwnerBrowserWindow();
  793. window.close();
  794. event.returnValue = null;
  795. });
  796. electron.ipcMain.on('window-toggle', (event) => {
  797. const window = event.sender.getOwnerBrowserWindow();
  798. if (window.isFullScreen()) {
  799. window.setFullScreen(false);
  800. } else if (window.isMaximized()) {
  801. window.unmaximize();
  802. } else {
  803. window.maximize();
  804. }
  805. event.returnValue = null;
  806. });
  807. electron.ipcMain.on('window-minimize', (event) => {
  808. const window = event.sender.getOwnerBrowserWindow();
  809. window.minimize();
  810. event.returnValue = null;
  811. });
  812. electron.ipcMain.on('window-update', (event, data) => {
  813. const window = event.sender.getOwnerBrowserWindow();
  814. if (this._views.has(window)) {
  815. const view = this._views.get(window);
  816. view.update(data);
  817. }
  818. event.returnValue = null;
  819. });
  820. electron.ipcMain.on('update-window-state', (event) => {
  821. const window = event.sender.getOwnerBrowserWindow();
  822. if (this._views.has(window)) {
  823. this._views.get(window).state();
  824. }
  825. event.returnValue = null;
  826. });
  827. }
  828. get application() {
  829. return this._application;
  830. }
  831. get views() {
  832. return this._views.values();
  833. }
  834. get empty() {
  835. return this._views.size === 0;
  836. }
  837. get(window) {
  838. return this._views.get(window);
  839. }
  840. openView() {
  841. const view = new app.View(this);
  842. view.on('activated', (view) => {
  843. this._activeView = view;
  844. this.emit('active-view-changed', { activeView: this._activeView });
  845. });
  846. view.on('updated', () => {
  847. this.emit('active-view-updated', { activeView: this._activeView });
  848. });
  849. view.on('deactivated', () => {
  850. this._activeView = null;
  851. this.emit('active-view-changed', { activeView: this._activeView });
  852. });
  853. this._views.set(view.window, view);
  854. this._updateActiveView();
  855. return view;
  856. }
  857. closeView(view) {
  858. this._views.delete(view.window);
  859. this._updateActiveView();
  860. }
  861. first() {
  862. return this.empty ? null : this._views.values().next().value;
  863. }
  864. get activeView() {
  865. return this._activeView;
  866. }
  867. on(event, callback) {
  868. this._events = this._events || {};
  869. this._events[event] = this._events[event] || [];
  870. this._events[event].push(callback);
  871. }
  872. emit(event, data) {
  873. if (this._events && this._events[event]) {
  874. for (const callback of this._events[event]) {
  875. callback(this, data);
  876. }
  877. }
  878. }
  879. _updateActiveView() {
  880. const window = electron.BrowserWindow.getFocusedWindow();
  881. const view = window && this._views.has(window) ? this._views.get(window) : null;
  882. if (view !== this._activeView) {
  883. this._activeView = view;
  884. this.emit('active-view-changed', { activeView: this._activeView });
  885. }
  886. }
  887. };
  888. app.ConfigurationService = class {
  889. constructor() {
  890. const dir = electron.app.getPath('userData');
  891. if (dir && dir.length > 0) {
  892. this._file = path.join(dir, 'configuration.json');
  893. }
  894. }
  895. open() {
  896. this._content = { 'recents': [] };
  897. if (this._file && fs.existsSync(this._file)) {
  898. const data = fs.readFileSync(this._file, 'utf-8');
  899. if (data) {
  900. try {
  901. this._content = JSON.parse(data);
  902. if (Array.isArray(this._content.recents)) {
  903. this._content.recents = this._content.recents.map((recent) => typeof recent !== 'string' && recent && recent.path ? recent.path : recent);
  904. }
  905. } catch {
  906. // continue regardless of error
  907. }
  908. }
  909. }
  910. }
  911. save() {
  912. if (this._content && this._file) {
  913. const data = JSON.stringify(this._content, null, 2);
  914. fs.writeFileSync(this._file, data);
  915. }
  916. }
  917. has(name) {
  918. return this._content && Object.prototype.hasOwnProperty.call(this._content, name);
  919. }
  920. set(name, value) {
  921. this._content[name] = value;
  922. }
  923. get(name) {
  924. return this._content[name];
  925. }
  926. delete(name) {
  927. delete this._content[name];
  928. }
  929. };
  930. app.MenuService = class {
  931. constructor(views) {
  932. this._views = views;
  933. }
  934. build(menuTemplate, commandTable) {
  935. this._menuTemplate = menuTemplate;
  936. this._commandTable = commandTable;
  937. this._itemTable = new Map();
  938. for (const menu of menuTemplate) {
  939. for (const item of menu.submenu) {
  940. if (item.id) {
  941. if (!item.label) {
  942. item.label = '';
  943. }
  944. this._itemTable.set(item.id, item);
  945. }
  946. }
  947. }
  948. this._rebuild();
  949. }
  950. update() {
  951. if (!this._menu && !this._commandTable) {
  952. return;
  953. }
  954. const view = this._views.activeView;
  955. if (this._updateLabel(view)) {
  956. this._rebuild();
  957. }
  958. this._updateEnabled(view);
  959. }
  960. _rebuild() {
  961. if (process.platform === 'darwin') {
  962. this._menu = electron.Menu.buildFromTemplate(this._menuTemplate);
  963. electron.Menu.setApplicationMenu(this._menu);
  964. } else if (!this._views.application.environment.titlebar) {
  965. this._menu = electron.Menu.buildFromTemplate(this._menuTemplate);
  966. for (const view of this._views.views) {
  967. view.window.setMenu(this._menu);
  968. }
  969. }
  970. }
  971. _updateLabel(view) {
  972. let rebuild = false;
  973. for (const [name, command] of this._commandTable.entries()) {
  974. if (this._menu) {
  975. const item = this._menu.getMenuItemById(name);
  976. if (command && command.label) {
  977. const label = command.label(view);
  978. if (label !== item.label) {
  979. if (this._itemTable.has(name)) {
  980. this._itemTable.get(name).label = label;
  981. rebuild = true;
  982. }
  983. }
  984. }
  985. }
  986. }
  987. return rebuild;
  988. }
  989. _updateEnabled(view) {
  990. for (const [name, command] of this._commandTable.entries()) {
  991. if (this._menu) {
  992. const item = this._menu.getMenuItemById(name);
  993. if (item && command.enabled) {
  994. item.enabled = command.enabled(view);
  995. }
  996. }
  997. }
  998. }
  999. };
  1000. try {
  1001. global.application = new app.Application();
  1002. await global.application.start();
  1003. } catch (error) {
  1004. /* eslint-disable no-console */
  1005. console.error(error.message);
  1006. /* eslint-enable no-console */
  1007. }