├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitpod.yml ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── assets ├── get-started-1.png ├── get-started.png ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── screenshot-4.png ├── screenshot-5.png ├── screenshot-6.png └── screenshot-7.png ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── @dataflow │ │ ├── core │ │ │ ├── ajax-flow.spec.ts │ │ │ ├── ajax-flow.ts │ │ │ ├── bare-flow.spec.ts │ │ │ ├── bare-flow.ts │ │ │ ├── cache-flow.spec.ts │ │ │ ├── cache-flow.ts │ │ │ ├── index.ts │ │ │ ├── nothing-flow.spec.ts │ │ │ ├── nothing-flow.ts │ │ │ ├── superset-flow.spec.ts │ │ │ └── superset-flow.ts │ │ ├── extra │ │ │ ├── browser-setting-flow.ts │ │ │ ├── browser-setting-schema.json │ │ │ ├── clipboard-flow.ts │ │ │ ├── current-user-flow.ts │ │ │ ├── index.ts │ │ │ ├── operations-list-extends-flow.ts │ │ │ ├── server-setting-schema.json │ │ │ ├── users-flow.spec.ts │ │ │ └── users-flow.ts │ │ └── rclone │ │ │ ├── async-post-flow.ts │ │ │ ├── connection-flow.ts │ │ │ ├── core-bwlimit-flow.ts │ │ │ ├── core-memstats-flow.ts │ │ │ ├── core-stats-delete-flow.ts │ │ │ ├── core-stats-flow.ts │ │ │ ├── core-stats-reset-flow.ts │ │ │ ├── core-version-flow.ts │ │ │ ├── download-file-flow.ts │ │ │ ├── get-flow.ts │ │ │ ├── index.ts │ │ │ ├── list-cmd-flow.ts │ │ │ ├── list-group-flow.ts │ │ │ ├── list-mounts-flow.ts │ │ │ ├── list-remotes-flow.ts │ │ │ ├── mount-mount-flow.ts │ │ │ ├── mount-unmount-all-flow.ts │ │ │ ├── mount-unmount-flow.ts │ │ │ ├── navigation-flow.ts │ │ │ ├── noop-auth-flow.spec.ts │ │ │ ├── noop-auth-flow.ts │ │ │ ├── operations-about-flow.ts │ │ │ ├── operations-copyfile-flow.ts │ │ │ ├── operations-deletefile-flow.ts │ │ │ ├── operations-fsinfo-flow.ts │ │ │ ├── operations-list-flow.ts │ │ │ ├── operations-mkdir-flow.ts │ │ │ ├── operations-movefile-flow.ts │ │ │ ├── operations-purge-flow.ts │ │ │ ├── options-get-flow.ts │ │ │ ├── options-set-flow.ts │ │ │ ├── post-flow.ts │ │ │ ├── sync-copy-flow.ts │ │ │ └── sync-move-flow.ts │ ├── app-routing.module.ts │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── diff │ │ │ ├── diff.component.spec.ts │ │ │ └── diff.component.ts │ │ ├── key-value-table │ │ │ ├── key-value-table.component.spec.ts │ │ │ └── key-value-table.component.ts │ │ ├── rng.module.ts │ │ ├── space-usage-chart │ │ │ ├── space-usage-chart.component.spec.ts │ │ │ └── space-usage-chart.component.ts │ │ ├── speed-chart │ │ │ ├── speed-chart.component.spec.ts │ │ │ └── speed-chart.component.ts │ │ └── summary │ │ │ ├── summary.component.spec.ts │ │ │ └── summary.component.ts │ ├── pages │ │ ├── about │ │ │ ├── about-routing.module.ts │ │ │ ├── about.component.spec.ts │ │ │ ├── about.component.ts │ │ │ └── about.module.ts │ │ ├── connection.service.spec.ts │ │ ├── connection.service.ts │ │ ├── current-user.service.spec.ts │ │ ├── current-user.service.ts │ │ ├── dashboard │ │ │ ├── dashboard-routing.module.ts │ │ │ ├── dashboard.component.spec.ts │ │ │ ├── dashboard.component.ts │ │ │ └── dashboard.module.ts │ │ ├── jobs │ │ │ ├── contextmenu │ │ │ │ └── group-options.contextmenu.ts │ │ │ ├── dialogs │ │ │ │ └── clean-finished-groups.dialog.ts │ │ │ ├── jobs-routing.module.ts │ │ │ ├── jobs.component.scss │ │ │ ├── jobs.component.spec.ts │ │ │ ├── jobs.component.ts │ │ │ ├── jobs.module.ts │ │ │ └── transferring │ │ │ │ ├── transferring.component.spec.ts │ │ │ │ └── transferring.component.ts │ │ ├── layout.service.spec.ts │ │ ├── layout.service.ts │ │ ├── manager │ │ │ ├── breadcrumb │ │ │ │ └── breadcrumb.component.ts │ │ │ ├── clipboard │ │ │ │ ├── clipboard-remotes-table │ │ │ │ │ ├── clipboard-remotes-table.component.spec.ts │ │ │ │ │ └── clipboard-remotes-table.component.ts │ │ │ │ ├── clipboard.dialog.ts │ │ │ │ ├── clipboard.service.spec.ts │ │ │ │ └── clipboard.service.ts │ │ │ ├── dialogs │ │ │ │ └── mkdir.dialog.ts │ │ │ ├── fileMode │ │ │ │ ├── download-file.service.spec.ts │ │ │ │ ├── download-file.service.ts │ │ │ │ ├── file.detail.ts │ │ │ │ ├── fileMode.component.ts │ │ │ │ └── listView │ │ │ │ │ └── listView.component.ts │ │ │ ├── homeMode │ │ │ │ ├── homeMode.component.scss │ │ │ │ ├── homeMode.component.ts │ │ │ │ ├── remote.component.ts │ │ │ │ └── remote.detail.ts │ │ │ ├── manager-routing.module.ts │ │ │ ├── manager.component.spec.ts │ │ │ ├── manager.component.ts │ │ │ └── manager.module.ts │ │ ├── mounts │ │ │ ├── mounts-routing.module.ts │ │ │ ├── mounts.component.spec.ts │ │ │ ├── mounts.component.ts │ │ │ ├── mounts.module.ts │ │ │ ├── mounts.service.spec.ts │ │ │ └── mounts.service.ts │ │ ├── pages-menu.ts │ │ ├── pages-routing.module.ts │ │ ├── pages.component.spec.ts │ │ ├── pages.component.ts │ │ ├── pages.module.ts │ │ ├── settings │ │ │ ├── browser-setting │ │ │ │ ├── browser-setting.component.spec.ts │ │ │ │ ├── browser-setting.component.ts │ │ │ │ ├── browser-setting.service.spec.ts │ │ │ │ └── browser-setting.service.ts │ │ │ ├── settings-routing.module.ts │ │ │ ├── settings.component.spec.ts │ │ │ ├── settings.component.ts │ │ │ ├── settings.module.ts │ │ │ └── sever-setting │ │ │ │ ├── server-setting.service.spec.ts │ │ │ │ ├── server-setting.service.ts │ │ │ │ ├── sever-setting.component.spec.ts │ │ │ │ └── sever-setting.component.ts │ │ ├── tasks │ │ │ ├── tasks-queue.service.spec.ts │ │ │ ├── tasks-queue.service.ts │ │ │ ├── tasks-queue.ts │ │ │ └── tasks.dialog.ts │ │ ├── user │ │ │ ├── add │ │ │ │ ├── add.component.spec.ts │ │ │ │ └── add.component.ts │ │ │ ├── del │ │ │ │ └── delete.dialog.ts │ │ │ ├── login │ │ │ │ └── UserLogin.component.ts │ │ │ ├── user-routing.module.ts │ │ │ ├── user.component.spec.ts │ │ │ ├── user.component.ts │ │ │ └── user.module.ts │ │ ├── users.service.spec.ts │ │ └── users.service.ts │ └── utils │ │ ├── format-bytes.ts │ │ └── format-duration.ts ├── assets │ ├── .gitkeep │ ├── favicon.png │ └── favicon.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts └── themes.scss ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | 18 | [{package*.json}] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | 20 | - name: Cache node modules 21 | uses: actions/cache@v1 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-node- 27 | - name: Node ${{ matrix.node-version }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | - name: install dependencies 32 | run: | 33 | npm i 34 | - name: lint 35 | run: | 36 | npm run lint 37 | - name: build production 38 | run: | 39 | npm run build:prod 40 | - name: deploy github page 41 | if: github.event_name == 'push' 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | run: | 45 | npm run release -- -p dev && git reset HEAD~ 46 | npm run deploy -- --name="ElonH" --email="elonhhuang@gmail.com" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: NG_CLI_ANALYTICS=ci npm install 3 | command: npm run start 4 | 5 | vscode: 6 | extensions: 7 | - DavidAnson.vscode-markdownlint@0.36.1:5xO8Po7lfq/3zIQ0RF9qXQ== 8 | - dbaeumer.vscode-eslint@2.1.3:1NRvj3UKNTNwmYjptmUmIw== 9 | - Angular.ng-template@0.1000.2:fow5NOWTo34TjwpJKA+ETw== 10 | - vscode-icons-team.vscode-icons@10.1.1:PKCY8FKQ+GNh32Vd7rsvuA== 11 | - esbenp.prettier-vscode@5.1.3:t532ajsImUSrA9N8Bd7jQw== 12 | - msjsdiag.debugger-for-chrome@4.12.6:IdQBlCQEnixzHAOkHC36ew== 13 | - oderwat.indent-rainbow@7.4.0:fDVCkGVYd1R2lfcs1tHk+Q== 14 | - CoenraadS.bracket-pair-colorizer-2@0.1.4:+JUeb/jFYZt2/0MS/gUllA== 15 | - mrmlnc.vscode-scss@0.9.1:Xtvl8tTagyF2RBdAFpjFvg== 16 | - johnpapa.Angular2@9.1.2:uM4PeIXJe98IILNHmOn+nA== 17 | - christian-kohler.path-intellisense@2.2.1:fMWBG5pfnZrunkTMwUwEDg== 18 | - formulahendry.auto-close-tag@0.5.7:ofk2Iz4wGQdoTFrnRwzI7w== 19 | - Gruntfuggly.todo-tree@0.0.177:GCyi2twUFe/rN35G99jXYQ== 20 | - wayou.vscode-todo-highlight@1.0.4:TEjqQVt71hdvHUZA7eWgHA== -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | dist 4 | node_modules 5 | **.md 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": true, 5 | "tabWidth": 2, 6 | "endOfLine": "lf", 7 | "trailingComma": "es5", 8 | "semi": true, 9 | "quoteProps": "as-needed", 10 | "jsxBracketSameLine": false, 11 | "arrowParens": "avoid" 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "angular.ng-template", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "natewallace.angular2-inline", 7 | "cyrilletuzi.angular-schematics", 8 | "kamikillerto.vscode-colorize", 9 | "msjsdiag.debugger-for-chrome", 10 | "oderwat.indent-rainbow", 11 | "2gua.rainbow-brackets", 12 | "mrmlnc.vscode-scss", 13 | "nrwl.angular-console", 14 | "editorconfig.editorconfig", 15 | "johnpapa.angular2", 16 | "christian-kohler.path-intellisense", 17 | "formulahendry.auto-close-tag", 18 | "gruntfuggly.todo-tree", 19 | "wayou.vscode-todo-highlight" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "tslint.exclude": "**", 4 | "eslint.options": { 5 | "configFile": ".eslintrc.json", 6 | "extensions": [".ts", ".html"] 7 | }, 8 | "eslint.validate": ["javascript", "javascriptreact", "html", "typescriptreact"], 9 | "[json]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[jsonc]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[typescript]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "editor.codeActionsOnSave": { 19 | "source.fixAll.eslint": true 20 | }, 21 | "prettier.requireConfig": true, 22 | "prettier.configPath": ".prettierrc.json", 23 | "editor.detectIndentation": false, 24 | "editor.tabSize": 2, 25 | "editor.insertSpaces": false, 26 | "typescript.suggest.paths": true, 27 | "todo-tree.highlights.enabled": false, 28 | "todohighlight.isEnable": true, 29 | "todohighlight.isCaseSensitive": false, 30 | "todohighlight.defaultStyle": { 31 | "backgroundColor": "#ffab00", 32 | "border": "1px solid #eee", 33 | "borderRadius": "2px", 34 | "color": "red", 35 | "cursor": "pointer", 36 | "isWholeLine": false, 37 | "overviewRulerColor": "#ffab00" 38 | }, 39 | "todohighlight.exclude": [ 40 | "**/node_modules/**", 41 | "**/bower_components/**", 42 | "**/dist/**", 43 | "**/build/**", 44 | "**/.vscode/**", 45 | "**/.github/**", 46 | "**/_output/**", 47 | "**/*.min.*", 48 | "**/*.map", 49 | "**/.next/**" 50 | ], 51 | "todohighlight.include": [ 52 | "**/*.js", 53 | "**/*.jsx", 54 | "**/*.py", 55 | "**/*.ts", 56 | "**/*.tsx", 57 | "**/*.html", 58 | "**/*.php", 59 | "**/*.css", 60 | "**/*.scss", 61 | "**/*.h", 62 | "**/*.cpp", 63 | "**/*.cxx" 64 | ], 65 | "todohighlight.keywords": [ 66 | "DEBUG:", 67 | "REVIEW:", 68 | { 69 | "backgroundColor": "cyan", 70 | "color": "#000", 71 | "overviewRulerColor": "cyan", 72 | "text": "NOTE:" 73 | }, 74 | "Warning:", 75 | { 76 | "backgroundColor": "red", 77 | "color": "#ffffff", 78 | "overviewRulerColor": "red", 79 | "text": "Error:" 80 | }, 81 | { 82 | "color": "#000", 83 | "isWholeLine": false, 84 | "text": "HACK:" 85 | }, 86 | { 87 | "backgroundColor": "rgba(0,0,0,.2)", 88 | "border": "1px solid red", 89 | "borderRadius": "2px", 90 | "color": "red", 91 | "overviewRulerColor": "grey", 92 | "text": "TODO:" 93 | }, 94 | { 95 | "text": "OPMZ:", 96 | "backgroundColor": "rgba(0,0,0,.2)", 97 | "color": "green", 98 | "border": "1px solid green", 99 | "borderRadius": "3px" 100 | } 101 | ], 102 | "todohighlight.maxFilesForSearch": 5120, 103 | "todohighlight.toggleURI": false 104 | } 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ElonH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RcloneNg 2 | 3 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/elonh/rcloneng) 4 | ![GitHub All Releases](https://img.shields.io/github/downloads/elonh/rcloneng/total) 5 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/ElonH/RcloneNg) 6 | [![Dependencies Status](https://david-dm.org/elonh/RcloneNG/status.svg)](https://david-dm.org/elonh/RcloneNG) 7 | 8 | An angular web application for rclone 9 | 10 | ## Features 11 | 12 | - Support multiple rclone server 13 | 14 | - Explore remote file system 15 | 16 | - Create asynchronous jobs of coping/moving objects between remotes 17 | 18 | - Download file from remote 19 | 20 | - Observe the progress of running jobs (by groups) 21 | 22 | - Allow editing of rclone server configuration (power by [monaco editor](https://github.com/microsoft/monaco-editor), supporting lint, document description) 23 | 24 | - Manager Rclone mounts 25 | 26 | ## Screenshots 27 | 28 | ![Screenshot 1](./assets/screenshot-1.png) 29 | 30 | ![Screenshot 5](./assets/screenshot-5.png) 31 | 32 | ![Screenshot 2](./assets/screenshot-2.png) 33 | 34 | ![Screenshot 6](./assets/screenshot-6.png) 35 | 36 | ![Screenshot 3](./assets/screenshot-3.png) 37 | 38 | ![Screenshot 4](./assets/screenshot-4.png) 39 | 40 | ![Screenshot 7](./assets/screenshot-7.png) 41 | 42 | ## Get Started 43 | 44 | 1. running rclone as server 45 | 46 | ```bash 47 | rclone rcd --rc-user= --rc-pass= --rc-allow-origin="http://localhost:4200" 48 | ``` 49 | 50 | 2. getting RcloneNg 51 | 52 | - local way 53 | 54 | ```bash 55 | git clone https://github.com/ElonH/RcloneNg.git 56 | cd RcloneNg 57 | npm install # NodeJs version >= 10 58 | npm run start 59 | ``` 60 | 61 | - online way 62 | 63 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/ElonH/RcloneNg) 64 | 65 | - lazy way 66 | 67 | if set `--rc-allow-origin="https://elonh.github.io"`, can be used directly. 68 | 69 | 3. editing server connection in RcloneNg. 70 | 71 | ![Get started](./assets/get-started.png) 72 | 73 | ## License 74 | 75 | This project and its dependencies ( except Rxjs, Apache-2.0 ) follows MIT license. 76 | -------------------------------------------------------------------------------- /assets/get-started-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/get-started-1.png -------------------------------------------------------------------------------- /assets/get-started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/get-started.png -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/screenshot-2.png -------------------------------------------------------------------------------- /assets/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/screenshot-3.png -------------------------------------------------------------------------------- /assets/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/screenshot-4.png -------------------------------------------------------------------------------- /assets/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/screenshot-5.png -------------------------------------------------------------------------------- /assets/screenshot-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/screenshot-6.png -------------------------------------------------------------------------------- /assets/screenshot-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/assets/screenshot-7.png -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('RcloneNg app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/RcloneNg'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true, 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/ajax-flow.spec.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlow } from './ajax-flow'; 2 | 3 | describe('AjaxFlow', () => { 4 | // xit('should create an instance', () => { 5 | // expect(new AjaxFlow()).toBeTruthy(); 6 | // }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/ajax-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { ajax, AjaxRequest, AjaxResponse } from 'rxjs/ajax'; 3 | import { catchError, map } from 'rxjs/operators'; 4 | import { CombErr, FlowInNode, FlowOutNode } from './bare-flow'; 5 | import { CacheFlow } from './cache-flow'; 6 | import { FlowSupNode } from './superset-flow'; 7 | 8 | export interface AjaxFlowInteralNode { 9 | ajaxRsp: AjaxResponse; 10 | } 11 | 12 | export abstract class AjaxFlow< 13 | Tin extends FlowInNode, 14 | Tout extends FlowOutNode, 15 | Tsup extends FlowSupNode = Tin & Tout 16 | > extends CacheFlow { 17 | // protected cacheSupport: boolean; 18 | // protected cachePath: string; 19 | 20 | protected abstract requestAjax(pre: CombErr): AjaxRequest; 21 | protected abstract reconstructAjaxResult(x: CombErr): CombErr; 22 | protected requestCache(pre: CombErr): Observable> { 23 | return ajax(this.requestAjax(pre)).pipe( 24 | map(x => [{ ajaxRsp: x }, []] as CombErr), 25 | catchError( 26 | (err): Observable> => 27 | of([{}, [err]] as CombErr) 28 | ), 29 | map(x => this.reconstructAjaxResult(x)) 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/bare-flow.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { TestScheduler } from 'rxjs/testing'; 3 | import { BareFlow, CombErr, FlowInNode, FlowOutNode } from './bare-flow'; 4 | 5 | describe('BareFlow', () => { 6 | let scheduler: TestScheduler; 7 | beforeEach( 8 | () => 9 | (scheduler = new TestScheduler((actual, expected) => { 10 | expect(actual).toEqual(expected); 11 | })) 12 | ); 13 | it('should not to request data, if previous errors exist', () => { 14 | scheduler.run(helpers => { 15 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 16 | const values = { 17 | a: [{}, [new Error('123')]] as CombErr, 18 | }; 19 | const inp = cold('a----', values); 20 | const expected = 'a----'; 21 | 22 | const rst = new (class extends BareFlow { 23 | public prerequest$ = inp; 24 | protected request(pre: CombErr): Observable> { 25 | throw new Error('Method not implemented.'); 26 | } 27 | })(); 28 | rst.deploy(); 29 | 30 | expectObservable(rst.getOutput()).toBe(expected, values); 31 | }); 32 | }); 33 | it('request twice, but got once only', () => { 34 | scheduler.run(helpers => { 35 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 36 | interface TestPreNode { 37 | a?: number; 38 | b?: number; 39 | } 40 | const values: { [id: string]: [TestPreNode, []] } = { 41 | a: [{ a: 555 }, []], 42 | b: [{ b: 123 }, []], 43 | }; 44 | const inp = cold('a----', values); 45 | const expected = 'b----'; 46 | 47 | const rst = new (class extends BareFlow { 48 | public prerequest$ = inp; 49 | protected request(pre: CombErr): Observable> { 50 | expect(pre).toEqual(values.a); 51 | return of(values.b, values.b); 52 | } 53 | })(); 54 | rst.deploy(); 55 | 56 | expectObservable(rst.getOutput()).toBe(expected, values); 57 | }); 58 | }); 59 | it('prerequest twice(same value), but got once only', () => { 60 | scheduler.run(helpers => { 61 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 62 | interface TestPreNode { 63 | a?: number; 64 | b?: number; 65 | } 66 | const values: { [id: string]: [TestPreNode, []] } = { 67 | a: [{ a: 555 }, []], 68 | b: [{ b: 123 }, []], 69 | }; 70 | const inp = cold('a--a-', values); 71 | const expected = 'b----'; 72 | 73 | const rst = new (class extends BareFlow { 74 | public prerequest$ = inp; 75 | protected request(pre: CombErr): Observable> { 76 | return of(values.b); 77 | } 78 | })(); 79 | rst.deploy(); 80 | 81 | expectObservable(rst.getOutput()).toBe(expected, values); 82 | }); 83 | }); 84 | it('prerequest twice(different value), got twice', () => { 85 | scheduler.run(helpers => { 86 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 87 | interface TestPreNode { 88 | ab: number; 89 | } 90 | const values: { [id: string]: [TestPreNode, []] } = { 91 | a: [{ ab: 555 }, []], 92 | b: [{ ab: 123 }, []], 93 | c: [{ ab: 556 }, []], 94 | d: [{ ab: 124 }, []], 95 | }; 96 | const inp = cold('a--b-', values); 97 | const expected = 'c--d-'; 98 | 99 | const rst = new (class extends BareFlow { 100 | // protected request(pre: import("./bare-flow").CombErr): Observable> { 101 | // throw new Error("Method not implemented."); 102 | // } 103 | public prerequest$ = inp; 104 | protected request(pre: CombErr): Observable> { 105 | return of([{ ab: pre[0].ab + 1 }, []]); 106 | } 107 | })(); 108 | rst.deploy(); 109 | 110 | expectObservable(rst.getOutput()).toBe(expected, values); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/bare-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { shareReplay, switchMap, take } from 'rxjs/operators'; 3 | 4 | export interface FlowInNode {} 5 | export interface FlowOutNode {} 6 | export type CombErr = [T, Error[]]; 7 | 8 | /** 9 | * @description 10 | * 11 | * Every dataflows are derived from `BareFlow`. 12 | * 13 | * `BareFlow` has one entry port and one out port. 14 | * 15 | * Entry port: `prerequest$`; 16 | * Out port: `getOutput`; 17 | * 18 | * Process: 19 | * 20 | * if Entry port recivied an error, `BareFlow` directly derive to Out port 21 | * (never handle errors). 22 | * 23 | * if Entry port recived an normal data, `BareFlow` derive it to `request` function, 24 | * and send output data from `request` function to Out port 25 | * 26 | * Usage: 27 | * ``` typescript 28 | * const flow$ = new (class extends BareFlow { 29 | * public prerequest$ = new Subject>(); // connect to previous flow. 30 | * protected request(pre: CombErr): Observable> { 31 | * // handle some thing. 32 | * return [{}, []]; 33 | * } 34 | * })(); 35 | * flow$.deploy(); 36 | * flow$.getoutput().subscript(); 37 | * ``` 38 | * @template Tin 39 | * @template Tout 40 | */ 41 | export abstract class BareFlow { 42 | /** 43 | * @description Prerequest$ is out port of previous flow. 44 | */ 45 | public abstract prerequest$: Observable>; 46 | private bareData$: Observable>; 47 | private deployed = false; 48 | /** 49 | * @description generate output data based on input data. 50 | * If input port recived some errors, this function isn't called. 51 | * @param pre Input data 52 | * @returns **Observable** of Output data 53 | */ 54 | protected abstract request(pre: CombErr): Observable>; 55 | protected deployBefore() { 56 | this.bareData$ = this.prerequest$.pipe( 57 | switchMap( 58 | (pre): Observable> => { 59 | if (pre[1].length === 0) return this.request(pre).pipe(take(1)); 60 | return of((pre as any) as CombErr); // force to convert. There are some errors at privious flow. 61 | // Just make sure that checking Error[] at first in subscription 62 | } 63 | ), 64 | shareReplay() 65 | ); 66 | this.deployed = true; 67 | } 68 | protected deployAfter() { 69 | this.bareData$.pipe(take(1)).subscribe(); 70 | } 71 | /** 72 | * @description setup dataflow 73 | */ 74 | public deploy() { 75 | this.deployBefore(); 76 | this.deployAfter(); 77 | } 78 | /** 79 | * @description out port of dataflow 80 | * 81 | * noticed: call `deploy()` before call this 82 | * @returns output 83 | */ 84 | public getOutput(): Observable> { 85 | if (!this.deployed) throw new Error('run deploy before getOutput'); 86 | return this.bareData$; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/cache-flow.ts: -------------------------------------------------------------------------------- 1 | import { iif, Observable, of } from 'rxjs'; 2 | import { take, tap } from 'rxjs/operators'; 3 | import { CombErr, FlowInNode, FlowOutNode } from './bare-flow'; 4 | import { FlowSupNode, SupersetFlow } from './superset-flow'; 5 | 6 | /** 7 | * @description 8 | * add cache mechanism in data flow 9 | * 10 | * if set `cacheSupport` is `true` and specify `cachePath`, 11 | * dataflow will auto cache Output data. 12 | * 13 | * @template Tin 14 | * @template Tout 15 | * @template Tsup 16 | */ 17 | export abstract class CacheFlow< 18 | Tin extends FlowInNode, 19 | Tout extends FlowOutNode, 20 | Tsup extends FlowSupNode = Tin & Tout 21 | > extends SupersetFlow { 22 | private static cacheStorage: { [id: string]: CombErr } = {}; 23 | 24 | /** 25 | * @description Enable cache 26 | */ 27 | protected abstract cacheSupport: boolean; 28 | /** 29 | * @description A key to hit cached 30 | */ 31 | protected abstract cachePath: string | undefined; 32 | private cleanCacheFlag = false; 33 | 34 | /** 35 | * @description Purges **all** cache 36 | */ 37 | public static purgeAllCache() { 38 | CacheFlow.cacheStorage = {}; 39 | } 40 | /** 41 | * @description generate output data based on input data. 42 | * If input port recived some errors or dataflow hit cached, this function isn't called. 43 | * @param pre Input data 44 | * @returns **Observable** of Output data 45 | */ 46 | protected abstract requestCache(pre: CombErr): Observable>; 47 | protected request(pre: CombErr): Observable> { 48 | return iif( 49 | () => this.cacheEnabled() && this.isCached(), 50 | of(this.getCache()), 51 | this.requestCache(pre).pipe( 52 | take(1), 53 | tap(x => { 54 | if (this.cacheEnabled()) this.setCache(x); 55 | }) 56 | ) 57 | ); 58 | } 59 | 60 | /** 61 | * @description check if satisfy cache basic requirements 62 | * @returns true if enabled 63 | */ 64 | private cacheEnabled(): boolean { 65 | return this.cacheSupport && typeof this.cachePath === 'string'; 66 | } 67 | private isCached() { 68 | if (this.cleanCacheFlag) return !(this.cleanCacheFlag = true); 69 | return !this.cleanCacheFlag && CacheFlow.cacheStorage.hasOwnProperty(this.cachePath); 70 | } 71 | private getCache(): CombErr { 72 | return CacheFlow.cacheStorage[this.cachePath] as CombErr; 73 | } 74 | private setCache(x: CombErr) { 75 | CacheFlow.cacheStorage[this.cachePath] = x; 76 | } 77 | public clearCache() { 78 | this.cleanCacheFlag = true; 79 | // delete CacheFlow.cacheStorage[this.cachePath]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bare-flow'; 2 | export * from './superset-flow'; 3 | export * from './cache-flow'; 4 | export * from './nothing-flow'; 5 | export * from './ajax-flow'; 6 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/nothing-flow.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestScheduler } from 'rxjs/testing'; 2 | import { FlowInNode } from './bare-flow'; 3 | import { NothingFlow } from './nothing-flow'; 4 | import { CombErr } from '.'; 5 | 6 | describe('NothingFlow', () => { 7 | let scheduler: TestScheduler; 8 | beforeEach( 9 | () => 10 | (scheduler = new TestScheduler((actual, expected) => { 11 | expect(actual).toEqual(expected); 12 | })) 13 | ); 14 | it('input is output', () => { 15 | scheduler.run(helpers => { 16 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 17 | const values = { 18 | a: [{ a: '123' }, []] as CombErr, 19 | }; 20 | const inp = cold('a----', values); 21 | const expected = 'a----'; 22 | 23 | const rst = new (class extends NothingFlow { 24 | public prerequest$ = inp; 25 | })(); 26 | rst.deploy(); 27 | 28 | expectObservable(rst.getOutput()).toBe(expected, values); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/nothing-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { BareFlow, CombErr, FlowInNode } from './bare-flow'; 3 | 4 | /** 5 | * do nothing 6 | */ 7 | export abstract class NothingFlow extends BareFlow { 8 | // public prerequest$: Observable>; 9 | protected request(pre: CombErr): Observable> { 10 | return of(pre); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/superset-flow.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { TestScheduler } from 'rxjs/testing'; 3 | import { CombErr, FlowInNode, FlowOutNode } from './bare-flow'; 4 | import { SupersetFlow } from './superset-flow'; 5 | 6 | describe('SupersetFlow', () => { 7 | let scheduler: TestScheduler; 8 | beforeEach( 9 | () => 10 | (scheduler = new TestScheduler((actual, expected) => { 11 | expect(actual).toEqual(expected); 12 | })) 13 | ); 14 | it('prerequest twice(different value), got twice', () => { 15 | scheduler.run(helpers => { 16 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 17 | const values: { [id: string]: CombErr } = { 18 | a: [{ ab: 555 }, []], 19 | b: [{ ab: 123 }, []], 20 | c: [{ ab: 555, cd: 556 }, []], 21 | d: [{ ab: 123, cd: 124 }, []], 22 | }; 23 | const inp = cold('a--b-', values); 24 | const expected = 'c--d-'; 25 | 26 | const rst = new (class extends SupersetFlow { 27 | public prerequest$ = inp; 28 | protected request(pre: CombErr): Observable> { 29 | return of([{ cd: pre[0].ab + 1 }, []]); 30 | } 31 | })(); 32 | rst.deploy(); 33 | 34 | expectObservable(rst.getSupersetOutput()).toBe(expected, values); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/@dataflow/core/superset-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { map, shareReplay, take, withLatestFrom } from 'rxjs/operators'; 3 | import { BareFlow, CombErr, FlowInNode, FlowOutNode } from './bare-flow'; 4 | 5 | export interface FlowSupNode {} 6 | 7 | /** 8 | * @description 9 | * 10 | * `Superset flow` has two out ports. 11 | * 12 | * Out port 1: `getOutput()` inherited from `BareFlow` 13 | * Out port 2: `getSuperset()` 14 | * 15 | * `getSuperset()` is extesion of `getOutput()`, it combine entry port and out port 1 together. 16 | * 17 | * eg: Entry port type: Tin 18 | * Out port 1 type: Tout 19 | * if `generateSuperset()` wasn't overrided, Out port 2 type is `Tin & Tout` 20 | * 21 | * @template Tin 22 | * @template Tout 23 | * @template Tsup 24 | */ 25 | export abstract class SupersetFlow< 26 | Tin extends FlowInNode, 27 | Tout extends FlowOutNode, 28 | Tsup extends FlowSupNode = Tin & Tout 29 | > extends BareFlow { 30 | private supersetData$: Observable>; 31 | private supersetDeployed = false; 32 | protected deployBefore() { 33 | super.deployBefore(); 34 | this.supersetData$ = this.getOutput().pipe( 35 | withLatestFrom(this.prerequest$), 36 | map(([cur, pre]) => this.generateSuperset(cur, pre)), 37 | shareReplay() 38 | ); 39 | this.supersetDeployed = true; 40 | } 41 | protected deployAfter() { 42 | this.supersetData$.pipe(take(1)).subscribe(); 43 | } 44 | /** 45 | * @description specify how to generate superset data based on Input and Output data 46 | * @param current Output data 47 | * @param previous Input data 48 | * @returns superset Superset data 49 | */ 50 | protected generateSuperset(current: CombErr, previous: CombErr): CombErr { 51 | return [ 52 | ({ ...current[0], ...previous[0] } as any) as Tsup, // if Tsup is not Tin & Tout, need to override generateSupreset 53 | [].concat(current[1], previous[1]).filter((x, i, a) => a.indexOf(x) === i), 54 | ]; 55 | } 56 | /** 57 | * @description Another out port of dataflow 58 | * @returns superset 59 | */ 60 | public getSupersetOutput(): Observable> { 61 | if (!this.supersetDeployed) throw new Error('run deploy before getSupersetOutput'); 62 | return this.supersetData$; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/@dataflow/extra/browser-setting-flow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * if modify any thing in `IBrowserSetting`, include name, type, or common description, 3 | * please run `npm run build:schemas:browser` to update schema.json file 4 | */ 5 | 6 | import { Observable, of } from 'rxjs'; 7 | import { BareFlow, CombErr } from '../core'; 8 | 9 | /** 10 | * @description 11 | */ 12 | export interface IBrowserSetting { 13 | /** 14 | * 请求间隔, 需要定时从rclone服务器中获取数据的时间间隔. 15 | * 16 | * 单位: 毫秒(ms) 17 | * 18 | * 影响范围: 19 | * - 响应时间的刷新频率(位于主侧栏左上角) 20 | * - Dashboard 和 Job Manager 的更新间隔 21 | * 22 | * 建议: 23 | * - 如果与 rclone 服务器响应时间小于 100ms(通常在局域网下), 可以适当调低数值 24 | */ 25 | 'rng.request-interval': number; 26 | /** 27 | * 速度图表的时间跨度, 速度图表只会保留最近一段时间内的数据 28 | * 29 | * 单位: 秒(s) 30 | * 31 | * 原则: 32 | * - 应小于 `rng.request-interval`(转化为相同单位后) 33 | * 34 | * 影响范围: 35 | * - dashboard 和 Job manager 中的速度图表时间跨度 36 | */ 37 | 'rng.speedChart.windowSize': number; 38 | } 39 | 40 | export const brwoserSettingDefault: IBrowserSetting = { 41 | 'rng.request-interval': 3000, 42 | 'rng.speedChart.windowSize': 60, 43 | }; 44 | 45 | export type NestedPartial = { 46 | [K in keyof T]?: T[K] extends (infer R)[] ? NestedPartial[] : NestedPartial; 47 | }; 48 | 49 | /** 50 | * @description override partical browser config from input port, 51 | * and output full browser configuration 52 | */ 53 | export abstract class BrowserSettingFlow extends BareFlow< 54 | NestedPartial, 55 | IBrowserSetting 56 | > { 57 | public abstract prerequest$: Observable>>; 58 | protected data: IBrowserSetting; 59 | private readonly defaultData = JSON.stringify(brwoserSettingDefault); 60 | constructor() { 61 | super(); 62 | let strg = localStorage.getItem('browserConfig'); 63 | if (!strg) { 64 | strg = this.defaultData; 65 | localStorage.setItem('browserConfig', strg); 66 | } 67 | this.data = { ...JSON.parse(this.defaultData), ...JSON.parse(strg) }; 68 | } 69 | protected request( 70 | pre: CombErr> 71 | ): Observable> { 72 | this.data = { ...this.data, ...pre[0] }; 73 | localStorage.setItem('browserConfig', JSON.stringify(this.data)); 74 | return of([this.data, []]); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/@dataflow/extra/browser-setting-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$ref": "#/definitions/IBrowserSetting", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "definitions": { 5 | "IBrowserSetting": { 6 | "additionalProperties": false, 7 | "properties": { 8 | "rng.request-interval": { 9 | "description": "请求间隔, 需要定时从rclone服务器中获取数据的时间间隔.\n\n单位: 毫秒(ms)\n\n影响范围:\n - 响应时间的刷新频率(位于主侧栏左上角)\n - Dashboard 和 Job Manager 的更新间隔\n\n建议:\n - 如果与 rclone 服务器响应时间小于 100ms(通常在局域网下), 可以适当调低数值", 10 | "type": "number" 11 | }, 12 | "rng.speedChart.windowSize": { 13 | "description": "速度图表的时间跨度, 速度图表只会保留最近一段时间内的数据\n\n单位: 秒(s)\n\n原则:\n - 应小于 `rng.request-interval`(转化为相同单位后)\n\n影响范围:\n - dashboard 和 Job manager 中的速度图表时间跨度", 14 | "type": "number" 15 | } 16 | }, 17 | "required": ["rng.request-interval", "rng.speedChart.windowSize"], 18 | "type": "object" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/@dataflow/extra/clipboard-flow.ts: -------------------------------------------------------------------------------- 1 | import { NothingFlow } from '../core'; 2 | import { OperationsListFlowOutItemNode } from '../rclone'; 3 | import { NavigationFlowOutNode } from '../rclone/navigation-flow'; 4 | 5 | export type IManipulate = 'copy' | 'move' | 'del'; 6 | 7 | /** 8 | * @param o operation 9 | */ 10 | export function Manipulate2Icon(o: IManipulate): string { 11 | if (o === 'del') return 'trash-2'; 12 | return o; 13 | } 14 | export interface ClipboardItem { 15 | oper: IManipulate; 16 | key: string; 17 | srcRemote: string; 18 | srcItem: OperationsListFlowOutItemNode; 19 | dst?: NavigationFlowOutNode; 20 | } 21 | 22 | /** 23 | * @description A tiny build-in clipboard system 24 | */ 25 | export class Clipboard { 26 | /** 27 | * @description All of items in clipboard 28 | */ 29 | public get values(): ClipboardItem[] { 30 | return Array.from(this.data.values()); 31 | } 32 | 33 | /** 34 | * @description count items in clipboard 35 | */ 36 | public get size(): number { 37 | return this.data.size; 38 | } 39 | private data = new Map(); 40 | 41 | /** 42 | * marshal remote path 43 | */ 44 | public static genKey(remote: string, path: string) { 45 | return JSON.stringify({ r: remote, p: path }); 46 | } 47 | 48 | public add( 49 | o: IManipulate, 50 | remote: string, 51 | row: OperationsListFlowOutItemNode, 52 | dst?: NavigationFlowOutNode 53 | ) { 54 | const key = Clipboard.genKey(remote, row.Path); 55 | this.data.set(key, { 56 | oper: o, 57 | key, 58 | srcItem: { ...row }, 59 | srcRemote: remote, 60 | dst: { ...dst }, 61 | }); 62 | } 63 | 64 | public pop(remote: string, path: string): ClipboardItem { 65 | const key = Clipboard.genKey(remote, path); 66 | if (!this.data.has(key)) return undefined; 67 | const ans = this.data.get(key); 68 | this.data.delete(key); 69 | return ans; 70 | } 71 | 72 | public getManipulation(remote: string, path: string): IManipulate { 73 | const key = Clipboard.genKey(remote, path); 74 | if (!this.data.has(key)) return undefined; 75 | return this.data.get(key).oper; 76 | } 77 | /** 78 | * count items whose manipulation kind is `o` 79 | */ 80 | public countManipulation(o: IManipulate): number { 81 | let cnt = 0; 82 | this.data.forEach(x => (x.oper === o ? cnt++ : null)); 83 | return cnt; 84 | } 85 | 86 | /** 87 | * clear items 88 | * 89 | * @param opers a list of operation kind 90 | */ 91 | public clear(...opers: IManipulate[]) { 92 | if (opers.length === 0) this.data.clear(); 93 | else 94 | this.values 95 | .filter(x => opers.some(y => x.oper === y)) 96 | .forEach(x => { 97 | this.data.delete(x.key); 98 | }); 99 | } 100 | } 101 | 102 | export interface ClipboardFlowNode { 103 | clipboard: Clipboard; 104 | } 105 | 106 | export abstract class ClipboardFlow extends NothingFlow {} 107 | -------------------------------------------------------------------------------- /src/app/@dataflow/extra/current-user-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { CombErr, SupersetFlow } from '../core'; 3 | import { IUser, UsersFlowOutNode } from './users-flow'; 4 | 5 | export interface CurrentUserFlowOutNode extends IUser {} 6 | 7 | export abstract class CurrentUserFlow extends SupersetFlow< 8 | UsersFlowOutNode, 9 | CurrentUserFlowOutNode 10 | > { 11 | /** 12 | * switch user 13 | */ 14 | public static setLogin(name: string) { 15 | localStorage.setItem('loginUser', name); 16 | } 17 | public static getLogin(): string { 18 | return localStorage.getItem('loginUser'); 19 | } 20 | // public prerequest$: Observable>; 21 | protected request(pre: CombErr): Observable> { 22 | if (pre[1].length !== 0) return of([{}, pre[1]]) as any; 23 | const loginUser = CurrentUserFlow.getLogin(); 24 | if (!loginUser) { 25 | // initilazation 26 | if (pre[0].users.length === 0) 27 | return of([{}, new Error('not exist any config in users.')]) as any; 28 | CurrentUserFlow.setLogin(pre[0].users[0].name); 29 | return of([{ ...pre[0].users[0] }, []]); 30 | } 31 | // find config by name 32 | for (const item of pre[0].users) if (item.name === loginUser) return of([{ ...item }, []]); 33 | // lost name 34 | return of([{ ...pre[0].users[0] }, []]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/@dataflow/extra/index.ts: -------------------------------------------------------------------------------- 1 | export * from './users-flow'; 2 | export * from './current-user-flow'; 3 | export * from './clipboard-flow'; 4 | export * from './operations-list-extends-flow'; 5 | export * from './browser-setting-flow'; 6 | -------------------------------------------------------------------------------- /src/app/@dataflow/extra/operations-list-extends-flow.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import { Observable, of } from 'rxjs'; 3 | import { getIconForFile, getIconForFolder } from 'vscode-icons-js'; 4 | import { FormatBytes } from '../../utils/format-bytes'; 5 | import { BareFlow, CombErr } from '../core'; 6 | import { 7 | OperationsListFlowInNode, 8 | OperationsListFlowOutItemNode, 9 | OperationsListFlowOutNode, 10 | } from '../rclone'; 11 | import { ClipboardFlowNode, IManipulate } from './clipboard-flow'; 12 | 13 | export interface OperationsListExtendsFlowInNode 14 | extends OperationsListFlowOutNode, 15 | OperationsListFlowInNode, 16 | ClipboardFlowNode {} 17 | 18 | export interface OperationsListExtendsFlowOutItemNode extends OperationsListFlowOutItemNode { 19 | remote: string; 20 | SizeHumanReadable: string; 21 | ModTimeHumanReadable: string; 22 | ModTimeMoment: moment.Moment; 23 | TypeIcon: string; 24 | Manipulation: IManipulate; 25 | } 26 | 27 | export interface OperationsListExtendsFlowOutNode extends OperationsListFlowInNode { 28 | list: OperationsListExtendsFlowOutItemNode[]; 29 | } 30 | 31 | // TODO: using cache flow? 32 | export abstract class OperationsListExtendsFlow extends BareFlow< 33 | OperationsListExtendsFlowInNode, 34 | OperationsListExtendsFlowOutNode 35 | > { 36 | // public prerequest$: Observable>; 37 | protected request( 38 | pre: CombErr 39 | ): Observable> { 40 | if (pre[1].length !== 0 || pre[1].length !== 0) return pre as any; 41 | const list = pre[0].list.map( 42 | (item): OperationsListExtendsFlowOutItemNode => { 43 | const ModTimeMoment = moment(item.ModTime); 44 | return { 45 | ...item, 46 | remote: pre[0].remote, 47 | SizeHumanReadable: FormatBytes(item.Size), 48 | ModTimeMoment, 49 | ModTimeHumanReadable: ModTimeMoment.fromNow(), 50 | Manipulation: pre[0].clipboard.getManipulation(pre[0].remote, item.Path), 51 | TypeIcon: item.IsDir ? getIconForFolder(item.Name) : getIconForFile(item.Name), 52 | }; 53 | } 54 | ); 55 | return of([{ ...pre[0], list }, []]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/@dataflow/extra/users-flow.spec.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { TestScheduler } from 'rxjs/testing'; 3 | import { CombErr, FlowInNode } from '../core'; 4 | import { IUser, UsersFlow } from './users-flow'; 5 | 6 | describe('UsersFlow', () => { 7 | let scheduler: TestScheduler; 8 | beforeEach( 9 | () => 10 | (scheduler = new TestScheduler((actual, expected) => { 11 | expect(actual).toEqual(expected); 12 | })) 13 | ); 14 | it('get init value', () => { 15 | scheduler.run(helpers => { 16 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 17 | const values = { 18 | a: 1, 19 | b: [{ users: UsersFlow.defaultUser }, []], 20 | }; 21 | const pre = cold('a-a--', values); 22 | const expected = 'b-b--'; 23 | 24 | const rst = new (class extends UsersFlow { 25 | public prerequest$ = pre.pipe(map((): CombErr => [{}, []])); 26 | })(); 27 | 28 | UsersFlow.purge(); 29 | rst.deploy(); 30 | 31 | expectObservable(rst.getOutput()).toBe(expected, values); 32 | }); 33 | }); 34 | it('get storagaed value', () => { 35 | scheduler.run(helpers => { 36 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 37 | const values = { 38 | a: 1, 39 | b: [{ users: [] }, []], 40 | }; 41 | const pre = cold('a-a--', values); 42 | const expected = 'b-b--'; 43 | 44 | const rst = new (class extends UsersFlow { 45 | public prerequest$ = pre.pipe(map((): CombErr => [{}, []])); 46 | })(); 47 | 48 | UsersFlow.setAll([]); 49 | rst.deploy(); 50 | 51 | expectObservable(rst.getOutput()).toBe(expected, values); 52 | }); 53 | }); 54 | it('update sigel user', () => { 55 | scheduler.run(helpers => { 56 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 57 | const values = { 58 | a: 1, 59 | b: [ 60 | { 61 | users: [{ name: '123', url: 'new' }] as IUser[], 62 | }, 63 | [], 64 | ], 65 | }; 66 | const pre = cold('a-a--', values); 67 | const expected = 'b-b--'; 68 | 69 | const rst = new (class extends UsersFlow { 70 | public prerequest$ = pre.pipe(map((): CombErr => [{}, []])); 71 | })(); 72 | 73 | UsersFlow.setAll([ 74 | { 75 | name: '123', 76 | url: 'poi', 77 | }, 78 | ]); 79 | UsersFlow.set({ name: '123', url: 'new' }); 80 | rst.deploy(); 81 | 82 | expectObservable(rst.getOutput()).toBe(expected, values); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/app/@dataflow/extra/users-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of, Subject } from 'rxjs'; 2 | import { BareFlow, CombErr, FlowInNode } from '../core'; 3 | import { IRcloneServer } from '../rclone'; 4 | 5 | export interface IUser extends IRcloneServer { 6 | name: string; 7 | } 8 | 9 | export interface UsersFlowOutNode extends FlowInNode { 10 | users: IUser[]; 11 | } 12 | 13 | export abstract class UsersFlow extends BareFlow { 14 | public static readonly defaultUser: IUser[] = [ 15 | { name: 'localhost', url: 'http://localhost:5572' }, 16 | ]; 17 | public static getAll(): IUser[] { 18 | const dataRaw = localStorage.getItem('users'); 19 | if (!dataRaw) { 20 | // initialization 21 | localStorage.setItem('users', JSON.stringify(UsersFlow.defaultUser)); 22 | return UsersFlow.defaultUser; 23 | } 24 | return JSON.parse(dataRaw); 25 | } 26 | 27 | public static get(name: string): IUser | undefined { 28 | const users = this.getAll(); 29 | return users.find(x => x.name === name); 30 | } 31 | 32 | public static setAll(data: IUser[]) { 33 | localStorage.setItem('users', JSON.stringify(data)); 34 | } 35 | public static set(user: IUser, preName: string = user.name) { 36 | const data = this.getAll(); 37 | for (let i = 0; i < data.length; i++) { 38 | if (preName !== data[i].name) continue; 39 | data[i] = user; 40 | this.setAll(data); 41 | return; 42 | } 43 | data.push(user); 44 | this.setAll(data); 45 | } 46 | public static del(name: string) { 47 | const data = this.getAll(); 48 | this.setAll(data.filter(x => x.name !== name)); 49 | } 50 | public static purge() { 51 | localStorage.removeItem('users'); 52 | } 53 | protected request(pre: CombErr): Observable> { 54 | return of([{ users: UsersFlow.getAll() }, []]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/async-post-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxRequest } from 'rxjs/ajax'; 2 | import { AjaxFlowInteralNode, CombErr, FlowSupNode } from '../core'; 3 | import { IRcloneServer, PostFlow } from './post-flow'; 4 | 5 | export interface AsyncPostFlowParamsNode { 6 | group?: string; 7 | } 8 | export interface AsyncPostFlowInNode extends IRcloneServer, AsyncPostFlowParamsNode {} 9 | 10 | export interface AsyncPostFlowOutNode { 11 | jobid: number; 12 | } 13 | 14 | export abstract class AsyncPostFlow< 15 | Tin extends AsyncPostFlowInNode, 16 | Tparms extends AsyncPostFlowParamsNode = AsyncPostFlowParamsNode, 17 | Tsup extends FlowSupNode = Tin & AsyncPostFlowOutNode 18 | > extends PostFlow { 19 | // protected cmd: string; 20 | // protected params: Tparms | ((pre: CombErr) => Tparms); 21 | // protected cacheSupport: boolean; 22 | // public prerequest$: Observable>; 23 | 24 | protected requestAjax(x: CombErr): AjaxRequest { 25 | const res = super.requestAjax(x); 26 | // eslint-disable-next-line no-underscore-dangle 27 | res.body._async = true; 28 | return res; 29 | } 30 | protected reconstructAjaxResult(x: CombErr): CombErr { 31 | if (x[1].length !== 0) return [{}, x[1]] as any; 32 | const rsp = x[0].ajaxRsp.response; 33 | return [{ jobid: rsp.jobid }, []]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/connection-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { distinctUntilChanged } from 'rxjs/operators'; 3 | import { CombErr, SupersetFlow } from '../core'; 4 | import { IRcloneServer } from './post-flow'; 5 | import { NoopAuthFlowSupNode } from './noop-auth-flow'; 6 | 7 | export abstract class ConnectionFlow extends SupersetFlow< 8 | NoopAuthFlowSupNode, 9 | IRcloneServer, 10 | IRcloneServer 11 | > { 12 | // public prerequest$: Observable>; 13 | protected request(pre: CombErr): Observable> { 14 | if (pre[1].length !== 0) return of([{} as any, pre[1]]); 15 | const data: IRcloneServer = { url: pre[0].url }; 16 | if (pre[0].user) data.user = pre[0].user; 17 | if (pre[0].password) data.password = pre[0].password; 18 | return of([data, []]); 19 | } 20 | protected generateSuperset( 21 | current: CombErr, 22 | previous: CombErr 23 | ): CombErr { 24 | const err = [].concat(current[1], previous[1]).filter((x, i, a) => a.indexOf(x) === i); 25 | if (err.length !== 0) return [{}, err] as any; 26 | return [{ url: previous[0].url, password: previous[0].password, user: previous[0].user }, []]; 27 | } 28 | public getSupersetOutput(): Observable> { 29 | return super 30 | .getSupersetOutput() 31 | .pipe(distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y))); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/core-bwlimit-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | 4 | export interface CoreBwlimitFlowParamsNode { 5 | rate?: string; 6 | } 7 | 8 | export interface CoreBwlimitFlowInNode extends IRcloneServer, CoreBwlimitFlowParamsNode {} 9 | 10 | export interface CoreBwlimitFlowOutNode { 11 | bandwidth: { 12 | bytesPerSecond: number; 13 | rate: string; 14 | }; 15 | } 16 | 17 | /** 18 | * newwork flow to query or setting rclone bandwidth 19 | */ 20 | export abstract class CoreBwlimitFlow extends PostFlow< 21 | CoreBwlimitFlowInNode, 22 | CoreBwlimitFlowOutNode, 23 | CoreBwlimitFlowParamsNode 24 | > { 25 | // public prerequest$: Observable>; 26 | protected cmd = 'core/bwlimit'; 27 | protected cacheSupport = false; 28 | protected params = (pre: CombErr): CoreBwlimitFlowParamsNode => { 29 | if (pre[1].length !== 0) return {}; 30 | if (pre[0].rate) return { rate: pre[0].rate }; 31 | return {}; 32 | }; 33 | protected reconstructAjaxResult( 34 | x: CombErr 35 | ): CombErr { 36 | if (x[1].length !== 0) return [{}, x[1]] as any; 37 | const rsp = x[0].ajaxRsp.response; 38 | return [{ bandwidth: rsp }, []]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/core-memstats-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | 4 | export interface CoreMemstatsFlowOutNode { 5 | 'mem-stats': { 6 | Alloc: number; 7 | BuckHashSys: number; 8 | Frees: number; 9 | GCSys: number; 10 | HeapAlloc: number; 11 | HeapIdle: number; 12 | HeapInuse: number; 13 | HeapObjects: number; 14 | HeapReleased: number; 15 | HeapSys: number; 16 | MCacheInuse: number; 17 | MCacheSys: number; 18 | MSpanInuse: number; 19 | MSpanSys: number; 20 | Mallocs: number; 21 | OtherSys: number; 22 | StackInuse: number; 23 | StackSys: number; 24 | Sys: number; 25 | TotalAlloc: number; 26 | }; 27 | } 28 | 29 | export abstract class CoreMemstatsFlow extends PostFlow { 30 | // public prerequest$: Observable>; 31 | protected cmd = 'core/memstats'; 32 | protected params = {}; 33 | protected cacheSupport = true; 34 | protected reconstructAjaxResult( 35 | x: CombErr 36 | ): CombErr { 37 | if (x[1].length !== 0) return [{}, x[1]] as any; 38 | const rsp = x[0].ajaxRsp.response; 39 | return [{ 'mem-stats': rsp }, []]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/core-stats-delete-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr, FlowOutNode } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | 4 | export interface CoreStatsDeleteFlowParamsNode { 5 | group: string; 6 | } 7 | 8 | export interface CoreStatsDeleteFlowInNode extends CoreStatsDeleteFlowParamsNode, IRcloneServer {} 9 | 10 | export abstract class CoreStatsDeleteFlow extends PostFlow< 11 | CoreStatsDeleteFlowInNode, 12 | FlowOutNode, 13 | CoreStatsDeleteFlowParamsNode 14 | > { 15 | // public prerequest$: Observable>; 16 | protected cmd = 'core/stats-delete'; 17 | protected cacheSupport = false; 18 | protected params = (pre: CombErr): CoreStatsDeleteFlowParamsNode => { 19 | if (pre[1].length !== 0 || !pre[0].group) return {} as any; 20 | return { group: pre[0].group }; 21 | }; 22 | protected reconstructAjaxResult(x: CombErr): CombErr { 23 | return x; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/core-stats-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr, FlowOutNode } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | import { NoopAuthFlowSupNode } from './noop-auth-flow'; 4 | 5 | export interface CoreStatsFlowParamsNode { 6 | group?: string; 7 | } 8 | 9 | export interface CoreStatsFlowInNode extends CoreStatsFlowParamsNode, IRcloneServer {} 10 | 11 | export interface ITransferring { 12 | bytes?: number; 13 | eta?: number; 14 | group?: string; 15 | name: string; 16 | percentage?: number; 17 | size: number; 18 | speed?: number; 19 | speedAvg?: number; 20 | } 21 | 22 | export interface CoreStatsFlowOutItemNode { 23 | bytes: number; 24 | checks: number; 25 | deletes: number; 26 | elapsedTime: number; 27 | errors: number; 28 | fatalError: boolean; 29 | retryError: boolean; 30 | speed: number; 31 | transfers: number; 32 | transferring?: ITransferring[]; 33 | } 34 | 35 | export interface CoreStatsFlowOutNode extends FlowOutNode { 36 | 'core-stats': CoreStatsFlowOutItemNode; 37 | } 38 | 39 | export interface CoreStatsFlowSupNode extends CoreStatsFlowOutNode, NoopAuthFlowSupNode {} 40 | 41 | export abstract class CoreStatsFlow extends PostFlow< 42 | CoreStatsFlowInNode, 43 | CoreStatsFlowOutNode, 44 | CoreStatsFlowParamsNode 45 | > { 46 | // public prerequest$: Observable>; 47 | protected cmd = 'core/stats'; 48 | protected cacheSupport = false; 49 | protected params = (pre: CombErr): CoreStatsFlowParamsNode => { 50 | if (pre[1].length !== 0 || !pre[0].group) return {}; 51 | return { group: pre[0].group }; 52 | }; 53 | protected reconstructAjaxResult(x: CombErr): CombErr { 54 | if (x[1].length !== 0) return [{}, x[1]] as any; 55 | const rsp = x[0].ajaxRsp.response; 56 | return [{ 'core-stats': rsp }, []]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/core-stats-reset-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr, FlowOutNode } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | 4 | export interface CoreStatsResetFlowParamsNode { 5 | group?: string; 6 | } 7 | 8 | export interface CoreStatsResetFlowInNode extends CoreStatsResetFlowParamsNode, IRcloneServer {} 9 | 10 | export abstract class CoreStatsResetFlow extends PostFlow< 11 | CoreStatsResetFlowInNode, 12 | FlowOutNode, 13 | CoreStatsResetFlowParamsNode 14 | > { 15 | // public prerequest$: Observable>; 16 | protected cmd = 'core/stats-reset'; 17 | protected cacheSupport = false; 18 | protected params = (pre: CombErr): CoreStatsResetFlowParamsNode => { 19 | if (pre[1].length !== 0 || !pre[0].group) return {} as any; 20 | if (pre[0].group && pre[0].group !== '') return { group: pre[0].group }; 21 | return {}; 22 | }; 23 | protected reconstructAjaxResult(x: CombErr): CombErr { 24 | return x; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/core-version-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | import { NoopAuthFlowSupNode } from './noop-auth-flow'; 4 | 5 | export interface CoreVersionOutNode { 6 | arch: string; 7 | decomposed: number[]; 8 | goVersion: string; 9 | isGit: boolean; 10 | os: string; 11 | version: string; 12 | } 13 | 14 | export interface CoreVersionSupNode extends CoreVersionOutNode, NoopAuthFlowSupNode {} 15 | 16 | export abstract class CoreVersionFlow extends PostFlow { 17 | // public prerequest$: Observable>; 18 | protected cmd = 'core/version'; 19 | protected params: any = {}; 20 | protected cacheSupport = true; 21 | protected reconstructAjaxResult(x: CombErr): CombErr { 22 | if (x[1].length !== 0) return [{}, x[1]] as any; 23 | const rsp = x[0].ajaxRsp.response; 24 | return [rsp, []]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/download-file-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxRequest, AjaxResponse } from 'rxjs/ajax'; 2 | import { AjaxFlowInteralNode, CombErr } from '../core'; 3 | import { GetFlow } from './get-flow'; 4 | import { IRcloneServer } from './post-flow'; 5 | 6 | export interface DownloadFileFlowParamsNode { 7 | remote: string; 8 | Path: string; 9 | Name: string; 10 | } 11 | export interface DownloadFileFlowInNode extends IRcloneServer, DownloadFileFlowParamsNode {} 12 | export abstract class DownloadFileFlow extends GetFlow< 13 | DownloadFileFlowInNode, 14 | { ajaxRsp: AjaxResponse } 15 | > { 16 | // public prerequest$: Observable>; 17 | protected requestAjax(x: CombErr): AjaxRequest { 18 | const req = super.requestAjax(x); 19 | req.url = `${x[0].url}/[${x[0].remote}:]/${x[0].Path}`; 20 | req.responseType = 'blob'; 21 | return req; 22 | } 23 | protected reconstructAjaxResult(x: CombErr): CombErr { 24 | return x; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/get-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxRequest } from 'rxjs/ajax'; 2 | import { AjaxFlow, CombErr, FlowOutNode, FlowSupNode } from '../core'; 3 | import { IRcloneServer } from './post-flow'; 4 | 5 | export abstract class GetFlow< 6 | Tin extends IRcloneServer, 7 | Tout extends FlowOutNode, 8 | Tsup extends FlowSupNode = Tin & Tout 9 | > extends AjaxFlow { 10 | // public prerequest$: Observable>; 11 | protected cacheSupport = false; 12 | protected cachePath = ''; 13 | protected requestAjax(x: CombErr): AjaxRequest { 14 | const headers: any = {}; 15 | if (x[0].user && x[0].user !== '' && x[0].password && x[0].password !== '') 16 | headers.Authorization = 'Basic ' + btoa(`${x[0].user}:${x[0].password}`); 17 | return { 18 | method: 'GET', 19 | headers, 20 | }; 21 | } 22 | // protected reconstructAjaxResult(x: CombErr): CombErr { 23 | // throw new Error('Method not implemented.'); 24 | // } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post-flow'; 2 | export * from './noop-auth-flow'; 3 | export * from './core-stats-flow'; 4 | export * from './list-remotes-flow'; 5 | export * from './operations-list-flow'; 6 | export * from './connection-flow'; 7 | export * from './list-cmd-flow'; 8 | export * from './list-group-flow'; 9 | export * from './operations-mkdir-flow'; 10 | export * from './async-post-flow'; 11 | export * from './operations-copyfile-flow'; 12 | export * from './operations-movefile-flow'; 13 | export * from './operations-deletefile-flow'; 14 | export * from './sync-copy-flow'; 15 | export * from './sync-move-flow'; 16 | export * from './operations-purge-flow'; 17 | export * from './core-memstats-flow'; 18 | export * from './core-bwlimit-flow'; 19 | export * from './options-get-flow'; 20 | export * from './options-set-flow'; 21 | export * from './operations-fsinfo-flow'; 22 | export * from './operations-about-flow'; 23 | export * from './get-flow'; 24 | export * from './download-file-flow'; 25 | export * from './core-stats-delete-flow'; 26 | export * from './core-stats-reset-flow'; 27 | export * from './core-version-flow'; 28 | export * from './navigation-flow'; 29 | export * from './list-mounts-flow'; 30 | export * from './mount-mount-flow'; 31 | export * from './mount-unmount-flow'; 32 | export * from './mount-unmount-all-flow'; 33 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/list-cmd-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { distinctUntilChanged, map } from 'rxjs/operators'; 3 | import { AjaxFlowInteralNode, CombErr } from '../core'; 4 | import { IRcloneServer, PostFlow } from './post-flow'; 5 | 6 | export interface IRcloneCmd { 7 | Path: string; 8 | Title: string; 9 | AuthRequired: boolean; 10 | Help: string; 11 | } 12 | 13 | export interface ListCmdFlowOutNode { 14 | commands: IRcloneCmd[]; 15 | } 16 | 17 | export abstract class ListCmdFlow extends PostFlow { 18 | // public prerequest$: Observable>; 19 | protected cmd = 'rc/list'; 20 | protected params = {}; 21 | protected cacheSupport = false; 22 | protected reconstructAjaxResult(x: CombErr): CombErr { 23 | if (x[1].length !== 0) return [{}, x[1]] as any; 24 | const rsp = x[0].ajaxRsp.response; 25 | return [{ commands: rsp.commands }, []]; 26 | } 27 | public verify(cmd: string): Observable> { 28 | return this.getSupersetOutput().pipe( 29 | map(sup => { 30 | if (sup[1].length !== 0) return [{}, sup[1]] as any; 31 | if (-1 === sup[0].commands.findIndex(x => x.Path === cmd)) 32 | return [{}, [new Error(`not support command: ${cmd}`)]]; 33 | else return [{ url: sup[0].url, password: sup[0].password, user: sup[0].user }, []]; 34 | }), 35 | distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y)) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/list-group-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | 4 | export interface ListGroupFlowOutNode { 5 | groups: string[]; 6 | } 7 | 8 | export abstract class ListGroupFlow extends PostFlow { 9 | // public prerequest$: Observable>; 10 | protected cmd = 'core/group-list'; 11 | protected params = {}; 12 | protected cacheSupport = true; 13 | protected reconstructAjaxResult(x: CombErr): CombErr { 14 | if (x[1].length !== 0) return [{}, x[1]] as any; 15 | return [{ groups: x[0].ajaxRsp.response.groups }, []]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/list-mounts-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | import { NoopAuthFlowSupNode } from './noop-auth-flow'; 4 | 5 | export interface ListMountsOutItemNode { 6 | MountPoint: string; 7 | MountedOn: string; 8 | Fs: string; 9 | } 10 | 11 | export interface ListMountsOutNode { 12 | /** list of current mount points */ 13 | mountPoints: ListMountsOutItemNode[]; 14 | } 15 | 16 | export interface ListMountsSupNode extends ListMountsOutNode, NoopAuthFlowSupNode {} 17 | 18 | /** 19 | * @description This shows currently mounted points, which can be used for performing an unmount 20 | * @abstract 21 | * @class ListMountsFlow 22 | */ 23 | export abstract class ListMountsFlow extends PostFlow { 24 | // public prerequest$: Observable>; 25 | protected cmd = 'mount/listmounts'; 26 | protected params: unknown = {}; 27 | protected cacheSupport = true; 28 | protected reconstructAjaxResult(x: CombErr): CombErr { 29 | if (x[1].length !== 0) return [{}, x[1]] as any; 30 | const rsp = x[0].ajaxRsp.response; 31 | return [{ mountPoints: rsp.mountPoints }, []]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/list-remotes-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | import { NoopAuthFlowSupNode } from './noop-auth-flow'; 4 | 5 | export interface ListRemotesOutNode { 6 | remotes: string[]; 7 | } 8 | 9 | export interface ListRemotesSupNode extends ListRemotesOutNode, NoopAuthFlowSupNode {} 10 | 11 | export abstract class ListRemotesFlow extends PostFlow { 12 | // public prerequest$: Observable>; 13 | protected cmd = 'config/listremotes'; 14 | protected params: unknown = {}; 15 | protected cacheSupport = true; 16 | protected reconstructAjaxResult(x: CombErr): CombErr { 17 | if (x[1].length !== 0) return [{}, x[1]] as any; 18 | const rsp = x[0].ajaxRsp.response; 19 | return [{ remotes: rsp.remotes }, []]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/mount-mount-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr, FlowOutNode, AjaxFlowInteralNode } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | 4 | export type IMountType = 'mount' | 'cmount' | 'mount2' | string; 5 | 6 | export interface MountMountFlowParamsNode { 7 | /** a remote path to be mounted */ 8 | fs: string; 9 | /** valid path on the local machine */ 10 | mountPoint: string; 11 | /** 12 | * One of the values (mount, cmount, mount2) specifies the mount implementation to use 13 | * 14 | * If no mountType is provided, the priority is given as follows: 1. mount 2.cmount 3.mount2 15 | */ 16 | mountType?: IMountType; 17 | } 18 | export interface MountMountFlowInNode extends MountMountFlowParamsNode, IRcloneServer {} 19 | 20 | /** 21 | * @description Create a new mount point. 22 | * 23 | * rclone allows Linux, FreeBSD, macOS and Windows to mount any of 24 | * Rclone's cloud storage systems as a file system with FUSE. 25 | * 26 | * If no mountType is provided, the priority is given as follows: 1. mount 2.cmount 3.mount2 27 | * 28 | * This takes the following parameters 29 | * 30 | * - fs - a remote path to be mounted (required) 31 | * - mountPoint: valid path on the local machine (required) 32 | * - mountType: One of the values (mount, cmount, mount2) specifies the mount implementation to use 33 | * @abstract 34 | * @class MountMountFlow 35 | */ 36 | export abstract class MountMountFlow extends PostFlow { 37 | // public prerequest$: Observable>; 38 | protected cmd = 'mount/mount'; 39 | protected cacheSupport = false; 40 | protected params = (pre: CombErr): MountMountFlowParamsNode => { 41 | if (pre[1].length !== 0) return {} as any; 42 | const ret: MountMountFlowParamsNode = { 43 | fs: pre[0].fs, 44 | mountPoint: pre[0].mountPoint, 45 | }; 46 | if (pre[0].mountType && pre[0].mountType !== '') ret.mountType = pre[0].mountType; 47 | return ret; 48 | }; 49 | protected reconstructAjaxResult(x: CombErr): CombErr { 50 | if (x[1].length !== 0) return x; 51 | return [{}, []]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/mount-unmount-all-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr, FlowOutNode, AjaxFlowInteralNode } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | 4 | /** 5 | * @description Unmount all active mounts. 6 | * @abstract 7 | * @class MountUnmountAllFlow 8 | */ 9 | export abstract class MountUnmountAllFlow extends PostFlow { 10 | // public prerequest$: Observable>; 11 | protected cmd = 'mount/unmountall'; 12 | protected cacheSupport = false; 13 | protected params: unknown = {}; 14 | protected reconstructAjaxResult(x: CombErr): CombErr { 15 | if (x[1].length !== 0) return x; 16 | return [{}, []]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/mount-unmount-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr, FlowOutNode, AjaxFlowInteralNode } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | 4 | export interface MountUnmountFlowParamsNode { 5 | /** valid path on the local machine where the mount was created */ 6 | mountPoint: string; 7 | } 8 | export interface MountUnmountFlowInNode extends MountUnmountFlowParamsNode, IRcloneServer {} 9 | 10 | /** 11 | * @description Unmount selected active mount. 12 | * 13 | * rclone allows Linux, FreeBSD, macOS and Windows to 14 | * mount any of Rclone's cloud storage systems as a file system with 15 | * FUSE. 16 | * 17 | * This takes the following parameters 18 | * 19 | * - mountPoint: valid path on the local machine where the mount was created (required) 20 | * 21 | * Eg 22 | * 23 | * rclone rc mount/unmount mountPoint=/home//mountPoint 24 | * 25 | * @abstract 26 | * @class MountUnmountFlow 27 | */ 28 | export abstract class MountUnmountFlow extends PostFlow { 29 | // public prerequest$: Observable>; 30 | protected cmd = 'mount/unmount'; 31 | protected cacheSupport = false; 32 | protected params = (pre: CombErr): MountUnmountFlowParamsNode => { 33 | if (pre[1].length !== 0) return {} as any; 34 | return { mountPoint: pre[0].mountPoint }; 35 | }; 36 | protected reconstructAjaxResult(x: CombErr): CombErr { 37 | if (x[1].length !== 0) return x; 38 | return [{}, []]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/navigation-flow.ts: -------------------------------------------------------------------------------- 1 | import { NothingFlow } from '../core'; 2 | 3 | export interface NavigationFlowOutNode { 4 | remote?: string; 5 | path?: string; 6 | } 7 | 8 | export abstract class NavigationFlow extends NothingFlow { 9 | // public prerequest$: Observable>; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/noop-auth-flow.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { map, tap } from 'rxjs/operators'; 3 | import { TestScheduler } from 'rxjs/testing'; 4 | import { CombErr } from '../core'; 5 | import { IRcloneServer } from './post-flow'; 6 | import { NoopAuthFlow, NoopAuthFlowOutNode } from './noop-auth-flow'; 7 | 8 | describe('NoopAuthFlow', () => { 9 | let scheduler: TestScheduler; 10 | beforeEach( 11 | () => 12 | (scheduler = new TestScheduler((actual, expected) => { 13 | expect(actual).toEqual(expected); 14 | })) 15 | ); 16 | xit('fetch success', () => { 17 | scheduler.run(helpers => { 18 | const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; 19 | const values: { [id: string]: CombErr | boolean } = { 20 | a: [{ url: 'http://127.0.0.1:5572', user: 'admin', password: 'admin' }, []], 21 | // b: [{ url: 'http://127.0.0.1:5572' }, []], // auth failure case 1 22 | // c: [{ url: 'http://127.0.0.1:5572', user: 'boo', password: 'foo' }, []], // auth failure case 2 23 | // d: [{ url: 'http://127.0.0.1:1' }, []], // network not archive. 24 | r: true, 25 | s: false, 26 | }; 27 | // const pre = cold('a--b--c--d', values); 28 | const inp = cold('a---', values) as Observable>; 29 | const expected = 'r---'; 30 | // const expected = 'a--b--b--b'; 31 | 32 | const rst = new (class extends NoopAuthFlow { 33 | public prerequest$ = inp; 34 | })(); 35 | rst.deploy(); 36 | 37 | expectObservable( 38 | rst.getOutput().pipe( 39 | // tap(x => console.log(x)), 40 | map(x => x[1].length === 0) 41 | ) 42 | ).toBe(expected, values); 43 | // ERROR: tap is ok, but toBe can't get any output. 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/noop-auth-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxRequest } from 'rxjs/ajax'; 2 | import { AjaxFlowInteralNode, CombErr } from '../core'; 3 | import { IRcloneServer, PostFlow } from './post-flow'; 4 | 5 | export interface NoopAuthFlowOutNode { 6 | 'response-time': number; 7 | } 8 | 9 | export interface NoopAuthFlowSupNode extends IRcloneServer, NoopAuthFlowOutNode {} 10 | 11 | export abstract class NoopAuthFlow extends PostFlow { 12 | // public prerequest$: Observable>; 13 | protected cmd = 'rc/noopauth'; 14 | protected params: unknown = {}; 15 | protected cacheSupport = false; 16 | protected requestAjax(x: CombErr): AjaxRequest { 17 | const ans = super.requestAjax(x); 18 | ans.body = { 19 | timestamp: new Date().getTime(), 20 | }; 21 | return ans; 22 | } 23 | protected reconstructAjaxResult(x: CombErr): CombErr { 24 | if (x[1].length !== 0) return [{}, x[1]] as any; 25 | const rspjson = x[0].ajaxRsp.response; 26 | const rst = new Date().getTime() - rspjson.timestamp; 27 | return [{ 'response-time': rst }, []]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/operations-about-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { AjaxFlowInteralNode, CombErr, FlowOutNode } from '../core'; 3 | import { NavigationFlowOutNode } from './navigation-flow'; 4 | import { OperationsFsinfoFlowOutNode } from './operations-fsinfo-flow'; 5 | import { PostFlow, IRcloneServer } from './post-flow'; 6 | 7 | export interface OperationsAboutFlowParamsNode { 8 | /** a remote name string eg "drive:" */ 9 | fs: string; 10 | } 11 | 12 | export interface OperationsAboutFlowInNode 13 | extends NavigationFlowOutNode, 14 | IRcloneServer, 15 | OperationsFsinfoFlowOutNode {} 16 | 17 | export interface OperationsAboutFlowOutItemNode { 18 | free?: number; 19 | other?: number; 20 | total?: number; 21 | trashed?: number; 22 | used?: number; 23 | } 24 | export interface OperationsAboutFlowOutNode extends FlowOutNode { 25 | about: OperationsAboutFlowOutItemNode; 26 | } 27 | 28 | export abstract class OperationsAboutFlow extends PostFlow< 29 | OperationsAboutFlowInNode, 30 | OperationsAboutFlowOutNode, 31 | OperationsAboutFlowParamsNode 32 | > { 33 | // public prerequest$: Observable>; 34 | protected cmd = 'operations/about'; 35 | protected cacheSupport = true; 36 | protected params = (pre: CombErr): OperationsAboutFlowParamsNode => { 37 | if (pre[1].length !== 0) return {} as any; 38 | if (pre[0].path) return { fs: `${pre[0].remote}:${pre[0].path}` }; 39 | return { fs: `${pre[0].remote}:` }; 40 | }; 41 | protected reconstructAjaxResult( 42 | x: CombErr 43 | ): CombErr { 44 | if (x[1].length !== 0) return [{}, x[1]] as any; 45 | const rsp = x[0].ajaxRsp.response; 46 | return [{ about: rsp }, []]; 47 | } 48 | protected request( 49 | pre: CombErr 50 | ): Observable> { 51 | if (pre[1].length !== 0) return of(pre as any); 52 | if (pre[0]['fs-info'].Features.About) return super.request(pre); 53 | return of([{}, [new Error(`${pre[0].remote} unable to get about info`)]] as any); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/operations-copyfile-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr } from '../core'; 2 | import { IRcloneServer } from './post-flow'; 3 | import { AsyncPostFlow, AsyncPostFlowParamsNode } from './async-post-flow'; 4 | 5 | export interface OperationsCopyfileFlowParamsNode extends AsyncPostFlowParamsNode { 6 | /** a remote name string eg “drive:” for the source */ 7 | srcFs: string; 8 | /** a path within that remote eg “file.txt” for the source */ 9 | srcRemote: string; 10 | /** a remote name string eg “drive2:” for the destination */ 11 | dstFs: string; 12 | /** a path within that remote eg “file2.txt” for the destination */ 13 | dstRemote: string; 14 | } 15 | export interface OperationsCopyfileFlowInNode 16 | extends OperationsCopyfileFlowParamsNode, 17 | IRcloneServer {} 18 | 19 | export abstract class OperationsCopyfileFlow extends AsyncPostFlow< 20 | OperationsCopyfileFlowInNode, 21 | OperationsCopyfileFlowParamsNode 22 | > { 23 | // public prerequest$: Observable>; 24 | protected cmd = 'operations/copyfile'; 25 | protected cacheSupport = false; 26 | protected params = ( 27 | pre: CombErr 28 | ): OperationsCopyfileFlowParamsNode => { 29 | if (pre[1].length !== 0) return {} as any; 30 | return { 31 | srcFs: pre[0].srcFs, 32 | srcRemote: pre[0].srcRemote, 33 | dstFs: pre[0].dstFs, 34 | dstRemote: pre[0].dstRemote, 35 | }; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/operations-deletefile-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr } from '../core'; 2 | import { IRcloneServer } from './post-flow'; 3 | import { AsyncPostFlow, AsyncPostFlowParamsNode } from './async-post-flow'; 4 | 5 | export interface OperationsDeletefileFlowParamsNode extends AsyncPostFlowParamsNode { 6 | /** a remote name string eg "drive:" */ 7 | srcFs: string; 8 | /** a path within that remote eg "dir" */ 9 | srcRemote: string; 10 | } 11 | 12 | export interface OperationsDeletefileFlowInNode 13 | extends OperationsDeletefileFlowParamsNode, 14 | IRcloneServer {} 15 | 16 | interface OperationsDeletefileFlowInnerParamsNode extends AsyncPostFlowParamsNode { 17 | /** a remote name string eg "drive:" */ 18 | fs: string; 19 | /** a path within that remote eg "dir" */ 20 | remote: string; 21 | } 22 | 23 | export abstract class OperationsDeletefileFlow extends AsyncPostFlow< 24 | OperationsDeletefileFlowInNode, 25 | OperationsDeletefileFlowInnerParamsNode 26 | > { 27 | // public prerequest$: Observable>; 28 | protected cmd = 'operations/deletefile'; 29 | protected cacheSupport = false; 30 | protected params = ( 31 | pre: CombErr 32 | ): OperationsDeletefileFlowInnerParamsNode => { 33 | if (pre[1].length !== 0) return {} as any; 34 | return { 35 | fs: pre[0].srcFs, 36 | remote: pre[0].srcRemote, 37 | }; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/operations-fsinfo-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr, FlowOutNode } from '../core'; 2 | import { NavigationFlowOutNode } from './navigation-flow'; 3 | import { IRcloneServer, PostFlow } from './post-flow'; 4 | 5 | export interface OperationsFsinfoFlowParamsNode { 6 | /** a remote name string eg "drive:" */ 7 | fs: string; 8 | } 9 | 10 | export interface OperationsFsinfoFlowInNode extends NavigationFlowOutNode, IRcloneServer {} 11 | 12 | export interface OperationsFsinfoFlowOutItemNode { 13 | Features: { 14 | About: boolean; 15 | BucketBased: boolean; 16 | CanHaveEmptyDirectories: boolean; 17 | CaseInsensitive: boolean; 18 | ChangeNotify: boolean; 19 | CleanUp: boolean; 20 | Copy: boolean; 21 | DirCacheFlush: boolean; 22 | DirMove: boolean; 23 | DuplicateFiles: boolean; 24 | GetTier: boolean; 25 | ListR: boolean; 26 | MergeDirs: boolean; 27 | Move: boolean; 28 | OpenWriterAt: boolean; 29 | PublicLink: boolean; 30 | Purge: boolean; 31 | PutStream: boolean; 32 | PutUnchecked: boolean; 33 | ReadMimeType: boolean; 34 | ServerSideAcrossConfigs: boolean; 35 | SetTier: boolean; 36 | SetWrapper: boolean; 37 | UnWrap: boolean; 38 | WrapFs: boolean; 39 | WriteMimeType: boolean; 40 | }; 41 | // Names of hashes available 42 | Hashes: ( 43 | | 'MD5' 44 | | 'SHA-1' 45 | | 'DropboxHash' 46 | | 'QuickXorHash' 47 | | 'Whirlpool' 48 | | 'CRC-32' 49 | | 'MailruHash' 50 | )[]; 51 | // Name as created 52 | Name: string; 53 | // Precision of timestamps in ns 54 | Precision: number; 55 | // Path as created 56 | Root: string; 57 | // how the remote will appear in logs 58 | String: string; 59 | } 60 | export interface OperationsFsinfoFlowOutNode extends FlowOutNode { 61 | 'fs-info': OperationsFsinfoFlowOutItemNode; 62 | } 63 | 64 | export abstract class OperationsFsinfoFlow extends PostFlow< 65 | OperationsFsinfoFlowInNode, 66 | OperationsFsinfoFlowOutNode, 67 | OperationsFsinfoFlowParamsNode 68 | > { 69 | // public prerequest$: Observable>; 70 | protected cmd = 'operations/fsinfo'; 71 | protected cacheSupport = true; 72 | protected params = (pre: CombErr): OperationsFsinfoFlowParamsNode => { 73 | if (pre[1].length !== 0) return {} as any; 74 | return { fs: `${pre[0].remote}:` }; 75 | }; 76 | protected reconstructAjaxResult( 77 | x: CombErr 78 | ): CombErr { 79 | if (x[1].length !== 0) return [{}, x[1]] as any; 80 | const rsp = x[0].ajaxRsp.response; 81 | return [{ 'fs-info': rsp }, []]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/operations-list-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr } from '../core'; 2 | import { NavigationFlowOutNode } from './navigation-flow'; 3 | import { PostFlow, IRcloneServer } from './post-flow'; 4 | 5 | export interface OperationsListFlowParmsNode { 6 | fs: string; 7 | remote: string; 8 | opt?: { 9 | recurse?: boolean; 10 | noModTime?: boolean; 11 | showEncrypted?: boolean; 12 | showOrigIDs?: boolean; 13 | showHash?: boolean; 14 | }; 15 | } 16 | 17 | export interface OperationsListFlowInNode extends NavigationFlowOutNode, IRcloneServer {} 18 | 19 | export interface OperationsListFlowOutItemNode { 20 | Path: string; 21 | Name: string; 22 | Size: number; 23 | MimeType: string; 24 | ModTime: string; 25 | IsDir: false; 26 | Hashes: { 27 | MD5: string; 28 | }; 29 | ID: string; 30 | OrigID: string; 31 | } 32 | 33 | export interface OperationsListFlowOutNode { 34 | list: OperationsListFlowOutItemNode[]; 35 | } 36 | 37 | export abstract class OperationsListFlow extends PostFlow< 38 | OperationsListFlowInNode, 39 | OperationsListFlowOutNode, 40 | OperationsListFlowParmsNode 41 | > { 42 | // public prerequest$: Observable>; 43 | protected cmd = 'operations/list'; 44 | protected cacheSupport = true; 45 | protected params = (pre: CombErr): OperationsListFlowParmsNode => { 46 | if (pre[1].length !== 0) return {} as any; 47 | if (!pre[0].remote) throw new Error('not provide remote'); 48 | return { 49 | fs: `${pre[0].remote}:`, 50 | remote: pre[0].path ? pre[0].path : '', 51 | // opt: { 52 | // showOrigIDs: false, // TODO: depends on remote type(local, not support) 53 | // showHash: false, 54 | // }, 55 | }; 56 | }; 57 | protected reconstructAjaxResult( 58 | x: CombErr 59 | ): CombErr { 60 | if (x[1].length !== 0) return [{}, x[1]] as any; 61 | const rsp = x[0].ajaxRsp.response; 62 | return [{ list: rsp.list }, []]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/operations-mkdir-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr, FlowOutNode } from '../core'; 2 | import { NavigationFlowOutNode } from './navigation-flow'; 3 | import { PostFlow, IRcloneServer } from './post-flow'; 4 | 5 | export interface OperationsMkdirFlowParamsNode { 6 | /** a remote name string eg “drive:” */ 7 | fs: string; 8 | /** a path within that remote eg “dir” */ 9 | remote: string; 10 | } 11 | export interface OperationsMkdirFlowInNode extends NavigationFlowOutNode, IRcloneServer {} 12 | export interface OperationsMkdirFlowOutNode extends FlowOutNode {} 13 | 14 | export abstract class OperationsMkdirFlow extends PostFlow< 15 | OperationsMkdirFlowInNode, 16 | OperationsMkdirFlowOutNode, 17 | OperationsMkdirFlowParamsNode 18 | > { 19 | // public prerequest$: Observable>; 20 | protected cmd = 'operations/mkdir'; 21 | protected cacheSupport = false; 22 | protected params = (pre: CombErr): OperationsMkdirFlowParamsNode => { 23 | if (pre[1].length !== 0) return {} as any; 24 | return { 25 | fs: `${pre[0].remote}:`, 26 | remote: pre[0].path, 27 | }; 28 | }; 29 | protected reconstructAjaxResult( 30 | x: CombErr 31 | ): CombErr { 32 | return [{}, x[1]]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/operations-movefile-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr } from '../core'; 2 | import { IRcloneServer } from './post-flow'; 3 | import { AsyncPostFlow, AsyncPostFlowParamsNode } from './async-post-flow'; 4 | 5 | export interface OperationsMovefileFlowParamsNode extends AsyncPostFlowParamsNode { 6 | /** a remote name string eg “drive:” for the source */ 7 | srcFs: string; 8 | /** a path within that remote eg “file.txt” for the source */ 9 | srcRemote: string; 10 | /** a remote name string eg “drive2:” for the destination */ 11 | dstFs: string; 12 | /** a path within that remote eg “file2.txt” for the destination */ 13 | dstRemote: string; 14 | } 15 | export interface OperationsMovefileFlowInNode 16 | extends OperationsMovefileFlowParamsNode, 17 | IRcloneServer {} 18 | 19 | export abstract class OperationsMovefileFlow extends AsyncPostFlow< 20 | OperationsMovefileFlowInNode, 21 | OperationsMovefileFlowParamsNode 22 | > { 23 | // public prerequest$: Observable>; 24 | protected cmd = 'operations/movefile'; 25 | protected cacheSupport = false; 26 | protected params = ( 27 | pre: CombErr 28 | ): OperationsMovefileFlowParamsNode => { 29 | if (pre[1].length !== 0) return {} as any; 30 | return { 31 | srcFs: pre[0].srcFs, 32 | srcRemote: pre[0].srcRemote, 33 | dstFs: pre[0].dstFs, 34 | dstRemote: pre[0].dstRemote, 35 | }; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/operations-purge-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr } from '../core'; 2 | import { IRcloneServer } from './post-flow'; 3 | import { AsyncPostFlow, AsyncPostFlowParamsNode } from './async-post-flow'; 4 | 5 | export interface OperationsPurgeFlowParamsNode extends AsyncPostFlowParamsNode { 6 | /** a remote name string eg "drive:" */ 7 | srcFs: string; 8 | /** a path within that remote eg "dir" */ 9 | srcRemote: string; 10 | } 11 | 12 | export interface OperationsPurgeFlowInNode extends OperationsPurgeFlowParamsNode, IRcloneServer {} 13 | 14 | interface OperationsPurgeFlowInnerParamsNode extends AsyncPostFlowParamsNode { 15 | /** a remote name string eg "drive:" */ 16 | fs: string; 17 | /** a path within that remote eg "dir" */ 18 | remote: string; 19 | } 20 | 21 | export abstract class OperationsPurgeFlow extends AsyncPostFlow< 22 | OperationsPurgeFlowInNode, 23 | OperationsPurgeFlowInnerParamsNode 24 | > { 25 | // public prerequest$: Observable>; 26 | protected cmd = 'operations/purge'; 27 | protected cacheSupport = false; 28 | protected params = ( 29 | pre: CombErr 30 | ): OperationsPurgeFlowInnerParamsNode => { 31 | if (pre[1].length !== 0) return {} as any; 32 | return { 33 | fs: pre[0].srcFs, 34 | remote: pre[0].srcRemote, 35 | }; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/options-set-flow.ts: -------------------------------------------------------------------------------- 1 | import { AjaxFlowInteralNode, CombErr, FlowOutNode } from '../core'; 2 | import { IRcloneServer, PostFlow } from './post-flow'; 3 | import { IRcloneOptions } from './options-get-flow'; 4 | 5 | type NestedPartial = { 6 | [K in keyof T]?: T[K] extends (infer R)[] ? NestedPartial[] : NestedPartial; 7 | }; 8 | 9 | export type OptionsSetFlowParamsNode = NestedPartial; 10 | 11 | export interface OptionsSetFlowInNode extends IRcloneServer { 12 | options: OptionsSetFlowParamsNode; 13 | } 14 | 15 | export abstract class OptionsSetFlow extends PostFlow< 16 | OptionsSetFlowInNode, 17 | FlowOutNode, 18 | OptionsSetFlowParamsNode 19 | > { 20 | // public prerequest$: Observable>; 21 | protected cmd = 'options/set'; 22 | protected cacheSupport = false; 23 | protected params = (pre: CombErr): OptionsSetFlowParamsNode => { 24 | if (pre[1].length !== 0) return {} as any; 25 | return pre[0].options; 26 | }; 27 | protected reconstructAjaxResult(x: CombErr): CombErr { 28 | return [{}, x[1]]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/post-flow.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { AjaxRequest, AjaxResponse } from 'rxjs/ajax'; 3 | import { AjaxFlow, CombErr, FlowInNode, FlowOutNode, FlowSupNode } from '../core'; 4 | 5 | export interface IRcloneServer { 6 | url: string; 7 | user?: string; 8 | password?: string; 9 | } 10 | 11 | export type AjaxFlowNode = [AjaxResponse | any, Error[]]; 12 | 13 | export abstract class PostFlow< 14 | Tin extends IRcloneServer, 15 | Tout extends FlowOutNode, 16 | Tparms extends FlowInNode = FlowInNode, 17 | Tsup extends FlowSupNode = Tin & Tout 18 | > extends AjaxFlow { 19 | // protected cacheSupport: boolean; 20 | // public prerequest$: Observable>; 21 | // protected reconstructAjaxResult(x: CombErr): CombErr { 22 | // throw new Error('Method not implemented.'); 23 | // } 24 | protected abstract cmd: string; 25 | protected abstract params: Tparms | ((pre: CombErr) => Tparms); 26 | protected cachePath: string; 27 | 28 | protected requestAjax(x: CombErr): AjaxRequest { 29 | const headers: any = { 30 | 'Content-Type': 'application/json', 31 | }; 32 | if (x[0].user && x[0].user !== '' && x[0].password && x[0].password !== '') 33 | headers.Authorization = 'Basic ' + btoa(`${x[0].user}:${x[0].password}`); 34 | let body: Tparms; 35 | if (typeof this.params === 'object') body = this.params; 36 | else if (typeof this.params === 'function') body = this.params(x); 37 | else throw Error('params type is unknow.'); 38 | return { 39 | url: x[0].url + '/' + this.cmd, 40 | method: 'POST', 41 | headers, 42 | body, 43 | }; 44 | } 45 | protected request(pre: CombErr): Observable> { 46 | if (pre[1].length === 0) { 47 | if (typeof this.params === 'object') 48 | this.cachePath = JSON.stringify([this.cmd, this.params, pre[0].url]); 49 | else if (typeof this.params === 'function') 50 | this.cachePath = JSON.stringify([this.cmd, this.params(pre), pre[0].url]); 51 | else throw Error('params type is unknow.'); 52 | } 53 | return super.request(pre); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/sync-copy-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr } from '../core'; 2 | import { IRcloneServer } from './post-flow'; 3 | import { AsyncPostFlow, AsyncPostFlowParamsNode } from './async-post-flow'; 4 | 5 | export interface SyncCopyFlowParamsNode extends AsyncPostFlowParamsNode { 6 | /** a remote name string eg "drive:src" for the source */ 7 | srcFs: string; 8 | /** a remote name string eg "drive:dst" for the destination */ 9 | dstFs: string; 10 | } 11 | export interface SyncCopyFlowInNode extends SyncCopyFlowParamsNode, IRcloneServer {} 12 | 13 | export abstract class SyncCopyFlow extends AsyncPostFlow< 14 | SyncCopyFlowInNode, 15 | SyncCopyFlowParamsNode 16 | > { 17 | // public prerequest$: Observable>; 18 | protected cmd = 'sync/copy'; 19 | protected cacheSupport = false; 20 | protected params = (pre: CombErr): SyncCopyFlowParamsNode => { 21 | if (pre[1].length !== 0) return {} as any; 22 | return { 23 | srcFs: pre[0].srcFs, 24 | dstFs: pre[0].dstFs, 25 | }; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/@dataflow/rclone/sync-move-flow.ts: -------------------------------------------------------------------------------- 1 | import { CombErr } from '../core'; 2 | import { IRcloneServer } from './post-flow'; 3 | import { AsyncPostFlow, AsyncPostFlowParamsNode } from './async-post-flow'; 4 | 5 | export interface SyncMoveFlowParamsNode extends AsyncPostFlowParamsNode { 6 | /** a remote name string eg "drive:src" for the source */ 7 | srcFs: string; 8 | /** a remote name string eg "drive:dst" for the destination */ 9 | dstFs: string; 10 | /** delete empty src directories if set */ 11 | deleteEmptySrcDirs: boolean; 12 | } 13 | export interface SyncMoveFlowInNode extends SyncMoveFlowParamsNode, IRcloneServer {} 14 | 15 | export abstract class SyncMoveFlow extends AsyncPostFlow< 16 | SyncMoveFlowInNode, 17 | SyncMoveFlowParamsNode 18 | > { 19 | // public prerequest$: Observable>; 20 | protected cmd = 'sync/move'; 21 | protected cacheSupport = false; 22 | protected params = (pre: CombErr): SyncMoveFlowParamsNode => { 23 | if (pre[1].length !== 0) return {} as any; 24 | return { 25 | srcFs: pre[0].srcFs, 26 | dstFs: pre[0].dstFs, 27 | deleteEmptySrcDirs: pre[0].deleteEmptySrcDirs, 28 | }; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { PagesModule } from './pages/pages.module'; 4 | 5 | const routes: Routes = [ 6 | // { path: 'pages', loadChildren: () => import('./pages/pages.module').then((m) => m.PagesModule) }, 7 | { path: 'pages', loadChildren: () => PagesModule }, 8 | { path: '', redirectTo: 'pages', pathMatch: 'full' }, 9 | { path: '**', redirectTo: 'pages' }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes, { useHash: true })], 14 | exports: [RouterModule], 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | xdescribe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [RouterTestingModule], 9 | declarations: [AppComponent], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'RcloneNg'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.componentInstance; 22 | expect(app.title).toEqual('RcloneNg'); 23 | }); 24 | 25 | // it('should render title', () => { 26 | // const fixture = TestBed.createComponent(AppComponent); 27 | // fixture.detectChanges(); 28 | // const compiled = fixture.nativeElement; 29 | // expect(compiled.querySelector('.content span').textContent).toContain('RcloneNg app is running!'); 30 | // }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-rng', 5 | template: ` `, 6 | styles: [], 7 | }) 8 | export class AppComponent { 9 | title = 'RcloneNg'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { NbLayoutModule, NbMenuModule, NbThemeModule, NbToastrModule } from '@nebular/theme'; 6 | import { ContextMenuModule } from 'ngx-contextmenu'; 7 | import { ModalModule } from 'ngx-modialog-7'; 8 | // tslint:disable-next-line: no-submodule-imports 9 | import { VexModalModule } from 'ngx-modialog-7/plugins/vex'; 10 | import { PaginationService } from 'ngx-pagination'; 11 | import { ResponsiveModule } from 'ngx-responsive'; 12 | import { AppRoutingModule } from './app-routing.module'; 13 | import { AppComponent } from './app.component'; 14 | 15 | @NgModule({ 16 | declarations: [AppComponent], 17 | imports: [ 18 | BrowserModule, 19 | AppRoutingModule, 20 | BrowserAnimationsModule, 21 | ResponsiveModule.forRoot(), 22 | NbThemeModule.forRoot({ name: 'default' }), 23 | NbMenuModule.forRoot(), 24 | NbToastrModule.forRoot(), 25 | NbLayoutModule, 26 | ModalModule.forRoot(), 27 | ContextMenuModule.forRoot({ useBootstrap4: true }), 28 | VexModalModule, 29 | ], 30 | providers: [PaginationService], 31 | bootstrap: [AppComponent], 32 | }) 33 | export class AppModule {} 34 | -------------------------------------------------------------------------------- /src/app/components/diff/diff.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { DebugElement } from '@angular/core'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { RngDiffComponent } from './diff.component'; 7 | 8 | describe('DiffComponent', () => { 9 | let component: RngDiffComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [RngDiffComponent], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(RngDiffComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/components/diff/diff.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { FormatBytes } from '../../utils/format-bytes'; 3 | 4 | @Component({ 5 | selector: 'app-rng-diff', 6 | template: ` 7 | {{ (val < 0 ? '-' + FormatBytes(-val, 0) : FormatBytes(val, 0)) + suffic }} 8 | 12 | 13 | `, 14 | styles: [ 15 | ` 16 | nb-icon { 17 | font-size: 1.5rem; 18 | line-height: 0.65; 19 | } 20 | `, 21 | ], 22 | }) 23 | export class RngDiffComponent implements OnInit { 24 | @Input() val = 0; 25 | 26 | @Input() suffic = ''; 27 | 28 | FormatBytes = FormatBytes; 29 | 30 | constructor() {} 31 | 32 | ngOnInit() {} 33 | } 34 | -------------------------------------------------------------------------------- /src/app/components/key-value-table/key-value-table.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { DebugElement } from '@angular/core'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { RngKeyValueTableComponent } from './key-value-table.component'; 7 | 8 | describe('KeyValueTableComponent', () => { 9 | let component: RngKeyValueTableComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [RngKeyValueTableComponent], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(RngKeyValueTableComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/components/key-value-table/key-value-table.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core'; 2 | import { Columns, Config, DefaultConfig } from 'ngx-easy-table'; 3 | 4 | @Component({ 5 | selector: 'app-rng-kv-table', 6 | template: ` 7 | 14 | 15 | 16 | {{ row.title ? row.title : row.key }} 17 | 18 | 19 | {{ isDefine(data[row.key]) ? data[row.key] : '??' }} 20 | 21 | 26 | 27 | 28 | 29 | `, 30 | styles: [ 31 | ` 32 | th::after { 33 | content: ':'; 34 | } 35 | tr { 36 | vertical-align: top; 37 | } 38 | `, 39 | ], 40 | }) 41 | export class RngKeyValueTableComponent implements OnInit { 42 | constructor() {} 43 | @ContentChild(TemplateRef, { static: true }) public addonTemplate: TemplateRef; 44 | 45 | public configuration: Config; 46 | 47 | columns: Columns[] = [ 48 | { key: 'keys', title: 'keys' }, 49 | { key: 'values', title: 'values' }, 50 | ]; 51 | 52 | @Input() keys: { key: string; title?: string }[] = []; 53 | @Input() data: { [idx: string]: any } = {}; 54 | 55 | isDefine(val: any) { 56 | return typeof val !== 'undefined'; 57 | } 58 | ngOnInit() { 59 | this.configuration = { ...DefaultConfig }; 60 | this.configuration.searchEnabled = false; 61 | this.configuration.isLoading = false; 62 | this.configuration.infiniteScroll = false; 63 | this.configuration.headerEnabled = false; 64 | this.configuration.paginationEnabled = false; 65 | this.configuration.paginationMaxSize = 100; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/components/rng.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { NbIconModule, NbSpinnerModule } from '@nebular/theme'; 4 | import { ChartsModule } from 'ng2-charts'; 5 | import { TableModule } from 'ngx-easy-table'; 6 | import { RngDiffComponent } from './diff/diff.component'; 7 | import { RngKeyValueTableComponent } from './key-value-table/key-value-table.component'; 8 | import { RngSpaceUsageChartComponent } from './space-usage-chart/space-usage-chart.component'; 9 | import { RngSpeedChartComponent } from './speed-chart/speed-chart.component'; 10 | import { RngSummaryComponent } from './summary/summary.component'; 11 | 12 | const RngComponents = [ 13 | RngSpeedChartComponent, 14 | RngDiffComponent, 15 | RngSummaryComponent, 16 | RngKeyValueTableComponent, 17 | RngSpaceUsageChartComponent, 18 | ]; 19 | 20 | @NgModule({ 21 | declarations: RngComponents, 22 | imports: [CommonModule, TableModule, ChartsModule, NbIconModule, NbSpinnerModule], 23 | exports: RngComponents, 24 | }) 25 | export class RngModule {} 26 | -------------------------------------------------------------------------------- /src/app/components/space-usage-chart/space-usage-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { DebugElement } from '@angular/core'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { RngSpaceUsageChartComponent } from './space-usage-chart.component'; 7 | 8 | describe('SpaceUsageChartComponent', () => { 9 | let component: RngSpaceUsageChartComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [RngSpaceUsageChartComponent], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(RngSpaceUsageChartComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/components/space-usage-chart/space-usage-chart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core'; 2 | import { ChartDataSets, ChartOptions } from 'chart.js'; 3 | import { BaseChartDirective, Label } from 'ng2-charts'; 4 | import { OperationsAboutFlowOutItemNode } from '../../@dataflow/rclone'; 5 | import { FormatBytes } from '../../utils/format-bytes'; 6 | 7 | @Component({ 8 | selector: 'app-rng-space-usage-chart', 9 | template: ` 10 |
17 | 26 | 27 |
28 | `, 29 | styles: [], 30 | }) 31 | export class RngSpaceUsageChartComponent implements OnInit { 32 | constructor() {} 33 | // Doughnut 34 | public doughnutChartOptions: ChartOptions = { 35 | legend: { display: false }, 36 | cutoutPercentage: 0, 37 | animation: { animateScale: true }, 38 | tooltips: { 39 | callbacks: { 40 | label(tooltipItem, data) { 41 | let label = (data.labels[tooltipItem.index] as string) || ''; 42 | if (label) { 43 | label += ': '; 44 | } 45 | const value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; 46 | if (typeof value === 'number') label += FormatBytes(value, 3); 47 | else label += value; 48 | return label; 49 | }, 50 | }, 51 | }, 52 | }; 53 | public doughnutChartLabels: Label[] = ['Totol', 'Used', 'Other', 'Trashed', 'Free']; 54 | public doughnutChartData: ChartDataSets[] = [ 55 | { 56 | data: [null, 0, 0, 0, 0], 57 | backgroundColor: ['', '#3366ff', '#0095ff', '#ffaa00', '#00d68f'], 58 | hoverBackgroundColor: ['', '#598bff', '#42aaff', '#ffc94d', '#2ce69b'], 59 | borderWidth: 0, 60 | label: 'Outside', 61 | }, 62 | { 63 | data: [0], 64 | backgroundColor: '#ffffff', 65 | hoverBackgroundColor: '#c5cee0', 66 | borderWidth: 0, 67 | label: 'Inside', 68 | }, 69 | ]; 70 | 71 | @Input() loading = false; 72 | 73 | @ViewChild(BaseChartDirective) chart: BaseChartDirective; 74 | 75 | set data(about: OperationsAboutFlowOutItemNode) { 76 | this.doughnutChartData[0].data = [null, about.used, about.other, about.trashed, about.free]; 77 | this.doughnutChartData[1].data = [about.total ? about.total : null]; 78 | this.chart.update(); 79 | } 80 | 81 | ngOnInit() {} 82 | } 83 | -------------------------------------------------------------------------------- /src/app/components/speed-chart/speed-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { DebugElement } from '@angular/core'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { RngSpeedChartComponent } from './speed-chart.component'; 7 | 8 | describe('SpeedChartComponent', () => { 9 | let component: RngSpeedChartComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [RngSpeedChartComponent], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(RngSpeedChartComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/components/summary/summary.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { DebugElement } from '@angular/core'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { RngSummaryComponent } from './summary.component'; 7 | 8 | describe('SummaryComponent', () => { 9 | let component: RngSummaryComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [RngSummaryComponent], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(RngSummaryComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/components/summary/summary.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { CoreStatsFlow, CoreStatsFlowOutItemNode } from '../../@dataflow/rclone'; 3 | import { FormatBytes } from '../../utils/format-bytes'; 4 | import { ForamtDuration } from '../../utils/format-duration'; 5 | 6 | @Component({ 7 | selector: 'app-rng-summary', 8 | template: ` `, 9 | styles: [], 10 | }) 11 | export class RngSummaryComponent implements OnInit { 12 | @Input() 13 | stats$: CoreStatsFlow; 14 | 15 | keys: { key: string; title?: string }[] = [ 16 | { key: 'bytesHumanReadable', title: 'Bytes' }, 17 | { key: 'speedHumanReadable', title: 'Speed' }, 18 | { key: 'transferring-count', title: 'Transferring' }, 19 | { key: 'transfers', title: 'Transferred' }, 20 | { key: 'checks', title: 'Checks' }, 21 | { key: 'deletes', title: 'Delete' }, 22 | { key: 'durationHumanReadable', title: 'Duration' }, 23 | { key: 'errors', title: 'Errors' }, 24 | { key: 'fatalError', title: 'Fatal Error' }, 25 | { key: 'retryError', title: 'Retry Error' }, 26 | ]; 27 | values: CoreStatsFlowOutItemNode & { 28 | speedHumanReadable: string; 29 | bytesHumanReadable: string; 30 | durationHumanReadable: string; 31 | } = {} as any; 32 | 33 | constructor() {} 34 | isDefine(val: any) { 35 | return typeof val !== 'undefined'; 36 | } 37 | 38 | ngOnInit() { 39 | this.stats$.getOutput().subscribe(([x, err]) => { 40 | if (err.length !== 0) return; 41 | let speed = 0; 42 | if (this.values.transferring) { 43 | this.values.transferring.forEach(y => { 44 | if (y.speed) speed += y.speed; 45 | }); 46 | } 47 | this.values = JSON.parse(JSON.stringify(x['core-stats'])); 48 | this.values.bytesHumanReadable = FormatBytes(this.values.bytes, 4); 49 | this.values.speedHumanReadable = FormatBytes(speed, 4) + '/s'; 50 | this.values.durationHumanReadable = ForamtDuration.humanize(this.values.elapsedTime * 1000, { 51 | language: 'shortEn', 52 | round: true, 53 | }); 54 | this.values['transferring-count'] = this.values.transferring 55 | ? this.values.transferring.length 56 | : 0; 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/pages/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { AboutComponent } from './about.component'; 5 | 6 | const routes: Routes = [{ path: '', component: AboutComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule], 11 | }) 12 | export class AboutRoutingModule {} 13 | -------------------------------------------------------------------------------- /src/app/pages/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutComponent } from './about.component'; 4 | 5 | describe('AboutComponent', () => { 6 | let component: AboutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [AboutComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(AboutComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 | 6 | 7 |
8 |

RcloneNg

9 | An angular web application for rclone 10 |
11 |
12 | 13 | 14 | 15 |
16 | `, 17 | styles: [ 18 | ` 19 | nb-card-header { 20 | text-align: center; 21 | } 22 | nb-card-header h1 { 23 | text-align: center; 24 | } 25 | nb-card-body { 26 | margin-left: auto; 27 | margin-right: auto; 28 | } 29 | :host ::ng-deep a { 30 | text-decoration: none; 31 | } 32 | `, 33 | ], 34 | }) 35 | export class AboutComponent implements OnInit { 36 | constructor() {} 37 | ngOnInit(): void {} 38 | } 39 | -------------------------------------------------------------------------------- /src/app/pages/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | // tslint:disable-next-line: no-submodule-imports 3 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 4 | import { NgModule } from '@angular/core'; 5 | import { NbCardModule } from '@nebular/theme'; 6 | import { MarkdownModule } from 'ngx-markdown'; 7 | import { AboutRoutingModule } from './about-routing.module'; 8 | import { AboutComponent } from './about.component'; 9 | 10 | @NgModule({ 11 | declarations: [AboutComponent], 12 | imports: [ 13 | CommonModule, 14 | AboutRoutingModule, 15 | HttpClientModule, 16 | MarkdownModule.forRoot({ loader: HttpClient }), 17 | NbCardModule, 18 | ], 19 | }) 20 | export class AboutModule {} 21 | -------------------------------------------------------------------------------- /src/app/pages/connection.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { async, inject, TestBed } from '@angular/core/testing'; 4 | import { ConnectionService } from './connection.service'; 5 | 6 | describe('Service: Connection', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [ConnectionService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([ConnectionService], (service: ConnectionService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/connection.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { combineLatest, interval, Observable } from 'rxjs'; 3 | import { map, switchMap } from 'rxjs/operators'; 4 | import { CombErr } from '../@dataflow/core'; 5 | import { 6 | ConnectionFlow, 7 | ListCmdFlow, 8 | NoopAuthFlow, 9 | NoopAuthFlowSupNode, 10 | } from '../@dataflow/rclone'; 11 | import { CurrentUserService } from './current-user.service'; 12 | import { BrowserSettingService } from './settings/browser-setting/browser-setting.service'; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class ConnectionService { 18 | private timer: Observable; 19 | public rst$: NoopAuthFlow; 20 | public connection$: ConnectionFlow; 21 | public listCmd$: ListCmdFlow; 22 | 23 | constructor( 24 | currentUserService: CurrentUserService, 25 | private browserSettingService: BrowserSettingService 26 | ) { 27 | this.timer = browserSettingService 28 | .partialBrowserSetting$('rng.request-interval') 29 | .pipe(switchMap(([int, err]) => interval(int))); 30 | const outer = this; 31 | this.rst$ = new (class extends NoopAuthFlow { 32 | public prerequest$ = combineLatest([ 33 | outer.timer, 34 | currentUserService.currentUserFlow$.getOutput(), 35 | ]).pipe(map(x => x[1])); 36 | })(); 37 | this.rst$.deploy(); 38 | 39 | this.connection$ = new (class extends ConnectionFlow { 40 | public prerequest$: Observable> = outer.rst$.getSupersetOutput(); 41 | })(); 42 | this.connection$.deploy(); 43 | 44 | this.listCmd$ = new (class extends ListCmdFlow { 45 | public prerequest$ = outer.connection$.getSupersetOutput(); 46 | })(); 47 | this.listCmd$.deploy(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/pages/current-user.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { async, inject, TestBed } from '@angular/core/testing'; 4 | import { CurrentUserService } from './current-user.service'; 5 | 6 | describe('Service: CurrentUser', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [CurrentUserService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([CurrentUserService], (service: CurrentUserService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/current-user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { combineLatest, Subject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { CurrentUserFlow } from '../@dataflow/extra'; 5 | import { UsersService } from './users.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class CurrentUserService { 11 | private Trigger = new Subject(); 12 | public currentUserFlow$: CurrentUserFlow; 13 | 14 | public switchUser(name: string) { 15 | this.Trigger.next(name); 16 | } 17 | constructor(private usersService: UsersService) { 18 | const outer = this; 19 | this.currentUserFlow$ = new (class extends CurrentUserFlow { 20 | public prerequest$ = combineLatest([outer.Trigger, usersService.usersFlow$.getOutput()]).pipe( 21 | map(([x, y]) => { 22 | CurrentUserFlow.setLogin(x); 23 | return y; 24 | }) 25 | ); 26 | })(); 27 | this.currentUserFlow$.deploy(); 28 | this.Trigger.next(CurrentUserFlow.getLogin()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { DashboardComponent } from './dashboard.component'; 5 | 6 | const routes: Routes = [{ path: '', component: DashboardComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule], 11 | }) 12 | export class DashboardRoutingModule {} 13 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | xdescribe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [DashboardComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(DashboardComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { FormsModule } from '@angular/forms'; 5 | import { 6 | NbButtonModule, 7 | NbCardModule, 8 | NbIconModule, 9 | NbInputModule, 10 | NbListModule, 11 | NbTabsetModule, 12 | } from '@nebular/theme'; 13 | import { ChartsModule } from 'ng2-charts'; 14 | import { ResponsiveModule } from 'ngx-responsive'; 15 | import { RngModule } from '../../components/rng.module'; 16 | import { DashboardRoutingModule } from './dashboard-routing.module'; 17 | import { DashboardComponent } from './dashboard.component'; 18 | 19 | @NgModule({ 20 | declarations: [DashboardComponent], 21 | imports: [ 22 | CommonModule, 23 | ResponsiveModule, 24 | DashboardRoutingModule, 25 | NbCardModule, 26 | NbButtonModule, 27 | NbIconModule, 28 | ChartsModule, 29 | NbListModule, 30 | RngModule, 31 | NbTabsetModule, 32 | NbInputModule, 33 | FormsModule, 34 | ], 35 | }) 36 | export class DashboardModule {} 37 | -------------------------------------------------------------------------------- /src/app/pages/jobs/contextmenu/group-options.contextmenu.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { NbToastrService } from '@nebular/theme'; 3 | import { ContextMenuComponent, ContextMenuService } from 'ngx-contextmenu'; 4 | import { Observable, Subject } from 'rxjs'; 5 | import { map, withLatestFrom } from 'rxjs/operators'; 6 | import { CombErr } from '../../../@dataflow/core'; 7 | import { CoreStatsResetFlow, CoreStatsResetFlowInNode } from '../../../@dataflow/rclone'; 8 | import { ConnectionService } from '../../connection.service'; 9 | 10 | @Component({ 11 | selector: 'app-job-group-options-context-menu', 12 | template: ` 13 | 14 | 15 | Reset stats 16 | 17 | 18 | 19 | `, 20 | styles: [], 21 | }) 22 | export class GroupOptionsContextMenuComponent implements OnInit { 23 | @Input() contextMenu: ContextMenuComponent; 24 | 25 | constructor( 26 | private contextMenuService: ContextMenuService, 27 | private cmdService: ConnectionService, 28 | private toastrService: NbToastrService 29 | ) {} 30 | 31 | resetTrigger = new Subject(); 32 | resetStats$: CoreStatsResetFlow; 33 | 34 | public onContextMenu($event: MouseEvent, groupName: string): void { 35 | this.contextMenuService.show.next({ 36 | contextMenu: this.contextMenu, 37 | event: $event, 38 | item: groupName, 39 | }); 40 | $event.preventDefault(); 41 | $event.stopPropagation(); 42 | } 43 | 44 | resetStats(groupName: string) { 45 | this.resetTrigger.next(groupName); 46 | } 47 | 48 | ngOnInit() { 49 | const outer = this; 50 | this.resetStats$ = new (class extends CoreStatsResetFlow { 51 | public prerequest$: Observable> = outer.resetTrigger.pipe( 52 | withLatestFrom(outer.cmdService.listCmd$.verify(this.cmd)), 53 | map( 54 | ([group, cmdNode]): CombErr => { 55 | if (cmdNode[1].length !== 0) return [{}, cmdNode[1]] as any; 56 | if (group && group !== '') return [{ ...cmdNode[0], group }, []]; 57 | return cmdNode; 58 | } 59 | ) 60 | ); 61 | })(); 62 | this.resetStats$.deploy(); 63 | this.resetStats$.getSupersetOutput().subscribe(x => { 64 | let group = x[0] && x[0].group; 65 | if (!group) group = '[All]'; 66 | if (x[1].length !== 0) { 67 | this.toastrService.danger(`${group}`, 'Reset stats failure'); 68 | return; 69 | } 70 | this.toastrService.success(`${group}`, 'Reset stats success'); 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/app/pages/jobs/dialogs/clean-finished-groups.dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DialogRef, ModalComponent } from 'ngx-modialog-7'; 3 | // tslint:disable-next-line: no-submodule-imports 4 | import { DialogPreset } from 'ngx-modialog-7/plugins/vex'; 5 | import { Observable, of, Subject, zip } from 'rxjs'; 6 | import { concatMap, delay, map, switchMap, withLatestFrom } from 'rxjs/operators'; 7 | import { CombErr } from '../../../@dataflow/core'; 8 | import { 9 | CoreStatsDeleteFlow, 10 | CoreStatsDeleteFlowInNode, 11 | CoreStatsFlow, 12 | ListGroupFlow, 13 | } from '../../../@dataflow/rclone'; 14 | import { ConnectionService } from '../../connection.service'; 15 | 16 | @Component({ 17 | template: ` 18 | 19 | 20 | Select groups to be deleted 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | `, 41 | styles: [ 42 | ` 43 | nb-card { 44 | margin: calc(-1em - 5px); 45 | } 46 | nb-card-header, 47 | nb-card-footer { 48 | display: flex; 49 | } 50 | label { 51 | padding-left: 0.75rem; 52 | } 53 | .push-to-right { 54 | margin-left: auto; 55 | } 56 | `, 57 | ], 58 | }) 59 | export class CleanFinishedGroupDialogComponent implements ModalComponent, OnInit { 60 | public context: DialogPreset; 61 | finishedGroup: string[] = []; 62 | check: boolean[] = []; 63 | loading = false; 64 | 65 | constructor(public dialog: DialogRef, private cmdService: ConnectionService) { 66 | this.context = dialog.context; 67 | } 68 | 69 | deleteTrigger = new Subject(); 70 | deleteStates$: CoreStatsDeleteFlow; 71 | 72 | confirm() { 73 | this.deleteTrigger.next(this.finishedGroup.filter((_, idx) => this.check[idx])); 74 | this.dialog.close(); 75 | } 76 | 77 | ngOnInit() { 78 | const outer = this; 79 | const trigger = new Subject(); 80 | const listGroup$ = new (class extends ListGroupFlow { 81 | public prerequest$ = trigger.pipe( 82 | withLatestFrom(outer.cmdService.listCmd$.verify(this.cmd)), 83 | map(x => x[1]) 84 | ); 85 | })(); 86 | listGroup$.deploy(); 87 | const stats$ = new (class extends CoreStatsFlow { 88 | public prerequest$ = trigger.pipe( 89 | withLatestFrom(outer.cmdService.listCmd$.verify(this.cmd)), 90 | map(x => x[1]) 91 | ); 92 | })(); 93 | stats$.deploy(); 94 | 95 | this.loading = true; 96 | listGroup$.clearCache(); 97 | stats$.clearCache(); 98 | zip(listGroup$.getOutput(), stats$.getOutput()).subscribe(([list, stats]) => { 99 | this.loading = false; 100 | if (list[1].length !== 0 || stats[1].length !== 0) return; 101 | const transferring = stats[0]['core-stats'].transferring; 102 | this.finishedGroup = !transferring 103 | ? list[0].groups 104 | : list[0].groups.filter(x => !transferring.some(y => x === y.group)); 105 | this.check = this.finishedGroup.map(() => true); 106 | }); 107 | trigger.next(); 108 | 109 | this.deleteStates$ = new (class extends CoreStatsDeleteFlow { 110 | public prerequest$: Observable> = outer.deleteTrigger.pipe( 111 | withLatestFrom(outer.cmdService.listCmd$.verify(this.cmd)), 112 | switchMap(([groups, y]) => { 113 | if (y[1].length !== 0) return of([{}, y[1]] as any); 114 | return of(...groups.map(group => [{ ...y[0], group }, []])); 115 | }), 116 | // TODO: need a tasks queue 117 | concatMap(x => of(x).pipe(delay(1000))) 118 | ); 119 | })(); 120 | this.deleteStates$.deploy(); 121 | this.deleteStates$.getOutput().subscribe(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/app/pages/jobs/jobs-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { JobsComponent } from './jobs.component'; 5 | 6 | const routes: Routes = [{ path: '', component: JobsComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule], 11 | }) 12 | export class JobsRoutingModule {} 13 | -------------------------------------------------------------------------------- /src/app/pages/jobs/jobs.component.scss: -------------------------------------------------------------------------------- 1 | nb-card-header { 2 | display: flex; 3 | } 4 | nb-card-header > nb-icon { 5 | margin-left: auto; 6 | margin-top: auto; 7 | margin-bottom: auto; 8 | font-size: 1.5rem; 9 | cursor: pointer; 10 | } 11 | nb-sidebar { 12 | border-left: solid; 13 | border-color: #edf1f7; 14 | border-left-width: 0.0668rem; 15 | } 16 | :host nb-select ::ng-deep button { 17 | min-width: unset; 18 | } 19 | :host ::ng-deep .scrollable { 20 | display: contents; 21 | } 22 | ul { 23 | list-style-type: none; 24 | } 25 | li { 26 | border-bottom: 1px solid #edf1f7; 27 | } 28 | div.row { 29 | padding-top: 1rem; 30 | } 31 | .active-group { 32 | background-color: #598bff88; 33 | border-radius: 0.25rem; 34 | } 35 | .speed-body { 36 | padding: 0; 37 | min-height: 10rem; 38 | overflow-y: hidden; 39 | } 40 | nb-list-item > nb-icon { 41 | margin-left: auto; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/pages/jobs/jobs.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { JobsComponent } from './jobs.component'; 4 | 5 | xdescribe('JobsComponent', () => { 6 | let component: JobsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [JobsComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(JobsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/jobs/jobs.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { 5 | NbButtonModule, 6 | NbCardModule, 7 | NbCheckboxModule, 8 | NbIconModule, 9 | NbLayoutModule, 10 | NbListModule, 11 | NbSelectModule, 12 | NbSidebarModule, 13 | NbSpinnerModule, 14 | } from '@nebular/theme'; 15 | import { ChartsModule } from 'ng2-charts'; 16 | import { ContextMenuModule } from 'ngx-contextmenu'; 17 | import { TableModule } from 'ngx-easy-table'; 18 | import { ResponsiveModule } from 'ngx-responsive'; 19 | import { RngModule } from '../../components/rng.module'; 20 | import { GroupOptionsContextMenuComponent } from './contextmenu/group-options.contextmenu'; 21 | import { CleanFinishedGroupDialogComponent } from './dialogs/clean-finished-groups.dialog'; 22 | import { JobsRoutingModule } from './jobs-routing.module'; 23 | import { JobsComponent } from './jobs.component'; 24 | import { TransfersComponent } from './transferring/transferring.component'; 25 | 26 | @NgModule({ 27 | declarations: [ 28 | JobsComponent, 29 | TransfersComponent, 30 | CleanFinishedGroupDialogComponent, 31 | GroupOptionsContextMenuComponent, 32 | ], 33 | imports: [ 34 | CommonModule, 35 | ResponsiveModule, 36 | JobsRoutingModule, 37 | NbLayoutModule, 38 | NbSidebarModule, 39 | NbCardModule, 40 | TableModule, 41 | NbListModule, 42 | NbIconModule, 43 | ChartsModule, 44 | RngModule, 45 | NbSelectModule, 46 | NbButtonModule, 47 | NbCheckboxModule, 48 | NbSpinnerModule, 49 | ContextMenuModule, 50 | ], 51 | }) 52 | export class JobsModule {} 53 | -------------------------------------------------------------------------------- /src/app/pages/jobs/transferring/transferring.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { DebugElement } from '@angular/core'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { TransfersComponent } from './transferring.component'; 7 | 8 | describe('TransfersComponent', () => { 9 | let component: TransfersComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [TransfersComponent], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(TransfersComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/pages/jobs/transferring/transferring.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { Columns, Config, DefaultConfig } from 'ngx-easy-table'; 3 | import { CoreStatsFlow, ITransferring } from '../../../@dataflow/rclone'; 4 | import { FormatBytes } from '../../../utils/format-bytes'; 5 | import { ForamtDuration } from '../../../utils/format-duration'; 6 | 7 | @Component({ 8 | selector: 'app-jobs-transferring', 9 | template: ` 10 | 11 | 12 | {{ row.name }} 13 | {{ row.sizeHumanReadable }} 14 | {{ row.percentage }} 15 | {{ row.speedHumanReadable }} 16 | {{ row.etaHumanReadable }} 17 | 18 | 19 | `, 20 | styles: [], 21 | }) 22 | export class TransfersComponent implements OnInit { 23 | @Input() 24 | stats$: CoreStatsFlow; 25 | 26 | public configuration: Config; 27 | public columns: Columns[] = [ 28 | { key: 'name', title: 'Name' }, 29 | { key: 'size', title: 'Size' }, 30 | { key: 'percentage', title: 'Percentage' }, 31 | { key: 'speed', title: 'Speed' }, 32 | { key: 'eta', title: 'eta' }, 33 | ]; 34 | public data: (ITransferring & { 35 | sizeHumanReadable: string; 36 | speedHumanReadable: string; 37 | etaHumanReadable: string; 38 | })[] = []; 39 | 40 | constructor() {} 41 | 42 | ngOnInit() { 43 | this.stats$.getOutput().subscribe(([x, err]) => { 44 | if (err.length !== 0) return; 45 | const data = x['core-stats'].transferring; 46 | this.data = data ? data : ([] as any); 47 | this.data.forEach(y => { 48 | y.sizeHumanReadable = FormatBytes(y.size, 3); 49 | y.speedHumanReadable = FormatBytes(y.speed) + '/s'; 50 | y.etaHumanReadable = 51 | typeof y.eta === 'number' ? ForamtDuration.humanize(y.eta * 1000, { largest: 3 }) : '-'; 52 | }); 53 | }); 54 | 55 | this.configuration = { ...DefaultConfig }; 56 | this.configuration.searchEnabled = true; 57 | this.configuration.isLoading = false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/pages/layout.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { async, inject, TestBed } from '@angular/core/testing'; 4 | import { LayoutService } from './layout.service'; 5 | 6 | describe('Service: Layout', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [LayoutService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([LayoutService], (service: LayoutService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/layout.service.ts: -------------------------------------------------------------------------------- 1 | import { mainModule } from 'process'; 2 | import { Injectable } from '@angular/core'; 3 | import { NbSidebarComponent } from '@nebular/theme'; 4 | import { Observable, Subject } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | import { CombErr, NothingFlow } from '../@dataflow/core'; 7 | 8 | export enum SidebarStatus { 9 | None, 10 | Icon, 11 | Full, 12 | } 13 | 14 | /** 15 | * @description provide some information to archive responsive view 16 | * @class LayoutService 17 | */ 18 | @Injectable({ 19 | providedIn: 'root', 20 | }) 21 | export class LayoutService { 22 | mainSidebarTrigger = new Subject(); 23 | // TODO: use it to replace other place 24 | mainSidebar$: NothingFlow; 25 | constructor() { 26 | const outer = this; 27 | this.mainSidebar$ = new (class extends NothingFlow { 28 | public prerequest$: Observable> = outer.mainSidebarTrigger.pipe( 29 | map(x => [x, []]) 30 | ); 31 | })(); 32 | this.mainSidebar$.deploy(); 33 | this.mainSidebar$.getOutput().subscribe(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/pages/manager/clipboard/clipboard-remotes-table/clipboard-remotes-table.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { DebugElement } from '@angular/core'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { ClipboardRemotesTableComponent } from './clipboard-remotes-table.component'; 7 | 8 | describe('ClipboardRemotesTableComponent', () => { 9 | let component: ClipboardRemotesTableComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [ClipboardRemotesTableComponent], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(ClipboardRemotesTableComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/pages/manager/clipboard/clipboard-remotes-table/clipboard-remotes-table.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core'; 2 | import { API, APIDefinition, Columns, Config, DefaultConfig } from 'ngx-easy-table'; 3 | import { ClipboardItem, IManipulate } from '../../../../@dataflow/extra'; 4 | import { ClipboardService } from '../clipboard.service'; 5 | 6 | interface IRemotesTableItem { 7 | remote: string; 8 | children: string[]; 9 | } 10 | 11 | @Component({ 12 | selector: 'app-clipboard-remotes-table', 13 | template: ` 14 | 21 | 22 | {{ row.srcRemote }} 23 | {{ row.srcItem.Path }} 24 | 25 | 26 | 27 | 28 | 29 | `, 30 | styles: [], 31 | }) 32 | export class ClipboardRemotesTableComponent implements OnInit { 33 | constructor(private service: ClipboardService) {} 34 | @Input() oper: IManipulate; 35 | public configuration: Config; 36 | public columns: Columns[] = [ 37 | { key: 'srcRemote', title: 'Remote', width: '10%' }, 38 | { key: 'srcItem.Path', title: 'Path', width: '80%' }, 39 | { key: '', title: 'Action', width: '10%' }, 40 | ]; 41 | 42 | data: ClipboardItem[] = []; 43 | 44 | @ViewChild('remotesTable') table: APIDefinition; 45 | ngOnInit() { 46 | this.service.clipboard$.getOutput().subscribe(node => { 47 | if (node[1].length !== 0) return; 48 | this.data = node[0].clipboard.values.filter(x => x.oper === this.oper); 49 | }); 50 | 51 | this.configuration = { ...DefaultConfig }; 52 | this.configuration.detailsTemplate = true; 53 | this.configuration.tableLayout.hover = true; 54 | } 55 | toggleDatail($event: MouseEvent, rowidx: number) { 56 | $event.preventDefault(); 57 | this.table.apiEvent({ 58 | type: API.toggleRowIndex, 59 | value: rowidx, 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/pages/manager/clipboard/clipboard.dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DialogRef, ModalComponent } from 'ngx-modialog-7'; 3 | // tslint:disable-next-line: no-submodule-imports 4 | import { DialogPreset } from 'ngx-modialog-7/plugins/vex/ngx-modialog-7-plugins-vex'; 5 | import { IManipulate } from '../../../@dataflow/extra'; 6 | import { ClipboardService } from './clipboard.service'; 7 | 8 | @Component({ 9 | selector: 'app-manager-clipboard-dialog', 10 | template: ` 11 | 12 | 13 | 14 | 15 | 16 | Clipboard 17 | 18 | 19 | 20 | 27 | 28 |
29 | 33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 | `, 41 | styles: [ 42 | ` 43 | nb-card-footer, 44 | nb-card-header { 45 | display: flex; 46 | } 47 | .dialog-card { 48 | margin: calc(-1em - 3px); 49 | } 50 | :host nb-tab { 51 | padding: 0; 52 | } 53 | .clipboard-icon { 54 | font-size: 1.5rem; 55 | margin-right: 0.5rem; 56 | } 57 | `, 58 | ], 59 | }) 60 | export class ClipboardDialogComponent implements OnInit, ModalComponent { 61 | constructor(private service: ClipboardService, public dialog: DialogRef) { 62 | this.context = dialog.context; 63 | } 64 | public context: DialogPreset; 65 | 66 | data: { oper: IManipulate; title: string; icon: string; len: number }[] = [ 67 | { oper: 'copy', title: 'Copy', icon: 'copy', len: 0 }, 68 | { oper: 'move', title: 'Move', icon: 'move', len: 0 }, 69 | { oper: 'del', title: 'Delete', icon: 'trash-2', len: 0 }, 70 | ]; 71 | 72 | ngOnInit() { 73 | this.service.clipboard$.getOutput().subscribe(node => { 74 | if (node[1].length !== 0) return; 75 | this.data.forEach(x => (x.len = node[0].clipboard.countManipulation(x.oper))); 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/pages/manager/clipboard/clipboard.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { async, inject, TestBed } from '@angular/core/testing'; 4 | import { ClipboardService } from './clipboard.service'; 5 | 6 | describe('Service: Clipboard', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [ClipboardService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([ClipboardService], (service: ClipboardService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/manager/clipboard/clipboard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { mapTo } from 'rxjs/operators'; 4 | import { CombErr } from '../../../@dataflow/core'; 5 | import { Clipboard, ClipboardFlow, ClipboardFlowNode, IManipulate } from '../../../@dataflow/extra'; 6 | import { Package, Task } from '../../tasks/tasks-queue'; 7 | import { TasksQueueService } from '../../tasks/tasks-queue.service'; 8 | import { NavigationFlowOutNode } from '../../../@dataflow/rclone'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class ClipboardService extends Clipboard { 14 | private trigger = new Subject(); 15 | clipboard$: ClipboardFlow; 16 | /** 17 | * @description notify clipboard was changed in dataflow 18 | */ 19 | public commit() { 20 | this.trigger.next(1); 21 | } 22 | /** 23 | * @description post copy/move/delete tasks to server 24 | * @param dst only needed by copy/move 25 | * @param opers a group of operation to be post 26 | */ 27 | public async post(dst: NavigationFlowOutNode, ...opers: IManipulate[]) { 28 | const pack: Package = this.values 29 | .filter(x => opers.some(y => y === x.oper)) 30 | .map( 31 | (item): Task => { 32 | // TODO: we need statical type check 33 | const copyOrMoveFile = () => { 34 | return { 35 | srcFs: `${item.srcRemote}:`, 36 | srcRemote: item.srcItem.Path, 37 | dstFs: `${dst.remote}:`, 38 | dstRemote: [dst.path, item.srcItem.Name].join('/'), // TODO: windows path delimiter '\' ? 39 | }; 40 | }; 41 | const copyOrMoveDir = () => { 42 | return { 43 | srcFs: `${item.srcRemote}:${item.srcItem.Path}`, 44 | dstFs: `${dst.remote}:${[dst.path, item.srcItem.Name].join('/')}`, 45 | }; 46 | }; 47 | let handler = ''; 48 | const params: CombErr = [{}, []]; 49 | if (item.oper === 'copy') { 50 | handler = item.srcItem.IsDir ? 'synccopy' : 'copyfile'; 51 | params[0] = item.srcItem.IsDir ? copyOrMoveDir() : copyOrMoveFile(); 52 | } else if (item.oper === 'move') { 53 | handler = item.srcItem.IsDir ? 'syncmove' : 'movefile'; 54 | params[0] = item.srcItem.IsDir ? copyOrMoveDir() : copyOrMoveFile(); 55 | } else if (item.oper === 'del') { 56 | handler = item.srcItem.IsDir ? 'purge' : 'delfile'; 57 | params[0] = { 58 | srcFs: `${item.srcRemote}:`, 59 | srcRemote: item.srcItem.Path, 60 | }; 61 | } 62 | return { handler, params }; 63 | } 64 | ); 65 | const rst = this.tasksQueueService.AddPack(pack); 66 | // .forEach(x => this.tasksPool.add(x.oper, x.srcRemote, x.srcItem, dst)); 67 | this.clear(...opers); 68 | this.commit(); 69 | return rst; 70 | } 71 | 72 | constructor(private tasksQueueService: TasksQueueService) { 73 | super(); 74 | const outer = this; 75 | this.clipboard$ = new (class extends ClipboardFlow { 76 | public prerequest$ = outer.trigger.pipe( 77 | mapTo>([{ clipboard: outer }, []]) 78 | ); 79 | })(); 80 | this.clipboard$.deploy(); 81 | this.clipboard$.getOutput().subscribe(); 82 | this.trigger.next(1); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/pages/manager/dialogs/mkdir.dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { DialogRef, ModalComponent } from 'ngx-modialog-7'; 3 | // tslint:disable-next-line: no-submodule-imports 4 | import { DialogPreset } from 'ngx-modialog-7/plugins/vex'; 5 | 6 | @Component({ 7 | template: ` 8 | 9 | 10 | Create Directory 11 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | `, 32 | styles: [ 33 | ` 34 | nb-card { 35 | margin: calc(-1em - 5px); 36 | } 37 | nb-card-header, 38 | nb-card-footer { 39 | display: flex; 40 | } 41 | .push-to-right { 42 | margin-left: auto; 43 | } 44 | `, 45 | ], 46 | }) 47 | export class MkdirDialogComponent implements ModalComponent { 48 | public context: DialogPreset; 49 | 50 | constructor(public dialog: DialogRef) { 51 | this.context = dialog.context; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/pages/manager/fileMode/download-file.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { async, inject, TestBed } from '@angular/core/testing'; 4 | import { DownloadFileService } from './download-file.service'; 5 | 6 | describe('Service: DownloadFile', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [DownloadFileService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([DownloadFileService], (service: DownloadFileService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/manager/fileMode/download-file.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NbToastrService } from '@nebular/theme'; 3 | import { FileSaverService } from 'ngx-filesaver'; 4 | import { Observable, Subject } from 'rxjs'; 5 | import { map, withLatestFrom } from 'rxjs/operators'; 6 | import { CombErr } from '../../..//@dataflow/core'; 7 | import { 8 | DownloadFileFlow, 9 | DownloadFileFlowInNode, 10 | DownloadFileFlowParamsNode, 11 | NestedGet, 12 | } from '../../../@dataflow/rclone'; 13 | import { ConnectionService } from '../../connection.service'; 14 | import { ServerSettingService } from '../../settings/sever-setting/server-setting.service'; 15 | 16 | @Injectable({ 17 | providedIn: 'root', 18 | }) 19 | export class DownloadFileService { 20 | private trigger = new Subject(); 21 | download$: DownloadFileFlow; 22 | 23 | post(task: DownloadFileFlowParamsNode) { 24 | this.trigger.next(task); 25 | this.toastr.default(task.Name, 'Downloading ...'); 26 | } 27 | 28 | constructor( 29 | private cmdService: ConnectionService, 30 | private toastr: NbToastrService, 31 | private fileSaverService: FileSaverService, 32 | private serverSettingService: ServerSettingService 33 | ) { 34 | const outer = this; 35 | this.download$ = new (class extends DownloadFileFlow { 36 | public prerequest$: Observable> = outer.trigger.pipe( 37 | withLatestFrom( 38 | outer.cmdService.connection$.getOutput(), 39 | outer.serverSettingService.options$.getOutput() 40 | ), 41 | map( 42 | ([item, connectNode, serverSettingNode]): CombErr => { 43 | if (serverSettingNode[1].length !== 0) return [{}, serverSettingNode[1]] as any; 44 | if (NestedGet(serverSettingNode[0].options, 'rc', 'Serve')) 45 | return [{ ...connectNode[0], ...item }, connectNode[1]]; 46 | else 47 | return [ 48 | {}, 49 | [new Error('rc-serve is closed. (Tip: Set server option: rc.Serve as true)')], 50 | ] as any; 51 | } 52 | ) 53 | ); 54 | })(); 55 | this.download$.deploy(); 56 | this.download$.getSupersetOutput().subscribe(x => { 57 | if (x[1].length !== 0) { 58 | this.toastr.danger(`${x[0].remote}:${x[0].Path}`, 'File download failure.'); 59 | return; 60 | } 61 | this.fileSaverService.save(x[0].ajaxRsp.response, x[0].Name); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/pages/manager/fileMode/fileMode.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; 2 | import { combineLatest, Subject } from 'rxjs'; 3 | import { filter, map } from 'rxjs/operators'; 4 | import { CombErr } from '../../../@dataflow/core'; 5 | import { 6 | IManipulate, 7 | OperationsListExtendsFlow, 8 | OperationsListExtendsFlowInNode, 9 | OperationsListExtendsFlowOutItemNode, 10 | } from '../../../@dataflow/extra'; 11 | import { 12 | OperationsListFlow, 13 | OperationsListFlowInNode, 14 | NavigationFlow, 15 | NavigationFlowOutNode, 16 | } from '../../../@dataflow/rclone'; 17 | import { ConnectionService } from '../../connection.service'; 18 | import { ClipboardService } from '../clipboard/clipboard.service'; 19 | import { ListViewComponent } from './listView/listView.component'; 20 | 21 | @Component({ 22 | selector: 'app-manager-file-mode', 23 | template: ` 24 | 30 | 31 | `, 32 | styles: [], 33 | }) 34 | export class FileModeComponent implements OnInit { 35 | constructor(private connectService: ConnectionService, private clipboard: ClipboardService) {} 36 | 37 | @Input() nav$: NavigationFlow; 38 | @Input() pcDetailViewEnable: boolean; 39 | 40 | @Output() jump = new EventEmitter(); 41 | @Output() showDetail = new EventEmitter(); 42 | 43 | private listTrigger = new Subject(); 44 | private list$: OperationsListFlow; 45 | listExtends$: OperationsListExtendsFlow; 46 | 47 | @ViewChild(ListViewComponent) listView: ListViewComponent; 48 | 49 | loading() { 50 | this.listView.loading(); 51 | } 52 | 53 | refresh() { 54 | this.loading(); 55 | this.list$.clearCache(); 56 | this.listTrigger.next(1); 57 | } 58 | manipulate(o: IManipulate) { 59 | this.listView.manipulate(o); 60 | this.clipboard.commit(); 61 | } 62 | 63 | ngOnInit() { 64 | const outer = this; 65 | this.list$ = new (class extends OperationsListFlow { 66 | public prerequest$ = combineLatest([ 67 | outer.listTrigger, 68 | outer.nav$.getOutput(), 69 | outer.connectService.listCmd$.verify(this.cmd), 70 | ]).pipe( 71 | map( 72 | ([, navNode, cmdNode]): CombErr => { 73 | if (navNode[1].length !== 0 || cmdNode[1].length !== 0) 74 | return [{}, [].concat(navNode[1], cmdNode[1])] as CombErr; 75 | return [{ ...navNode[0], ...cmdNode[0] }, []]; 76 | } 77 | ), 78 | filter(x => x[1].length !== 0 || !!x[0].remote) 79 | ); 80 | })(); 81 | this.list$.deploy(); 82 | 83 | this.listExtends$ = new (class extends OperationsListExtendsFlow { 84 | public prerequest$ = combineLatest([ 85 | outer.list$.getSupersetOutput(), 86 | outer.clipboard.clipboard$.getOutput(), 87 | ]).pipe( 88 | map( 89 | ([listNode, cbNode]): CombErr => [ 90 | { ...listNode[0], ...cbNode[0] }, 91 | [].concat(listNode[1], cbNode[1]), 92 | ] 93 | ) 94 | ); 95 | })(); 96 | this.listExtends$.deploy(); 97 | 98 | this.listTrigger.next(1); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/app/pages/manager/homeMode/homeMode.component.scss: -------------------------------------------------------------------------------- 1 | .cloud { 2 | border-radius: 0.25rem; 3 | } 4 | 5 | .cloud:hover { 6 | background-color: #598bff88; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/pages/manager/homeMode/homeMode.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import { combineLatest, Subject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { NavigationFlowOutNode, ListRemotesFlow } from '../../../@dataflow/rclone'; 5 | import { ConnectionService } from '../../connection.service'; 6 | 7 | @Component({ 8 | selector: 'app-manager-home-mode', 9 | template: ` 10 |
17 |
25 | 32 | 33 | 34 | 43 | 44 |
45 |
46 | `, 47 | styleUrls: ['./homeMode.component.scss'], 48 | }) 49 | export class HomeModeComponent implements OnInit { 50 | constructor(private cmdService: ConnectionService) {} 51 | 52 | remotes: string[] = []; 53 | 54 | @Input() pcDetailView: boolean; 55 | @Output() jump = new EventEmitter(); 56 | @Output() showDetail = new EventEmitter(); 57 | 58 | remotesTrigger = new Subject(); 59 | remotes$: ListRemotesFlow; 60 | 61 | isLoading = true; 62 | loading() { 63 | this.isLoading = true; 64 | } 65 | refresh() { 66 | this.loading(); 67 | this.remotes$.clearCache(); 68 | this.remotesTrigger.next(1); 69 | } 70 | ngOnInit() { 71 | this.loading(); 72 | const outer = this; 73 | this.remotes$ = new (class extends ListRemotesFlow { 74 | public prerequest$ = combineLatest([ 75 | outer.remotesTrigger, 76 | outer.cmdService.listCmd$.verify(this.cmd), 77 | ]).pipe(map(([, y]) => y)); 78 | })(); 79 | this.remotes$.deploy(); 80 | this.remotesTrigger.next(1); 81 | this.remotes$.getOutput().subscribe(x => { 82 | this.isLoading = false; 83 | if (x[1].length !== 0) return; 84 | this.remotes = x[0].remotes; 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/app/pages/manager/homeMode/remote.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home-view-remote', 5 | template: ` 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 23 | 24 | 25 |
10 |

