1 const { app, shell, 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');
6 const pkg = require(`${app.getAppPath()}/package.json`);
7 const protocolStr = 'flast';
8 const fileProtocolStr = `${protocolStr}-file`;
10 const { download } = require('electron-dl');
11 const platform = require('electron-platform');
12 const localShortcut = require('electron-localshortcut');
14 const Config = require('electron-store');
15 const config = new Config({
21 isCustomTitlebar: true,
25 defaultPage: `${protocolStr}://home`,
26 defaultEngine: 'Google',
30 url: 'https://www.google.com/search?q=%s'
34 url: 'https://www.bing.com/search?q=%s'
38 url: 'https://search.yahoo.co.jp/search?p=%s'
42 url: 'https://search.goo.ne.jp/web.jsp?MT=%s'
46 url: 'https://www.baidu.com/s?wd=%s'
49 name: 'Google Translate',
50 url: 'https://translate.google.com/?text=%s'
54 url: 'https://www.youtube.com/results?search_query=%s'
58 url: 'https://www.twitter.com/search?q=%s'
62 url: 'https://github.com/search?q=%s'
79 const lang = require(`${app.getAppPath()}/langs/${config.get('language')}.js`);
81 const Datastore = require('nedb');
83 db.pageSettings = new Datastore({
84 filename: path.join(app.getPath('userData'), 'Files', 'PageSettings.db'),
89 db.historys = new Datastore({
90 filename: path.join(app.getPath('userData'), 'Files', 'History.db'),
94 db.downloads = new Datastore({
95 filename: path.join(app.getPath('userData'), 'Files', 'Download.db'),
99 db.bookmarks = new Datastore({
100 filename: path.join(app.getPath('userData'), 'Files', 'Bookmarks.db'),
105 db.apps = new Datastore({
106 filename: path.join(app.getPath('userData'), 'Files', 'Apps.db'),
111 const { loadFilters, updateFilters, runAdblockService, removeAds } = require('./AdBlocker');
113 let floatingWindows = [];
116 getBaseWindow = (width = 1100, height = 680, minWidth = 500, minHeight = 360, x, y, frame = false) => {
117 return new BrowserWindow({
118 width, height, minWidth, minHeight, x, y, titleBarStyle: 'hidden', frame, fullscreenable: true,
119 icon: `${__dirname}/static/app/icon.png`,
122 nodeIntegration: true,
125 experimentalFeatures: true,
126 contextIsolation: false,
131 loadSessionAndProtocol = () => {
132 const ses = session.defaultSession;
134 setPermissionRequestHandler(ses, false);
136 protocol.isProtocolHandled(protocolStr, (handled) => {
138 protocol.registerFileProtocol(protocolStr, (request, callback) => {
139 const parsed = parse(request.url);
142 path: path.join(app.getAppPath(), 'pages', `${parsed.hostname}.html`),
145 if (error) console.error('Failed to register protocol: ' + error);
150 protocol.isProtocolHandled(fileProtocolStr, (handled) => {
152 protocol.registerFileProtocol(fileProtocolStr, (request, callback) => {
153 const parsed = parse(request.url);
156 path: path.join(app.getAppPath(), 'pages', 'static', parsed.pathname),
159 if (error) console.error('Failed to register protocol: ' + error);
165 loadSessionAndProtocolWithPrivateMode = (windowId) => {
166 const ses = session.fromPartition(windowId);
167 ses.setUserAgent(ses.getUserAgent().replace(/ Electron\/[0-9\.]*/g, '') + ' PrivMode');
169 setPermissionRequestHandler(ses, true);
171 ses.protocol.registerFileProtocol(protocolStr, (request, callback) => {
172 const parsed = parse(request.url);
175 path: path.join(app.getAppPath(), 'pages', `${parsed.hostname}.html`),
178 if (error) console.error('Failed to register protocol: ' + error);
181 ses.protocol.registerFileProtocol(fileProtocolStr, (request, callback) => {
182 const parsed = parse(request.url);
185 path: path.join(app.getAppPath(), 'pages', 'static', parsed.pathname),
188 if (error) console.error('Failed to register protocol: ' + error);
192 setPermissionRequestHandler = (ses, isPrivate = false) => {
194 ses.setPermissionRequestHandler((webContents, permission, callback) => {
195 const url = parse(webContents.getURL());
197 db.pageSettings.findOne({ origin: `${url.protocol}//${url.hostname}` }, (err, doc) => {
198 if (doc != undefined) {
199 if (permission == 'media' && doc.media != undefined && doc.media > -1)
200 return callback(doc.media === 0);
201 if (permission == 'geolocation' && doc.geolocation != undefined && doc.geolocation > -1)
202 return callback(doc.geolocation === 0);
203 if (permission == 'notifications' && doc.notifications != undefined && doc.notifications > -1)
204 return callback(doc.notifications === 0);
205 if (permission == 'midiSysex' && doc.midiSysex != undefined && doc.midiSysex > -1)
206 return callback(doc.midiSysex === 0);
207 if (permission == 'pointerLock' && doc.pointerLock != undefined && doc.pointerLock > -1)
208 return callback(doc.pointerLock === 0);
209 if (permission == 'fullscreen' && doc.fullscreen != undefined && doc.fullscreen > -1)
210 return callback(doc.fullscreen === 0);
211 if (permission == 'openExternal' && doc.openExternal != undefined && doc.openExternal > -1)
212 return callback(doc.openExternal === 0);
214 if (Notification.isSupported()) {
215 const notify = new Notification({
216 icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
217 title: `${url.protocol}//${url.hostname} が権限を要求しています。`,
218 body: '詳細はここをクリックしてください。',
224 notify.on('click', (e) => {
225 dialog.showMessageBox({
228 message: `${url.protocol}//${url.hostname} が権限を要求しています。`,
229 detail: `要求内容: ${permission}`,
230 checkboxLabel: 'このサイトでは今後も同じ処理をする',
231 checkboxChecked: false,
233 buttons: ['はい / Yes', 'いいえ / No'],
236 }, (res, checked) => {
237 console.log(res, checked);
239 if (permission == 'media')
240 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, media: res }, { upsert: true });
241 if (permission == 'geolocation')
242 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, geolocation: res }, { upsert: true });
243 if (permission == 'notifications')
244 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, notifications: res }, { upsert: true });
245 if (permission == 'midiSysex')
246 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, midiSysex: res }, { upsert: true });
247 if (permission == 'pointerLock')
248 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, pointerLock: res }, { upsert: true });
249 if (permission == 'fullscreen')
250 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, fullscreen: res }, { upsert: true });
251 if (permission == 'openExternal')
252 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, openExternal: res }, { upsert: true });
254 return callback(res === 0);
257 notify.on('close', (e) => {
258 return callback(false);
261 dialog.showMessageBox({
264 message: `${url.protocol}//${url.hostname} が権限を要求しています。`,
265 detail: `要求内容: ${permission}`,
266 checkboxLabel: 'このサイトでは今後も同じ処理をする',
267 checkboxChecked: false,
269 buttons: ['はい / Yes', 'いいえ / No'],
272 }, (res, checked) => {
274 if (permission == 'media')
275 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, media: res }, { upsert: true });
276 if (permission == 'geolocation')
277 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, geolocation: res }, { upsert: true });
278 if (permission == 'notifications')
279 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, notifications: res }, { upsert: true });
280 if (permission == 'midiSysex')
281 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, midiSysex: res }, { upsert: true });
282 if (permission == 'pointerLock')
283 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, pointerLock: res }, { upsert: true });
284 if (permission == 'fullscreen')
285 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, fullscreen: res }, { upsert: true });
286 if (permission == 'openExternal')
287 db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, openExternal: res }, { upsert: true });
289 return callback(res === 0);
296 ses.setPermissionRequestHandler((webContents, permission, callback) => {
297 const url = parse(webContents.getURL());
298 if (Notification.isSupported()) {
299 const notify = new Notification({
300 icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
301 title: `${url.protocol}//${url.hostname} が権限を要求しています。`,
302 body: '詳細はここをクリックしてください。\nプライベート ウィンドウ',
308 notify.on('click', (e) => {
309 dialog.showMessageBox({
312 message: `${url.protocol}//${url.hostname} が権限を要求しています。`,
313 detail: `要求内容: ${permission}`,
315 buttons: ['はい / Yes', 'いいえ / No'],
319 return callback(res === 0);
322 notify.on('close', (e) => {
323 return callback(false);
326 dialog.showMessageBox({
329 message: `${url.protocol}//${url.hostname} が権限を要求しています。`,
330 detail: `要求内容: ${permission}`,
332 buttons: ['はい / Yes', 'いいえ / No'],
336 return callback(res === 0);
343 module.exports = class WindowManager {
345 this.windows = new Map();
347 ipcMain.on('window-add', (e, args) => {
348 this.addWindow(args.isPrivate);
351 ipcMain.on('window-fixBounds', (e, args) => {
352 this.windows.forEach((value, key) => {
353 this.fixBounds(key, (floatingWindows.indexOf(key) != -1));
357 ipcMain.on('window-change-settings', (e, args) => {
358 this.windows.forEach((value, key) => {
359 value.webContents.send('window-change-settings', {});
363 ipcMain.on('update-filters', (e, args) => {
367 ipcMain.on('data-history-get', (e, args) => {
368 db.historys.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
369 e.sender.send('data-history-get', { historys: docs });
373 ipcMain.on('data-history-clear', (e, args) => {
374 db.historys.remove({}, { multi: true });
377 ipcMain.on('data-downloads-get', (e, args) => {
378 db.downloads.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
379 e.sender.send('data-downloads-get', { downloads: docs });
383 ipcMain.on('data-downloads-clear', (e, args) => {
384 db.downloads.remove({}, { multi: true });
387 ipcMain.on('data-bookmarks-get', (e, args) => {
388 db.bookmarks.find({ isPrivate: args.isPrivate }).sort({ createdAt: -1 }).exec((err, docs) => {
389 e.sender.send('data-bookmarks-get', { bookmarks: docs });
393 ipcMain.on('data-bookmarks-clear', (e, args) => {
394 db.bookmarks.remove({}, { multi: true });
397 ipcMain.on('data-apps-add', (e, args) => {
398 db.apps.update({ id: args.id }, { id: args.id, name: args.name, description: args.description, url: args.url }, { upsert: true });
400 db.apps.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
405 ipcMain.on('data-apps-remove', (e, args) => {
406 db.apps.remove({ id: args.id }, {});
409 ipcMain.on('data-apps-get', (e, args) => {
410 db.apps.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
411 e.sender.send('data-apps-get', { apps: docs });
415 ipcMain.on('data-apps-is', (e, args) => {
416 db.apps.find({ id: args.id }).exec((err, docs) => {
417 e.sender.send('data-apps-is', { id: args.id, isInstalled: (docs.length > 0 ? true : false) });
421 ipcMain.on('data-apps-clear', (e, args) => {
422 db.apps.remove({}, { multi: true });
425 ipcMain.on('clear-browsing-data', () => {
426 const ses = session.defaultSession;
427 ses.clearCache((err) => {
428 if (err) log.error(err);
431 ses.clearStorageData({
446 db.pageSettings.remove({}, { multi: true });
448 db.historys.remove({}, { multi: true });
449 db.downloads.remove({}, { multi: true });
450 db.bookmarks.remove({}, { multi: true });
451 db.apps.remove({}, { multi: true });
455 addWindow = (isPrivate = false) => {
456 loadSessionAndProtocol();
459 const { width, height, x, y } = config.get('window.bounds');
460 const window = getBaseWindow(config.get('window.isMaximized') ? 1110 : width, config.get('window.isMaximized') ? 680 : height, 500, 360, x, y, !config.get('design.isCustomTitlebar'));
462 const id = (!isPrivate ? `window-${window.id}` : `private-${window.id}`);
464 config.get('window.isMaximized') && window.maximize();
466 const startUrl = process.env.ELECTRON_START_URL || format({
467 pathname: path.join(__dirname, '/../build/index.html'), // 警告:このファイルを移動する場合ここの相対パスの指定に注意してください
470 hash: `/window/${id}`,
473 window.loadURL(startUrl);
475 window.once('ready-to-show', () => {
479 window.on('closed', () => {
480 this.windows.delete(id);
483 window.on('close', (e) => {
486 config.set('window.isMaximized', window.isMaximized());
487 config.set('window.bounds', window.getBounds());
490 window.on('maximize', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
491 window.on('unmaximize', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
492 window.on('enter-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
493 window.on('leave-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
494 window.on('enter-html-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
495 window.on('leave-html-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
497 // registerProtocols();
498 this.registerListeners(id);
500 localShortcut.register(window, 'CmdOrCtrl+Shift+I', () => {
501 if (window.getBrowserView() == undefined) return;
502 const view = window.getBrowserView();
504 if (view.webContents.isDevToolsOpened()) {
505 view.webContents.devToolsWebContents.focus();
507 view.webContents.openDevTools();
511 localShortcut.register(window, 'CmdOrCtrl+R', () => {
512 if (window.getBrowserView() == undefined) return;
513 const view = window.getBrowserView();
515 view.webContents.reload();
518 localShortcut.register(window, 'CmdOrCtrl+Shift+R', () => {
519 if (window.getBrowserView() == undefined) return;
520 const view = window.getBrowserView();
522 view.webContents.reloadIgnoringCache();
525 this.windows.set(id, window);
527 if (process.argv != undefined) {
528 window.webContents.send(`tab-add-${id}`, { url: process.argv[process.argv.length - 1] });
532 registerListeners = (id) => {
533 ipcMain.on(`browserview-add-${id}`, (e, args) => {
534 this.addView(id, args.url, args.isActive);
537 ipcMain.on(`browserview-remove-${id}`, (e, args) => {
538 this.removeView(id, args.id);
541 ipcMain.on(`browserview-select-${id}`, (e, args) => {
542 this.selectView(id, args.id);
545 ipcMain.on(`browserview-get-${id}`, (e, args) => {
548 views[id].map((item) => {
549 const url = item.view.webContents.getURL();
551 datas.push({ id: item.view.webContents.id, title: item.view.webContents.getTitle(), url, icon: this.getFavicon(url), color: '#0a84ff', isBookmarked: false });
553 e.sender.send(`browserview-get-${id}`, { views: datas });
556 ipcMain.on(`browserview-goBack-${id}`, (e, args) => {
557 views[id].filter(function (view, i) {
558 if (view.view.webContents.id == args.id) {
559 let webContents = views[id][i].view.webContents;
560 if (webContents.canGoBack())
561 webContents.goBack();
566 ipcMain.on(`browserview-goForward-${id}`, (e, args) => {
567 views[id].filter(function (view, i) {
568 if (view.view.webContents.id == args.id) {
569 let webContents = views[id][i].view.webContents;
570 if (webContents.canGoForward())
571 webContents.goForward();
576 ipcMain.on(`browserview-reload-${id}`, (e, args) => {
577 views[id].filter(function (view, i) {
578 if (view.view.webContents.id == args.id) {
579 let webContents = views[id][i].view.webContents;
580 webContents.reload();
585 ipcMain.on(`browserview-stop-${id}`, (e, args) => {
586 views[id].filter(function (view, i) {
587 if (view.view.webContents.id == args.id) {
588 let webContents = views[id][i].view.webContents;
594 ipcMain.on(`browserview-goHome-${id}`, (e, args) => {
595 views[id].filter(function (view, i) {
596 if (view.view.webContents.id == args.id) {
597 let webContents = views[id][i].view.webContents;
598 webContents.loadURL(config.get('homePage.defaultPage'));
603 ipcMain.on(`browserview-loadURL-${id}`, (e, args) => {
604 views[id].filter(function (view, i) {
605 if (view.view.webContents.id == args.id) {
606 let webContents = views[id][i].view.webContents;
607 webContents.loadURL(args.url);
612 ipcMain.on(`browserview-loadFile-${id}`, (e, args) => {
613 views[id].filter(function (view, i) {
614 if (view.view.webContents.id == args.id) {
615 let webContents = views[id][i].view.webContents;
616 webContents.loadFile(args.url);
621 ipcMain.on(`data-bookmark-add-${id}`, (e, args) => {
622 views[id].filter((view, i) => {
623 if (view.view.webContents.id == args.id) {
624 let v = views[id][i].view;
625 db.bookmarks.insert({ title: v.webContents.getTitle(), url: v.webContents.getURL(), isPrivate: args.isPrivate });
626 this.updateBookmarkState(id, v);
631 ipcMain.on(`data-bookmark-remove-${id}`, (e, args) => {
632 views[id].filter((view, i) => {
633 if (view.view.webContents.id == args.id) {
634 let v = views[id][i].view;
635 db.bookmarks.remove({ url: v.webContents.getURL(), isPrivate: args.isPrivate }, {});
636 this.updateBookmarkState(id, v);
641 ipcMain.on(`data-bookmark-has-${id}`, (e, args) => {
642 views[id].filter((view, i) => {
643 if (view.view.webContents.id == args.id) {
644 let v = views[id][i].view;
645 db.bookmarks.find({ url: v.webContents.getURL(), isPrivate: args.isPrivate }, (err, docs) => {
646 e.sender.send(`data-bookmark-has-${id}`, { isBookmarked: (docs.length > 0 ? true : false) });
653 getFavicon = (url) => {
654 const parsed = parse(url);
655 return url.startsWith(`${protocolStr}://`) || url.startsWith(`${fileProtocolStr}://`) ? undefined : `https://www.google.com/s2/favicons?domain=${parsed.protocol}//${parsed.hostname}`;
658 updateNavigationState = (id, view) => {
659 const window = this.windows.get(id);
660 window.webContents.send(`update-navigation-state-${id}`, {
661 id: view.webContents.id,
662 canGoBack: view.webContents.canGoBack(),
663 canGoForward: view.webContents.canGoForward(),
667 updateBookmarkState = (id, view) => {
668 const window = this.windows.get(id);
669 db.bookmarks.find({ url: view.webContents.getURL(), isPrivate: (String(id).startsWith('private')) }, (err, docs) => {
670 const url = view.webContents.getURL();
672 window.webContents.send(`browserview-load-${id}`, { id: view.webContents.id, title: view.webContents.getTitle(), url: url, icon: this.getFavicon(url), color: '#0a84ff', isBookmarked: (docs.length > 0 ? true : false) });
676 getColor = (view) => {
677 return new Promise((resolve, reject) => {
678 if (view !== null && !view.isDestroyed() && view.webContents !== null) {
679 view.webContents.executeJavaScript(
681 const heads = document.head.children;
682 for (var i = 0; i < heads.length; i++) {
683 if (heads[i].getAttribute('name') === 'theme-color') {
684 return heads[i].getAttribute('content');
687 })()`, false, async (result) => {
688 resolve(result !== null ? result : '#0a84ff');
691 reject(new Error('WebContents are not available'))
696 fixBounds = (windowId, isFloating = false) => {
697 const window = this.windows.get(windowId);
699 if (window.getBrowserView() == undefined) return;
700 const view = window.getBrowserView();
702 const { width, height } = window.getContentBounds();
704 const baseBarHeight = 73;
705 const bookMarkBarHeight = 28;
707 view.setAutoResize({ width: true, height: true });
709 window.setMinimizable(false);
710 window.setMaximizable(false);
711 window.setAlwaysOnTop(true);
712 window.setVisibleOnAllWorkspaces(true);
720 window.setMinimizable(true);
721 window.setMaximizable(true);
722 window.setAlwaysOnTop(false);
723 window.setVisibleOnAllWorkspaces(false);
724 if (window.isFullScreen()) {
733 x: config.get('design.isCustomTitlebar') ? 1 : 0,
734 y: config.get('design.isCustomTitlebar') ? this.getHeight(true, height, baseBarHeight, bookMarkBarHeight) + 1 : this.getHeight(true, height, baseBarHeight, bookMarkBarHeight),
735 width: config.get('design.isCustomTitlebar') ? width - 2 : width,
736 height: window.isMaximized() ? this.getHeight(false, height, baseBarHeight, bookMarkBarHeight) : (config.get('design.isCustomTitlebar') ? (this.getHeight(false, height, baseBarHeight, bookMarkBarHeight)) - 2 : (this.getHeight(false, height, baseBarHeight, bookMarkBarHeight)) - 1),
740 view.setAutoResize({ width: true, height: true });
743 getHeight = (b, height, baseBarHeight, bookMarkBarHeight) => {
745 return config.get('design.isBookmarkBar') ? (baseBarHeight + bookMarkBarHeight) : baseBarHeight;
747 return height - (config.get('design.isBookmarkBar') ? (baseBarHeight + bookMarkBarHeight) : baseBarHeight);
751 addView = (id, url, isActive) => {
752 if (String(id).startsWith('private')) {
753 loadSessionAndProtocolWithPrivateMode(id);
756 this.addTab(id, url, isActive);
759 removeView = (windowId, id) => {
760 views[windowId].filter((view, i) => {
761 if (view.view.webContents.id == id) {
764 if (index + 1 < views[windowId].length) {
765 this.selectView2(windowId, index + 1);
766 } else if (index - 1 >= 0) {
767 this.selectView2(windowId, index - 1);
770 views[windowId][index].view.destroy();
771 views[windowId].splice(index, 1);
776 selectView = (windowId, id) => {
777 const window = this.windows.get(windowId);
778 views[windowId].filter((view, i) => {
779 if (id == view.view.webContents.id) {
780 window.setBrowserView(views[windowId][i].view);
781 window.setTitle(`${views[windowId][i].view.webContents.getTitle()} - ${pkg.name}`);
782 window.webContents.send(`browserview-set-${windowId}`, { id: id });
783 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
788 selectView2 = (windowId, i) => {
789 const window = this.windows.get(windowId);
790 const item = views[windowId][i];
792 window.setBrowserView(item.view);
793 window.setTitle(`${item.view.webContents.getTitle()} - ${pkg.name}`);
794 window.webContents.send(`browserview-set-${windowId}`, { id: item.id });
795 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
798 getViews = (windowId) => {
800 for (var i = 0; i < views[windowId].length; i++) {
801 const url = views[windowId][i].view.webContents.getURL();
803 datas.push({ id: views[windowId][i].view.webContents.id, title: views[windowId][i].view.webContents.getTitle(), url: url, icon: this.getFavicon(url) });
805 const window = this.windows.get(windowId);
806 window.webContents.send(`browserview-get-${windowId}`, { views: datas });
809 addTab = (windowId, url = config.get('homePage.defaultPage'), isActive = true) => {
810 const view = new BrowserView({
812 nodeIntegration: false,
813 contextIsolation: false,
815 experimentalFeatures: true,
817 safeDialogsMessage: '今後このページではダイアログを表示しない',
818 ...(String(windowId).startsWith('private') && { partition: windowId }),
819 preload: require.resolve('./Preload')
823 const window = this.windows.get(windowId);
824 const id = view.webContents.id;
826 runAdblockService(window, windowId, id, view.webContents.session);
828 view.webContents.on('did-start-loading', () => {
829 if (view.isDestroyed()) return;
831 window.webContents.send(`browserview-start-loading-${windowId}`, { id: id });
833 view.webContents.on('did-stop-loading', () => {
834 if (view.isDestroyed()) return;
836 window.webContents.send(`browserview-stop-loading-${windowId}`, { id: id });
839 view.webContents.on('did-finish-load', (e) => {
840 if (view.isDestroyed()) return;
842 window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
843 this.updateBookmarkState(windowId, view);
845 this.updateNavigationState(windowId, view);
847 view.webContents.on('did-fail-load', (e, code, description, url, isMainFrame, processId, routingId) => {
848 if (view.isDestroyed() || !isMainFrame || code === -3) return;
850 dialog.showMessageBox({ message: `${code}: ${description}` });
853 view.webContents.on('did-start-navigation', (e) => {
854 if (view.isDestroyed()) return;
856 const url = view.webContents.getURL();
858 if (config.get('isAdBlock') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
859 removeAds(url, view.webContents);
861 this.updateNavigationState(windowId, view);
864 view.webContents.on('page-title-updated', (e) => {
865 if (view.isDestroyed()) return;
867 window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
868 this.updateBookmarkState(windowId, view);
870 if (!String(windowId).startsWith('private') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
871 db.historys.insert({ title: view.webContents.getTitle(), url: view.webContents.getURL() });
873 this.updateNavigationState(windowId, view);
876 view.webContents.on('page-favicon-updated', (e, favicons) => {
877 if (view.isDestroyed()) return;
879 window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
881 this.updateBookmarkState(windowId, view);
882 this.updateNavigationState(windowId, view);
885 view.webContents.on('did-change-theme-color', (e, color) => {
886 if (view.isDestroyed()) return;
888 window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
890 this.updateBookmarkState(windowId, view);
891 this.updateNavigationState(windowId, view);
893 window.webContents.send(`browserview-theme-color-${windowId}`, { id: view.webContents.id, color });
896 view.webContents.on('new-window', (e, url) => {
897 if (view.isDestroyed()) return;
900 this.addView(windowId, url, true);
903 view.webContents.on('certificate-error', (e, url, error, certificate, callback) => {
905 if (Notification.isSupported()) {
906 const notify = new Notification({
907 icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
909 body: '詳細はここをクリックしてください。',
915 notify.on('click', (e) => {
916 dialog.showMessageBox({
919 message: 'この接続ではプライバシーが保護されません',
920 detail: `${parse(url).hostname} の証明書を信頼することができませんでした。\n信頼できるページに戻ることをおすすめします。\nこのまま閲覧することも可能ですが安全ではありません。`,
922 buttons: ['続行', 'キャンセル'],
929 notify.on('close', (e) => {
933 dialog.showMessageBox({
936 message: 'この接続ではプライバシーが保護されません',
937 detail: `${parse(url).hostname} の証明書を信頼することができませんでした。\n信頼できるページに戻ることをおすすめします。\nこのまま閲覧することも可能ですが安全ではありません。`,
939 buttons: ['続行', 'キャンセル'],
948 view.webContents.on('context-menu', (e, params) => {
949 if (view.isDestroyed()) return;
952 if (params.linkURL !== '') {
953 menu = Menu.buildFromTemplate(
956 label: lang.window.view.contextMenu.link.newTab,
958 this.addView(windowId, `view-source:${view.webContents.getURL()}`, false);
959 this.addView(windowId, params.linkURL, true);
963 label: lang.window.view.contextMenu.link.newWindow,
965 click: () => { view.webContents.openDevTools(); }
968 label: lang.window.view.contextMenu.link.openPrivateWindow,
970 click: () => { view.webContents.openDevTools(); }
972 { type: 'separator' },
974 label: lang.window.view.contextMenu.link.copy,
975 accelerator: 'CmdOrCtrl+C',
978 clipboard.writeText(params.linkURL);
981 { type: 'separator' },
983 label: lang.window.view.contextMenu.devTool,
984 accelerator: 'CmdOrCtrl+Shift+I',
985 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
987 if (view.webContents.isDevToolsOpened())
988 view.webContents.devToolsWebContents.focus();
990 view.webContents.openDevTools();
995 } else if (params.hasImageContents) {
996 menu = Menu.buildFromTemplate(
999 label: lang.window.view.contextMenu.image.newTab,
1001 this.addView(windowId, params.srcURL, true);
1005 label: lang.window.view.contextMenu.image.saveImage,
1006 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
1008 download(window, params.srcURL, {
1009 directory: app.getPath('downloads'),
1015 label: lang.window.view.contextMenu.image.copyImage,
1017 const img = nativeImage.createFromDataURL(params.srcURL);
1020 clipboard.writeImage(img);
1024 label: lang.window.view.contextMenu.image.copyLink,
1027 clipboard.writeText(params.srcURL);
1030 { type: 'separator' },
1032 label: lang.window.view.contextMenu.devTool,
1033 accelerator: 'CmdOrCtrl+Shift+I',
1034 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
1036 if (view.webContents.isDevToolsOpened())
1037 view.webContents.devToolsWebContents.focus();
1039 view.webContents.openDevTools();
1044 } else if (params.isEditable) {
1045 menu = Menu.buildFromTemplate(
1048 label: lang.window.view.contextMenu.editable.undo,
1049 accelerator: 'CmdOrCtrl+Z',
1050 enabled: params.editFlags.canUndo,
1051 click: () => { view.webContents.undo(); }
1054 label: lang.window.view.contextMenu.editable.redo,
1055 accelerator: 'CmdOrCtrl+Y',
1056 enabled: params.editFlags.canRedo,
1057 click: () => { view.webContents.redo(); }
1059 { type: 'separator' },
1061 label: lang.window.view.contextMenu.editable.cut,
1062 accelerator: 'CmdOrCtrl+X',
1063 enabled: params.editFlags.canCut,
1064 click: () => { view.webContents.cut(); }
1067 label: lang.window.view.contextMenu.editable.copy,
1068 accelerator: 'CmdOrCtrl+C',
1069 enabled: params.editFlags.canCopy,
1070 click: () => { view.webContents.copy(); }
1073 label: lang.window.view.contextMenu.editable.paste,
1074 accelerator: 'CmdOrCtrl+V',
1075 enabled: params.editFlags.canPaste,
1076 click: () => { view.webContents.paste(); }
1078 { type: 'separator' },
1080 label: lang.window.view.contextMenu.editable.selectAll,
1081 accelerator: 'CmdOrCtrl+A',
1082 enabled: params.editFlags.canSelectAll,
1083 click: () => { view.webContents.selectAll(); }
1085 { type: 'separator' },
1087 label: lang.window.view.contextMenu.devTool,
1088 accelerator: 'CmdOrCtrl+Shift+I',
1089 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
1091 if (view.webContents.isDevToolsOpened())
1092 view.webContents.devToolsWebContents.focus();
1094 view.webContents.openDevTools();
1099 } else if (params.selectionText !== '' && !params.isEditable) {
1100 menu = Menu.buildFromTemplate(
1103 label: lang.window.view.contextMenu.selection.copy,
1104 accelerator: 'CmdOrCtrl+C',
1105 click: () => { view.webContents.copy(); }
1108 label: String(lang.window.view.contextMenu.selection.textSearch).replace(/{name}/, 'Google').replace(/{text}/, params.selectionText),
1110 this.addView(windowId, `https://www.google.co.jp/search?q=${params.selectionText}`, true);
1113 { type: 'separator' },
1115 label: lang.window.view.contextMenu.devTool,
1116 accelerator: 'CmdOrCtrl+Shift+I',
1117 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
1119 if (view.webContents.isDevToolsOpened())
1120 view.webContents.devToolsWebContents.focus();
1122 view.webContents.openDevTools();
1128 menu = Menu.buildFromTemplate(
1131 label: lang.window.view.contextMenu.back,
1132 accelerator: 'Alt+Left',
1133 icon: view.webContents.canGoBack() ? `${app.getAppPath()}/static/arrow_back.png` : `${app.getAppPath()}/static/arrow_back_inactive.png`,
1134 enabled: view.webContents.canGoBack(),
1135 click: () => { view.webContents.goBack(); }
1138 label: lang.window.view.contextMenu.forward,
1139 accelerator: 'Alt+Right',
1140 icon: view.webContents.canGoForward() ? `${app.getAppPath()}/static/arrow_forward.png` : `${app.getAppPath()}/static/arrow_forward_inactive.png`,
1141 enabled: view.webContents.canGoForward(),
1142 click: () => { view.webContents.goForward(); }
1145 label: !view.webContents.isLoadingMainFrame() ? lang.window.view.contextMenu.reload.reload : lang.window.view.contextMenu.reload.stop,
1146 accelerator: 'CmdOrCtrl+R',
1147 icon: !view.webContents.isLoadingMainFrame() ? `${app.getAppPath()}/static/refresh.png` : `${app.getAppPath()}/static/close.png`,
1148 click: () => { !view.webContents.isLoadingMainFrame() ? view.webContents.reload() : view.webContents.stop(); }
1150 { type: 'separator' },
1152 label: lang.window.view.contextMenu.floatingWindow,
1154 checked: (floatingWindows.indexOf(windowId) != -1),
1155 enabled: (!window.isFullScreen() && !window.isMaximized() && config.get('design.isCustomTitlebar')),
1157 if (floatingWindows.indexOf(windowId) != -1) {
1158 floatingWindows.filter((win, i) => {
1159 if (windowId == win) {
1160 floatingWindows.splice(i, 1);
1164 floatingWindows.push(windowId);
1166 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
1169 { type: 'separator' },
1171 label: lang.window.view.contextMenu.savePage,
1172 accelerator: 'CmdOrCtrl+S',
1173 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
1175 dialog.showSaveDialog(window, {
1176 defaultPath: `${app.getPath('downloads')}/${view.webContents.getTitle()}.html`,
1178 { name: 'HTML', extensions: ['htm', 'html'] },
1179 { name: 'All Files', extensions: ['*'] }
1182 if (fileName != undefined) {
1183 view.webContents.savePage(fileName, 'HTMLComplete', (err) => {
1184 if (!err) console.log('Page Save successfully');
1191 label: lang.window.view.contextMenu.print,
1192 accelerator: 'CmdOrCtrl+P',
1193 icon: `${app.getAppPath()}/static/print.png`,
1194 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
1195 click: () => { view.webContents.print(); }
1197 { type: 'separator' },
1199 label: lang.window.view.contextMenu.viewSource,
1200 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
1201 click: () => { this.addView(windowId, `view-source:${view.webContents.getURL()}`, true); }
1204 label: lang.window.view.contextMenu.devTool,
1205 accelerator: 'CmdOrCtrl+Shift+I',
1206 enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
1208 if (view.webContents.isDevToolsOpened())
1209 view.webContents.devToolsWebContents.focus();
1211 view.webContents.openDevTools();
1221 view.webContents.on('before-input-event', (e, input) => {
1222 if (view.isDestroyed()) return;
1223 console.log((input.control || (platform.isDarwin && input.meta)) || input.alt)
1224 window.webContents.setIgnoreMenuShortcuts(true);
1226 if (input.control) {
1227 if (input.shift && input.key == 'I') {
1229 window.webContents.setIgnoreMenuShortcuts(true);
1230 view.webContents.setIgnoreMenuShortcuts(true);
1232 if (view.webContents.isDevToolsOpened()) {
1233 view.webContents.devToolsWebContents.focus();
1235 view.webContents.openDevTools();
1237 } else if (input.shift && input.key == 'R') {
1239 window.webContents.setIgnoreMenuShortcuts(true);
1240 view.webContents.setIgnoreMenuShortcuts(true);
1242 view.webContents.reloadIgnoringCache();
1243 } else if (input.key == 'T') {
1245 window.webContents.setIgnoreMenuShortcuts(true);
1246 // view.webContents.setIgnoreMenuShortcuts(true);
1248 console.log('test');
1249 this.addView(windowId, config.get('homePage.defaultPage'), true);
1251 } else if (input.alt) {
1252 if (input.key == 'ArrowLeft') {
1254 // window.webContents.setIgnoreMenuShortcuts(true);
1255 view.webContents.setIgnoreMenuShortcuts(true);
1257 if (view.webContents.canGoBack())
1258 view.webContents.goBack();
1259 } else if (input.key == 'ArrowRight') {
1261 // window.webContents.setIgnoreMenuShortcuts(true);
1262 view.webContents.setIgnoreMenuShortcuts(true);
1264 if (view.webContents.canGoForward())
1265 view.webContents.goForward();
1268 if (input.key == 'F11') {
1270 window.webContents.setIgnoreMenuShortcuts(true);
1271 view.webContents.setIgnoreMenuShortcuts(true);
1273 window.setFullScreen(!window.isFullScreen());
1274 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
1279 view.webContents.session.on('will-download', (event, item, webContents) => {
1280 const str = this.getRandString(12);
1281 db.downloads.find({ id: str }, (err, docs) => {
1283 db.downloads.insert({ id: str, name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: item.getState() });
1285 item.on('updated', (e, state) => {
1286 db.downloads.update({ id: str }, { $set: { name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: state } });
1289 item.once('done', (e, state) => {
1290 const filePath = item.getSavePath();
1291 db.downloads.update({ id: str }, { $set: { name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: state } });
1292 if (state === 'completed') {
1293 window.webContents.send(`notification-${windowId}`, { id: id, content: `${item.getFilename()} のダウンロードが完了しました。` });
1295 if (!Notification.isSupported()) return;
1296 const notify = new Notification({
1297 icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
1299 body: `${item.getFilename()} のダウンロードが完了しました。\n詳細はここをクリックしてください。`
1305 notify.on('click', (e) => {
1306 if (filePath !== undefined)
1307 shell.openItem(filePath);
1310 console.log(`Download failed: ${state}`);
1315 view.webContents.loadURL(url);
1317 if (views[windowId] == undefined)
1318 views[windowId] = [];
1319 views[windowId].push({ id, view, isNotificationBar: false });
1323 window.webContents.send(`browserview-set-${windowId}`, { id: id });
1324 window.setBrowserView(view);
1327 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
1328 this.getViews(windowId);
1331 getRandString = (length) => {
1332 const char = 'abcdefghijklmnopqrstuvwxyz0123456789';
1333 const charLength = char.length;
1336 for (var i = 0; i < length; i++) {
1337 str += char[Math.floor(Math.random() * charLength)];