OSDN Git Service

v1.6.9
[serene/MyBrowser.git] / app / electron / WindowManager.js
1 const { app, ipcMain, protocol, session, BrowserWindow, BrowserView, Menu, nativeImage, clipboard, dialog, Notification } = require('electron');
2 const path = require('path');
3 const { parse, format } = require('url');
4 const os = require('os');
5
6 const pkg = require(`${app.getAppPath()}/package.json`);
7 const protocolStr = 'flast';
8 const fileProtocolStr = `${protocolStr}-file`;
9
10 const platform = require('electron-platform');
11 const localShortcut = require('electron-localshortcut');
12
13 const Config = require('electron-store');
14 const config = new Config({
15     defaults: {
16         design: {
17             homeButton: false,
18             darkTheme: false,
19             theme: 'default'
20         },
21         homePage: {
22             defaultPage: `${protocolStr}://home`,
23             defaultEngine: 'Google',
24             searchEngines: [
25                 {
26                     name: 'Google',
27                     url: 'https://www.google.com/search?q=%s'
28                 },
29                 {
30                     name: 'Bing',
31                     url: 'https://www.bing.com/search?q=%s'
32                 },
33                 {
34                     name: 'Yahoo! Japan',
35                     url: 'https://search.yahoo.co.jp/search?p=%s'
36                 },
37                 {
38                     name: 'goo',
39                     url: 'https://search.goo.ne.jp/web.jsp?MT=%s'
40                 },
41                 {
42                     name: 'Baido',
43                     url: 'https://www.baidu.com/s?wd=%s'
44                 },
45                 {
46                     name: 'Google Translate',
47                     url: 'https://translate.google.com/?text=%s'
48                 },
49                 {
50                     name: 'Youtube',
51                     url: 'https://www.youtube.com/results?search_query=%s'
52                 },
53                 {
54                     name: 'Twitter',
55                     url: 'https://www.twitter.com/search?q=%s'
56                 },
57                 {
58                     name: 'GitHub',
59                     url: 'https://github.com/search?q=%s'
60                 }
61             ]
62         },
63         adBlocker: true,
64         window: {
65             isCustomTitlebar: true,
66             isMaximized: false,
67             bounds: {
68                 width: 1100,
69                 height: 680
70             }
71         }
72     },
73 });
74
75 const Datastore = require('nedb');
76 let db = {};
77 db.pageSettings = new Datastore({
78     filename: path.join(app.getPath('userData'), 'Files', 'PageSettings.db'),
79     autoload: true,
80     timestampData: true
81 });
82
83 db.historys = new Datastore({
84     filename: path.join(app.getPath('userData'), 'Files', 'History.db'),
85     autoload: true,
86     timestampData: true
87 });
88 db.downloads = new Datastore({
89     filename: path.join(app.getPath('userData'), 'Files', 'Download.db'),
90     autoload: true,
91     timestampData: true
92 });
93 db.bookmarks = new Datastore({
94     filename: path.join(app.getPath('userData'), 'Files', 'Bookmarks.db'),
95     autoload: true,
96     timestampData: true
97 });
98
99 db.apps = new Datastore({
100     filename: path.join(app.getPath('userData'), 'Files', 'Apps.db'),
101     autoload: true,
102     timestampData: true
103 });
104
105 const { loadFilters, updateFilters, removeAds } = require('./AdBlocker');
106
107 let floatingWindows = [];
108 let views = [];
109 let tabCount = 0;
110
111 getBaseWindow = (width = 1100, height = 680, minWidth = 320, minHeight = 200, x, y, frame = false) => {
112     return new BrowserWindow({
113         width, height, minWidth, minHeight, x, y, 'titleBarStyle': 'hidden', frame, fullscreenable: true,
114         show: false,
115         icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
116         webPreferences: {
117             nodeIntegration: true,
118             webviewTag: true,
119             plugins: true,
120             experimentalFeatures: true,
121             contextIsolation: false,
122         }
123     });
124 }
125
126 loadSessionAndProtocol = () => {
127     const ses = session.defaultSession;
128
129     setPermissionRequestHandler(ses, false);
130
131     protocol.isProtocolHandled(protocolStr, (handled) => {
132         if (!handled) {
133             protocol.registerFileProtocol(protocolStr, (request, callback) => {
134                 const parsed = parse(request.url);
135
136                 return callback({
137                     path: path.join(app.getAppPath(), 'pages', `${parsed.hostname}.html`),
138                 });
139             }, (error) => {
140                 if (error) console.error('Failed to register protocol: ' + error);
141             });
142         }
143     });
144
145     protocol.isProtocolHandled(fileProtocolStr, (handled) => {
146         if (!handled) {
147             protocol.registerFileProtocol(fileProtocolStr, (request, callback) => {
148                 const parsed = parse(request.url);
149
150                 return callback({
151                     path: path.join(app.getAppPath(), 'pages', 'static', parsed.pathname),
152                 });
153             }, (error) => {
154                 if (error) console.error('Failed to register protocol: ' + error);
155             });
156         }
157     });
158 }
159
160 loadSessionAndProtocolWithPrivateMode = (windowId) => {
161     const ses = session.fromPartition(windowId);
162     ses.setUserAgent(ses.getUserAgent().replace(/ Electron\/[0-9\.]*/g, '') + ' PrivMode');
163
164     setPermissionRequestHandler(ses, true);
165
166     ses.protocol.registerFileProtocol(protocolStr, (request, callback) => {
167         const parsed = parse(request.url);
168
169         return callback({
170             path: path.join(app.getAppPath(), 'pages', `${parsed.hostname}.html`),
171         });
172     }, (error) => {
173         if (error) console.error('Failed to register protocol: ' + error);
174     });
175
176     ses.protocol.registerFileProtocol(fileProtocolStr, (request, callback) => {
177         const parsed = parse(request.url);
178
179         return callback({
180             path: path.join(app.getAppPath(), 'pages', 'static', parsed.pathname),
181         });
182     }, (error) => {
183         if (error) console.error('Failed to register protocol: ' + error);
184     });
185 }
186
187 setPermissionRequestHandler = (ses, isPrivate = false) => {
188     if (!isPrivate) {
189         ses.setPermissionRequestHandler((webContents, permission, callback) => {
190             db.pageSettings.findOne({ origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}` }, (err, doc) => {
191                 if (doc != undefined) {
192                     if (permission == 'media' && doc.media != undefined && doc.media > -1)
193                         return callback(doc.media === 0);
194                     if (permission == 'geolocation' && doc.geolocation != undefined && doc.geolocation > -1)
195                         return callback(doc.geolocation === 0);
196                     if (permission == 'notifications' && doc.notifications != undefined && doc.notifications > -1)
197                         return callback(doc.notifications === 0);
198                     if (permission == 'midiSysex' && doc.midiSysex != undefined && doc.midiSysex > -1)
199                         return callback(doc.midiSysex === 0);
200                     if (permission == 'pointerLock' && doc.pointerLock != undefined && doc.pointerLock > -1)
201                         return callback(doc.pointerLock === 0);
202                     if (permission == 'fullscreen' && doc.fullscreen != undefined && doc.fullscreen > -1)
203                         return callback(doc.fullscreen === 0);
204                     if (permission == 'openExternal' && doc.openExternal != undefined && doc.openExternal > -1)
205                         return callback(doc.openExternal === 0);
206                 } else {
207                     dialog.showMessageBox({
208                         type: 'info',
209                         title: '権限の要求',
210                         message: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname} が権限を要求しています。`,
211                         detail: `要求内容: ${permission}`,
212                         checkboxLabel: 'このサイトでは今後も同じ処理をする',
213                         checkboxChecked: false,
214                         noLink: true,
215                         buttons: ['Yes', 'No'],
216                         defaultId: 0,
217                         cancelId: 1
218                     }, (res, checked) => {
219                         console.log(res, checked);
220                         if (checked) {
221                             if (permission == 'media')
222                                 db.pageSettings.update({ origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}` }, { origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}`, media: res }, { upsert: true });
223                             if (permission == 'geolocation')
224                                 db.pageSettings.update({ origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}` }, { origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}`, geolocation: res }, { upsert: true });
225                             if (permission == 'notifications')
226                                 db.pageSettings.update({ origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}` }, { origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}`, notifications: res }, { upsert: true });
227                             if (permission == 'midiSysex')
228                                 db.pageSettings.update({ origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}` }, { origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}`, midiSysex: res }, { upsert: true });
229                             if (permission == 'pointerLock')
230                                 db.pageSettings.update({ origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}` }, { origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}`, pointerLock: res }, { upsert: true });
231                             if (permission == 'fullscreen')
232                                 db.pageSettings.update({ origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}` }, { origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}`, fullscreen: res }, { upsert: true });
233                             if (permission == 'openExternal')
234                                 db.pageSettings.update({ origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}` }, { origin: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname}`, openExternal: res }, { upsert: true });
235                         }
236                         return callback(res === 0);
237                     });
238                 }
239             });
240         });
241     } else {
242         ses.setPermissionRequestHandler((webContents, permission, callback) => {
243             dialog.showMessageBox({
244                 type: 'info',
245                 title: '権限の要求',
246                 message: `${parse(webContents.getURL()).protocol}//${parse(webContents.getURL()).hostname} が権限を要求しています。`,
247                 detail: `要求内容: ${permission}`,
248                 noLink: true,
249                 buttons: ['Yes', 'No'],
250                 defaultId: 0,
251                 cancelId: 1
252             }, (res) => {
253                 return callback(res === 0);
254             });
255         });
256     }
257 }
258
259 module.exports = class WindowManager {
260     constructor() {
261         this.windows = new Map();
262
263         ipcMain.on('window-add', (e, args) => {
264             this.addWindow(args.isPrivate);
265         });
266
267         ipcMain.on('update-filters', (e, args) => {
268             updateFilters();
269         });
270
271         ipcMain.on('data-history-get', (e, args) => {
272             db.historys.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
273                 e.sender.send('data-history-get', { historys: docs });
274             });
275         });
276
277         ipcMain.on('data-history-clear', (e, args) => {
278             db.historys.remove({}, { multi: true });
279         });
280
281         ipcMain.on('data-downloads-get', (e, args) => {
282             db.downloads.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
283                 e.sender.send('data-downloads-get', { downloads: docs });
284             });
285         });
286
287         ipcMain.on('data-downloads-clear', (e, args) => {
288             db.downloads.remove({}, { multi: true });
289         });
290
291         ipcMain.on('data-bookmarks-get', (e, args) => {
292             db.bookmarks.find({ isPrivate: args.isPrivate }).sort({ createdAt: -1 }).exec((err, docs) => {
293                 e.sender.send('data-bookmarks-get', { bookmarks: docs });
294             });
295         });
296
297         ipcMain.on('data-bookmarks-clear', (e, args) => {
298             db.bookmarks.remove({}, { multi: true });
299         });
300
301         ipcMain.on('data-apps-add', (e, args) => {
302             db.apps.update({ id: args.id }, { id: args.id, name: args.name, description: args.description, url: args.url }, { upsert: true });
303
304             db.apps.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
305                 console.log(docs)
306             });
307         });
308
309         ipcMain.on('data-apps-remove', (e, args) => {
310             db.apps.remove({ id: args.id }, {});
311         });
312
313         ipcMain.on('data-apps-get', (e, args) => {
314             db.apps.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
315                 e.sender.send('data-apps-get', { apps: docs });
316             });
317         });
318
319         ipcMain.on('data-apps-is', (e, args) => {
320             db.apps.find({ id: args.id }).exec((err, docs) => {
321                 e.sender.send('data-apps-is', { id: args.id, isInstalled: (docs.length > 0 ? true : false) });
322             });
323         });
324
325         ipcMain.on('data-apps-clear', (e, args) => {
326             db.apps.remove({}, { multi: true });
327         });
328
329         ipcMain.on('clear-browsing-data', () => {
330             const ses = session.defaultSession;
331             ses.clearCache((err) => {
332                 if (err) log.error(err);
333             });
334
335             ses.clearStorageData({
336                 storages: [
337                     'appcache',
338                     'cookies',
339                     'filesystem',
340                     'indexdb',
341                     'localstorage',
342                     'shadercache',
343                     'websql',
344                     'serviceworkers',
345                     'cachestorage',
346                 ],
347             });
348
349             config.clear();
350             db.historys.remove({}, { multi: true });
351             db.downloads.remove({}, { multi: true });
352             db.bookmarks.remove({}, { multi: true });
353             db.apps.remove({}, { multi: true });
354         });
355     }
356
357     addWindow = (isPrivate = false) => {
358         loadSessionAndProtocol();
359         loadFilters();
360
361         const { width, height, x, y } = config.get('window.bounds');
362         const window = getBaseWindow(config.get('window.isMaximized') ? 1110 : width, config.get('window.isMaximized') ? 680 : height, 320, 200, x, y, !config.get('window.isCustomTitlebar'));
363
364         const id = (!isPrivate ? `window-${window.id}` : `private-${window.id}`);
365
366         config.get('window.isMaximized') && window.maximize();
367
368         const startUrl = process.env.ELECTRON_START_URL || format({
369             pathname: path.join(__dirname, '/../build/index.html'), // 警告:このファイルを移動する場合ここの相対パスの指定に注意してください
370             protocol: 'file:',
371             slashes: true,
372             hash: `/window/${id}`,
373         });
374
375         window.loadURL(startUrl);
376
377         window.once('ready-to-show', () => {
378             window.show();
379         });
380
381         window.on('closed', () => {
382             this.windows.delete(id);
383         });
384
385         /*
386         ['resize', 'move'].forEach(ev => {
387             window.on(ev, () => {
388                 config.set('window.isMaximized', window.isMaximized());
389                 config.set('window.bounds', window.getBounds());
390             })
391         });
392         */
393
394         window.on('close', (e) => {
395             delete views[id];
396
397             config.set('window.isMaximized', window.isMaximized());
398             config.set('window.bounds', window.getBounds());
399         });
400
401         window.on('maximize', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
402         window.on('unmaximize', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
403         window.on('enter-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
404         window.on('leave-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
405         window.on('enter-html-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
406         window.on('leave-html-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
407
408         // registerProtocols();
409         this.registerListeners(id);
410
411         localShortcut.register(window, 'CmdOrCtrl+Shift+I', () => {
412             if (window.getBrowserView() == undefined) return;
413             const view = window.getBrowserView();
414
415             if (view.webContents.isDevToolsOpened()) {
416                 view.webContents.devToolsWebContents.focus();
417             } else {
418                 view.webContents.openDevTools();
419             }
420         });
421
422         localShortcut.register(window, 'CmdOrCtrl+R', () => {
423             if (window.getBrowserView() == undefined) return;
424             const view = window.getBrowserView();
425
426             view.webContents.reload();
427         });
428
429         localShortcut.register(window, 'CmdOrCtrl+Shift+R', () => {
430             if (window.getBrowserView() == undefined) return;
431             const view = window.getBrowserView();
432
433             view.webContents.reloadIgnoringCache();
434         });
435
436         this.windows.set(id, window);
437
438         if (process.argv != undefined) {
439             window.webContents.send(`tab-add-${id}`, { url: process.argv[process.argv.length - 1] });
440         }
441     }
442
443     registerListeners = (id) => {
444         ipcMain.on(`browserview-add-${id}`, (e, args) => {
445             this.addView(id, args.url, args.isActive);
446         });
447
448         ipcMain.on(`browserview-remove-${id}`, (e, args) => {
449             this.removeView(id, args.id);
450         });
451
452         ipcMain.on(`browserview-select-${id}`, (e, args) => {
453             this.selectView(id, args.id);
454         });
455
456         ipcMain.on(`browserview-get-${id}`, (e, args) => {
457             let datas = [];
458             for (var i = 0; i < views[id].length; i++) {
459                 const url = views[id][i].view.webContents.getURL();
460
461                 datas.push({ id: views[id][i].id, title: views[id][i].view.webContents.getTitle(), url: url, icon: this.getFavicon(url) });
462             }
463             e.sender.send(`browserview-get-${id}`, { views: datas });
464         });
465
466         ipcMain.on(`browserview-goBack-${id}`, (e, args) => {
467             views[id].filter(function (view, i) {
468                 if (view.id == args.id) {
469                     let webContents = views[id][i].view.webContents;
470                     if (webContents.canGoBack())
471                         webContents.goBack();
472                 }
473             });
474         });
475
476         ipcMain.on(`browserview-goForward-${id}`, (e, args) => {
477             views[id].filter(function (view, i) {
478                 if (view.id == args.id) {
479                     let webContents = views[id][i].view.webContents;
480                     if (webContents.canGoForward())
481                         webContents.goForward();
482                 }
483             });
484         });
485
486         ipcMain.on(`browserview-reload-${id}`, (e, args) => {
487             views[id].filter(function (view, i) {
488                 if (view.id == args.id) {
489                     let webContents = views[id][i].view.webContents;
490                     webContents.reload();
491                 }
492             });
493         });
494
495         ipcMain.on(`browserview-stop-${id}`, (e, args) => {
496             views[id].filter(function (view, i) {
497                 if (view.id == args.id) {
498                     let webContents = views[id][i].view.webContents;
499                     webContents.stop();
500                 }
501             });
502         });
503
504         ipcMain.on(`browserview-goHome-${id}`, (e, args) => {
505             views[id].filter(function (view, i) {
506                 if (view.id == args.id) {
507                     let webContents = views[id][i].view.webContents;
508                     webContents.loadURL(config.get('homePage.defaultPage'));
509                 }
510             });
511         });
512
513         ipcMain.on(`browserview-loadURL-${id}`, (e, args) => {
514             views[id].filter(function (view, i) {
515                 if (view.id == args.id) {
516                     let webContents = views[id][i].view.webContents;
517                     webContents.loadURL(args.url);
518                 }
519             });
520         });
521
522         ipcMain.on(`browserview-loadFile-${id}`, (e, args) => {
523             views[id].filter(function (view, i) {
524                 if (view.id == args.id) {
525                     let webContents = views[id][i].view.webContents;
526                     webContents.loadFile(args.url);
527                 }
528             });
529         });
530
531         ipcMain.on(`data-bookmark-add-${id}`, (e, args) => {
532             views[id].filter((view, i) => {
533                 if (view.id == args.id) {
534                     let v = views[id][i].view;
535                     db.bookmarks.insert({ title: v.webContents.getTitle(), url: v.webContents.getURL(), isPrivate: args.isPrivate });
536                     this.updateBookmarkState(id, args.id, v);
537                 }
538             });
539         });
540
541         ipcMain.on(`data-bookmark-remove-${id}`, (e, args) => {
542             views[id].filter((view, i) => {
543                 if (view.id == args.id) {
544                     let v = views[id][i].view;
545                     db.bookmarks.remove({ url: v.webContents.getURL(), isPrivate: args.isPrivate }, {});
546                     this.updateBookmarkState(id, args.id, v);
547                 }
548             });
549         });
550
551         ipcMain.on(`data-bookmark-has-${id}`, (e, args) => {
552             views[id].filter((view, i) => {
553                 if (view.id == args.id) {
554                     let v = views[id][i].view;
555                     db.bookmarks.find({ url: v.webContents.getURL(), isPrivate: args.isPrivate }, (err, docs) => {
556                         e.sender.send(`data-bookmark-has-${id}`, { isBookmarked: (docs.length > 0 ? true : false) });
557                     });
558                 }
559             });
560         });
561     }
562
563     getFavicon = (url) => {
564         const parsed = parse(url);
565         return url.startsWith(`${protocolStr}://`) || url.startsWith(`${fileProtocolStr}://`) ? undefined : `https://www.google.com/s2/favicons?domain=${parsed.protocol}//${parsed.hostname}`;
566     }
567
568     updateNavigationState = (windowId, id, view) => {
569         const window = this.windows.get(windowId);
570         window.webContents.send(`update-navigation-state-${windowId}`, {
571             id: id,
572             canGoBack: view.webContents.canGoBack(),
573             canGoForward: view.webContents.canGoForward(),
574         });
575     }
576
577     updateBookmarkState = (windowId, id, view) => {
578         const window = this.windows.get(windowId);
579         db.bookmarks.find({ url: view.webContents.getURL(), isPrivate: (String(windowId).startsWith('private')) }, (err, docs) => {
580             const url = view.webContents.getURL();
581             window.webContents.send(`browserview-load-${windowId}`, { id: id, title: view.webContents.getTitle(), url: url, icon: this.getFavicon(url), isBookmarked: (docs.length > 0 ? true : false) });
582         });
583     }
584
585     fixBounds = (windowId, isFloating = false) => {
586         const window = this.windows.get(windowId);
587
588         if (window.getBrowserView() == undefined) return;
589         const view = window.getBrowserView();
590
591         const { width, height } = window.getContentBounds();
592
593         view.setAutoResize({ width: true, height: true });
594         if (isFloating) {
595             window.setMinimizable(false);
596             window.setMaximizable(false);
597             window.setAlwaysOnTop(true);
598             window.setVisibleOnAllWorkspaces(true);
599             view.setBounds({
600                 x: 1,
601                 y: 1,
602                 width: width - 2,
603                 height: height - 2,
604             });
605         } else {
606             window.setMinimizable(true);
607             window.setMaximizable(true);
608             window.setAlwaysOnTop(false);
609             window.setVisibleOnAllWorkspaces(false);
610             if (window.isFullScreen()) {
611                 view.setBounds({
612                     x: 0,
613                     y: 0,
614                     width: width,
615                     height: height,
616                 });
617             } else {
618                 view.setBounds({
619                     x: 1,
620                     y: 73 + 1,
621                     width: width - 2,
622                     height: window.isMaximized() ? height - 73 : (height - 73) - 2,
623                 });
624             }
625         }
626         view.setAutoResize({ width: true, height: true });
627     }
628
629     addView = (windowId, url, isActive) => {
630         if (String(windowId).startsWith('private')) {
631             loadSessionAndProtocolWithPrivateMode(windowId);
632         }
633
634         const id = tabCount++;
635         this.addTab(windowId, id, url, isActive);
636     }
637
638     removeView = (windowId, id) => {
639         views[windowId].filter((view, i) => {
640             if (view.id == id) {
641                 const index = i;
642
643                 if (index + 1 < views[windowId].length) {
644                     this.selectView2(windowId, index + 1);
645                 } else if (index - 1 >= 0) {
646                     this.selectView2(windowId, index - 1);
647                 }
648
649                 views[windowId][index].view.destroy();
650                 views[windowId].splice(index, 1);
651             }
652         });
653     }
654
655     selectView = (windowId, id) => {
656         const window = this.windows.get(windowId);
657         views[windowId].filter((view, i) => {
658             if (id == view.id) {
659                 window.setBrowserView(views[windowId][i].view);
660                 window.setTitle(views[windowId][i].view.webContents.getTitle());
661                 window.webContents.send(`browserview-set-${windowId}`, { id: id });
662                 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
663             }
664         });
665     }
666
667     selectView2 = (windowId, i) => {
668         const window = this.windows.get(windowId);
669         const item = views[windowId][i];
670
671         window.setBrowserView(item.view);
672         window.setTitle(item.view.webContents.getTitle());
673         window.webContents.send(`browserview-set-${windowId}`, { id: item.id });
674         this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
675     }
676
677     getViews = (windowId) => {
678         let datas = [];
679         for (var i = 0; i < views[windowId].length; i++) {
680             const url = views[windowId][i].view.webContents.getURL();
681
682             datas.push({ id: views[windowId][i].id, title: views[windowId][i].view.webContents.getTitle(), url: url, icon: this.getFavicon(url) });
683         }
684         const window = this.windows.get(windowId);
685         window.webContents.send(`browserview-get-${windowId}`, { views: datas });
686     }
687
688     addTab = (windowId, id, url = config.get('homePage.defaultPage'), isActive = true) => {
689         const window = this.windows.get(windowId);
690
691         const view = new BrowserView({
692             webPreferences: {
693                 nodeIntegration: false,
694                 contextIsolation: false,
695                 plugins: true,
696                 experimentalFeatures: true,
697                 safeDialogs: true,
698                 safeDialogsMessage: '今後このページではダイアログを表示しない',
699                 ...(String(windowId).startsWith('private') && { partition: windowId }),
700                 preload: require.resolve('./Preload')
701             }
702         });
703
704         view.webContents.on('did-start-loading', () => {
705             if (view.isDestroyed()) return;
706
707             window.webContents.send(`browserview-start-loading-${windowId}`, { id: id });
708         });
709         view.webContents.on('did-stop-loading', () => {
710             if (view.isDestroyed()) return;
711
712             window.webContents.send(`browserview-stop-loading-${windowId}`, { id: id });
713         });
714
715         view.webContents.on('did-finish-load', (e) => {
716             if (view.isDestroyed()) return;
717
718             window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
719             this.updateBookmarkState(windowId, id, view);
720
721             this.updateNavigationState(windowId, id, view);
722         });
723
724         view.webContents.on('did-start-navigation', (e) => {
725             if (view.isDestroyed()) return;
726
727             const url = view.webContents.getURL();
728
729             if (config.get('adBlocker') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
730                 removeAds(url, view.webContents);
731
732             this.updateNavigationState(windowId, id, view);
733         });
734
735         view.webContents.on('page-title-updated', (e) => {
736             if (view.isDestroyed()) return;
737
738             window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
739             this.updateBookmarkState(windowId, id, view);
740
741             if (!String(windowId).startsWith('private') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
742                 db.historys.insert({ title: view.webContents.getTitle(), url: view.webContents.getURL() });
743
744             this.updateNavigationState(windowId, id, view);
745         });
746
747         view.webContents.on('page-favicon-updated', (e, favicons) => {
748             if (view.isDestroyed()) return;
749
750             window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
751             db.bookmarks.find({ url: view.webContents.getURL(), isPrivate: (String(windowId).startsWith('private')) }, (err, docs) => {
752                 const url = view.webContents.getURL();
753                 window.webContents.send(`browserview-load-${windowId}`, { id: id, title: view.webContents.getTitle(), url: url, icon: this.getFavicon(url), isBookmarked: (docs.length > 0 ? true : false) });
754             });
755
756             this.updateNavigationState(windowId, id, view);
757         });
758
759         view.webContents.on('did-change-theme-color', (e, color) => {
760             if (view.isDestroyed()) return;
761
762             window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
763             db.bookmarks.find({ url: view.webContents.getURL(), isPrivate: (String(windowId).startsWith('private')) }, (err, docs) => {
764                 window.webContents.send(`browserview-load-${windowId}`, { id: id, title: view.webContents.getTitle(), url: view.webContents.getURL(), color: color, isBookmarked: (docs.length > 0 ? true : false) });
765             });
766         });
767
768         view.webContents.on('new-window', (e, url) => {
769             if (view.isDestroyed()) return;
770
771             e.preventDefault();
772             this.addView(windowId, url, true);
773         });
774
775         view.webContents.on('certificate-error', (e, url, error, certificate, callback) => {
776             e.preventDefault();
777             // callback(true);
778             const dlg = dialog.showMessageBox({
779                 type: 'warning',
780                 title: 'プライバシーエラー',
781                 message: 'この接続ではプライバシーが保護されません',
782                 detail: `${parse(url).hostname} の証明書を信頼することができませんでした。\n信頼できるページに戻ることをおすすめします。\nこのまま閲覧することも可能ですが安全ではありません。`,
783                 noLink: true,
784                 buttons: ['続行', 'キャンセル'],
785                 defaultId: 1,
786                 cancelId: 1
787             });
788             // e.preventDefault();
789             callback(dlg === 0);
790         });
791
792         view.webContents.on('context-menu', (e, params) => {
793             if (view.isDestroyed()) return;
794
795             const menu = Menu.buildFromTemplate(
796                 [
797                     ...(params.linkURL !== '' ?
798                         [
799                             {
800                                 label: '新しいタブで開く',
801                                 click: () => {
802                                     this.addView(windowId, params.linkURL, true);
803                                 }
804                             },
805                             {
806                                 label: '新しいウィンドウで開く',
807                                 enabled: false,
808                                 click: () => { view.webContents.openDevTools(); }
809                             },
810                             {
811                                 label: 'プライベート ウィンドウで開く',
812                                 enabled: false,
813                                 click: () => { view.webContents.openDevTools(); }
814                             },
815                             { type: 'separator' },
816                             {
817                                 label: 'リンクをコピー',
818                                 accelerator: 'CmdOrCtrl+C',
819                                 click: () => {
820                                     clipboard.clear();
821                                     clipboard.writeText(params.linkURL);
822                                 }
823                             },
824                             { type: 'separator' }
825                         ] : []),
826                     ...(params.hasImageContents ?
827                         [
828                             {
829                                 label: '新しいタブで画像を開く',
830                                 click: () => {
831                                     this.addView(windowId, params.srcURL, true);
832                                 }
833                             },
834                             {
835                                 label: '画像をコピー',
836                                 click: () => {
837                                     const img = nativeImage.createFromDataURL(params.srcURL);
838
839                                     clipboard.clear();
840                                     clipboard.writeImage(img);
841                                 }
842                             },
843                             {
844                                 label: '画像アドレスをコピー',
845                                 click: () => {
846                                     clipboard.clear();
847                                     clipboard.writeText(params.srcURL);
848                                 }
849                             },
850                             { type: 'separator' }
851                         ] : []),
852                     ...(params.isEditable ?
853                         [
854                             {
855                                 label: '元に戻す',
856                                 accelerator: 'CmdOrCtrl+Z',
857                                 enabled: params.editFlags.canUndo,
858                                 click: () => { view.webContents.undo(); }
859                             },
860                             {
861                                 label: 'やり直す',
862                                 accelerator: 'CmdOrCtrl+Y',
863                                 enabled: params.editFlags.canRedo,
864                                 click: () => { view.webContents.redo(); }
865                             },
866                             { type: 'separator' },
867                             {
868                                 label: '切り取り',
869                                 accelerator: 'CmdOrCtrl+X',
870                                 enabled: params.editFlags.canCut,
871                                 click: () => { view.webContents.cut(); }
872                             },
873                             {
874                                 label: 'コピー',
875                                 accelerator: 'CmdOrCtrl+C',
876                                 enabled: params.editFlags.canCopy,
877                                 click: () => { view.webContents.copy(); }
878                             },
879                             {
880                                 label: '貼り付け',
881                                 accelerator: 'CmdOrCtrl+V',
882                                 enabled: params.editFlags.canPaste,
883                                 click: () => { view.webContents.paste(); }
884                             },
885                             { type: 'separator' },
886                             {
887                                 label: 'すべて選択',
888                                 accelerator: 'CmdOrCtrl+A',
889                                 enabled: params.editFlags.canSelectAll,
890                                 click: () => { view.webContents.selectAll(); }
891                             },
892                             { type: 'separator' }
893                         ] : []),
894                     ...(params.selectionText !== '' && !params.isEditable ?
895                         [
896                             {
897                                 label: 'コピー',
898                                 accelerator: 'CmdOrCtrl+C',
899                                 click: () => { view.webContents.copy(); }
900                             },
901                             {
902                                 label: `Googleで「${params.selectionText}」を検索`,
903                                 click: () => {
904                                     this.addView(windowId, `https://www.google.co.jp/search?q=${params.selectionText}`, true);
905                                 }
906                             },
907                             { type: 'separator' }
908                         ] : []),
909                     {
910                         label: '戻る',
911                         accelerator: 'Alt+Left',
912                         icon: view.webContents.canGoBack() ? `${app.getAppPath()}/static/arrow_back.png` : `${app.getAppPath()}/static/arrow_back_inactive.png`,
913                         enabled: view.webContents.canGoBack(),
914                         click: () => { view.webContents.goBack(); }
915                     },
916                     {
917                         label: '進む',
918                         accelerator: 'Alt+Right',
919                         icon: view.webContents.canGoForward() ? `${app.getAppPath()}/static/arrow_forward.png` : `${app.getAppPath()}/static/arrow_forward_inactive.png`,
920                         enabled: view.webContents.canGoForward(),
921                         click: () => { view.webContents.goForward(); }
922                     },
923                     {
924                         label: !view.webContents.isLoadingMainFrame() ? '再読み込み' : '読み込み中止',
925                         accelerator: 'CmdOrCtrl+R',
926                         icon: !view.webContents.isLoadingMainFrame() ? `${app.getAppPath()}/static/refresh.png` : `${app.getAppPath()}/static/close.png`,
927                         click: () => { !view.webContents.isLoadingMainFrame() ? view.webContents.reload() : view.webContents.stop(); }
928                     },
929                     { type: 'separator' },
930                     {
931                         label: 'Floating Window (Beta)',
932                         type: 'checkbox',
933                         checked: (floatingWindows.indexOf(windowId) != -1),
934                         enabled: (!window.isFullScreen() && !window.isMaximized() && config.get('window.isCustomTitlebar')),
935                         click: () => {
936                             if (floatingWindows.indexOf(windowId) != -1) {
937                                 floatingWindows.filter((win, i) => {
938                                     if (windowId == win) {
939                                         floatingWindows.splice(i, 1);
940                                     }
941                                 });
942                             } else {
943                                 floatingWindows.push(windowId);
944                             }
945                             this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
946                         }
947                     },
948                     { type: 'separator' },
949                     {
950                         label: 'ページの保存',
951                         accelerator: 'CmdOrCtrl+S',
952                         enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
953                         click: () => {
954                             view.webContents.savePage(`${app.getPath('downloads')}/${view.webContents.getTitle()}.html`, 'HTMLComplete', (err) => {
955                                 if (!err) console.log('Page Save successfully');
956                             });
957                         }
958                     },
959                     {
960                         label: '印刷',
961                         accelerator: 'CmdOrCtrl+P',
962                         icon: `${app.getAppPath()}/static/print.png`,
963                         enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
964                         click: () => { view.webContents.print(); }
965                     },
966                     { type: 'separator' },
967                     {
968                         label: 'デベロッパーツール',
969                         accelerator: 'CmdOrCtrl+Shift+I',
970                         enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
971                         click: () => { if (view.webContents.isDevToolsOpened()) { view.webContents.devToolsWebContents.focus(); } else { view.webContents.openDevTools(); } }
972                     }
973                 ]
974             );
975
976             menu.popup();
977         });
978
979         view.webContents.on('before-input-event', (e, input) => {
980             if (view.isDestroyed()) return;
981
982             if (input.control || (platform.isDarwin && input.meta)) {
983                 console.log(input.control, input.alt, input.shift, input.key);
984
985                 if (input.shift && input.key == 'I') {
986                     e.preventDefault();
987                     if (view.webContents.isDevToolsOpened()) {
988                         view.webContents.devToolsWebContents.focus();
989                     } else {
990                         view.webContents.openDevTools();
991                     }
992                 } else if (input.shift && input.key == 'R') {
993                     e.preventDefault();
994                     view.webContents.reloadIgnoringCache();
995                 } else if (input.key == 'T') {
996                     e.preventDefault();
997                     this.addView(windowId, config.get('homePage.defaultPage'), true)
998                 }
999             } else if (input.alt) {
1000                 if (input.key == 'ArrowLeft') {
1001                     e.preventDefault();
1002                     if (view.webContents.canGoBack())
1003                         view.webContents.goBack();
1004                 } else if (input.key == 'ArrowRight') {
1005                     e.preventDefault();
1006                     if (view.webContents.canGoForward())
1007                         view.webContents.goForward();
1008                 }
1009             } else {
1010                 if (input.key == 'F11') {
1011                     e.preventDefault();
1012                     window.setFullScreen(!window.isFullScreen());
1013                     this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
1014                 }
1015             }
1016         });
1017
1018         view.webContents.session.on('will-download', (event, item, webContents) => {
1019             const str = this.getRandString(12);
1020             db.downloads.insert({ id: str, name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: item.getState() });
1021             item.on('updated', (e, state) => {
1022                 db.downloads.update({ id: str }, { $set: { name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: state } });
1023             });
1024
1025             item.once('done', (e, state) => {
1026                 db.downloads.update({ id: str }, { $set: { name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: state } });
1027             });
1028         });
1029
1030         view.webContents.loadURL(url);
1031
1032         if (views[windowId] == undefined)
1033             views[windowId] = [];
1034         views[windowId].push({ id, view, isNotificationBar: false });
1035         console.log(views);
1036
1037         if (isActive) {
1038             window.webContents.send(`browserview-set-${windowId}`, { id: id });
1039             window.setBrowserView(view);
1040         }
1041
1042         this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
1043         this.getViews(windowId);
1044     }
1045
1046     getRandString = (length) => {
1047         const char = 'abcdefghijklmnopqrstuvwxyz0123456789';
1048         const charLength = char.length;
1049
1050         let str = '';
1051         for (var i = 0; i < length; i++) {
1052             str += char[Math.floor(Math.random() * charLength)];
1053         }
1054
1055         return str;
1056     }
1057 }