flood: rearrange, remove misc files and reformat

This commit is contained in:
Jesse Chan
2020-11-15 22:54:36 +08:00
parent ba23f0166e
commit 1a878d5423
86 changed files with 700 additions and 917 deletions

View File

@@ -1,87 +0,0 @@
module.exports = {
extends: [
'airbnb',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'prettier',
'prettier/react',
'prettier/@typescript-eslint',
],
parser: 'babel-eslint',
plugins: ['import'],
rules: {
'arrow-parens': 0,
'class-methods-use-this': 0,
'consistent-return': 0,
'implicit-arrow-linebreak': 0,
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-extraneous-dependencies': 0,
'import/prefer-default-export': 0,
'lines-between-class-members': ['error', 'always', {exceptAfterSingleLine: true}],
'max-len': [
'error',
{
code: 120,
ignoreRegExpLiterals: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreUrls: true,
},
],
'no-console': 0,
'no-param-reassign': 0,
'no-plusplus': 0,
'no-underscore-dangle': [2, {allow: ['_id']}],
'no-unused-vars': [0, {argsIgnorePattern: '^_'}],
'object-curly-newline': 0,
'object-curly-spacing': 0,
'prefer-destructuring': [
2,
{
array: false,
object: true,
},
{
enforceForRenamedProperties: false,
},
],
},
overrides: [
{
files: ['*.ts', '*.tsx', '**/*.ts', '**/*.tsx'],
extends: [
'airbnb-typescript',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/react',
'prettier/@typescript-eslint',
],
parserOptions: {
project: './tsconfig.json',
},
rules: {
'import/no-extraneous-dependencies': 0,
'no-underscore-dangle': [2, {allow: ['_id']}],
'no-unused-vars': 0,
'@typescript-eslint/lines-between-class-members': ['error', 'always', {exceptAfterSingleLine: true}],
'@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}],
// TODO: Explicit return type
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
// TODO: Re-enable after everything is module
'@typescript-eslint/no-var-requires': 0,
},
},
],
};

29
.eslintrc.json Normal file
View File

@@ -0,0 +1,29 @@
{
"extends": ["plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"],
"parserOptions": {
"project": "./tsconfig.json"
},
"env": {
"browser": false,
"node": true
},
"rules": {
"import/no-extraneous-dependencies": 0,
"no-underscore-dangle": [2, {"allow": ["_id"]}],
"@typescript-eslint/lines-between-class-members": ["error", "always", {"exceptAfterSingleLine": true}],
"@typescript-eslint/no-unused-vars": ["error", {"argsIgnorePattern": "^_"}]
},
"overrides": [
{
"files": ["*.js", "*.jsx"],
"extends": ["prettier", "prettier/react"],
"parser": "babel-eslint",
"rules": {
"@typescript-eslint/no-var-requires": 0
}
}
]
}

View File

@@ -4,7 +4,7 @@ We love contributions from everyone.
By participating in this project, By participating in this project,
you agree to abide by the thoughtbot [code of conduct]. you agree to abide by the thoughtbot [code of conduct].
[code of conduct]: https://thoughtbot.com/open-source-code-of-conduct [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct
# Issue # Issue
@@ -16,21 +16,21 @@ See [PULL_REQUEST_TEMPLATE.md](PULL_REQUEST_TEMPLATE.md).
# Code quality # Code quality
+ Be sure to use 2 spaces instead of tabulations. - Be sure to use 2 spaces instead of tabulations.
# Labels # Labels
Category | Label(s) | Color(s) | Category | Label(s) | Color(s) |
--- | --- | --- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
Platform | ![](labels/bsd.png) ![](labels/docker.png) ![](labels/linux.png) ![](labels/macOS.png) ![](labels/windows.png) | #BFD4F2 | Platform | ![](labels/bsd.png) ![](labels/docker.png) ![](labels/linux.png) ![](labels/macOS.png) ![](labels/windows.png) | #BFD4F2 |
Problems | ![](labels/bug.png) ![](labels/security.png) | #EE3F46 | Problems | ![](labels/bug.png) ![](labels/security.png) | #EE3F46 |
Severity | ![](labels/critical.png) | #B60205 | Severity | ![](labels/critical.png) | #B60205 |
Type | ![](labels/code.png) ![](labels/design.png) ![](labels/documentation.png) ![](labels/test.png) | #FFC274 | Type | ![](labels/code.png) ![](labels/design.png) ![](labels/documentation.png) ![](labels/test.png) | #FFC274 |
Feedback | ![](labels/discussion.png) ![](labels/question.png) | #CC317C | Feedback | ![](labels/discussion.png) ![](labels/question.png) | #CC317C |
Improvements | ![](labels/enhancement.png) ![](labels/optimization.png) ![](labels/performance.png) | #5EBEFF | Improvements | ![](labels/enhancement.png) ![](labels/optimization.png) ![](labels/performance.png) | #5EBEFF |
Help | ![](labels/help%20wanted.png) | #76C3A9 | Help | ![](labels/help%20wanted.png) | #76C3A9 |
Additions | ![](labels/feature.png) | #90C954 | Additions | ![](labels/feature.png) | #90C954 |
Pending | ![](labels/can't%20reproduce.png) ![](labels/in%20progress.png) ![](labels/more%20info%20needed.png) ![](labels/waiting%20feedback.png) ![](labels/watchlist.png) | #FBCA04 | Pending | ![](labels/can't%20reproduce.png) ![](labels/in%20progress.png) ![](labels/more%20info%20needed.png) ![](labels/waiting%20feedback.png) ![](labels/watchlist.png) | #FBCA04 |
Inactive | ![](labels/duplicate.png) ![](labels/invalid.png) ![](labels/not%20a%20bug.png) ![](labels/on%20hold.png) ![](labels/wontfix.png) | #D2DAE1 | Inactive | ![](labels/duplicate.png) ![](labels/invalid.png) ![](labels/not%20a%20bug.png) ![](labels/on%20hold.png) ![](labels/wontfix.png) | #D2DAE1 |
**Note (if there is a need to add labels)**: in order to take a sharp screenshot of labels with Firefox: Right click the label => Inspect element => Right click the element on the inspector => Screenshot Node **Note (if there is a need to add labels)**: in order to take a sharp screenshot of labels with Firefox: Right click the label => Inspect element => Right click the element on the inspector => Screenshot Node

View File

