diff --git a/.eslintignore b/.eslintignore index 2b6e224f..8a73bd95 100644 --- a/.eslintignore +++ b/.eslintignore @@ -46,6 +46,7 @@ dist dll main.js main.js.map +app/vendors .idea @@ -67,3 +68,24 @@ app/dll .prettierrc .stylelintrc .eslintrc.json + + +package-lock.json +sentry.properties + +app/certs/*.pem +certs/* +todo.txt +*yarn-error.log +eslint-common-rules.txt + +build/entitlements.mas.plist +*embedded.provisionprofile +extras/* +mtp-mock-files/* +resources +tmp/* +*.prod.js +*.js.map +*.js.LICENSE.txt +*.prod.js.LICENSE.txt diff --git a/.eslintrc.js b/.eslintrc.js index 8ab2a483..fe47baab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,33 +1,42 @@ module.exports = { - parser: 'babel-eslint', + parser: '@babel/eslint-parser', parserOptions: { sourceType: 'module', - allowImportExportEverywhere: true + allowImportExportEverywhere: true, }, env: { browser: true, - node: true + node: true, }, plugins: ['import', 'promise', 'compat', 'react'], - extends: ['airbnb', 'plugin:prettier/recommended', 'prettier/react'], + extends: ['airbnb', 'plugin:prettier/recommended'], settings: { react: { - version: '16.7' + version: '16.7', }, 'import/resolver': { webpack: { - config: 'webpack/config.eslint.js' - } - } + config: 'webpack/config.eslint.js', + }, + }, }, rules: { + 'import/no-import-module-exports': [ + 'error', + { + exceptions: ['**/*/webpack/**/*'], + }, + ], 'arrow-body-style': 'off', 'arrow-parens': 'off', 'class-methods-use-this': 'off', - 'compat/compat': 'error', + 'compat/compat': 'off', 'consistent-return': 'off', 'comma-dangle': 'off', + 'default-param-last': 'off', + 'func-names': 'off', 'generator-star-spacing': 'off', + 'no-nested-ternary': 'off', 'import/no-cycle': 'off', 'import/no-dynamic-require': 'off', 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], @@ -39,23 +48,39 @@ module.exports = { 'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/no-static-element-interactions': 'off', 'jsx-a11y/click-events-have-key-events': 'off', + 'react/jsx-no-useless-fragment': 'off', + 'react/jsx-curly-brace-presence': 'off', 'no-console': [ 'error', { - allow: ['info', 'error', 'warn'] - } + allow: ['info', 'error', 'warn'], + }, ], 'no-use-before-define': 'off', + 'no-restricted-syntax': 'off', 'no-multi-assign': 'off', + 'no-promise-executor-return': 'off', 'no-shadow': 'off', 'no-underscore-dangle': 'off', + 'no-throw-literal': 'off', 'no-unused-vars': [ 'error', { args: 'after-used', - argsIgnorePattern: '^(theme|props|state|ownProps|dispatch|getState)|_', - varsIgnorePattern: '^(variables|mixins|args|log)' - } + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: '*', next: 'return' }, + { blankLine: 'always', prev: ['block-like'], next: '*' }, + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { + blankLine: 'any', + prev: ['const', 'let', 'var'], + next: ['const', 'let', 'var'], + }, ], 'prettier/prettier': ['error', { singleQuote: true }], 'promise/param-names': 'error', @@ -70,17 +95,16 @@ module.exports = { 'static-methods', 'lifecycle', 'everything-else', - 'render' - ] - } + 'render', + ], + }, ], 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx'] }], 'react/jsx-fragments': 'off', 'react/jsx-no-bind': 'off', 'react/jsx-props-no-spreading': 'off', 'react/prefer-stateless-function': 'off', - strict: 'off', 'react/prop-types': 'off', - 'react/no-deprecate': 'off' - } + 'react/no-deprecate': 'off', + }, }; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..cfd591ce --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +custom: +- https://paypal.me/ganeshrvel +- https://buymeacoffee.com/ganeshrvel diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fc929e3b..190389d5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,60 +7,18 @@ labels: 'bug' **Prerequisites** -If the following boxes are not ALL checked, your issue is likely to be closed. - -- [ ] Using yarn -- [ ] Using node 10.x -- [ ] Using an up-to-date [`master` branch](https://github.com/ganeshrvel/openmtp/tree/master) -- [ ] Using latest version of devtools. See [wiki for howto update](https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/DevTools) -- [ ] For issue in production release, devtools output of `DEBUG_PROD=true yarn build && yarn start` -- [ ] Tried solutions mentioned in [#400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400) +- [ ] Have you reviewed the steps in the following link to see if they address your issue? + - https://github.com/ganeshrvel/openmtp/issues/276 **Describe the bug** A clear and concise description of what the bug is. -**Expected behaviour** -If you're describing a bug, tell us what should happen. -If you're suggesting a change/improvement, tell us how it should work. - -**Current Behavior** - If describing a bug, tell us what happens instead of the expected behaviour. - If suggesting a change/improvement, explain the difference between current behaviour. - - **Possible Solution** - - Not obligatory, but suggest a fix/reason for the bug or ideas how to implement the addition or change. - -**Steps to Reproduce (for bugs)** -Provide a link to a live example or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant - -1. - -2. - -3. - -4. - -**Context** -How has this issue affected you? What are you trying to accomplish? -Did you make any changes to the codes after cloning it? -Providing context helps us come up with a solution that is most useful in the real world. - -**Your Environment** -Include as many relevant details about the environment you experienced the bug in - -- Node version : -- Version or Branch used : -- Operating System and version [e.g. macOS 10.14 mojave]: -- App Version [e.g. OpenMTP-v1.0]: -- Link to your project : **Attachments** Include if relevant, 1. Open your Terminal and run the following code: ```shell -zip -r -X ~/Desktop/OpenMTP-log.zip ~/.io.ganeshrvel/openmtp/logs/ +zip -r -X ~/Desktop/OpenMTP-log.zip "$HOME/Library/Application Support/io.ganeshrvel.openmtp/logs/" ``` 2. Attach the file *OpenMTP-log.zip* found in your Desktop folder here. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8e9ed081..31bf379b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,18 +2,18 @@ name: CI on: push: - branches: [ master, development ] + branches: [ master ] pull_request: - branches: [ master, development ] + branches: [ master ] jobs: build: - runs-on: macOS-10.15 + runs-on: macOS-11.00 steps: - name: Setup build environment uses: actions/setup-node@v1 with: - node-version: '10.x' + node-version: '16.x' - run: npm install -g cross-env create-dmg - name: Cloning Git repository uses: actions/checkout@v1 @@ -24,7 +24,7 @@ jobs: - name: Attempt to generate unsigned macOS app run: | export CSC_IDENTITY_AUTO_DISCOVERY=false - yarn package-mac 2>&1 || true + yarn package-mac-without-notarize 2>&1 || true - name: Check if macOS app was generated run: test -e dist/mac/OpenMTP.app - name: Attempt to create disk image for generated macOS app diff --git a/.gitignore b/.gitignore index 2226b85e..d3551c35 100644 --- a/.gitignore +++ b/.gitignore @@ -52,10 +52,10 @@ main.js.map npm-debug.log.* package-lock.json .env* +sentry.properties app/certs/*.pem certs/* -*electron-builder.yml todo.txt *yarn-error.log eslint-common-rules.txt @@ -63,3 +63,10 @@ eslint-common-rules.txt build/entitlements.mas.plist *embedded.provisionprofile extras/* +mtp-mock-files/* +resources +tmp/* +*.prod.js +*.js.map +*.js.LICENSE.txt +*.prod.js.LICENSE.txt diff --git a/.stylelintrc b/.stylelintrc index 1f2f7f8a..ad16a721 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,7 +1,22 @@ { "extends": ["stylelint-config-standard", "stylelint-config-prettier"], "rules": { + "color-function-notation": null, + "value-no-vendor-prefix": null, + "property-no-vendor-prefix": null, + "font-family-name-quotes": null, + "rule-empty-line-before": null, + "at-rule-no-vendor-prefix": null, + "shorthand-property-no-redundant-values": null, + "selector-class-pattern": null, + "annotation-no-unknown": null, + "value-keyword-case": null, + "function-no-unknown": null, + "selector-no-vendor-prefix": null, + "media-feature-name-no-vendor-prefix": null, + "alpha-value-notation": null, "at-rule-no-unknown": null, + "import-notation": null, "no-descending-specificity": null } } diff --git a/README.md b/README.md index 04ce4505..0b248254 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # OpenMTP | Android File Transfer for macOS -- Author: [Ganesh Rathinavel](https://www.linkedin.com/in/ganeshrvel "Ganesh Rathinavel") -- License: [MIT](https://github.com/ganeshrvel/openmtp/blob/master/LICENSE "MIT") -- System Requirements: macOS 10.10 or higher -- Website URL: [https://ganeshrvel.github.io/openmtp](https://ganeshrvel.github.io/openmtp/ "https://ganeshrvel.github.io/openmtp") -- Repo URL: [https://github.com/ganeshrvel/openmtp](https://github.com/ganeshrvel/openmtp/ "https://github.com/ganeshrvel/openmtp") +- Author: [Ganesh Rathinavel](https://www.linkedin.com/in/ganeshrvel 'Ganesh Rathinavel') +- License: [MIT](https://github.com/ganeshrvel/openmtp/blob/master/LICENSE 'MIT') +- System Requirements: macOS 11.0 (Big Sur) or higher +- Website URL: [https://openmtp.ganeshrvel.com](https://openmtp.ganeshrvel.com/ 'https://openmtp.ganeshrvel.com') +- Repo URL: [https://github.com/ganeshrvel/openmtp](https://github.com/ganeshrvel/openmtp/ 'https://github.com/ganeshrvel/openmtp') - Contacts: ganeshrvel@outlook.com - ## Introduction ### Advanced Android File Transfer Application for macOS. @@ -17,65 +16,93 @@ Transferring files between macOS and Android or any other MTP devices has always Countless searches to find an app to solve these problems and failing to find one made me restless. So, I took the leap and decided to create an app for us that could help us have a smooth and hassle-free file transfer process from macOS to Android/MTP devices. Created with the objective of giving back to the community, we can all use this app for free in this lifetime. ### Features + - Safe, Transparent and Open-Source - Plug and Play via USB. No hassles, easy and instant connection. - Select between Internal Memory and SD Card - Transfer multiple files which are larger than 4GB +- Dark mode - Drag-and-drop support - Split pane views for both Local Computer and Android device - Choose between Grid and List view. - Use Keyboard Shortcuts to navigate through your files. - No collection of personally identifiable information. +### Kalam Kernel + +OpenMTP 3.0 features a new MTP kernel and it was written from the scratch. It promises a file copy speed of 30 to 40 MB/s on low and mid range devices and 100 to 120 MB/s on higher end devices. The all new and powerful MTP kernel is named after [Dr. A. P. J. Abdul Kalam](https://en.wikipedia.org/wiki/A._P._J._Abdul_Kalam 'Dr. A. P. J. Abdul Kalam') + +Do checkout the Go package which I've written to build Kalam Kernel: [github.com/ganeshrvel/go-mtpx](https://github.com/ganeshrvel/go-mtpx 'https://github.com/ganeshrvel/go-mtpx'). Feel free to raise PRs. + +### System Requirements and Support + +- To support macOS version below Big Sur the Kalam kernel needs to be compiled on an older macOS machine everytime there is an update, which is practically very difficult +- Only the latest 3 versions of macOS will receive the `Kalam` Kernel updates, which includes new device supports, fixes, stability improvements. macOS Big Sur (11.0) or above will receive the above said updates +- We have now officially retired the support for `Kalam` Kernel on macOS 10.13 (OS X El High Sierra) and lower. Only the "Legacy" MTP mode will continue working on these outdated machines. +- We will continue releasing the updates for both `Intel` and `ARM64` machines ### Installation -* Download the [Latest Version](https://ganeshrvel.github.io/openmtp/?downloadApp=github&release=stable&platform=mac 'Latest Version') -* Using Homebrew Cask - * ```brew cask install openmtp``` +- Download the [Mac Apple Silicon](https://openmtp.ganeshrvel.com/?downloadApp=github&release=stable&platform=mac&arch=arm64 'Mac Apple Silicon') version +- Download the [Mac Intel Silicon](https://openmtp.ganeshrvel.com/?downloadApp=github&release=stable&platform=mac&arch=x64 'Mac Intel Silicon') version +- Using Homebrew Cask -* Find the latest *dmg* file from [GitHub Releases](https://github.com/ganeshrvel/openmtp/releases "GitHub Releases") +```shell + # newer versions: + brew install openmtp --cask + # older versions: + brew cask install openmtp +``` + +- Find the latest _dmg_ file from [GitHub Releases](https://github.com/ganeshrvel/openmtp/releases 'GitHub Releases') ### Screengrabs -![OpenMTP File Explorer](https://github.com/ganeshrvel/openmtp/raw/master/blobs/images/file-explorer-bluebg.png "OpenMTP File Explorer") +![OpenMTP File Explorer](https://github.com/ganeshrvel/openmtp/raw/master/blobs/images/file-explorer-bluebg.jpg 'OpenMTP File Explorer') -![OpenMTP File Transfer](https://github.com/ganeshrvel/openmtp/raw/master/blobs/images/file-transfer-bluebg.png "OpenMTP File Transfer") +![OpenMTP File Transfer](https://github.com/ganeshrvel/openmtp/raw/master/blobs/images/file-transfer-bluebg.jpg 'OpenMTP File Transfer') ### Keyboard Shortcuts -| Command | Keyboard Shortcut | -| ------------------------------------------------- | ------------------------------------------------------------ | -| Delete | backspace | -| New Folder | command (⌘)+n | -| Copy | command (⌘)+c | -| Copy to Queue | command (⌘)+shift+c | -| Paste | command (⌘)+v | -| Refresh | command (⌘) +r | -| Folder Up | command (⌘)+b | -| Select All | command (⌘)+a | -| Rename | command (⌘)+d | -| Switch Tab | command(⌘)+1 | -| Open | enter | -| Navigate Left | left | -| Navigate Right | right | -| Navigate Up | up | -| Navigate Down | down | -| Select Multiple Items Forward **(in Grid View)** | shift+left | -| Select Multiple Items Backward **(in Grid View)** | shift+right | -| Select Multiple Items Forward **(in List View)** | shift+up | -| Select Multiple Items Backward **(in List View)** | shift+down | + +| Command | Keyboard Shortcut | +| ------------------------------------------------- | ---------------------------------------------------------------------------- | +| Delete | backspace | +| New Folder | command (⌘)+n | +| Copy | command (⌘)+c | +| Copy to Queue | command (⌘)+shift+c | +| Paste | command (⌘)+v | +| Refresh | command (⌘) +r | +| Folder Up | command (⌘)+b | +| Select All | command (⌘)+a | +| Rename | command (⌘)+d | +| Switch Tab | command(⌘)+1 | +| Open | enter | +| Navigate Left | left | +| Navigate Right | right | +| Navigate Up | up | +| Navigate Down | down | +| Select Multiple Items Forward **(in Grid View)** | shift+left | +| Select Multiple Items Backward **(in Grid View)** | shift+right | +| Select Multiple Items Forward **(in List View)** | shift+up | +| Select Multiple Items Backward **(in List View)** | shift+down | | Select Multiple Items **(with mouse)** | command (⌘)+click or shift+click | ## Building from Source -Requirements: [Node.js v10](https://nodejs.org/en/download/ "Install Node.js v10"), [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git "Install Git") and [Yarn package manager](https://yarnpkg.com/lang/en/docs/install/ "Install Yarn package manager") - +Requirements: [Node.js v16](https://nodejs.org/en/download/ 'Install Node.js v16'), [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git 'Install Git') and [Yarn package manager](https://yarnpkg.com/lang/en/docs/install/ 'Install Yarn package manager') ### Clone + ```shell -$ git clone --depth 1 --single-branch --branch master https://github.com/ganeshrvel/openmtp.git +$ git clone https://github.com/ganeshrvel/openmtp.git $ cd openmtp + +# install yarn +npm install -g yarn + +# install sentry cli +npm -g i @sentry/cli ``` ```shell @@ -83,7 +110,8 @@ $ yarn ``` ### Run -A fresh clone might throw *undefined state* error. Run the following commands once to fix the issue. + +A fresh clone might throw _undefined state_ error. Run the following commands once to fix the issue. ```shell # For Mac and Linux @@ -102,27 +130,117 @@ $ yarn start ``` -### Package +### Debugging a Packaged app + +```shell +# On terminal run +$ "/path/to/OpenMTP.app/Contents/MacOS/OpenMTP" --remote-debugging-port=6363 +``` -Setup the *code signing* to build, package and publish the app. +- Open a Chromium browser +- Input "about://inspect" into the URL bar +- Add a new connection `localhost:6363` +- Inspect OpenMTP @ port `6363` + +### Publishing using CI/CD: + +- CodeMagic.io + - Create a new App (Choose others -> Enter Electron) + - Environment variables: + - `APPLEID`: `` + - `APPLE_APP_SPECIFIC_PASSWORD`: `` + - Log into your [Apple Account](https://appleid.apple.com/account/manage 'Apple Account') + - Goto **Sign-In and Security > App-Specific Passwords** + - Click on **Generate Password...**, enter a password label and click _Create_ + - Copy the displayed _app-specific-password_ + - `APPLE_TEAM_ID`: `` + - To get Team ID, go to your [Apple Developer Account](https://developer.apple.com/account#MembershipDetailsCard) + - Then click on "Membership details", and there you will find your Team ID. + - `SENTRY_URL`: `https://sentry.io/` + - `SENTRY_ORG`: `` + - `SENTRY_PROJECT`: `` + - `SENTRY_TOKEN_ID`: `` + - Find it from here: [Auth Tokens](https://sentry.io/settings/account/api/auth-tokens) + - Scopes: `event:admin, event:read, member:read, org:read, project:read, project:releases, team:read` + - `GITHUB_TOKEN`: `Personal access token` + - There are two options available: + 1. **Fine-grained personal access tokens** + - https://github.com/settings/tokens?type=beta + - Permissions: `Contents - (Read and Write access to code)` + 2. **Personal access tokens (classic)** + - https://github.com/settings/tokens + - Scopes: `admin:gpg_key, admin:public_key, repo, user, workflow` + - `CSC_LINK`: + - Keychain -> `Default Keychains` menu in the left -> Login -> My Certificates + - Search for `Developer ID Application` in the top search bar + - If there are no results for the `Developer ID Application`, for the organization, create one from here: [Apple Developer Certificates](https://developer.apple.com/account/resources/certificates/add) + - Follow these steps to get the Apple Developer certificated installed in the local machine [Obtaining-an-Apple-Developer-ID-Certificate-for-macOS-Provisioning](https://forums.ivanti.com/s/article/Obtaining-an-Apple-Developer-ID-Certificate-for-macOS-Provisioning?language=en_US&ui-force-components-controllers-recordGlobalValueProvider.RecordGvp.getRecord=1) + - Search for `Developer ID Application` in the top search bar + - Expand `Developer ID Application: (XXXYYYZZZ)` + - See if the private key's name matches this: `Mac Developer ID Application: ` + - Else rename the private key as (right click -> get info) `Mac Developer ID Application: ` + - Close the window + - Right Click on the private key -> `Mac Developer ID Application: ` + - Export `Mac Developer ID Application: ` + - File name: `CERTIFICATE_PRIVATE_KEY.p12` + - Enter Password. This is the `CSC_KEY_PASSWORD`, note this down + - Run (this step doesnt work if you are using fig or ohmyzsh, use raw terminal): + - `base64 -i CERTIFICATE_PRIVATE_KEY.p12 -o CERTIFICATE_PRIVATE_KEY.txt` + - Copy the whole content of the file `CERTIFICATE_PRIVATE_KEY.txt` + - Paste the content as the value for the field `CSC_LINK` + - `CSC_KEY_PASSWORD` is the password from the above step + - `CODEMAGIC_AUTH_TOKEN_ID`: `` + - Find it from here: [Sidebar -> Teams -> Personal Account -> Integrations -> Codemagic API](https://codemagic.io/teams) + - `CODEMAGIC_INTEL_X64_WORKFLOW_ID_PROD`: `` + - Find the relevant workflow id from `codemagic.yaml`, (mostly `macos-intel-x64-build-prod`) + - `CODEMAGIC_INTEL_X64_WORKFLOW_ID_DEV`: `` + - Find the relevant workflow id from `codemagic.yaml`, (mostly `macos-intel-x64-build-dev`) + - `PUBLISH_PROD_REPOSITORY`: `` + - `PUBLISH_DEV_REPOSITORY`: `` + - `CODEMAGIC_PUBLISH_PROJECT_ID`: `` + - `PUBLISH_EMAIL`: `Email address to receive the updates on publish` + - References: + - [https://www.electron.build/code-signing.html](https://www.electron.build/code-signing.html) + - [https://docs.codemagic.io/yaml-code-signing/signing-macos/#saving-the-api-key-to-environment-variables](https://docs.codemagic.io/yaml-code-signing/signing-macos/#saving-the-api-key-to-environment-variables) + +### Packaging (locally) and Publishing + +Setup the _code signing_ to build, package (locally) and publish the app. **App Notarization for macOS** (skip this section for non macOS builds) -- Rename *sample.env* file as *.env* -- Update APPLEID in *.env* file -- Log into your [Apple Account](https://appleid.apple.com/account/manage "Apple Account") -- Goto **Security > App-Specific Passwords** -- Click on **Generate Password...**, enter a password label and click *Create* -- Copy the displayed *app-specific-password* + +- Rename _sample.env_ file as _.env_ +- To update `APPLEID` and `APPLE_APP_SPECIFIC_PASSWORD` in _.env_ file +- Log into your [Apple Account](https://appleid.apple.com/account/manage 'Apple Account') +- Goto **Sign-In and Security > App-Specific Passwords** +- Click on **Generate Password...**, enter a password label and click _Create_ +- Copy the displayed _app-specific-password_ - Run + ```shell -security add-generic-password -a "" -w -s "ELECTRON_NOTORIZE_PASSWORD" +security add-generic-password -a "" -w -s "APPLE_APP_SPECIFIC_PASSWORD" ``` -- Log into your [Apple App Store Connect Account](https://appstoreconnect.apple.com/agreements/# "Apple App Store Connect Account") and accept the presented terms and conditions -- The statuses shall turn *Active* +- Log into your [Apple App Store Connect Account](https://appstoreconnect.apple.com/agreements/# 'Apple App Store Connect Account') and accept the presented terms and conditions +- The statuses shall turn _Active_ + +**Sentry** + +- Auth Tokens Settings page: [https://sentry.io/settings/account/api/auth-tokens/](https://sentry.io/settings/account/api/auth-tokens/) + +```shell +npm install -g @sentry/wizard +sentry-wizard --integration electron + +# Upload Debug Information +# Everytime the electron.js version is upgraded run: +node sentry-symbols.js + +sentry-cli login +``` **Packaging** -Instructions: [https://www.electron.build/code-signing](https://www.electron.build/code-signing "https://www.electron.build/code-signing") +Instructions: [https://www.electron.build/code-signing](https://www.electron.build/code-signing 'https://www.electron.build/code-signing') ```shell $ export GH_TOKEN="" @@ -136,10 +254,10 @@ $ yarn package $ yarn package-all ``` - ### Technical Features -- Built using Electron v6, React v16.8, Redux v4, Webpack v4, Babel v7 and Material UI v4.2 and highly modular -- Loadables, dynamic reducer injection, selectors for efficient code. splitting and performance/startup optimisation + +- Built using Electron v17 and React v18 +- Loadables, Dynamic Reducer Injection, Selectors for code splitting and performance optimization - Hot module reload (HMR) for instant feedback - Inbuilt error logging and profile/settings management - Industry standard state management @@ -147,23 +265,24 @@ $ yarn package-all - Port assigned: **4642** ### Configurations -- *config/env/env.dev.js* and *config/env/env.prod.js* contain the PORT number of the app. -- *config/dev-app-update.yml* file holds the GitHub repo variables required by *electron-updater*. -- *config/google-analytics-key.js* file contains the Google Analytics Tracking ID. -- *package.json* **build.publish** object holds the values for publishing the packaged application. -- *app/constants* folder contains all the constants required by the app. + +- _config/env/env.dev.js_ and _config/env/env.prod.js_ contain the PORT number of the app. +- _config/dev-app-update.yml_ file holds the GitHub repo variables required by _electron-updater_. +- _config/google-analytics-key.js_ file contains the Google Analytics Tracking ID. +- _package.json_ **build.publish** object holds the values for publishing the packaged application. +- _app/constants_ folder contains all the constants required by the app. ### Debugging #### **Debugging Guide** -[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400 "Debugging Guide") +[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400 'Debugging Guide') #### **Dispatching redux actions from the main process** -[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118 "https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118") +[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118 'https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118') -[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108 "https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108") +[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108 'https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108') #### **VM112:2 Uncaught TypeError: Cannot read property 'state' of undefined error** @@ -176,40 +295,75 @@ $ set UPGRADE_EXTENSIONS=1 && npm run dev ``` ### Troubleshooting + +#### Your device is not recognized + +#### **node-mac-permissions** throws `Speech framework is not compatible with macOS < 10.15` + +- On macOS <= 10.14.x (mojave) the `yarn install` will throw a npm-rebuild error +- To "test" or "debug" the app on macOS mojave: + - remove the `node-mac-permissions` dependency from `package.json` + - Add the ignorePlugin line to `default.plugins` in the file `webpack/config.base.js` + - `new webpack.IgnorePlugin({ resourceRegExp: /^(node-mac-permissions)$/u }),` + - WARNING: **DO NOT commit** these changes to the upstream!! +- The `NODE_MAC_PERMISSIONS_MIN_OS` constant defines the minimum os version that is required to show the macos usage access permission popup +- For distribution make sure to build the app on a machine which is at least 10.15 (Catalina) + +[https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on](https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on 'https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on') + +- Raise an issue if your device is undetected: https://github.com/ganeshrvel/openmtp/issues/new?template=contribute.md + +#### The app goes blank while trying to connect a Samsung device + +- Uninstall Samsung SmartSwitch, if installed: [https://farazfazli.medium.com/how-i-reverse-engineered-keis-and-sidesync-and-fixed-mtp-8949acbb1c29](https://farazfazli.medium.com/how-i-reverse-engineered-keis-and-sidesync-and-fixed-mtp-8949acbb1c29 'https://farazfazli.medium.com/how-i-reverse-engineered-keis-and-sidesync-and-fixed-mtp-8949acbb1c29'), [https://github.com/ganeshrvel/openmtp/issues/212](https://github.com/ganeshrvel/openmtp/issues/212 'https://github.com/ganeshrvel/openmtp/issues/212'). + #### **Notarizing Electron apps throws - “You must first sign the relevant contracts online. (1048)” error** -[https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on](https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on "https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on") +[https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on](https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on 'https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on') ### More repos -- [npm: electron-root-path](https://github.com/ganeshrvel/npm-electron-root-path "Get the root path of an Electron Application") -- [Electron React Redux Advanced Boilerplate](https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate "Electron React Redux advanced boilerplate") -- [Tutorial Series by Ganesh Rathinavel](https://github.com/ganeshrvel/tutorial-series-ganesh-rathinavel "Tutorial Series by Ganesh Rathinavel") +- [npm: electron-root-path](https://github.com/ganeshrvel/npm-electron-root-path 'Get the root path of an Electron Application') +- [Electron React Redux Advanced Boilerplate](https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate 'Electron React Redux advanced boilerplate') +- [Tutorial Series by Ganesh Rathinavel](https://github.com/ganeshrvel/tutorial-series-ganesh-rathinavel 'Tutorial Series by Ganesh Rathinavel') ### Credits -- Sincere thanks to [Vladimir Menshakov](https://github.com/whoozle "Vladimir Menshakov") for [android-file-transfer-linux](https://github.com/whoozle/android-file-transfer-linux "android-file-transfer-linux") c++ library and for all his help and support that were extended towards this project. +- A special thanks to [CodeMagic](http://codemagic.io/ 'Codemagic - CI/CD') and [Kevin Suhajda](https://www.linkedin.com/in/kevinsuhajda 'Kevin Suhajda') for sponsoring their CI/CD VMs, thus making the app releases more streamlined and much easier now. 🎊🎊 Do checkout their [products](https://codemagic.io/integrations 'Codemagic - integrations') section for more. + +- Special shoutout to [@CodyJung](https://github.com/CodyJung 'CodyJung') for adding the [Fujifilm](https://github.com/ganeshrvel/go-mtpfs/pull/2) and [Garmin](https://github.com/ganeshrvel/go-mtpfs/pull/1) devices support. 🔥🔥 + +- Thanks to Ms [Ayushi Bothra](https://www.linkedin.com/in/ayushi-bothra-3103/ 'Ayushi Bothra') for contributing to the documentation and pages. + +- App logo was contributed by [Shubhendu Mitra](https://www.linkedin.com/in/shubhendum/ 'Shubhendu Mitra - LinkedIn'). Make sure to check out more of his works on [Behance](https://www.behance.net/soponhara 'Shubhendu Mitra - Behance'). -- Thanks to Ms [Ayushi Bothra](https://www.linkedin.com/in/ayushi-bothra-3103/ "Ayushi Bothra") for contributing to the documentation and pages. +- Thanks to [Vladimir Menshakov](https://github.com/whoozle 'Vladimir Menshakov') for [android-file-transfer-linux](https://github.com/whoozle/android-file-transfer-linux 'android-file-transfer-linux') (the MTP legacy Kernel) -- This app is built upon [https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate](https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate "https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate") which is a heavily modified fork of [https://github.com/electron-react-boilerplate/electron-react-boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate "https://github.com/electron-react-boilerplate/electron-react-boilerplate"). +- Shoutout to [@yennsarah](https://github.com/yennsarah 'yennsarah'), [@h0tk3y](https://github.com/h0tk3y), [@riginoommen](https://github.com/riginoommen 'riginoommen'), [@AjithKumarvm](https://github.com/AjithKumarvm 'AjithKumarvm'), [@kiranshaji555](https://github.com/kiranshaji555), Dick Cowan, Kjell Dankert, Thorolf E.R. Weißhuhn and to all other community members who helped me test the application. -- The icons used in the app were made by [Prosymbols](https://www.flaticon.com/authors/prosymbols "Prosymbols"), [Nikita Golubev](https://www.flaticon.com/authors/nikita-golubev "Nikita Golubev"), [Those Icons](https://www.flaticon.com/authors/those-icons "Those Icons"), [Kiranshastry](https://www.flaticon.com/authors/kiranshastry "Kiranshastry"), [Elias Bikbulatov](https://www.flaticon.com/authors/elias-bikbulatov "Elias Bikbulatov") & [Pixel perfect](https://www.flaticon.com/authors/pixel-perfect "Pixel perfect") and were distributed by [www.flaticon.com](https://www.flaticon.com/ "Flaticon") which is licensed under [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/ "Creative Commons BY 3.0"). +- This app was built upon [https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate](https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate 'https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate') which is a heavily modified fork of [https://github.com/electron-react-boilerplate/electron-react-boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate 'https://github.com/electron-react-boilerplate/electron-react-boilerplate'). -- The "no image found" icon was made by [Phonlaphat Thongsriphong](https://www.iconfinder.com/phatpc "Phonlaphat Thongsriphong"). +- The icons used in the app were made by [flaticon](https://www.flaticon.com), [good-ware](https://www.flaticon.com/authors/good-ware) and [kiranshastry](https://www.flaticon.com/authors/kiranshastry) which is licensed under [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0/ 'Creative Commons BY 3.0'). +- The "no image found" icon was made by [Phonlaphat Thongsriphong](https://www.iconfinder.com/phatpc 'Phonlaphat Thongsriphong'). ### Contribute -If you are interested in fixing issues and contributing directly to the code base, please see the [guidelines](https://github.com/ganeshrvel/openmtp/blob/master/CONTRIBUTING.md "guidelines"). + +If you are interested in fixing issues and contributing directly to the code base, please see the [guidelines](https://github.com/ganeshrvel/openmtp/blob/master/CONTRIBUTING.md 'guidelines'). ### Support OpenMTP + Help me keep the app FREE and open for all. -Paypal: [paypal.me/ganeshrvel](https://paypal.me/ganeshrvel "paypal.me/ganeshrvel") + +- Support us via PayPal: [paypal.me/ganeshrvel](https://paypal.me/ganeshrvel 'https://paypal.me/ganeshrvel') +- Buy Me A Coffee (UPI, PayPal, Credit/Debit Cards, Internet Banking): [buymeacoffee.com/ganeshrvel](https://buymeacoffee.com/ganeshrvel 'https://buymeacoffee.com/ganeshrvel') ### Contacts + Please feel free to contact me at ganeshrvel@outlook.com ### License -OpenMTP | Android File Transfer for macOS is released under [MIT License](https://github.com/ganeshrvel/openmtp/blob/master/LICENSE "MIT License"). + +OpenMTP | Android File Transfer for macOS is released under [MIT License](https://github.com/ganeshrvel/openmtp/blob/master/LICENSE 'MIT License'). Copyright © 2018-Present Ganesh Rathinavel diff --git a/app/api/sys/index.js b/app/api/sys/index.js deleted file mode 100644 index 7d5f2531..00000000 --- a/app/api/sys/index.js +++ /dev/null @@ -1,974 +0,0 @@ -'use strict'; - -/* eslint no-await-in-loop: off */ - -import { - readdir as fsReaddir, - rename as fsRename, - existsSync, - statSync, - lstatSync -} from 'fs'; -import Promise from 'bluebird'; -import junk from 'junk'; -import rimraf from 'rimraf'; -import mkdirp from 'mkdirp'; -import path from 'path'; -import moment from 'moment'; -import { spawn, exec } from 'child_process'; -import findLodash from 'lodash/find'; -import { log } from '../../utils/log'; -import { mtp as _mtpCli } from '../../utils/binaries'; - -import { DEVICES_LABEL, DEVICES_TYPE_CONST } from '../../constants'; -import { baseName, getExtension } from '../../utils/paths'; -import { - clearFileTransfer, - fetchDirList, - processMtpOutput, - setFileTransferProgress -} from '../../containers/HomePage/actions'; -import { - niceBytes, - percentage, - splitIntoLines, - truncate, - isArray, - undefinedOrNull -} from '../../utils/funcs'; -import { msToTime, unixTimestampNow } from '../../utils/date'; - -const readdir = Promise.promisify(fsReaddir); -const execPromise = Promise.promisify(exec); - -/** - * This hack is to support flex quotes parser for mtp cli. - */ -export const escapeShellMtp = cmd => { - if (cmd.indexOf(`\\"`) !== -1 && cmd.indexOf(`"\\`) !== -1) { - return cmd - .replace(/`/g, '\\`') - .replace(/\\/g, `\\\\\\\\`) - .replace(/"/g, `\\\\\\"`); - } - if (cmd.indexOf(`"\\"`) !== -1) { - return cmd - .replace(/`/g, '\\`') - .replace(/\\/g, `\\\\\\\\`) - .replace(/"/g, `\\\\\\"`); - } - if (cmd.indexOf(`\\"`) !== -1) { - return cmd - .replace(/`/g, '\\`') - .replace(/\\/g, `\\\\\\`) - .replace(/"/g, `\\\\\\\\"`); - } - if (cmd.indexOf(`"\\`) !== -1) { - return cmd - .replace(/`/g, '\\`') - .replace(/\\/g, `\\\\\\\\`) - .replace(/"/g, `\\\\\\"`); - } - return cmd - .replace(/`/g, '\\`') - .replace(/\\/g, `\\\\\\`) - .replace(/"/g, `\\\\\\"`); -}; - -const mtpCli = `"${escapeShellMtp(_mtpCli)}"`; - -const filterOutMtpLines = (string, index) => { - return ( - filterJunkMtpErrors(string) || - (index < 2 && string.toLowerCase().indexOf(`selected storage`) !== -1) - ); -}; - -const filterJunkMtpErrors = string => { - return ( - string === '\n' || - string === '\r\n' || - string === '' || - string.toLowerCase().indexOf(`device::find failed`) !== -1 || - string.toLowerCase().indexOf(`iocreateplugininterfaceforservice`) !== -1 || - string.toLowerCase().indexOf(`Device::Find failed`) !== -1 - ); -}; - -const cleanJunkMtpError = ({ error = null, stdout = null, stderr = null }) => { - const splittedError = splitIntoLines(error); - const filteredError = splittedError - ? splittedError.filter(a => !filterJunkMtpErrors(a)) - : []; - - const splittedStderr = splitIntoLines(stderr); - const filteredStderr = splittedStderr - ? splittedStderr.filter(a => !filterJunkMtpErrors(a)) - : []; - - return { - filteredError, - filteredStderr, - filteredStdout: stdout - }; -}; - -const promisifiedExec = command => { - try { - return new Promise(resolve => { - execPromise(command, (error, stdout, stderr) => { - const { - filteredStderr, - filteredError, - filteredStdout - } = cleanJunkMtpError({ error, stdout, stderr }); - - if ( - (undefinedOrNull(filteredStderr) || filteredStderr.length < 1) && - (undefinedOrNull(filteredError) || filteredError.length < 1) - ) { - return resolve({ - data: filteredStdout, - stderr: null, - error: null - }); - } - - return resolve({ - data: filteredStdout, - stderr: filteredStderr.join('\n'), - error: filteredError.join('\n') - }); - }); - }); - } catch (e) { - log.error(e); - } -}; - -const promisifiedExecNoCatch = command => { - return new Promise(resolve => { - execPromise(command, (error, stdout, stderr) => { - const { - filteredStderr, - filteredError, - filteredStdout - } = cleanJunkMtpError({ error, stdout, stderr }); - - if ( - (undefinedOrNull(filteredStderr) || filteredStderr.length < 1) && - (undefinedOrNull(filteredError) || filteredError.length < 1) - ) { - return resolve({ - data: filteredStdout, - stderr: null, - error: null - }); - } - - return resolve({ - data: stdout, - stderr, - error - }); - }); - }); -}; - -const checkMtpFileExists = async (filePath, mtpStoragesListSelected) => { - const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`; - const escapedFilePath = `${escapeShellMtp(filePath)}`; - - const { stderr } = await promisifiedExecNoCatch( - `${mtpCli} ${storageSelectCmd} "properties \\"${escapedFilePath}\\""` - ); - - return !stderr; -}; - -export const checkFileExists = async ( - filePath, - deviceType, - mtpStoragesListSelected -) => { - try { - if (typeof filePath === 'undefined' || filePath === null) { - return null; - } - - let _isArray = false; - if (isArray(filePath)) { - _isArray = true; - } - - let fullPath = null; - switch (deviceType) { - case DEVICES_TYPE_CONST.local: - if (_isArray) { - for (let i = 0; i < filePath.length; i += 1) { - const item = filePath[i]; - fullPath = path.resolve(item); - if (await existsSync(fullPath)) { - return true; - } - } - return null; - } - - fullPath = path.resolve(filePath); - return await existsSync(fullPath); - - case DEVICES_TYPE_CONST.mtp: - if (_isArray) { - for (let i = 0; i < filePath.length; i += 1) { - const item = filePath[i]; - fullPath = path.resolve(item); - if (await checkMtpFileExists(fullPath, mtpStoragesListSelected)) { - return true; - } - } - return null; - } - - fullPath = path.resolve(filePath); - return await checkMtpFileExists(fullPath, mtpStoragesListSelected); - - default: - break; - } - - return true; - } catch (e) { - log.error(e); - } -}; - -/** - Local device -> - */ -export const asyncReadLocalDir = async ({ filePath, ignoreHidden }) => { - try { - const response = []; - const { error, data } = await readdir(filePath, 'utf8') - .then(res => { - return { - data: res, - error: null - }; - }) - .catch(e => { - return { - data: null, - error: e - }; - }); - - if (error) { - log.error(error, `asyncReadLocalDir`); - return { error: true, data: null }; - } - - let files = data; - - files = data.filter(junk.not); - if (ignoreHidden) { - // eslint-disable-next-line no-useless-escape - files = data.filter(item => !/(^|\/)\.[^\/\.]/g.test(item)); - } - - for (let i = 0; i < files.length; i += 1) { - const file = files[i]; - const fullPath = path.resolve(filePath, file); - - if (!existsSync(fullPath)) { - continue; // eslint-disable-line no-continue - } - const stat = statSync(fullPath); - const isFolder = lstatSync(fullPath).isDirectory(); - const extension = path.extname(fullPath); - const { size, atime: dateTime } = stat; - - if (findLodash(response, { path: fullPath })) { - continue; // eslint-disable-line no-continue - } - - response.push({ - name: file, - path: fullPath, - extension, - size, - isFolder, - dateAdded: moment(dateTime).format('YYYY-MM-DD HH:mm:ss') - }); - } - return { error, data: response }; - } catch (e) { - log.error(e); - } -}; - -export const promisifiedRimraf = item => { - try { - return new Promise(resolve => { - rimraf(item, {}, error => { - resolve({ - data: null, - stderr: error, - error - }); - }); - }); - } catch (e) { - log.error(e); - } -}; - -export const delLocalFiles = async ({ fileList }) => { - try { - if (!fileList || fileList.length < 1) { - return { error: `No files selected.`, stderr: null, data: null }; - } - - for (let i = 0; i < fileList.length; i += 1) { - const item = fileList[i]; - const { error } = await promisifiedRimraf(item); - if (error) { - log.error(`${error}`, `delLocalFiles -> rm error`); - return { error, stderr: null, data: false }; - } - } - - return { error: null, stderr: null, data: true }; - } catch (e) { - log.error(e); - } -}; - -const promisifiedRename = ({ oldFilePath, newFilePath }) => { - try { - return new Promise(resolve => { - fsRename(oldFilePath, newFilePath, error => { - resolve({ - data: null, - stderr: error, - error - }); - }); - }); - } catch (e) { - log.error(e); - } -}; - -export const renameLocalFiles = async ({ oldFilePath, newFilePath }) => { - try { - if ( - typeof oldFilePath === 'undefined' || - oldFilePath === null || - typeof newFilePath === 'undefined' || - newFilePath === null - ) { - return { error: `No files selected.`, stderr: null, data: null }; - } - - const { error } = await promisifiedRename({ oldFilePath, newFilePath }); - if (error) { - log.error(`${error}`, `renameLocalFiles -> mv error`); - return { error, stderr: null, data: false }; - } - - return { error: null, stderr: null, data: true }; - } catch (e) { - log.error(e); - } -}; - -const promisifiedMkdir = ({ newFolderPath }) => { - try { - return new Promise(resolve => { - mkdirp(newFolderPath, error => { - resolve({ data: null, stderr: error, error }); - }); - }); - } catch (e) { - log.error(e); - } -}; - -export const newLocalFolder = async ({ newFolderPath }) => { - try { - if (typeof newFolderPath === 'undefined' || newFolderPath === null) { - return { error: `Invalid path.`, stderr: null, data: null }; - } - - const { error } = await promisifiedMkdir({ newFolderPath }); - if (error) { - log.error(`${error}`, `newLocalFolder -> mkdir error`); - return { error, stderr: null, data: false }; - } - - return { error: null, stderr: null, data: true }; - } catch (e) { - log.error(e); - } -}; - -/** - MTP device -> - */ -export const fetchMtpStorageOptions = async () => { - try { - const { data, error, stderr } = await promisifiedExec( - `${mtpCli} "storage-list"` - ); - - if (error || stderr) { - log.error( - `${error} : ${stderr}`, - `fetchMtpStorageOptions -> storage-list error`, - false - ); - return { error, stderr, data: null }; - } - - const _storageList = splitIntoLines(data); - - const descMatchPattern = /description:(.*)/i; - const storageIdMatchPattern = /([^\D]+)/; - - let storageList = {}; - _storageList - .filter((a, index) => !filterOutMtpLines(a, index)) - .map((a, index) => { - if (!a) { - return null; - } - const _matchDesc = descMatchPattern.exec(a); - const _matchedStorageId = storageIdMatchPattern.exec(a); - - if ( - typeof _matchDesc === 'undefined' || - _matchDesc === null || - typeof _matchDesc[1] === 'undefined' || - typeof _matchedStorageId === 'undefined' || - _matchedStorageId === null || - typeof _matchedStorageId[1] === 'undefined' - ) { - return null; - } - - const matchDesc = _matchDesc[1].trim(); - const matchedStorageId = _matchedStorageId[1].trim(); - storageList = { - ...storageList, - [matchedStorageId]: { - name: matchDesc, - selected: index === 0 - } - }; - - return storageList; - }); - - if ( - typeof storageList === 'undefined' || - storageList === null || - storageList.length < 1 - ) { - return { - error: `${ - DEVICES_LABEL[DEVICES_TYPE_CONST.mtp] - } storage not accessible`, - stderr: null, - data: null - }; - } - - return { error: null, stderr: null, data: storageList }; - } catch (e) { - log.error(e); - } -}; - -export const asyncReadMtpDir = async ({ - filePath, - ignoreHidden, - mtpStoragesListSelected -}) => { - try { - const mtpCmdChopIndex = { - type: 2, - dateAdded: 4, - timeAdded: 5 - }; - const response = []; - const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`; - - const { - data: filePropsData, - error: filePropsError, - stderr: filePropsStderr - } = await promisifiedExec( - `${mtpCli} ${storageSelectCmd} "lsext \\"${escapeShellMtp(filePath)}\\""` - ); - - if (filePropsError || filePropsStderr) { - log.error( - `${filePropsError} : ${filePropsStderr}`, - `asyncReadMtpDir -> lsext error` - ); - return { error: filePropsError, stderr: filePropsStderr, data: null }; - } - - let fileProps = splitIntoLines(filePropsData); - - fileProps = fileProps.filter((a, index) => !filterOutMtpLines(a, index)); - - for (let i = 0; i < fileProps.length; i += 1) { - const item = fileProps[i]; - const matchedProps = item.match( - /^(.*?)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/g - ); - - if (matchedProps === null || matchedProps.length < 1) { - continue; // eslint-disable-line no-continue - } - - const _matchedProps = matchedProps[0]; - const itemSplit = item.split(_matchedProps); - - if (itemSplit === null || itemSplit.length < 2 || itemSplit[1] === '') { - continue; // eslint-disable-line no-continue - } - const matchedFileName = itemSplit[1].replace(/^\s{2}|\s$/g, ''); - const filePropsList = _matchedProps.replace(/\s\s+/g, ' ').split(' '); - - // eslint-disable-next-line no-useless-escape - if (ignoreHidden && /(^|\/)\.[^\/\.]/g.test(matchedFileName)) { - continue; // eslint-disable-line no-continue - } - - const fullPath = path.resolve(filePath, matchedFileName); - const isFolder = filePropsList[mtpCmdChopIndex.type] === '3001'; - const dateTime = `${filePropsList[mtpCmdChopIndex.dateAdded]} ${ - filePropsList[mtpCmdChopIndex.timeAdded] - }`; - const extension = getExtension(fullPath, isFolder); - - // avoid duplicate values - if (findLodash(response, { path: fullPath })) { - continue; // eslint-disable-line no-continue - } - response.push({ - name: matchedFileName, - path: fullPath, - extension, - size: null, - isFolder, - dateAdded: moment(dateTime).format('YYYY-MM-DD HH:mm:ss') - }); - } - - return { error: null, stderr: null, data: response }; - } catch (e) { - log.error(e); - } -}; - -export const renameMtpFiles = async ({ - oldFilePath, - newFilePath, - mtpStoragesListSelected -}) => { - try { - if ( - typeof oldFilePath === 'undefined' || - oldFilePath === null || - typeof newFilePath === 'undefined' || - newFilePath === null - ) { - return { error: `No files selected.`, stderr: null, data: null }; - } - - const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`; - const escapedOldFilePath = `${escapeShellMtp(oldFilePath)}`; - const escapedNewFilePath = `${escapeShellMtp(baseName(newFilePath))}`; - - const { error, stderr } = await promisifiedExec( - `${mtpCli} ${storageSelectCmd} "rename \\"${escapedOldFilePath}\\" \\"${escapedNewFilePath}\\""` - ); - - if (error || stderr) { - log.error(`${error} : ${stderr}`, `renameMtpFiles -> rename error`); - return { error, stderr, data: false }; - } - - return { error: null, stderr: null, data: true }; - } catch (e) { - log.error(e); - } -}; - -export const delMtpFiles = async ({ fileList, mtpStoragesListSelected }) => { - try { - if (!fileList || fileList.length < 1) { - return { error: `No files selected.`, stderr: null, data: null }; - } - - const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`; - for (let i = 0; i < fileList.length; i += 1) { - const { error, stderr } = await promisifiedExec( - `${mtpCli} ${storageSelectCmd} "rm \\"${escapeShellMtp( - fileList[i] - )}\\""` - ); - - if (error || stderr) { - log.error(`${error} : ${stderr}`, `delMtpDir -> rm error`); - return { error, stderr, data: false }; - } - } - - return { error: null, stderr: null, data: true }; - } catch (e) { - log.error(e); - } -}; - -export const newMtpFolder = async ({ - newFolderPath, - mtpStoragesListSelected -}) => { - try { - if (typeof newFolderPath === 'undefined' || newFolderPath === null) { - return { error: `Invalid path.`, stderr: null, data: null }; - } - - const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`; - const escapedNewFolderPath = `${escapeShellMtp(newFolderPath)}`; - const { error, stderr } = await promisifiedExec( - `${mtpCli} ${storageSelectCmd} "mkpath \\"${escapedNewFolderPath}\\""` - ); - - if (error || stderr) { - log.error(`${error} : ${stderr}`, `newMtpFolder -> mkpath error`); - return { error, stderr, data: false }; - } - - return { error: null, stderr: null, data: true }; - } catch (e) { - log.error(e); - } -}; - -export const pasteFiles = ( - { ...pasteArgs }, - { ...fetchDirListArgs }, - direction, - deviceType, - dispatch, - getState, - getCurrentWindow -) => { - try { - const { - destinationFolder, - mtpStoragesListSelected, - fileTransferClipboard - } = pasteArgs; - - if ( - typeof destinationFolder === 'undefined' || - destinationFolder === null - ) { - dispatch( - processMtpOutput({ - deviceType, - error: `Invalid path.`, - stderr: null, - data: null, - callback: () => { - dispatch( - fetchDirList({ ...fetchDirListArgs }, deviceType, getState) - ); - } - }) - ); - } - - const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`; - const { queue } = fileTransferClipboard; - - if (typeof queue === 'undefined' || queue === null || queue.length < 1) { - dispatch( - processMtpOutput({ - deviceType, - error: `No files selected`, - stderr: null, - data: null, - callback: () => { - dispatch( - fetchDirList({ ...fetchDirListArgs }, deviceType, getState) - ); - } - }) - ); - } - - let _queue = []; - let cmdArgs = {}; - switch (direction) { - case 'mtpToLocal': - _queue = (queue || []).map(sourcePath => { - const destinationPath = path.resolve(destinationFolder); - const escapedDestinationPath = escapeShellMtp( - `${destinationPath}/${baseName(sourcePath)}` - ); - const escapedSourcePath = `${escapeShellMtp(sourcePath)}`; - - return `-e ${storageSelectCmd} "get \\"${escapedSourcePath}\\" \\"${escapedDestinationPath}\\""`; - }); - - cmdArgs = { - _queue - }; - return _pasteFiles( - { ...pasteArgs }, - { ...fetchDirListArgs }, - { ...cmdArgs }, - deviceType, - dispatch, - getState, - getCurrentWindow - ); - - case 'localtoMtp': - _queue = (queue || []).map(sourcePath => { - const destinationPath = path.resolve(destinationFolder); - const escapedDestinationPath = `${escapeShellMtp(destinationPath)}`; - const escapedSourcePath = `${escapeShellMtp(sourcePath)}`; - - return `-e ${storageSelectCmd} "put \\"${escapedSourcePath}\\" \\"${escapedDestinationPath}\\""`; - }); - - cmdArgs = { - _queue - }; - - return _pasteFiles( - { ...pasteArgs }, - { ...fetchDirListArgs }, - { ...cmdArgs }, - deviceType, - dispatch, - getState, - getCurrentWindow - ); - - default: - break; - } - } catch (e) { - log.error(e); - } -}; - -const _pasteFiles = ( - { ...pasteArgs }, // eslint-disable-line no-unused-vars - { ...fetchDirListArgs }, // eslint-disable-line no-unused-vars - { ...cmdArgs }, - deviceType, - dispatch, - getState, - getCurrentWindow -) => { - try { - const { _queue } = cmdArgs; - const handletransferListTimeInterval = 1000; - let transferList = {}; - let prevCopiedBlockSize = 0; - let currentCopiedBlockSize = 0; - let startTime = 0; - let prevCopiedTime = 0; - let currentCopiedTime = 0; - let bufferedOutput = null; - - let handleTransferListInterval = setInterval(() => { - if (transferList === null) { - clearInterval(handleTransferListInterval); - handleTransferListInterval = 0; - return null; - } - - if (Object.keys(transferList).length < 1) { - return null; - } - - const { percentage: _percentage, bodyText1, bodyText2 } = transferList; - const copiedTimeDiff = currentCopiedTime - prevCopiedTime; - const speed = - prevCopiedBlockSize && prevCopiedBlockSize - currentCopiedBlockSize > 0 - ? (prevCopiedBlockSize - currentCopiedBlockSize) * - (1000 / copiedTimeDiff) - : 0; - const _speed = speed ? `${niceBytes(speed)}` : `--`; - const elapsedTime = msToTime(currentCopiedTime - startTime); - prevCopiedTime = currentCopiedTime; - prevCopiedBlockSize = currentCopiedBlockSize; - - getCurrentWindow().setProgressBar(_percentage / 100); - dispatch( - setFileTransferProgress({ - toggle: true, - bodyText1, - bodyText2: `Elapsed: ${elapsedTime} | Progress: ${bodyText2} @ ${_speed}/sec`, - percentage: _percentage - }) - ); - }, handletransferListTimeInterval); - - const cmd = spawn(mtpCli, [..._queue], { - shell: true - }); - - cmd.stdout.on('data', data => { - bufferedOutput = data.toString(); - - if (startTime === 0) { - startTime = unixTimestampNow(); - } - - if ( - typeof bufferedOutput === 'undefined' || - bufferedOutput === null || - bufferedOutput.length < 1 - ) { - return null; - } - - const _bufferedOutput = splitIntoLines(bufferedOutput).filter( - (a, index) => !filterOutMtpLines(a, index) - ); - - if (_bufferedOutput.length < 1) { - return null; - } - - for (let i = 0; i < _bufferedOutput.length; i += 1) { - const item = _bufferedOutput[i]; - const bufferedOutputSplit = item.split(' '); - - if (bufferedOutputSplit.length < 1) { - return null; - } - - const totalLength = bufferedOutputSplit.length; - const eventIndex = 0; - const filePathStartIndex = 1; - const filePathEndIndex = totalLength - 3; - const currentProgressSizeIndex = totalLength - 2; - const totalFileSizeIndex = totalLength - 1; - - const event = bufferedOutputSplit[eventIndex]; - const matchedItem = item.match(/(\d+?\d*)\s(\d+?\d*)$/); - if (matchedItem === null) { - return null; - } - - const matchedItemSplit = matchedItem[0].split(' '); - const currentProgressSize = parseInt(matchedItemSplit[0], 10); - const totalFileSize = parseInt(matchedItemSplit[1], 10); - - if (event === `:done`) { - prevCopiedBlockSize = 0; - currentCopiedBlockSize = 0; - prevCopiedTime = 0; - currentCopiedTime = 0; - return null; - } - - if ( - totalLength < 3 || - event !== `:progress` || - currentProgressSizeIndex < 2 || - totalFileSizeIndex < 3 - ) { - return null; - } - - const filePath = bufferedOutputSplit - .slice(filePathStartIndex, filePathEndIndex + 1) - .join(' '); - - const perc = percentage(currentProgressSize, totalFileSize); - currentCopiedBlockSize = totalFileSize - currentProgressSize; - currentCopiedTime = unixTimestampNow(); - - transferList = { - bodyText1: `${perc}% complete of ${truncate(baseName(filePath), 45)}`, - bodyText2: `${niceBytes(currentProgressSize)} / ${niceBytes( - totalFileSize - )}`, - percentage: perc, - currentCopiedBlockSize, - currentCopiedTime - }; - } - }); - - cmd.stderr.on('data', error => { - const { filteredError } = cleanJunkMtpError({ error }); - - if (undefinedOrNull(filteredError) || filteredError.length < 1) { - return null; - } - - dispatch( - processMtpOutput({ - deviceType, - error, - stderr: null, - data: null, - callback: () => { - transferList = null; - getCurrentWindow().setProgressBar(-1); - dispatch(clearFileTransfer()); - dispatch( - fetchDirList({ ...fetchDirListArgs }, deviceType, getState) - ); - } - }) - ); - }); - - cmd.on('exit', () => { - transferList = null; - getCurrentWindow().setProgressBar(-1); - dispatch(clearFileTransfer()); - dispatch(fetchDirList({ ...fetchDirListArgs }, deviceType, getState)); - }); - - return { error: null, stderr: null, data: true }; - } catch (e) { - log.error(e); - } -}; - -export const mtpVerboseReport = async () => { - try { - const { data, error, stderr } = await promisifiedExec(`${mtpCli} "pwd" -v`); - - if (error) { - log.doLog(`${error}`); - return { error, stderr, data: null }; - } - if (stderr) { - log.doLog(`${stderr}`); - return { error, stderr, data: null }; - } - - return { error: null, stderr: null, data }; - } catch (e) { - log.error(e); - } -}; diff --git a/app/api/www/index.js b/app/api/www/index.js deleted file mode 100644 index 1bfe2890..00000000 --- a/app/api/www/index.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -export function fetchUrl({ url }) { - return fetch(`${url}`).then(res => { - if (res.status === 200) { - return res.json(); - } - return null; - }); -} diff --git a/app/app.html b/app/app.html index b8c39c21..a5ac4b08 100644 --- a/app/app.html +++ b/app/app.html @@ -1,45 +1,57 @@ - - - - OpenMTP | Android File Transfer for macOS - - - -
- + + +
+ - + PORT = require("../config/env").PORT; + } + if (process.env.START_HOT) { + // Dynamically insert the bundled app script in the renderer process + scripts.push(`http://localhost:${PORT}/dist/renderer.dev.js`); + } else { + scripts.push("./dist/renderer.prod.js"); + } + + if (scripts.length) { + document.write( + scripts + .map(script => ` + diff --git a/app/classes/AppUpdate.js b/app/classes/AppUpdate.js index 62d55bbc..fe7071bf 100644 --- a/app/classes/AppUpdate.js +++ b/app/classes/AppUpdate.js @@ -1,20 +1,27 @@ -'use strict'; - -import { dialog, BrowserWindow, ipcMain } from 'electron'; +import { BrowserWindow, dialog, ipcMain } from 'electron'; import { autoUpdater } from 'electron-updater'; import { isConnected } from '../utils/isOnline'; import { log } from '../utils/log'; import { isPackaged } from '../utils/isPackaged'; -import { PATHS } from '../utils/paths'; +import { PATHS } from '../constants/paths'; import { unixTimestampNow } from '../utils/date'; import { undefinedOrNull } from '../utils/funcs'; -import { getMainWindowMainProcess } from '../utils/windowHelper'; -import { appUpdateAvailableWindow } from '../utils/createWindows'; +import { + getMainWindowMainProcess, + getWindowBackgroundColor, +} from '../helpers/windowHelper'; +import { appUpdateAvailableWindow } from '../helpers/createWindows'; +import { UPDATER_STATUS } from '../enums/appUpdater'; +import { getRemoteWindow } from '../helpers/remoteWindowHelpers'; let progressbarWindow = null; let isFileTransferActiveFlag = false; let mainWindow = null; +const remote = getRemoteWindow(); + +remote.initialize(); + const createChildWindow = () => { try { return new BrowserWindow({ @@ -30,8 +37,11 @@ const createChildWindow = () => { fullscreenable: false, movable: false, webPreferences: { - nodeIntegration: true - } + nodeIntegration: true, + contextIsolation: false, + enableRemoteModule: true, + }, + backgroundColor: getWindowBackgroundColor(), }); } catch (e) { log.error(e, `AppUpdate -> createChildWindow`); @@ -43,11 +53,12 @@ const fireProgressbar = () => { if (progressbarWindow) { progressbarWindow.show(); progressbarWindow.focus(); + return null; } mainWindow.webContents.send('isFileTransferActiveSeek', { - check: true + check: true, }); ipcMain.once('isFileTransferActiveReply', (event, { ...args }) => { @@ -57,6 +68,8 @@ const fireProgressbar = () => { }); progressbarWindow = createChildWindow(); + remote.enable(progressbarWindow.webContents); + progressbarWindow.loadURL( `${PATHS.loadUrlPath}#appUpdatePage/updateProgress` ); @@ -70,7 +83,7 @@ const fireProgressbar = () => { progressbarWindow = null; }); - progressbarWindow.onerror = error => { + progressbarWindow.onerror = (error) => { log.error(error, `AppUpdate -> progressbarWindow -> onerror`); }; } catch (e) { @@ -82,22 +95,23 @@ export default class AppUpdate { constructor({ autoUpdateCheck, autoDownload, allowPrerelease }) { this.autoUpdater = autoUpdater; if (!isPackaged) { - this.autoUpdater.updateConfigPath = PATHS.appUpdateFile; + this.autoUpdater.updateConfigPath = PATHS.devAppUpdateFile; } - this.autoUpdater.autoDownload = autoDownload; + this.autoUpdater.autoDownload = autoUpdateCheck && autoDownload; this.autoUpdater.allowPrerelease = allowPrerelease; - this.autoUpdateCheck = autoUpdateCheck; + this.progressbarWindowDomReadyFlag = null; this.updateInitFlag = false; this.updateForceCheckFlag = false; this._errorDialog = { timeGenerated: 0, title: null, - message: null + message: null, }; - this.updateIsActive = 0; // 0 = no, 1 = update check in progress, -1 = update in progress + + this.updateStatus = UPDATER_STATUS.inactive; this.disableAutoUpdateCheck = false; } @@ -107,13 +121,11 @@ export default class AppUpdate { return; } - this.autoUpdater.on('error', error => { - const errorMsg = - error == null ? 'unknown' : (error.stack || error).toString(); - + this.autoUpdater.on('error', (error) => { if (progressbarWindow !== null) { progressbarWindow.close(); } + this.closeActiveUpdates(); if (this.isNetworkError(error)) { @@ -122,7 +134,8 @@ export default class AppUpdate { 'Oops.. A network error occured. Try again!', 'error' ); - log.doLog(error); + + log.doLog(error, `AppUpdate -> onerror -> isNetworkError`); return null; } @@ -133,11 +146,14 @@ export default class AppUpdate { 'error' ); - log.error(errorMsg, `AppUpdate -> onerror`); + log.error(error, `AppUpdate -> onerror`); }); - this.autoUpdater.on('update-available', info => { - if (progressbarWindow !== null && this.updateIsActive !== -1) { + this.autoUpdater.on('update-available', (info) => { + if ( + progressbarWindow !== null && + this.updateStatus !== UPDATER_STATUS.updateInProgress + ) { progressbarWindow.close(); } @@ -151,7 +167,7 @@ export default class AppUpdate { const _appUpdateAvailableWindow = appUpdateAvailableWindow(); _appUpdateAvailableWindow.on('close', () => { - if (this.updateIsActive !== -1) { + if (this.updateStatus !== UPDATER_STATUS.updateInProgress) { this.closeActiveUpdates(); } }); @@ -165,23 +181,30 @@ export default class AppUpdate { ipcMain.once('appUpdatesUpdateAvailableReply', (event, { ...args }) => { const { confirm } = args; + if (!confirm) { - if (this.updateIsActive !== -1) { + if (this.updateStatus !== UPDATER_STATUS.updateInProgress) { this.closeActiveUpdates(); } + return null; } if (progressbarWindow !== null) { progressbarWindow.close(); } + this.closeActiveUpdates(-1); this.initDownloadUpdatesProgress(); - this.autoUpdater.downloadUpdate(); + + // this is to prevent race condition + if (!this.autoUpdater.autoDownload) { + this.autoUpdater.downloadUpdate(); + } }); }); - this.autoUpdater.on('download-progress', progress => { + this.autoUpdater.on('download-progress', (progress) => { if (progressbarWindow === null) { return null; } @@ -189,27 +212,24 @@ export default class AppUpdate { this.setUpdateProgressWindow({ value: progress.percent || 0 }); }); - this.autoUpdater.on('update-downloaded', () => { + this.autoUpdater.on('update-downloaded', async () => { this.closeActiveUpdates(); if (progressbarWindow !== null) { progressbarWindow.close(); } - dialog.showMessageBox( - { - title: 'Install Updates', - message: 'Updates downloaded. Application will quit now...', - buttons: ['Install and Relaunch'] - }, - buttonIndex => { - switch (buttonIndex) { - case 0: - default: - this.autoUpdater.quitAndInstall(); - break; - } - } - ); + const { response: buttonIndex } = await dialog.showMessageBox({ + title: 'Install Updates', + message: 'Updates downloaded. Application will quit now...', + buttons: ['Install and Relaunch'], + }); + + switch (buttonIndex) { + case 0: + default: + this.autoUpdater.quitAndInstall(); + break; + } }); this.updateInitFlag = true; @@ -227,21 +247,26 @@ export default class AppUpdate { } isConnected() - .then(connected => { + .then((connected) => { if (!connected) { return null; } - if (this.updateIsActive === 1 || this.disableAutoUpdateCheck) { + if ( + this.updateStatus === UPDATER_STATUS.checkInProgress || + this.disableAutoUpdateCheck + ) { return null; } this.autoUpdater.on('update-not-available', () => { - this.updateIsActive = 0; + this.updateStatus = UPDATER_STATUS.inactive; }); this.autoUpdater.checkForUpdates(); - this.updateIsActive = 1; + + this.updateStatus = UPDATER_STATUS.checkInProgress; + return true; }) .catch(() => {}); @@ -250,7 +275,7 @@ export default class AppUpdate { } } - forceCheck() { + async forceCheck() { try { this.setMainWindow(); @@ -258,66 +283,65 @@ export default class AppUpdate { return; } - if (!this.updateForceCheckFlag && this.updateIsActive !== 1) { + if ( + !this.updateForceCheckFlag && + this.updateStatus !== UPDATER_STATUS.checkInProgress + ) { this.autoUpdater.on('checking-for-update', () => { this.setCheckUpdatesProgress(); }); - this.autoUpdater.on('update-not-available', () => { + this.autoUpdater.on('update-not-available', async () => { // an another 'update-not-available' event is registered at checkForUpdates() as well this.closeActiveUpdates(); if (progressbarWindow !== null) { progressbarWindow.close(); } - dialog.showMessageBox( - { - title: 'No Updates Found', - message: 'You have the latest version installed.', - buttons: ['Close'] - }, - buttonIndex => { - switch (buttonIndex) { - case 0: - default: - break; - } - } - ); + + const { response: buttonIndex } = await dialog.showMessageBox({ + title: 'No Updates Found', + message: 'You have the latest version installed.', + buttons: ['Close'], + }); + + switch (buttonIndex) { + case 0: + default: + break; + } }); } - if (this.updateIsActive === 1) { + if (this.updateStatus === UPDATER_STATUS.checkInProgress) { return null; } - if (this.updateIsActive === -1) { - dialog.showMessageBox( - { - title: 'Update in progress', - message: - 'Another update is in progess. Are you sure want to restart the update?', - buttons: ['No', 'Yes'] - }, - buttonIndex => { - switch (buttonIndex) { - case 0: - default: - break; - case 1: - this.autoUpdater.checkForUpdates(); - this.updateIsActive = -1; - break; - } - } - ); + if (this.updateStatus === UPDATER_STATUS.updateInProgress) { + const { response: buttonIndex } = await dialog.showMessageBox({ + title: 'Update in progress', + message: + 'Another update is in progess. Are you sure want to restart the update?', + buttons: ['Cancel', 'Yes'], + }); + + switch (buttonIndex) { + case 1: + this.autoUpdater.checkForUpdates(); + this.updateStatus = UPDATER_STATUS.updateInProgress; + break; + case 0: + default: + break; + } + return null; } this.autoUpdater.checkForUpdates(); this.updateForceCheckFlag = true; this.disableAutoUpdateCheck = true; - this.updateIsActive = 1; + this.updateStatus = UPDATER_STATUS.checkInProgress; } catch (e) { log.error(e, `AppUpdate -> forceCheck`); } @@ -326,12 +350,13 @@ export default class AppUpdate { setCheckUpdatesProgress() { try { isConnected() - .then(connected => { + .then((connected) => { if (!connected) { this.spitMessageDialog( 'Checking For Updates', 'Internet connection is unavailable.' ); + return null; } @@ -345,10 +370,11 @@ export default class AppUpdate { progressTitle: `Checking For Updates`, progressBodyText: `Please wait...`, value: 0, - variant: `indeterminate` + variant: `indeterminate`, } ); }); + return true; }) .catch(() => {}); @@ -360,18 +386,20 @@ export default class AppUpdate { initDownloadUpdatesProgress() { try { isConnected() - .then(connected => { + .then((connected) => { if (!connected) { this.spitMessageDialog( 'Downloading Updates', 'Internet connection is unavailable.' ); + return null; } fireProgressbar(); this.progressbarWindowDomReadyFlag = false; this.setUpdateProgressWindow({ value: 0 }); + return true; }) .catch(() => {}); @@ -386,7 +414,7 @@ export default class AppUpdate { progressTitle: `Downloading Updates`, progressBodyText: `Please wait...`, value, - variant: `determinate` + variant: `determinate`, }; this.setTaskBarProgressBar(value / 100); @@ -395,6 +423,7 @@ export default class AppUpdate { 'appUpdatesProgressBarCommunication', data ); + return null; } @@ -413,6 +442,7 @@ export default class AppUpdate { setMainWindow() { const _mainWindow = getMainWindowMainProcess(); + if (undefinedOrNull(_mainWindow)) { return null; } @@ -443,7 +473,7 @@ export default class AppUpdate { ); } - spitMessageDialog(title, message, type = 'message') { + async spitMessageDialog(title, message, type = 'message') { const { timeGenerated: _timeGenerated } = this._errorDialog; const delayTime = 1000; @@ -457,35 +487,35 @@ export default class AppUpdate { this._errorDialog = { timeGenerated: unixTimestampNow(), title, - message + message, }; switch (type) { - default: - case 'message': - dialog.showMessageBox( - { - title, - message, - buttons: ['Close'] - }, - buttonIndex => { - switch (buttonIndex) { - case 0: - default: - break; - } - } - ); - break; case 'error': dialog.showErrorBox(title, message); break; + + case 'message': + default: + // eslint-disable-next-line no-case-declarations + const { response: buttonIndex } = await dialog.showMessageBox({ + title, + message, + buttons: ['Close'], + }); + + switch (buttonIndex) { + case 0: + default: + break; + } + + break; } } - closeActiveUpdates(updateIsActive = 0) { + closeActiveUpdates(updateStatus = UPDATER_STATUS.inactive) { this.setTaskBarProgressBar(-1); - this.updateIsActive = updateIsActive; + this.updateStatus = updateStatus; } } diff --git a/app/classes/Boot.js b/app/classes/Boot.js index ea8b82f3..d303ca9a 100644 --- a/app/classes/Boot.js +++ b/app/classes/Boot.js @@ -1,5 +1,3 @@ -'use strict'; - /* eslint no-await-in-loop: off */ /** @@ -8,17 +6,18 @@ */ import { readdirSync } from 'fs'; -import { baseName, PATHS } from '../utils/paths'; +import { PATHS } from '../constants/paths'; import { fileExistsSync, writeFileAsync, createDirSync, - deleteFilesSync -} from '../api/sys/fileOps'; -import { daysDiff, yearMonthNow } from '../utils/date'; + deleteFilesSync, +} from '../helpers/fileOps'; +import { dateNow, daysDiff } from '../utils/date'; import { LOG_FILE_ROTATION_CLEANUP_THRESHOLD } from '../constants'; +import { baseName } from '../utils/files'; -const { logFile, settingsFile, logDir } = PATHS; +const { logFile, settingsFile, logDir, prevProfileDir } = PATHS; const logFileRotationCleanUpThreshold = LOG_FILE_ROTATION_CLEANUP_THRESHOLD; export default class Boot { @@ -50,6 +49,12 @@ export default class Boot { } } + // if the previous version of the profile directory exists then remove it + // issue: https://github.com/ganeshrvel/openmtp/issues/143 + if (await this.verifyDir(prevProfileDir)) { + await deleteFilesSync(prevProfileDir); + } + return true; } catch (e) { console.error(e); @@ -65,6 +70,7 @@ export default class Boot { return false; } } + for (let i = 0; i < this.verifyFileList.length; i += 1) { const item = this.verifyFileList[i]; @@ -103,7 +109,7 @@ export default class Boot { async createDir(newFolderPath) { try { - createDirSync(newFolderPath); + await createDirSync(newFolderPath); } catch (e) { console.error(e); } @@ -130,7 +136,7 @@ export default class Boot { const dirFileList = readdirSync(logDir); const pattern = `^\\${baseName(logFile)}`; const _regex = new RegExp(pattern, 'gi'); - const filesList = dirFileList.filter(elm => { + const filesList = dirFileList.filter((elm) => { return !elm.match(_regex); }); @@ -138,8 +144,9 @@ export default class Boot { return null; } - filesList.map(async a => { + filesList.map(async (a) => { const dateMatch = a.match(/\d{4}-\d{2}/g); + if ( dateMatch === null || dateMatch.length < 1 || @@ -149,7 +156,8 @@ export default class Boot { return null; } - const _diff = daysDiff(yearMonthNow({}), dateMatch[0]); + const _diff = daysDiff(dateNow({}), dateMatch[0]); + if (_diff >= logFileRotationCleanUpThreshold) { deleteFilesSync(`${logDir}/${a}`); } diff --git a/app/classes/Storage.js b/app/classes/Storage.js index 4a18b1ba..0051a95a 100644 --- a/app/classes/Storage.js +++ b/app/classes/Storage.js @@ -1,39 +1,53 @@ -'use strict'; - import { log } from '../utils/log'; -import { readFileSync, writeFileSync } from '../api/sys/fileOps'; +import { readFileSync, writeFileSync } from '../helpers/fileOps'; +import { checkIf } from '../utils/checkIf'; +import { isEmpty } from '../utils/funcs'; export default class Storage { - constructor(filePath) { + constructor(filePath, doNotLog = false) { this.filePath = filePath; + this.doNotLog = doNotLog; } getAll() { try { const _stream = readFileSync(this.filePath); + if ( typeof _stream === 'undefined' || + _stream === '' || _stream === null || Object.keys(_stream).length < 1 ) { return {}; } + return JSON.parse(_stream); } catch (e) { - log.error(e, `Storage -> getAll`); + if (this.doNotLog) { + console.error(e, `Storage -> getAll`); + } else { + log.error(e, `Storage -> getAll`); + } } } getItems(keys) { + checkIf(keys, 'array'); + try { - if (typeof keys === 'undefined' || keys === null || keys.length < 0) { + if (isEmpty(keys)) { return {}; } const allItem = this.getAll(); const _return = {}; - keys.map(a => { + if (!allItem) { + return _return; + } + + keys.map((a) => { if (typeof allItem[a] === 'undefined' || allItem[a] === null) { return null; } @@ -45,7 +59,11 @@ export default class Storage { return _return; } catch (e) { - log.error(e, `Storage -> getItems`); + if (this.doNotLog) { + console.error(e, `Storage -> getItems`); + } else { + log.error(e, `Storage -> getItems`); + } } } @@ -53,7 +71,28 @@ export default class Storage { try { writeFileSync(this.filePath, JSON.stringify({ ...data })); } catch (e) { - log.error(e, `Storage -> setAll`); + if (this.doNotLog) { + console.error(e, `Storage -> setAll`); + } else { + log.error(e, `Storage -> setAll`); + } + } + } + + setItems({ ...data }) { + try { + const currentSettings = this.getAll(); + + writeFileSync( + this.filePath, + JSON.stringify({ ...currentSettings, ...data }) + ); + } catch (e) { + if (this.doNotLog) { + console.error(e, `Storage -> setItems`); + } else { + log.error(e, `Storage -> setItems`); + } } } } diff --git a/app/components/Breadcrumb/index.jsx b/app/components/Breadcrumb/index.jsx index ac8e0603..4e079b38 100644 --- a/app/components/Breadcrumb/index.jsx +++ b/app/components/Breadcrumb/index.jsx @@ -1,5 +1,3 @@ -'use strict'; - import React, { PureComponent, Fragment } from 'react'; import { withStyles } from '@material-ui/core/styles'; import classNames from 'classnames'; @@ -7,19 +5,30 @@ import Paper from '@material-ui/core/Paper'; import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight'; import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; import Tooltip from '@material-ui/core/Tooltip'; -import { sanitizePath } from '../../utils/paths'; import { quickHash } from '../../utils/funcs'; import { styles } from './styles'; +import { sanitizePath } from '../../utils/files'; +import { analyticsService } from '../../services/analytics'; +import { EVENT_TYPE } from '../../enums/events'; class Breadcrumb extends PureComponent { _handleClickPath = (enabled, value, event) => { - const { onBreadcrumbPathClick } = this.props; + const { onBreadcrumbPathClick, deviceType } = this.props; + event.preventDefault(); if (!enabled) { return null; } + onBreadcrumbPathClick({ path: value }); + + const deviceTypeUpperCase = deviceType.toUpperCase(); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_BREADCRUMB_PATH_TAP`], + {} + ); }; tokenizeCurrentBrowsePath(currentBrowsePath) { @@ -39,18 +48,21 @@ class Breadcrumb extends PureComponent { currentBrowsePathBroken.map((a, index) => { const label = a; let _isCompressed = false; + if (index === currentBrowsePathBrokenLength - 1) { _bold = true; _enabled = false; } + if (a === '' && index === 0) { _currentBrowsePath.push({ label: 'Root', path: '/', isCompressed: _isCompressed, enabled: _enabled, - bold: _bold + bold: _bold, }); + return null; } @@ -67,7 +79,7 @@ class Breadcrumb extends PureComponent { path: `${currentBrowsePathBroken.slice(0, index + 1).join('/')}`, isCompressed: _isCompressed, enabled: _enabled, - bold: _bold + bold: _bold, }); }); @@ -80,15 +92,17 @@ class Breadcrumb extends PureComponent { return tokenizeCurrentBrowsePath.map((item, index) => { const { label, path, isCompressed, enabled, bold } = item; + if (isCompressed) { compressedCounter += 1; } + return ( {isCompressed ? ( this.CompressedBreadcrumbCellRender({ isCompressed, - compressedCounter + compressedCounter, }) ) : ( @@ -103,9 +117,9 @@ class Breadcrumb extends PureComponent { { + onClick={(event) => { this._handleClickPath(enabled, path, event); }} > @@ -122,6 +136,7 @@ class Breadcrumb extends PureComponent { CompressedBreadcrumbCellRender({ compressedCounter }) { const { classes: styles } = this.props; + return compressedCounter < 2 ? ( diff --git a/app/components/Breadcrumb/styles/index.js b/app/components/Breadcrumb/styles/index.js index 9e77092c..af4034cf 100644 --- a/app/components/Breadcrumb/styles/index.js +++ b/app/components/Breadcrumb/styles/index.js @@ -1,45 +1,45 @@ -'use strict'; +import { mixins } from '../../../styles/js'; -import { variables, mixins } from '../../../styles/js'; +export const styles = (theme) => { + return { + root: { + width: `100%`, + height: `100%`, + }, -export const styles = theme => ({ - root: { - width: `100%`, - height: `100%` - }, + rootBreadcrumbs: { + width: `100%`, + height: `100%`, + }, - rootBreadcrumbs: { - width: `100%`, - height: `100%` - }, + breadcrumb: { + padding: '10px 15px', + backgroundColor: theme.palette.background.paper, + display: 'flex', + alignItems: 'center', + ...mixins({ theme }).resetUl, + }, - breadcrumb: { - padding: '10px 15px', - backgroundColor: variables().styles.primaryColor.main, - display: 'flex', - alignItems: 'center', - ...mixins().resetUl - }, + breadcrumbLi: { + display: 'inline-block', + padding: '0 2px 4px 2px', + overflow: 'hidden', + fontSize: 14, + maxWidth: 59, + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }, - breadcrumbLi: { - display: 'inline-block', - padding: '0 2px 4px 2px', - overflow: 'hidden', - fontSize: 14, - maxWidth: 59, - whiteSpace: 'nowrap', - textOverflow: 'ellipsis' - }, - - breadcrumbLiA: { - cursor: `pointer`, - color: variables().styles.secondaryColor.main, - textDecoration: 'none', - [`&.bold`]: { - fontWeight: `bold` - } - }, - breadcrumbSeperator: { - fontSize: 18 - } -}); + breadcrumbLiA: { + cursor: `pointer`, + color: theme.palette.secondary.main, + textDecoration: 'none', + [`&.bold`]: { + fontWeight: `bold`, + }, + }, + breadcrumbSeperator: { + fontSize: 18, + }, + }; +}; diff --git a/app/components/DialogBox/components/Confirm.jsx b/app/components/DialogBox/components/Confirm.jsx index 6e83bd07..dd04d40b 100644 --- a/app/components/DialogBox/components/Confirm.jsx +++ b/app/components/DialogBox/components/Confirm.jsx @@ -10,17 +10,42 @@ import DialogTitle from '@material-ui/core/DialogTitle'; import { styles } from '../styles/Confirm'; class Confirm extends PureComponent { - _handleBtnClick = ({ confirm = false }) => { + _handleBtnClick = (value) => { const { onClickHandler } = this.props; - onClickHandler(confirm); + + onClickHandler(value); }; - _handleKeyPress = event => { + _handleKeyPress = (event) => { if (event.key === 'Enter') { - this._handleBtnClick({ confirm: true }); + const { actions } = this.props; + const defaultAction = + actions?.find((action) => action.defaultAction) ?? actions?.[0]; + + if (defaultAction) { + this._handleBtnClick(defaultAction.value); + + return; + } + + this._handleBtnClick(true); } }; + _handleEscapeKeyDown = () => { + const { actions, onClickHandler } = this.props; + const cancelAction = + actions?.find((action) => action.value === 'cancel') ?? actions?.[0]; + + if (cancelAction) { + onClickHandler(cancelAction.value); + + return; + } + + onClickHandler(false); + }; + render() { const { classes: styles, @@ -28,8 +53,10 @@ class Confirm extends PureComponent { bodyText, trigger, fullWidthDialog, - maxWidthDialog + maxWidthDialog, + actions, } = this.props; + return ( this._handleBtnClick({ confirm: false })} + onEscapeKeyDown={this._handleEscapeKeyDown} > {titleText} {bodyText} - - + {actions ? ( + actions.map((action) => ( + + )) + ) : ( + <> + + + + )} ); diff --git a/app/components/DialogBox/components/Notification.jsx b/app/components/DialogBox/components/Notification.jsx index 2d020185..2e5d8a97 100644 --- a/app/components/DialogBox/components/Notification.jsx +++ b/app/components/DialogBox/components/Notification.jsx @@ -13,10 +13,11 @@ import { isString } from '../../../utils/funcs'; class Notification extends PureComponent { _handleBtnClick = ({ confirm = false }) => { const { onClickHandler } = this.props; + onClickHandler(confirm); }; - _handleKeyPress = event => { + _handleKeyPress = (event) => { if (event.key === 'Enter') { this._handleBtnClick({ confirm: true }); } @@ -29,8 +30,9 @@ class Notification extends PureComponent { bodyText, trigger, fullWidthDialog, - maxWidthDialog + maxWidthDialog, } = this.props; + return ( - - {bodyText1} - + {values.map((a, index) => { + return ( + // eslint-disable-next-line react/no-array-index-key + + 0, + })} + > + {a.bodyText1 ?? ''} + - + - - {bodyText2} - + + {a.bodyText2 ?? ''} + + + ); + })} + {bottomText &&
{bottomText}
} {children &&
{children}
}
diff --git a/app/components/DialogBox/components/Selection.jsx b/app/components/DialogBox/components/Selection.jsx index fb001f04..a6907704 100644 --- a/app/components/DialogBox/components/Selection.jsx +++ b/app/components/DialogBox/components/Selection.jsx @@ -1,6 +1,6 @@ -'use strict'; - import React, { PureComponent, Fragment } from 'react'; +import classnames from 'classnames'; +import Tooltip from '@material-ui/core/Tooltip'; import { withStyles } from '@material-ui/core/styles'; import List from '@material-ui/core/List'; import Avatar from '@material-ui/core/Avatar'; @@ -9,26 +9,30 @@ import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; import DialogTitle from '@material-ui/core/DialogTitle'; import Dialog from '@material-ui/core/Dialog'; -import SdStorageIcon from '@material-ui/icons/SdStorage'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { styles } from '../styles/Selection'; +import { isEmpty } from '../../../utils/funcs'; class Selection extends PureComponent { _handleListItemClick = ({ ...args }) => { const { onClose } = this.props; + onClose({ ...args }); }; render() { - const { list, titleText, open, showDiskAvatars } = this.props; - if (Object.keys(list).length < 1) { + const { list, titleText, open, showAvatar, classes: styles } = this.props; + + if (isEmpty(list)) { return ; } + return ( this._handleListItemClick({ selectedValue: null, - triggerChange: false + triggerChange: false, }) } open={open} @@ -36,28 +40,39 @@ class Selection extends PureComponent { {titleText}
- {Object.keys(list).map(a => { - const item = list[a]; + {list.map((item) => { return ( - + + onClick={() => { this._handleListItemClick({ - selectedValue: a, - triggerChange: true - }) - } + selectedValue: item.value, + triggerChange: true, + }); + }} > - {showDiskAvatars && ( + {showAvatar && ( - - + + )} - + + + ); diff --git a/app/components/DialogBox/components/TextFieldEdit.jsx b/app/components/DialogBox/components/TextFieldEdit.jsx index b3e55408..083ae2d0 100644 --- a/app/components/DialogBox/components/TextFieldEdit.jsx +++ b/app/components/DialogBox/components/TextFieldEdit.jsx @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; +import TextField from '@material-ui/core/TextField'; import { withStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; @@ -8,7 +9,7 @@ import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; -import { StyledTextField, styles } from '../styles/TextFieldEdit'; +import { styles } from '../styles/TextFieldEdit'; class TextFieldEdit extends PureComponent { constructor(props) { @@ -19,12 +20,13 @@ class TextFieldEdit extends PureComponent { _handleBtnClick = ({ confirm = false }, event) => { const { onClickHandler } = this.props; + event.preventDefault(); onClickHandler({ confirm, textFieldValue: this.textFieldValue }); }; - _handleChange = event => { + _handleChange = (event) => { this.textFieldValue = event.target.value; }; @@ -46,7 +48,7 @@ class TextFieldEdit extends PureComponent { id, btnPositiveText, btnNegativeText, - errors + errors, } = this.props; return ( @@ -56,26 +58,29 @@ class TextFieldEdit extends PureComponent { fullWidth={fullWidthDialog} maxWidth={maxWidthDialog} disableEscapeKeyDown={false} - onEscapeKeyDown={event => + onEscapeKeyDown={(event) => this._handleBtnClick({ confirm: false }, event) } > {titleText}
this._handleBtnClick({ confirm: true }, event)} + onSubmit={(event) => this._handleBtnClick({ confirm: true }, event)} noValidate autoComplete="off" > - {bodyText} + {bodyText} - {typeof secondaryText !== 'undefined' && secondaryText !== null - ? secondaryText - : ''} + {typeof secondaryText !== 'undefined' && + secondaryText !== null ? ( + {secondaryText} + ) : ( + '' + )} - this._handleChange(event)} - onBlur={event => this._handleChange(event)} - onChange={event => this._handleChange(event)} + onFocus={(event) => this._handleChange(event)} + onBlur={(event) => this._handleChange(event)} + onChange={(event) => this._handleChange(event)} error={errors.toggle} className={styles.textFieldRoot} + color="secondary" /> , ]} {...other} /> ); -}; +} export default withStyles(styles)(SnackbarThemeWrapper); diff --git a/app/components/Snackbars/index.jsx b/app/components/Snackbars/index.jsx index d499cc13..520ae348 100644 --- a/app/components/Snackbars/index.jsx +++ b/app/components/Snackbars/index.jsx @@ -20,6 +20,7 @@ class Snackbars extends PureComponent { _handleClose = (event, reason) => { const { OnSnackBarsCloseAlerts } = this.props; + if (reason === 'clickaway') { return; } diff --git a/app/components/Snackbars/styles/SnackbarThemeWrapper.js b/app/components/Snackbars/styles/SnackbarThemeWrapper.js index d9224160..c53f1e4f 100644 --- a/app/components/Snackbars/styles/SnackbarThemeWrapper.js +++ b/app/components/Snackbars/styles/SnackbarThemeWrapper.js @@ -1,37 +1,42 @@ -'use strict'; - import green from '@material-ui/core/colors/green'; import amber from '@material-ui/core/colors/amber'; -import { variables, mixins } from '../../../styles/js'; -export const styles = theme => ({ +export const styles = (theme) => ({ success: { - backgroundColor: green[600] + backgroundColor: green[600], }, error: { - backgroundColor: theme.palette.error.dark + backgroundColor: theme.palette.snackbar.error, }, info: { - backgroundColor: theme.palette.primary.dark + backgroundColor: theme.palette.primary.dark, }, warning: { - backgroundColor: amber[700] + backgroundColor: amber[700], }, icon: { - fontSize: 20 + fontSize: 25, + color: '#fff', + }, + closeBtn: { + color: '#fff', }, iconVariant: { opacity: 0.9, - marginRight: 10 + marginRight: 10, }, message: { display: 'flex', - alignItems: 'center' + alignItems: 'center', + color: '#fff', + fontWeight: 500, }, root: { minWidth: 288, maxWidth: 568, - borderRadius: 4, - flexGrow: `unset` - } + minHeight: 60, + borderRadius: 15, + flexGrow: `unset`, + cursor: `pointer`, + }, }); diff --git a/app/components/Snackbars/styles/index.js b/app/components/Snackbars/styles/index.js index 1b1c5e1c..9e1b0413 100644 --- a/app/components/Snackbars/styles/index.js +++ b/app/components/Snackbars/styles/index.js @@ -1,14 +1,10 @@ -'use strict'; - -import { variables, mixins } from '../../../styles/js'; - -export const styles = theme => ({ +export const styles = (_) => ({ margin: { - margin: 10 + margin: 10, }, root: { top: 10, right: 15, - left: `unset` - } + left: `unset`, + }, }); diff --git a/app/constants/dom.js b/app/constants/dom.js index 9fab7f08..ee22d166 100644 --- a/app/constants/dom.js +++ b/app/constants/dom.js @@ -1,5 +1,3 @@ -'use strict'; - export const APP_TITLEBAR_DOM_ID = `app-main-titlebar`; export const FILE_EXPLORER_BODY_WRAPPER_ID = `file-explorer-body-wrapper`; diff --git a/app/constants/env.js b/app/constants/env.js index 7a2a8719..1f462438 100644 --- a/app/constants/env.js +++ b/app/constants/env.js @@ -1,12 +1,50 @@ -'use strict'; - /** * Constants * Note: Don't import log helper file from utils here */ -module.exports.IS_DEV = process.env.NODE_ENV !== 'production'; +const isDev = process.env.NODE_ENV !== 'production'; +const isProd = process.env.NODE_ENV === 'production'; +const isDebug = process.env.DEBUG_PROD === 'true'; + +const config = { + dev: { + reportToSenty: false, + enableGoogleAnalytics: false, + enableMixpanelAnalytics: false, + disableReactWarnings: true, + allowDevelopmentEnvironment: true, + }, + prod: { + reportToSenty: true, + enableGoogleAnalytics: true, + enableMixpanelAnalytics: true, + disableReactWarnings: false, + allowDevelopmentEnvironment: false, + }, + debug: { + reportToSenty: true, + enableGoogleAnalytics: true, + enableMixPanelAnalytics: true, + disableReactWarnings: false, + allowDevelopmentEnvironment: true, + }, +}; + +let _env = 'dev'; + +if (isProd) { + _env = 'prod'; +} else if (isDebug) { + _env = 'debug'; +} + +module.exports.ENV_FLAVOR = config[_env]; + +module.exports.IS_DEV = isDev; + +module.exports.IS_PROD = isProd; -module.exports.IS_PROD = process.env.NODE_ENV === 'production'; +module.exports.DEBUG_PROD = isDebug; -module.exports.DEBUG_PROD = process.env.DEBUG_PROD === 'true'; +module.exports.IS_RENDERER = process && process.type === 'renderer'; diff --git a/app/constants/index.js b/app/constants/index.js index 6c0edd0e..7a134db5 100644 --- a/app/constants/index.js +++ b/app/constants/index.js @@ -1,26 +1,43 @@ -'use strict'; - /** * Constants * Note: Don't import log helper file from utils here */ -import { PATHS } from '../utils/paths'; +import { PATHS } from './paths'; +import { DEVICE_TYPE } from '../enums'; + +export const OS_ARCH_TYPE = { + amd64: 'amd64', + arm64: 'arm64', +}; + +// To support macOS version below Big Sur the Kalam kernel needs to be compiled on an older macOS machine everytime there is an update, which is practically very difficult. +// So any version included in the [KALAM_HISTORIC_VERSION_TYPE] will not receive the latest Kalam Kernel updates +export const KALAM_HISTORIC_VERSION_TYPE = { + medieval: 'medieval', // macOS 10.14 (Mojave) and 10.15 (Catalina). libusb support is still available but since it requires Mojave or lower to compile the kernel, it is being deprecated. +}; + +// if the user's os version is higher than the ones listed here then latest kalam kernel binaies will be used +// reference: https://github.com/npm/node-semver#ranges +export const KALAM_HISTORIC_MACOS_VERSION_RANGE = { + [KALAM_HISTORIC_VERSION_TYPE.medieval]: `>=10.14 <=10.15.999`, +}; + +export const NODE_MAC_PERMISSIONS_MIN_OS = `11.0.0`; -export const DEVICES_TYPE_CONST = { mtp: 'mtp', local: 'local' }; +export const KALAM_MODE_MIN_MACOS_VERSION = `>=10.14`; export const DEVICES_DEFAULT_PATH = { - [DEVICES_TYPE_CONST.mtp]: '/', - [DEVICES_TYPE_CONST.local]: PATHS.homeDir + [DEVICE_TYPE.mtp]: '/', + [DEVICE_TYPE.local]: PATHS.homeDir, }; export const DEVICES_LABEL = { - [DEVICES_TYPE_CONST.mtp]: 'Phone', - [DEVICES_TYPE_CONST.local]: `Computer` + [DEVICE_TYPE.mtp]: `Phone`, + [DEVICE_TYPE.local]: `Computer`, }; -export const FILE_EXPLORER_DEFAULT_FOCUSSED_DEVICE_TYPE = - DEVICES_TYPE_CONST.local; +export const FILE_EXPLORER_DEFAULT_FOCUSSED_DEVICE_TYPE = DEVICE_TYPE.local; export const LOG_FILE_ROTATION_CLEANUP_THRESHOLD = 60; // in days @@ -30,4 +47,12 @@ export const FILE_EXPLORER_TABLE_TRUNCATE_MAX_CHARS = 37; export const FILE_EXPLORER_GRID_TRUNCATE_MAX_CHARS = 20; -export const DONATE_PAYPAL_URL = `https://paypal.me/ganeshrvel`; +export const SUPPORT_PAYPAL_URL = `https://paypal.me/ganeshrvel`; + +export const BUY_ME_A_COFFEE_URL = `https://www.buymeacoffee.com/ganeshrvel`; + +export const DELETE_KEIS_SMARTSWITCH_URL = `https://farazfazli.medium.com/how-i-reverse-engineered-keis-and-sidesync-and-fixed-mtp-8949acbb1c29`; + +export const USB_HOTPLUG_MAX_ATTEMPTS = 6; + +export const USB_HOTPLUG_MAX_ATTEMPTS_TIMEOUT = 15000; // in ms diff --git a/app/constants/keymaps.js b/app/constants/keymaps.js index 394bb5ff..e89c9520 100644 --- a/app/constants/keymaps.js +++ b/app/constants/keymaps.js @@ -1,87 +1,85 @@ -'use strict'; - /* keymaps must be in lowercase */ import { IS_DEV } from './env'; export const fileExplorerKeymaps = { newFolder: { label: `New Folder`, - keys: ['command+n'] + keys: ['command+n'], }, copy: { label: `Copy`, - keys: ['command+c'] + keys: ['command+c'], }, copyToQueue: { label: `Copy to Queue`, - keys: ['command+shift+c'] + keys: ['command+shift+c'], }, paste: { label: `Paste`, - keys: ['command+v'] + keys: ['command+v'], }, delete: { label: `Delete`, - keys: ['backspace', 'delete'] + keys: ['backspace', 'delete'], }, refresh: { label: `Refresh`, - keys: IS_DEV ? ['command+alt+r'] : ['command+r'] + keys: IS_DEV ? ['command+alt+r'] : ['command+r'], }, up: { label: `Folder Up`, - keys: ['command+b'] + keys: ['command+b'], }, selectAll: { label: `Select All`, - keys: ['command+a'] + keys: ['command+a'], }, rename: { label: `Rename`, - keys: ['command+d'] + keys: ['command+d'], }, open: { label: `Open`, - keys: ['enter'] + keys: ['enter'], }, fileExplorerTabSwitch: { label: `Switch Tab`, - keys: ['command+1'] + keys: ['command+1'], }, navigationLeft: { label: `Navigate Left`, - keys: ['left'] + keys: ['left'], }, navigationRight: { label: `Navigate Right`, - keys: ['right'] + keys: ['right'], }, navigationUp: { label: `Navigate Up`, - keys: ['up'] + keys: ['up'], }, navigationDown: { label: `Navigate Down`, - keys: ['down'] + keys: ['down'], }, multipleSelectLeft: { label: `Select Multiple Items Forward (for Grid view)`, - keys: ['shift+left'] + keys: ['shift+left'], }, multipleSelectRight: { label: `Select Multiple Items Backward (for Grid view)`, - keys: ['shift+right'] + keys: ['shift+right'], }, multipleSelectUp: { label: `Select Multiple Items Forward (for List view)`, - keys: ['shift+up'] + keys: ['shift+up'], }, multipleSelectDown: { label: `Select Multiple Items Backward (for List view)`, - keys: ['shift+down'] + keys: ['shift+down'], }, multipleSelectClick: { label: `Select Multiple Items (using mouse)`, - keys: ['command+click', 'shift+click'] - } + keys: ['command+click', 'shift+click'], + }, }; diff --git a/app/constants/meta.js b/app/constants/meta.js index 5d6c04c5..55906d35 100644 --- a/app/constants/meta.js +++ b/app/constants/meta.js @@ -1,12 +1,9 @@ -'use strict'; - /** * Constants * Note: Don't import log helper file from utils here */ import { pkginfo } from '../utils/pkginfo'; -import { undefinedOrNullChained } from '../utils/funcs'; const { productName, @@ -16,20 +13,18 @@ const { version, repository, homepage, - bugs + bugs, } = pkginfo; +export const APP_BUNDLE_ID = 'io.ganeshrvel.openmtp'; + export const APP_NAME = `${productName}`; export const APP_VERSION = `${version}`; -export const AUTHOR_EMAIL = undefinedOrNullChained(author, 'email') - ? author.email - : null; +export const AUTHOR_EMAIL = author?.email ?? null; -export const AUTHOR_NAME = undefinedOrNullChained(author, 'name') - ? author.name - : null; +export const AUTHOR_NAME = author?.name ?? null; export const APP_DESC = `${description}`; @@ -37,14 +32,12 @@ export const APP_TITLE = `${APP_DESC}`; export const APP_IDENTIFIER = `${name}`; -export const APP_GITHUB_URL = undefinedOrNullChained(repository, 'url') +export const APP_GITHUB_URL = repository?.url ? repository.url.replace(/^git\+|\.git/g, '') : null; export const APP_GITHUB_RELEASES_URL = `${APP_GITHUB_URL}/releases`; -export const APP_GITHUB_ISSUES_URL = undefinedOrNullChained(bugs, 'url') - ? bugs.url - : null; +export const APP_GITHUB_ISSUES_URL = bugs?.url ?? null; export const APP_WEBSITE = `${homepage}`; diff --git a/app/constants/onboarding.js b/app/constants/onboarding.js index 357ba3e5..b31b2fbc 100644 --- a/app/constants/onboarding.js +++ b/app/constants/onboarding.js @@ -1,3 +1,3 @@ -'use strict'; - -export const latestUpdatePushVersion = '2.4.0-beta.1'; +// most recently used version number to show the onboarding screen +// search keywords: new, next, update, onboarding +export const latestUpdatePushVersion = '3.2.20'; diff --git a/app/constants/paths.js b/app/constants/paths.js new file mode 100644 index 00000000..4e1c30e6 --- /dev/null +++ b/app/constants/paths.js @@ -0,0 +1,61 @@ +/** + * Paths + * Note: Don't import log helper file from utils here + */ + +import { join, resolve } from 'path'; +import { homedir as homedirOs } from 'os'; +import url from 'url'; +import { rootPath as root } from 'electron-root-path'; +import { isPackaged } from '../utils/isPackaged'; +import { IS_DEV } from './env'; +import { yearMonthNow } from '../utils/date'; +import { APP_NAME } from './meta'; +import { getAppDataPath } from '../utils/files'; + +const appPath = join(root, `./app`); +const configDir = join(root, `./config`); +const homeDir = homedirOs(); +const profileDir = getAppDataPath(); + +// old generation [profileDir] path. Used until OpenMTP < v3.0.0 +const prevProfileDir = join(homeDir, `./.io.ganeshrvel`); + +const rotateFile = yearMonthNow({}); +const logFileName = IS_DEV + ? `error-${rotateFile}.dev.log` + : `error-${rotateFile}.log`; +const logDir = join(profileDir, `./logs`); +const logFile = join(logDir, `./${APP_NAME}-${logFileName}`); +const settingsFile = join(profileDir, `./settings.json`); +const identifierFile = join(profileDir, `./identifier.json`); +const devAppUpdateFile = join(configDir, `./dev-app-update.yml`); + +export const PATHS = { + root: resolve(root), + app: resolve(appPath), + dist: resolve(join(appPath, `./dist`)), + nodeModules: resolve(join(root, `./node_modules`)), + homeDir: resolve(homeDir), + profileDir: resolve(profileDir), + configDir: resolve(configDir), + logDir: resolve(logDir), + logFile: resolve(logFile), + settingsFile: resolve(settingsFile), + identifierFile: resolve(identifierFile), + devAppUpdateFile: resolve(devAppUpdateFile), + prevProfileDir: resolve(prevProfileDir), + desktopDir: join(homeDir, `/Desktop`), + documentsDir: join(homeDir, `/Documents`), + downloadsDir: join(homeDir, `/Downloads`), + picturesDir: join(homeDir, `/Pictures`), + volumesDir: '/Volumes', + systemRootDir: '/', + loadUrlPath: url.format({ + protocol: 'file', + slashes: true, + pathname: !isPackaged + ? join(appPath, './app.html') + : join(__dirname, './app.html'), + }), +}; diff --git a/app/constants/serviceKeys.js b/app/constants/serviceKeys.js new file mode 100644 index 00000000..7177cdbe --- /dev/null +++ b/app/constants/serviceKeys.js @@ -0,0 +1,5 @@ +export const SERVICE_KEYS = { + sentryDsn: `https://1f60d05960cc4c10a744bebd19bc2814@o410539.ingest.sentry.io/5544425`, + googleAnalytics: `UA-131227413-1`, + mixpanelAnalytics: `e2ae6803122e622822a5fbf32ff0e5ae`, +}; diff --git a/app/containers/Alerts/actions.js b/app/containers/Alerts/actions.js index 56b5d375..bd36da39 100644 --- a/app/containers/Alerts/actions.js +++ b/app/containers/Alerts/actions.js @@ -1,6 +1,4 @@ -'use strict'; - -import prefixer from '../../utils/reducerPrefixer'; +import prefixer from '../../helpers/reducerPrefixer'; const prefix = '@@Alerts'; const actionTypesList = ['THROW_ALERT', 'CLEAR_ALERT']; @@ -11,12 +9,13 @@ export function throwAlert(data) { return { type: actionTypes.THROW_ALERT, payload: { - ...data - } + ...data, + }, }; } + export function clearAlert() { return { - type: actionTypes.CLEAR_ALERT + type: actionTypes.CLEAR_ALERT, }; } diff --git a/app/containers/Alerts/index.jsx b/app/containers/Alerts/index.jsx index a5c2de98..f1bce6ef 100644 --- a/app/containers/Alerts/index.jsx +++ b/app/containers/Alerts/index.jsx @@ -1,5 +1,3 @@ -'use strict'; - import React, { Component } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -11,12 +9,14 @@ import Snackbars from '../../components/Snackbars'; class Alerts extends Component { _handleClose = () => { const { actionCreateClearAlert } = this.props; + actionCreateClearAlert(); }; render() { const { Alerts } = this.props; const { message, variant, autoHideDuration } = Alerts; + return ( message && ( +const mapDispatchToProps = (dispatch, __) => bindActionCreators( { - actionCreateClearAlert: () => (_, getState) => { + actionCreateClearAlert: () => (_, __) => { dispatch(clearAlert()); - } + }, }, dispatch ); -const mapStateToProps = (state, props) => { +const mapStateToProps = (state, __) => { return { - Alerts: state.Alerts + Alerts: state.Alerts, }; }; diff --git a/app/containers/Alerts/reducers.js b/app/containers/Alerts/reducers.js index 8a84bc26..42e4be3a 100644 --- a/app/containers/Alerts/reducers.js +++ b/app/containers/Alerts/reducers.js @@ -1,25 +1,24 @@ -'use strict'; - import { actionTypes } from './actions'; export const initialState = { message: null, - autoHideDuration: 6000, - variant: `error` + autoHideDuration: 2500, // in ms + variant: `error`, }; export default function Alerts(state = initialState, action) { const { type, payload } = action; + switch (type) { case actionTypes.THROW_ALERT: return { ...state, - ...payload + ...payload, }; case actionTypes.CLEAR_ALERT: return { ...state, - ...initialState + ...initialState, }; default: return state; diff --git a/app/containers/App/Root.jsx b/app/containers/App/Root.jsx index cb209bb6..1f97c557 100644 --- a/app/containers/App/Root.jsx +++ b/app/containers/App/Root.jsx @@ -1,14 +1,14 @@ -'use strict'; - import React, { Component } from 'react'; import { Provider } from 'react-redux'; import { ConnectedRouter } from 'react-router-redux'; +import '../../helpers/console'; import App from '.'; export default class Root extends Component { render() { const { store, history } = this.props; + return ( diff --git a/app/containers/App/actions.js b/app/containers/App/actions.js index cebe1c87..b9f95c65 100644 --- a/app/containers/App/actions.js +++ b/app/containers/App/actions.js @@ -1,6 +1,4 @@ -'use strict'; - -import prefixer from '../../utils/reducerPrefixer'; +import prefixer from '../../helpers/reducerPrefixer'; const prefix = '@@App'; const actionTypesList = ['REQ_LOAD', 'RES_LOAD', 'FAIL_LOAD']; @@ -9,12 +7,13 @@ export const actionTypes = prefixer(prefix, actionTypesList); export function reqLoadApp() { return { - type: actionTypes.REQ_LOAD + type: actionTypes.REQ_LOAD, }; } + export function resLoadApp() { return { - type: actionTypes.RES_LOAD + type: actionTypes.RES_LOAD, }; } @@ -22,7 +21,7 @@ export function failLoadApp(e) { return { type: actionTypes.FAIL_LOAD, payload: { - error: e - } + error: e, + }, }; } diff --git a/app/containers/App/components/Titlebar.jsx b/app/containers/App/components/Titlebar.jsx index c36cdfee..24fd4956 100644 --- a/app/containers/App/components/Titlebar.jsx +++ b/app/containers/App/components/Titlebar.jsx @@ -1,14 +1,19 @@ -'use strict'; - import React, { PureComponent } from 'react'; import { withStyles } from '@material-ui/core/styles'; import { styles } from '../styles/Titlebar'; -import { toggleWindowSizeOnDoubleClick } from '../../../utils/titlebarDoubleClick'; +import { toggleWindowSizeOnDoubleClick } from '../../../helpers/titlebarDoubleClick'; import { APP_TITLEBAR_DOM_ID } from '../../../constants/dom'; +import { capitalize, isEmpty, niceBytes } from '../../../utils/funcs'; +import { getSelectedStorage } from '../../HomePage/actions'; +import { getCurrentWindowHash } from '../../../helpers/windowHelper'; class Titlebar extends PureComponent { render() { - const { classes: styles } = this.props; + const { classes: styles, mtpDevice, mtpStoragesList, mtpMode } = this.props; + + const selectedStorage = getSelectedStorage(mtpStoragesList); + const windowHash = getCurrentWindowHash(); + return (
{ @@ -16,7 +21,27 @@ class Titlebar extends PureComponent { }} className={styles.root} id={APP_TITLEBAR_DOM_ID} - /> + > + {/* Only show the device info. on the main window */} + {windowHash !== '/' ? null : mtpDevice?.isAvailable && + mtpDevice?.info?.mtpDeviceInfo && + !isEmpty(selectedStorage?.data?.info) ? ( + + + {`${mtpDevice?.info?.mtpDeviceInfo?.Model} (${selectedStorage?.data?.name}) - `} + + {`${niceBytes( + parseInt(selectedStorage?.data.info?.FreeSpaceInBytes ?? 0, 10) + )} Free of ${niceBytes( + parseInt(selectedStorage?.data.info?.MaxCapability ?? 0, 10) + )}, ${capitalize(mtpMode)} Mode`} + + ) : ( + + {`${capitalize(mtpMode)} Mode`} + + )} +
); } } diff --git a/app/containers/App/index.jsx b/app/containers/App/index.jsx index af7f6b7b..549f3302 100644 --- a/app/containers/App/index.jsx +++ b/app/containers/App/index.jsx @@ -1,46 +1,49 @@ -'use strict'; - import { hot } from 'react-hot-loader/root'; +import { ipcRenderer } from 'electron'; import React, { Component } from 'react'; import CssBaseline from '@material-ui/core/CssBaseline'; import { MuiThemeProvider, createMuiTheme, - withStyles + withStyles, } from '@material-ui/core/styles'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import Analytics from 'electron-ga'; -import { log } from '@Log'; -import { IS_PROD } from '../../constants/env'; -import { theme, styles } from './styles'; +import { materialUiTheme, styles } from './styles'; import Alerts from '../Alerts'; import Titlebar from './components/Titlebar'; import ErrorBoundary from '../ErrorBoundary'; import Routes from '../../routing'; -import { bootLoader } from '../../utils/bootHelper'; -import { settingsStorage } from '../../utils/storageHelper'; +import { bootLoader } from '../../helpers/bootHelper'; +import { settingsStorage } from '../../helpers/storageHelper'; import SettingsDialog from '../Settings'; import { withReducer } from '../../store/reducers/withReducer'; import reducers from './reducers'; import { copyJsonFileToSettings, freshInstall } from '../Settings/actions'; -import { isConnected } from '../../utils/isOnline'; -import { TRACKING_ID } from '../../../config/google-analytics-key'; -import { APP_NAME, APP_VERSION } from '../../constants/meta'; - -const appTheme = createMuiTheme(theme()); +import { + makeAppThemeMode, + makeAppThemeModeSettings, + makeMtpMode, +} from '../Settings/selectors'; +import { getAppThemeMode } from '../../helpers/theme'; +import { getMainWindowRendererProcess } from '../../helpers/windowHelper'; +import { log } from '../../utils/log'; +import { makeMtpDevice, makeMtpStoragesList } from '../HomePage/selectors'; +import { analyticsService } from '../../services/analytics'; class App extends Component { constructor(props) { super(props); - this.state = {}; + this.mainWindowRendererProcess = getMainWindowRendererProcess(); + this.allowWritingJsonToSettings = false; } componentWillMount() { try { this.setFreshInstall(); + if (this.allowWritingJsonToSettings) { this.writeJsonToSettings(); } @@ -53,19 +56,46 @@ class App extends Component { componentDidMount() { try { + ipcRenderer.on('nativeThemeUpdated', this.nativeThemeUpdatedEvent); + bootLoader.cleanRotationFiles(); } catch (e) { log.error(e, `App -> componentDidMount`); } } + componentWillUnmount() { + this.deregisterAccelerators(); + ipcRenderer.removeListener( + 'nativeThemeUpdated', + this.nativeThemeUpdatedEvent + ); + + this.mainWindowRendererProcess.webContents.removeListener( + 'nativeThemeUpdated', + () => {} + ); + } + + nativeThemeUpdatedEvent = () => { + // force update the component + this.setState({}); + }; + + getMuiTheme = () => { + const { appThemeModeSettings } = this.props; + const appThemeMode = getAppThemeMode(appThemeModeSettings); + + return createMuiTheme(materialUiTheme({ appThemeMode })); + }; + setFreshInstall() { try { const { actionCreateFreshInstall } = this.props; - const isFreshInstallSettings = settingsStorage.getItems(['freshInstall']); + const setting = settingsStorage.getItems(['freshInstall']); let isFreshInstall = 0; - switch (isFreshInstallSettings.freshInstall) { + switch (setting.freshInstall) { case undefined: case null: // app was just installed @@ -84,6 +114,7 @@ class App extends Component { // more than 2 boot ups have occured isFreshInstall = 0; this.allowWritingJsonToSettings = true; + return null; } @@ -104,66 +135,60 @@ class App extends Component { } } - runAnalytics() { - const isAnalyticsEnabledSettings = settingsStorage.getItems([ - 'enableAnalytics' - ]); - - try { - if (isAnalyticsEnabledSettings.enableAnalytics && IS_PROD) { - isConnected() - .then(connected => { - const analytics = new Analytics(TRACKING_ID, { - appName: APP_NAME, - appVersion: APP_VERSION - }); - - analytics.send('screenview', { cd: '/Home' }); - analytics.send(`pageview`, { dp: '/Home' }); - - return connected; - }) - .catch(() => {}); - } - } catch (e) { - log.error(e, `App -> runAnalytics`); - } + async runAnalytics() { + await analyticsService.init(); } render() { - const { classes: styles } = this.props; + const { classes: styles, mtpDevice, mtpStoragesList, mtpMode } = this.props; + const muiTheme = this.getMuiTheme(); + return (
- - - - - - - - - - + + + + + + + + +
); } } -const mapDispatchToProps = (dispatch, ownProps) => + +const mapDispatchToProps = (dispatch) => bindActionCreators( { - actionCreateCopyJsonFileToSettings: ({ ...data }) => (_, getState) => { - dispatch(copyJsonFileToSettings({ ...data })); - }, - - actionCreateFreshInstall: ({ ...data }) => (_, getState) => { - dispatch(freshInstall({ ...data }, getState)); - } + actionCreateCopyJsonFileToSettings: + ({ ...data }) => + (_, __) => { + dispatch(copyJsonFileToSettings({ ...data })); + }, + + actionCreateFreshInstall: + ({ ...data }) => + (_, getState) => { + dispatch(freshInstall({ ...data }, getState)); + }, }, dispatch ); -const mapStateToProps = (state, props) => { - return {}; +const mapStateToProps = (state) => { + return { + appThemeModeSettings: makeAppThemeModeSettings(state), + appThemeMode: makeAppThemeMode(state), + mtpDevice: makeMtpDevice(state), + mtpMode: makeMtpMode(state), + mtpStoragesList: makeMtpStoragesList(state), + }; }; export default withReducer( diff --git a/app/containers/App/reducers.js b/app/containers/App/reducers.js index 2bb0ceba..866a459b 100644 --- a/app/containers/App/reducers.js +++ b/app/containers/App/reducers.js @@ -1,9 +1,8 @@ -'use strict'; - export const initialState = {}; export default function App(state = initialState, action) { const { type } = action; + switch (type) { default: return state; diff --git a/app/containers/App/selectors.js b/app/containers/App/selectors.js index 87001638..b1927f9b 100644 --- a/app/containers/App/selectors.js +++ b/app/containers/App/selectors.js @@ -1,5 +1,3 @@ -'use strict'; - /* eslint-disable no-unused-vars */ import { createSelector } from 'reselect'; diff --git a/app/containers/App/styles/Titlebar.js b/app/containers/App/styles/Titlebar.js index ed4d14b3..68818e2a 100644 --- a/app/containers/App/styles/Titlebar.js +++ b/app/containers/App/styles/Titlebar.js @@ -1,13 +1,24 @@ -'use strict'; +import { mixins } from '../../../styles/js'; -import { variables, mixins } from '../../../styles/js'; - -export const styles = themes => { +export const styles = (theme) => { return { root: { width: `100%`, height: 14, - ...mixins().appDragEnable - } + textAlign: 'center', + ...mixins({ theme }).appDragEnable, + ...mixins({ theme }).center, + }, + deviceInfo: { + ...mixins({ theme }).center, + width: `100%`, + textAlign: 'center', + color: theme.palette.lightText1Color, + fontWeight: 'bold', + fontSize: '12px', + }, + deviceModel: { + textTransform: 'capitalize', + }, }; }; diff --git a/app/containers/App/styles/index.js b/app/containers/App/styles/index.js index c864cf42..d23d36e6 100644 --- a/app/containers/App/styles/index.js +++ b/app/containers/App/styles/index.js @@ -1,21 +1,116 @@ -'use strict'; - import { variables, mixins } from '../../../styles/js'; +import { commonThemes } from '../../../styles/js/mixins'; + +// Styles for App/index.jsx component +export const styles = (theme) => { + return { + root: {}, + noProfileError: { + textAlign: `center`, + ...mixins({ theme }).center, + ...mixins({ theme }).absoluteCenter, + }, + }; +}; + +export const getColorPalette = () => { + const lightPrimaryColor = '#fff'; + const lightSecondaryColor = '#007af5'; + + const darkPrimaryColor = '#242424'; + const darkSecondaryColor = '#007af5'; + + const snackbarError = `#f33950`; + + return { + get light() { + return { + primary: { + main: lightPrimaryColor, + contrastText: '#000', + }, + secondary: { + main: lightSecondaryColor, + contrastText: '#fff', + }, + background: { + default: darkPrimaryColor, + paper: lightPrimaryColor, + }, + snackbar: { + error: snackbarError, + }, + btnTextColor: '#fff', + fileColor: '#000', + tableHeaderFooterBgColor: `#fbfbfb`, + lightText1Color: `rgba(0, 0, 0, 0.50)`, + fileExplorerThinLineDividerColor: `rgba(0, 0, 0, 0.12)`, + fileDrop: `rgba(0, 122, 245, 0.08)`, + disabledBgColor: `#f3f3f3`, + nativeSystemColor: `#ececec`, + contrastPrimaryMainColor: darkPrimaryColor, + }; + }, + get dark() { + return { + primary: { + main: darkPrimaryColor, + contrastText: '#fff', + }, + secondary: { + main: darkSecondaryColor, + contrastText: '#fff', + }, + background: { + default: darkPrimaryColor, + paper: darkPrimaryColor, + }, + text: { + primary: '#fff', + secondary: 'rgba(255, 255, 255, 0.65)', + disabled: 'rgba(255, 255, 255, 0.4)', + }, + snackbar: { + error: snackbarError, + }, + action: { + active: 'rgba(255, 255, 255, 0.65)', + hover: 'rgba(255, 255, 255, 0.2)', + selected: 'rgba(255, 255, 255, 0.16)', + disabled: 'rgba(255, 255, 255, 0.3)', + disabledBackground: 'rgba(255, 255, 255, 0.12)', + }, + divider: `rgba(255, 255, 255, 0.12)`, + btnTextColor: '#fff', + fileColor: '#d5d5d5', + tableHeaderFooterBgColor: `#313131`, + lightText1Color: `rgba(255, 255, 255, 0.50)`, + fileExplorerThinLineDividerColor: `rgba(255, 255, 255, .12)`, + fileDrop: `rgba(0, 122, 245, 0.08)`, + disabledBgColor: `rgba(255, 255, 255, 0.15)`, + nativeSystemColor: `#323232`, + contrastPrimaryMainColor: lightPrimaryColor, + }; + }, + }; +}; + +export const getCurrentThemePalette = (appThemeMode) => { + return getColorPalette()[appThemeMode]; +}; + +export const materialUiTheme = ({ ...args }) => { + const { appThemeMode } = args; + + const palette = getCurrentThemePalette(appThemeMode); -// eslint-disable-next-line no-unused-vars -export const theme = args => { return { palette: { - primary: { - ...variables().styles.primaryColor - }, - secondary: { - ...variables().styles.secondaryColor - } + ...palette, }, typography: { useNextVariants: true, - fontSize: variables().regularFontSize, + fontSize: variables().sizes.regularFontSize, fontFamily: [ 'Roboto', '-apple-system', @@ -26,23 +121,21 @@ export const theme = args => { 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', - '"Segoe UI Symbol"' - ].join(',') + '"Segoe UI Symbol"', + ].join(','), }, - overrides: {} - }; -}; - -// eslint-disable-next-line no-unused-vars -export const styles = args => { - // eslint-disable-line no-unused-vars - return { - root: {}, - noProfileError: { - textAlign: `center`, - ...mixins().center, - ...mixins().absoluteCenter - } + overrides: { + MuiCssBaseline: { + '@global': { + html: { + '--app-bg-color': palette.background.paper, + '--app-secondary-main-color': palette.secondary.main, + '--app-native-system-color': palette.nativeSystemColor, + ...commonThemes.noselect, + }, + }, + }, + }, }; }; diff --git a/app/containers/AppFeaturesPage/Loadable.js b/app/containers/AppFeaturesPage/Loadable.js index 9e5e081a..f2a5ba11 100644 --- a/app/containers/AppFeaturesPage/Loadable.js +++ b/app/containers/AppFeaturesPage/Loadable.js @@ -1,11 +1,9 @@ -'use strict'; - import React, { lazy, Suspense } from 'react'; import LoadingIndicator from '../../components/LoadingIndicator'; const Component = lazy(() => import('.')); -export default function() { +export default function loadable() { return ( }> diff --git a/app/containers/AppFeaturesPage/index.jsx b/app/containers/AppFeaturesPage/index.jsx index aaf02fef..cddb7bcf 100644 --- a/app/containers/AppFeaturesPage/index.jsx +++ b/app/containers/AppFeaturesPage/index.jsx @@ -1,5 +1,3 @@ -'use strict'; - import React, { Component } from 'react'; import { withStyles } from '@material-ui/core/styles'; import { Helmet } from 'react-helmet'; @@ -17,6 +15,7 @@ class AppFeaturesPage extends Component { render() { const { classes: styles } = this.props; + return (
diff --git a/app/containers/AppFeaturesPage/styles/index.js b/app/containers/AppFeaturesPage/styles/index.js index 58c29ac9..0f61a761 100644 --- a/app/containers/AppFeaturesPage/styles/index.js +++ b/app/containers/AppFeaturesPage/styles/index.js @@ -1,19 +1,15 @@ -'use strict'; - -import { variables, mixins } from '../../../styles/js'; - -export const styles = theme => ({ +export const styles = (_) => ({ root: { textAlign: `left`, padding: '10px 30px 30px 30px', maxWidth: '800px', marginRight: 'auto', marginLeft: 'auto', - overflow: 'auto' + overflow: 'auto', }, heading: {}, body: {}, marginDivider: { - height: 15 - } + height: 15, + }, }); diff --git a/app/containers/AppUpdatePage/UpdateAvailable/Loadable.js b/app/containers/AppUpdatePage/UpdateAvailable/Loadable.js index 3fdf52f7..8971ccfb 100644 --- a/app/containers/AppUpdatePage/UpdateAvailable/Loadable.js +++ b/app/containers/AppUpdatePage/UpdateAvailable/Loadable.js @@ -1,11 +1,9 @@ -'use strict'; - import React, { lazy, Suspense } from 'react'; import LoadingIndicator from '../../../components/LoadingIndicator'; const Component = lazy(() => import('.')); -export default function() { +export default function loadable() { return ( }> diff --git a/app/containers/AppUpdatePage/UpdateAvailable/index.jsx b/app/containers/AppUpdatePage/UpdateAvailable/index.jsx index 5fa63dad..943fbe1c 100644 --- a/app/containers/AppUpdatePage/UpdateAvailable/index.jsx +++ b/app/containers/AppUpdatePage/UpdateAvailable/index.jsx @@ -1,5 +1,3 @@ -'use strict'; - import { ipcRenderer } from 'electron'; import React, { Component } from 'react'; import { withStyles } from '@material-ui/core/styles'; @@ -11,13 +9,16 @@ import { styles } from './styles'; import releaseNotesStyles from './styles/release-notes.scss'; import { undefinedOrNull } from '../../../utils/funcs'; import { APP_NAME, APP_VERSION } from '../../../constants/meta'; +import { setStyle } from '../../../utils/styles'; +import { getAppThemeMode } from '../../../helpers/theme'; +import { getCurrentThemePalette } from '../../App/styles'; class AppUpdatePage extends Component { constructor(props) { super(props); this.state = { - releaseInfo: {} + releaseInfo: {}, }; this.sanitizeHtmlConfig = { @@ -44,8 +45,8 @@ class AppUpdatePage extends Component { 'br', 'div', 'caption', - 'pre' - ] + 'pre', + ], }; } @@ -56,7 +57,12 @@ class AppUpdatePage extends Component { } componentDidMount() { - document.body.setAttribute('style', 'background:#ececec !important'); + const appThemeMode = getAppThemeMode(); + const { nativeSystemColor } = getCurrentThemePalette(appThemeMode); + + setStyle(document.body, { + background: `${nativeSystemColor} !important`, + }); } componentWillUnmount() { @@ -117,7 +123,7 @@ class AppUpdatePage extends Component {
{/* eslint-enable react/no-danger */} diff --git a/app/containers/AppUpdatePage/UpdateAvailable/styles/index.js b/app/containers/AppUpdatePage/UpdateAvailable/styles/index.js index 15afddd2..c44433db 100644 --- a/app/containers/AppUpdatePage/UpdateAvailable/styles/index.js +++ b/app/containers/AppUpdatePage/UpdateAvailable/styles/index.js @@ -1,47 +1,45 @@ -'use strict'; +import { mixins } from '../../../../styles/js'; -import { variables, mixins } from '../../../../styles/js'; - -export const styles = theme => ({ +export const styles = (theme) => ({ root: { - padding: `0 25px 10px 25px` + padding: `0 25px 10px 25px`, }, loadingText: { padding: 15 }, title: { - fontWeight: `bold` + fontWeight: `bold`, }, releaseNotes: { fontWeight: `bold`, - marginTop: 10 + marginTop: 10, }, scrollContainer: { - ...mixins().center, + ...mixins({ theme }).center, marginTop: 5, border: `1px solid #000`, - background: `#fff`, + background: theme.palette.background.paper, height: 350, width: '100%', padding: 15, overflowX: 'auto', - overflowY: 'auto' + overflowY: 'auto', }, btnWrapper: { position: `absolute`, bottom: 15, - right: 24 + right: 24, }, btnPositive: { - ...mixins().btnPositive, - margin: `0 10px 0 0` + ...mixins({ theme }).btnPositive, + margin: `0 0 0 10px`, }, btnNegative: { - ...mixins().btnNegativeWhite - } + ...mixins({ theme }).btnNegativeWhite, + }, }); diff --git a/app/containers/AppUpdatePage/UpdateProgress/Loadable.js b/app/containers/AppUpdatePage/UpdateProgress/Loadable.js index 3fdf52f7..8971ccfb 100644 --- a/app/containers/AppUpdatePage/UpdateProgress/Loadable.js +++ b/app/containers/AppUpdatePage/UpdateProgress/Loadable.js @@ -1,11 +1,9 @@ -'use strict'; - import React, { lazy, Suspense } from 'react'; import LoadingIndicator from '../../../components/LoadingIndicator'; const Component = lazy(() => import('.')); -export default function() { +export default function loadable() { return ( }> diff --git a/app/containers/AppUpdatePage/UpdateProgress/index.jsx b/app/containers/AppUpdatePage/UpdateProgress/index.jsx index 6579f277..0e01a049 100644 --- a/app/containers/AppUpdatePage/UpdateProgress/index.jsx +++ b/app/containers/AppUpdatePage/UpdateProgress/index.jsx @@ -1,5 +1,3 @@ -'use strict'; - import { ipcRenderer } from 'electron'; import React, { Component } from 'react'; import { withStyles } from '@material-ui/core/styles'; @@ -15,11 +13,11 @@ class ProgressbarPage extends Component { progressTitle: `Progress...`, progressBodyText: `Progress...`, value: 0, - variant: `indeterminate` + variant: `indeterminate`, }; this.state = { - ...this.initialState + ...this.initialState, }; } @@ -39,6 +37,7 @@ class ProgressbarPage extends Component { render() { const { classes: styles } = this.props; const { progressTitle, progressBodyText, value, variant } = this.state; + return (
diff --git a/app/containers/AppUpdatePage/UpdateProgress/styles/index.js b/app/containers/AppUpdatePage/UpdateProgress/styles/index.js index 77d162d0..d5a1bc22 100644 --- a/app/containers/AppUpdatePage/UpdateProgress/styles/index.js +++ b/app/containers/AppUpdatePage/UpdateProgress/styles/index.js @@ -1,19 +1,17 @@ -'use strict'; +import { mixins } from '../../../../styles/js'; -import { variables, mixins } from '../../../../styles/js'; - -export const styles = theme => ({ +export const styles = (theme) => ({ root: { textAlign: `left`, - ...mixins().center, + ...mixins({ theme }).center, width: 500, - marginTop: 10 + marginTop: 10, }, progressBodyText: { - marginBottom: 10 + marginBottom: 10, }, progressTitle: { fontWeight: 'bold', - marginBottom: 10 - } + marginBottom: 10, + }, }); diff --git a/app/containers/ErrorBoundary/components/GenerateErrorReport.jsx b/app/containers/ErrorBoundary/components/GenerateErrorReport.jsx index 067ad5d2..e886dd00 100644 --- a/app/containers/ErrorBoundary/components/GenerateErrorReport.jsx +++ b/app/containers/ErrorBoundary/components/GenerateErrorReport.jsx @@ -1,25 +1,30 @@ -'use strict'; - -import React, { Component } from 'react'; -import { shell, remote } from 'electron'; +import React, { PureComponent } from 'react'; +import { shell, ipcRenderer } from 'electron'; import path from 'path'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { withStyles } from '@material-ui/core/styles'; -import { log } from '@Log'; import { styles } from '../styles/GenerateErrorReport'; -import { baseName, PATHS } from '../../../utils/paths'; -import { promisifiedRimraf, mtpVerboseReport } from '../../../api/sys'; -import { fileExistsSync } from '../../../api/sys/fileOps'; +import { PATHS } from '../../../constants/paths'; +import { fileExistsSync } from '../../../helpers/fileOps'; import { AUTHOR_EMAIL } from '../../../constants/meta'; import { throwAlert } from '../../Alerts/actions'; import { mailToInstructions as _mailToInstructions, reportGenerateError, - mailTo + mailTo, } from '../../../templates/generateErrorReport'; import { compressFile } from '../../../utils/gzip'; import GenerateErrorReportBody from './GenerateErrorReportBody'; +import { baseName } from '../../../utils/files'; +import { log } from '../../../utils/log'; +import { getMainWindowRendererProcess } from '../../../helpers/windowHelper'; +import fileExplorerController from '../../../data/file-explorer/controllers/FileExplorerController'; +import { DEVICE_TYPE } from '../../../enums'; +import { getRemoteWindow } from '../../../helpers/remoteWindowHelpers'; +import { IpcEvents } from '../../../services/ipc-events/IpcEventType'; + +const remote = getRemoteWindow(); const { logFile } = PATHS; const { getPath } = remote.app; @@ -30,51 +35,106 @@ const logFileZippedPath = path.resolve( ); const mailToInstructions = _mailToInstructions(zippedLogFileBaseName); -class GenerateErrorReport extends Component { - compressLog = () => { +class GenerateErrorReport extends PureComponent { + constructor() { + super(); + + this.mainWindowRendererProcess = getMainWindowRendererProcess(); + } + + componentWillUnmount() { + ipcRenderer.removeListener( + IpcEvents.REPORT_BUGS_DISPOSE_MTP_REPLY_FROM_MAIN, + this._reportBugsDisposeMtpReplyEvent + ); + } + + compressLog = async () => { try { - compressFile(logFile, logFileZippedPath); + await compressFile(logFile, logFileZippedPath); } catch (e) { log.error(e, `GenerateErrorReport -> compressLog`); } }; + _reportBugsDisposeMtpReplyEvent = async (_, { error }) => { + await this.startGeneratingReport({ error }); + }; + _handleGenerateErrorLogs = async () => { try { - const { actionCreateThrowError } = this.props; + const { isReportBugsPage } = this.props; - await mtpVerboseReport(); + // if the generate button click action originated from the 'report bugs' page then use ipc channels to communicate + // else use direct method click + if (isReportBugsPage) { + this.mainWindowRendererProcess.webContents.send( + IpcEvents.REPORT_BUGS_DISPOSE_MTP, + { logFileZippedPath } + ); - const { error } = await promisifiedRimraf(logFileZippedPath); + ipcRenderer.once( + IpcEvents.REPORT_BUGS_DISPOSE_MTP_REPLY_FROM_MAIN, + this._reportBugsDisposeMtpReplyEvent + ); - if (error) { - actionCreateThrowError({ - message: reportGenerateError - }); - return null; + return; } - this.compressLog(); + // direct button click action if the generate button is within the error boundary + await fileExplorerController.dispose({ deviceType: DEVICE_TYPE.mtp }); - if (!fileExistsSync(logFileZippedPath)) { - actionCreateThrowError({ - message: reportGenerateError - }); - return null; - } + await fileExplorerController.fetchDebugReport({ + deviceType: DEVICE_TYPE.mtp, + }); - if (window) { - window.location.href = `${mailTo} ${mailToInstructions}`; - } + const { error } = await fileExplorerController.deleteFiles({ + deviceType: DEVICE_TYPE.local, + fileList: [logFileZippedPath], + storageId: null, + }); - shell.showItemInFolder(logFileZippedPath); + await this.startGeneratingReport({ error }); } catch (e) { log.error(e, `GenerateErrorReport -> generateErrorLogs`); } }; + startGeneratingReport = async ({ error }) => { + const { actionCreateThrowError } = this.props; + + if (error) { + actionCreateThrowError({ + message: reportGenerateError, + }); + + log.error(error, reportGenerateError); + + return null; + } + + await this.compressLog(); + + if (!fileExistsSync(logFileZippedPath)) { + actionCreateThrowError({ + message: reportGenerateError, + }); + + log.error(`${logFileZippedPath} doesn't exist`, reportGenerateError); + + return null; + } + + if (window) { + window.location.href = `${mailTo} ${mailToInstructions}`; + } + + shell.showItemInFolder(logFileZippedPath); + }; + render() { const { classes: styles } = this.props; + return ( +const mapDispatchToProps = (dispatch, __) => bindActionCreators( { - actionCreateThrowError: ({ ...args }) => (_, getState) => { - dispatch(throwAlert({ ...args })); - } + actionCreateThrowError: + ({ ...args }) => + (_, __) => { + dispatch(throwAlert({ ...args })); + }, }, dispatch ); -const mapStateToProps = (state, props) => { +const mapStateToProps = (_, __) => { return {}; }; diff --git a/app/containers/ErrorBoundary/components/GenerateErrorReportBody.jsx b/app/containers/ErrorBoundary/components/GenerateErrorReportBody.jsx index 44650049..a0097ccd 100644 --- a/app/containers/ErrorBoundary/components/GenerateErrorReportBody.jsx +++ b/app/containers/ErrorBoundary/components/GenerateErrorReportBody.jsx @@ -1,5 +1,3 @@ -'use strict'; - import React, { PureComponent, Fragment } from 'react'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; @@ -12,7 +10,8 @@ import SendIcon from '@material-ui/icons/Send'; import UsbIcon from '@material-ui/icons/Usb'; import EmailIcon from '@material-ui/icons/Email'; import Button from '@material-ui/core/Button'; -import { DEVICES_LABEL, DEVICES_TYPE_CONST } from '../../../constants'; +import { DEVICES_LABEL } from '../../../constants'; +import { DEVICE_TYPE } from '../../../enums'; export default class GenerateErrorReportBody extends PureComponent { render() { @@ -22,8 +21,9 @@ export default class GenerateErrorReportBody extends PureComponent { mailTo, mailToInstructions, AUTHOR_EMAIL, - onGenerateErrorLogs + onGenerateErrorLogs, } = this.props; + return ( @@ -33,9 +33,9 @@ export default class GenerateErrorReportBody extends PureComponent { diff --git a/app/containers/ErrorBoundary/index.jsx b/app/containers/ErrorBoundary/index.jsx index 72e36aa7..d2b7fd03 100644 --- a/app/containers/ErrorBoundary/index.jsx +++ b/app/containers/ErrorBoundary/index.jsx @@ -1,33 +1,34 @@ -'use strict'; - import React, { Component } from 'react'; -import { remote } from 'electron'; import { withStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; -import { EOL } from 'os'; -import { log } from '@Log'; import { styles } from './styles'; import { imgsrc } from '../../utils/imgsrc'; import GenerateErrorReport from './components/GenerateErrorReport'; +import { log } from '../../utils/log'; +import { getRemoteWindow } from '../../helpers/remoteWindowHelpers'; + +const remote = getRemoteWindow(); class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { - errorInfo: null + errorInfo: null, }; } componentDidCatch(error, errorInfo) { this.setState({ - errorInfo + errorInfo, }); - const _errorInfo = JSON.stringify(errorInfo); + + log.doLog(error, `ErrorBoundary.componentDidCatch.error`, null, true); log.doLog( - `Error boundary log capture:${EOL}${error.toString()}${EOL}${_errorInfo}`, - true, - error + errorInfo?.componentStack, + `ErrorBoundary.componentDidCatch.errorInfo`, + null, + true ); } @@ -42,6 +43,7 @@ class ErrorBoundary extends Component { render() { const { classes: styles, children } = this.props; const { errorInfo } = this.state; + if (errorInfo) { return (
diff --git a/app/containers/ErrorBoundary/styles/GenerateErrorReport.js b/app/containers/ErrorBoundary/styles/GenerateErrorReport.js index 9da2302d..98804d8a 100644 --- a/app/containers/ErrorBoundary/styles/GenerateErrorReport.js +++ b/app/containers/ErrorBoundary/styles/GenerateErrorReport.js @@ -1,32 +1,30 @@ -'use strict'; +import { mixins } from '../../../styles/js'; -import { variables, mixins } from '../../../styles/js'; - -export const styles = theme => ({ +export const styles = (theme) => ({ subHeading: { - ...mixins().noDrag, - ...mixins().noselect, - marginTop: 15 + ...mixins({ theme }).noDrag, + ...mixins({ theme }).noselect, + marginTop: 15, }, instructions: { listStyle: `none`, - color: variables().styles.textLightColor, + color: theme.palette.lightText1Color, lineHeight: '24px', marginTop: 15, paddingLeft: 0, - marginBottom: 15 + marginBottom: 15, }, generateLogsBtnWrapper: {}, generateLogsBtn: { marginTop: 0, - ...mixins().btnPositive + ...mixins({ theme }).btnPositive, }, emailIdWrapper: { - color: variables().styles.textLightColor, - marginTop: 15 + color: theme.palette.lightText1Color, + marginTop: 15, }, emailId: { marginLeft: 16, - fontWeight: `bold` - } + fontWeight: `bold`, + }, }); diff --git a/app/containers/ErrorBoundary/styles/index.js b/app/containers/ErrorBoundary/styles/index.js index 895cc7b1..ac178de4 100644 --- a/app/containers/ErrorBoundary/styles/index.js +++ b/app/containers/ErrorBoundary/styles/index.js @@ -1,29 +1,27 @@ -'use strict'; +import { mixins } from '../../../styles/js'; -import { variables, mixins } from '../../../styles/js'; - -export const styles = theme => ({ +export const styles = (theme) => ({ root: { textAlign: `center`, - ...mixins().center, - ...mixins().absoluteCenter + ...mixins({ theme }).center, + ...mixins({ theme }).absoluteCenter, }, bugImg: { - ...mixins().noDrag, + ...mixins({ theme }).noDrag, height: `auto`, - width: 150 + width: 150, }, headings: { - ...mixins().noDrag, - ...mixins().noselect, - marginTop: 15 + ...mixins({ theme }).noDrag, + ...mixins({ theme }).noselect, + marginTop: 15, }, subHeading: { - ...mixins().noDrag, - ...mixins().noselect, - marginTop: 15 + ...mixins({ theme }).noDrag, + ...mixins({ theme }).noselect, + marginTop: 15, }, goBackBtn: { - marginTop: 5 - } + marginTop: 5, + }, }); diff --git a/app/containers/HelpFaqsPage/Loadable.js b/app/containers/HelpFaqsPage/Loadable.js new file mode 100644 index 00000000..ff61d07a --- /dev/null +++ b/app/containers/HelpFaqsPage/Loadable.js @@ -0,0 +1,12 @@ +import React, { lazy, Suspense } from 'react'; +import LoadingIndicator from '../../components/LoadingIndicator'; + +const Component = lazy(() => import('.')); + +export default function loadable({ ...props }) { + return ( + }> + + + ); +} diff --git a/app/containers/HelpFaqsPage/components/HelpPhoneNotRecognized.jsx b/app/containers/HelpFaqsPage/components/HelpPhoneNotRecognized.jsx new file mode 100644 index 00000000..79409fd2 --- /dev/null +++ b/app/containers/HelpFaqsPage/components/HelpPhoneNotRecognized.jsx @@ -0,0 +1,922 @@ +import React, { PureComponent } from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import DeleteIcon from '@material-ui/icons/Delete'; +import CloseIcon from '@material-ui/icons/Close'; +import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; +import ToggleOffIcon from '@material-ui/icons/ToggleOff'; +import CheckIcon from '@material-ui/icons/Check'; +import ListAltIcon from '@material-ui/icons/ListAlt'; +import SettingsIcon from '@material-ui/icons/Settings'; +import ThumbUpIcon from '@material-ui/icons/ThumbUp'; +import FolderSpecialIcon from '@material-ui/icons/FolderSpecial'; +import CachedIcon from '@material-ui/icons/Cached'; +import UsbIcon from '@material-ui/icons/Usb'; +import TouchAppIcon from '@material-ui/icons/TouchApp'; +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked'; +import LockOpenIcon from '@material-ui/icons/Lock'; +import Accordion from '@material-ui/core/Accordion'; +import AccordionSummary from '@material-ui/core/AccordionSummary'; +import AccordionDetails from '@material-ui/core/AccordionDetails'; +import Typography from '@material-ui/core/Typography'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import PowerIcon from '@material-ui/icons/Power'; +import ReplayIcon from '@material-ui/icons/Replay'; +import SystemUpdate from '@material-ui/icons/SystemUpdate'; +import { styles } from '../styles/HelpPhoneNotRecognized'; +import { openExternalUrl } from '../../../utils/url'; +import { + APP_GITHUB_ISSUES_URL, + APP_NAME, + APP_VERSION, + AUTHOR_EMAIL, +} from '../../../constants/meta'; +import { analyticsService } from '../../../services/analytics'; +import { EVENT_TYPE } from '../../../enums/events'; +import { + BUY_ME_A_COFFEE_URL, + DELETE_KEIS_SMARTSWITCH_URL, + DEVICES_LABEL, + SUPPORT_PAYPAL_URL, +} from '../../../constants'; +import { DEVICE_TYPE, MTP_MODE } from '../../../enums'; +import { + localErrorDictionary, + mtpErrors, +} from '../../../helpers/processBufferOutput'; +import { MTP_ERROR } from '../../../enums/mtpError'; +import { imgsrc } from '../../../utils/imgsrc'; +import { helpPhoneNotConnecting } from '../../../templates/fileExplorer'; +import { isKalamModeSupported } from '../../../helpers/binaries'; + +const hotplugSettingText = `Check if 'Enable auto device detection (USB Hotplug)' is enabled under Settings > General Tab`; +const deviceLabel = DEVICES_LABEL[DEVICE_TYPE.mtp]; + +class HelpPhoneNotRecognized extends PureComponent { + _handleGithubThreadTap = (events) => { + openExternalUrl(`${APP_GITHUB_ISSUES_URL}8`, events); + + analyticsService.sendEvent( + EVENT_TYPE.MTP_HELP_PHONE_NOT_CONNECTED_GITHUB_THREAD_TAP, + {} + ); + }; + + RenderFileTransfer = () => { + const { classes: styles } = this.props; + + return ( + <> + + + + + + } + /> + + + + + + + } + /> + + + ); + }; + + RenderRefreshButtonIsStuck = () => { + const { classes: styles } = this.props; + + const { RenderFileTransfer } = this; + + return ( + <> + + + + + + + + + + + + + + + + + + + + } + /> + + + + + + + } + /> + + + + + + + + + + + + + + + + + + + + + + + } + /> + + + ); + }; + + RenderBasicConnection = ({ + showUnplugPhone = true, + showUnlockPhone = true, + }) => { + const { RenderFileTransfer } = this; + + return ( + <> + {showUnlockPhone && ( + + + + + + + )} + + {showUnplugPhone && ( + + + + + + + )} + + + + + + + + + + + + + + + + + ); + }; + + render() { + const { classes: styles, showPhoneNotRecognizedNote } = this.props; + const { RenderBasicConnection, RenderRefreshButtonIsStuck } = this; + const isKalamModeDisabled = !isKalamModeSupported(); + + return ( +
+ + {showPhoneNotRecognizedNote && ( + <> + + {APP_NAME} was a project that I started to + solve a problem that was so personal to me. But I always knew, + that there's a community, whose facing the same problem as + I did. + + + I wasn't wrong, I guess. Now, we are a strong community + with users from over  + 180 countries. It's overwhelming to see + the response that I have received from all of you, not just + appreciating the app, but also giving me suggestions and + feedback to improve it. + + + As they say, you build for the community and learn from it. + + + I read each and every message that you send and am constantly + working to improve the app based on your feedback. Keep sending + more of those :) + + + Some of you have been telling me that there are issues with + connecting certain mobile phones (mostly Samsung) to{' '} + {APP_NAME}. I have been working hard to fix this issue by + migrating the existing MTP Kernel to a better one. + + + You may reach out to me at  + + {AUTHOR_EMAIL} + +  or check out this  + thread +  on GitHub for tracking the same,  + + to collaborate and make this community bigger and stronger + + ! + + + If you'd like to support my work or buy me up a cup of + coffee, you can contribute via Paypal:  + { + openExternalUrl(SUPPORT_PAYPAL_URL, events); + }} + > + {SUPPORT_PAYPAL_URL} + +  or Buy me a coffee:  + { + openExternalUrl(BUY_ME_A_COFFEE_URL, events); + }} + > + {BUY_ME_A_COFFEE_URL} + + . + + + FAQs + + + )} + + {isKalamModeDisabled && ( + + {/* <----- Kalam Mode is disabed -----> */} + + }> + + {`Upgrade you mac's OS version for better app experience`} + + + + + + + + + + + + + + + + + + + + + )} + + + {/* <----- my device is not connecting -----> */} + + }> + + {`My ${deviceLabel.toLowerCase()} is not connecting`} + + + + + + + + + + {`Uninstall 'Android File Transfer' by Google if it + keeps popping up everytime you connect your + Android device. The most recent versions of Google + drive and Dropbox are known to interfere with ${APP_NAME}. Completely quiting these apps may fix + this issue. `} + { + openExternalUrl( + 'https://github.com/ganeshrvel/openmtp/issues/276', + events + ); + }} + > + Read more... + + + } + /> + + + + + + + General Tab`} + /> + + + + + + + + {/* <----- Google drive is interfering with OpenMTP-----> */} + + }> + + {`I have Google drive installed on my ${ + DEVICES_LABEL[DEVICE_TYPE.local] + }`} + + + + + + + + + + + } + /> + + + + + + + + {/* <----- Dropbox is interfering with OpenMTP-----> */} + + }> + + {`I have Dropbox installed on my ${ + DEVICES_LABEL[DEVICE_TYPE.local] + }`} + + + + + + + + + + + + + + + + + + {/* <----- The app goes blank while trying to connect a Samsung device -----> */} + + + }> + + {`The app goes blank while trying to connect a Samsung device`} + + + + + + + + + { + openExternalUrl(DELETE_KEIS_SMARTSWITCH_URL, events); + }} + > + How to remove Samsung SmartSwitch and drivers from your + MacBook + + } + /> + + + + + + + + + + + + + + + {/* <----- i keep seeing setting up device -----> */} + + + }> + + {`I keep seeing "${mtpErrors[[MTP_ERROR.ErrorDeviceSetup]]}"`} + + + + + + + + + + {/* <----- i keep seeing allow storage access -----> */} + + }> + + {`I keep seeing "${ + mtpErrors[[MTP_ERROR.ErrorAllowStorageAccess]] + }"`} + + + + + + + + + + + + + + + + + } + /> + + + + + + + + + + + + + + + + + + + {/* <----- Allow access to the device data" multiple times -----> */} + + }> + + {`I am prompted to "Allow access to the device data" multiple times`} + + + + + + + + + + + {/* <----- refresh button is stuck -----> */} + + }> + + {`Refresh button is stuck`} + + + + + + + + + + + {/* <----- i keep seeing multiple devices error -----> */} + + }> + + {`I keep seeing "${ + mtpErrors[[MTP_ERROR.ErrorMultipleDevice]] + }"`} + + + + + + + + + + + + + + + + + + + + + + + + {/* <----- phone gets disconnected everytime screen goes into sleep -----> */} + + }> + + {`My ${deviceLabel.toLowerCase()} gets disconnected everytime the display goes into sleep`} + + + + + + + + + + + + + + + + + + + + + + + + Display > Sleep and set it as 30 minutes or whatever is the highest`} + secondary={ + Sleep settings + } + /> + + + + + + + + {/* <----- i keep seeing quit android file transfer error -----> */} + + }> + + {`I keep seeing "Quit 'Android File Transfer' app (by Google) and Refresh"`} + + + + + + + + + + + + + + + + + + {/* <----- my phone is still not detected -----> */} + + }> + + {`My phone is still not connecting`} + + + + + + + + + + { + openExternalUrl(DELETE_KEIS_SMARTSWITCH_URL, events); + }} + > + How to remove Samsung SmartSwitch and drivers from your + MacBook + + } + /> + + + + + + + Tab > Change the "MTP Mode"`} + /> + + + + + + + + {/* <----- Operation not permitted error -----> */} + + }> + + {`I keep seeing "${localErrorDictionary.noPerm}" error whenever I try to open a folder in the Local Disk pane`} + + + + + + + + + + + + + + + + + + } + /> + + + + + + + + + + + + "Security and Privacy" > "Privacy Tab"`} + secondary={`Tap on the "Click the lock to make changes" button and authenticate yourself`} + /> + + + + + + + + + + + + + + + } + /> + + + + + + {/* <----- Full disk access -----> */} + + }> + + {`I am still being denied access to some of the folders in the Local Disk pane`} + + + + + + + + + + + + + + + + + + + + + + "Security and Privacy" > "Privacy Tab"`} + secondary={`Tap on the "Click the lock to make changes" button and authenticate yourself`} + /> + + + + + + + + } + /> + + + + + + + + } + /> + + + + + +
+ ); + } +} + +export default withStyles(styles)(HelpPhoneNotRecognized); diff --git a/app/containers/HelpFaqsPage/index.jsx b/app/containers/HelpFaqsPage/index.jsx new file mode 100644 index 00000000..c5748ca0 --- /dev/null +++ b/app/containers/HelpFaqsPage/index.jsx @@ -0,0 +1,55 @@ +import React, { Component } from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { Helmet } from 'react-helmet'; +import { APP_TITLE } from '../../constants/meta'; +import { resetOverFlowY } from '../../utils/styleResets'; +import { styles } from './styles'; +import { + FAQS_PAGE_TITLE, + HELP_PHONE_IS_NOT_CONNECTING, +} from '../../templates/helpFaqsPage'; +import HelpPhoneNotRecognized from './components/HelpPhoneNotRecognized'; + +class FaqsPage extends Component { + componentWillMount() { + resetOverFlowY(); + } + + render() { + const { classes: styles, showPhoneNotRecognizedNote } = this.props; + + const title = showPhoneNotRecognizedNote + ? HELP_PHONE_IS_NOT_CONNECTING + : FAQS_PAGE_TITLE; + + return ( +
+ + {title} + + + {title} + +
+ +
+
+ ); + } +} + +const mapDispatchToProps = (dispatch, __) => bindActionCreators({}, dispatch); + +const mapStateToProps = (_, __) => { + return {}; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(FaqsPage)); diff --git a/app/containers/HelpFaqsPage/styles/HelpPhoneNotRecognized.js b/app/containers/HelpFaqsPage/styles/HelpPhoneNotRecognized.js new file mode 100644 index 00000000..25ab7080 --- /dev/null +++ b/app/containers/HelpFaqsPage/styles/HelpPhoneNotRecognized.js @@ -0,0 +1,19 @@ +import mixins from '../../../styles/js/mixins'; + +export const styles = (theme) => ({ + root: {}, + expansionRoot: { + background: theme.palette.tableHeaderFooterBgColor, + }, + heading: { + fontWeight: 600, + }, + imagePlaceholder: { + width: 200, + height: 'auto', + }, + imgWrapper: { + ...mixins({ theme }).center, + textAlign: 'center', + }, +}); diff --git a/app/containers/HelpFaqsPage/styles/index.js b/app/containers/HelpFaqsPage/styles/index.js new file mode 100644 index 00000000..b745cd00 --- /dev/null +++ b/app/containers/HelpFaqsPage/styles/index.js @@ -0,0 +1,17 @@ +export const styles = (_) => ({ + root: { + textAlign: `left`, + padding: '30px 30px 30px 30px', + maxWidth: 950, + marginRight: 'auto', + marginLeft: 'auto', + overflow: 'auto', + }, + a: { + fontWeight: `bold`, + }, + heading: {}, + body: { + paddingTop: 25, + }, +}); diff --git a/app/containers/HomePage/Loadable.js b/app/containers/HomePage/Loadable.js index 9e5e081a..f2a5ba11 100644 --- a/app/containers/HomePage/Loadable.js +++ b/app/containers/HomePage/Loadable.js @@ -1,11 +1,9 @@ -'use strict'; - import React, { lazy, Suspense } from 'react'; import LoadingIndicator from '../../components/LoadingIndicator'; const Component = lazy(() => import('.')); -export default function() { +export default function loadable() { return ( }> diff --git a/app/containers/HomePage/actions.js b/app/containers/HomePage/actions.js index cfe14859..83ea13e5 100644 --- a/app/containers/HomePage/actions.js +++ b/app/containers/HomePage/actions.js @@ -1,19 +1,17 @@ -'use strict'; - -import { log } from '@Log'; -import prefixer from '../../utils/reducerPrefixer'; -import { - asyncReadLocalDir, - asyncReadMtpDir, - fetchMtpStorageOptions -} from '../../api/sys'; +import prefixer from '../../helpers/reducerPrefixer'; import { throwAlert } from '../Alerts/actions'; -import { DEVICES_TYPE_CONST } from '../../constants'; import { processMtpBuffer, - processLocalBuffer -} from '../../utils/processBufferOutput'; -import { isArraysEqual } from '../../utils/funcs'; + processLocalBuffer, +} from '../../helpers/processBufferOutput'; +import { isArraysEqual, isEmpty, undefinedOrNull } from '../../utils/funcs'; +import { DEVICE_TYPE, MTP_MODE } from '../../enums'; +import { log } from '../../utils/log'; +import fileExplorerController from '../../data/file-explorer/controllers/FileExplorerController'; +import { checkIf } from '../../utils/checkIf'; +import { MTP_ERROR } from '../../enums/mtpError'; +import { DEVICES_DEFAULT_PATH } from '../../constants'; +import { analyticsService } from '../../services/analytics'; const prefix = '@@Home'; const actionTypesList = [ @@ -21,7 +19,7 @@ const actionTypesList = [ 'SET_CURRENT_BROWSE_PATH', 'SET_SORTING_DIR_LISTS', 'SET_SELECTED_DIR_LISTS', - 'FETCH_DIR_LIST', + 'LIST_DIRECTORY', 'SET_MTP_ERRORS', 'SET_MTP_STATUS', 'CHANGE_MTP_STORAGE', @@ -29,7 +27,7 @@ const actionTypesList = [ 'SET_FILE_TRANSFER_PROGRESS', 'CLEAR_FILE_TRANSFER', 'SET_FILES_DRAG', - 'CLEAR_FILES_DRAG' + 'CLEAR_FILES_DRAG', ]; export const actionTypes = prefixer(prefix, actionTypesList); @@ -38,8 +36,8 @@ export function setFocussedFileExplorerDeviceType(data) { return { type: actionTypes.SET_FOCUSSED_FILE_EXPLORER_DEVICE_TYPE, payload: { - ...data - } + ...data, + }, }; } @@ -48,18 +46,18 @@ export function setSortingDirLists(data, deviceType) { type: actionTypes.SET_SORTING_DIR_LISTS, deviceType, payload: { - ...data - } + ...data, + }, }; } -export function setSelectedDirLists(data, deviceType) { +export function actionSetSelectedDirLists(data, deviceType) { return { type: actionTypes.SET_SELECTED_DIR_LISTS, deviceType, payload: { - ...data - } + ...data, + }, }; } @@ -67,76 +65,423 @@ export function setCurrentBrowsePath(path, deviceType) { return { type: actionTypes.SET_CURRENT_BROWSE_PATH, deviceType, - payload: path + payload: path, }; } -function _fetchDirList(data, deviceType) { +function actionListDirectory(data, deviceType, _) { return { - type: actionTypes.FETCH_DIR_LIST, + type: actionTypes.LIST_DIRECTORY, deviceType, payload: { - nodes: data || [], - isLoaded: true - } + nodes: data ?? [], + isLoaded: true, + }, }; } -export function getMtpStoragesListSelected(state) { - if ( - typeof Object.keys(state.mtpStoragesList).length === 'undefined' || - Object.keys(state.mtpStoragesList).length < 1 - ) { +export function getSelectedStorageIdFromState(state) { + const selectedStorage = getSelectedStorage(state.mtpStoragesList); + + if (isEmpty(selectedStorage?.id)) { + return null; + } + + return parseInt(selectedStorage.id, 10); +} + +export function getSelectedStorage(mtpStoragesList) { + checkIf(mtpStoragesList, 'object'); + + if (isEmpty(mtpStoragesList)) { return null; } - const { mtpStoragesList } = state; const mtpStoragesListKeys = Object.keys(mtpStoragesList); for (let i = 0; i < mtpStoragesListKeys.length; i += 1) { const itemKey = mtpStoragesListKeys[i]; + if (mtpStoragesList[itemKey].selected) { - return itemKey; + return { id: itemKey, data: mtpStoragesList[itemKey] }; } } return null; } -export function setMtpStorageOptions( - { ...fetchDirArgs }, - deviceType, - { ...deviceChangeCheck }, +export function initializeMtp( + { + filePath, + ignoreHidden, + changeLegacyMtpStorageOnlyOnDeviceChange, + deviceType, + }, getState ) { - return async dispatch => { + checkIf(deviceType, 'string'); + checkIf(filePath, 'string'); + checkIf(ignoreHidden, 'boolean'); + checkIf(changeLegacyMtpStorageOnlyOnDeviceChange, 'boolean'); + checkIf(getState, 'function'); + + const { mtpStoragesList } = getState().Home; + const { mtpMode } = getState().Settings; + + return async (dispatch) => { + try { + switch (mtpMode) { + case MTP_MODE.kalam: + return dispatch( + initKalamMtp( + { + filePath, + ignoreHidden, + deviceType, + }, + getState + ) + ); + + case MTP_MODE.legacy: + return dispatch( + initLegacyMtp( + { + filePath, + ignoreHidden, + deviceType, + mtpStoragesList, + changeLegacyMtpStorageOnlyOnDeviceChange, + }, + getState + ) + ); + + default: + return; + } + } catch (e) { + log.error(e); + } + }; +} + +export function disposeMtp({ deviceType, onSuccess, onError }, getState) { + return async (dispatch) => { + const { mtpMode } = getState().Settings; + + checkIf(deviceType, 'string'); + checkIf(onSuccess, 'function'); + checkIf(onError, 'function'); + checkIf(mtpMode, 'string'); + + try { + switch (mtpMode) { + case MTP_MODE.kalam: + // eslint-disable-next-line no-case-declarations + const { error, stderr, data } = await fileExplorerController.dispose({ + deviceType, + }); + + await new Promise((resolve) => { + dispatch( + churnMtpBuffer({ + deviceType, + error, + stderr, + data, + mtpMode, + onSuccess: ({ _, __, data }) => { + dispatch( + actionSetMtpStatus({ info: {}, isAvailable: false }) + ); + dispatch(actionListDirectory([], deviceType)); + dispatch( + actionSetSelectedDirLists({ selected: [] }, deviceType) + ); + dispatch(actionChangeMtpStorage({})); + + const _return = { + error: null, + stderr: null, + data, + }; + + onSuccess(_return); + + return resolve(_return); + }, + onError: ({ _, __, ___ }) => { + const _return = { + error, + stderr, + data: null, + }; + + onError(_return); + + return resolve(_return); + }, + }) + ); + }); + + return; + + default: + return; + } + } catch (e) { + log.error(e); + } + }; +} + +function initKalamMtp({ filePath, ignoreHidden, deviceType }, getState) { + return async (dispatch) => { + checkIf(filePath, 'string'); + checkIf(ignoreHidden, 'boolean'); + checkIf(deviceType, 'string'); + try { - const { error, stderr, data } = await fetchMtpStorageOptions(); + const { mtpMode } = getState().Settings; + const { mtpDevice: preInitMtpDevice } = getState().Home; + + checkIf(preInitMtpDevice, 'object'); + + dispatch( + actionSetMtpStatus({ + isLoading: true, + }) + ); + + // if the app was expecting the user to allow access to mtp storage + // then don't reinitialize mtp + const { error, stderr, data } = await fileExplorerController.initialize({ + deviceType, + }); + + await new Promise((resolve) => { + dispatch( + churnMtpBuffer({ + deviceType, + error, + stderr, + data, + mtpMode, + onSuccess: ({ _, __, data }) => { + dispatch(actionSetMtpStatus({ info: data })); + + analyticsService.sendDeviceInfo(); + + return resolve({ + error: null, + stderr: null, + data, + }); + }, + onError: ({ _, __, ___ }) => { + return resolve({ + error, + stderr, + data: null, + }); + }, + }) + ); + }); + + const { mtpDevice: postInitMtpDevice } = getState().Home; + + checkIf(postInitMtpDevice, 'object'); + + if (!postInitMtpDevice.isAvailable) { + return; + } + + let _filePath = filePath; + + if ( + !undefinedOrNull(preInitMtpDevice?.info?.mtpDeviceInfo?.SerialNumber) && + !undefinedOrNull( + postInitMtpDevice?.info?.mtpDeviceInfo?.SerialNumber + ) && + preInitMtpDevice?.info?.mtpDeviceInfo?.SerialNumber !== + postInitMtpDevice?.info?.mtpDeviceInfo?.SerialNumber + ) { + _filePath = DEVICES_DEFAULT_PATH[deviceType]; + dispatch(actionChangeMtpStorage({})); + } + + dispatch( + actionSetMtpStatus({ + isLoading: true, + }) + ); + + await new Promise((resolve) => { + dispatch( + listKalamStorages( + { + filePath, + ignoreHidden, + deviceType, + onSuccess: () => { + resolve(); + }, + onError: () => { + resolve(); + }, + }, + getState + ) + ); + }); + + const { mtpDevice: postStorageAccessMtpDevice } = getState().Home; + + checkIf(postStorageAccessMtpDevice, 'object'); + + if (!postStorageAccessMtpDevice.isAvailable) { + return; + } + dispatch( - processMtpOutput({ + actionSetMtpStatus({ + isLoading: true, + }) + ); + + dispatch( + reloadDirList( + { filePath: _filePath, ignoreHidden, deviceType }, + getState + ) + ); + } catch (e) { + log.error(e); + } + }; +} + +function listKalamStorages( + { filePath, ignoreHidden, deviceType, onSuccess, onError }, + getState +) { + return async (dispatch) => { + checkIf(filePath, 'string'); + checkIf(ignoreHidden, 'boolean'); + checkIf(deviceType, 'string'); + checkIf(onSuccess, 'function'); + checkIf(onError, 'function'); + + try { + const { mtpMode } = getState().Settings; + + checkIf(mtpMode, 'string'); + + const { error, stderr, data } = await fileExplorerController.listStorages( + { + deviceType, + } + ); + + return new Promise((resolve) => { + dispatch( + churnMtpBuffer({ + deviceType, + error, + stderr, + data, + mtpMode, + onSuccess: async () => { + dispatch(actionChangeMtpStorage({ ...data })); + + onSuccess(); + + return resolve({ + error: null, + stderr: null, + data, + }); + }, + onError: async () => { + onError(); + + return resolve({ + error, + stderr, + data: null, + }); + }, + }) + ); + }); + } catch (e) { + log.error(e); + } + }; +} + +function initLegacyMtp( + { + filePath, + ignoreHidden, + deviceType, + mtpStoragesList, + changeLegacyMtpStorageOnlyOnDeviceChange, + }, + getState +) { + return async (dispatch) => { + checkIf(filePath, 'string'); + checkIf(ignoreHidden, 'boolean'); + checkIf(deviceType, 'string'); + checkIf(mtpStoragesList, 'object'); + checkIf(changeLegacyMtpStorageOnlyOnDeviceChange, 'boolean'); + + const { mtpMode } = getState().Settings; + + try { + const { error, stderr, data } = await fileExplorerController.listStorages( + { + deviceType, + } + ); + + dispatch( + churnMtpBuffer({ deviceType, error, stderr, data, - callback: () => { - let changeMtpIdsFlag = true; + mtpMode, + onSuccess: () => { + let updateMtpStorage = true; + if ( - Object.keys(deviceChangeCheck).length > 0 && - deviceChangeCheck.changeMtpStorageIdsOnlyOnDeviceChange && - Object.keys(deviceChangeCheck.mtpStoragesList).length > 0 && - isArraysEqual( - Object.keys(data), - Object.keys(deviceChangeCheck.mtpStoragesList) - ) + changeLegacyMtpStorageOnlyOnDeviceChange && + !isEmpty(mtpStoragesList) && + isArraysEqual(Object.keys(data), Object.keys(mtpStoragesList)) ) { - changeMtpIdsFlag = false; + updateMtpStorage = false; } - if (changeMtpIdsFlag) { - dispatch(changeMtpStorage({ ...data })); + if (updateMtpStorage) { + dispatch(actionChangeMtpStorage({ ...data })); } - dispatch(fetchDirList({ ...fetchDirArgs }, deviceType, getState)); - } + + dispatch( + listDirectory( + { + filePath, + ignoreHidden, + }, + deviceType, + getState + ) + ); + }, }) ); } catch (e) { @@ -145,131 +490,220 @@ export function setMtpStorageOptions( }; } -export function changeMtpStorage({ ...data }) { +export function actionChangeMtpStorage({ ...data }) { return { type: actionTypes.CHANGE_MTP_STORAGE, - payload: data + payload: data, }; } -export function setMtpStatus(data) { +/** + * + * @param args {isAvailable, error, isLoading, info} + * @return {{payload: {}, type: *}} + */ +export function actionSetMtpStatus({ ...args }) { return { type: actionTypes.SET_MTP_STATUS, - payload: data + payload: args, }; } -export function processMtpOutput({ +// This is the main entry point of data received from the MTP kernel. +// The data received here undergoes processing and the neccessary actions are taken accordingly +export function churnMtpBuffer({ deviceType, error, stderr, - data, // eslint-disable-line no-unused-vars - callback + data, + mtpMode, + onSuccess, + onError, }) { - return async dispatch => { + checkIf(onSuccess, 'function'); + checkIf(mtpMode, 'string'); + + return async (dispatch) => { try { const { - status: mtpStatus, + mtpStatus, error: mtpError, throwAlert: mtpThrowAlert, - logError: mtpLogError - } = await processMtpBuffer({ error, stderr }); + logError: mtpLogError, + reportError: mtpReportError, + } = await processMtpBuffer({ error, stderr, mtpMode }); - dispatch(setMtpStatus(mtpStatus)); + dispatch( + actionSetMtpStatus({ + isAvailable: mtpStatus, + error: mtpMode === MTP_MODE.kalam ? stderr : error, + isLoading: false, + }) + ); if (!mtpStatus) { - dispatch(_fetchDirList([], deviceType)); - dispatch(setSelectedDirLists({ selected: [] }, deviceType)); + dispatch(actionListDirectory([], deviceType)); + dispatch(actionSetSelectedDirLists({ selected: [] }, deviceType)); + + if (onError) { + onError({ error, stderr, data: null }); + } } if (mtpError) { - log.error(mtpError, 'processMtpOutput', mtpLogError); + log.error( + mtpError, + 'churnMtpBuffer.mtpError', + mtpLogError, + true, + mtpReportError === true, + false + ); + log.error(error, 'churnMtpBuffer.error', true, true, false); + log.error(stderr, 'churnMtpBuffer.stderr', true, true, false); + if (mtpThrowAlert) { dispatch(throwAlert({ message: mtpError.toString() })); } - return false; + + return; } - callback(); + return onSuccess({ error: null, stderr: null, data }); } catch (e) { log.error(e); } }; } -export function processLocalOutput({ - deviceType, // eslint-disable-line no-unused-vars +// this is the main entry point of data received from the local disk file actions. +// the data received here undergoes processing and the neccessary actions are taken accordingly +export function churnLocalBuffer({ + _, error, stderr, - data, // eslint-disable-line no-unused-vars - callback + data, + onSuccess, + onError, }) { - return dispatch => { + return (dispatch) => { try { const { error: localError, throwAlert: localThrowAlert, - logError: localLogError + logError: localLogError, } = processLocalBuffer({ error, stderr }); if (localError) { - log.error(localError, 'processLocalOutput', localLogError); + log.error(localError, 'churnLocalBuffer', localLogError); if (localThrowAlert) { dispatch(throwAlert({ message: localError.toString() })); } + + if (onError) { + onError({ error, stderr, data: null }); + } + return false; } - callback(); + checkIf(onSuccess, 'function'); + + onSuccess({ error: null, stderr: null, data }); } catch (e) { log.error(e); } }; } -export function fetchDirList({ ...args }, deviceType, getState) { +export function listDirectory( + { filePath, ignoreHidden, onError, onSuccess }, + deviceType, + getState +) { + checkIf(filePath, 'string'); + checkIf(ignoreHidden, 'boolean'); + checkIf(getState, 'function'); + + const { mtpMode } = getState().Settings; + try { switch (deviceType) { - case DEVICES_TYPE_CONST.local: - return async dispatch => { - const { error, data } = await asyncReadLocalDir({ ...args }); + case DEVICE_TYPE.local: + return async (dispatch) => { + const { + error: localError, + stderr: localStderr, + data: localData, + } = await fileExplorerController.listFiles({ + deviceType, + filePath, + ignoreHidden, + storageId: null, + }); + + if (localError) { + log.error(localError, 'listDirectory -> listFiles'); - if (error) { - log.error(error, 'fetchDirList -> asyncReadLocalDir'); dispatch( - throwAlert({ message: `Unable fetch data from the Local disk.` }) + churnLocalBuffer({ + deviceType, + error: localError, + stderr: localStderr, + data: localData, + onSuccess: () => {}, + }) ); + return; } - dispatch(_fetchDirList(data, deviceType)); - dispatch(setCurrentBrowsePath(args.filePath, deviceType)); - dispatch(setSelectedDirLists({ selected: [] }, deviceType)); + dispatch(actionListDirectory(localData, deviceType), getState); + dispatch(setCurrentBrowsePath(filePath, deviceType)); + dispatch(actionSetSelectedDirLists({ selected: [] }, deviceType)); }; - case DEVICES_TYPE_CONST.mtp: - return async dispatch => { - const mtpStoragesListSelected = getMtpStoragesListSelected( - getState().Home - ); + case DEVICE_TYPE.mtp: + return async (dispatch) => { + const storageId = getSelectedStorageIdFromState(getState().Home); - const { error, stderr, data } = await asyncReadMtpDir({ - ...args, - mtpStoragesListSelected - }); + if (undefinedOrNull(storageId)) { + return; + } + + const { error, stderr, data } = + await fileExplorerController.listFiles({ + deviceType, + filePath, + ignoreHidden, + storageId, + }); dispatch( - processMtpOutput({ + churnMtpBuffer({ deviceType, error, stderr, data, - callback: () => { - dispatch(_fetchDirList(data, deviceType)); - dispatch(setSelectedDirLists({ selected: [] }, deviceType)); - dispatch(setCurrentBrowsePath(args.filePath, deviceType)); - } + mtpMode, + onSuccess: ({ error, stderr, data }) => { + dispatch(actionListDirectory(data, deviceType), getState); + dispatch( + actionSetSelectedDirLists({ selected: [] }, deviceType) + ); + dispatch(setCurrentBrowsePath(filePath, deviceType)); + + if (onSuccess) { + onSuccess({ error, stderr, data }); + } + }, + + onError: ({ error, stderr, data }) => { + if (onError) { + onError({ error, stderr, data }); + } + }, }) ); }; @@ -283,27 +717,92 @@ export function fetchDirList({ ...args }, deviceType, getState) { } export function reloadDirList( - { ...args }, - deviceType, - mtpStoragesList, + { filePath, ignoreHidden, deviceType }, getState ) { - return dispatch => { - switch (deviceType) { - case DEVICES_TYPE_CONST.local: - dispatch(fetchDirList({ ...args }, deviceType, getState)); - break; + checkIf(deviceType, 'inObjectValues', DEVICE_TYPE); + checkIf(filePath, 'string'); + checkIf(ignoreHidden, 'boolean'); + checkIf(getState, 'function'); - case DEVICES_TYPE_CONST.mtp: - dispatch( - setMtpStorageOptions( - { ...args }, - deviceType, - { changeMtpStorageIdsOnlyOnDeviceChange: true, mtpStoragesList }, - getState - ) + const { mtpMode, mtpDevice } = getState().Home; + + checkIf(mtpDevice, 'object'); + + return (dispatch) => { + switch (deviceType) { + case DEVICE_TYPE.local: + return dispatch( + listDirectory({ filePath, ignoreHidden }, deviceType, getState) ); - break; + + case DEVICE_TYPE.mtp: + switch (mtpMode) { + case MTP_MODE.legacy: + return dispatch( + initializeMtp( + { + filePath, + ignoreHidden, + changeLegacyMtpStorageOnlyOnDeviceChange: true, + deviceType, + }, + getState + ) + ); + + case MTP_MODE.kalam: + default: + dispatch( + actionSetMtpStatus({ + isLoading: true, + }) + ); + + // if mtpdevice is available then list directory + if (mtpDevice.isAvailable) { + return dispatch( + listDirectory( + { + filePath, + ignoreHidden, + onError: ({ stderr }) => { + // if device was changed then reinitialize the mtp + if (stderr === MTP_ERROR.ErrorDeviceChanged) { + dispatch( + initializeMtp( + { + filePath, + ignoreHidden, + changeLegacyMtpStorageOnlyOnDeviceChange: true, + deviceType, + }, + getState + ) + ); + } + }, + onSuccess: () => {}, + }, + deviceType, + getState + ) + ); + } + + // if the mtp was not previously initialized then initialize it + return dispatch( + initializeMtp( + { + filePath, + ignoreHidden, + changeLegacyMtpStorageOnlyOnDeviceChange: true, + deviceType, + }, + getState + ) + ); + } default: break; @@ -315,8 +814,8 @@ export function setFileTransferClipboard({ ...data }) { return { type: actionTypes.SET_FILE_TRANSFER_CLIPBOARD, payload: { - ...data - } + ...data, + }, }; } @@ -324,14 +823,14 @@ export function setFileTransferProgress({ ...data }) { return { type: actionTypes.SET_FILE_TRANSFER_PROGRESS, payload: { - ...data - } + ...data, + }, }; } export function clearFileTransfer() { return { - type: actionTypes.CLEAR_FILE_TRANSFER + type: actionTypes.CLEAR_FILE_TRANSFER, }; } @@ -339,13 +838,13 @@ export function setFilesDrag({ ...data }) { return { type: actionTypes.SET_FILES_DRAG, payload: { - ...data - } + ...data, + }, }; } export function clearFilesDrag() { return { - type: actionTypes.CLEAR_FILES_DRAG + type: actionTypes.CLEAR_FILES_DRAG, }; } diff --git a/app/containers/HomePage/components/FileExplorer.jsx b/app/containers/HomePage/components/FileExplorer.jsx index 49c599e5..370a6724 100644 --- a/app/containers/HomePage/components/FileExplorer.jsx +++ b/app/containers/HomePage/components/FileExplorer.jsx @@ -1,200 +1,257 @@ -'use strict'; - /* eslint no-case-declarations: off */ import React, { Component, Fragment } from 'react'; +import * as path from 'path'; +import classnames from 'classnames'; import Typography from '@material-ui/core/Typography'; +import { + faGithub, + faTwitter, + faFacebook, + faReddit, +} from '@fortawesome/free-brands-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { withStyles } from '@material-ui/core/styles'; -import { remote, ipcRenderer, shell } from 'electron'; +import { ipcRenderer, shell } from 'electron'; import lodashSortBy from 'lodash/sortBy'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; -import { log } from '@Log'; import { styles } from '../styles/FileExplorer'; import { TextFieldEdit as TextFieldEditDialog, ProgressBar as ProgressBarDialog, - Confirm as ConfirmDialog + Confirm as ConfirmDialog, } from '../../../components/DialogBox'; import { withReducer } from '../../../store/reducers/withReducer'; import reducers from '../reducers'; import { setSortingDirLists, - setSelectedDirLists, - fetchDirList, - processMtpOutput, - processLocalOutput, - setMtpStorageOptions, - getMtpStoragesListSelected, + actionSetSelectedDirLists, + listDirectory, + churnMtpBuffer, + churnLocalBuffer, + initializeMtp, + getSelectedStorageIdFromState, setFileTransferClipboard, setFilesDrag, clearFilesDrag, - setFocussedFileExplorerDeviceType + setFocussedFileExplorerDeviceType, + clearFileTransfer, + setFileTransferProgress, + disposeMtp, + actionSetMtpStatus, + reloadDirList, } from '../actions'; import { makeDirectoryLists, makeCurrentBrowsePath, makeMtpDevice, makeContextMenuList, - makeMtpStoragesListSelected, + makeStorageId, makeFileTransferClipboard, makeFileTransferProgess, makeFilesDrag, - makeFocussedFileExplorerDeviceType + makeFocussedFileExplorerDeviceType, } from '../selectors'; import { + makeAppThemeMode, makeEnableStatusBar, + makeEnableUsbHotplug, makeFileExplorerListingType, - makeHideHiddenFiles + makeHideHiddenFiles, + makeMtpMode, + makeShowDirectoriesFirst, } from '../../Settings/selectors'; import { + BUY_ME_A_COFFEE_URL, DEVICES_LABEL, - DEVICES_TYPE_CONST, - DONATE_PAYPAL_URL + SUPPORT_PAYPAL_URL, + USB_HOTPLUG_MAX_ATTEMPTS, + USB_HOTPLUG_MAX_ATTEMPTS_TIMEOUT, } from '../../../constants'; import { - renameLocalFiles, - checkFileExists, - newLocalFolder, - newMtpFolder, - pasteFiles, - renameMtpFiles -} from '../../../api/sys'; -import { baseName, pathInfo, pathUp, sanitizePath } from '../../../utils/paths'; -import { + arrayAverage, + getPluralText, isArray, + isEmpty, isFloat, isInt, isNumber, + niceBytes, removeArrayDuplicates, - undefinedOrNull + springTruncate, + undefinedOrNull, } from '../../../utils/funcs'; -import { getMainWindowRendererProcess } from '../../../utils/windowHelper'; +import { getMainWindowRendererProcess } from '../../../helpers/windowHelper'; import { throwAlert } from '../../Alerts/actions'; import { imgsrc } from '../../../utils/imgsrc'; import FileExplorerBodyRender from './FileExplorerBodyRender'; import { openExternalUrl } from '../../../utils/url'; -import { APP_GITHUB_URL } from '../../../constants/meta'; +import { APP_GITHUB_URL, APP_NAME } from '../../../constants/meta'; import { fbShareUrl, redditShareUrl, - twitterShareUrl + twitterShareUrl, } from '../../../templates/socialMediaShareBtns'; - +import { baseName, pathInfo, pathUp, sanitizePath } from '../../../utils/files'; +import { + DEVICE_TYPE, + FILE_EXPLORER_VIEW_TYPE, + FILE_TRANSFER_DIRECTION, + MTP_MODE, + USB_HOTPLUG_EVENTS, +} from '../../../enums'; +import { log } from '../../../utils/log'; +import fileExplorerController from '../../../data/file-explorer/controllers/FileExplorerController'; +import { checkIf } from '../../../utils/checkIf'; +import { analyticsService } from '../../../services/analytics'; +import { EVENT_TYPE } from '../../../enums/events'; +import { + buyMeACoffeeText, + supportUsingPayPal, +} from '../../../templates/fileExplorer'; +import { fileExistsSync } from '../../../helpers/fileOps'; +import { getRemoteWindow } from '../../../helpers/remoteWindowHelpers'; +import { IpcEvents } from '../../../services/ipc-events/IpcEventType'; + +const remote = getRemoteWindow(); const { Menu, getCurrentWindow } = remote; -const _mainWindowRendererProcess = getMainWindowRendererProcess(); -const filesDragGhostImg = new Image(0, 0); -filesDragGhostImg.src = imgsrc('FileExplorer/copy.svg'); + let allowFileDropFlag = false; let multipleSelectDirection = null; +const supportBtnsList = [ + { + enabled: true, + label: buyMeACoffeeText, + url: BUY_ME_A_COFFEE_URL, + image: 'FileExplorer/buymeacoffee-button.png', + icon: null, + invert: false, + name: 'buymeacoffee', + }, + { + enabled: true, + label: supportUsingPayPal, + image: 'FileExplorer/paypal-logo.png', + icon: null, + url: SUPPORT_PAYPAL_URL, + invert: false, + name: 'paypal', + }, +]; + const socialMediaShareBtnsList = [ { enabled: true, label: 'Find us on GitHub', - imgSrc: 'SocialMediaShare/github.svg', + icon: faGithub, url: APP_GITHUB_URL, - invert: false + invert: false, }, { enabled: true, label: 'Share it on Twitter', - imgSrc: 'SocialMediaShare/twitter.svg', + icon: faTwitter, url: twitterShareUrl, - invert: false + invert: false, }, { enabled: true, label: 'Share it on Facebook', - imgSrc: 'SocialMediaShare/facebook.svg', + icon: faFacebook, url: fbShareUrl, - invert: false + invert: false, }, { enabled: true, label: 'Share it on Reddit', - imgSrc: 'SocialMediaShare/reddit.svg', + icon: faReddit, url: redditShareUrl, - invert: false + invert: false, }, - { - enabled: true, - label: 'Buy me a Coffee', - imgSrc: 'SocialMediaShare/paypal.svg', - url: DONATE_PAYPAL_URL, - invert: false - } ]; class FileExplorer extends Component { constructor(props) { super(props); + + this.mainWindowRendererProcess = getMainWindowRendererProcess(); + this.filesDragGhostImg = this._createDragIcon(); + this.initialState = { togglePasteConfirmDialog: false, toggleDialog: { rename: { errors: { toggle: false, - message: null + message: null, }, toggle: false, - data: {} + data: {}, }, newFolder: { errors: { toggle: false, - message: null + message: null, }, toggle: false, - data: {} - } + data: {}, + }, }, - directoryGeneratedTime: Date.now() + directoryGeneratedTime: Date.now(), }; + this.state = { - ...this.initialState + ...this.initialState, }; this.electronMenu = new Menu(); this.keyedAcceleratorList = { - shift: false + shift: false, + }; + + this.usbHotplug = { + attempts: 0, + lastAttempted: Date.now(), }; } - componentWillMount() { + componentDidMount() { const { currentBrowsePath, deviceType, - actionCreateFetchMtpStorageOptions, - hideHiddenFiles + actionCreateInitializeMtp, + hideHiddenFiles, } = this.props; - if (deviceType === DEVICES_TYPE_CONST.mtp) { - actionCreateFetchMtpStorageOptions( - { - filePath: currentBrowsePath[deviceType], - ignoreHidden: hideHiddenFiles[deviceType] - }, - deviceType - ); + if (deviceType === DEVICE_TYPE.mtp) { + actionCreateInitializeMtp({ + filePath: currentBrowsePath[deviceType], + ignoreHidden: hideHiddenFiles[deviceType], + deviceType, + }); } else { - this._handleFetchDirList({ + this._handleListDirectory({ path: currentBrowsePath[deviceType], - deviceType + deviceType, }); } - } - componentDidMount() { this.registerAccelerators(); this.registerAppUpdate(); + this.registerGenerateErrorReport(); + this.registerUsbHotplug(); } - componentWillReceiveProps({ directoryLists: nextDirectoryLists }) { - const { deviceType, directoryLists } = this.props; + componentWillReceiveProps({ + directoryLists: nextDirectoryLists, + showDirectoriesFirst: nextShowDirectoriesFirst, + }) { + const { deviceType, directoryLists, showDirectoriesFirst } = this.props; const { nodes: prevDirectoryNodes } = directoryLists[deviceType]; const { nodes: nextDirectoryNodes } = nextDirectoryLists[deviceType]; @@ -202,17 +259,36 @@ class FileExplorer extends Component { if (nextDirectoryNodes !== prevDirectoryNodes) { this._handleDirectoryGeneratedTime(); } + + if (nextShowDirectoriesFirst !== showDirectoriesFirst) { + this._handleDirectoryGeneratedTime(); + } } componentWillUnmount() { + const { actionCreatedDisposeMtp, deviceType } = this.props; + this.deregisterAccelerators(); - _mainWindowRendererProcess.webContents.removeListener( + this.mainWindowRendererProcess.webContents.removeListener( 'fileExplorerToolbarActionCommunication', () => {} ); ipcRenderer.removeListener('isFileTransferActiveSeek', () => {}); ipcRenderer.removeListener('isFileTransferActiveReply', () => {}); + + if (deviceType === DEVICE_TYPE.mtp) { + ipcRenderer.removeListener( + IpcEvents.REPORT_BUGS_DISPOSE_MTP, + this._reportBugsDisposeMtpEvent + ); + ipcRenderer.removeListener( + IpcEvents.USB_HOTPLUG, + this._handleUsbHotplugEvent + ); + } + + actionCreatedDisposeMtp({ deviceType }); } registerAccelerators = () => { @@ -241,13 +317,14 @@ class FileExplorer extends Component { const { deviceType } = this.props; /** - * check whether an active file trasfer window is available. + * check whether an active file trasnfer window is available. * This is to prevent race between file transfer and app update taskbar progressbar access */ - if (deviceType === DEVICES_TYPE_CONST.local) { + if (deviceType === DEVICE_TYPE.local) { ipcRenderer.on('isFileTransferActiveSeek', (event, { ...args }) => { const { check: checkIsFileTransferActiveSeek } = args; + if (!checkIsFileTransferActiveSeek) { return null; } @@ -256,12 +333,181 @@ class FileExplorer extends Component { const { toggle: isActiveFileTransferProgess } = fileTransferProgess; ipcRenderer.send('isFileTransferActiveReply', { - isActive: isActiveFileTransferProgess + isActive: isActiveFileTransferProgess, }); }); } }; + registerGenerateErrorReport = () => { + const { deviceType } = this.props; + + if (deviceType === DEVICE_TYPE.mtp) { + ipcRenderer.on( + IpcEvents.REPORT_BUGS_DISPOSE_MTP, + this._reportBugsDisposeMtpEvent + ); + } + }; + + registerUsbHotplug = () => { + const { deviceType } = this.props; + + if (deviceType === DEVICE_TYPE.mtp) { + ipcRenderer.on(IpcEvents.USB_HOTPLUG, this._handleUsbHotplugEvent); + } + }; + + _reportBugsDisposeMtpEvent = async (_, { logFileZippedPath }) => { + // dispose the mtp before generating the report + await fileExplorerController.dispose({ deviceType: DEVICE_TYPE.mtp }); + + await fileExplorerController.fetchDebugReport({ + deviceType: DEVICE_TYPE.mtp, + }); + + const { error } = await fileExplorerController.deleteFiles({ + deviceType: DEVICE_TYPE.local, + fileList: [logFileZippedPath], + storageId: null, + }); + + ipcRenderer.send(IpcEvents.REPORT_BUGS_DISPOSE_MTP_REPLY, { error }); + }; + + _handleUsbHotplugEvent = async (_, { device, eventName }) => { + const { + mtpDevice, + actionCreateReloadDirList, + currentBrowsePath, + deviceType, + hideHiddenFiles, + enableUsbHotplug, + mtpMode, + } = this.props; + + checkIf(device, 'string'); + checkIf(eventName, 'inObjectValues', USB_HOTPLUG_EVENTS); + checkIf(mtpMode, 'inObjectValues', MTP_MODE); + + checkIf(actionCreateReloadDirList, 'function'); + checkIf(currentBrowsePath, 'object'); + checkIf(deviceType, 'inObjectValues', DEVICE_TYPE); + checkIf(hideHiddenFiles, 'object'); + + try { + if (isEmpty(device) || isEmpty(eventName)) { + return; + } + + const _usbDeviceInfo = JSON.parse(device); + + analyticsService.sendEvent(EVENT_TYPE.MTP_USB_HOTPLUG_RECEIVED, { + manufacturer: _usbDeviceInfo.manufacturer, + deviceName: _usbDeviceInfo.deviceName, + productId: _usbDeviceInfo.productId, + vendorId: _usbDeviceInfo.vendorId, + eventName, + }); + + // if the mtp mode is not kalam then dont proceed. + if (mtpMode !== MTP_MODE.kalam) { + return; + } + + if (!enableUsbHotplug) { + return; + } + + // if [this.usbHotplug] is null then set the object + if (!this.usbHotplug) { + this.usbHotplug = { + attempts: 1, + lastAttempted: Date.now(), + }; + } else { + // if the last attempt to connect the device was made more than [USB_HOTPLUG_MAX_ATTEMPTS_TIMEOUT] milliseconds ago then reset the attempts counter + if ( + Date.now() - this.usbHotplug.lastAttempted >= + USB_HOTPLUG_MAX_ATTEMPTS_TIMEOUT + ) { + this.usbHotplug = { + // update the number of attempts + attempts: 0, + lastAttempted: Date.now(), + }; + } + + // check for the number of connect attempts + // if the number of connect attempts are greater than [USB_HOTPLUG_MAX_ATTEMPTS] + // and if the [lastAttempted] and was made within [USB_HOTPLUG_MAX_ATTEMPTS_TIMEOUT] then don't connect + else if ( + this.usbHotplug.attempts > USB_HOTPLUG_MAX_ATTEMPTS && + Date.now() - this.usbHotplug.lastAttempted < + USB_HOTPLUG_MAX_ATTEMPTS_TIMEOUT + ) { + return; + } + + // update the number of attempts + this.usbHotplug.attempts += 1; + } + + switch (eventName) { + case USB_HOTPLUG_EVENTS.detach: + // if an usb device was detached and mtp device is disconnected then + // try to disconnect the mtp device + if (mtpDevice.isAvailable) { + // check to see if the detached usb device was the connected mtp device itself + if ( + _usbDeviceInfo.serialNumber === + mtpDevice?.info?.usbDeviceInfo?.SerialNumber + ) { + analyticsService.sendEvent(EVENT_TYPE.MTP_USB_HOTPLUG_DETTACHED, { + manufacturer: _usbDeviceInfo.manufacturer, + deviceName: _usbDeviceInfo.deviceName, + productId: _usbDeviceInfo.productId, + vendorId: _usbDeviceInfo.vendorId, + eventName, + }); + + actionCreateReloadDirList({ + filePath: currentBrowsePath[deviceType], + ignoreHidden: hideHiddenFiles[deviceType], + deviceType, + }); + } + } + + break; + + case USB_HOTPLUG_EVENTS.attach: + default: + // if an usb device was attached and mtp device is connected then + // try to connect the mtp device + if (!mtpDevice.isAvailable) { + analyticsService.sendEvent(EVENT_TYPE.MTP_USB_HOTPLUG_ATTACHED, { + manufacturer: _usbDeviceInfo.manufacturer, + deviceName: _usbDeviceInfo.deviceName, + productId: _usbDeviceInfo.productId, + vendorId: _usbDeviceInfo.vendorId, + eventName, + }); + + actionCreateReloadDirList({ + filePath: currentBrowsePath[deviceType], + ignoreHidden: hideHiddenFiles[deviceType], + deviceType, + }); + } + + break; + } + } catch (e) { + log.error(e, 'FileExplorer._handleUsbHotplugEvent'); + } + }; + _handleAccelerator = (pressed, event) => { if (undefinedOrNull(event)) { return; @@ -274,7 +520,7 @@ class FileExplorer extends Component { case 'meta': this.keyedAcceleratorList = { ...this.keyedAcceleratorList, - shift: pressed + shift: pressed, }; break; default: @@ -282,14 +528,14 @@ class FileExplorer extends Component { } }; - _handleFetchDirList({ ...args }) { - const { actionCreateFetchDirList, hideHiddenFiles } = this.props; + _handleListDirectory({ ...args }) { + const { actionCreateListDirectory, hideHiddenFiles } = this.props; const { path, deviceType } = args; - actionCreateFetchDirList( + actionCreateListDirectory( { filePath: path, - ignoreHidden: hideHiddenFiles[deviceType] + ignoreHidden: hideHiddenFiles[deviceType], }, deviceType ); @@ -298,7 +544,7 @@ class FileExplorer extends Component { lastSelectedNode = (nodes, selected) => { let _return = { index: -1, - item: [] + item: [], }; nodes.filter((item, index) => { @@ -316,7 +562,7 @@ class FileExplorer extends Component { _return = { index, - item + item, }; return _return; @@ -328,7 +574,7 @@ class FileExplorer extends Component { lastSelectedNodeOfTableSort = (nodes, selected, reverse = false) => { let _return = { index: -1, - item: [] + item: [], }; nodes.filter((item, index) => { @@ -346,14 +592,15 @@ class FileExplorer extends Component { if (_return.index < 0) { _return = { index, - item + item, }; + return _return; } } else { _return = { index, - item + item, }; } } @@ -374,7 +621,7 @@ class FileExplorer extends Component { actionCreateCopy, fileTransferClipboard, currentBrowsePath, - fileExplorerListingType + fileExplorerListingType, } = this.props; const { tableData, deviceType, event } = data; const { queue, nodes, order, orderBy } = directoryLists[deviceType]; @@ -397,13 +644,15 @@ class FileExplorer extends Component { } if ( - _focussedFileExplorerDeviceType === DEVICES_TYPE_CONST.mtp && + _focussedFileExplorerDeviceType === DEVICE_TYPE.mtp && !mtpDevice.isAvailable && type !== 'refresh' ) { return null; } + const deviceTypeUpperCase = deviceType.toUpperCase(); + switch (type) { case 'navigationLeft': case 'navigationRight': @@ -416,7 +665,7 @@ class FileExplorer extends Component { _tableSort = this.tableSort({ nodes, order, - orderBy + orderBy, }); _lastSelectedNodeOfTableSort = this.lastSelectedNodeOfTableSort( @@ -442,9 +691,14 @@ class FileExplorer extends Component { break; } + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_COPY_FILES`], + {} + ); + actionCreateCopy({ selected, - deviceType + deviceType, }); break; @@ -453,10 +707,15 @@ class FileExplorer extends Component { break; } + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_COPY_TO_QUEUE_FILES`], + {} + ); + actionCreateCopy({ selected, deviceType, - toQueue: true + toQueue: true, }); break; @@ -476,21 +735,21 @@ class FileExplorer extends Component { break; } - _mainWindowRendererProcess.webContents.send( + this.mainWindowRendererProcess.webContents.send( 'fileExplorerToolbarActionCommunication', { type, - deviceType: _focussedFileExplorerDeviceType + deviceType: _focussedFileExplorerDeviceType, } ); break; case 'refresh': - _mainWindowRendererProcess.webContents.send( + this.mainWindowRendererProcess.webContents.send( 'fileExplorerToolbarActionCommunication', { type, - deviceType: _focussedFileExplorerDeviceType + deviceType: _focussedFileExplorerDeviceType, } ); break; @@ -500,11 +759,11 @@ class FileExplorer extends Component { break; } - _mainWindowRendererProcess.webContents.send( + this.mainWindowRendererProcess.webContents.send( 'fileExplorerToolbarActionCommunication', { type, - deviceType: _focussedFileExplorerDeviceType + deviceType: _focussedFileExplorerDeviceType, } ); break; @@ -528,6 +787,7 @@ class FileExplorer extends Component { if (selected.length !== 1) { break; } + this._handleTableDoubleClick(_lastSelectedNode.item, deviceType); break; @@ -539,12 +799,12 @@ class FileExplorer extends Component { if ( type === 'navigationLeft' && - fileExplorerListingType[deviceType] === 'list' + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.list ) { break; } else if ( type === 'navigationUp' && - fileExplorerListingType[deviceType] === 'grid' + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.grid ) { break; } @@ -575,12 +835,12 @@ class FileExplorer extends Component { if ( type === 'navigationRight' && - fileExplorerListingType[deviceType] === 'list' + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.list ) { break; } else if ( type === 'navigationDown' && - fileExplorerListingType[deviceType] === 'grid' + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.grid ) { break; } @@ -606,12 +866,12 @@ class FileExplorer extends Component { if ( type === 'multipleSelectLeft' && - fileExplorerListingType[deviceType] === 'list' + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.list ) { break; } else if ( type === 'multipleSelectUp' && - fileExplorerListingType[deviceType] === 'grid' + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.grid ) { break; } @@ -629,11 +889,8 @@ class FileExplorer extends Component { : _lastSelectedNodeOfTableSort.index ]; } else { - _lastSelectedNodeOfTableSortReverse = this.lastSelectedNodeOfTableSort( - _tableSort, - selected, - true - ); + _lastSelectedNodeOfTableSortReverse = + this.lastSelectedNodeOfTableSort(_tableSort, selected, true); nextPathToNavigate = _tableSort[ @@ -669,12 +926,12 @@ class FileExplorer extends Component { if ( type === 'multipleSelectRight' && - fileExplorerListingType[deviceType] === 'list' + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.list ) { break; } else if ( type === 'multipleSelectDown' && - fileExplorerListingType[deviceType] === 'grid' + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.grid ) { break; } @@ -685,11 +942,8 @@ class FileExplorer extends Component { ) !== -1 && selected.length > 1; if (navigationInReverse) { - _lastSelectedNodeOfTableSortReverse = this.lastSelectedNodeOfTableSort( - _tableSort, - selected, - true - ); + _lastSelectedNodeOfTableSortReverse = + this.lastSelectedNodeOfTableSort(_tableSort, selected, true); nextPathToNavigate = _tableSort[_lastSelectedNodeOfTableSortReverse.index]; @@ -720,7 +974,7 @@ class FileExplorer extends Component { _handleFocussedFileExplorerDeviceType = (toggle, deviceType) => { const { actionCreateFocussedFileExplorerDeviceType, - focussedFileExplorerDeviceType + focussedFileExplorerDeviceType, } = this.props; if (focussedFileExplorerDeviceType.value === deviceType) { @@ -732,17 +986,17 @@ class FileExplorer extends Component { if (toggle) { _focussedFileExplorerDeviceType = { accelerator: deviceType, - value: deviceType + value: deviceType, }; } else { _focussedFileExplorerDeviceType = { onClick: deviceType, - value: deviceType + value: deviceType, }; } actionCreateFocussedFileExplorerDeviceType({ - ..._focussedFileExplorerDeviceType + ..._focussedFileExplorerDeviceType, }); }; @@ -759,11 +1013,11 @@ class FileExplorer extends Component { ) => { const { deviceType, mtpDevice, fileExplorerListingType } = this.props; const allowContextMenuClickThrough = - fileExplorerListingType[deviceType] === 'grid' && + fileExplorerListingType[deviceType] === FILE_EXPLORER_VIEW_TYPE.grid && !undefinedOrNull(rowData) && Object.keys(rowData).length < 1; - if (deviceType === DEVICES_TYPE_CONST.mtp && !mtpDevice.isAvailable) { + if (deviceType === DEVICE_TYPE.mtp && !mtpDevice.isAvailable) { return null; } @@ -783,22 +1037,21 @@ class FileExplorer extends Component { ); this.fireElectronMenu(contextMenuActiveList); + return null; } }; activeContextMenuList(deviceType, { ...rowData }, { ...tableData }) { - const { - contextMenuList, - fileTransferClipboard, - directoryLists - } = this.props; + const { contextMenuList, fileTransferClipboard, directoryLists } = + this.props; const { queue } = directoryLists[deviceType]; const _contextMenuList = contextMenuList[deviceType]; const contextMenuActiveList = []; - Object.keys(_contextMenuList).map(a => { + Object.keys(_contextMenuList).map((a) => { const item = _contextMenuList[a]; + switch (a) { case 'rename': contextMenuActiveList.push({ @@ -809,10 +1062,10 @@ class FileExplorer extends Component { this._handleContextMenuListActions({ [a]: { ...item, - data: rowData - } + data: rowData, + }, }); - } + }, }); break; @@ -825,10 +1078,10 @@ class FileExplorer extends Component { this._handleContextMenuListActions({ [a]: { ...item, - data: {} - } + data: {}, + }, }); - } + }, }); break; @@ -842,10 +1095,10 @@ class FileExplorer extends Component { this._handleContextMenuListActions({ [a]: { ...item, - data: {} - } + data: {}, + }, }); - } + }, }); break; @@ -858,10 +1111,26 @@ class FileExplorer extends Component { this._handleContextMenuListActions({ [a]: { ...item, - data: tableData - } + data: tableData, + }, }); - } + }, + }); + + break; + case 'showInEnclosingFolder': + contextMenuActiveList.push({ + label: item.label, + enabled: Object.keys(rowData).length > 0, + data: rowData, + click: () => { + this._handleContextMenuListActions({ + [a]: { + ...item, + data: rowData, + }, + }); + }, }); break; @@ -878,17 +1147,19 @@ class FileExplorer extends Component { /* activate actions using mouse */ _handleContextMenuListActions = ({ ...args }) => { const { deviceType, directoryLists, actionCreateCopy } = this.props; + const deviceTypeUpperCase = deviceType.toUpperCase(); - Object.keys(args).map(a => { + Object.keys(args).map((a) => { const item = args[a]; + switch (a) { case 'rename': this._handleToggleDialogBox( { toggle: true, data: { - ...item.data - } + ...item.data, + }, }, 'rename' ); @@ -897,18 +1168,32 @@ class FileExplorer extends Component { case 'copy': // eslint-disable-next-line prefer-destructuring const selectedItemsToCopy = directoryLists[deviceType].queue.selected; + actionCreateCopy({ selected: selectedItemsToCopy, deviceType }); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_COPY_FILES`], + {} + ); + break; case 'copyToQueue': // eslint-disable-next-line prefer-destructuring const selectedItemsToCopyToQueue = directoryLists[deviceType].queue.selected; + actionCreateCopy({ selected: selectedItemsToCopyToQueue, deviceType, - toQueue: true + toQueue: true, }); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_COPY_TO_QUEUE_FILES`], + {} + ); + break; case 'paste': @@ -920,13 +1205,18 @@ class FileExplorer extends Component { { toggle: true, data: { - ...item.data - } + ...item.data, + }, }, 'newFolder' ); break; + case 'showInEnclosingFolder': + this._handleShowInEnclosingFolder({ ...item }); + + break; + case 'cancel': break; @@ -946,9 +1236,22 @@ class FileExplorer extends Component { ...toggleDialog, [targetAction]: { ...toggleDialog[targetAction], - ...args - } - } + ...args, + }, + }, + }); + }; + + _handleClearEditDialog = (targetAction) => { + const { toggleDialog } = this.state; + + this.setState({ + toggleDialog: { + ...toggleDialog, + [targetAction]: { + ...this.initialState.toggleDialog[targetAction], + }, + }, }); }; @@ -958,61 +1261,108 @@ class FileExplorer extends Component { actionCreateRenameFile, hideHiddenFiles, currentBrowsePath, - mtpStoragesListSelected + storageId, } = this.props; // eslint-disable-next-line react/destructuring-assignment const { data } = this.state.toggleDialog.rename; - const { confirm, textFieldValue: newFileName } = args; + const { confirm, textFieldValue: newFilename } = args; const targetAction = 'rename'; + const deviceTypeUpperCase = deviceType.toUpperCase(); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_RENAME_STARTED`], + {} + ); - if (!confirm || newFileName === null) { + if (!confirm || newFilename === null) { this._handleClearEditDialog(targetAction); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_RENAME_EXIT`], + { + Reason: 'EXIT', + } + ); + return null; } - if (newFileName.trim() === '' || /[/\\?%*:|"<>]/g.test(newFileName)) { + if (newFilename.trim() === '' || /[/\\?%*:|"<>]/g.test(newFilename)) { this._handleErrorsEditDialog( { toggle: true, - message: `Error: Illegal characters.` + message: `Error: Illegal characters.`, }, targetAction ); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_RENAME_EXIT`], + { + Reason: 'ILLEGAL_CHARACTERS', + } + ); + return null; } - // same file name; no change - const _pathUp = pathUp(data.path); - const newFilePath = sanitizePath(`${_pathUp}/${newFileName}`); - const oldFilePath = data.path; + const sanitizedNewFilename = sanitizePath(newFilename); + const filePath = data.path; + const filename = data.name; + + const newFilepath = path.join(pathUp(filePath), sanitizedNewFilename); - if (newFilePath === data.path) { + if (newFilepath === data.path) { this._handleClearEditDialog(targetAction); - return null; - } - if ( - await checkFileExists(newFilePath, deviceType, mtpStoragesListSelected) - ) { - this._handleErrorsEditDialog( + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_RENAME_EXIT`], { - toggle: true, - message: `Error: The name "${newFileName}" is already taken.` - }, - targetAction + Reason: 'NO_CHANGE', + } ); + return null; } + + // if the new filename and the existing filename are just case different then skip the edit dialog + if (sanitizedNewFilename.toLowerCase() !== filename.toLowerCase()) { + if ( + await fileExplorerController.filesExist({ + deviceType, + fileList: [newFilepath], + storageId, + }) + ) { + this._handleErrorsEditDialog( + { + toggle: true, + message: `Error: The name "${sanitizedNewFilename}" is already taken.`, + }, + targetAction + ); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_RENAME_EXIT`], + { + Reason: 'FILE_EXISTS', + } + ); + + return null; + } + } + actionCreateRenameFile( { - oldFilePath, - newFilePath, - deviceType + filePath, + newFilename: sanitizedNewFilename, + deviceType, }, { filePath: currentBrowsePath[deviceType], - ignoreHidden: hideHiddenFiles[deviceType] + ignoreHidden: hideHiddenFiles[deviceType], } ); @@ -1021,50 +1371,87 @@ class FileExplorer extends Component { _handleErrorsEditDialog = ({ ...args }, targetAction) => { const { toggleDialog } = this.state; + this.setState({ toggleDialog: { ...toggleDialog, [targetAction]: { ...toggleDialog[targetAction], - errors: { ...args } - } - } + errors: { ...args }, + }, + }, }); }; - _handleClearEditDialog = targetAction => { - const { toggleDialog } = this.state; + _handleTogglePasteConfirmDialog = (status) => { this.setState({ - toggleDialog: { - ...toggleDialog, - [targetAction]: { - ...this.initialState.toggleDialog[targetAction] - } - } + togglePasteConfirmDialog: status, }); }; - _handleTogglePasteConfirmDialog = status => { - this.setState({ - togglePasteConfirmDialog: status - }); + _handleShowInEnclosingFolder = async ({ data, enabled, label }) => { + checkIf(data, 'object'); + checkIf(enabled, 'boolean'); + checkIf(label, 'string'); + + try { + const filePath = data?.path; + + if (isEmpty(filePath)) { + return; + } + + if (!fileExistsSync(filePath)) { + return; + } + + shell.showItemInFolder(filePath); + } catch (e) { + log.error(e, 'FileExplorer._handleShowInEnclosingFolder'); + } }; + _createDragIcon() { + const dragIcon = document.createElement('img'); + + dragIcon.src = imgsrc(`FileExplorer/files-archive.svg`); + dragIcon.style.width = '100px'; + + const div = document.createElement('div'); + + div.appendChild(dragIcon); + div.style.position = 'absolute'; + div.style.top = '0px'; + div.style.left = '-500px'; + document.querySelector('body').appendChild(div); + + return div; + } + _handleFilesDragStart = (e, { sourceDeviceType }) => { + const sourceDeviceTypeUpperCase = sourceDeviceType?.toUpperCase(); + this._handleSetFilesDrag({ sourceDeviceType, destinationDeviceType: null, enter: false, - lock: false + lock: false, }); - e.dataTransfer.setDragImage(filesDragGhostImg, 0, 0); + analyticsService.sendEvent( + EVENT_TYPE[`${sourceDeviceTypeUpperCase}_DRAG_FILES_STARTED`], + {} + ); + + e.dataTransfer.setDragImage(this.filesDragGhostImg, 0, 0); + }; + + _handleExternalFileDragLeave = (_) => { + this._handleClearFilesDrag(); }; _handleFilesDragOver = (e, { destinationDeviceType }) => { const { filesDrag } = this.props; - e.preventDefault(); - e.stopPropagation(); if (destinationDeviceType === filesDrag.sourceDeviceType) { if (filesDrag.sameSourceDestinationLock) { @@ -1077,11 +1464,17 @@ class FileExplorer extends Component { destinationDeviceType, enter: false, lock: false, - sameSourceDestinationLock: true + sameSourceDestinationLock: true, }); + return null; } + /* Beyond this point we want to allow dropping */ + /* so prevent the default behavior */ + e.preventDefault(); + e.stopPropagation(); + if (filesDrag.lock) { return null; } @@ -1092,7 +1485,7 @@ class FileExplorer extends Component { destinationDeviceType, enter: true, lock: true, - sameSourceDestinationLock: false + sameSourceDestinationLock: false, }); }; @@ -1100,21 +1493,63 @@ class FileExplorer extends Component { this._handleClearFilesDrag(); }; - _handleTableDrop = () => { - const { directoryLists, actionCreateCopy, filesDrag } = this.props; + _handleFilesDrop = ({ externalFiles }) => { + const { directoryLists, filesDrag } = this.props; + const { sourceDeviceType } = filesDrag; + + const isExternalFiles = !isEmpty(externalFiles); + const sourceDeviceTypeUpperCase = isExternalFiles + ? 'EXTERNAL' + : sourceDeviceType?.toUpperCase(); + + analyticsService.sendEvent( + EVENT_TYPE[`${sourceDeviceTypeUpperCase}_DRAG_FILES_DROPPED`], + { + isExternalFiles, + } + ); + + // if files were dragged from the app pane itself + if (!isExternalFiles) { + return directoryLists[sourceDeviceType]?.queue?.selected ?? []; + } + + // if files were dragged from the finder window then go here + return [...externalFiles].map((f) => f.path); + }; + + _handleTableDrop = async (_, { __, externalFiles }) => { + const { actionCreateCopy, filesDrag } = this.props; const { sourceDeviceType, destinationDeviceType } = filesDrag; if ( !allowFileDropFlag || - destinationDeviceType === null || - destinationDeviceType === null || - sourceDeviceType === destinationDeviceType + sourceDeviceType === destinationDeviceType || + destinationDeviceType === null ) { + const isExternalFiles = !isEmpty(externalFiles); + const sourceDeviceTypeUpperCase = isExternalFiles + ? 'EXTERNAL' + : sourceDeviceType?.toUpperCase(); + + analyticsService.sendEvent( + EVENT_TYPE[`${sourceDeviceTypeUpperCase}_DRAG_FILES_CANCELLED`], + { + 'Is file drop allowed': allowFileDropFlag, + Reason: + sourceDeviceType === destinationDeviceType + ? 'Source and destination are same' + : false, + } + ); + return null; } - // eslint-disable-next-line prefer-destructuring - const selected = directoryLists[sourceDeviceType].queue.selected; + const selected = this._handleFilesDrop({ + externalFiles, + }); + actionCreateCopy({ selected, deviceType: sourceDeviceType }); setTimeout(() => { @@ -1123,7 +1558,7 @@ class FileExplorer extends Component { }, 200); }; - _handleOnHoverDropZoneActivate = deviceType => { + _handleonHoverDropZoneActivate = (deviceType) => { const { filesDrag, mtpDevice } = this.props; const { sourceDeviceType, destinationDeviceType } = filesDrag; @@ -1134,7 +1569,7 @@ class FileExplorer extends Component { return destinationDeviceType === deviceType; }; - _handleIsDraggable = deviceType => { + _handleIsDraggable = (deviceType) => { const { directoryLists, mtpDevice } = this.props; const { queue } = directoryLists[deviceType]; const { selected } = queue; @@ -1142,17 +1577,17 @@ class FileExplorer extends Component { return selected.length > 0 && mtpDevice.isAvailable; }; - _handleSetFilesDrag({ ...args }) { + _handleSetFilesDrag = ({ ...args }) => { const { actionCreateSetFilesDrag } = this.props; actionCreateSetFilesDrag({ ...args }); - } + }; - _handleClearFilesDrag() { + _handleClearFilesDrag = () => { const { actionCreateClearFilesDrag } = this.props; actionCreateClearFilesDrag(); - } + }; _handleNewFolderEditDialog = async ({ ...args }) => { const { @@ -1160,16 +1595,30 @@ class FileExplorer extends Component { actionCreateNewFolder, hideHiddenFiles, currentBrowsePath, - mtpStoragesListSelected + storageId, } = this.props; // eslint-disable-next-line react/destructuring-assignment const { data } = this.state.toggleDialog.newFolder; const { confirm, textFieldValue: newFolderName } = args; const targetAction = 'newFolder'; + const deviceTypeUpperCase = deviceType.toUpperCase(); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_NEW_FOLDER_STARTED`], + {} + ); if (!confirm) { this._handleClearEditDialog(targetAction); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_NEW_FOLDER_EXIT`], + { + Reason: 'NO_CHANGE', + } + ); + return null; } @@ -1177,10 +1626,18 @@ class FileExplorer extends Component { this._handleErrorsEditDialog( { toggle: true, - message: `Error: Folder name cannot be empty.` + message: `Error: Folder name cannot be empty.`, }, targetAction ); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_NEW_FOLDER_EXIT`], + { + Reason: 'EMPTY_FOLDER_NAME', + } + ); + return null; } @@ -1188,36 +1645,56 @@ class FileExplorer extends Component { this._handleErrorsEditDialog( { toggle: true, - message: `Error: Illegal characters.` + message: `Error: Illegal characters.`, }, targetAction ); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_NEW_FOLDER_EXIT`], + { + Reason: 'ILLEGAL_CHARACTERS', + } + ); + return null; } const newFolderPath = sanitizePath(`${data.path}/${newFolderName}`); if ( - await checkFileExists(newFolderPath, deviceType, mtpStoragesListSelected) + await fileExplorerController.filesExist({ + deviceType, + fileList: [newFolderPath], + storageId, + }) ) { this._handleErrorsEditDialog( { toggle: true, - message: `Error: The name "${newFolderName}" is already taken.` + message: `Error: The name "${newFolderName}" is already taken.`, }, targetAction ); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_RENAME_EXIT`], + { + Reason: 'FILE_EXISTS', + } + ); + return null; } actionCreateNewFolder( { newFolderPath, - deviceType + deviceType, }, { filePath: currentBrowsePath[deviceType], - ignoreHidden: hideHiddenFiles[deviceType] + ignoreHidden: hideHiddenFiles[deviceType], } ); @@ -1228,18 +1705,20 @@ class FileExplorer extends Component { const { deviceType, currentBrowsePath, - mtpStoragesListSelected, + storageId, fileTransferClipboard, - actionCreateThrowError + actionCreateThrowError, } = this.props; let { queue } = fileTransferClipboard; const destinationFolder = currentBrowsePath[deviceType]; let invalidFileNameFlag = false; + const deviceTypeUpperCase = deviceType.toUpperCase(); - queue = queue.map(a => { + queue = queue.map((a) => { const _baseName = baseName(a); const fullPath = `${destinationFolder}/${_baseName}`; + if (fullPath.trim() === '' || /[\\:]/g.test(fullPath)) { invalidFileNameFlag = true; } @@ -1247,64 +1726,130 @@ class FileExplorer extends Component { return fullPath; }); + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_PASTE_FILES`], + {} + ); + if (invalidFileNameFlag) { actionCreateThrowError({ - message: `Invalid file name in the path. \\: characters are not allowed.` + message: `Invalid file name in the path. \\: characters are not allowed.`, }); + return null; } - if (await checkFileExists(queue, deviceType, mtpStoragesListSelected)) { + if ( + await fileExplorerController.filesExist({ + deviceType, + fileList: queue, + storageId, + }) + ) { + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_PASTE_FILES_DIALOG_OPEN`], + { + Reason: 'FILES_EXIST', + } + ); + this._handleTogglePasteConfirmDialog(true); + return null; } - this._handlePasteConfirmDialog(true); + this._handlePasteConfirm(true); }; - _handlePasteConfirmDialog = confirm => { + _handlePasteConfirm = (action) => { const { deviceType, hideHiddenFiles, currentBrowsePath, - mtpStoragesListSelected, + storageId, actionCreatePaste, - fileTransferClipboard + actionCreateThrowError, + directoryLists, + fileTransferClipboard, } = this.props; const destinationFolder = currentBrowsePath[deviceType]; + const pasteAction = typeof action === 'string' ? action : 'cancel'; this._handleTogglePasteConfirmDialog(false); + const deviceTypeUpperCase = deviceType.toUpperCase(); + + if (pasteAction === 'cancel') { + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_PASTE_FILES_DIALOG_CLOSE`], + { + Reason: 'REPLACE_FILES_DENIED', + } + ); - if (!confirm) { return null; } + let fileTransferClipboardForPaste = fileTransferClipboard; + + if (pasteAction === 'skip') { + const destinationNodes = directoryLists[deviceType]?.nodes ?? []; + const clipboardItems = + fileTransferClipboard.items ?? + fileTransferClipboard.queue.map((filePath) => ({ + path: filePath, + name: baseName(filePath), + size: null, + isFolder: false, + })); + + const filteredItems = clipboardItems.filter((item) => { + if (item.isFolder) { + return true; + } + + return !destinationNodes.some((node) => node.name === item.name); + }); + + const filteredQueue = filteredItems.map((item) => item.path); + + if (filteredQueue.length < 1) { + actionCreateThrowError({ + message: `All matching files were skipped.`, + }); + + return null; + } + + fileTransferClipboardForPaste = { + ...fileTransferClipboard, + queue: filteredQueue, + items: filteredItems, + }; + } + actionCreatePaste( { destinationFolder, - mtpStoragesListSelected, - fileTransferClipboard + storageId, + fileTransferClipboard: fileTransferClipboardForPaste, }, { filePath: destinationFolder, - ignoreHidden: hideHiddenFiles[deviceType] + ignoreHidden: hideHiddenFiles[deviceType], }, deviceType ); }; _handleBreadcrumbPathClick = ({ ...args }) => { - const { - actionCreateFetchDirList, - hideHiddenFiles, - deviceType - } = this.props; + const { actionCreateListDirectory, hideHiddenFiles, deviceType } = + this.props; const { path } = args; - actionCreateFetchDirList( + actionCreateListDirectory( { filePath: path, - ignoreHidden: hideHiddenFiles[deviceType] + ignoreHidden: hideHiddenFiles[deviceType], }, deviceType ); @@ -1328,7 +1873,7 @@ class FileExplorer extends Component { _handleSelectAllClick = (deviceType, event) => { const { directoryLists, actionCreateSelectAllClick } = this.props; const selected = - directoryLists[deviceType].nodes.map(item => item.path) || []; + directoryLists[deviceType].nodes.map((item) => item.path) || []; let isChecked = true; if (event) { @@ -1380,34 +1925,70 @@ class FileExplorer extends Component { _handleTableDoubleClick = (item, deviceType) => { const { isFolder, path } = item; + const deviceTypeUpperCase = deviceType.toUpperCase(); + if (!isFolder) { - if (deviceType === DEVICES_TYPE_CONST.local) { - shell.openItem(path); + if (deviceType === DEVICE_TYPE.local) { + shell.openPath(path); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_OPEN_FILE`], + {} + ); } + return null; } - this._handleFetchDirList({ + this._handleListDirectory({ path, - deviceType + deviceType, }); + + analyticsService.sendEvent( + EVENT_TYPE[`${deviceTypeUpperCase}_OPEN_DIRECTORY`], + {} + ); }; tableSort = ({ ...args }) => { + const { showDirectoriesFirst } = this.props; const { nodes, order, orderBy } = args; if (typeof nodes === 'undefined' || !nodes.length < 0) { return []; } + let _sortedNode = []; + if (order === 'asc') { - return lodashSortBy(nodes, [ - value => this._lodashSortConstraints({ value, orderBy }) + _sortedNode = lodashSortBy(nodes, [ + (value) => this._lodashSortConstraints({ value, orderBy }), ]); + } else { + _sortedNode = lodashSortBy(nodes, [ + (value) => this._lodashSortConstraints({ value, orderBy }), + ]).reverse(); + } + + const _folders = []; + const _files = []; + + if (showDirectoriesFirst) { + _sortedNode.forEach((a) => { + if (a.isFolder) { + _folders.push(a); + + return a; + } + + _files.push(a); + }); + + _sortedNode = [..._folders, ..._files]; } - return lodashSortBy(nodes, [ - value => this._lodashSortConstraints({ value, orderBy }) - ]).reverse(); + + return _sortedNode; }; _lodashSortConstraints = ({ value, orderBy }) => { @@ -1441,7 +2022,7 @@ class FileExplorer extends Component { _handleDirectoryGeneratedTime = () => { this.setState({ - directoryGeneratedTime: Date.now() + directoryGeneratedTime: Date.now(), }); }; @@ -1457,21 +2038,18 @@ class FileExplorer extends Component { filesDrag, fileExplorerListingType, isStatusBarEnabled, - fileTransferClipboard + fileTransferClipboard, } = this.props; - const { - toggleDialog, - togglePasteConfirmDialog, - directoryGeneratedTime - } = this.state; + const { toggleDialog, togglePasteConfirmDialog, directoryGeneratedTime } = + this.state; const { rename, newFolder } = toggleDialog; const togglePasteDialog = - deviceType === DEVICES_TYPE_CONST.mtp && fileTransferProgess.toggle; + deviceType === DEVICE_TYPE.mtp && fileTransferProgess.toggle; const renameSecondaryText = - deviceType === DEVICES_TYPE_CONST.mtp - ? `Not all ${ - DEVICES_LABEL[DEVICES_TYPE_CONST.mtp] - } support the rename feature.` + deviceType === DEVICE_TYPE.mtp + ? `Not all ${DEVICES_LABEL[ + DEVICE_TYPE.mtp + ].toLowerCase()}s will support the rename feature.` : ``; return ( @@ -1497,7 +2075,6 @@ class FileExplorer extends Component { btnNegativeText="Cancel" errors={rename.errors} /> - -
+ + {`I've invested a significant amount of my time and energy into developing and maintaining this OpenSource application.`} + + {`I hate to run ads.`} Help me keep {APP_NAME} +   + Free +  and  + Open! + + +
+ {supportBtnsList.map((a, index) => ( + // eslint-disable-next-line react/no-array-index-key + +
+
{ + analyticsService.sendEvent( + EVENT_TYPE.SUPPORT_CTAS_DURING_TRANSFERRING, + { + name: a.name, + } + ); + openExternalUrl(a.url); + }} + className={classnames(styles.supportBtnWrapper, { + [styles.supportBtnWrapperForImage]: !!a.image, + })} + > + {a.image && ( + {a.label} + )} +
+
+
+ ))} +
+ Liked using the App? -
+
{socialMediaShareBtnsList.map((a, index) => ( // eslint-disable-next-line react/no-array-index-key @@ -1545,12 +2160,25 @@ class FileExplorer extends Component { aria-label={a.label} disabled={!a.enabled} onClick={() => openExternalUrl(a.url)} + className={classnames(styles.socialMediaBtnWrapper, { + [styles.socialMediaBtnWrapperForImage]: !!a.image, + })} > - {a.label} + {a.image && ( + {a.label} + )} + + {a.icon && ( + + )}
@@ -1558,15 +2186,36 @@ class FileExplorer extends Component {
- - + ; ); } } -const mapDispatchToProps = (dispatch, ownProps) => +const mapDispatchToProps = (dispatch, _) => bindActionCreators( { - actionCreateThrowError: ({ ...args }) => (_, getState) => { - dispatch(throwAlert({ ...args })); - }, + actionCreateThrowError: + ({ ...args }) => + (_, __) => { + dispatch(throwAlert({ ...args })); + }, - actionCreateFocussedFileExplorerDeviceType: ({ ...args }) => ( - _, - getState - ) => { - dispatch(setFocussedFileExplorerDeviceType({ ...args })); - }, + actionCreateFocussedFileExplorerDeviceType: + ({ ...args }) => + (_, __) => { + dispatch(setFocussedFileExplorerDeviceType({ ...args })); + }, - actionCreateRequestSort: ({ ...args }, deviceType) => (_, getState) => { - dispatch(setSortingDirLists({ ...args }, deviceType)); - }, + actionCreateRequestSort: + ({ ...args }, deviceType) => + (_, __) => { + dispatch(setSortingDirLists({ ...args }, deviceType)); + }, + + actionCreateSelectAllClick: + ({ selected }, isChecked, deviceType) => + (_, __) => { + if (isChecked) { + dispatch( + actionSetSelectedDirLists( + { + selected, + }, + deviceType + ) + ); + + return; + } + + dispatch(actionSetSelectedDirLists({ selected: [] }, deviceType)); + }, - actionCreateSelectAllClick: ({ selected }, isChecked, deviceType) => ( - _, - getState - ) => { - if (isChecked) { + actionCreateTableClick: + ({ selected }, deviceType) => + (_, __) => { + dispatch(actionSetSelectedDirLists({ selected }, deviceType)); + }, + + actionCreateInitializeMtp: + ({ filePath, ignoreHidden, deviceType }) => + (_, getState) => { dispatch( - setSelectedDirLists( + initializeMtp( { - selected + filePath, + ignoreHidden, + changeLegacyMtpStorageOnlyOnDeviceChange: false, + deviceType, }, - deviceType + getState ) ); - return; - } + }, - dispatch(setSelectedDirLists({ selected: [] }, deviceType)); - }, + /** + * + * @param args {isAvailable, error, isLoading, info} + * @return {{payload: {}, type: *}} + */ + actionCreateSetMtpStatus: + ({ ...args }) => + (_, __) => { + dispatch(actionSetMtpStatus(args)); + }, - actionCreateTableClick: ({ selected }, deviceType) => (_, getState) => { - dispatch(setSelectedDirLists({ selected }, deviceType)); - }, + actionCreateListDirectory: + ({ ...args }, deviceType) => + (_, getState) => { + dispatch(listDirectory({ ...args }, deviceType, getState)); + }, - actionCreateFetchMtpStorageOptions: ({ ...args }, deviceType) => ( - _, - getState - ) => { - dispatch( - setMtpStorageOptions( - { ...args }, - deviceType, - { - changeMtpStorageIdsOnlyOnDeviceChange: false, - mtpStoragesList: {} - }, - getState - ) - ); - }, + actionCreateReloadDirList: + ({ filePath, ignoreHidden, deviceType }) => + (_, getState) => { + checkIf(deviceType, 'inObjectValues', DEVICE_TYPE); - actionCreateFetchDirList: ({ ...args }, deviceType) => (_, getState) => { - dispatch(fetchDirList({ ...args }, deviceType, getState)); - }, + dispatch( + reloadDirList( + { + filePath, + ignoreHidden, + deviceType, + }, + getState + ) + ); + }, - actionCreateRenameFile: ( - { oldFilePath, newFilePath, deviceType }, - { ...fetchDirListArgs } - ) => async (_, getState) => { - try { - switch (deviceType) { - case DEVICES_TYPE_CONST.local: - const { - error: localError, - stderr: localStderr, - data: localData - } = await renameLocalFiles({ - oldFilePath, - newFilePath - }); + actionCreateRenameFile: + ({ filePath, newFilename, deviceType }, { ...listDirectoryArgs }) => + async (_, getState) => { + const { mtpMode } = getState().Settings; - dispatch( - processLocalOutput({ - deviceType, + try { + switch (deviceType) { + case DEVICE_TYPE.local: + const { error: localError, stderr: localStderr, data: localData, - callback: () => { - dispatch( - fetchDirList( - { ...fetchDirListArgs }, - deviceType, - getState - ) - ); - } - }) - ); - break; - case DEVICES_TYPE_CONST.mtp: - const mtpStoragesListSelected = getMtpStoragesListSelected( - getState().Home - ); - const { - error: mtpError, - stderr: mtpStderr, - data: mtpData - } = await renameMtpFiles({ - oldFilePath, - newFilePath, - mtpStoragesListSelected - }); - - dispatch( - processMtpOutput({ + } = await fileExplorerController.renameFile({ deviceType, + filePath, + newFilename, + storageId: null, + }); + + dispatch( + churnLocalBuffer({ + deviceType, + error: localError, + stderr: localStderr, + data: localData, + onSuccess: () => { + dispatch( + listDirectory( + { ...listDirectoryArgs }, + deviceType, + getState + ) + ); + }, + }) + ); + break; + case DEVICE_TYPE.mtp: + const storageId = getSelectedStorageIdFromState( + getState().Home + ); + const { error: mtpError, stderr: mtpStderr, data: mtpData, - callback: () => { - dispatch( - fetchDirList( - { ...fetchDirListArgs }, - deviceType, - getState - ) - ); - } - }) - ); - break; - default: - break; + } = await fileExplorerController.renameFile({ + deviceType, + filePath, + newFilename, + storageId, + }); + + dispatch( + churnMtpBuffer({ + deviceType, + error: mtpError, + stderr: mtpStderr, + data: mtpData, + mtpMode, + onSuccess: () => { + dispatch( + listDirectory( + { ...listDirectoryArgs }, + deviceType, + getState + ) + ); + }, + }) + ); + break; + default: + break; + } + } catch (e) { + log.error(e); } - } catch (e) { - log.error(e); - } - }, + }, - actionCreateNewFolder: ( - { newFolderPath, deviceType }, - { ...fetchDirListArgs } - ) => async (_, getState) => { - try { - switch (deviceType) { - case DEVICES_TYPE_CONST.local: - const { - error: localError, - stderr: localStderr, - data: localData - } = await newLocalFolder({ - newFolderPath - }); + actionCreateNewFolder: + ({ newFolderPath, deviceType }, { ...listDirectoryArgs }) => + async (_, getState) => { + try { + const { mtpMode } = getState().Settings; - dispatch( - processLocalOutput({ - deviceType, + switch (deviceType) { + case DEVICE_TYPE.local: + const { error: localError, stderr: localStderr, data: localData, - callback: () => { - dispatch( - fetchDirList( - { ...fetchDirListArgs }, - deviceType, - getState - ) - ); - } + } = await fileExplorerController.makeDirectory({ + deviceType, + filePath: newFolderPath, + storageId: null, + }); + + dispatch( + churnLocalBuffer({ + deviceType, + error: localError, + stderr: localStderr, + data: localData, + onSuccess: () => { + dispatch( + listDirectory( + { ...listDirectoryArgs }, + deviceType, + getState + ) + ); + }, + }) + ); + break; + case DEVICE_TYPE.mtp: + const storageId = getSelectedStorageIdFromState( + getState().Home + ); + const { + error: mtpError, + stderr: mtpStderr, + data: mtpData, + } = await fileExplorerController.makeDirectory({ + deviceType, + filePath: newFolderPath, + storageId, + }); + + dispatch( + churnMtpBuffer({ + deviceType, + error: mtpError, + stderr: mtpStderr, + data: mtpData, + mtpMode, + onSuccess: () => { + dispatch( + listDirectory( + { ...listDirectoryArgs }, + deviceType, + getState + ) + ); + }, + }) + ); + break; + default: + break; + } + } catch (e) { + log.error(e); + } + }, + + actionCreateCopy: + ({ selected, deviceType, toQueue = false }) => + async (_, getState) => { + try { + let queue = []; + let items = []; + const { fileTransfer, directoryLists } = getState().Home; + const currentClipboardQueue = fileTransfer.clipboard.queue; + const currentClipboardItems = fileTransfer.clipboard.items ?? []; + const sourceNodes = directoryLists[deviceType]?.nodes ?? []; + const selectedItems = (selected || []).map((filePath) => { + const node = sourceNodes.find((item) => item.path === filePath); + + return { + path: filePath, + name: node?.name ?? baseName(filePath), + size: node?.size ?? null, + isFolder: node?.isFolder ?? false, + }; + }); + + if (toQueue && isArray(selected) && selected.length > 0) { + queue = [...currentClipboardQueue, ...selected]; + items = [...currentClipboardItems, ...selectedItems]; + } else { + queue = selected || []; + items = selectedItems; + } + + queue = removeArrayDuplicates(queue); + items = queue.map((filePath) => { + const item = items.find((entry) => entry.path === filePath); + + return { + path: filePath, + name: item?.name ?? baseName(filePath), + size: item?.size ?? null, + isFolder: item?.isFolder ?? false, + }; + }); + + dispatch( + setFileTransferClipboard({ + queue, + items, + source: deviceType, + }) + ); + + dispatch(actionSetSelectedDirLists({ selected: [] }, deviceType)); + } catch (e) { + log.error(e); + } + }, + + actionCreatePaste: + ({ ...pasteArgs }, { ...listDirectoryArgs }, deviceType) => + (_, getState) => { + let sessionElapsedTime = 0; + const sessionTransferSpeeds = []; + let sessionTotalFiles = 0; + let sessionTransferDirection; + + try { + const { mtpMode, filesPreprocessingBeforeTransfer } = + getState().Settings; + + const { destinationFolder, storageId, fileTransferClipboard } = + pasteArgs; + + analyticsService.sendEvent(EVENT_TYPE.FILE_TRANSFER_STARTED, {}); + + // on pre process callback for file transfer + const onPreprocess = ({ fullPath }) => { + const bodyText1 = `Processing "${ + springTruncate(fullPath, 45).truncatedText + }"`; + + getCurrentWindow().setProgressBar(0); + dispatch( + setFileTransferProgress({ + titleText: `Copying files to ${DEVICES_LABEL[deviceType]}...`, + bottomText: `If file processing is taking too much time, you may disable it from 'Settings' > 'FILE MANAGER' > 'Display overall progress on the file transfer screen'`, + toggle: true, + values: [ + { + bodyText1, + bodyText2: null, + percentage: 0, + variant: `indeterminate`, + }, + ], }) ); - break; - case DEVICES_TYPE_CONST.mtp: - const mtpStoragesListSelected = getMtpStoragesListSelected( - getState().Home + }; + + // on progress callback for file transfer + const onProgress = ({ + elapsedTime, + speed, + activeFileProgress, + currentFile, + activeFileSize, + activeFileSizeSent, + totalFiles, + filesSent, + totalFileSize, + totalFileSizeSent, + totalFileProgress, + direction, + }) => { + let windowProgressBar = 0; + let bodyText1 = 0; + let progressText = 0; + + let progressInfo = []; + + sessionElapsedTime = elapsedTime; + sessionTotalFiles = totalFiles; + sessionTransferDirection = direction; + + /// file transfer progress on legacy mode + if (mtpMode === MTP_MODE.legacy) { + bodyText1 = `${Math.floor(activeFileProgress)}% complete of "${ + springTruncate(currentFile, 45).truncatedText + }"`; + progressText = `${niceBytes(activeFileSizeSent)} / ${niceBytes( + activeFileSize + )}`; + windowProgressBar = activeFileProgress / 100; + + sessionTransferSpeeds.push(parseFloat(speed) / 1000 / 1000); + + const _speed = speed ? `${niceBytes(speed)}` : `--`; + + progressInfo = [ + { + bodyText1, + bodyText2: `Elapsed: ${elapsedTime} | Progress: ${progressText} @ ${_speed}/sec`, + variant: `determinate`, + percentage: activeFileProgress, + }, + ]; + } else { + checkIf(direction, 'string'); + checkIf(direction, 'inObjectValues', FILE_TRANSFER_DIRECTION); + + sessionTransferSpeeds.push(parseFloat(speed)); + + // active file progress + bodyText1 = `${Math.floor(activeFileProgress)}% complete of "${ + springTruncate(currentFile, 45).truncatedText + }"`; + progressText = `${niceBytes(activeFileSizeSent)} / ${niceBytes( + activeFileSize + )}`; + const elapsedTimeText = `Elapsed: ${elapsedTime} | `; + + progressInfo = [ + { + bodyText1, + bodyText2: `${ + !filesPreprocessingBeforeTransfer[direction] + ? elapsedTimeText + : '' + }Progress: ${progressText} @ ${speed} MB/sec`, + variant: `determinate`, + percentage: activeFileProgress, + }, + ]; + windowProgressBar = activeFileProgress / 100; + + /// if preprocessing of file transfer is enabled then show total file transfer information as well + if (filesPreprocessingBeforeTransfer[direction]) { + // if preprocessing of file transfer is enabled then [windowProgressBar] + // progress value should be the [totalFileProgress] else [activeFileProgress] will be used + windowProgressBar = totalFileProgress / 100; + + const bodyText1 = `${filesSent} of ${totalFiles} ${getPluralText( + 'file', + totalFiles + )} copied | ${Math.floor(totalFileProgress)}% completed`; + const progressText = `${niceBytes( + totalFileSizeSent + )} / ${niceBytes(totalFileSize)}`; + + progressInfo.push({ + bodyText1, + bodyText2: `${elapsedTimeText}Progress: ${progressText}`, + variant: `determinate`, + percentage: totalFileProgress, + }); + } + } + + getCurrentWindow().setProgressBar(windowProgressBar); + dispatch( + setFileTransferProgress({ + titleText: `Copying files to ${DEVICES_LABEL[deviceType]}...`, + bottomText: null, + toggle: true, + values: progressInfo, + }) ); - const { - error: mtpError, - stderr: mtpStderr, - data: mtpData - } = await newMtpFolder({ - newFolderPath, - mtpStoragesListSelected - }); + }; + // on error callback for file transfer + const onError = ({ error, stderr, data }) => { dispatch( - processMtpOutput({ - deviceType, - error: mtpError, - stderr: mtpStderr, - data: mtpData, - callback: () => { + churnMtpBuffer({ + deviceType: DEVICE_TYPE.mtp, + error, + stderr, + data, + mtpMode, + onSuccess: () => { + getCurrentWindow().setProgressBar(-1); + dispatch(clearFileTransfer()); dispatch( - fetchDirList( - { ...fetchDirListArgs }, + listDirectory( + { ...listDirectoryArgs }, deviceType, getState ) ); - } + }, }) ); - break; - default: - break; - } - } catch (e) { - log.error(e); - } - }, - actionCreateCopy: ({ selected, deviceType, toQueue = false }) => async ( - _, - getState - ) => { - try { - let queue = []; - - if (toQueue && isArray(selected) && selected.length > 0) { - const currentClipboardQueue = getState().Home.fileTransfer.clipboard - .queue; - - queue = [...currentClipboardQueue, ...selected]; - } else { - queue = selected || []; - } + analyticsService.sendEvent(EVENT_TYPE.FILE_TRANSFER_ERROR, {}); + }; - queue = removeArrayDuplicates(queue); + // on completed callback for file transfer + const onCompleted = () => { + getCurrentWindow().setProgressBar(-1); + dispatch(clearFileTransfer()); + dispatch( + listDirectory({ ...listDirectoryArgs }, deviceType, getState) + ); - dispatch( - setFileTransferClipboard({ - queue, - source: deviceType - }) - ); + analyticsService.sendEvent(EVENT_TYPE.FILE_TRANSFER_COMPLETED, { + 'Transfer direction': sessionTransferDirection, + 'Total files': sessionTotalFiles, + 'Average transfer speed': `${arrayAverage( + sessionTransferSpeeds + )} MB/s`, + 'Elapsed time': sessionElapsedTime, + 'Is files preprocessing enabled': + filesPreprocessingBeforeTransfer[sessionTransferDirection], + }); + }; - dispatch(setSelectedDirLists({ selected: [] }, deviceType)); - } catch (e) { - log.error(e); - } - }, + switch (deviceType) { + case DEVICE_TYPE.local: + fileExplorerController.transferFiles({ + deviceType: DEVICE_TYPE.mtp, + destination: destinationFolder, + storageId, + fileList: fileTransferClipboard?.queue ?? [], + direction: FILE_TRANSFER_DIRECTION.download, + onCompleted, + onError, + onProgress, + onPreprocess, + }); + + break; + case DEVICE_TYPE.mtp: + fileExplorerController.transferFiles({ + deviceType: DEVICE_TYPE.mtp, + destination: destinationFolder, + storageId, + fileList: fileTransferClipboard?.queue ?? [], + direction: FILE_TRANSFER_DIRECTION.upload, + onCompleted, + onError, + onProgress, + onPreprocess, + }); + + break; + default: + break; + } + } catch (e) { + log.error(e); + } + }, - actionCreatePaste: ( - { ...pasteArgs }, - { ...fetchDirListArgs }, - deviceType - ) => (_, getState) => { - try { - switch (deviceType) { - case DEVICES_TYPE_CONST.local: - pasteFiles( - { ...pasteArgs }, - { ...fetchDirListArgs }, - 'mtpToLocal', - deviceType, - dispatch, - getState, - getCurrentWindow - ); - break; - case DEVICES_TYPE_CONST.mtp: - pasteFiles( - { ...pasteArgs }, - { ...fetchDirListArgs }, - 'localtoMtp', - deviceType, - dispatch, - getState, - getCurrentWindow - ); - break; - default: - break; + actionCreateSetFilesDrag: + ({ ...args }) => + (_, __) => { + try { + dispatch(setFilesDrag({ ...args })); + } catch (e) { + log.error(e); } - } catch (e) { - log.error(e); - } - }, + }, - actionCreateSetFilesDrag: ({ ...args }) => (_, getState) => { + actionCreateClearFilesDrag: () => (_, __) => { try { - dispatch(setFilesDrag({ ...args })); + dispatch(clearFilesDrag()); } catch (e) { log.error(e); } }, + actionCreatedDisposeMtp: + ({ deviceType }) => + (_, getState) => { + try { + if (deviceType === DEVICE_TYPE.local) { + return; + } - actionCreateClearFilesDrag: () => (_, getState) => { - try { - dispatch(clearFilesDrag()); - } catch (e) { - log.error(e); - } - } + dispatch( + disposeMtp( + { + deviceType, + onError: () => {}, + onSuccess: () => {}, + }, + getState + ) + ); + } catch (e) { + log.error(e); + } + }, }, dispatch ); -const mapStateToProps = (state, props) => { +const mapStateToProps = (state, _) => { return { currentBrowsePath: makeCurrentBrowsePath(state), mtpDevice: makeMtpDevice(state), @@ -1904,12 +2844,16 @@ const mapStateToProps = (state, props) => { hideHiddenFiles: makeHideHiddenFiles(state), isStatusBarEnabled: makeEnableStatusBar(state), contextMenuList: makeContextMenuList(state), - mtpStoragesListSelected: makeMtpStoragesListSelected(state), + storageId: makeStorageId(state), fileTransferClipboard: makeFileTransferClipboard(state), fileTransferProgess: makeFileTransferProgess(state), filesDrag: makeFilesDrag(state), fileExplorerListingType: makeFileExplorerListingType(state), - focussedFileExplorerDeviceType: makeFocussedFileExplorerDeviceType(state) + focussedFileExplorerDeviceType: makeFocussedFileExplorerDeviceType(state), + appThemeMode: makeAppThemeMode(state), + mtpMode: makeMtpMode(state), + enableUsbHotplug: makeEnableUsbHotplug(state), + showDirectoriesFirst: makeShowDirectoriesFirst(state), }; }; diff --git a/app/containers/HomePage/components/FileExplorerBodyRender.jsx b/app/containers/HomePage/components/FileExplorerBodyRender.jsx index 9f101c78..afd96ea0 100644 --- a/app/containers/HomePage/components/FileExplorerBodyRender.jsx +++ b/app/containers/HomePage/components/FileExplorerBodyRender.jsx @@ -1,5 +1,3 @@ -'use strict'; - import React, { PureComponent } from 'react'; import { withStyles } from '@material-ui/core/styles'; import Paper from '@material-ui/core/Paper'; @@ -12,13 +10,11 @@ import { fileExplorerKeymaps } from '../../../constants/keymaps'; import { isFileExplorerOnFocus, toggleFileExplorerDeviceType, - undefinedOrNull + undefinedOrNull, } from '../../../utils/funcs'; -import { - DEVICES_TYPE_CONST, - FILE_EXPLORER_DEFAULT_FOCUSSED_DEVICE_TYPE -} from '../../../constants'; +import { FILE_EXPLORER_DEFAULT_FOCUSSED_DEVICE_TYPE } from '../../../constants'; import { FILE_EXPLORER_BODY_WRAPPER_ID } from '../../../constants/dom'; +import { DEVICE_TYPE } from '../../../enums'; class FileExplorerBodyRender extends PureComponent { constructor(props) { @@ -26,7 +22,8 @@ class FileExplorerBodyRender extends PureComponent { const { deviceType } = this.props; this.fileExplorerKeymapString = null; - this.focussedFileExplorerDeviceTypeCached = FILE_EXPLORER_DEFAULT_FOCUSSED_DEVICE_TYPE; + this.focussedFileExplorerDeviceTypeCached = + FILE_EXPLORER_DEFAULT_FOCUSSED_DEVICE_TYPE; this.fileExplorerBodyWrapperId = `${FILE_EXPLORER_BODY_WRAPPER_ID}-${deviceType}`; this.acceleratorIgnoreList = ['multipleSelectClick']; } @@ -34,6 +31,10 @@ class FileExplorerBodyRender extends PureComponent { componentDidMount() { this.accelerators(); this.focusItem(); + + this.fileExplorerBodyWrapper = document.getElementById( + this.fileExplorerBodyWrapperId + ); } componentWillUnmount() { @@ -68,15 +69,17 @@ class FileExplorerBodyRender extends PureComponent { multipleSelectLeft: this.acceleratorCreateAction, multipleSelectRight: this.acceleratorCreateAction, multipleSelectUp: this.acceleratorCreateAction, - multipleSelectDown: this.acceleratorCreateAction + multipleSelectDown: this.acceleratorCreateAction, }; this.fileExplorerKeymapString = Object.keys(fileExplorerKeymaps).reduce( (accumulator, currentKey) => { const itemCurrentKey = fileExplorerKeymaps[currentKey].keys; + if (this.acceleratorIgnoreList.indexOf(currentKey) !== -1) { return accumulator; } + if (undefinedOrNull(accumulator) || accumulator.trim() === '') { return itemCurrentKey.join(', '); } @@ -87,8 +90,9 @@ class FileExplorerBodyRender extends PureComponent { ); hotkeys(this.fileExplorerKeymapString, (event, handler) => { - Object.keys(fileExplorerKeymaps).map(a => { + Object.keys(fileExplorerKeymaps).map((a) => { const item = fileExplorerKeymaps[a].keys; + if ( undefinedOrNull(keymapActionsList[a]) || undefinedOrNull(item) || @@ -111,7 +115,7 @@ class FileExplorerBodyRender extends PureComponent { }); }; - acceleratorNewFolder = event => { + acceleratorNewFolder = (event) => { const { onAcceleratorActivation, deviceType } = this.props; onAcceleratorActivation({ @@ -119,12 +123,12 @@ class FileExplorerBodyRender extends PureComponent { data: { event, tableData: this.tableData(), - deviceType - } + deviceType, + }, }); }; - acceleratorRename = event => { + acceleratorRename = (event) => { const { onAcceleratorActivation, deviceType } = this.props; onAcceleratorActivation({ @@ -132,8 +136,8 @@ class FileExplorerBodyRender extends PureComponent { data: { event, tableData: this.tableData(), - deviceType - } + deviceType, + }, }); }; @@ -144,11 +148,11 @@ class FileExplorerBodyRender extends PureComponent { if (toggle) { this.focussedFileExplorerDeviceTypeCached = toggleFileExplorerDeviceType( this.focussedFileExplorerDeviceTypeCached, - DEVICES_TYPE_CONST + DEVICE_TYPE ); - _focussedFileExplorerDeviceType = this - .focussedFileExplorerDeviceTypeCached; + _focussedFileExplorerDeviceType = + this.focussedFileExplorerDeviceTypeCached; } else { _focussedFileExplorerDeviceType = deviceType; } @@ -170,33 +174,113 @@ class FileExplorerBodyRender extends PureComponent { type, data: { event, - deviceType - } + deviceType, + }, }); }; tableData = () => { const { deviceType, currentBrowsePath, directoryLists } = this.props; + return { path: currentBrowsePath[deviceType], - directoryLists: directoryLists[deviceType] + directoryLists: directoryLists[deviceType], }; }; + isExternalFileDragged = (event) => { + const dt = event.dataTransfer; + + return ( + dt.types && + (dt.types.indexOf + ? dt.types.indexOf('Files') !== -1 + : dt.types.contains('Files')) + ); + }; + + _handleOnDragOver = (event) => { + const { deviceType, onFilesDragOver } = this.props; + + // if an extenal file is being dragged into the screen + // then do not activate the local pane + // because local files can only to be transferred to a mtp device + if (this.isExternalFileDragged(event)) { + if (deviceType === DEVICE_TYPE.local) { + return false; + } + } + + onFilesDragOver(event, { + destinationDeviceType: deviceType, + }); + }; + + _handleOnDragEnd = (event) => { + const { deviceType, onFilesDragEnd } = this.props; + + // if an extenal file is being dragged into the screen + // then do not activate the local pane + // because local files can only to be transferred to a mtp device + if (this.isExternalFileDragged(event)) { + if (deviceType === DEVICE_TYPE.local) { + return false; + } + } + + onFilesDragEnd(event, { + destinationDeviceType: deviceType, + }); + }; + + _handleOnDrop = (event) => { + event.preventDefault(); + + const { deviceType, onFilesDrop } = this.props; + + onFilesDrop(event, { + destinationDeviceType: deviceType, + externalFiles: event?.dataTransfer?.files ?? [], + }); + }; + + _handleExternalFileDragLeave = (event) => { + event.preventDefault(); + const { deviceType, onExternalFileDragLeave } = this.props; + + if (this.isExternalFileDragged(event)) { + if (deviceType === DEVICE_TYPE.local) { + return false; + } + + // prevent dragleave being fired when hovering a child element + const rect = this.fileExplorerBodyWrapper.getBoundingClientRect(); + + if ( + event.clientY < rect.top || + event.clientY >= rect.bottom || + event.clientX < rect.left || + event.clientX >= rect.right + ) { + onExternalFileDragLeave(event, { deviceType }); + } + } + + return false; + }; + render() { const { classes: styles, deviceType, currentBrowsePath, - OnHoverDropZoneActivate, + onHoverDropZoneActivate, filesDrag, // eslint-disable-line no-unused-vars onContextMenuClick, - onFilesDragOver, - onFilesDragEnd, - onTableDrop, onBreadcrumbPathClick, isStatusBarEnabled, fileTransferClipboard, + mtpDevice, ...parentProps } = this.props; const { directoryLists } = this.props; @@ -205,7 +289,7 @@ class FileExplorerBodyRender extends PureComponent { return ( + onClick={(event) => this.acceleratorFileExplorerTabSwitch( event, 'fileExplorerTabSwitch', @@ -220,29 +304,23 @@ class FileExplorerBodyRender extends PureComponent { tabIndex={-1} id={this.fileExplorerBodyWrapperId} className={classNames(styles.tableWrapper, { - [`onHoverDropZone`]: OnHoverDropZoneActivate(deviceType), - [`statusBarActive`]: isStatusBarEnabled + [`onHoverDropZone`]: onHoverDropZoneActivate(deviceType), + [`statusBarActive`]: isStatusBarEnabled, })} - onContextMenu={event => + onContextMenu={(event) => onContextMenuClick(event, {}, { ...this.tableData() }, _eventTarget) } - onDragOver={event => { - onFilesDragOver(event, { - destinationDeviceType: deviceType - }); - }} - onDragEnd={event => { - onFilesDragEnd(event); - }} - onDrop={event => { - onTableDrop(event); - }} + onDragOver={this._handleOnDragOver} + onDragEnd={this._handleOnDragEnd} + onDrop={this._handleOnDrop} + onDragLeave={this._handleExternalFileDragLeave} >
@@ -253,6 +331,7 @@ class FileExplorerBodyRender extends PureComponent { isStatusBarEnabled={isStatusBarEnabled} directoryLists={directoryLists[deviceType]} fileTransferClipboard={fileTransferClipboard} + mtpDevice={mtpDevice} /> ); diff --git a/app/containers/HomePage/components/FileExplorerTableBodyEmptyHelpPhoneNotRecognizedRender.jsx b/app/containers/HomePage/components/FileExplorerTableBodyEmptyHelpPhoneNotRecognizedRender.jsx deleted file mode 100644 index 98df6180..00000000 --- a/app/containers/HomePage/components/FileExplorerTableBodyEmptyHelpPhoneNotRecognizedRender.jsx +++ /dev/null @@ -1,104 +0,0 @@ -'use strict'; - -import React, { PureComponent } from 'react'; -import { withStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import { styles } from '../styles/FileExplorerTableBodyEmptyHelpPhoneNotRecognizedRender'; -import { openExternalUrl } from '../../../utils/url'; -import { - APP_GITHUB_ISSUES_URL, - APP_NAME, - APP_VERSION, - AUTHOR_EMAIL -} from '../../../constants/meta'; -import { DONATE_PAYPAL_URL } from '../../../constants'; -import { helpPhoneNotConnecting } from '../../../templates/fileExplorer'; - -class FileExplorerTableBodyEmptyHelpPhoneNotRecognizedRender extends PureComponent { - render() { - const { classes: styles } = this.props; - - return ( -
- - - OpenMTP was a project that I started to solve a - problem that was so personal to me. But I always knew, that - there's a community, whose facing the same problem as I did. - - - I wasn't wrong, I guess. Within a few months of launch, we are - a strong community with users from over  - 70 countries. It's overwhelming to see the - response that I have received from all of you, not just appreciating - the app, but also giving me suggestions and feedback to improve it. - - - - As they say, you build for the community and learn from it. - - - - I read each and every message that you send and am constantly - working to improve the app based on your feedback. Keep sending more - of those :) - - - - Some of you have been telling me that there are issues with - connecting certain mobile phones (mostly Samsung) to OpenMTP. - I have been working hard to fix this issue by migrating the existing - MTP Kernel to a better one but the development has slowed down due - to various reasons. - - - - I am looking for developers who can write Native Node.js modules and - are interested in contributing to the development of the next - generation OpenMTP MTP Kernel. - - - You may reach out to me at  - - {AUTHOR_EMAIL} - -  or check out this  - { - openExternalUrl(`${APP_GITHUB_ISSUES_URL}8`, events); - }} - > - thread - -  on GitHub for tracking the same,  - to collaborate and make this community bigger and stronger! - - - - If you'd like to support my work or buy me up a cup of coffee, - donate via  - { - openExternalUrl(DONATE_PAYPAL_URL, events); - }} - > - {DONATE_PAYPAL_URL} - - . - - - - Happy Transferring! - - -
- ); - } -} - -export default withStyles(styles)( - FileExplorerTableBodyEmptyHelpPhoneNotRecognizedRender -); diff --git a/app/containers/HomePage/components/FileExplorerTableBodyEmptyRender.jsx b/app/containers/HomePage/components/FileExplorerTableBodyEmptyRender.jsx index 1adb1574..779b1e49 100644 --- a/app/containers/HomePage/components/FileExplorerTableBodyEmptyRender.jsx +++ b/app/containers/HomePage/components/FileExplorerTableBodyEmptyRender.jsx @@ -1,12 +1,12 @@ -'use strict'; - import React, { PureComponent } from 'react'; +import { ipcRenderer } from 'electron'; import { withStyles } from '@material-ui/core/styles'; import TableCell from '@material-ui/core/TableCell'; import TableRow from '@material-ui/core/TableRow'; import Collapse from '@material-ui/core/Collapse'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; +import ToggleOffIcon from '@material-ui/icons/ToggleOff'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import Divider from '@material-ui/core/Divider'; @@ -19,6 +19,7 @@ import UsbIcon from '@material-ui/icons/Usb'; import TouchAppIcon from '@material-ui/icons/TouchApp'; import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked'; import CachedIcon from '@material-ui/icons/Cached'; +import PermDeviceInformationIcon from '@material-ui/icons/PermDeviceInformation'; import SettingsInputHdmiIcon from '@material-ui/icons/SettingsInputHdmi'; import ExpandLessIcon from '@material-ui/icons/ExpandLess'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; @@ -27,9 +28,12 @@ import Button from '@material-ui/core/Button'; import { styles } from '../styles/FileExplorerTableBodyEmptyRender'; import KeyboadShortcuts from '../../KeyboardShortcutsPage/components/KeyboadShortcuts'; import Features from '../../Onboarding/components/Features'; -import { Notification as NotificationDialog } from '../../../components/DialogBox'; -import FileExplorerTableBodyEmptyHelpPhoneNotRecognizedRender from './FileExplorerTableBodyEmptyHelpPhoneNotRecognizedRender'; import { helpPhoneNotConnecting } from '../../../templates/fileExplorer'; +import { analyticsService } from '../../../services/analytics'; +import { EVENT_TYPE } from '../../../enums/events'; +import { IpcEvents } from '../../../services/ipc-events/IpcEventType'; +import { APP_NAME } from '../../../constants/meta'; +import { openExternalUrl } from '../../../utils/url'; class FileExplorerTableBodyEmptyRender extends PureComponent { constructor(props) { @@ -39,29 +43,29 @@ class FileExplorerTableBodyEmptyRender extends PureComponent { expansionPanel: { noMtpInstructions: true, keyboardNavigation: false, - features: false + features: false, }, - showHelpPhoneNotRecognizedDialog: false }; } _handleExpansionPanel = ({ key }) => { - this.setState(prevState => { + this.setState((prevState) => { return { expansionPanel: { ...prevState.expansionPanel, - [key]: !prevState.expansionPanel[key] - } + [key]: !prevState.expansionPanel[key], + }, }; }); }; - _handleHelpPhoneNotRecognizedBtn = value => { - this.setState({ showHelpPhoneNotRecognizedDialog: value }); - }; + _handleHelpPhoneNotRecognizedBtn = () => { + ipcRenderer.send(IpcEvents.OPEN_HELP_PHONE_NOT_CONNECTING_WINDOW); - _handleHelpPhoneNotRecognizedDialog = () => { - this._handleHelpPhoneNotRecognizedBtn(false); + analyticsService.sendEvent( + EVENT_TYPE.MTP_HELP_PHONE_NOT_CONNECTED_DIALOG_OPEN, + {} + ); }; render() { @@ -72,16 +76,16 @@ class FileExplorerTableBodyEmptyRender extends PureComponent { currentBrowsePath, deviceType, directoryLists, - onContextMenuClick + onContextMenuClick, } = this.props; - const { expansionPanel, showHelpPhoneNotRecognizedDialog } = this.state; + const { expansionPanel } = this.state; const _eventTarget = 'emptyRowTarget'; const tableData = { path: currentBrowsePath[deviceType], - directoryLists: directoryLists[deviceType] + directoryLists: directoryLists[deviceType], }; if (isMtp && !mtpDevice.isAvailable) { @@ -90,9 +94,10 @@ class FileExplorerTableBodyEmptyRender extends PureComponent { @@ -102,7 +107,7 @@ class FileExplorerTableBodyEmptyRender extends PureComponent { button onClick={() => this._handleExpansionPanel({ - key: 'noMtpInstructions' + key: 'noMtpInstructions', }) } > @@ -135,11 +140,39 @@ class FileExplorerTableBodyEmptyRender extends PureComponent { + {`Uninstall 'Android File Transfer' by Google if it + keeps popping up everytime you connect your + Android device. The most recent versions of Google + drive and Dropbox are known to interfere with ${APP_NAME}. Completely quiting these apps may fix + this issue. `} + { + openExternalUrl( + 'https://github.com/ganeshrvel/openmtp/issues/276', + events + ); + }} + > + Read more... + + + } /> + + + + + + General Tab`} + /> + + @@ -171,17 +204,23 @@ class FileExplorerTableBodyEmptyRender extends PureComponent { - + - + + + + + + +
@@ -192,7 +231,7 @@ class FileExplorerTableBodyEmptyRender extends PureComponent { button onClick={() => this._handleExpansionPanel({ - key: 'keyboardNavigation' + key: 'keyboardNavigation', }) } > @@ -233,7 +272,7 @@ class FileExplorerTableBodyEmptyRender extends PureComponent { button onClick={() => this._handleExpansionPanel({ - key: 'features' + key: 'features', }) } > @@ -264,28 +303,18 @@ class FileExplorerTableBodyEmptyRender extends PureComponent {
- - - } - trigger={showHelpPhoneNotRecognizedDialog} - onClickHandler={this._handleHelpPhoneNotRecognizedDialog} - /> ); } + return ( + onContextMenu={(event) => onContextMenuClick(event, {}, { ...tableData }, _eventTarget) } /> diff --git a/app/containers/HomePage/components/FileExplorerTableBodyGridRender.jsx b/app/containers/HomePage/components/FileExplorerTableBodyGridRender.jsx index 0fadd4fc..ae8773eb 100644 --- a/app/containers/HomePage/components/FileExplorerTableBodyGridRender.jsx +++ b/app/containers/HomePage/components/FileExplorerTableBodyGridRender.jsx @@ -1,18 +1,75 @@ -'use strict'; - import React, { PureComponent } from 'react'; import classNames from 'classnames'; import { withStyles } from '@material-ui/core/styles'; import Checkbox from '@material-ui/core/Checkbox'; import Typography from '@material-ui/core/Typography'; import Tooltip from '@material-ui/core/Tooltip'; -import FolderIcon from '@material-ui/icons/Folder'; -import InsertDriveFileIcon from '@material-ui/icons/InsertDriveFile'; +// eslint-disable-next-line import/no-relative-packages +import prettyFileIcons from '../../../vendors/pretty-file-icons'; import { springTruncate } from '../../../utils/funcs'; import { FILE_EXPLORER_GRID_TRUNCATE_MAX_CHARS } from '../../../constants'; import { styles } from '../styles/FileExplorerTableBodyGridRender'; +import { imgsrc } from '../../../utils/imgsrc'; class FileExplorerTableBodyGridRender extends PureComponent { + RenderFileIcon = () => { + const { + classes: styles, + item, + _eventTarget, + tableData, + onContextMenuClick, + } = this.props; + + const fileIcon = prettyFileIcons.getIcon(item.name, 'svg'); + + return ( +
+ {item.name} + onContextMenuClick( + event, + { ...item }, + { ...tableData }, + _eventTarget + ) + } + /> +
+ ); + }; + + RenderFolderIcon = () => { + const { + classes: styles, + item, + _eventTarget, + tableData, + onContextMenuClick, + } = this.props; + + return ( +
+ {item.name} + onContextMenuClick( + event, + { ...item }, + { ...tableData }, + _eventTarget + ) + } + /> +
+ ); + }; + render() { const { classes: styles, @@ -23,8 +80,9 @@ class FileExplorerTableBodyGridRender extends PureComponent { tableData, onContextMenuClick, onTableClick, - onTableDoubleClick + onTableDoubleClick, } = this.props; + const { RenderFileIcon, RenderFolderIcon } = this; const fileName = springTruncate( item.name, @@ -33,55 +91,34 @@ class FileExplorerTableBodyGridRender extends PureComponent { return (
onTableDoubleClick(item, deviceType, event)} - onContextMenu={event => + onDoubleClick={(event) => onTableDoubleClick(item, deviceType, event)} + onContextMenu={(event) => onContextMenuClick(event, { ...item }, { ...tableData }, _eventTarget) } + onDragStart={(event) => { + if (!isSelected) { + onTableClick(item.path, deviceType, event, true, true); + } + }} >