├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── gulpfile.js ├── i18n └── en.sample.txt ├── package-lock.json ├── package.json ├── scripts └── publish_dailybuild.sh └── src ├── favicon.ico ├── favicon.png ├── index.html ├── langs ├── cz_CZ.txt ├── es.txt ├── fr_FR.txt ├── it_IT.txt ├── pl_PL.txt ├── ru_RU.txt ├── zh_Hans.txt └── zh_Hant.txt ├── robots.txt ├── scripts ├── config │ ├── aria2Errors.js │ ├── aria2Options.js │ ├── aria2RpcConstants.js │ ├── buildConfiguration.js │ ├── configuration.js │ ├── constants.js │ ├── defaultLanguage.js │ ├── fileTypes.js │ ├── initiator.js │ └── languages.js ├── controllers │ ├── command.js │ ├── debug.js │ ├── list.js │ ├── main.js │ ├── new.js │ ├── settings-aria2.js │ ├── settings-ariang.js │ ├── status.js │ └── task-detail.js ├── core │ ├── __core.js │ ├── __fix.js │ ├── app.js │ ├── root.js │ └── router.js ├── directives │ ├── autoFocus.js │ ├── blobDownload.js │ ├── chart.js │ ├── exportCommandApiDialog.js │ ├── indeterminate.js │ ├── pieceBar.js │ ├── pieceMap.js │ ├── placeholder.js │ ├── setting.js │ ├── settingDialog.js │ ├── tooltip.js │ └── validUrls.js ├── filters │ ├── dateDuration.js │ ├── fileOrderBy.js │ ├── logOrderBy.js │ ├── longDate.js │ ├── peerOrderBy.js │ ├── percent.js │ ├── reverse.js │ ├── taskOrderBy.js │ ├── taskStatus.js │ ├── timeDisplayName.js │ └── volume.js └── services │ ├── aria2HttpRpcService.js │ ├── aria2RpcService.js │ ├── aria2SettingService.js │ ├── aria2TaskService.js │ ├── aria2WebSocketRpcService.js │ ├── ariaNgAssetsCacheService.js │ ├── ariaNgCommonService.js │ ├── ariaNgFileService.js │ ├── ariaNgKeyboardService.js │ ├── ariaNgLanguageLoader.js │ ├── ariaNgLocalizationService.js │ ├── ariaNgLogService.js │ ├── ariaNgMonitorService.js │ ├── ariaNgNotificationService.js │ ├── ariaNgSettingService.js │ ├── ariaNgStorageService.js │ ├── ariaNgTitleService.js │ └── ariaNgVersionService.js ├── styles ├── controls │ ├── angular-promise-buttons.css │ ├── chart.css │ ├── global-status.css │ ├── new-task-table.css │ ├── piece-bar-map.css │ ├── settings-table.css │ └── task-table.css ├── core │ ├── core.css │ └── extend.css └── theme │ ├── default-dark.css │ └── default.css ├── tileicon.png ├── touchicon.png └── views ├── debug.html ├── export-command-api-dialog.html ├── list.html ├── new.html ├── notification-reloadable.html ├── setting-dialog.html ├── setting.html ├── settings-aria2.html ├── settings-ariang.html ├── status.html └── task-detail.html /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | docker: 5 | - image: circleci/node:14.17.0-browsers 6 | working_directory: ~/AriaNg 7 | 8 | jobs: 9 | build: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | - run: npm install 14 | - run: sudo npm install -g gulp 15 | - run: gulp clean build 16 | - save_cache: 17 | key: v1-ariang-{{ .Environment.CIRCLE_SHA1 }} 18 | paths: 19 | - ~/AriaNg 20 | publish_daily_build: 21 | <<: *defaults 22 | steps: 23 | - restore_cache: 24 | key: v1-ariang-{{ .Environment.CIRCLE_SHA1 }} 25 | - add_ssh_keys: 26 | fingerprints: 27 | - run: bash ./scripts/publish_dailybuild.sh 28 | 29 | workflows: 30 | version: 2 31 | build_and_publish: 32 | jobs: 33 | - build 34 | - publish_daily_build: 35 | requires: 36 | - build 37 | filters: 38 | branches: 39 | only: master 40 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [package.json] 24 | indent_style = space 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "angular", 8 | "eslint:recommended" 9 | ], 10 | "parserOptions": { 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "indent": [ 15 | "error", 16 | 4 17 | ], 18 | "linebreak-style": [ 19 | "error", 20 | "unix" 21 | ], 22 | "quotes": [ 23 | "error", 24 | "single" 25 | ], 26 | "semi": [ 27 | "error", 28 | "always" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Yeoman template 3 | node_modules/ 4 | bower_components/ 5 | *.log 6 | 7 | build/ 8 | dist/ 9 | ### Windows template 10 | # Windows image file caches 11 | Thumbs.db 12 | ehthumbs.db 13 | 14 | # Folder config file 15 | Desktop.ini 16 | 17 | # Recycle Bin used on file shares 18 | $RECYCLE.BIN/ 19 | 20 | # Windows Installer files 21 | *.cab 22 | *.msi 23 | *.msm 24 | *.msp 25 | 26 | # Windows shortcuts 27 | *.lnk 28 | ### Linux template 29 | *~ 30 | 31 | # temporary files which can be created if a process still has a handle open of a deleted file 32 | .fuse_hidden* 33 | 34 | # KDE directory preferences 35 | .directory 36 | 37 | # Linux trash folder which might appear on any partition or disk 38 | .Trash-* 39 | ### JetBrains template 40 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 41 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 42 | 43 | .idea/ 44 | *.iml 45 | 46 | # Mongo Explorer plugin: 47 | .idea/mongoSettings.xml 48 | 49 | ## File-based project format: 50 | *.iws 51 | 52 | ## Plugin-specific files: 53 | 54 | # IntelliJ 55 | /out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Crashlytics plugin (for Android Studio and IntelliJ) 64 | com_crashlytics_export_strings.xml 65 | crashlytics.properties 66 | crashlytics-build.properties 67 | fabric.properties 68 | ### Node template 69 | # Logs 70 | logs 71 | *.log 72 | npm-debug.log* 73 | 74 | # Runtime data 75 | pids 76 | *.pid 77 | *.seed 78 | 79 | # Directory for instrumented libs generated by jscoverage/JSCover 80 | lib-cov 81 | 82 | # Coverage directory used by tools like istanbul 83 | coverage 84 | 85 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 86 | .grunt 87 | 88 | # node-waf configuration 89 | .lock-wscript 90 | 91 | # Compiled binary addons (http://nodejs.org/api/addons.html) 92 | build/Release 93 | 94 | # Dependency directories 95 | node_modules 96 | jspm_packages 97 | 98 | # Optional npm cache directory 99 | .npm 100 | 101 | # Optional REPL history 102 | .node_repl_history 103 | ### OSX template 104 | .DS_Store 105 | .AppleDouble 106 | .LSOverride 107 | 108 | # Icon must end with two \r 109 | Icon 110 | 111 | # Thumbnails 112 | ._* 113 | 114 | # Files that might appear in the root of a volume 115 | .DocumentRevisions-V100 116 | .fseventsd 117 | .Spotlight-V100 118 | .TemporaryItems 119 | .Trashes 120 | .VolumeIcon.icns 121 | 122 | # Directories potentially created on remote AFP share 123 | .AppleDB 124 | .AppleDesktop 125 | Network Trash Folder 126 | Temporary Items 127 | .apdisk 128 | 129 | # Visual Studio Code 130 | .vscode/ 131 | *.code-workspace 132 | 133 | .tmp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 MaysWind (i@mayswind.net) 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 | # AriaNg 2 | [![License](https://img.shields.io/github/license/mayswind/AriaNg.svg?style=flat)](https://github.com/mayswind/AriaNg/blob/master/LICENSE) 3 | [![Lastest Build](https://img.shields.io/circleci/project/github/mayswind/AriaNg.svg?style=flat)](https://circleci.com/gh/mayswind/AriaNg/tree/master) 4 | [![Lastest Release](https://img.shields.io/github/release/mayswind/AriaNg.svg?style=flat)](https://github.com/mayswind/AriaNg/releases) 5 | 6 | ## Introduction 7 | AriaNg is a modern web frontend making [aria2](https://github.com/aria2/aria2) easier to use. AriaNg is written in pure html & javascript, thus it does not need any compilers or runtime environment. You can just put AriaNg in your web server and open it in your browser. AriaNg uses responsive layout, and supports any desktop or mobile devices. 8 | 9 | ## Features 10 | 1. Pure Html & Javascript, no runtime required 11 | 2. Responsive design, supporting desktop and mobile devices 12 | 3. User-friendly interface 13 | * Sort tasks (by name, size, progress, remaining time, download speed, etc.), files, bittorrent peers 14 | * Search tasks 15 | * Retry tasks 16 | * Adjust task order by dragging 17 | * More information of tasks (health percentage, client information of bt peers, etc.) 18 | * Filter files by specified file types (videos, audios, pictures, documents, applications, archives, etc.) or file extensions 19 | * Tree view for multi-directory task 20 | * Download / upload speed chart for aria2 or single task 21 | * Full support for aria2 settings 22 | 4. Dark theme 23 | 5. Url command line api support 24 | 6. Download finished notification 25 | 7. Multi-languages support 26 | * Czech 27 | * English 28 | * Spanish 29 | * French 30 | * Italian 31 | * Polish 32 | * Russian 33 | * Chinese (Simplified Chinese / Traditional Chinese) 34 | 8. Multi aria2 RPC host support 35 | 9. Exporting and Importing settings support 36 | 10. Less bandwidth usage, only requesting incremental data 37 | 38 | ## Screenshots 39 | #### Desktop 40 | ![AriaNg](https://raw.githubusercontent.com/mayswind/AriaNg-WebSite/master/screenshots/desktop.png) 41 | #### Mobile Device 42 | ![AriaNg](https://raw.githubusercontent.com/mayswind/AriaNg-WebSite/master/screenshots/mobile.png) 43 | 44 | ## Installation 45 | AriaNg now provides three versions, standard version, all-in-one version and [AriaNg Native](https://github.com/mayswind/AriaNg-Native). Standard version is suitable for deployment in the web server, and provides on-demand loading. All-In-One version is suitable for local using, and you can download it and just open the only html file in browser. [AriaNg Native](https://github.com/mayswind/AriaNg-Native) is also suitable for local using, and is no need for browser. 46 | 47 | #### Prebuilt release 48 | Latest Release: [https://github.com/mayswind/AriaNg/releases](https://github.com/mayswind/AriaNg/releases) 49 | 50 | Latest Daily Build (Standard Version): [https://github.com/mayswind/AriaNg-DailyBuild/archive/master.zip](https://github.com/mayswind/AriaNg-DailyBuild/archive/master.zip) 51 | 52 | #### Building from source 53 | Make sure you have [Node.js](https://nodejs.org/), [NPM](https://www.npmjs.com/) and [Gulp](https://gulpjs.com/) installed. Then download the source code, and follow these steps. 54 | 55 | ##### Standard Version 56 | 57 | $ npm install 58 | $ gulp clean build 59 | 60 | ##### All-In-One Version 61 | 62 | $ npm install 63 | $ gulp clean build-bundle 64 | 65 | The builds will be placed in the dist directory. 66 | 67 | #### Usage Notes 68 | Since AriaNg standard version loads language resources asynchronously, you may not open index.html directly on the local file system to run AriaNg. It is recommended that you can use the all-in-one version or deploy AriaNg in a web container or download [AriaNg Native](https://github.com/mayswind/AriaNg-Native) that does not require a browser to run. 69 | 70 | ## Translating 71 | 72 | Everyone is welcome to contribute translations. All translations files are put in `/src/langs/`. You can just modify and commit a new pull request. 73 | 74 | If you want to translate AriaNg to a new language, you can add language configuration to `/src/scripts/config/languages.js`, then copy `/i18n/en.sample.txt` to `/src/langs/` and rename it to the language code to be translated, then you can start the translation work. 75 | 76 | ## Documents 77 | 1. [English](http://ariang.mayswind.net) 78 | 2. [Simplified Chinese (简体中文)](http://ariang.mayswind.net/zh_Hans) 79 | 80 | ## Demo 81 | Please visit [http://ariang.mayswind.net/latest](http://ariang.mayswind.net/latest) 82 | 83 | ## Third Party Extensions 84 | There are some third-party applications based on AriaNg, so you can use AriaNg in more scenarios or devices. Please visit [Third Party Extensions](http://ariang.mayswind.net/3rd-extensions.html) for more information. 85 | 86 | ## License 87 | [MIT](https://github.com/mayswind/AriaNg/blob/master/LICENSE) 88 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const gulpLoadPlugins = require('gulp-load-plugins'); 3 | const browserSync = require('browser-sync'); 4 | const del = require('del'); 5 | const fs = require('fs'); 6 | const git = require('git-rev-sync'); 7 | const tryFn = require('nice-try'); 8 | const saveLicense = require('uglify-save-license'); 9 | 10 | const $ = gulpLoadPlugins(); 11 | const reload = browserSync.reload; 12 | 13 | gulp.task('clean', () => del(['.tmp', 'dist'])); 14 | 15 | gulp.task('lint', () => gulp.src([ 16 | 'src/scripts/**/*.js' 17 | ]).pipe(reload({stream: true, once: true})) 18 | .pipe($.eslint.format()) 19 | .pipe($.if(!browserSync.active, $.eslint.failAfterError())) 20 | .pipe(gulp.dest('src/scripts'))); 21 | 22 | gulp.task('prepare-fonts', () => gulp.src([ 23 | 'node_modules/font-awesome/fonts/fontawesome-webfont.*' 24 | ]).pipe(gulp.dest('.tmp/fonts'))); 25 | 26 | gulp.task('process-fonts', gulp.series('prepare-fonts', () => gulp.src([ 27 | '.tmp/fonts/**/*' 28 | ]).pipe(gulp.dest('dist/fonts')))); 29 | 30 | gulp.task('prepare-langs', () => gulp.src([ 31 | 'src/langs/**/*' 32 | ]).pipe(gulp.dest('.tmp/langs'))); 33 | 34 | gulp.task('process-langs', gulp.series('prepare-langs', () => gulp.src([ 35 | '.tmp/langs/**/*' 36 | ]).pipe(gulp.dest('dist/langs')))); 37 | 38 | gulp.task('prepare-styles', () => gulp.src([ 39 | 'src/styles/**/*.css' 40 | ]).pipe($.autoprefixer()) 41 | .pipe(gulp.dest('.tmp/styles')) 42 | .pipe(reload({stream: true}))); 43 | 44 | gulp.task('prepare-scripts', () => gulp.src([ 45 | 'src/scripts/**/*.js' 46 | ]).pipe($.plumber()) 47 | .pipe($.injectVersion({replace: '${ARIANG_VERSION}'})) 48 | .pipe($.replace(/\${ARIANG_BUILD_COMMIT}/g, tryFn(git.short) || 'Local')) 49 | .pipe(gulp.dest('.tmp/scripts')) 50 | .pipe(reload({stream: true}))); 51 | 52 | gulp.task('prepare-views', () => gulp.src([ 53 | 'src/views/**/*.html' 54 | ]).pipe($.htmlmin({collapseWhitespace: true})) 55 | .pipe($.angularTemplatecache({module: 'ariaNg', filename: 'views/templates.js', root: 'views/'})) 56 | .pipe(gulp.dest('.tmp/scripts'))); 57 | 58 | gulp.task('prepare-html', gulp.series('prepare-styles', 'prepare-scripts', 'prepare-views', () => gulp.src([ 59 | 'src/*.html' 60 | ]).pipe($.useref({searchPath: ['.tmp', 'src', '.']})) 61 | .pipe($.if('js/*.js', $.replace(/\/\/# sourceMappingURL=.*/g, ''))) 62 | .pipe($.if('css/*.css', $.replace(/\/\*# sourceMappingURL=.* \*\/$/g, ''))) 63 | .pipe($.if(['js/moment-with-locales-*.min.js', 'js/plugins.min.js', 'js/aria-ng.min.js'], $.uglify({output: {comments: saveLicense}}))) 64 | .pipe($.if(['css/plugins.min.css', 'css/aria-ng.min.css'], $.cssnano({safe: true, autoprefixer: false}))) 65 | .pipe($.replace(/url\((\.\.\/fonts\/[a-zA-Z0-9\-]+\.woff2)(\?[a-zA-Z0-9\-_=.]+)?\)/g, (match, fileName) => { 66 | return 'url(' + fileName + ')'; // remove version of woff2 file (woff2 file should be cached via application cache) 67 | })) 68 | .pipe($.if(['js/plugins.min.js', 'js/aria-ng.min.js', 'css/plugins.min.css', 'css/aria-ng.min.css'], $.rev())) 69 | .pipe($.if('*.html', $.htmlmin({collapseWhitespace: true}))) 70 | .pipe($.revReplace()) 71 | .pipe(gulp.dest('.tmp')))); 72 | 73 | gulp.task('process-html', gulp.series('prepare-html', () => gulp.src([ 74 | '.tmp/*.html' 75 | ]).pipe($.replace(//, '')) 76 | .pipe(gulp.dest('dist')))); 77 | 78 | gulp.task('process-assets', gulp.series('process-html', () => gulp.src([ 79 | '.tmp/css/**/*', 80 | '.tmp/js/**/*' 81 | ], {base: '.tmp'}) 82 | .pipe(gulp.dest('dist')))); 83 | 84 | gulp.task('prepare-assets-bundle', () => gulp.src([ 85 | 'src/favicon.png' 86 | ]).pipe(gulp.dest('.tmp'))); 87 | 88 | gulp.task('process-assets-bundle', gulp.series('prepare-fonts', 'prepare-langs', 'prepare-html', 'prepare-assets-bundle', () => gulp.src('.tmp/index.html') 89 | .pipe($.replace(//g, (match, fileName) => { 90 | const content = fs.readFileSync('.tmp/' + fileName, 'utf8'); 91 | return ''; 92 | })) 93 | .pipe($.replace(/'; 96 | })) 97 | .pipe($.replace(/url\(\.\.\/(fonts\/[a-zA-Z0-9\-]+\.woff)(\?[a-zA-Z0-9\-_=.]+)?\)/g, (match, fileName) => { 98 | if (!fs.existsSync('.tmp/' + fileName)) { 99 | return match; 100 | } 101 | 102 | const contentBuffer = fs.readFileSync('.tmp/' + fileName); 103 | const contentBase64 = contentBuffer.toString('base64'); 104 | return 'url(data:application/x-font-woff;base64,' + contentBase64 + ')'; 105 | })) 106 | .pipe($.replace(//g, (match, fileName) => { 107 | if (!fs.existsSync('.tmp/' + fileName)) { 108 | return match; 109 | } 110 | 111 | const contentBuffer = fs.readFileSync('.tmp/' + fileName); 112 | const contentBase64 = contentBuffer.toString('base64'); 113 | return ''; 114 | })) 115 | .pipe($.replace('', () => { 116 | const langDir = '.tmp/langs/'; 117 | let result = ''; 118 | const fileNames = fs.readdirSync(langDir, 'utf8'); 119 | 120 | if (fileNames.length > 0) { 121 | result = ''; 138 | } 139 | 140 | return result; 141 | })) 142 | .pipe($.replace(/<[a-z]+( [a-z\-]+="[a-zA-Z0-9\- ]+")* [a-z\-]+="((favicon.ico)|(tileicon.png)|(touchicon.png))"\/?>/g, '')) 143 | .pipe(gulp.dest('dist')))); 144 | 145 | gulp.task('process-full-extras', () => gulp.src([ 146 | 'LICENSE', 147 | 'src/*.*', 148 | '!src/*.html' 149 | ], { 150 | dot: true 151 | }).pipe(gulp.dest('dist'))); 152 | 153 | gulp.task('process-tiny-extras', () => gulp.src([ 154 | 'LICENSE' 155 | ]).pipe(gulp.dest('dist'))); 156 | 157 | gulp.task('info', () => gulp.src([ 158 | 'dist/**/*' 159 | ]).pipe($.size({title: 'build', gzip: true}))); 160 | 161 | gulp.task('build', gulp.series('lint', 'process-fonts', 'process-langs', 'process-assets', 'process-full-extras', 'info')); 162 | 163 | gulp.task('build-bundle', gulp.series('lint', 'process-assets-bundle', 'process-tiny-extras', 'info')); 164 | 165 | gulp.task('serve', gulp.series('prepare-styles', 'prepare-scripts', 'prepare-fonts', () => { 166 | browserSync({ 167 | notify: false, 168 | port: 9000, 169 | server: { 170 | baseDir: ['.tmp', 'src'], 171 | routes: { 172 | '/node_modules': 'node_modules' 173 | } 174 | } 175 | }); 176 | 177 | gulp.watch([ 178 | 'src/*.html', 179 | 'src/*.ico', 180 | 'src/*.png', 181 | 'src/langs/*.txt', 182 | 'src/views/*.html', 183 | 'src/imgs/**/*', 184 | '.tmp/fonts/**/*' 185 | ]).on('change', reload); 186 | 187 | gulp.watch('src/styles/**/*.css', gulp.series('prepare-styles')); 188 | gulp.watch('src/scripts/**/*.js', gulp.series('prepare-scripts')); 189 | gulp.watch('src/fonts/**/*', gulp.series('prepare-fonts')); 190 | })); 191 | 192 | gulp.task('serve:dist', () => { 193 | browserSync({ 194 | notify: false, 195 | port: 9000, 196 | server: { 197 | baseDir: ['dist'] 198 | } 199 | }); 200 | }); 201 | 202 | gulp.task('default', gulp.series('clean', 'build')); 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": ">=14" 5 | }, 6 | "dependencies": { 7 | "admin-lte": "2.4.18", 8 | "angular": "1.6.10", 9 | "angular-animate": "1.6.10", 10 | "angular-bittorrent-peerid": "^1.3.4", 11 | "angular-busy": "^4.1.4", 12 | "angular-clipboard": "^1.7.0", 13 | "angular-cookies": "1.6.10", 14 | "angular-input-dropdown": "git+https://github.com/mayswind/angular-input-dropdown.git#68670e39816698b3eb98c0e740a0efe77d5fbdd1", 15 | "angular-local-storage": "^0.7.1", 16 | "angular-messages": "1.6.10", 17 | "angular-moment": "1.3.0", 18 | "angular-promise-buttons": "^0.1.23", 19 | "angular-route": "1.6.10", 20 | "angular-sanitize": "1.6.10", 21 | "angular-sweetalert": "^1.1.2", 22 | "angular-touch": "1.6.10", 23 | "angular-translate": "^2.19.0", 24 | "angular-ui-notification": "^0.3.6", 25 | "angular-utf8-base64": "^0.0.5", 26 | "angular-websocket": "^2.0.1", 27 | "angularjs-dragula": "^2.0.0", 28 | "awesome-bootstrap-checkbox": "^0.3.7", 29 | "bootstrap": "3.4.1", 30 | "bootstrap-contextmenu": "^1.0.0", 31 | "echarts": "3.8.5", 32 | "font-awesome": "^4.7.0", 33 | "jquery": "3.4.1", 34 | "jquery-slimscroll": "^1.3.8", 35 | "moment": "2.29.4", 36 | "natural-compare": "1.4.0", 37 | "sweetalert": "^1.1.3" 38 | }, 39 | "devDependencies": { 40 | "browser-sync": "^2.27.7", 41 | "del": "^6.0.0", 42 | "eslint-config-angular": "^0.5.0", 43 | "eslint-plugin-angular": "^4.1.0", 44 | "git-rev-sync": "^3.0.1", 45 | "gulp": "^4.0.2", 46 | "gulp-angular-templatecache": "^2.2.7", 47 | "gulp-autoprefixer": "^8.0.0", 48 | "gulp-cssnano": "^2.1.3", 49 | "gulp-eslint": "^4.0.2", 50 | "gulp-htmlmin": "^5.0.1", 51 | "gulp-if": "^3.0.0", 52 | "gulp-inject-version": "^1.0.1", 53 | "gulp-load-plugins": "^2.0.7", 54 | "gulp-plumber": "^1.2.1", 55 | "gulp-replace": "^1.0.0", 56 | "gulp-rev": "^9.0.0", 57 | "gulp-rev-replace": "^0.4.4", 58 | "gulp-size": "^4.0.1", 59 | "gulp-sourcemaps": "^3.0.0", 60 | "gulp-uglify": "^3.0.2", 61 | "gulp-useref": "^5.0.0", 62 | "nice-try": "^3.0.0", 63 | "uglify-save-license": "^0.4.1" 64 | }, 65 | "name": "ariang", 66 | "description": "AriaNg, a modern web frontend making aria2 easier to use.", 67 | "version": "1.3.10", 68 | "main": "index.html", 69 | "scripts": { 70 | "test": "echo \"Error: no test specified\" && exit 0", 71 | "build": "gulp clean build" 72 | }, 73 | "browsers": [ 74 | "> 1%", 75 | "last 2 versions", 76 | "Firefox ESR" 77 | ], 78 | "repository": { 79 | "type": "git", 80 | "url": "git+https://github.com/mayswind/AriaNg.git" 81 | }, 82 | "keywords": [ 83 | "aria2", 84 | "Web", 85 | "Frontend", 86 | "UI" 87 | ], 88 | "author": "MaysWind ", 89 | "license": "MIT", 90 | "bugs": { 91 | "url": "https://github.com/mayswind/AriaNg/issues" 92 | }, 93 | "homepage": "http://ariang.mayswind.net/" 94 | } 95 | -------------------------------------------------------------------------------- /scripts/publish_dailybuild.sh: -------------------------------------------------------------------------------- 1 | if [ $CI == "true" ] && [ $CIRCLE_BRANCH == "master" ]; then 2 | git config --global user.name "CircleCI"; 3 | git config --global user.email "CircleCI"; 4 | 5 | ssh -o StrictHostKeyChecking=no git@github.com 6 | 7 | echo "Publishing daily build..."; 8 | git clone git@github.com:mayswind/AriaNg-DailyBuild.git $HOME/AriaNg-DailyBuild/ 9 | 10 | rm -rf $HOME/AriaNg-DailyBuild/* 11 | cp dist/* $HOME/AriaNg-DailyBuild/ -r; 12 | 13 | cd $HOME/AriaNg-DailyBuild/; 14 | 15 | git add -A; 16 | git commit -a -m "daily build #$CIRCLE_SHA1"; 17 | git push origin master; 18 | 19 | echo "Done."; 20 | fi 21 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayswind/AriaNg/ee48603a27a4b08b604fef93a6c8ee9242f20d1e/src/favicon.ico -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayswind/AriaNg/ee48603a27a4b08b604fef93a6c8ee9242f20d1e/src/favicon.png -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | # AriaNg 2 | 3 | User-agent: * 4 | Disallow: / 5 | -------------------------------------------------------------------------------- /src/scripts/config/aria2Errors.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').constant('aria2Errors', { 5 | //'0': { }, //All downloads were successful. 6 | '1': { 7 | descriptionKey: 'error.unknown' 8 | }, 9 | '2': { 10 | descriptionKey: 'error.operation.timeout' 11 | }, 12 | '3': { 13 | descriptionKey: 'error.resource.notfound' 14 | }, 15 | '4': { 16 | descriptionKey: 'error.resource.notfound.max-file-not-found' 17 | }, 18 | '5': { 19 | descriptionKey: 'error.download.aborted.lowest-speed-limit' 20 | }, 21 | '6': { 22 | descriptionKey: 'error.network.problem' 23 | }, 24 | //'7': { },//If there were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal. 25 | '8': { 26 | descriptionKey: 'error.resume.notsupported' 27 | }, 28 | '9': { 29 | descriptionKey: 'error.space.notenough' 30 | }, 31 | '10': { 32 | descriptionKey: 'error.piece.length.different' 33 | }, 34 | '11': { 35 | descriptionKey: 'error.download.sametime' 36 | }, 37 | '12': { 38 | descriptionKey: 'error.download.torrent.sametime' 39 | }, 40 | '13': { 41 | descriptionKey: 'error.file.exists' 42 | }, 43 | '14': { 44 | descriptionKey: 'error.file.rename.failed' 45 | }, 46 | '15': { 47 | descriptionKey: 'error.file.open.failed' 48 | }, 49 | '16': { 50 | descriptionKey: 'error.file.create.failed' 51 | }, 52 | '17': { 53 | descriptionKey: 'error.io.error' 54 | }, 55 | '18': { 56 | descriptionKey: 'error.directory.create.failed' 57 | }, 58 | '19': { 59 | descriptionKey: 'error.name.resolution.failed' 60 | }, 61 | '20': { 62 | descriptionKey: 'error.metalink.file.parse.failed' 63 | }, 64 | '21': { 65 | descriptionKey: 'error.ftp.command.failed' 66 | }, 67 | '22': { 68 | descriptionKey: 'error.http.response.header.bad' 69 | }, 70 | '23': { 71 | descriptionKey: 'error.redirects.toomany' 72 | }, 73 | '24': { 74 | descriptionKey: 'error.http.authorization.failed' 75 | }, 76 | '25': { 77 | descriptionKey: 'error.bencoded.file.parse.failed' 78 | }, 79 | '26': { 80 | descriptionKey: 'error.torrent.file.corrupted' 81 | }, 82 | '27': { 83 | descriptionKey: 'error.magnet.uri.bad' 84 | }, 85 | '28': { 86 | descriptionKey: 'error.option.bad' 87 | }, 88 | '29': { 89 | descriptionKey: 'error.server.overload' 90 | }, 91 | '30': { 92 | descriptionKey: 'error.rpc.request.parse.failed' 93 | }, 94 | //'31': { }, //Reserved. Not used. 95 | '32': { 96 | descriptionKey: 'error.checksum.failed' 97 | } 98 | }); 99 | }()); 100 | -------------------------------------------------------------------------------- /src/scripts/config/aria2RpcConstants.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').constant('aria2RpcConstants', { 5 | rpcServiceVersion: '2.0', 6 | rpcServiceName: 'aria2', 7 | rpcSystemServiceName: 'system', 8 | rpcTokenPrefix: 'token:' 9 | }).constant('aria2RpcErrors', { 10 | Unauthorized: { 11 | message: 'Unauthorized', 12 | tipTextKey: 'rpc.error.unauthorized' 13 | } 14 | }); 15 | }()); 16 | -------------------------------------------------------------------------------- /src/scripts/config/buildConfiguration.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').constant('ariaNgBuildConfiguration', { 5 | buildVersion: '${ARIANG_VERSION}', 6 | buildCommit: '${ARIANG_BUILD_COMMIT}' 7 | }); 8 | }()); 9 | -------------------------------------------------------------------------------- /src/scripts/config/configuration.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').config(['$qProvider', '$translateProvider', 'localStorageServiceProvider', 'NotificationProvider', 'ariaNgConstants', 'ariaNgLanguages', function ($qProvider, $translateProvider, localStorageServiceProvider, NotificationProvider, ariaNgConstants, ariaNgLanguages) { 5 | $qProvider.errorOnUnhandledRejections(false); 6 | 7 | localStorageServiceProvider 8 | .setPrefix(ariaNgConstants.appPrefix) 9 | .setStorageType('localStorage') 10 | .setStorageCookie(365, '/'); 11 | 12 | var supportedLangs = []; 13 | var languageAliases = {}; 14 | 15 | for (var langName in ariaNgLanguages) { 16 | if (!ariaNgLanguages.hasOwnProperty(langName)) { 17 | continue; 18 | } 19 | 20 | var language = ariaNgLanguages[langName]; 21 | var aliases = language.aliases; 22 | 23 | supportedLangs.push(langName); 24 | 25 | if (!angular.isArray(aliases) || aliases.length < 1) { 26 | continue; 27 | } 28 | 29 | for (var i = 0; i < aliases.length; i++) { 30 | var langAlias = aliases[i]; 31 | languageAliases[langAlias] = langName; 32 | } 33 | } 34 | 35 | $translateProvider.useLoader('ariaNgLanguageLoader') 36 | .useLoaderCache(true) 37 | .registerAvailableLanguageKeys(supportedLangs, languageAliases) 38 | .fallbackLanguage(ariaNgConstants.defaultLanguage) 39 | .useSanitizeValueStrategy('escapeParameters'); 40 | 41 | NotificationProvider.setOptions({ 42 | delay: ariaNgConstants.notificationInPageTimeout 43 | }); 44 | }]); 45 | }()); 46 | -------------------------------------------------------------------------------- /src/scripts/config/constants.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').constant('ariaNgConstants', { 5 | title: 'AriaNg', 6 | appPrefix: 'AriaNg', 7 | optionStorageKey: 'Options', 8 | browserNotificationHistoryStorageKey: 'Notifications', 9 | languageStorageKeyPrefix: 'Language', 10 | settingHistoryKeyPrefix: 'History', 11 | languagePath: 'langs', 12 | languageFileExtension: '.txt', 13 | defaultLanguage: 'en', 14 | defaultHost: 'localhost', 15 | defaultSecureProtocol: 'https', 16 | defaultPathSeparator: '/', 17 | httpRequestTimeout: 20000, 18 | globalStatStorageCapacity: 120, 19 | taskStatStorageCapacity: 300, 20 | lazySaveTimeout: 500, 21 | errorTooltipDelay: 500, 22 | notificationInPageTimeout: 2000, 23 | historyMaxStoreCount: 10, 24 | cachedDebugLogsLimit: 100 25 | }).constant('ariaNgDefaultOptions', { 26 | language: 'en', 27 | theme: 'light', 28 | title: '${downspeed}, ${upspeed} - ${title}', 29 | titleRefreshInterval: 5000, 30 | browserNotification: false, 31 | browserNotificationSound: true, 32 | browserNotificationFrequency: 'unlimited', 33 | rpcAlias: '', 34 | rpcHost: '', 35 | rpcPort: '6800', 36 | rpcInterface: 'jsonrpc', 37 | protocol: 'http', 38 | httpMethod: 'POST', 39 | rpcRequestHeaders: '', 40 | secret: '', 41 | extendRpcServers: [], 42 | webSocketReconnectInterval: 5000, 43 | globalStatRefreshInterval: 1000, 44 | downloadTaskRefreshInterval: 1000, 45 | keyboardShortcuts: true, 46 | swipeGesture: true, 47 | dragAndDropTasks: true, 48 | rpcListDisplayOrder: 'recentlyUsed', 49 | afterCreatingNewTask: 'task-list', 50 | removeOldTaskAfterRetrying: false, 51 | confirmTaskRemoval: true, 52 | includePrefixWhenCopyingFromTaskDetails: true, 53 | showPiecesInfoInTaskDetailPage: 'le10240', 54 | afterRetryingTask: 'task-list-downloading', 55 | taskListIndependentDisplayOrder: false, 56 | displayOrder: 'default:asc', 57 | waitingTaskListPageDisplayOrder: 'default:asc', 58 | stoppedTaskListPageDisplayOrder: 'default:asc', 59 | fileListDisplayOrder: 'default:asc', 60 | peerListDisplayOrder: 'default:asc' 61 | }); 62 | }()); 63 | -------------------------------------------------------------------------------- /src/scripts/config/fileTypes.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').constant('ariaNgFileTypes', { 5 | video: { 6 | name: 'Videos', 7 | extensions: [ 8 | '.3g2', 9 | '.3gp', 10 | '.3gp2', 11 | '.3gpp', 12 | '.asf', 13 | '.asx', 14 | '.avi', 15 | '.dat', 16 | '.divx', 17 | '.flv', 18 | '.m1v', 19 | '.m2ts', 20 | '.m2v', 21 | '.m4v', 22 | '.mkv', 23 | '.mov', 24 | '.mp4', 25 | '.mpe', 26 | '.mpeg', 27 | '.mpg', 28 | '.mts', 29 | '.ogv', 30 | '.qt', 31 | '.ram', 32 | '.rm', 33 | '.rmvb', 34 | '.ts', 35 | '.vob', 36 | '.wmv' 37 | ] 38 | }, 39 | audio: { 40 | name: 'Audios', 41 | extensions: [ 42 | '.aac', 43 | '.ac3', 44 | '.adts', 45 | '.amr', 46 | '.ape', 47 | '.eac3', 48 | '.flac', 49 | '.m1a', 50 | '.m2a', 51 | '.m4a', 52 | '.mid', 53 | '.mka', 54 | '.mp2', 55 | '.mp3', 56 | '.mpa', 57 | '.mpc', 58 | '.ogg', 59 | '.ra', 60 | '.tak', 61 | '.vqf', 62 | '.wm', 63 | '.wav', 64 | '.wma', 65 | '.wv' 66 | ] 67 | }, 68 | picture: { 69 | name: 'Pictures', 70 | extensions: [ 71 | '.abr', 72 | '.bmp', 73 | '.emf', 74 | '.gif', 75 | '.j2c', 76 | '.j2k', 77 | '.jfif', 78 | '.jif', 79 | '.jp2', 80 | '.jpc', 81 | '.jpe', 82 | '.jpeg', 83 | '.jpf', 84 | '.jpg', 85 | '.jpk', 86 | '.jpx', 87 | '.pcx', 88 | '.pct', 89 | '.pic', 90 | '.pict', 91 | '.png', 92 | '.pns', 93 | '.psd', 94 | '.psdx', 95 | '.raw', 96 | '.svg', 97 | '.svgz', 98 | '.tga', 99 | '.tif', 100 | '.tiff', 101 | '.wbm', 102 | '.wbmp', 103 | '.webp', 104 | '.wmf', 105 | '.xif' 106 | ] 107 | }, 108 | document: { 109 | name: 'Documents', 110 | extensions: [ 111 | '.csv', 112 | '.doc', 113 | '.docm', 114 | '.docx', 115 | '.dot', 116 | '.dotm', 117 | '.dotx', 118 | '.key', 119 | '.mpp', 120 | '.numbers', 121 | '.odp', 122 | '.ods', 123 | '.odt', 124 | '.pages', 125 | '.pdf', 126 | '.pot', 127 | '.potm', 128 | '.potx', 129 | '.pps', 130 | '.ppsm', 131 | '.ppsx', 132 | '.ppt', 133 | '.pptm', 134 | '.pptx', 135 | '.rtf', 136 | '.txt', 137 | '.vsd', 138 | '.vsdx', 139 | '.wk1', 140 | '.wk2', 141 | '.wk3', 142 | '.wk4', 143 | '.wks', 144 | '.wpd', 145 | '.wps', 146 | '.xla', 147 | '.xlam', 148 | '.xll', 149 | '.xlm', 150 | '.xls', 151 | '.xlsb', 152 | '.xlsm', 153 | '.xlsx', 154 | '.xlt', 155 | '.xltx', 156 | '.xlw', 157 | '.xps' 158 | ] 159 | }, 160 | application: { 161 | name: 'Applications', 162 | extensions: [ 163 | '.apk', 164 | '.bat', 165 | '.com', 166 | '.deb', 167 | '.dll', 168 | '.dmg', 169 | '.exe', 170 | '.ipa', 171 | '.jar', 172 | '.msi', 173 | '.rpm', 174 | '.sh' 175 | ] 176 | }, 177 | archive: { 178 | name: 'Archives', 179 | extensions: [ 180 | '.001', 181 | '.7z', 182 | '.ace', 183 | '.arj', 184 | '.bz2', 185 | '.cab', 186 | '.cbr', 187 | '.cbz', 188 | '.gz', 189 | '.img', 190 | '.iso', 191 | '.lzh', 192 | '.qcow2', 193 | '.r', 194 | '.rar', 195 | '.sef', 196 | '.tar', 197 | '.taz', 198 | '.tbz', 199 | '.tbz2', 200 | '.uue', 201 | '.vdi', 202 | '.vhd', 203 | '.vmdk', 204 | '.wim', 205 | '.xar', 206 | '.xz', 207 | '.z', 208 | '.zip' 209 | ] 210 | } 211 | }); 212 | }()); 213 | -------------------------------------------------------------------------------- /src/scripts/config/initiator.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').run(['moment', 'ariaNgLocalizationService', 'ariaNgSettingService', function (moment, ariaNgLocalizationService, ariaNgSettingService) { 5 | var language = ariaNgSettingService.getLanguage(); 6 | 7 | moment.updateLocale('zh-cn', { 8 | week: null 9 | }); 10 | 11 | ariaNgLocalizationService.applyLanguage(language); 12 | }]); 13 | }()); 14 | -------------------------------------------------------------------------------- /src/scripts/config/languages.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').constant('ariaNgLanguages', { 5 | 'cz_CZ': { 6 | name: 'Czech', 7 | displayName: 'Čeština' 8 | }, 9 | 'en': { 10 | name: 'English', 11 | displayName: 'English' 12 | }, 13 | 'es': { 14 | name: 'Spanish', 15 | displayName: 'Español' 16 | }, 17 | 'fr_FR': { 18 | name: 'French', 19 | displayName: 'Français' 20 | }, 21 | 'it_IT': { 22 | name: 'Italian', 23 | displayName: 'Italiano' 24 | }, 25 | 'pl_PL': { 26 | name: 'Polish', 27 | displayName: 'Polski' 28 | }, 29 | 'ru_RU': { 30 | name: 'Russian', 31 | displayName: 'Русский' 32 | }, 33 | 'zh_Hans': { 34 | name: 'Simplified Chinese', 35 | displayName: '简体中文', 36 | aliases: ['zh_CHS', 'zh_CN', 'zh_SG'] 37 | }, 38 | 'zh_Hant': { 39 | name: 'Traditional Chinese', 40 | displayName: '繁體中文', 41 | aliases: ['zh_CHT', 'zh_TW', 'zh_HK', 'zh_MO'] 42 | } 43 | }); 44 | }()); 45 | -------------------------------------------------------------------------------- /src/scripts/controllers/command.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').controller('CommandController', ['$rootScope', '$window', '$location', '$routeParams', 'ariaNgDefaultOptions', 'ariaNgCommonService', 'ariaNgLogService', 'ariaNgSettingService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $window, $location, $routeParams, ariaNgDefaultOptions, ariaNgCommonService, ariaNgLogService, ariaNgSettingService, aria2TaskService, aria2SettingService) { 5 | var path = $location.path(); 6 | 7 | var doNewTaskCommand = function (url, params) { 8 | try { 9 | url = ariaNgCommonService.base64UrlDecode(url); 10 | } catch (ex) { 11 | ariaNgCommonService.showError('URL is not base64 encoded!'); 12 | return false; 13 | } 14 | 15 | var options = {}; 16 | var isPaused = false; 17 | 18 | if (params) { 19 | for (var key in params) { 20 | if (!params.hasOwnProperty(key)) { 21 | continue; 22 | } 23 | 24 | if (aria2SettingService.isOptionKeyValid(key)) { 25 | options[key] = params[key]; 26 | } 27 | } 28 | 29 | if (params.pause === 'true') { 30 | isPaused = true; 31 | } 32 | } 33 | 34 | $rootScope.loadPromise = aria2TaskService.newUriTask({ 35 | urls: [url], 36 | options: options 37 | }, isPaused, function (response) { 38 | if (!response.success) { 39 | return false; 40 | } 41 | 42 | if (isPaused) { 43 | $location.path('/waiting'); 44 | } else { 45 | $location.path('/downloading'); 46 | } 47 | }); 48 | 49 | ariaNgLogService.info('[CommandController] new download: ' + url); 50 | 51 | return true; 52 | }; 53 | 54 | var doSetRpcCommand = function (rpcProtocol, rpcHost, rpcPort, rpcInterface, secret) { 55 | rpcPort = rpcPort || ariaNgDefaultOptions.rpcPort; 56 | rpcInterface = rpcInterface || ariaNgDefaultOptions.rpcInterface; 57 | secret = secret || ariaNgDefaultOptions.secret; 58 | 59 | ariaNgLogService.info('[CommandController] set rpc: ' + rpcProtocol + '://' + rpcHost + ':' + rpcPort + '/' + rpcInterface + ', secret: ' + secret); 60 | 61 | if (!rpcProtocol || (rpcProtocol !== 'http' && rpcProtocol !== 'https' && rpcProtocol !== 'ws' && rpcProtocol !== 'wss')) { 62 | ariaNgCommonService.showError('Protocol is invalid!'); 63 | return false; 64 | } 65 | 66 | if (!rpcHost) { 67 | ariaNgCommonService.showError('RPC host cannot be empty!'); 68 | return false; 69 | } 70 | 71 | if (secret) { 72 | try { 73 | secret = ariaNgCommonService.base64UrlDecode(secret); 74 | } catch (ex) { 75 | ariaNgCommonService.showError('RPC secret is not base64 encoded!'); 76 | return false; 77 | } 78 | } 79 | 80 | var newSetting = { 81 | rpcAlias: '', 82 | rpcHost: rpcHost, 83 | rpcPort: rpcPort, 84 | rpcInterface: rpcInterface, 85 | protocol: rpcProtocol, 86 | httpMethod: ariaNgDefaultOptions.httpMethod, 87 | rpcRequestHeaders: '', 88 | secret: secret 89 | }; 90 | 91 | if (ariaNgSettingService.isRpcSettingEqualsDefault(newSetting)) { 92 | $location.path('/downloading'); 93 | } else { 94 | ariaNgSettingService.setDefaultRpcSetting(newSetting, { 95 | keepCurrent: false, 96 | forceSet: true 97 | }); 98 | 99 | $location.path('/downloading'); 100 | $window.location.reload(); 101 | } 102 | 103 | return true; 104 | }; 105 | 106 | var doCommand = function (path, params) { 107 | if (path.indexOf('/new') === 0) { 108 | return doNewTaskCommand(params.url, params); 109 | } else if (path.indexOf('/settings/rpc/set') === 0) { 110 | return doSetRpcCommand(params.protocol, params.host, params.port, params.interface, params.secret); 111 | } else { 112 | ariaNgCommonService.showError('Parameter is invalid!'); 113 | return false; 114 | } 115 | }; 116 | 117 | var allParameters = angular.extend({}, $routeParams, $location.search()); 118 | 119 | if (!doCommand(path, allParameters)) { 120 | $location.path('/downloading'); 121 | } 122 | }]); 123 | }()); 124 | -------------------------------------------------------------------------------- /src/scripts/controllers/debug.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').controller('AriaNgDebugController', ['$rootScope', '$scope', '$location', '$interval', '$timeout', '$filter', 'ariaNgConstants', 'ariaNgCommonService', 'ariaNgLocalizationService', 'ariaNgLogService', 'ariaNgKeyboardService', 'ariaNgSettingService', 'aria2RpcService', function ($rootScope, $scope, $location, $interval, $timeout, $filter, ariaNgConstants, ariaNgCommonService, ariaNgLocalizationService, ariaNgLogService, ariaNgKeyboardService, ariaNgSettingService, aria2RpcService) { 5 | var tabStatusItems = [ 6 | { 7 | name: 'logs', 8 | show: true 9 | }, 10 | { 11 | name: 'rpc', 12 | show: true 13 | } 14 | ]; 15 | var debugLogRefreshPromise = null; 16 | 17 | var getVisibleTabOrders = function () { 18 | var items = []; 19 | 20 | for (var i = 0; i < tabStatusItems.length; i++) { 21 | if (tabStatusItems[i].show) { 22 | items.push(tabStatusItems[i].name); 23 | } 24 | } 25 | 26 | return items; 27 | }; 28 | 29 | var loadAria2RPCMethods = function () { 30 | if ($scope.context.availableRpcMethods && $scope.context.availableRpcMethods.length) { 31 | return; 32 | } 33 | 34 | return aria2RpcService.listMethods({ 35 | silent: false, 36 | callback: function (response) { 37 | if (response.success) { 38 | $scope.context.availableRpcMethods = response.data; 39 | } 40 | } 41 | }); 42 | }; 43 | 44 | $scope.context = { 45 | currentTab: 'logs', 46 | logMaxCount: ariaNgConstants.cachedDebugLogsLimit, 47 | logAutoRefreshAvailableInterval: ariaNgCommonService.getTimeOptions([100, 200, 500, 1000, 2000], true), 48 | logAutoRefreshInterval: 1000, 49 | logListDisplayOrder: 'time:desc', 50 | logLevelFilter: 'DEBUG', 51 | logs: [], 52 | currentLog: null, 53 | availableRpcMethods: [], 54 | rpcRequestMethod: '', 55 | rpcRequestParameters: '{}', 56 | rpcResponse: null 57 | }; 58 | 59 | $scope.enableDebugMode = function () { 60 | return ariaNgSettingService.isEnableDebugMode(); 61 | }; 62 | 63 | $scope.changeTab = function (tabName) { 64 | if (tabName === 'rpc') { 65 | $rootScope.loadPromise = loadAria2RPCMethods(); 66 | } 67 | 68 | $scope.context.currentTab = tabName; 69 | }; 70 | 71 | $scope.changeLogListDisplayOrder = function (type) { 72 | var oldType = ariaNgCommonService.parseOrderType($scope.context.logListDisplayOrder); 73 | var newType = ariaNgCommonService.parseOrderType(type); 74 | 75 | if (newType.type === oldType.type) { 76 | newType.reverse = !oldType.reverse; 77 | } 78 | 79 | $scope.context.logListDisplayOrder = newType.getValue(); 80 | }; 81 | 82 | $scope.isLogListSetDisplayOrder = function (type) { 83 | var orderType = ariaNgCommonService.parseOrderType($scope.context.logListDisplayOrder); 84 | var targetType = ariaNgCommonService.parseOrderType(type); 85 | 86 | return orderType.equals(targetType); 87 | }; 88 | 89 | $scope.getLogListOrderType = function () { 90 | return $scope.context.logListDisplayOrder; 91 | }; 92 | 93 | $scope.filterLog = function (log) { 94 | if (!log || !angular.isString(log.level)) { 95 | return false; 96 | } 97 | 98 | if (!$scope.context.logLevelFilter) { 99 | return true; 100 | } 101 | 102 | return ariaNgLogService.compareLogLevel(log.level, $scope.context.logLevelFilter) >= 0; 103 | }; 104 | 105 | $scope.setLogLevelFilter = function (filter) { 106 | $scope.context.logLevelFilter = filter; 107 | }; 108 | 109 | $scope.isSetLogLevelFilter = function (filter) { 110 | return $scope.context.logLevelFilter === filter; 111 | }; 112 | 113 | $scope.getLogLevelFilter = function () { 114 | return $scope.context.logLevelFilter; 115 | }; 116 | 117 | $scope.setAutoRefreshInterval = function (interval) { 118 | $scope.context.logAutoRefreshInterval = interval; 119 | 120 | if (debugLogRefreshPromise) { 121 | $interval.cancel(debugLogRefreshPromise); 122 | } 123 | 124 | if (interval > 0) { 125 | $scope.reloadLogs(); 126 | 127 | debugLogRefreshPromise = $interval(function () { 128 | $scope.reloadLogs(); 129 | }, $scope.context.logAutoRefreshInterval); 130 | } 131 | }; 132 | 133 | $scope.reloadLogs = function () { 134 | $scope.context.logs = ariaNgLogService.getDebugLogs().slice(); 135 | }; 136 | 137 | $scope.clearDebugLogs = function () { 138 | ariaNgCommonService.confirm('Confirm Clear', 'Are you sure you want to clear debug logs?', 'warning', function () { 139 | ariaNgLogService.clearDebugLogs(); 140 | $scope.reloadLogs(); 141 | }, false); 142 | }; 143 | 144 | $scope.showLogDetail = function (log) { 145 | $scope.context.currentLog = log; 146 | angular.element('#log-detail-modal').modal(); 147 | }; 148 | 149 | $('#log-detail-modal').on('hide.bs.modal', function (e) { 150 | $scope.context.currentLog = null; 151 | }); 152 | 153 | $scope.executeAria2Method = function () { 154 | if (!$scope.context.rpcRequestMethod || $scope.context.rpcRequestMethod.indexOf('.') < 0) { 155 | ariaNgCommonService.showError('RPC method is illegal!'); 156 | return; 157 | } 158 | 159 | var methodNameParts = $scope.context.rpcRequestMethod.split('.'); 160 | 161 | if (methodNameParts.length !== 2) { 162 | ariaNgCommonService.showError('RPC method is illegal!'); 163 | return; 164 | } 165 | 166 | var methodName = methodNameParts[1]; 167 | 168 | if (!angular.isFunction(aria2RpcService[methodName])) { 169 | ariaNgCommonService.showError('AriaNg does not support this RPC method!'); 170 | return; 171 | } 172 | 173 | var context = { 174 | silent: false, 175 | callback: function (response) { 176 | if (response) { 177 | $scope.context.rpcResponse = $filter('json')(response.data); 178 | } else { 179 | $scope.context.rpcResponse = $filter('json')(response); 180 | } 181 | } 182 | }; 183 | 184 | var parameters = {}; 185 | 186 | try { 187 | parameters = angular.fromJson($scope.context.rpcRequestParameters); 188 | } catch (ex) { 189 | ariaNgLogService.error('[AriaNgDebugController.executeAria2Method] failed to parse request parameters: ' + $scope.context.rpcRequestParameters, ex); 190 | ariaNgCommonService.showError('RPC request parameters are invalid!'); 191 | return; 192 | } 193 | 194 | for (var key in parameters) { 195 | if (!parameters.hasOwnProperty(key) || key === 'silent' || key === 'callback') { 196 | continue; 197 | } 198 | 199 | context[key] = parameters[key]; 200 | } 201 | 202 | return aria2RpcService[methodName](context); 203 | }; 204 | 205 | $scope.requestParametersTextboxKeyDown = function (event) { 206 | if (!ariaNgSettingService.getKeyboardShortcuts()) { 207 | return; 208 | } 209 | 210 | if (ariaNgKeyboardService.isCtrlEnterPressed(event) && $scope.executeMethodForm.$valid) { 211 | if (event.preventDefault) { 212 | event.preventDefault(); 213 | } 214 | 215 | $scope.executeAria2Method(); 216 | 217 | return false; 218 | } 219 | }; 220 | 221 | $scope.$on('$destroy', function () { 222 | if (debugLogRefreshPromise) { 223 | $interval.cancel(debugLogRefreshPromise); 224 | } 225 | }); 226 | 227 | $rootScope.swipeActions.extendLeftSwipe = function () { 228 | var tabItems = getVisibleTabOrders(); 229 | var tabIndex = tabItems.indexOf($scope.context.currentTab); 230 | 231 | if (tabIndex < tabItems.length - 1) { 232 | $scope.changeTab(tabItems[tabIndex + 1]); 233 | return true; 234 | } else { 235 | return false; 236 | } 237 | }; 238 | 239 | $rootScope.swipeActions.extendRightSwipe = function () { 240 | var tabItems = getVisibleTabOrders(); 241 | var tabIndex = tabItems.indexOf($scope.context.currentTab); 242 | 243 | if (tabIndex > 0) { 244 | $scope.changeTab(tabItems[tabIndex - 1]); 245 | return true; 246 | } else { 247 | return false; 248 | } 249 | }; 250 | 251 | $rootScope.loadPromise = $timeout(function () { 252 | if (!ariaNgSettingService.isEnableDebugMode()) { 253 | ariaNgCommonService.showError('Access Denied!', function () { 254 | if (!ariaNgSettingService.isEnableDebugMode()) { 255 | $location.path('/settings/ariang'); 256 | } 257 | }); 258 | return; 259 | } 260 | 261 | $scope.setAutoRefreshInterval($scope.context.logAutoRefreshInterval); 262 | }, 100); 263 | }]); 264 | }()); 265 | -------------------------------------------------------------------------------- /src/scripts/controllers/list.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').controller('DownloadListController', ['$rootScope', '$scope', '$window', '$location', '$route', '$interval', 'dragulaService', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2TaskService', function ($rootScope, $scope, $window, $location, $route, $interval, dragulaService, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, aria2TaskService) { 5 | var location = $location.path().substring(1); 6 | var downloadTaskRefreshPromise = null; 7 | var pauseDownloadTaskRefresh = false; 8 | var needRequestWholeInfo = true; 9 | 10 | var refreshDownloadTask = function (silent) { 11 | if (pauseDownloadTaskRefresh) { 12 | return; 13 | } 14 | 15 | return aria2TaskService.getTaskList(location, needRequestWholeInfo, function (response) { 16 | if (pauseDownloadTaskRefresh) { 17 | return; 18 | } 19 | 20 | if (!response.success) { 21 | if (response.data.message === aria2RpcErrors.Unauthorized.message) { 22 | $interval.cancel(downloadTaskRefreshPromise); 23 | } 24 | 25 | return; 26 | } 27 | 28 | var isRequestWholeInfo = response.context.requestWholeInfo; 29 | var taskList = response.data; 30 | 31 | if (isRequestWholeInfo) { 32 | $rootScope.taskContext.list = taskList; 33 | needRequestWholeInfo = false; 34 | } else { 35 | if ($rootScope.taskContext.list && $rootScope.taskContext.list.length > 0) { 36 | for (var i = 0; i < $rootScope.taskContext.list.length; i++) { 37 | var task = $rootScope.taskContext.list[i]; 38 | delete task.verifiedLength; 39 | delete task.verifyIntegrityPending; 40 | } 41 | } 42 | 43 | if (ariaNgCommonService.extendArray(taskList, $rootScope.taskContext.list, 'gid')) { 44 | needRequestWholeInfo = false; 45 | } else { 46 | needRequestWholeInfo = true; 47 | } 48 | } 49 | 50 | if ($rootScope.taskContext.list && $rootScope.taskContext.list.length > 0) { 51 | aria2TaskService.processDownloadTasks($rootScope.taskContext.list); 52 | 53 | if (!isRequestWholeInfo) { 54 | var hasFullStruct = false; 55 | 56 | for (var i = 0; i < $rootScope.taskContext.list.length; i++) { 57 | var task = $rootScope.taskContext.list[i]; 58 | 59 | if (task.hasTaskName || task.files || task.bittorrent) { 60 | hasFullStruct = true; 61 | break; 62 | } 63 | } 64 | 65 | if (!hasFullStruct) { 66 | needRequestWholeInfo = true; 67 | $rootScope.taskContext.list.length = 0; 68 | return; 69 | } 70 | } 71 | } 72 | 73 | $rootScope.taskContext.enableSelectAll = $rootScope.taskContext.list && $rootScope.taskContext.list.length > 0; 74 | }, silent); 75 | }; 76 | 77 | $scope.getOrderType = function () { 78 | return ariaNgSettingService.getDisplayOrder(location); 79 | }; 80 | 81 | $scope.isSupportDragTask = function () { 82 | if (!ariaNgSettingService.getDragAndDropTasks()) { 83 | return false; 84 | } 85 | 86 | var displayOrder = ariaNgCommonService.parseOrderType(ariaNgSettingService.getDisplayOrder(location)); 87 | 88 | return location === 'waiting' && displayOrder.type === 'default'; 89 | }; 90 | 91 | if (ariaNgSettingService.getDownloadTaskRefreshInterval() > 0) { 92 | downloadTaskRefreshPromise = $interval(function () { 93 | refreshDownloadTask(true); 94 | }, ariaNgSettingService.getDownloadTaskRefreshInterval()); 95 | } 96 | 97 | dragulaService.options($scope, 'task-list', { 98 | revertOnSpill: true, 99 | moves: function () { 100 | return $scope.isSupportDragTask(); 101 | } 102 | }); 103 | 104 | $scope.$on('task-list.drop-model', function (el, target, source) { 105 | var element = angular.element(target); 106 | var gid = element.attr('data-gid'); 107 | var index = element.index(); 108 | 109 | pauseDownloadTaskRefresh = true; 110 | 111 | aria2TaskService.changeTaskPosition(gid, index, function () { 112 | pauseDownloadTaskRefresh = false; 113 | }, true); 114 | }); 115 | 116 | $scope.$on('$destroy', function () { 117 | pauseDownloadTaskRefresh = true; 118 | 119 | if (downloadTaskRefreshPromise) { 120 | $interval.cancel(downloadTaskRefreshPromise); 121 | } 122 | }); 123 | 124 | $rootScope.keydownActions.selectAll = function (event) { 125 | if (event.preventDefault) { 126 | event.preventDefault(); 127 | } 128 | 129 | $scope.$apply(function () { 130 | $scope.selectAllTasks(); 131 | }); 132 | 133 | return false; 134 | }; 135 | 136 | $rootScope.keydownActions.delete = function (event) { 137 | if (event.preventDefault) { 138 | event.preventDefault(); 139 | } 140 | 141 | $scope.$apply(function () { 142 | $scope.removeTasks(); 143 | }); 144 | 145 | return false; 146 | } 147 | 148 | $rootScope.loadPromise = refreshDownloadTask(false); 149 | }]); 150 | }()); 151 | -------------------------------------------------------------------------------- /src/scripts/controllers/new.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').controller('NewTaskController', ['$rootScope', '$scope', '$location', '$timeout', 'ariaNgCommonService', 'ariaNgLogService', 'ariaNgKeyboardService', 'ariaNgFileService', 'ariaNgSettingService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $location, $timeout, ariaNgCommonService, ariaNgLogService, ariaNgKeyboardService, ariaNgFileService, ariaNgSettingService, aria2TaskService, aria2SettingService) { 5 | var tabStatusItems = [ 6 | { 7 | name: 'links', 8 | show: true 9 | }, 10 | { 11 | name: 'options', 12 | show: true 13 | } 14 | ]; 15 | var parameters = $location.search(); 16 | 17 | var getVisibleTabOrders = function () { 18 | var items = []; 19 | 20 | for (var i = 0; i < tabStatusItems.length; i++) { 21 | if (tabStatusItems[i].show) { 22 | items.push(tabStatusItems[i].name); 23 | } 24 | } 25 | 26 | return items; 27 | }; 28 | 29 | var setTabItemShow = function (name, status) { 30 | for (var i = 0; i < tabStatusItems.length; i++) { 31 | if (tabStatusItems[i].name === name) { 32 | tabStatusItems[i].show = status; 33 | break; 34 | } 35 | } 36 | }; 37 | 38 | var saveDownloadPath = function (options) { 39 | if (!options || !options.dir) { 40 | return; 41 | } 42 | 43 | aria2SettingService.addSettingHistory('dir', options.dir); 44 | }; 45 | 46 | var getDownloadTasksByLinks = function (options) { 47 | var urls = ariaNgCommonService.parseUrlsFromOriginInput($scope.context.urls); 48 | var tasks = []; 49 | 50 | if (!options) { 51 | options = angular.copy($scope.context.options); 52 | } 53 | 54 | for (var i = 0; i < urls.length; i++) { 55 | if (urls[i] === '' || urls[i].trim() === '') { 56 | continue; 57 | } 58 | 59 | tasks.push({ 60 | urls: [urls[i].trim()], 61 | options: options 62 | }); 63 | } 64 | 65 | return tasks; 66 | }; 67 | 68 | var downloadByLinks = function (pauseOnAdded, responseCallback) { 69 | var options = angular.copy($scope.context.options); 70 | var tasks = getDownloadTasksByLinks(options); 71 | 72 | saveDownloadPath(options); 73 | 74 | return aria2TaskService.newUriTasks(tasks, pauseOnAdded, responseCallback); 75 | }; 76 | 77 | var downloadByTorrent = function (pauseOnAdded, responseCallback) { 78 | var options = angular.copy($scope.context.options); 79 | 80 | var task = { 81 | content: $scope.context.uploadFile.base64Content, 82 | options: options 83 | }; 84 | 85 | saveDownloadPath(task.options); 86 | 87 | return aria2TaskService.newTorrentTask(task, pauseOnAdded, responseCallback); 88 | }; 89 | 90 | var downloadByMetalink = function (pauseOnAdded, responseCallback) { 91 | var task = { 92 | content: $scope.context.uploadFile.base64Content, 93 | options: angular.copy($scope.context.options) 94 | }; 95 | 96 | saveDownloadPath(task.options); 97 | 98 | return aria2TaskService.newMetalinkTask(task, pauseOnAdded, responseCallback); 99 | }; 100 | 101 | $scope.context = { 102 | currentTab: 'links', 103 | taskType: 'urls', 104 | urls: '', 105 | uploadFile: null, 106 | availableOptions: (function () { 107 | var keys = aria2SettingService.getNewTaskOptionKeys(); 108 | 109 | return aria2SettingService.getSpecifiedOptions(keys, { 110 | disableRequired: true 111 | }); 112 | })(), 113 | globalOptions: null, 114 | options: {}, 115 | optionFilter: { 116 | global: true, 117 | http: false, 118 | bittorrent: false 119 | }, 120 | exportCommandApiOptions: null 121 | }; 122 | 123 | if (parameters.url) { 124 | try { 125 | $scope.context.urls = ariaNgCommonService.base64UrlDecode(parameters.url); 126 | } catch (ex) { 127 | ariaNgLogService.error('[NewTaskController] base64 decode error, url=' + parameters.url, ex); 128 | } 129 | } 130 | 131 | $scope.changeTab = function (tabName) { 132 | if (tabName === 'options') { 133 | $scope.loadDefaultOption(); 134 | } 135 | 136 | $scope.context.currentTab = tabName; 137 | }; 138 | 139 | $rootScope.swipeActions.extendLeftSwipe = function () { 140 | var tabItems = getVisibleTabOrders(); 141 | var tabIndex = tabItems.indexOf($scope.context.currentTab); 142 | 143 | if (tabIndex < tabItems.length - 1) { 144 | $scope.changeTab(tabItems[tabIndex + 1]); 145 | return true; 146 | } else { 147 | return false; 148 | } 149 | }; 150 | 151 | $rootScope.swipeActions.extendRightSwipe = function () { 152 | var tabItems = getVisibleTabOrders(); 153 | var tabIndex = tabItems.indexOf($scope.context.currentTab); 154 | 155 | if (tabIndex > 0) { 156 | $scope.changeTab(tabItems[tabIndex - 1]); 157 | return true; 158 | } else { 159 | return false; 160 | } 161 | }; 162 | 163 | $scope.loadDefaultOption = function () { 164 | if ($scope.context.globalOptions) { 165 | return; 166 | } 167 | 168 | $rootScope.loadPromise = aria2SettingService.getGlobalOption(function (response) { 169 | if (response.success) { 170 | $scope.context.globalOptions = response.data; 171 | } 172 | }); 173 | }; 174 | 175 | $scope.openTorrent = function () { 176 | ariaNgFileService.openFileContent({ 177 | scope: $scope, 178 | fileFilter: '.torrent', 179 | fileType: 'binary' 180 | }, function (result) { 181 | $scope.context.uploadFile = result; 182 | $scope.context.taskType = 'torrent'; 183 | $scope.changeTab('options'); 184 | }, function (error) { 185 | ariaNgCommonService.showError(error); 186 | }, angular.element('#file-holder')); 187 | }; 188 | 189 | $scope.openMetalink = function () { 190 | ariaNgFileService.openFileContent({ 191 | scope: $scope, 192 | fileFilter: '.meta4,.metalink', 193 | fileType: 'binary' 194 | }, function (result) { 195 | $scope.context.uploadFile = result; 196 | $scope.context.taskType = 'metalink'; 197 | $scope.changeTab('options'); 198 | }, function (error) { 199 | ariaNgCommonService.showError(error); 200 | }, angular.element('#file-holder')); 201 | }; 202 | 203 | $scope.isNewTaskValid = function () { 204 | if (!$scope.context.uploadFile) { 205 | return $scope.newTaskForm.$valid; 206 | } 207 | 208 | return true; 209 | }; 210 | 211 | $scope.startDownload = function (pauseOnAdded) { 212 | var responseCallback = function (response) { 213 | if (!response.hasSuccess && !response.success) { 214 | return; 215 | } 216 | 217 | var firstTask = null; 218 | 219 | if (response.results && response.results.length > 0) { 220 | firstTask = response.results[0]; 221 | } else if (response) { 222 | firstTask = response; 223 | } 224 | 225 | if (ariaNgSettingService.getAfterCreatingNewTask() === 'task-detail' && firstTask && firstTask.data) { 226 | $location.path('/task/detail/' + firstTask.data); 227 | } else { 228 | if (pauseOnAdded) { 229 | $location.path('/waiting'); 230 | } else { 231 | $location.path('/downloading'); 232 | } 233 | } 234 | }; 235 | 236 | if ($scope.context.taskType === 'urls') { 237 | $rootScope.loadPromise = downloadByLinks(pauseOnAdded, responseCallback); 238 | } else if ($scope.context.taskType === 'torrent') { 239 | $rootScope.loadPromise = downloadByTorrent(pauseOnAdded, responseCallback); 240 | } else if ($scope.context.taskType === 'metalink') { 241 | $rootScope.loadPromise = downloadByMetalink(pauseOnAdded, responseCallback); 242 | } 243 | }; 244 | 245 | $scope.showExportCommandAPIModal = function () { 246 | $scope.context.exportCommandApiOptions = { 247 | type: 'new-task', 248 | data: getDownloadTasksByLinks() 249 | }; 250 | }; 251 | 252 | $scope.setOption = function (key, value, optionStatus) { 253 | if (value !== '' || !aria2SettingService.isOptionKeyRequired(key)) { 254 | $scope.context.options[key] = value; 255 | } else { 256 | delete $scope.context.options[key]; 257 | } 258 | 259 | optionStatus.setReady(); 260 | }; 261 | 262 | $scope.urlTextboxKeyDown = function (event) { 263 | if (!ariaNgSettingService.getKeyboardShortcuts()) { 264 | return; 265 | } 266 | 267 | if (ariaNgKeyboardService.isCtrlEnterPressed(event) && $scope.newTaskForm.$valid) { 268 | if (event.preventDefault) { 269 | event.preventDefault(); 270 | } 271 | 272 | $scope.startDownload(); 273 | 274 | return false; 275 | } 276 | }; 277 | 278 | $scope.getValidUrlsCount = function () { 279 | var urls = ariaNgCommonService.parseUrlsFromOriginInput($scope.context.urls); 280 | return urls ? urls.length : 0; 281 | }; 282 | 283 | $rootScope.loadPromise = $timeout(function () {}, 100); 284 | }]); 285 | }()); 286 | -------------------------------------------------------------------------------- /src/scripts/controllers/settings-aria2.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').controller('Aria2SettingsController', ['$rootScope', '$scope', '$location', 'ariaNgConstants', 'ariaNgCommonService', 'aria2SettingService', function ($rootScope, $scope, $location, ariaNgConstants, ariaNgCommonService, aria2SettingService) { 5 | var location = $location.path().substring($location.path().lastIndexOf('/') + 1); 6 | 7 | $scope.context = { 8 | availableOptions: (function (type) { 9 | var keys = aria2SettingService.getAvailableGlobalOptionsKeys(type); 10 | 11 | if (!keys) { 12 | ariaNgCommonService.showError('Type is illegal!'); 13 | return; 14 | } 15 | 16 | return aria2SettingService.getSpecifiedOptions(keys); 17 | })(location), 18 | globalOptions: [] 19 | }; 20 | 21 | $scope.setGlobalOption = function (key, value, optionStatus) { 22 | return aria2SettingService.setGlobalOption(key, value, function (response) { 23 | if (response.success && response.data === 'OK') { 24 | optionStatus.setSuccess(); 25 | } else { 26 | optionStatus.setFailed(response.data.message); 27 | } 28 | }, true); 29 | }; 30 | 31 | $rootScope.loadPromise = (function () { 32 | return aria2SettingService.getGlobalOption(function (response) { 33 | if (response.success) { 34 | $scope.context.globalOptions = response.data; 35 | } 36 | }); 37 | })(); 38 | }]); 39 | }()); 40 | -------------------------------------------------------------------------------- /src/scripts/controllers/status.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').controller('Aria2StatusController', ['$rootScope', '$scope', '$timeout', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2SettingService', function ($rootScope, $scope, $timeout, ariaNgCommonService, ariaNgSettingService, aria2SettingService) { 5 | $scope.context = { 6 | host: ariaNgSettingService.getCurrentRpcUrl(), 7 | serverStatus: null, 8 | isSupportReconnect: aria2SettingService.canReconnect() 9 | }; 10 | 11 | $scope.reconnect = function () { 12 | if (!$scope.context.isSupportReconnect || ($rootScope.taskContext.rpcStatus !== 'Disconnected' && $rootScope.taskContext.rpcStatus !== 'Waiting to reconnect')) { 13 | return; 14 | } 15 | 16 | aria2SettingService.reconnect(); 17 | }; 18 | 19 | $scope.saveSession = function () { 20 | return aria2SettingService.saveSession(function (response) { 21 | if (response.success && response.data === 'OK') { 22 | ariaNgCommonService.showOperationSucceeded('Session has been saved successfully.'); 23 | } 24 | }); 25 | }; 26 | 27 | $scope.shutdown = function () { 28 | ariaNgCommonService.confirm('Confirm Shutdown', 'Are you sure you want to shutdown aria2?', 'warning', function (status) { 29 | return aria2SettingService.shutdown(function (response) { 30 | if (response.success && response.data === 'OK') { 31 | ariaNgCommonService.showOperationSucceeded('Aria2 has been shutdown successfully.'); 32 | } 33 | }); 34 | }, true); 35 | }; 36 | 37 | $rootScope.$watch('taskContext.rpcStatus', function (value) { 38 | if (value === 'Connected') { 39 | aria2SettingService.getAria2Status(function (response) { 40 | if (response.success) { 41 | $scope.context.serverStatus = response.data; 42 | } 43 | }); 44 | } else { 45 | $scope.context.serverStatus = null; 46 | } 47 | }); 48 | 49 | $rootScope.loadPromise = $timeout(function () {}, 100); 50 | }]); 51 | }()); 52 | -------------------------------------------------------------------------------- /src/scripts/core/__core.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * AriaNg 3 | * https://github.com/mayswind/AriaNg 4 | */ 5 | 6 | (function () { 7 | 'use strict'; 8 | 9 | var ltIE10 = (function () { 10 | var browserName = navigator.appName; 11 | var browserVersions = navigator.appVersion.split(';'); 12 | var browserVersion = (browserVersions && browserVersions.length > 1 ? browserVersions[1].replace(/[ ]/g, '') : ''); 13 | 14 | if (browserName === 'Microsoft Internet Explorer' && (browserVersion === 'MSIE6.0' || browserVersion === 'MSIE7.0' || browserVersion === 'MSIE8.0' || browserVersion === 'MSIE9.0')) { 15 | return true; 16 | } 17 | 18 | return false; 19 | })(); 20 | 21 | if (ltIE10) { 22 | var tip = document.createElement('div'); 23 | tip.className = 'alert alert-danger'; 24 | tip.innerHTML = 'Sorry, AriaNg cannot support this browser, please upgrade your browser!'; 25 | document.getElementById('content-wrapper').appendChild(tip); 26 | } 27 | }()); 28 | -------------------------------------------------------------------------------- /src/scripts/core/__fix.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | //copy from AdminLTE app.js 5 | var fixContentWrapperHeight = function () { 6 | var windowHeight = $(window).height(); 7 | var neg = $('.main-header').outerHeight() + $('.main-footer').outerHeight(); 8 | 9 | $('.content-body').css('height', windowHeight - neg); 10 | }; 11 | 12 | $(window, '.wrapper').resize(function () { 13 | fixContentWrapperHeight(); 14 | 15 | setInterval(function(){ 16 | fixContentWrapperHeight(); 17 | }, 1); 18 | }); 19 | 20 | fixContentWrapperHeight(); 21 | }()); 22 | -------------------------------------------------------------------------------- /src/scripts/core/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var ariaNg = angular.module('ariaNg', [ 5 | 'ngRoute', 6 | 'ngSanitize', 7 | 'ngTouch', 8 | 'ngMessages', 9 | 'ngCookies', 10 | 'ngAnimate', 11 | 'pascalprecht.translate', 12 | 'angularMoment', 13 | 'ngWebSocket', 14 | 'utf8-base64', 15 | 'LocalStorageModule', 16 | 'ui-notification', 17 | 'angularBittorrentPeerid', 18 | 'cgBusy', 19 | 'angularPromiseButtons', 20 | 'oitozero.ngSweetAlert', 21 | 'angular-clipboard', 22 | 'inputDropdown', 23 | angularDragula(angular) 24 | ]); 25 | }()); 26 | -------------------------------------------------------------------------------- /src/scripts/core/router.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').config(['$routeProvider', function ($routeProvider) { 5 | $routeProvider 6 | .when('/downloading', { 7 | templateUrl: 'views/list.html', 8 | controller: 'DownloadListController' 9 | }) 10 | .when('/waiting', { 11 | templateUrl: 'views/list.html', 12 | controller: 'DownloadListController' 13 | }) 14 | .when('/stopped', { 15 | templateUrl: 'views/list.html', 16 | controller: 'DownloadListController' 17 | }) 18 | .when('/new', { 19 | templateUrl: 'views/new.html', 20 | controller: 'NewTaskController' 21 | }) 22 | .when('/new/:url', { 23 | template: '', 24 | controller: 'CommandController' 25 | }) 26 | .when('/task/detail/:gid', { 27 | templateUrl: 'views/task-detail.html', 28 | controller: 'TaskDetailController' 29 | }) 30 | .when('/settings/ariang', { 31 | templateUrl: 'views/settings-ariang.html', 32 | controller: 'AriaNgSettingsController' 33 | }) 34 | .when('/settings/ariang/:extendType', { 35 | templateUrl: 'views/settings-ariang.html', 36 | controller: 'AriaNgSettingsController' 37 | }) 38 | .when('/settings/aria2/basic', { 39 | templateUrl: 'views/settings-aria2.html', 40 | controller: 'Aria2SettingsController' 41 | }) 42 | .when('/settings/aria2/http-ftp-sftp', { 43 | templateUrl: 'views/settings-aria2.html', 44 | controller: 'Aria2SettingsController' 45 | }) 46 | .when('/settings/aria2/http', { 47 | templateUrl: 'views/settings-aria2.html', 48 | controller: 'Aria2SettingsController' 49 | }) 50 | .when('/settings/aria2/ftp-sftp', { 51 | templateUrl: 'views/settings-aria2.html', 52 | controller: 'Aria2SettingsController' 53 | }) 54 | .when('/settings/aria2/bt', { 55 | templateUrl: 'views/settings-aria2.html', 56 | controller: 'Aria2SettingsController' 57 | }) 58 | .when('/settings/aria2/metalink', { 59 | templateUrl: 'views/settings-aria2.html', 60 | controller: 'Aria2SettingsController' 61 | }) 62 | .when('/settings/aria2/rpc', { 63 | templateUrl: 'views/settings-aria2.html', 64 | controller: 'Aria2SettingsController' 65 | }) 66 | .when('/settings/aria2/advanced', { 67 | templateUrl: 'views/settings-aria2.html', 68 | controller: 'Aria2SettingsController' 69 | }) 70 | .when('/settings/rpc/set', { 71 | template: '', 72 | controller: 'CommandController' 73 | }) 74 | .when('/settings/rpc/set/:protocol/:host/:port/:interface/:secret?', { 75 | template: '', 76 | controller: 'CommandController' 77 | }) 78 | .when('/debug', { 79 | templateUrl: 'views/debug.html', 80 | controller: 'AriaNgDebugController' 81 | }) 82 | .when('/status', { 83 | templateUrl: 'views/status.html', 84 | controller: 'Aria2StatusController' 85 | }) 86 | .otherwise({ 87 | redirectTo: '/downloading' 88 | }); 89 | }]); 90 | }()); 91 | -------------------------------------------------------------------------------- /src/scripts/directives/autoFocus.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngAutoFocus', ['$timeout', function ($timeout) { 5 | return { 6 | restrict: 'A', 7 | link: function (scope, element) { 8 | $timeout(function () { 9 | element[0].focus(); 10 | }); 11 | } 12 | }; 13 | }]); 14 | }()); 15 | -------------------------------------------------------------------------------- /src/scripts/directives/blobDownload.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngBlobDownload', ['ariaNgFileService', function (ariaNgFileService) { 5 | return { 6 | restrict: 'A', 7 | scope: { 8 | ngBlobDownload: '=ngBlobDownload', 9 | ngFileName: '@', 10 | ngContentType: '@' 11 | }, 12 | link: function (scope, element) { 13 | scope.$watch('ngBlobDownload', function (value) { 14 | if (value) { 15 | ariaNgFileService.saveFileContent(value, element, { 16 | fileName: scope.ngFileName, 17 | contentType: scope.ngContentType 18 | }); 19 | } 20 | }); 21 | } 22 | }; 23 | }]); 24 | }()); 25 | -------------------------------------------------------------------------------- /src/scripts/directives/chart.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngChart', ['$window', 'chartTheme', function ($window, chartTheme) { 5 | return { 6 | restrict: 'E', 7 | template: '
', 8 | scope: { 9 | options: '=ngData', 10 | theme: '=ngTheme' 11 | }, 12 | link: function (scope, element, attrs) { 13 | var options = {}; 14 | 15 | angular.extend(options, attrs); 16 | 17 | var wrapper = element.find('div'); 18 | var wrapperParent = element.parent(); 19 | var parentHeight = wrapperParent.height(); 20 | 21 | var height = parseInt(attrs.height) || parentHeight || 200; 22 | wrapper.css('height', height + 'px'); 23 | 24 | var chart = echarts.init(wrapper[0], chartTheme.get(scope.theme)); 25 | 26 | var setOptions = function (value) { 27 | chart.setOption(value); 28 | }; 29 | 30 | angular.element($window).on('resize', function () { 31 | chart.resize(); 32 | scope.$apply(); 33 | }); 34 | 35 | scope.$watch('options', function (value) { 36 | if (value) { 37 | setOptions(value); 38 | } 39 | }, true); 40 | 41 | scope.$on('$destroy', function() { 42 | if (chart && !chart.isDisposed()) { 43 | chart.dispose(); 44 | } 45 | }); 46 | } 47 | }; 48 | }]).directive('ngPopChart', ['$window', 'chartTheme', function ($window, chartTheme) { 49 | return { 50 | restrict: 'A', 51 | scope: { 52 | options: '=ngData', 53 | theme: '=ngTheme' 54 | }, 55 | link: function (scope, element, attrs) { 56 | var options = { 57 | ngPopoverClass: '', 58 | ngContainer: 'body', 59 | ngTrigger: 'click', 60 | ngPlacement: 'top' 61 | }; 62 | 63 | angular.extend(options, attrs); 64 | 65 | var chart = null; 66 | var loadingIcon = '
'; 67 | 68 | element.popover({ 69 | container: options.ngContainer, 70 | content: '
' + loadingIcon +'
', 71 | html: true, 72 | placement: options.ngPlacement, 73 | template: '', 74 | trigger: options.ngTrigger 75 | }).on('shown.bs.popover', function () { 76 | var wrapper = angular.element('.chart-pop'); 77 | var wrapperParent = wrapper.parent(); 78 | var parentHeight = wrapperParent.height(); 79 | 80 | wrapper.empty(); 81 | 82 | var height = parseInt(attrs.height) || parentHeight || 200; 83 | wrapper.css('height', height + 'px'); 84 | 85 | chart = echarts.init(wrapper[0], chartTheme.get(scope.theme)); 86 | }).on('hide.bs.popover', function () { 87 | if (chart && !chart.isDisposed()) { 88 | chart.dispose(); 89 | } 90 | }).on('hidden.bs.popover', function () { 91 | angular.element('.chart-pop').empty().append(loadingIcon); 92 | }); 93 | 94 | var setOptions = function (value) { 95 | if (chart && !chart.isDisposed()) { 96 | chart.setOption(value); 97 | } 98 | }; 99 | 100 | scope.$watch('options', function (value) { 101 | if (value) { 102 | setOptions(value); 103 | } 104 | }, true); 105 | } 106 | }; 107 | }]).factory('chartTheme', ['chartDefaultTheme', 'chartDarkTheme', function (chartDefaultTheme, chartDarkTheme) { 108 | var themes = { 109 | defaultTheme: chartDefaultTheme, 110 | darkTheme: chartDarkTheme, 111 | }; 112 | 113 | return { 114 | get: function (name) { 115 | if (name !== 'default' && themes[name + 'Theme']) { 116 | return angular.extend({}, themes.defaultTheme, themes[name + 'Theme']); 117 | } else { 118 | return themes.defaultTheme; 119 | } 120 | } 121 | }; 122 | }]).factory('chartDefaultTheme', function () { 123 | return { 124 | color: ['#74a329', '#3a89e9'], 125 | legend: { 126 | top: 'bottom' 127 | }, 128 | toolbox: { 129 | show: false 130 | }, 131 | tooltip: { 132 | show: true, 133 | trigger: 'axis', 134 | backgroundColor: 'rgba(0, 0, 0, 0.7)', 135 | axisPointer: { 136 | type: 'line', 137 | lineStyle: { 138 | color: '#233333', 139 | type: 'dashed', 140 | width: 1 141 | }, 142 | crossStyle: { 143 | color: '#008acd', 144 | width: 1 145 | }, 146 | shadowStyle: { 147 | color: 'rgba(200,200,200,0.2)' 148 | } 149 | } 150 | }, 151 | grid: { 152 | x: 40, 153 | y: 20, 154 | x2: 30, 155 | y2: 50 156 | }, 157 | categoryAxis: { 158 | axisLine: { 159 | show: false 160 | }, 161 | axisTick: { 162 | show: false 163 | }, 164 | splitLine: { 165 | lineStyle: { 166 | color: '#f3f3f3' 167 | } 168 | } 169 | }, 170 | valueAxis: { 171 | axisLine: { 172 | show: false 173 | }, 174 | axisTick: { 175 | show: false 176 | }, 177 | splitLine: { 178 | lineStyle: { 179 | color: '#f3f3f3' 180 | } 181 | }, 182 | splitArea: { 183 | show: false 184 | } 185 | }, 186 | line: { 187 | itemStyle: { 188 | normal: { 189 | lineStyle: { 190 | width: 2, 191 | type: 'solid' 192 | } 193 | } 194 | }, 195 | smooth: true, 196 | symbolSize: 6 197 | }, 198 | animationDuration: 500 199 | }; 200 | }).factory('chartDarkTheme', function () { 201 | return { 202 | tooltip: { 203 | show: true, 204 | trigger: 'axis', 205 | backgroundColor: 'rgba(0, 0, 0, 0.7)', 206 | axisPointer: { 207 | type: 'line', 208 | lineStyle: { 209 | color: '#ddd', 210 | type: 'dashed', 211 | width: 1 212 | }, 213 | crossStyle: { 214 | color: '#ddd', 215 | width: 1 216 | }, 217 | shadowStyle: { 218 | color: 'rgba(200,200,200,0.2)' 219 | } 220 | } 221 | }, 222 | categoryAxis: { 223 | axisLine: { 224 | show: false 225 | }, 226 | axisTick: { 227 | show: false 228 | }, 229 | splitLine: { 230 | lineStyle: { 231 | color: '#333' 232 | } 233 | } 234 | }, 235 | valueAxis: { 236 | axisLine: { 237 | show: false 238 | }, 239 | axisTick: { 240 | show: false 241 | }, 242 | axisLabel: { 243 | show: true, 244 | textStyle: { 245 | color: '#eee', 246 | } 247 | }, 248 | splitLine: { 249 | lineStyle: { 250 | color: '#333' 251 | } 252 | }, 253 | splitArea: { 254 | show: false 255 | } 256 | } 257 | }; 258 | }); 259 | }()); 260 | -------------------------------------------------------------------------------- /src/scripts/directives/exportCommandApiDialog.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngExportCommandApiDialog', ['clipboard', 'ariaNgCommonService', function (clipboard, ariaNgCommonService) { 5 | return { 6 | restrict: 'E', 7 | templateUrl: 'views/export-command-api-dialog.html', 8 | replace: true, 9 | scope: { 10 | options: '=' 11 | }, 12 | link: function (scope, element, attrs) { 13 | scope.context = { 14 | trueFalseOptions: [{name: 'Enabled', value: true}, {name: 'Disabled', value: false}], 15 | baseUrl: ariaNgCommonService.getFullPageUrl(), 16 | commandAPIUrl: null, 17 | pauseOnAdded: true, 18 | isCopied: false 19 | }; 20 | 21 | var getBaseUrl = function () { 22 | var baseUrl = scope.context.baseUrl; 23 | 24 | if (baseUrl.indexOf('#') >= 0) { 25 | baseUrl = baseUrl.substring(0, baseUrl.indexOf('#')); 26 | } 27 | 28 | return baseUrl; 29 | }; 30 | 31 | var getNewTaskCommandAPIUrl = function (task) { 32 | var commandAPIUrl = getBaseUrl() + '#!/new/task?' + 33 | 'url=' + ariaNgCommonService.base64UrlEncode(task.urls[0]); 34 | 35 | if (scope.context.pauseOnAdded) { 36 | commandAPIUrl += '&pause=true'; 37 | } 38 | 39 | if (task.options) { 40 | for (var key in task.options) { 41 | if (!task.options.hasOwnProperty(key)) { 42 | continue; 43 | } 44 | 45 | commandAPIUrl += '&' + key + '=' + task.options[key]; 46 | } 47 | } 48 | 49 | return commandAPIUrl; 50 | }; 51 | 52 | var getNewTasksCommandAPIUrl = function (tasks) { 53 | var commandAPIUrls = ''; 54 | 55 | for (var i = 0; i < tasks.length; i++) { 56 | if (i > 0) { 57 | commandAPIUrls += '\n'; 58 | } 59 | 60 | commandAPIUrls += getNewTaskCommandAPIUrl(tasks[i]); 61 | } 62 | 63 | return commandAPIUrls; 64 | }; 65 | 66 | var getSettingCommandAPIUrl = function (setting) { 67 | var commandAPIUrl = getBaseUrl() + '#!/settings/rpc/set?' + 68 | 'protocol=' + setting.protocol + 69 | '&host=' + setting.rpcHost + 70 | '&port=' + setting.rpcPort + 71 | '&interface=' + setting.rpcInterface; 72 | 73 | if (setting.secret) { 74 | commandAPIUrl += '&secret=' + ariaNgCommonService.base64UrlEncode(setting.secret); 75 | } 76 | 77 | return commandAPIUrl; 78 | }; 79 | 80 | scope.generateCommandAPIUrl = function () { 81 | if (!scope.options) { 82 | return; 83 | } 84 | 85 | if (scope.options.type === 'new-task') { 86 | scope.context.commandAPIUrl = getNewTasksCommandAPIUrl(scope.options.data); 87 | } else if (scope.options.type === 'setting') { 88 | scope.context.commandAPIUrl = getSettingCommandAPIUrl(scope.options.data); 89 | } 90 | 91 | scope.context.isCopied = false; 92 | }; 93 | 94 | scope.copyCommandAPI = function () { 95 | clipboard.copyText(scope.context.commandAPIUrl, { 96 | container: angular.element(element)[0] 97 | }); 98 | scope.context.isCopied = true; 99 | }; 100 | 101 | angular.element(element).on('hidden.bs.modal', function () { 102 | scope.$apply(function () { 103 | scope.options = null; 104 | scope.context.commandAPIUrl = null; 105 | scope.context.isCopied = false; 106 | }); 107 | }); 108 | 109 | scope.$watch('options', function (options) { 110 | if (options) { 111 | scope.generateCommandAPIUrl(); 112 | scope.context.isCopied = false; 113 | 114 | angular.element(element).modal('show'); 115 | } 116 | }, true); 117 | } 118 | }; 119 | }]); 120 | }()); 121 | -------------------------------------------------------------------------------- /src/scripts/directives/indeterminate.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngIndeterminate', function () { 5 | return { 6 | restrict: 'A', 7 | scope: { 8 | indeterminate: '=ngIndeterminate' 9 | }, 10 | link: function (scope, element) { 11 | scope.$watch('indeterminate', function () { 12 | element[0].indeterminate = (scope.indeterminate === 'true' || scope.indeterminate === true); 13 | }); 14 | } 15 | }; 16 | }); 17 | }()); 18 | -------------------------------------------------------------------------------- /src/scripts/directives/pieceBar.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngPieceBar', ['aria2TaskService', function (aria2TaskService) { 5 | return { 6 | restrict: 'E', 7 | template: '', 8 | replace: true, 9 | scope: { 10 | bitField: '=', 11 | pieceCount: '=', 12 | color: '@' 13 | }, 14 | link: function (scope, element) { 15 | var redraw = function () { 16 | var canvas = element[0]; 17 | var combinedPieces = aria2TaskService.getCombinedPieces(scope.bitField, scope.pieceCount); 18 | var context = canvas.getContext('2d'); 19 | context.fillStyle = scope.color || '#000'; 20 | context.clearRect(0, 0, canvas.width, canvas.height); 21 | 22 | var posX = 0; 23 | var width = canvas.width; 24 | var height = canvas.height; 25 | 26 | for (var i = 0; i < combinedPieces.length; i++) { 27 | var piece = combinedPieces[i]; 28 | var pieceWidth = piece.count / scope.pieceCount * width; 29 | 30 | if (piece.isCompleted) { 31 | context.fillRect(posX, 0, pieceWidth, height); 32 | } 33 | 34 | posX += pieceWidth; 35 | } 36 | }; 37 | 38 | scope.$watch('bitField', function () { 39 | redraw(); 40 | }); 41 | 42 | scope.$watch('pieceNumber', function () { 43 | redraw(); 44 | }); 45 | } 46 | }; 47 | }]); 48 | }()); 49 | -------------------------------------------------------------------------------- /src/scripts/directives/pieceMap.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngPieceMap', ['aria2TaskService', function (aria2TaskService) { 5 | return { 6 | restrict: 'E', 7 | template: '
', 8 | replace: true, 9 | scope: { 10 | bitField: '=', 11 | pieceCount: '=' 12 | }, 13 | link: function (scope, element) { 14 | var pieces = []; 15 | var currentPieceStatus = []; 16 | 17 | var redraw = function () { 18 | currentPieceStatus = aria2TaskService.getPieceStatus(scope.bitField, scope.pieceCount); 19 | pieces.length = 0; 20 | element.empty(); 21 | 22 | var pieceCount = Math.max(1, currentPieceStatus.length); 23 | 24 | for (var i = 0; i < pieceCount; i++) { 25 | var piece = angular.element('
'); 26 | pieces.push(piece); 27 | element.append(piece); 28 | } 29 | }; 30 | 31 | var refresh = function () { 32 | var newPieceStatus = aria2TaskService.getPieceStatus(scope.bitField, scope.pieceCount); 33 | 34 | if (!currentPieceStatus || !newPieceStatus || currentPieceStatus.length !== newPieceStatus.length || newPieceStatus.length !== pieces.length) { 35 | redraw(); 36 | return; 37 | } 38 | 39 | for (var i = 0; i < pieces.length; i++) { 40 | if (currentPieceStatus[i] !== newPieceStatus[i]) { 41 | if (newPieceStatus[i]) { 42 | pieces[i].addClass('piece-completed'); 43 | } else { 44 | pieces[i].removeClass('piece-completed'); 45 | } 46 | } 47 | } 48 | 49 | currentPieceStatus = newPieceStatus; 50 | }; 51 | 52 | scope.$watch('bitField', function () { 53 | refresh(); 54 | }); 55 | 56 | scope.$watch('pieceCount', function () { 57 | redraw(); 58 | }); 59 | } 60 | }; 61 | }]); 62 | }()); 63 | -------------------------------------------------------------------------------- /src/scripts/directives/placeholder.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngPlaceholder', function () { 5 | return { 6 | restrict: 'A', 7 | scope: { 8 | placeholder: '=ngPlaceholder' 9 | }, 10 | link: function (scope, element) { 11 | scope.$watch('placeholder', function () { 12 | element[0].placeholder = scope.placeholder; 13 | }); 14 | } 15 | }; 16 | }); 17 | }()); 18 | -------------------------------------------------------------------------------- /src/scripts/directives/settingDialog.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngSettingDialog', ['ariaNgCommonService', 'aria2SettingService', function (ariaNgCommonService, aria2SettingService) { 5 | return { 6 | restrict: 'E', 7 | templateUrl: 'views/setting-dialog.html', 8 | replace: true, 9 | scope: { 10 | setting: '=' 11 | }, 12 | link: function (scope, element, attrs) { 13 | scope.context = { 14 | isLoading: false, 15 | availableOptions: [], 16 | globalOptions: [] 17 | }; 18 | 19 | scope.setGlobalOption = function (key, value, optionStatus) { 20 | return aria2SettingService.setGlobalOption(key, value, function (response) { 21 | if (response.success && response.data === 'OK') { 22 | optionStatus.setSuccess(); 23 | } else { 24 | optionStatus.setFailed(response.data.message); 25 | } 26 | }, true); 27 | }; 28 | 29 | var loadOptions = function (type) { 30 | var keys = aria2SettingService.getAria2QuickSettingsAvailableOptions(type); 31 | 32 | if (!keys) { 33 | ariaNgCommonService.showError('Type is illegal!'); 34 | return; 35 | } 36 | 37 | scope.context.availableOptions = aria2SettingService.getSpecifiedOptions(keys); 38 | }; 39 | 40 | var loadAria2OptionsValue = function () { 41 | scope.context.isLoading = true; 42 | 43 | return aria2SettingService.getGlobalOption(function (response) { 44 | scope.context.isLoading = false; 45 | 46 | if (response.success) { 47 | scope.context.globalOptions = response.data; 48 | } 49 | }); 50 | }; 51 | 52 | angular.element(element).on('hidden.bs.modal', function () { 53 | scope.$apply(function () { 54 | scope.setting = null; 55 | scope.context.availableOptions = []; 56 | scope.context.globalOptions = []; 57 | }); 58 | }); 59 | 60 | scope.$watch('setting', function (setting) { 61 | if (setting) { 62 | loadOptions(setting.type); 63 | loadAria2OptionsValue(); 64 | 65 | angular.element(element).modal('show'); 66 | } 67 | }, true); 68 | } 69 | }; 70 | }]); 71 | }()); 72 | -------------------------------------------------------------------------------- /src/scripts/directives/tooltip.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngTooltip', function () { 5 | return { 6 | restrict: 'A', 7 | scope: { 8 | title: '@ngTooltip' 9 | }, 10 | link: function (scope, element, attrs) { 11 | var options = { 12 | ngTooltipIf: true, 13 | ngTooltipPlacement: 'top', 14 | ngTooltipContainer: null, 15 | ngTooltipTrigger: 'hover' 16 | }; 17 | 18 | angular.extend(options, attrs); 19 | 20 | var showTooltip = options.ngTooltipIf === true || options.ngTooltipIf === 'true'; 21 | 22 | var addTooltip = function () { 23 | angular.element(element).tooltip({ 24 | title: scope.title, 25 | placement: options.ngTooltipPlacement, 26 | container: options.ngTooltipContainer, 27 | trigger: options.ngTooltipTrigger, 28 | delay: { 29 | show: 100, 30 | hide: 0 31 | } 32 | }); 33 | }; 34 | 35 | var refreshTooltip = function () { 36 | angular.element(element).attr('title', scope.title).tooltip('fixTitle'); 37 | }; 38 | 39 | var removeTooltip = function () { 40 | angular.element(element).tooltip('destroy'); 41 | }; 42 | 43 | if (showTooltip) { 44 | addTooltip(); 45 | } 46 | 47 | scope.$watch('title', function () { 48 | if (showTooltip) { 49 | refreshTooltip(); 50 | } 51 | }); 52 | 53 | scope.$watch('ngTooltipIf', function (value) { 54 | if (angular.isUndefined(value)) { 55 | return; 56 | } 57 | 58 | value = (value === true || value === 'true'); 59 | 60 | if (showTooltip === value) { 61 | return; 62 | } 63 | 64 | if (value) { 65 | addTooltip(); 66 | } else { 67 | removeTooltip(); 68 | } 69 | 70 | showTooltip = value; 71 | }); 72 | } 73 | }; 74 | }); 75 | }()); 76 | -------------------------------------------------------------------------------- /src/scripts/directives/validUrls.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').directive('ngValidUrls', ['ariaNgCommonService', function (ariaNgCommonService) { 5 | var DIRECTIVE_ID = 'invalidUrls'; 6 | 7 | return { 8 | restrict: 'A', 9 | require: '?ngModel', 10 | link: function (scope, element, attrs, ngModel) { 11 | var handleChange = function (value) { 12 | if (angular.isUndefined(value) || value === '') { 13 | return; 14 | } 15 | 16 | var urls = ariaNgCommonService.parseUrlsFromOriginInput(value); 17 | var valid = urls && urls.length > 0; 18 | 19 | ngModel.$setValidity(DIRECTIVE_ID, valid); 20 | }; 21 | 22 | scope.$watch(function () { 23 | return ngModel.$viewValue; 24 | }, handleChange); 25 | } 26 | }; 27 | }]); 28 | }()); 29 | -------------------------------------------------------------------------------- /src/scripts/filters/dateDuration.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('dateDuration', ['moment', function (moment) { 5 | return function (duration, sourceUnit, format) { 6 | var timespan = moment.duration(duration, sourceUnit); 7 | var time = moment.utc(timespan.asMilliseconds()); 8 | return time.format(format); 9 | }; 10 | }]); 11 | }()); 12 | -------------------------------------------------------------------------------- /src/scripts/filters/fileOrderBy.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('fileOrderBy', ['$filter', 'ariaNgCommonService', function ($filter, ariaNgCommonService) { 5 | return function (array, type) { 6 | if (!angular.isArray(array) || !type) { 7 | return array; 8 | } 9 | 10 | var orderType = ariaNgCommonService.parseOrderType(type); 11 | 12 | if (orderType === null) { 13 | return array; 14 | } 15 | 16 | if (orderType.type === 'index') { 17 | return $filter('orderBy')(array, ['index'], orderType.reverse); 18 | } else if (orderType.type === 'name') { 19 | return $filter('orderBy')(array, ['fileName'], orderType.reverse); 20 | } else if (orderType.type === 'size') { 21 | return $filter('orderBy')(array, ['length'], orderType.reverse); 22 | } else if (orderType.type === 'percent') { 23 | return $filter('orderBy')(array, ['completePercent'], orderType.reverse); 24 | } else if (orderType.type === 'selected') { 25 | return $filter('orderBy')(array, ['selected'], orderType.reverse); 26 | } else { 27 | return array; 28 | } 29 | }; 30 | }]); 31 | }()); 32 | -------------------------------------------------------------------------------- /src/scripts/filters/logOrderBy.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('logOrderBy', ['$filter', 'ariaNgCommonService', function ($filter, ariaNgCommonService) { 5 | return function (array, type) { 6 | if (!angular.isArray(array) || !type) { 7 | return array; 8 | } 9 | 10 | var orderType = ariaNgCommonService.parseOrderType(type); 11 | 12 | if (orderType === null) { 13 | return array; 14 | } 15 | 16 | if (orderType.type === 'time') { 17 | return $filter('orderBy')(array, ['time'], orderType.reverse); 18 | } else { 19 | return array; 20 | } 21 | }; 22 | }]); 23 | }()); 24 | -------------------------------------------------------------------------------- /src/scripts/filters/longDate.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('longDate', ['ariaNgCommonService', 'ariaNgLocalizationService', function (ariaNgCommonService, ariaNgLocalizationService) { 5 | return function (time) { 6 | var format = ariaNgLocalizationService.getLongDateFormat(); 7 | return ariaNgCommonService.formatDateTime(time, format); 8 | }; 9 | }]); 10 | }()); 11 | -------------------------------------------------------------------------------- /src/scripts/filters/peerOrderBy.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('peerOrderBy', ['$filter', 'ariaNgCommonService', function ($filter, ariaNgCommonService) { 5 | return function (array, type) { 6 | if (!angular.isArray(array)) { 7 | return array; 8 | } 9 | 10 | var orderType = ariaNgCommonService.parseOrderType(type); 11 | 12 | if (orderType === null) { 13 | return array; 14 | } 15 | 16 | if (orderType.type === 'address') { 17 | return $filter('orderBy')(array, ['ip', 'port'], orderType.reverse); 18 | } else if (orderType.type === 'client') { 19 | return $filter('orderBy')(array, ['client.name', 'client.version'], orderType.reverse); 20 | } else if (orderType.type === 'percent') { 21 | return $filter('orderBy')(array, ['completePercent'], orderType.reverse); 22 | } else if (orderType.type === 'dspeed') { 23 | return $filter('orderBy')(array, ['downloadSpeed'], orderType.reverse); 24 | } else if (orderType.type === 'uspeed') { 25 | return $filter('orderBy')(array, ['uploadSpeed'], orderType.reverse); 26 | } else { 27 | return array; 28 | } 29 | }; 30 | }]); 31 | }()); 32 | -------------------------------------------------------------------------------- /src/scripts/filters/percent.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('percent', ['$filter', function ($filter) { 5 | return function (value, precision) { 6 | var ratio = Math.pow(10, precision); 7 | var result = parseInt(value * ratio) / ratio; 8 | 9 | return $filter('number')(result, precision); 10 | }; 11 | }]); 12 | }()); 13 | -------------------------------------------------------------------------------- /src/scripts/filters/reverse.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('reverse', function () { 5 | return function(array) { 6 | if (!array) { 7 | return array; 8 | } 9 | 10 | return array.slice().reverse(); 11 | }; 12 | }); 13 | }()); 14 | -------------------------------------------------------------------------------- /src/scripts/filters/taskOrderBy.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('taskOrderBy', ['$filter', 'ariaNgCommonService', function ($filter, ariaNgCommonService) { 5 | return function (array, type) { 6 | if (!angular.isArray(array)) { 7 | return array; 8 | } 9 | 10 | var orderType = ariaNgCommonService.parseOrderType(type); 11 | 12 | if (orderType === null) { 13 | return array; 14 | } 15 | 16 | if (orderType.type === 'name') { 17 | return $filter('orderBy')(array, ['taskName'], orderType.reverse); 18 | } else if (orderType.type === 'size') { 19 | return $filter('orderBy')(array, ['totalLength'], orderType.reverse); 20 | } else if (orderType.type === 'percent') { 21 | return $filter('orderBy')(array, ['completePercent'], orderType.reverse); 22 | } else if (orderType.type === 'remain') { 23 | return $filter('orderBy')(array, ['idle', 'remainTime', 'remainLength'], orderType.reverse); 24 | } else if (orderType.type === 'dspeed') { 25 | return $filter('orderBy')(array, ['downloadSpeed'], orderType.reverse); 26 | } else if (orderType.type === 'uspeed') { 27 | return $filter('orderBy')(array, ['uploadSpeed'], orderType.reverse); 28 | } else { 29 | return array; 30 | } 31 | }; 32 | }]); 33 | }()); 34 | -------------------------------------------------------------------------------- /src/scripts/filters/taskStatus.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('taskStatus', function () { 5 | return function (task, simplify) { 6 | if (!task) { 7 | return ''; 8 | } 9 | 10 | if (task.status === 'active') { 11 | if (task.verifyIntegrityPending) { 12 | return 'Pending Verification'; 13 | } else if (task.verifiedLength) { 14 | return (task.verifiedPercent ? 'format.task.verifying-percent' : 'Verifying'); 15 | } else if (task.seeder === true || task.seeder === 'true') { 16 | return 'Seeding'; 17 | } else { 18 | return 'Downloading'; 19 | } 20 | } else if (task.status === 'waiting') { 21 | return 'Waiting'; 22 | } else if (task.status === 'paused') { 23 | return 'Paused'; 24 | } else if (!simplify && task.status === 'complete') { 25 | return 'Completed'; 26 | } else if (!simplify && task.status === 'error') { 27 | return (task.errorCode ? 'format.task.error-occurred' : 'Error Occurred'); 28 | } else if (!simplify && task.status === 'removed') { 29 | return 'Removed'; 30 | } else { 31 | return ''; 32 | } 33 | }; 34 | }); 35 | }()); 36 | -------------------------------------------------------------------------------- /src/scripts/filters/timeDisplayName.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('timeDisplayName', ['ariaNgCommonService', 'ariaNgLocalizationService', function (ariaNgCommonService, ariaNgLocalizationService) { 5 | return function (time, defaultName) { 6 | if (!time) { 7 | return ariaNgLocalizationService.getLocalizedText(defaultName); 8 | } 9 | 10 | var option = ariaNgCommonService.getTimeOption(time); 11 | 12 | return ariaNgLocalizationService.getLocalizedText(option.name, { 13 | value: option.value 14 | }); 15 | }; 16 | }]); 17 | }()); 18 | -------------------------------------------------------------------------------- /src/scripts/filters/volume.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').filter('readableVolume', ['$filter', function ($filter) { 5 | var units = [ 'B', 'KB', 'MB', 'GB' ]; 6 | var defaultFractionSize = 2; 7 | 8 | var getAutoFractionSize = function (value) { 9 | if (value < 1) { 10 | return 2; 11 | } else if (value < 10) { 12 | return 1; 13 | } else { 14 | return 0; 15 | } 16 | }; 17 | 18 | return function (value, fractionSize) { 19 | var unit = units[0]; 20 | var actualFractionSize = defaultFractionSize; 21 | var autoFractionSize = false; 22 | 23 | if (angular.isNumber(fractionSize)) { 24 | actualFractionSize = fractionSize; 25 | } else if (fractionSize === 'auto') { 26 | autoFractionSize = true; 27 | } 28 | 29 | if (!value) { 30 | value = 0; 31 | } 32 | 33 | if (!angular.isNumber(value)) { 34 | value = parseInt(value); 35 | } 36 | 37 | for (var i = 1; i < units.length; i++) { 38 | if (value >= 1024) { 39 | value = value / 1024; 40 | unit = units[i]; 41 | } else { 42 | break; 43 | } 44 | } 45 | 46 | if (autoFractionSize) { 47 | actualFractionSize = getAutoFractionSize(value); 48 | } 49 | 50 | value = $filter('number')(value, actualFractionSize); 51 | 52 | return value + ' ' + unit; 53 | }; 54 | }]); 55 | }()); 56 | -------------------------------------------------------------------------------- /src/scripts/services/aria2HttpRpcService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('aria2HttpRpcService', ['$http', 'ariaNgConstants', 'ariaNgCommonService', 'ariaNgSettingService', 'ariaNgLogService', function ($http, ariaNgConstants, ariaNgCommonService, ariaNgSettingService, ariaNgLogService) { 5 | var rpcUrl = ariaNgSettingService.getCurrentRpcUrl(); 6 | var method = ariaNgSettingService.getCurrentRpcHttpMethod(); 7 | var requestHeaders = ariaNgSettingService.getCurrentRpcRequestHeaders(); 8 | 9 | var getUrlWithQueryString = function (url, parameters) { 10 | if (!url || url.length < 1) { 11 | return url; 12 | } 13 | 14 | var queryString = ''; 15 | 16 | for (var key in parameters) { 17 | if (!parameters.hasOwnProperty(key)) { 18 | continue; 19 | } 20 | 21 | var value = parameters[key]; 22 | 23 | if (value === null || angular.isUndefined(value)) { 24 | continue; 25 | } 26 | 27 | if (queryString.length > 0) { 28 | queryString += '&'; 29 | } 30 | 31 | if (angular.isObject(value) || angular.isArray(value)) { 32 | value = angular.toJson(value); 33 | value = ariaNgCommonService.base64Encode(value); 34 | value = encodeURIComponent(value); 35 | } 36 | 37 | queryString += key + '=' + value; 38 | } 39 | 40 | if (queryString.length < 1) { 41 | return url; 42 | } 43 | 44 | if (url.indexOf('?') < 0) { 45 | queryString = '?' + queryString; 46 | } else { 47 | queryString = '&' + queryString; 48 | } 49 | 50 | return url + queryString; 51 | }; 52 | 53 | return { 54 | request: function (context) { 55 | if (!context) { 56 | return; 57 | } 58 | 59 | var requestContext = { 60 | url: rpcUrl, 61 | method: method, 62 | headers: {}, 63 | timeout: ariaNgConstants.httpRequestTimeout 64 | }; 65 | 66 | if (requestContext.method === 'POST') { 67 | requestContext.data = angular.toJson(context.requestBody); 68 | requestContext.headers['Content-Type'] = 'application/json'; 69 | } else if (requestContext.method === 'GET') { 70 | requestContext.url = getUrlWithQueryString(requestContext.url, context.requestBody); 71 | } 72 | 73 | if (requestHeaders) { 74 | var lines = requestHeaders.split('\n'); 75 | 76 | for (var i = 0; i < lines.length; i++) { 77 | var items = lines[i].split(':'); 78 | 79 | if (items.length !== 2) { 80 | continue; 81 | } 82 | 83 | var name = items[0].trim(); 84 | var value = items[1].trim(); 85 | 86 | requestContext.headers[name] = value; 87 | } 88 | } 89 | 90 | ariaNgLogService.debug('[aria2HttpRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'request start', requestContext); 91 | 92 | return $http(requestContext).then(function onSuccess(response) { 93 | var data = response.data; 94 | 95 | ariaNgLogService.debug('[aria2HttpRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'response success', response); 96 | 97 | if (!data) { 98 | return; 99 | } 100 | 101 | if (context.connectionSuccessCallback) { 102 | context.connectionSuccessCallback({ 103 | rpcUrl: rpcUrl, 104 | method: method 105 | }); 106 | } 107 | 108 | if (context.successCallback) { 109 | context.successCallback(data.id, data.result); 110 | } 111 | }).catch(function onError(response) { 112 | var data = response.data; 113 | 114 | ariaNgLogService.debug('[aria2HttpRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'response error', response); 115 | 116 | if (!data) { 117 | data = { 118 | id: '-1', 119 | error: { 120 | message: 'Cannot connect to aria2!' 121 | } 122 | }; 123 | 124 | if (context.connectionFailedCallback) { 125 | context.connectionFailedCallback({ 126 | rpcUrl: rpcUrl, 127 | method: method 128 | }); 129 | } 130 | } 131 | 132 | if (context.errorCallback) { 133 | context.errorCallback(data.id, data.error); 134 | } 135 | }); 136 | }, 137 | reconnect: function () { 138 | //Not implement 139 | }, 140 | on: function (eventName, callback) { 141 | //Not implement 142 | } 143 | }; 144 | }]); 145 | }()); 146 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgAssetsCacheService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').provider('ariaNgAssetsCacheService', [function () { 5 | var assetsRoot = {}; 6 | var languageAssetsPrefix = 'languages.'; 7 | 8 | var getAsset = function (path) { 9 | var parts = path.split('.'), 10 | result = assetsRoot; 11 | 12 | for (var i = 0; i < parts.length; i++) { 13 | if (angular.isUndefined(result[parts[i]])) { 14 | return null; 15 | } 16 | 17 | result = result[parts[i]]; 18 | } 19 | 20 | return result; 21 | }; 22 | 23 | var setAsset = function (path, value) { 24 | var parts = path.split('.'), 25 | result = assetsRoot; 26 | 27 | for (var i = 0; i < parts.length - 1; i++) { 28 | if (angular.isUndefined(result[parts[i]])) { 29 | result[parts[i]] = {}; 30 | } 31 | 32 | result = result[parts[i]]; 33 | } 34 | 35 | result[parts[parts.length - 1]] = value; 36 | }; 37 | 38 | this.getLanguageAsset = function (languageName) { 39 | return getAsset(languageAssetsPrefix + languageName); 40 | }; 41 | 42 | this.setLanguageAsset = function (languageName, languageContent) { 43 | setAsset(languageAssetsPrefix + languageName, languageContent); 44 | }; 45 | 46 | this.$get = function () { 47 | var that = this; 48 | 49 | return { 50 | getLanguageAsset: function (languageName) { 51 | return that.getLanguageAsset(languageName); 52 | } 53 | } 54 | }; 55 | }]); 56 | }()); 57 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgFileService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgFileService', ['$window', function ($window) { 5 | var isSupportFileReader = !!$window.FileReader; 6 | var isSupportBlob = !!$window.Blob; 7 | 8 | var getAllowedExtensions = function (fileFilter) { 9 | var extensions = []; 10 | 11 | if (!fileFilter || fileFilter.length < 1) { 12 | extensions.push(/.+$/); 13 | return extensions; 14 | } 15 | 16 | var fileFilters = fileFilter.split(','); 17 | 18 | for (var i = 0; i < fileFilters.length; i++) { 19 | var extension = fileFilters[i]; 20 | 21 | if (extension === '*.*') { 22 | extensions.push(/.+$/); 23 | continue; 24 | } 25 | 26 | extension = extension.replace('.', '\\.'); 27 | extension = extension + '$'; 28 | 29 | extensions.push(new RegExp(extension)); 30 | } 31 | 32 | return extensions; 33 | }; 34 | 35 | var checkFileExtension = function (fileName, extensions) { 36 | if (!extensions || extensions.length < 1) { 37 | return true; 38 | } 39 | 40 | for (var i = 0; i < extensions.length; i++) { 41 | if (extensions[i].test(fileName)) { 42 | return true; 43 | } 44 | } 45 | 46 | return false; 47 | }; 48 | 49 | return { 50 | isSupportFileReader: function () { 51 | return isSupportFileReader; 52 | }, 53 | isSupportBlob: function () { 54 | return isSupportBlob; 55 | }, 56 | openFileContent: function (options, successCallback, errorCallback, element) { 57 | if (!isSupportFileReader) { 58 | if (errorCallback) { 59 | errorCallback('Your browser does not support loading file!'); 60 | } 61 | 62 | return; 63 | } 64 | 65 | options = angular.extend({ 66 | scope: null, 67 | fileFilter: null, 68 | fileType: 'binary', // or 'text' 69 | successCallback: successCallback, 70 | errorCallback: errorCallback 71 | }, options); 72 | 73 | if (!element || !element.change) { 74 | element = angular.element(''); 75 | } 76 | 77 | element.data('options', options); 78 | 79 | if (options.fileFilter) { 80 | element.attr('accept', options.fileFilter); 81 | } 82 | 83 | element.val(''); 84 | 85 | if (element.attr('data-ariang-file-initialized') !== 'true') { 86 | element.change(function () { 87 | if (!this.files || this.files.length < 1) { 88 | return; 89 | } 90 | 91 | var thisOptions = element.data('options'); 92 | var allowedExtensions = getAllowedExtensions(thisOptions.fileFilter); 93 | var file = this.files[0]; 94 | var fileName = file.name; 95 | 96 | if (!checkFileExtension(fileName, allowedExtensions)) { 97 | if (thisOptions.errorCallback) { 98 | if (thisOptions.scope) { 99 | thisOptions.scope.$apply(function () { 100 | thisOptions.errorCallback('The selected file type is invalid!'); 101 | }); 102 | } else { 103 | thisOptions.errorCallback('The selected file type is invalid!'); 104 | } 105 | } 106 | 107 | return; 108 | } 109 | 110 | var reader = new FileReader(); 111 | 112 | reader.onload = function () { 113 | var result = { 114 | fileName: fileName 115 | }; 116 | 117 | switch (thisOptions.fileType) { 118 | case 'text': 119 | result.content = this.result; 120 | break; 121 | case 'binary': 122 | default: 123 | result.base64Content = this.result.replace(/.*?base64,/, ''); 124 | break; 125 | } 126 | 127 | if (thisOptions.successCallback) { 128 | if (thisOptions.scope) { 129 | thisOptions.scope.$apply(function () { 130 | thisOptions.successCallback(result); 131 | }); 132 | } else { 133 | thisOptions.successCallback(result); 134 | } 135 | } 136 | }; 137 | 138 | reader.onerror = function () { 139 | if (thisOptions.errorCallback) { 140 | if (thisOptions.scope) { 141 | thisOptions.scope.$apply(function () { 142 | thisOptions.errorCallback('Failed to load file!'); 143 | }); 144 | } else { 145 | thisOptions.errorCallback('Failed to load file!'); 146 | } 147 | } 148 | }; 149 | 150 | switch (thisOptions.fileType) { 151 | case 'text': 152 | reader.readAsText(file); 153 | break; 154 | case 'binary': 155 | default: 156 | reader.readAsDataURL(file); 157 | break; 158 | } 159 | }).attr('data-ariang-file-initialized', 'true'); 160 | } 161 | 162 | element.trigger('click'); 163 | }, 164 | saveFileContent: function (content, element, options) { 165 | if (!isSupportBlob) { 166 | return; 167 | } 168 | 169 | options = angular.extend({ 170 | fileName: null, 171 | contentType: 'application/octet-stream', 172 | autoTrigger: false, 173 | autoRevoke: false 174 | }, options); 175 | 176 | var blob = new Blob([content], { type: options.contentType }); 177 | var objectUrl = URL.createObjectURL(blob); 178 | 179 | if (!element) { 180 | element = angular.element(''); 181 | } 182 | 183 | element.attr('href', objectUrl); 184 | 185 | if (options.fileName) { 186 | element.attr('download', options.fileName); 187 | } 188 | 189 | if (options.autoTrigger) { 190 | element.trigger('click'); 191 | } 192 | 193 | if (options.autoRevoke) { 194 | URL.revokeObjectURL(objectUrl); 195 | } 196 | } 197 | }; 198 | }]); 199 | }()); 200 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgKeyboardService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgKeyboardService', ['$window', function ($window) { 5 | var platform = ''; 6 | 7 | if ($window.navigator && $window.navigator.userAgentData && $window.navigator.userAgentData.platform) { 8 | platform = $window.navigator.userAgentData.platform; 9 | } else if ($window.navigator && $window.navigator.platform) { 10 | platform = $window.navigator.platform; 11 | } 12 | 13 | var isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(platform); 14 | 15 | var isModifierKeyPressed = function (event) { 16 | if (isMacLike) { 17 | return event.metaKey; 18 | } else { 19 | return event.ctrlKey; 20 | } 21 | }; 22 | 23 | var getKeyCode = function (event) { 24 | return event.keyCode || event.which || event.charCode; 25 | }; 26 | 27 | return { 28 | isMacKeyboardLike: function () { 29 | return isMacLike; 30 | }, 31 | isCtrlAPressed: function (event) { 32 | return (isModifierKeyPressed(event) && (event.code === 'KeyA' || getKeyCode(event) === 65)); // Ctrl+A / Command+A 33 | }, 34 | isCtrlFPressed: function (event) { 35 | return (isModifierKeyPressed(event) && (event.code === 'KeyF' || getKeyCode(event) === 70)); // Ctrl+F / Command+F 36 | }, 37 | isCtrlEnterPressed: function (event) { 38 | return (isModifierKeyPressed(event) && (event.code === 'Enter' || getKeyCode(event) === 13)); // Ctrl+Enter / Command+Return 39 | }, 40 | isBackspacePressed: function (event) { 41 | return (event.code === 'Backspace' || getKeyCode(event) === 8); // Backspace 42 | }, 43 | isDeletePressed: function (event) { 44 | return (event.code === 'Delete' || getKeyCode(event) === 46); // Delete 45 | } 46 | }; 47 | }]); 48 | }()); 49 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgLanguageLoader.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgLanguageLoader', ['$http', '$q', 'ariaNgConstants', 'ariaNgLanguages', 'ariaNgAssetsCacheService', 'ariaNgNotificationService', 'ariaNgLogService', 'ariaNgStorageService', function ($http, $q, ariaNgConstants, ariaNgLanguages, ariaNgAssetsCacheService, ariaNgNotificationService, ariaNgLogService, ariaNgStorageService) { 5 | var getKeyValuePair = function (line) { 6 | for (var i = 0; i < line.length; i++) { 7 | if (i > 0 && line.charAt(i - 1) !== '\\' && line.charAt(i) === '=') { 8 | return { 9 | key: line.substring(0, i).replace('\\=', '='), 10 | value: line.substring(i + 1, line.length).replace('\\=', '=') 11 | }; 12 | } 13 | } 14 | 15 | return { 16 | value: line 17 | }; 18 | }; 19 | 20 | var getCategory = function (langObj, category) { 21 | var currentCategory = langObj; 22 | 23 | if (!category) { 24 | return currentCategory; 25 | } 26 | 27 | if (category[0] === '[' && category[category.length - 1] === ']') { 28 | category = category.substring(1, category.length - 1); 29 | } 30 | 31 | if (category === 'global') { 32 | return currentCategory; 33 | } 34 | 35 | var categoryNames = category.split('.'); 36 | 37 | for (var i = 0; i < categoryNames.length; i++) { 38 | var categoryName = categoryNames[i]; 39 | 40 | if (!currentCategory[categoryName]) { 41 | currentCategory[categoryName] = {}; 42 | } 43 | 44 | currentCategory = currentCategory[categoryName]; 45 | } 46 | 47 | return currentCategory; 48 | }; 49 | 50 | var getLanguageObject = function (languageContent) { 51 | var langObj = {}; 52 | 53 | if (!languageContent) { 54 | return langObj; 55 | } 56 | 57 | var lines = languageContent.split('\n'); 58 | var currentCatagory = langObj; 59 | 60 | for (var i = 0; i < lines.length; i++) { 61 | var line = lines[i]; 62 | 63 | if (!line) { 64 | continue; 65 | } 66 | 67 | line = line.replace('\r', ''); 68 | 69 | if (/^\[.+\]$/.test(line)) { 70 | currentCatagory = getCategory(langObj, line); 71 | continue; 72 | } 73 | 74 | var pair = getKeyValuePair(line); 75 | 76 | if (pair && pair.key && pair.value && pair.value !== '') { 77 | currentCatagory[pair.key] = pair.value; 78 | } 79 | } 80 | 81 | return langObj; 82 | }; 83 | 84 | var isLanguageResourceEquals = function (langObj1, langObj2) { 85 | if (!angular.isObject(langObj1) || !angular.isObject(langObj2)) { 86 | return false; 87 | } 88 | 89 | for (var key in langObj2) { 90 | if (!langObj2.hasOwnProperty(key)) { 91 | continue; 92 | } 93 | 94 | var value = langObj2[key]; 95 | 96 | if (angular.isObject(value)) { 97 | var result = isLanguageResourceEquals(langObj1[key], value); 98 | if (!result) { 99 | return false; 100 | } 101 | } else { 102 | if (value !== langObj1[key]) { 103 | return false; 104 | } 105 | } 106 | } 107 | 108 | return true; 109 | }; 110 | 111 | return function (options) { 112 | var deferred = $q.defer(); 113 | 114 | if (!ariaNgLanguages[options.key]) { 115 | deferred.reject(options.key); 116 | return deferred.promise; 117 | } 118 | 119 | var languageKey = ariaNgConstants.languageStorageKeyPrefix + '.' + options.key; 120 | var languageResource = ariaNgStorageService.get(languageKey); 121 | 122 | if (languageResource) { 123 | deferred.resolve(languageResource); 124 | } 125 | 126 | if (ariaNgAssetsCacheService.getLanguageAsset(options.key)) { 127 | var languageObject = getLanguageObject(ariaNgAssetsCacheService.getLanguageAsset(options.key)); 128 | ariaNgStorageService.set(languageKey, languageObject); 129 | deferred.resolve(languageObject); 130 | 131 | return deferred.promise; 132 | } 133 | 134 | var languagePath = ariaNgConstants.languagePath + '/' + options.key + ariaNgConstants.languageFileExtension; 135 | 136 | $http({ 137 | url: languagePath, 138 | method: 'GET' 139 | }).then(function onSuccess(response) { 140 | var languageObject = getLanguageObject(response.data); 141 | var languageUpdated = false; 142 | 143 | if (languageResource) { 144 | languageUpdated = !isLanguageResourceEquals(languageResource, languageObject); 145 | } 146 | 147 | ariaNgStorageService.set(languageKey, languageObject); 148 | 149 | if (languageUpdated) { 150 | ariaNgLogService.info('[ariaNgLanguageLoader] load language resource successfully, and resource is updated'); 151 | ariaNgNotificationService.notifyInPage('', 'Language resource has been updated, please reload the page for the changes to take effect.', { 152 | delay: false, 153 | type: 'info', 154 | templateUrl: 'views/notification-reloadable.html' 155 | }); 156 | } else { 157 | ariaNgLogService.info('[ariaNgLanguageLoader] load language resource successfully, but resource is not updated'); 158 | } 159 | 160 | return deferred.resolve(languageObject); 161 | }).catch(function onError(response) { 162 | ariaNgLogService.warn('[ariaNgLanguageLoader] cannot get language resource'); 163 | if (!languageResource) { 164 | ariaNgNotificationService.notifyInPage('', 'AriaNg cannot get language resources, and will display in default language.', { 165 | type: 'error', 166 | delay: false 167 | }); 168 | } 169 | return deferred.reject(options.key); 170 | }); 171 | 172 | return deferred.promise; 173 | }; 174 | }]); 175 | }()); 176 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgLocalizationService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgLocalizationService', ['$translate', 'amMoment', function ($translate, amMoment) { 5 | return { 6 | applyLanguage: function (lang) { 7 | $translate.use(lang); 8 | amMoment.changeLocale(lang); 9 | 10 | return true; 11 | }, 12 | getLocalizedText: function (text, params) { 13 | return $translate.instant(text, params); 14 | }, 15 | getLongDateFormat: function () { 16 | return this.getLocalizedText('format.longdate'); 17 | } 18 | }; 19 | }]); 20 | }()); 21 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgLogService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgLogService', ['$log', 'ariaNgConstants', function ($log, ariaNgConstants) { 5 | var logLevels = { 6 | DEBUG: 1, 7 | INFO: 2, 8 | WARN: 3, 9 | ERROR: 4 10 | }; 11 | var logIndex = 0; 12 | var enableDebugLog = false; 13 | var cachedDebugLogs = []; 14 | 15 | var createNewCacheLogItem = function (msg, level, obj) { 16 | return { 17 | id: ++logIndex, 18 | time: new Date(), 19 | level: level, 20 | content: msg, 21 | attachment: obj 22 | }; 23 | }; 24 | 25 | var pushLogToCache = function (msg, level, obj) { 26 | if (!enableDebugLog) { 27 | return; 28 | } 29 | 30 | if (cachedDebugLogs.length >= ariaNgConstants.cachedDebugLogsLimit) { 31 | cachedDebugLogs.shift(); 32 | } 33 | 34 | cachedDebugLogs.push(createNewCacheLogItem(msg, level, obj)); 35 | }; 36 | 37 | return { 38 | setEnableDebugLog: function (value) { 39 | enableDebugLog = value; 40 | }, 41 | compareLogLevel: function (level1, level2) { 42 | var level1Val = logLevels[level1]; 43 | var level2Val = logLevels[level2]; 44 | 45 | if (!level1Val) { 46 | level1Val = 0; 47 | } 48 | 49 | if (!level2Val) { 50 | level2Val = 0; 51 | } 52 | 53 | if (level1Val > level2Val) { 54 | return 1; 55 | } else if (level1Val < level2Val) { 56 | return -1; 57 | } else { 58 | return 0; 59 | } 60 | }, 61 | debug: function (msg, obj) { 62 | if (enableDebugLog) { 63 | if (obj) { 64 | $log.debug('[AriaNg Debug]' + msg, obj); 65 | } else { 66 | $log.debug('[AriaNg Debug]' + msg); 67 | } 68 | 69 | pushLogToCache(msg, 'DEBUG', obj); 70 | } 71 | }, 72 | info: function (msg, obj) { 73 | if (obj) { 74 | $log.info('[AriaNg Info]' + msg, obj); 75 | } else { 76 | $log.info('[AriaNg Info]' + msg); 77 | } 78 | 79 | pushLogToCache(msg, 'INFO', obj); 80 | }, 81 | warn: function (msg, obj) { 82 | if (obj) { 83 | $log.warn('[AriaNg Warn]' + msg, obj); 84 | } else { 85 | $log.warn('[AriaNg Warn]' + msg); 86 | } 87 | 88 | pushLogToCache(msg, 'WARN', obj); 89 | }, 90 | error: function (msg, obj) { 91 | if (obj) { 92 | $log.error('[AriaNg Error]' + msg, obj); 93 | } else { 94 | $log.error('[AriaNg Error]' + msg); 95 | } 96 | 97 | pushLogToCache(msg, 'ERROR', obj); 98 | }, 99 | getDebugLogs: function () { 100 | if (enableDebugLog) { 101 | return cachedDebugLogs; 102 | } else { 103 | return []; 104 | } 105 | }, 106 | clearDebugLogs: function () { 107 | logIndex = 0; 108 | cachedDebugLogs.length = 0; 109 | } 110 | }; 111 | }]); 112 | }()); 113 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgMonitorService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgMonitorService', ['$filter', 'ariaNgConstants', 'ariaNgCommonService', 'ariaNgLocalizationService', function ($filter, ariaNgConstants, ariaNgCommonService, ariaNgLocalizationService) { 5 | var currentGlobalStat = {}; 6 | var storagesInMemory = {}; 7 | var globalStorageKey = 'global'; 8 | 9 | var getStorageCapacity = function (key) { 10 | if (key === globalStorageKey) { 11 | return ariaNgConstants.globalStatStorageCapacity; 12 | } else { 13 | return ariaNgConstants.taskStatStorageCapacity; 14 | } 15 | }; 16 | 17 | var initStorage = function (key) { 18 | var data = { 19 | legend: { 20 | show: false 21 | }, 22 | grid: { 23 | x: 50, 24 | y: 10, 25 | x2: 10, 26 | y2: 10 27 | }, 28 | tooltip: { 29 | show: true, 30 | formatter: function (params) { 31 | if (params[0].name === '') { 32 | return '
' + ariaNgLocalizationService.getLocalizedText('No Data') + '
'; 33 | } 34 | 35 | var time = ariaNgCommonService.getLongTimeFromUnixTime(params[0].name); 36 | var uploadSpeed = $filter('readableVolume')(params[0].value) + '/s'; 37 | var downloadSpeed = $filter('readableVolume')(params[1].value) + '/s'; 38 | 39 | return '
' + time + '
' 40 | + '
' + downloadSpeed +'
' 41 | + '
' + uploadSpeed + '
'; 42 | } 43 | }, 44 | xAxis: { 45 | data: [], 46 | type: 'category', 47 | boundaryGap: false, 48 | axisLabel: { 49 | show: false 50 | } 51 | }, 52 | yAxis: { 53 | type: 'value', 54 | axisLabel: { 55 | formatter: function (value) { 56 | return $filter('readableVolume')(value, 'auto'); 57 | } 58 | } 59 | }, 60 | series: [{ 61 | type: 'line', 62 | areaStyle: { 63 | normal: { 64 | opacity: 0.1 65 | } 66 | }, 67 | smooth: true, 68 | symbolSize: 6, 69 | showAllSymbol: false, 70 | data: [] 71 | }, { 72 | type: 'line', 73 | areaStyle: { 74 | normal: { 75 | opacity: 0.1 76 | } 77 | }, 78 | smooth: true, 79 | symbolSize: 6, 80 | showAllSymbol: false, 81 | data: [] 82 | }] 83 | }; 84 | 85 | var timeData = data.xAxis.data; 86 | var uploadData = data.series[0].data; 87 | var downloadData = data.series[1].data; 88 | 89 | for (var i = 0; i < getStorageCapacity(key); i++) { 90 | timeData.push(''); 91 | uploadData.push(''); 92 | downloadData.push(''); 93 | } 94 | 95 | storagesInMemory[key] = data; 96 | 97 | return data; 98 | }; 99 | 100 | var isStorageExist = function (key) { 101 | return angular.isDefined(storagesInMemory[key]); 102 | }; 103 | 104 | var pushToStorage = function (key, stat) { 105 | var storage = storagesInMemory[key]; 106 | var timeData = storage.xAxis.data; 107 | var uploadData = storage.series[0].data; 108 | var downloadData = storage.series[1].data; 109 | 110 | if (timeData.length >= getStorageCapacity(key)) { 111 | timeData.shift(); 112 | uploadData.shift(); 113 | downloadData.shift(); 114 | } 115 | 116 | timeData.push(stat.time); 117 | uploadData.push(stat.uploadSpeed); 118 | downloadData.push(stat.downloadSpeed); 119 | }; 120 | 121 | var getStorage = function (key) { 122 | return storagesInMemory[key]; 123 | }; 124 | 125 | var removeStorage = function (key) { 126 | delete storagesInMemory[key]; 127 | }; 128 | 129 | return { 130 | recordStat: function (key, stat) { 131 | if (!isStorageExist(key)) { 132 | initStorage(key); 133 | } 134 | 135 | stat.time = ariaNgCommonService.getCurrentUnixTime(); 136 | pushToStorage(key, stat); 137 | }, 138 | getStatsData: function (key) { 139 | if (!isStorageExist(key)) { 140 | initStorage(key); 141 | } 142 | 143 | return getStorage(key); 144 | }, 145 | getEmptyStatsData: function (key) { 146 | if (isStorageExist(key)) { 147 | removeStorage(key); 148 | } 149 | 150 | return this.getStatsData(key); 151 | }, 152 | recordGlobalStat: function (stat) { 153 | this.recordStat(globalStorageKey, stat); 154 | currentGlobalStat = stat; 155 | }, 156 | getGlobalStatsData: function () { 157 | return this.getStatsData(globalStorageKey); 158 | }, 159 | getCurrentGlobalStat: function () { 160 | return currentGlobalStat; 161 | } 162 | }; 163 | }]); 164 | }()); 165 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgNotificationService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgNotificationService', ['$window', 'Notification', 'ariaNgConstants', 'ariaNgCommonService', 'ariaNgStorageService', 'ariaNgLocalizationService', 'ariaNgLogService', 'ariaNgSettingService', function ($window, Notification, ariaNgConstants, ariaNgCommonService, ariaNgStorageService, ariaNgLocalizationService, ariaNgLogService, ariaNgSettingService) { 5 | var isSupportBrowserNotification = !!$window.Notification; 6 | 7 | var isBrowserNotifactionGranted = function (permission) { 8 | return permission === 'granted'; 9 | }; 10 | 11 | var getBrowserNotifactionPermission = function () { 12 | if (!$window.Notification) { 13 | return null; 14 | } 15 | 16 | return $window.Notification.permission; 17 | }; 18 | 19 | var requestBrowserNotifactionPermission = function (callback) { 20 | if (!$window.Notification) { 21 | return; 22 | } 23 | 24 | $window.Notification.requestPermission(function (permission) { 25 | if (callback) { 26 | callback({ 27 | granted: isBrowserNotifactionGranted(permission), 28 | permission: permission 29 | }); 30 | } 31 | }); 32 | }; 33 | 34 | var isReachBrowserNotificationFrequencyLimit = function () { 35 | if (!ariaNgSettingService.getBrowserNotificationFrequency() || ariaNgSettingService.getBrowserNotificationFrequency() === 'unlimited') { 36 | return false; 37 | } 38 | 39 | var lastNotifications = ariaNgStorageService.get(ariaNgConstants.browserNotificationHistoryStorageKey) || []; 40 | 41 | if (!angular.isArray(lastNotifications)) { 42 | return false; 43 | } 44 | 45 | if (lastNotifications.length < 1) { 46 | return false; 47 | } 48 | 49 | var oldestTime = null; 50 | var isReachLimit = false; 51 | 52 | if (ariaNgSettingService.getBrowserNotificationFrequency() === 'high') { 53 | if (lastNotifications.length < 10) { 54 | return false; 55 | } 56 | 57 | oldestTime = lastNotifications[lastNotifications.length - 10].time; 58 | isReachLimit = ariaNgCommonService.isUnixTimeAfter(oldestTime, '-1', 'minute'); 59 | } else if (ariaNgSettingService.getBrowserNotificationFrequency() === 'middle') { 60 | oldestTime = lastNotifications[lastNotifications.length - 1].time; 61 | isReachLimit = ariaNgCommonService.isUnixTimeAfter(oldestTime, '-1', 'minute'); 62 | } else if (ariaNgSettingService.getBrowserNotificationFrequency() === 'low') { 63 | oldestTime = lastNotifications[lastNotifications.length - 1].time; 64 | isReachLimit = ariaNgCommonService.isUnixTimeAfter(oldestTime, '-5', 'minute'); 65 | } 66 | 67 | if (isReachLimit) { 68 | ariaNgLogService.debug('[ariaNgNotificationService.isReachBrowserNotificationFrequencyLimit] reach frequency limit' 69 | + (oldestTime ? ', the oldest time is ' + oldestTime : '')); 70 | } 71 | 72 | return isReachLimit; 73 | }; 74 | 75 | var recordBrowserNotificationHistory = function () { 76 | if (!ariaNgSettingService.getBrowserNotificationFrequency() || ariaNgSettingService.getBrowserNotificationFrequency() === 'unlimited') { 77 | return; 78 | } 79 | 80 | var lastNotifications = ariaNgStorageService.get(ariaNgConstants.browserNotificationHistoryStorageKey) || []; 81 | 82 | if (!angular.isArray(lastNotifications)) { 83 | lastNotifications = []; 84 | } 85 | 86 | lastNotifications.push({ 87 | time: ariaNgCommonService.getCurrentUnixTime() 88 | }); 89 | 90 | if (lastNotifications.length > 10) { 91 | lastNotifications.splice(0, lastNotifications.length - 10); 92 | } 93 | 94 | ariaNgStorageService.set(ariaNgConstants.browserNotificationHistoryStorageKey, lastNotifications); 95 | }; 96 | 97 | var showBrowserNotifaction = function (title, options) { 98 | if (!$window.Notification) { 99 | return; 100 | } 101 | 102 | if (!isBrowserNotifactionGranted(getBrowserNotifactionPermission())) { 103 | return; 104 | } 105 | 106 | if (isReachBrowserNotificationFrequencyLimit()) { 107 | return; 108 | } 109 | 110 | options = angular.extend({ 111 | icon: 'tileicon.png' 112 | }, options); 113 | 114 | recordBrowserNotificationHistory(); 115 | 116 | new $window.Notification(title, options); 117 | }; 118 | 119 | var notifyViaBrowser = function (title, content, options) { 120 | if (!options) { 121 | options = {}; 122 | } 123 | 124 | options.body = content; 125 | 126 | if (!ariaNgSettingService.getBrowserNotificationSound()) { 127 | options.silent = true; 128 | } 129 | 130 | if (isSupportBrowserNotification && ariaNgSettingService.getBrowserNotification()) { 131 | showBrowserNotifaction(title, options); 132 | } 133 | }; 134 | 135 | var notifyInPage = function (title, content, options) { 136 | if (!options) { 137 | options = {}; 138 | } 139 | 140 | if (!content) { 141 | options.message = title; 142 | } else { 143 | options.title = title; 144 | options.message = content; 145 | } 146 | 147 | if (!options.type || !Notification[options.type]) { 148 | options.type = 'primary'; 149 | } 150 | 151 | if (!options.positionY) { 152 | options.positionY = 'top'; 153 | } 154 | 155 | return Notification[options.type](options); 156 | }; 157 | 158 | return { 159 | isSupportBrowserNotification: function () { 160 | return isSupportBrowserNotification; 161 | }, 162 | hasBrowserPermission: function () { 163 | if (!isSupportBrowserNotification) { 164 | return false; 165 | } 166 | 167 | return isBrowserNotifactionGranted(getBrowserNotifactionPermission()); 168 | }, 169 | requestBrowserPermission: function (callback) { 170 | if (!isSupportBrowserNotification) { 171 | return; 172 | } 173 | 174 | requestBrowserNotifactionPermission(function (result) { 175 | if (!result.granted) { 176 | ariaNgSettingService.setBrowserNotification(false); 177 | } 178 | 179 | if (callback) { 180 | callback(result); 181 | } 182 | }); 183 | }, 184 | notifyViaBrowser: function (title, content, options) { 185 | if (!options) { 186 | options = {}; 187 | } 188 | 189 | if (title) { 190 | title = ariaNgLocalizationService.getLocalizedText(title, options.titleParams); 191 | } 192 | 193 | if (content) { 194 | content = ariaNgLocalizationService.getLocalizedText(content, options.contentParams); 195 | } 196 | 197 | return notifyViaBrowser(title, content, options); 198 | }, 199 | notifyTaskComplete: function (task) { 200 | this.notifyViaBrowser('Download Completed', (task && task.taskName ? task.taskName : '')); 201 | }, 202 | notifyBtTaskComplete: function (task) { 203 | this.notifyViaBrowser('BT Download Completed', (task && task.taskName ? task.taskName : '')); 204 | }, 205 | notifyTaskError: function (task) { 206 | this.notifyViaBrowser('Download Error', (task && task.taskName ? task.taskName : '')); 207 | }, 208 | notifyInPage: function (title, content, options) { 209 | if (!options) { 210 | options = {}; 211 | } 212 | 213 | if (title) { 214 | title = ariaNgLocalizationService.getLocalizedText(title, options.titleParams); 215 | } 216 | 217 | if (content) { 218 | content = ariaNgLocalizationService.getLocalizedText(content, options.contentParams); 219 | 220 | if (options.contentPrefix) { 221 | content = options.contentPrefix + content; 222 | } 223 | } 224 | 225 | return notifyInPage(title, content, options); 226 | }, 227 | clearNotificationInPage: function () { 228 | Notification.clearAll(); 229 | } 230 | }; 231 | }]); 232 | }()); 233 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgStorageService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgStorageService', ['$window', 'localStorageService', function ($window, localStorageService) { 5 | return { 6 | isLocalStorageSupported: function () { 7 | return localStorageService.isSupported; 8 | }, 9 | isCookiesSupported: function () { 10 | return localStorageService.cookie.isSupported; 11 | }, 12 | get: function (key) { 13 | return localStorageService.get(key); 14 | }, 15 | set: function (key, value) { 16 | return localStorageService.set(key, value); 17 | }, 18 | remove: function (key) { 19 | return localStorageService.remove(key); 20 | }, 21 | clearAll: function () { 22 | return localStorageService.clearAll(); 23 | }, 24 | keys: function (prefix) { 25 | var allKeys = localStorageService.keys(); 26 | 27 | if (!allKeys || !allKeys.length || !prefix) { 28 | return allKeys; 29 | } 30 | 31 | var result = []; 32 | 33 | for (var i = 0; i < allKeys.length; i++) { 34 | if (allKeys[i].indexOf(prefix) >= 0) { 35 | result.push(allKeys[i]); 36 | } 37 | } 38 | 39 | return result; 40 | } 41 | }; 42 | }]); 43 | }()); 44 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgTitleService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgTitleService', ['$filter', 'ariaNgConstants', 'ariaNgLocalizationService', 'ariaNgSettingService', function ($filter, ariaNgConstants, ariaNgLocalizationService, ariaNgSettingService) { 5 | var parseSettings = function (placeholder) { 6 | if (!placeholder) { 7 | return {}; 8 | } 9 | 10 | var innerText = placeholder.substring(2, placeholder.length - 1); // remove ${ and } 11 | var items = innerText.split(':'); 12 | 13 | var settings = { 14 | oldValue: placeholder 15 | }; 16 | 17 | for (var i = 1; i < items.length; i++) { 18 | var pairs = items[i].split('='); 19 | 20 | if (pairs.length === 1) { 21 | settings[pairs[0]] = true; 22 | } else if (pairs.length === 2) { 23 | settings[pairs[0]] = pairs[1]; 24 | } 25 | } 26 | 27 | return settings; 28 | }; 29 | 30 | var replacePlaceholder = function (title, context) { 31 | var value = context.value; 32 | 33 | if (context.type === 'volume') { 34 | value = $filter('readableVolume')(value, context.scale); 35 | } 36 | 37 | if (context.prefix && !context.noprefix) { 38 | value = context.prefix + value; 39 | } 40 | 41 | if (context.suffix && !context.nosuffix) { 42 | value = value + context.suffix; 43 | } 44 | 45 | return title.replace(context.oldValue, value); 46 | }; 47 | 48 | var replacePlaceholders = function (title, condition, context) { 49 | var regex = new RegExp('\\$\\{' + condition + '(:[a-zA-Z0-9]+(=[a-zA-Z0-9]+)?)*\\}', 'g'); 50 | var results = title.match(regex); 51 | 52 | if (results && results.length > 0) { 53 | for (var i = 0; i < results.length; i++) { 54 | var innerContext = parseSettings(results[i]); 55 | angular.extend(innerContext, context); 56 | 57 | title = replacePlaceholder(title, innerContext); 58 | } 59 | } 60 | 61 | return title; 62 | }; 63 | 64 | var replaceCurrentRPCAlias = function (title, value) { 65 | return replacePlaceholders(title, 'rpcprofile', { 66 | value: value 67 | }); 68 | }; 69 | 70 | var replaceDownloadingCount = function (title, value) { 71 | return replacePlaceholders(title, 'downloading', { 72 | prefix: ariaNgLocalizationService.getLocalizedText('Downloading') + ': ', 73 | value: value 74 | }); 75 | }; 76 | 77 | var replaceWaitingCount = function (title, value) { 78 | return replacePlaceholders(title, 'waiting', { 79 | prefix: ariaNgLocalizationService.getLocalizedText('Waiting') + ': ', 80 | value: value 81 | }); 82 | }; 83 | 84 | var replaceStoppedCount = function (title, value) { 85 | return replacePlaceholders(title, 'stopped', { 86 | prefix: ariaNgLocalizationService.getLocalizedText('Finished / Stopped') + ': ', 87 | value: value 88 | }); 89 | }; 90 | 91 | var replaceDownloadSpeed = function (title, value) { 92 | return replacePlaceholders(title, 'downspeed', { 93 | prefix: ariaNgLocalizationService.getLocalizedText('Download') + ': ', 94 | value: value, 95 | type: 'volume', 96 | suffix: '/s' 97 | }); 98 | }; 99 | 100 | var replaceUploadSpeed = function (title, value) { 101 | return replacePlaceholders(title, 'upspeed', { 102 | prefix: ariaNgLocalizationService.getLocalizedText('Upload') + ': ', 103 | value: value, 104 | type: 'volume', 105 | suffix: '/s' 106 | }); 107 | }; 108 | 109 | var replaceAgiaNgTitle = function (title) { 110 | return replacePlaceholders(title, 'title', { 111 | value: ariaNgConstants.title 112 | }); 113 | }; 114 | 115 | return { 116 | getFinalTitle: function (context) { 117 | var title = ariaNgSettingService.getTitle(); 118 | 119 | if (!title) { 120 | return ariaNgConstants.title; 121 | } 122 | 123 | context = angular.extend({ 124 | downloadingCount: 0, 125 | waitingCount: 0, 126 | stoppedCount: 0, 127 | downloadSpeed: 0, 128 | uploadSpeed: 0 129 | }, context); 130 | 131 | title = replaceCurrentRPCAlias(title, context.currentRPCAlias); 132 | title = replaceDownloadingCount(title, context.downloadingCount); 133 | title = replaceWaitingCount(title, context.waitingCount); 134 | title = replaceStoppedCount(title, context.stoppedCount); 135 | title = replaceDownloadSpeed(title, context.downloadSpeed); 136 | title = replaceUploadSpeed(title, context.uploadSpeed); 137 | title = replaceAgiaNgTitle(title); 138 | 139 | return title; 140 | }, 141 | getFinalTitleByGlobalStat: function (params) { 142 | var context = { 143 | currentRPCAlias: (params && params.currentRpcProfile ? (params.currentRpcProfile.rpcAlias || (params.currentRpcProfile.rpcHost + ':' + params.currentRpcProfile.rpcPort)) : ''), 144 | downloadingCount: (params && params.globalStat ? params.globalStat.numActive : 0), 145 | waitingCount: (params && params.globalStat ? params.globalStat.numWaiting : 0), 146 | stoppedCount: (params && params.globalStat ? params.globalStat.numStopped : 0), 147 | downloadSpeed: (params && params.globalStat ? params.globalStat.downloadSpeed : 0), 148 | uploadSpeed: (params && params.globalStat ? params.globalStat.uploadSpeed : 0) 149 | }; 150 | 151 | return this.getFinalTitle(context); 152 | } 153 | }; 154 | }]); 155 | }()); 156 | -------------------------------------------------------------------------------- /src/scripts/services/ariaNgVersionService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('ariaNg').factory('ariaNgVersionService', ['ariaNgBuildConfiguration', function (ariaNgBuildConfiguration) { 5 | return { 6 | getBuildVersion: function () { 7 | return ariaNgBuildConfiguration.buildVersion; 8 | }, 9 | getBuildCommit: function () { 10 | return ariaNgBuildConfiguration.buildCommit; 11 | } 12 | }; 13 | }]); 14 | }()); 15 | -------------------------------------------------------------------------------- /src/styles/controls/angular-promise-buttons.css: -------------------------------------------------------------------------------- 1 | /* angular-promise-buttons */ 2 | @-webkit-keyframes three-quarters { 3 | 0% { 4 | -webkit-transform: rotate(0deg); 5 | -moz-transform: rotate(0deg); 6 | -ms-transform: rotate(0deg); 7 | -o-transform: rotate(0deg); 8 | transform: rotate(0deg); 9 | } 10 | 11 | 100% { 12 | -webkit-transform: rotate(360deg); 13 | -moz-transform: rotate(360deg); 14 | -ms-transform: rotate(360deg); 15 | -o-transform: rotate(360deg); 16 | transform: rotate(360deg); 17 | } 18 | } 19 | 20 | @-moz-keyframes three-quarters { 21 | 0% { 22 | -webkit-transform: rotate(0deg); 23 | -moz-transform: rotate(0deg); 24 | -ms-transform: rotate(0deg); 25 | -o-transform: rotate(0deg); 26 | transform: rotate(0deg); 27 | } 28 | 29 | 100% { 30 | -webkit-transform: rotate(360deg); 31 | -moz-transform: rotate(360deg); 32 | -ms-transform: rotate(360deg); 33 | -o-transform: rotate(360deg); 34 | transform: rotate(360deg); 35 | } 36 | } 37 | 38 | @-o-keyframes three-quarters { 39 | 0% { 40 | -webkit-transform: rotate(0deg); 41 | -moz-transform: rotate(0deg); 42 | -ms-transform: rotate(0deg); 43 | -o-transform: rotate(0deg); 44 | transform: rotate(0deg); 45 | } 46 | 47 | 100% { 48 | -webkit-transform: rotate(360deg); 49 | -moz-transform: rotate(360deg); 50 | -ms-transform: rotate(360deg); 51 | -o-transform: rotate(360deg); 52 | transform: rotate(360deg); 53 | } 54 | } 55 | 56 | @keyframes three-quarters { 57 | 0% { 58 | -webkit-transform: rotate(0deg); 59 | -moz-transform: rotate(0deg); 60 | -ms-transform: rotate(0deg); 61 | -o-transform: rotate(0deg); 62 | transform: rotate(0deg); 63 | } 64 | 65 | 100% { 66 | -webkit-transform: rotate(360deg); 67 | -moz-transform: rotate(360deg); 68 | -ms-transform: rotate(360deg); 69 | -o-transform: rotate(360deg); 70 | transform: rotate(360deg); 71 | } 72 | } 73 | 74 | .btn-spinner { 75 | font-family: sans-serif; 76 | font-weight: 100; 77 | } 78 | 79 | .btn-spinner:not(:required) { 80 | -webkit-animation: three-quarters 1250ms infinite linear; 81 | -moz-animation: three-quarters 1250ms infinite linear; 82 | -ms-animation: three-quarters 1250ms infinite linear; 83 | -o-animation: three-quarters 1250ms infinite linear; 84 | animation: three-quarters 1250ms infinite linear; 85 | border: 3px solid #8c8c8c; 86 | border-right-color: transparent; 87 | border-radius: 100%; 88 | box-sizing: border-box; 89 | display: inline-block; 90 | position: relative; 91 | vertical-align: middle; 92 | overflow: hidden; 93 | text-indent: -9999px; 94 | width: 18px; 95 | height: 18px; 96 | } 97 | 98 | .btn-primary .btn-spinner:not(:required), .btn-danger .btn-spinner:not(:required) { 99 | border: 3px solid #efefef; 100 | border-right-color: transparent; 101 | } 102 | 103 | .btn-spinner:not(:required) { 104 | margin-left: -17px; 105 | opacity: 0; 106 | transition: 0.4s margin ease-out, 0.2s opacity ease-out; 107 | } 108 | 109 | .is-loading .btn-spinner { 110 | transition: 0.2s margin ease-in, 0.4s opacity ease-in; 111 | margin-left: 5px; 112 | opacity: 1; 113 | } 114 | -------------------------------------------------------------------------------- /src/styles/controls/chart.css: -------------------------------------------------------------------------------- 1 | /* chart */ 2 | .chart-popover { 3 | max-width: 320px; 4 | } 5 | 6 | .chart-popover .popover-content { 7 | padding: 0; 8 | } 9 | 10 | .chart-pop-wrapper { 11 | padding-left: 4px; 12 | padding-right: 4px; 13 | overflow-x: hidden; 14 | } 15 | 16 | .chart-pop { 17 | display: table; 18 | } 19 | 20 | .chart-pop .loading { 21 | width: 100%; 22 | height: 100%; 23 | display: table-cell; 24 | text-align: center; 25 | vertical-align: middle; 26 | } 27 | 28 | .global-status-chart { 29 | width: 312px; 30 | height: 200px; 31 | } 32 | 33 | .task-status-chart-wrapper { 34 | overflow-x: hidden; 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/controls/global-status.css: -------------------------------------------------------------------------------- 1 | /* global-status */ 2 | .global-status { 3 | cursor: pointer; 4 | } 5 | 6 | .global-status > .realtime-speed { 7 | padding: 0 15px 0 15px; 8 | } 9 | 10 | .global-status > .realtime-speed:first-child { 11 | padding-left: 5px; 12 | } 13 | 14 | .global-status > .realtime-speed:last-child { 15 | padding-right: 5px; 16 | } 17 | 18 | .global-status span.realtime-speed > i { 19 | padding-right: 2px; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/controls/new-task-table.css: -------------------------------------------------------------------------------- 1 | /* new-task-table */ 2 | .new-task-table { 3 | margin-left: 15px; 4 | margin-right: 15px; 5 | } 6 | 7 | @media screen and (orientation: landscape) { 8 | .content > .new-task-table, 9 | .tab-pane > .new-task-table { 10 | margin-right: calc(15px + env(safe-area-inset-right)); 11 | } 12 | } 13 | 14 | .new-task-table > div.row { 15 | padding-top: 8px; 16 | padding-bottom: 8px; 17 | } 18 | 19 | @media screen and (orientation: landscape) { 20 | .content > .new-task-table > div.row, 21 | .tab-pane > .new-task-table > div.row { 22 | margin-right: calc(-1 * calc(15px + env(safe-area-inset-right))); 23 | padding-right: env(safe-area-inset-right); 24 | } 25 | } 26 | 27 | .new-task-table > div.row:first-child { 28 | border-top: inherit; 29 | } 30 | 31 | .new-task-table .new-task-toollink > a { 32 | margin-right: 20px; 33 | } 34 | 35 | @media (max-width: 767px) { 36 | .new-task-table .new-task-toollink > a { 37 | display: block; 38 | } 39 | } 40 | 41 | .settings-table .new-task-filter-title { 42 | padding-top: 6px; 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/controls/piece-bar-map.css: -------------------------------------------------------------------------------- 1 | /* piece-bar / piece-map */ 2 | .piece-bar-wrapper { 3 | height: 20px; 4 | } 5 | 6 | .piece-bar { 7 | width: 100%; 8 | } 9 | 10 | .piece-map { 11 | padding-left: 6px; 12 | padding-right: 2px; 13 | line-height: 11px; 14 | } 15 | 16 | @media screen and (orientation: landscape) { 17 | .tab-pane > .piece-map { 18 | padding-right: calc(2px + env(safe-area-inset-right)); 19 | } 20 | } 21 | 22 | .piece-legends { 23 | text-align: center; 24 | margin-top: 4px; 25 | margin-bottom: 4px; 26 | } 27 | 28 | @media screen and (orientation: landscape) { 29 | .tab-pane > .piece-legends { 30 | padding-right: env(safe-area-inset-right); 31 | } 32 | } 33 | 34 | .piece-legend { 35 | display: inline-block; 36 | margin-right: 4px; 37 | } 38 | 39 | .piece-map .piece, .piece-legend > .piece { 40 | width: 10px; 41 | height: 10px; 42 | background-color: #eef2f4; 43 | border: #dee2e5 solid 1px; 44 | display: inline-block; 45 | margin-right: 1px; 46 | } 47 | 48 | .piece-map .piece.piece-completed, .piece-legend > .piece.piece-completed { 49 | background-color: #b8dd69; 50 | border-color: #b8dd69; 51 | } 52 | 53 | .piece-legend > .piece { 54 | margin-right: 4px; 55 | } 56 | -------------------------------------------------------------------------------- /src/styles/controls/settings-table.css: -------------------------------------------------------------------------------- 1 | /* settings-table */ 2 | .settings-table { 3 | margin-left: 15px; 4 | margin-right: 15px; 5 | } 6 | 7 | @media screen and (orientation: landscape) { 8 | .content > .settings-table, 9 | .tab-pane > .settings-table { 10 | margin-right: calc(15px + env(safe-area-inset-right)); 11 | } 12 | } 13 | 14 | .settings-table .settings-table-title { 15 | font-size: 12px; 16 | padding-top: 4px; 17 | padding-bottom: 4px; 18 | } 19 | 20 | .settings-table .settings-table-title a { 21 | color: #000; 22 | cursor: pointer; 23 | } 24 | 25 | .settings-table .settings-table-title .settings-table-title-toolbar { 26 | display: inline-block; 27 | margin-left: 10px; 28 | } 29 | 30 | .settings-table > div.row { 31 | padding-top: 8px; 32 | padding-bottom: 8px; 33 | border-top: 1px solid #ddd; 34 | } 35 | 36 | @media screen and (orientation: landscape) { 37 | .content > .settings-table > div.row, 38 | .tab-pane > .settings-table > div.row { 39 | margin-right: calc(-1 * calc(15px + env(safe-area-inset-right))); 40 | padding-right: env(safe-area-inset-right); 41 | } 42 | } 43 | 44 | .settings-table > div.row:first-child { 45 | border-top: inherit; 46 | } 47 | 48 | .settings-table + .settings-table > div.row:first-child { 49 | border-top: 1px solid #ddd; 50 | } 51 | 52 | .settings-table .input-group-addon { 53 | background-color: #eee; 54 | } 55 | 56 | .settings-table .asterisk { 57 | color: red; 58 | } 59 | 60 | .settings-table .description, .settings-table .description-inline { 61 | color: #888; 62 | font-size: 12px; 63 | font-weight: normal; 64 | font-style: normal; 65 | } 66 | 67 | .settings-table .description { 68 | display: block; 69 | } 70 | 71 | .settings-table .description-inline { 72 | display: inline-block; 73 | } 74 | 75 | .settings-table em { 76 | color: #888; 77 | font-size: 12px; 78 | font-weight: normal; 79 | } 80 | 81 | .settings-table .setting-value .form-group { 82 | margin-bottom: 0; 83 | } 84 | 85 | .settings-table .setting-value .form-group .form-control-icon { 86 | color: #3c8dbc; 87 | } 88 | 89 | .settings-table .setting-value .form-group select.form-control + .form-control-icon > .form-control-feedback { 90 | right: 10px; 91 | } 92 | 93 | .settings-table .setting-value .input-group .form-group .form-control:focus { 94 | z-index: inherit; 95 | } 96 | 97 | .settings-table .setting-value .input-group .form-control-rpcport { 98 | min-width: 70px; 99 | } 100 | 101 | .settings-table .setting-value .input-group .form-control-rpcinterface { 102 | min-width: 100px; 103 | } 104 | 105 | @media (max-width: 991px) { 106 | .settings-table .setting-value .input-group .form-control-rpcport { 107 | min-width: 60px; 108 | } 109 | 110 | .settings-table .setting-value .input-group .form-control-rpcinterface { 111 | min-width: 60px; 112 | } 113 | } 114 | 115 | .settings-table .tip { 116 | font-size: 12px; 117 | padding: 4px 8px 4px 8px; 118 | } 119 | 120 | .settings-table .multi-line { 121 | display: block; 122 | } 123 | 124 | @media (max-width: 767px) { 125 | .settings-table .setting-key { 126 | font-weight: bold; 127 | } 128 | 129 | .settings-table .description { 130 | display: inline-block; 131 | } 132 | } 133 | 134 | @media (min-width: 768px) { 135 | .settings-table .setting-key-without-desc { 136 | padding-top: 6px; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/styles/controls/task-table.css: -------------------------------------------------------------------------------- 1 | /* task-table */ 2 | .task-table { 3 | margin-left: 15px; 4 | margin-right: 15px; 5 | } 6 | 7 | @media screen and (orientation: landscape) { 8 | .content > .task-table, 9 | .tab-pane > .task-table { 10 | margin-right: calc(15px + env(safe-area-inset-right)); 11 | } 12 | } 13 | 14 | .task-table .task-table-title { 15 | font-size: 12px; 16 | padding-top: 4px; 17 | padding-bottom: 4px; 18 | } 19 | 20 | .task-table .task-table-title a { 21 | color: #000; 22 | cursor: pointer; 23 | } 24 | 25 | .task-table > .task-table-body.draggable { 26 | cursor: move; 27 | cursor: grab; 28 | cursor: -moz-grab; 29 | cursor: -webkit-grab; 30 | } 31 | 32 | @media screen and (orientation: landscape) { 33 | .content > .task-table div.row, 34 | .tab-pane > .task-table div.row { 35 | margin-right: calc(-1 * calc(15px + env(safe-area-inset-right))); 36 | padding-right: env(safe-area-inset-right); 37 | } 38 | } 39 | 40 | .task-table > .task-table-body > div.row { 41 | padding-top: 8px; 42 | padding-bottom: 8px; 43 | border-top: 1px solid #ddd; 44 | } 45 | 46 | .task-table > div.row:first-child { 47 | border-top: inherit; 48 | } 49 | 50 | @media (max-width: 767px) { 51 | .task-table > .task-table-title { 52 | display: none !important; 53 | } 54 | 55 | .task-table > .task-table-body > div.row:first-child { 56 | border-top: inherit; 57 | } 58 | } 59 | 60 | .task-table .task-name { 61 | font-size: 14px; 62 | display: block; 63 | } 64 | 65 | .task-table .peer-name-wrapper { 66 | display: inline-block; 67 | width: 100%; 68 | } 69 | 70 | .task-table .task-files, .task-table .task-size { 71 | font-size: 12px; 72 | display: block; 73 | } 74 | 75 | .task-table .progress { 76 | margin-bottom: 0; 77 | } 78 | 79 | .task-table .task-last-time, .task-table .task-seeders, .task-table .task-last-time + .task-download-speed { 80 | color: #888; 81 | font-size: 12px; 82 | } 83 | 84 | .task-table .task-seeders, .task-table .task-last-time + .task-download-speed { 85 | margin-top: 1px; 86 | } 87 | 88 | .task-table .task-last-time + .task-download-speed { 89 | padding-left: 20px; 90 | } 91 | 92 | .task-table .task-download-speed, .task-table .task-peer-download-speed { 93 | font-size: 12px; 94 | } 95 | 96 | .task-table .checkbox, .task-table .radio { 97 | margin-top: 0; 98 | margin-bottom: 0; 99 | } 100 | 101 | .task-table .progress { 102 | position: relative; 103 | } 104 | 105 | .task-table .progress span { 106 | position: absolute; 107 | display: block; 108 | width: 100%; 109 | } 110 | 111 | .task-table .progress span.progress-lower { 112 | color: #000; 113 | } 114 | 115 | @media (max-width: 767px) { 116 | .task-table .task-peer-download-speed { 117 | float: right; 118 | } 119 | } 120 | 121 | .task-table .task-right-arrow { 122 | visibility: hidden; 123 | position: absolute; 124 | right: 14px; 125 | margin-top: -12px; 126 | } 127 | 128 | .task-table .row:hover .task-right-arrow, .task-table .row[data-selected=true] .task-right-arrow { 129 | visibility: visible; 130 | } 131 | 132 | .task-table .task-right-arrow i { 133 | color: #c8c8c8; 134 | font-size: 60px; 135 | } 136 | 137 | .task-table .task-right-arrow i:hover { 138 | color: #d8d8d8; 139 | } 140 | -------------------------------------------------------------------------------- /src/styles/core/core.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * AriaNg 3 | * https://github.com/mayswind/AriaNg 4 | */ 5 | 6 | /* basic */ 7 | html { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | -ms-user-select: none; 16 | -webkit-user-select: none; 17 | -moz-user-select: none; 18 | user-select: none; 19 | } 20 | 21 | select { 22 | -webkit-appearance: none; 23 | -moz-appearance: none; 24 | appearance: none; 25 | background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23555555%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E') !important; 26 | background-repeat: no-repeat, repeat !important; 27 | background-position: right 0.7em top 50%, 0 0 !important; 28 | background-size: 0.65em auto, 100% !important; 29 | border: none; 30 | border-radius: 0; 31 | padding: 0 0 0 0.35em; 32 | } 33 | 34 | select::-ms-expand { 35 | display: none; 36 | } 37 | 38 | td { 39 | vertical-align: middle !important; 40 | } 41 | 42 | .modal textarea { 43 | resize: none; 44 | } 45 | 46 | .blur { 47 | -webkit-filter: blur(5px); 48 | -moz-filter: blur(5px); 49 | -ms-filter: blur(5px); 50 | filter: blur(5px); 51 | filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius=5, MakeShadow=false); 52 | } 53 | 54 | .disable-overlay { 55 | position: fixed; 56 | left: 0; 57 | top: 0; 58 | right: 0; 59 | bottom: 0; 60 | z-index: 9999; 61 | cursor: not-allowed; 62 | } 63 | 64 | .main-header .logo { 65 | overflow: visible; 66 | } 67 | 68 | .main-header .logo .dropdown-menu { 69 | z-index: 2000; 70 | } 71 | 72 | .main-header .navbar .nav > li { 73 | display: inline-block; 74 | } 75 | 76 | .main-header .navbar .nav > li > a { 77 | padding-left: 10px; 78 | padding-right: 10px; 79 | } 80 | 81 | .main-header .navbar .nav > li.disabled > a { 82 | pointer-events: none !important; 83 | } 84 | 85 | .main-header .navbar .navbar-nav { 86 | margin-left: 5px; 87 | } 88 | 89 | .main-header .navbar .navbar-searchbar { 90 | padding-top: 8px; 91 | padding-right: 20px; 92 | float: right; 93 | } 94 | 95 | .main-header .logo .logo-mini { 96 | font-size: 14px !important; 97 | } 98 | 99 | .main-header .logo .logo-lg { 100 | cursor: pointer; 101 | } 102 | 103 | .main-header .rpcselect-dropdown { 104 | max-height: 200px; 105 | overflow-x: hidden; 106 | } 107 | 108 | @media (min-height: 360px) { 109 | .main-header .rpcselect-dropdown { 110 | max-height: 260px; 111 | } 112 | } 113 | 114 | @media (min-height: 480px) { 115 | .main-header .rpcselect-dropdown { 116 | max-height: 360px; 117 | } 118 | } 119 | 120 | @media (min-height: 600px) { 121 | .main-header .rpcselect-dropdown { 122 | max-height: 480px; 123 | } 124 | } 125 | 126 | @media (min-height: 720px) { 127 | .main-header .rpcselect-dropdown { 128 | max-height: 600px; 129 | } 130 | } 131 | 132 | .main-sidebar { 133 | z-index: 1010; 134 | } 135 | 136 | .main-sidebar .sidebar-menu > li.treeview > ul.treeview-menu > li > a { 137 | padding: 6px 5px 6px 41px; 138 | } 139 | 140 | @supports (padding-left: max(15px, 0px)) { 141 | @media screen and (orientation: landscape) { 142 | .main-sidebar ul.sidebar-menu > li.header { 143 | padding-left: max(15px, env(safe-area-inset-left)); 144 | } 145 | 146 | .main-sidebar ul.sidebar-menu > li > a { 147 | padding-left: max(15px, env(safe-area-inset-left)); 148 | } 149 | 150 | .main-sidebar ul.sidebar-menu > li.treeview > ul.treeview-menu > li > a { 151 | padding-left: max(41px, calc(26px + env(safe-area-inset-left))); 152 | } 153 | } 154 | } 155 | 156 | .content-wrapper { 157 | min-height: calc(100vh - 48px); 158 | } 159 | 160 | .content-wrapper, .right-side { 161 | background-color: #fff; 162 | } 163 | 164 | .content-wrapper > .content-body { 165 | overflow-y: scroll; 166 | } 167 | 168 | @media screen and (orientation: portrait) { 169 | .main-footer { 170 | padding-bottom: calc(15px + env(safe-area-inset-bottom)); 171 | } 172 | } 173 | 174 | .main-footer > .navbar { 175 | margin-bottom: 0; 176 | min-height: inherit; 177 | } 178 | 179 | .main-footer > .navbar > .navbar-toolbar > .nav { 180 | float: left; 181 | margin: 0; 182 | } 183 | 184 | .main-footer > .navbar > .navbar-toolbar > .nav > li { 185 | display: inline-block; 186 | float: left; 187 | } 188 | 189 | .main-footer > .navbar > .navbar-toolbar > .nav > li > a { 190 | padding: 0 10px 0 10px; 191 | } 192 | 193 | .main-footer > .navbar > .navbar-toolbar > .nav > li:first-child > a { 194 | padding-left: 0; 195 | } 196 | 197 | .dropdown-menu.right-align { 198 | left: inherit; 199 | right: 0; 200 | } 201 | 202 | .default-cursor { 203 | cursor: default !important; 204 | } 205 | 206 | .pointer-cursor { 207 | cursor: pointer !important; 208 | } 209 | 210 | .text-cursor { 211 | cursor: text !important; 212 | } 213 | 214 | .allow-word-break { 215 | word-wrap: break-word; 216 | word-break: break-all; 217 | } 218 | 219 | .auto-ellipsis { 220 | white-space: nowrap; 221 | overflow: hidden; 222 | text-overflow: ellipsis; 223 | } 224 | 225 | @media (max-width: 767px) { 226 | .navbar-nav .open .dropdown-menu { 227 | position: absolute; 228 | border: 1px solid #eee; 229 | background-color: #fff; 230 | } 231 | 232 | .main-footer > .navbar > .navbar-toolbar > .nav > li > a { 233 | padding-left: 8px; 234 | padding-right: 8px; 235 | } 236 | } 237 | 238 | /* toolbar */ 239 | .toolbar { 240 | cursor: pointer; 241 | } 242 | 243 | .toolbar:active { 244 | -webkit-box-shadow: inset 0 2px 6px rgba(0, 0, 0, .125); 245 | -moz-box-shadow: inset 0 2px 6px rgba(0, 0, 0, .125); 246 | box-shadow: inset 0 2px 6px rgba(0, 0, 0, .125); 247 | } 248 | 249 | /* dropdown-submenu */ 250 | .dropdown-menu small { 251 | color: #999; 252 | } 253 | 254 | .dropdown-submenu { 255 | position: relative; 256 | } 257 | 258 | .dropdown-submenu > .dropdown-menu { 259 | top: 0; 260 | left: 100%; 261 | margin-top: -6px; 262 | margin-left: -1px; 263 | -webkit-border-radius: 0 6px 6px 6px; 264 | -moz-border-radius: 0 6px 6px; 265 | border-radius: 0 6px 6px 6px; 266 | } 267 | 268 | .dropdown-submenu:hover > .dropdown-menu { 269 | display: block; 270 | } 271 | 272 | .dropdown-submenu > a:after { 273 | display: block; 274 | content: " "; 275 | float: right; 276 | width: 0; 277 | height: 0; 278 | border-color: transparent; 279 | border-style: solid; 280 | border-width: 5px 0 5px 5px; 281 | border-left-color: #ccc; 282 | margin-top: 5px; 283 | margin-right: -10px; 284 | } 285 | 286 | .dropdown-menu > li.dropdown-submenu:hover { 287 | background-color: #e1e3e9; 288 | } 289 | 290 | .dropdown-submenu:hover > a:after { 291 | border-left-color: #fff; 292 | } 293 | 294 | .dropdown-submenu.pull-left { 295 | float: none; 296 | } 297 | 298 | .dropdown-submenu.pull-left > .dropdown-menu { 299 | left: -100%; 300 | margin-left: 10px; 301 | -webkit-border-radius: 6px 0 6px 6px; 302 | -moz-border-radius: 6px 0 6px 6px; 303 | border-radius: 6px 0 6px 6px; 304 | } 305 | 306 | /* scrollbar */ 307 | ::-webkit-scrollbar { 308 | width: 10px; 309 | } 310 | 311 | ::-webkit-scrollbar-thumb { 312 | background-clip: padding-box; 313 | background-color: #c4d2db; 314 | min-height: 28px; 315 | } 316 | 317 | ::-webkit-scrollbar-track { 318 | background-color: #fff; 319 | } 320 | 321 | ::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:active { 322 | background-color: #d4dfe7; 323 | } 324 | 325 | @media (max-width: 767px) { 326 | ::-webkit-scrollbar { 327 | width: 6px; 328 | } 329 | } 330 | 331 | /* animation */ 332 | .fade-in.ng-enter { 333 | transition:0.3s linear all; 334 | opacity:0; 335 | } 336 | .fade-in.ng-enter.ng-enter-active { 337 | opacity:1; 338 | } 339 | 340 | /* misc */ 341 | .keyboard-key { 342 | font-size: 0.85em; 343 | padding: 1px 3px; 344 | -webkit-border-radius: 2px; 345 | -moz-border-radius: 2px; 346 | border-radius: 2px; 347 | } 348 | -------------------------------------------------------------------------------- /src/styles/core/extend.css: -------------------------------------------------------------------------------- 1 | /* bootstrap */ 2 | .btn.active.focus, 3 | .btn.active:focus, 4 | .btn.focus, 5 | .btn:active.focus, 6 | .btn:active:focus, 7 | .btn:focus { 8 | outline: none !important; 9 | } 10 | 11 | .btn-group + .btn { 12 | margin-left: 4px; 13 | } 14 | 15 | .btn-sm.promise-btn-style { 16 | padding-top: 6px; 17 | padding-bottom: 6px; 18 | } 19 | 20 | .btn.btn-xs:not(.is-loading) .btn-spinner:not(:required) { 21 | margin-left: -21px; 22 | } 23 | 24 | .progress-bar { 25 | -webkit-transition: initial !important; 26 | -moz-transition: initial !important; 27 | -ms-transition: initial !important; 28 | -o-transition: initial !important; 29 | transition: initial !important; 30 | } 31 | 32 | .input-group-addon-compact { 33 | padding: 0 4px 0 4px; 34 | } 35 | 36 | .nav-tabs-custom .nav-tabs > li > a { 37 | display: inline-block; 38 | } 39 | 40 | .nav-tabs-custom .nav-tabs > li > a.nav-tab-close { 41 | padding-left: 0; 42 | margin-left: -12px; 43 | } 44 | 45 | .nav-tabs-custom .nav-tabs > li.nav-tab-title-rpcname > a { 46 | max-width: 180px; 47 | overflow: hidden; 48 | text-overflow: ellipsis; 49 | white-space: nowrap; 50 | vertical-align: bottom; 51 | } 52 | 53 | @media (max-width: 991px) { 54 | .nav-tabs-custom .nav-tabs > li.nav-tab-title-rpcname > a { 55 | max-width: 150px; 56 | } 57 | } 58 | 59 | @media (max-width: 767px) { 60 | .nav-tabs-custom .nav-tabs > li.nav-tab-title-rpcname > a { 61 | max-width: 120px; 62 | } 63 | } 64 | 65 | .input-group.input-group-multiple > .input-group-addon { 66 | border-left: 0; 67 | border-right: 0; 68 | } 69 | 70 | .input-group.input-group-multiple > .input-group-addon:first-child, 71 | .input-group.input-group-multiple > .input-group-addon-container:first-child { 72 | border-left: 1px solid #d2d6de; 73 | } 74 | 75 | .input-group .input-group-addon-container { 76 | width: 1%; 77 | display: table-cell; 78 | } 79 | 80 | .label { 81 | font-size: 85%; 82 | } 83 | 84 | /* font-awesome extend */ 85 | .fa-half { 86 | font-size: 0.5em; 87 | } 88 | 89 | .fa-1_1x { 90 | font-size: 1.1em; 91 | } 92 | 93 | .fa-rotate-45 { 94 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; 95 | -webkit-transform: rotate(45deg); 96 | -moz-transform: rotate(45deg); 97 | -ms-transform: rotate(45deg); 98 | -o-transform: rotate(45deg); 99 | transform: rotate(45deg); 100 | filter: none; 101 | } 102 | 103 | .fa-right-bottom { 104 | position: relative; 105 | right: 0; 106 | bottom: -6px; 107 | } 108 | 109 | .fa-display-order { 110 | margin-left: 3px; 111 | } 112 | 113 | .fa-order-asc, .fa-order-desc { 114 | position: relative; 115 | } 116 | 117 | .fa-order-asc { 118 | bottom: -2px; 119 | } 120 | 121 | .fa-order-desc { 122 | bottom: 2px; 123 | } 124 | 125 | /* awesome-bootstrap-checkbox extend */ 126 | .checkbox input[type="checkbox"]:focus + label::before, 127 | .checkbox input[type="radio"]:focus + label::before { 128 | outline: none !important; 129 | } 130 | 131 | .checkbox input[type="checkbox"], 132 | .checkbox input[type="radio"] { 133 | cursor: pointer; 134 | } 135 | 136 | .checkbox input.disable-clickable { 137 | pointer-events: none !important; 138 | } 139 | 140 | .checkbox.checkbox-hide { 141 | padding-left: 0; 142 | } 143 | 144 | .checkbox.checkbox-hide > input, .checkbox.checkbox-hide > input + label::before, .checkbox.checkbox-hide > input + label::after { 145 | display: none !important; 146 | } 147 | 148 | .checkbox.checkbox-hide > label { 149 | padding-left: 0; 150 | } 151 | 152 | .checkbox-compact { 153 | margin-top: 2px; 154 | margin-bottom: 2px; 155 | } 156 | 157 | .checkbox-inline { 158 | display: inline-block; 159 | } 160 | 161 | .icon-dir-expand + .checkbox { 162 | margin-left: 6px; 163 | } 164 | 165 | /* angular-input-dropdown */ 166 | input-dropdown[input-class-name="form-control"] > .input-dropdown { 167 | width: 100% 168 | } 169 | 170 | .input-dropdown ul { 171 | border: 1px solid #888; 172 | } 173 | 174 | .input-dropdown ul > li.active { 175 | background-color: #e1e3e9; 176 | } 177 | 178 | .input-dropdown ul > li { 179 | padding: 2px 14px 2px 14px; 180 | } 181 | 182 | /* angular-dragula extend */ 183 | .gu-mirror { 184 | cursor: grabbing; 185 | cursor: -moz-grabbing; 186 | cursor: -webkit-grabbing; 187 | } 188 | -------------------------------------------------------------------------------- /src/tileicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayswind/AriaNg/ee48603a27a4b08b604fef93a6c8ee9242f20d1e/src/tileicon.png -------------------------------------------------------------------------------- /src/touchicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayswind/AriaNg/ee48603a27a4b08b604fef93a6c8ee9242f20d1e/src/touchicon.png -------------------------------------------------------------------------------- /src/views/export-command-api-dialog.html: -------------------------------------------------------------------------------- 1 |
55 | -------------------------------------------------------------------------------- /src/views/new.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 95 | 96 |
97 |
98 | -------------------------------------------------------------------------------- /src/views/notification-reloadable.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Reload AriaNg 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/views/setting-dialog.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/views/setting.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 7 | 9 | 10 |
11 |
12 |
13 |
14 |

15 |                 
18 |                 
25 |                 
29 |                 
35 |                 
36 | 37 |
38 |
39 | 40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /src/views/settings-aria2.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /src/views/status.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Aria2 RPC Address 6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 | Aria2 Status 14 |
15 |
16 | 18 |
19 |
20 |
21 |
22 | Aria2 Version 23 |
24 |
25 | 26 | - 27 | 28 |
29 |
30 |
31 |
32 | Enabled Features 33 |
34 |
35 | 36 | - 37 |
39 | 40 | 43 |
44 |
45 |
46 |
47 |
48 | Operations 49 |
50 |
51 | 54 | 56 | 58 |
59 |
60 |
61 |
62 | --------------------------------------------------------------------------------