├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FEATURE_PROPOSAL_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml └── workflows │ └── build.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build-helpers ├── default.entitlements.mac.plist ├── ensure-mac-dependency.js └── images │ ├── dmgInstaller.tiff │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png │ ├── win-app-ico.ico │ └── win-installer-loading-splash.gif ├── docs ├── example-feature │ ├── actions.js │ ├── api.js │ ├── index.js │ ├── state.js │ └── store.js └── linux.md ├── electron-builder.yml ├── gulpfile.babel.js ├── jest.config.js ├── lerna.json ├── misty.yml ├── package-lock.json ├── package.json ├── packages ├── forms │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── button │ │ │ └── index.tsx │ │ ├── error │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.ts │ │ ├── input │ │ │ ├── index.tsx │ │ │ ├── scorePassword.ts │ │ │ └── styles.ts │ │ ├── label │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── select │ │ │ └── index.tsx │ │ ├── textarea │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── toggle │ │ │ └── index.tsx │ │ ├── typings │ │ │ └── generic.ts │ │ └── wrapper │ │ │ └── index.tsx │ ├── tsconfig.json │ └── tslint.json ├── misty.yml ├── theme │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── themes │ │ │ ├── dark │ │ │ └── index.ts │ │ │ ├── default │ │ │ └── index.ts │ │ │ └── legacy │ │ │ └── index.ts │ ├── tsconfig.json │ └── tslint.json ├── typings │ ├── package.json │ └── types │ │ ├── mobx-react-form.d.ts │ │ ├── react-html-attributes.d.ts │ │ ├── react-jss.d.ts │ │ └── react-loader.d.ts └── ui │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── badge │ │ ├── ProBadge.tsx │ │ └── index.tsx │ ├── headline │ │ └── index.tsx │ ├── icon │ │ └── index.tsx │ ├── index.ts │ ├── infobox │ │ └── index.tsx │ ├── loader │ │ └── index.tsx │ └── typings │ │ └── generic.ts │ ├── tsconfig.json │ └── tslint.json ├── src ├── I18n.js ├── actions │ ├── app.js │ ├── index.js │ ├── lib │ │ └── actions.js │ ├── news.js │ ├── payment.js │ ├── recipe.js │ ├── recipePreview.js │ ├── requests.js │ ├── service.js │ ├── settings.js │ ├── ui.js │ └── user.js ├── api │ ├── AppApi.js │ ├── FeaturesApi.js │ ├── LocalApi.js │ ├── NewsApi.js │ ├── PaymentApi.js │ ├── RecipePreviewsApi.js │ ├── RecipesApi.js │ ├── ServicesApi.js │ ├── UserApi.js │ ├── index.js │ ├── server │ │ ├── LocalApi.js │ │ └── ServerApi.js │ └── utils │ │ └── auth.js ├── app.js ├── assets │ ├── fonts │ │ ├── OpenSans-Bold.ttf │ │ ├── OpenSans-BoldItalic.ttf │ │ ├── OpenSans-ExtraBold.ttf │ │ ├── OpenSans-ExtraBoldItalic.ttf │ │ ├── OpenSans-Light.ttf │ │ └── OpenSans-Regular.ttf │ └── images │ │ ├── adlk.svg │ │ ├── emoji │ │ ├── dontknow.png │ │ ├── sad.png │ │ └── star.png │ │ ├── logo.svg │ │ ├── sm.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 │ │ └── tray │ │ ├── darwin-dark │ │ ├── tray-active.png │ │ ├── tray-active@2x.png │ │ ├── tray-unread-active.png │ │ ├── tray-unread-active@2x.png │ │ ├── tray-unread.png │ │ ├── tray-unread@2x.png │ │ ├── tray.png │ │ └── tray@2x.png │ │ ├── darwin │ │ ├── tray-unread.png │ │ ├── tray-unread@2x.png │ │ ├── tray.png │ │ └── tray@2x.png │ │ ├── linux │ │ ├── tray-unread.png │ │ ├── tray-unread@2x.png │ │ ├── tray.png │ │ └── tray@2x.png │ │ └── win32 │ │ ├── tray-unread.ico │ │ └── tray.ico ├── components │ ├── AppUpdateInfoBar.js │ ├── TrialActivationInfoBar.js │ ├── auth │ │ ├── AuthLayout.js │ │ ├── Import.js │ │ ├── Invite.js │ │ ├── Login.js │ │ ├── Password.js │ │ ├── Pricing.js │ │ ├── SetupAssistant.js │ │ ├── Signup.js │ │ └── Welcome.js │ ├── layout │ │ ├── AppLayout.js │ │ └── Sidebar.js │ ├── services │ │ ├── content │ │ │ ├── ErrorHandlers │ │ │ │ ├── WebviewErrorHandler.js │ │ │ │ └── styles.js │ │ │ ├── ServiceDisabled.js │ │ │ ├── ServiceRestricted.js │ │ │ ├── ServiceView.js │ │ │ ├── Services.js │ │ │ └── WebviewCrashHandler.js │ │ └── tabs │ │ │ ├── TabBarSortableList.js │ │ │ ├── TabItem.js │ │ │ └── Tabbar.js │ ├── settings │ │ ├── SettingsLayout.js │ │ ├── account │ │ │ └── AccountDashboard.js │ │ ├── navigation │ │ │ └── SettingsNavigation.js │ │ ├── recipes │ │ │ ├── RecipeItem.js │ │ │ └── RecipesDashboard.js │ │ ├── services │ │ │ ├── EditServiceForm.js │ │ │ ├── ServiceError.js │ │ │ ├── ServiceItem.js │ │ │ └── ServicesDashboard.js │ │ ├── settings │ │ │ └── EditSettingsForm.js │ │ ├── team │ │ │ └── TeamDashboard.js │ │ └── user │ │ │ └── EditUserForm.js │ ├── subscription │ │ ├── SubscriptionForm.js │ │ ├── SubscriptionPopup.js │ │ └── TrialForm.js │ ├── ui │ │ ├── ActivateTrialButton │ │ │ └── index.js │ │ ├── AppLoader │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Button.js │ │ ├── FeatureItem.js │ │ ├── FeatureList.js │ │ ├── FullscreenLoader │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── ImageUpload.js │ │ ├── InfoBar.js │ │ ├── Infobox.js │ │ ├── Input.js │ │ ├── Link.js │ │ ├── Loader.js │ │ ├── Modal │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── News.js │ │ ├── PremiumFeatureContainer │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Radio.js │ │ ├── SearchInput.js │ │ ├── Select.js │ │ ├── ServiceIcon.js │ │ ├── StatusBarTargetUrl.js │ │ ├── Tabs │ │ │ ├── TabItem.js │ │ │ ├── Tabs.js │ │ │ └── index.js │ │ ├── Toggle.js │ │ ├── UpgradeButton │ │ │ └── index.js │ │ ├── WebviewLoader │ │ │ ├── index.js │ │ │ └── styles.js │ │ └── effects │ │ │ └── Appear.js │ └── util │ │ └── ErrorBoundary │ │ ├── index.js │ │ └── styles.js ├── config.js ├── configVanilla.js ├── containers │ ├── auth │ │ ├── AuthLayoutContainer.js │ │ ├── ImportScreen.js │ │ ├── InviteScreen.js │ │ ├── LoginScreen.js │ │ ├── PasswordScreen.js │ │ ├── PricingScreen.js │ │ ├── SetupAssistantScreen.js │ │ ├── SignupScreen.js │ │ └── WelcomeScreen.js │ ├── layout │ │ └── AppLayoutContainer.js │ ├── settings │ │ ├── AccountScreen.js │ │ ├── EditServiceScreen.js │ │ ├── EditSettingsScreen.js │ │ ├── EditUserScreen.js │ │ ├── InviteScreen.js │ │ ├── RecipesScreen.js │ │ ├── ServicesScreen.js │ │ ├── SettingsWindow.js │ │ └── TeamScreen.js │ └── subscription │ │ ├── SubscriptionFormScreen.js │ │ └── SubscriptionPopupScreen.js ├── dev-app-update.yml ├── electron │ ├── Settings.js │ ├── deepLinking.js │ ├── exception.js │ ├── ipc-api │ │ ├── appIndicator.js │ │ ├── autoUpdate.js │ │ ├── browserViewManager.ts │ │ ├── cld.js │ │ ├── desktopCapturer.ts │ │ ├── focusState.js │ │ ├── fullscreen.js │ │ ├── index.js │ │ ├── macOSPermissions.ts │ │ ├── overlayWindow.ts │ │ ├── serviceCache.js │ │ ├── settings.js │ │ └── subscriptionWindow.js │ ├── macOSPermissions.js │ ├── serviceContextMenuTemplate.ts │ └── windowUtils.js ├── environment.js ├── features │ ├── announcements │ │ ├── actions.js │ │ ├── api.js │ │ ├── components │ │ │ └── AnnouncementScreen.js │ │ ├── index.js │ │ └── store.js │ ├── appMenu │ │ └── index.js │ ├── basicAuth │ │ ├── Component.js │ │ ├── Form.js │ │ ├── index.js │ │ ├── mainIpcHandler.js │ │ └── styles.js │ ├── communityRecipes │ │ ├── index.js │ │ └── store.js │ ├── delayApp │ │ ├── Component.js │ │ ├── api.js │ │ ├── index.js │ │ ├── store.js │ │ └── styles.js │ ├── desktopCapturer │ │ ├── Component.js │ │ ├── config.js │ │ ├── index.js │ │ └── sourceItem.js │ ├── planSelection │ │ ├── actions.js │ │ ├── api.js │ │ ├── components │ │ │ ├── PlanItem.js │ │ │ └── PlanSelection.js │ │ ├── containers │ │ │ └── PlanSelectionScreen.js │ │ ├── index.js │ │ └── store.js │ ├── serviceLimit │ │ ├── components │ │ │ └── LimitReachedInfobox.js │ │ ├── index.js │ │ └── store.js │ ├── serviceProxy │ │ └── index.js │ ├── shareFranz │ │ ├── Component.js │ │ └── index.js │ ├── spellchecker │ │ └── index.js │ ├── todos │ │ ├── actions.js │ │ ├── components │ │ │ └── TodosWebview.js │ │ ├── constants.js │ │ ├── containers │ │ │ └── TodosScreen.js │ │ ├── index.js │ │ ├── preload.js │ │ └── store.js │ ├── trialStatusBar │ │ ├── actions.js │ │ ├── components │ │ │ ├── ProgressBar.js │ │ │ └── TrialStatusBar.js │ │ ├── containers │ │ │ └── TrialStatusBarScreen.js │ │ ├── index.js │ │ └── store.js │ ├── utils │ │ ├── ActionBinding.js │ │ ├── FeatureStore.js │ │ └── FeatureStore.test.js │ ├── webControls │ │ ├── components │ │ │ └── WebControls.js │ │ ├── constants.js │ │ └── containers │ │ │ └── WebControlsScreen.js │ └── workspaces │ │ ├── actions.js │ │ ├── api.js │ │ ├── components │ │ ├── CreateWorkspaceForm.js │ │ ├── EditWorkspaceForm.js │ │ ├── WorkspaceDrawer.js │ │ ├── WorkspaceDrawerItem.js │ │ ├── WorkspaceItem.js │ │ ├── WorkspaceServiceListItem.js │ │ ├── WorkspaceSwitchingIndicator.js │ │ └── WorkspacesDashboard.js │ │ ├── containers │ │ ├── EditWorkspaceScreen.js │ │ └── WorkspacesScreen.js │ │ ├── index.js │ │ ├── models │ │ └── Workspace.js │ │ └── store.js ├── helpers │ ├── array-helpers.js │ ├── asar-helpers.js │ ├── async-helpers.js │ ├── i18n-helpers.js │ ├── password-helpers.js │ ├── plan-helpers.js │ ├── recipe-helpers.js │ ├── routing-helpers.js │ ├── service-helpers.js │ ├── url-helpers.js │ ├── userAgent-helpers.js │ ├── validation-helpers.js │ └── visibility-helper.js ├── i18n │ ├── globalMessages.js │ ├── languages.js │ ├── locales │ │ ├── af.json │ │ ├── ar.json │ │ ├── bg.json │ │ ├── bs.json │ │ ├── ca.json │ │ ├── cs.json │ │ ├── da.json │ │ ├── de.json │ │ ├── defaultMessages.json │ │ ├── el.json │ │ ├── en-US.json │ │ ├── es.json │ │ ├── et.json │ │ ├── fa.json │ │ ├── fi.json │ │ ├── fil.json │ │ ├── fr.json │ │ ├── ga.json │ │ ├── he.json │ │ ├── hi.json │ │ ├── hr.json │ │ ├── hu.json │ │ ├── id.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ka.json │ │ ├── kk.json │ │ ├── ko.json │ │ ├── ms.json │ │ ├── nb-NO.json │ │ ├── nb.json │ │ ├── nl-BE.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── pt-BR.json │ │ ├── pt.json │ │ ├── ro.json │ │ ├── ru.json │ │ ├── si.json │ │ ├── sk.json │ │ ├── sl.json │ │ ├── sq.json │ │ ├── sr.json │ │ ├── sv.json │ │ ├── tr.json │ │ ├── ua.json │ │ ├── uk.json │ │ ├── vi.json │ │ ├── whitelist_en-US.json │ │ ├── zh-HANS.json │ │ └── zh-TW.json │ ├── manage-translations.js │ └── translations.js ├── index.html ├── index.js ├── ipcChannels.ts ├── lib │ ├── Form.js │ ├── Menu.js │ ├── TouchBar.js │ ├── Tray.js │ ├── analytics.js │ └── download.ts ├── models │ ├── News.js │ ├── Order.js │ ├── Plan.js │ ├── Recipe.js │ ├── RecipePreview.js │ ├── Service.js │ ├── ServiceBrowserView.ts │ └── User.js ├── overlay.html ├── overlayApp.js ├── prop-types.js ├── stores │ ├── AppStore.js │ ├── FeaturesStore.js │ ├── GlobalErrorStore.js │ ├── NewsStore.js │ ├── PaymentStore.js │ ├── RecipePreviewsStore.js │ ├── RecipesStore.js │ ├── RequestStore.js │ ├── ServicesStore.js │ ├── SettingsStore.js │ ├── UIStore.js │ ├── UserStore.js │ ├── index.js │ └── lib │ │ ├── CachedRequest.js │ │ ├── Reaction.js │ │ ├── Request.js │ │ └── Store.js ├── styles │ ├── animations.scss │ ├── auth.scss │ ├── badge.scss │ ├── button.scss │ ├── colors.scss │ ├── config.scss │ ├── content-tabs.scss │ ├── fonts.scss │ ├── image-upload.scss │ ├── info-bar.scss │ ├── infobox.scss │ ├── input.scss │ ├── invite.scss │ ├── layout.scss │ ├── main.scss │ ├── mixins.scss │ ├── radio.scss │ ├── recipes.scss │ ├── reset.scss │ ├── searchInput.scss │ ├── select.scss │ ├── service-table.scss │ ├── services.scss │ ├── settings.scss │ ├── status-bar-target-url.scss │ ├── subscription-popup.scss │ ├── subscription.scss │ ├── tabs.scss │ ├── toggle.scss │ ├── tooltip.scss │ ├── type-helper.scss │ ├── type.scss │ ├── util.scss │ └── welcome.scss ├── theme │ └── default │ │ └── legacy.js └── webview │ ├── darkmode.js │ ├── desktopCapturer.js │ ├── lib │ └── RecipeWebview.js │ ├── notifications.js │ ├── recipe.js │ ├── spellchecker.js │ └── zoom.js ├── tsconfig.json ├── tsconfig.settings.json ├── tslint.json ├── types.d.ts ├── uidev ├── src │ ├── app.html │ ├── app.tsx │ ├── index.tsx │ ├── stores │ │ ├── index.ts │ │ └── stories.ts │ ├── stories │ │ ├── badge.stories.tsx │ │ ├── button.stories.tsx │ │ ├── headline.stories.tsx │ │ ├── icon.stories.tsx │ │ ├── infobox.stories.tsx │ │ ├── input.stories.tsx │ │ ├── loader.stories.tsx │ │ ├── select.stories.tsx │ │ ├── textarea.stories.tsx │ │ └── toggle.stories.tsx │ └── withTheme │ │ └── index.tsx ├── tsconfig.json ├── tslint.json └── webpack.config.js └── webpack.config.base.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | out/ 3 | packages/*/lib 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ### Expected Behavior 8 | 9 | 10 | 11 | ### Current Behavior 12 | 13 | 14 | 15 | ### Screenshots (if appropriate): 16 | 17 | ### Possible Solution 18 | 19 | 20 | 21 | ### Steps to Reproduce (for bugs) 22 | 23 | 24 | 1. 25 | 2. 26 | 3. 27 | 4. 28 | 29 | ### Context 30 | 31 | 32 | 33 | ### Your Environment 34 | 35 | * Franz Version used: 36 | * Operating System and version: 37 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | ### Motivation and Context 7 | 8 | 9 | 10 | ### How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ### Screenshots (if appropriate): 16 | 17 | ### Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 22 | 23 | ### Checklist: 24 | 25 | 26 | - [ ] My code follows the code style of this project (run `$ yarn lint`). 27 | 29 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 365 # 1 year 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: -1 # Close the issue almost immediately. See: https://github.com/probot/stale/issues/131 9 | 10 | # Issues with these labels will never be considered stale 11 | exemptLabels: 12 | - blocker 13 | - security 14 | - feature request 15 | - bug 16 | 17 | # Label to use when marking an issue as stale 18 | staleLabel: "[Status] Stale" 19 | 20 | # Comment to post when marking an issue as stale. Set to `false` to disable 21 | markComment: > 22 | This issue has been automatically marked as stale because it has not had 23 | recent activity. It will be closed if no further activity occurs. Thank you 24 | for your contributions. 25 | 26 | # Comment to post when closing a stale issue. Set to `false` to disable 27 | closeComment: false 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | legacy-peer-deps = true 3 | # python = python2.7 -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.0.0 2 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of the Franz project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 4 | 5 | Communication through GitHub, Slack, email or any other channel must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 6 | 7 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same. 8 | 9 | If any member of the community violates this code of conduct, the maintainers of the Franz project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. 10 | 11 | If you are subject to or witness unacceptable behavior, or have any other concerns, please open an issue or send an email to [Stefan](stefan@adlk.io). 12 | -------------------------------------------------------------------------------- /build-helpers/default.entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.disable-library-validation 8 | 9 | com.apple.security.device.camera 10 | 11 | com.apple.security.device.audio-input 12 | 13 | com.apple.security.cs.allow-jit 14 | 15 | com.apple.security.automation.apple-events 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /build-helpers/images/dmgInstaller.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/dmgInstaller.tiff -------------------------------------------------------------------------------- /build-helpers/images/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icon.icns -------------------------------------------------------------------------------- /build-helpers/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icon.ico -------------------------------------------------------------------------------- /build-helpers/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icon.png -------------------------------------------------------------------------------- /build-helpers/images/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/1024x1024.png -------------------------------------------------------------------------------- /build-helpers/images/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/128x128.png -------------------------------------------------------------------------------- /build-helpers/images/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/16x16.png -------------------------------------------------------------------------------- /build-helpers/images/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/24x24.png -------------------------------------------------------------------------------- /build-helpers/images/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/256x256.png -------------------------------------------------------------------------------- /build-helpers/images/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/32x32.png -------------------------------------------------------------------------------- /build-helpers/images/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/48x48.png -------------------------------------------------------------------------------- /build-helpers/images/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/512x512.png -------------------------------------------------------------------------------- /build-helpers/images/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/64x64.png -------------------------------------------------------------------------------- /build-helpers/images/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/icons/96x96.png -------------------------------------------------------------------------------- /build-helpers/images/win-app-ico.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/win-app-ico.ico -------------------------------------------------------------------------------- /build-helpers/images/win-installer-loading-splash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/build-helpers/images/win-installer-loading-splash.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/example-feature/api.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async getName() { 3 | return Promise.resolve('Franz'); 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/example-feature/index.js: -------------------------------------------------------------------------------- 1 | import { reaction, runInAction } from 'mobx'; 2 | import { ExampleFeatureStore } from './store'; 3 | import state, { resetState } from './state'; 4 | import api from './api'; 5 | 6 | const debug = require('debug')('Franz:feature:EXAMPLE_FEATURE'); 7 | 8 | let store = null; 9 | 10 | export default function initAnnouncements(stores, actions) { 11 | const { features } = stores; 12 | 13 | // Toggle workspace feature 14 | reaction( 15 | () => ( 16 | features.features.isExampleFeatureEnabled 17 | ), 18 | (isEnabled) => { 19 | if (isEnabled) { 20 | debug('Initializing `EXAMPLE_FEATURE` feature'); 21 | store = new ExampleFeatureStore(stores, api, actions, state); 22 | store.initialize(); 23 | runInAction(() => { state.isFeatureActive = true; }); 24 | } else if (store) { 25 | debug('Disabling `EXAMPLE_FEATURE` feature'); 26 | runInAction(() => { state.isFeatureActive = false; }); 27 | store.teardown(); 28 | store = null; 29 | resetState(); // Reset state to default 30 | } 31 | }, 32 | { 33 | fireImmediately: true, 34 | }, 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['src'], 3 | }; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /misty.yml: -------------------------------------------------------------------------------- 1 | code: 2 | cmd: npm run dev 3 | 4 | app: 5 | cmd: npx electron ./build 6 | waitOn: http://localhost:8000 -------------------------------------------------------------------------------- /packages/forms/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /packages/forms/src/input/scorePassword.ts: -------------------------------------------------------------------------------- 1 | interface ILetters { 2 | [key: string]: number; 3 | } 4 | 5 | interface IVariations { 6 | [index: string]: boolean; 7 | digits: boolean; 8 | lower: boolean; 9 | nonWords: boolean; 10 | upper: boolean; 11 | } 12 | 13 | export function scorePasswordFunc(password: string): number { 14 | let score: number = 0; 15 | if (!password) { 16 | return score; 17 | } 18 | 19 | // award every unique letter until 5 repetitions 20 | const letters: ILetters = {}; 21 | for (let i = 0; i < password.length; i += 1) { 22 | letters[password[i]] = (letters[password[i]] || 0) + 1; 23 | score += 5.0 / letters[password[i]]; 24 | } 25 | 26 | // bonus points for mixing it up 27 | const variations: IVariations = { 28 | digits: /\d/.test(password), 29 | lower: /[a-z]/.test(password), 30 | nonWords: /\W/.test(password), 31 | upper: /[A-Z]/.test(password), 32 | }; 33 | 34 | let variationCount = 0; 35 | Object.keys(variations).forEach((key) => { 36 | variationCount += (variations[key] === true) ? 1 : 0; 37 | }); 38 | 39 | score += (variationCount - 1) * 10; 40 | 41 | return Math.round(score); 42 | } 43 | -------------------------------------------------------------------------------- /packages/forms/src/label/index.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import React, { Component } from 'react'; 3 | import injectSheet from 'react-jss'; 4 | 5 | import { IFormField } from '../typings/generic'; 6 | 7 | import styles from './styles'; 8 | 9 | interface ILabel extends IFormField, React.LabelHTMLAttributes { 10 | classes: any; 11 | isRequired: boolean; 12 | } 13 | 14 | class LabelComponent extends Component { 15 | static defaultProps = { 16 | showLabel: true, 17 | }; 18 | 19 | render() { 20 | const { 21 | title, 22 | showLabel, 23 | classes, 24 | className, 25 | children, 26 | htmlFor, 27 | isRequired, 28 | } = this.props; 29 | 30 | if (!showLabel) return children; 31 | 32 | return ( 33 | 49 | ); 50 | } 51 | } 52 | 53 | export const Label = injectSheet(styles)(LabelComponent); 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/forms/src/textarea/styles.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@meetfranz/theme'; 2 | 3 | export default (theme: Theme) => ({ 4 | label: { 5 | '& > div': { 6 | marginTop: 5, 7 | }, 8 | }, 9 | disabled: { 10 | opacity: theme.inputDisabledOpacity, 11 | }, 12 | formModifier: { 13 | background: 'none', 14 | border: 0, 15 | borderLeft: theme.inputBorder, 16 | padding: '4px 20px 0', 17 | outline: 'none', 18 | 19 | '&:active': { 20 | opacity: 0.5, 21 | }, 22 | 23 | '& svg': { 24 | fill: theme.inputModifierColor, 25 | }, 26 | }, 27 | textarea: { 28 | background: 'none', 29 | border: 0, 30 | fontSize: theme.uiFontSize, 31 | outline: 'none', 32 | padding: 8, 33 | width: '100%', 34 | color: theme.inputColor, 35 | 36 | '&::placeholder': { 37 | color: theme.inputPlaceholderColor, 38 | }, 39 | }, 40 | wrapper: { 41 | background: theme.inputBackground, 42 | border: theme.inputBorder, 43 | borderRadius: theme.borderRadiusSmall, 44 | boxSizing: 'border-box', 45 | display: 'flex', 46 | order: 1, 47 | width: '100%', 48 | }, 49 | hasError: { 50 | borderColor: theme.brandDanger, 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/forms/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/theme/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib 3 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/theme/src/themes/legacy/index.ts: -------------------------------------------------------------------------------- 1 | /* legacy config, injected into sass */ 2 | export const themeBrandPrimary = '#3498db'; 3 | export const themeBrandSuccess = '#5cb85c'; 4 | export const themeBrandInfo = '#5bc0de'; 5 | export const themeBrandWarning = '#FF9F00'; 6 | export const themeBrandDanger = '#d9534f'; 7 | 8 | export const themeGrayDark = '#373a3c'; 9 | export const themeGray = '#55595c'; 10 | export const themeGrayLight = '#818a91'; 11 | export const themeGrayLighter = '#eceeef'; 12 | export const themeGrayLightest = '#f7f7f9'; 13 | 14 | export const themeBorderRadius = '6px'; 15 | export const themeBorderRadiusSmall = '3px'; 16 | 17 | export const themeSidebarWidth = '68px'; 18 | 19 | export const themeTextColor = themeGrayDark; 20 | 21 | export const themeTransitionTime = '.5s'; 22 | 23 | export const themeInsetShadow = 'inset 0 2px 5px rgba(0, 0, 0, .03)'; 24 | 25 | export const darkThemeBlack = '#1A1A1A'; 26 | 27 | export const darkThemeGrayDarkest = '#1E1E1E'; 28 | export const darkThemeGrayDarker = '#2D2F31'; 29 | export const darkThemeGrayDark = '#383A3B'; 30 | 31 | export const darkThemeGray = '#47494B'; 32 | 33 | export const darkThemeGrayLight = '#515355'; 34 | export const darkThemeGrayLighter = '#8a8b8b'; 35 | export const darkThemeGrayLightest = '#FFFFFF'; 36 | 37 | export const darkThemeGraySmoke = '#CED0D1'; 38 | export const darkThemeTextColor = '#FFFFFF'; 39 | -------------------------------------------------------------------------------- /packages/theme/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src", 6 | "allowJs": true 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/theme/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/typings/types/mobx-react-form.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mobx-react-form'; 2 | -------------------------------------------------------------------------------- /packages/typings/types/react-html-attributes.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-html-attributes'; 2 | -------------------------------------------------------------------------------- /packages/typings/types/react-jss.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-jss'; 2 | -------------------------------------------------------------------------------- /packages/typings/types/react-loader.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-loader 2.4 2 | // Project: https://github.com/quickleft/react-loader 3 | // Definitions by: Sudarsan Balaji 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // TypeScript Version: 2.8 6 | 7 | import { Component } from 'react'; 8 | 9 | interface LoaderOptions { 10 | lines?: number; 11 | length?: number; 12 | width?: number; 13 | radius?: number; 14 | scale?: number; 15 | corners?: number; 16 | color?: string; 17 | opacity?: number; 18 | rotate?: number; 19 | direction?: number; 20 | speed?: number; 21 | trail?: number; 22 | fps?: number; 23 | zIndex?: number; 24 | top?: string; 25 | left?: string; 26 | shadow?: boolean; 27 | hwaccel?: boolean; 28 | position?: string; 29 | loadedClassName?: string; 30 | parentClassName?: string; 31 | } 32 | 33 | interface LoaderProps extends LoaderOptions { 34 | loaded: boolean; 35 | options?: LoaderOptions; 36 | className?: string; 37 | } 38 | 39 | declare class ReactLoader extends Component { 40 | } 41 | 42 | declare namespace ReactLoader { 43 | } 44 | 45 | export = ReactLoader; 46 | -------------------------------------------------------------------------------- /packages/ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/ui/src/badge/ProBadge.tsx: -------------------------------------------------------------------------------- 1 | import { mdiStar } from '@mdi/js'; 2 | import { Theme } from '@meetfranz/theme'; 3 | import classnames from 'classnames'; 4 | import React, { Component } from 'react'; 5 | import injectStyle from 'react-jss'; 6 | 7 | import { Badge, Icon } from '../'; 8 | import { IWithStyle } from '../typings/generic'; 9 | 10 | interface IProps extends IWithStyle { 11 | badgeClasses?: string; 12 | iconClasses?: string; 13 | inverted?: boolean; 14 | className?: string; 15 | } 16 | 17 | const styles = (theme: Theme) => ({ 18 | badge: { 19 | height: 'auto', 20 | padding: [4, 6, 2, 7], 21 | borderRadius: theme.borderRadiusSmall, 22 | }, 23 | invertedBadge: { 24 | background: theme.styleTypes.primary.contrast, 25 | color: theme.styleTypes.primary.accent, 26 | }, 27 | icon: { 28 | fill: theme.styleTypes.primary.contrast, 29 | }, 30 | invertedIcon: { 31 | fill: theme.styleTypes.primary.accent, 32 | }, 33 | }); 34 | 35 | class ProBadgeComponent extends Component { 36 | render() { 37 | const { 38 | classes, 39 | badgeClasses, 40 | iconClasses, 41 | inverted, 42 | className, 43 | } = this.props; 44 | 45 | return ( 46 | 55 | 63 | 64 | ); 65 | } 66 | } 67 | 68 | export const ProBadge = injectStyle(styles)(ProBadgeComponent); 69 | -------------------------------------------------------------------------------- /packages/ui/src/headline/index.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@meetfranz/theme'; 2 | import classnames from 'classnames'; 3 | import React, { Component } from 'react'; 4 | import injectStyle from 'react-jss'; 5 | 6 | import { IWithStyle, Omit } from '../typings/generic'; 7 | 8 | interface IProps extends IWithStyle { 9 | level?: number; 10 | className?: string; 11 | children: string | React.ReactNode; 12 | id?: string; 13 | } 14 | 15 | const styles = (theme: Theme) => ({ 16 | headline: { 17 | fontWeight: 'lighter', 18 | color: theme.colorText, 19 | marginTop: 0, 20 | marginBottom: 10, 21 | textAlign: 'left', 22 | }, 23 | h1: { 24 | fontSize: 30, 25 | marginTop: 0, 26 | }, 27 | h2: { 28 | fontSize: 20, 29 | }, 30 | h3: { 31 | fontSize: 18, 32 | }, 33 | h4: { 34 | fontSize: theme.uiFontSize, 35 | }, 36 | }); 37 | 38 | class HeadlineComponent extends Component { 39 | render() { 40 | const { 41 | classes, 42 | level, 43 | className, 44 | children, 45 | id, 46 | } = this.props; 47 | 48 | return React.createElement( 49 | `h${level}`, 50 | { 51 | id, 52 | className: classnames({ 53 | [classes.headline]: true, 54 | [classes[level ? `h${level}` : 'h1']]: true, 55 | [`${className}`]: className, 56 | }), 57 | 'data-type': 'franz-headline', 58 | }, 59 | children, 60 | ); 61 | } 62 | } 63 | 64 | const Headline = injectStyle(styles)(HeadlineComponent); 65 | 66 | const createH = (level: number) => (props: Omit) => {props.children}; 67 | 68 | export const H1 = createH(1); 69 | export const H2 = createH(2); 70 | export const H3 = createH(3); 71 | export const H4 = createH(4); 72 | -------------------------------------------------------------------------------- /packages/ui/src/icon/index.tsx: -------------------------------------------------------------------------------- 1 | import MdiIcon from '@mdi/react'; 2 | import { Theme } from '@meetfranz/theme'; 3 | import classnames from 'classnames'; 4 | import React, { Component } from 'react'; 5 | import injectStyle from 'react-jss'; 6 | 7 | import { IWithStyle } from '../typings/generic'; 8 | 9 | interface IProps extends IWithStyle { 10 | icon: string; 11 | size?: number; 12 | className?: string; 13 | } 14 | 15 | const styles = (theme: Theme) => ({ 16 | icon: { 17 | fill: theme.colorText, 18 | }, 19 | }); 20 | 21 | class IconComponent extends Component { 22 | public static defaultProps = { 23 | size: 1, 24 | }; 25 | 26 | render() { 27 | const { 28 | classes, 29 | icon, 30 | size, 31 | className, 32 | } = this.props; 33 | 34 | if (!icon) { 35 | console.warn('No Icon specified'); 36 | } 37 | 38 | return ( 39 | 47 | ); 48 | } 49 | } 50 | 51 | export const Icon = injectStyle(styles)(IconComponent); 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/ui/src/loader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@meetfranz/theme'; 2 | import classnames from 'classnames'; 3 | import React, { Component } from 'react'; 4 | import injectStyle, { withTheme } from 'react-jss'; 5 | import ReactLoader from 'react-loader'; 6 | 7 | import { IWithStyle } from '../typings/generic'; 8 | 9 | interface IProps extends IWithStyle { 10 | className?: string; 11 | color?: string; 12 | } 13 | 14 | const styles = (theme: Theme) => ({ 15 | container: { 16 | position: 'relative', 17 | height: 60, 18 | }, 19 | }); 20 | 21 | class LoaderComponent extends Component { 22 | render() { 23 | const { 24 | classes, 25 | className, 26 | color, 27 | theme, 28 | } = this.props; 29 | 30 | return ( 31 |
38 | 45 |
46 | ); 47 | } 48 | } 49 | 50 | export const Loader = injectStyle(styles)(withTheme(LoaderComponent)); 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "references": [ 8 | { 9 | "path": "../theme" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /src/I18n.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { IntlProvider } from 'react-intl'; 5 | 6 | import { oneOrManyChildElements } from './prop-types'; 7 | import translations from './i18n/translations'; 8 | import UserStore from './stores/UserStore'; 9 | 10 | export default @inject('stores') @observer class I18N extends Component { 11 | // componentDidUpdate() { 12 | // window.franz.menu.rebuild(); 13 | // } 14 | 15 | render() { 16 | const { stores, children } = this.props; 17 | const { locale } = stores.app; 18 | return ( 19 | { window.franz.intl = intlProvider ? intlProvider.getChildContext().intl : null; }} 22 | > 23 | {children} 24 | 25 | ); 26 | } 27 | } 28 | 29 | I18N.wrappedComponent.propTypes = { 30 | stores: PropTypes.shape({ 31 | user: PropTypes.instanceOf(UserStore).isRequired, 32 | }).isRequired, 33 | children: oneOrManyChildElements.isRequired, 34 | }; 35 | -------------------------------------------------------------------------------- /src/actions/app.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | setBadge: { 5 | unreadDirectMessageCount: PropTypes.number.isRequired, 6 | unreadIndirectMessageCount: PropTypes.number, 7 | }, 8 | notify: { 9 | title: PropTypes.string.isRequired, 10 | options: PropTypes.object.isRequired, 11 | serviceId: PropTypes.string, 12 | }, 13 | launchOnStartup: { 14 | enable: PropTypes.bool.isRequired, 15 | }, 16 | openExternalUrl: { 17 | url: PropTypes.string.isRequired, 18 | }, 19 | checkForUpdates: {}, 20 | resetUpdateStatus: {}, 21 | installUpdate: {}, 22 | healthCheck: {}, 23 | muteApp: { 24 | isMuted: PropTypes.bool.isRequired, 25 | overrideSystemMute: PropTypes.bool, 26 | }, 27 | toggleMuteApp: {}, 28 | clearAllCache: {}, 29 | }; 30 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import defineActions from './lib/actions'; 4 | import service from './service'; 5 | import recipe from './recipe'; 6 | import recipePreview from './recipePreview'; 7 | import ui from './ui'; 8 | import app from './app'; 9 | import user from './user'; 10 | import payment from './payment'; 11 | import news from './news'; 12 | import settings from './settings'; 13 | import requests from './requests'; 14 | import announcements from '../features/announcements/actions'; 15 | import workspaces from '../features/workspaces/actions'; 16 | import todos from '../features/todos/actions'; 17 | import planSelection from '../features/planSelection/actions'; 18 | import trialStatusBar from '../features/trialStatusBar/actions'; 19 | 20 | const actions = Object.assign({}, { 21 | service, 22 | recipe, 23 | recipePreview, 24 | ui, 25 | app, 26 | user, 27 | payment, 28 | news, 29 | settings, 30 | requests, 31 | }); 32 | 33 | export default Object.assign( 34 | defineActions(actions, PropTypes.checkPropTypes), 35 | { announcements }, 36 | { workspaces }, 37 | { todos }, 38 | { planSelection }, 39 | { trialStatusBar }, 40 | ); 41 | -------------------------------------------------------------------------------- /src/actions/lib/actions.js: -------------------------------------------------------------------------------- 1 | export const createActionsFromDefinitions = (actionDefinitions, validate) => { 2 | const actions = {}; 3 | Object.keys(actionDefinitions).forEach((actionName) => { 4 | const action = (params = {}) => { 5 | const schema = actionDefinitions[actionName]; 6 | validate(schema, params, actionName); 7 | action.notify(params); 8 | }; 9 | actions[actionName] = action; 10 | action.listeners = []; 11 | action.listen = listener => action.listeners.push(listener); 12 | action.off = (listener) => { 13 | const { listeners } = action; 14 | listeners.splice(listeners.indexOf(listener), 1); 15 | }; 16 | action.notify = params => action.listeners.forEach(listener => listener(params)); 17 | }); 18 | return actions; 19 | }; 20 | 21 | export default (definitions, validate) => { 22 | const newActions = {}; 23 | Object.keys(definitions).forEach((scopeName) => { 24 | newActions[scopeName] = createActionsFromDefinitions(definitions[scopeName], validate); 25 | }); 26 | return newActions; 27 | }; 28 | -------------------------------------------------------------------------------- /src/actions/news.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | hide: { 5 | newsId: PropTypes.string.isRequired, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/actions/payment.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | createHostedPage: { 5 | planId: PropTypes.string.isRequired, 6 | }, 7 | upgradeAccount: { 8 | planId: PropTypes.string.isRequired, 9 | onCloseWindow: PropTypes.func, 10 | overrideParent: PropTypes.number, 11 | }, 12 | createDashboardUrl: {}, 13 | }; 14 | -------------------------------------------------------------------------------- /src/actions/recipe.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | install: { 5 | recipeId: PropTypes.string.isRequired, 6 | update: PropTypes.bool, 7 | }, 8 | update: {}, 9 | }; 10 | -------------------------------------------------------------------------------- /src/actions/recipePreview.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | search: { 5 | needle: PropTypes.string.isRequired, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/actions/requests.js: -------------------------------------------------------------------------------- 1 | export default { 2 | retryRequiredRequests: {}, 3 | }; 4 | -------------------------------------------------------------------------------- /src/actions/settings.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | update: { 5 | type: PropTypes.string.isRequired, 6 | data: PropTypes.object.isRequired, 7 | }, 8 | remove: { 9 | type: PropTypes.string.isRequired, 10 | key: PropTypes.string.isRequired, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/actions/ui.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | openSettings: { 5 | path: PropTypes.string, 6 | }, 7 | closeSettings: {}, 8 | toggleServiceUpdatedInfoBar: { 9 | visible: PropTypes.bool, 10 | }, 11 | hideServices: {}, 12 | showServices: {}, 13 | }; 14 | -------------------------------------------------------------------------------- /src/actions/user.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default { 4 | login: { 5 | email: PropTypes.string.isRequired, 6 | password: PropTypes.string.isRequired, 7 | }, 8 | logout: {}, 9 | signup: { 10 | firstname: PropTypes.string.isRequired, 11 | lastname: PropTypes.string.isRequired, 12 | email: PropTypes.string.isRequired, 13 | password: PropTypes.string.isRequired, 14 | accountType: PropTypes.string, 15 | company: PropTypes.string, 16 | plan: PropTypes.string, 17 | currency: PropTypes.string, 18 | }, 19 | retrievePassword: { 20 | email: PropTypes.string.isRequired, 21 | }, 22 | activateTrial: { 23 | planId: PropTypes.string.isRequired, 24 | }, 25 | invite: { 26 | invites: PropTypes.array.isRequired, 27 | }, 28 | update: { 29 | userData: PropTypes.object.isRequired, 30 | }, 31 | resetStatus: {}, 32 | importLegacyServices: PropTypes.arrayOf(PropTypes.shape({ 33 | recipe: PropTypes.string.isRequired, 34 | })).isRequired, 35 | delete: {}, 36 | }; 37 | -------------------------------------------------------------------------------- /src/api/AppApi.js: -------------------------------------------------------------------------------- 1 | export default class AppApi { 2 | constructor(server) { 3 | this.server = server; 4 | } 5 | 6 | health() { 7 | return this.server.healthCheck(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/api/FeaturesApi.js: -------------------------------------------------------------------------------- 1 | export default class FeaturesApi { 2 | constructor(server) { 3 | this.server = server; 4 | } 5 | 6 | default() { 7 | return this.server.getDefaultFeatures(); 8 | } 9 | 10 | features() { 11 | return this.server.getFeatures(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/api/LocalApi.js: -------------------------------------------------------------------------------- 1 | export default class LocalApi { 2 | constructor(server, local) { 3 | this.server = server; 4 | this.local = local; 5 | } 6 | 7 | getAppSettings(type) { 8 | return this.local.getAppSettings(type); 9 | } 10 | 11 | updateAppSettings(type, data) { 12 | return this.local.updateAppSettings(type, data); 13 | } 14 | 15 | getAppCacheSize() { 16 | return this.local.getAppCacheSize(); 17 | } 18 | 19 | clearAppCache() { 20 | return this.local.clearAppCache(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/api/NewsApi.js: -------------------------------------------------------------------------------- 1 | export default class NewsApi { 2 | constructor(server, local) { 3 | this.server = server; 4 | this.local = local; 5 | } 6 | 7 | latest() { 8 | return this.server.getLatestNews(); 9 | } 10 | 11 | hide(id) { 12 | return this.server.hideNews(id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/api/PaymentApi.js: -------------------------------------------------------------------------------- 1 | export default class PaymentApi { 2 | constructor(server, local) { 3 | this.server = server; 4 | this.local = local; 5 | } 6 | 7 | plans() { 8 | return this.server.getPlans(); 9 | } 10 | 11 | getHostedPage(planId) { 12 | return this.server.getHostedPage(planId); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/api/RecipePreviewsApi.js: -------------------------------------------------------------------------------- 1 | export default class ServicesApi { 2 | constructor(server) { 3 | this.server = server; 4 | } 5 | 6 | all() { 7 | return this.server.getRecipePreviews(); 8 | } 9 | 10 | featured() { 11 | return this.server.getFeaturedRecipePreviews(); 12 | } 13 | 14 | search(needle) { 15 | return this.server.searchRecipePreviews(needle); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/api/RecipesApi.js: -------------------------------------------------------------------------------- 1 | export default class RecipesApi { 2 | constructor(server) { 3 | this.server = server; 4 | } 5 | 6 | all() { 7 | return this.server.getInstalledRecipes(); 8 | } 9 | 10 | install(recipeId) { 11 | return this.server.getRecipePackage(recipeId); 12 | } 13 | 14 | update(recipes) { 15 | return this.server.getRecipeUpdates(recipes); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/api/ServicesApi.js: -------------------------------------------------------------------------------- 1 | export default class ServicesApi { 2 | constructor(server, local) { 3 | this.local = local; 4 | this.server = server; 5 | } 6 | 7 | all() { 8 | return this.server.getServices(); 9 | } 10 | 11 | // one(customerId) { 12 | // return this.server.getCustomer(customerId); 13 | // } 14 | // 15 | // search(needle) { 16 | // return this.server.searchCustomers(needle); 17 | // } 18 | // 19 | create(recipeId, data) { 20 | return this.server.createService(recipeId, data); 21 | } 22 | 23 | delete(serviceId) { 24 | return this.server.deleteService(serviceId); 25 | } 26 | 27 | update(serviceId, data) { 28 | return this.server.updateService(serviceId, data); 29 | } 30 | 31 | reorder(data) { 32 | return this.server.reorderService(data); 33 | } 34 | 35 | clearCache(serviceId) { 36 | return this.local.clearCache(serviceId); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/api/UserApi.js: -------------------------------------------------------------------------------- 1 | import { hash } from '../helpers/password-helpers'; 2 | 3 | export default class UserApi { 4 | constructor(server, local) { 5 | this.server = server; 6 | this.local = local; 7 | } 8 | 9 | login(email, password) { 10 | return this.server.login(email, hash(password)); 11 | } 12 | 13 | logout() { 14 | return this; 15 | } 16 | 17 | signup(data) { 18 | Object.assign(data, { 19 | password: hash(data.password), 20 | }); 21 | return this.server.signup(data); 22 | } 23 | 24 | password(email) { 25 | return this.server.retrievePassword(email); 26 | } 27 | 28 | activateTrial(data) { 29 | return this.server.activateTrial(data); 30 | } 31 | 32 | invite(data) { 33 | return this.server.inviteUser(data); 34 | } 35 | 36 | getInfo() { 37 | return this.server.userInfo(); 38 | } 39 | 40 | updateInfo(data) { 41 | const userData = data; 42 | if (userData.oldPassword && userData.newPassword) { 43 | userData.oldPassword = hash(userData.oldPassword); 44 | userData.newPassword = hash(userData.newPassword); 45 | } 46 | 47 | return this.server.updateUserInfo(userData); 48 | } 49 | 50 | getLegacyServices() { 51 | return this.server.getLegacyServices(); 52 | } 53 | 54 | delete() { 55 | return this.server.deleteAccount(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import AppApi from './AppApi'; 2 | import ServicesApi from './ServicesApi'; 3 | import RecipePreviewsApi from './RecipePreviewsApi'; 4 | import RecipesApi from './RecipesApi'; 5 | import UserApi from './UserApi'; 6 | import LocalApi from './LocalApi'; 7 | import PaymentApi from './PaymentApi'; 8 | import NewsApi from './NewsApi'; 9 | import FeaturesApi from './FeaturesApi'; 10 | 11 | export default (server, local) => ({ 12 | app: new AppApi(server, local), 13 | services: new ServicesApi(server, local), 14 | recipePreviews: new RecipePreviewsApi(server, local), 15 | recipes: new RecipesApi(server, local), 16 | features: new FeaturesApi(server, local), 17 | user: new UserApi(server, local), 18 | local: new LocalApi(server, local), 19 | payment: new PaymentApi(server, local), 20 | news: new NewsApi(server, local), 21 | }); 22 | -------------------------------------------------------------------------------- /src/api/server/LocalApi.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import { session } from '@electron/remote'; 3 | import du from 'du'; 4 | 5 | import { getServicePartitionsDirectory } from '../../helpers/service-helpers.js'; 6 | 7 | const debug = require('debug')('Franz:LocalApi'); 8 | 9 | export default class LocalApi { 10 | // Settings 11 | getAppSettings(type) { 12 | return new Promise((resolve) => { 13 | ipcRenderer.once('appSettings', (event, resp) => { 14 | debug('LocalApi::getAppSettings resolves', resp.type, resp.data); 15 | resolve(resp); 16 | }); 17 | 18 | ipcRenderer.send('getAppSettings', type); 19 | }); 20 | } 21 | 22 | async updateAppSettings(type, data) { 23 | debug('LocalApi::updateAppSettings resolves', type, data); 24 | ipcRenderer.send('updateAppSettings', { 25 | type, 26 | data, 27 | }); 28 | } 29 | 30 | // Services 31 | async getAppCacheSize() { 32 | const partitionsDir = getServicePartitionsDirectory(); 33 | return new Promise((resolve, reject) => { 34 | du(partitionsDir, (err, size) => { 35 | if (err) reject(err); 36 | 37 | debug('LocalApi::getAppCacheSize resolves', size); 38 | resolve(size); 39 | }); 40 | }); 41 | } 42 | 43 | async clearCache(serviceId) { 44 | const s = session.fromPartition(`persist:service-${serviceId}`); 45 | 46 | debug('LocalApi::clearCache resolves', serviceId); 47 | return s.clearCache(); 48 | } 49 | 50 | async clearAppCache() { 51 | const s = session.defaultSession; 52 | 53 | debug('LocalApi::clearCache clearAppCache'); 54 | return s.clearCache(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/api/utils/auth.js: -------------------------------------------------------------------------------- 1 | import { app } from '@electron/remote'; 2 | import localStorage from 'mobx-localstorage'; 3 | 4 | export const prepareAuthRequest = (options = { method: 'GET' }, auth = true) => { 5 | const request = Object.assign(options, { 6 | mode: 'cors', 7 | headers: Object.assign({ 8 | 'Content-Type': 'application/json', 9 | 'X-Franz-Source': 'desktop', 10 | 'X-Franz-Version': app.getVersion(), 11 | 'X-Franz-platform': process.platform, 12 | 'X-Franz-Timezone-Offset': new Date().getTimezoneOffset(), 13 | 'X-Franz-System-Locale': app.getLocale(), 14 | }, options.headers), 15 | }); 16 | 17 | if (auth) { 18 | request.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`; 19 | } 20 | 21 | return request; 22 | }; 23 | 24 | export const sendAuthRequest = (url, options, auth) => ( 25 | window.fetch(url, prepareAuthRequest(options, auth)) 26 | ); 27 | -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/images/emoji/dontknow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/emoji/dontknow.png -------------------------------------------------------------------------------- /src/assets/images/emoji/sad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/emoji/sad.png -------------------------------------------------------------------------------- /src/assets/images/emoji/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/emoji/star.png -------------------------------------------------------------------------------- /src/assets/images/sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/sm.png -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/display.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/display.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-1.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-1.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-10.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-10.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-2.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-3.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-4.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-4.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-5.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-5.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-6.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-6.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-7.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-7.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-8.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-8.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-9.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-9.ico -------------------------------------------------------------------------------- /src/assets/images/taskbar/win32/taskbar-alert.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/taskbar/win32/taskbar-alert.ico -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-active.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-active@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-unread-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-unread-active.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-unread-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-unread-active@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-unread.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray-unread@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray-unread@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin-dark/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin-dark/tray@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin/tray-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin/tray-unread.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin/tray-unread@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin/tray-unread@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin/tray.png -------------------------------------------------------------------------------- /src/assets/images/tray/darwin/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/darwin/tray@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/linux/tray-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/linux/tray-unread.png -------------------------------------------------------------------------------- /src/assets/images/tray/linux/tray-unread@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/linux/tray-unread@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/linux/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/linux/tray.png -------------------------------------------------------------------------------- /src/assets/images/tray/linux/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/linux/tray@2x.png -------------------------------------------------------------------------------- /src/assets/images/tray/win32/tray-unread.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/win32/tray-unread.ico -------------------------------------------------------------------------------- /src/assets/images/tray/win32/tray.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meetfranz/franz/e4dc2fea74c08c0cd391a976dec7af728e30c7c2/src/assets/images/tray/win32/tray.ico -------------------------------------------------------------------------------- /src/components/services/content/ErrorHandlers/styles.js: -------------------------------------------------------------------------------- 1 | export default theme => ({ 2 | component: { 3 | left: 0, 4 | position: 'absolute', 5 | top: 0, 6 | width: '100%', 7 | zIndex: 0, 8 | alignItems: 'center', 9 | background: theme.colorWebviewErrorHandlerBackground, 10 | display: 'flex', 11 | flexDirection: 'column', 12 | justifyContent: 'center', 13 | textAlign: 'center', 14 | }, 15 | buttonContainer: { 16 | display: 'flex', 17 | flexDirection: 'row', 18 | height: 'auto', 19 | margin: [40, 0, 20], 20 | 21 | '& button': { 22 | margin: [0, 10, 0, 10], 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/services/content/ServiceDisabled.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | import { defineMessages, intlShape } from 'react-intl'; 5 | 6 | import Button from '../../ui/Button'; 7 | 8 | const messages = defineMessages({ 9 | headline: { 10 | id: 'service.disabledHandler.headline', 11 | defaultMessage: '!!!{name} is disabled', 12 | }, 13 | action: { 14 | id: 'service.disabledHandler.action', 15 | defaultMessage: '!!!Enable {name}', 16 | }, 17 | }); 18 | 19 | export default @observer class ServiceDisabled extends Component { 20 | static propTypes = { 21 | name: PropTypes.string.isRequired, 22 | enable: PropTypes.func.isRequired, 23 | }; 24 | 25 | static contextTypes = { 26 | intl: intlShape, 27 | }; 28 | 29 | countdownInterval = null; 30 | 31 | countdownIntervalTimeout = 1000; 32 | 33 | render() { 34 | const { name, enable } = this.props; 35 | const { intl } = this.context; 36 | 37 | return ( 38 |
39 |

{intl.formatMessage(messages.headline, { name })}

40 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/settings/recipes/RecipeItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | 5 | import RecipePreviewModel from '../../../models/RecipePreview'; 6 | 7 | export default @observer class RecipeItem extends Component { 8 | static propTypes = { 9 | recipe: PropTypes.instanceOf(RecipePreviewModel).isRequired, 10 | onClick: PropTypes.func.isRequired, 11 | }; 12 | 13 | render() { 14 | const { recipe, onClick } = this.props; 15 | 16 | return ( 17 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ui/AppLoader/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | component: { 3 | color: '#FFF', 4 | }, 5 | slogan: { 6 | display: 'block', 7 | opacity: 0, 8 | transition: 'opacity 1s ease', 9 | position: 'absolute', 10 | textAlign: 'center', 11 | width: '100%', 12 | }, 13 | visible: { 14 | opacity: 1, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/ui/FeatureItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import injectSheet from 'react-jss'; 3 | import { Icon } from '@meetfranz/ui'; 4 | import classnames from 'classnames'; 5 | import { mdiCheckCircle } from '@mdi/js'; 6 | 7 | const styles = theme => ({ 8 | featureItem: { 9 | borderBottom: [1, 'solid', theme.defaultContentBorder], 10 | padding: [8, 0], 11 | display: 'flex', 12 | alignItems: 'center', 13 | textAlign: 'left', 14 | }, 15 | featureIcon: { 16 | fill: theme.brandSuccess, 17 | marginRight: 10, 18 | }, 19 | }); 20 | 21 | export const FeatureItem = injectSheet(styles)(({ 22 | classes, className, name, icon, 23 | }) => ( 24 |
  • 29 | {icon ? ( 30 | {icon} 31 | ) : ( 32 | 33 | )} 34 | {name} 35 |
  • 36 | )); 37 | 38 | export default FeatureItem; 39 | -------------------------------------------------------------------------------- /src/components/ui/FullscreenLoader/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | import injectSheet, { withTheme } from 'react-jss'; 5 | import classnames from 'classnames'; 6 | 7 | import Loader from '../Loader'; 8 | 9 | import styles from './styles'; 10 | 11 | export default @observer @withTheme @injectSheet(styles) class FullscreenLoader extends Component { 12 | static propTypes = { 13 | className: PropTypes.string, 14 | title: PropTypes.string.isRequired, 15 | classes: PropTypes.object.isRequired, 16 | theme: PropTypes.object.isRequired, 17 | spinnerColor: PropTypes.string, 18 | children: PropTypes.node, 19 | }; 20 | 21 | static defaultProps = { 22 | className: null, 23 | spinnerColor: null, 24 | children: null, 25 | }; 26 | 27 | render() { 28 | const { 29 | classes, 30 | title, 31 | children, 32 | spinnerColor, 33 | className, 34 | theme, 35 | } = this.props; 36 | 37 | return ( 38 |
    39 |
    45 |

    {title}

    46 | 47 | {children && ( 48 |
    49 | {children} 50 |
    51 | )} 52 |
    53 |
    54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/ui/FullscreenLoader/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | wrapper: { 3 | display: 'flex', 4 | alignItems: 'center', 5 | position: ({ isAbsolutePositioned }) => (isAbsolutePositioned ? 'absolute' : 'relative'), 6 | width: '100%', 7 | }, 8 | component: { 9 | width: '100%', 10 | display: 'flex', 11 | flexDirection: 'column', 12 | alignItems: 'center', 13 | textAlign: 'center', 14 | height: 'auto', 15 | }, 16 | title: { 17 | fontSize: 35, 18 | }, 19 | content: { 20 | marginTop: 20, 21 | width: '100%', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/ui/Loader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Loader from 'react-loader'; 4 | 5 | import { oneOrManyChildElements } from '../../prop-types'; 6 | 7 | export default class LoaderComponent extends Component { 8 | static propTypes = { 9 | children: oneOrManyChildElements, 10 | loaded: PropTypes.bool, 11 | className: PropTypes.string, 12 | color: PropTypes.string, 13 | }; 14 | 15 | static defaultProps = { 16 | children: null, 17 | loaded: false, 18 | className: '', 19 | color: '#373a3c', 20 | }; 21 | 22 | render() { 23 | const { 24 | children, 25 | loaded, 26 | className, 27 | color, 28 | } = this.props; 29 | 30 | return ( 31 | 40 | {children} 41 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ui/Modal/styles.js: -------------------------------------------------------------------------------- 1 | export default theme => ({ 2 | component: { 3 | zIndex: 500, 4 | position: 'absolute', 5 | }, 6 | overlay: { 7 | background: theme.colorModalOverlayBackground, 8 | position: 'fixed', 9 | top: 0, 10 | left: 0, 11 | right: 0, 12 | bottom: 0, 13 | display: 'flex', 14 | }, 15 | modal: { 16 | background: theme.colorModalBackground, 17 | maxWidth: '90%', 18 | height: 'auto', 19 | margin: 'auto auto', 20 | borderRadius: 6, 21 | boxShadow: '0px 13px 40px 0px rgba(0,0,0,0.2)', 22 | position: 'relative', 23 | }, 24 | content: { 25 | padding: 20, 26 | }, 27 | close: { 28 | position: 'absolute', 29 | top: 0, 30 | right: 0, 31 | padding: 20, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/ui/PremiumFeatureContainer/styles.js: -------------------------------------------------------------------------------- 1 | export default theme => ({ 2 | container: { 3 | background: theme.colorSubscriptionContainerBackground, 4 | border: theme.colorSubscriptionContainerBorder, 5 | margin: [0, 0, 20, -20], 6 | padding: 20, 7 | 'border-radius': theme.borderRadius, 8 | pointerEvents: 'none', 9 | height: 'auto', 10 | }, 11 | titleContainer: { 12 | display: 'flex', 13 | }, 14 | title: { 15 | 'font-weight': 'bold', 16 | color: theme.colorSubscriptionContainerTitle, 17 | }, 18 | actionButton: { 19 | background: theme.colorSubscriptionContainerActionButtonBackground, 20 | color: theme.colorSubscriptionContainerActionButtonColor, 21 | 'margin-left': 'auto', 22 | 'border-radius': theme.borderRadiusSmall, 23 | padding: [4, 8], 24 | 'font-size': 12, 25 | pointerEvents: 'initial', 26 | }, 27 | content: { 28 | opacity: 0.5, 29 | 'margin-top': 20, 30 | '& > :last-child': { 31 | 'margin-bottom': 0, 32 | }, 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/ui/ServiceIcon.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | import injectSheet from 'react-jss'; 5 | import classnames from 'classnames'; 6 | 7 | import ServiceModel from '../../models/Service'; 8 | 9 | const styles = theme => ({ 10 | root: { 11 | height: 'auto', 12 | }, 13 | icon: { 14 | width: theme.serviceIcon.width, 15 | }, 16 | isCustomIcon: { 17 | width: theme.serviceIcon.isCustom.width, 18 | border: theme.serviceIcon.isCustom.border, 19 | borderRadius: theme.serviceIcon.isCustom.borderRadius, 20 | }, 21 | isDisabled: { 22 | filter: 'grayscale(100%)', 23 | opacity: '.5', 24 | }, 25 | }); 26 | 27 | @injectSheet(styles) @observer 28 | class ServiceIcon extends Component { 29 | static propTypes = { 30 | classes: PropTypes.object.isRequired, 31 | service: PropTypes.instanceOf(ServiceModel).isRequired, 32 | className: PropTypes.string, 33 | }; 34 | 35 | static defaultProps = { 36 | className: '', 37 | }; 38 | 39 | render() { 40 | const { 41 | classes, 42 | className, 43 | service, 44 | } = this.props; 45 | 46 | return ( 47 |
    53 | 62 |
    63 | ); 64 | } 65 | } 66 | 67 | export default ServiceIcon; 68 | -------------------------------------------------------------------------------- /src/components/ui/StatusBarTargetUrl.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | import classnames from 'classnames'; 5 | 6 | import Appear from './effects/Appear'; 7 | 8 | export default @observer class StatusBarTargetUrl extends Component { 9 | static propTypes = { 10 | className: PropTypes.string, 11 | text: PropTypes.string, 12 | }; 13 | 14 | static defaultProps = { 15 | className: '', 16 | text: '', 17 | }; 18 | 19 | render() { 20 | const { 21 | className, 22 | text, 23 | } = this.props; 24 | 25 | return ( 26 | 32 |
    33 | {text} 34 |
    35 |
    36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/ui/Tabs/TabItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | 3 | import { oneOrManyChildElements } from '../../../prop-types'; 4 | 5 | export default class TabItem extends Component { 6 | static propTypes = { 7 | children: oneOrManyChildElements.isRequired, 8 | } 9 | 10 | render() { 11 | const { children } = this.props; 12 | 13 | return ( 14 | {children} 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ui/Tabs/index.js: -------------------------------------------------------------------------------- 1 | import Tabs from './Tabs'; 2 | import TabItem from './TabItem'; 3 | 4 | export default Tabs; 5 | 6 | export { TabItem }; 7 | -------------------------------------------------------------------------------- /src/components/ui/WebviewLoader/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | import injectSheet from 'react-jss'; 5 | import { defineMessages, intlShape } from 'react-intl'; 6 | 7 | import FullscreenLoader from '../FullscreenLoader'; 8 | import styles from './styles'; 9 | 10 | const messages = defineMessages({ 11 | loading: { 12 | id: 'service.webviewLoader.loading', 13 | defaultMessage: '!!!Loading', 14 | }, 15 | }); 16 | 17 | export default @observer @injectSheet(styles) class WebviewLoader extends Component { 18 | static propTypes = { 19 | name: PropTypes.string.isRequired, 20 | classes: PropTypes.object.isRequired, 21 | }; 22 | 23 | static contextTypes = { 24 | intl: intlShape, 25 | }; 26 | 27 | render() { 28 | const { classes, name } = this.props; 29 | const { intl } = this.context; 30 | return ( 31 | 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/ui/WebviewLoader/styles.js: -------------------------------------------------------------------------------- 1 | export default theme => ({ 2 | component: { 3 | background: theme.colorWebviewLoaderBackground, 4 | padding: 20, 5 | width: 'auto', 6 | margin: [0, 'auto'], 7 | borderRadius: 6, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/ui/effects/Appear.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-did-mount-set-state */ 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 5 | 6 | export default class Appear extends Component { 7 | static propTypes = { 8 | children: PropTypes.any.isRequired, // eslint-disable-line 9 | transitionName: PropTypes.string, 10 | className: PropTypes.string, 11 | }; 12 | 13 | static defaultProps = { 14 | transitionName: 'fadeIn', 15 | className: '', 16 | }; 17 | 18 | state = { 19 | mounted: false, 20 | }; 21 | 22 | componentDidMount() { 23 | this.setState({ mounted: true }); 24 | } 25 | 26 | render() { 27 | const { 28 | children, 29 | transitionName, 30 | className, 31 | } = this.props; 32 | 33 | if (!this.state.mounted) { 34 | return null; 35 | } 36 | 37 | return ( 38 | 47 | {children} 48 | 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/util/ErrorBoundary/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import injectSheet from 'react-jss'; 4 | import { defineMessages, intlShape } from 'react-intl'; 5 | 6 | import Button from '../../ui/Button'; 7 | 8 | import styles from './styles'; 9 | 10 | const messages = defineMessages({ 11 | headline: { 12 | id: 'app.errorHandler.headline', 13 | defaultMessage: '!!!Something went wrong.', 14 | }, 15 | action: { 16 | id: 'app.errorHandler.action', 17 | defaultMessage: '!!!Reload', 18 | }, 19 | }); 20 | 21 | export default @injectSheet(styles) class ErrorBoundary extends Component { 22 | state = { 23 | hasError: false, 24 | } 25 | 26 | static propTypes = { 27 | classes: PropTypes.object.isRequired, 28 | children: PropTypes.node.isRequired, 29 | } 30 | 31 | static contextTypes = { 32 | intl: intlShape, 33 | }; 34 | 35 | componentDidCatch() { 36 | this.setState({ hasError: true }); 37 | } 38 | 39 | render() { 40 | const { classes } = this.props; 41 | const { intl } = this.context; 42 | 43 | if (this.state.hasError) { 44 | return ( 45 |
    46 |

    47 | {intl.formatMessage(messages.headline)} 48 |

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

    Welcome to the world of tomorrow

    50 |

    Welcome to the world of tomorrow

    51 |

    Welcome to the world of tomorrow

    52 |

    Welcome to the world of tomorrow

    53 | 54 | )); 55 | -------------------------------------------------------------------------------- /uidev/src/stories/icon.stories.tsx: -------------------------------------------------------------------------------- 1 | import { mdiAccountCircle } from '@mdi/js'; 2 | import React from 'react'; 3 | 4 | import { Icon } from '@meetfranz/ui'; 5 | import { storiesOf } from '../stores/stories'; 6 | 7 | storiesOf('Icon') 8 | .add('Basic', () => ( 9 | <> 10 | 11 | 12 | 13 | 14 | )); 15 | -------------------------------------------------------------------------------- /uidev/src/stories/loader.stories.tsx: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | import { observer } from 'mobx-react'; 3 | import React from 'react'; 4 | import uuid from 'uuid/v4'; 5 | 6 | import { Loader } from '@meetfranz/ui'; 7 | import { storiesOf } from '../stores/stories'; 8 | 9 | storiesOf('Loader') 10 | .add('Basic', () => ( 11 | <> 12 | 13 | 14 | )); 15 | -------------------------------------------------------------------------------- /uidev/src/stories/textarea.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import uuid from 'uuid/v4'; 3 | 4 | import { Textarea } from '@meetfranz/forms'; 5 | import { storiesOf } from '../stores/stories'; 6 | 7 | const defaultProps = () => { 8 | const id = uuid(); 9 | return { 10 | label: 'Label', 11 | id: `test-${id}`, 12 | name: `test-${id}`, 13 | rows: 5, 14 | onChange: (e: React.ChangeEvent) => console.log('changed event', e), 15 | }; 16 | }; 17 | 18 | storiesOf('Textarea') 19 | .add('Basic', () => ( 20 |