app.js 38 KB

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