├── .nvmrc ├── packages ├── ui │ ├── .gitignore │ ├── tslint.json │ ├── src │ │ ├── index.ts │ │ ├── typings │ │ │ └── generic.ts │ │ ├── icon │ │ │ └── index.tsx │ │ ├── loader │ │ │ └── index.tsx │ │ ├── badge │ │ │ ├── ProBadge.tsx │ │ │ └── index.tsx │ │ └── headline │ │ │ └── index.tsx │ ├── tsconfig.json │ └── package.json ├── forms │ ├── .gitignore │ ├── tslint.json │ ├── src │ │ ├── index.ts │ │ ├── error │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── label │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── typings │ │ │ └── generic.ts │ │ ├── wrapper │ │ │ └── index.tsx │ │ ├── input │ │ │ └── scorePassword.ts │ │ └── textarea │ │ │ └── styles.ts │ ├── tsconfig.json │ └── package.json ├── theme │ ├── .gitignore │ ├── tslint.json │ ├── README.md │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── themes │ │ │ └── legacy │ │ │ └── index.ts │ └── package.json ├── typings │ ├── types │ │ ├── react-jss.d.ts │ │ ├── mobx-react-form.d.ts │ │ ├── react-html-attributes.d.ts │ │ └── react-loader.d.ts │ └── package.json └── misty.yml ├── src ├── i18n │ ├── locales │ │ └── whitelist_en-US.json │ ├── manage-translations.js │ ├── translations.js │ └── globalMessages.js ├── dev-app-update.yml ├── actions │ ├── requests.js │ ├── news.js │ ├── recipePreview.js │ ├── recipe.js │ ├── ui.js │ ├── settings.js │ ├── payment.js │ ├── app.js │ ├── user.js │ ├── lib │ │ └── actions.js │ └── index.js ├── features │ ├── webControls │ │ └── constants.js │ ├── todos │ │ ├── constants.js │ │ ├── actions.js │ │ ├── preload.js │ │ └── index.js │ ├── basicAuth │ │ ├── styles.js │ │ ├── mainIpcHandler.js │ │ ├── Form.js │ │ └── index.js │ ├── desktopCapturer │ │ ├── config.js │ │ └── index.js │ ├── delayApp │ │ ├── store.js │ │ └── api.js │ ├── planSelection │ │ ├── actions.js │ │ └── api.js │ ├── announcements │ │ ├── actions.js │ │ ├── index.js │ │ └── api.js │ ├── trialStatusBar │ │ ├── actions.js │ │ ├── index.js │ │ └── components │ │ │ └── ProgressBar.js │ ├── utils │ │ ├── ActionBinding.js │ │ └── FeatureStore.js │ ├── workspaces │ │ ├── models │ │ │ └── Workspace.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── components │ │ │ └── WorkspaceItem.js │ │ └── containers │ │ │ └── WorkspacesScreen.js │ ├── communityRecipes │ │ ├── store.js │ │ └── index.js │ ├── serviceLimit │ │ ├── index.js │ │ └── store.js │ └── spellchecker │ │ └── index.js ├── assets │ ├── images │ │ ├── sm.png │ │ ├── emoji │ │ │ ├── sad.png │ │ │ ├── star.png │ │ │ └── dontknow.png │ │ ├── tray │ │ │ ├── darwin │ │ │ │ ├── tray.png │ │ │ │ ├── tray@2x.png │ │ │ │ ├── tray-unread.png │ │ │ │ └── tray-unread@2x.png │ │ │ ├── linux │ │ │ │ ├── tray.png │ │ │ │ ├── tray@2x.png │ │ │ │ ├── tray-unread.png │ │ │ │ └── tray-unread@2x.png │ │ │ ├── win32 │ │ │ │ ├── tray.ico │ │ │ │ └── tray-unread.ico │ │ │ └── darwin-dark │ │ │ │ ├── tray.png │ │ │ │ ├── tray@2x.png │ │ │ │ ├── tray-active.png │ │ │ │ ├── tray-unread.png │ │ │ │ ├── tray-active@2x.png │ │ │ │ ├── tray-unread@2x.png │ │ │ │ ├── tray-unread-active.png │ │ │ │ └── tray-unread-active@2x.png │ │ └── taskbar │ │ │ └── win32 │ │ │ ├── display.ico │ │ │ ├── taskbar-1.ico │ │ │ ├── taskbar-10.ico │ │ │ ├── taskbar-2.ico │ │ │ ├── taskbar-3.ico │ │ │ ├── taskbar-4.ico │ │ │ ├── taskbar-5.ico │ │ │ ├── taskbar-6.ico │ │ │ ├── taskbar-7.ico │ │ │ ├── taskbar-8.ico │ │ │ ├── taskbar-9.ico │ │ │ └── taskbar-alert.ico │ └── fonts │ │ ├── OpenSans-Bold.ttf │ │ ├── OpenSans-Light.ttf │ │ ├── OpenSans-Regular.ttf │ │ ├── OpenSans-BoldItalic.ttf │ │ ├── OpenSans-ExtraBold.ttf │ │ └── OpenSans-ExtraBoldItalic.ttf ├── styles │ ├── config.scss │ ├── mixins.scss │ ├── invite.scss │ ├── status-bar-target-url.scss │ ├── subscription-popup.scss │ ├── tooltip.scss │ ├── util.scss │ ├── badge.scss │ ├── searchInput.scss │ ├── services.scss │ ├── subscription.scss │ ├── radio.scss │ ├── fonts.scss │ ├── main.scss │ ├── infobox.scss │ ├── toggle.scss │ ├── info-bar.scss │ ├── service-table.scss │ ├── type.scss │ ├── content-tabs.scss │ ├── animations.scss │ ├── recipes.scss │ └── welcome.scss ├── helpers │ ├── asar-helpers.js │ ├── array-helpers.js │ ├── async-helpers.js │ ├── routing-helpers.js │ ├── url-helpers.js │ ├── visibility-helper.js │ ├── service-helpers.js │ ├── password-helpers.js │ ├── recipe-helpers.js │ ├── userAgent-helpers.js │ ├── plan-helpers.js │ └── i18n-helpers.js ├── electron │ ├── exception.js │ ├── deepLinking.js │ ├── ipc-api │ │ ├── focusState.js │ │ ├── settings.js │ │ ├── serviceCache.js │ │ ├── macOSPermissions.ts │ │ ├── fullscreen.js │ │ ├── cld.js │ │ ├── index.js │ │ ├── subscriptionWindow.js │ │ └── desktopCapturer.ts │ ├── windowUtils.js │ └── Settings.js ├── components │ ├── ui │ │ ├── Tabs │ │ │ ├── index.js │ │ │ └── TabItem.js │ │ ├── WebviewLoader │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── AppLoader │ │ │ └── styles.js │ │ ├── FullscreenLoader │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Modal │ │ │ └── styles.js │ │ ├── StatusBarTargetUrl.js │ │ ├── PremiumFeatureContainer │ │ │ └── styles.js │ │ ├── FeatureItem.js │ │ ├── Loader.js │ │ ├── effects │ │ │ └── Appear.js │ │ └── ServiceIcon.js │ ├── util │ │ └── ErrorBoundary │ │ │ ├── styles.js │ │ │ └── index.js │ ├── services │ │ └── content │ │ │ ├── ErrorHandlers │ │ │ └── styles.js │ │ │ └── ServiceDisabled.js │ └── settings │ │ ├── recipes │ │ └── RecipeItem.js │ │ └── SettingsLayout.js ├── api │ ├── AppApi.js │ ├── FeaturesApi.js │ ├── NewsApi.js │ ├── PaymentApi.js │ ├── RecipePreviewsApi.js │ ├── RecipesApi.js │ ├── LocalApi.js │ ├── index.js │ ├── ServicesApi.js │ ├── utils │ │ └── auth.js │ ├── UserApi.js │ └── server │ │ └── LocalApi.js ├── models │ ├── Plan.js │ ├── RecipePreview.js │ ├── Order.js │ ├── News.js │ └── User.js ├── prop-types.js ├── webview │ ├── spellchecker.js │ ├── zoom.js │ ├── desktopCapturer.js │ ├── darkmode.js │ └── notifications.js ├── configVanilla.js ├── stores │ ├── lib │ │ ├── Reaction.js │ │ └── Store.js │ ├── GlobalErrorStore.js │ ├── RecipePreviewsStore.js │ └── NewsStore.js ├── containers │ ├── auth │ │ ├── InviteScreen.js │ │ ├── WelcomeScreen.js │ │ ├── PasswordScreen.js │ │ ├── ImportScreen.js │ │ ├── LoginScreen.js │ │ └── SignupScreen.js │ ├── subscription │ │ └── SubscriptionPopupScreen.js │ └── settings │ │ ├── InviteScreen.js │ │ └── SettingsWindow.js ├── lib │ ├── Form.js │ ├── analytics.js │ └── TouchBar.js ├── I18n.js ├── environment.js └── theme │ └── default │ └── legacy.js ├── .eslintignore ├── uidev ├── tslint.json ├── src │ ├── stores │ │ ├── index.ts │ │ └── stories.ts │ ├── index.tsx │ ├── app.html │ ├── stories │ │ ├── loader.stories.tsx │ │ ├── icon.stories.tsx │ │ ├── badge.stories.tsx │ │ ├── textarea.stories.tsx │ │ └── headline.stories.tsx │ └── withTheme │ │ └── index.tsx ├── tsconfig.json └── webpack.config.js ├── jest.config.js ├── .npmrc ├── types.d.ts ├── misty.yml ├── docs ├── example-feature │ ├── api.js │ ├── state.js │ ├── actions.js │ ├── store.js │ └── index.js └── linux.md ├── .editorconfig ├── .gitignore ├── lerna.json ├── tslint.json ├── tsconfig.json ├── .vscode └── tasks.json ├── .github ├── FEATURE_PROPOSAL_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── stale.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── tsconfig.settings.json ├── webpack.config.base.js ├── .babelrc └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.0.0 2 | -------------------------------------------------------------------------------- /packages/ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib 3 | -------------------------------------------------------------------------------- /src/i18n/locales/whitelist_en-US.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | out/ 3 | packages/*/lib 4 | -------------------------------------------------------------------------------- /packages/forms/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib 3 | -------------------------------------------------------------------------------- /packages/theme/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib 3 | -------------------------------------------------------------------------------- /uidev/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['src'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/typings/types/react-jss.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-jss'; 2 | -------------------------------------------------------------------------------- /packages/ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | legacy-peer-deps = true 3 | # python = python2.7 -------------------------------------------------------------------------------- /packages/forms/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/theme/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/typings/types/mobx-react-form.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mobx-react-form'; 2 | -------------------------------------------------------------------------------- /src/dev-app-update.yml: -------------------------------------------------------------------------------- 1 | owner: meetfranz 2 | repo: franz 3 | provider: github 4 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module 'react-jss' 4 | -------------------------------------------------------------------------------- /src/actions/requests.js: -------------------------------------------------------------------------------- 1 | export default { 2 | retryRequiredRequests: {}, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/typings/types/react-html-attributes.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-html-attributes'; 2 | -------------------------------------------------------------------------------- /src/features/webControls/constants.js: -------------------------------------------------------------------------------- 1 | export const CUSTOM_WEBSITE_ID = 'franz-custom-website'; 2 | -------------------------------------------------------------------------------- /src/assets/images/sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/sm.png -------------------------------------------------------------------------------- /src/assets/images/emoji/sad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/emoji/sad.png -------------------------------------------------------------------------------- /src/assets/images/emoji/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/emoji/star.png -------------------------------------------------------------------------------- /misty.yml: -------------------------------------------------------------------------------- 1 | code: 2 | cmd: npm run dev 3 | 4 | app: 5 | cmd: npx electron ./build 6 | waitOn: http://localhost:8000 -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /src/assets/images/emoji/dontknow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/emoji/dontknow.png -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/images/tray/darwin/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin/tray.png -------------------------------------------------------------------------------- /src/assets/images/tray/linux/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/linux/tray.png -------------------------------------------------------------------------------- /src/assets/images/tray/win32/tray.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/win32/tray.ico -------------------------------------------------------------------------------- /src/styles/config.scss: -------------------------------------------------------------------------------- 1 | @import './colors.scss'; 2 | 3 | $windows-title-bar-height: to-number($raw-windows-title-bar-height); -------------------------------------------------------------------------------- /docs/example-feature/api.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async getName() { 3 | return Promise.resolve('Franz'); 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /src/assets/images/tray/darwin/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin/tray@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/linux/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/linux/tray@2x.png -------------------------------------------------------------------------------- /src/helpers/asar-helpers.js: -------------------------------------------------------------------------------- 1 | export function asarPath(dir = '') { 2 | return dir.replace('app.asar', 'app.asar.unpacked'); 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/display.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/display.ico -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray.png -------------------------------------------------------------------------------- /uidev/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { storyStore } from './stories'; 2 | 3 | export const store = { 4 | stories: storyStore, 5 | }; 6 | -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-1.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-1.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-10.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-10.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-2.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-3.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-4.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-4.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-5.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-5.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-6.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-6.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-7.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-7.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-8.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-8.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-9.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-9.ico -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin/tray-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin/tray-unread.png -------------------------------------------------------------------------------- /src/assets/images/tray/linux/tray-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/linux/tray-unread.png -------------------------------------------------------------------------------- /src/assets/images/tray/win32/tray-unread.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/win32/tray-unread.ico -------------------------------------------------------------------------------- /src/electron/exception.js: -------------------------------------------------------------------------------- 1 | process.on('uncaughtException', (err) => { 2 | // handle the error safely 3 | console.error(err); 4 | }); 5 | -------------------------------------------------------------------------------- /src/assets/images/tray/darwin/tray-unread@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin/tray-unread@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/linux/tray-unread@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/linux/tray-unread@2x.png -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-alert.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-alert.ico -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-active.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-unread.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-active@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-unread@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-unread@2x.png -------------------------------------------------------------------------------- /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/actions/news.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | hide: { 5 | newsId: PropTypes.string.isRequired, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /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/assets/images/tray/darwin-dark/tray-unread-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-unread-active.png -------------------------------------------------------------------------------- /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/actions/recipePreview.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | search: { 5 | needle: PropTypes.string.isRequired, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-unread-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-unread-active@2x.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/theme/README.md: -------------------------------------------------------------------------------- 1 | # `theme` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const theme = require('theme'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/misty.yml: -------------------------------------------------------------------------------- 1 | theme: 2 | cwd: ./theme 3 | cmd: npm run dev 4 | 5 | forms: 6 | cwd: ./forms 7 | cmd: npm run dev 8 | 9 | ui: 10 | cwd: ./ui 11 | cmd: npm run dev 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/theme/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src", 6 | "allowJs": true 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /packages/forms/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Input } from './input'; 2 | export { Textarea } from './textarea'; 3 | export { Toggle } from './toggle'; 4 | export { Button } from './button'; 5 | export { Select } from './select'; 6 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Icon } from './icon'; 2 | export { Infobox } from './infobox'; 3 | export * from './headline'; 4 | export { Loader } from './loader'; 5 | export { Badge } from './badge'; 6 | export { ProBadge } from './badge/ProBadge'; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /packages/forms/src/error/styles.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '../../../theme/lib'; 2 | 3 | export default (theme: Theme) => ({ 4 | message: { 5 | color: theme.brandDanger, 6 | margin: '5px 0 0', 7 | fontSize: theme.uiFontSize, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/forms/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/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | flow-typed 4 | out 5 | .DS_Store 6 | .idea 7 | build 8 | .tmp 9 | .stage 10 | .env 11 | yarn-error.log 12 | npm-debug.log* 13 | lerna-debug.log 14 | uidev/lib 15 | *.tsbuildinfo 16 | src/i18n/messages/ 17 | extensions 18 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /uidev/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDev 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/linux.md: -------------------------------------------------------------------------------- 1 | # Linux distribution specific dependencies 2 | 3 | ## Debian/Ubuntu 4 | ```bash 5 | $ apt install libx11-dev libxext-dev libxss-dev libxkbfile-dev 6 | ``` 7 | 8 | ## Fedora 9 | ```bash 10 | $ dnf install libX11-devel libXext-devel libXScrnSaver-devel libxkbfile-devel 11 | ``` 12 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /packages/forms/src/label/styles.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '../../../theme/lib'; 2 | 3 | export default (theme: Theme) => ({ 4 | content: {}, 5 | label: { 6 | color: theme.labelColor, 7 | fontSize: theme.uiFontSize, 8 | }, 9 | hasError: { 10 | color: theme.brandDanger, 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/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 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/theme", 4 | "packages/forms", 5 | "packages/ui", 6 | "packages/typings" 7 | ], 8 | "version": "independent", 9 | "ignoreChanges": [ 10 | "**/*.md", 11 | "**/.eslintrc.{js,json,yaml,yml}", 12 | "**/package-lock.json" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/ui/src/typings/generic.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@meetfranz/theme/lib'; 2 | 3 | export interface IWithStyle { 4 | classes: any; 5 | theme: Theme; 6 | } 7 | 8 | export type Merge = Omit> & N; 9 | export type Omit = Pick>; 10 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /docs/example-feature/state.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | const defaultState = { 4 | name: null, 5 | isFeatureActive: false, 6 | }; 7 | 8 | export const exampleFeatureState = observable(defaultState); 9 | 10 | export function resetState() { 11 | Object.assign(exampleFeatureState, defaultState); 12 | } 13 | 14 | export default exampleFeatureState; 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/example-feature/actions.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { createActionsFromDefinitions } from '../../src/actions/lib/actions'; 3 | 4 | export const exampleFeatureActions = createActionsFromDefinitions({ 5 | greet: { 6 | name: PropTypes.string.isRequired, 7 | }, 8 | }, PropTypes.checkPropTypes); 9 | 10 | export default exampleFeatureActions; 11 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "dev", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | } 11 | }, 12 | { 13 | "type": "npm", 14 | "script": "lint", 15 | "group": "test" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /.github/FEATURE_PROPOSAL_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Feature Description 4 | 5 | 6 | ### Motivation and Context 7 | 12 | 13 | ### Mockups, Screenshots (if available): 14 | 15 | -------------------------------------------------------------------------------- /packages/forms/src/typings/generic.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@meetfranz/theme/lib'; 2 | 3 | export interface IFormField { 4 | showLabel?: boolean; 5 | label?: string; 6 | error?: string; 7 | required?: boolean; 8 | noMargin?: boolean; 9 | } 10 | 11 | export interface IWithStyle { 12 | classes: any; 13 | theme: Theme; 14 | } 15 | 16 | export type Merge = Omit> & N; 17 | export type Omit = Pick>; 18 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /packages/theme/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as darkThemeConfig from './themes/dark'; 2 | import * as defaultThemeConfig from './themes/default'; 3 | import * as legacyStyles from './themes/legacy'; 4 | 5 | export enum ThemeType { 6 | default = 'default', 7 | dark = 'dark', 8 | } 9 | 10 | export function theme(themeId: ThemeType) { 11 | if (themeId === ThemeType.dark) { 12 | return Object.assign({}, defaultThemeConfig, darkThemeConfig, { legacyStyles }); 13 | } 14 | 15 | return Object.assign({}, defaultThemeConfig, { legacyStyles }); 16 | } 17 | 18 | export type Theme = typeof defaultThemeConfig; 19 | -------------------------------------------------------------------------------- /packages/forms/src/error/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import injectSheet from 'react-jss'; 3 | 4 | import styles from './styles'; 5 | 6 | interface IProps { 7 | classes: any; 8 | message: string; 9 | } 10 | 11 | class ErrorComponent extends Component { 12 | render() { 13 | const { 14 | classes, 15 | message, 16 | } = this.props; 17 | 18 | return ( 19 |