@@ -1,7 +1,8 @@
--- ---
name: "🐞 Bug Report" name: '🐞 Bug Report'
about: "Report a general bug in flood" about: 'Report a general bug in flood'
--- ---
Type: Bug Report Type: Bug Report
- [ ] Try to follow the update procedure described in the README and try again before opening this issue. - [ ] Try to follow the update procedure described in the README and try again before opening this issue.
@@ -9,36 +10,45 @@ Type: Bug Report
- [ ] Please check the [Troubleshooting](https://github.com/Flood-UI/flood/wiki/Troubleshooting) wiki section. - [ ] Please check the [Troubleshooting](https://github.com/Flood-UI/flood/wiki/Troubleshooting) wiki section.
## Your Environment ## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in --> <!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used:
+ Version (stable release) `git --no-pager tag` - Version used:
+ Commit ID (development release) `git --no-pager log -1` - Version (stable release) `git --no-pager tag`
* Environment name and version: - Commit ID (development release) `git --no-pager log -1`
+ Node.js version `node --version` - Environment name and version:
+ npm version `npm --version` - Node.js version `node --version`
+ Web browser `name and version` - npm version `npm --version`
* Operating System and version: - Web browser `name and version`
- Operating System and version:
## Summary ## Summary
<!--- Provide a general summary of the issue in the Title above --> <!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior ## Expected Behavior
<!--- (Optional) Tell us what should happen --> <!--- (Optional) Tell us what should happen -->
## Current Behavior ## Current Behavior
<!--- (Optional) Tell us what happens instead of the expected behavior --> <!--- (Optional) Tell us what happens instead of the expected behavior -->
## Possible Solution ## Possible Solution
<!--- (Optional) suggest a fix/reason for the bug, --> <!--- (Optional) suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change --> <!--- or ideas how to implement the addition or change -->
## Steps to Reproduce ## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to --> <!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant --> <!--- reproduce this bug. Include code to reproduce, if relevant -->
1. 1.
2. 2.
3. 3.
4. 4.
## Context ## Context
<!--- (Optional) What are you trying to accomplish? --> <!--- (Optional) What are you trying to accomplish? -->

View File

@@ -1,14 +1,16 @@
--- ---
name: "💡 Feature Request" name: '💡 Feature Request'
about: "Suggest an idea for this project" about: 'Suggest an idea for this project'
--- ---
Type: Feature Request Type: Feature Request
- [ ] If you want to contribute to the project please review the [contributing guidelines](https://github.com/Flood-UI/flood/blob/master/.github/CONTRIBUTING.md). - [ ] If you want to contribute to the project please review the [contributing guidelines](https://github.com/Flood-UI/flood/blob/master/.github/CONTRIBUTING.md).
## Summary ## Summary
<!--- Provide a general summary of the feature in the Title above --> <!--- Provide a general summary of the feature in the Title above -->
## Idea of implementation ## Idea of implementation
<!--- Suggest ideas how to implement the addition or change --> <!--- Suggest ideas how to implement the addition or change -->

View File

@@ -1,7 +1,8 @@
--- ---
name: "🔒 Security Vulnerability" name: '🔒 Security Vulnerability'
about: "Report a security vulnerability in flood" about: 'Report a security vulnerability in flood'
--- ---
PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW. PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW.
If you discover a security vulnerability within flood, please send an e-mail to jfurrow (me@johnfurrow.com) or noraj (cybersecurity@tutamail.com). If you discover a security vulnerability within flood, please send an e-mail to jfurrow (me@johnfurrow.com) or noraj (cybersecurity@tutamail.com).

View File

@@ -1,8 +1,10 @@
--- ---
name: "📚 Documentation Issue" name: '📚 Documentation Issue'
about: 'Report an issue or missing part in the documentation' about: 'Report an issue or missing part in the documentation'
--- ---
Type: Documentation Issue Type: Documentation Issue
## Summary ## Summary
<!--- Provide a general summary of the issue in the Title above --> <!--- Provide a general summary of the issue in the Title above -->

View File

@@ -1,7 +1,8 @@
--- ---
name: "❓ Question" name: '❓ Question'
about: "Ask your questions here" about: 'Ask your questions here'
--- ---
Type: Question Type: Question
## Question ## Question

View File

@@ -1,7 +1,8 @@
--- ---
name: "🎙️ Discussion" name: '🎙️ Discussion'
about: "Start a discussion here" about: 'Start a discussion here'
--- ---
Type: Discussion Type: Discussion
## Discussion ## Discussion

View File

@@ -1,5 +1,6 @@
--- ---
name: "🆘 Support" name: '🆘 Support'
about: "Ask for help on Discord" about: 'Ask for help on Discord'
--- ---
If you need support, ask on Discord https://discord.gg/Z7yR5Uf If you need support, ask on Discord https://discord.gg/Z7yR5Uf

View File

@@ -1,18 +1,22 @@
<!--- Provide a general summary of your changes in the Title above --> <!--- Provide a general summary of your changes in the Title above -->
## Description ## Description
<!--- Describe your changes in detail --> <!--- Describe your changes in detail -->
## Related Issue ## Related Issue
<!--- This project only accepts pull requests related to open issues --> <!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first --> <!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce --> <!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: --> <!--- Please link to the issue here: -->
## Motivation and Context ## Motivation and Context
<!--- Why is this change required? What problem does it solve? --> <!--- Why is this change required? What problem does it solve? -->
## How Has This Been Tested? ## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. --> <!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to --> <!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. --> <!--- see how your change affects other areas of the code, etc. -->
@@ -20,14 +24,18 @@
## Screenshots (if appropriate): ## Screenshots (if appropriate):
## Types of changes ## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> <!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Breaking change (fix or feature that would cause existing functionality to change)
## Checklist: ## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project. - [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation. - [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly. - [ ] I have updated the documentation accordingly.

View File

@@ -1,16 +1,16 @@
# Labels # Labels
Category | Label(s) | Color(s) | Category | Label(s) | Color(s) |
--- | --- | --- | ------------ | ------------------------------------------------------------------------------------------------------------------------------ | -------- |
Platform | ![](bsd.png) ![](docker.png) ![](linux.png) ![](macOS.png) ![](windows.png) | #BFD4F2 | Platform | ![](bsd.png) ![](docker.png) ![](linux.png) ![](macOS.png) ![](windows.png) | #BFD4F2 |
Problems | ![](bug.png) ![](security.png) | #EE3F46 | Problems | ![](bug.png) ![](security.png) | #EE3F46 |
Severity | ![](critical.png) | #B60205 | Severity | ![](critical.png) | #B60205 |
Type | ![](code.png) ![](design.png) ![](documentation.png) ![](test.png) | #FFC274 | Type | ![](code.png) ![](design.png) ![](documentation.png) ![](test.png) | #FFC274 |
Feedback | ![](discussion.png) ![](question.png) | #CC317C | Feedback | ![](discussion.png) ![](question.png) | #CC317C |
Improvements | ![](enhancement.png) ![](optimization.png) ![](performance.png) | #5EBEFF | Improvements | ![](enhancement.png) ![](optimization.png) ![](performance.png) | #5EBEFF |
Help | ![](help%20wanted.png) | #76C3A9 | Help | ![](help%20wanted.png) | #76C3A9 |
Additions | ![](feature.png) | #90C954 | Additions | ![](feature.png) | #90C954 |
Pending | ![](can't%20reproduce.png) ![](in%20progress.png) ![](more%20info%20needed.png) ![](waiting%20feedback.png) ![](watchlist.png) | #FBCA04 | Pending | ![](can't%20reproduce.png) ![](in%20progress.png) ![](more%20info%20needed.png) ![](waiting%20feedback.png) ![](watchlist.png) | #FBCA04 |
Inactive | ![](duplicate.png) ![](invalid.png) ![](not%20a%20bug.png) ![](on%20hold.png) ![](wontfix.png) | #D2DAE1 | Inactive | ![](duplicate.png) ![](invalid.png) ![](not%20a%20bug.png) ![](on%20hold.png) ![](wontfix.png) | #D2DAE1 |
Note: in order to take a sharp screenshot of labels with Firefox: Right click the label => Inspect element => Right click the element on the inspector => Screenshot Node Note: in order to take a sharp screenshot of labels with Firefox: Right click the label => Inspect element => Right click the element on the inspector => Screenshot Node

View File

@@ -1,25 +0,0 @@
{
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["client", "server", "shared", "package.json", "README.md"],
"includePattern": ".js$",
"excludePattern": "(node_modules/|docs)"
},
"plugins": ["plugins/markdown"],
"templates": {
"cleverLinks": false,
"monospaceLinks": true,
"useLongnameInNav": false,
"showInheritedInNav": true
},
"opts": {
"destination": "./docs/",
"encoding": "utf8",
"private": true,
"recurse": true,
"template": "./node_modules/minami"
}
}

View File

@@ -1,7 +1,3 @@
# Markdown and HTML
*.md
*.html
# Distribution files # Distribution files
dist/ dist/

View File

@@ -1,16 +1,14 @@
dist: trusty dist: focal
language: node_js language: node_js
node_js: node_js:
- '10' - 'node'
- '12' - 'lts/*'
- '13' - '13'
matrix: matrix:
fast_finish: true fast_finish: true
allow_failures:
- node_js: 10
script: script:
- npm run check-source-formatting - npm run check-source-formatting
- npm run lint - npm run lint
- npm run check-types - npm run check-types
- npm run test
- npm run build - npm run build
- npm run test

View File

@@ -1,152 +1,159 @@
# Changelog # Changelog
## [4.0.2] (November 11, 2020) ## [4.0.2] (November 11, 2020)
* New translations
* German, thanks to @chint95 - New translations
* Romanian, thanks to @T-z3P - German, thanks to @chint95
- Romanian, thanks to @T-z3P
## [4.0.1] (November 10, 2020) ## [4.0.1] (November 10, 2020)
* Fix the unreliable clear all notification button
* Bump dependencies - Fix the unreliable clear all notification button
- Bump dependencies
## [4.0.0] (November 9, 2020) ## [4.0.0] (November 9, 2020)
* Experimental multi-client support
* qBittorrent - Experimental multi-client support
* Transmission - qBittorrent
* Stabilized and documented public API endpoints - Transmission
* Defined and documented internal interfaces, data structures and APIs - Stabilized and documented public API endpoints
* Better documentation for users and developers - Defined and documented internal interfaces, data structures and APIs
* Full migration to TypeScript - Better documentation for users and developers
* Reasonable test coverages for API endpoints - Full migration to TypeScript
* Torrent creation support - Reasonable test coverages for API endpoints
* Add torrents as completed - Torrent creation support
* Dropdown selector for existing tags - Add torrents as completed
* Seeding status in status filter - Dropdown selector for existing tags
* Set tracker URLs of torrents - Seeding status in status filter
* Improved handling of rendering, updating and scrolling of torrent list - Set tracker URLs of torrents
* Preliminary tests show that Flood can now handle 500,000 torrents at least in the frontend. - Improved handling of rendering, updating and scrolling of torrent list
* Note: real-world performance depends on other factors such as method call and deserialization operations in the backend and data transfer between backend and frontend. - Preliminary tests show that Flood can now handle 500,000 torrents at least in the frontend.
* Better performance, less memory and CPU consumption in both frontend and backend - Note: real-world performance depends on other factors such as method call and deserialization operations in the backend and data transfer between backend and frontend.
* New translations - Better performance, less memory and CPU consumption in both frontend and backend
* Chinese (Traditional), thanks to @vongola12324 - New translations
* Czech, thanks to Jan Březina - Chinese (Traditional), thanks to @vongola12324
* French, thanks to @Zopieux and @Mystere98 - Czech, thanks to Jan Březina
* German, thanks to @chint95 - French, thanks to @Zopieux and @Mystere98
* Bug fixes - German, thanks to @chint95
* Security enhancements - Bug fixes
* Dockerfile revamp - Security enhancements
* Native build tools no longer needed as native dependency is replaced with WebAssembly variant - Dockerfile revamp
* Server is packed before distribution, reduced number of dependencies in production, faster installation - Native build tools no longer needed as native dependency is replaced with WebAssembly variant
- Server is packed before distribution, reduced number of dependencies in production, faster installation
## [3.1.0] (September 4, 2020) ## [3.1.0] (September 4, 2020)
* Allow to replace main tracker of torrents
* Allow adjustment of visible context menu items - Allow to replace main tracker of torrents
* config.cli: make all configs configurable by options and env - Allow adjustment of visible context menu items
* styles: properly set width of clipboard icon (fixes #26) - config.cli: make all configs configurable by options and env
* client: hide logout button when auth is disabled - styles: properly set width of clipboard icon (fixes #26)
* Hungarian support (#21), thanks to @sfu420 - client: hide logout button when auth is disabled
* New translations: - Hungarian support (#21), thanks to @sfu420
* Chinese Traditional, thanks to @vongola12324 - New translations:
* Czech, thanks to @brezina.jn - Chinese Traditional, thanks to @vongola12324
* Portuguese, thanks to @Zamalor - Czech, thanks to @brezina.jn
* Security enhancements: - Portuguese, thanks to @Zamalor
* Allow restriction on file operations by paths - Security enhancements:
* Do not bypass authentication token validation with disableUsersAndAuth - Allow restriction on file operations by paths
* server: prohibit Cross-Origin Resource Sharing - Do not bypass authentication token validation with disableUsersAndAuth
* server: auth: strictly prohibit cross-site cookie - server: prohibit Cross-Origin Resource Sharing
* Minor security fixes: - server: auth: strictly prohibit cross-site cookie
* rTorrentDeserializer: avoid double unescaping - Minor security fixes:
* SettingsModal: mergeObjects: prevent prototype pollution - rTorrentDeserializer: avoid double unescaping
* server: setSettings: turn inboundTransformations into a Map to validate dynamic call - SettingsModal: mergeObjects: prevent prototype pollution
* server: be explicit about client app routes - server: setSettings: turn inboundTransformations into a Map to validate dynamic call
* server: cache index.html into memory - server: be explicit about client app routes
* Minor refactoring and other changes - server: cache index.html into memory
* Bump dependencies to the latest revisions - Minor refactoring and other changes
- Bump dependencies to the latest revisions
## [3.0.0] (August 25, 2020) ## [3.0.0] (August 25, 2020)
* BREAKING CHANGES:
* If `baseURI` is set, server will only respond to requests with baseURI. For instance, if you use `location /flood {proxy_pass http://127.0.0.1:3000;}`, you would have to change it to `location /flood {proxy_pass http://127.0.0.1:3000/flood;}`. - BREAKING CHANGES:
* Static assets now use relative paths only. It is no longer needed to recompile after `baseURI` change. - If `baseURI` is set, server will only respond to requests with baseURI. For instance, if you use `location /flood {proxy_pass http://127.0.0.1:3000;}`, you would have to change it to `location /flood {proxy_pass http://127.0.0.1:3000/flood;}`.
* Location of runtime files are rearranged. New default location for runtime files is `./run` folder. `tempPath` is now made configurable. - Static assets now use relative paths only. It is no longer needed to recompile after `baseURI` change.
* Static assets are relocated to `./dist` folder. You have to change the path from `./server/assets` to `./dist/assets` if you serve static assets from web server. - Location of runtime files are rearranged. New default location for runtime files is `./run` folder. `tempPath` is now made configurable.
* Flood will refuse to start if secrets are detected in static assets. Former default secret `flood` and some other weak secrets are no longer accepted. - Static assets are relocated to `./dist` folder. You have to change the path from `./server/assets` to `./dist/assets` if you serve static assets from web server.
* A command line interface is added as `config.cli.js`. Rename it to `config.js` and run `npm run start -- --help` for more details. - Flood will refuse to start if secrets are detected in static assets. Former default secret `flood` and some other weak secrets are no longer accepted.
* With some changes, Flood is now ready for publish to `npm`. You can now use `sudo npm install -g flood` to get a ready-to-use copy of Flood, then run `flood`. It is even easier with `npx`, try `npx flood --help` now. - A command line interface is added as `config.cli.js`. Rename it to `config.js` and run `npm run start -- --help` for more details.
* Better localization: - With some changes, Flood is now ready for publish to `npm`. You can now use `sudo npm install -g flood` to get a ready-to-use copy of Flood, then run `flood`. It is even easier with `npx`, try `npx flood --help` now.
* Flood project is now integrated with [Crowdin](https://crwd.in/flood), a renowned translation management system. It is now easier than ever to contribute your translations to Flood. - Better localization:
* Language will now be automatically detected from your browser by default. - Flood project is now integrated with [Crowdin](https://crwd.in/flood), a renowned translation management system. It is now easier than ever to contribute your translations to Flood.
* New languages are supported: `Čeština`, `Deutsch`, `italiano`, `norsk`, `Polskie`, `русский язык`, `Romanian`, `svenska`, `українська мова`, `日本語` and `اَلْعَرَبِيَّةُ` thanks to `Crowdin Machine Translation`. - Language will now be automatically detected from your browser by default.
* New translations for `Chinese (Traditional)` thanks to @vongola12324. - New languages are supported: `Čeština`, `Deutsch`, `italiano`, `norsk`, `Polskie`, `русский язык`, `Romanian`, `svenska`, `українська мова`, `日本語` and `اَلْعَرَبِيَّةُ` thanks to `Crowdin Machine Translation`.
* New translations for `Dutch` thanks to @NLxDoDge. - New translations for `Chinese (Traditional)` thanks to @vongola12324.
* New translations for `Portuguese` thanks to @MiguelNdeCarvalho. - New translations for `Dutch` thanks to @NLxDoDge.
* Support for touch and smaller screen devices: - New translations for `Portuguese` thanks to @MiguelNdeCarvalho.
* Sidebar is able to be collapsed via a button. It is collapsed by default when screen width is lower than `720px`. - Support for touch and smaller screen devices:
* Modals (Settings, Torrent Details, Add Torrent, etc.) are tuned for smaller screen devices. - Sidebar is able to be collapsed via a button. It is collapsed by default when screen width is lower than `720px`.
* It is now possible to open context (right click) menu on iOS/Safari devices by long pressing the item. - Modals (Settings, Torrent Details, Add Torrent, etc.) are tuned for smaller screen devices.
* Drag and drop is now possible for touch devices. You can now adjust the order of columns in Settings on touch devices. - It is now possible to open context (right click) menu on iOS/Safari devices by long pressing the item.
* Widths of columns are now adjustable on touch devices. (condensed view) - Drag and drop is now possible for touch devices. You can now adjust the order of columns in Settings on touch devices.
* Dark color scheme support: - Widths of columns are now adjustable on touch devices. (condensed view)
* Flood now automatically switches between light and dark color scheme based on your system settings. - Dark color scheme support:
* XML special chars (`&`, `<`, `>`, `'`, `"`) are properly handled. For instance, escaped chars like `&` will be properly displayed as `&` instead of `&amp;`. File operations on torrent with special chars no longer fail. - Flood now automatically switches between light and dark color scheme based on your system settings.
* `squashfs` and `tmpfs` mount points are now excluded by default in disk usage. This hopefully makes sure that useless system mounts won't spam the list. - XML special chars (`&`, `<`, `>`, `'`, `"`) are properly handled. For instance, escaped chars like `&` will be properly displayed as `&` instead of `&amp;`. File operations on torrent with special chars no longer fail.
* `More Info` button in expanded view is removed. - `squashfs` and `tmpfs` mount points are now excluded by default in disk usage. This hopefully makes sure that useless system mounts won't spam the list.
* More dependencies are bumped to the latest revisions. - `More Info` button in expanded view is removed.
- More dependencies are bumped to the latest revisions.
## [2.0.0] (August 5, 2020) ## [2.0.0] (August 5, 2020)
* BREAKING CHANGES:
* Bump dependencies to the latest version if possible - BREAKING CHANGES:
* Node 12 or later is now required - Bump dependencies to the latest version if possible
* Supports connecting to multiple rtorrent instances (one per user) - Node 12 or later is now required
* Moved rtorrent configuration to user database - Supports connecting to multiple rtorrent instances (one per user)
* Prompts user for connection details in UI when can't connect to rtorrent - Moved rtorrent configuration to user database
* Changed `/list/` route to `/overview/` - Prompts user for connection details in UI when can't connect to rtorrent
* Reorganized and renamed component source files - Changed `/list/` route to `/overview/`
* Removed verbose logging from `HistoryEra` - Reorganized and renamed component source files
* Check existing feed items against new download rules - Removed verbose logging from `HistoryEra`
* Switch URL and Label textboxes in Add Feed form to match the Download Rules form - Check existing feed items against new download rules
* Rate-limit the SCGI calls to rTorrent - Switch URL and Label textboxes in Add Feed form to match the Download Rules form
* Sends only one call at a time - Rate-limit the SCGI calls to rTorrent
* Sends at most one call every 250 miliseconds - Sends only one call at a time
* Implement "actity stream" - Sends at most one call every 250 miliseconds
* The Flood client no longer polls the Flood server on an interval. Instead, - Implement "actity stream"
the Flood server polls rTorrent on a more regular interval and emits changes - The Flood client no longer polls the Flood server on an interval. Instead,
via an event-stream. This significantly reduces data usage on the Flood client the Flood server polls rTorrent on a more regular interval and emits changes
* Stream covers torrent list, transfer rate summary & history, via an event-stream. This significantly reduces data usage on the Flood client
torrent taxonomy, and notification count. - Stream covers torrent list, transfer rate summary & history,
* Close event stream after the window/tab has been inactive for 30 seconds torrent taxonomy, and notification count.
* Refactor development experience, using `Webpack` & `WebpackDevServer` - Close event stream after the window/tab has been inactive for 30 seconds
* Require users to build static assets again - Refactor development experience, using `Webpack` & `WebpackDevServer`
* Simplify peer geo flag handling - Require users to build static assets again
* Flag images now serves as static asset - Simplify peer geo flag handling
* moveTorrents: Allow hash check to be skipped by user - Flag images now serves as static asset
* Add an option to completely disable users and authentication - moveTorrents: Allow hash check to be skipped by user
* server: Takes baseURI into account for routes and assets - Add an option to completely disable users and authentication
* torrentListPropMap: use d.hashing= instead of d.is_hash_checking= - server: Takes baseURI into account for routes and assets
* Torrents queued for checking are now shown - torrentListPropMap: use d.hashing= instead of d.is_hash_checking=
* sidebar: Add Checking filter view - Torrents queued for checking are now shown
- sidebar: Add Checking filter view
## [1.0.0] (April 21, 2017) ## [1.0.0] (April 21, 2017)
* First "official" release
* Change log and semver versioning (finally)
* Control basic rTorrent settings via web UI
* Transfer rate limiting
* Connection settings
* Resource utilization
* Add torrents via URLs or files
* User authentication
* UI translations (only en, fr, and nl)
* Custom torrent tags
* Customizable torrent list
* "Expanded" and "condensed" views
* Customizable torrent detail columns
* Basic torrent list filtering (by status, tag, and tracker)
* Auto-download torrents from RSS feeds
[Unreleased]:https://github.com/Flood-UI/flood/compare/v1.0.0...HEAD - First "official" release
[1.0.0]:https://github.com/Flood-UI/flood/compare/ae520c0a33ffb4ae6f21e47bc6f7e6007dd1e6dc...v1.0.0 - Change log and semver versioning (finally)
[2.0.0]:https://github.com/jesec/flood/compare/v1.0.0...v2.0.0 - Control basic rTorrent settings via web UI
[3.0.0]:https://github.com/jesec/flood/compare/v2.0.0...v3.0.0 - Transfer rate limiting
[3.1.0]:https://github.com/jesec/flood/compare/v3.0.0...v3.1.0 - Connection settings
[4.0.0]:https://github.com/jesec/flood/compare/v3.1.0...v4.0.0 - Resource utilization
[4.0.1]:https://github.com/jesec/flood/compare/v4.0.0...v4.0.1 - Add torrents via URLs or files
[4.0.2]:https://github.com/jesec/flood/compare/v4.0.1...v4.0.2 - User authentication
- UI translations (only en, fr, and nl)
- Custom torrent tags
- Customizable torrent list
- "Expanded" and "condensed" views
- Customizable torrent detail columns
- Basic torrent list filtering (by status, tag, and tracker)
- Auto-download torrents from RSS feeds
[unreleased]: https://github.com/Flood-UI/flood/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/Flood-UI/flood/compare/ae520c0a33ffb4ae6f21e47bc6f7e6007dd1e6dc...v1.0.0
[2.0.0]: https://github.com/jesec/flood/compare/v1.0.0...v2.0.0
[3.0.0]: https://github.com/jesec/flood/compare/v2.0.0...v3.0.0
[3.1.0]: https://github.com/jesec/flood/compare/v3.0.0...v3.1.0
[4.0.0]: https://github.com/jesec/flood/compare/v3.1.0...v4.0.0
[4.0.1]: https://github.com/jesec/flood/compare/v4.0.0...v4.0.1
[4.0.2]: https://github.com/jesec/flood/compare/v4.0.1...v4.0.2

View File

@@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at me@johnfurrow.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -7,8 +7,9 @@
Flood is a monitoring service for various torrent clients. It's a Node.js service that communicates with your favorite torrent client and serves a decent web UI for administration. This project is based on the [original Flood project](https://github.com/Flood-UI/flood). Flood is a monitoring service for various torrent clients. It's a Node.js service that communicates with your favorite torrent client and serves a decent web UI for administration. This project is based on the [original Flood project](https://github.com/Flood-UI/flood).
#### Supported Clients #### Supported Clients
| Client | Support | | Client | Support |
|--------------------------------------------------------------|--------------------------------------| | ------------------------------------------------------------ | ------------------------------------ |
| [rTorrent](https://github.com/rakshasa/rtorrent) | Stable and Tested :white_check_mark: | | [rTorrent](https://github.com/rakshasa/rtorrent) | Stable and Tested :white_check_mark: |
| [qBittorrent](https://github.com/qbittorrent/qBittorrent) | Experimental :alembic: | | [qBittorrent](https://github.com/qbittorrent/qBittorrent) | Experimental :alembic: |
| [Transmission](https://github.com/transmission/transmission) | Experimental :alembic: | | [Transmission](https://github.com/transmission/transmission) | Experimental :alembic: |
@@ -26,6 +27,7 @@ Check out the [Wiki](https://github.com/jesec/flood/wiki) for more information.
### Pre-Requisites ### Pre-Requisites
Install [Node.js runtime](https://nodejs.org/). Flood tracks `Current` and provides support to `Active LTS` as well. Install [Node.js runtime](https://nodejs.org/). Flood tracks `Current` and provides support to `Active LTS` as well.
- Debian, Ubuntu and RHEL-based distributions users can install latest `nodejs` from [NodeSource](https://github.com/nodesource/distributions) software repository. - Debian, Ubuntu and RHEL-based distributions users can install latest `nodejs` from [NodeSource](https://github.com/nodesource/distributions) software repository.
- Windows and MacOS users may use installer. - Windows and MacOS users may use installer.

View File

@@ -2,7 +2,7 @@
## Supported Versions ## Supported Versions
| Version | Supported | | Version | Supported |
| ------------------------ | ------------------ | | ------------------------ | ------------------ |
| HEAD of master | :white_check_mark: | | HEAD of master | :white_check_mark: |
| Latest released revision | :white_check_mark: | | Latest released revision | :white_check_mark: |
@@ -12,7 +12,7 @@ Flood does NOT provide LTS support. Older revisions are deprecated as soon as a
Generally only the latest revision (`HEAD of master`) is supported. However, [issue tracker](https://github.com/jesec/flood/issues) is still open to reports from users of the `Latest released revision`. In rare circumstances, if there is a vulnerability that requires urgent attention, and, if the `HEAD of master` is occupied with changes which maintainers are not comfortable to release, relevant changes may be backported to the `Latest released revision` to release a patch revision. Generally only the latest revision (`HEAD of master`) is supported. However, [issue tracker](https://github.com/jesec/flood/issues) is still open to reports from users of the `Latest released revision`. In rare circumstances, if there is a vulnerability that requires urgent attention, and, if the `HEAD of master` is occupied with changes which maintainers are not comfortable to release, relevant changes may be backported to the `Latest released revision` to release a patch revision.
You are advised to upgrade to the latest release as soon as possible. You are advised to upgrade to the latest release as soon as possible.
## Reporting a Vulnerability ## Reporting a Vulnerability
@@ -20,7 +20,7 @@ If you discover a security vulnerability within Flood, please send an e-mail to
Suggestions for general security enhancements and/or mitigations shall be reported to [issue tracker](https://github.com/jesec/flood/issues). Suggestions for general security enhancements and/or mitigations shall be reported to [issue tracker](https://github.com/jesec/flood/issues).
If you are unsure about the severity, send an email first. If you are unsure about the severity, send an email first.
## More information ## More information

View File

@@ -1,50 +0,0 @@
const path = require('path');
module.exports = {
extends: '../.eslintrc',
env: {
browser: 1,
node: 0,
},
globals: {
global: 'writable',
process: 'writable',
window: 'writable',
},
rules: {
'import/no-extraneous-dependencies': 0,
'no-restricted-imports': [
'error',
{
patterns: ['**/config', '**/server/**/*'],
},
],
'no-restricted-modules': [
'error',
{
patterns: ['**/server/**/*'],
},
],
// TODO: Enable a11y features
'jsx-a11y/click-events-have-key-events': 0,
'jsx-a11y/control-has-associated-label': 0,
'jsx-a11y/label-has-associated-control': 0,
'jsx-a11y/label-has-for': 0,
'jsx-a11y/mouse-events-have-key-events': 0,
'jsx-a11y/no-noninteractive-element-interactions': 0,
'jsx-a11y/no-static-element-interactions': 0,
'no-console': [2, {allow: ['warn', 'error']}],
'react/destructuring-assignment': 0,
'react/jsx-props-no-spreading': 0,
'react/jsx-uses-react': 0,
'react/react-in-jsx-scope': 0,
'react/static-property-placement': [2, 'static public field'],
},
settings: {
'import/resolver': {
webpack: {
config: path.join(__dirname, 'config/webpack.config.dev.js'),
},
},
},
};

View File

@@ -1,10 +0,0 @@
module.exports = {
env: {
browser: 0,
node: 1,
},
rules: {
'no-console': 0,
'global-require': 0,
},
};

View File

@@ -1,12 +0,0 @@
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/tutorial-webpack.html
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
// The output is always the same.
return 'cssTransform';
},
};

View File

@@ -1,10 +0,0 @@
const path = require('path');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/tutorial-webpack.html
module.exports = {
process(src, filename) {
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
},
};

View File

@@ -1,10 +0,0 @@
module.exports = {
env: {
browser: 0,
node: 1,
},
rules: {
'no-console': 0,
'global-require': 0,
},
};

67
client/src/.eslintrc.json Normal file
View File

@@ -0,0 +1,67 @@
{
"extends": [
"react-app",
"airbnb-typescript",
"plugin:@typescript-eslint/recommended",
"prettier",
"prettier/@typescript-eslint",
"prettier/react"
],
"parserOptions": {
"project": "./tsconfig.json"
},
"env": {
"browser": true,
"node": false
},
"globals": {
"global": "writable",
"process": "writable",
"window": "writable"
},
"rules": {
"import/no-extraneous-dependencies": 0,
"no-restricted-imports": [
"error",
{
"patterns": ["**/config", "**/server/**/*"]
}
],
"no-restricted-modules": [
"error",
{
"patterns": ["**/config", "**/server/**/*"]
}
],
// TODO: Enable a11y features
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/control-has-associated-label": 0,
"jsx-a11y/label-has-associated-control": 0,
"jsx-a11y/label-has-for": 0,
"jsx-a11y/mouse-events-have-key-events": 0,
"jsx-a11y/no-noninteractive-element-interactions": 0,
"jsx-a11y/no-static-element-interactions": 0,
"no-console": [2, {"allow": ["warn", "error"]}],
"no-underscore-dangle": [2, {"allow": ["_id"]}],
"react/destructuring-assignment": 0,
"react/jsx-props-no-spreading": 0,
"react/jsx-uses-react": 0,
"react/react-in-jsx-scope": 0,
"react/static-property-placement": [2, "static public field"],
"@typescript-eslint/lines-between-class-members": ["error", "always", {"exceptAfterSingleLine": true}],
// TODO: Explicit return type
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0
},
"settings": {
"import/resolver": {
"webpack": {
"config": "client/config/webpack.config.dev.js"
}
}
}
}

View File

@@ -1,23 +1,19 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" href="favicon.ico" />
<script type="text/javascript">
var _jipt = [];
_jipt.push(['project', 'flood']);
</script>
<title>Flood</title>
</head>
<head> <body>
<meta charset="utf-8"> <noscript> You need to enable JavaScript to run this app. </noscript>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <div id="app"></div>
<meta name="theme-color" content="#000000"> </body>
<link rel="shortcut icon" href="favicon.ico"> </html>
<script type="text/javascript">
var _jipt = [];
_jipt.push(['project', 'flood']);
</script>
<title>Flood</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="app"></div>
</body>
</html>

View File

@@ -76,12 +76,18 @@ class AuthForm extends React.Component<AuthFormProps, AuthFormStates> {
const formData = submission.formData as Partial<LoginFormData> | Partial<RegisterFormData>; const formData = submission.formData as Partial<LoginFormData> | Partial<RegisterFormData>;
if (formData.username == null || formData.username === '') { if (formData.username == null || formData.username === '') {
this.setState({isSubmitting: false, errorMessage: intl.formatMessage({id: 'auth.error.username.empty'})}); this.setState({
isSubmitting: false,
errorMessage: intl.formatMessage({id: 'auth.error.username.empty'}),
});
return; return;
} }
if (formData.password == null || formData.password === '') { if (formData.password == null || formData.password === '') {
this.setState({isSubmitting: false, errorMessage: intl.formatMessage({id: 'auth.error.password.empty'})}); this.setState({
isSubmitting: false,
errorMessage: intl.formatMessage({id: 'auth.error.password.empty'}),
});
return; return;
} }
@@ -102,13 +108,23 @@ class AuthForm extends React.Component<AuthFormProps, AuthFormStates> {
const config = formData as RegisterFormData; const config = formData as RegisterFormData;
if (this.settingsFormRef.current == null) { if (this.settingsFormRef.current == null) {
this.setState({isSubmitting: false, errorMessage: intl.formatMessage({id: 'connection.settings.error.empty'})}); this.setState({
isSubmitting: false,
errorMessage: intl.formatMessage({
id: 'connection.settings.error.empty',
}),
});
return; return;
} }
const connectionSettings = this.settingsFormRef.current.getConnectionSettings(); const connectionSettings = this.settingsFormRef.current.getConnectionSettings();
if (connectionSettings == null) { if (connectionSettings == null) {
this.setState({isSubmitting: false, errorMessage: intl.formatMessage({id: 'connection.settings.error.empty'})}); this.setState({
isSubmitting: false,
errorMessage: intl.formatMessage({
id: 'connection.settings.error.empty',
}),
});
return; return;
} }

View File

@@ -45,7 +45,9 @@ const ClientConnectionInterruption: FC = observer(() => {
} }
try { try {
await AuthActions.updateUser(currentUsername, {client: connectionSettings}) await AuthActions.updateUser(currentUsername, {
client: connectionSettings,
})
.then(() => { .then(() => {
// do nothing. // do nothing.
}) })

View File

@@ -1,7 +1,41 @@
import {FC, ReactNode} from 'react'; import {FC, ReactNode} from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import formatUtil from '@shared/util/formatUtil'; const secondsToDuration = (
cumSeconds: number,
): {
years?: number;
weeks?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
cumSeconds: number;
} => {
const years = Math.floor(cumSeconds / 31536000);
const weeks = Math.floor((cumSeconds % 31536000) / 604800);
const days = Math.floor(((cumSeconds % 31536000) % 604800) / 86400);
const hours = Math.floor((((cumSeconds % 31536000) % 604800) % 86400) / 3600);
const minutes = Math.floor(((((cumSeconds % 31536000) % 604800) % 86400) % 3600) / 60);
const seconds = Math.floor(cumSeconds - minutes * 60);
let timeRemaining = null;
if (years > 0) {
timeRemaining = {years, weeks, cumSeconds};
} else if (weeks > 0) {
timeRemaining = {weeks, days, cumSeconds};
} else if (days > 0) {
timeRemaining = {days, hours, cumSeconds};
} else if (hours > 0) {
timeRemaining = {hours, minutes, cumSeconds};
} else if (minutes > 0) {
timeRemaining = {minutes, seconds, cumSeconds};
} else {
timeRemaining = {seconds, cumSeconds};
}
return timeRemaining;
};
interface DurationProps { interface DurationProps {
suffix?: ReactNode; suffix?: ReactNode;
@@ -22,7 +56,7 @@ const Duration: FC<DurationProps> = (props: DurationProps) => {
suffixElement = <span className="duration--segment">{suffix}</span>; suffixElement = <span className="duration--segment">{suffix}</span>;
} }
const duration = value === -1 ? -1 : formatUtil.secondsToDuration(value); const duration = value === -1 ? -1 : secondsToDuration(value);
if (duration === -1) { if (duration === -1) {
content = <FormattedMessage id="unit.time.infinity" />; content = <FormattedMessage id="unit.time.infinity" />;

View File

@@ -34,12 +34,15 @@ const Overflow = forwardRef<HTMLDivElement, ComponentProps<'div'>>((props: Compo
viewport.removeEventListener('scroll', (e) => onScroll((e as unknown) as UIEvent<HTMLDivElement>)); viewport.removeEventListener('scroll', (e) => onScroll((e as unknown) as UIEvent<HTMLDivElement>));
} }
}; };
}, [onScroll]); }, [onScroll, ref]);
return ( return (
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
{...props} {...props}
options={{scrollbars: {autoHide: 'leave', clickScrolling: true}, className}} options={{
scrollbars: {autoHide: 'leave', clickScrolling: true},
className,
}}
ref={osRef}> ref={osRef}>
{children} {children}
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>

View File

@@ -11,36 +11,38 @@ const ICONS = {
satisfied: <Checkmark />, satisfied: <Checkmark />,
}; };
interface LoadingOverlayProps { const LoadingDependencyList: FC<{dependencies: Dependencies}> = ({dependencies}: {dependencies: Dependencies}) => {
dependencies?: Dependencies; const intl = useIntl();
}
const LoadingOverlay: FC<LoadingOverlayProps> = (props: LoadingOverlayProps) => { return (
<ul className="dependency-list">
{Object.keys(dependencies).map((id: string) => {
const {message, satisfied} = dependencies[id];
const statusIcon = ICONS.satisfied;
const classes = classnames('dependency-list__dependency', {
'dependency-list__dependency--satisfied': satisfied,
});
return (
<li className={classes} key={id}>
{satisfied != null ? <span className="dependency-list__dependency__icon">{statusIcon}</span> : null}
<span className="dependency-list__dependency__message">
{typeof message === 'string' ? message : intl.formatMessage(message)}
</span>
</li>
);
})}
</ul>
);
};
const LoadingOverlay: FC<{dependencies?: Dependencies}> = (props: {dependencies?: Dependencies}) => {
const {dependencies} = props; const {dependencies} = props;
return ( return (
<div className="application__loading-overlay"> <div className="application__loading-overlay">
<LoadingIndicator inverse /> <LoadingIndicator inverse />
<ul className="dependency-list"> {dependencies != null ? <LoadingDependencyList dependencies={dependencies} /> : null}
{dependencies != null
? Object.keys(dependencies).map((id: string) => {
const {message, satisfied} = dependencies[id];
const statusIcon = ICONS.satisfied;
const classes = classnames('dependency-list__dependency', {
'dependency-list__dependency--satisfied': satisfied,
});
return (
<li className={classes} key={id}>
{satisfied != null ? <span className="dependency-list__dependency__icon">{statusIcon}</span> : null}
<span className="dependency-list__dependency__message">
{typeof message === 'string' ? message : useIntl().formatMessage(message)}
</span>
</li>
);
})
: null}
</ul>
</div> </div>
); );
}; };

View File

@@ -22,7 +22,9 @@ const Size: FC<SizeProps> = ({value, isSpeed, className, precision}: SizeProps)
const computed = compute(value, precision); const computed = compute(value, precision);
const intl = useIntl(); const intl = useIntl();
let translatedUnit = intl.formatMessage({id: getTranslationString(computed.unit)}); let translatedUnit = intl.formatMessage({
id: getTranslationString(computed.unit),
});
if (isSpeed) { if (isSpeed) {
translatedUnit = intl.formatMessage( translatedUnit = intl.formatMessage(

View File

@@ -333,12 +333,12 @@ class Tooltip extends React.Component<TooltipProps, TooltipStates> {
}; };
addScrollListener(): void { addScrollListener(): void {
this.container.addEventListener('scroll', (_e) => this.dismissTooltip()); this.container.addEventListener('scroll', () => this.dismissTooltip());
} }
removeScrollListener(): void { removeScrollListener(): void {
if (this.container) { if (this.container) {
this.container.removeEventListener('scroll', (_e) => this.dismissTooltip()); this.container.removeEventListener('scroll', () => this.dismissTooltip());
} }
} }
@@ -403,8 +403,8 @@ class Tooltip extends React.Component<TooltipProps, TooltipStates> {
<div <div
className={wrapperClassName} className={wrapperClassName}
onClick={onClick} onClick={onClick}
onMouseEnter={(_e) => this.handleMouseEnter()} onMouseEnter={() => this.handleMouseEnter()}
onMouseLeave={(_e) => this.handleMouseLeave()} onMouseLeave={() => this.handleMouseLeave()}
ref={(ref) => { ref={(ref) => {
this.triggerNode = ref; this.triggerNode = ref;
}}> }}>

View File

@@ -76,9 +76,13 @@ class ClientConnectionSettingsForm extends React.Component<WrappedComponentProps
<FormRow> <FormRow>
<Select <Select
id="client" id="client"
label={intl.formatMessage({id: 'connection.settings.client.select'})} label={intl.formatMessage({
id: 'connection.settings.client.select',
})}
onSelect={(selectedClient) => { onSelect={(selectedClient) => {
this.setState({client: selectedClient as ClientConnectionSettings['client']}); this.setState({
client: selectedClient as ClientConnectionSettings['client'],
});
}} }}
defaultID={DEFAULT_SELECTION}> defaultID={DEFAULT_SELECTION}>
{getClientSelectItems()} {getClientSelectItems()}

View File

@@ -55,7 +55,10 @@ class DirectoryFiles extends React.Component<DirectoryFilesProps> {
handlePriorityChange = (fileIndex: React.ReactText, priorityLevel: number): void => { handlePriorityChange = (fileIndex: React.ReactText, priorityLevel: number): void => {
const {hash} = this.props; const {hash} = this.props;
TorrentActions.setFilePriority(hash, {indices: [Number(fileIndex)], priority: priorityLevel}); TorrentActions.setFilePriority(hash, {
indices: [Number(fileIndex)],
priority: priorityLevel,
});
}; };
handleFileSelect = (file: TorrentContent, isSelected: boolean): void => { handleFileSelect = (file: TorrentContent, isSelected: boolean): void => {

View File

@@ -18,7 +18,7 @@ const FileDropzone: FC<FileDropzoneProps> = ({onFilesChanged}: FileDropzoneProps
useEffect(() => { useEffect(() => {
onFilesChanged(files); onFilesChanged(files);
}, [files]); }, [files, onFilesChanged]);
return ( return (
<FormRowItem> <FormRowItem>

View File

@@ -61,7 +61,10 @@ const TextboxRepeater: FC<TextboxRepeaterProps> = ({defaultValues, id, label, pl
idCounter.current += 1; idCounter.current += 1;
const newTextboxes = textboxes.slice(); const newTextboxes = textboxes.slice();
newTextboxes.splice(index + 1, 0, {id: idCounter.current, value: ''}); newTextboxes.splice(index + 1, 0, {
id: idCounter.current,
value: '',
});
setTextboxes(newTextboxes); setTextboxes(newTextboxes);
}}> }}>
<AddMini size="mini" /> <AddMini size="mini" />

View File

@@ -59,9 +59,8 @@ const Modal: FC<ModalProps> = (props: ModalProps) => {
let footer; let footer;
let headerTabs; let headerTabs;
const [activeTabId, setActiveTabId] = useState<string>(initialTabId ?? Object.keys(tabs || {})[0]);
if (tabs) { if (tabs) {
const [activeTabId, setActiveTabId] = useState(initialTabId ?? Object.keys(tabs)[0]);
const activeTab = tabs[activeTabId]; const activeTab = tabs[activeTabId];
const contentClasses = classnames('modal__content', activeTab.modalContentClasses); const contentClasses = classnames('modal__content', activeTab.modalContentClasses);

View File

@@ -120,7 +120,11 @@ const AddTorrentsByCreation: FC = () => {
UIStore.dismissModal(); UIStore.dismissModal();
}); });
saveAddTorrentsUserPreferences({start: formData.start, destination: formData.sourcePath, tab: 'by-creation'}); saveAddTorrentsUserPreferences({
start: formData.start,
destination: formData.sourcePath,
tab: 'by-creation',
});
}} }}
isAddingTorrents={isCreatingTorrents} isAddingTorrents={isCreatingTorrents}
/> />

View File

@@ -84,7 +84,11 @@ const AddTorrentsByFile: FC = () => {
UIStore.dismissModal(); UIStore.dismissModal();
}); });
saveAddTorrentsUserPreferences({start, destination, tab: 'by-file'}); saveAddTorrentsUserPreferences({
start,
destination,
tab: 'by-file',
});
}} }}
isAddingTorrents={isAddingTorrents} isAddingTorrents={isAddingTorrents}
/> />

View File

@@ -105,7 +105,11 @@ const AddTorrentsByURL: FC = () => {
UIStore.dismissModal(); UIStore.dismissModal();
}); });
saveAddTorrentsUserPreferences({start: formData.start, destination: formData.destination, tab: 'by-url'}); saveAddTorrentsUserPreferences({
start: formData.start,
destination: formData.destination,
tab: 'by-url',
});
}} }}
isAddingTorrents={isAddingTorrents} isAddingTorrents={isAddingTorrents}
/> />

View File

@@ -352,7 +352,11 @@ class DownloadRulesTab extends React.Component<WrappedComponentProps, DownloadRu
<li <li
className="interactive-list__detail-list__item className="interactive-list__detail-list__item
interactive-list__detail interactive-list__detail--tertiary" interactive-list__detail interactive-list__detail--tertiary"
style={{maxWidth: '50%', overflow: 'hidden', textOverflow: 'ellipsis'}}> style={{
maxWidth: '50%',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
<FormattedMessage id="feeds.match" /> <FormattedMessage id="feeds.match" />
{': '} {': '}
{rule.match} {rule.match}
@@ -489,7 +493,10 @@ class DownloadRulesTab extends React.Component<WrappedComponentProps, DownloadRu
this.setState({doesPatternMatchTest}); this.setState({doesPatternMatchTest});
} }
validateForm(): {errors?: DownloadRulesTabStates['errors']; isValid: boolean} { validateForm(): {
errors?: DownloadRulesTabStates['errors'];
isValid: boolean;
} {
const formData = this.getAmendedFormData(); const formData = this.getAmendedFormData();
if (formData == null) { if (formData == null) {

View File

@@ -500,7 +500,10 @@ class FeedsTab extends React.Component<WrappedComponentProps, FeedsTabStates> {
const feedBrowseForm = input.formData as {feedID: string; search: string}; const feedBrowseForm = input.formData as {feedID: string; search: string};
if ((input.event.target as HTMLInputElement).type !== 'checkbox') { if ((input.event.target as HTMLInputElement).type !== 'checkbox') {
this.setState({selectedFeedID: feedBrowseForm.feedID}); this.setState({selectedFeedID: feedBrowseForm.feedID});
FeedActions.fetchItems({id: feedBrowseForm.feedID, search: feedBrowseForm.search}); FeedActions.fetchItems({
id: feedBrowseForm.feedID,
search: feedBrowseForm.search,
});
} }
}; };
@@ -516,7 +519,10 @@ class FeedsTab extends React.Component<WrappedComponentProps, FeedsTabStates> {
.filter((_item, index) => formData[index]) .filter((_item, index) => formData[index])
.map((item, index) => ({id: index, value: item.urls[0]})); .map((item, index) => ({id: index, value: item.urls[0]}));
UIActions.displayModal({id: 'add-torrents', initialURLs: torrentsToDownload}); UIActions.displayModal({
id: 'add-torrents',
initialURLs: torrentsToDownload,
});
}; };
validateForm(): {errors?: FeedsTabStates['errors']; isValid: boolean} { validateForm(): {errors?: FeedsTabStates['errors']; isValid: boolean} {

View File

@@ -56,7 +56,10 @@ const SetTagsModal: FC = () => {
const tags = formData.tags ? formData.tags.split(',') : []; const tags = formData.tags ? formData.tags.split(',') : [];
setIsSettingTags(true); setIsSettingTags(true);
TorrentActions.setTags({hashes: TorrentStore.selectedTorrents, tags}); TorrentActions.setTags({
hashes: TorrentStore.selectedTorrents,
tags,
});
}, },
isLoading: isSettingTags, isLoading: isSettingTags,
triggerDismiss: false, triggerDismiss: false,

View File

@@ -14,7 +14,10 @@ const SetTrackersModal: FC = () => {
const formRef = useRef<Form>(null); const formRef = useRef<Form>(null);
const intl = useIntl(); const intl = useIntl();
const [isSettingTrackers, setIsSettingTrackers] = useState<boolean>(false); const [isSettingTrackers, setIsSettingTrackers] = useState<boolean>(false);
const [trackerState, setTrackerState] = useState<{isLoadingTrackers: boolean; trackerURLs: Array<string>}>({ const [trackerState, setTrackerState] = useState<{
isLoadingTrackers: boolean;
trackerURLs: Array<string>;
}>({
isLoadingTrackers: false, isLoadingTrackers: false,
trackerURLs: [], trackerURLs: [],
}); });
@@ -58,7 +61,10 @@ const SetTrackersModal: FC = () => {
defaultValues={ defaultValues={
trackerState.trackerURLs.length === 0 trackerState.trackerURLs.length === 0
? undefined ? undefined
: trackerState.trackerURLs.map((url, index) => ({id: index, value: url})) : trackerState.trackerURLs.map((url, index) => ({
id: index,
value: url,
}))
} }
/> />
)} )}
@@ -85,7 +91,10 @@ const SetTrackersModal: FC = () => {
const formData = formRef.current.getFormData() as Record<string, string>; const formData = formRef.current.getFormData() as Record<string, string>;
const trackers = getTextArray(formData, 'trackers').filter((tracker) => tracker !== ''); const trackers = getTextArray(formData, 'trackers').filter((tracker) => tracker !== '');
TorrentActions.setTrackers({hashes: TorrentStore.selectedTorrents, trackers}).then(() => { TorrentActions.setTrackers({
hashes: TorrentStore.selectedTorrents,
trackers,
}).then(() => {
setIsSettingTrackers(false); setIsSettingTrackers(false);
UIStore.dismissModal(); UIStore.dismissModal();
}); });

View File

@@ -98,7 +98,10 @@ class AuthTab extends React.Component<WrappedComponentProps, AuthTabStates> {
level: this.formData.isAdmin === true ? AccessLevel.ADMINISTRATOR : AccessLevel.USER, level: this.formData.isAdmin === true ? AccessLevel.ADMINISTRATOR : AccessLevel.USER,
}) })
.then(AuthActions.fetchUsers, (error) => { .then(AuthActions.fetchUsers, (error) => {
this.setState({addUserError: error.response.data.message, isAddingUser: false}); this.setState({
addUserError: error.response.data.message,
isAddingUser: false,
});
}) })
.then(() => { .then(() => {
if (this.formRef != null) { if (this.formRef != null) {

View File

@@ -42,7 +42,9 @@ class UITab extends SettingsTab {
if (inputElement.type === 'radio') { if (inputElement.type === 'radio') {
this.torrentListViewSize = formData['ui-torrent-size'] as FloodSettings['torrentListViewSize']; this.torrentListViewSize = formData['ui-torrent-size'] as FloodSettings['torrentListViewSize'];
this.props.onSettingsChange({torrentListViewSize: this.torrentListViewSize}); this.props.onSettingsChange({
torrentListViewSize: this.torrentListViewSize,
});
} }
if (inputElement.name === 'language') { if (inputElement.name === 'language') {

View File

@@ -51,7 +51,9 @@ class TorrentListColumnsList extends React.Component<TorrentListColumnsListProps
}; };
}); });
this.props.onSettingsChange({torrentListColumns: changedTorrentListColumns}); this.props.onSettingsChange({
torrentListColumns: changedTorrentListColumns,
});
this.setState({torrentListColumns: changedTorrentListColumns}); this.setState({torrentListColumns: changedTorrentListColumns});
}; };

View File

@@ -30,14 +30,18 @@ const TorrentHeading: FC = observer(() => {
} else { } else {
setTorrentStatus('start'); setTorrentStatus('start');
} }
}, []); }, [torrent?.status]);
if (torrent == null) { if (torrent == null) {
return null; return null;
} }
const torrentClasses = torrentStatusClasses( const torrentClasses = torrentStatusClasses(
{status: torrent.status, upRate: torrent.upRate, downRate: torrent.downRate}, {
status: torrent.status,
upRate: torrent.upRate,
downRate: torrent.downRate,
},
'torrent-details__header', 'torrent-details__header',
); );
const torrentStatusIcon = torrentStatusIcons(torrent.status); const torrentStatusIcon = torrentStatusIcons(torrent.status);
@@ -76,7 +80,10 @@ const TorrentHeading: FC = observer(() => {
maxLevel={3} maxLevel={3}
priorityType="torrent" priorityType="torrent"
onChange={(hash, level) => { onChange={(hash, level) => {
TorrentActions.setPriority({hashes: [`${hash}`], priority: level}); TorrentActions.setPriority({
hashes: [`${hash}`],
priority: level,
});
}} }}
/> />
</li> </li>

View File

@@ -144,7 +144,11 @@ class NotificationsButton extends Component<WrappedComponentProps, Notifications
getNotification = (notification: Notification, index: number) => { getNotification = (notification: Notification, index: number) => {
const {intl} = this.props; const {intl} = this.props;
const date = intl.formatDate(notification.ts, {year: 'numeric', month: 'long', day: '2-digit'}); const date = intl.formatDate(notification.ts, {
year: 'numeric',
month: 'long',
day: '2-digit',
});
const time = intl.formatTime(notification.ts); const time = intl.formatTime(notification.ts);
return ( return (
@@ -160,7 +164,9 @@ class NotificationsButton extends Component<WrappedComponentProps, Notifications
</div> </div>
<div className="notification__message"> <div className="notification__message">
{intl.formatMessage( {intl.formatMessage(
MESSAGES[`${notification.id}.body` as keyof typeof MESSAGES] || {id: 'general.error.unknown'}, MESSAGES[`${notification.id}.body` as keyof typeof MESSAGES] || {
id: 'general.error.unknown',
},
notification.data, notification.data,
)} )}
</div> </div>

View File

@@ -18,7 +18,11 @@ const Sidebar: FC = () => {
return ( return (
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
options={{ options={{
scrollbars: {autoHide: 'scroll', clickScrolling: false, dragScrolling: false}, scrollbars: {
autoHide: 'scroll',
clickScrolling: false,
dragScrolling: false,
},
className: 'application__sidebar os-theme-thin', className: 'application__sidebar os-theme-thin',
}}> }}>
<SidebarActions> <SidebarActions>

View File

@@ -49,7 +49,13 @@ class TransferRateGraph extends React.Component<TransferRateGraphProps> {
inspectPoint?: Selection<SVGCircleElement, unknown, HTMLElement, unknown>; inspectPoint?: Selection<SVGCircleElement, unknown, HTMLElement, unknown>;
rateLine?: Selection<SVGPathElement, unknown, HTMLElement, unknown>; rateLine?: Selection<SVGPathElement, unknown, HTMLElement, unknown>;
} }
> = {graph: null, areDefined: false, isHovered: false, download: {}, upload: {}}; > = {
graph: null,
areDefined: false,
isHovered: false,
download: {},
upload: {},
};
static defaultProps = { static defaultProps = {
width: 240, width: 240,

View File

@@ -53,7 +53,11 @@ const TorrentDropzone: FC<{children: ReactNode}> = ({children}: {children: React
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
}; };
const {getRootProps, isDragActive} = useDropzone({onDrop: handleFileDrop, noClick: true, noKeyboard: true}); const {getRootProps, isDragActive} = useDropzone({
onDrop: handleFileDrop,
noClick: true,
noKeyboard: true,
});
return ( return (
<div <div
@@ -96,7 +100,10 @@ class TorrentList extends Component<WrappedComponentProps> {
listHeaderRef = createRef<HTMLDivElement>(); listHeaderRef = createRef<HTMLDivElement>();
listViewportRef = createRef<FixedSizeList>(); listViewportRef = createRef<FixedSizeList>();
torrentListViewportSize = observable.object<{width: number; height: number}>({ torrentListViewportSize = observable.object<{
width: number;
height: number;
}>({
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
}); });

View File

@@ -81,7 +81,9 @@ class UIStore {
dependencies: Dependencies = {}; dependencies: Dependencies = {};
globalStyles: Array<string> = []; globalStyles: Array<string> = [];
haveUIDependenciesResolved = false; haveUIDependenciesResolved = false;
styleElement: HTMLStyleElement & {styleSheet?: {cssText: string}} = this.createStyleElement(); styleElement: HTMLStyleElement & {
styleSheet?: {cssText: string};
} = this.createStyleElement();
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);

View File

@@ -39,7 +39,13 @@ export default class Button extends React.Component<ButtonProps> {
getButtonContent() { getButtonContent() {
const {children, addonPlacement} = this.props; const {children, addonPlacement} = this.props;
const buttonContent = React.Children.toArray(children).reduce( const buttonContent = React.Children.toArray(children).reduce(
(accumulator: {addonNodes: Array<React.ReactNode>; childNodes: Array<React.ReactNode>}, child) => { (
accumulator: {
addonNodes: Array<React.ReactNode>;
childNodes: Array<React.ReactNode>;
},
child,
) => {
const childAsElement = child as React.ReactElement; const childAsElement = child as React.ReactElement;
if (childAsElement.type === FormElementAddon) { if (childAsElement.type === FormElementAddon) {
accumulator.addonNodes.push( accumulator.addonNodes.push(

View File

@@ -4,7 +4,10 @@ import FormRowItem from './FormRowItem';
import type {FormRowItemProps} from './FormRowItem'; import type {FormRowItemProps} from './FormRowItem';
export default class FormRowItemGroup extends React.Component<{label?: string; width?: FormRowItemProps['width']}> { export default class FormRowItemGroup extends React.Component<{
label?: string;
width?: FormRowItemProps['width'];
}> {
getLabel(): React.ReactNode { getLabel(): React.ReactNode {
const {label} = this.props; const {label} = this.props;

View File

@@ -3,6 +3,8 @@ import {createBrowserHistory} from 'history';
import stringUtil from '@shared/util/stringUtil'; import stringUtil from '@shared/util/stringUtil';
import ConfigStore from '../stores/ConfigStore'; import ConfigStore from '../stores/ConfigStore';
const history = createBrowserHistory({basename: stringUtil.withoutTrailingSlash(ConfigStore.baseURI)}); const history = createBrowserHistory({
basename: stringUtil.withoutTrailingSlash(ConfigStore.baseURI),
});
export default history; export default history;

View File

@@ -1,9 +1,12 @@
context('Login', () => { context('Login', () => {
beforeEach(() => { beforeEach(() => {
cy.server(); cy.server();
cy.route({method: 'GET', url: 'http://127.0.0.1:4200/api/auth/verify?*', response: {}, status: 401}).as( cy.route({
'verify-request', method: 'GET',
); url: 'http://127.0.0.1:4200/api/auth/verify?*',
response: {},
status: 401,
}).as('verify-request');
cy.visit('http://127.0.0.1:4200/login'); cy.visit('http://127.0.0.1:4200/login');
cy.url().should('include', 'login'); cy.url().should('include', 'login');
}); });

View File

@@ -84,9 +84,12 @@ context('Register', () => {
cy.get('.input--text[name="socket"]').type('/data/rtorrent.sock'); cy.get('.input--text[name="socket"]').type('/data/rtorrent.sock');
cy.server(); cy.server();
cy.route({method: 'POST', url: 'http://127.0.0.1:4200/api/auth/register', response: {}, status: 500}).as( cy.route({
'register-request', method: 'POST',
); url: 'http://127.0.0.1:4200/api/auth/register',
response: {},
status: 500,
}).as('register-request');
cy.get('.button[type="submit"]').click(); cy.get('.button[type="submit"]').click();

270
package-lock.json generated
View File

@@ -107,10 +107,8 @@
"http-errors": "^1.8.0", "http-errors": "^1.8.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"jsdoc": "^3.6.6",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"minami": "^1.2.3",
"mini-css-extract-plugin": "^1.3.1", "mini-css-extract-plugin": "^1.3.1",
"mobx": "^6.0.4", "mobx": "^6.0.4",
"mobx-react": "^7.0.5", "mobx-react": "^7.0.5",
@@ -4518,12 +4516,6 @@
"readable-stream": "^3.4.0" "readable-stream": "^3.4.0"
} }
}, },
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
"node_modules/bn.js": { "node_modules/bn.js": {
"version": "5.1.3", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz",
@@ -5026,18 +5018,6 @@
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true "dev": true
}, },
"node_modules/catharsis": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz",
"integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
@@ -12878,73 +12858,11 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/js2xmlparser": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz",
"integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==",
"dev": true,
"dependencies": {
"xmlcreate": "^2.0.3"
}
},
"node_modules/jsbn": { "node_modules/jsbn": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA="
}, },
"node_modules/jsdoc": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.6.tgz",
"integrity": "sha512-znR99e1BHeyEkSvgDDpX0sTiTu+8aQyDl9DawrkOGZTTW8hv0deIFXx87114zJ7gRaDZKVQD/4tr1ifmJp9xhQ==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.9.4",
"bluebird": "^3.7.2",
"catharsis": "^0.8.11",
"escape-string-regexp": "^2.0.0",
"js2xmlparser": "^4.0.1",
"klaw": "^3.0.0",
"markdown-it": "^10.0.0",
"markdown-it-anchor": "^5.2.7",
"marked": "^0.8.2",
"mkdirp": "^1.0.4",
"requizzle": "^0.2.3",
"strip-json-comments": "^3.1.0",
"taffydb": "2.6.2",
"underscore": "~1.10.2"
},
"bin": {
"jsdoc": "jsdoc.js"
},
"engines": {
"node": ">=8.15.0"
}
},
"node_modules/jsdoc/node_modules/linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"dev": true,
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/jsdoc/node_modules/markdown-it": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/jsdom": { "node_modules/jsdom": {
"version": "16.4.0", "version": "16.4.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz",
@@ -13194,15 +13112,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/klaw": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
"integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.9"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -13630,27 +13539,6 @@
"markdown-it": "bin/markdown-it.js" "markdown-it": "bin/markdown-it.js"
} }
}, },
"node_modules/markdown-it-anchor": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz",
"integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==",
"dev": true,
"peerDependencies": {
"markdown-it": "*"
}
},
"node_modules/marked": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz",
"integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==",
"dev": true,
"bin": {
"marked": "bin/marked"
},
"engines": {
"node": ">= 8.16.2"
}
},
"node_modules/md5.js": { "node_modules/md5.js": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -13963,12 +13851,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/minami": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/minami/-/minami-1.2.3.tgz",
"integrity": "sha1-mbbc37LwpU2hycj3qjoyd4eq+fg=",
"dev": true
},
"node_modules/mini-create-react-context": { "node_modules/mini-create-react-context": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
@@ -20886,15 +20768,6 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true "dev": true
}, },
"node_modules/requizzle": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
"integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/resize-observer-polyfill": { "node_modules/resize-observer-polyfill": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -23863,12 +23736,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/taffydb": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz",
"integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=",
"dev": true
},
"node_modules/tapable": { "node_modules/tapable": {
"version": "0.1.10", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz",
@@ -24601,12 +24468,6 @@
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"dev": true "dev": true
}, },
"node_modules/underscore": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
"integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==",
"dev": true
},
"node_modules/unicode-canonical-property-names-ecmascript": { "node_modules/unicode-canonical-property-names-ecmascript": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -26355,12 +26216,6 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true "dev": true
}, },
"node_modules/xmlcreate": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz",
"integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==",
"dev": true
},
"node_modules/xmlrpc": { "node_modules/xmlrpc": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz", "resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz",
@@ -30204,12 +30059,6 @@
"readable-stream": "^3.4.0" "readable-stream": "^3.4.0"
} }
}, },
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
"bn.js": { "bn.js": {
"version": "5.1.3", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz",
@@ -30655,15 +30504,6 @@
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true "dev": true
}, },
"catharsis": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz",
"integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"chalk": { "chalk": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
@@ -36909,66 +36749,11 @@
"esprima": "^4.0.0" "esprima": "^4.0.0"
} }
}, },
"js2xmlparser": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz",
"integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==",
"dev": true,
"requires": {
"xmlcreate": "^2.0.3"
}
},
"jsbn": { "jsbn": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA="
}, },
"jsdoc": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.6.tgz",
"integrity": "sha512-znR99e1BHeyEkSvgDDpX0sTiTu+8aQyDl9DawrkOGZTTW8hv0deIFXx87114zJ7gRaDZKVQD/4tr1ifmJp9xhQ==",
"dev": true,
"requires": {
"@babel/parser": "^7.9.4",
"bluebird": "^3.7.2",
"catharsis": "^0.8.11",
"escape-string-regexp": "^2.0.0",
"js2xmlparser": "^4.0.1",
"klaw": "^3.0.0",
"markdown-it": "^10.0.0",
"markdown-it-anchor": "^5.2.7",
"marked": "^0.8.2",
"mkdirp": "^1.0.4",
"requizzle": "^0.2.3",
"strip-json-comments": "^3.1.0",
"taffydb": "2.6.2",
"underscore": "~1.10.2"
},
"dependencies": {
"linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"dev": true,
"requires": {
"uc.micro": "^1.0.1"
}
},
"markdown-it": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
}
}
},
"jsdom": { "jsdom": {
"version": "16.4.0", "version": "16.4.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz",
@@ -37172,15 +36957,6 @@
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true "dev": true
}, },
"klaw": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
"integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.9"
}
},
"kleur": { "kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -37544,19 +37320,6 @@
"uc.micro": "^1.0.5" "uc.micro": "^1.0.5"
} }
}, },
"markdown-it-anchor": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz",
"integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==",
"dev": true,
"requires": {}
},
"marked": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz",
"integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==",
"dev": true
},
"md5.js": { "md5.js": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -37809,12 +37572,6 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true "dev": true
}, },
"minami": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/minami/-/minami-1.2.3.tgz",
"integrity": "sha1-mbbc37LwpU2hycj3qjoyd4eq+fg=",
"dev": true
},
"mini-create-react-context": { "mini-create-react-context": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
@@ -43240,15 +42997,6 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true "dev": true
}, },
"requizzle": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
"integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"resize-observer-polyfill": { "resize-observer-polyfill": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -45675,12 +45423,6 @@
} }
} }
}, },
"taffydb": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz",
"integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=",
"dev": true
},
"tapable": { "tapable": {
"version": "0.1.10", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz",
@@ -46247,12 +45989,6 @@
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"dev": true "dev": true
}, },
"underscore": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
"integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==",
"dev": true
},
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -47653,12 +47389,6 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true "dev": true
}, },
"xmlcreate": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz",
"integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==",
"dev": true
},
"xmlrpc": { "xmlrpc": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz", "resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz",

View File

@@ -31,9 +31,7 @@
"build": "npm run build-assets && npm run build-ts", "build": "npm run build-assets && npm run build-ts",
"build-assets": "node client/scripts/build.js", "build-assets": "node client/scripts/build.js",
"build-ts": "ncc build server/bin/start.ts -m -t -e geoip-country", "build-ts": "ncc build server/bin/start.ts -m -t -e geoip-country",
"build-docs": "jsdoc -c ./.jsdoc.json",
"build-i18n": "formatjs compile --ast --format simple client/src/javascript/i18n/strings.json --out-file client/src/javascript/i18n/strings.compiled.json && formatjs compile-folder --ast --format simple client/src/javascript/i18n/translations client/src/javascript/i18n/compiled", "build-i18n": "formatjs compile --ast --format simple client/src/javascript/i18n/strings.json --out-file client/src/javascript/i18n/strings.compiled.json && formatjs compile-folder --ast --format simple client/src/javascript/i18n/translations client/src/javascript/i18n/compiled",
"deprecated-warning": "node client/scripts/deprecated-warning.js && sleep 10",
"format-source": "prettier -w .", "format-source": "prettier -w .",
"check-compiled-i18n": "npm run build-i18n && npm run format-source && git diff --quiet client/src/javascript/i18n/compiled", "check-compiled-i18n": "npm run build-i18n && npm run format-source && git diff --quiet client/src/javascript/i18n/compiled",
"check-source-formatting": "prettier -c .", "check-source-formatting": "prettier -c .",
@@ -44,8 +42,6 @@
"start:development": "UPDATED_SCRIPT=start:development:server npm run deprecated-warning && npm run start:development:server", "start:development": "UPDATED_SCRIPT=start:development:server npm run deprecated-warning && npm run start:development:server",
"start:development:client": "node client/scripts/start.js", "start:development:client": "node client/scripts/start.js",
"start:development:server": "NODE_ENV=development TS_NODE_PROJECT=server/tsconfig.json ts-node-dev --transpile-only server/bin/start.ts", "start:development:server": "NODE_ENV=development TS_NODE_PROJECT=server/tsconfig.json ts-node-dev --transpile-only server/bin/start.ts",
"start:production": "UPDATED_SCRIPT=start npm run deprecated-warning && npm start",
"start:watch": "UPDATED_SCRIPT=start:development:client npm run deprecated-warning && npm run start:development:client",
"test": "jest --forceExit", "test": "jest --forceExit",
"test:watch": "jest --watchAll --forceExit", "test:watch": "jest --watchAll --forceExit",
"test:client": "FLOOD_OPTION_port=4200 start-server-and-test start 4200 'cypress run'" "test:client": "FLOOD_OPTION_port=4200 start-server-and-test start 4200 'cypress run'"
@@ -147,10 +143,8 @@
"http-errors": "^1.8.0", "http-errors": "^1.8.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"jsdoc": "^3.6.6",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"minami": "^1.2.3",
"mini-css-extract-plugin": "^1.3.1", "mini-css-extract-plugin": "^1.3.1",
"mobx": "^6.0.4", "mobx": "^6.0.4",
"mobx-react": "^7.0.5", "mobx-react": "^7.0.5",
@@ -202,9 +196,6 @@
"yargs": "^16.1.0", "yargs": "^16.1.0",
"zod": "^1.11.10" "zod": "^1.11.10"
}, },
"eslintConfig": {
"extends": "react-app"
},
"engines": { "engines": {
"node": ">=12.0.0", "node": ">=12.0.0",
"npm": ">=6.0.0" "npm": ">=6.0.0"

View File

@@ -1,24 +0,0 @@
module.exports = {
extends: '../.eslintrc',
env: {
browser: 0,
node: 1,
},
rules: {
'no-console': 0,
'no-restricted-imports': [
'error',
{
patterns: ['**/client/**/*'],
},
],
'no-restricted-modules': [
'error',
{
patterns: ['**/client/**/*'],
},
],
},
};

21
server/.eslintrc.json Normal file
View File

@@ -0,0 +1,21 @@
{
"extends": "../.eslintrc.json",
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": ["**/client/**/*"]
}
],
"no-restricted-modules": [
"error",
{
"patterns": ["**/client/**/*"]
}
],
// TODO: Explicit return type
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0
}
}

