Skip to content

Commit 9af5fd0

Browse files
authored
feat(Windows): Replace window frame with custom menu bar
Title bar for Windows & Linux
2 parents 27b0c02 + a17cfb3 commit 9af5fd0

File tree

14 files changed

+786
-168
lines changed

14 files changed

+786
-168
lines changed

.vscode/launch.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"program": "${workspaceFolder}/build/index.js",
1010
"protocol": "inspector",
1111
"env": {
12-
"NODE_ENV": "development"
12+
"NODE_ENV": "development",
13+
"OS_PLATFORM": "win32"
1314
}
1415
},
1516
{
@@ -18,9 +19,10 @@
1819
"name": "Franz – Live API",
1920
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
2021
"program": "${workspaceFolder}/build/index.js",
21-
"protocol": "inspector",
22+
"protocol": "inspector",
2223
"env": {
23-
"LIVE_API": "1"
24+
"LIVE_API": "1",
25+
"OS_PLATFORM": "win32"
2426
}
2527
},
2628
{
@@ -31,7 +33,8 @@
3133
"program": "${workspaceFolder}/build/index.js",
3234
"protocol": "inspector",
3335
"env": {
34-
"LOCAL_API": "1"
36+
"LOCAL_API": "1",
37+
"OS_PLATFORM": "win32"
3538
}
3639
}
3740
]

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"classnames": "^2.2.5",
3636
"du": "^0.1.0",
3737
"electron-fetch": "^1.1.0",
38+
"electron-react-titlebar": "^0.7.1",
3839
"electron-spellchecker": "^1.1.2",
3940
"electron-updater": "^2.4.3",
4041
"electron-window-state": "^4.1.0",

src/I18n.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@ import UserStore from './stores/UserStore';
99

1010
@inject('stores') @observer
1111
export default class I18N extends Component {
12+
componentDidUpdate() {
13+
window.franz.menu.rebuild();
14+
}
15+
1216
render() {
1317
const { stores, children } = this.props;
1418
const { locale } = stores.app;
1519
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+
>
1724
{children}
1825
</IntlProvider>
1926
);

src/components/layout/AppLayout.js

Lines changed: 64 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
44
import { defineMessages, intlShape } from 'react-intl';
5+
import { TitleBar } from 'electron-react-titlebar';
56

67
import InfoBar from '../ui/InfoBar';
78
import globalMessages from '../../i18n/globalMessages';
89

10+
import { isMac } from '../../environment';
11+
912
function createMarkup(HTMLString) {
1013
return { __html: HTMLString };
1114
}
@@ -87,64 +90,67 @@ export default class AppLayout extends Component {
8790
return (
8891
<div>
8992
<div className="app">
90-
{sidebar}
91-
<div className="app__service">
92-
{news.length > 0 && news.map(item => (
93-
<InfoBar
94-
key={item.id}
95-
position="top"
96-
type={item.type}
97-
sticky={item.sticky}
98-
onHide={() => removeNewsItem({ newsId: item.id })}
99-
>
100-
<span dangerouslySetInnerHTML={createMarkup(item.message)} />
101-
</InfoBar>
102-
))}
103-
{!isOnline && (
104-
<InfoBar
105-
type="danger"
106-
>
107-
<span className="mdi mdi-flash" />
108-
{intl.formatMessage(globalMessages.notConnectedToTheInternet)}
109-
</InfoBar>
110-
)}
111-
{!areRequiredRequestsSuccessful && showRequiredRequestsError && (
112-
<InfoBar
113-
type="danger"
114-
ctaLabel="Try again"
115-
ctaLoading={areRequiredRequestsLoading}
116-
sticky
117-
onClick={retryRequiredRequests}
118-
>
119-
<span className="mdi mdi-flash" />
120-
{intl.formatMessage(messages.requiredRequestsFailed)}
121-
</InfoBar>
122-
)}
123-
{showServicesUpdatedInfoBar && (
124-
<InfoBar
125-
type="primary"
126-
ctaLabel={intl.formatMessage(messages.buttonReloadServices)}
127-
onClick={reloadServicesAfterUpdate}
128-
sticky
129-
>
130-
<span className="mdi mdi-power-plug" />
131-
{intl.formatMessage(messages.servicesUpdated)}
132-
</InfoBar>
133-
)}
134-
{appUpdateIsDownloaded && (
135-
<InfoBar
136-
type="primary"
137-
ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)}
138-
onClick={installAppUpdate}
139-
sticky
140-
>
141-
<span className="mdi mdi-information" />
142-
{intl.formatMessage(messages.updateAvailable)} <a href="https://meetfranz.com/changelog" target="_blank">
143-
<u>{intl.formatMessage(messages.changelog)}</u>
144-
</a>
145-
</InfoBar>
146-
)}
147-
{services}
93+
{!isMac && <TitleBar menu={window.franz.menu.template} icon={'assets/images/logo.svg'} />}
94+
<div className="app__content">
95+
{sidebar}
96+
<div className="app__service">
97+
{news.length > 0 && news.map(item => (
98+
<InfoBar
99+
key={item.id}
100+
position="top"
101+
type={item.type}
102+
sticky={item.sticky}
103+
onHide={() => removeNewsItem({ newsId: item.id })}
104+
>
105+
<span dangerouslySetInnerHTML={createMarkup(item.message)} />
106+
</InfoBar>
107+
))}
108+
{!isOnline && (
109+
<InfoBar
110+
type="danger"
111+
>
112+
<span className="mdi mdi-flash" />
113+
{intl.formatMessage(globalMessages.notConnectedToTheInternet)}
114+
</InfoBar>
115+
)}
116+
{!areRequiredRequestsSuccessful && showRequiredRequestsError && (
117+
<InfoBar
118+
type="danger"
119+
ctaLabel="Try again"
120+
ctaLoading={areRequiredRequestsLoading}
121+
sticky
122+
onClick={retryRequiredRequests}
123+
>
124+
<span className="mdi mdi-flash" />
125+
{intl.formatMessage(messages.requiredRequestsFailed)}
126+
</InfoBar>
127+
)}
128+
{showServicesUpdatedInfoBar && (
129+
<InfoBar
130+
type="primary"
131+
ctaLabel={intl.formatMessage(messages.buttonReloadServices)}
132+
onClick={reloadServicesAfterUpdate}
133+
sticky
134+
>
135+
<span className="mdi mdi-power-plug" />
136+
{intl.formatMessage(messages.servicesUpdated)}
137+
</InfoBar>
138+
)}
139+
{appUpdateIsDownloaded && (
140+
<InfoBar
141+
type="primary"
142+
ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)}
143+
onClick={installAppUpdate}
144+
sticky
145+
>
146+
<span className="mdi mdi-information" />
147+
{intl.formatMessage(messages.updateAvailable)} <a href="https://meetfranz.com/changelog" target="_blank">
148+
<u>{intl.formatMessage(messages.changelog)}</u>
149+
</a>
150+
</InfoBar>
151+
)}
152+
{services}
153+
</div>
148154
</div>
149155
</div>
150156
{children}

