├── .all-contributorsrc ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 01-bug-report.md │ └── 02-feature-request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── opencollective.yml ├── stale.yml └── workflows │ ├── codeql-analysis.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .idea └── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── SECURITY.md ├── browser-extension ├── .gitignore ├── .vscode │ └── settings.json ├── package.json ├── public │ ├── logo_128.png │ ├── logo_16.png │ ├── logo_48.png │ ├── manifest.json │ └── popup.html ├── src │ ├── background.js │ ├── popup.js │ └── spinner.svg └── webpack.config.js ├── desktop-app ├── . prettierignore ├── .editorconfig ├── .erb │ ├── configs │ │ ├── .eslintrc │ │ ├── webpack.config.base.ts │ │ ├── webpack.config.eslint.ts │ │ ├── webpack.config.main.prod.ts │ │ ├── webpack.config.preload-webview.dev.ts │ │ ├── webpack.config.preload.dev.ts │ │ ├── webpack.config.renderer.dev.dll.ts │ │ ├── webpack.config.renderer.dev.ts │ │ ├── webpack.config.renderer.prod.ts │ │ └── webpack.paths.ts │ ├── img │ │ ├── erb-banner.svg │ │ └── erb-logo.png │ ├── mocks │ │ └── fileMock.js │ └── scripts │ │ ├── .eslintrc │ │ ├── check-build-exists.ts │ │ ├── check-native-dep.js │ │ ├── check-node-env.js │ │ ├── check-port-in-use.js │ │ ├── clean.js │ │ ├── delete-source-maps.js │ │ ├── electron-rebuild.js │ │ ├── link-modules.ts │ │ └── notarize.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .husky │ └── pre-commit ├── .prettierrc ├── .vscode │ └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── assets │ ├── assets.d.ts │ ├── entitlements.mac.plist │ ├── icon.png │ ├── icon.svg │ └── icons │ │ ├── 1024x1024.png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ ├── 64x64.png │ │ └── 96x96.png ├── declarations.d.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── postinstall.ts ├── release │ └── app │ │ ├── package-lock.json │ │ ├── package.json │ │ └── yarn.lock ├── setupTests.js ├── src │ ├── __tests__ │ │ └── App.test.tsx │ ├── common │ │ ├── constants.ts │ │ ├── deviceList.ts │ │ └── webViewUtils.ts │ ├── main │ │ ├── app-meta │ │ │ └── index.ts │ │ ├── app-updater.ts │ │ ├── browser-sync.ts │ │ ├── cli.ts │ │ ├── devtools │ │ │ └── index.ts │ │ ├── http-basic-auth │ │ │ └── index.ts │ │ ├── main.ts │ │ ├── menu │ │ │ ├── help.ts │ │ │ ├── index.ts │ │ │ └── view.ts │ │ ├── native-functions │ │ │ └── index.ts │ │ ├── preload-webview.ts │ │ ├── preload.ts │ │ ├── protocol-handler │ │ │ └── index.ts │ │ ├── screenshot │ │ │ ├── index.ts │ │ │ └── webpage.ts │ │ ├── util.ts │ │ ├── web-permissions │ │ │ ├── PermissionsManager.ts │ │ │ └── index.ts │ │ ├── webview-context-menu │ │ │ ├── common.ts │ │ │ └── register.ts │ │ └── webview-storage-manager │ │ │ └── index.ts │ ├── renderer │ │ ├── App.css │ │ ├── AppContent.tsx │ │ ├── assets │ │ │ ├── img │ │ │ │ └── logo.png │ │ │ └── sfx │ │ │ │ └── screenshot.mp3 │ │ ├── components │ │ │ ├── AboutDialog │ │ │ │ └── index.tsx │ │ │ ├── Accordion │ │ │ │ ├── Accordion.tsx │ │ │ │ ├── AccordionItem.tsx │ │ │ │ └── index.tsx │ │ │ ├── Button │ │ │ │ ├── Button.test.tsx │ │ │ │ └── index.tsx │ │ │ ├── ButtonGroup │ │ │ │ └── index.tsx │ │ │ ├── ConfirmDialog │ │ │ │ └── index.tsx │ │ │ ├── DeviceManager │ │ │ │ ├── DeviceDetailsModal.tsx │ │ │ │ ├── DeviceLabel.tsx │ │ │ │ ├── PreviewSuites │ │ │ │ │ ├── CreateSuiteButton │ │ │ │ │ │ ├── CreateSuiteModal.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ManageSuitesTool │ │ │ │ │ │ ├── ManageSuitesTool.test.tsx │ │ │ │ │ │ ├── ManageSuitesTool.tsx │ │ │ │ │ │ ├── ManageSuitesToolError.test.tsx │ │ │ │ │ │ ├── ManageSuitesToolError.tsx │ │ │ │ │ │ ├── helpers.test.tsx │ │ │ │ │ │ ├── helpers.ts │ │ │ │ │ │ ├── test.json │ │ │ │ │ │ ├── utils.test.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── Suite.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Divider │ │ │ │ └── index.tsx │ │ │ ├── DropDown │ │ │ │ └── index.tsx │ │ │ ├── FileUploader │ │ │ │ ├── FileUploader.test.tsx │ │ │ │ ├── FileUploader.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useFileUpload.test.tsx │ │ │ │ │ └── useFileUpload.tsx │ │ │ │ └── index.ts │ │ │ ├── InfoPopups │ │ │ │ └── index.tsx │ │ │ ├── Input │ │ │ │ └── index.tsx │ │ │ ├── KeyboardShortcutsManager │ │ │ │ ├── constants.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── useKeyboardShortcut.ts │ │ │ │ └── useMousetrapEmitter.ts │ │ │ ├── Modal │ │ │ │ └── index.tsx │ │ │ ├── ModalLoader │ │ │ │ └── index.tsx │ │ │ ├── Notifications │ │ │ │ ├── Notification.tsx │ │ │ │ ├── NotificationEmptyStatus.tsx │ │ │ │ ├── Notifications.tsx │ │ │ │ └── NotificationsBubble.tsx │ │ │ ├── Previewer │ │ │ │ ├── Device │ │ │ │ │ ├── ColorBlindnessTools │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Toolbar.tsx │ │ │ │ │ ├── assets.ts │ │ │ │ │ ├── common.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── DevtoolsResizer │ │ │ │ │ └── index.tsx │ │ │ │ ├── Guides │ │ │ │ │ ├── guide.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── IndividualLayoutToolBar │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ └── index.tsx │ │ │ ├── ReleaseNotes │ │ │ │ └── index.tsx │ │ │ ├── Select │ │ │ │ └── index.tsx │ │ │ ├── Spinner │ │ │ │ └── index.tsx │ │ │ ├── Sponsorship │ │ │ │ └── index.tsx │ │ │ ├── Toggle │ │ │ │ └── index.tsx │ │ │ ├── ToolBar │ │ │ │ ├── AddressBar │ │ │ │ │ ├── AuthModal.tsx │ │ │ │ │ ├── BookmarkButton.tsx │ │ │ │ │ ├── SuggestionList.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── ColorBlindnessControls │ │ │ │ │ └── index.tsx │ │ │ │ ├── ColorSchemeToggle │ │ │ │ │ └── index.tsx │ │ │ │ ├── Menu │ │ │ │ │ ├── Flyout │ │ │ │ │ │ ├── AllowInSecureSSL │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Bookmark │ │ │ │ │ │ │ ├── ViewAllBookmarks │ │ │ │ │ │ │ │ ├── BookmarkFlyout.tsx │ │ │ │ │ │ │ │ ├── BookmarkListButton.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── ClearHistory │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Devtools │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── PreviewLayout │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Settings │ │ │ │ │ │ │ ├── SettingsContent.test.tsx │ │ │ │ │ │ │ ├── SettingsContent.tsx │ │ │ │ │ │ │ ├── SettingsContentHeaders.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── UITheme │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Zoom.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── NavigationControls.tsx │ │ │ │ ├── PreviewSuiteSelector │ │ │ │ │ └── index.tsx │ │ │ │ ├── Shortcuts │ │ │ │ │ ├── ShortcutsModal │ │ │ │ │ │ ├── ShortcutButton.tsx │ │ │ │ │ │ ├── ShortcutName.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── VisionSimulationDropDown │ │ │ │ └── index.tsx │ │ │ └── useLocalStorage │ │ │ │ └── useLocalStorage.tsx │ │ ├── context │ │ │ └── ThemeProvider │ │ │ │ └── index.tsx │ │ ├── index.ejs │ │ ├── index.tsx │ │ ├── lib │ │ │ └── pubsub │ │ │ │ ├── index.test.ts │ │ │ │ └── index.ts │ │ ├── preload.d.ts │ │ ├── store │ │ │ ├── features │ │ │ │ ├── bookmarks │ │ │ │ │ └── index.ts │ │ │ │ ├── device-manager │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── devtools │ │ │ │ │ └── index.ts │ │ │ │ ├── renderer │ │ │ │ │ └── index.ts │ │ │ │ ├── ruler │ │ │ │ │ └── index.ts │ │ │ │ └── ui │ │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── titlebar-styles.css │ └── store │ │ ├── index.ts │ │ └── migrations.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock └── dev.code-workspace /.gitattributes: -------------------------------------------------------------------------------- 1 | /desktop-app-legacy/* export-ignore 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: responsively-org # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: responsively 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Report a bug in Responsively 4 | --- 5 | 6 | 11 | 12 | # 🐞 bug report 13 | 14 | ### ✍️ Description 15 | 16 | 17 | 18 | ### 🕵🏼‍♂️ Is this a regression? 19 | 20 | 21 | 22 | ### 🔬 Minimal Reproduction 23 | 24 | 25 | 26 | ### 🌍 Your Environment 27 | 28 | 29 |

30 | 
31 | 
32 | 33 | ### 🔥 Exception or Error or Screenshot 34 | 35 |

36 | 
37 | 
38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest a feature for Responsively. 4 | --- 5 | 6 | # 🚀 Feature Request 7 | 8 | ### 📝 Description 9 | 10 | 11 | 12 | ### ✨ Describe the solution you'd like 13 | 14 | 15 | 16 | ### ✍️ Describe alternatives you've considered 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # ✨ Pull Request 2 | 3 | ### 📓 Referenced Issue 4 | 5 | 6 | 7 | ### ℹ️ About the PR 8 | 9 | 10 | 11 | ### 🖼️ Testing Scenarios / Screenshots 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/desktop-app" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/opencollective.yml: -------------------------------------------------------------------------------- 1 | collective: responsively 2 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - discussion 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '44 16 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | defaults: 32 | run: 33 | working-directory: ./desktop-app 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: [ 'javascript' ] 39 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v3 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v2 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 56 | # queries: security-extended,security-and-quality 57 | 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v2 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 66 | 67 | # If the Autobuild fails above, remove it and uncomment the following three lines. 68 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 69 | 70 | # - run: | 71 | # echo "Run, Build Application using script" 72 | # ./location_of_script_within_repo/buildscript.sh 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@v2 -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish: 8 | runs-on: ${{ matrix.os }} 9 | defaults: 10 | run: 11 | working-directory: ./desktop-app 12 | 13 | strategy: 14 | matrix: 15 | os: [macos-13] 16 | 17 | steps: 18 | - name: Checkout git repo 19 | uses: actions/checkout@v3 20 | 21 | - name: Install Node and NPM 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | cache: ${{ !env.ACT && 'npm' || '' }} # Disable cache for nektos/act 26 | cache-dependency-path: ./desktop-app/yarn.lock 27 | 28 | - name: Install and build 29 | run: | 30 | yarn install 31 | yarn run postinstall 32 | yarn run build 33 | 34 | - name: Bump version 35 | run: | 36 | cd release/app 37 | yarn version --preid beta --prerelease 38 | git push 39 | 40 | - name: Publish releases 41 | env: 42 | # These values are used for auto updates signing 43 | APPLE_ID: ${{ secrets.APPLEID }} 44 | APPLE_ID_PASS: ${{ secrets.APPLEIDPASS }} 45 | CSC_LINK: ${{ secrets.CSC_LINK }} 46 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 47 | # This is used for uploading release assets to github 48 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | run: | 50 | yarn exec electron-builder -- --publish always --win --mac --linux --arm64 --x64 51 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | defaults: 9 | run: 10 | working-directory: ./desktop-app 11 | 12 | strategy: 13 | matrix: 14 | os: [macos-latest] 15 | fail-fast: false 16 | 17 | steps: 18 | - name: Check out Git repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Install Node.js and NPM 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | cache: ${{ !env.ACT && 'npm' || '' }} # Disable cache for nektos/act 26 | cache-dependency-path: ./desktop-app/yarn.lock 27 | 28 | - name: yarn install 29 | run: | 30 | yarn install --network-timeout 120000 31 | 32 | - name: yarn test 33 | env: 34 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: | 36 | yarn run package 37 | yarn run lint 38 | yarn exec tsc 39 | yarn test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | ## Contributing 3 | 4 | Contributions are welcome and always appreciated! 5 | 6 | To begin working on an issue, simply leave a comment indicating that you're taking it on. There's no need to be officially assigned to the issue before you start. 7 | 8 | ### Before Starting 9 | Do keep in mind before you start working on an issue / posting a PR: 10 | - Search existing PRs related to that issue which might close them 11 | - Confirm if other contributors are working on the same issue 12 | 13 | ### Tips & Things to Consider 14 | - We are active in Discord and can help out if you get stuck, [join us!](https://responsively.app/join-discord) 15 | - PRs with tests are highly appreciated 16 | - Avoid adding third party libraries, whenever possible 17 | - Unless you are helping out by updating dependencies, you should not be uploading your yarn.lock or updating any dependencies in your PR 18 | - If you are unsure where to start, contact us and we will point you to a first good issue 19 | 20 | ## Run Locally 21 | Ensure you have the following dependencies installed: 22 | - Install `node` and `yarn` 23 | - Configure your IDE to support ESLint and Prettier extensions. 24 | 25 | After having above installed, proceed through the following steps to setup the codebase locally. 26 | 27 | - Fork the project & [clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) it locally. 28 | 29 | ![fork-project](https://github.com/responsively-org/responsively-app/assets/87022870/2cae8b2a-850c-4f80-8ede-32eba622a854) 30 | 31 | - Create a new separate branch. 32 | 33 | ```bash 34 | git checkout -b BRANCH_NAME 35 | ``` 36 | - Go to the desktop-app directory. 37 | 38 | ```bash 39 | cd desktop-app 40 | ``` 41 | 42 | - Run the following command to install dependencies inside the desktop-app directory. 43 | 44 | ```bash 45 | yarn 46 | ``` 47 | 48 | - This will start the app for local development with live reloading. 49 | 50 | ```bash 51 | yarn dev 52 | ``` 53 | 54 | ## Running Tests 55 | 56 | It is crucial to test your code before submitting a pull request. Please ensure that you can make a complete production build before you submit your code for merging. 57 | 58 | - Build the project 59 | ```bash 60 | yarn build 61 | ``` 62 | 63 | - Now test your code using the following command 64 | ```bash 65 | yarn test 66 | ``` 67 | 68 | Make sure the tests have successfully passed. 69 | 70 | ## Pull Request 71 | 72 | 🎉 Now that you're ready to submit your code for merging, there are some points to keep in mind. 73 | 74 | - Fill your PR description template accordingly. 75 | - Have an appropriate title and description. 76 | - Include relevant screenshots/gifs. 77 | 78 | - If your PR fixes some issue, be sure to add this line with the issue **in the body** of the Pull Request description. 79 | ```text 80 | Fixes #00000 81 | ``` 82 | 83 | - If your PR is referencing an issue 84 | ```text 85 | Refs #00000 86 | ``` 87 | 88 | - Ensure that "Allow edits from maintainers" option is checked. 89 | 90 | ## Community 91 | Need help on a solution from fellow contributors or want to discuss about a feature/issue? 92 | 93 | Join our [Discord](https://responsively.app/join-discord)! -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Project Maintainer 2 | 3 | ## How to become a maintainer 4 | The following are the criteria to get considered becoming a maintainer: 5 | - Implemented two or more features to the project. 6 | - Being active and sticking around on GitHub and Discord, engaging with other user's on their problems and providing useful solutions. 7 | 8 | Contributors who match the above criteria would be evaluated by the existing maintainers and invited as maintainers if they see fit. 9 | 10 | ## Responsibilities 11 | As a project maintainer for Responsively App, your responsibilities include: 12 | 13 | #### Code/Tech: 14 | - Do issue triage on user reported bugs. 15 | - Review and merge contributions. 16 | - Advise new contributors on better practices in their code. 17 | - Automate any manual process in the project/CI workflow. 18 | - Constantly strive to improve the DX of the project 19 | 20 | #### Community 21 | - Be nice to everyone on Discord and GitHub. 22 | - Try and provide sensible solution to user reported problems. 23 | 24 | Always feel free to reach out to `p.manoj.vivek@gmail.com` or `manojVivek` on Discord if you have any questions. 25 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Below are the versions that are currently being supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.x.x | :white_check_mark: | 10 | | < 1.0 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Vulnerabilities can be reported in the following ways: 15 | 1. Create an issue required details (here)[https://github.com/responsively-org/responsively-app/issues]. 16 | 2. Send an email to `p.manoj.vivek` at `gmail.com` with details. 17 | -------------------------------------------------------------------------------- /browser-extension/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | dist 4 | setCreds.sh 5 | web-ext-artifacts 6 | -------------------------------------------------------------------------------- /browser-extension/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } 5 | -------------------------------------------------------------------------------- /browser-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Responsively-Helper", 3 | "version": "0.0.1", 4 | "description": "An extension to open current browser page in Responsively app", 5 | "private": true, 6 | "scripts": { 7 | "lint": "run-p lint:*", 8 | "lint:js": "xo", 9 | "lint:css": "stylelint source/**/*.css", 10 | "lint-fix": "run-p 'lint:* -- --fix'", 11 | "test": "run-s lint:* build", 12 | "start": "webpack serve --mode=development --hot --open", 13 | "build": "webpack --mode=production", 14 | "release:cws": "webstore upload --source=dist --auto-publish", 15 | "release:amo": "web-ext-submit --source-dir dist", 16 | "release": "run-s build release:*" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.10.1", 20 | "@babel/plugin-proposal-class-properties": "^7.10.1", 21 | "@babel/plugin-proposal-object-rest-spread": "^7.10.1", 22 | "@babel/plugin-transform-react-constant-elements": "^7.10.1", 23 | "@babel/plugin-transform-runtime": "^7.10.1", 24 | "@babel/preset-env": "^7.10.1", 25 | "@babel/preset-react": "^7.10.1", 26 | "@ianwalter/web-ext-webpack-plugin": "^0.1.0", 27 | "babel-loader": "^8.1.0", 28 | "chrome-webstore-upload-cli": "^1.2.0", 29 | "copy-webpack-plugin": "^5.0.3", 30 | "daily-version": "^0.12.0", 31 | "dot-json": "^1.1.0", 32 | "eslint": "^6.1.0", 33 | "eslint-config-xo": "^0.26.0", 34 | "npm-run-all": "^4.1.5", 35 | "size-plugin": "^1.2.0", 36 | "stylelint": "^10.1.0", 37 | "stylelint-config-xo": "^0.15.0", 38 | "terser-webpack-plugin": "^1.3.0", 39 | "web-ext": "^4.2.0", 40 | "web-ext-submit": "^4.2.0", 41 | "webpack": "^4.36.1", 42 | "webpack-cli": "^3.3.6", 43 | "xo": "^0.24.0" 44 | }, 45 | "dependencies": { 46 | "custom-protocol-check": "^1.1.0", 47 | "react": "^16.13.1", 48 | "react-dom": "^16.13.1", 49 | "url-loader": "^4.1.0", 50 | "webextension-polyfill": "^0.4.0" 51 | }, 52 | "xo": { 53 | "envs": [ 54 | "browser" 55 | ], 56 | "ignores": [ 57 | "dist" 58 | ], 59 | "globals": [ 60 | "browser" 61 | ] 62 | }, 63 | "stylelint": { 64 | "extends": "stylelint-config-xo" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /browser-extension/public/logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/browser-extension/public/logo_128.png -------------------------------------------------------------------------------- /browser-extension/public/logo_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/browser-extension/public/logo_16.png -------------------------------------------------------------------------------- /browser-extension/public/logo_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/browser-extension/public/logo_48.png -------------------------------------------------------------------------------- /browser-extension/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Responsively Helper", 3 | "version": "0.0.2", 4 | "description": "An extension to open current browser page in Responsively app", 5 | "homepage_url": "https://responsively.app", 6 | "manifest_version": 2, 7 | "icons": { 8 | "128": "logo_128.png", 9 | "48": "logo_48.png", 10 | "16": "logo_16.png" 11 | }, 12 | "background": { 13 | "persistent": false, 14 | "scripts": [ 15 | "browser-polyfill.min.js", 16 | "background.js" 17 | ] 18 | }, 19 | "browser_action": { 20 | "default_icon": "logo_128.png", 21 | "default_title": "Open page in Responsively app", 22 | "default_popup": "popup.html" 23 | }, 24 | "permissions": [ 25 | "activeTab" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /browser-extension/public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 59 | 60 | 61 | 62 |
63 | 64 |
65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /browser-extension/src/background.js: -------------------------------------------------------------------------------- 1 | browser.browserAction.onClicked.addListener((tab) => { 2 | browser.tabs.executeScript({ 3 | file: './openURL.js' 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /browser-extension/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const SizePlugin = require('size-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const WebExtWebpackPlugin = require('@ianwalter/web-ext-webpack-plugin'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | 8 | module.exports = { 9 | devtool: 'source-map', 10 | stats: 'errors-only', 11 | entry: { 12 | background: './src/background', 13 | popup: './src/popup', 14 | // Add HMR client 15 | main: [ 16 | 'webpack-hot-middleware/client?reload=true', // Use 'reload=true' for CSS 17 | './src/index.js', // Adjust to your main file 18 | ], 19 | }, 20 | output: { 21 | path: path.join(__dirname, 'dist'), 22 | filename: '[name].js', 23 | publicPath: '/', // Required for HMR 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.js$/, 29 | include: [ 30 | path.resolve(__dirname, "src") 31 | ], 32 | use: { 33 | loader: 'babel-loader', 34 | options: { 35 | presets: [ 36 | ["@babel/preset-env", { modules: false }], 37 | "@babel/preset-react" 38 | ], 39 | plugins: [ 40 | "@babel/plugin-transform-react-constant-elements", 41 | "@babel/plugin-proposal-object-rest-spread", 42 | "@babel/plugin-proposal-class-properties", 43 | "@babel/plugin-transform-runtime", 44 | 'react-refresh/babel', // Add React Refresh for HMR 45 | ], 46 | }, 47 | }, 48 | }, 49 | { 50 | test: /\.(svg|gif|png|jpg)$/, 51 | use: 'url-loader', 52 | } 53 | ], 54 | }, 55 | plugins: [ 56 | new webpack.HotModuleReplacementPlugin(), // Enable HMR 57 | new SizePlugin(), 58 | new CopyWebpackPlugin({ 59 | patterns: [ 60 | { 61 | from: '**/*', 62 | context: 'public', 63 | }, 64 | { 65 | from: 'node_modules/webextension-polyfill/dist/browser-polyfill.min.js' 66 | } 67 | ], 68 | }), 69 | new WebExtWebpackPlugin({ sourceDir: path.join(__dirname, 'dist'), verbose: true }), 70 | ], 71 | optimization: { 72 | minimizer: [ 73 | new TerserPlugin({ 74 | terserOptions: { 75 | mangle: false, 76 | compress: false, 77 | output: { 78 | beautify: true, 79 | indent_level: 2 80 | } 81 | } 82 | }) 83 | ] 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /desktop-app/. prettierignore: -------------------------------------------------------------------------------- 1 | .erb/* -------------------------------------------------------------------------------- /desktop-app/.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 | -------------------------------------------------------------------------------- /desktop-app/.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /desktop-app/.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'; 7 | import webpackPaths from './webpack.paths'; 8 | import { dependencies as externals } from '../../release/app/package.json'; 9 | 10 | const configuration: webpack.Configuration = { 11 | externals: [...Object.keys(externals || {})], 12 | 13 | stats: 'errors-only', 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.[jt]sx?$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'ts-loader', 22 | options: { 23 | // Remove this line to enable type checking in webpack builds 24 | transpileOnly: true, 25 | compilerOptions: { 26 | module: 'esnext', 27 | }, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | 34 | output: { 35 | path: webpackPaths.srcPath, 36 | // https://github.com/webpack/webpack/issues/1114 37 | library: { 38 | type: 'commonjs2', 39 | }, 40 | }, 41 | 42 | /** 43 | * Determine the array of extensions that should be used to resolve modules. 44 | */ 45 | resolve: { 46 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 47 | modules: [webpackPaths.srcPath, 'node_modules'], 48 | // There is no need to add aliases here, the paths in tsconfig get mirrored 49 | plugins: [new TsconfigPathsPlugins()], 50 | }, 51 | 52 | plugins: [ 53 | new webpack.EnvironmentPlugin({ 54 | NODE_ENV: 'production', 55 | }), 56 | ], 57 | }; 58 | 59 | export default configuration; 60 | -------------------------------------------------------------------------------- /desktop-app/.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /desktop-app/.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const devtoolsConfig = 19 | process.env.DEBUG_PROD === 'true' 20 | ? { 21 | devtool: 'source-map', 22 | } 23 | : {}; 24 | 25 | const configuration: webpack.Configuration = { 26 | ...devtoolsConfig, 27 | 28 | mode: 'production', 29 | 30 | target: 'electron-main', 31 | 32 | entry: { 33 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 34 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 35 | 'preload-webview': path.join( 36 | webpackPaths.srcMainPath, 37 | 'preload-webview.ts' 38 | ), 39 | }, 40 | 41 | externals: ['fsevents'], 42 | 43 | output: { 44 | path: webpackPaths.distMainPath, 45 | filename: '[name].js', 46 | library: { 47 | type: 'umd', 48 | }, 49 | }, 50 | 51 | optimization: { 52 | minimizer: [ 53 | new TerserPlugin({ 54 | parallel: true, 55 | }), 56 | ], 57 | }, 58 | 59 | plugins: [ 60 | new BundleAnalyzerPlugin({ 61 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 62 | }), 63 | 64 | /** 65 | * Create global constants which can be configured at compile time. 66 | * 67 | * Useful for allowing different behaviour between development builds and 68 | * release builds 69 | * 70 | * NODE_ENV should be production so that modules do not perform certain 71 | * development checks 72 | */ 73 | new webpack.EnvironmentPlugin({ 74 | NODE_ENV: 'production', 75 | DEBUG_PROD: false, 76 | START_MINIMIZED: false, 77 | }), 78 | 79 | new webpack.DefinePlugin({ 80 | 'process.type': '"browser"', 81 | }), 82 | ], 83 | 84 | /** 85 | * Disables webpack processing of __dirname and __filename. 86 | * If you run the bundle in node.js it falls back to these values of node.js. 87 | * https://github.com/webpack/webpack/issues/2010 88 | */ 89 | node: { 90 | __dirname: false, 91 | __filename: false, 92 | }, 93 | }; 94 | 95 | export default merge(baseConfig, configuration); 96 | -------------------------------------------------------------------------------- /desktop-app/.erb/configs/webpack.config.preload-webview.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload-webview.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload-webview.js', 27 | library: { 28 | type: 'umd', 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /desktop-app/.erb/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload.js', 27 | library: { 28 | type: 'umd', 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /desktop-app/.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | const configuration: webpack.Configuration = { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }; 76 | 77 | export default merge(baseConfig, configuration); 78 | -------------------------------------------------------------------------------- /desktop-app/.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const releasePath = path.join(rootPath, 'release'); 12 | const appPath = path.join(releasePath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const buildPath = path.join(releasePath, 'build'); 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | }; 39 | -------------------------------------------------------------------------------- /desktop-app/.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /desktop-app/.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "yarn run build:main"' 14 | ) 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "yarn run build:renderer"' 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency) 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.' 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":' 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold( 42 | 'cd ./release/app && npm install your-package' 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log('Native dependencies could not be checked'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn start` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import rimraf from 'rimraf'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const foldersToRemove = [ 5 | webpackPaths.distPath, 6 | webpackPaths.buildPath, 7 | webpackPaths.dllPath, 8 | ]; 9 | 10 | foldersToRemove.forEach((folder) => { 11 | rimraf.sync(folder); 12 | }); 13 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | export default function deleteSourceMaps() { 6 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 7 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 8 | } 9 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import { dependencies } from '../../release/app/package.json'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | if ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 12 | const cmd = 13 | process.platform === 'win32' 14 | ? electronRebuildCmd.replace(/\//g, '\\') 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: 'inherit', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const { srcNodeModulesPath } = webpackPaths; 5 | const { appNodeModulesPath } = webpackPaths; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /desktop-app/.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (process.env.CI !== 'true') { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 16 | console.warn( 17 | 'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set' 18 | ); 19 | return; 20 | } 21 | 22 | const appName = context.packager.appInfo.productFilename; 23 | 24 | await notarize({ 25 | appBundleId: build.appId, 26 | appPath: `${appOutDir}/${appName}.app`, 27 | appleId: process.env.APPLE_ID, 28 | appleIdPassword: process.env.APPLE_ID_PASS, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /desktop-app/.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb 34 | -------------------------------------------------------------------------------- /desktop-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | rules: { 4 | // A temporary hack related to IDE not resolving correct package.json 5 | 'import/no-extraneous-dependencies': 'off', 6 | 'import/no-unresolved': 'error', 7 | 8 | 'import/prefer-default-export': 'off', 9 | 10 | // Since React 17 and typescript 4.1 you can safely disable the rule 11 | 'react/react-in-jsx-scope': 'off', 12 | 'react/require-default-props': 'off', 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 2020, 16 | sourceType: 'module', 17 | project: './tsconfig.json', 18 | tsconfigRootDir: __dirname, 19 | createDefaultProgram: true, 20 | }, 21 | settings: { 22 | 'import/resolver': { 23 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 24 | node: {}, 25 | webpack: { 26 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 27 | }, 28 | typescript: {}, 29 | }, 30 | 'import/parsers': { 31 | '@typescript-eslint/parser': ['.ts', '.tsx'], 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /desktop-app/.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /desktop-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | -------------------------------------------------------------------------------- /desktop-app/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /desktop-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"], 5 | "options": { 6 | "parser": "json" 7 | } 8 | } 9 | ], 10 | "arrowParens": "avoid", 11 | "singleQuote": true, 12 | "bracketSpacing": false, 13 | "printWidth": 100, 14 | "tailwindConfig": "./tailwind.config.js", 15 | "trailingComma": "es5" 16 | } -------------------------------------------------------------------------------- /desktop-app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "html", 12 | "typescriptreact" 13 | ], 14 | 15 | "javascript.validate.enable": false, 16 | "javascript.format.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "search.exclude": { 20 | ".git": true, 21 | ".eslintcache": true, 22 | ".erb/dll": true, 23 | "release/{build,app/dist}": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "package-lock.json": true, 28 | "*.{css,sass,scss}.d.ts": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /desktop-app/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at electronreactboilerplate@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /desktop-app/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Electron React Boilerplate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /desktop-app/assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | export const ReactComponent: React.FC>; 5 | 6 | const content: string; 7 | export default content; 8 | } 9 | 10 | declare module '*.png' { 11 | const content: string; 12 | export default content; 13 | } 14 | 15 | declare module '*.jpg' { 16 | const content: string; 17 | export default content; 18 | } 19 | 20 | declare module '*.scss' { 21 | const content: Styles; 22 | export default content; 23 | } 24 | 25 | declare module '*.sass' { 26 | const content: Styles; 27 | export default content; 28 | } 29 | 30 | declare module '*.css' { 31 | const content: Styles; 32 | export default content; 33 | } 34 | -------------------------------------------------------------------------------- /desktop-app/assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /desktop-app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icon.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/128x128.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/16x16.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/24x24.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/256x256.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/32x32.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/48x48.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/512x512.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/64x64.png -------------------------------------------------------------------------------- /desktop-app/assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/assets/icons/96x96.png -------------------------------------------------------------------------------- /desktop-app/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.mp3'; 2 | declare module 'electron-args'; 3 | -------------------------------------------------------------------------------- /desktop-app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleDirectories: ['node_modules', 'release/app/node_modules', 'src'], 3 | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'], 4 | moduleNameMapper: { 5 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 6 | '/.erb/mocks/fileMock.js', 7 | '\\.(css|less|sass|scss)$': 'identity-obj-proxy', 8 | }, 9 | setupFiles: [ 10 | './.erb/scripts/check-build-exists.ts', 11 | '/setupTests.js', 12 | ], 13 | testEnvironment: 'jsdom', 14 | testEnvironmentOptions: { 15 | url: 'http://localhost/', 16 | }, 17 | testPathIgnorePatterns: ['release/app/dist', '.erb/dll'], 18 | transform: { 19 | '\\.(ts|tsx|js|jsx)$': 'ts-jest', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /desktop-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-extraneous-dependencies: off */ 2 | 3 | module.exports = { 4 | plugins: [require('tailwindcss'), require('autoprefixer')], 5 | }; 6 | -------------------------------------------------------------------------------- /desktop-app/postinstall.ts: -------------------------------------------------------------------------------- 1 | import { replaceInFile } from 'replace-in-file'; 2 | 3 | async function performReplacements() { 4 | const replaceOptions = { 5 | files: 'node_modules/browser-sync-ui/lib/UI.js', 6 | from: /"network-throttle".*/, 7 | to: '', 8 | }; 9 | 10 | const howlerOptions = { 11 | files: 'node_modules/use-sound/dist/types.d.ts', 12 | from: '/// ', 13 | to: 'import { Howl } from "howler";', 14 | }; 15 | 16 | try { 17 | await replaceInFile(replaceOptions); 18 | console.log('Replacement in UI.js completed successfully.'); 19 | 20 | await replaceInFile(howlerOptions); 21 | console.log('Replacement in types.d.ts completed successfully.'); 22 | } catch (error) { 23 | console.error('Error occurred during replacements:', error); 24 | } 25 | } 26 | 27 | async function performPostInstall() { 28 | await performReplacements(); 29 | } 30 | 31 | performPostInstall(); 32 | -------------------------------------------------------------------------------- /desktop-app/release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ResponsivelyApp", 3 | "version": "1.16.0", 4 | "description": "A developer-friendly browser for developing responsive web apps", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Responsively", 8 | "email": "p.manoj.vivek@gmail.com" 9 | }, 10 | "main": "./dist/main/main.js", 11 | "scripts": { 12 | "electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 13 | "postinstall": "yarn run electron-rebuild && yarn run link-modules", 14 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 15 | }, 16 | "dependencies": { 17 | "browser-sync": "^2.27.12" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /desktop-app/setupTests.js: -------------------------------------------------------------------------------- 1 | window.electron = { 2 | ipcRenderer: { 3 | sendMessage: jest.fn(), 4 | on: jest.fn(), 5 | once: jest.fn(), 6 | invoke: jest.fn(), 7 | removeListener: jest.fn(), 8 | removeAllListeners: jest.fn(), 9 | }, 10 | store: { 11 | set: jest.fn(), 12 | get: jest.fn(), 13 | }, 14 | }; 15 | 16 | global.IntersectionObserver = jest.fn(() => ({ 17 | root: null, 18 | rootMargin: '', 19 | thresholds: [], 20 | observe: jest.fn(), 21 | unobserve: jest.fn(), 22 | disconnect: jest.fn(), 23 | takeRecords: jest.fn(), 24 | })); 25 | -------------------------------------------------------------------------------- /desktop-app/src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | // import { render } from '@testing-library/react'; 3 | // import App from '../renderer/AppContent'; 4 | 5 | describe('App', () => { 6 | it('should render', () => { 7 | // TODO Fix this 8 | // expect(render()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /desktop-app/src/common/constants.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | export const DOCK_POSITION = { 4 | BOTTOM: 'BOTTOM', 5 | RIGHT: 'RIGHT', 6 | UNDOCKED: 'UNDOCKED', 7 | } as const; 8 | 9 | export const PREVIEW_LAYOUTS = { 10 | COLUMN: 'COLUMN', 11 | FLEX: 'FLEX', 12 | INDIVIDUAL: 'INDIVIDUAL', 13 | MASONRY: 'MASONRY', 14 | } as const; 15 | 16 | export type PreviewLayout = 17 | typeof PREVIEW_LAYOUTS[keyof typeof PREVIEW_LAYOUTS]; 18 | 19 | export type Notification = { 20 | id: string; 21 | link?: string; 22 | linkText?: string; 23 | text: string; 24 | }; 25 | 26 | export interface OpenUrlArgs { 27 | url: string; 28 | } 29 | 30 | export const IPC_MAIN_CHANNELS = { 31 | APP_META: 'app-meta', 32 | PERMISSION_REQUEST: 'permission-request', 33 | PERMISSION_RESPONSE: 'permission-response', 34 | AUTH_REQUEST: 'auth-request', 35 | AUTH_RESPONSE: 'auth-response', 36 | OPEN_EXTERNAL: 'open-external', 37 | OPEN_URL: 'open-url', 38 | START_WATCHING_FILE: 'start-watching-file', 39 | STOP_WATCHER: 'stop-watcher', 40 | OPEN_ABOUT_DIALOG: 'open-about-dialog', 41 | } as const; 42 | 43 | export type Channels = typeof IPC_MAIN_CHANNELS[keyof typeof IPC_MAIN_CHANNELS]; 44 | 45 | export const PROTOCOL = 'responsively'; 46 | -------------------------------------------------------------------------------- /desktop-app/src/common/webViewUtils.ts: -------------------------------------------------------------------------------- 1 | export function updateWebViewHeightAndScale( 2 | webView: HTMLElement | Electron.WebviewTag, 3 | pageHeight: number 4 | ) { 5 | webView.style.height = `${pageHeight}px`; 6 | webView.style.transform = `scale(0.1)`; 7 | } 8 | -------------------------------------------------------------------------------- /desktop-app/src/main/app-meta/index.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain, shell } from 'electron'; 2 | import path from 'path'; 3 | import { IPC_MAIN_CHANNELS } from '../../common/constants'; 4 | import store from '../../store'; 5 | 6 | export interface AppMetaResponse { 7 | appVersion: string; 8 | webviewPreloadPath: string; 9 | } 10 | 11 | export const initAppMetaHandlers = () => { 12 | ipcMain.handle( 13 | IPC_MAIN_CHANNELS.APP_META, 14 | async (): Promise => { 15 | return { 16 | webviewPreloadPath: app.isPackaged 17 | ? path.join(__dirname, 'preload-webview.js') 18 | : path.join(__dirname, '../../../.erb/dll/preload-webview.js'), 19 | appVersion: app.getVersion(), 20 | }; 21 | } 22 | ); 23 | 24 | ipcMain.on('electron-store-get', async (event, val) => { 25 | event.returnValue = store.get(val); 26 | }); 27 | ipcMain.on('electron-store-set', async (_, key, val) => { 28 | store.set(key, val); 29 | }); 30 | 31 | ipcMain.on(IPC_MAIN_CHANNELS.OPEN_EXTERNAL, async (_, { url }) => { 32 | console.log('Opening external url', url); 33 | shell.openExternal(url); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /desktop-app/src/main/app-updater.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdater } from 'electron-updater'; 2 | 3 | export interface AppUpdaterStatus { 4 | status: string; 5 | version?: string; 6 | lastChecked?: number; 7 | progress?: number; 8 | size?: number; 9 | error?: Error; 10 | } 11 | 12 | export class AppUpdater { 13 | status: string = 'IDLE'; 14 | 15 | version?: string; 16 | 17 | lastChecked?: number; 18 | 19 | progress?: number; 20 | 21 | size?: number; 22 | 23 | error?: Error; 24 | 25 | constructor() { 26 | autoUpdater.logger = console; 27 | autoUpdater.checkForUpdatesAndNotify(); 28 | autoUpdater.on('checking-for-update', () => { 29 | this.status = 'CHECKING'; 30 | this.lastChecked = Date.now(); 31 | }); 32 | autoUpdater.on('update-available', (info) => { 33 | this.status = 'AVAILABLE'; 34 | this.version = info.version; 35 | this.lastChecked = Date.now(); 36 | }); 37 | autoUpdater.on('update-not-available', (info) => { 38 | this.status = 'UP_TO_DATE'; 39 | this.lastChecked = Date.now(); 40 | }); 41 | autoUpdater.on('error', (err) => { 42 | this.status = 'ERROR'; 43 | this.error = err; 44 | this.lastChecked = Date.now(); 45 | }); 46 | autoUpdater.on('download-progress', (progressObj) => { 47 | const logMessage = `Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`; 48 | // eslint-disable-next-line no-console 49 | console.log(logMessage); 50 | this.status = `DOWNLOADING - ${progressObj.percent}%`; 51 | this.progress = progressObj.percent; 52 | this.size = progressObj.total; 53 | this.lastChecked = Date.now(); 54 | }); 55 | autoUpdater.on('update-downloaded', (info) => { 56 | this.status = 'DOWNLOADED (Restart to apply update)'; 57 | this.lastChecked = Date.now(); 58 | }); 59 | } 60 | 61 | getStatus(): AppUpdaterStatus { 62 | return { 63 | status: this.status, 64 | version: this.version, 65 | lastChecked: this.lastChecked, 66 | progress: this.progress, 67 | size: this.size, 68 | error: this.error, 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /desktop-app/src/main/browser-sync.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import BrowserSync, { BrowserSyncInstance } from 'browser-sync'; 3 | import fs from 'fs-extra'; 4 | 5 | export const BROWSER_SYNC_PORT = 12719; 6 | export const BROWSER_SYNC_HOST = `localhost:${BROWSER_SYNC_PORT}`; 7 | export const BROWSER_SYNC_URL = `https://${BROWSER_SYNC_HOST}/browser-sync/browser-sync-client.js?v=2.27.10`; 8 | 9 | const browserSyncEmbed: BrowserSyncInstance = BrowserSync.create('embed'); 10 | 11 | let created = false; 12 | let filesWatcher: ReturnType | null = null; 13 | let cssWatcher: ReturnType | null = null; 14 | 15 | export async function initInstance(): Promise { 16 | if (created) { 17 | return browserSyncEmbed; 18 | } 19 | created = true; 20 | return new Promise((resolve, reject) => { 21 | browserSyncEmbed.init( 22 | { 23 | open: false, 24 | localOnly: true, 25 | https: true, 26 | notify: false, 27 | ui: false, 28 | port: BROWSER_SYNC_PORT, 29 | }, 30 | (err: Error, bs: BrowserSyncInstance) => { 31 | if (err) { 32 | return reject(err); 33 | } 34 | return resolve(bs); 35 | } 36 | ); 37 | }); 38 | } 39 | 40 | export function watchFiles(filePath: string) { 41 | if (filePath && fs.existsSync(filePath)) { 42 | const fileDir = filePath.substring(0, filePath.lastIndexOf('/')); 43 | 44 | filesWatcher = browserSyncEmbed 45 | // @ts-expect-error 46 | .watch([filePath, `${fileDir}/**/**.js`]) 47 | .on('change', browserSyncEmbed.reload); 48 | 49 | cssWatcher = browserSyncEmbed.watch( 50 | `${fileDir}/**/**.css`, 51 | // @ts-expect-error 52 | (event: string, file: string) => { 53 | if (event === 'change') { 54 | browserSyncEmbed.reload(file); 55 | } 56 | } 57 | ); 58 | } 59 | } 60 | 61 | export async function stopWatchFiles() { 62 | if (filesWatcher) { 63 | // @ts-expect-error 64 | await filesWatcher.close(); 65 | } 66 | if (cssWatcher) { 67 | // @ts-expect-error 68 | await cssWatcher.close(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /desktop-app/src/main/cli.ts: -------------------------------------------------------------------------------- 1 | import parseArgs from 'electron-args'; 2 | 3 | let binaryName = 'ResponsivelyApp'; 4 | 5 | if (process.platform === 'darwin') { 6 | binaryName = 7 | '/Applications/ResponsivelyApp.app/Contents/MacOS/ResponsivelyApp'; 8 | } 9 | 10 | if (process.platform === 'win32') { 11 | binaryName = 'ResponsivelyApp.exe'; 12 | } 13 | 14 | const cli = parseArgs( 15 | ` 16 | ResponsivelyApp 17 | 18 | Usage 19 | $ ${binaryName} [path] 20 | 21 | Options 22 | --help show help 23 | --version show version 24 | 25 | Examples 26 | $ ${binaryName} https://example.com 27 | $ ${binaryName} /path/to/index.html 28 | `, 29 | { 30 | alias: { 31 | h: 'help', 32 | }, 33 | } 34 | ); 35 | 36 | export default cli; 37 | -------------------------------------------------------------------------------- /desktop-app/src/main/http-basic-auth/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthInfo, app, BrowserWindow, ipcMain } from 'electron'; 2 | import { IPC_MAIN_CHANNELS } from '../../common/constants'; 3 | 4 | export type AuthRequestArgs = AuthInfo; 5 | 6 | export interface AuthResponseArgs { 7 | username: string; 8 | password: string; 9 | authInfo: AuthInfo; 10 | } 11 | 12 | type Callback = (username: string, password: string) => void; 13 | 14 | const inProgressAuthentications: { [key: string]: Callback[] } = {}; 15 | 16 | const handleLogin = async ( 17 | authInfo: AuthInfo, 18 | mainWindow: BrowserWindow, 19 | callback: (username: string, password: string) => void 20 | ) => { 21 | if (inProgressAuthentications[authInfo.host]) { 22 | inProgressAuthentications[authInfo.host].push(callback); 23 | return; 24 | } 25 | inProgressAuthentications[authInfo.host] = [callback]; 26 | 27 | mainWindow.webContents.send(IPC_MAIN_CHANNELS.AUTH_REQUEST, authInfo); 28 | ipcMain.once( 29 | IPC_MAIN_CHANNELS.AUTH_RESPONSE, 30 | (_, { authInfo: respAuthInfo, username, password }: AuthResponseArgs) => { 31 | inProgressAuthentications[respAuthInfo.host].forEach((cb) => 32 | cb(username, password) 33 | ); 34 | delete inProgressAuthentications[respAuthInfo.host]; 35 | } 36 | ); 37 | }; 38 | 39 | export const initHttpBasicAuthHandlers = (mainWindow: BrowserWindow) => { 40 | app.on('login', (event, _webContents, _request, authInfo, callback) => { 41 | event.preventDefault(); 42 | handleLogin(authInfo, mainWindow, callback); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /desktop-app/src/main/menu/help.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserWindow, 3 | MenuItemConstructorOptions, 4 | ipcMain, 5 | shell, 6 | } from 'electron'; 7 | 8 | import { EnvironmentInfo, getEnvironmentInfo } from '../util'; 9 | import { IPC_MAIN_CHANNELS } from '../../common/constants'; 10 | import { AppUpdater, AppUpdaterStatus } from '../app-updater'; 11 | 12 | export interface AboutDialogArgs { 13 | environmentInfo: EnvironmentInfo; 14 | updaterStatus: AppUpdaterStatus; 15 | } 16 | 17 | export const subMenuHelp = ( 18 | mainWindow: BrowserWindow, 19 | appUpdater: AppUpdater 20 | ): MenuItemConstructorOptions => { 21 | const environmentInfo = getEnvironmentInfo(); 22 | ipcMain.handle('get-about-info', async (_): Promise => { 23 | return { 24 | environmentInfo, 25 | updaterStatus: appUpdater.getStatus(), 26 | }; 27 | }); 28 | 29 | return { 30 | label: 'Help', 31 | submenu: [ 32 | { 33 | label: 'Learn More', 34 | click() { 35 | shell.openExternal('https://responsively.app'); 36 | }, 37 | }, 38 | { 39 | label: 'Open Source', 40 | click() { 41 | shell.openExternal( 42 | 'https://github.com/responsively-org/responsively-app' 43 | ); 44 | }, 45 | }, 46 | { 47 | label: 'Join Discord', 48 | click() { 49 | shell.openExternal('https://responsively.app/join-discord/'); 50 | }, 51 | }, 52 | { 53 | label: 'Search Issues', 54 | click() { 55 | shell.openExternal( 56 | 'https://github.com/responsively-org/responsively-app/issues' 57 | ); 58 | }, 59 | }, 60 | { 61 | label: 'Sponsor Responsively', 62 | click() { 63 | shell.openExternal( 64 | 'https://responsively.app/sponsor?utm_source=app&utm_medium=menu&utm_campaign=sponsor' 65 | ); 66 | }, 67 | }, 68 | { 69 | type: 'separator', 70 | }, 71 | { 72 | label: 'About', 73 | accelerator: 'F1', 74 | click: () => { 75 | mainWindow.webContents.send(IPC_MAIN_CHANNELS.OPEN_ABOUT_DIALOG, { 76 | environmentInfo, 77 | updaterStatus: appUpdater.getStatus(), 78 | }); 79 | }, 80 | }, 81 | ], 82 | }; 83 | }; 84 | -------------------------------------------------------------------------------- /desktop-app/src/main/menu/view.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, MenuItemConstructorOptions } from 'electron'; 2 | 3 | const isMac = process.platform === 'darwin'; 4 | const isDev = 5 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 6 | 7 | const getToggleFullScreen = ( 8 | mainWindow: BrowserWindow 9 | ): MenuItemConstructorOptions => ({ 10 | label: 'Toggle &Full Screen', 11 | accelerator: isMac ? 'Ctrl+CommandOrControl+F' : 'F11', 12 | click: () => { 13 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 14 | }, 15 | }); 16 | 17 | const getToggleDevTools = ( 18 | mainWindow: BrowserWindow 19 | ): MenuItemConstructorOptions => ({ 20 | label: 'Toggle &Developer Tools', 21 | accelerator: isMac ? 'Alt+CommandOrControl+I' : 'Alt+Ctrl+I', 22 | click: () => { 23 | mainWindow.webContents.toggleDevTools(); 24 | }, 25 | }); 26 | 27 | const getReloadMenu = ( 28 | mainWindow: BrowserWindow 29 | ): MenuItemConstructorOptions => ({ 30 | label: '&Reload', 31 | accelerator: 'CommandOrControl+R', 32 | click: () => { 33 | if (isDev) { 34 | mainWindow.webContents.reload(); 35 | return; 36 | } 37 | mainWindow.webContents.send('reload', {}); 38 | }, 39 | }); 40 | 41 | const getReloadIgnoringCacheMenu = ( 42 | mainWindow: BrowserWindow 43 | ): MenuItemConstructorOptions => ({ 44 | label: 'Reload Ignoring Cache', 45 | accelerator: 'CommandOrControl+Shift+R', 46 | click: () => { 47 | mainWindow.webContents.send('reload', { ignoreCache: true }); 48 | }, 49 | }); 50 | 51 | const getViewMenuProd = ( 52 | mainWindow: BrowserWindow 53 | ): MenuItemConstructorOptions => ({ 54 | label: '&View', 55 | submenu: [ 56 | getReloadMenu(mainWindow), 57 | getReloadIgnoringCacheMenu(mainWindow), 58 | getToggleFullScreen(mainWindow), 59 | ], 60 | }); 61 | 62 | const getViewMenuDev = ( 63 | mainWindow: BrowserWindow 64 | ): MenuItemConstructorOptions => ({ 65 | label: '&View', 66 | submenu: [ 67 | getReloadMenu(mainWindow), 68 | getToggleDevTools(mainWindow), 69 | getToggleFullScreen(mainWindow), 70 | ], 71 | }); 72 | 73 | export const getViewMenu = isDev ? getViewMenuDev : getViewMenuProd; 74 | -------------------------------------------------------------------------------- /desktop-app/src/main/native-functions/index.ts: -------------------------------------------------------------------------------- 1 | import { clipboard, ipcMain, nativeTheme, webContents } from 'electron'; 2 | 3 | export interface DisableDefaultWindowOpenHandlerArgs { 4 | webContentsId: number; 5 | } 6 | 7 | export interface DisableDefaultWindowOpenHandlerResult { 8 | done: boolean; 9 | } 10 | 11 | export interface SetNativeThemeArgs { 12 | theme: 'dark' | 'light'; 13 | } 14 | 15 | export interface SetNativeThemeResult { 16 | done: boolean; 17 | } 18 | 19 | export const initNativeFunctionHandlers = () => { 20 | ipcMain.handle( 21 | 'disable-default-window-open-handler', 22 | async ( 23 | _, 24 | arg: DisableDefaultWindowOpenHandlerArgs 25 | ): Promise => { 26 | webContents.fromId(arg.webContentsId)?.setWindowOpenHandler(() => { 27 | return { action: 'deny' }; 28 | }); 29 | return { done: true }; 30 | } 31 | ); 32 | 33 | ipcMain.handle( 34 | 'set-native-theme', 35 | async (_, arg: SetNativeThemeArgs): Promise => { 36 | const { theme } = arg; 37 | nativeTheme.themeSource = theme; 38 | return { done: true }; 39 | } 40 | ); 41 | 42 | ipcMain.handle('copy-to-clipboard', async (_, arg: string): Promise => { 43 | clipboard.writeText(arg); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /desktop-app/src/main/preload-webview.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | const documentBodyInit = () => { 4 | // Browser Sync 5 | const bsScript = window.document.createElement('script'); 6 | bsScript.src = 7 | 'https://localhost:12719/browser-sync/browser-sync-client.js?v=2.27.10'; 8 | bsScript.async = true; 9 | window.document.body.appendChild(bsScript); 10 | 11 | // Context Menu 12 | window.addEventListener('contextmenu', (e) => { 13 | e.preventDefault(); 14 | ipcRenderer.send('show-context-menu', { 15 | contextMenuMeta: { x: e.x, y: e.y }, 16 | }); 17 | }); 18 | 19 | window.addEventListener('wheel', (e) => { 20 | ipcRenderer.sendToHost('pass-scroll-data', { 21 | coordinates: { x: e.deltaX, y: e.deltaY }, 22 | innerHeight: document.body.scrollHeight, 23 | innerWidth: window.innerWidth, 24 | }); 25 | }); 26 | 27 | window.addEventListener('dom-ready', () => { 28 | const { body } = document; 29 | const html = document.documentElement; 30 | 31 | const height = Math.max( 32 | body.scrollHeight, 33 | body.offsetHeight, 34 | html.clientHeight, 35 | html.scrollHeight, 36 | html.offsetHeight 37 | ); 38 | 39 | ipcRenderer.sendToHost('pass-scroll-data', { 40 | coordinates: { x: 0, y: 0 }, 41 | innerHeight: height, 42 | innerWidth: window.innerWidth, 43 | }); 44 | }); 45 | }; 46 | 47 | ipcRenderer.on('context-menu-command', (_, command) => { 48 | ipcRenderer.sendToHost('context-menu-command', command); 49 | }); 50 | 51 | const documentBodyWaitHandle = setInterval(() => { 52 | window.onerror = function logError(errorMsg, url, lineNumber) { 53 | // eslint-disable-next-line no-console 54 | console.log(`Unhandled error: ${errorMsg} ${url} ${lineNumber}`); 55 | // Code to run when an error has occurred on the page 56 | }; 57 | 58 | if (window?.document?.body) { 59 | clearInterval(documentBodyWaitHandle); 60 | try { 61 | documentBodyInit(); 62 | } catch (err) { 63 | // eslint-disable-next-line no-console 64 | console.log('Error in documentBodyInit:', err); 65 | } 66 | 67 | return; 68 | } 69 | // eslint-disable-next-line no-console 70 | console.log('document.body not ready'); 71 | }, 300); 72 | -------------------------------------------------------------------------------- /desktop-app/src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { Channels } from 'common/constants'; 2 | import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; 3 | import { Titlebar, TitlebarColor } from 'custom-electron-titlebar'; 4 | 5 | contextBridge.exposeInMainWorld('electron', { 6 | ipcRenderer: { 7 | sendMessage(channel: Channels, args: T[]) { 8 | ipcRenderer.send(channel, args); 9 | }, 10 | on(channel: Channels, func: (...args: T[]) => void) { 11 | const subscription = (_event: IpcRendererEvent, ...args: T[]) => 12 | func(...args); 13 | ipcRenderer.on(channel, subscription); 14 | 15 | return () => ipcRenderer.removeListener(channel, subscription); 16 | }, 17 | once(channel: Channels, func: (...args: T[]) => void) { 18 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 19 | }, 20 | invoke(channel: Channels, ...args: T[]): Promise

{ 21 | return ipcRenderer.invoke(channel, ...args); 22 | }, 23 | removeListener(channel: Channels, listener: (...args: T[]) => void) { 24 | ipcRenderer.removeListener(channel, listener); 25 | }, 26 | removeAllListeners(channel: Channels) { 27 | ipcRenderer.removeAllListeners(channel); 28 | }, 29 | }, 30 | store: { 31 | get(val: any) { 32 | return ipcRenderer.sendSync('electron-store-get', val); 33 | }, 34 | set(property: string, val: any) { 35 | ipcRenderer.send('electron-store-set', property, val); 36 | }, 37 | // Other method you want to add like has(), reset(), etc. 38 | }, 39 | }); 40 | 41 | window.onerror = function (errorMsg, url, lineNumber) { 42 | // eslint-disable-next-line no-console 43 | console.log(`Unhandled error: ${errorMsg} ${url} ${lineNumber}`); 44 | // Code to run when an error has occurred on the page 45 | }; 46 | 47 | window.addEventListener('DOMContentLoaded', () => { 48 | const customTitlebarStatus = ipcRenderer.sendSync( 49 | 'electron-store-get', 50 | 'userPreferences.customTitlebar' 51 | ) as boolean; 52 | 53 | if (customTitlebarStatus && process.platform === 'win32') { 54 | // eslint-disable-next-line no-new 55 | new Titlebar({ 56 | backgroundColor: TitlebarColor.fromHex('#0f172a'), // slate-900 57 | itemBackgroundColor: TitlebarColor.fromHex('#1e293b'), // slate-800 58 | }); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /desktop-app/src/main/protocol-handler/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | import { IPC_MAIN_CHANNELS } from '../../common/constants'; 3 | 4 | // eslint-disable-next-line import/prefer-default-export 5 | export const openUrl = (url: string, mainWindow: BrowserWindow | null) => { 6 | mainWindow?.webContents.send(IPC_MAIN_CHANNELS.OPEN_URL, { 7 | url, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /desktop-app/src/main/screenshot/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable promise/always-return */ 2 | import { Device } from 'common/deviceList'; 3 | import { ipcMain, shell, webContents } from 'electron'; 4 | import { writeFile, ensureDir } from 'fs-extra'; 5 | import path from 'path'; 6 | import store from '../../store'; 7 | 8 | export interface ScreenshotArgs { 9 | webContentsId: number; 10 | fullPage?: boolean; 11 | device: Device; 12 | } 13 | 14 | export interface ScreenshotAllArgs { 15 | webContentsId: number; 16 | device: Device; 17 | previousHeight: string; 18 | previousTransform: string; 19 | pageHeight: number; 20 | } 21 | 22 | export interface ScreenshotResult { 23 | done: boolean; 24 | } 25 | const captureImage = async ( 26 | webContentsId: number 27 | ): Promise => { 28 | const WebContents = webContents.fromId(webContentsId); 29 | 30 | const isExecuted = await WebContents?.executeJavaScript(` 31 | if (window.isExecuted) { 32 | true; 33 | } 34 | `); 35 | 36 | if (!isExecuted) { 37 | await WebContents?.executeJavaScript(` 38 | const bgColor = window.getComputedStyle(document.body).backgroundColor; 39 | if (bgColor === 'rgba(0, 0, 0, 0)') { 40 | document.body.style.backgroundColor = 'white'; 41 | } 42 | window.isExecuted = true; 43 | `); 44 | } 45 | 46 | const Image = await WebContents?.capturePage(); 47 | return Image; 48 | }; 49 | 50 | const quickScreenshot = async ( 51 | arg: ScreenshotArgs 52 | ): Promise => { 53 | const { 54 | webContentsId, 55 | device: { name }, 56 | } = arg; 57 | const image = await captureImage(webContentsId); 58 | if (image === undefined) { 59 | return { done: false }; 60 | } 61 | const fileName = name.replaceAll('/', '-').replaceAll(':', '-'); 62 | const dir = store.get('userPreferences.screenshot.saveLocation'); 63 | const filePath = path.join(dir, `/${fileName}-${Date.now()}.jpeg`); 64 | await ensureDir(dir); 65 | await writeFile(filePath, image.toJPEG(100)); 66 | setTimeout(() => shell.showItemInFolder(filePath), 100); 67 | 68 | return { done: true }; 69 | }; 70 | 71 | const captureAllDecies = async ( 72 | args: Array 73 | ): Promise => { 74 | const screenShots = args.map((arg) => { 75 | const { device, webContentsId } = arg; 76 | const screenShotArg: ScreenshotArgs = { device, webContentsId }; 77 | return quickScreenshot(screenShotArg); 78 | }); 79 | 80 | await Promise.all(screenShots); 81 | return { done: true }; 82 | }; 83 | 84 | export const initScreenshotHandlers = () => { 85 | ipcMain.handle( 86 | 'screenshot', 87 | async (_, arg: ScreenshotArgs): Promise => { 88 | return quickScreenshot(arg); 89 | } 90 | ); 91 | 92 | ipcMain.handle( 93 | 'screenshot:All', 94 | async (event, args: Array) => { 95 | return captureAllDecies(args); 96 | } 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /desktop-app/src/main/screenshot/webpage.ts: -------------------------------------------------------------------------------- 1 | class WebPage { 2 | webview: Electron.WebContents; 3 | 4 | constructor(webview: Electron.WebContents) { 5 | this.webview = webview; 6 | } 7 | 8 | async getPageHeight() { 9 | return this.webview.executeJavaScript('document.body.scrollHeight'); 10 | } 11 | 12 | async getViewportHeight() { 13 | return this.webview.executeJavaScript('window.innerHeight'); 14 | } 15 | 16 | async scrollTo(x: number, y: number) { 17 | return this.webview.executeJavaScript(`window.scrollTo(${x}, ${y})`); 18 | } 19 | } 20 | 21 | export default WebPage; 22 | -------------------------------------------------------------------------------- /desktop-app/src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | import { app } from 'electron'; 5 | import fs from 'fs-extra'; 6 | import os from 'os'; 7 | 8 | export function resolveHtmlPath(htmlFileName: string) { 9 | if (process.env.NODE_ENV === 'development') { 10 | const port = process.env.PORT || 1212; 11 | const url = new URL(`http://localhost:${port}`); 12 | url.pathname = htmlFileName; 13 | return url.href; 14 | } 15 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 16 | } 17 | 18 | let isCliArgResult: boolean | undefined; 19 | 20 | export function isValidCliArgURL(arg?: string): boolean { 21 | if (isCliArgResult !== undefined) { 22 | return isCliArgResult; 23 | } 24 | if (arg == null || arg === '') { 25 | isCliArgResult = false; 26 | return false; 27 | } 28 | try { 29 | const url = new URL(arg); 30 | if ( 31 | url.protocol === 'http:' || 32 | url.protocol === 'https:' || 33 | url.protocol === 'file:' 34 | ) { 35 | isCliArgResult = true; 36 | return true; 37 | } 38 | // eslint-disable-next-line no-console 39 | console.warn('Protocol not supported', url.protocol); 40 | } catch (e) { 41 | // eslint-disable-next-line no-console 42 | console.warn('Not a valid URL', arg, e); 43 | } 44 | isCliArgResult = false; 45 | return false; 46 | } 47 | 48 | export const getPackageJson = () => { 49 | let appPath; 50 | if (process.env.NODE_ENV === 'production') appPath = app.getAppPath(); 51 | else appPath = process.cwd(); 52 | 53 | const pkgPath = path.join(appPath, 'package.json'); 54 | if (fs.existsSync(pkgPath)) { 55 | const pkgContent = fs.readFileSync(pkgPath, 'utf-8'); 56 | return JSON.parse(pkgContent); 57 | } 58 | console.error(`cant find package.json in: '${appPath}'`); 59 | return {}; 60 | }; 61 | 62 | export interface EnvironmentInfo { 63 | appVersion: string; 64 | electronVersion: string; 65 | chromeVersion: string; 66 | nodeVersion: string; 67 | v8Version: string; 68 | osInfo: string; 69 | } 70 | 71 | export const getEnvironmentInfo = (): EnvironmentInfo => { 72 | const pkg = getPackageJson(); 73 | const appVersion = pkg.version || 'Unknown'; 74 | const electronVersion = process.versions.electron || 'Unknown'; 75 | const chromeVersion = process.versions.chrome || 'Unknown'; 76 | const nodeVersion = process.versions.node || 'Unknown'; 77 | const v8Version = process.versions.v8 || 'Unknown'; 78 | const osInfo = 79 | `${os.type()} ${os.arch()} ${os.release()}`.trim() || 'Unknown'; 80 | 81 | return { 82 | appVersion, 83 | electronVersion, 84 | chromeVersion, 85 | nodeVersion, 86 | v8Version, 87 | osInfo, 88 | }; 89 | }; 90 | -------------------------------------------------------------------------------- /desktop-app/src/main/web-permissions/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, session } from 'electron'; 2 | import PermissionsManager, { PERMISSION_STATE } from './PermissionsManager'; 3 | import store from '../../store'; 4 | 5 | // eslint-disable-next-line import/prefer-default-export 6 | export const WebPermissionHandlers = (mainWindow: BrowserWindow) => { 7 | const permissionsManager = new PermissionsManager(mainWindow); 8 | return { 9 | init: () => { 10 | session.defaultSession.setPermissionRequestHandler( 11 | (webContents, permission, callback) => { 12 | permissionsManager.requestPermission( 13 | new URL(webContents.getURL()).origin, 14 | permission, 15 | callback 16 | ); 17 | } 18 | ); 19 | 20 | session.defaultSession.setPermissionCheckHandler( 21 | (_webContents, permission, requestingOrigin) => { 22 | const status = permissionsManager.getPermissionState( 23 | requestingOrigin, 24 | permission 25 | ); 26 | return status === PERMISSION_STATE.GRANTED; 27 | } 28 | ); 29 | 30 | session.defaultSession.webRequest.onBeforeSendHeaders( 31 | { 32 | urls: [''], 33 | }, 34 | (details, callback) => { 35 | const acceptLanguage = store.get( 36 | 'userPreferences.webRequestHeaderAcceptLanguage' 37 | ); 38 | if (acceptLanguage != null && acceptLanguage !== '') { 39 | details.requestHeaders['Accept-Language'] = store.get( 40 | 'userPreferences.webRequestHeaderAcceptLanguage' 41 | ); 42 | } 43 | callback({ requestHeaders: details.requestHeaders }); 44 | } 45 | ); 46 | }, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /desktop-app/src/main/webview-context-menu/common.ts: -------------------------------------------------------------------------------- 1 | interface ContextMenuMetadata { 2 | id: string; 3 | label: string; 4 | } 5 | 6 | export const CONTEXT_MENUS: { [key: string]: ContextMenuMetadata } = { 7 | INSPECT_ELEMENT: { id: 'INSPECT_ELEMENT', label: 'Inspect Element' }, 8 | OPEN_CONSOLE: { id: 'OPEN_CONSOLE', label: 'Open Console' }, 9 | }; 10 | -------------------------------------------------------------------------------- /desktop-app/src/main/webview-context-menu/register.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain, Menu } from 'electron'; 2 | import { CONTEXT_MENUS } from './common'; 3 | // import { webViewPubSub } from '../../renderer/lib/pubsub'; 4 | // import { MOUSE_EVENTS } from '../ruler'; 5 | 6 | export const initWebviewContextMenu = () => { 7 | ipcMain.removeAllListeners('show-context-menu'); 8 | ipcMain.on('show-context-menu', (event, ...args) => { 9 | const template: Electron.MenuItemConstructorOptions[] = Object.values( 10 | CONTEXT_MENUS 11 | ).map((menu) => { 12 | return { 13 | label: menu.label, 14 | click: () => { 15 | event.sender.send('context-menu-command', { 16 | command: menu.id, 17 | arg: args[0], 18 | }); 19 | }, 20 | }; 21 | }); 22 | const menu = Menu.buildFromTemplate(template); 23 | menu.popup( 24 | BrowserWindow.fromWebContents(event.sender) as Electron.PopupOptions 25 | ); 26 | }); 27 | // ipcMain.on('pass-scroll-data', (event, ...args) => { 28 | // console.log(args[0].coordinates); 29 | // webViewPubSub.publish(MOUSE_EVENTS.SCROLL, [args[0].coordinates]); 30 | // }); 31 | }; 32 | 33 | export default initWebviewContextMenu; 34 | -------------------------------------------------------------------------------- /desktop-app/src/main/webview-storage-manager/index.ts: -------------------------------------------------------------------------------- 1 | import { ClearStorageDataOptions, ipcMain, webContents } from 'electron'; 2 | 3 | export interface DeleteStorageArgs { 4 | webContentsId: number; 5 | storages?: string[]; 6 | } 7 | 8 | export interface DeleteStorageResult { 9 | done: boolean; 10 | } 11 | 12 | const deleteStorage = async ( 13 | arg: DeleteStorageArgs 14 | ): Promise => { 15 | const { webContentsId, storages } = arg; 16 | if (storages?.length === 1 && storages[0] === 'network-cache') { 17 | await webContents.fromId(webContentsId)?.session.clearCache(); 18 | } else { 19 | await webContents 20 | .fromId(webContentsId) 21 | ?.session.clearStorageData({ storages } as ClearStorageDataOptions); 22 | } 23 | return { done: true }; 24 | }; 25 | 26 | export const initWebviewStorageManagerHandlers = () => { 27 | ipcMain.handle( 28 | 'delete-storage', 29 | async (_, arg: DeleteStorageArgs): Promise => { 30 | return deleteStorage(arg); 31 | } 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules 3 | * See https://github.com/webpack-contrib/sass-loader#imports 4 | */ 5 | 6 | @import '~@fontsource/lato/300.css'; 7 | @import '~@fontsource/lato/400.css'; 8 | @import '~@fontsource/lato/400-italic.css'; 9 | @import '~@fontsource/lato/700.css'; 10 | 11 | @tailwind base; 12 | @tailwind components; 13 | @tailwind utilities; 14 | 15 | html { 16 | user-select: none; 17 | } 18 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/AppContent.tsx: -------------------------------------------------------------------------------- 1 | import { Provider, useSelector } from 'react-redux'; 2 | 3 | import ToolBar from './components/ToolBar'; 4 | import Previewer from './components/Previewer'; 5 | import { store } from './store'; 6 | 7 | import './App.css'; 8 | import ThemeProvider from './context/ThemeProvider'; 9 | import type { AppView } from './store/features/ui'; 10 | import { APP_VIEWS, selectAppView } from './store/features/ui'; 11 | import DeviceManager from './components/DeviceManager'; 12 | import KeyboardShortcutsManager from './components/KeyboardShortcutsManager'; 13 | import { ReleaseNotes } from './components/ReleaseNotes'; 14 | import { Sponsorship } from './components/Sponsorship'; 15 | import { AboutDialog } from './components/AboutDialog'; 16 | 17 | if ((navigator as any).userAgentData.platform === 'Windows') { 18 | import('./titlebar-styles.css'); 19 | } 20 | 21 | const Browser = () => { 22 | return ( 23 |

24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | const getView = (appView: AppView) => { 31 | switch (appView) { 32 | case APP_VIEWS.BROWSER: 33 | return ; 34 | case APP_VIEWS.DEVICE_MANAGER: 35 | return ; 36 | default: 37 | return ; 38 | } 39 | }; 40 | 41 | const ViewComponent = () => { 42 | const appView = useSelector(selectAppView); 43 | 44 | return <>{getView(appView)}; 45 | }; 46 | 47 | const AppContent = () => { 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | }; 60 | export default AppContent; 61 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/src/renderer/assets/img/logo.png -------------------------------------------------------------------------------- /desktop-app/src/renderer/assets/sfx/screenshot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/responsively-org/responsively-app/5323ae73f815f44a555f47f1631323df6e186360/desktop-app/src/renderer/assets/sfx/screenshot.mp3 -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Accordion/Accordion.tsx: -------------------------------------------------------------------------------- 1 | export const Accordion = ({ children }: { children: JSX.Element }) => { 2 | return ( 3 |
4 | {children} 5 |
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Accordion/AccordionItem.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | type AccordionItemProps = { 4 | title: string; 5 | children: JSX.Element; 6 | }; 7 | 8 | export const AccordionItem = ({ title, children }: AccordionItemProps) => { 9 | const [isOpen, setIsOpen] = useState(true); 10 | 11 | const toggle = () => { 12 | setIsOpen(!isOpen); 13 | }; 14 | 15 | return ( 16 |
17 |

18 | 42 |

43 |
48 |
49 | {children} 50 |
51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Accordion/index.tsx: -------------------------------------------------------------------------------- 1 | export { Accordion } from './Accordion'; 2 | export { AccordionItem } from './AccordionItem'; 3 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Button/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { act, render, screen } from '@testing-library/react'; 3 | import Button from './index'; 4 | 5 | jest.mock('@iconify/react', () => ({ 6 | Icon: () =>
, 7 | })); 8 | 9 | describe('Button Component', () => { 10 | it('renders with default props', () => { 11 | render(); 12 | const buttonElement = screen.getByRole('button', { name: /click me/i }); 13 | expect(buttonElement).toBeInTheDocument(); 14 | }); 15 | 16 | it('applies custom class name', () => { 17 | render(); 18 | const buttonElement = screen.getByRole('button', { name: /click me/i }); 19 | expect(buttonElement).toHaveClass('custom-class'); 20 | }); 21 | 22 | it('renders loading icon when isLoading is true', () => { 23 | render(); 24 | const loadingIcon = screen.getByTestId('icon'); 25 | expect(loadingIcon).toBeInTheDocument(); 26 | }); 27 | 28 | it('renders confirmation icon when loading is done', () => { 29 | jest.useFakeTimers(); 30 | const { rerender } = render(); 31 | 32 | act(() => { 33 | rerender(); 34 | jest.runAllTimers(); // Use act to advance timers 35 | }); 36 | 37 | const confirmationIcon = screen.getByTestId('icon'); 38 | expect(confirmationIcon).toBeInTheDocument(); 39 | jest.useRealTimers(); 40 | }); 41 | 42 | it('applies primary button styles', () => { 43 | render(); 44 | const buttonElement = screen.getByRole('button', { name: /click me/i }); 45 | expect(buttonElement).toHaveClass('bg-emerald-500'); 46 | expect(buttonElement).toHaveClass('text-white'); 47 | }); 48 | 49 | it('applies action button styles', () => { 50 | render(); 51 | const buttonElement = screen.getByRole('button', { name: /click me/i }); 52 | expect(buttonElement).toHaveClass('bg-slate-200'); 53 | }); 54 | 55 | it('applies subtle hover styles', () => { 56 | render(); 57 | const buttonElement = screen.getByRole('button', { name: /click me/i }); 58 | expect(buttonElement).toHaveClass('hover:bg-slate-200'); 59 | }); 60 | 61 | it('disables hover effects when disableHoverEffects is true', () => { 62 | render( 63 | 66 | ); 67 | const buttonElement = screen.getByRole('button', { name: /click me/i }); 68 | expect(buttonElement).not.toHaveClass('hover:bg-slate-200'); 69 | }); 70 | 71 | it('renders children correctly when not loading or loading done', () => { 72 | render(); 73 | const buttonElement = screen.getByText('Click me'); 74 | expect(buttonElement).toBeInTheDocument(); 75 | }); 76 | 77 | it('does not render children when loading or loading done', () => { 78 | const { rerender } = render(); 79 | expect(screen.queryByText('Click me')).not.toBeInTheDocument(); 80 | 81 | act(() => { 82 | rerender(); 83 | }); 84 | 85 | expect(screen.queryByText('Click me')).not.toBeInTheDocument(); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import cx from 'classnames'; 3 | import { Icon } from '@iconify/react'; 4 | 5 | interface CustomProps { 6 | className?: string; 7 | isActive?: boolean; 8 | isLoading?: boolean; 9 | isPrimary?: boolean; 10 | isTextButton?: boolean; 11 | disableHoverEffects?: boolean; 12 | isActionButton?: boolean; 13 | subtle?: boolean; 14 | } 15 | 16 | const Button = ({ 17 | className = '', 18 | isActive = false, 19 | isLoading = false, 20 | isPrimary = false, 21 | isTextButton = false, 22 | isActionButton = false, 23 | subtle = false, 24 | disableHoverEffects = false, 25 | children, 26 | ...props 27 | }: CustomProps & 28 | React.DetailedHTMLProps< 29 | React.ButtonHTMLAttributes, 30 | HTMLButtonElement 31 | >) => { 32 | const [isLoadingDone, setIsLoadingDone] = useState(false); 33 | const prevLoadingState = useRef(false); 34 | 35 | useEffect(() => { 36 | if (!isLoading && prevLoadingState.current === true) { 37 | setIsLoadingDone(true); 38 | setTimeout(() => { 39 | setIsLoadingDone(false); 40 | }, 800); 41 | } 42 | prevLoadingState.current = isLoading; 43 | }, [isLoading]); 44 | 45 | let hoverBg = 'hover:bg-slate-400'; 46 | let hoverBgDark = 'dark:hover:bg-slate-600'; 47 | if (subtle) { 48 | hoverBg = 'hover:bg-slate-200'; 49 | hoverBgDark = 'dark:hover:bg-slate-700'; 50 | } else if (isPrimary) { 51 | hoverBg = 'hover:bg-emerald-600'; 52 | hoverBgDark = 'dark:hover:bg-emerald-600'; 53 | } 54 | 55 | return ( 56 | 81 | ); 82 | }; 83 | 84 | export default Button; 85 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/ButtonGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | interface Props { 5 | buttons: { 6 | content: ReactElement; 7 | srContent: string; 8 | onClick: () => void; 9 | isActive: boolean; 10 | }[]; 11 | } 12 | 13 | export const ButtonGroup = ({ buttons }: Props) => { 14 | return ( 15 | 16 | {buttons.map(({ content, srContent, onClick, isActive }, index) => ( 17 | 32 | ))} 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/ConfirmDialog/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Button from '../Button'; 3 | import Modal from '../Modal'; 4 | 5 | export const ConfirmDialog = ({ 6 | onClose, 7 | onConfirm, 8 | open, 9 | confirmText, 10 | }: { 11 | onClose?: () => void; 12 | onConfirm?: () => void; 13 | open: boolean; 14 | confirmText?: string; 15 | }) => { 16 | const [isOpen, setIsOpen] = useState(open); 17 | 18 | useEffect(() => { 19 | setIsOpen(open); 20 | }, [open]); 21 | 22 | const handleClose = () => { 23 | if (onClose) { 24 | onClose(); 25 | } 26 | setIsOpen(false); 27 | }; 28 | 29 | const handleConfirm = () => { 30 | if (onConfirm) { 31 | onConfirm(); 32 | } 33 | setIsOpen(false); 34 | }; 35 | 36 | return ( 37 | 38 |
42 |

43 |

{confirmText || 'Are you sure?'}

44 |

45 |
46 | 52 | 58 |
59 |
60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/CreateSuiteButton/CreateSuiteModal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | import { addSuite } from 'renderer/store/features/device-manager'; 6 | 7 | import Button from '../../../Button'; 8 | import Input from '../../../Input'; 9 | import Modal from '../../../Modal'; 10 | 11 | interface Props { 12 | isOpen: boolean; 13 | onClose: () => void; 14 | } 15 | 16 | export const CreateSuiteModal = ({ isOpen, onClose }: Props) => { 17 | const [name, setName] = useState(''); 18 | const dispatch = useDispatch(); 19 | 20 | const handleAddSuite = async (): Promise => { 21 | if (name === '') { 22 | // eslint-disable-next-line no-alert 23 | return alert( 24 | 'Suite name cannot be empty. Please enter a name for the suite.' 25 | ); 26 | } 27 | dispatch(addSuite({ id: uuidv4(), name, devices: ['10008'] })); 28 | return onClose(); 29 | }; 30 | 31 | return ( 32 | <> 33 | 34 |
35 |
36 | setName(e.target.value)} 42 | /> 43 |
44 |
45 |
46 | 49 | 52 |
53 |
54 |
55 |
56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/CreateSuiteButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react'; 2 | import { useState } from 'react'; 3 | import Button from 'renderer/components/Button'; 4 | import { CreateSuiteModal } from './CreateSuiteModal'; 5 | 6 | export const CreateSuiteButton = () => { 7 | const [open, setOpen] = useState(false); 8 | return ( 9 |
10 | Add Suite 11 | 17 | setOpen(false)} /> 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/ManageSuitesTool/ManageSuitesToolError.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent } from '@testing-library/react'; 2 | import '@testing-library/jest-dom'; 3 | import { ManageSuitesToolError } from './ManageSuitesToolError'; 4 | 5 | describe('ManageSuitesToolError', () => { 6 | it('renders the error message and close button', () => { 7 | const onClose = jest.fn(); 8 | render(); 9 | 10 | expect( 11 | screen.getByText('There has been an error, please try again.') 12 | ).toBeInTheDocument(); 13 | expect(screen.getByText('Close')).toBeInTheDocument(); 14 | }); 15 | 16 | it('calls onClose when the close button is clicked', () => { 17 | const onClose = jest.fn(); 18 | render(); 19 | 20 | fireEvent.click(screen.getByText('Close')); 21 | expect(onClose).toHaveBeenCalledTimes(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/ManageSuitesTool/ManageSuitesToolError.tsx: -------------------------------------------------------------------------------- 1 | import Button from 'renderer/components/Button'; 2 | 3 | export const ManageSuitesToolError = ({ onClose }: { onClose: () => void }) => { 4 | return ( 5 |
9 |
10 |

There has been an error, please try again.

11 |
12 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/ManageSuitesTool/helpers.ts: -------------------------------------------------------------------------------- 1 | import { defaultDevices, Device } from 'common/deviceList'; 2 | 3 | export const downloadFile = >( 4 | fileData: T 5 | ) => { 6 | const jsonString = JSON.stringify(fileData, null, 2); 7 | const blob = new Blob([jsonString], { type: 'application/json' }); 8 | const url = URL.createObjectURL(blob); 9 | const link = document.createElement('a'); 10 | 11 | link.href = url; 12 | link.download = `responsively_backup_${new Date().toLocaleDateString()}.json`; 13 | 14 | document.body.appendChild(link); 15 | link.click(); 16 | document.body.removeChild(link); 17 | URL.revokeObjectURL(url); 18 | }; 19 | 20 | export const setCustomDevices = (customDevices: Device[]) => { 21 | const importedCustomDevices = customDevices.filter( 22 | (item: Device) => !defaultDevices.includes(item) 23 | ); 24 | 25 | window.electron.store.set( 26 | 'deviceManager.customDevices', 27 | importedCustomDevices 28 | ); 29 | 30 | return importedCustomDevices; 31 | }; 32 | 33 | export const onFileDownload = () => { 34 | const fileData = { 35 | customDevices: window.electron.store.get('deviceManager.customDevices'), 36 | suites: window.electron.store.get('deviceManager.previewSuites'), 37 | }; 38 | downloadFile(fileData); 39 | }; 40 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/ManageSuitesTool/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "customDevices": [ 3 | { 4 | "id": "123", 5 | "name": "a new test", 6 | "width": 400, 7 | "height": 600, 8 | "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", 9 | "typse": "phone", 10 | "dpxxi": 1, 11 | "isTouchCapable": true, 12 | "isMobileCapable": true, 13 | "capabilities": [ 14 | "touch", 15 | "mobile" 16 | ], 17 | "isCustom": true 18 | } 19 | ], 20 | "suites": [ 21 | { 22 | "id": "default", 23 | "name": "Default", 24 | "devices": [ 25 | "10008" 26 | ] 27 | }, 28 | { 29 | "id": "a4c142fc-debd-4eaa-beba-aef60093151c", 30 | "name": "my custom suite", 31 | "devices": [ 32 | "10008", 33 | "30014" 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/ManageSuitesTool/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { transformFile } from './utils'; 2 | 3 | describe('transformFile', () => { 4 | it('should parse JSON content of the file', async () => { 5 | const jsonContent = { key: 'value' }; 6 | const file = new Blob([JSON.stringify(jsonContent)], { 7 | type: 'application/json', 8 | }) as File; 9 | Object.defineProperty(file, 'name', { value: 'test.json' }); 10 | 11 | const result = await transformFile(file); 12 | expect(result).toEqual(jsonContent); 13 | }); 14 | 15 | it('should throw an error for invalid JSON', async () => { 16 | const invalidJsonContent = "{ key: 'value' }"; // Invalid JSON 17 | const file = new Blob([invalidJsonContent], { 18 | type: 'application/json', 19 | }) as File; 20 | Object.defineProperty(file, 'name', { value: 'test.json' }); 21 | 22 | await expect(transformFile(file)).rejects.toThrow(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/ManageSuitesTool/utils.ts: -------------------------------------------------------------------------------- 1 | export const transformFile = (file: File): Promise<{ [key: string]: any }> => { 2 | return new Promise((resolve, reject) => { 3 | const reader = new FileReader(); 4 | 5 | reader.onload = () => { 6 | try { 7 | const jsonContent = JSON.parse(reader.result as string); 8 | resolve(jsonContent); 9 | } catch (error) { 10 | reject(error); 11 | } 12 | }; 13 | 14 | reader.onerror = () => { 15 | reject(reader.error); 16 | }; 17 | 18 | reader.readAsText(file); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/Suite.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react'; 2 | import cx from 'classnames'; 3 | import { Device, getDevicesMap } from 'common/deviceList'; 4 | import { useDrop } from 'react-dnd'; 5 | import { useDispatch } from 'react-redux'; 6 | import Button from 'renderer/components/Button'; 7 | import { 8 | PreviewSuite, 9 | deleteSuite, 10 | setActiveSuite, 11 | setSuiteDevices, 12 | } from 'renderer/store/features/device-manager'; 13 | import DeviceLabel, { DND_TYPE } from '../DeviceLabel'; 14 | 15 | interface Props { 16 | suite: PreviewSuite; 17 | isActive: boolean; 18 | } 19 | 20 | export const Suite = ({ suite: { id, name, devices }, isActive }: Props) => { 21 | const [, drop] = useDrop(() => ({ accept: DND_TYPE })); 22 | const dispatch = useDispatch(); 23 | 24 | const moveDevice = (device: Device, atIndex: number) => { 25 | const newDevices = devices.filter((d) => d !== device.id); 26 | newDevices.splice(atIndex, 0, device.id); 27 | dispatch(setSuiteDevices({ suite: id, devices: newDevices })); 28 | }; 29 | return ( 30 |
38 | {!isActive ? ( 39 |
40 | 46 |
47 | ) : null} 48 |
49 |
50 |

{name}

51 | {id !== 'default' ? ( 52 | 55 | ) : null} 56 |
57 |
58 | {devices.map((deviceId) => ( 59 | {}} 62 | hideSelectionControls={!isActive} 63 | disableSelectionControls={devices.length === 1} 64 | enableDnd={isActive} 65 | key={deviceId} 66 | moveDevice={moveDevice} 67 | /> 68 | ))} 69 |
70 |
71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/DeviceManager/PreviewSuites/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react'; 2 | import { useSelector } from 'react-redux'; 3 | import Button from 'renderer/components/Button'; 4 | import { 5 | selectActiveSuite, 6 | selectSuites, 7 | } from 'renderer/store/features/device-manager'; 8 | import { useState } from 'react'; 9 | import { FileUploader } from 'renderer/components/FileUploader'; 10 | import Modal from 'renderer/components/Modal'; 11 | import { Suite } from './Suite'; 12 | import { CreateSuiteButton } from './CreateSuiteButton'; 13 | import { ManageSuitesTool } from './ManageSuitesTool/ManageSuitesTool'; 14 | 15 | export const PreviewSuites = () => { 16 | const suites = useSelector(selectSuites); 17 | const activeSuite = useSelector(selectActiveSuite); 18 | 19 | return ( 20 |
21 |
22 |
23 | {suites.map((suite) => ( 24 | 29 | ))} 30 |
31 | 32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Divider/index.tsx: -------------------------------------------------------------------------------- 1 | export const Divider = () => ( 2 |
3 | ); 4 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/FileUploader/FileUploader.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, fireEvent } from '@testing-library/react'; 2 | import '@testing-library/jest-dom'; 3 | import { FileUploader, FileUploaderProps } from './FileUploader'; 4 | import { useFileUpload } from './hooks'; 5 | 6 | jest.mock('./hooks'); 7 | 8 | const mockHandleFileUpload = jest.fn(); 9 | const mockHandleUpload = jest.fn(); 10 | const mockResetUploadedFile = jest.fn(); 11 | 12 | describe('FileUploader', () => { 13 | beforeEach(() => { 14 | (useFileUpload as jest.Mock).mockReturnValue({ 15 | uploadedFile: null, 16 | handleUpload: mockHandleUpload, 17 | resetUploadedFile: mockResetUploadedFile, 18 | }); 19 | }); 20 | 21 | const renderComponent = (props?: FileUploaderProps) => 22 | render( 23 | 28 | ); 29 | 30 | it('renders the component', () => { 31 | const { getByTestId } = renderComponent(); 32 | 33 | const fileInput = getByTestId('fileUploader'); 34 | 35 | expect(fileInput).toBeInTheDocument(); 36 | }); 37 | 38 | it('calls handleUpload when file input changes', () => { 39 | const { getByTestId } = renderComponent(); 40 | const fileInput = getByTestId('fileUploader'); 41 | fireEvent.change(fileInput, { 42 | target: { files: [new File(['content'], 'file.txt')] }, 43 | }); 44 | expect(mockHandleUpload).toHaveBeenCalled(); 45 | }); 46 | 47 | it('calls handleFileUpload when uploadedFile is set', () => { 48 | const mockFile = new File(['content'], 'file.txt'); 49 | (useFileUpload as jest.Mock).mockReturnValue({ 50 | uploadedFile: mockFile, 51 | handleUpload: mockHandleUpload, 52 | resetUploadedFile: mockResetUploadedFile, 53 | }); 54 | renderComponent(); 55 | expect(mockHandleFileUpload).toHaveBeenCalledWith(mockFile); 56 | }); 57 | 58 | it('sets the accept attribute correctly', () => { 59 | const { getByTestId } = renderComponent({ 60 | acceptedFileTypes: 'application/json', 61 | handleFileUpload: mockHandleFileUpload, 62 | }); 63 | const fileInput = getByTestId('fileUploader'); 64 | expect(fileInput).toHaveAttribute('accept', 'application/json'); 65 | }); 66 | 67 | it('allows multiple file uploads when multiple prop is true', () => { 68 | const { getByTestId } = renderComponent({ 69 | multiple: true, 70 | handleFileUpload: mockHandleFileUpload, 71 | }); 72 | const fileInput = getByTestId('fileUploader'); 73 | expect(fileInput).toHaveAttribute('multiple'); 74 | }); 75 | 76 | it('does not allow multiple file uploads when multiple prop is false', () => { 77 | const { getByTestId } = renderComponent({ 78 | multiple: false, 79 | handleFileUpload: mockHandleFileUpload, 80 | }); 81 | const fileInput = getByTestId('fileUploader'); 82 | expect(fileInput).not.toHaveAttribute('multiple'); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/FileUploader/FileUploader.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { useFileUpload } from './hooks'; 3 | 4 | export type FileUploaderProps = { 5 | handleFileUpload: (file: File) => void; 6 | multiple?: boolean; 7 | acceptedFileTypes?: string; 8 | }; 9 | 10 | export const FileUploader = ({ 11 | handleFileUpload, 12 | multiple, 13 | acceptedFileTypes, 14 | }: FileUploaderProps) => { 15 | const { uploadedFile, handleUpload, resetUploadedFile } = useFileUpload(); 16 | const fileInputRef = useRef(null); 17 | 18 | useEffect(() => { 19 | if (uploadedFile) { 20 | handleFileUpload(uploadedFile); 21 | resetUploadedFile(); 22 | if (fileInputRef.current) { 23 | fileInputRef.current.value = ''; 24 | } 25 | } 26 | }, [handleFileUpload, resetUploadedFile, uploadedFile]); 27 | 28 | return ( 29 |
30 | 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/FileUploader/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useFileUpload } from './useFileUpload'; 2 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/FileUploader/hooks/useFileUpload.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react'; 2 | import { useFileUpload } from './useFileUpload'; 3 | 4 | describe('useFileUpload', () => { 5 | it('should initialize with null uploadedFile', () => { 6 | const { result } = renderHook(() => useFileUpload()); 7 | 8 | expect(result.current.uploadedFile).toBeNull(); 9 | }); 10 | 11 | it('should set uploadedFile when handleUpload is called with a file', () => { 12 | const { result } = renderHook(() => useFileUpload()); 13 | const mockFile = new File(['dummy content'], 'example.png', { 14 | type: 'image/png', 15 | }); 16 | 17 | act(() => { 18 | result.current.handleUpload({ 19 | target: { 20 | files: [mockFile], 21 | }, 22 | } as unknown as React.ChangeEvent); 23 | }); 24 | 25 | expect(result.current.uploadedFile).toEqual(mockFile); 26 | }); 27 | 28 | it('should reset uploadedFile when resetUploadedFile is called', () => { 29 | const { result } = renderHook(() => useFileUpload()); 30 | const mockFile = new File(['dummy content'], 'example.png', { 31 | type: 'image/png', 32 | }); 33 | 34 | act(() => { 35 | result.current.handleUpload({ 36 | target: { 37 | files: [mockFile], 38 | }, 39 | } as unknown as React.ChangeEvent); 40 | }); 41 | 42 | expect(result.current.uploadedFile).toEqual(mockFile); 43 | 44 | act(() => { 45 | result.current.resetUploadedFile(); 46 | }); 47 | 48 | expect(result.current.uploadedFile).toBeNull(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/FileUploader/hooks/useFileUpload.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useFileUpload = () => { 4 | const [uploadedFile, setUploadedFile] = useState(null); 5 | 6 | const handleUpload = (event: React.ChangeEvent) => { 7 | if (event?.target?.files || event?.target?.files?.length) { 8 | setUploadedFile(event.target.files[0]); 9 | } 10 | }; 11 | 12 | const resetUploadedFile = () => { 13 | setUploadedFile(null); 14 | }; 15 | 16 | return { 17 | uploadedFile, 18 | handleUpload, 19 | resetUploadedFile, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/FileUploader/index.ts: -------------------------------------------------------------------------------- 1 | export { FileUploader } from './FileUploader'; 2 | export { useFileUpload } from './hooks'; 3 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/InfoPopups/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { isReleaseNotesUnseen, ReleaseNotes } from '../ReleaseNotes'; 3 | import { Sponsorship } from '../Sponsorship'; 4 | 5 | export const InfoPopups = () => { 6 | const [showReleaseNotes, setShowReleaseNotes] = useState(false); 7 | const [showSponsorship, setShowSponsorship] = useState(false); 8 | 9 | useEffect(() => { 10 | (async () => { 11 | if (await isReleaseNotesUnseen()) { 12 | setShowReleaseNotes(true); 13 | return; 14 | } 15 | setShowSponsorship(true); 16 | })(); 17 | }, []); 18 | 19 | if (showReleaseNotes) { 20 | return ; 21 | } 22 | 23 | if (showSponsorship) { 24 | return ; 25 | } 26 | 27 | return null; 28 | }; 29 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | interface Props { 5 | label: string; 6 | } 7 | 8 | const Input = ({ 9 | label, 10 | ...props 11 | }: Props & 12 | React.DetailedHTMLProps< 13 | React.InputHTMLAttributes, 14 | HTMLInputElement 15 | >) => { 16 | const id = useId(); 17 | const isCheckbox = props.type === 'checkbox'; 18 | return ( 19 |
25 | 26 | 33 |
34 | ); 35 | }; 36 | 37 | export default Input; 38 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/KeyboardShortcutsManager/constants.ts: -------------------------------------------------------------------------------- 1 | export const SHORTCUT_CHANNEL = { 2 | BACK: 'BACK', 3 | BOOKMARK: 'BOOKMARK', 4 | DELETE_ALL: 'DELETE_ALL', 5 | DELETE_CACHE: 'DELETE_CACHE', 6 | DELETE_COOKIES: 'DELETE_COOKIES', 7 | DELETE_STORAGE: 'DELETE_STORAGE', 8 | EDIT_URL: 'EDIT_URL', 9 | FORWARD: 'FORWARD', 10 | INSPECT_ELEMENTS: 'INSPECT_ELEMENTS', 11 | PREVIEW_LAYOUT: 'PREVIEW_LAYOUT', 12 | RELOAD: 'RELOAD', 13 | ROTATE_ALL: 'ROTATE_ALL', 14 | SCREENSHOT_ALL: 'SCREENSHOT_ALL', 15 | THEME: 'THEME', 16 | TOGGLE_RULERS: 'TOGGLE_RULERS', 17 | ZOOM_IN: 'ZOOM_IN', 18 | ZOOM_OUT: 'ZOOM_OUT', 19 | } as const; 20 | 21 | export type ShortcutChannel = 22 | typeof SHORTCUT_CHANNEL[keyof typeof SHORTCUT_CHANNEL]; 23 | 24 | export const SHORTCUT_KEYS: { [key in ShortcutChannel]: string[] } = { 25 | [SHORTCUT_CHANNEL.BACK]: ['alt+left'], 26 | [SHORTCUT_CHANNEL.BOOKMARK]: ['mod+d'], 27 | [SHORTCUT_CHANNEL.DELETE_ALL]: ['mod+alt+del', 'mod+alt+backspace'], 28 | [SHORTCUT_CHANNEL.DELETE_CACHE]: ['mod+alt+z'], 29 | [SHORTCUT_CHANNEL.DELETE_COOKIES]: ['mod+alt+a'], 30 | [SHORTCUT_CHANNEL.DELETE_STORAGE]: ['mod+alt+q'], 31 | [SHORTCUT_CHANNEL.EDIT_URL]: ['mod+l'], 32 | [SHORTCUT_CHANNEL.FORWARD]: ['alt+right'], 33 | [SHORTCUT_CHANNEL.INSPECT_ELEMENTS]: ['mod+i'], 34 | [SHORTCUT_CHANNEL.PREVIEW_LAYOUT]: ['mod+shift+l'], 35 | [SHORTCUT_CHANNEL.RELOAD]: ['mod+r'], 36 | [SHORTCUT_CHANNEL.ROTATE_ALL]: ['mod+alt+r'], 37 | [SHORTCUT_CHANNEL.SCREENSHOT_ALL]: ['mod+s'], 38 | [SHORTCUT_CHANNEL.THEME]: ['mod+t'], 39 | [SHORTCUT_CHANNEL.TOGGLE_RULERS]: ['alt+r'], 40 | [SHORTCUT_CHANNEL.ZOOM_IN]: ['mod+=', 'mod++', 'mod+shift+='], 41 | [SHORTCUT_CHANNEL.ZOOM_OUT]: ['mod+-'], 42 | }; 43 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/KeyboardShortcutsManager/index.tsx: -------------------------------------------------------------------------------- 1 | import { SHORTCUT_KEYS, ShortcutChannel } from './constants'; 2 | import useMousetrapEmitter from './useMousetrapEmitter'; 3 | 4 | const KeyboardShortcutsManager = () => { 5 | // eslint-disable-next-line no-restricted-syntax 6 | for (const [channel, keys] of Object.entries(SHORTCUT_KEYS)) { 7 | // eslint-disable-next-line react-hooks/rules-of-hooks 8 | useMousetrapEmitter(keys, channel as ShortcutChannel); 9 | } 10 | 11 | return null; 12 | }; 13 | 14 | export default KeyboardShortcutsManager; 15 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/KeyboardShortcutsManager/useKeyboardShortcut.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { ShortcutChannel } from './constants'; 3 | import { keyboardShortcutsPubsub } from './useMousetrapEmitter'; 4 | 5 | const useKeyboardShortcut = ( 6 | eventChannel: ShortcutChannel, 7 | callback: () => void 8 | ) => { 9 | useEffect(() => { 10 | keyboardShortcutsPubsub.subscribe(eventChannel, callback); 11 | return () => { 12 | keyboardShortcutsPubsub.unsubscribe(eventChannel, callback); 13 | }; 14 | }, [eventChannel, callback]); 15 | return null; 16 | }; 17 | 18 | export default useKeyboardShortcut; 19 | export * from './constants'; 20 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/KeyboardShortcutsManager/useMousetrapEmitter.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import Mousetrap from 'mousetrap'; 3 | import PubSub from 'renderer/lib/pubsub'; 4 | import { ShortcutChannel } from './constants'; 5 | 6 | export const keyboardShortcutsPubsub = new PubSub(); 7 | 8 | const useMousetrapEmitter = ( 9 | accelerator: string | string[], 10 | eventChannel: ShortcutChannel, 11 | action?: string | undefined 12 | ) => { 13 | useEffect(() => { 14 | const callback = async ( 15 | _e: Mousetrap.ExtendedKeyboardEvent, 16 | _combo: string 17 | ) => { 18 | try { 19 | await keyboardShortcutsPubsub.publish(eventChannel); 20 | } catch (err) { 21 | // eslint-disable-next-line no-console 22 | console.error('useMousetrapEmitter: callback: error: ', err); 23 | } 24 | }; 25 | Mousetrap.bind(accelerator, callback, action); 26 | 27 | return () => { 28 | Mousetrap.unbind(accelerator, action); 29 | }; 30 | }, [accelerator, eventChannel, action]); 31 | 32 | return null; 33 | }; 34 | 35 | export default useMousetrapEmitter; 36 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from '@headlessui/react'; 2 | import { Fragment } from 'react'; 3 | 4 | interface Props { 5 | isOpen: boolean; 6 | onClose: () => void; 7 | title?: JSX.Element | string; 8 | description?: JSX.Element | string; 9 | children?: JSX.Element | string; 10 | } 11 | 12 | const Modal = ({ isOpen, onClose, title, description, children }: Props) => { 13 | return ( 14 | 15 | 16 | 25 | 56 | 57 | ); 58 | }; 59 | 60 | export default Modal; 61 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/ModalLoader/index.tsx: -------------------------------------------------------------------------------- 1 | import Modal from '../Modal'; 2 | 3 | interface Props { 4 | isOpen: boolean; 5 | onClose: () => void; 6 | title: JSX.Element | string; 7 | } 8 | 9 | const ModalLoader = ({ isOpen, onClose, title }: Props) => { 10 | return ( 11 | 12 |
13 | Capturing screen... 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default ModalLoader; 20 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Notifications/Notification.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | IPC_MAIN_CHANNELS, 3 | Notification as NotificationType, 4 | } from 'common/constants'; 5 | import Button from '../Button'; 6 | 7 | const Notification = ({ notification }: { notification: NotificationType }) => { 8 | const handleLinkClick = (url: string) => { 9 | window.electron.ipcRenderer.sendMessage(IPC_MAIN_CHANNELS.OPEN_EXTERNAL, { 10 | url, 11 | }); 12 | }; 13 | 14 | return ( 15 |
16 |

{notification.text}

17 | {notification.link && notification.linkText && ( 18 | 28 | )} 29 |
30 | ); 31 | }; 32 | 33 | export default Notification; 34 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Notifications/NotificationEmptyStatus.tsx: -------------------------------------------------------------------------------- 1 | const NotificationEmptyStatus = () => { 2 | return ( 3 |
4 |

You are all caught up! No new notifications at the moment.

5 |
6 | ); 7 | }; 8 | 9 | export default NotificationEmptyStatus; 10 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Notifications/Notifications.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { selectNotifications } from 'renderer/store/features/renderer'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import { Notification as NotificationType } from 'common/constants'; 5 | import Notification from './Notification'; 6 | import NotificationEmptyStatus from './NotificationEmptyStatus'; 7 | 8 | const Notifications = () => { 9 | const notificationsState = useSelector(selectNotifications); 10 | 11 | return ( 12 |
13 | Notifications 14 |
15 | {(!notificationsState || 16 | (notificationsState && notificationsState?.length === 0)) && ( 17 | 18 | )} 19 | {notificationsState && 20 | notificationsState?.length > 0 && 21 | notificationsState?.map((notification: NotificationType) => ( 22 | 23 | ))} 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default Notifications; 30 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Notifications/NotificationsBubble.tsx: -------------------------------------------------------------------------------- 1 | const NotificationsBubble = () => { 2 | return ( 3 | 4 | 5 | 6 | 7 | ); 8 | }; 9 | 10 | export default NotificationsBubble; 11 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Previewer/Device/common.ts: -------------------------------------------------------------------------------- 1 | interface ContextMenuMetadata { 2 | id: string; 3 | label: string; 4 | } 5 | 6 | export const CONTEXT_MENUS: { [key: string]: ContextMenuMetadata } = { 7 | INSPECT_ELEMENT: { id: 'INSPECT_ELEMENT', label: 'Inspect Element' }, 8 | OPEN_CONSOLE: { id: 'OPEN_CONSOLE', label: 'Open Console' }, 9 | }; 10 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Previewer/Device/utils.ts: -------------------------------------------------------------------------------- 1 | import { HistoryItem } from 'renderer/components/ToolBar/AddressBar/SuggestionList'; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const appendHistory = (url: string, title: string) => { 5 | if (url === `${title}/`) { 6 | return; 7 | } 8 | const history: HistoryItem[] = window.electron.store.get('history'); 9 | window.electron.store.set( 10 | 'history', 11 | [ 12 | { url, title, lastVisited: new Date().getTime() }, 13 | ...history.filter(({ url: _url }) => url !== _url), 14 | ].slice(0, 100) 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Previewer/Guides/guide.css: -------------------------------------------------------------------------------- 1 | .box { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 30px; 6 | height: 30px; 7 | box-sizing: border-box; 8 | background: transparent; 9 | z-index: 21; 10 | } 11 | 12 | .box:before, 13 | .box:after { 14 | position: absolute; 15 | content: ''; 16 | /*background: rgb(55, 65, 81);*/ 17 | } 18 | 19 | .box:before { 20 | width: 1px; 21 | height: 100%; 22 | left: 100%; 23 | } 24 | 25 | .box:after { 26 | height: 1px; 27 | width: 100%; 28 | top: 100%; 29 | } 30 | 31 | .scena-guides-horizontal .scena-guides-guide-pos { 32 | left: 48% !important; 33 | } 34 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Previewer/IndividualLayoutToolBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { Tab, Tabs, TabList } from 'react-tabs'; 4 | import { Icon } from '@iconify/react'; 5 | import cx from 'classnames'; 6 | import { setLayout } from 'renderer/store/features/renderer'; 7 | import { PREVIEW_LAYOUTS } from 'common/constants'; 8 | import { Device as IDevice } from 'common/deviceList'; 9 | import './styles.css'; 10 | 11 | interface Props { 12 | individualDevice: IDevice; 13 | setIndividualDevice: (device: IDevice) => void; 14 | devices: IDevice[]; 15 | } 16 | 17 | const IndividualLayoutToolbar = ({ 18 | individualDevice, 19 | setIndividualDevice, 20 | devices, 21 | }: Props) => { 22 | const dispatch = useDispatch(); 23 | const [activeTab, setActiveTab] = useState(0); 24 | 25 | const onTabClick = (newTabIndex: number) => { 26 | setActiveTab(newTabIndex); 27 | setIndividualDevice(devices[newTabIndex]); 28 | }; 29 | 30 | const isActive = (idx: number) => activeTab === idx; 31 | const handleCloseBtn = () => dispatch(setLayout(PREVIEW_LAYOUTS.COLUMN)); 32 | 33 | useEffect(() => { 34 | const activeTabIndex = devices.findIndex( 35 | (device) => device.id === individualDevice.id 36 | ); 37 | setActiveTab(activeTabIndex); 38 | }, [individualDevice, devices]); 39 | 40 | return ( 41 |
42 | 47 | 52 | {devices.map((device, idx) => ( 53 | 64 | {device.name} 65 | 66 | ))} 67 | 68 | 69 |
70 | 76 |
77 |
78 | ); 79 | }; 80 | 81 | export default IndividualLayoutToolbar; 82 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Previewer/IndividualLayoutToolBar/styles.css: -------------------------------------------------------------------------------- 1 | .custom-scrollbar::-webkit-scrollbar, 2 | .custom-scrollbar::-webkit-scrollbar:hover, 3 | .dark .custom-scrollbar::-webkit-scrollbar, 4 | .dark .custom-scrollbar::-webkit-scrollbar:hover { 5 | width: 0.25rem; 6 | height: 0.25rem; 7 | border-radius: 6px; 8 | } 9 | 10 | .custom-scrollbar::-webkit-scrollbar { 11 | background-color: rgba(255, 255, 255, 0.9); 12 | } 13 | 14 | .custom-scrollbar::-webkit-scrollbar-thumb, 15 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 16 | background-color: rgba(148, 163, 184, 0.7); 17 | } 18 | 19 | .dark .custom-scrollbar::-webkit-scrollbar { 20 | background-color: rgba(148, 163, 184, 0.6); 21 | } 22 | 23 | .dark .custom-scrollbar::-webkit-scrollbar-thumb, 24 | .dark .custom-scrollbar::-webkit-scrollbar-thumb:hover { 25 | background-color: rgba(241, 245, 249, 0.8); 26 | } 27 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Select/index.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react'; 2 | 3 | interface Props { 4 | label: string; 5 | } 6 | 7 | const Select = ({ 8 | label, 9 | ...props 10 | }: Props & 11 | React.DetailedHTMLProps< 12 | React.SelectHTMLAttributes, 13 | HTMLSelectElement 14 | >) => { 15 | const id = useId(); 16 | return ( 17 |
18 | 19 | 27 |
28 | ); 29 | }; 30 | 31 | export default Select; 32 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react'; 2 | 3 | interface Props { 4 | spinnerHeight?: number; 5 | } 6 | 7 | const Spinner = ({ spinnerHeight = undefined }: Props) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default Spinner; 16 | -------------------------------------------------------------------------------- /desktop-app/src/renderer/components/Toggle/index.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | isOn: boolean; 3 | onChange?: React.ChangeEventHandler; 4 | } 5 | 6 | const Toggle = ({ isOn, onChange }: Props) => { 7 | return ( 8 | // eslint-disable-next-line jsx-a11y/label-has-associated-control 9 |