OSDN Git Service

v1.3.4
[serene/MyBrowser.git] / src / Browser.js
1 import React, { Component } from 'react';
2
3 import Window from './Components/Window';
4 import WindowButtons from './Components/WindowButtons';
5 import WindowButton from './Components/WindowButton';
6 import WindowContent from './Components/WindowContent';
7 import TitleBar from './Components/TitleBar';
8 import Tabs from './Components/Tabs';
9 import { TabContainer, Tab, TabIcon, TabTitle, TabCloseButton } from './Components/Tab';
10 import TabButton from './Components/TabButton';
11 import TabContent from './Components/TabContent';
12 import ToolBar from './Components/ToolBar';
13 import ToolBarButton from './Components/ToolBarButton';
14 import ToolBarTextBox from './Components/ToolBarTextBox';
15 import ContentWrapper from './Components/ContentWrapper';
16
17 import WindowMinimizeIcon from './Resources/windows/minimize.svg';
18 import WindowMaximizeIcon from './Resources/windows/maximize.svg';
19 import WindowCloseIcon from './Resources/windows/close.svg';
20
21 import BackIcon from './Resources/arrow_back.svg';
22 import ForwardIcon from './Resources/arrow_forward.svg';
23 import BackInActiveIcon from './Resources/arrow_back_inactive.svg';
24 import ForwardInActiveIcon from './Resources/arrow_forward_inactive.svg';
25 import ReloadIcon from './Resources/reload.svg';
26 import HomeIcon from './Resources/home.svg';
27 import AccountIcon from './Resources/account.svg';
28 import MoreIcon from './Resources/more.svg';
29 import PublicIcon from './Resources/public.svg';
30 import PublicInActiveIcon from './Resources/public_inactive.svg';
31 import ShieldIcon from './Resources/shield.svg';
32 import AddIcon from './Resources/add.svg';
33 import CloseIcon from './Resources/close.svg';
34
35 import isURL from './Utils/isURL';
36
37 const { remote, ipcRenderer, shell } = window.require('electron');
38 const systemPreferences = remote.systemPreferences;
39 const Menu = remote.Menu;
40 const MenuItem = remote.MenuItem;
41 const dialog = remote.dialog;
42
43 const platform = window.require('electron-platform');
44 const process = window.require('process');
45
46 const Config = window.require('electron-store');
47 const config = new Config({
48         defaults: {
49                 design: {
50                         homeButton: false,
51                         darkTheme: false,
52                         theme: 'default'
53                 },
54                 homePage: {
55                         defaultPage: 'my://newtab',
56                         defaultEngine: 'Google',
57                         searchEngines: [
58                                 {
59                                         name: 'Google',
60                                         url: 'https://www.google.com/search?q=%s'
61                                 },
62                                 {
63                                         name: 'Bing',
64                                         url: 'https://www.bing.com/search?q=%s'
65                                 },
66                                 {
67                                         name: 'Yahoo! Japan',
68                                         url: 'https://search.yahoo.co.jp/search?p=%s'
69                                 },
70                                 {
71                                         name: 'goo',
72                                         url: 'https://search.goo.ne.jp/web.jsp?MT=%s'
73                                 },
74                                 {
75                                         name: 'Google Translate',
76                                         url: 'https://translate.google.com/?text=%s'
77                                 },
78                                 {
79                                         name: 'Youtube',
80                                         url: 'https://www.youtube.com/results?search_query=%s'
81                                 },
82                                 {
83                                         name: 'Twitter',
84                                         url: 'https://www.twitter.com/search?q=%s'
85                                 },
86                                 {
87                                         name: 'GitHub',
88                                         url: 'https://github.com/search?q=%s'
89                                 }
90                         ]
91                 },
92                 adBlocker: true,
93                 window: {
94                         isCustomTitlebar: true,
95                         isMaximized: false,
96                         bounds: {
97                                 width: 1100,
98                                 height: 680
99                         }
100                 }
101         },
102 });
103
104 class BrowserView extends Component {
105         constructor(props) {
106                 super(props);
107                 this.state = {
108                         barText: '',
109                         findText: '',
110                         previousText: '',
111                         resultString: '',
112                         activeMatch: 0,
113                         isLoading: false,
114                         canGoBack: false,
115                         canGoForward: false,
116                 };
117         }
118
119         componentDidMount() {
120                 /*
121                 let webView = this.refs.webView;
122                 let props = this.props;
123
124                 webView.addEventListener('did-finish-load', (e) => {
125                         document.title = webView.getTitle();
126                         this.setText(webView.getURL());
127                         this.props.updateTab(webView.getTitle(), PublicIcon, webView.getURL(), this.props.index);
128                         this.setState({ barText: webView.getURL() });
129                 }, false);
130
131                 webView.addEventListener('page-title-updated', (e) => {
132                         document.title = webView.getTitle();
133                         this.setText(webView.getURL());
134                         this.props.updateTab(webView.getTitle(), PublicIcon, webView.getURL(), this.props.index);
135                         this.setState({ barText: webView.getURL() });
136                 }, false);
137
138                 webView.addEventListener('page-favicon-updated', (e) => {
139                         if (e.favicons.length > 0) {
140                                 this.props.updateTab(webView.getTitle(), e.favicons[0], webView.getURL(), this.props.index);
141                         } else {
142                                 this.props.updateTab(webView.getTitle(), PublicIcon, webView.getURL(), this.props.index);
143                         }
144                 });
145
146                 webView.addEventListener('load-commit', (e) => {
147                         if (e.isMainFrame) {
148                                 document.title = webView.getTitle();
149                                 this.setText(webView.getURL());
150                                 this.props.updateTab(webView.getTitle(), PublicIcon, webView.getURL(), this.props.index);
151                                 this.setState({ barText: webView.getURL() });
152                         }
153                 });
154
155                 webView.addEventListener('found-in-page', (e, result) => {
156                         if (result.activeMatchOrdinal) {
157                                 this.setState({
158                                         activeMatch: result.activeMatchOrdinal,
159                                         resultString: `${this.state.activeMatch}/${result.matches}`
160                                 });
161                         }
162
163                         if (result.finalUpdate) {
164                                 this.setState({ resultString: `${this.state.activeMatch}/${result.matches}` });
165                         }
166                 });
167
168                 webView.addEventListener('new-window', (e, url) => {
169                         this.props.addTab('新しいタブ', '', e.url);
170                 });
171                 */
172
173                 ipcRenderer.on(`browserview-start-loading-${this.props.windowId}`, (e, args) => {
174                         if (args.id == this.props.index) {
175                                 this.setState({ isLoading: true });
176                         }
177                 });
178
179                 ipcRenderer.on(`browserview-stop-loading-${this.props.windowId}`, (e, args) => {
180                         if (args.id == this.props.index) {
181                                 this.setState({ isLoading: false });
182                         }
183                 });
184
185                 ipcRenderer.on(`browserview-load-${this.props.windowId}`, (e, args) => {
186                         if (args.id == this.props.index) {
187                                 this.props.updateTab();
188                                 this.setText(args.url);
189                         }
190                 });
191
192                 ipcRenderer.on(`update-navigation-state-${this.props.windowId}`, (e, args) => {
193                         if (args.id == this.props.index) {
194                                 this.setState({ canGoBack: args.canGoBack, canGoForward: args.canGoForward });
195                         }
196                 });
197
198                 // console.log(webView);
199         }
200
201         handleKeyDown = (e) => {
202                 if (e.key != 'Enter' || this.state.barText.length == 0 || this.state.barText == '') return;
203
204                 if (isURL(this.state.barText)) {
205                         const pattern = /^(file:\/\/\S.*)\S*$/;
206
207                         if (pattern.test(this.state.barText)) {
208                                 ipcRenderer.send(`browserview-loadFile-${this.props.windowId}`, { id: this.props.index, url: this.state.barText.replace('file://', '') });
209                         } else {
210                                 ipcRenderer.send(`browserview-loadURL-${this.props.windowId}`, { id: this.props.index, url: this.state.barText });
211                         }
212                 } else {
213                         ipcRenderer.send(`browserview-loadURL-${this.props.windowId}`, { id: this.props.index, url: this.getSearchEngine().url.replace('%s', this.state.barText) });
214                 }
215         }
216
217         handleContextMenu = (e) => {
218                 const menu = Menu.buildFromTemplate([
219                         {
220                                 label: '元に戻す',
221                                 accelerator: 'CmdOrCtrl+Z',
222                                 role: 'undo'
223                         },
224                         {
225                                 label: 'やり直す',
226                                 accelerator: 'CmdOrCtrl+Y',
227                                 role: 'redo'
228                         },
229                         { type: 'separator' },
230                         {
231                                 label: '切り取り',
232                                 accelerator: 'CmdOrCtrl+X',
233                                 role: 'cut'
234                         },
235                         {
236                                 label: 'コピー',
237                                 accelerator: 'CmdOrCtrl+C',
238                                 role: 'copy'
239                         },
240                         {
241                                 label: '貼り付け',
242                                 accelerator: 'CmdOrCtrl+V',
243                                 role: 'paste'
244                         },
245                         { type: 'separator' },
246                         {
247                                 label: 'すべて選択',
248                                 accelerator: 'CmdOrCtrl+A',
249                                 role: 'selectAll'
250                         }
251                 ]);
252                 menu.popup();
253         }
254
255         getSearchEngine = () => {
256                 for (var i = 0; i < config.get('homePage.searchEngines').length; i++) {
257                         if (config.get('homePage.searchEngines')[i].name == config.get('homePage.defaultEngine')) {
258                                 return config.get('homePage.searchEngines')[i];
259                         }
260                 }
261         }
262
263         setText = (text) => {
264                 this.setState({ barText: text });
265         }
266
267         goBack = () => {
268                 ipcRenderer.send(`browserview-goBack-${this.props.windowId}`, { id: this.props.index });
269         }
270
271         goForward = () => {
272                 ipcRenderer.send(`browserview-goForward-${this.props.windowId}`, { id: this.props.index });
273         }
274
275         reload = () => {
276                 if (!this.state.isLoading) {
277                         ipcRenderer.send(`browserview-reload-${this.props.windowId}`, { id: this.props.index });
278                 } else {
279                         ipcRenderer.send(`browserview-stop-${this.props.windowId}`, { id: this.props.index });
280                 }
281         }
282
283         goHome = () => {
284                 ipcRenderer.send(`browserview-goHome-${this.props.windowId}`, { id: this.props.index });
285         }
286
287         userMenu = async () => {
288                 const menu = Menu.buildFromTemplate([
289                         {
290                                 label: (!this.props.windowId.startsWith('private') ? `${process.env.USERNAME} でログイン中` : 'プライベートモード'),
291                                 enabled: false,
292                         },
293                         { type: 'separator' },
294                         {
295                                 label: '閉じる',
296                                 click: () => { remote.getCurrentWindow().close(); }
297                         }
298                 ]);
299                 menu.popup({
300                         x: remote.getCurrentWindow().getSize()[0] - 57,
301                         y: 65
302                 });
303         }
304
305         moreMenu = () => {
306                 const menu = Menu.buildFromTemplate([
307                         {
308                                 label: '新しいタブ',
309                                 accelerator: 'CmdOrCtrl+T',
310                                 click: () => { this.props.addTab('https://www.google.co.jp/'); }
311                         },
312                         {
313                                 label: '新しいウィンドウ',
314                                 accelerator: 'CmdOrCtrl+N',
315                                 click: () => { ipcRenderer.send(`window-add`, { isPrivate: false }); }
316                         },
317                         {
318                                 label: 'シークレット ウィンドウを開く',
319                                 accelerator: 'CmdOrCtrl+Shift+N',
320                                 click: () => { ipcRenderer.send(`window-add`, { isPrivate: true }); }
321                         },
322                         { type: 'separator' },
323                         {
324                                 label: '履歴',
325                                 click: () => { this.props.addTab('my://history'); }
326                         },
327                         {
328                                 label: 'ダウンロード',
329                                 click: () => { this.props.addTab('my://history'); }
330                         },
331                         {
332                                 label: 'ブックマーク',
333                                 click: () => { this.props.addTab('my://history'); }
334                         },
335                         { type: 'separator' },
336                         {
337                                 label: '設定',
338                                 click: () => { this.props.addTab('my://settings'); }
339                         },
340                         {
341                                 label: 'ヘルプ',
342                                 click: () => { this.props.addTab('my://help'); }
343                         },
344                         { type: 'separator' },
345                         {
346                                 label: '閉じる',
347                                 click: () => { remote.getCurrentWindow().close(); }
348                         }
349                 ]);
350                 menu.popup({
351                         x: remote.getCurrentWindow().getSize()[0] - 24,
352                         y: 65
353                 });
354         }
355
356         render() {
357                 return (
358                         <ContentWrapper>
359                                 <ToolBar>
360                                         <ToolBarButton isShowing={true} isRight={false} isMarginLeft={true} isEnabled={this.state.canGoBack} src={this.state.canGoBack ? BackIcon : BackInActiveIcon} size={24} title="前のページに戻る" onClick={() => { this.goBack(); }} />
361                                         <ToolBarButton isShowing={true} isRight={false} isMarginLeft={false} isEnabled={this.state.canGoForward} src={this.state.canGoForward ? ForwardIcon : ForwardInActiveIcon} size={24} title="次のページに進む" onClick={() => { this.goForward(); }} />
362                                         <ToolBarButton isShowing={true} isRight={false} isMarginLeft={false} isEnabled={true} src={!this.state.isLoading ? ReloadIcon : CloseIcon} size={24} title={!this.state.isLoading ? '再読み込み' : '読み込み中止'} onClick={() => { this.reload(); }} />
363                                         <ToolBarButton isShowing={config.get('design.homeButton')} isRight={false} isMarginLeft={false} isEnabled={true} src={HomeIcon} size={24} title="ホームページに移動" onClick={() => { this.goHome(); }} />
364                                         <ToolBarTextBox buttonCount={config.get('design.homeButton') ? 5 : 4} value={this.state.barText} onChange={(e) => { this.setState({ barText: e.target.value }); }} onKeyDown={this.handleKeyDown} onContextMenu={this.handleContextMenu} />
365                                         <ToolBarButton isShowing={true} isRight={true} isMarginLeft={true} isEnabled={true} src={!this.props.windowId.startsWith('private') ? AccountIcon : ShieldIcon} size={24} title={!this.props.windowId.startsWith('private') ? process.env.USERNAME : 'プライベートモード'} onClick={() => { this.userMenu(); }} />
366                                         <ToolBarButton isShowing={true} isRight={true} isMarginLeft={false} isEnabled={true} src={MoreIcon} size={24} title="メニュー" onClick={() => { this.moreMenu(); }} />
367                                 </ToolBar>
368                         </ContentWrapper>
369                 );
370         }
371 }
372
373 class App extends Component {
374         constructor(props) {
375                 super(props);
376
377                 this.state = {
378                         tabs: [],
379                         current: 0
380                 };
381
382                 ipcRenderer.on(`browserview-get-${this.props.match.params.windowId}`, (e, args) => {
383                         this.setState({ tabs: args.views });
384                 });
385
386                 ipcRenderer.on(`browserview-set-${this.props.match.params.windowId}`, (e, args) => {
387                         this.setState({ current: args.id });
388                 });
389         }
390
391         componentDidMount = () => {
392                 this.addTab();
393         }
394
395         handleContextMenu = (i) => {
396                 let t = this;
397                 const menu = Menu.buildFromTemplate([
398                         {
399                                 label: '新しいタブ',
400                                 accelerator: 'CmdOrCtrl+T',
401                                 click: function () { t.addTab('https://www.google.co.jp/'); }
402                         },
403                         {
404                                 label: 'タブを閉じる',
405                                 accelerator: 'CmdOrCtrl+W',
406                                 click: function () { t.removeTab(i); }
407                         },
408                         { type: 'separator' },
409                         {
410                                 type: 'checkbox',
411                                 label: 'タブを固定',
412                                 checked: t.state.tabs[i].fixed,
413                                 click: function () {
414                                         var newTabs = t.state.tabs.concat();
415                                         newTabs[i].fixed = !t.state.tabs[i].fixed;
416                                         t.setState({ tabs: newTabs });
417                                 }
418                         }
419                 ]);
420                 menu.popup(remote.getCurrentWindow());
421         }
422
423         getTabs = () => {
424                 ipcRenderer.send(`browserview-get-${this.props.match.params.windowId}`, {});
425         }
426
427         addTab = (url = config.get('homePage.defaultPage')) => {
428                 ipcRenderer.send(`browserview-add-${this.props.match.params.windowId}`, { url, isActive: true });
429                 ipcRenderer.send(`browserview-get-${this.props.match.params.windowId}`, {});
430                 this.setState({ current: this.state.tabs.length });
431         }
432
433         removeTab = (i) => {
434                 ipcRenderer.send(`browserview-remove-${this.props.match.params.windowId}`, { id: i });
435                 ipcRenderer.send(`browserview-get-${this.props.match.params.windowId}`, {});
436
437                 if ((this.state.tabs.length - 1) > 0) {
438                         const id = this.state.current--;
439                         ipcRenderer.send(`browserview-select-${this.props.match.params.windowId}`, { id: id });
440                 } else {
441                         remote.getCurrentWindow().close();
442                 }
443         }
444
445         updateTab = () => {
446                 ipcRenderer.send(`browserview-get-${this.props.match.params.windowId}`, {});
447         }
448
449         getForegroundColor = (hexColor) => {
450                 var r = parseInt(hexColor.substr(1, 2), 16);
451                 var g = parseInt(hexColor.substr(3, 2), 16);
452                 var b = parseInt(hexColor.substr(5, 2), 16);
453
454                 return ((((r * 299) + (g * 587) + (b * 114)) / 1000) < 128) ? '#ffffff' : '#000000';
455         }
456
457         render() {
458                 return (
459                         <Window isCustomTitlebar={config.get('window.isCustomTitlebar')}>
460                                 <TitleBar color={platform.isWin32 || platform.isDarwin ? `#${systemPreferences.getAccentColor()}` : '#353535'}>
461                                         <Tabs isCustomTitlebar={config.get('window.isCustomTitlebar')}>
462                                                 <TabContainer>
463                                                         {this.state.tabs.map((tab, i) => {
464                                                                 return (
465                                                                         <Tab isActive={tab.id == this.state.current} isFixed={tab.fixed} onClick={() => { ipcRenderer.send(`browserview-select-${this.props.match.params.windowId}`, { id: tab.id }); }} onContextMenu={this.handleContextMenu.bind(this, tab.id)}>
466                                                                                 <TabIcon src={tab.icon != undefined ? tab.icon : PublicIcon} width={18} height={18} />
467                                                                                 <TabTitle isFixed={tab.fixed} title={tab.title}>{tab.title}</TabTitle>
468                                                                                 <TabCloseButton isActive={tab.id == this.state.current} isFixed={tab.fixed} isRight={true} src={CloseIcon} size={14} title="このタブを閉じる" onClick={() => { this.removeTab(tab.id); }} />
469                                                                         </Tab>
470                                                                 );
471                                                         })}
472                                                 </TabContainer>
473                                                 <TabButton isRight={true} src={AddIcon} size={24} title="新しいタブを開く" onClick={() => { this.addTab(); }} />
474                                         </Tabs>
475                                         <WindowButtons isCustomTitlebar={config.get('window.isCustomTitlebar')}>
476                                                 <WindowButton isClose={false} title="最小化" onClick={() => { remote.getCurrentWindow().minimize(); }}>
477                                                         <svg name="TitleBarMinimize" width="12" height="12" viewBox="0 0 12 12">
478                                                                 <rect fill={this.getForegroundColor(platform.isWin32 || platform.isDarwin ? `#${systemPreferences.getAccentColor()}` : '#353535')} width="10" height="1" x="1" y="6" />
479                                                         </svg>
480                                                 </WindowButton>
481                                                 <WindowButton isClose={false} title={remote.getCurrentWindow().isMaximized() ? '元に戻す (縮小)' : '最大化'} onClick={() => { remote.getCurrentWindow().isMaximized() ? remote.getCurrentWindow().unmaximize() : remote.getCurrentWindow().maximize(); }}>
482                                                         <svg name="TitleBarMaximize" width="12" height="12" viewBox="0 0 12 12">
483                                                                 <rect stroke={this.getForegroundColor(platform.isWin32 || platform.isDarwin ? `#${systemPreferences.getAccentColor()}` : '#353535')} width="9" height="9" x="1.5" y="1.5" fill="none" />
484                                                         </svg>
485                                                 </WindowButton>
486                                                 <WindowButton isClose={true} title="閉じる" onClick={() => { remote.getCurrentWindow().close(); }}>
487                                                         <svg name="TitleBarClose" width="12" height="12" viewBox="0 0 12 12">
488                                                                 <polygon fill={this.getForegroundColor(platform.isWin32 || platform.isDarwin ? `#${systemPreferences.getAccentColor()}` : '#353535')} fill-rule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1" />
489                                                         </svg>
490                                                 </WindowButton>
491                                         </WindowButtons>
492                                 </TitleBar>
493                                 <WindowContent>
494                                         {this.state.tabs.map((tab, i) => {
495                                                 return (
496                                                         <TabContent isActive={tab.id == this.state.current}>
497                                                                 <BrowserView key={i} windowId={this.props.match.params.windowId} index={tab.id} url={tab.url}
498                                                                         addTab={(url) => { this.addTab(url); }}
499                                                                         updateTab={() => { this.updateTab(); }} />
500                                                         </TabContent>
501                                                 );
502                                         })}
503                                 </WindowContent>
504                         </Window>
505                 );
506         }
507 }
508
509 export default App;