OSDN Git Service

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