22 | {message} 23 |

24 | ); 25 | } 26 | } 27 | 28 | export const Error = injectSheet(styles)(ErrorComponent); 29 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/typings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@meetfranz/typings", 3 | "version": "0.0.11", 4 | "description": "TypeScript typings for internal and external projects", 5 | "author": "Stefan Malzner ", 6 | "homepage": "https://github.com/meetfranz/franz", 7 | "license": "Apache-2.0", 8 | "directories": { 9 | "types": "types" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/meetfranz/franz.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/meetfranz/franz/issues" 20 | }, 21 | "gitHead": "e9b9079dc921e85961954727a7b2a8eabe5b9798" 22 | } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /packages/theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@meetfranz/theme", 3 | "version": "1.0.14", 4 | "description": "Theme configuration for Franz", 5 | "author": "Stefan Malzner ", 6 | "homepage": "https://github.com/meetfranz/franz", 7 | "license": "Apache-2.0", 8 | "main": "lib/index.js", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/meetfranz/franz.git" 15 | }, 16 | "scripts": { 17 | "dev": "tsc -w", 18 | "build": "tsc" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/meetfranz/franz/issues" 22 | }, 23 | "dependencies": { 24 | "color": "^3.1.0" 25 | }, 26 | "gitHead": "9f2ab40b7602bc3df26ebb093b484b9917768f69" 27 | } 28 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "electron": 4 8 | } 9 | } 10 | ], 11 | "@babel/react" 12 | ], 13 | "plugins": [ 14 | "react-require", 15 | [ 16 | "@babel/plugin-proposal-decorators", 17 | { 18 | "legacy": true 19 | } 20 | ], 21 | "@babel/proposal-export-default-from", 22 | [ 23 | "@babel/proposal-class-properties", 24 | { 25 | "loose": true 26 | } 27 | ], 28 | "@babel/proposal-throw-expressions", 29 | "@babel/syntax-dynamic-import", 30 | ["react-intl", { 31 | "messagesDir": "./src/i18n/messages/", 32 | "enforceDescriptions": false, 33 | "extractSourceLocation": true 34 | }] 35 | ], 36 | "sourceMaps": "inline" 37 | } 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@meetfranz/ui", 3 | "version": "1.1.0", 4 | "description": "React UI components for Franz", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "dev": "tsc -w", 8 | "build": "tsc" 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/meetfranz/franz.git" 16 | }, 17 | "keywords": [ 18 | "Franz", 19 | "Forms", 20 | "React", 21 | "UI" 22 | ], 23 | "author": "Stefan Malzner ", 24 | "license": "Apache-2.0", 25 | "dependencies": { 26 | "@mdi/react": "^1.1.0", 27 | "@meetfranz/theme": "^1.0.14", 28 | "react-loader": "^2.4.5" 29 | }, 30 | "peerDependencies": { 31 | "classnames": "^2.2.6", 32 | "react": "^16.7.0", 33 | "react-dom": "16.7.0", 34 | "react-jss": "^8.6.1" 35 | }, 36 | "gitHead": "254da30f801169fac376bda1439b46cabbb491ad" 37 | } 38 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/example-feature/store.js: -------------------------------------------------------------------------------- 1 | import { action, observable, reaction } from 'mobx'; 2 | import Store from '../../src/stores/lib/Store'; 3 | import Request from '../../src/stores/lib/Request'; 4 | 5 | const debug = require('debug')('Franz:feature:EXAMPLE_FEATURE:store'); 6 | 7 | export class ExampleFeatureStore extends Store { 8 | @observable getNameRequest = new Request(this.api, 'getName'); 9 | 10 | constructor(stores, api, actions, state) { 11 | super(stores, api, actions); 12 | this.state = state; 13 | } 14 | 15 | setup() { 16 | debug('fetching name from api'); 17 | this.getNameRequest.execute(); 18 | 19 | // Update the name on the state when the request resolved 20 | reaction( 21 | () => this.getNameRequest.result, 22 | name => this._setName(name), 23 | ); 24 | } 25 | 26 | @action _setName = (name) => { 27 | debug('setting name', name); 28 | this.state.name = name; 29 | }; 30 | } 31 | 32 | export default ExampleFeatureStore; 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /packages/forms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@meetfranz/forms", 3 | "version": "1.2.1", 4 | "description": "React form components for Franz", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "dev": "tsc -w", 8 | "build": "tsc" 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/meetfranz/franz.git" 16 | }, 17 | "keywords": [ 18 | "Franz", 19 | "Forms", 20 | "React", 21 | "UI" 22 | ], 23 | "author": "Stefan Malzner ", 24 | "license": "Apache-2.0", 25 | "dependencies": { 26 | "@mdi/js": "^3.3.92", 27 | "@mdi/react": "^1.1.0", 28 | "@meetfranz/theme": "^1.0.14", 29 | "react-html-attributes": "^1.4.3", 30 | "react-loader": "^2.4.5" 31 | }, 32 | "peerDependencies": { 33 | "classnames": "^2.2.6", 34 | "react": "^16.7.0", 35 | "react-dom": "16.7.0", 36 | "react-jss": "^8.6.1" 37 | }, 38 | "gitHead": "00db2bddccb8bb8ad7d29b8d032876c798b8bbf3" 39 | } 40 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /packages/forms/src/wrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import React, { Component } from 'react'; 3 | import injectStyle from 'react-jss'; 4 | import { IWithStyle } from '../typings/generic'; 5 | 6 | interface IProps extends IWithStyle { 7 | children: React.ReactNode; 8 | className?: string; 9 | identifier: string; 10 | noMargin?: boolean; 11 | } 12 | 13 | const styles = { 14 | container: { 15 | marginBottom: (props: IProps) => props.noMargin ? 0 : 20, 16 | }, 17 | }; 18 | 19 | class WrapperComponent extends Component { 20 | render() { 21 | const { 22 | children, 23 | classes, 24 | className, 25 | identifier, 26 | } = this.props; 27 | 28 | return ( 29 |
    36 | {children} 37 |
    38 | ); 39 | } 40 | } 41 | 42 | export const Wrapper = injectStyle(styles)(WrapperComponent); 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |