OSDN Git Service

v2.8.1
[serene/MyBrowser.git] / app / electron / WindowManager.js
index d78f82a..e79d9c0 100644 (file)
@@ -2,6 +2,8 @@ const { app, shell, ipcMain, protocol, session, BrowserWindow, BrowserView, Menu
 const path = require('path');
 const { parse, format } = require('url');
 const os = require('os');
+const https = require('https');
+const http = require('http');
 
 const pkg = require(`${app.getAppPath()}/package.json`);
 const protocolStr = 'flast';
@@ -22,7 +24,10 @@ const config = new Config({
             theme: 'default'
         },
         homePage: {
+            isDefaultHomePage: true,
             defaultPage: `${protocolStr}://home`,
+        },
+        searchEngine: {
             defaultEngine: 'Google',
             searchEngines: [
                 {
@@ -42,6 +47,10 @@ const config = new Config({
                     url: 'https://search.goo.ne.jp/web.jsp?MT=%s'
                 },
                 {
+                    name: 'OCN',
+                    url: 'https://search.goo.ne.jp/web.jsp?MT=%s'
+                },
+                {
                     name: 'Baidu',
                     url: 'https://www.baidu.com/s?wd=%s'
                 },
@@ -60,9 +69,30 @@ const config = new Config({
                 {
                     name: 'GitHub',
                     url: 'https://github.com/search?q=%s'
+                },
+                {
+                    name: 'DuckDuckGo',
+                    url: 'https://duckduckgo.com/?q=%s'
+                },
+                {
+                    name: 'Yahoo',
+                    url: 'https://search.yahoo.com/search?p=%s'
+                },
+                {
+                    name: 'Amazon',
+                    url: 'https://www.amazon.co.jp/s?k=%s'
                 }
             ]
         },
+        pageSettings: {
+            media: -1,
+            geolocation: -1,
+            notifications: -1,
+            midiSysex: -1,
+            pointerLock: -1,
+            fullscreen: 1,
+            openExternal: -1,
+        },
         isAdBlock: true,
         language: 'ja',
         window: {
@@ -211,17 +241,53 @@ setPermissionRequestHandler = (ses, isPrivate = false) => {
                     if (permission == 'openExternal' && doc.openExternal != undefined && doc.openExternal > -1)
                         return callback(doc.openExternal === 0);
                 } else {
-                    if (Notification.isSupported()) {
-                        const notify = new Notification({
-                            icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
-                            title: `${url.protocol}//${url.hostname} が権限を要求しています。`,
-                            body: '詳細はここをクリックしてください。',
-                            silent: true
-                        });
-
-                        notify.show();
-
-                        notify.on('click', (e) => {
+                    if (config.get(`pageSettings.${permission}`) === null || config.get(`pageSettings.${permission}`) === -1) {
+                        if (Notification.isSupported()) {
+                            const notify = new Notification({
+                                icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
+                                title: `${url.protocol}//${url.hostname} が権限を要求しています。`,
+                                body: '詳細はここをクリックしてください。',
+                                silent: true
+                            });
+    
+                            notify.show();
+    
+                            notify.on('click', (e) => {
+                                dialog.showMessageBox({
+                                    type: 'info',
+                                    title: '権限の要求',
+                                    message: `${url.protocol}//${url.hostname} が権限を要求しています。`,
+                                    detail: `要求内容: ${permission}`,
+                                    checkboxLabel: 'このサイトでは今後も同じ処理をする',
+                                    checkboxChecked: false,
+                                    noLink: true,
+                                    buttons: ['はい / Yes', 'いいえ / No'],
+                                    defaultId: 0,
+                                    cancelId: 1
+                                }, (res, checked) => {
+                                    if (checked) {
+                                        if (permission == 'media')
+                                            db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, media: res }, { upsert: true });
+                                        if (permission == 'geolocation')
+                                            db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, geolocation: res }, { upsert: true });
+                                        if (permission == 'notifications')
+                                            db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, notifications: res }, { upsert: true });
+                                        if (permission == 'midiSysex')
+                                            db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, midiSysex: res }, { upsert: true });
+                                        if (permission == 'pointerLock')
+                                            db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, pointerLock: res }, { upsert: true });
+                                        if (permission == 'fullscreen')
+                                            db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, fullscreen: res }, { upsert: true });
+                                        if (permission == 'openExternal')
+                                            db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, openExternal: res }, { upsert: true });
+                                    }
+                                    return callback(res === 0);
+                                });
+                            });
+                            notify.on('close', (e) => {
+                                return callback(false);
+                            });
+                        } else {
                             dialog.showMessageBox({
                                 type: 'info',
                                 title: '権限の要求',
@@ -234,7 +300,6 @@ setPermissionRequestHandler = (ses, isPrivate = false) => {
                                 defaultId: 0,
                                 cancelId: 1
                             }, (res, checked) => {
-                                console.log(res, checked);
                                 if (checked) {
                                     if (permission == 'media')
                                         db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, media: res }, { upsert: true });
@@ -253,59 +318,48 @@ setPermissionRequestHandler = (ses, isPrivate = false) => {
                                 }
                                 return callback(res === 0);
                             });
-                        });
-                        notify.on('close', (e) => {
-                            return callback(false);
-                        });
-                    } else {
+                        }
+                    } else if (config.get(`pageSettings.${permission}`) === 0) {
+                        return callback(false);
+                    } else if (config.get(`pageSettings.${permission}`) === 1) {
+                        return callback(true);
+                    }
+                }
+            });
+        });
+    } else {
+        ses.setPermissionRequestHandler((webContents, permission, callback) => {
+            const url = parse(webContents.getURL());
+            
+            if (config.get(`pageSettings.${permission}`) === null || config.get(`pageSettings.${permission}`) === -1) {
+                if (Notification.isSupported()) {
+                    const notify = new Notification({
+                        icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
+                        title: `${url.protocol}//${url.hostname} が権限を要求しています。`,
+                        body: '詳細はここをクリックしてください。\nプライベート ウィンドウ',
+                        silent: true
+                    });
+    
+                    notify.show();
+    
+                    notify.on('click', (e) => {
                         dialog.showMessageBox({
                             type: 'info',
                             title: '権限の要求',
                             message: `${url.protocol}//${url.hostname} が権限を要求しています。`,
                             detail: `要求内容: ${permission}`,
-                            checkboxLabel: 'このサイトでは今後も同じ処理をする',
-                            checkboxChecked: false,
                             noLink: true,
                             buttons: ['はい / Yes', 'いいえ / No'],
                             defaultId: 0,
                             cancelId: 1
-                        }, (res, checked) => {
-                            if (checked) {
-                                if (permission == 'media')
-                                    db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, media: res }, { upsert: true });
-                                if (permission == 'geolocation')
-                                    db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, geolocation: res }, { upsert: true });
-                                if (permission == 'notifications')
-                                    db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, notifications: res }, { upsert: true });
-                                if (permission == 'midiSysex')
-                                    db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, midiSysex: res }, { upsert: true });
-                                if (permission == 'pointerLock')
-                                    db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, pointerLock: res }, { upsert: true });
-                                if (permission == 'fullscreen')
-                                    db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, fullscreen: res }, { upsert: true });
-                                if (permission == 'openExternal')
-                                    db.pageSettings.update({ origin: `${url.protocol}//${url.hostname}` }, { origin: `${url.protocol}//${url.hostname}`, openExternal: res }, { upsert: true });
-                            }
+                        }, (res) => {
                             return callback(res === 0);
                         });
