>;
10 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.settings.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src"
6 | },
7 | "references": [
8 | {
9 | "path": "../theme"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/ui/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json"
3 | }
4 |
--------------------------------------------------------------------------------
/src/I18n.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import { IntlProvider } from 'react-intl';
5 |
6 | import { oneOrManyChildElements } from './prop-types';
7 | import translations from './i18n/translations';
8 | import UserStore from './stores/UserStore';
9 |
10 | export default @inject('stores') @observer class I18N extends Component {
11 | // componentDidUpdate() {
12 | // window.franz.menu.rebuild();
13 | // }
14 |
15 | render() {
16 | const { stores, children } = this.props;
17 | const { locale } = stores.app;
18 | return (
19 | { window.franz.intl = intlProvider ? intlProvider.getChildContext().intl : null; }}
22 | >
23 | {children}
24 |
25 | );
26 | }
27 | }
28 |
29 | I18N.wrappedComponent.propTypes = {
30 | stores: PropTypes.shape({
31 | user: PropTypes.instanceOf(UserStore).isRequired,
32 | }).isRequired,
33 | children: oneOrManyChildElements.isRequired,
34 | };
35 |
--------------------------------------------------------------------------------
/src/actions/app.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | setBadge: {
5 | unreadDirectMessageCount: PropTypes.number.isRequired,
6 | unreadIndirectMessageCount: PropTypes.number,
7 | },
8 | notify: {
9 | title: PropTypes.string.isRequired,
10 | options: PropTypes.object.isRequired,
11 | serviceId: PropTypes.string,
12 | },
13 | launchOnStartup: {
14 | enable: PropTypes.bool.isRequired,
15 | },
16 | openExternalUrl: {
17 | url: PropTypes.string.isRequired,
18 | },
19 | checkForUpdates: {},
20 | resetUpdateStatus: {},
21 | installUpdate: {},
22 | healthCheck: {},
23 | muteApp: {
24 | isMuted: PropTypes.bool.isRequired,
25 | overrideSystemMute: PropTypes.bool,
26 | },
27 | toggleMuteApp: {},
28 | clearAllCache: {},
29 | };
30 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | import defineActions from './lib/actions';
4 | import service from './service';
5 | import recipe from './recipe';
6 | import recipePreview from './recipePreview';
7 | import ui from './ui';
8 | import app from './app';
9 | import user from './user';
10 | import payment from './payment';
11 | import news from './news';
12 | import settings from './settings';
13 | import requests from './requests';
14 | import announcements from '../features/announcements/actions';
15 | import workspaces from '../features/workspaces/actions';
16 | import todos from '../features/todos/actions';
17 | import planSelection from '../features/planSelection/actions';
18 | import trialStatusBar from '../features/trialStatusBar/actions';
19 |
20 | const actions = Object.assign({}, {
21 | service,
22 | recipe,
23 | recipePreview,
24 | ui,
25 | app,
26 | user,
27 | payment,
28 | news,
29 | settings,
30 | requests,
31 | });
32 |
33 | export default Object.assign(
34 | defineActions(actions, PropTypes.checkPropTypes),
35 | { announcements },
36 | { workspaces },
37 | { todos },
38 | { planSelection },
39 | { trialStatusBar },
40 | );
41 |
--------------------------------------------------------------------------------
/src/actions/lib/actions.js:
--------------------------------------------------------------------------------
1 | export const createActionsFromDefinitions = (actionDefinitions, validate) => {
2 | const actions = {};
3 | Object.keys(actionDefinitions).forEach((actionName) => {
4 | const action = (params = {}) => {
5 | const schema = actionDefinitions[actionName];
6 | validate(schema, params, actionName);
7 | action.notify(params);
8 | };
9 | actions[actionName] = action;
10 | action.listeners = [];
11 | action.listen = listener => action.listeners.push(listener);
12 | action.off = (listener) => {
13 | const { listeners } = action;
14 | listeners.splice(listeners.indexOf(listener), 1);
15 | };
16 | action.notify = params => action.listeners.forEach(listener => listener(params));
17 | });
18 | return actions;
19 | };
20 |
21 | export default (definitions, validate) => {
22 | const newActions = {};
23 | Object.keys(definitions).forEach((scopeName) => {
24 | newActions[scopeName] = createActionsFromDefinitions(definitions[scopeName], validate);
25 | });
26 | return newActions;
27 | };
28 |
--------------------------------------------------------------------------------
/src/actions/news.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | hide: {
5 | newsId: PropTypes.string.isRequired,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/actions/payment.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | createHostedPage: {
5 | planId: PropTypes.string.isRequired,
6 | },
7 | upgradeAccount: {
8 | planId: PropTypes.string.isRequired,
9 | onCloseWindow: PropTypes.func,
10 | overrideParent: PropTypes.number,
11 | },
12 | createDashboardUrl: {},
13 | };
14 |
--------------------------------------------------------------------------------
/src/actions/recipe.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | install: {
5 | recipeId: PropTypes.string.isRequired,
6 | update: PropTypes.bool,
7 | },
8 | update: {},
9 | };
10 |
--------------------------------------------------------------------------------
/src/actions/recipePreview.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | search: {
5 | needle: PropTypes.string.isRequired,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/actions/requests.js:
--------------------------------------------------------------------------------
1 | export default {
2 | retryRequiredRequests: {},
3 | };
4 |
--------------------------------------------------------------------------------
/src/actions/settings.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | update: {
5 | type: PropTypes.string.isRequired,
6 | data: PropTypes.object.isRequired,
7 | },
8 | remove: {
9 | type: PropTypes.string.isRequired,
10 | key: PropTypes.string.isRequired,
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/actions/ui.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | openSettings: {
5 | path: PropTypes.string,
6 | },
7 | closeSettings: {},
8 | toggleServiceUpdatedInfoBar: {
9 | visible: PropTypes.bool,
10 | },
11 | hideServices: {},
12 | showServices: {},
13 | };
14 |
--------------------------------------------------------------------------------
/src/actions/user.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | login: {
5 | email: PropTypes.string.isRequired,
6 | password: PropTypes.string.isRequired,
7 | },
8 | logout: {},
9 | signup: {
10 | firstname: PropTypes.string.isRequired,
11 | lastname: PropTypes.string.isRequired,
12 | email: PropTypes.string.isRequired,
13 | password: PropTypes.string.isRequired,
14 | accountType: PropTypes.string,
15 | company: PropTypes.string,
16 | plan: PropTypes.string,
17 | currency: PropTypes.string,
18 | },
19 | retrievePassword: {
20 | email: PropTypes.string.isRequired,
21 | },
22 | activateTrial: {
23 | planId: PropTypes.string.isRequired,
24 | },
25 | invite: {
26 | invites: PropTypes.array.isRequired,
27 | },
28 | update: {
29 | userData: PropTypes.object.isRequired,
30 | },
31 | resetStatus: {},
32 | importLegacyServices: PropTypes.arrayOf(PropTypes.shape({
33 | recipe: PropTypes.string.isRequired,
34 | })).isRequired,
35 | delete: {},
36 | };
37 |
--------------------------------------------------------------------------------
/src/api/AppApi.js:
--------------------------------------------------------------------------------
1 | export default class AppApi {
2 | constructor(server) {
3 | this.server = server;
4 | }
5 |
6 | health() {
7 | return this.server.healthCheck();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/api/FeaturesApi.js:
--------------------------------------------------------------------------------
1 | export default class FeaturesApi {
2 | constructor(server) {
3 | this.server = server;
4 | }
5 |
6 | default() {
7 | return this.server.getDefaultFeatures();
8 | }
9 |
10 | features() {
11 | return this.server.getFeatures();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/api/LocalApi.js:
--------------------------------------------------------------------------------
1 | export default class LocalApi {
2 | constructor(server, local) {
3 | this.server = server;
4 | this.local = local;
5 | }
6 |
7 | getAppSettings(type) {
8 | return this.local.getAppSettings(type);
9 | }
10 |
11 | updateAppSettings(type, data) {
12 | return this.local.updateAppSettings(type, data);
13 | }
14 |
15 | getAppCacheSize() {
16 | return this.local.getAppCacheSize();
17 | }
18 |
19 | clearAppCache() {
20 | return this.local.clearAppCache();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/api/NewsApi.js:
--------------------------------------------------------------------------------
1 | export default class NewsApi {
2 | constructor(server, local) {
3 | this.server = server;
4 | this.local = local;
5 | }
6 |
7 | latest() {
8 | return this.server.getLatestNews();
9 | }
10 |
11 | hide(id) {
12 | return this.server.hideNews(id);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/PaymentApi.js:
--------------------------------------------------------------------------------
1 | export default class PaymentApi {
2 | constructor(server, local) {
3 | this.server = server;
4 | this.local = local;
5 | }
6 |
7 | plans() {
8 | return this.server.getPlans();
9 | }
10 |
11 | getHostedPage(planId) {
12 | return this.server.getHostedPage(planId);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/RecipePreviewsApi.js:
--------------------------------------------------------------------------------
1 | export default class ServicesApi {
2 | constructor(server) {
3 | this.server = server;
4 | }
5 |
6 | all() {
7 | return this.server.getRecipePreviews();
8 | }
9 |
10 | featured() {
11 | return this.server.getFeaturedRecipePreviews();
12 | }
13 |
14 | search(needle) {
15 | return this.server.searchRecipePreviews(needle);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/api/RecipesApi.js:
--------------------------------------------------------------------------------
1 | export default class RecipesApi {
2 | constructor(server) {
3 | this.server = server;
4 | }
5 |
6 | all() {
7 | return this.server.getInstalledRecipes();
8 | }
9 |
10 | install(recipeId) {
11 | return this.server.getRecipePackage(recipeId);
12 | }
13 |
14 | update(recipes) {
15 | return this.server.getRecipeUpdates(recipes);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/api/ServicesApi.js:
--------------------------------------------------------------------------------
1 | export default class ServicesApi {
2 | constructor(server, local) {
3 | this.local = local;
4 | this.server = server;
5 | }
6 |
7 | all() {
8 | return this.server.getServices();
9 | }
10 |
11 | // one(customerId) {
12 | // return this.server.getCustomer(customerId);
13 | // }
14 | //
15 | // search(needle) {
16 | // return this.server.searchCustomers(needle);
17 | // }
18 | //
19 | create(recipeId, data) {
20 | return this.server.createService(recipeId, data);
21 | }
22 |
23 | delete(serviceId) {
24 | return this.server.deleteService(serviceId);
25 | }
26 |
27 | update(serviceId, data) {
28 | return this.server.updateService(serviceId, data);
29 | }
30 |
31 | reorder(data) {
32 | return this.server.reorderService(data);
33 | }
34 |
35 | clearCache(serviceId) {
36 | return this.local.clearCache(serviceId);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/api/UserApi.js:
--------------------------------------------------------------------------------
1 | import { hash } from '../helpers/password-helpers';
2 |
3 | export default class UserApi {
4 | constructor(server, local) {
5 | this.server = server;
6 | this.local = local;
7 | }
8 |
9 | login(email, password) {
10 | return this.server.login(email, hash(password));
11 | }
12 |
13 | logout() {
14 | return this;
15 | }
16 |
17 | signup(data) {
18 | Object.assign(data, {
19 | password: hash(data.password),
20 | });
21 | return this.server.signup(data);
22 | }
23 |
24 | password(email) {
25 | return this.server.retrievePassword(email);
26 | }
27 |
28 | activateTrial(data) {
29 | return this.server.activateTrial(data);
30 | }
31 |
32 | invite(data) {
33 | return this.server.inviteUser(data);
34 | }
35 |
36 | getInfo() {
37 | return this.server.userInfo();
38 | }
39 |
40 | updateInfo(data) {
41 | const userData = data;
42 | if (userData.oldPassword && userData.newPassword) {
43 | userData.oldPassword = hash(userData.oldPassword);
44 | userData.newPassword = hash(userData.newPassword);
45 | }
46 |
47 | return this.server.updateUserInfo(userData);
48 | }
49 |
50 | getLegacyServices() {
51 | return this.server.getLegacyServices();
52 | }
53 |
54 | delete() {
55 | return this.server.deleteAccount();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import AppApi from './AppApi';
2 | import ServicesApi from './ServicesApi';
3 | import RecipePreviewsApi from './RecipePreviewsApi';
4 | import RecipesApi from './RecipesApi';
5 | import UserApi from './UserApi';
6 | import LocalApi from './LocalApi';
7 | import PaymentApi from './PaymentApi';
8 | import NewsApi from './NewsApi';
9 | import FeaturesApi from './FeaturesApi';
10 |
11 | export default (server, local) => ({
12 | app: new AppApi(server, local),
13 | services: new ServicesApi(server, local),
14 | recipePreviews: new RecipePreviewsApi(server, local),
15 | recipes: new RecipesApi(server, local),
16 | features: new FeaturesApi(server, local),
17 | user: new UserApi(server, local),
18 | local: new LocalApi(server, local),
19 | payment: new PaymentApi(server, local),
20 | news: new NewsApi(server, local),
21 | });
22 |
--------------------------------------------------------------------------------
/src/api/server/LocalApi.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import { session } from '@electron/remote';
3 | import du from 'du';
4 |
5 | import { getServicePartitionsDirectory } from '../../helpers/service-helpers.js';
6 |
7 | const debug = require('debug')('Franz:LocalApi');
8 |
9 | export default class LocalApi {
10 | // Settings
11 | getAppSettings(type) {
12 | return new Promise((resolve) => {
13 | ipcRenderer.once('appSettings', (event, resp) => {
14 | debug('LocalApi::getAppSettings resolves', resp.type, resp.data);
15 | resolve(resp);
16 | });
17 |
18 | ipcRenderer.send('getAppSettings', type);
19 | });
20 | }
21 |
22 | async updateAppSettings(type, data) {
23 | debug('LocalApi::updateAppSettings resolves', type, data);
24 | ipcRenderer.send('updateAppSettings', {
25 | type,
26 | data,
27 | });
28 | }
29 |
30 | // Services
31 | async getAppCacheSize() {
32 | const partitionsDir = getServicePartitionsDirectory();
33 | return new Promise((resolve, reject) => {
34 | du(partitionsDir, (err, size) => {
35 | if (err) reject(err);
36 |
37 | debug('LocalApi::getAppCacheSize resolves', size);
38 | resolve(size);
39 | });
40 | });
41 | }
42 |
43 | async clearCache(serviceId) {
44 | const s = session.fromPartition(`persist:service-${serviceId}`);
45 |
46 | debug('LocalApi::clearCache resolves', serviceId);
47 | return s.clearCache();
48 | }
49 |
50 | async clearAppCache() {
51 | const s = session.defaultSession;
52 |
53 | debug('LocalApi::clearCache clearAppCache');
54 | return s.clearCache();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/api/utils/auth.js:
--------------------------------------------------------------------------------
1 | import { app } from '@electron/remote';
2 | import localStorage from 'mobx-localstorage';
3 |
4 | export const prepareAuthRequest = (options = { method: 'GET' }, auth = true) => {
5 | const request = Object.assign(options, {
6 | mode: 'cors',
7 | headers: Object.assign({
8 | 'Content-Type': 'application/json',
9 | 'X-Franz-Source': 'desktop',
10 | 'X-Franz-Version': app.getVersion(),
11 | 'X-Franz-platform': process.platform,
12 | 'X-Franz-Timezone-Offset': new Date().getTimezoneOffset(),
13 | 'X-Franz-System-Locale': app.getLocale(),
14 | }, options.headers),
15 | });
16 |
17 | if (auth) {
18 | request.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`;
19 | }
20 |
21 | return request;
22 | };
23 |
24 | export const sendAuthRequest = (url, options, auth) => (
25 | window.fetch(url, prepareAuthRequest(options, auth))
26 | );
27 |
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-Bold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-BoldItalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-ExtraBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/src/assets/images/emoji/dontknow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/emoji/dontknow.png
--------------------------------------------------------------------------------
/src/assets/images/emoji/sad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/emoji/sad.png
--------------------------------------------------------------------------------
/src/assets/images/emoji/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/emoji/star.png
--------------------------------------------------------------------------------
/src/assets/images/sm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/sm.png
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/display.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/display.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-1.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-1.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-10.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-10.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-2.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-2.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-3.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-3.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-4.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-4.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-5.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-5.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-6.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-6.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-7.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-7.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-8.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-8.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-9.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-9.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-alert.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-alert.ico
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-active.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-active@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-active@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-unread-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-unread-active.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-unread-active@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-unread-active@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-unread.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-unread@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-unread@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin/tray-unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin/tray-unread.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin/tray-unread@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin/tray-unread@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin/tray.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin/tray@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin/tray@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/linux/tray-unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/linux/tray-unread.png
--------------------------------------------------------------------------------
/src/assets/images/tray/linux/tray-unread@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/linux/tray-unread@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/linux/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/linux/tray.png
--------------------------------------------------------------------------------
/src/assets/images/tray/linux/tray@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/linux/tray@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/win32/tray-unread.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/win32/tray-unread.ico
--------------------------------------------------------------------------------
/src/assets/images/tray/win32/tray.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/win32/tray.ico
--------------------------------------------------------------------------------
/src/components/services/content/ErrorHandlers/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | component: {
3 | left: 0,
4 | position: 'absolute',
5 | top: 0,
6 | width: '100%',
7 | zIndex: 0,
8 | alignItems: 'center',
9 | background: theme.colorWebviewErrorHandlerBackground,
10 | display: 'flex',
11 | flexDirection: 'column',
12 | justifyContent: 'center',
13 | textAlign: 'center',
14 | },
15 | buttonContainer: {
16 | display: 'flex',
17 | flexDirection: 'row',
18 | height: 'auto',
19 | margin: [40, 0, 20],
20 |
21 | '& button': {
22 | margin: [0, 10, 0, 10],
23 | },
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/services/content/ServiceDisabled.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import { defineMessages, intlShape } from 'react-intl';
5 |
6 | import Button from '../../ui/Button';
7 |
8 | const messages = defineMessages({
9 | headline: {
10 | id: 'service.disabledHandler.headline',
11 | defaultMessage: '!!!{name} is disabled',
12 | },
13 | action: {
14 | id: 'service.disabledHandler.action',
15 | defaultMessage: '!!!Enable {name}',
16 | },
17 | });
18 |
19 | export default @observer class ServiceDisabled extends Component {
20 | static propTypes = {
21 | name: PropTypes.string.isRequired,
22 | enable: PropTypes.func.isRequired,
23 | };
24 |
25 | static contextTypes = {
26 | intl: intlShape,
27 | };
28 |
29 | countdownInterval = null;
30 |
31 | countdownIntervalTimeout = 1000;
32 |
33 | render() {
34 | const { name, enable } = this.props;
35 | const { intl } = this.context;
36 |
37 | return (
38 |
39 |
{intl.formatMessage(messages.headline, { name })}
40 |
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/settings/recipes/RecipeItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 |
5 | import RecipePreviewModel from '../../../models/RecipePreview';
6 |
7 | export default @observer class RecipeItem extends Component {
8 | static propTypes = {
9 | recipe: PropTypes.instanceOf(RecipePreviewModel).isRequired,
10 | onClick: PropTypes.func.isRequired,
11 | };
12 |
13 | render() {
14 | const { recipe, onClick } = this.props;
15 |
16 | return (
17 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/ui/AppLoader/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | component: {
3 | color: '#FFF',
4 | },
5 | slogan: {
6 | display: 'block',
7 | opacity: 0,
8 | transition: 'opacity 1s ease',
9 | position: 'absolute',
10 | textAlign: 'center',
11 | width: '100%',
12 | },
13 | visible: {
14 | opacity: 1,
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/ui/FeatureItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import injectSheet from 'react-jss';
3 | import { Icon } from '@meetfranz/ui';
4 | import classnames from 'classnames';
5 | import { mdiCheckCircle } from '@mdi/js';
6 |
7 | const styles = theme => ({
8 | featureItem: {
9 | borderBottom: [1, 'solid', theme.defaultContentBorder],
10 | padding: [8, 0],
11 | display: 'flex',
12 | alignItems: 'center',
13 | textAlign: 'left',
14 | },
15 | featureIcon: {
16 | fill: theme.brandSuccess,
17 | marginRight: 10,
18 | },
19 | });
20 |
21 | export const FeatureItem = injectSheet(styles)(({
22 | classes, className, name, icon,
23 | }) => (
24 |
29 | {icon ? (
30 | {icon}
31 | ) : (
32 |
33 | )}
34 | {name}
35 |
36 | ));
37 |
38 | export default FeatureItem;
39 |
--------------------------------------------------------------------------------
/src/components/ui/FullscreenLoader/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import injectSheet, { withTheme } from 'react-jss';
5 | import classnames from 'classnames';
6 |
7 | import Loader from '../Loader';
8 |
9 | import styles from './styles';
10 |
11 | export default @observer @withTheme @injectSheet(styles) class FullscreenLoader extends Component {
12 | static propTypes = {
13 | className: PropTypes.string,
14 | title: PropTypes.string.isRequired,
15 | classes: PropTypes.object.isRequired,
16 | theme: PropTypes.object.isRequired,
17 | spinnerColor: PropTypes.string,
18 | children: PropTypes.node,
19 | };
20 |
21 | static defaultProps = {
22 | className: null,
23 | spinnerColor: null,
24 | children: null,
25 | };
26 |
27 | render() {
28 | const {
29 | classes,
30 | title,
31 | children,
32 | spinnerColor,
33 | className,
34 | theme,
35 | } = this.props;
36 |
37 | return (
38 |
39 |
45 |
{title}
46 |
47 | {children && (
48 |
49 | {children}
50 |
51 | )}
52 |
53 |
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/ui/FullscreenLoader/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | wrapper: {
3 | display: 'flex',
4 | alignItems: 'center',
5 | position: ({ isAbsolutePositioned }) => (isAbsolutePositioned ? 'absolute' : 'relative'),
6 | width: '100%',
7 | },
8 | component: {
9 | width: '100%',
10 | display: 'flex',
11 | flexDirection: 'column',
12 | alignItems: 'center',
13 | textAlign: 'center',
14 | height: 'auto',
15 | },
16 | title: {
17 | fontSize: 35,
18 | },
19 | content: {
20 | marginTop: 20,
21 | width: '100%',
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/ui/Loader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Loader from 'react-loader';
4 |
5 | import { oneOrManyChildElements } from '../../prop-types';
6 |
7 | export default class LoaderComponent extends Component {
8 | static propTypes = {
9 | children: oneOrManyChildElements,
10 | loaded: PropTypes.bool,
11 | className: PropTypes.string,
12 | color: PropTypes.string,
13 | };
14 |
15 | static defaultProps = {
16 | children: null,
17 | loaded: false,
18 | className: '',
19 | color: '#373a3c',
20 | };
21 |
22 | render() {
23 | const {
24 | children,
25 | loaded,
26 | className,
27 | color,
28 | } = this.props;
29 |
30 | return (
31 |
40 | {children}
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/ui/Modal/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | component: {
3 | zIndex: 500,
4 | position: 'absolute',
5 | },
6 | overlay: {
7 | background: theme.colorModalOverlayBackground,
8 | position: 'fixed',
9 | top: 0,
10 | left: 0,
11 | right: 0,
12 | bottom: 0,
13 | display: 'flex',
14 | },
15 | modal: {
16 | background: theme.colorModalBackground,
17 | maxWidth: '90%',
18 | height: 'auto',
19 | margin: 'auto auto',
20 | borderRadius: 6,
21 | boxShadow: '0px 13px 40px 0px rgba(0,0,0,0.2)',
22 | position: 'relative',
23 | },
24 | content: {
25 | padding: 20,
26 | },
27 | close: {
28 | position: 'absolute',
29 | top: 0,
30 | right: 0,
31 | padding: 20,
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/src/components/ui/PremiumFeatureContainer/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | container: {
3 | background: theme.colorSubscriptionContainerBackground,
4 | border: theme.colorSubscriptionContainerBorder,
5 | margin: [0, 0, 20, -20],
6 | padding: 20,
7 | 'border-radius': theme.borderRadius,
8 | pointerEvents: 'none',
9 | height: 'auto',
10 | },
11 | titleContainer: {
12 | display: 'flex',
13 | },
14 | title: {
15 | 'font-weight': 'bold',
16 | color: theme.colorSubscriptionContainerTitle,
17 | },
18 | actionButton: {
19 | background: theme.colorSubscriptionContainerActionButtonBackground,
20 | color: theme.colorSubscriptionContainerActionButtonColor,
21 | 'margin-left': 'auto',
22 | 'border-radius': theme.borderRadiusSmall,
23 | padding: [4, 8],
24 | 'font-size': 12,
25 | pointerEvents: 'initial',
26 | },
27 | content: {
28 | opacity: 0.5,
29 | 'margin-top': 20,
30 | '& > :last-child': {
31 | 'margin-bottom': 0,
32 | },
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/src/components/ui/ServiceIcon.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import injectSheet from 'react-jss';
5 | import classnames from 'classnames';
6 |
7 | import ServiceModel from '../../models/Service';
8 |
9 | const styles = theme => ({
10 | root: {
11 | height: 'auto',
12 | },
13 | icon: {
14 | width: theme.serviceIcon.width,
15 | },
16 | isCustomIcon: {
17 | width: theme.serviceIcon.isCustom.width,
18 | border: theme.serviceIcon.isCustom.border,
19 | borderRadius: theme.serviceIcon.isCustom.borderRadius,
20 | },
21 | isDisabled: {
22 | filter: 'grayscale(100%)',
23 | opacity: '.5',
24 | },
25 | });
26 |
27 | @injectSheet(styles) @observer
28 | class ServiceIcon extends Component {
29 | static propTypes = {
30 | classes: PropTypes.object.isRequired,
31 | service: PropTypes.instanceOf(ServiceModel).isRequired,
32 | className: PropTypes.string,
33 | };
34 |
35 | static defaultProps = {
36 | className: '',
37 | };
38 |
39 | render() {
40 | const {
41 | classes,
42 | className,
43 | service,
44 | } = this.props;
45 |
46 | return (
47 |
53 |

62 |
63 | );
64 | }
65 | }
66 |
67 | export default ServiceIcon;
68 |
--------------------------------------------------------------------------------
/src/components/ui/StatusBarTargetUrl.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import classnames from 'classnames';
5 |
6 | import Appear from './effects/Appear';
7 |
8 | export default @observer class StatusBarTargetUrl extends Component {
9 | static propTypes = {
10 | className: PropTypes.string,
11 | text: PropTypes.string,
12 | };
13 |
14 | static defaultProps = {
15 | className: '',
16 | text: '',
17 | };
18 |
19 | render() {
20 | const {
21 | className,
22 | text,
23 | } = this.props;
24 |
25 | return (
26 |
32 |
33 | {text}
34 |
35 |
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/ui/Tabs/TabItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 |
3 | import { oneOrManyChildElements } from '../../../prop-types';
4 |
5 | export default class TabItem extends Component {
6 | static propTypes = {
7 | children: oneOrManyChildElements.isRequired,
8 | }
9 |
10 | render() {
11 | const { children } = this.props;
12 |
13 | return (
14 | {children}
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/ui/Tabs/index.js:
--------------------------------------------------------------------------------
1 | import Tabs from './Tabs';
2 | import TabItem from './TabItem';
3 |
4 | export default Tabs;
5 |
6 | export { TabItem };
7 |
--------------------------------------------------------------------------------
/src/components/ui/WebviewLoader/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import injectSheet from 'react-jss';
5 | import { defineMessages, intlShape } from 'react-intl';
6 |
7 | import FullscreenLoader from '../FullscreenLoader';
8 | import styles from './styles';
9 |
10 | const messages = defineMessages({
11 | loading: {
12 | id: 'service.webviewLoader.loading',
13 | defaultMessage: '!!!Loading',
14 | },
15 | });
16 |
17 | export default @observer @injectSheet(styles) class WebviewLoader extends Component {
18 | static propTypes = {
19 | name: PropTypes.string.isRequired,
20 | classes: PropTypes.object.isRequired,
21 | };
22 |
23 | static contextTypes = {
24 | intl: intlShape,
25 | };
26 |
27 | render() {
28 | const { classes, name } = this.props;
29 | const { intl } = this.context;
30 | return (
31 |
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/ui/WebviewLoader/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | component: {
3 | background: theme.colorWebviewLoaderBackground,
4 | padding: 20,
5 | width: 'auto',
6 | margin: [0, 'auto'],
7 | borderRadius: 6,
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/src/components/ui/effects/Appear.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-did-mount-set-state */
2 | import React, { Component } from 'react';
3 | import PropTypes from 'prop-types';
4 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
5 |
6 | export default class Appear extends Component {
7 | static propTypes = {
8 | children: PropTypes.any.isRequired, // eslint-disable-line
9 | transitionName: PropTypes.string,
10 | className: PropTypes.string,
11 | };
12 |
13 | static defaultProps = {
14 | transitionName: 'fadeIn',
15 | className: '',
16 | };
17 |
18 | state = {
19 | mounted: false,
20 | };
21 |
22 | componentDidMount() {
23 | this.setState({ mounted: true });
24 | }
25 |
26 | render() {
27 | const {
28 | children,
29 | transitionName,
30 | className,
31 | } = this.props;
32 |
33 | if (!this.state.mounted) {
34 | return null;
35 | }
36 |
37 | return (
38 |
47 | {children}
48 |
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/util/ErrorBoundary/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import injectSheet from 'react-jss';
4 | import { defineMessages, intlShape } from 'react-intl';
5 |
6 | import Button from '../../ui/Button';
7 |
8 | import styles from './styles';
9 |
10 | const messages = defineMessages({
11 | headline: {
12 | id: 'app.errorHandler.headline',
13 | defaultMessage: '!!!Something went wrong.',
14 | },
15 | action: {
16 | id: 'app.errorHandler.action',
17 | defaultMessage: '!!!Reload',
18 | },
19 | });
20 |
21 | export default @injectSheet(styles) class ErrorBoundary extends Component {
22 | state = {
23 | hasError: false,
24 | }
25 |
26 | static propTypes = {
27 | classes: PropTypes.object.isRequired,
28 | children: PropTypes.node.isRequired,
29 | }
30 |
31 | static contextTypes = {
32 | intl: intlShape,
33 | };
34 |
35 | componentDidCatch() {
36 | this.setState({ hasError: true });
37 | }
38 |
39 | render() {
40 | const { classes } = this.props;
41 | const { intl } = this.context;
42 |
43 | if (this.state.hasError) {
44 | return (
45 |
46 |
47 | {intl.formatMessage(messages.headline)}
48 |
49 |
55 | );
56 | }
57 |
58 | return this.props.children;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/util/ErrorBoundary/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | component: {
3 | display: 'flex',
4 | width: '100%',
5 | alignItems: 'center',
6 | justifyContent: 'center',
7 | flexDirection: 'column',
8 | },
9 | title: {
10 | fontSize: 20,
11 | color: theme.colorText,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/src/configVanilla.js:
--------------------------------------------------------------------------------
1 | export const DEFAULT_APP_SETTINGS_VANILLA = {
2 | autoLaunchInBackground: false,
3 | runInBackground: true,
4 | enableSystemTray: true,
5 | minimizeToSystemTray: false,
6 | showDisabledServices: true,
7 | showMessageBadgeWhenMuted: true,
8 | enableSpellchecking: true,
9 | spellcheckerLanguage: 'en-US',
10 | darkMode: process.type === 'renderer' ? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches : false,
11 | locale: '',
12 | fallbackLocale: 'en-US',
13 | beta: false,
14 | isAppMuted: false,
15 | enableGPUAcceleration: true,
16 | serviceLimit: 5,
17 | };
18 |
--------------------------------------------------------------------------------
/src/containers/auth/ImportScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Import from '../../components/auth/Import';
5 | import UserStore from '../../stores/UserStore';
6 |
7 | export default @inject('stores', 'actions') @observer class ImportScreen extends Component {
8 | render() {
9 | const { actions, stores } = this.props;
10 |
11 | if (stores.user.isImportLegacyServicesCompleted) {
12 | stores.router.push(stores.user.inviteRoute);
13 | }
14 |
15 | return (
16 |
22 | );
23 | }
24 | }
25 |
26 | ImportScreen.wrappedComponent.propTypes = {
27 | actions: PropTypes.shape({
28 | user: PropTypes.shape({
29 | importLegacyServices: PropTypes.func.isRequired,
30 | }).isRequired,
31 | }).isRequired,
32 | stores: PropTypes.shape({
33 | user: PropTypes.instanceOf(UserStore).isRequired,
34 | }).isRequired,
35 | };
36 |
--------------------------------------------------------------------------------
/src/containers/auth/InviteScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Invite from '../../components/auth/Invite';
5 |
6 | export default @inject('stores', 'actions') @observer class InviteScreen extends Component {
7 | render() {
8 | const { actions } = this.props;
9 |
10 | return (
11 |
15 | );
16 | }
17 | }
18 |
19 | InviteScreen.wrappedComponent.propTypes = {
20 | actions: PropTypes.shape({
21 | user: PropTypes.shape({
22 | invite: PropTypes.func.isRequired,
23 | }).isRequired,
24 | }).isRequired,
25 | };
26 |
--------------------------------------------------------------------------------
/src/containers/auth/LoginScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Login from '../../components/auth/Login';
5 | import UserStore from '../../stores/UserStore';
6 |
7 | import { globalError as globalErrorPropType } from '../../prop-types';
8 |
9 | export default @inject('stores', 'actions') @observer class LoginScreen extends Component {
10 | static propTypes = {
11 | error: globalErrorPropType.isRequired,
12 | };
13 |
14 | render() {
15 | const { actions, stores, error } = this.props;
16 | return (
17 |
26 | );
27 | }
28 | }
29 |
30 | LoginScreen.wrappedComponent.propTypes = {
31 | actions: PropTypes.shape({
32 | user: PropTypes.shape({
33 | login: PropTypes.func.isRequired,
34 | }).isRequired,
35 | }).isRequired,
36 | stores: PropTypes.shape({
37 | user: PropTypes.instanceOf(UserStore).isRequired,
38 | }).isRequired,
39 | };
40 |
--------------------------------------------------------------------------------
/src/containers/auth/PasswordScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Password from '../../components/auth/Password';
5 | import UserStore from '../../stores/UserStore';
6 |
7 | export default @inject('stores', 'actions') @observer class PasswordScreen extends Component {
8 | render() {
9 | const { actions, stores } = this.props;
10 |
11 | return (
12 |
19 | );
20 | }
21 | }
22 |
23 | PasswordScreen.wrappedComponent.propTypes = {
24 | actions: PropTypes.shape({
25 | user: PropTypes.shape({
26 | retrievePassword: PropTypes.func.isRequired,
27 | }).isRequired,
28 | }).isRequired,
29 | stores: PropTypes.shape({
30 | user: PropTypes.instanceOf(UserStore).isRequired,
31 | }).isRequired,
32 | };
33 |
--------------------------------------------------------------------------------
/src/containers/auth/SignupScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 |
5 | import Signup from '../../components/auth/Signup';
6 | import UserStore from '../../stores/UserStore';
7 | import FeaturesStore from '../../stores/FeaturesStore';
8 |
9 | import { globalError as globalErrorPropType } from '../../prop-types';
10 |
11 | export default @inject('stores', 'actions') @observer class SignupScreen extends Component {
12 | static propTypes = {
13 | error: globalErrorPropType.isRequired,
14 | };
15 |
16 | onSignup(values) {
17 | const { actions, stores } = this.props;
18 |
19 | const { canSkipTrial, defaultTrialPlan, pricingConfig } = stores.features.anonymousFeatures;
20 |
21 | if (!canSkipTrial) {
22 | Object.assign(values, {
23 | plan: defaultTrialPlan,
24 | currency: pricingConfig.currencyID,
25 | });
26 | }
27 |
28 | actions.user.signup(values);
29 | }
30 |
31 | render() {
32 | const { stores, error } = this.props;
33 |
34 | return (
35 | this.onSignup(values)}
37 | isSubmitting={stores.user.signupRequest.isExecuting}
38 | loginRoute={stores.user.loginRoute}
39 | error={error}
40 | />
41 | );
42 | }
43 | }
44 |
45 | SignupScreen.wrappedComponent.propTypes = {
46 | actions: PropTypes.shape({
47 | user: PropTypes.shape({
48 | signup: PropTypes.func.isRequired,
49 | }).isRequired,
50 | }).isRequired,
51 | stores: PropTypes.shape({
52 | user: PropTypes.instanceOf(UserStore).isRequired,
53 | features: PropTypes.instanceOf(FeaturesStore).isRequired,
54 | }).isRequired,
55 | };
56 |
--------------------------------------------------------------------------------
/src/containers/auth/WelcomeScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 |
5 | import Welcome from '../../components/auth/Welcome';
6 | import UserStore from '../../stores/UserStore';
7 | import RecipePreviewsStore from '../../stores/RecipePreviewsStore';
8 |
9 | export default @inject('stores', 'actions') @observer class LoginScreen extends Component {
10 | render() {
11 | const { user, recipePreviews } = this.props.stores;
12 |
13 | return (
14 |
19 | );
20 | }
21 | }
22 |
23 | LoginScreen.wrappedComponent.propTypes = {
24 | stores: PropTypes.shape({
25 | user: PropTypes.instanceOf(UserStore).isRequired,
26 | recipePreviews: PropTypes.instanceOf(RecipePreviewsStore).isRequired,
27 | }).isRequired,
28 | };
29 |
--------------------------------------------------------------------------------
/src/containers/settings/InviteScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 |
5 | import Invite from '../../components/auth/Invite';
6 | import ErrorBoundary from '../../components/util/ErrorBoundary';
7 |
8 | export default @inject('stores', 'actions') @observer class InviteScreen extends Component {
9 | componentWillUnmount() {
10 | this.props.stores.user.inviteRequest.reset();
11 | }
12 |
13 | render() {
14 | const { actions } = this.props;
15 | const { user } = this.props.stores;
16 |
17 | return (
18 |
19 |
25 |
26 | );
27 | }
28 | }
29 |
30 | InviteScreen.wrappedComponent.propTypes = {
31 | actions: PropTypes.shape({
32 | user: PropTypes.shape({
33 | invite: PropTypes.func.isRequired,
34 | }).isRequired,
35 | }).isRequired,
36 | stores: PropTypes.shape({
37 | user: PropTypes.shape({
38 | inviteRequest: PropTypes.object,
39 | }).isRequired,
40 | }).isRequired,
41 | };
42 |
--------------------------------------------------------------------------------
/src/containers/settings/SettingsWindow.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer, inject } from 'mobx-react';
4 |
5 | import ServicesStore from '../../stores/ServicesStore';
6 |
7 | import Layout from '../../components/settings/SettingsLayout';
8 | import Navigation from '../../components/settings/navigation/SettingsNavigation';
9 | import ErrorBoundary from '../../components/util/ErrorBoundary';
10 | import { workspaceStore } from '../../features/workspaces';
11 |
12 | export default @inject('stores', 'actions') @observer class SettingsContainer extends Component {
13 | render() {
14 | const { children, stores } = this.props;
15 | const { closeSettings } = this.props.actions.ui;
16 |
17 |
18 | const navigation = (
19 |
23 | );
24 |
25 | return (
26 |
27 |
31 | {children}
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | SettingsContainer.wrappedComponent.propTypes = {
39 | children: PropTypes.element.isRequired,
40 | stores: PropTypes.shape({
41 | services: PropTypes.instanceOf(ServicesStore).isRequired,
42 | }).isRequired,
43 | actions: PropTypes.shape({
44 | ui: PropTypes.shape({
45 | closeSettings: PropTypes.func.isRequired,
46 | }),
47 | }).isRequired,
48 | };
49 |
--------------------------------------------------------------------------------
/src/containers/subscription/SubscriptionPopupScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import SubscriptionPopup from '../../components/subscription/SubscriptionPopup';
5 | import { isDevMode } from '../../environment';
6 |
7 |
8 | export default class SubscriptionPopupScreen extends Component {
9 | static propTypes = {
10 | params: PropTypes.shape({
11 | url: PropTypes.string.isRequired,
12 | }).isRequired,
13 | }
14 |
15 | state = {
16 | complete: false,
17 | };
18 |
19 | completeCheck(event) {
20 | const { url } = event;
21 |
22 | if ((url.includes('recurly') && url.includes('confirmation')) || ((url.includes('meetfranz') || isDevMode) && url.includes('success'))) {
23 | this.setState({
24 | complete: true,
25 | });
26 | }
27 | }
28 |
29 | render() {
30 | return (
31 | window.close()}
34 | completeCheck={e => this.completeCheck(e)}
35 | isCompleted={this.state.complete}
36 | />
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/dev-app-update.yml:
--------------------------------------------------------------------------------
1 | owner: meetfranz
2 | repo: franz
3 | provider: github
4 |
--------------------------------------------------------------------------------
/src/electron/Settings.js:
--------------------------------------------------------------------------------
1 | import { observable, toJS } from 'mobx';
2 | import { pathExistsSync, outputJsonSync, readJsonSync } from 'fs-extra';
3 | import path from 'path';
4 |
5 | import { SETTINGS_PATH } from '../config';
6 |
7 | const debug = require('debug')('Franz:Settings');
8 |
9 | export default class Settings {
10 | type = '';
11 |
12 | @observable store = {};
13 |
14 | constructor(type, defaultState = {}) {
15 | this.type = type;
16 | this.store = defaultState;
17 | this.defaultState = defaultState;
18 |
19 | if (!pathExistsSync(this.settingsFile)) {
20 | this._writeFile();
21 | } else {
22 | this._hydrate();
23 | }
24 | }
25 |
26 | set(settings) {
27 | this.store = this._merge(settings);
28 |
29 | this._writeFile();
30 | }
31 |
32 | get all() {
33 | return this.store;
34 | }
35 |
36 | get allSerialized() {
37 | return toJS(this.store);
38 | }
39 |
40 | get(key) {
41 | return this.store[key];
42 | }
43 |
44 | _merge(settings) {
45 | return Object.assign(this.defaultState, this.store, settings);
46 | }
47 |
48 | _hydrate() {
49 | this.store = this._merge(readJsonSync(this.settingsFile));
50 | debug('Hydrate store', this.type, toJS(this.store));
51 | }
52 |
53 | _writeFile() {
54 | outputJsonSync(this.settingsFile, this.store, {
55 | spaces: 2,
56 | });
57 | debug('Write settings file', this.type, toJS(this.store));
58 | }
59 |
60 | get settingsFile() {
61 | return path.join(SETTINGS_PATH, `${this.type === 'app' ? 'settings' : this.type}.json`);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/electron/deepLinking.js:
--------------------------------------------------------------------------------
1 | export default function handleDeepLink(window, rawUrl) {
2 | const url = rawUrl.replace('franz://', '');
3 |
4 | if (!url) return;
5 |
6 | window.webContents.send('navigateFromDeepLink', { url });
7 | }
8 |
--------------------------------------------------------------------------------
/src/electron/exception.js:
--------------------------------------------------------------------------------
1 | process.on('uncaughtException', (err) => {
2 | // handle the error safely
3 | console.error(err);
4 | });
5 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/cld.js:
--------------------------------------------------------------------------------
1 | import { loadModule } from 'cld3-asm';
2 | import { ipcMain } from 'electron';
3 |
4 | const debug = require('debug')('Franz:ipcApi:cld');
5 |
6 | export default async () => {
7 | const cldFactory = await loadModule();
8 | const cld = cldFactory.create(0, 1000);
9 | ipcMain.handle('detect-language', async (event, { sample }) => {
10 | try {
11 | const result = cld.findLanguage(sample);
12 | debug('Checking language', result.language);
13 | if (result.is_reliable) {
14 | debug('Language detected reliably, setting spellchecker language to', result.language);
15 |
16 | return result.language;
17 | }
18 | } catch (e) {
19 | console.error(e);
20 | }
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/desktopCapturer.ts:
--------------------------------------------------------------------------------
1 | import { desktopCapturer, ipcMain, webContents } from 'electron';
2 | import { RELAY_DESKTOP_CAPTURER_SOURCES_IPC_KEY, REQUEST_DESKTOP_CAPTURER_SOURCES_IPC_KEY, SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY } from '../../features/desktopCapturer/config';
3 |
4 | const debug = require('debug')('Franz:ipcApi:desktopCapturer');
5 |
6 | export default async () => {
7 | ipcMain.handle(REQUEST_DESKTOP_CAPTURER_SOURCES_IPC_KEY, async () => {
8 | try {
9 | const sources = await desktopCapturer.getSources({
10 | types: ['window', 'screen'],
11 | fetchWindowIcons: true,
12 | thumbnailSize: { width: 1920, height: 1080 },
13 | });
14 | debug('Available sources', sources);
15 | return sources.map((source) => {
16 | const thumbnail = source.thumbnail ? source.thumbnail.toDataURL() : null;
17 | const appIcon = source.appIcon ? source.appIcon.toDataURL() : null;
18 |
19 | return {
20 | id: source.id,
21 | name: source.name,
22 | displayId: source.display_id,
23 | thumbnail,
24 | appIcon,
25 | };
26 | });
27 | } catch (e) {
28 | console.error(e);
29 | }
30 | });
31 |
32 | ipcMain.on(RELAY_DESKTOP_CAPTURER_SOURCES_IPC_KEY, (event, { webContentsId, sourceId }) => {
33 | const contents = webContents.fromId(webContentsId);
34 | contents.send(SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY, { sourceId });
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/focusState.js:
--------------------------------------------------------------------------------
1 | export default (params) => {
2 | params.mainWindow.on('focus', () => {
3 | params.mainWindow.webContents.send('isWindowFocused', true);
4 | });
5 |
6 | params.mainWindow.on('blur', () => {
7 | params.mainWindow.webContents.send('isWindowFocused', false);
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/fullscreen.js:
--------------------------------------------------------------------------------
1 | import { ipcMain } from "electron";
2 | import { TOGGLE_FULL_SCREEN } from "../../ipcChannels";
3 |
4 | export const UPDATE_FULL_SCREEN_STATUS = 'set-full-screen-status';
5 |
6 | export default ({ mainWindow }) => {
7 | ipcMain.on(TOGGLE_FULL_SCREEN, (e) => {
8 | mainWindow.setFullScreen(!mainWindow.isFullScreen());
9 | })
10 |
11 | mainWindow.on('enter-full-screen', () => {
12 | mainWindow.webContents.send(UPDATE_FULL_SCREEN_STATUS, true);
13 | });
14 | mainWindow.on('leave-full-screen', () => {
15 | mainWindow.webContents.send(UPDATE_FULL_SCREEN_STATUS, false);
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/index.js:
--------------------------------------------------------------------------------
1 | import appIndicator from './appIndicator';
2 | import autoUpdate from './autoUpdate';
3 | import browserViewManager from './browserViewManager';
4 | import cld from './cld';
5 | import desktopCapturer from './desktopCapturer';
6 | import focusState from './focusState';
7 | import fullscreenStatus from './fullscreen';
8 | import macOSPermissions from './macOSPermissions';
9 | import overlayWindow from './overlayWindow';
10 | import serviceCache from './serviceCache';
11 | import settings from './settings';
12 | import subscriptionWindow from './subscriptionWindow';
13 |
14 | export default (params) => {
15 | settings(params);
16 | autoUpdate(params);
17 | appIndicator(params);
18 | cld(params);
19 | desktopCapturer();
20 | focusState(params);
21 | fullscreenStatus(params);
22 | subscriptionWindow(params);
23 | serviceCache();
24 | browserViewManager(params);
25 | overlayWindow(params);
26 | macOSPermissions(params);
27 | };
28 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/macOSPermissions.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, ipcMain } from 'electron';
2 | import { isMac } from '../../environment';
3 | import { CHECK_MACOS_PERMISSIONS } from '../../ipcChannels';
4 |
5 | export default ({ mainWindow }: { mainWindow: BrowserWindow }) => {
6 | // workaround to not break app on non macOS systems
7 | if (isMac) {
8 | ipcMain.on(CHECK_MACOS_PERMISSIONS, () => {
9 | // eslint-disable-next-line global-require
10 | const { default: askFormacOSPermissions } = require('../macOSPermissions');
11 | askFormacOSPermissions(mainWindow);
12 | });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/serviceCache.js:
--------------------------------------------------------------------------------
1 | import { ipcMain } from 'electron';
2 |
3 | const debug = require('debug')('Franz:ipcApi:serviceCache');
4 |
5 | export default () => {
6 | ipcMain.handle('clearServiceCache', ({ sender: webContents }) => {
7 | debug('Clearing cache for service');
8 | const { session } = webContents;
9 |
10 | session.flushStorageData();
11 | session.clearStorageData({
12 | storages: ['appcache', 'serviceworkers', 'cachestorage', 'websql', 'indexdb'],
13 | });
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/settings.js:
--------------------------------------------------------------------------------
1 | import { ipcMain } from 'electron';
2 | import { GET_SETTINGS, SEND_SETTINGS } from '../../ipcChannels';
3 |
4 | export default (params) => {
5 | ipcMain.on(GET_SETTINGS, (event, type) => {
6 | event.sender.send(SEND_SETTINGS, {
7 | type,
8 | data: params.settings[type]?.allSerialized,
9 | });
10 | });
11 |
12 | ipcMain.on('updateAppSettings', (event, args) => {
13 | params.settings[args.type].set(args.data);
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/subscriptionWindow.js:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, ipcMain } from 'electron';
2 | import * as remoteMain from '@electron/remote/main';
3 |
4 | const debug = require('debug')('Franz:ipcApi:subscriptionWindow');
5 |
6 | export default async ({ mainWindow }) => {
7 | let subscriptionWindow;
8 | ipcMain.handle('open-inline-subscription-window', async (event, { url }) => {
9 | debug('Opening subscription window with url', url);
10 | try {
11 | const windowBounds = mainWindow.getBounds();
12 |
13 | subscriptionWindow = new BrowserWindow({
14 | parent: mainWindow,
15 | modal: true,
16 | title: '🔒 Franz Supporter License',
17 | width: 800,
18 | height: windowBounds.height - 100,
19 | maxWidth: 800,
20 | minWidth: 600,
21 | webPreferences: {
22 | nodeIntegration: true,
23 | webviewTag: true,
24 | enableRemoteModule: true,
25 | contextIsolation: false,
26 | },
27 | });
28 |
29 | remoteMain.enable(subscriptionWindow.webContents);
30 |
31 | subscriptionWindow.loadURL(`file://${__dirname}/../../index.html#/payment/${encodeURIComponent(url)}`);
32 |
33 | return await new Promise((resolve) => {
34 | subscriptionWindow.on('closed', () => resolve('closed'));
35 | });
36 | // return isDND;
37 | } catch (e) {
38 | console.error(e);
39 | }
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/src/electron/windowUtils.js:
--------------------------------------------------------------------------------
1 | /* eslint import/prefer-default-export: 0 */
2 |
3 | import { screen } from 'electron';
4 |
5 | export function isPositionValid(position) {
6 | const displays = screen.getAllDisplays();
7 | const { x, y } = position;
8 | return displays.some(({
9 | workArea,
10 | }) => x >= workArea.x && x <= workArea.x + workArea.width && y >= workArea.y && y <= workArea.y + workArea.height);
11 | }
12 |
--------------------------------------------------------------------------------
/src/environment.js:
--------------------------------------------------------------------------------
1 | import {
2 | DEV_API,
3 | DEV_API_WEBSITE,
4 | GA_ID_DEV,
5 | GA_ID_PROD,
6 | LIVE_API,
7 | LIVE_API_WEBSITE,
8 | LOCAL_API,
9 | LOCAL_API_WEBSITE,
10 | } from './config';
11 |
12 | const { app } = process.type === 'renderer' ? require('@electron/remote') : require('electron');
13 |
14 | export const isDevMode = !app.isPackaged;
15 | export const useLiveAPI = process.env.LIVE_API;
16 | export const useLocalAPI = process.env.LOCAL_API;
17 |
18 | let { platform } = process;
19 | if (process.env.OS_PLATFORM) {
20 | platform = process.env.OS_PLATFORM;
21 | }
22 |
23 | export const isMac = platform === 'darwin';
24 | export const isWindows = platform === 'win32';
25 | export const isLinux = platform === 'linux';
26 |
27 | export const ctrlKey = isMac ? '⌘' : 'Ctrl';
28 | export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
29 |
30 | let api;
31 | let web;
32 | if (!isDevMode || (isDevMode && useLiveAPI)) {
33 | api = LIVE_API;
34 | web = LIVE_API_WEBSITE;
35 | } else if (isDevMode && useLocalAPI) {
36 | api = LOCAL_API;
37 | web = LOCAL_API_WEBSITE;
38 | } else {
39 | api = DEV_API;
40 | web = DEV_API_WEBSITE;
41 | }
42 |
43 | export const API = api;
44 | export const API_VERSION = 'v1';
45 | export const WEBSITE = web;
46 |
47 | export const GA_ID = !isDevMode ? GA_ID_PROD : GA_ID_DEV;
48 |
--------------------------------------------------------------------------------
/src/features/announcements/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 |
4 | export const announcementActions = createActionsFromDefinitions({
5 | show: {
6 | targetVersion: PropTypes.string,
7 | },
8 | }, PropTypes.checkPropTypes);
9 |
10 | export default announcementActions;
11 |
--------------------------------------------------------------------------------
/src/features/announcements/api.js:
--------------------------------------------------------------------------------
1 | import { app } from '@electron/remote';
2 | import Request from '../../stores/lib/Request';
3 | import { API, API_VERSION } from '../../environment';
4 |
5 | const debug = require('debug')('Franz:feature:announcements:api');
6 |
7 | export const announcementsApi = {
8 | async getCurrentVersion() {
9 | debug('getting current version of electron app');
10 | return Promise.resolve(app.getVersion());
11 | },
12 |
13 | async getChangelog(version) {
14 | debug('fetching release changelog from Github');
15 | const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`;
16 | const request = await window.fetch(url, { method: 'GET' });
17 | if (!request.ok) return null;
18 | const data = await request.json();
19 | return data.body;
20 | },
21 |
22 | async getAnnouncement(version) {
23 | debug('fetching release announcement from api');
24 | const url = `${API}/${API_VERSION}/announcements/${version}`;
25 | const response = await window.fetch(url, { method: 'GET' });
26 | if (!response.ok) return null;
27 | return response.json();
28 | },
29 | };
30 |
31 | export const getCurrentVersionRequest = new Request(announcementsApi, 'getCurrentVersion');
32 | export const getChangelogRequest = new Request(announcementsApi, 'getChangelog');
33 | export const getAnnouncementRequest = new Request(announcementsApi, 'getAnnouncement');
34 |
--------------------------------------------------------------------------------
/src/features/announcements/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import { AnnouncementsStore } from './store';
3 |
4 | const debug = require('debug')('Franz:feature:announcements');
5 |
6 | export const GA_CATEGORY_ANNOUNCEMENTS = 'Announcements';
7 |
8 | export const announcementsStore = new AnnouncementsStore();
9 |
10 | export const ANNOUNCEMENTS_ROUTES = {
11 | TARGET: '/announcements/:id',
12 | };
13 |
14 | export default function initAnnouncements(stores, actions) {
15 | // const { features } = stores;
16 |
17 | // Toggle workspace feature
18 | reaction(
19 | () => (
20 | true
21 | // features.features.isAnnouncementsEnabled
22 | ),
23 | (isEnabled) => {
24 | if (isEnabled) {
25 | debug('Initializing `announcements` feature');
26 | announcementsStore.start(stores, actions);
27 | } else if (announcementsStore.isFeatureActive) {
28 | debug('Disabling `announcements` feature');
29 | announcementsStore.stop();
30 | }
31 | },
32 | {
33 | fireImmediately: true,
34 | },
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/features/basicAuth/Form.js:
--------------------------------------------------------------------------------
1 | import Form from '../../lib/Form';
2 |
3 | export default new Form({
4 | fields: {
5 | user: {
6 | label: 'user',
7 | placeholder: 'Username',
8 | value: '',
9 | },
10 | password: {
11 | label: 'Password',
12 | placeholder: 'Password',
13 | value: '',
14 | type: 'password',
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/features/basicAuth/index.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 |
3 | import BasicAuthComponent from './Component';
4 |
5 | const debug = require('debug')('Franz:feature:basicAuth');
6 |
7 | export default function initialize() {
8 | debug('Initialize basicAuth feature');
9 | }
10 |
11 | export function sendCredentials(user, password) {
12 | debug('Sending credentials to main', user, password);
13 |
14 | ipcRenderer.send('feature-basic-auth-credentials', {
15 | user,
16 | password,
17 | });
18 | }
19 |
20 | export function cancelLogin() {
21 | debug('Cancel basic auth event');
22 |
23 | ipcRenderer.send('feature-basic-auth-cancel');
24 | }
25 |
26 | export const Component = BasicAuthComponent;
27 |
--------------------------------------------------------------------------------
/src/features/basicAuth/mainIpcHandler.js:
--------------------------------------------------------------------------------
1 | const debug = require('debug')('Franz:feature:basicAuth:main');
2 |
3 | export default function mainIpcHandler(mainWindow, authInfo) {
4 | debug('Sending basic auth call', authInfo);
5 |
6 | mainWindow.webContents.send('feature:basic-auth', {
7 | authInfo,
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/src/features/basicAuth/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | container: {
3 | padding: 20,
4 | color: theme.colorText,
5 | },
6 | buttons: {
7 | display: 'flex',
8 | justifyContent: 'space-between',
9 | },
10 | form: {
11 | marginTop: 15,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/src/features/communityRecipes/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import { CommunityRecipesStore } from './store';
3 |
4 | const debug = require('debug')('Franz:feature:communityRecipes');
5 |
6 | export const DEFAULT_SERVICE_LIMIT = 3;
7 |
8 | export const communityRecipesStore = new CommunityRecipesStore();
9 |
10 | export default function initCommunityRecipes(stores, actions) {
11 | const { features } = stores;
12 |
13 | communityRecipesStore.start(stores, actions);
14 |
15 | // Toggle communityRecipe premium status
16 | reaction(
17 | () => (
18 | features.features.isCommunityRecipesIncludedInCurrentPlan
19 | ),
20 | (isPremiumFeature) => {
21 | debug('Community recipes is premium feature: ', isPremiumFeature);
22 | communityRecipesStore.isCommunityRecipesIncludedInCurrentPlan = isPremiumFeature;
23 | },
24 | {
25 | fireImmediately: true,
26 | },
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/features/communityRecipes/store.js:
--------------------------------------------------------------------------------
1 | import { computed, observable } from 'mobx';
2 | import { FeatureStore } from '../utils/FeatureStore';
3 |
4 | const debug = require('debug')('Franz:feature:communityRecipes:store');
5 |
6 | export class CommunityRecipesStore extends FeatureStore {
7 | @observable isCommunityRecipesIncludedInCurrentPlan = false;
8 |
9 | start(stores, actions) {
10 | debug('start');
11 | this.stores = stores;
12 | this.actions = actions;
13 | }
14 |
15 | stop() {
16 | debug('stop');
17 | super.stop();
18 | }
19 |
20 | @computed get communityRecipes() {
21 | if (!this.stores) return [];
22 |
23 | return this.stores.recipePreviews.dev.map((r) => {
24 | r.isDevRecipe = !!r.author.find(a => a.email === this.stores.user.data.email);
25 |
26 | return r;
27 | });
28 | }
29 | }
30 |
31 | export default CommunityRecipesStore;
32 |
--------------------------------------------------------------------------------
/src/features/delayApp/api.js:
--------------------------------------------------------------------------------
1 | // import Request from '../../stores/lib/Request';
2 | import { API, API_VERSION } from '../../environment';
3 | import { sendAuthRequest } from '../../api/utils/auth';
4 | import CachedRequest from '../../stores/lib/CachedRequest';
5 |
6 |
7 | const debug = require('debug')('Franz:feature:delayApp:api');
8 |
9 | export const delayAppApi = {
10 | async getPoweredBy() {
11 | debug('fetching release changelog from Github');
12 | const url = `${API}/${API_VERSION}/poweredby`;
13 | const response = await sendAuthRequest(url, {
14 | method: 'GET',
15 | });
16 |
17 | if (!response.ok) return null;
18 | return response.json();
19 | },
20 | };
21 |
22 | export const getPoweredByRequest = new CachedRequest(delayAppApi, 'getPoweredBy');
23 |
--------------------------------------------------------------------------------
/src/features/delayApp/store.js:
--------------------------------------------------------------------------------
1 | import {
2 | computed,
3 | } from 'mobx';
4 |
5 | import { FeatureStore } from '../utils/FeatureStore';
6 | import { getPoweredByRequest } from './api';
7 |
8 | export class DelayAppStore extends FeatureStore {
9 | @computed get poweredBy() {
10 | return getPoweredByRequest.execute().result || {};
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/features/desktopCapturer/config.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_DESKTOP_CAPTURER_SOURCES_IPC_KEY = 'get-desktop-capturer-sources';
2 | export const RELAY_DESKTOP_CAPTURER_SOURCES_IPC_KEY = 'relay-desktop-capturer-sources';
3 | export const SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY = 'set-desktop-capturer-sources';
4 |
--------------------------------------------------------------------------------
/src/features/desktopCapturer/index.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export { default as Component } from './Component';
4 |
5 | const debug = require('debug')('Franz:feature:desktopCapturer');
6 |
7 | const defaultState = {
8 | isModalVisible: false,
9 | sources: [],
10 | selectedSource: null,
11 | webview: null,
12 | };
13 |
14 | export const state = observable(defaultState);
15 |
16 | export default function initialize() {
17 | debug('Initialize shareFranz feature');
18 |
19 | window.franz.features.desktopCapturer = {
20 | state,
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/src/features/planSelection/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 |
4 | export const planSelectionActions = createActionsFromDefinitions({
5 | downgradeAccount: {},
6 | hideOverlay: {},
7 | }, PropTypes.checkPropTypes);
8 |
9 | export default planSelectionActions;
10 |
--------------------------------------------------------------------------------
/src/features/planSelection/api.js:
--------------------------------------------------------------------------------
1 | import { sendAuthRequest } from '../../api/utils/auth';
2 | import { API, API_VERSION } from '../../environment';
3 | import Request from '../../stores/lib/Request';
4 |
5 | const debug = require('debug')('Franz:feature:planSelection:api');
6 |
7 | export const planSelectionApi = {
8 | downgrade: async () => {
9 | const url = `${API}/${API_VERSION}/payment/downgrade`;
10 | const options = {
11 | method: 'PUT',
12 | };
13 | debug('downgrade UPDATE', url, options);
14 | const result = await sendAuthRequest(url, options);
15 | debug('downgrade RESULT', result);
16 | if (!result.ok) throw result;
17 |
18 | return result.ok;
19 | },
20 | };
21 |
22 | export const downgradeUserRequest = new Request(planSelectionApi, 'downgrade');
23 |
24 | export const resetApiRequests = () => {
25 | downgradeUserRequest.reset();
26 | };
27 |
--------------------------------------------------------------------------------
/src/features/serviceLimit/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import { ServiceLimitStore } from './store';
3 |
4 | const debug = require('debug')('Franz:feature:serviceLimit');
5 |
6 | export const DEFAULT_SERVICE_LIMIT = 3;
7 |
8 | let store = null;
9 |
10 | export const serviceLimitStore = new ServiceLimitStore();
11 |
12 | export default function initServiceLimit(stores, actions) {
13 | const { features } = stores;
14 |
15 | // Toggle serviceLimit feature
16 | reaction(
17 | () => (
18 | features.features.isServiceLimitEnabled
19 | ),
20 | (isEnabled) => {
21 | if (isEnabled) {
22 | debug('Initializing `serviceLimit` feature');
23 | store = serviceLimitStore.start(stores, actions);
24 | } else if (store) {
25 | debug('Disabling `serviceLimit` feature');
26 | serviceLimitStore.stop();
27 | }
28 | },
29 | {
30 | fireImmediately: true,
31 | },
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/features/serviceLimit/store.js:
--------------------------------------------------------------------------------
1 | import { computed, observable } from 'mobx';
2 | import { FeatureStore } from '../utils/FeatureStore';
3 | import { DEFAULT_SERVICE_LIMIT } from '.';
4 |
5 | const debug = require('debug')('Franz:feature:serviceLimit:store');
6 |
7 | export class ServiceLimitStore extends FeatureStore {
8 | @observable isServiceLimitEnabled = false;
9 |
10 | start(stores, actions) {
11 | debug('start');
12 | this.stores = stores;
13 | this.actions = actions;
14 |
15 | this.isServiceLimitEnabled = true;
16 | }
17 |
18 | stop() {
19 | super.stop();
20 |
21 | this.isServiceLimitEnabled = false;
22 | }
23 |
24 | @computed get userHasReachedServiceLimit() {
25 | if (!this.isServiceLimitEnabled) return false;
26 |
27 | return this.serviceLimit !== 0 && this.serviceCount >= this.serviceLimit;
28 | }
29 |
30 | @computed get serviceLimit() {
31 | if (!this.isServiceLimitEnabled || this.stores.features.features.serviceLimitCount === 0) return 0;
32 |
33 | return this.stores.features.features.serviceLimitCount || DEFAULT_SERVICE_LIMIT;
34 | }
35 |
36 | @computed get serviceCount() {
37 | return this.stores.services.all.length;
38 | }
39 | }
40 |
41 | export default ServiceLimitStore;
42 |
--------------------------------------------------------------------------------
/src/features/spellchecker/index.js:
--------------------------------------------------------------------------------
1 | import { autorun, observable } from 'mobx';
2 |
3 | import { DEFAULT_FEATURES_CONFIG } from '../../config';
4 |
5 | const debug = require('debug')('Franz:feature:spellchecker');
6 |
7 | export const config = observable({
8 | isIncludedInCurrentPlan: DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan,
9 | });
10 |
11 | export default function init(stores) {
12 | debug('Initializing `spellchecker` feature');
13 |
14 | autorun(() => {
15 | const { isSpellcheckerIncludedInCurrentPlan } = stores.features.features;
16 |
17 | config.isIncludedInCurrentPlan = isSpellcheckerIncludedInCurrentPlan !== undefined ? isSpellcheckerIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan;
18 |
19 | if (!stores.user.data.isPremium && !config.isIncludedInCurrentPlan && stores.settings.app.enableSpellchecking) {
20 | debug('Override settings.spellcheckerEnabled flag to false');
21 |
22 | Object.assign(stores.settings.app, {
23 | enableSpellchecking: false,
24 | });
25 | }
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/features/todos/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 |
4 | export const todoActions = createActionsFromDefinitions({
5 | resize: {
6 | width: PropTypes.number.isRequired,
7 | },
8 | toggleTodosPanel: {},
9 | toggleTodosFeatureVisibility: {},
10 | setTodosWebview: {
11 | webview: PropTypes.instanceOf(Element).isRequired,
12 | },
13 | handleHostMessage: {
14 | action: PropTypes.string.isRequired,
15 | data: PropTypes.object,
16 | },
17 | handleClientMessage: {
18 | channel: PropTypes.string.isRequired,
19 | message: PropTypes.shape({
20 | action: PropTypes.string.isRequired,
21 | data: PropTypes.object,
22 | }),
23 | },
24 | toggleDevTools: {},
25 | reload: {},
26 | }, PropTypes.checkPropTypes);
27 |
28 | export default todoActions;
29 |
--------------------------------------------------------------------------------
/src/features/todos/constants.js:
--------------------------------------------------------------------------------
1 | export const IPC = {
2 | TODOS_HOST_CHANNEL: 'TODOS_HOST_CHANNEL',
3 | TODOS_CLIENT_CHANNEL: 'TODOS_CLIENT_CHANNEL',
4 | };
5 |
--------------------------------------------------------------------------------
/src/features/todos/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import { TODOS_RECIPE_ID as TODOS_RECIPE } from '../../config';
3 | import TodoStore from './store';
4 |
5 | const debug = require('debug')('Franz:feature:todos');
6 |
7 | export const GA_CATEGORY_TODOS = 'Todos';
8 |
9 | export const DEFAULT_TODOS_WIDTH = 300;
10 | export const TODOS_MIN_WIDTH = 200;
11 | export const DEFAULT_TODOS_VISIBLE = true;
12 | export const DEFAULT_IS_FEATURE_ENABLED_BY_USER = true;
13 | export const TODOS_RECIPE_ID = TODOS_RECIPE;
14 | export const TODOS_PARTITION_ID = 'persist:todos';
15 |
16 | export const TODOS_ROUTES = {
17 | TARGET: '/todos',
18 | };
19 |
20 | export const todosStore = new TodoStore();
21 |
22 | export default function initTodos(stores, actions) {
23 | stores.todos = todosStore;
24 | const { features } = stores;
25 |
26 | reaction(
27 | () => stores.recipes.hasFinishedLoading,
28 | (hasFinishedLoading) => {
29 | if (hasFinishedLoading) {
30 | if (!stores.recipes.isInstalled(TODOS_RECIPE_ID)) {
31 | console.log('Todos recipe is not installed, installing now...');
32 | actions.recipe.install({ recipeId: TODOS_RECIPE_ID });
33 | }
34 | }
35 | },
36 | {
37 | fireImmediately: true,
38 | },
39 | );
40 |
41 | // Toggle todos feature
42 | reaction(
43 | () => features.features.isTodosEnabled,
44 | (isEnabled) => {
45 | if (isEnabled) {
46 | debug('Initializing `todos` feature');
47 | todosStore.start(stores, actions);
48 | } else if (todosStore.isFeatureActive) {
49 | debug('Disabling `todos` feature');
50 | todosStore.stop();
51 | }
52 | },
53 | {
54 | fireImmediately: true,
55 | },
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/features/todos/preload.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | // import { DEFAULT_WEB_CONTENTS_ID } from '../../config';
3 | import { IPC } from './constants';
4 |
5 | const debug = require('debug')('Franz:feature:todos:preload');
6 |
7 | debug('Preloading Todos Webview');
8 |
9 | let hostMessageListener = ({ action }) => {
10 | switch (action) {
11 | case 'todos:initialize-as-service': ipcRenderer.send('hello'); break;
12 | default:
13 | }
14 | };
15 |
16 | ipcRenderer.send('hello');
17 |
18 | // ipcRenderer.on('initialize-recipe', () => {
19 | // // ipcRenderer.sendTo(1, IPC.TODOS_HOST_CHANNEL, { action: 'todos:initialized' });
20 | // });
21 |
22 | window.franz = {
23 | onInitialize(ipcHostMessageListener) {
24 | hostMessageListener = ipcHostMessageListener;
25 | ipcRenderer.send(IPC.TODOS_CLIENT_CHANNEL, { action: 'todos:initialized' });
26 | },
27 | sendToHost(message) {
28 | console.log('send to host', message);
29 | ipcRenderer.send(IPC.TODOS_CLIENT_CHANNEL, message);
30 | },
31 | };
32 |
33 | ipcRenderer.on(IPC.TODOS_HOST_CHANNEL, (event, message) => {
34 | debug('Received host message', event, message);
35 | hostMessageListener(message);
36 | });
37 |
--------------------------------------------------------------------------------
/src/features/trialStatusBar/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 |
4 | export const trialStatusBarActions = createActionsFromDefinitions({
5 | upgradeAccount: {
6 | planId: PropTypes.string.isRequired,
7 | onCloseWindow: PropTypes.func.isRequired,
8 | },
9 | downgradeAccount: {},
10 | hideOverlay: {},
11 | }, PropTypes.checkPropTypes);
12 |
13 | export default trialStatusBarActions;
14 |
--------------------------------------------------------------------------------
/src/features/trialStatusBar/components/ProgressBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import injectSheet from 'react-jss';
5 |
6 | const styles = theme => ({
7 | root: {
8 | background: theme.trialStatusBar.progressBar.background,
9 | width: '25%',
10 | maxWidth: 200,
11 | height: 8,
12 | display: 'flex',
13 | alignItems: 'center',
14 | borderRadius: theme.borderRadius,
15 | overflow: 'hidden',
16 | },
17 | progress: {
18 | background: theme.trialStatusBar.progressBar.progressIndicator,
19 | width: ({ percent }) => `${percent}%`,
20 | height: '100%',
21 | },
22 | });
23 |
24 | @injectSheet(styles) @observer
25 | class ProgressBar extends Component {
26 | static propTypes = {
27 | classes: PropTypes.object.isRequired,
28 | };
29 |
30 | render() {
31 | const {
32 | classes,
33 | } = this.props;
34 |
35 | return (
36 |
41 | );
42 | }
43 | }
44 |
45 | export default ProgressBar;
46 |
--------------------------------------------------------------------------------
/src/features/trialStatusBar/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import TrialStatusBarStore from './store';
3 |
4 | const debug = require('debug')('Franz:feature:trialStatusBar');
5 |
6 | export const GA_CATEGORY_TRIAL_STATUS_BAR = 'trialStatusBar';
7 |
8 | export const trialStatusBarStore = new TrialStatusBarStore();
9 |
10 | export default function initTrialStatusBar(stores, actions) {
11 | stores.trialStatusBar = trialStatusBarStore;
12 | const { features } = stores;
13 |
14 | // Toggle trialStatusBar feature
15 | reaction(
16 | () => features.features.isTrialStatusBarEnabled,
17 | (isEnabled) => {
18 | if (isEnabled) {
19 | debug('Initializing `trialStatusBar` feature');
20 | trialStatusBarStore.start(stores, actions);
21 | } else if (trialStatusBarStore.isFeatureActive) {
22 | debug('Disabling `trialStatusBar` feature');
23 | trialStatusBarStore.stop();
24 | }
25 | },
26 | {
27 | fireImmediately: true,
28 | },
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/features/utils/ActionBinding.js:
--------------------------------------------------------------------------------
1 | export default class ActionBinding {
2 | action;
3 |
4 | isActive = false;
5 |
6 | constructor(action) {
7 | this.action = action;
8 | }
9 |
10 | start() {
11 | if (!this.isActive) {
12 | const { action } = this;
13 | action[0].listen(action[1]);
14 | this.isActive = true;
15 | }
16 | }
17 |
18 | stop() {
19 | if (this.isActive) {
20 | const { action } = this;
21 | action[0].off(action[1]);
22 | this.isActive = false;
23 | }
24 | }
25 | }
26 |
27 | export const createActionBindings = actions => (
28 | actions.map(a => new ActionBinding(a))
29 | );
30 |
--------------------------------------------------------------------------------
/src/features/utils/FeatureStore.js:
--------------------------------------------------------------------------------
1 | export class FeatureStore {
2 | _actions = [];
3 |
4 | _reactions = [];
5 |
6 | stop() {
7 | this._stopActions();
8 | this._stopReactions();
9 | }
10 |
11 | // ACTIONS
12 |
13 | _registerActions(actions) {
14 | this._actions = actions;
15 | this._startActions();
16 | }
17 |
18 | _startActions(actions = this._actions) {
19 | actions.forEach(a => a.start());
20 | }
21 |
22 | _stopActions(actions = this._actions) {
23 | actions.forEach(a => a.stop());
24 | }
25 |
26 | // REACTIONS
27 |
28 | _registerReactions(reactions) {
29 | this._reactions = reactions;
30 | this._startReactions();
31 | }
32 |
33 | _startReactions(reactions = this._reactions) {
34 | reactions.forEach(r => r.start());
35 | }
36 |
37 | _stopReactions(reactions = this._reactions) {
38 | reactions.forEach(r => r.stop());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/features/webControls/constants.js:
--------------------------------------------------------------------------------
1 | export const CUSTOM_WEBSITE_ID = 'franz-custom-website';
2 |
--------------------------------------------------------------------------------
/src/features/workspaces/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Workspace from './models/Workspace';
3 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
4 |
5 | export const workspaceActions = createActionsFromDefinitions({
6 | edit: {
7 | workspace: PropTypes.instanceOf(Workspace).isRequired,
8 | },
9 | create: {
10 | name: PropTypes.string.isRequired,
11 | },
12 | delete: {
13 | workspace: PropTypes.instanceOf(Workspace).isRequired,
14 | },
15 | update: {
16 | workspace: PropTypes.instanceOf(Workspace).isRequired,
17 | },
18 | activate: {
19 | workspace: PropTypes.instanceOf(Workspace).isRequired,
20 | },
21 | deactivate: {},
22 | toggleWorkspaceDrawer: {},
23 | openWorkspaceSettings: {},
24 | toggleKeepAllWorkspacesLoadedSetting: {},
25 | }, PropTypes.checkPropTypes);
26 |
27 | export default workspaceActions;
28 |
--------------------------------------------------------------------------------
/src/features/workspaces/components/WorkspaceItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { intlShape } from 'react-intl';
4 | import { observer } from 'mobx-react';
5 | import injectSheet from 'react-jss';
6 |
7 | import Workspace from '../models/Workspace';
8 |
9 | const styles = theme => ({
10 | row: {
11 | height: theme.workspaces.settings.listItems.height,
12 | borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
13 | '&:hover': {
14 | background: theme.workspaces.settings.listItems.hoverBgColor,
15 | },
16 | },
17 | columnName: {},
18 | });
19 |
20 | @injectSheet(styles) @observer
21 | class WorkspaceItem extends Component {
22 | static propTypes = {
23 | classes: PropTypes.object.isRequired,
24 | workspace: PropTypes.instanceOf(Workspace).isRequired,
25 | onItemClick: PropTypes.func.isRequired,
26 | };
27 |
28 | static contextTypes = {
29 | intl: intlShape,
30 | };
31 |
32 | render() {
33 | const { classes, workspace, onItemClick } = this.props;
34 |
35 | return (
36 |
37 | onItemClick(workspace)}>
38 | {workspace.name}
39 | |
40 |
41 | );
42 | }
43 | }
44 |
45 | export default WorkspaceItem;
46 |
--------------------------------------------------------------------------------
/src/features/workspaces/containers/WorkspacesScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { inject, observer } from 'mobx-react';
3 | import PropTypes from 'prop-types';
4 | import WorkspacesDashboard from '../components/WorkspacesDashboard';
5 | import ErrorBoundary from '../../../components/util/ErrorBoundary';
6 | import { workspaceStore } from '../index';
7 | import {
8 | createWorkspaceRequest,
9 | deleteWorkspaceRequest,
10 | getUserWorkspacesRequest,
11 | updateWorkspaceRequest,
12 | } from '../api';
13 |
14 | @inject('stores', 'actions') @observer
15 | class WorkspacesScreen extends Component {
16 | static propTypes = {
17 | actions: PropTypes.shape({
18 | workspace: PropTypes.shape({
19 | edit: PropTypes.func.isRequired,
20 | }),
21 | }).isRequired,
22 | };
23 |
24 | render() {
25 | const { actions } = this.props;
26 | return (
27 |
28 | actions.workspaces.create(data)}
35 | onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })}
36 | />
37 |
38 | );
39 | }
40 | }
41 |
42 | export default WorkspacesScreen;
43 |
--------------------------------------------------------------------------------
/src/features/workspaces/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import WorkspacesStore from './store';
3 | import { resetApiRequests } from './api';
4 |
5 | const debug = require('debug')('Franz:feature:workspaces');
6 |
7 | export const GA_CATEGORY_WORKSPACES = 'Workspaces';
8 | export const DEFAULT_SETTING_KEEP_ALL_WORKSPACES_LOADED = false;
9 |
10 | export const workspaceStore = new WorkspacesStore();
11 |
12 | export default function initWorkspaces(stores, actions) {
13 | stores.workspaces = workspaceStore;
14 | const { features } = stores;
15 |
16 | // Toggle workspace feature
17 | reaction(
18 | () => features.features.isWorkspaceEnabled,
19 | (isEnabled) => {
20 | if (isEnabled && !workspaceStore.isFeatureActive) {
21 | debug('Initializing `workspaces` feature');
22 | workspaceStore.start(stores, actions);
23 | } else if (workspaceStore.isFeatureActive) {
24 | debug('Disabling `workspaces` feature');
25 | workspaceStore.stop();
26 | resetApiRequests();
27 | }
28 | },
29 | {
30 | fireImmediately: true,
31 | },
32 | );
33 | }
34 |
35 | export const WORKSPACES_ROUTES = {
36 | ROOT: '/settings/workspaces',
37 | EDIT: '/settings/workspaces/:action/:id',
38 | };
39 |
--------------------------------------------------------------------------------
/src/features/workspaces/models/Workspace.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export default class Workspace {
4 | id = null;
5 |
6 | @observable name = null;
7 |
8 | @observable order = null;
9 |
10 | @observable services = [];
11 |
12 | @observable userId = null;
13 |
14 | @observable isActive = false
15 |
16 | constructor(data) {
17 | if (!data.id) {
18 | throw Error('Workspace requires Id');
19 | }
20 |
21 | this.id = data.id;
22 | this.name = data.name;
23 | this.order = data.order;
24 | this.services.replace(data.services);
25 | this.userId = data.userId;
26 | this.isActive = data.isActive ?? false;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/helpers/array-helpers.js:
--------------------------------------------------------------------------------
1 | export const shuffleArray = arr => arr
2 | .map(a => [Math.random(), a])
3 | .sort((a, b) => a[0] - b[0])
4 | .map(a => a[1]);
5 |
--------------------------------------------------------------------------------
/src/helpers/asar-helpers.js:
--------------------------------------------------------------------------------
1 | export function asarPath(dir = '') {
2 | return dir.replace('app.asar', 'app.asar.unpacked');
3 | }
4 |
--------------------------------------------------------------------------------
/src/helpers/async-helpers.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 |
3 | export function sleep(ms = 0) {
4 | return new Promise(r => setTimeout(r, ms));
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/i18n-helpers.js:
--------------------------------------------------------------------------------
1 | export function getLocale({
2 | locale, locales, defaultLocale, fallbackLocale,
3 | }) {
4 | let localeStr = locale;
5 | if (locales[locale] === undefined) {
6 | let localeFuzzy;
7 | Object.keys(locales).forEach((localStr) => {
8 | if (locales && Object.hasOwnProperty.call(locales, localStr)) {
9 | if (locale.substring(0, 2) === localStr.substring(0, 2)) {
10 | localeFuzzy = localStr;
11 | }
12 | }
13 | });
14 |
15 | if (localeFuzzy !== undefined) {
16 | localeStr = localeFuzzy;
17 | }
18 | }
19 |
20 | if (locales[localeStr] === undefined) {
21 | localeStr = defaultLocale;
22 | }
23 |
24 | if (!localeStr) {
25 | localeStr = fallbackLocale;
26 | }
27 |
28 | return localeStr;
29 | }
30 |
31 | export function getSelectOptions({ locales, resetToDefaultText = '', automaticDetectionText = '' }) {
32 | const options = [];
33 |
34 | if (resetToDefaultText) {
35 | options.push(
36 | {
37 | value: '',
38 | label: resetToDefaultText,
39 | },
40 | );
41 | }
42 |
43 | if (automaticDetectionText) {
44 | options.push(
45 | {
46 | value: 'automatic',
47 | label: automaticDetectionText,
48 | },
49 | );
50 | }
51 |
52 | options.push({
53 | value: '───',
54 | label: '───',
55 | disabled: true,
56 | });
57 |
58 | Object.keys(locales).sort(Intl.Collator().compare).forEach((key) => {
59 | options.push({
60 | value: key,
61 | label: locales[key],
62 | });
63 | });
64 |
65 | return options;
66 | }
67 |
--------------------------------------------------------------------------------
/src/helpers/password-helpers.js:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto';
2 |
3 | export function hash(password) {
4 | return crypto.createHash('sha256').update(password).digest('base64');
5 | }
6 |
7 | export function scorePassword(password) {
8 | let score = 0;
9 | if (!password) {
10 | return score;
11 | }
12 |
13 | // award every unique letter until 5 repetitions
14 | const letters = {};
15 | for (let i = 0; i < password.length; i += 1) {
16 | letters[password[i]] = (letters[password[i]] || 0) + 1;
17 | score += 5.0 / letters[password[i]];
18 | }
19 |
20 | // bonus points for mixing it up
21 | const variations = {
22 | digits: /\d/.test(password),
23 | lower: /[a-z]/.test(password),
24 | upper: /[A-Z]/.test(password),
25 | nonWords: /\W/.test(password),
26 | };
27 |
28 | let variationCount = 0;
29 | Object.keys(variations).forEach((key) => {
30 | variationCount += (variations[key] === true) ? 1 : 0;
31 | });
32 |
33 | score += (variationCount - 1) * 10;
34 |
35 | return parseInt(score, 10);
36 | }
37 |
--------------------------------------------------------------------------------
/src/helpers/plan-helpers.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 | import { PLANS_MAPPING, PLANS } from '../config';
3 |
4 | const messages = defineMessages({
5 | [PLANS.PRO]: {
6 | id: 'pricing.plan.pro',
7 | defaultMessage: '!!!Professional',
8 | },
9 | [PLANS.PERSONAL]: {
10 | id: 'pricing.plan.personal',
11 | defaultMessage: '!!!Personal',
12 | },
13 | [PLANS.FREE]: {
14 | id: 'pricing.plan.free',
15 | defaultMessage: '!!!Free',
16 | },
17 | [PLANS.LEGACY]: {
18 | id: 'pricing.plan.legacy',
19 | defaultMessage: '!!!Premium',
20 | },
21 | });
22 |
23 | export function cleanupPlanId(id) {
24 | return id.replace(/(.*)-x[0-9]/, '$1');
25 | }
26 |
27 | export function i18nPlanName(planId, intl) {
28 | if (!planId) {
29 | throw new Error('planId is required');
30 | }
31 |
32 | if (!intl) {
33 | throw new Error('intl context is required');
34 | }
35 |
36 | const id = cleanupPlanId(planId);
37 |
38 | const plan = PLANS_MAPPING[id];
39 |
40 | return intl.formatMessage(messages[plan]);
41 | }
42 |
43 | export function getPlan(planId) {
44 | if (!planId) {
45 | throw new Error('planId is required');
46 | }
47 |
48 | const id = cleanupPlanId(planId);
49 |
50 | const plan = PLANS_MAPPING[id];
51 |
52 | return plan;
53 | }
54 |
--------------------------------------------------------------------------------
/src/helpers/recipe-helpers.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | const { app } = process.type === 'renderer' ? require('@electron/remote') : require('electron');
4 |
5 | export function getRecipeDirectory(id = '') {
6 | return path.join(app.getPath('userData'), 'recipes', id);
7 | }
8 |
9 | export function getDevRecipeDirectory(id = '') {
10 | return path.join(app.getPath('userData'), 'recipes', 'dev', id);
11 | }
12 |
13 | export function loadRecipeConfig(recipeId) {
14 | try {
15 | const configPath = `${recipeId}/package.json`;
16 | // Delete module from cache
17 | delete require.cache[require.resolve(configPath)];
18 |
19 | // eslint-disable-next-line
20 | let config = require(configPath);
21 |
22 | const moduleConfigPath = require.resolve(configPath);
23 | const paths = path.parse(moduleConfigPath);
24 | config.path = paths.dir;
25 |
26 | return config;
27 | } catch (e) {
28 | console.error(e);
29 | return null;
30 | }
31 | }
32 |
33 | module.paths.unshift(
34 | getDevRecipeDirectory(),
35 | getRecipeDirectory(),
36 | );
37 |
--------------------------------------------------------------------------------
/src/helpers/routing-helpers.js:
--------------------------------------------------------------------------------
1 | import RouteParser from 'route-parser';
2 |
3 | // eslint-disable-next-line
4 | export const matchRoute = (pattern, path) => new RouteParser(pattern).match(path);
5 |
--------------------------------------------------------------------------------
/src/helpers/service-helpers.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { app } from '@electron/remote';
3 | import fs from 'fs-extra';
4 |
5 | export function getServicePartitionsDirectory() {
6 | return path.join(app.getPath('userData'), 'Partitions');
7 | }
8 |
9 | export function removeServicePartitionDirectory(id = '', addServicePrefix = false) {
10 | const servicePartition = path.join(getServicePartitionsDirectory(), `${addServicePrefix ? 'service-' : ''}${id}`);
11 |
12 | return fs.remove(servicePartition);
13 | }
14 |
15 | export async function getServiceIdsFromPartitions() {
16 | const files = await fs.readdir(getServicePartitionsDirectory());
17 | return files.filter(n => n !== '__chrome_extension');
18 | }
19 |
--------------------------------------------------------------------------------
/src/helpers/url-helpers.js:
--------------------------------------------------------------------------------
1 | import { URL } from 'url';
2 |
3 | import { ALLOWED_PROTOCOLS } from '../config';
4 |
5 | const debug = require('debug')('Franz:Helpers:url');
6 |
7 | export function isValidExternalURL(url) {
8 | const parsedUrl = new URL(url);
9 |
10 | const isAllowed = ALLOWED_PROTOCOLS.includes(parsedUrl.protocol);
11 |
12 | debug('protocol check is', isAllowed, 'for:', url);
13 |
14 | return isAllowed;
15 | }
16 |
--------------------------------------------------------------------------------
/src/helpers/userAgent-helpers.js:
--------------------------------------------------------------------------------
1 | import { isMac, isWindows } from '../environment';
2 |
3 | function macOS() {
4 | // used fixed version (https://bugzilla.mozilla.org/show_bug.cgi?id=1679929)
5 | return 'Macintosh; Intel Mac OS X 10_15_7';
6 | }
7 |
8 | function windows() {
9 | return 'Windows NT 10.0; Win64; x64';
10 | }
11 |
12 | function linux() {
13 | return 'X11; Ubuntu; Linux x86_64';
14 | }
15 |
16 | export default function userAgent(removeChromeVersion = false) {
17 | let platformString = '';
18 |
19 | if (isMac) {
20 | platformString = macOS();
21 | } else if (isWindows) {
22 | platformString = windows();
23 | } else {
24 | platformString = linux();
25 | }
26 |
27 | // TODO: Update AppleWebKit and Safari version after electron update
28 | return `Mozilla/5.0 (${platformString}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome${!removeChromeVersion ? `/${process.versions.chrome}` : ''} Safari/537.36`;
29 | // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36
30 | }
31 |
--------------------------------------------------------------------------------
/src/helpers/visibility-helper.js:
--------------------------------------------------------------------------------
1 | export function onVisibilityChange(cb) {
2 | let isVisible = true;
3 |
4 | if (!cb) {
5 | throw new Error('no callback given');
6 | }
7 |
8 | function focused() {
9 | if (!isVisible) {
10 | cb(isVisible = true);
11 | }
12 | }
13 |
14 | function unfocused() {
15 | if (isVisible) {
16 | cb(isVisible = false);
17 | }
18 | }
19 |
20 | document.addEventListener('visibilitychange', () => { (document.hidden ? unfocused : focused)(); });
21 |
22 | window.onpageshow = focused;
23 | window.onfocus = focused;
24 |
25 | window.onpagehid = unfocused;
26 | window.onblur = unfocused;
27 | }
28 |
--------------------------------------------------------------------------------
/src/i18n/globalMessages.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | export default defineMessages({
4 | APIUnhealthy: {
5 | id: 'global.api.unhealthy',
6 | defaultMessage: '!!!Can\'t connect to Franz Online Services',
7 | },
8 | notConnectedToTheInternet: {
9 | id: 'global.notConnectedToTheInternet',
10 | defaultMessage: '!!!You are not connected to the internet.',
11 | },
12 | spellcheckerLanguage: {
13 | id: 'global.spellchecking.language',
14 | defaultMessage: '!!!Spell checking language',
15 | },
16 | spellcheckerSystemDefault: {
17 | id: 'global.spellchecker.useDefault',
18 | defaultMessage: '!!!Use System Default ({default})',
19 | },
20 | spellcheckerAutomaticDetection: {
21 | id: 'global.spellchecking.autodetect',
22 | defaultMessage: '!!!Detect language automatically',
23 | },
24 | spellcheckerAutomaticDetectionShort: {
25 | id: 'global.spellchecking.autodetect.short',
26 | defaultMessage: '!!!Automatic',
27 | },
28 | proRequired: {
29 | id: 'global.franzProRequired',
30 | defaultMessage: '!!!Franz Professional Required',
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/src/i18n/locales/whitelist_en-US.json:
--------------------------------------------------------------------------------
1 | [
2 | ]
--------------------------------------------------------------------------------
/src/i18n/manage-translations.js:
--------------------------------------------------------------------------------
1 | require('@babel/register');
2 | const manageTranslations = require('react-intl-translations-manager').default;
3 |
4 | manageTranslations({
5 | messagesDirectory: 'src/i18n/messages',
6 | translationsDirectory: 'src/i18n/locales',
7 | singleMessagesFile: true,
8 | languages: ['en-US'],
9 | });
10 |
--------------------------------------------------------------------------------
/src/i18n/translations.js:
--------------------------------------------------------------------------------
1 | import { APP_LOCALES } from './languages';
2 |
3 | const translations = [];
4 | Object.keys(APP_LOCALES).forEach((key) => {
5 | try {
6 | const translation = require(`./locales/${key}.json`); // eslint-disable-line
7 | translations[key] = translation;
8 | } catch (err) {
9 | console.warn(`Can't find translations for ${key}`);
10 | }
11 | });
12 |
13 | module.exports = translations;
14 |
--------------------------------------------------------------------------------
/src/lib/Form.js:
--------------------------------------------------------------------------------
1 | import Form from 'mobx-react-form';
2 |
3 | export default class DefaultForm extends Form {
4 | bindings() {
5 | return {
6 | default: {
7 | id: 'id',
8 | name: 'name',
9 | type: 'type',
10 | value: 'value',
11 | label: 'label',
12 | placeholder: 'placeholder',
13 | disabled: 'disabled',
14 | onChange: 'onChange',
15 | onFocus: 'onFocus',
16 | onBlur: 'onBlur',
17 | error: 'error',
18 | },
19 | };
20 | }
21 |
22 | options() {
23 | return {
24 | validateOnInit: false, // default: true
25 | // validateOnBlur: true, // default: true
26 | // validateOnChange: true // default: false
27 | // // validationDebounceWait: {
28 | // // trailing: true,
29 | // // },
30 | };
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/lib/analytics.js:
--------------------------------------------------------------------------------
1 | // import { app } from '@electron/remote';
2 | // import ElectronCookies from '@meetfranz/electron-cookies';
3 | // import querystring from 'querystring';
4 |
5 | // import { STATS_API } from '../config';
6 | // import { isDevMode, GA_ID } from '../environment';
7 |
8 | // ElectronCookies.enable({
9 | // origin: 'https://app.meetfranz.com',
10 | // });
11 |
12 | const debug = require('debug')('Franz:Analytics');
13 |
14 | /* eslint-disable */
15 | // var _paq = window._paq = window._paq || [];
16 |
17 | // _paq.push(["setCookieDomain", "app.meetfranz.com"]);
18 | // _paq.push(['setCustomDimension', 1, app.getVersion()]);
19 | // _paq.push(['setDomains', 'app.meetfranz.com']);
20 | // _paq.push(['setCustomUrl', '/']);
21 | // _paq.push(['trackPageView']);
22 |
23 |
24 | // (function() {
25 | // var u="https://analytics.franzinfra.com/";
26 | // _paq.push(['setTrackerUrl', u+'matomo.php']);
27 | // _paq.push(['setSiteId', '1']);
28 | // var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
29 | // g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
30 | // })();
31 | /* eslint-enable */
32 |
33 | export function gaPage(page) {
34 | debug('Track page', page);
35 | // window._paq.push(['setCustomUrl', page]);
36 | // window._paq.push(['trackPageView']);
37 |
38 | // debug('Track page', page);
39 | }
40 |
41 | export function gaEvent(category, action, label) {
42 | debug('Track Event', category, action, label);
43 | // window._paq.push(['trackEvent', category, action, label]);
44 | // debug('Track event', category, action, label);
45 | }
46 |
--------------------------------------------------------------------------------
/src/models/News.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export default class News {
4 | id = '';
5 |
6 | message = '';
7 |
8 | meta = {};
9 |
10 | type = 'primary';
11 |
12 | sticky = false;
13 |
14 | constructor(data) {
15 | if (!data.id) {
16 | throw Error('News requires Id');
17 | }
18 |
19 | this.id = data.id;
20 | this.message = data.message || this.message;
21 | this.meta = data.meta || this.meta;
22 | this.type = data.type || this.type;
23 | this.sticky = data.sticky !== undefined ? data.sticky : this.sticky;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/models/Order.js:
--------------------------------------------------------------------------------
1 | export default class Order {
2 | id = '';
3 |
4 | subscriptionId = '';
5 |
6 | name = '';
7 |
8 | invoiceUrl = '';
9 |
10 | price = '';
11 |
12 | date = '';
13 |
14 | constructor(data) {
15 | this.id = data.id;
16 | this.subscriptionId = data.subscriptionId;
17 | this.name = data.name || this.name;
18 | this.invoiceUrl = data.invoiceUrl || this.invoiceUrl;
19 | this.price = data.price || this.price;
20 | this.date = data.date || this.date;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/models/Plan.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export default class Plan {
4 | month = {
5 | id: '',
6 | price: 0,
7 | }
8 |
9 | year = {
10 | id: '',
11 | price: 0,
12 | }
13 |
14 | constructor(data) {
15 | Object.assign(this, data);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/models/RecipePreview.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export default class RecipePreview {
4 | id = '';
5 |
6 | name = '';
7 |
8 | icon = '';
9 |
10 | // TODO: check if this isn't replaced by `icons`
11 | featured = false;
12 |
13 | constructor(data) {
14 | if (!data.id) {
15 | throw Error('RecipePreview requires Id');
16 | }
17 |
18 | Object.assign(this, data);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/models/User.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export default class User {
4 | id = null;
5 |
6 | @observable email = null;
7 |
8 | @observable firstname = null;
9 |
10 | @observable lastname = null;
11 |
12 | @observable organization = null;
13 |
14 | @observable accountType = null;
15 |
16 | @observable emailIsConfirmed = true;
17 |
18 | // better assume it's confirmed to avoid noise
19 | @observable subscription = {};
20 |
21 | @observable isSubscriptionOwner = false;
22 |
23 | @observable hasSubscription = false;
24 |
25 | @observable hadSubscription = false;
26 |
27 | @observable isPremium = false;
28 |
29 | @observable beta = false;
30 |
31 | @observable donor = {};
32 |
33 | @observable isDonor = false;
34 |
35 | @observable locale = false;
36 |
37 | @observable team = {};
38 |
39 |
40 | constructor(data) {
41 | if (!data.id) {
42 | throw Error('User requires Id');
43 | }
44 |
45 | this.id = data.id;
46 | this.email = data.email || this.email;
47 | this.firstname = data.firstname || this.firstname;
48 | this.lastname = data.lastname || this.lastname;
49 | this.organization = data.organization || this.organization;
50 | this.accountType = data.accountType || this.accountType;
51 | this.isPremium = data.isPremium || this.isPremium;
52 | this.beta = data.beta || this.beta;
53 | this.donor = data.donor || this.donor;
54 | this.isDonor = data.isDonor || this.isDonor;
55 | this.locale = data.locale || this.locale;
56 |
57 | this.isSubscriptionOwner = data.isSubscriptionOwner || this.isSubscriptionOwner;
58 | this.hasSubscription = data.hasSubscription || this.hasSubscription;
59 | this.hadSubscription = data.hadSubscription || this.hadSubscription;
60 |
61 | this.team = data.team || this.team;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/prop-types.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | // eslint-disable-next-line
4 | export const oneOrManyChildElements = PropTypes.oneOfType([
5 | PropTypes.arrayOf(PropTypes.element),
6 | PropTypes.element,
7 | PropTypes.array,
8 | ]);
9 |
10 | export const globalError = PropTypes.shape({
11 | status: PropTypes.number,
12 | message: PropTypes.string,
13 | code: PropTypes.string,
14 | });
15 |
--------------------------------------------------------------------------------
/src/stores/GlobalErrorStore.js:
--------------------------------------------------------------------------------
1 | import { observable, action } from 'mobx';
2 | import Store from './lib/Store';
3 | import Request from './lib/Request';
4 |
5 | export default class GlobalErrorStore extends Store {
6 | @observable error = null;
7 |
8 | @observable response = {};
9 |
10 | constructor(...args) {
11 | super(...args);
12 |
13 | Request.registerHook(this._handleRequests);
14 | }
15 |
16 | _handleRequests = action(async (request) => {
17 | if (request.isError) {
18 | this.error = request.error;
19 |
20 | if (request.error.json) {
21 | try {
22 | this.response = await request.error.json();
23 | } catch (error) {
24 | this.response = {};
25 | }
26 | if (this.error.status === 401) {
27 | this.actions.user.logout({ serverLogout: true });
28 | }
29 | }
30 | }
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/stores/NewsStore.js:
--------------------------------------------------------------------------------
1 | import { computed, observable } from 'mobx';
2 | import { remove } from 'lodash';
3 |
4 | import Store from './lib/Store';
5 | import CachedRequest from './lib/CachedRequest';
6 | import Request from './lib/Request';
7 | import { CHECK_INTERVAL } from '../config';
8 |
9 | export default class NewsStore extends Store {
10 | @observable latestNewsRequest = new CachedRequest(this.api.news, 'latest');
11 |
12 | @observable hideNewsRequest = new Request(this.api.news, 'hide');
13 |
14 | constructor(...args) {
15 | super(...args);
16 |
17 | // Register action handlers
18 | this.actions.news.hide.listen(this._hide.bind(this));
19 | this.actions.user.logout.listen(this._resetNewsRequest.bind(this));
20 | }
21 |
22 | setup() {
23 | // Check for news updates every couple of hours
24 | setInterval(() => {
25 | if (this.latestNewsRequest.wasExecuted && this.stores.user.isLoggedIn) {
26 | this.latestNewsRequest.invalidate({ immediately: true });
27 | }
28 | }, CHECK_INTERVAL);
29 | }
30 |
31 | @computed get latest() {
32 | return this.latestNewsRequest.execute().result || [];
33 | }
34 |
35 | // Actions
36 | _hide({ newsId }) {
37 | this.hideNewsRequest.execute(newsId);
38 |
39 | this.latestNewsRequest.invalidate().patch((result) => {
40 | // TODO: check if we can use mobx.array remove
41 | remove(result, n => n.id === newsId);
42 | });
43 | }
44 |
45 | /**
46 | * Reset the news request when current user logs out so that when another user
47 | * logs in again without an app restart, the request will be fetched again and
48 | * the news will be shown to the user.
49 | *
50 | * @private
51 | */
52 | _resetNewsRequest() {
53 | this.latestNewsRequest.reset();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/stores/RecipePreviewsStore.js:
--------------------------------------------------------------------------------
1 | import { action, computed, observable } from 'mobx';
2 | import { debounce } from 'lodash';
3 | import ms from 'ms';
4 |
5 | import Store from './lib/Store';
6 | import CachedRequest from './lib/CachedRequest';
7 | import Request from './lib/Request';
8 | import { gaEvent } from '../lib/analytics';
9 |
10 | export default class RecipePreviewsStore extends Store {
11 | @observable allRecipePreviewsRequest = new CachedRequest(this.api.recipePreviews, 'all');
12 |
13 | @observable featuredRecipePreviewsRequest = new CachedRequest(this.api.recipePreviews, 'featured');
14 |
15 | @observable searchRecipePreviewsRequest = new Request(this.api.recipePreviews, 'search');
16 |
17 | constructor(...args) {
18 | super(...args);
19 |
20 | // Register action handlers
21 | this.actions.recipePreview.search.listen(this._search.bind(this));
22 | }
23 |
24 | @computed get all() {
25 | return this.allRecipePreviewsRequest.execute().result || [];
26 | }
27 |
28 | @computed get featured() {
29 | return this.featuredRecipePreviewsRequest.execute().result || [];
30 | }
31 |
32 | @computed get searchResults() {
33 | return this.searchRecipePreviewsRequest.result || [];
34 | }
35 |
36 | @computed get dev() {
37 | return this.stores.recipes.all.filter(r => r.local);
38 | }
39 |
40 | // Actions
41 | @action _search({ needle }) {
42 | if (needle !== '') {
43 | this.searchRecipePreviewsRequest.execute(needle);
44 |
45 | this._analyticsSearch(needle);
46 | }
47 | }
48 |
49 | // Helper
50 | _analyticsSearch = debounce((needle) => {
51 | gaEvent('Recipe', 'search', needle);
52 | }, ms('3s'));
53 | }
54 |
--------------------------------------------------------------------------------
/src/stores/lib/Reaction.js:
--------------------------------------------------------------------------------
1 | import { autorun } from 'mobx';
2 |
3 | export default class Reaction {
4 | reaction;
5 |
6 | options;
7 |
8 | isRunning = false;
9 |
10 | dispose;
11 |
12 | constructor(reaction, options = {}) {
13 | this.reaction = reaction;
14 | this.options = options;
15 | }
16 |
17 | start() {
18 | if (!this.isRunning) {
19 | this.dispose = autorun(this.reaction, this.options);
20 | this.isRunning = true;
21 | }
22 | }
23 |
24 | stop() {
25 | if (this.isRunning) {
26 | this.dispose();
27 | this.isRunning = false;
28 | }
29 | }
30 | }
31 |
32 | export const createReactions = reactions => (
33 | reactions.map(r => new Reaction(r))
34 | );
35 |
--------------------------------------------------------------------------------
/src/stores/lib/Store.js:
--------------------------------------------------------------------------------
1 | import { computed, observable } from 'mobx';
2 | import Reaction from './Reaction';
3 |
4 | export default class Store {
5 | stores = {};
6 |
7 | api = {};
8 |
9 | actions = {};
10 |
11 | _reactions = [];
12 |
13 | // status implementation
14 | @observable _status = null;
15 |
16 | @computed get actionStatus() {
17 | return this._status || [];
18 | }
19 |
20 | set actionStatus(status) {
21 | this._status = status;
22 | }
23 |
24 | constructor(stores, api, actions) {
25 | this.stores = stores;
26 | this.api = api;
27 | this.actions = actions;
28 | }
29 |
30 | registerReactions(reactions) {
31 | reactions.forEach((reaction) => {
32 | if (Array.isArray(reaction)) {
33 | this._reactions.push(new Reaction(reaction[0], reaction[1]));
34 | } else {
35 | this._reactions.push(new Reaction(reaction));
36 | }
37 | });
38 | }
39 |
40 | setup() {}
41 |
42 | initialize() {
43 | this.setup();
44 | this._reactions.forEach(reaction => reaction.start());
45 | }
46 |
47 | teardown() {
48 | this._reactions.forEach(reaction => reaction.stop());
49 | }
50 |
51 | resetStatus() {
52 | this._status = null;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/styles/animations.scss:
--------------------------------------------------------------------------------
1 | // FadeIn
2 | .fadeIn-appear { opacity: .01; }
3 |
4 | .fadeIn-appear.fadeIn-appear-active {
5 | opacity: 1;
6 | transition: opacity .5s ease-out;
7 | }
8 |
9 | .fadeIn-enter {
10 | opacity: .01;
11 | transition: opacity .5s ease-out;
12 | }
13 |
14 | .fadeIn-leave { opacity: 1; }
15 |
16 | .fadeIn-leave.fadeIn-leave-active {
17 | opacity: .01;
18 | transition: opacity 300ms ease-in;
19 | }
20 |
21 | // FadeIn Fast
22 | .fadeIn-fast-appear { opacity: .01; }
23 |
24 | .fadeIn-fast-appear.fadeIn-fast-appear-active {
25 | opacity: 1;
26 | transition: opacity .25s ease-out;
27 | }
28 |
29 | .fadeIn-fast-enter {
30 | opacity: .01;
31 | transition: opacity .25s ease-out;
32 | }
33 |
34 | .fadeIn-fast-leave { opacity: 1; }
35 |
36 | .fadeIn-fast-leave.fadeIn-fast-leave-active {
37 | opacity: .01;
38 | transition: opacity .25s ease-in;
39 | }
40 |
41 | // Slide down
42 | .slideDown-appear {
43 | max-height: 0;
44 | overflow-y: hidden;
45 | }
46 |
47 | .slideDown-appear.slideDown-appear-active {
48 | max-height: 500px;
49 | transition: max-height .5s ease-out;
50 | }
51 |
52 | .slideDown-enter {
53 | max-height: 0;
54 | transition: max-height .5s ease-out;
55 | }
56 |
57 | // Slide up
58 | .slideUp-appear {
59 | opacity: 0;
60 | transform: translateY(20px);
61 | }
62 |
63 | .slideUp-appear.slideUp-appear-active {
64 | opacity: 1;
65 | transform: translateY(0px);
66 | transition: all .3s ease-out;
67 | }
68 |
69 | .slideUp-enter {
70 | opacity: 0;
71 | transform: translateY(20px);
72 | transition: all .3s ease-out;
73 | }
74 |
75 | .slideUp-leave { opacity: 1; }
76 |
77 | .slideUp-leave.slideUp-leave-active {
78 | opacity: .01;
79 | transition: opacity 300ms ease-in;
80 | }
81 |
--------------------------------------------------------------------------------
/src/styles/badge.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark .badge {
4 | background: $dark-theme-gray;
5 | border-radius: $theme-border-radius-small;
6 | color: $dark-theme-gray-lightest;
7 |
8 | &.badge--primary,
9 | &.badge--premium {
10 | background: $theme-brand-primary;
11 | color: $dark-theme-gray-lightest;
12 | }
13 | }
14 |
15 |
16 | .badge {
17 | background: $theme-gray-lighter;
18 | border-radius: $theme-border-radius;
19 | display: inline-block;
20 | font-size: 14px;
21 | padding: 5px 10px;
22 | letter-spacing: 0;
23 |
24 | &.badge--primary,
25 | &.badge--premium {
26 | background: $theme-brand-primary;
27 | color: #FFF;
28 | }
29 |
30 | &.badge--success {
31 | background: $theme-brand-success;
32 | color: #FFF;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/styles/config.scss:
--------------------------------------------------------------------------------
1 | @import './colors.scss';
2 |
3 | $windows-title-bar-height: to-number($raw-windows-title-bar-height);
--------------------------------------------------------------------------------
/src/styles/content-tabs.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark {
4 | .content-tabs {
5 | .content-tabs__content {
6 | background: $dark-theme-gray-darker;
7 | }
8 |
9 | .content-tabs__tabs {
10 | .content-tabs__item {
11 | background: $dark-theme-gray;
12 | color: #FFF;
13 | border: 0;
14 | }
15 | }
16 | }
17 | }
18 |
19 | .content-tabs {
20 | .content-tabs__tabs {
21 | border-top-left-radius: $theme-border-radius-small;
22 | border-top-right-radius: $theme-border-radius-small;
23 | display: flex;
24 | overflow: hidden;
25 |
26 | .content-tabs__item {
27 | background: linear-gradient($theme-gray-lightest 80%, darken($theme-gray-lightest, 3%));
28 | border-right: 1px solid $theme-gray-lighter;
29 | color: $theme-gray-dark;
30 | flex: 1;
31 | padding: 10px;
32 | transition: background $theme-transition-time;
33 |
34 | &:last-of-type { border-right: 0; }
35 |
36 | &.is-active {
37 | background: $theme-brand-primary;
38 | box-shadow: none;
39 | color: #FFF;
40 | }
41 | }
42 | }
43 |
44 | .content-tabs__content {
45 | background: $theme-gray-lightest;
46 | border-bottom-left-radius: $theme-border-radius-small;
47 | border-bottom-right-radius: $theme-border-radius-small;
48 | padding: 20px 20px;
49 |
50 | .content-tabs__item {
51 | display: none;
52 | top: 0;
53 |
54 | &.is-active { display: block; }
55 | }
56 |
57 | .franz-form__input-wrapper { background: #FFF; }
58 | .franz-form__field:last-of-type { margin-bottom: 0; }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/styles/fonts.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 | // @import './node_modules/mdi/scss/materialdesignicons.scss';
3 |
4 | @font-face {
5 | font-family: 'Open Sans';
6 | src: url('../assets/fonts/OpenSans-Light.ttf');
7 | font-weight: 300;
8 | font-style: normal;
9 | }
10 |
11 | @font-face {
12 | font-family: 'Open Sans';
13 | src: url('../assets/fonts/OpenSans-Regular.ttf');
14 | font-weight: normal;
15 | font-style: normal;
16 | }
17 |
18 | @font-face {
19 | font-family: 'Open Sans';
20 | src: url('../assets/fonts/OpenSans-Bold.ttf');
21 | font-weight: bold;
22 | font-style: normal;
23 | }
24 |
25 | @font-face {
26 | font-family: 'Open Sans';
27 | src: url('../assets/fonts/OpenSans-BoldItalic.ttf');
28 | font-weight: bold;
29 | font-style: italic;
30 | }
31 |
32 | @font-face {
33 | font-family: 'Open Sans';
34 | src: url('../assets/fonts/OpenSans-ExtraBold.ttf');
35 | font-weight: 800;
36 | font-style: normal;
37 | }
38 |
39 | @font-face {
40 | font-family: 'Open Sans';
41 | src: url('../assets/fonts/OpenSans-ExtraBoldItalic.ttf');
42 | font-weight: 800;
43 | font-style: italic;
44 | }
45 |
--------------------------------------------------------------------------------
/src/styles/info-bar.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .info-bar {
4 | align-items: center;
5 | background: $theme-brand-primary;
6 | box-shadow: 0 0 8px rgba(black, .2);
7 | display: flex;
8 | height: 50px;
9 | justify-content: center;
10 | padding: 0 20px;
11 | position: relative;
12 | width: 100%;
13 | z-index: 100;
14 |
15 | .info-bar__content {
16 | height: auto;
17 |
18 | .mdi { margin-right: 5px; }
19 | }
20 |
21 | .info-bar__close {
22 | color: #FFF;
23 | position: absolute;
24 | right: 10px;
25 | }
26 |
27 | .info-bar__cta {
28 | border-color: #FFF;
29 | border-radius: $theme-border-radius-small;
30 | border-style: solid;
31 | border-width: 2px;
32 | color: #FFF;
33 | margin-left: 15px;
34 | padding: 3px 8px;
35 |
36 | .loader {
37 | display: inline-block;
38 | height: 12px;
39 | margin-right: 5px;
40 | position: relative;
41 | width: 20px;
42 | z-index: 9999;
43 | }
44 | }
45 |
46 | .info-bar__inline-button {
47 | color: white;
48 | }
49 |
50 | &.info-bar--bottom { order: 10; }
51 |
52 | &.info-bar--primary {
53 | background: $theme-brand-primary;
54 | color: #FFF;
55 |
56 | a { color: #FFF; }
57 | }
58 |
59 | &.info-bar--warning {
60 | background: $theme-brand-warning;
61 | color: #FFF;
62 |
63 | a { color: #FFF; }
64 | }
65 |
66 | &.info-bar--danger {
67 | background: $theme-brand-danger;
68 | color: #FFF;
69 |
70 | a { color: #FFF; }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/styles/infobox.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .infobox {
4 | align-items: center;
5 | border-radius: $theme-border-radius-small;
6 | display: flex;
7 | height: auto;
8 | margin-bottom: 30px;
9 | padding: 15px 20px;
10 |
11 | a { color: #FFF; }
12 |
13 | .infobox__content { flex: 1; }
14 |
15 | &.infobox--success {
16 | background: $theme-brand-success;
17 | color: #FFF;
18 | }
19 |
20 | &.infobox--primary {
21 | background: $theme-brand-primary;
22 | color: #FFF;
23 | }
24 |
25 | &.infobox--danger {
26 | background: $theme-brand-danger;
27 | color: #FFF;
28 | }
29 |
30 | &.infobox--warning {
31 | background: $theme-brand-warning;
32 | color: #FFF;
33 | }
34 |
35 | .mdi { margin-right: 10px; }
36 |
37 | .infobox__cta {
38 | border-color: #FFF;
39 | border-radius: $theme-border-radius-small;
40 | border-style: solid;
41 | border-width: 2px;
42 | color: #FFF;
43 | margin-left: 15px;
44 | padding: 3px 8px;
45 |
46 | .loader {
47 | display: inline-block;
48 | height: 12px;
49 | margin-right: 5px;
50 | position: relative;
51 | width: 20px;
52 | z-index: 9999;
53 | }
54 | }
55 |
56 | .infobox__delete {
57 | color: #FFF;
58 | margin-right: 0;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/styles/invite.scss:
--------------------------------------------------------------------------------
1 | .invite__form {
2 | align-items: center;
3 | align-self: center;
4 | justify-content: center;
5 | }
6 |
7 | .invite__embed { text-align: center; }
8 | .invite__embed--button { width: 100%; }
9 |
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | $mdi-font-path: '../node_modules/mdi/fonts';
2 | @if $env == development {
3 | $mdi-font-path: '../../node_modules/mdi/fonts';
4 | }
5 |
6 | @import './node_modules/mdi/scss/materialdesignicons.scss';
7 |
8 | // modules
9 | @import './reset.scss';
10 | @import './util.scss';
11 | @import './layout.scss';
12 | @import './tabs.scss';
13 | @import './services.scss';
14 | @import './settings.scss';
15 | @import './service-table.scss';
16 | @import './recipes.scss';
17 | @import './fonts.scss';
18 | @import './type.scss';
19 | @import './welcome.scss';
20 | @import './auth.scss';
21 | @import './tooltip.scss';
22 | @import './info-bar.scss';
23 | @import './status-bar-target-url.scss';
24 | @import './animations.scss';
25 | @import './infobox.scss';
26 | @import './badge.scss';
27 | @import './subscription.scss';
28 | @import './subscription-popup.scss';
29 | @import './content-tabs.scss';
30 | @import './invite.scss';
31 |
32 | // form
33 | @import './input.scss';
34 | @import './radio.scss';
35 | @import './toggle.scss';
36 | @import './button.scss';
37 | @import './searchInput.scss';
38 | @import './select.scss';
39 | @import './image-upload.scss';
40 |
--------------------------------------------------------------------------------
/src/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | @mixin formLabel {
4 | color: $theme-gray-light;
5 | display: block;
6 | margin-bottom: 5px;
7 | order: 0;
8 | width: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/radio.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark .franz-form .franz-form__radio {
4 | border: 1px solid $dark-theme-gray-lighter;
5 | color: $dark-theme-gray-lightest;
6 |
7 | &.is-selected {
8 | background: $dark-theme-gray-lighter;
9 | border: 1px solid $dark-theme-gray-lighter;
10 | color: $dark-theme-gray-smoke;
11 | }
12 | }
13 |
14 |
15 | .franz-form {
16 | .franz-form__radio-wrapper { display: flex; }
17 |
18 | .franz-form__radio {
19 | border: 2px solid $theme-gray-lighter;
20 | border-radius: $theme-border-radius-small;
21 | box-shadow: $theme-inset-shadow;
22 | color: $theme-gray;
23 | flex: 1;
24 | margin-right: 20px;
25 | padding: 11px;
26 | text-align: center;
27 | transition: background $theme-transition-time;
28 |
29 | &:last-of-type { margin-right: 0; }
30 |
31 | &.is-selected {
32 | background: #FFF;
33 | border: 2px solid $theme-brand-primary;
34 | color: $theme-brand-primary;
35 | }
36 |
37 | input { display: none; }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/styles/recipes.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark .recipe-teaser {
4 | background-color: $dark-theme-gray-dark;
5 | color: $dark-theme-text-color;
6 |
7 | &:hover { background-color: $dark-theme-gray; }
8 | }
9 |
10 | .recipes {
11 | .recipes__list {
12 | align-content: flex-start;
13 | display: flex;
14 | flex-flow: row wrap;
15 | height: auto;
16 | // min-height: 70%;
17 |
18 | &.recipes__list--disabled {
19 | filter: grayscale(100%);
20 | opacity: .3;
21 | pointer-events: none;
22 | }
23 | }
24 |
25 | .recipes__navigation {
26 | height: auto;
27 | margin-bottom: 35px;
28 |
29 | .badge { margin-right: 10px; }
30 |
31 | &.recipes__navigation--disabled {
32 | filter: grayscale(100%);
33 | opacity: .3;
34 | pointer-events: none;
35 | }
36 | }
37 |
38 | &__service-request { float: right; }
39 | }
40 |
41 | .recipe-teaser {
42 | background-color: $theme-gray-lightest;
43 | border-radius: $theme-border-radius;
44 | height: 120px;
45 | margin: 0 20px 20px 0;
46 | overflow: hidden;
47 | position: relative;
48 | transition: background $theme-transition-time;
49 | width: calc(25% - 20px);
50 |
51 | &:hover { background-color: $theme-gray-lighter; }
52 |
53 | .recipe-teaser__icon {
54 | margin-bottom: 10px;
55 | width: 50px;
56 | }
57 |
58 | .recipe-teaser__label { display: block; }
59 |
60 | h2 { z-index: 10; }
61 |
62 | &__dev-badge {
63 | background: $theme-brand-warning;
64 | box-shadow: 0 0 4px rgba(black, .2);
65 | color: #FFF;
66 | font-size: 10px;
67 | position: absolute;
68 | right: -13px;
69 | top: 5px;
70 | transform: rotateZ(45deg);
71 | width: 50px;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/styles/searchInput.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 | @import './mixins.scss';
3 |
4 | .theme__dark .search-input {
5 | @extend %headline__dark;
6 | background: $dark-theme-gray-dark;
7 | border: 1px solid $dark-theme-gray-light;
8 | border-radius: $theme-border-radius;
9 | color: $dark-theme-gray-lightest;
10 |
11 | input { color: $dark-theme-gray-lightest; }
12 | }
13 |
14 | .search-input {
15 | @extend %headline;
16 | align-items: center;
17 | background: $theme-gray-lightest;
18 | border-radius: 30px;
19 | color: $theme-gray-light;
20 | display: flex;
21 | height: auto;
22 | padding: 5px 10px;
23 | width: 100%;
24 |
25 | label {
26 | width: 100%;
27 | }
28 |
29 | input {
30 | background: none;
31 | border: 0;
32 | color: $theme-gray-light;
33 | flex: 1;
34 | padding-left: 10px;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/styles/service-table.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark .service-table {
4 | .service-table__icon.has-custom-icon { border: 1px solid $dark-theme-gray-dark; }
5 | .service-table__column-info .mdi { color: $dark-theme-gray-lightest; }
6 |
7 | .service-table__row {
8 | border-bottom: 1px solid $dark-theme-gray-darker;
9 |
10 | &:hover { background: $dark-theme-gray-darker; }
11 | &.service-table__row--disabled { color: $dark-theme-gray; }
12 | }
13 | }
14 |
15 | .service-table {
16 | width: 100%;
17 |
18 | .service-table__toggle {
19 | width: 60px;
20 |
21 | .franz-form__field { margin-bottom: 0; }
22 | }
23 |
24 | .service-table__icon {
25 | width: 35px;
26 |
27 | &.has-custom-icon {
28 | border: 1px solid $theme-gray-lighter;
29 | border-radius: $theme-border-radius;
30 | width: 37px;
31 | }
32 | }
33 |
34 | .service-table__column-icon,
35 | .service-table__column-action { width: 40px }
36 |
37 | .service-table__column-info {
38 | width: 40px;
39 |
40 | .mdi {
41 | color: $theme-gray-light;
42 | display: block;
43 | font-size: 18px;
44 | }
45 | }
46 |
47 | .service-table__row {
48 | border-bottom: 1px solid $theme-gray-lightest;
49 |
50 | &:hover { background: $theme-gray-lightest; }
51 |
52 | &.service-table__row--disabled {
53 | color: $theme-gray-light;
54 |
55 | .service-table__column-icon {
56 | filter: grayscale(100%);
57 | opacity: .5;
58 | }
59 | }
60 | }
61 |
62 | td { padding: 10px; }
63 | }
64 |
--------------------------------------------------------------------------------
/src/styles/services.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark {
4 | .services {
5 | background: $dark-theme-gray-darkest;
6 |
7 | .services__webview-wrapper { background: $dark-theme-gray-darkest; }
8 | }
9 |
10 | .services__no-service,
11 | .services__info-layer {
12 | background: $dark-theme-gray-darkest;
13 |
14 | h1 { color: $dark-theme-gray-lightest; }
15 | }
16 | }
17 |
18 |
19 | .services {
20 | background: #FFF;
21 | flex: 1;
22 | height: 100%;
23 | // order: 5;
24 | overflow: hidden;
25 | position: relative;
26 |
27 | .services__webview-wrapper { background: $theme-gray-lighter; }
28 |
29 | }
30 |
31 | .services__no-service,
32 | .services__info-layer {
33 | align-items: center;
34 | display: flex;
35 | flex: 1;
36 | flex-direction: column;
37 | justify-content: center;
38 | text-align: center;
39 |
40 | h1 {
41 | color: $theme-gray-dark;
42 | margin: 25px 0 40px;
43 | }
44 |
45 | a.button,
46 | button { margin: 40px 0 20px; }
47 | }
--------------------------------------------------------------------------------
/src/styles/status-bar-target-url.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .status-bar-target-url {
4 | background: $theme-gray-lighter;
5 | border-top-left-radius: 5px;
6 | bottom: 0;
7 | box-shadow: 0 0 8px rgba(black, .2);
8 | color: $theme-gray-dark;
9 | font-size: 12px;
10 | height: auto;
11 | right: 0;
12 | padding: 4px;
13 | position: absolute;
14 | }
15 |
--------------------------------------------------------------------------------
/src/styles/subscription-popup.scss:
--------------------------------------------------------------------------------
1 | .subscription-popup {
2 | height: 100%;
3 |
4 | &__content { height: calc(100% - 60px); }
5 | &__webview {
6 | height: 100%;
7 | background: #FFF;
8 | }
9 |
10 | &__toolbar {
11 | background: $theme-gray-lightest;
12 | border-top: 1px solid $theme-gray-lighter;
13 | display: flex;
14 | height: 60px;
15 | justify-content: space-between;
16 | padding: 10px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/styles/subscription.scss:
--------------------------------------------------------------------------------
1 | .subscription {
2 | .subscription__premium-features {
3 | margin: 10px 0;
4 |
5 | li {
6 | align-items: center;
7 | display: flex;
8 | height: 30px;
9 |
10 | &:before {
11 | content: "👍";
12 | margin-right: 10px;
13 | }
14 |
15 | .badge { margin-left: 10px; }
16 | }
17 | }
18 |
19 | .subscription__premium-info { margin: 15px 0 25px; }
20 | }
21 |
22 | .paymentTiers .franz-form__radio-wrapper {
23 | flex-flow: wrap;
24 |
25 | .franz-form__radio {
26 | flex: initial;
27 | margin-right: 2%;
28 | width: 32%;
29 |
30 | &:nth-child(3) { margin-right: 0; }
31 |
32 | &:nth-child(4) {
33 | margin-right: 0;
34 | margin-top: 2%;
35 | width: 100%;
36 | }
37 | }
38 | }
39 |
40 | .settings .paymentTiers .franz-form__radio-wrapper .franz-form__radio {
41 | width: 49%;
42 |
43 | &:nth-child(2) { margin-right: 0; }
44 |
45 | &:nth-child(3) {
46 | margin-top: 2%;
47 | width: 100%;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/styles/toggle.scss:
--------------------------------------------------------------------------------
1 | @use "sass:math";
2 | @import './config.scss';
3 |
4 | $toggle-size: 14px;
5 | $toggle-width: 40px;
6 | $toggle-button-size: 22px;
7 |
8 | .theme__dark .franz-form .franz-form__toggle-wrapper .franz-form__toggle {
9 | background: $dark-theme-gray;
10 | border-radius: math.div($toggle-size, 2);
11 |
12 | .franz-form__toggle-button {
13 | background: $dark-theme-gray-lighter;
14 | box-shadow: 0 1px 4px rgba($dark-theme-black, .3);
15 | }
16 | }
17 |
18 | .franz-form .franz-form__toggle-wrapper {
19 | display: flex;
20 | flex-direction: row;
21 |
22 | .franz-form__label { margin-left: 20px; }
23 |
24 | .franz-form__toggle {
25 | background: $theme-gray-lighter;
26 | border-radius: $theme-border-radius;
27 | height: $toggle-size;
28 | position: relative;
29 | width: $toggle-width;
30 |
31 | .franz-form__toggle-button {
32 | background: $theme-gray-light;
33 | border-radius: 100%;
34 | box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
35 | height: $toggle-size - 2;
36 | left: 1px;
37 | top: 1px;
38 | position: absolute;
39 | transition: all .5s;
40 | width: $toggle-size - 2;
41 | }
42 |
43 | &.is-active .franz-form__toggle-button {
44 | background: $theme-brand-primary;
45 | left: $toggle-width - $toggle-size - 3;
46 | }
47 |
48 | input { display: none; }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/styles/tooltip.scss:
--------------------------------------------------------------------------------
1 | .__react_component_tooltip {
2 | height: auto;
3 | padding: 4px !important;
4 | font-size: 8px !important;
5 | }
6 |
7 | .sidebar .__react_component_tooltip {
8 | width: $theme-sidebar-width - 4px !important;
9 | margin-top: -10px !important;
10 | margin-left: 2px !important;
11 |
12 | &.place-right {
13 | margin-left: 2px !important;
14 | }
15 |
16 | &.place-top {
17 | margin-top: 10px !important;
18 | }
19 | }
--------------------------------------------------------------------------------
/src/styles/type.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 | @import './mixins.scss';
3 |
4 | .theme__dark {
5 | a { color: $dark-theme-gray-smoke; }
6 | .label { color: $dark-theme-gray-lightest; }
7 | .footnote { color: $dark-theme-gray-lightest; }
8 | }
9 |
10 | h1 {
11 | font-size: 30px;
12 | font-weight: 300;
13 | letter-spacing: -1px;
14 | margin-bottom: 25px;
15 | }
16 |
17 | h2 {
18 | font-size: 20px;
19 | font-weight: 500;
20 | letter-spacing: -1px;
21 | margin-bottom: 25px;
22 | margin-top: 55px;
23 |
24 | &:first-of-type { margin-top: 0; }
25 | }
26 |
27 | p {
28 | margin-bottom: 10px;
29 | line-height: 1.7rem;
30 |
31 | &:last-of-type { margin-bottom: 0; }
32 | }
33 |
34 | strong { font-weight: bold; }
35 |
36 | a {
37 | color: $theme-text-color;
38 | text-decoration: none;
39 |
40 | &.button {
41 | background: none;
42 | border: 2px solid $theme-brand-primary;
43 | border-radius: 3px;
44 | color: $theme-brand-primary;
45 | display: inline-block;
46 | padding: 10px 20px;
47 | position: relative;
48 | text-align: center;
49 | transition: background .5s, color .5s;
50 |
51 | &:hover {
52 | background: darken($theme-brand-primary, 5%);
53 | color: #FFF;
54 | }
55 | }
56 |
57 | &.link { color: $theme-brand-primary; }
58 | }
59 |
60 | .error-message, .error-message:last-of-type {
61 | color: $theme-brand-danger;
62 | margin: 10px 0;
63 | }
64 |
65 | .center { text-align: center; }
66 |
67 | .label { @include formLabel(); }
68 |
69 | .footnote {
70 | color: $theme-gray-light;
71 | font-size: 12px;
72 | }
73 |
--------------------------------------------------------------------------------
/src/styles/util.scss:
--------------------------------------------------------------------------------
1 | .scroll-container {
2 | flex: 1;
3 | height: 100%;
4 | overflow-x: hidden;
5 | overflow-y: scroll;
6 | }
7 |
8 | .loader {
9 | display: block;
10 | height: 40px;
11 | position: relative;
12 | width: 100%;
13 | z-index: 9999;
14 | }
15 |
16 | .align-middle {
17 | display: flex;
18 | flex-direction: column;
19 | justify-content: center;
20 | }
21 |
22 | .pulsating {
23 | animation: pulse-animation 1s alternate infinite ease-in-out;
24 | }
25 |
26 | @keyframes pulse-animation {
27 | 0% {
28 | transform: scale(0.7)
29 | }
30 | 100% {
31 | transform: scale(1)
32 | }
33 | }
--------------------------------------------------------------------------------
/src/styles/welcome.scss:
--------------------------------------------------------------------------------
1 | .auth .welcome {
2 | height: auto;
3 |
4 | &__content {
5 | align-items: center;
6 | color: #FFF;
7 | display: flex;
8 | justify-content: center;
9 | height: auto;
10 | }
11 |
12 | &__logo { width: 100px; }
13 |
14 | &__text {
15 | border-left: 1px solid #FFF;
16 | margin-left: 40px;
17 | padding-left: 40px;
18 |
19 | h1 {
20 | font-size: 60px;
21 | letter-spacing: -.4rem;
22 | margin-bottom: 5px;
23 | }
24 |
25 | h2 {
26 | margin-bottom: 0;
27 | margin-left: 2px;
28 | }
29 | }
30 |
31 | &__services {
32 | height: 100%;
33 | margin-left: -450px;
34 | max-height: 600px;
35 | max-width: 800px;
36 | width: 100%;
37 | }
38 |
39 | &__buttons {
40 | display: block;
41 | margin-top: 100px;
42 | text-align: center;
43 | height: auto;
44 |
45 | .button:first-of-type { margin-right: 25px; }
46 | }
47 |
48 | .button {
49 | border-color: #FFF;
50 | color: #FFF;
51 |
52 | &:hover {
53 | background: #FFF;
54 | color: $theme-brand-primary;
55 | }
56 |
57 | &__inverted {
58 | background: #FFF;
59 | color: $theme-brand-primary;
60 | }
61 |
62 | &__inverted:hover {
63 | background: none;
64 | color: #FFF;
65 | }
66 | }
67 |
68 | &__featured-services {
69 | align-items: center;
70 | background: #FFF;
71 | border-radius: 6px;
72 | display: flex;
73 | flex-wrap: wrap;
74 | margin: 80px auto 0 auto;
75 | padding: 20px 20px 5px;
76 | text-align: center;
77 | width: 480px;
78 | height: auto;
79 | }
80 |
81 | &__featured-service {
82 | margin: 0 10px 15px;
83 | height: 35px;
84 | transition: .5s filter, .5s opacity;
85 | width: 35px;
86 |
87 | img { width: 35px; }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/theme/default/legacy.js:
--------------------------------------------------------------------------------
1 | /* legacy config, injected into sass */
2 | export const themeBrandPrimary = '#3498db';
3 | export const themeBrandSuccess = '#5cb85c';
4 | export const themeBrandInfo = '#5bc0de';
5 | export const themeBrandWarning = '#FF9F00';
6 | export const themeBrandDanger = '#d9534f';
7 |
8 | export const themeGrayDark = '#373a3c';
9 | export const themeGray = '#55595c';
10 | export const themeGrayLight = '#818a91';
11 | export const themeGrayLighter = '#eceeef';
12 | export const themeGrayLightest = '#f7f7f9';
13 |
14 | export const themeBorderRadius = '6px';
15 | export const themeBorderRadiusSmall = '3px';
16 |
17 | export const themeSidebarWidth = '68px';
18 |
19 | export const themeTextColor = themeGrayDark;
20 |
21 | export const themeTransitionTime = '.5s';
22 |
23 | export const themeInsetShadow = 'inset 0 2px 5px rgba(0, 0, 0, .03)';
24 |
25 |
26 | export const darkThemeBlack = '#1A1A1A';
27 |
28 | export const darkThemeGrayDarkest = '#1E1E1E';
29 | export const darkThemeGrayDarker = '#2D2F31';
30 | export const darkThemeGrayDark = '#383A3B';
31 |
32 | export const darkThemeGray = '#47494B';
33 |
34 | export const darkThemeGrayLight = '#515355';
35 | export const darkThemeGrayLighter = '#8a8b8b';
36 | export const darkThemeGrayLightest = '#FFFFFF';
37 |
38 | export const darkThemeGraySmoke = '#CED0D1';
39 | export const darkThemeTextColor = '#FFFFFF';
40 |
41 | export const windowsTitleBarHeight = '31px';
42 |
--------------------------------------------------------------------------------
/src/webview/darkmode.js:
--------------------------------------------------------------------------------
1 | /* eslint no-bitwise: ["error", { "int32Hint": true }] */
2 |
3 | import path from 'path';
4 | import fs from 'fs-extra';
5 |
6 | const debug = require('debug')('Franz:DarkMode');
7 |
8 | const chars = [...'abcdefghijklmnopqrstuvwxyz'];
9 |
10 | const ID = [...Array(20)].map(() => chars[Math.random() * chars.length | 0]).join``;
11 |
12 | export function injectDarkModeStyle(recipePath) {
13 | const darkModeStyle = path.join(recipePath, 'darkmode.css');
14 | if (fs.pathExistsSync(darkModeStyle)) {
15 | const data = fs.readFileSync(darkModeStyle);
16 | const styles = document.createElement('style');
17 | styles.id = ID;
18 | styles.innerHTML = data.toString();
19 |
20 | document.querySelector('head').appendChild(styles);
21 |
22 | debug('Injected Dark Mode style with ID', ID);
23 | }
24 | }
25 |
26 | export function removeDarkModeStyle() {
27 | const style = document.querySelector(`#${ID}`);
28 |
29 | if (style) {
30 | style.remove();
31 |
32 | debug('Removed Dark Mode Style with ID', ID);
33 | }
34 | }
35 |
36 | export function isDarkModeStyleInjected() {
37 | return !!document.querySelector(`#${ID}`);
38 | }
39 |
--------------------------------------------------------------------------------
/src/webview/desktopCapturer.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import { SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY } from '../features/desktopCapturer/config';
3 | import { OVERLAY_OPEN } from '../ipcChannels';
4 |
5 | function getDisplayMedia() {
6 | return new Promise(async (resolve, reject) => {
7 | ipcRenderer.once(SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY, async (event, { sourceId }) => {
8 | const stream = await navigator.mediaDevices.getUserMedia({
9 | audio: false,
10 | video: {
11 | mandatory: {
12 | chromeMediaSource: 'desktop',
13 | chromeMediaSourceId: sourceId,
14 | },
15 | },
16 | });
17 |
18 | resolve(stream);
19 | });
20 |
21 | const overlayAction = await ipcRenderer.invoke(OVERLAY_OPEN, {
22 | route: '/screen-share/{webContentsId}',
23 | modal: false,
24 | width: 600,
25 | });
26 |
27 | setTimeout(() => {
28 | if (overlayAction === 'closed') {
29 | reject(new Error('Source selection canceled'));
30 | }
31 | }, 250);
32 | });
33 | }
34 |
35 | window.navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
36 |
--------------------------------------------------------------------------------
/src/webview/notifications.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import uuidV1 from 'uuid/v1';
3 |
4 | const debug = require('debug')('Franz:Notifications');
5 |
6 | class Notification {
7 | static permission = 'granted';
8 |
9 | constructor(title = '', options = {}) {
10 | debug('New notification', title, options);
11 | this.title = title;
12 | this.options = options;
13 | this.notificationId = uuidV1();
14 |
15 | ipcRenderer.send('notification', this.onNotify({
16 | title: this.title,
17 | options: this.options,
18 | notificationId: this.notificationId,
19 | }));
20 |
21 | ipcRenderer.once(`notification-onclick:${this.notificationId}`, () => {
22 | if (typeof this.onclick === 'function') {
23 | this.onclick();
24 | }
25 | });
26 | }
27 |
28 | static requestPermission(cb = null) {
29 | if (!cb) {
30 | return new Promise((resolve) => {
31 | resolve(Notification.permission);
32 | });
33 | }
34 |
35 | if (typeof (cb) === 'function') {
36 | return cb(Notification.permission);
37 | }
38 |
39 | return Notification.permission;
40 | }
41 |
42 | onNotify(data) {
43 | return data;
44 | }
45 |
46 | onClick() {}
47 |
48 | close() {}
49 | }
50 |
51 | window.Notification = Notification;
52 |
--------------------------------------------------------------------------------
/src/webview/spellchecker.js:
--------------------------------------------------------------------------------
1 | import { SPELLCHECKER_LOCALES } from '../i18n/languages';
2 |
3 | export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) {
4 | const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key.toLocaleLowerCase() === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase());
5 |
6 | if (locales.length >= 1) {
7 | return locales[0];
8 | }
9 |
10 | return null;
11 | }
12 |
--------------------------------------------------------------------------------
/src/webview/zoom.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 |
3 | const { ipcRenderer, webFrame } = electron;
4 |
5 | const maxZoomLevel = 9;
6 | const minZoomLevel = -8;
7 | let zoomLevel = 0;
8 |
9 | ipcRenderer.on('zoomIn', () => {
10 | if (maxZoomLevel > zoomLevel) {
11 | zoomLevel += 1;
12 | }
13 | webFrame.setZoomLevel(zoomLevel);
14 |
15 | ipcRenderer.sendToHost('zoomLevel', { zoom: zoomLevel });
16 | });
17 |
18 | ipcRenderer.on('zoomOut', () => {
19 | if (minZoomLevel < zoomLevel) {
20 | zoomLevel -= 1;
21 | }
22 | webFrame.setZoomLevel(zoomLevel);
23 |
24 | ipcRenderer.sendToHost('zoomLevel', { zoom: zoomLevel });
25 | });
26 |
27 | ipcRenderer.on('zoomReset', () => {
28 | zoomLevel = 0;
29 | webFrame.setZoomLevel(zoomLevel);
30 |
31 | ipcRenderer.sendToHost('zoomLevel', { zoom: zoomLevel });
32 | });
33 |
34 | ipcRenderer.on('setZoom', (e, arg) => {
35 | zoomLevel = arg;
36 | webFrame.setZoomLevel(zoomLevel);
37 | });
38 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "target": "esnext",
5 | "module": "commonjs",
6 | "outDir": ".tstmp",
7 | "rootDir": "./src",
8 | "allowJs": true,
9 | "strict": false,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "experimentalDecorators": true
13 | },
14 | "include": [
15 | "src/**/*"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "esnext",
5 | "module": "commonjs",
6 | "lib": [
7 | "es2015",
8 | "es2017",
9 | "dom"
10 | ],
11 | "jsx": "react",
12 | "sourceMap": true,
13 | "strict": true,
14 | "allowSyntheticDefaultImports": true,
15 | "experimentalDecorators": true,
16 | "composite": true,
17 | "esModuleInterop": true,
18 | "typeRoots": ["packages/typings/types", "node_modules/@types"],
19 | "paths": {
20 | "@types/*": ["packages/typings/types/*.d.ts"],
21 | "*": ["packages/typings/types/*.d.ts"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-config-airbnb"],
3 | "rules": {
4 | "import-name": false,
5 | "variable-name": false,
6 | "class-name": false,
7 | "prefer-array-literal": false,
8 | "semicolon": [true, "always"],
9 | "max-line-length": false,
10 | "ordered-imports": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/types.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module 'react-jss'
4 |
--------------------------------------------------------------------------------
/uidev/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIDev
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/uidev/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { App } from './app';
4 |
5 | const app = () => (
6 |
7 | );
8 |
9 | render(app(), document.getElementById('root'));
10 |
--------------------------------------------------------------------------------
/uidev/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | import { storyStore } from './stories';
2 |
3 | export const store = {
4 | stories: storyStore,
5 | };
6 |
--------------------------------------------------------------------------------
/uidev/src/stores/stories.ts:
--------------------------------------------------------------------------------
1 | import { store } from './index';
2 |
3 | export type StorySectionName = string;
4 | export type StoryName = string;
5 | export type StoryComponent = () => JSX.Element;
6 |
7 | export interface IStories {
8 | name: string;
9 | component: StoryComponent;
10 | }
11 |
12 | export interface ISections {
13 | name: StorySectionName;
14 | stories: IStories[];
15 | }
16 |
17 | export interface IStoryStore {
18 | sections: ISections[];
19 | }
20 |
21 | export const storyStore: IStoryStore = {
22 | sections: [],
23 | };
24 |
25 | export const storiesOf = (name: StorySectionName) => {
26 | const length = storyStore.sections.push({
27 | name,
28 | stories: [],
29 | });
30 |
31 | const actions = {
32 | add: (name: StoryName, component: StoryComponent) => {
33 | storyStore.sections[length - 1].stories.push({
34 | name,
35 | component,
36 | });
37 |
38 | return actions;
39 | },
40 | };
41 |
42 | return actions;
43 | };
44 |
--------------------------------------------------------------------------------
/uidev/src/stories/badge.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Badge, ProBadge } from '@meetfranz/ui';
4 | import { storiesOf } from '../stores/stories';
5 |
6 | storiesOf('Badge')
7 | .add('Basic', () => (
8 | <>
9 | New
10 | >
11 | ))
12 | .add('Styles', () => (
13 | <>
14 | Primary
15 | secondary
16 | success
17 | warning
18 | danger
19 | inverted
20 | >
21 | ))
22 | .add('Pro Badge', () => (
23 | <>
24 |
25 | >
26 | ))
27 | .add('Pro Badge inverted', () => (
28 | <>
29 |
30 | >
31 | ));
32 |
--------------------------------------------------------------------------------
/uidev/src/stories/headline.stories.tsx:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 | import { observer } from 'mobx-react';
3 | import React from 'react';
4 | import uuid from 'uuid/v4';
5 |
6 | import { H1, H2, H3, H4 } from '@meetfranz/ui';
7 | import { storiesOf } from '../stores/stories';
8 |
9 | // interface IStoreArgs {
10 | // value?: boolean;
11 | // checked?: boolean;
12 | // label?: string;
13 | // id?: string;
14 | // name?: string;
15 | // disabled?: boolean;
16 | // error?: string;
17 | // }
18 |
19 | // const createStore = (args?: IStoreArgs) => {
20 | // return observable(Object.assign({
21 | // id: `element-${uuid()}`,
22 | // name: 'toggle',
23 | // label: 'Label',
24 | // value: true,
25 | // checked: false,
26 | // disabled: false,
27 | // error: '',
28 | // }, args));
29 | // };
30 |
31 | // const WithStoreToggle = observer(({ store }: { store: any }) => (
32 | // <>
33 | // store.checked = !store.checked}
42 | // />
43 | // >
44 | // ));
45 |
46 | storiesOf('Typo')
47 | .add('Headlines', () => (
48 | <>
49 | Welcome to the world of tomorrow
50 | Welcome to the world of tomorrow
51 | Welcome to the world of tomorrow
52 | Welcome to the world of tomorrow
53 | >
54 | ));
55 |
--------------------------------------------------------------------------------
/uidev/src/stories/icon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { mdiAccountCircle } from '@mdi/js';
2 | import React from 'react';
3 |
4 | import { Icon } from '@meetfranz/ui';
5 | import { storiesOf } from '../stores/stories';
6 |
7 | storiesOf('Icon')
8 | .add('Basic', () => (
9 | <>
10 |
11 |
12 |
13 | >
14 | ));
15 |
--------------------------------------------------------------------------------
/uidev/src/stories/loader.stories.tsx:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 | import { observer } from 'mobx-react';
3 | import React from 'react';
4 | import uuid from 'uuid/v4';
5 |
6 | import { Loader } from '@meetfranz/ui';
7 | import { storiesOf } from '../stores/stories';
8 |
9 | storiesOf('Loader')
10 | .add('Basic', () => (
11 | <>
12 |
13 | >
14 | ));
15 |
--------------------------------------------------------------------------------
/uidev/src/stories/textarea.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import uuid from 'uuid/v4';
3 |
4 | import { Textarea } from '@meetfranz/forms';
5 | import { storiesOf } from '../stores/stories';
6 |
7 | const defaultProps = () => {
8 | const id = uuid();
9 | return {
10 | label: 'Label',
11 | id: `test-${id}`,
12 | name: `test-${id}`,
13 | rows: 5,
14 | onChange: (e: React.ChangeEvent) => console.log('changed event', e),
15 | };
16 | };
17 |
18 | storiesOf('Textarea')
19 | .add('Basic', () => (
20 |
24 | ))
25 | .add('10 rows', () => (
26 |
30 | ))
31 | .add('With error', () => (
32 |
36 | ))
37 | .add('Disabled', () => (
38 |
43 | ));
44 |
--------------------------------------------------------------------------------
/uidev/src/withTheme/index.tsx:
--------------------------------------------------------------------------------
1 | import { theme, Theme, ThemeType } from '@meetfranz/theme';
2 | import { Classes } from 'jss';
3 | import React from 'react';
4 | import injectSheet, { ThemeProvider } from 'react-jss';
5 |
6 | const defaultTheme = {
7 | name: 'Default',
8 | variables: theme(ThemeType.default),
9 | };
10 |
11 | const darkTheme = {
12 | name: 'Dark Mode',
13 | variables: theme(ThemeType.dark),
14 | };
15 |
16 | const themes = [defaultTheme, darkTheme];
17 |
18 | const styles = (theme: Theme) => ({
19 | title: {
20 | fontSize: 14,
21 | },
22 | container: {
23 | border: theme.inputBorder,
24 | borderRadius: theme.borderRadiusSmall,
25 | marginBottom: 20,
26 | padding: 20,
27 | background: theme.colorContentBackground,
28 | },
29 | });
30 |
31 | const Container = injectSheet(styles)(({ name, classes, story }: { name: string, classes: Classes, story: React.ReactNode }) => (
32 |
33 | {name}
34 |
35 | {story}
36 |
37 |
38 | ));
39 |
40 | export const WithTheme = ({ children }: {children: React.ReactChild}) => {
41 | return (
42 | <>
43 | {themes.map((theme, key) => (
44 |
45 |
46 |
47 | ))}
48 | >
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/uidev/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.settings.json",
3 | "compilerOptions": {
4 | "baseUrl": "..",
5 | "outDir": "lib",
6 | "rootDir": "src",
7 | },
8 | "references": [{
9 | "path": "../packages/theme"
10 | },
11 | {
12 | "path": "../packages/forms"
13 | }]
14 | }
15 |
--------------------------------------------------------------------------------
/uidev/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tslint.json"
3 | }
4 |
--------------------------------------------------------------------------------
/uidev/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './src/index.tsx',
6 | module: {
7 | rules: [{
8 | test: /\.tsx?$/,
9 | use: 'ts-loader',
10 | exclude: /node_modules/,
11 | }],
12 | },
13 | resolve: {
14 | extensions: ['.tsx', '.ts', '.js'],
15 | alias: {
16 | react: path.resolve('../node_modules/react'),
17 | },
18 | },
19 | mode: 'none',
20 | plugins: [
21 | new HtmlWebpackPlugin({
22 | template: path.join('src', 'app.html'),
23 | }),
24 | ],
25 | devServer: {
26 | inline: true,
27 | port: 8008,
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 |
4 | const IS_DEV = process.env.NODE_ENV === 'development';
5 |
6 | module.exports = dir => ({
7 | context: dir,
8 | entry: path.join(dir, '/src/index.ts'),
9 | module: {
10 | rules: [{
11 | test: /\.tsx?$/,
12 | loader: 'ts-loader',
13 | exclude: /node_modules/,
14 | }],
15 | },
16 | resolve: {
17 | extensions: ['.tsx', '.ts', '.js'],
18 | },
19 | devtool: 'inline-source-map',
20 | mode: IS_DEV ? 'development' : 'production',
21 | optimization: {
22 | minimizer: !IS_DEV ? [new TerserPlugin()] : [],
23 | },
24 | });
25 |
--------------------------------------------------------------------------------