src/environment.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@ export const isDevMode = Boolean(process.execPath.match(/[\\/]electron/));
44
export const useLiveAPI = process.env.LIVE_API;
55
export const useLocalAPI = process.env.LOCAL_API;
66

7-
export const isMac = process.platform === 'darwin';
8-
export const isWindows = process.platform === 'win32';
9-
export const isLinux = process.platform === 'linux';
7+
let platform = process.platform;
8+
if (process.env.OS_PLATFORM) {
9+
platform = process.env.OS_PLATFORM;
10+
}
11+
12+
export const isMac = platform === 'darwin';
13+
export const isWindows = platform === 'win32';
14+
export const isLinux = platform === 'linux';
1015

1116
export const ctrlKey = isMac ? '⌘' : 'Ctrl';
17+
export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
1218

1319
let api;
1420
if (!isDevMode || (isDevMode && useLiveAPI)) {

src/helpers/validation-helpers.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
1+
import { defineMessages } from 'react-intl';
2+
3+
const 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+
126
export function required({ field }) {
227
const isValid = (field.value.trim() !== '');
3-
return [isValid, `${field.label} is required`];
28+
return [isValid, window.franz.intl.formatMessage(messages.required, { field: field.label })];
429
}
530

631
export function email({ field }) {
@@ -13,7 +38,7 @@ export function email({ field }) {
1338
isValid = true;
1439
}
1540

16-
return [isValid, `${field.label} not valid`];
41+
return [isValid, window.franz.intl.formatMessage(messages.email, { field: field.label })];
1742
}
1843

1944
export function url({ field }) {
@@ -27,7 +52,7 @@ export function url({ field }) {
2752
isValid = true;
2853
}
2954

30-
return [isValid, `${field.label} is not a valid url`];
55+
return [isValid, window.franz.intl.formatMessage(messages.url, { field: field.label })];
3156
}
3257

3358
export function minLength(length) {
@@ -36,13 +61,13 @@ export function minLength(length) {
3661
if (field.touched) {
3762
isValid = field.value.length >= length;
3863
}
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 })];
4065
};
4166
}
4267

4368
export function oneRequired(targets) {
4469
return ({ field, form }) => {
4570
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 })];
4772
};
4873
}

src/i18n/locales/en-US.json

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,5 +199,52 @@
199199
"service.crashHandler.action": "Reload {name}",
200200
"service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds",
201201
"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"
203250
}

src/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<div class="dev-warning">DEV MODE</div>
1212
<div id="root"></div>
1313
<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);
1515

1616
const { isDevMode } = require('./environment');
1717
if (isDevMode) {

src/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import path from 'path';
44

55
import windowStateKeeper from 'electron-window-state';
66

7-
import { isDevMode, isWindows } from './environment';
7+
import { isDevMode, isMac, isWindows } from './environment';
88
import ipcApi from './electron/ipc-api';
99
import Tray from './lib/Tray';
1010
import Settings from './electron/Settings';
@@ -72,9 +72,9 @@ const createWindow = () => {
7272
height: mainWindowState.height,
7373
minWidth: 600,
7474
minHeight: 500,
75-
titleBarStyle: 'hidden',
75+
titleBarStyle: isMac ? 'hidden' : '',
76+
frame: false,
7677
backgroundColor: '#3498db',
77-
autoHideMenuBar: true,
7878
});
7979

8080
// Initialize System Tray

0 commit comments

Comments
 (0)