{{ title }}

11 |
16 |

{{ 123 }}

17 |
21 | 22 |
26 | `, 27 | styles: [ 28 | ` 29 | .grid-container { 30 | width: 100%; 31 | height: 4rem; 32 | cursor: pointer; 33 | /* background-color: #2196f3; */ 34 | } 35 | td > p { 36 | margin-bottom: 0; 37 | } 38 | `, 39 | ], 40 | }) 41 | export class RemoteComponent implements OnInit { 42 | @Input() 43 | title = 'unknow'; 44 | 45 | @Input() 46 | subtitle = ''; 47 | 48 | @Input() 49 | value = 0; 50 | 51 | @Input() 52 | easyMode = false; 53 | 54 | constructor() {} 55 | 56 | ngOnInit() {} 57 | } 58 | -------------------------------------------------------------------------------- /src/app/pages/manager/homeMode/remote.detail.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core'; 2 | import { combineLatest, Subject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { CombErr } from '../../../@dataflow/core'; 5 | import { 6 | OperationsAboutFlow, 7 | OperationsFsinfoFlow, 8 | OperationsFsinfoFlowInNode, 9 | NavigationFlowOutNode, 10 | } from '../../../@dataflow/rclone'; 11 | import { RngSpaceUsageChartComponent } from '../../../components/space-usage-chart/space-usage-chart.component'; 12 | import { ConnectionService } from '../../connection.service'; 13 | 14 | @Component({ 15 | selector: 'app-home-remote-detail', 16 | template: ` 17 |
{{ remote }}
18 | 19 | 20 | 21 | Feature 22 | 23 | 24 | 25 | 29 | 30 |
{{ item.k }}
31 |
32 |
33 |
34 |
35 | 36 | Hash Support 37 | 38 | 39 | 40 |
{{ item }}
41 |
42 |
43 |
44 |
45 |
46 | `, 47 | styles: [ 48 | ` 49 | h5 { 50 | padding: 0 1.25rem; 51 | } 52 | nb-list { 53 | margin: 0 -0.5rem; 54 | } 55 | nb-list-item { 56 | padding: 0.5rem 0; 57 | } 58 | nb-list-item > nb-icon { 59 | width: 1rem; 60 | height: 1rem; 61 | } 62 | nb-list-item > div { 63 | padding-left: 0.5rem; 64 | } 65 | `, 66 | ], 67 | }) 68 | export class RemoteDetailComponent implements OnInit { 69 | constructor(private cmdService: ConnectionService) {} 70 | remote = ''; 71 | loadingFsinfo = false; 72 | loadingAbout = false; 73 | feature: { k: string; v: boolean }[] = []; 74 | hashes: string[] = []; 75 | 76 | private trigger = new Subject(); 77 | fsinfo$: OperationsFsinfoFlow; 78 | about$: OperationsAboutFlow; 79 | 80 | @ViewChild(RngSpaceUsageChartComponent) chart: RngSpaceUsageChartComponent; 81 | 82 | @Input() initNode: NavigationFlowOutNode; 83 | // TODO: replace it as initNode? 84 | navNode(x: NavigationFlowOutNode) { 85 | this.remote = x.remote || ''; 86 | this.loadingFsinfo = true; 87 | this.loadingAbout = true; 88 | this.trigger.next(x.remote); 89 | } 90 | 91 | ngOnInit() { 92 | const outer = this; 93 | this.loadingFsinfo = false; 94 | this.loadingAbout = false; 95 | this.fsinfo$ = new (class extends OperationsFsinfoFlow { 96 | public prerequest$ = combineLatest([ 97 | outer.trigger, 98 | outer.cmdService.listCmd$.verify(this.cmd), 99 | ]).pipe( 100 | map( 101 | ([remote, cmdNode]): CombErr => { 102 | if (cmdNode[1].length !== 0) return [{}, cmdNode[1]] as any; 103 | return [{ ...cmdNode[0], remote }, []]; 104 | } 105 | ) 106 | ); 107 | })(); 108 | this.fsinfo$.deploy(); 109 | this.fsinfo$.getOutput().subscribe(x => { 110 | this.loadingFsinfo = false; 111 | if (x[1].length !== 0) return; 112 | const fsinfo = x[0]['fs-info']; 113 | this.feature = Object.keys(fsinfo.Features).map(k => ({ k, v: fsinfo.Features[k] })); 114 | this.hashes = fsinfo.Hashes; 115 | }); 116 | this.about$ = new (class extends OperationsAboutFlow { 117 | public prerequest$ = outer.fsinfo$.getSupersetOutput(); 118 | })(); 119 | this.about$.deploy(); 120 | this.about$.getOutput().subscribe(x => { 121 | this.loadingAbout = false; 122 | if (x[1].length !== 0) return; 123 | this.chart.data = x[0].about; 124 | }); 125 | if (this.initNode) 126 | setTimeout(() => { 127 | this.navNode(this.initNode); 128 | }, 100); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/app/pages/manager/manager-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { ManagerComponent } from './manager.component'; 5 | 6 | const routes: Routes = [{ path: '', component: ManagerComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule], 11 | }) 12 | export class ManagerRoutingModule {} 13 | -------------------------------------------------------------------------------- /src/app/pages/manager/manager.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ManagerComponent } from './manager.component'; 4 | 5 | xdescribe('ManagerComponent', () => { 6 | let component: ManagerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ManagerComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ManagerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/manager/manager.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { FormsModule } from '@angular/forms'; 5 | import { 6 | NbAccordionModule, 7 | NbActionsModule, 8 | NbBadgeModule, 9 | NbButtonModule, 10 | NbCardModule, 11 | NbCheckboxModule, 12 | NbIconModule, 13 | NbInputModule, 14 | NbLayoutModule, 15 | NbListModule, 16 | NbProgressBarModule, 17 | NbSidebarModule, 18 | NbSpinnerModule, 19 | NbTabsetModule, 20 | NbTooltipModule, 21 | } from '@nebular/theme'; 22 | import { ChartsModule } from 'ng2-charts'; 23 | import { TableModule } from 'ngx-easy-table'; 24 | import { FileSaverModule } from 'ngx-filesaver'; 25 | import { ResponsiveModule } from 'ngx-responsive'; 26 | import { RngModule } from '../../components/rng.module'; 27 | import { BreadcrumbComponent } from './breadcrumb/breadcrumb.component'; 28 | import { ClipboardRemotesTableComponent } from './clipboard/clipboard-remotes-table/clipboard-remotes-table.component'; 29 | import { ClipboardDialogComponent } from './clipboard/clipboard.dialog'; 30 | import { MkdirDialogComponent } from './dialogs/mkdir.dialog'; 31 | import { FileDetailComponent } from './fileMode/file.detail'; 32 | import { FileModeComponent } from './fileMode/fileMode.component'; 33 | import { ListViewComponent } from './fileMode/listView/listView.component'; 34 | import { HomeModeComponent } from './homeMode/homeMode.component'; 35 | import { RemoteComponent } from './homeMode/remote.component'; 36 | import { RemoteDetailComponent } from './homeMode/remote.detail'; 37 | import { ManagerRoutingModule } from './manager-routing.module'; 38 | import { ManagerComponent } from './manager.component'; 39 | 40 | @NgModule({ 41 | declarations: [ 42 | ManagerComponent, 43 | BreadcrumbComponent, 44 | HomeModeComponent, 45 | RemoteComponent, 46 | FileModeComponent, 47 | ListViewComponent, 48 | ClipboardDialogComponent, 49 | ClipboardRemotesTableComponent, 50 | MkdirDialogComponent, 51 | RemoteDetailComponent, 52 | FileDetailComponent, 53 | ], 54 | imports: [ 55 | CommonModule, 56 | ManagerRoutingModule, 57 | NbActionsModule, 58 | NbCardModule, 59 | NbIconModule, 60 | NbProgressBarModule, 61 | TableModule, 62 | NbLayoutModule, 63 | NbSidebarModule, 64 | NbCheckboxModule, 65 | NbButtonModule, 66 | NbInputModule, 67 | NbTooltipModule, 68 | NbBadgeModule, 69 | NbTabsetModule, 70 | NbAccordionModule, 71 | NbSpinnerModule, 72 | NbListModule, 73 | ChartsModule, 74 | FileSaverModule, 75 | RngModule, 76 | FormsModule, 77 | ResponsiveModule, 78 | ], 79 | }) 80 | export class ManagerModule {} 81 | -------------------------------------------------------------------------------- /src/app/pages/mounts/mounts-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { MountsComponent } from './mounts.component'; 5 | 6 | const routes: Routes = [{ path: '', component: MountsComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule], 11 | }) 12 | export class MountsRoutingModule {} 13 | -------------------------------------------------------------------------------- /src/app/pages/mounts/mounts.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MountsComponent } from './mounts.component'; 4 | 5 | describe('MountsComponent', () => { 6 | let component: MountsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [MountsComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(MountsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/mounts/mounts.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { TableModule } from 'ngx-easy-table'; 5 | import { 6 | NbCardModule, 7 | NbIconModule, 8 | NbInputModule, 9 | NbAutocompleteModule, 10 | NbButtonModule, 11 | NbActionsModule, 12 | } from '@nebular/theme'; 13 | import { FormsModule } from '@angular/forms'; 14 | import { MountsRoutingModule } from './mounts-routing.module'; 15 | import { MountsComponent } from './mounts.component'; 16 | 17 | @NgModule({ 18 | declarations: [MountsComponent], 19 | imports: [ 20 | CommonModule, 21 | MountsRoutingModule, 22 | TableModule, 23 | NbCardModule, 24 | NbIconModule, 25 | NbInputModule, 26 | NbAutocompleteModule, 27 | NbButtonModule, 28 | FormsModule, 29 | NbActionsModule, 30 | ], 31 | }) 32 | export class MountsModule {} 33 | -------------------------------------------------------------------------------- /src/app/pages/mounts/mounts.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async, inject } from '@angular/core/testing'; 4 | import { MountsService } from './mounts.service'; 5 | 6 | describe('Service: Mounts', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [MountsService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([MountsService], (service: MountsService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/mounts/mounts.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, Observable, combineLatest } from 'rxjs'; 3 | import { 4 | ListMountsFlow, 5 | IRcloneServer, 6 | MountMountFlowParamsNode, 7 | MountMountFlow, 8 | MountMountFlowInNode, 9 | MountUnmountFlow, 10 | MountUnmountFlowInNode, 11 | MountUnmountFlowParamsNode, 12 | MountUnmountAllFlow, 13 | } from '../../@dataflow/rclone'; 14 | import { CombErr } from '../../@dataflow/core'; 15 | import { ConnectionService } from '../connection.service'; 16 | 17 | @Injectable({ 18 | providedIn: 'root', 19 | }) 20 | export class MountsService { 21 | private listTrigger = new Subject(); 22 | list$: ListMountsFlow; 23 | 24 | private addTrigger = new Subject(); 25 | add$: MountMountFlow; 26 | 27 | private unmountTrigger = new Subject(); 28 | unmount$: MountUnmountFlow; 29 | 30 | private unmountAllTrigger = new Subject(); 31 | unmountAll$: MountUnmountAllFlow; 32 | 33 | constructor(private connectService: ConnectionService) { 34 | const outer = this; 35 | this.list$ = new (class extends ListMountsFlow { 36 | public prerequest$: Observable> = combineLatest( 37 | [outer.listTrigger, outer.connectService.listCmd$.verify(this.cmd)], 38 | (_, node) => node 39 | ); 40 | })(); 41 | this.list$.deploy(); 42 | this.add$ = new (class extends MountMountFlow { 43 | public prerequest$: Observable> = combineLatest( 44 | [outer.addTrigger, outer.connectService.listCmd$.verify(this.cmd)], 45 | (params, cmdNode) => [{ ...cmdNode[0], ...params }, cmdNode[1]] 46 | ); 47 | })(); 48 | this.add$.deploy(); 49 | this.add$.getOutput().subscribe(node => { 50 | if (node[1].length !== 0) return; 51 | this.refreshList(); 52 | }); 53 | 54 | this.unmount$ = new (class extends MountUnmountFlow { 55 | public prerequest$: Observable> = combineLatest( 56 | [outer.unmountTrigger, outer.connectService.listCmd$.verify(this.cmd)], 57 | (params, cmdNode) => [{ ...cmdNode[0], ...params }, cmdNode[1]] 58 | ); 59 | })(); 60 | this.unmount$.deploy(); 61 | this.unmount$.getOutput().subscribe(node => { 62 | if (node[1].length !== 0) return; 63 | this.refreshList(); 64 | }); 65 | 66 | this.unmountAll$ = new (class extends MountUnmountAllFlow { 67 | public prerequest$: Observable> = combineLatest( 68 | [outer.unmountAllTrigger, outer.connectService.listCmd$.verify(this.cmd)], 69 | (_, node) => node 70 | ); 71 | })(); 72 | this.unmountAll$.deploy(); 73 | this.unmountAll$.getOutput().subscribe(node => { 74 | if (node[1].length !== 0) return; 75 | this.refreshList(); 76 | }); 77 | } 78 | refreshList() { 79 | this.list$.clearCache(); 80 | this.listTrigger.next(1); 81 | } 82 | 83 | mount(params: MountMountFlowParamsNode) { 84 | this.addTrigger.next(params); 85 | } 86 | 87 | unmount(params: MountUnmountFlowParamsNode) { 88 | this.unmountTrigger.next(params); 89 | } 90 | 91 | unmountAll() { 92 | this.unmountAllTrigger.next(1); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/app/pages/pages-menu.ts: -------------------------------------------------------------------------------- 1 | import { NbMenuItem, NbIconConfig } from '@nebular/theme'; 2 | 3 | export const MENU_ITEMS: NbMenuItem[] = [ 4 | { 5 | title: 'User', 6 | icon: 'people-outline', 7 | children: [ 8 | { 9 | title: 'Manage', 10 | icon: 'grid-outline', 11 | link: 'user', 12 | }, 13 | ], 14 | }, 15 | { title: 'General', group: true }, 16 | { 17 | title: 'Dashboard', 18 | icon: 'home-outline', 19 | link: 'dashboard', 20 | home: true, 21 | }, 22 | { 23 | title: 'File Manager', 24 | icon: 'folder-outline', 25 | link: 'manager', 26 | }, 27 | { 28 | title: 'Jobs Manager', 29 | icon: 'briefcase-outline', 30 | link: 'jobs', 31 | }, 32 | { 33 | title: 'Mounts Manager', 34 | icon: { 35 | icon: 'list-tree', 36 | pack: 'css.gg', 37 | } as NbIconConfig, 38 | link: 'mounts', 39 | }, 40 | { 41 | title: 'Settings', 42 | group: true, 43 | }, 44 | { 45 | title: 'Server Setting', 46 | icon: 'settings-2-outline', 47 | link: 'settings/server', 48 | }, 49 | { 50 | title: 'Browser Setting', 51 | icon: 'browser-outline', 52 | link: 'settings/browser', 53 | }, 54 | { title: 'Other', group: true }, 55 | { title: 'About', icon: 'npm-outline', link: 'about' }, 56 | ]; 57 | -------------------------------------------------------------------------------- /src/app/pages/pages-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { DashboardModule } from './dashboard/dashboard.module'; 5 | import { PagesComponent } from './pages.component'; 6 | import { UserModule } from './user/user.module'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: PagesComponent, 12 | children: [ 13 | { 14 | path: 'dashboard', 15 | // loadChildren: () => import('./dashboard/dashboard.module').then((m) => m.DashboardModule), 16 | loadChildren: () => DashboardModule, 17 | }, 18 | { 19 | path: 'user', 20 | // loadChildren: () => import('./user/user.module').then(m => m.UserModule), 21 | loadChildren: () => UserModule, 22 | }, 23 | { 24 | path: 'manager', 25 | loadChildren: () => import('./manager/manager.module').then(m => m.ManagerModule), 26 | // loadChildren: () => ManagerModule, 27 | }, 28 | { 29 | path: 'jobs', 30 | loadChildren: () => import('./jobs/jobs.module').then(m => m.JobsModule), 31 | // loadChildren: () => JobsModule, 32 | }, 33 | { 34 | path: 'mounts', 35 | loadChildren: () => import('./mounts/mounts.module').then(m => m.MountsModule), 36 | }, 37 | { 38 | path: 'settings', 39 | loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule), 40 | }, 41 | { 42 | path: 'about', 43 | loadChildren: () => import('./about/about.module').then(m => m.AboutModule), 44 | }, 45 | { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, 46 | { path: '**', redirectTo: 'dashboard' }, 47 | ], 48 | }, 49 | ]; 50 | 51 | @NgModule({ 52 | imports: [RouterModule.forChild(routes)], 53 | exports: [RouterModule], 54 | }) 55 | export class PagesRoutingModule {} 56 | -------------------------------------------------------------------------------- /src/app/pages/pages.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PagesComponent } from './pages.component'; 4 | 5 | xdescribe('PagesComponent', () => { 6 | let component: PagesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [PagesComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(PagesComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | // it('should create', () => { 22 | // expect(component).toBeTruthy(); 23 | // }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/pages.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { NbEvaIconsModule } from '@nebular/eva-icons'; 5 | import { 6 | NbActionsModule, 7 | NbButtonModule, 8 | NbIconModule, 9 | NbLayoutModule, 10 | NbMenuModule, 11 | NbSidebarModule, 12 | } from '@nebular/theme'; 13 | import { ResponsiveModule } from 'ngx-responsive'; 14 | import { PagesRoutingModule } from './pages-routing.module'; 15 | import { PagesComponent } from './pages.component'; 16 | 17 | @NgModule({ 18 | declarations: [PagesComponent], 19 | imports: [ 20 | CommonModule, 21 | ResponsiveModule, 22 | PagesRoutingModule, 23 | NbLayoutModule, 24 | NbActionsModule, 25 | NbSidebarModule.forRoot(), 26 | NbEvaIconsModule, 27 | NbMenuModule, 28 | NbIconModule, 29 | NbButtonModule, 30 | ], 31 | }) 32 | export class PagesModule {} 33 | -------------------------------------------------------------------------------- /src/app/pages/settings/browser-setting/browser-setting.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BrowserSettingComponent } from './browser-setting.component'; 4 | 5 | describe('BrowserSettingComponent', () => { 6 | let component: BrowserSettingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [BrowserSettingComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(BrowserSettingComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/settings/browser-setting/browser-setting.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { EditorComponent as MonacoEditorComponent, NgxEditorModel } from 'ngx-monaco-editor'; 3 | import { ResponsiveSizeInfoRx } from 'ngx-responsive'; 4 | import { Subject, Subscription } from 'rxjs'; 5 | import { IBrowserSetting } from '../../../@dataflow/extra'; 6 | import * as browserSettingSchema from '../../../@dataflow/extra/browser-setting-schema.json'; 7 | import { BrowserSettingService } from './browser-setting.service'; 8 | 9 | @Component({ 10 | template: ` 11 | 12 | 13 | Browser Conf 14 | 17 | 18 | 19 | 20 | 22 | 27 | 29 | 30 | 31 | `, 32 | styles: [ 33 | ` 34 | nb-card { 35 | margin: 0; 36 | } 37 | nb-card-header { 38 | display: flex; 39 | } 40 | button { 41 | margin: 0 0.5rem; 42 | } 43 | .push-to-right { 44 | margin-left: auto; 45 | } 46 | nb-card-body { 47 | padding: 0; 48 | overflow-y: hidden; 49 | } 50 | :host ngx-monaco-editor { 51 | height: calc(100vh - 4.75rem - 4.25rem - 0.15rem); 52 | } 53 | :host ngx-monaco-editor ::ng-deep .editor-container { 54 | height: 100%; 55 | } 56 | `, 57 | ], 58 | }) 59 | export class BrowserSettingComponent implements OnInit { 60 | constructor(private service: BrowserSettingService, private rsp: ResponsiveSizeInfoRx) {} 61 | editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = { 62 | theme: 'vs', 63 | language: 'json', 64 | wordWrap: 'on', 65 | // Set this to false to not auto word wrap minified files 66 | wordWrapMinified: true, 67 | // try "same", "indent" or "none" 68 | wrappingIndent: 'indent', 69 | }; 70 | editorModel: NgxEditorModel = { 71 | value: '', 72 | language: 'json', 73 | // uri: monaco.Uri.parse('server-setting.json'), 74 | }; 75 | 76 | private connector: Subscription[] = []; 77 | 78 | private saveTrigger = new Subject(); 79 | private recoverTrigger = new Subject(); 80 | private originOptions: IBrowserSetting; 81 | 82 | @ViewChild(MonacoEditorComponent, { static: true }) editorComponent: MonacoEditorComponent; 83 | save() { 84 | this.saveTrigger.next(1); 85 | } 86 | recover() { 87 | this.recoverTrigger.next(1); 88 | } 89 | 90 | editorOnInit(editor: monaco.editor.IStandaloneCodeEditor) { 91 | monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ 92 | validate: true, 93 | schemas: [ 94 | { 95 | uri: 'browser-setting-schema.json', 96 | fileMatch: [''], 97 | schema: { ...browserSettingSchema.definitions.IBrowserSetting }, 98 | }, 99 | ], 100 | }); 101 | /** 102 | */ 103 | function updateValue(v: IBrowserSetting) { 104 | editor.setValue(JSON.stringify(v, null, 4)); 105 | editor.trigger('', 'editor.action.formatDocument', {}); 106 | } 107 | this.connector.forEach(x => x.unsubscribe()); 108 | this.connector = []; 109 | this.connector.push( 110 | this.service.browserSetting$.getOutput().subscribe(x => { 111 | if (x[1].length !== 0) return; 112 | this.originOptions = x[0]; 113 | updateValue(x[0]); 114 | }), 115 | this.rsp.getResponsiveSize.subscribe(x => { 116 | editor.updateOptions({ minimap: { enabled: x !== 'xs' } }); 117 | }), 118 | this.saveTrigger.subscribe(() => { 119 | this.service.update(JSON.parse(editor.getValue())); 120 | }), 121 | this.recoverTrigger.subscribe(() => { 122 | updateValue(this.originOptions); 123 | }) 124 | ); 125 | } 126 | ngOnInit() {} 127 | } 128 | -------------------------------------------------------------------------------- /src/app/pages/settings/browser-setting/browser-setting.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { async, inject, TestBed } from '@angular/core/testing'; 4 | import { BrowserSettingService } from './browser-setting.service'; 5 | 6 | describe('Service: BrowserSetting', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [BrowserSettingService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([BrowserSettingService], (service: BrowserSettingService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/settings/browser-setting/browser-setting.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, Subject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { CombErr } from '../../../@dataflow/core'; 5 | import { BrowserSettingFlow, IBrowserSetting, NestedPartial } from '../../../@dataflow/extra'; 6 | import { NestedGet } from '../../../@dataflow/rclone'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class BrowserSettingService { 12 | browserSetting$: BrowserSettingFlow; 13 | 14 | private trigger = new Subject>(); 15 | constructor() { 16 | const outer = this; 17 | this.browserSetting$ = new (class extends BrowserSettingFlow { 18 | public prerequest$: Observable>> = outer.trigger.pipe( 19 | map(x => [x, []]) 20 | ); 21 | })(); 22 | this.browserSetting$.deploy(); 23 | this.browserSetting$.getOutput().subscribe(); 24 | this.trigger.next({}); 25 | } 26 | public partialBrowserSetting$(...path: (number | string)[]) { 27 | return this.browserSetting$.getOutput().pipe( 28 | map( 29 | (x): CombErr => { 30 | if (x[1].length !== 0) return x; 31 | return [NestedGet(x[0], ...path), []]; 32 | } 33 | ) 34 | ); 35 | } 36 | public update(data: NestedPartial) { 37 | this.trigger.next(data); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/pages/settings/settings-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { BrowserSettingComponent } from './browser-setting/browser-setting.component'; 5 | import { SettingsComponent } from './settings.component'; 6 | import { SeverSettingComponent } from './sever-setting/sever-setting.component'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: SettingsComponent, 12 | children: [ 13 | { path: 'server', component: SeverSettingComponent }, 14 | { path: 'browser', component: BrowserSettingComponent }, 15 | { path: '', redirectTo: 'server', pathMatch: 'full' }, 16 | ], 17 | }, 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forChild(routes)], 22 | exports: [RouterModule], 23 | }) 24 | export class SettingsRoutingModule {} 25 | -------------------------------------------------------------------------------- /src/app/pages/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SettingsComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SettingsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-settings', 5 | template: ` `, 6 | styles: [], 7 | }) 8 | export class SettingsComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/pages/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { NbButtonModule, NbCardModule, NbLayoutModule } from '@nebular/theme'; 5 | import { MonacoEditorModule } from 'ngx-monaco-editor'; 6 | import { BrowserSettingComponent } from './browser-setting/browser-setting.component'; 7 | import { SettingsRoutingModule } from './settings-routing.module'; 8 | import { SettingsComponent } from './settings.component'; 9 | import { SeverSettingComponent } from './sever-setting/sever-setting.component'; 10 | 11 | @NgModule({ 12 | declarations: [SettingsComponent, SeverSettingComponent, BrowserSettingComponent], 13 | imports: [ 14 | CommonModule, 15 | SettingsRoutingModule, 16 | MonacoEditorModule.forRoot(), 17 | NbCardModule, 18 | NbButtonModule, 19 | NbLayoutModule, 20 | ], 21 | }) 22 | export class SettingsModule {} 23 | -------------------------------------------------------------------------------- /src/app/pages/settings/sever-setting/server-setting.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { async, inject, TestBed } from '@angular/core/testing'; 4 | import { ServerSettingService } from './server-setting.service'; 5 | 6 | describe('Service: ServerSetting', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [ServerSettingService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([ServerSettingService], (service: ServerSettingService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/settings/sever-setting/server-setting.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { combineLatest, Subject } from 'rxjs'; 3 | import { map, withLatestFrom } from 'rxjs/operators'; 4 | import { CombErr } from '../../../@dataflow/core'; 5 | import { 6 | OptionsGetFlow, 7 | OptionsSetFlow, 8 | OptionsSetFlowInNode, 9 | OptionsSetFlowParamsNode, 10 | } from '../../../@dataflow/rclone'; 11 | import { ConnectionService } from '../../connection.service'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class ServerSettingService { 17 | private getTrigger = new Subject(); 18 | options$: OptionsGetFlow; 19 | private setTrigger = new Subject(); 20 | private optionsSet$: OptionsSetFlow; 21 | 22 | public setOption(ops: OptionsSetFlowParamsNode) { 23 | this.setTrigger.next(ops); 24 | } 25 | 26 | constructor(private cmdService: ConnectionService) { 27 | const outer = this; 28 | this.options$ = new (class extends OptionsGetFlow { 29 | public prerequest$ = combineLatest([ 30 | outer.getTrigger, 31 | outer.cmdService.listCmd$.verify(this.cmd), 32 | ]).pipe(map(([, cmdNode]) => cmdNode)); 33 | })(); 34 | this.options$.deploy(); 35 | this.getTrigger.next(1); 36 | 37 | this.optionsSet$ = new (class extends OptionsSetFlow { 38 | public prerequest$ = outer.setTrigger.pipe( 39 | withLatestFrom(outer.cmdService.listCmd$.verify(this.cmd)), 40 | map( 41 | ([ops, cmdNode]): CombErr => [ 42 | { ...cmdNode[0], options: ops }, 43 | cmdNode[1], 44 | ] 45 | ) 46 | ); 47 | })(); 48 | this.optionsSet$.deploy(); 49 | this.optionsSet$.getOutput().subscribe(x => { 50 | this.getTrigger.next(1); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/pages/settings/sever-setting/sever-setting.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { DebugElement } from '@angular/core'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { SeverSettingComponent } from './sever-setting.component'; 7 | 8 | describe('SeverSettingComponent', () => { 9 | let component: SeverSettingComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [SeverSettingComponent], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(SeverSettingComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/pages/tasks/tasks-queue.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TasksQueueService } from './tasks-queue.service'; 4 | 5 | describe('TasksQueueService', () => { 6 | let service: TasksQueueService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(TasksQueueService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/user/add/add.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserAddComponent } from './add.component'; 4 | 5 | describe('AddComponent', () => { 6 | let component: UserAddComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [UserAddComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(UserAddComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/user/del/delete.dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { DialogRef, ModalComponent } from 'ngx-modialog-7'; 3 | // tslint:disable-next-line: no-submodule-imports 4 | import { DialogPreset } from 'ngx-modialog-7/plugins/vex'; 5 | import { IUser } from '../../../@dataflow/extra'; 6 | 7 | @Component({ 8 | template: ` 9 | 10 | 11 | Delete User 12 | 13 | 15 | name : {{ content?.name }} 16 | url : {{ content?.url }} 17 | user : {{ content?.user }} 18 | pass : ***** 19 | 21 | 22 | 23 | 26 | 27 | 28 | `, 29 | styles: [ 30 | ` 31 | nb-card { 32 | margin: calc(-1em - 5px); 33 | } 34 | nb-card-header, 35 | nb-card-footer { 36 | display: flex; 37 | } 38 | .push-to-right { 39 | margin-left: auto; 40 | } 41 | `, 42 | ], 43 | }) 44 | export class UserDeleteDialogComponent implements ModalComponent { 45 | public content: IUser; 46 | 47 | constructor(public dialog: DialogRef) { 48 | this.content = dialog.context.content as any; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/pages/user/login/UserLogin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { UsersFlow } from '../../../@dataflow/extra'; 4 | import { CurrentUserService } from '../../current-user.service'; 5 | 6 | @Component({ 7 | template: ``, 8 | styles: [], 9 | }) 10 | export class UserLoginComponent implements OnInit { 11 | constructor( 12 | private route: ActivatedRoute, 13 | private router: Router, 14 | private currUserService: CurrentUserService 15 | ) {} 16 | 17 | ngOnInit() { 18 | const user = UsersFlow.get(this.route.snapshot.queryParams.name); 19 | if (user) { 20 | this.currUserService.switchUser(user.name); 21 | } 22 | this.router.navigate(['/dashboard']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/pages/user/user-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { UserAddComponent } from './add/add.component'; 5 | import { UserLoginComponent } from './login/UserLogin.component'; 6 | import { UserComponent } from './user.component'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: UserComponent, 12 | pathMatch: 'full', 13 | }, 14 | { 15 | path: 'add', 16 | component: UserAddComponent, 17 | }, 18 | { 19 | path: 'login', 20 | component: UserLoginComponent, 21 | }, 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [RouterModule.forChild(routes)], 26 | exports: [RouterModule], 27 | }) 28 | export class UserRoutingModule {} 29 | -------------------------------------------------------------------------------- /src/app/pages/user/user.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserComponent } from './user.component'; 4 | 5 | xdescribe('UserComponent', () => { 6 | let component: UserComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [UserComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(UserComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { FormsModule } from '@angular/forms'; 5 | import { 6 | NbActionsModule, 7 | NbButtonModule, 8 | NbCardModule, 9 | NbFormFieldModule, 10 | NbIconModule, 11 | NbInputModule, 12 | NbListModule, 13 | } from '@nebular/theme'; 14 | import { UserAddComponent } from './add/add.component'; 15 | import { UserDeleteDialogComponent } from './del/delete.dialog'; 16 | import { UserRoutingModule } from './user-routing.module'; 17 | import { UserComponent } from './user.component'; 18 | 19 | @NgModule({ 20 | declarations: [UserComponent, UserAddComponent, UserDeleteDialogComponent], 21 | imports: [ 22 | CommonModule, 23 | UserRoutingModule, 24 | NbFormFieldModule, 25 | NbCardModule, 26 | NbIconModule, 27 | NbInputModule, 28 | NbActionsModule, 29 | NbButtonModule, 30 | FormsModule, 31 | NbListModule, 32 | ], 33 | }) 34 | export class UserModule {} 35 | -------------------------------------------------------------------------------- /src/app/pages/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { async, inject, TestBed } from '@angular/core/testing'; 4 | import { UsersService } from './users.service'; 5 | 6 | describe('Service: Users', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [UsersService], 10 | }); 11 | }); 12 | 13 | it('should ...', inject([UsersService], (service: UsersService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { CombErr, FlowInNode } from '../@dataflow/core'; 5 | import { CurrentUserFlow, UsersFlow } from '../@dataflow/extra'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class UsersService { 11 | private usersTrigger = new Subject(); 12 | public usersFlow$: UsersFlow; 13 | 14 | public update() { 15 | this.usersTrigger.next(1); 16 | } 17 | 18 | constructor() { 19 | const outer = this; 20 | this.usersFlow$ = new (class extends UsersFlow { 21 | public prerequest$ = outer.usersTrigger.pipe(map((): CombErr => [{}, []])); 22 | })(); 23 | this.usersFlow$.deploy(); 24 | 25 | this.usersTrigger.next(1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/utils/format-bytes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Source: https://bit.dev/amazingdesign/utils/format-bytes 3 | * License: MIT 4 | */ 5 | export function FormatBytes(bytes: number, decimals = 2) { 6 | if (typeof bytes !== 'number' || bytes < 0) { 7 | return '-'; 8 | } 9 | if (bytes === 0) { 10 | return '0 B'; 11 | } 12 | 13 | const k = 1024; 14 | const dm = decimals < 0 ? 0 : decimals; 15 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 16 | 17 | const i = Math.max(Math.floor(Math.log(bytes) / Math.log(k)), 0); 18 | 19 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/utils/format-duration.ts: -------------------------------------------------------------------------------- 1 | import { HumanizeDuration, HumanizeDurationLanguage } from 'humanize-duration-ts'; 2 | 3 | export const langService = new HumanizeDurationLanguage(); 4 | langService.addLanguage('shortEn', { 5 | y: () => 'y', 6 | mo: () => 'mo', 7 | w: () => 'w', 8 | d: () => 'd', 9 | h: () => 'h', 10 | m: () => 'm', 11 | s: () => 's', 12 | ms: () => 'ms', 13 | decimal: '', 14 | }); 15 | export const ForamtDuration = new HumanizeDuration(langService); 16 | ForamtDuration.setOptions({ 17 | language: 'shortEn', 18 | round: true, 19 | units: ['y', 'mo', 'd', 'h', 'm', 's'], 20 | largest: 6, 21 | }); 22 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElonH/RcloneNg/dded12e50d0c5c483016e0121a3f33f735b321f3/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RcloneNg 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

RcloneNg

15 |

An angular web application for Rclone

16 | 17 | 21 | 25 | 29 | 30 | Loading ... 31 |
32 |
33 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /** ************************************************************************************************* 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /** ************************************************************************************************* 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /** ************************************************************************************************* 61 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'themes'; 2 | @import '~@nebular/theme/styles/globals'; 3 | @import '~nebular-icons/css/nebular-icons.css'; 4 | @import '~bootstrap/dist/css/bootstrap.min.css'; 5 | @import '~ngx-easy-table/style.scss'; 6 | @import '~vex-js/dist/css/vex.css'; 7 | @import '~vex-js/dist/css/vex-theme-bottom-right-corner.css'; 8 | @import '~vex-js/dist/css/vex-theme-default.css'; 9 | @import '~vex-js/dist/css/vex-theme-flat-attack.css'; 10 | @import '~vex-js/dist/css/vex-theme-os.css'; 11 | @import '~vex-js/dist/css/vex-theme-plain.css'; 12 | @import '~vex-js/dist/css/vex-theme-top.css'; 13 | @import '~vex-js/dist/css/vex-theme-wireframe.css'; 14 | 15 | @include nb-install() { 16 | @include nb-theme-global(); 17 | } 18 | 19 | .rng-noselect { 20 | -webkit-touch-callout: none; /* iOS Safari */ 21 | -webkit-user-select: none; /* Safari */ 22 | -khtml-user-select: none; /* Konqueror HTML */ 23 | -moz-user-select: none; /* Old versions of Firefox */ 24 | -ms-user-select: none; /* Internet Explorer/Edge */ 25 | user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ 26 | } 27 | 28 | .cdk-overlay-container { 29 | /* hack for between compatible nbTooltip and ngx-modialog-7 */ 30 | z-index: 3000 !important; 31 | } 32 | 33 | .infinte-rotate { 34 | animation: rotation 1.5s infinite linear; 35 | } 36 | 37 | a { 38 | text-decoration: none !important; 39 | } 40 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { 5 | BrowserDynamicTestingModule, 6 | platformBrowserDynamicTesting, 7 | } from '@angular/platform-browser-dynamic/testing'; 8 | import 'zone.js/dist/zone-testing'; 9 | 10 | declare const require: { 11 | context( 12 | path: string, 13 | deep?: boolean, 14 | filter?: RegExp 15 | ): { 16 | keys(): string[]; 17 | (id: string): T; 18 | }; 19 | }; 20 | 21 | // First, initialize the Angular testing environment. 22 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /src/themes.scss: -------------------------------------------------------------------------------- 1 | @import '~@nebular/theme/styles/theming'; 2 | @import '~@nebular/theme/styles/themes/default'; 3 | 4 | $nb-themes: nb-register-theme( 5 | ( 6 | // add your variables here like: 7 | // color-bg: #4ca6ff, 8 | ), 9 | default, 10 | default 11 | ); 12 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "resolveJsonModule": true, 15 | "lib": ["es2018", "dom"] 16 | }, 17 | "angularCompilerOptions": { 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-angular"], 3 | "rules": { 4 | "array-type": false, 5 | "arrow-return-shorthand": true, 6 | "curly": false, 7 | "deprecation": { 8 | "severity": "warning" 9 | }, 10 | "component-class-suffix": true, 11 | "contextual-lifecycle": true, 12 | "directive-class-suffix": true, 13 | "directive-selector": [true, "attribute", "app", "camelCase"], 14 | "component-selector": [true, "element", "app", "kebab-case"], 15 | "import-blacklist": [true, "rxjs/Rx"], 16 | "max-classes-per-file": false, 17 | "member-ordering": [ 18 | true, 19 | { 20 | "order": ["static-field", "instance-field", "static-method", "instance-method"] 21 | } 22 | ], 23 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 24 | "no-empty": false, 25 | "no-inferrable-types": [true, "ignore-params"], 26 | "no-non-null-assertion": true, 27 | "no-redundant-jsdoc": false, 28 | "no-switch-case-fall-through": true, 29 | "no-var-requires": false, 30 | "variable-name": { 31 | "options": ["ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"] 32 | }, 33 | "no-conflicting-lifecycle": true, 34 | "no-host-metadata-property": true, 35 | "no-input-rename": true, 36 | "no-inputs-metadata-property": true, 37 | "no-output-native": true, 38 | "no-output-on-prefix": true, 39 | "no-output-rename": true, 40 | "no-outputs-metadata-property": true, 41 | "template-banana-in-box": true, 42 | "template-no-negated-async": true, 43 | "use-lifecycle-interface": true, 44 | "use-pipe-transform-interface": true, 45 | "no-submodule-imports": [ 46 | true, 47 | "rxjs", 48 | "@angular/platform-browser", 49 | "@angular/core/testing", 50 | "@angular/router/testing", 51 | "@angular/platform-browser-dynamic", 52 | "zone.js" 53 | ], 54 | "no-this-assignment": false, 55 | "no-empty-interface": false, 56 | "interface-name": false 57 | }, 58 | "rulesDirectory": ["codelyzer"] 59 | } 60 | --------------------------------------------------------------------------------