-                    }
-                }
-            });
-        });
-    } else {
-        ses.setPermissionRequestHandler((webContents, permission, callback) => {
-            const url = parse(webContents.getURL());
-            if (Notification.isSupported()) {
-                const notify = new Notification({
-                    icon: path.join(app.getAppPath(), 'static', 'app', 'icon.png'),
-                    title: `${url.protocol}//${url.hostname} が権限を要求しています。`,
-                    body: '詳細はここをクリックしてください。\nプライベート ウィンドウ',
-                    silent: true
-                });
-
-                notify.show();
-
-                notify.on('click', (e) => {
+                    });
+                    notify.on('close', (e) => {
+                        return callback(false);
+                    });
+                } else {
                     dialog.showMessageBox({
                         type: 'info',
                         title: '権限の要求',
@@ -318,23 +372,11 @@ setPermissionRequestHandler = (ses, isPrivate = false) => {
                     }, (res) => {
                         return callback(res === 0);
                     });
-                });
-                notify.on('close', (e) => {
-                    return callback(false);
-                });
-            } else {
-                dialog.showMessageBox({
-                    type: 'info',
-                    title: '権限の要求',
-                    message: `${url.protocol}//${url.hostname} が権限を要求しています。`,
-                    detail: `要求内容: ${permission}`,
-                    noLink: true,
-                    buttons: ['はい / Yes', 'いいえ / No'],
-                    defaultId: 0,
-                    cancelId: 1
-                }, (res) => {
-                    return callback(res === 0);
-                });
+                }
+            } else if (config.get(`pageSettings.${permission}`) === 0) {
+                return callback(false);
+            } else if (config.get(`pageSettings.${permission}`) === 1) {
+                return callback(true);
             }
         });
     }
