aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2018-03-04 21:10:29 +0100
committerLibravatar GitHub <noreply@github.com>2018-03-04 21:10:29 +0100
commit9af5fd05910615b8101ef84bd1b43d7fe78dedc4 (patch)
treec768a9682a71b658b4675fe6608b1cc45c5e4e90 /src
parentfix(App): Fix app getting stuck on load when recipe has an invalid version id... (diff)
parentFinalize titlebar styling (diff)
downloadferdium-app-9af5fd05910615b8101ef84bd1b43d7fe78dedc4.tar.gz
ferdium-app-9af5fd05910615b8101ef84bd1b43d7fe78dedc4.tar.zst
ferdium-app-9af5fd05910615b8101ef84bd1b43d7fe78dedc4.zip
feat(Windows): Replace window frame with custom menu bar
Title bar for Windows & Linux
Diffstat (limited to 'src')
-rw-r--r--src/I18n.js9
-rw-r--r--src/components/layout/AppLayout.js122
-rw-r--r--src/environment.js12
-rw-r--r--src/helpers/validation-helpers.js35
-rw-r--r--src/i18n/locales/en-US.json49
-rw-r--r--src/index.html2
-rw-r--r--src/index.js6
-rw-r--r--src/lib/Menu.js600
-rw-r--r--src/styles/layout.scss10
-rw-r--r--src/styles/main.scss2
-rw-r--r--src/styles/title-bar.scss50
11 files changed, 735 insertions, 162 deletions
diff --git a/src/I18n.js b/src/I18n.js
index ae3ba2fa9..4ee34157c 100644
--- a/src/I18n.js
+++ b/src/I18n.js
@@ -9,11 +9,18 @@ import UserStore from './stores/UserStore';
9 9
10@inject('stores') @observer 10@inject('stores') @observer
11export default class I18N extends Component { 11export default class I18N extends Component {
12 componentDidUpdate() {
13 window.franz.menu.rebuild();
14 }
15
12 render() { 16 render() {
13 const { stores, children } = this.props; 17 const { stores, children } = this.props;
14 const { locale } = stores.app; 18 const { locale } = stores.app;
15 return ( 19 return (
16 <IntlProvider {...{ locale, key: locale, messages: translations[locale] }}> 20 <IntlProvider
21 {...{ locale, key: locale, messages: translations[locale] }}
22 ref={(intlProvider) => { window.franz.intl = intlProvider ? intlProvider.getChildContext().intl : null; }}
23 >
17 {children} 24 {children}
18 </IntlProvider> 25 </IntlProvider>
19 ); 26 );
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 20dc2f764..686476317 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -2,10 +2,13 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { TitleBar } from 'electron-react-titlebar';
5 6
6import InfoBar from '../ui/InfoBar'; 7import InfoBar from '../ui/InfoBar';
7import globalMessages from '../../i18n/globalMessages'; 8import globalMessages from '../../i18n/globalMessages';
8 9
10import { isMac } from '../../environment';
11
9function createMarkup(HTMLString) { 12function createMarkup(HTMLString) {
10 return { __html: HTMLString }; 13 return { __html: HTMLString };
11} 14}
@@ -87,64 +90,67 @@ export default class AppLayout extends Component {
87 return ( 90 return (
88 <div> 91 <div>
89 <div className="app"> 92 <div className="app">
90 {sidebar} 93 {!isMac && <TitleBar menu={window.franz.menu.template} icon={'assets/images/logo.svg'} />}
91 <div className="app__service"> 94 <div className="app__content">
92 {news.length > 0 && news.map(item => ( 95 {sidebar}
93 <InfoBar 96 <div className="app__service">
94 key={item.id} 97 {news.length > 0 && news.map(item => (
95 position="top" 98 <InfoBar
96 type={item.type} 99 key={item.id}
97 sticky={item.sticky} 100 position="top"
98 onHide={() => removeNewsItem({ newsId: item.id })} 101 type={item.type}
99 > 102 sticky={item.sticky}
100 <span dangerouslySetInnerHTML={createMarkup(item.message)} /> 103 onHide={() => removeNewsItem({ newsId: item.id })}
101 </InfoBar> 104 >
102 ))} 105 <span dangerouslySetInnerHTML={createMarkup(item.message)} />
103 {!isOnline && ( 106 </InfoBar>
104 <InfoBar 107 ))}
105 type="danger" 108 {!isOnline && (
106 > 109 <InfoBar
107 <span className="mdi mdi-flash" /> 110 type="danger"
108 {intl.formatMessage(globalMessages.notConnectedToTheInternet)} 111 >
109 </InfoBar> 112 <span className="mdi mdi-flash" />
110 )} 113 {intl.formatMessage(globalMessages.notConnectedToTheInternet)}
111 {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( 114 </InfoBar>
112 <InfoBar 115 )}
113 type="danger" 116 {!areRequiredRequestsSuccessful && showRequiredRequestsError && (
114 ctaLabel="Try again" 117 <InfoBar
115 ctaLoading={areRequiredRequestsLoading} 118 type="danger"
116 sticky 119 ctaLabel="Try again"
117 onClick={retryRequiredRequests} 120 ctaLoading={areRequiredRequestsLoading}
118 > 121 sticky
119 <span className="mdi mdi-flash" /> 122 onClick={retryRequiredRequests}
120 {intl.formatMessage(messages.requiredRequestsFailed)} 123 >
121 </InfoBar> 124 <span className="mdi mdi-flash" />
122 )} 125 {intl.formatMessage(messages.requiredRequestsFailed)}
123 {showServicesUpdatedInfoBar && ( 126 </InfoBar>
124 <InfoBar 127 )}
125 type="primary" 128 {showServicesUpdatedInfoBar && (
126 ctaLabel={intl.formatMessage(messages.buttonReloadServices)} 129 <InfoBar
127 onClick={reloadServicesAfterUpdate} 130 type="primary"
128 sticky 131 ctaLabel={intl.formatMessage(messages.buttonReloadServices)}
129 > 132 onClick={reloadServicesAfterUpdate}
130 <span className="mdi mdi-power-plug" /> 133 sticky
131 {intl.formatMessage(messages.servicesUpdated)} 134 >
132 </InfoBar> 135 <span className="mdi mdi-power-plug" />
133 )} 136 {intl.formatMessage(messages.servicesUpdated)}
134 {appUpdateIsDownloaded && ( 137 </InfoBar>
135 <InfoBar 138 )}
136 type="primary" 139 {appUpdateIsDownloaded && (
137 ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)} 140 <InfoBar
138 onClick={installAppUpdate} 141 type="primary"
139 sticky 142 ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)}
140 > 143 onClick={installAppUpdate}
141 <span className="mdi mdi-information" /> 144 sticky
142 {intl.formatMessage(messages.updateAvailable)} <a href="https://meetfranz.com/changelog" target="_blank"> 145 >
143 <u>{intl.formatMessage(messages.changelog)}</u> 146 <span className="mdi mdi-information" />
144 </a> 147 {intl.formatMessage(messages.updateAvailable)} <a href="https://meetfranz.com/changelog" target="_blank">
145 </InfoBar> 148 <u>{intl.formatMessage(messages.changelog)}</u>
146 )} 149 </a>
147 {services} 150 </InfoBar>
151 )}
152 {services}
153 </div>
148 </div> 154 </div>
149 </div> 155 </div>
150 {children} 156 {children}
diff --git a/src/environment.js b/src/environment.js
index e185120c0..e1762129b 100644
--- a/src/environment.js
+++ b/src/environment.js
@@ -4,11 +4,17 @@ export const isDevMode = Boolean(process.execPath.match(/[\\/]electron/));
4export const useLiveAPI = process.env.LIVE_API; 4export const useLiveAPI = process.env.LIVE_API;
5export const useLocalAPI = process.env.LOCAL_API; 5export const useLocalAPI = process.env.LOCAL_API;
6 6
7export const isMac = process.platform === 'darwin'; 7let platform = process.platform;
8export const isWindows = process.platform === 'win32'; 8if (process.env.OS_PLATFORM) {
9export const isLinux = process.platform === 'linux'; 9 platform = process.env.OS_PLATFORM;
10}
11
12export const isMac = platform === 'darwin';
13export const isWindows = platform === 'win32';
14export const isLinux = platform === 'linux';
10 15
11export const ctrlKey = isMac ? '⌘' : 'Ctrl'; 16export const ctrlKey = isMac ? '⌘' : 'Ctrl';
17export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
12 18
13let api; 19let api;
14if (!isDevMode || (isDevMode && useLiveAPI)) { 20if (!isDevMode || (isDevMode && useLiveAPI)) {
diff --git a/src/helpers/validation-helpers.js b/src/helpers/validation-helpers.js
index a8a242d54..2f762437d 100644
--- a/src/helpers/validation-helpers.js
+++ b/src/helpers/validation-helpers.js
@@ -1,6 +1,31 @@
1import { defineMessages } from 'react-intl';
2
3const messages = defineMessages({
4 required: {
5 id: 'validation.required',
6 defaultMessage: '!!!Field is required',
7 },
8 email: {
9 id: 'validation.email',
10 defaultMessage: '!!!Email not valid',
11 },
12 url: {
13 id: 'validation.url',
14 defaultMessage: '!!!Not a valid URL',
15 },
16 minLength: {
17 id: 'validation.minLength',
18 defaultMessage: '!!!Too few characters',
19 },
20 oneRequired: {
21 id: 'validation.oneRequired',
22 defaultMessage: '!!!At least one is required',
23 },
24});
25
1export function required({ field }) { 26export function required({ field }) {
2 const isValid = (field.value.trim() !== ''); 27 const isValid = (field.value.trim() !== '');
3 return [isValid, `${field.label} is required`]; 28 return [isValid, window.franz.intl.formatMessage(messages.required, { field: field.label })];
4} 29}
5 30
6export function email({ field }) { 31export function email({ field }) {
@@ -13,7 +38,7 @@ export function email({ field }) {
13 isValid = true; 38 isValid = true;
14 } 39 }
15 40
16 return [isValid, `${field.label} not valid`]; 41 return [isValid, window.franz.intl.formatMessage(messages.email, { field: field.label })];
17} 42}
18 43
19export function url({ field }) { 44export function url({ field }) {
@@ -27,7 +52,7 @@ export function url({ field }) {
27 isValid = true; 52 isValid = true;
28 } 53 }
29 54
30 return [isValid, `${field.label} is not a valid url`]; 55 return [isValid, window.franz.intl.formatMessage(messages.url, { field: field.label })];
31} 56}
32 57
33export function minLength(length) { 58export function minLength(length) {
@@ -36,13 +61,13 @@ export function minLength(length) {
36 if (field.touched) { 61 if (field.touched) {
37 isValid = field.value.length >= length; 62 isValid = field.value.length >= length;
38 } 63 }
39 return [isValid, `${field.label} should be at least ${length} characters long.`]; 64 return [isValid, window.franz.intl.formatMessage(messages.minLength, { field: field.label, length })];
40 }; 65 };
41} 66}
42 67
43export function oneRequired(targets) { 68export function oneRequired(targets) {
44 return ({ field, form }) => { 69 return ({ field, form }) => {
45 const invalidFields = targets.filter(target => form.$(target).value === ''); 70 const invalidFields = targets.filter(target => form.$(target).value === '');
46 return [targets.length !== invalidFields.length, `${field.label} is required`]; 71 return [targets.length !== invalidFields.length, window.franz.intl.formatMessage(messages.required, { field: field.label })];
47 }; 72 };
48} 73}
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index d5c0ea441..400a9a5d8 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -199,5 +199,52 @@
199 "service.crashHandler.action": "Reload {name}", 199 "service.crashHandler.action": "Reload {name}",
200 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds", 200 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds",
201 "service.disabledHandler.headline": "{name} is disabled", 201 "service.disabledHandler.headline": "{name} is disabled",
202 "service.disabledHandler.action": "Enable {name}" 202 "service.disabledHandler.action": "Enable {name}",
203 "menu.edit": "Edit",
204 "menu.edit.undo": "Undo",
205 "menu.edit.redo": "Redo",
206 "menu.edit.cut": "Cut",
207 "menu.edit.copy": "Copy",
208 "menu.edit.paste": "Paste",
209 "menu.edit.pasteAndMatchStyle": "Paste And Match Style",
210 "menu.edit.delete": "Delete",
211 "menu.edit.selectAll": "Select All",
212 "menu.edit.speech": "Speech",
213 "menu.edit.startSpeaking": "Start Speaking",
214 "menu.edit.stopSpeaking": "Stop Speaking",
215 "menu.edit.startDictation": "Start Dictation",
216 "menu.edit.emojiSymbols": "Emoji & Symbols",
217 "menu.view.resetZoom": "Actual Size",
218 "menu.view.zoomIn": "Zoom In",
219 "menu.view.zoomOut": "Zoom Out",
220 "menu.view.enterFullScreen": "Enter Full Screen",
221 "menu.view.exitFullScreen": "Exit Full Screen",
222 "menu.view.toggleFullScreen": "Toggle Full Screen",
223 "menu.view.toggleDevTools": "Toggle Developer Tools",
224 "menu.view.toggleServiceDevTools": "Toggle Service Developer Tools",
225 "menu.view.reloadService": "Reload Service",
226 "menu.view.reloadFranz": "Reload Franz",
227 "menu.window.minimize": "Minimize",
228 "menu.window.close": "Close",
229 "menu.help.learnMore": "Learn More",
230 "menu.help.changelog": "Changelog",
231 "menu.help.support": "Support",
232 "menu.help.tos": "Terms of Service",
233 "menu.help.privacy": "Privacy Statement",
234 "menu.file": "File",
235 "menu.view": "View",
236 "menu.services": "Services",
237 "menu.window": "Window",
238 "menu.help": "Help",
239 "menu.app.about": "About Franz",
240 "menu.app.settings": "Settings",
241 "menu.app.hide": "Hide",
242 "menu.app.hideOthers": "Hide Others",
243 "menu.app.unhide": "Unhide",
244 "menu.app.quit": "Quit",
245 "menu.services.addNewService": "Add New Service...",
246 "validation.required": "{field} is required",
247 "validation.email": "{field} is not valid",
248 "validation.url": "{field} is not a valid URL",
249 "validation.minLength": "{field} should be at least {length} characters long"
203} 250}
diff --git a/src/index.html b/src/index.html
index 9e5acd705..6c259e5be 100644
--- a/src/index.html
+++ b/src/index.html
@@ -11,7 +11,7 @@
11 <div class="dev-warning">DEV MODE</div> 11 <div class="dev-warning">DEV MODE</div>
12 <div id="root"></div> 12 <div id="root"></div>
13 <script> 13 <script>
14 document.querySelector('body').classList.add(process.platform); 14 document.querySelector('body').classList.add(process.env.OS_PLATFORM ? process.env.OS_PLATFORM : process.platform);
15 15
16 const { isDevMode } = require('./environment'); 16 const { isDevMode } = require('./environment');
17 if (isDevMode) { 17 if (isDevMode) {
diff --git a/src/index.js b/src/index.js
index f82bb3590..bb09bf42c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,7 +4,7 @@ import path from 'path';
4 4
5import windowStateKeeper from 'electron-window-state'; 5import windowStateKeeper from 'electron-window-state';
6 6
7import { isDevMode, isWindows } from './environment'; 7import { isDevMode, isMac, isWindows } from './environment';
8import ipcApi from './electron/ipc-api'; 8import ipcApi from './electron/ipc-api';
9import Tray from './lib/Tray'; 9import Tray from './lib/Tray';
10import Settings from './electron/Settings'; 10import Settings from './electron/Settings';
@@ -72,9 +72,9 @@ const createWindow = () => {
72 height: mainWindowState.height, 72 height: mainWindowState.height,
73 minWidth: 600, 73 minWidth: 600,
74 minHeight: 500, 74 minHeight: 500,
75 titleBarStyle: 'hidden', 75 titleBarStyle: isMac ? 'hidden' : '',
76 frame: false,
76 backgroundColor: '#3498db', 77 backgroundColor: '#3498db',
77 autoHideMenuBar: true,
78 }); 78 });
79 79
80 // Initialize System Tray 80 // Initialize System Tray
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 16e91374e..5a05e47b3 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -1,113 +1,477 @@
1import { remote, shell } from 'electron'; 1import { remote, shell } from 'electron';
2import { autorun, computed, observable, toJS } from 'mobx'; 2import { observable, autorun, computed } from 'mobx';
3import { defineMessages } from 'react-intl';
3 4
4import { isMac } from '../environment'; 5import { isMac, ctrlKey, cmdKey } from '../environment';
5 6
6const { app, Menu, dialog } = remote; 7const { app, Menu, dialog } = remote;
7 8
8const template = [ 9const menuItems = defineMessages({
10 edit: {
11 id: 'menu.edit',
12 defaultMessage: '!!!Edit',
13 },
14 undo: {
15 id: 'menu.edit.undo',
16 defaultMessage: '!!!Undo',
17 },
18 redo: {
19 id: 'menu.edit.redo',
20 defaultMessage: '!!!Redo',
21 },
22 cut: {
23 id: 'menu.edit.cut',
24 defaultMessage: '!!!Cut',
25 },
26 copy: {
27 id: 'menu.edit.copy',
28 defaultMessage: '!!!Copy',
29 },
30 paste: {
31 id: 'menu.edit.paste',
32 defaultMessage: '!!!Paste',
33 },
34 pasteAndMatchStyle: {
35 id: 'menu.edit.pasteAndMatchStyle',
36 defaultMessage: '!!!Paste And Match Style',
37 },
38 delete: {
39 id: 'menu.edit.delete',
40 defaultMessage: '!!!Delete',
41 },
42 selectAll: {
43 id: 'menu.edit.selectAll',
44 defaultMessage: '!!!Select All',
45 },
46 speech: {
47 id: 'menu.edit.speech',
48 defaultMessage: '!!!Speech',
49 },
50 startSpeaking: {
51 id: 'menu.edit.startSpeaking',
52 defaultMessage: '!!!Start Speaking',
53 },
54 stopSpeaking: {
55 id: 'menu.edit.stopSpeaking',
56 defaultMessage: '!!!Stop Speaking',
57 },
58 startDictation: {
59 id: 'menu.edit.startDictation',
60 defaultMessage: '!!!Start Dictation',
61 },
62 emojiSymbols: {
63 id: 'menu.edit.emojiSymbols',
64 defaultMessage: '!!!Emoji & Symbols',
65 },
66 resetZoom: {
67 id: 'menu.view.resetZoom',
68 defaultMessage: '!!!Actual Size',
69 },
70 zoomIn: {
71 id: 'menu.view.zoomIn',
72 defaultMessage: '!!!Zoom In',
73 },
74 zoomOut: {
75 id: 'menu.view.zoomOut',
76 defaultMessage: '!!!Zoom Out',
77 },
78 enterFullScreen: {
79 id: 'menu.view.enterFullScreen',
80 defaultMessage: '!!!Enter Full Screen',
81 },
82 exitFullScreen: {
83 id: 'menu.view.exitFullScreen',
84 defaultMessage: '!!!Exit Full Screen',
85 },
86 toggleFullScreen: {
87 id: 'menu.view.toggleFullScreen',
88 defaultMessage: '!!!Toggle Full Screen',
89 },
90 toggleDevTools: {
91 id: 'menu.view.toggleDevTools',
92 defaultMessage: '!!!Toggle Developer Tools',
93 },
94 toggleServiceDevTools: {
95 id: 'menu.view.toggleServiceDevTools',
96 defaultMessage: '!!!Toggle Service Developer Tools',
97 },
98 reloadService: {
99 id: 'menu.view.reloadService',
100 defaultMessage: '!!!Reload Service',
101 },
102 reloadFranz: {
103 id: 'menu.view.reloadFranz',
104 defaultMessage: '!!!Reload Franz',
105 },
106 minimize: {
107 id: 'menu.window.minimize',
108 defaultMessage: '!!!Minimize',
109 },
110 close: {
111 id: 'menu.window.close',
112 defaultMessage: '!!!Close',
113 },
114 learnMore: {
115 id: 'menu.help.learnMore',
116 defaultMessage: '!!!Learn More',
117 },
118 changelog: {
119 id: 'menu.help.changelog',
120 defaultMessage: '!!!Changelog',
121 },
122 support: {
123 id: 'menu.help.support',
124 defaultMessage: '!!!Support',
125 },
126 tos: {
127 id: 'menu.help.tos',
128 defaultMessage: '!!!Terms of Service',
129 },
130 privacy: {
131 id: 'menu.help.privacy',
132 defaultMessage: '!!!Privacy Statement',
133 },
134 file: {
135 id: 'menu.file',
136 defaultMessage: '!!!File',
137 },
138 view: {
139 id: 'menu.view',
140 defaultMessage: '!!!View',
141 },
142 services: {
143 id: 'menu.services',
144 defaultMessage: '!!!Services',
145 },
146 window: {
147 id: 'menu.window',
148 defaultMessage: '!!!Window',
149 },
150 help: {
151 id: 'menu.help',
152 defaultMessage: '!!!Help',
153 },
154 about: {
155 id: 'menu.app.about',
156 defaultMessage: '!!!About Franz',
157 },
158 settings: {
159 id: 'menu.app.settings',
160 defaultMessage: '!!!Settings',
161 },
162 hide: {
163 id: 'menu.app.hide',
164 defaultMessage: '!!!Hide',
165 },
166 hideOthers: {
167 id: 'menu.app.hideOthers',
168 defaultMessage: '!!!Hide Others',
169 },
170 unhide: {
171 id: 'menu.app.unhide',
172 defaultMessage: '!!!Unhide',
173 },
174 quit: {
175 id: 'menu.app.quit',
176 defaultMessage: '!!!Quit',
177 },
178 addNewService: {
179 id: 'menu.services.addNewService',
180 defaultMessage: '!!!Add New Service...',
181 },
182});
183
184function getActiveWebview() {
185 return window.franz.stores.services.active.webview;
186}
187
188const _templateFactory = intl => [
9 { 189 {
10 label: 'Edit', 190 label: intl.formatMessage(menuItems.edit),
11 submenu: [ 191 submenu: [
12 { 192 {
193 label: intl.formatMessage(menuItems.undo),
13 role: 'undo', 194 role: 'undo',
14 }, 195 },
15 { 196 {
197 label: intl.formatMessage(menuItems.redo),
16 role: 'redo', 198 role: 'redo',
17 }, 199 },
18 { 200 {
19 type: 'separator', 201 type: 'separator',
20 }, 202 },
21 { 203 {
22 role: 'cut', 204 label: intl.formatMessage(menuItems.cut),
205 accelerator: 'Cmd+X',
206 selector: 'cut:',
23 }, 207 },
24 { 208 {
25 label: 'Copy', 209 label: intl.formatMessage(menuItems.copy),
26 accelerator: 'CmdOrCtrl+C', 210 accelerator: 'Cmd+C',
27 selector: 'copy:', 211 selector: 'copy:',
28 }, 212 },
29 { 213 {
30 label: 'Paste', 214 label: intl.formatMessage(menuItems.paste),
31 accelerator: 'CmdOrCtrl+V', 215 accelerator: 'Cmd+V',
32 selector: 'paste:', 216 selector: 'paste:',
33 }, 217 },
34 { 218 {
35 role: 'pasteandmatchstyle', 219 label: intl.formatMessage(menuItems.pasteAndMatchStyle),
220 accelerator: 'Cmd+Shift+V',
221 selector: 'pasteAndMatchStyle:',
36 }, 222 },
37 { 223 {
224 label: intl.formatMessage(menuItems.delete),
38 role: 'delete', 225 role: 'delete',
39 }, 226 },
40 { 227 {
41 role: 'selectall', 228 label: intl.formatMessage(menuItems.selectAll),
229 accelerator: 'Cmd+A',
230 selector: 'selectAll:',
42 }, 231 },
43 ], 232 ],
44 }, 233 },
45 { 234 {
46 label: 'View', 235 label: intl.formatMessage(menuItems.view),
47 submenu: [ 236 submenu: [
48 { 237 {
49 type: 'separator', 238 type: 'separator',
50 }, 239 },
51 { 240 {
241 label: intl.formatMessage(menuItems.resetZoom),
52 role: 'resetzoom', 242 role: 'resetzoom',
53 }, 243 },
54 { 244 {
245 label: intl.formatMessage(menuItems.zoomIn),
246 // accelerator: 'Cmd+=',
55 role: 'zoomin', 247 role: 'zoomin',
56 accelerator: 'CommandOrControl+=',
57 }, 248 },
58 { 249 {
250 label: intl.formatMessage(menuItems.zoomOut),
59 role: 'zoomout', 251 role: 'zoomout',
60 }, 252 },
61 { 253 {
62 type: 'separator', 254 type: 'separator',
63 }, 255 },
64 { 256 {
257 label: app.mainWindow.isFullScreen() // label doesn't work, gets overridden by Electron
258 ? intl.formatMessage(menuItems.exitFullScreen)
259 : intl.formatMessage(menuItems.enterFullScreen),
65 role: 'togglefullscreen', 260 role: 'togglefullscreen',
66 }, 261 },
67 ], 262 ],
68 }, 263 },
69 { 264 {
70 label: 'Services', 265 label: intl.formatMessage(menuItems.services),
71 submenu: [], 266 submenu: [],
72 }, 267 },
73 { 268 {
269 label: intl.formatMessage(menuItems.window),
74 role: 'window', 270 role: 'window',
75 submenu: [ 271 submenu: [
76 { 272 {
273 label: intl.formatMessage(menuItems.minimize),
77 role: 'minimize', 274 role: 'minimize',
78 }, 275 },
79 { 276 {
277 label: intl.formatMessage(menuItems.close),
80 role: 'close', 278 role: 'close',
81 }, 279 },
82 ], 280 ],
83 }, 281 },
84 { 282 {
283 label: intl.formatMessage(menuItems.help),
85 role: 'help', 284 role: 'help',
86 submenu: [ 285 submenu: [
87 { 286 {
88 label: 'Learn More', 287 label: intl.formatMessage(menuItems.learnMore),
89 click() { shell.openExternal('http://meetfranz.com'); }, 288 click() { shell.openExternal('http://meetfranz.com'); },
90 }, 289 },
91 { 290 {
92 label: 'Changelog', 291 label: intl.formatMessage(menuItems.changelog),
93 click() { shell.openExternal('https://github.com/meetfranz/franz/blob/master/CHANGELOG.md'); }, 292 click() { shell.openExternal('https://github.com/meetfranz/franz/blob/master/CHANGELOG.md'); },
94 }, 293 },
95 { 294 {
96 type: 'separator', 295 type: 'separator',
97 }, 296 },
98 { 297 {
99 label: 'Support', 298 label: intl.formatMessage(menuItems.support),
100 click() { shell.openExternal('http://meetfranz.com/support'); }, 299 click() { shell.openExternal('http://meetfranz.com/support'); },
101 }, 300 },
102 { 301 {
103 type: 'separator', 302 type: 'separator',
104 }, 303 },
105 { 304 {
106 label: 'Terms of Service', 305 label: intl.formatMessage(menuItems.tos),
107 click() { shell.openExternal('https://meetfranz.com/terms'); }, 306 click() { shell.openExternal('https://meetfranz.com/terms'); },
108 }, 307 },
109 { 308 {
110 label: 'Privacy Statement', 309 label: intl.formatMessage(menuItems.privacy),
310 click() { shell.openExternal('https://meetfranz.com/privacy'); },
311 },
312 ],
313 },
314];
315
316const _titleBarTemplateFactory = intl => [
317 {
318 label: intl.formatMessage(menuItems.edit),
319 submenu: [
320 {
321 label: intl.formatMessage(menuItems.undo),
322 accelerator: `${ctrlKey}+Z`,
323 click() {
324 getActiveWebview().undo();
325 },
326 },
327 {
328 label: intl.formatMessage(menuItems.redo),
329 accelerator: `${ctrlKey}+Y`,
330 click() {
331 getActiveWebview().redo();
332 },
333 },
334 {
335 type: 'separator',
336 },
337 {
338 label: intl.formatMessage(menuItems.cut),
339 accelerator: `${ctrlKey}+X`,
340 click() {
341 getActiveWebview().cut();
342 },
343 },
344 {
345 label: intl.formatMessage(menuItems.copy),
346 accelerator: `${ctrlKey}+C`,
347 click() {
348 getActiveWebview().copy();
349 },
350 },
351 {
352 label: intl.formatMessage(menuItems.paste),
353 accelerator: `${ctrlKey}+V`,
354 click() {
355 getActiveWebview().paste();
356 },
357 },
358 {
359 label: intl.formatMessage(menuItems.pasteAndMatchStyle),
360 accelerator: `${ctrlKey}+Shift+V`,
361 click() {
362 getActiveWebview().pasteAndMatchStyle();
363 },
364 },
365 {
366 label: intl.formatMessage(menuItems.delete),
367 click() {
368 getActiveWebview().delete();
369 },
370 },
371 {
372 label: intl.formatMessage(menuItems.selectAll),
373 accelerator: `${ctrlKey}+A`,
374 click() {
375 getActiveWebview().selectAll();
376 },
377 },
378 ],
379 },
380 {
381 label: intl.formatMessage(menuItems.view),
382 submenu: [
383 {
384 type: 'separator',
385 },
386 {
387 label: intl.formatMessage(menuItems.resetZoom),
388 accelerator: `${ctrlKey}+0`,
389 click() {
390 getActiveWebview().setZoomLevel(0);
391 },
392 },
393 {
394 label: intl.formatMessage(menuItems.zoomIn),
395 accelerator: `${ctrlKey}+Plus`,
396 click() {
397 getActiveWebview().getZoomLevel((zoomLevel) => {
398 getActiveWebview().setZoomLevel(zoomLevel === 5 ? zoomLevel : zoomLevel + 1);
399 });
400 },
401 },
402 {
403 label: intl.formatMessage(menuItems.zoomOut),
404 accelerator: `${ctrlKey}+-`,
405 click() {
406 getActiveWebview().getZoomLevel((zoomLevel) => {
407 getActiveWebview().setZoomLevel(zoomLevel === -5 ? zoomLevel : zoomLevel - 1);
408 });
409 },
410 },
411 {
412 type: 'separator',
413 },
414 {
415 label: app.mainWindow.isFullScreen() // label doesn't work, gets overridden by Electron
416 ? intl.formatMessage(menuItems.exitFullScreen)
417 : intl.formatMessage(menuItems.enterFullScreen),
418 accelerator: `${ctrlKey}+F`,
419 click(menuItem, browserWindow) {
420 browserWindow.setFullScreen(!browserWindow.isFullScreen());
421 },
422 },
423 ],
424 },
425 {
426 label: intl.formatMessage(menuItems.services),
427 submenu: [],
428 },
429 {
430 label: intl.formatMessage(menuItems.window),
431 submenu: [
432 {
433 label: intl.formatMessage(menuItems.minimize),
434 accelerator: 'Alt+M',
435 click(menuItem, browserWindow) {
436 browserWindow.minimize();
437 },
438 },
439 {
440 label: intl.formatMessage(menuItems.close),
441 accelerator: 'Alt+W',
442 click(menuItem, browserWindow) {
443 browserWindow.close();
444 },
445 },
446 ],
447 },
448 {
449 label: '?',
450 submenu: [
451 {
452 label: intl.formatMessage(menuItems.learnMore),
453 click() { shell.openExternal('http://meetfranz.com'); },
454 },
455 {
456 label: intl.formatMessage(menuItems.changelog),
457 click() { shell.openExternal('https://github.com/meetfranz/franz/blob/master/CHANGELOG.md'); },
458 },
459 {
460 type: 'separator',
461 },
462 {
463 label: intl.formatMessage(menuItems.support),
464 click() { shell.openExternal('http://meetfranz.com/support'); },
465 },
466 {
467 type: 'separator',
468 },
469 {
470 label: intl.formatMessage(menuItems.tos),
471 click() { shell.openExternal('https://meetfranz.com/terms'); },
472 },
473 {
474 label: intl.formatMessage(menuItems.privacy),
111 click() { shell.openExternal('https://meetfranz.com/privacy'); }, 475 click() { shell.openExternal('https://meetfranz.com/privacy'); },
112 }, 476 },
113 ], 477 ],
@@ -115,7 +479,7 @@ const template = [
115]; 479];
116 480
117export default class FranzMenu { 481export default class FranzMenu {
118 @observable tpl = template; 482 @observable currentTemplate = [];
119 483
120 constructor(stores, actions) { 484 constructor(stores, actions) {
121 this.stores = stores; 485 this.stores = stores;
@@ -124,23 +488,45 @@ export default class FranzMenu {
124 autorun(this._build.bind(this)); 488 autorun(this._build.bind(this));
125 } 489 }
126 490
491 rebuild() {
492 this._build();
493 }
494
495 get template() {
496 return this.currentTemplate.toJS();
497 }
498
127 _build() { 499 _build() {
128 const tpl = toJS(this.tpl); 500 // console.log(window.franz);
501 const serviceTpl = Object.assign([], this.serviceTpl); // need to clone object so we don't modify computed (cached) object
502
503 if (window.franz === undefined) {
504 return;
505 }
506
507 const intl = window.franz.intl;
508 const tpl = isMac ? _templateFactory(intl) : _titleBarTemplateFactory(intl);
129 509
130 tpl[1].submenu.push({ 510 tpl[1].submenu.push({
131 role: 'toggledevtools', 511 type: 'separator',
512 }, {
513 label: intl.formatMessage(menuItems.toggleDevTools),
514 accelerator: `${cmdKey}+Alt+I`,
515 click: (menuItem, browserWindow) => {
516 browserWindow.webContents.toggleDevTools();
517 },
132 }, { 518 }, {
133 label: 'Toggle Service Developer Tools', 519 label: intl.formatMessage(menuItems.toggleServiceDevTools),
134 accelerator: 'CmdOrCtrl+Shift+Alt+i', 520 accelerator: `${cmdKey}+Shift+Alt+I`,
135 click: () => { 521 click: () => {
136 this.actions.service.openDevToolsForActiveService(); 522 this.actions.service.openDevToolsForActiveService();
137 }, 523 },
138 }); 524 });
139 525
140 tpl[1].submenu.unshift({ 526 tpl[1].submenu.unshift({
141 label: 'Reload Service', 527 label: intl.formatMessage(menuItems.reloadService),
142 id: 'reloadService', 528 id: 'reloadService', // TODO: needed?
143 accelerator: 'CmdOrCtrl+R', 529 accelerator: `${cmdKey}+R`,
144 click: () => { 530 click: () => {
145 if (this.stores.user.isLoggedIn 531 if (this.stores.user.isLoggedIn
146 && this.stores.services.enabled.length > 0) { 532 && this.stores.services.enabled.length > 0) {
@@ -150,93 +536,128 @@ export default class FranzMenu {
150 } 536 }
151 }, 537 },
152 }, { 538 }, {
153 label: 'Reload Franz', 539 label: intl.formatMessage(menuItems.reloadFranz),
154 accelerator: 'CmdOrCtrl+Shift+R', 540 accelerator: `${cmdKey}+Shift+R`,
155 click: () => { 541 click: () => {
156 window.location.reload(); 542 window.location.reload();
157 }, 543 },
158 }); 544 });
159 545
160 if (isMac) { 546 tpl.unshift({
161 tpl.unshift({ 547 label: isMac ? app.getName() : intl.formatMessage(menuItems.file),
162 label: app.getName(), 548 submenu: [
163 submenu: [ 549 {
164 { 550 label: intl.formatMessage(menuItems.about),
165 role: 'about', 551 role: 'about',
166 }, 552 },
167 { 553 {
168 type: 'separator', 554 type: 'separator',
169 }, 555 },
170 { 556 {
171 label: 'Settings', 557 label: intl.formatMessage(menuItems.settings),
172 accelerator: 'CmdOrCtrl+,', 558 accelerator: 'CmdOrCtrl+,',
173 click: () => { 559 click: () => {
174 this.actions.ui.openSettings({ path: 'app' }); 560 this.actions.ui.openSettings({ path: 'app' });
175 },
176 },
177 {
178 type: 'separator',
179 },
180 {
181 role: 'services',
182 submenu: [],
183 },
184 {
185 type: 'separator',
186 },
187 {
188 role: 'hide',
189 },
190 {
191 role: 'hideothers',
192 },
193 {
194 role: 'unhide',
195 },
196 {
197 type: 'separator',
198 },
199 {
200 role: 'quit',
201 }, 561 },
202 ], 562 },
203 }); 563 {
564 type: 'separator',
565 },
566 {
567 label: intl.formatMessage(menuItems.services),
568 role: 'services',
569 submenu: [],
570 },
571 {
572 type: 'separator',
573 },
574 {
575 label: intl.formatMessage(menuItems.hide),
576 role: 'hide',
577 },
578 {
579 label: intl.formatMessage(menuItems.hideOthers),
580 role: 'hideothers',
581 },
582 {
583 label: intl.formatMessage(menuItems.unhide),
584 role: 'unhide',
585 },
586 {
587 type: 'separator',
588 },
589 {
590 label: intl.formatMessage(menuItems.quit),
591 role: 'quit',
592 },
593 ],
594 });
595
596 const about = {
597 label: intl.formatMessage(menuItems.about),
598 click: () => {
599 dialog.showMessageBox({
600 type: 'info',
601 title: 'Franz',
602 message: 'Franz',
603 detail: `Version: ${remote.app.getVersion()}\nRelease: ${process.versions.electron} / ${process.platform} / ${process.arch}`,
604 });
605 },
606 };
607
608 if (isMac) {
204 // Edit menu. 609 // Edit menu.
205 tpl[1].submenu.push( 610 tpl[1].submenu.push(
206 { 611 {
207 type: 'separator', 612 type: 'separator',
208 }, 613 },
209 { 614 {
210 label: 'Speech', 615 label: intl.formatMessage(menuItems.speech),
211 submenu: [ 616 submenu: [
212 { 617 {
618 label: intl.formatMessage(menuItems.startSpeaking),
213 role: 'startspeaking', 619 role: 'startspeaking',
214 }, 620 },
215 { 621 {
622 label: intl.formatMessage(menuItems.stopSpeaking),
216 role: 'stopspeaking', 623 role: 'stopspeaking',
217 }, 624 },
218 ], 625 ],
219 }, 626 },
220 ); 627 );
628
629 tpl[4].submenu.unshift(about, {
630 type: 'separator',
631 });
221 } else { 632 } else {
222 tpl[4].submenu.unshift({ 633 tpl[0].submenu = [
223 role: 'about', 634 {
224 click: () => { 635 label: intl.formatMessage(menuItems.settings),
225 dialog.showMessageBox({ 636 accelerator: 'Ctrl+P',
226 type: 'info', 637 click: () => {
227 title: 'Franz', 638 this.actions.ui.openSettings({ path: 'app' });
228 message: 'Franz', 639 },
229 detail: `Version: ${remote.app.getVersion()}\nRelease: ${process.versions.electron} / ${process.platform} / ${process.arch}`,
230 });
231 }, 640 },
232 }); 641 {
233 } 642 type: 'separator',
643 },
644 {
645 label: intl.formatMessage(menuItems.quit),
646 accelerator: 'Alt+F4',
647 click: () => {
648 app.quit();
649 },
650 },
651 ];
234 652
235 const serviceTpl = this.serviceTpl; 653 tpl[5].submenu.push({
654 type: 'separator',
655 }, about);
656 }
236 657
237 serviceTpl.unshift({ 658 serviceTpl.unshift({
238 label: 'Add new Service', 659 label: intl.formatMessage(menuItems.addNewService),
239 accelerator: 'CmdOrCtrl+N', 660 accelerator: `${cmdKey}+N`,
240 click: () => { 661 click: () => {
241 this.actions.ui.openSettings({ path: 'recipes' }); 662 this.actions.ui.openSettings({ path: 'recipes' });
242 }, 663 },
@@ -245,9 +666,10 @@ export default class FranzMenu {
245 }); 666 });
246 667
247 if (serviceTpl.length > 0) { 668 if (serviceTpl.length > 0) {
248 tpl[isMac ? 3 : 2].submenu = toJS(this.serviceTpl); 669 tpl[3].submenu = serviceTpl;
249 } 670 }
250 671
672 this.currentTemplate = tpl;
251 const menu = Menu.buildFromTemplate(tpl); 673 const menu = Menu.buildFromTemplate(tpl);
252 Menu.setApplicationMenu(menu); 674 Menu.setApplicationMenu(menu);
253 } 675 }
@@ -258,7 +680,7 @@ export default class FranzMenu {
258 if (this.stores.user.isLoggedIn) { 680 if (this.stores.user.isLoggedIn) {
259 return services.map((service, i) => ({ 681 return services.map((service, i) => ({
260 label: this._getServiceName(service), 682 label: this._getServiceName(service),
261 accelerator: i <= 9 ? `CmdOrCtrl+${i + 1}` : null, 683 accelerator: i <= 9 ? `${cmdKey}+${i + 1}` : null,
262 type: 'radio', 684 type: 'radio',
263 checked: service.isActive, 685 checked: service.isActive,
264 click: () => { 686 click: () => {
diff --git a/src/styles/layout.scss b/src/styles/layout.scss
index afdd7dec7..964a9fcea 100644
--- a/src/styles/layout.scss
+++ b/src/styles/layout.scss
@@ -6,7 +6,11 @@ html {
6 6
7.app { 7.app {
8 display: flex; 8 display: flex;
9 flex-direction: row; 9 flex-direction: column;
10
11 .app__content {
12 display: flex;
13 }
10 14
11 .app__service { 15 .app__service {
12 display: flex; 16 display: flex;
@@ -15,6 +19,10 @@ html {
15 } 19 }
16} 20}
17 21
22.electron-app-title-bar {
23 z-index: 99999999;
24}
25
18.window-draggable { 26.window-draggable {
19 position: absolute; 27 position: absolute;
20 width: 100%; 28 width: 100%;
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 446bdca14..784a04d3d 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -4,6 +4,7 @@ $mdi-font-path: '../node_modules/mdi/fonts';
4} 4}
5 5
6@import './node_modules/mdi/scss/materialdesignicons.scss'; 6@import './node_modules/mdi/scss/materialdesignicons.scss';
7@import './node_modules/electron-react-titlebar/assets/style';
7 8
8// modules 9// modules
9@import './reset.scss'; 10@import './reset.scss';
@@ -28,6 +29,7 @@ $mdi-font-path: '../node_modules/mdi/fonts';
28@import './subscription-popup.scss'; 29@import './subscription-popup.scss';
29@import './content-tabs.scss'; 30@import './content-tabs.scss';
30@import './invite.scss'; 31@import './invite.scss';
32@import './title-bar.scss';
31 33
32// form 34// form
33@import './input.scss'; 35@import './input.scss';
diff --git a/src/styles/title-bar.scss b/src/styles/title-bar.scss
new file mode 100644
index 000000000..492245e2f
--- /dev/null
+++ b/src/styles/title-bar.scss
@@ -0,0 +1,50 @@
1#electron-app-title-bar {
2 background: $theme-gray-lightest;
3 border-bottom: 0;
4 box-shadow: 0px 0 8px rgba(#000, 0.1);
5
6 span {
7 line-height: normal;
8 }
9
10 div {
11 height: auto;
12 }
13
14 .toolbar-dropdown {
15 &.open {
16 box-shadow: 0px 0 8px rgba(#000, 0.1);
17 }
18
19 &:not(.open) {
20 .menu-item .menu-label {
21 opacity: 1;
22 }
23
24 &>.toolbar-button > button:hover {
25 background: $theme-brand-primary;
26 }
27 }
28 }
29
30 .list-item {
31 .menu-item {
32 margin: 4px;
33 border-radius: $theme-border-radius-small;
34 }
35 &.selected {
36 // background: $theme-brand-primary;
37 background: none;
38
39 .menu-item {
40 background: $theme-brand-primary;
41 }
42 }
43 }
44
45 .menu-pane {
46 box-shadow: 0px 0 10px rgba(#000, 0.5);
47 border-bottom-left-radius: $theme-border-radius-small;
48 border-bottom-right-radius: $theme-border-radius-small;
49 }
50}