1 const { app, ipcMain, protocol, session, BrowserWindow, BrowserView, Menu, nativeImage, clipboard, dialog, Notification } = require('electron');
2 const path = require('path');
3 const fs = require('fs');
4 const url = require('url');
5 const os = require('os');
7 const localShortcut = require("electron-localshortcut");
9 const Config = require('electron-store');
10 const config = new Config({
18 defaultPage: 'my://newtab',
19 defaultEngine: 'Google',
23 url: 'https://www.google.com/search?q=%s'
27 url: 'https://www.bing.com/search?q=%s'
31 url: 'https://search.yahoo.co.jp/search?p=%s'
35 url: 'https://search.goo.ne.jp/web.jsp?MT=%s'
38 name: 'Google Translate',
39 url: 'https://translate.google.com/?text=%s'
43 url: 'https://www.youtube.com/results?search_query=%s'
47 url: 'https://www.twitter.com/search?q=%s'
51 url: 'https://github.com/search?q=%s'
57 isCustomTitlebar: true,
67 const Datastore = require('nedb');
70 db.history = new Datastore({
71 filename: path.join(app.getPath('userData'), 'Files', 'History.db'),
75 db.bookmark = new Datastore({
76 filename: path.join(app.getPath('userData'), 'Files', 'Bookmark.db'),
81 const { loadFilters, updateFilters, removeAds } = require('./AdBlocker');
83 let floatingWindows = [];
87 getBaseWindow = (width = 1100, height = 680, minWidth = 320, minHeight = 200, x, y, frame = false) => {
88 return new BrowserWindow({
89 width, height, minWidth, minHeight, x, y, 'titleBarStyle': 'hidden', frame, fullscreenable: true,
91 nodeIntegration: true,
94 experimentalFeatures: true,
95 contextIsolation: false,
100 registerProtocols = () => {
101 protocol.isProtocolHandled('my', (handled) => {
102 console.log(handled);
104 protocol.registerFileProtocol('my', (request, callback) => {
105 const parsed = url.parse(request.url);
107 if (parsed.hostname.endsWith('.css') || parsed.hostname.endsWith('.js')) {
109 path: path.join(app.getAppPath(), 'pages', parsed.hostname),
113 path: path.join(app.getAppPath(), 'pages', `${parsed.hostname}.html`),
117 if (error) console.error('Failed to register protocol: ' + error);
123 registerProtocolWithPrivateMode = (windowId) => {
124 session.fromPartition(windowId).protocol.registerFileProtocol('my', (request, callback) => {
125 const parsed = url.parse(request.url);
127 if (parsed.hostname.endsWith('.css') || parsed.hostname.endsWith('.js')) {
129 path: path.join(app.getAppPath(), 'pages', parsed.hostname),
133 path: path.join(app.getAppPath(), 'pages', `${parsed.hostname}.html`),
137 if (error) console.error('Failed to register protocol: ' + error);
141 module.exports = class WindowManager {
143 this.windows = new Map();
145 ipcMain.on('window-add', (e, args) => {
146 this.addWindow(args.isPrivate);
149 ipcMain.on('update-filters', (e, args) => {
153 ipcMain.on('data-history-get', (e, args) => {
154 db.history.find({}).sort({ createdAt: -1 }).exec((err, docs) => {
155 e.sender.send('data-history-get', { historys: docs });
159 ipcMain.on('data-history-clear', (e, args) => {
160 db.history.remove({}, { multi: true });
163 ipcMain.on('data-bookmark-get', (e, args) => {
164 db.bookmark.find({ isPrivate: args.isPrivate }).sort({ createdAt: -1 }).exec((err, docs) => {
165 e.sender.send('data-bookmark-get', { bookmarks: docs });
169 ipcMain.on('data-bookmark-clear', (e, args) => {
170 db.bookmark.remove({}, { multi: true });
173 ipcMain.on('clear-browsing-data', () => {
174 const ses = session.defaultSession;
175 ses.clearCache((err) => {
176 if (err) log.error(err);
179 ses.clearStorageData({
194 db.history.remove({}, { multi: true });
195 db.bookmark.remove({}, { multi: true });
199 addWindow = (isPrivate = false) => {
203 const { width, height, x, y } = config.get('window.bounds');
204 const window = getBaseWindow(config.get('window.isMaximized') ? 1110 : width, config.get('window.isMaximized') ? 680 : height, 320, 200, x, y, !config.get('window.isCustomTitlebar'));
206 const id = (!isPrivate ? window.id : `private-${window.id}`);
208 config.get('window.isMaximized') && window.maximize();
210 const startUrl = process.env.ELECTRON_START_URL || url.format({
211 pathname: path.join(__dirname, '/../build/index.html'), // 警告:このファイルを移動する場合ここの相対パスの指定に注意してください
214 hash: `/window/${id}`,
217 window.loadURL(startUrl);
219 localShortcut.register(window, 'CmdOrCtrl+Shift+I', () => {
220 if (window.getBrowserView() == undefined) return;
221 const view = window.getBrowserView();
223 if (view.webContents.isDevToolsOpened()) {
224 view.webContents.devToolsWebContents.focus();
226 view.webContents.openDevTools();
230 localShortcut.register(window, 'CmdOrCtrl+R', () => {
231 if (window.getBrowserView() == undefined) return;
232 const view = window.getBrowserView();
234 view.webContents.reload();
237 localShortcut.register(window, 'CmdOrCtrl+Shift+R', () => {
238 if (window.getBrowserView() == undefined) return;
239 const view = window.getBrowserView();
241 view.webContents.reloadIgnoringCache();
244 window.on('closed', () => {
245 this.windows.delete(id);
248 ['resize', 'move'].forEach(ev => {
249 window.on(ev, () => {
250 config.set('window.isMaximized', window.isMaximized());
251 config.set('window.bounds', window.getBounds());
255 window.on('close', (e) => {
256 for (var i = 0; i < views.length; i++) {
257 if (views[i].windowId == id) {
263 window.on('maximize', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
264 window.on('unmaximize', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
265 window.on('enter-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
266 window.on('leave-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
267 window.on('enter-html-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
268 window.on('leave-html-full-screen', this.fixBounds.bind(this, id, (floatingWindows.indexOf(id) != -1)));
270 // registerProtocols();
271 this.registerListeners(id);
273 this.windows.set(id, window);
276 registerListeners = (id) => {
277 ipcMain.on(`browserview-add-${id}`, (e, args) => {
278 this.addView(id, args.url, args.isActive);
281 ipcMain.on(`browserview-remove-${id}`, (e, args) => {
282 this.removeView(id, args.id);
285 ipcMain.on(`browserview-select-${id}`, (e, args) => {
286 this.selectView(id, args.id);
289 ipcMain.on(`browserview-get-${id}`, (e, args) => {
291 for (var i = 0; i < views.length; i++) {
292 if (views[i].windowId == id) {
293 const url = views[i].view.webContents.getURL();
294 datas.push({ id: views[i].id, title: views[i].view.webContents.getTitle(), url: url, icon: url.startsWith('my://') ? undefined : `http://www.google.com/s2/favicons?domain=${url}` });
297 e.sender.send(`browserview-get-${id}`, { views: datas });
300 ipcMain.on(`browserview-goBack-${id}`, (e, args) => {
301 views.filter(function (view, i) {
302 if (view.id == args.id) {
303 let webContents = views[i].view.webContents;
304 if (webContents.canGoBack())
305 webContents.goBack();
310 ipcMain.on(`browserview-goForward-${id}`, (e, args) => {
311 views.filter(function (view, i) {
312 if (view.id == args.id) {
313 let webContents = views[i].view.webContents;
314 if (webContents.canGoForward())
315 webContents.goForward();
320 ipcMain.on(`browserview-reload-${id}`, (e, args) => {
321 views.filter(function (view, i) {
322 if (view.id == args.id) {
323 let webContents = views[i].view.webContents;
324 webContents.reload();
329 ipcMain.on(`browserview-stop-${id}`, (e, args) => {
330 views.filter(function (view, i) {
331 if (view.id == args.id) {
332 let webContents = views[i].view.webContents;
338 ipcMain.on(`browserview-goHome-${id}`, (e, args) => {
339 views.filter(function (view, i) {
340 if (view.id == args.id) {
341 let webContents = views[i].view.webContents;
342 webContents.loadURL(config.get('homePage.defaultPage'));
347 ipcMain.on(`browserview-loadURL-${id}`, (e, args) => {
348 views.filter(function (view, i) {
349 if (view.id == args.id) {
350 let webContents = views[i].view.webContents;
351 webContents.loadURL(args.url);
356 ipcMain.on(`browserview-loadFile-${id}`, (e, args) => {
357 views.filter(function (view, i) {
358 if (view.id == args.id) {
359 let webContents = views[i].view.webContents;
360 webContents.loadFile(args.url);
365 ipcMain.on(`data-bookmark-add-${id}`, (e, args) => {
366 views.filter((view, i) => {
367 if (view.id == args.id) {
368 let v = views[i].view;
369 db.bookmark.insert({ title: v.webContents.getTitle(), url: v.webContents.getURL(), isPrivate: args.isPrivate });
370 this.updateBookmarkState(id, args.id, v);
375 ipcMain.on(`data-bookmark-remove-${id}`, (e, args) => {
376 views.filter((view, i) => {
377 if (view.id == args.id) {
378 let v = views[i].view;
379 db.bookmark.remove({ url: v.webContents.getURL(), isPrivate: args.isPrivate }, {});
380 this.updateBookmarkState(id, args.id, v);
385 ipcMain.on(`data-bookmark-has-${id}`, (e, args) => {
386 views.filter((view, i) => {
387 if (view.id == args.id) {
388 let v = views[i].view;
389 db.bookmark.find({ url: v.webContents.getURL(), isPrivate: args.isPrivate }, (err, docs) => {
390 e.sender.send(`data-bookmark-has-${id}`, { isBookmarked: (docs.length > 0 ? true : false) });
397 updateNavigationState = (windowId, id, view) => {
398 const window = this.windows.get(windowId);
399 window.webContents.send(`update-navigation-state-${windowId}`, {
401 canGoBack: view.webContents.canGoBack(),
402 canGoForward: view.webContents.canGoForward(),
406 updateBookmarkState = (windowId, id, view) => {
407 const window = this.windows.get(windowId);
408 db.bookmark.find({ url: view.webContents.getURL(), isPrivate: (String(windowId).startsWith('private')) }, (err, docs) => {
409 window.webContents.send(`browserview-load-${windowId}`, { id: id, title: view.webContents.getTitle(), url: view.webContents.getURL(), isBookmarked: (docs.length > 0 ? true : false) });
413 fixBounds = (windowId, isFloating = false) => {
414 const window = this.windows.get(windowId);
416 if (window.getBrowserView() == undefined) return;
417 const view = window.getBrowserView();
419 const { width, height } = window.getContentBounds();
421 view.setAutoResize({ width: true, height: true });
423 window.setMinimizable(false);
424 window.setMaximizable(false);
425 window.setAlwaysOnTop(true);
426 window.setVisibleOnAllWorkspaces(true);
434 window.setMinimizable(true);
435 window.setMaximizable(true);
436 window.setAlwaysOnTop(false);
437 window.setVisibleOnAllWorkspaces(false);
438 if (window.isFullScreen()) {
450 height: window.isMaximized() ? height - 73 : (height - 73) - 2,
454 view.setAutoResize({ width: true, height: true });
457 addView = (windowId, url, isActive) => {
458 const id = tabCount++;
459 this.addTab(windowId, id, url, isActive);
462 removeView = (windowId, id) => {
463 views.filter((view, i) => {
464 if (windowId == view.windowId && id == view.id) {
470 selectView = (windowId, id) => {
471 const window = this.windows.get(windowId);
472 views.filter((view, i) => {
473 if (windowId == view.windowId && id == view.id) {
474 window.setBrowserView(views[i].view);
475 window.setTitle(views[i].view.webContents.getTitle());
476 window.webContents.send(`browserview-set-${windowId}`, { id: id });
477 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
482 getViews = (windowId) => {
484 for (var i = 0; i < views.length; i++) {
485 if (views[i].windowId == windowId) {
486 const url = views[i].view.webContents.getURL();
487 datas.push({ id: views[i].id, title: views[i].view.webContents.getTitle(), url: url, icon: url.startsWith('my://') ? undefined : `http://www.google.com/s2/favicons?domain=${url}` });
490 const window = this.windows.get(windowId);
491 window.webContents.send(`browserview-get-${windowId}`, { views: datas });
494 addTab = (windowId, id, url = config.get('homePage.defaultPage'), isActive = true) => {
495 const window = this.windows.get(windowId);
497 const view = new BrowserView({
499 nodeIntegration: false,
500 contextIsolation: false,
502 experimentalFeatures: true,
504 safeDialogsMessage: '今後このページではダイアログを表示しない',
505 ...(String(windowId).startsWith('private') && { partition: windowId }),
506 preload: require.resolve('./Preload')
510 const defaultUserAgent = view.webContents.getUserAgent();
512 if (String(windowId).startsWith('private')) {
513 registerProtocolWithPrivateMode(windowId);
516 view.webContents.on('did-start-loading', () => {
517 if (view.isDestroyed()) return;
519 window.webContents.send(`browserview-start-loading-${windowId}`, { id: id });
521 view.webContents.on('did-stop-loading', () => {
522 if (view.isDestroyed()) return;
524 window.webContents.send(`browserview-stop-loading-${windowId}`, { id: id });
527 view.webContents.on('did-finish-load', (e) => {
528 if (view.isDestroyed()) return;
530 if (String(windowId).startsWith('private'))
531 view.webContents.setUserAgent(defaultUserAgent + ' PrivMode');
533 window.setTitle(view.webContents.getTitle());
534 this.updateBookmarkState(windowId, id, view);
536 this.updateNavigationState(windowId, id, view);
539 view.webContents.on('did-start-navigation', (e) => {
540 if (view.isDestroyed()) return;
542 const url = view.webContents.getURL();
544 if (config.get('adBlocker'))
545 removeAds(url, view.webContents);
547 this.updateNavigationState(windowId, id, view);
550 view.webContents.on('page-title-updated', (e) => {
551 if (view.isDestroyed()) return;
553 window.setTitle(view.webContents.getTitle());
554 this.updateBookmarkState(windowId, id, view);
556 if (!String(windowId).startsWith('private') && !view.webContents.getURL().startsWith('my://'))
557 db.history.insert({ title: view.webContents.getTitle(), url: view.webContents.getURL() });
559 this.updateNavigationState(windowId, id, view);
562 view.webContents.on('page-favicon-updated', (e, favicons) => {
563 if (view.isDestroyed()) return;
565 window.setTitle(view.webContents.getTitle());
566 db.bookmark.find({ url: view.webContents.getURL(), isPrivate: (String(windowId).startsWith('private')) }, (err, docs) => {
567 window.webContents.send(`browserview-load-${windowId}`, { id: id, title: view.webContents.getTitle(), url: view.webContents.getURL(), favicon: favicons[0], isBookmarked: (docs.length > 0 ? true : false) });
570 this.updateNavigationState(windowId, id, view);
573 view.webContents.on('did-change-theme-color', (e, color) => {
574 if (view.isDestroyed()) return;
576 window.setTitle(view.webContents.getTitle());
577 db.bookmark.find({ url: view.webContents.getURL(), isPrivate: (String(windowId).startsWith('private')) }, (err, docs) => {
578 window.webContents.send(`browserview-load-${windowId}`, { id: id, title: view.webContents.getTitle(), url: view.webContents.getURL(), color: color, isBookmarked: (docs.length > 0 ? true : false) });
582 view.webContents.on('new-window', (e, url) => {
583 if (view.isDestroyed()) return;
586 this.addView(windowId, url, true);
589 view.webContents.on('context-menu', (e, params) => {
590 if (view.isDestroyed()) return;
592 const menu = Menu.buildFromTemplate(
594 ...(params.linkURL !== '' ?
599 this.addView(windowId, params.linkURL, true);
603 label: '新しいウィンドウで開く',
605 click: () => { view.webContents.openDevTools(); }
608 label: 'プライベート ウィンドウで開く',
610 click: () => { view.webContents.openDevTools(); }
612 { type: 'separator' },
615 accelerator: 'CmdOrCtrl+C',
618 clipboard.writeText(params.linkURL);
621 { type: 'separator' }
623 ...(params.hasImageContents ?
626 label: '新しいタブで画像を開く',
628 this.addView(windowId, params.srcURL, true);
634 const img = nativeImage.createFromDataURL(params.srcURL);
637 clipboard.writeImage(img);
644 clipboard.writeText(params.srcURL);
647 { type: 'separator' }
649 ...(params.isEditable ?
653 accelerator: 'CmdOrCtrl+Z',
654 enabled: params.editFlags.canUndo,
655 click: () => { view.webContents.undo(); }
659 accelerator: 'CmdOrCtrl+Y',
660 enabled: params.editFlags.canRedo,
661 click: () => { view.webContents.redo(); }
663 { type: 'separator' },
666 accelerator: 'CmdOrCtrl+X',
667 enabled: params.editFlags.canCut,
668 click: () => { view.webContents.cut(); }
672 accelerator: 'CmdOrCtrl+C',
673 enabled: params.editFlags.canCopy,
674 click: () => { view.webContents.copy(); }
678 accelerator: 'CmdOrCtrl+V',
679 enabled: params.editFlags.canPaste,
680 click: () => { view.webContents.paste(); }
682 { type: 'separator' },
685 accelerator: 'CmdOrCtrl+A',
686 enabled: params.editFlags.canSelectAll,
687 click: () => { view.webContents.selectAll(); }
689 { type: 'separator' }
691 ...(params.selectionText !== '' && !params.isEditable ?
695 accelerator: 'CmdOrCtrl+C',
696 click: () => { view.webContents.copy(); }
699 label: `Googleで「${params.selectionText}」を検索`,
701 this.addView(windowId, `https://www.google.co.jp/search?q=${params.selectionText}`, true);
704 { type: 'separator' }
708 accelerator: 'Alt+Left',
709 enabled: view.webContents.canGoBack(),
710 click: () => { view.webContents.goBack(); }
714 accelerator: 'Alt+Right',
715 enabled: view.webContents.canGoForward(),
716 click: () => { view.webContents.goForward(); }
720 accelerator: 'CmdOrCtrl+R',
721 click: () => { view.webContents.reload(); }
723 { type: 'separator' },
725 label: 'Floating Window (Beta)',
727 checked: (floatingWindows.indexOf(windowId) != -1),
728 enabled: (!window.isFullScreen() && !window.isMaximized()),
730 if (floatingWindows.indexOf(windowId) != -1) {
731 floatingWindows.filter((win, i) => {
732 if (windowId == win) {
733 floatingWindows.splice(i, 1);
737 floatingWindows.push(windowId);
739 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
742 { type: 'separator' },
745 accelerator: 'CmdOrCtrl+S',
746 enabled: !view.webContents.getURL().startsWith('my://'),
748 view.webContents.savePage(`${app.getPath('downloads')}/${view.webContents.getTitle()}.html`, 'HTMLComplete', (err) => {
749 if (!err) console.log('Page Save successfully');
755 accelerator: 'CmdOrCtrl+P',
756 enabled: !view.webContents.getURL().startsWith('my://'),
757 click: () => { view.webContents.print(); }
759 { type: 'separator' },
762 accelerator: 'CmdOrCtrl+Shift+I',
763 enabled: !view.webContents.getURL().startsWith('my://'),
764 click: () => { if (view.webContents.isDevToolsOpened()) { view.webContents.devToolsWebContents.focus(); } else { view.webContents.openDevTools(); } }
772 view.webContents.on('before-input-event', (e, input) => {
773 if (view.isDestroyed()) return;
775 if ((input.control || input.meta) && input.shift && input.key == 'I') {
777 if (view.webContents.isDevToolsOpened()) {
778 view.webContents.devToolsWebContents.focus();
780 view.webContents.openDevTools();
782 } else if ((input.control || input.meta) && input.key == 'R') {
784 view.webContents.reload();
785 } else if ((input.control || input.meta) && input.shift && input.key == 'R') {
787 view.webContents.reloadIgnoringCache();
791 view.webContents.session.on('will-download', (event, item, webContents) => {
792 item.on('updated', (e, state) => {
793 if (state === 'interrupted') {
794 console.log('Download is interrupted but can be resumed')
795 } else if (state === 'progressing') {
796 if (item.isPaused()) {
797 console.log('Download is paused')
799 console.log(`Received bytes: ${item.getReceivedBytes()}`)
804 item.once('done', (e, state) => {
805 if (state === 'completed') {
806 console.log('Download successfully')
808 console.log(`Download failed: ${state}`)
813 view.webContents.loadURL(url);
815 views.push({ windowId, id, view });
819 window.webContents.send(`browserview-set-${windowId}`, { id: id });
820 window.setBrowserView(view);
823 this.fixBounds(windowId, (floatingWindows.indexOf(windowId) != -1));
824 this.getViews(windowId);