View File

@@ -50,7 +50,9 @@ app.use(`${paths.servedPath}api`, apiRoutes);
app.use(paths.servedPath, express.static(paths.appDist)); app.use(paths.servedPath, express.static(paths.appDist));
// Client app routes, serve index.html and client js will figure it out // Client app routes, serve index.html and client js will figure it out
const html = fs.readFileSync(path.join(paths.appDist, 'index.html'), {encoding: 'utf8'}); const html = fs.readFileSync(path.join(paths.appDist, 'index.html'), {
encoding: 'utf8',
});
app.get(`${paths.servedPath}login`, (_req, res) => { app.get(`${paths.servedPath}login`, (_req, res) => {
res.send(html); res.send(html);

View File

@@ -11,7 +11,7 @@ enforcePrerequisites()
.then(migrateData) .then(migrateData)
.then(() => { .then(() => {
// We do this because we don't want the side effects of importing server functions before migration is completed. // We do this because we don't want the side effects of importing server functions before migration is completed.
const startWebServer = require('./web-server').default; // eslint-disable-line global-require const startWebServer = require('./web-server').default; // eslint-disable-line @typescript-eslint/no-var-requires
return startWebServer(); return startWebServer();
}) })
.catch((error) => { .catch((error) => {

View File

@@ -85,7 +85,11 @@ export default async (req: Request<unknown, unknown, unknown, {historySnapshot:
if (error == null && snapshot != null && lastTimestamp != null) { if (error == null && snapshot != null && lastTimestamp != null) {
serverEvent.emit(lastTimestamp, 'TRANSFER_HISTORY_FULL_UPDATE', snapshot); serverEvent.emit(lastTimestamp, 'TRANSFER_HISTORY_FULL_UPDATE', snapshot);
} else { } else {
const fallbackHistory: TransferHistory = {download: [0], upload: [0], timestamps: [Date.now()]}; const fallbackHistory: TransferHistory = {
download: [0],
upload: [0],
timestamps: [Date.now()],
};
serverEvent.emit(Date.now(), 'TRANSFER_HISTORY_FULL_UPDATE', fallbackHistory); serverEvent.emit(Date.now(), 'TRANSFER_HISTORY_FULL_UPDATE', fallbackHistory);
} }
}); });

View File

@@ -71,7 +71,10 @@ class Users {
return; return;
} }
argon2Verify({password: credentials.password, hash: user.password}).then( argon2Verify({
password: credentials.password,
hash: user.password,
}).then(
(isMatch) => { (isMatch) => {
if (isMatch) { if (isMatch) {
resolve(user.level); resolve(user.level);

View File

@@ -50,7 +50,11 @@ export const getAuthToken = (username: string, res?: Response): string => {
}); });
if (res != null) { if (res != null) {
res.cookie('jwt', token, {expires: new Date(cookieExpiration), httpOnly: true, sameSite: 'strict'}); res.cookie('jwt', token, {
expires: new Date(cookieExpiration),
httpOnly: true,
sameSite: 'strict',
});
} }
return token; return token;

View File

@@ -99,7 +99,10 @@ describe('POST /api/torrents/add-urls', () => {
it('Adds torrents to disallowed path via URLs', (done) => { it('Adds torrents to disallowed path via URLs', (done) => {
request request
.post('/api/torrents/add-urls') .post('/api/torrents/add-urls')
.send({...addTorrentByURLOptions, destination: path.join(os.tmpdir(), 'notAllowed')}) .send({
...addTorrentByURLOptions,
destination: path.join(os.tmpdir(), 'notAllowed'),
})
.set('Cookie', [authToken]) .set('Cookie', [authToken])
.set('Accept', 'application/json') .set('Accept', 'application/json')
.expect(500) .expect(500)
@@ -198,7 +201,10 @@ describe('POST /api/torrents/add-files', () => {
it('Adds torrents to disallowed path via files', (done) => { it('Adds torrents to disallowed path via files', (done) => {
request request
.post('/api/torrents/add-files') .post('/api/torrents/add-files')
.send({...addTorrentByFileOptions, destination: path.join(os.tmpdir(), 'notAllowed')}) .send({
...addTorrentByFileOptions,
destination: path.join(os.tmpdir(), 'notAllowed'),
})
.set('Cookie', [authToken]) .set('Cookie', [authToken])
.set('Accept', 'application/json') .set('Accept', 'application/json')
.expect(500) .expect(500)

View File

@@ -566,7 +566,15 @@ router.get('/:hash/contents/:indices/data', (req, res) => {
res.attachment(`${selectedTorrent.name}.tar`); res.attachment(`${selectedTorrent.name}.tar`);
return tar return tar
.c({cwd: archiveRootFolder, follow: false, noDirRecurse: true, portable: true}, relativeFilePaths) .c(
{
cwd: archiveRootFolder,
follow: false,
noDirRecurse: true,
portable: true,
},
relativeFilePaths,
)
.pipe(res); .pipe(res);
}); });
} catch (error) { } catch (error) {

View File

@@ -5,7 +5,9 @@ import type {UserInDatabase} from '@shared/schema/Auth';
import type {UserServices} from '.'; import type {UserServices} from '.';
class BaseService<E = unknown> extends (EventEmitter as {new <T>(): TypedEmitter<T>})<E> { class BaseService<E = unknown> extends (EventEmitter as {
new <T>(): TypedEmitter<T>;
})<E> {
user: UserInDatabase; user: UserInDatabase;
services?: UserServices; services?: UserServices;

View File

@@ -39,7 +39,11 @@ class TransmissionClientGatewayService extends ClientGatewayService {
files.map(async (file) => { files.map(async (file) => {
const {hashString} = const {hashString} =
(await this.clientRequestManager (await this.clientRequestManager
.addTorrent({metainfo: file, 'download-dir': destination, paused: !start}) .addTorrent({
metainfo: file,
'download-dir': destination,
paused: !start,
})
.then(this.processClientRequestSuccess, this.processClientRequestError) .then(this.processClientRequestSuccess, this.processClientRequestError)
.catch(() => undefined)) || {}; .catch(() => undefined)) || {};
return hashString; return hashString;
@@ -197,7 +201,10 @@ class TransmissionClientGatewayService extends ClientGatewayService {
} }
return this.clientRequestManager return this.clientRequestManager
.setTorrentsProperties({ids: hashes, bandwidthPriority: transmissionPriority}) .setTorrentsProperties({
ids: hashes,
bandwidthPriority: transmissionPriority,
})
.then(this.processClientRequestSuccess, this.processClientRequestError); .then(this.processClientRequestSuccess, this.processClientRequestError);
} }

View File

@@ -266,7 +266,10 @@ class ClientRequestManager {
return axios return axios
.post( .post(
this.rpcURL, this.rpcURL,
{method: 'torrent-set-location', arguments: torrentsSetLocationArguments}, {
method: 'torrent-set-location',
arguments: torrentsSetLocationArguments,
},
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
}, },

View File

@@ -297,7 +297,10 @@ class FeedService extends BaseService {
} }
// Create two arrays, one for feeds and one for rules. // Create two arrays, one for feeds and one for rules.
const feedsSummary: {feeds: Array<Feed>; rules: Array<Rule>} = docs.reduce( const feedsSummary: {
feeds: Array<Feed>;
rules: Array<Rule>;
} = docs.reduce(
(accumulator: {feeds: Array<Feed>; rules: Array<Rule>}, doc) => { (accumulator: {feeds: Array<Feed>; rules: Array<Rule>}, doc) => {
if (doc.type === 'feed') { if (doc.type === 'feed') {
accumulator.feeds.push(doc); accumulator.feeds.push(doc);
@@ -404,7 +407,14 @@ class FeedService extends BaseService {
} }
this.feedReaders.push( this.feedReaders.push(
new FeedReader({feedID, feedLabel, url, interval, maxHistory: 100, onNewItems: this.handleNewItems}), new FeedReader({
feedID,
feedLabel,
url,
interval,
maxHistory: 100,
onNewItems: this.handleNewItems,
}),
); );
return true; return true;

View File

@@ -86,7 +86,13 @@ class NotificationService extends BaseService<NotificationServiceEvents> {
getNotifications( getNotifications(
query: NotificationFetchOptions, query: NotificationFetchOptions,
callback: (data: {notifications: Notification[][]; count: NotificationCount} | null, err?: Error) => void, callback: (
data: {
notifications: Notification[][];
count: NotificationCount;
} | null,
err?: Error,
) => void,
) { ) {
const sortedNotifications = this.db.find({}).sort({ts: -1}); const sortedNotifications = this.db.find({}).sort({ts: -1});
const queryCallback = (err: Error | null, notifications: Notification[][]) => { const queryCallback = (err: Error | null, notifications: Notification[][]) => {

View File

@@ -205,7 +205,10 @@ class ClientRequestManager {
form.append(property, `${options[property]}`); form.append(property, `${options[property]}`);
}); });
const headers = form.getHeaders({Cookie: await this.authCookie, 'Content-Length': form.getLengthSync()}); const headers = form.getHeaders({
Cookie: await this.authCookie,
'Content-Length': form.getLengthSync(),
});
axios axios
.post(`${this.apiBase}/torrents/add`, form, { .post(`${this.apiBase}/torrents/add`, form, {
@@ -226,7 +229,10 @@ class ClientRequestManager {
form.append(property, `${options[property]}`); form.append(property, `${options[property]}`);
}); });
const headers = form.getHeaders({Cookie: await this.authCookie, 'Content-Length': form.getLengthSync()}); const headers = form.getHeaders({
Cookie: await this.authCookie,
'Content-Length': form.getLengthSync(),
});
axios axios
.post(`${this.apiBase}/torrents/add`, form, { .post(`${this.apiBase}/torrents/add`, form, {

View File

@@ -48,7 +48,9 @@ import {
import type {MultiMethodCalls} from './util/rTorrentMethodCallUtil'; import type {MultiMethodCalls} from './util/rTorrentMethodCallUtil';
const filePathMethodCalls = getMethodCalls({pathComponents: torrentContentMethodCallConfigs.pathComponents}); const filePathMethodCalls = getMethodCalls({
pathComponents: torrentContentMethodCallConfigs.pathComponents,
});
class RTorrentClientGatewayService extends ClientGatewayService { class RTorrentClientGatewayService extends ClientGatewayService {
clientRequestManager = new ClientRequestManager(this.user.client as RTorrentConnectionSettings); clientRequestManager = new ClientRequestManager(this.user.client as RTorrentConnectionSettings);
@@ -63,7 +65,9 @@ class RTorrentClientGatewayService extends ClientGatewayService {
}: Required<AddTorrentByFileOptions>): Promise<void> { }: Required<AddTorrentByFileOptions>): Promise<void> {
const torrentPaths = await Promise.all( const torrentPaths = await Promise.all(
files.map(async (file) => { files.map(async (file) => {
return saveBufferToTempFile(Buffer.from(file, 'base64'), 'torrent', {mode: 0o664}); return saveBufferToTempFile(Buffer.from(file, 'base64'), 'torrent', {
mode: 0o664,
});
}), }),
); );

View File

@@ -7,7 +7,10 @@ export type MethodCallConfigs = Readonly<{
[propLabel: string]: MethodCallConfig; [propLabel: string]: MethodCallConfig;
}>; }>;
export type MultiMethodCalls = Array<{methodName: string; params: Array<string | Buffer>}>; export type MultiMethodCalls = Array<{
methodName: string;
params: Array<string | Buffer>;
}>;
export const stringTransformer = (value: unknown): string => { export const stringTransformer = (value: unknown): string => {
return value as string; return value as string;

View File

@@ -99,7 +99,10 @@ class TorrentService extends BaseService<TorrentServiceEvents> {
handleFetchTorrentListSuccess = (nextTorrentListSummary: this['torrentListSummary']) => { handleFetchTorrentListSuccess = (nextTorrentListSummary: this['torrentListSummary']) => {
const diff = jsonpatch.compare(this.torrentListSummary.torrents, nextTorrentListSummary.torrents); const diff = jsonpatch.compare(this.torrentListSummary.torrents, nextTorrentListSummary.torrents);
if (diff.length > 0) { if (diff.length > 0) {
this.emit('TORRENT_LIST_DIFF_CHANGE', {diff, id: nextTorrentListSummary.id}); this.emit('TORRENT_LIST_DIFF_CHANGE', {
diff,
id: nextTorrentListSummary.id,
});
} }
this.torrentListSummary = nextTorrentListSummary; this.torrentListSummary = nextTorrentListSummary;

View File

@@ -20,7 +20,7 @@ const delayedDelete = (tempPath: string): void => {
setTimeout(() => { setTimeout(() => {
try { try {
fs.unlinkSync(tempPath); fs.unlinkSync(tempPath);
} catch (_e) { } catch {
// do nothing. // do nothing.
} }
}, 1000 * 60 * 5); }, 1000 * 60 * 5);

View File

@@ -11,7 +11,7 @@ const openAndDecodeTorrent = async (torrentPath: string): Promise<TorrentFile |
try { try {
torrentData = bencode.decode(fs.readFileSync(torrentPath)); torrentData = bencode.decode(fs.readFileSync(torrentPath));
} catch (_e) { } catch {
return null; return null;
} }
@@ -59,7 +59,7 @@ export const setTrackers = async (torrent: string, trackers: Array<string>): Pro
try { try {
fs.writeFileSync(torrent, bencode.encode(torrentData)); fs.writeFileSync(torrent, bencode.encode(torrentData));
} catch (_e) { } catch {
return false; return false;
} }
@@ -140,7 +140,7 @@ export const setCompleted = async (torrent: string, destination: string, isBaseP
try { try {
fs.writeFileSync(torrent, bencode.encode(torrentDataWithResume)); fs.writeFileSync(torrent, bencode.encode(torrentDataWithResume));
} catch (_e) { } catch {
return false; return false;
} }

View File

@@ -8,7 +8,10 @@ import type {AuthMethod} from '../Auth';
// All auth requests are schema validated to ensure security. // All auth requests are schema validated to ensure security.
// POST /api/auth/authenticate // POST /api/auth/authenticate
export const authAuthenticationSchema = credentialsSchema.pick({username: true, password: true}); export const authAuthenticationSchema = credentialsSchema.pick({
username: true,
password: true,
});
export type AuthAuthenticationOptions = Required<zodInfer<typeof authAuthenticationSchema>>; export type AuthAuthenticationOptions = Required<zodInfer<typeof authAuthenticationSchema>>;
// POST /api/auth/authenticate - success response // POST /api/auth/authenticate - success response

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export enum AccessLevel { export enum AccessLevel {
USER = 5, USER = 5,
ADMINISTRATOR = 10, ADMINISTRATOR = 10,

View File

@@ -1,2 +1 @@
// eslint-disable-next-line import/prefer-default-export
export const SUPPORTED_CLIENTS = ['qBittorrent', 'rTorrent', 'Transmission'] as const; export const SUPPORTED_CLIENTS = ['qBittorrent', 'rTorrent', 'Transmission'] as const;

View File

@@ -3,16 +3,6 @@ import type {TorrentPeer} from './TorrentPeer';
import type {TorrentStatus} from '../constants/torrentStatusMap'; import type {TorrentStatus} from '../constants/torrentStatusMap';
import type {TorrentTracker} from './TorrentTracker'; import type {TorrentTracker} from './TorrentTracker';
export interface Duration {
years?: number;
weeks?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
cumSeconds: number;
}
export interface TorrentDetails { export interface TorrentDetails {
contents: Array<TorrentContent>; contents: Array<TorrentContent>;
peers: Array<TorrentPeer>; peers: Array<TorrentPeer>;

View File

@@ -1,29 +0,0 @@
const formatUtil = {
secondsToDuration: (cumSeconds: number) => {
const years = Math.floor(cumSeconds / 31536000);
const weeks = Math.floor((cumSeconds % 31536000) / 604800);
const days = Math.floor(((cumSeconds % 31536000) % 604800) / 86400);
const hours = Math.floor((((cumSeconds % 31536000) % 604800) % 86400) / 3600);
const minutes = Math.floor(((((cumSeconds % 31536000) % 604800) % 86400) % 3600) / 60);
const seconds = Math.floor(cumSeconds - minutes * 60);
let timeRemaining = null;
if (years > 0) {
timeRemaining = {years, weeks, cumSeconds};
} else if (weeks > 0) {
timeRemaining = {weeks, days, cumSeconds};
} else if (days > 0) {
timeRemaining = {days, hours, cumSeconds};
} else if (hours > 0) {
timeRemaining = {hours, minutes, cumSeconds};
} else if (minutes > 0) {
timeRemaining = {minutes, seconds, cumSeconds};
} else {
timeRemaining = {seconds, cumSeconds};
}
return timeRemaining;
},
};
export default formatUtil;