@@ -347,6 +389,10 @@ module.exports = class WindowManager {
         ipcMain.on('window-add', (e, args) => {
             this.addWindow(args.isPrivate);
         });
+        
+        ipcMain.on('window-fullScreen', (e, args) => {
+            this.addWindow(args.isPrivate);
+        });
 
         ipcMain.on('window-fixBounds', (e, args) => {
             this.windows.forEach((value, key) => {
@@ -397,9 +443,7 @@ module.exports = class WindowManager {
         ipcMain.on('data-apps-add', (e, args) => {
             db.apps.update({ id: args.id }, { id: args.id, name: args.name, description: args.description, url: args.url }, { upsert: true });
 
-            db.apps.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
-                console.log(docs)
-            });
+            db.apps.find({}).sort({ createdAt: -1 }).exec();
         });
 
         ipcMain.on('data-apps-remove', (e, args) => {
@@ -487,6 +531,13 @@ module.exports = class WindowManager {
             config.set('window.bounds', window.getBounds());
         });
 
+        window.on('focus', () => {
+            window.webContents.send(`window-focus-${id}`, {});
+        });
+        window.on('blur', () => {
+            window.webContents.send(`window-blur-${id}`, {});
+        });
+
         window.on('maximize', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
         window.on('unmaximize', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
         window.on('enter-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
@@ -494,7 +545,6 @@ module.exports = class WindowManager {
         window.on('enter-html-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
         window.on('leave-html-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
 
-        // registerProtocols();
         this.registerListeners(id);
 
         localShortcut.register(window, 'CmdOrCtrl+Shift+I', () => {
@@ -522,6 +572,13 @@ module.exports = class WindowManager {
             view.webContents.reloadIgnoringCache();
         });
 
+        localShortcut.register(window, 'F11', () => {
+            const window = this.windows.get(id);
+            
+            window.setFullScreen(!window.isFullScreen());
+            this.fixBounds(id, (floatingWindows.indexOf(id) != -1));
+        })
+
         this.windows.set(id, window);
 
         if (process.argv != undefined) {
@@ -530,19 +587,26 @@ module.exports = class WindowManager {
     }
 
     registerListeners = (id) => {
-        ipcMain.on(`browserview-add-${id}`, (e, args) => {
+        ipcMain.on(`window-fullScreen-${id}`, (e, args) => {
+            const window = this.windows.get(id);
+            
+            window.setFullScreen(!window.isFullScreen());
+            this.fixBounds(id, (floatingWindows.indexOf(id) != -1));
+        });
+
+        ipcMain.on(`browserView-add-${id}`, (e, args) => {
             this.addView(id, args.url, args.isActive);
         });
 
-        ipcMain.on(`browserview-remove-${id}`, (e, args) => {
+        ipcMain.on(`browserView-remove-${id}`, (e, args) => {
             this.removeView(id, args.id);
         });
 
-        ipcMain.on(`browserview-select-${id}`, (e, args) => {
+        ipcMain.on(`browserView-select-${id}`, (e, args) => {
             this.selectView(id, args.id);
         });
 
-        ipcMain.on(`browserview-get-${id}`, (e, args) => {
+        ipcMain.on(`browserView-get-${id}`, (e, args) => {
             let datas = [];
 
             views[id].map((item) => {
@@ -550,10 +614,10 @@ module.exports = class WindowManager {
 
                 datas.push({ id: item.view.webContents.id, title: item.view.webContents.getTitle(), url, icon: this.getFavicon(url), color: '#0a84ff', isBookmarked: false });
             });
-            e.sender.send(`browserview-get-${id}`, { views: datas });
+            e.sender.send(`browserView-get-${id}`, { views: datas });
         });
 
-        ipcMain.on(`browserview-goBack-${id}`, (e, args) => {
+        ipcMain.on(`browserView-goBack-${id}`, (e, args) => {
             views[id].filter(function (view, i) {
                 if (view.view.webContents.id == args.id) {
                     let webContents = views[id][i].view.webContents;
@@ -563,7 +627,7 @@ module.exports = class WindowManager {
             });
         });
 
-        ipcMain.on(`browserview-goForward-${id}`, (e, args) => {
+        ipcMain.on(`browserView-goForward-${id}`, (e, args) => {
             views[id].filter(function (view, i) {
                 if (view.view.webContents.id == args.id) {
                     let webContents = views[id][i].view.webContents;
@@ -573,7 +637,7 @@ module.exports = class WindowManager {
             });
         });
 
-        ipcMain.on(`browserview-reload-${id}`, (e, args) => {
+        ipcMain.on(`browserView-reload-${id}`, (e, args) => {
             views[id].filter(function (view, i) {
                 if (view.view.webContents.id == args.id) {
                     let webContents = views[id][i].view.webContents;
@@ -582,7 +646,7 @@ module.exports = class WindowManager {
             });
         });
 
-        ipcMain.on(`browserview-stop-${id}`, (e, args) => {
+        ipcMain.on(`browserView-stop-${id}`, (e, args) => {
             views[id].filter(function (view, i) {
                 if (view.view.webContents.id == args.id) {
                     let webContents = views[id][i].view.webContents;
@@ -591,16 +655,16 @@ module.exports = class WindowManager {
             });
         });
 
-        ipcMain.on(`browserview-goHome-${id}`, (e, args) => {
+        ipcMain.on(`browserView-goHome-${id}`, (e, args) => {
             views[id].filter(function (view, i) {
                 if (view.view.webContents.id == args.id) {
                     let webContents = views[id][i].view.webContents;
-                    webContents.loadURL(config.get('homePage.defaultPage'));
+                    webContents.loadURL(config.get('homePage.isDefaultHomePage') ? `${protocolStr}://home/` : config.get('homePage.defaultPage'));
                 }
             });
         });
 
-        ipcMain.on(`browserview-loadURL-${id}`, (e, args) => {
+        ipcMain.on(`browserView-loadURL-${id}`, (e, args) => {
             views[id].filter(function (view, i) {
                 if (view.view.webContents.id == args.id) {
                     let webContents = views[id][i].view.webContents;
@@ -609,7 +673,7 @@ module.exports = class WindowManager {
             });
         });
 
-        ipcMain.on(`browserview-loadFile-${id}`, (e, args) => {
+        ipcMain.on(`browserView-loadFile-${id}`, (e, args) => {
             views[id].filter(function (view, i) {
                 if (view.view.webContents.id == args.id) {
                     let webContents = views[id][i].view.webContents;
@@ -618,12 +682,32 @@ module.exports = class WindowManager {
             });
         });
 
+        ipcMain.on(`browserView-zoomIn-${id}`, (e, args) => {
+            views[id].filter(function (view, i) {
+                if (view.view.webContents.id == args.id) {
+                    let webContents = views[id][i].view.webContents;
+                    console.log(webContents.getZoomFactor());
+                    webContents.setZoomFactor(webContents.getZoomFactor() + 0.1);
+                }
+            });
+        });
+
+        ipcMain.on(`browserView-zoomOut-${id}`, (e, args) => {
+            views[id].filter(function (view, i) {
+                if (view.view.webContents.id == args.id) {
+                    let webContents = views[id][i].view.webContents;
+                    console.log(webContents.getZoomFactor());
+                    webContents.setZoomFactor(webContents.getZoomFactor() - 0.1);
+                }
+            });
+        });
+
         ipcMain.on(`data-bookmark-add-${id}`, (e, args) => {
             views[id].filter((view, i) => {
                 if (view.view.webContents.id == args.id) {
                     let v = views[id][i].view;
                     db.bookmarks.insert({ title: v.webContents.getTitle(), url: v.webContents.getURL(), isPrivate: args.isPrivate });
-                    this.updateBookmarkState(id, v);
+                    this.updateViewState(id, v);
                 }
             });
         });
@@ -633,7 +717,7 @@ module.exports = class WindowManager {
                 if (view.view.webContents.id == args.id) {
                     let v = views[id][i].view;
                     db.bookmarks.remove({ url: v.webContents.getURL(), isPrivate: args.isPrivate }, {});
-                    this.updateBookmarkState(id, v);
+                    this.updateViewState(id, v);
                 }
             });
         });
@@ -664,12 +748,12 @@ module.exports = class WindowManager {
         });
     }
 
-    updateBookmarkState = (id, view) => {
+    updateViewState = (id, view) => {
         const window = this.windows.get(id);
         db.bookmarks.find({ url: view.webContents.getURL(), isPrivate: (String(id).startsWith('private')) }, (err, docs) => {
             const url = view.webContents.getURL();
 
-            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) });
+            window.webContents.send(`browserView-load-${id}`, { id: view.webContents.id, title: view.webContents.getTitle(), url: url, icon: this.getFavicon(url), color: '#0a84ff', isAudioPlaying: !view.webContents.isCurrentlyAudible(), isBookmarked: (docs.length > 0 ? true : false) });
         });
     }
 
@@ -688,7 +772,7 @@ module.exports = class WindowManager {
                         resolve(result !== null ? result : '#0a84ff');
                     });
             } else {
-                reject(new Error('WebContents are not available'))
+                reject(new Error('WebContents are not available'));
             }
         });
     };
@@ -740,6 +824,55 @@ module.exports = class WindowManager {
         view.setAutoResize({ width: true, height: true });
     }
 
+    getDomain = (url) => {
+        let hostname = url;
+
+        if (hostname.indexOf('http://') !== -1 || hostname.indexOf('https://') !== -1) {
+            hostname = hostname.split('://')[1];
+        }
+
+        if (hostname.indexOf('?') !== -1) {
+            hostname = hostname.split('?')[0];
+        }
+
+        if (hostname.indexOf('://') !== -1) {
+            hostname = `${hostname.split('://')[0]}://${hostname.split('/')[2]}`;
+        } else {
+            hostname = hostname.split('/')[0];
+        }
+
+        return hostname;
+    }
+
+    getCertificate = (url) => {
+        return new Promise((resolve, reject) => {
+            if (url.startsWith('http://')) return resolve({ type: 'InSecure' });
+            else if (url.startsWith(`${protocolStr}://`) || url.startsWith(`${fileProtocolStr}://`)) return resolve({ type: 'Internal' });
+
+            const domain = this.getDomain(url);
+
+            let options = {
+                host: domain,
+                port: 443,
+                method: 'GET'
+            };
+
+            let req = https.request(options, (res) => {
+                let certificate = res.connection.getPeerCertificate();
+                if (certificate.subject == null) return;
+
+                const data = {
+                    type: 'Secure',
+                    title: certificate.subject.O,
+                    country: certificate.subject.C
+                };
+                resolve(data);
+            });
+
+            req.end();
+        });
+    }
+
     getHeight = (b, height, baseBarHeight, bookMarkBarHeight) => {
         if (b) {
             return config.get('design.isBookmarkBar') ? (baseBarHeight + bookMarkBarHeight) : baseBarHeight;
@@ -775,11 +908,15 @@ module.exports = class WindowManager {
 
     selectView = (windowId, id) => {
         const window = this.windows.get(windowId);
-        views[windowId].filter((view, i) => {
-            if (id == view.view.webContents.id) {
-                window.setBrowserView(views[windowId][i].view);
-                window.setTitle(`${views[windowId][i].view.webContents.getTitle()} - ${pkg.name}`);
-                window.webContents.send(`browserview-set-${windowId}`, { id: id });
+        views[windowId].filter((item, i) => {
+            if (id == item.view.webContents.id) {
+                window.setBrowserView(item.view);
+                window.setTitle(`${item.view.webContents.getTitle()} - ${pkg.name}`);
+                
+                this.updateNavigationState(windowId, item.view);
+                this.updateViewState(windowId, item.view);
+
+                window.webContents.send(`browserView-set-${windowId}`, { id: id });
                 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
             }
         });
@@ -791,7 +928,7 @@ module.exports = class WindowManager {
 
         window.setBrowserView(item.view);
         window.setTitle(`${item.view.webContents.getTitle()} - ${pkg.name}`);
-        window.webContents.send(`browserview-set-${windowId}`, { id: item.id });
+        window.webContents.send(`browserView-set-${windowId}`, { id: item.id });
         this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
     }
 
@@ -803,7 +940,7 @@ module.exports = class WindowManager {
             datas.push({ id: views[windowId][i].view.webContents.id, title: views[windowId][i].view.webContents.getTitle(), url: url, icon: this.getFavicon(url) });
         }
         const window = this.windows.get(windowId);
-        window.webContents.send(`browserview-get-${windowId}`, { views: datas });
+        window.webContents.send(`browserView-get-${windowId}`, { views: datas });
     }
 
     addTab = (windowId, url = config.get('homePage.defaultPage'), isActive = true) => {
@@ -820,28 +957,75 @@ module.exports = class WindowManager {
             }
         });
 
+        view.webContents.setVisualZoomLevelLimits(1, 3);
+
         const window = this.windows.get(windowId);
         const id = view.webContents.id;
 
+        const executeJs = this.getRandString(12);
+        let viewId = '';
+
         runAdblockService(window, windowId, id, view.webContents.session);
 
         view.webContents.on('did-start-loading', () => {
             if (view.isDestroyed()) return;
 
-            window.webContents.send(`browserview-start-loading-${windowId}`, { id: id });
+            window.webContents.send(`browserView-start-loading-${windowId}`, { id: id });
         });
         view.webContents.on('did-stop-loading', () => {
             if (view.isDestroyed()) return;
 
-            window.webContents.send(`browserview-stop-loading-${windowId}`, { id: id });
+            window.webContents.send(`browserView-stop-loading-${windowId}`, { id: id });
         });
 
+        view.webContents.on('did-start-navigation', (e) => {
+            if (view.isDestroyed()) return;
+
+            const url = view.webContents.getURL();
+
+            /*
+            view.webContents.executeJavaScript(
+                `(function () {
+                    if (document.getElementById('tip-${executeJs}') === null) {
+                        let ${executeJs} = document.createElement('span');
+                        document.body.insertBefore(${executeJs}, document.body.firstChild);
+    
+                        ${executeJs}.id = 'tip-${executeJs}';
+                        ${executeJs}.style.display = 'none';
+                        ${executeJs}.style.position = 'fixed';
+                        ${executeJs}.style.bottom = 0;
+                        ${executeJs}.style.left = 0;
+                        ${executeJs}.style.zIndex = 999999;
+                        ${executeJs}.style.padding = '2px';
+                        ${executeJs}.style.userSelect = 'none';
+                        ${executeJs}.style.backgroundColor = '#f9f9fa';
+                        ${executeJs}.style.color = '#c7c7c7';
+                        ${executeJs}.style.borderTop = 'solid 1px #c7c7c7';
+                        ${executeJs}.style.borderRight = 'solid 1px #c7c7c7';
+                        ${executeJs}.style.fontSize = '12px';
+                        document.getElementById('tip-${executeJs}').style.fontFamily = '"Lato","Helvetica Neue",Helvetica,Arial,sans-serif';
+                    }
+                })()`
+            );
+            */
+
+            if (config.get('isAdBlock') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
+                removeAds(url, view.webContents);
+
+            this.updateNavigationState(windowId, view);
+        });
         view.webContents.on('did-finish-load', (e) => {
             if (view.isDestroyed()) return;
 
+            viewId = this.getRandString(12);
+
+            this.getCertificate(view.webContents.getURL()).then((certificate) => {
+                window.webContents.send(`browserView-certificate-${windowId}`, { id, certificate });
+            });
+
             window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
-            this.updateBookmarkState(windowId, view);
 
+            this.updateViewState(windowId, view);
             this.updateNavigationState(windowId, view);
         });
         view.webContents.on('did-fail-load', (e, code, description, url, isMainFrame, processId, routingId) => {
@@ -850,49 +1034,63 @@ module.exports = class WindowManager {
             dialog.showMessageBox({ message: `${code}: ${description}` });
         });
 
-        view.webContents.on('did-start-navigation', (e) => {
-            if (view.isDestroyed()) return;
-
-            const url = view.webContents.getURL();
-
-            if (config.get('isAdBlock') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
-                removeAds(url, view.webContents);
-
-            this.updateNavigationState(windowId, view);
-        });
-
         view.webContents.on('page-title-updated', (e) => {
             if (view.isDestroyed()) return;
 
-            window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
-            this.updateBookmarkState(windowId, view);
-
             if (!String(windowId).startsWith('private') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
-                db.historys.insert({ title: view.webContents.getTitle(), url: view.webContents.getURL() });
+                db.historys.update({ id: viewId }, { id: viewId, title: view.webContents.getTitle(), url: view.webContents.getURL() }, { upsert: true });
+
+            window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
 
+            this.updateViewState(windowId, view);
             this.updateNavigationState(windowId, view);
         });
-
         view.webContents.on('page-favicon-updated', (e, favicons) => {
             if (view.isDestroyed()) return;
 
+            if (!String(windowId).startsWith('private') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
+                db.historys.update({ id: viewId }, { id: viewId, title: view.webContents.getTitle(), url: view.webContents.getURL() }, { upsert: true });
+
             window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
 
-            this.updateBookmarkState(windowId, view);
+            this.updateViewState(windowId, view);
             this.updateNavigationState(windowId, view);
         });
-
         view.webContents.on('did-change-theme-color', (e, color) => {
             if (view.isDestroyed()) return;
 
+            if (!String(windowId).startsWith('private') && !(view.webContents.getURL().startsWith(`${protocolStr}://`) || view.webContents.getURL().startsWith(`${fileProtocolStr}://`)))
+                db.historys.update({ id: viewId }, { id: viewId, title: view.webContents.getTitle(), url: view.webContents.getURL() }, { upsert: true });
+
             window.setTitle(`${view.webContents.getTitle()} - ${pkg.name}`);
 
-            this.updateBookmarkState(windowId, view);
+            this.updateViewState(windowId, view);
             this.updateNavigationState(windowId, view);
 
-            window.webContents.send(`browserview-theme-color-${windowId}`, { id: view.webContents.id, color });
+            window.webContents.send(`browserView-theme-color-${windowId}`, { id: view.webContents.id, color });
         });
 
+        view.webContents.on('update-target-url', (e, url) => {
+            /*
+            if (url.length > 0) {
+                view.webContents.executeJavaScript(
+                    `(function () {
+                        let dom = document.getElementById('tip-${executeJs}');
+                        
+                        document.getElementById('tip-${executeJs}').style.display = 'block';
+                        dom.textContent = '${url}';
+                    })()`
+                );
+            } else {
+                view.webContents.executeJavaScript(
+                    `(function () {
+                        document.getElementById('tip-${executeJs}').style.display = 'none';
+                    })()`
+                );
+            }
+            */
+        })
+
         view.webContents.on('new-window', (e, url) => {
             if (view.isDestroyed()) return;
 
@@ -949,13 +1147,12 @@ module.exports = class WindowManager {
             if (view.isDestroyed()) return;
 
             let menu;
-            if (params.linkURL !== '') {
+            if (params.linkURL !== '' && !params.hasImageContents) {
                 menu = Menu.buildFromTemplate(
                     [
                         {
                             label: lang.window.view.contextMenu.link.newTab,
                             click: () => {
-                                this.addView(windowId, `view-source:${view.webContents.getURL()}`, false);
                                 this.addView(windowId, params.linkURL, true);
                             }
                         },
@@ -992,7 +1189,7 @@ module.exports = class WindowManager {
                         }
                     ]
                 );
-            } else if (params.hasImageContents) {
+            } else if (params.linkURL === '' && params.hasImageContents) {
                 menu = Menu.buildFromTemplate(
                     [
                         {
@@ -1041,6 +1238,81 @@ module.exports = class WindowManager {
                         }
                     ]
                 );
+            } else if (params.linkURL !== '' && params.hasImageContents) {
+                menu = Menu.buildFromTemplate(
+                    [
+                        {
+                            label: lang.window.view.contextMenu.link.newTab,
+                            click: () => {
+                                this.addView(windowId, params.linkURL, true);
+                            }
+                        },
+                        {
+                            label: lang.window.view.contextMenu.link.newWindow,
+                            enabled: false,
+                            click: () => { view.webContents.openDevTools(); }
+                        },
+                        {
+                            label: lang.window.view.contextMenu.link.openPrivateWindow,
+                            enabled: false,
+                            click: () => { view.webContents.openDevTools(); }
+                        },
+                        { type: 'separator' },
+                        {
+                            label: lang.window.view.contextMenu.link.copy,
+                            accelerator: 'CmdOrCtrl+C',
+                            click: () => {
+                                clipboard.clear();
+                                clipboard.writeText(params.linkURL);
+                            }
+                        },
+                        { type: 'separator' },
+                        {
+                            label: lang.window.view.contextMenu.image.newTab,
+                            click: () => {
+                                this.addView(windowId, params.srcURL, true);
+                            }
+                        },
+                        {
+                            label: lang.window.view.contextMenu.image.saveImage,
+                            enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
+                            click: () => {
+                                download(window, params.srcURL, {
+                                    directory: app.getPath('downloads'),
+                                    saveAs: true
+                                });
+                            }
+                        },
+                        {
+                            label: lang.window.view.contextMenu.image.copyImage,
+                            click: () => {
+                                const img = nativeImage.createFromDataURL(params.srcURL);
+
+                                clipboard.clear();
+                                clipboard.writeImage(img);
+                            }
+                        },
+                        {
+                            label: lang.window.view.contextMenu.image.copyLink,
+                            click: () => {
+                                clipboard.clear();
+                                clipboard.writeText(params.srcURL);
+                            }
+                        },
+                        { type: 'separator' },
+                        {
+                            label: lang.window.view.contextMenu.devTool,
+                            accelerator: 'CmdOrCtrl+Shift+I',
+                            enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
+                            click: () => {
+                                if (view.webContents.isDevToolsOpened())
+                                    view.webContents.devToolsWebContents.focus();
+                                else
+                                    view.webContents.openDevTools();
+                            }
+                        }
+                    ]
+                );
             } else if (params.isEditable) {
                 menu = Menu.buildFromTemplate(
                     [
@@ -1170,6 +1442,7 @@ module.exports = class WindowManager {
                         {
                             label: lang.window.view.contextMenu.savePage,
                             accelerator: 'CmdOrCtrl+S',
+                            icon: `${app.getAppPath()}/static/save.png`,
                             enabled: !view.webContents.getURL().startsWith(`${protocolStr}://`),
                             click: () => {
                                 dialog.showSaveDialog(window, {
@@ -1220,7 +1493,6 @@ module.exports = class WindowManager {
 
         view.webContents.on('before-input-event', (e, input) => {
             if (view.isDestroyed()) return;
-            console.log((input.control || (platform.isDarwin && input.meta)) || input.alt)
             window.webContents.setIgnoreMenuShortcuts(true);
 
             if (input.control) {
@@ -1245,7 +1517,6 @@ module.exports = class WindowManager {
                     window.webContents.setIgnoreMenuShortcuts(true);
                     // view.webContents.setIgnoreMenuShortcuts(true);
 
-                    console.log('test');
                     this.addView(windowId, config.get('homePage.defaultPage'), true);
                 }
             } else if (input.alt) {
@@ -1277,18 +1548,18 @@ module.exports = class WindowManager {
         });
 
         view.webContents.session.on('will-download', (event, item, webContents) => {
+            if (id !== webContents.id) return;
+
             const str = this.getRandString(12);
-            db.downloads.find({ id: str }, (err, docs) => {
-                if (docs < 1)
-                    db.downloads.insert({ id: str, name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: item.getState() });
-            });
+            db.downloads.update({ id: str }, { id: str, name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: item.getState() }, { upsert: true });
+
             item.on('updated', (e, state) => {
-                db.downloads.update({ id: str }, { $set: { name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: state } });
+                db.downloads.update({ id: str }, { id: str, name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: item.getState() }, { upsert: true });
             });
 
             item.once('done', (e, state) => {
                 const filePath = item.getSavePath();
-                db.downloads.update({ id: str }, { $set: { name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: state } });
+                db.downloads.update({ id: str }, { id: str, name: item.getFilename(), url: item.getURL(), type: item.getMimeType(), size: item.getTotalBytes(), path: item.getSavePath(), status: item.getState() }, { upsert: true });
                 if (state === 'completed') {
                     window.webContents.send(`notification-${windowId}`, { id: id, content: `${item.getFilename()} のダウンロードが完了しました。` });
 
@@ -1317,10 +1588,9 @@ module.exports = class WindowManager {
         if (views[windowId] == undefined)
             views[windowId] = [];
         views[windowId].push({ id, view, isNotificationBar: false });
-        console.log(views);
 
         if (isActive) {
-            window.webContents.send(`browserview-set-${windowId}`, { id: id });
+            window.webContents.send(`browserView-set-${windowId}`, { id: id });
             window.setBrowserView(view);
         }