├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── electorrent-workflow.yml ├── .gitignore ├── .jsbeautifyrc ├── .jshintrc ├── .mocharc.yml ├── .nvmrc ├── LICENSE ├── README.md ├── Vagrantfile ├── app └── package.json ├── assets ├── electron-icon-128x128.png ├── electron-icon-256x256.png ├── electron-icon.png ├── screen0-win.png ├── screen1-win.png └── screen2-win.png ├── bower.json ├── build ├── icon.icns ├── icon.ico ├── install-spinner.gif ├── png │ ├── 128x128.png │ ├── 16x16.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 512x512.png │ └── 64x64.png ├── torrent.icns └── torrent.ico ├── electron-builder.yml ├── gulpfile.js ├── package-lock.json ├── package.json ├── resources └── torrentfile.ico ├── semantic.json ├── src ├── app.js ├── css │ ├── fonts │ │ ├── bittorrent.eot │ │ ├── bittorrent.svg │ │ ├── bittorrent.ttf │ │ ├── bittorrent.woff │ │ └── icons │ │ │ ├── icon_deluge.svg │ │ │ ├── icon_downloadstation.svg │ │ │ ├── icon_qbittorrent.svg │ │ │ ├── icon_rtorrent.svg │ │ │ ├── icon_transmission.svg │ │ │ └── icon_utorrent.svg │ ├── icons.less │ ├── main.less │ ├── semantic │ │ └── theme.config │ ├── settings.less │ ├── styles.less │ ├── themes │ │ ├── dark │ │ │ ├── collections │ │ │ │ ├── breadcrumb.overrides │ │ │ │ ├── form.overrides │ │ │ │ ├── form.variables │ │ │ │ ├── menu.overrides │ │ │ │ ├── menu.variables │ │ │ │ ├── message.overrides │ │ │ │ ├── message.variables │ │ │ │ └── table.variables │ │ │ ├── elements │ │ │ │ ├── button.overrides │ │ │ │ ├── button.variables │ │ │ │ ├── divider.variables │ │ │ │ ├── header.variables │ │ │ │ ├── input.overrides │ │ │ │ ├── input.variables │ │ │ │ ├── label.variables │ │ │ │ ├── list.variables │ │ │ │ ├── segment.overrides │ │ │ │ ├── segment.variables │ │ │ │ └── step.variables │ │ │ ├── globals │ │ │ │ ├── site.overrides │ │ │ │ └── site.variables │ │ │ ├── modules │ │ │ │ ├── accordion.variables │ │ │ │ ├── checkbox.overrides │ │ │ │ ├── checkbox.variables │ │ │ │ ├── dropdown.overrides │ │ │ │ ├── dropdown.variables │ │ │ │ ├── modal.variables │ │ │ │ ├── popup.variables │ │ │ │ └── progress.variables │ │ │ └── views │ │ │ │ ├── card.variables │ │ │ │ ├── feed.variables │ │ │ │ └── statistic.variables │ │ └── light │ │ │ ├── globals │ │ │ └── site.variables │ │ │ └── modules │ │ │ └── checkbox.overrides │ └── zindex.less ├── index.html ├── lib │ ├── certificates.js │ ├── config.js │ ├── electorrent.js │ ├── logger.js │ ├── startup.js │ ├── themes.js │ ├── torrents.js │ ├── update.js │ └── worker.js ├── main.ts ├── scripts │ ├── bittorrent │ │ ├── abstracttorrent.ts │ │ ├── deluge │ │ │ ├── delugeservice.ts │ │ │ ├── index.ts │ │ │ └── torrentd.ts │ │ ├── index.ts │ │ ├── qbittorrent │ │ │ ├── index.ts │ │ │ ├── qbittorrentservice.ts │ │ │ └── torrentq.ts │ │ ├── rtorrent │ │ │ ├── index.ts │ │ │ ├── rtorrentservice.ts │ │ │ └── torrentr.ts │ │ ├── synology │ │ │ ├── index.ts │ │ │ ├── synologyservice.ts │ │ │ └── synologytorrent.ts │ │ ├── torrentclient.ts │ │ ├── transmission │ │ │ ├── index.ts │ │ │ ├── torrentt.ts │ │ │ ├── transmissionconfig.ts │ │ │ └── transmissionservice.ts │ │ └── utorrent │ │ │ ├── index.ts │ │ │ ├── torrentu.ts │ │ │ └── utorrentservice.ts │ ├── components │ │ ├── menuMac.ts │ │ └── menuWin.ts │ ├── controllers │ │ ├── main.ts │ │ ├── notifications.ts │ │ ├── settings.ts │ │ ├── theme.ts │ │ ├── torrents.ts │ │ └── welcome.ts │ ├── directives │ │ ├── actionheader.ts │ │ ├── add-torrent-modal │ │ │ ├── add-torrent-modal.controller.ts │ │ │ ├── add-torrent-modal.directive.ts │ │ │ └── add-torrent-modal.template.html │ │ ├── checkbox.ts │ │ ├── contextmenu.ts │ │ ├── draganddrop.ts │ │ ├── drop.ts │ │ ├── dropdown.ts │ │ ├── labelsdropdown.ts │ │ ├── limit.ts │ │ ├── modal.ts │ │ ├── modal │ │ │ ├── modal.controller.ts │ │ │ ├── modal.directive.ts │ │ │ └── modal.template.html │ │ ├── progress.ts │ │ ├── readyBroadcast.ts │ │ ├── repeatdone.ts │ │ ├── rightclick.ts │ │ ├── search.ts │ │ ├── sorting.ts │ │ ├── time.ts │ │ ├── torrent-upload-form │ │ │ ├── torrent-upload-form.controller.ts │ │ │ ├── torrent-upload-form.directive.ts │ │ │ └── torrent-upload-form.template.html │ │ └── torrenttable.ts │ ├── filters │ │ ├── bytes.ts │ │ ├── dateFilter.ts │ │ └── torrentfilters.ts │ ├── services │ │ ├── bittorrent.ts │ │ ├── column.ts │ │ ├── config.ts │ │ ├── electron.ts │ │ ├── httpFormService.ts │ │ ├── notification.ts │ │ ├── remote.ts │ │ └── server.ts │ └── workers │ │ ├── deluge.js │ │ ├── qbittorrent.js │ │ └── rtorrent.js ├── types │ ├── angular.d.ts │ ├── electorrent.d.ts │ ├── electron.d.ts │ ├── globals.d.ts │ └── html.d.ts └── views │ ├── misc │ └── labels.html │ ├── modals │ ├── addtorrent.html │ ├── certificate.html │ ├── newlabel.html │ ├── rename.html │ └── update.html │ ├── notifications.html │ ├── servers.html │ ├── settings.html │ ├── settings │ ├── about.html │ ├── connection.html │ ├── general.html │ ├── layout.html │ └── servers.html │ ├── torrents.html │ └── welcome.html ├── test ├── .gitignore ├── e2e │ ├── e2e_app.ts │ ├── e2e_torrent.ts │ └── index.ts ├── fixtures │ ├── deluge │ │ ├── deluge.spec.ts │ │ └── docker-compose.yml │ ├── qbittorrent │ │ ├── docker-compose.yml │ │ ├── qBittorrent.conf │ │ └── qbittorrent.spec.ts │ ├── rutorrent │ │ ├── docker-compose.yml │ │ └── rtorrent.spec.ts │ ├── transmission │ │ ├── docker-compose.yml │ │ └── transmission.spec.ts │ └── utorrent │ │ ├── docker-compose.yml │ │ └── utorrent.spec.ts ├── shared │ ├── app.hook.ts │ ├── backend.hook.ts │ ├── compose.hook.ts │ ├── index.ts │ ├── nginx │ │ ├── Dockerfile │ │ ├── docker-compose.yml │ │ └── rootfs │ │ │ ├── docker-entrypoint.d │ │ │ └── 40-generate-x509.sh │ │ │ └── etc │ │ │ └── nginx │ │ │ └── templates │ │ │ └── default.conf.template │ └── opentracker │ │ ├── docker-compose.yml │ │ ├── peer │ │ ├── Dockerfile │ │ └── rootfs │ │ │ └── start │ │ └── tracker │ │ ├── Dockerfile │ │ └── rootfs │ │ └── etc │ │ └── opentracker │ │ └── opentracker.conf ├── testlib.ts ├── testutil.ts ├── tsconfig.json └── types │ └── index.d.ts ├── tsconfig.json ├── util ├── after-pack.js └── mac_icon_gen.sh ├── vagrant └── install.sh ├── wdio.conf.ts └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Client:** 2 | - [ ] µTorrent 3 | - [ ] qBittorrent 4 | - [ ] Transmission 5 | - [ ] rTorrent 6 | - [ ] Deluge 7 | 8 | **Client Version:** 9 | 10 | **Operating System:** 11 | 12 | **Application Version:** 13 | 14 | **Description:** 15 | -------------------------------------------------------------------------------- /.github/workflows/electorrent-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Electorrent Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: '0 0 * * 0' # every Sunday at midnight 10 | 11 | env: 12 | NODE_VERSION: 22.x 13 | ELECTRON_IS_DEV: 0 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | testspec: 23 | - qbittorrent-4 24 | - qbittorrent-5 25 | - qbittorrent-latest 26 | - rtorrent-latest 27 | - transmission-latest 28 | - deluge-1 29 | - utorrent-latest 30 | 31 | steps: 32 | - name: Checkout source code 33 | uses: actions/checkout@v4 34 | - name: Install dependencies 35 | run: > 36 | sudo apt-get install -y 37 | xvfb 38 | libnss3-dev 39 | libdbus-1-dev 40 | libatk1.0-dev 41 | libatk-bridge2.0-dev 42 | libcups2-dev 43 | libgtk-3-0 44 | libgbm1 45 | libasound2t64 46 | - name: Install nodejs 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{ env.NODE_VERSION }} 50 | cache: 'npm' 51 | - name: Cache npm modules 52 | uses: actions/cache@v4 53 | with: 54 | path: | 55 | node_modules 56 | */*/node_modules 57 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 58 | - name: Cache bower modules 59 | uses: actions/cache@v4 60 | with: 61 | path: bower.json 62 | key: ${{ runner.os }}-${{ hashFiles('bower.json') }} 63 | - name: Install npm dependencies 64 | run: npm install 65 | - name: Build web application 66 | run: npm run build 67 | - name: Run tests 68 | uses: nick-fields/retry@v3 69 | timeout-minutes: 15 70 | with: 71 | max_attempts: 3 72 | timeout_minutes: 10 73 | retry_on: error 74 | command: xvfb-run -a -- npm test -- --mochaOpts.grep "${{ matrix.testspec }}" 75 | 76 | build: 77 | strategy: 78 | fail-fast: false 79 | matrix: 80 | os: [ubuntu-latest, windows-latest, macos-13] 81 | 82 | runs-on: ${{ matrix.os }} 83 | 84 | steps: 85 | - name: Install apt packages 86 | run: sudo apt-get update && sudo apt-get install -y graphicsmagick icnsutils 87 | if: runner.os == 'Linux' 88 | - name: Checkout source code 89 | uses: actions/checkout@v4 90 | - name: Install nodejs 91 | uses: actions/setup-node@v4 92 | with: 93 | node-version: ${{ env.NODE_VERSION }} 94 | cache: 'npm' 95 | - name: Cache npm modules 96 | uses: actions/cache@v4 97 | with: 98 | path: | 99 | node_modules 100 | */*/node_modules 101 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 102 | - name: Install npm dependencies 103 | run: npm install 104 | - name: Build distribution 105 | run: npm run dist 106 | if: github.ref != 'refs/heads/master' 107 | - name: Release artifacts 108 | if: github.ref == 'refs/heads/master' 109 | run: npm run release 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Vim ### 2 | # Swap 3 | [._]*.s[a-v][a-z] 4 | !*.svg # comment out if you don't need vector files 5 | [._]*.sw[a-p] 6 | [._]s[a-rt-v][a-z] 7 | [._]ss[a-gi-z] 8 | [._]sw[a-p] 9 | 10 | # Session 11 | Session.vim 12 | Sessionx.vim 13 | 14 | # Temporary 15 | .netrwhist 16 | *~ 17 | # Auto-generated tag files 18 | tags 19 | # Persistent undo 20 | [._]*.un~ 21 | 22 | # Logs 23 | logs 24 | *.log 25 | npm-debug.log* 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | 38 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 39 | .grunt 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directory 48 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 49 | node_modules 50 | 51 | # ========================= 52 | # Operating System Files 53 | # ========================= 54 | 55 | # OSX 56 | # ========================= 57 | 58 | .DS_Store 59 | .AppleDouble 60 | .LSOverride 61 | 62 | # Thumbnails 63 | ._* 64 | 65 | # Files that might appear in the root of a volume 66 | .DocumentRevisions-V100 67 | .fseventsd 68 | .Spotlight-V100 69 | .TemporaryItems 70 | .Trashes 71 | .VolumeIcon.icns 72 | 73 | # Directories potentially created on remote AFP share 74 | .AppleDB 75 | .AppleDesktop 76 | Network Trash Folder 77 | Temporary Items 78 | .apdisk 79 | 80 | # Windows 81 | # ========================= 82 | 83 | # Windows image file caches 84 | Thumbs.db 85 | ehthumbs.db 86 | 87 | # Folder config file 88 | Desktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msm 97 | *.msp 98 | 99 | # Windows shortcuts 100 | *.lnk 101 | 102 | # Bower components 103 | bower_components 104 | 105 | # Ignore distibution folder 106 | dist 107 | 108 | # Database configuration 109 | config 110 | 111 | # Ignore app directory 112 | app/* 113 | !app/package.json 114 | 115 | # Vagrant folder 116 | .vagrant 117 | 118 | # Visual Studio code 119 | .vscode 120 | 121 | # Yarn 122 | yarn.lock 123 | 124 | # SemanticUI 125 | src/css/semantic/* 126 | !src/css/semantic/theme.config 127 | src/css/themes/default 128 | 129 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "html": { 3 | "allowed_file_extensions": ["htm", "html", "xhtml", "shtml", "xml", "svg"], 4 | "brace_style": "collapse", 5 | "end_with_newline": false, 6 | "indent_char": " ", 7 | "indent_handlebars": false, 8 | "indent_inner_html": false, 9 | "indent_scripts": "keep", 10 | "indent_size": 4, 11 | "max_preserve_newlines": 0, 12 | "preserve_newlines": true, 13 | "unformatted": ["a", "span", "img", "code", "pre", "sub", "sup", "em", "strong", "b", "i", "u", "strike", "big", 14 | "small", "pre", "h1", "h2", "h3", "h4", "h5", "h6"], 15 | "wrap_line_length": 120 16 | }, 17 | "css": { 18 | "allowed_file_extensions": ["css", "scss", "sass", "less"], 19 | "end_with_newline": false, 20 | "indent_char": " ", 21 | "indent_size": 4, 22 | "newline_between_rules": true, 23 | "selector_separator_newline": true 24 | }, 25 | "js": { 26 | "allowed_file_extensions": ["js", "jsx", "json", "eslintrc", "jsbeautifyrc"], 27 | "brace_style": "collapse", 28 | "break_chained_methods": false, 29 | "comma_first": false, 30 | "e4x": false, 31 | "end_with_newline": false, 32 | "indent_char": " ", 33 | "indent_level": 0, 34 | "indent_size": 4, 35 | "jslint_happy": false, 36 | "keep_array_indentation": false, 37 | "keep_function_indentation": false, 38 | "max_preserve_newlines": 0, 39 | "preserve_newlines": true, 40 | "space_after_anon_function": false, 41 | "space_before_conditional": false, 42 | "space_in_empty_paren": false, 43 | "space_in_paren": false, 44 | "unescape_strings": false, 45 | "wrap_line_length": 120 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | # This is an example Mocha config containing every Mocha option plus others. 2 | # allow-uncaught: false 3 | # async-only: false 4 | bail: true 5 | # color: true 6 | # diff: true 7 | exit: true # could be expressed as "no-exit: true" 8 | # #extension: ['ts', 'js', 'cjs', 'mjs'] 9 | # fail-zero: true 10 | node-option: 11 | - 'unhandled-rejections=strict' # without leading "--", also V8 flags 12 | # parallel: false 13 | # recursive: false 14 | # reporter: 'spec' 15 | require: 16 | - 'ts-node/register' 17 | file: 18 | - 'test/shared/root.spec.ts' 19 | # retries: 1 20 | watch-files: 21 | - 'test/**/*.{ts}' 22 | - 'app/**/*.{ts,js,html,css}' 23 | spec: 24 | - 'test/**/*.spec.ts' # the positional arguments! 25 | timeout: 20000 26 | slow: 20000 27 | # watch: false 28 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | 16 | # Electorrent 17 | No more! Stop copy/pasting magnet links and uploading torrent files through a tedious webinterface. Electorrent is your new desktop remote torrenting application. Remote control your NAS, VPS, seedbox - you name it. 18 | 19 | ## Support 20 | Electorrent can connect to the following bittorrent clients: 21 | * [µTorrent](http://www.utorrent.com/) 22 | * [qBittorrent](http://www.qbittorrent.org/) (v3.2.x and above) 23 | * [Transmission](https://transmissionbt.com) 24 | * [rTorrent](https://rakshasa.github.io/rtorrent/) 25 | * [Synology Download Station](https://www.synology.com/en-global/knowledgebase/DSM/help/DownloadStation/DownloadStation_desc) 26 | * [Deluge](https://deluge-torrent.org/) 27 | 28 | ## Downloads 29 | *Please note: I do not own code signing certificates which may results in anti-virus warnings!* 30 | * [Windows](https://electorrent.vercel.app/download/win32) (64 bit only) 31 | * [MacOS](https://electorrent.vercel.app/download/dmg) 32 | * [Linux](https://electorrent.vercel.app/download/appimage) 33 | 34 | ## Features 35 | - [x] Connects to your favorite torrent client 36 | - [x] Handles the magnet link protocol when browsing websites 37 | - [x] Upload local torrent files by browsing your filesystem (Ctrl/Cmd+O) 38 | - [x] Drag-and-drop support for torrent files 39 | - [x] Paste magnet links directly from your clipboard (Ctrl/Cmd+I) 40 | - [x] Quickly change between multiple server configurations 41 | - [x] Native desktop notifications 42 | - [x] Fuzzy searching of torrents 43 | - [x] Built in certificate trust system (for self-signed certificates) 44 | - [x] Easy one click installer using Squirrel framework 45 | - [x] Automatic updates straight from the GitHub repository! 46 | 47 | ## Screenshots 48 |

49 | 50 | 51 | 52 |

53 |

54 | 55 | 56 | 57 |

58 |

59 | 60 | 61 | 62 |

63 | 64 | ## FAQ 65 | * **Your program sucks. It doesn't support my bittorrent client** 66 | 67 | What an opportunity! Now open an issue telling me which bittorrent client you would like to see next :) 68 | 69 | * **What kind of technologies are used to build this?** 70 | 71 | The application is build around [Electron](http://electron.atom.io/), [AngularJS](https://angularjs.org/) and [SemanticUI](http://semantic-ui.com/) 72 | 73 | * **I can't connect to rTorrent what is wrong?** 74 | 75 | When using rTorrent you have to configure your http server correctly. Follow [this guide](https://github.com/rakshasa/rtorrent/wiki/RPC-Setup-XMLRPC) to make sure you have it set up correctly. Alternative you may be able to use `/plugins/httprpc/action.php` as the path if your have the HTTPRPC plugin installed. 76 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure("2") do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://vagrantcloud.com/search. 15 | config.vm.box = "ubuntu/focal64" 16 | 17 | # Disable automatic box update checking. If you disable this, then 18 | # boxes will only be checked for updates when the user runs 19 | # `vagrant box outdated`. This is not recommended. 20 | # config.vm.box_check_update = false 21 | 22 | # Create a forwarded port mapping which allows access to a specific port 23 | # within the machine from a port on the host machine. In the example below, 24 | # accessing "localhost:8080" will access port 80 on the guest machine. 25 | # NOTE: This will enable public access to the opened port 26 | # config.vm.network "forwarded_port", guest: 80, host: 8080 27 | 28 | # Create a forwarded port mapping which allows access to a specific port 29 | # within the machine from a port on the host machine and only allow access 30 | # via 127.0.0.1 to disable public access 31 | # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1" 32 | 33 | # Create a private network, which allows host-only access to the machine 34 | # using a specific IP. 35 | # config.vm.network "private_network", ip: "192.168.33.10" 36 | 37 | # Create a public network, which generally matched to bridged network. 38 | # Bridged networks make the machine appear as another physical device on 39 | # your network. 40 | # config.vm.network "public_network" 41 | 42 | # Share an additional folder to the guest VM. The first argument is 43 | # the path on the host to the actual folder. The second argument is 44 | # the path on the guest to mount the folder. And the optional third 45 | # argument is a set of non-required options. 46 | # config.vm.synced_folder "../data", "/vagrant_data" 47 | 48 | # Provider-specific configuration so you can fine-tune various 49 | # backing providers for Vagrant. These expose provider-specific options. 50 | # Example for VirtualBox: 51 | # 52 | # config.vm.provider "virtualbox" do |vb| 53 | # # Display the VirtualBox GUI when booting the machine 54 | # vb.gui = true 55 | # 56 | # # Customize the amount of memory on the VM: 57 | # vb.memory = "1024" 58 | # end 59 | # 60 | # View the documentation for the provider you are using for more 61 | # information on available options. 62 | 63 | # config.vm.provision "file", source: "vagrant/rootfs/", destination: "/" 64 | 65 | # Enable provisioning with a shell script. Additional provisioners such as 66 | # Ansible, Chef, Docker, Puppet and Salt are also available. Please see the 67 | # documentation for more information about their specific syntax and use. 68 | config.vm.provision "shell", path: "vagrant/install.sh", privileged: false 69 | end 70 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electorrent", 3 | "productName": "Electorrent", 4 | "version": "2.8.5", 5 | "description": "A thin client for your torrenting needs", 6 | "main": "app.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 0" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/tympanix/Electorrent.git" 13 | }, 14 | "keywords": [ 15 | "Electorrent", 16 | "Electron", 17 | "uTorrent", 18 | "Server", 19 | "GUI" 20 | ], 21 | "author": { 22 | "name": "tympanix", 23 | "email": "tympanix@gmail.com" 24 | }, 25 | "license": "GPL-3.0", 26 | "bugs": { 27 | "url": "https://github.com/tympanix/Electorrent/issues" 28 | }, 29 | "homepage": "https://github.com/tympanix/Electorrent#readme", 30 | "dependencies": { 31 | "@electorrent/node-deluge": "^1.0.3", 32 | "@electorrent/node-qbittorrent": "^1.2.1", 33 | "@electorrent/node-rtorrent": "^1.2.1", 34 | "@electron/remote": "^2.0.8", 35 | "axios": "^0.24.0", 36 | "electron-is": "^3.0.0", 37 | "electron-regedit": "^2.0.0", 38 | "electron-squirrel-startup": "^1.0.0", 39 | "qs": "^6.10.1", 40 | "request": "^2.74.0", 41 | "semver": "^5.3.0", 42 | "winston": "^2.2.0", 43 | "yargs": "^4.8.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /assets/electron-icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/assets/electron-icon-128x128.png -------------------------------------------------------------------------------- /assets/electron-icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/assets/electron-icon-256x256.png -------------------------------------------------------------------------------- /assets/electron-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/assets/electron-icon.png -------------------------------------------------------------------------------- /assets/screen0-win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/assets/screen0-win.png -------------------------------------------------------------------------------- /assets/screen1-win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/assets/screen1-win.png -------------------------------------------------------------------------------- /assets/screen2-win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/assets/screen2-win.png -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electorrent", 3 | "description": "An Electron/Node/AngularJS remote client app for uTorrent server", 4 | "main": "app.js", 5 | "authors": [ 6 | "tympanix" 7 | ], 8 | "license": "GPL-3.0", 9 | "keywords": [ 10 | "utorrent" 11 | ], 12 | "private": true, 13 | "ignore": [ 14 | "**/.*", 15 | "node_modules", 16 | "bower_components", 17 | "test", 18 | "tests" 19 | ], 20 | "dependencies": { 21 | "angular": "^1.8.2", 22 | "angular-resource": "^1.8.2", 23 | "semantic": "~2.4.1", 24 | "moment": "momentjs#^2.13.0", 25 | "jquery": "^3.0.0", 26 | "angular-animate": "^1.8.2", 27 | "ngInfiniteScroll": "^1.3.4", 28 | "angular-marked": "^1.2.2", 29 | "angular-ui-sortable": "^0.19.0", 30 | "underscore": "^1.8.3", 31 | "angular-table-resize": "^2.0.1", 32 | "base64-js": "^1.2.0", 33 | "fuse.js": "^3.0.5", 34 | "mousetrap": "^1.6.2" 35 | }, 36 | "resolutions": { 37 | "angular": "1.5.8", 38 | "jquery": ">=3.1.x" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/icon.ico -------------------------------------------------------------------------------- /build/install-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/install-spinner.gif -------------------------------------------------------------------------------- /build/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/png/128x128.png -------------------------------------------------------------------------------- /build/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/png/16x16.png -------------------------------------------------------------------------------- /build/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/png/256x256.png -------------------------------------------------------------------------------- /build/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/png/32x32.png -------------------------------------------------------------------------------- /build/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/png/512x512.png -------------------------------------------------------------------------------- /build/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/png/64x64.png -------------------------------------------------------------------------------- /build/torrent.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/torrent.icns -------------------------------------------------------------------------------- /build/torrent.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/build/torrent.ico -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | directories: 2 | output: dist 3 | buildResources: build 4 | productName: Electorrent 5 | appId: com.github.tympanix.electorrent 6 | mac: 7 | category: public.app-category.utilities 8 | win: 9 | target: squirrel 10 | publish: 11 | - github 12 | dmg: 13 | publish: 14 | - github 15 | pkg: 16 | publish: 17 | - github 18 | linux: 19 | icon: build/png 20 | category: Network;FileTransfer;P2P; 21 | desktop: 22 | MimeType: application/x-bittorrent;x-scheme-handler/magnet; 23 | Keywords: p2p;bittorrent; 24 | target: 25 | - snap 26 | - AppImage 27 | snap: 28 | publish: 29 | - github 30 | appImage: 31 | publish: 32 | - github 33 | extraFiles: 34 | - filter: 35 | - resources 36 | fileAssociations: 37 | - ext: torrent 38 | name: Bittorrent Document 39 | role: Viewer 40 | description: Torrent Files 41 | afterPack: ./util/after-pack.js 42 | electronVersion: 36.2.1 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electorrent", 3 | "version": "2.8.5", 4 | "description": "A thin client for your torrenting needs", 5 | "main": "app/app.js", 6 | "license": "GPL-3.0", 7 | "scripts": { 8 | "start": "gulp", 9 | "build": "gulp build", 10 | "postinstall": "bower --allow-root install && gulp install && electron-builder install-app-deps", 11 | "pack": "build --dir", 12 | "predist": "gulp build", 13 | "dist": "electron-builder --publish never", 14 | "prerelease": "gulp build", 15 | "release": "electron-builder", 16 | "test": "wdio run ./wdio.conf.ts" 17 | }, 18 | "keywords": [ 19 | "utorrent" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/tympanix/Electorrent.git" 24 | }, 25 | "author": "tympanix", 26 | "dependencies": { 27 | "electron-regedit": "^2.0.0", 28 | "less": "^2.7.3", 29 | "q": "^1.5.1", 30 | "winreg": "^1.2.2" 31 | }, 32 | "devDependencies": { 33 | "@electorrent/node-deluge": "^1.0.3", 34 | "@electorrent/node-qbittorrent": "^1.2.1", 35 | "@electorrent/node-rtorrent": "^1.2.1", 36 | "@electron/remote": "^2.0.8", 37 | "@types/angular": "^1.5.8", 38 | "@types/angular-animate": "^1.5.10", 39 | "@types/angular-resource": "^1.5.16", 40 | "@types/axios": "^0.14.0", 41 | "@types/chai": "^4.3.20", 42 | "@types/chai-as-promised": "^7.1.5", 43 | "@types/form-data": "^2.5.0", 44 | "@types/jquery": "^3.5.5", 45 | "@types/magnet-uri": "^5.1.3", 46 | "@types/mocha": "^10.0.4", 47 | "@types/moment": "^2.13.0", 48 | "@types/mousetrap": "^1.6.5", 49 | "@types/node": "^14.18.16", 50 | "@types/parse-torrent": "^5.8.4", 51 | "@types/qs": "^6.9.7", 52 | "@types/tapable": "^0.2.5", 53 | "@types/underscore": "^1.10.24", 54 | "@wdio/cli": "^9.14.0", 55 | "@wdio/electron-types": "^8.1.0", 56 | "@wdio/local-runner": "^9.14.0", 57 | "@wdio/mocha-framework": "^9.14.0", 58 | "@wdio/spec-reporter": "^9.14.0", 59 | "@wdio/types": "^7.24.0", 60 | "angular": "^1.8.2", 61 | "angular-animate": "^1.5.11", 62 | "angular-marked": "^1.2.2", 63 | "angular-resource": "^1.5.11", 64 | "angular-table-resize": "^2.0.1", 65 | "angular-ui-sortable": "^0.16.1", 66 | "axios": "^1.6.0", 67 | "base64-js": "^1.5.1", 68 | "bower": "^1.8.8", 69 | "browserify": "^17.0.0", 70 | "chai": "^4.2.0", 71 | "chai-as-promised": "^7.1.1", 72 | "docker-compose": "^1.2.0", 73 | "electron": "^36.2.1", 74 | "electron-builder": "^23.6.0", 75 | "electron-builder-squirrel-windows": "^23.6.0", 76 | "electron-is": "^3.0.0", 77 | "electron-reloader": "^1.2.1", 78 | "electron-squirrel-startup": "^1.0.0", 79 | "form-data": "^4.0.0", 80 | "fuse.js": "^3.6.1", 81 | "gulp": "^4.0.2", 82 | "gulp-clean": "^0.4.0", 83 | "gulp-concat": "^2.6.1", 84 | "gulp-iconfont": "^10.0.3", 85 | "gulp-if": "^2.0.2", 86 | "gulp-less": "^4.0.1", 87 | "gulp-rename": "^1.4.0", 88 | "gulp-run": "^1.7.1", 89 | "gulp-run-electron": "^3.0.2", 90 | "gulp-sourcemaps": "^2.6.5", 91 | "gulp-typescript": "^6.0.0-alpha.1", 92 | "gulp-useref": "^3.1.6", 93 | "hoek": "^5.0.4", 94 | "html-loader": "^3.0.1", 95 | "html-webpack-plugin": "^4.5.0", 96 | "jquery": "^3.5.1", 97 | "jquery-ui": "^1.13.2", 98 | "lazypipe": "^1.0.2", 99 | "magnet-uri": "^6.2.0", 100 | "merge-stream": "^1.0.1", 101 | "mini-css-extract-plugin": "^1.3.3", 102 | "mocha": "^10.2.0", 103 | "moment": "^2.29.4", 104 | "mousetrap": "^1.6.5", 105 | "ng-infinite-scroll": "^1.3.0", 106 | "parse-torrent": "^9.1.5", 107 | "qs": "^6.10.3", 108 | "request": "^2.88.2", 109 | "rollup": "^2.2.0", 110 | "run-sequence": "^1.2.2", 111 | "semver": "^5.7.1", 112 | "terser-webpack-plugin": "^5.3.12", 113 | "through2": "^2.0.5", 114 | "ts-loader": "^9.5.2", 115 | "ts-node": "^10.9.2", 116 | "typescript": "^4.1.3", 117 | "underscore": "^1.12.1", 118 | "vinyl-buffer": "^1.0.1", 119 | "vinyl-source-stream": "^2.0.0", 120 | "wdio": "^6.0.1", 121 | "wdio-electron-service": "^8.1.0", 122 | "webpack": "^5.99.8", 123 | "webpack-cli": "^6.0.1", 124 | "webpack-node-externals": "^3.0.0", 125 | "webpack-stream": "^7.0.0", 126 | "winston": "^2.4.4", 127 | "yargs": "^4.8.1" 128 | }, 129 | "resolutions": { 130 | "graceful-fs": "^4.2.4", 131 | "@types/tapable": "1.0.0" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /resources/torrentfile.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/resources/torrentfile.ico -------------------------------------------------------------------------------- /semantic.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "src/css/semantic", 3 | "paths": { 4 | "source": { 5 | "config": "src/theme.config", 6 | "definitions": "src/definitions/", 7 | "site": "src/site/", 8 | "themes": "src/themes/" 9 | }, 10 | "output": { 11 | "packaged": "dist/", 12 | "uncompressed": "dist/components/", 13 | "compressed": "dist/components/", 14 | "themes": "dist/themes/" 15 | }, 16 | "clean": "dist/" 17 | }, 18 | "permission": false, 19 | "autoInstall": false, 20 | "rtl": false, 21 | "version": "2.8.7", 22 | "components": ["reset", "site", "button", "container", "divider", "emoji", "flag", "header", "icon", "image", "input", "label", "list", "loader", "rail", "reveal", "segment", "step", "breadcrumb", "form", "grid", "menu", "message", "table", "ad", "card", "comment", "feed", "item", "statistic", "accordion", "calendar", "checkbox", "dimmer", "dropdown", "embed", "modal", "nag", "placeholder", "popup", "progress", "slider", "rating", "search", "shape", "sidebar", "sticky", "tab", "text", "toast", "transition", "api", "form", "state", "visibility"] 23 | } -------------------------------------------------------------------------------- /src/css/fonts/bittorrent.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/src/css/fonts/bittorrent.eot -------------------------------------------------------------------------------- /src/css/fonts/bittorrent.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/src/css/fonts/bittorrent.ttf -------------------------------------------------------------------------------- /src/css/fonts/bittorrent.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/src/css/fonts/bittorrent.woff -------------------------------------------------------------------------------- /src/css/fonts/icons/icon_deluge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/css/fonts/icons/icon_downloadstation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/css/fonts/icons/icon_qbittorrent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/css/fonts/icons/icon_rtorrent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/css/fonts/icons/icon_transmission.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 19 | 21 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/css/fonts/icons/icon_utorrent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/css/icons.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'bittorrent'; 3 | src: url('../fonts/bittorrent.eot'); 4 | src: url('../fonts/bittorrent.eot?#iefix') format('embedded-opentype'), 5 | url('../fonts/bittorrent.woff') format('woff'), 6 | url('../fonts/bittorrent.ttf') format('truetype'), 7 | url('../fonts/bittorrent.svg#bittorrent') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | .torrent.icon { 12 | font-family: 'bittorrent' !important; 13 | display: inline-block; 14 | font-style: normal; 15 | font-weight: normal; 16 | line-height: 1; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale 19 | } 20 | 21 | .deluge:before{content:'\EA01'} 22 | .downloadstation:before{content:'\EA02';} 23 | .qbittorrent:before{content:'\EA03';} 24 | .rtorrent:before{content:'\EA04';} 25 | .transmission:before{content:'\EA05';} 26 | .utorrent:before{content:'\EA06';} -------------------------------------------------------------------------------- /src/css/semantic/theme.config: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | ████████╗██╗ ██╗███████╗███╗ ███╗███████╗███████╗ 4 | ╚══██╔══╝██║ ██║██╔════╝████╗ ████║██╔════╝██╔════╝ 5 | ██║ ███████║█████╗ ██╔████╔██║█████╗ ███████╗ 6 | ██║ ██╔══██║██╔══╝ ██║╚██╔╝██║██╔══╝ ╚════██║ 7 | ██║ ██║ ██║███████╗██║ ╚═╝ ██║███████╗███████║ 8 | ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝ 9 | 10 | */ 11 | 12 | /******************************* 13 | Theme Selection 14 | *******************************/ 15 | 16 | /* To override a theme for an individual element 17 | specify theme name below 18 | */ 19 | 20 | /* Global */ 21 | @site : 'default'; 22 | @reset : 'default'; 23 | 24 | /* Elements */ 25 | @button : @renderTheme; 26 | @container : @renderTheme; 27 | @divider : @renderTheme; 28 | @flag : @renderTheme; 29 | @header : @renderTheme; 30 | @icon : 'default'; 31 | @image : @renderTheme; 32 | @input : @renderTheme; 33 | @label : @renderTheme; 34 | @list : @renderTheme; 35 | @loader : @renderTheme; 36 | @rail : @renderTheme; 37 | @reveal : @renderTheme; 38 | @segment : @renderTheme; 39 | @step : @renderTheme; 40 | 41 | /* Collections */ 42 | @breadcrumb : @renderTheme; 43 | @form : @renderTheme; 44 | @grid : @renderTheme; 45 | @menu : @renderTheme; 46 | @message : @renderTheme; 47 | @table : @renderTheme; 48 | 49 | /* Modules */ 50 | @accordion : @renderTheme; 51 | @checkbox : @renderTheme; 52 | @dimmer : @renderTheme; 53 | @dropdown : @renderTheme; 54 | @embed : @renderTheme; 55 | @modal : @renderTheme; 56 | @nag : @renderTheme; 57 | @popup : @renderTheme; 58 | @progress : @renderTheme; 59 | @rating : @renderTheme; 60 | @search : @renderTheme; 61 | @shape : @renderTheme; 62 | @sidebar : @renderTheme; 63 | @sticky : @renderTheme; 64 | @tab : @renderTheme; 65 | @transition : 'default'; 66 | 67 | /* Views */ 68 | @ad : @renderTheme; 69 | @card : @renderTheme; 70 | @comment : @renderTheme; 71 | @feed : @renderTheme; 72 | @item : @renderTheme; 73 | @statistic : @renderTheme; 74 | 75 | /******************************* 76 | Folders 77 | *******************************/ 78 | 79 | /* Path to theme packages */ 80 | @themesFolder : '../themes'; 81 | 82 | /* Path to site override folder */ 83 | @siteFolder : 'site'; 84 | 85 | 86 | /******************************* 87 | Import Theme 88 | *******************************/ 89 | 90 | @import "theme.less"; 91 | 92 | /* Force override assets path */ 93 | @imagePath : './default/assets/images'; 94 | @fontPath : './default/assets/fonts'; 95 | 96 | /* End Config */ -------------------------------------------------------------------------------- /src/css/settings.less: -------------------------------------------------------------------------------- 1 | /*#########################################*/ 2 | /* Settings Styles */ 3 | /*#########################################*/ 4 | 5 | .settings.sidebar .nav .active i { 6 | color: #2185d0 !important; 7 | } 8 | 9 | .settings.sidebar .nav .active { 10 | font-weight: 700; 11 | } 12 | 13 | .sort.handle { 14 | vertical-align: middle; 15 | cursor: move; 16 | } 17 | -------------------------------------------------------------------------------- /src/css/styles.less: -------------------------------------------------------------------------------- 1 | // Import modules 2 | @site: 'default'; 3 | @reset: 'default'; 4 | 5 | @theme: @renderTheme; 6 | @themesFolder : 'themes'; 7 | @siteFolder : 'semantic/site'; 8 | 9 | /* Default site.variables */ 10 | @import "@{themesFolder}/default/globals/site.variables"; 11 | 12 | /* Packaged site.variables */ 13 | @import "@{themesFolder}/@{site}/globals/site.variables"; 14 | 15 | /* Component's site.variables */ 16 | @import (optional) "@{themesFolder}/@{theme}/globals/site.variables"; 17 | 18 | /* Site theme site.variables */ 19 | @import (optional) "@{siteFolder}/globals/site.variables"; 20 | 21 | @import 'main'; 22 | @import 'icons'; 23 | @import 'settings'; 24 | @import 'zindex'; -------------------------------------------------------------------------------- /src/css/themes/dark/collections/breadcrumb.overrides: -------------------------------------------------------------------------------- 1 | .ui.breadcrumb { 2 | .button-shadow(@gray-dark); 3 | 4 | color: @gray-light; 5 | border: 1px solid rgba(0, 0, 0, 0.6); 6 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); 7 | padding: 0.6em; 8 | border-radius: 4px; 9 | 10 | a { 11 | color: @gray-light; 12 | } 13 | 14 | .divider { 15 | color: @gray-light; 16 | } 17 | 18 | .active.section { 19 | color: darken(@white, 10%); 20 | &:hover { 21 | color: darken(@white, 20%); 22 | } 23 | } 24 | } 25 | 26 | #gradient { 27 | .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { 28 | background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color); 29 | background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color); 30 | background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color); 31 | background-repeat: no-repeat; 32 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback 33 | } 34 | } 35 | 36 | .button-shadow(@color) { 37 | #gradient > .vertical-three-colors(lighten(@color, 6%), @color, 60%, darken(@color, 4%)); 38 | filter: none; 39 | } 40 | -------------------------------------------------------------------------------- /src/css/themes/dark/collections/form.overrides: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Theme Overrides 3 | *******************************/ 4 | -------------------------------------------------------------------------------- /src/css/themes/dark/collections/form.variables: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Form 3 | *******************************/ 4 | 5 | @loaderDimmerColor: fadeout(@pageForeground, 50%); -------------------------------------------------------------------------------- /src/css/themes/dark/collections/menu.overrides: -------------------------------------------------------------------------------- 1 | #gradient { 2 | .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { 3 | background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color); 4 | background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color); 5 | background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color); 6 | background-repeat: no-repeat; 7 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback 8 | } 9 | } 10 | 11 | .button-shadow(@color) { 12 | #gradient > .vertical-three-colors(lighten(@color, 6%), @color, 60%, darken(@color, 4%)); 13 | filter: none; 14 | } 15 | 16 | .button-shadow-inverse(@color) { 17 | #gradient > .vertical-three-colors(darken(@color, 24%), darken(@color, 18%), 40%, darken(@color, 16%)); 18 | filter: none; 19 | } 20 | 21 | .ui.menu { 22 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); 23 | .button-shadow(@gray-dark); 24 | 25 | .item:hover { 26 | .button-shadow-inverse(@gray-dark); 27 | } 28 | 29 | &.inverted { 30 | .button-shadow(@primaryColor); 31 | 32 | .item:hover { 33 | .button-shadow-inverse(@primaryColor); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/css/themes/dark/collections/menu.variables: -------------------------------------------------------------------------------- 1 | @background: @gray-dark; 2 | @itemTextColor: @textColor; 3 | @hoverItemTextColor: #fff; 4 | @hoverItemBackground: darken(@gray-dark, 8%); 5 | @activeItemTextColor: #fff; 6 | @activeItemBackground: darken(@gray-darker, 8%); 7 | @labelBackground: #fff; 8 | @pressedItemBackground: darken(@gray-dark, 8%); 9 | @invertedBackground: @primaryColor; 10 | @invertedItemTextColor: white; 11 | @invertedHoverBackground: darken(@primaryColor, 12%); 12 | @invertedActiveBackground: darken(@primaryColor, 12%); 13 | @invertedMenuPressedBackground: darken(@primaryColor, 12%); 14 | @hoverItemBackground: lighten(@pageBackground, 10%); 15 | @tabularBorderColor: @gray; 16 | @tabularActiveBackground: lighten(@pageBackground, 10%); 17 | @tabularActiveColor: #fff; 18 | @paginationActiveBackground: darken(@gray-dark, 10%); 19 | @dropdownBackground: @gray-dark; 20 | @dropdownHoveredItemBackground: @gray-darker; 21 | @subMenuTextColor: @gray-light; 22 | @activeHoverItemBackground: @gray-darker; 23 | @labelBackground: @gray; 24 | @verticalBackground: @gray; 25 | @secondaryPointingActiveBorderColor: @gray-lighter; 26 | @secondaryActiveItemBackground: darken(@gray-darker, 5%); 27 | @secondaryHoverItemBackground: darken(@gray-darker, 5%); 28 | @secondaryActiveHoverItemBackground: darken(@gray-darker, 5%); 29 | 30 | @border: 1px solid @borderColor; 31 | @boxShadow: none; 32 | @textMenuItemColor: @gray; 33 | -------------------------------------------------------------------------------- /src/css/themes/dark/collections/message.overrides: -------------------------------------------------------------------------------- 1 | .ui.message { 2 | a { 3 | text-decoration: underline; 4 | } 5 | 6 | border: 1px solid rgba(0,0,0,.1); 7 | } 8 | -------------------------------------------------------------------------------- /src/css/themes/dark/collections/message.variables: -------------------------------------------------------------------------------- 1 | @background: @gray; 2 | @boxShadow: none; 3 | -------------------------------------------------------------------------------- /src/css/themes/dark/collections/table.variables: -------------------------------------------------------------------------------- 1 | @background: lighten(@pageBackground, 3%); 2 | @stripedBackground: lighten(@background, 3%); 3 | @activeBackgroundHover: lighten(@background, 12%); 4 | @basicTableCellBorder: 1px solid rgba(255, 255, 255, 0.1); 5 | @border: @borderWidth solid @borderColor; 6 | @headerBackground: darken(@pageBackground, 2%); 7 | @definitionPageBackground: transparent; 8 | @footerBackground: darken(@pageBackground, 2%); 9 | @activeBackgroundColor: @gray; 10 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/button.overrides: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tympanix/Electorrent/a15e6059c25a2cae5a72544be50fbf55868918cc/src/css/themes/dark/elements/button.overrides -------------------------------------------------------------------------------- /src/css/themes/dark/elements/button.variables: -------------------------------------------------------------------------------- 1 | @textColor: #fff; 2 | @backgroundColor: @gray-dark; 3 | @primaryTextColor: #fff; 4 | @positiveTextColor: #fff; 5 | @negativeTextColor: #fff; 6 | @hoverBackgroundColor: lighten(@gray-dark, 5%); 7 | @basicHoverBackground: lighten(@gray-dark, 10%); 8 | @activeBackgroundColor: @gray; 9 | @textShadow: 1px 1px 1px rgba(0, 0, 0, 0.3); 10 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/divider.variables: -------------------------------------------------------------------------------- 1 | @textColor: @gray-light; 2 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/header.variables: -------------------------------------------------------------------------------- 1 | @textColor: #c8c8c8; 2 | @blockBackground: @gray-darker; 3 | @attachedBackground: @gray-darker; 4 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/input.overrides: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/input.variables: -------------------------------------------------------------------------------- 1 | @placeholderColor: @gray-lighter; 2 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/label.variables: -------------------------------------------------------------------------------- 1 | @backgroundColor: @gray-dark; 2 | @labelActiveBackgroundColor: @gray-light; 3 | @ribbonShadowColor: @gray-dark; 4 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/list.variables: -------------------------------------------------------------------------------- 1 | @itemLinkColor: @textColor; 2 | @itemHeaderLinkColor: #fff; 3 | @itemDescriptionColor: @gray-light; 4 | @invertedLinkListItemColor: @gray-light; 5 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/segment.overrides: -------------------------------------------------------------------------------- 1 | .ui.piled.segments:after, 2 | .ui.piled.segments:before, 3 | .ui.piled.segment:after, 4 | .ui.piled.segment:before { 5 | background-color: lighten(@background, 5%); 6 | } 7 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/segment.variables: -------------------------------------------------------------------------------- 1 | @background: @pageForeground; 2 | @borderColor: @gray; 3 | @stackedPageBackground: darken(@background, 10%); 4 | @attachedBorder: @borderWidth solid @gray; 5 | -------------------------------------------------------------------------------- /src/css/themes/dark/elements/step.variables: -------------------------------------------------------------------------------- 1 | @backgroundColor: @gray-darker; 2 | 3 | @titleColor: @gray-lighter; 4 | @descriptionColor: @gray-light; 5 | 6 | @activeBackground: darken(@gray-darker, 5%); 7 | @activeColor: @white; 8 | @hoverBackground: @gray-dark; 9 | @activeHoverBackground: @gray-dark; 10 | 11 | @disabledColor: darken(@white, 30%); 12 | @disabledBackground: @gray; 13 | -------------------------------------------------------------------------------- /src/css/themes/dark/globals/site.overrides: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4, h5, h6, .ui.header { 2 | color: #c8c8c8; 3 | text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3); 4 | } 5 | 6 | // '.text' class is newly defined 7 | .text.primary, 8 | .text.primary:hover { 9 | color: @primaryColor; 10 | } 11 | 12 | .text.positive, 13 | .text.positive:hover { 14 | color: @positiveColor; 15 | } 16 | 17 | .text.negative, 18 | .text.negative:hover { 19 | color: @negativeColor; 20 | } 21 | 22 | .text.warning, 23 | .text.warning:hover { 24 | color: @warningColor; 25 | } 26 | 27 | .text.info, 28 | .text.info:hover { 29 | color: @infoColor; 30 | } 31 | 32 | // code color 33 | pre .code { 34 | color: @gray-light !important; 35 | } 36 | 37 | .ui.rating i.icon { 38 | color: @gray-light !important; 39 | } 40 | -------------------------------------------------------------------------------- /src/css/themes/dark/globals/site.variables: -------------------------------------------------------------------------------- 1 | /* Text colors */ 2 | @textColor : rgba(255, 255, 255, 0.9); 3 | @mutedTextColor : rgba(255, 255, 255, 0.8); 4 | @lightTextColor : rgba(255, 255, 255, 0.7); 5 | @unselectedTextColor : rgba(255, 255, 255, 0.5); 6 | @hoveredTextColor : rgba(255, 255, 255, 1); 7 | @pressedTextColor : rgba(255, 255, 255, 1); 8 | @selectedTextColor : rgba(255, 255, 255, 1); 9 | @disabledTextColor : rgba(255, 255, 255, 0.2); 10 | @highlightColor : @selectedTextColor; 11 | 12 | /* Form input */ 13 | @focusedFormBorderColor: #446f90; 14 | @focusedFormMutedBorderColor: #446f90; 15 | @inputBackground: @gray-darker; 16 | @inputColor: @white; 17 | @selectionTextColor: @textColor; 18 | @solidInternalBorderColor: lighten(@gray-darker, 10%); 19 | @focusColor: @white; 20 | @inputHighlightColor: @white; 21 | 22 | /* Page background and primary colors */ 23 | @gray-base: #000; 24 | @gray-darker: #272B30; 25 | @gray-dark: #3A3F44; 26 | @gray: #52575C; 27 | @gray-light: #7A8288; 28 | @gray-lighter: #999; 29 | @positiveColor: #62c462; 30 | @warningColor: #f89406; 31 | @negativeColor: #ee5f5b; 32 | @solidBorderColor: fadeout(@gray, 10%); 33 | @borderColor: fadeout(@gray, 10%); 34 | @pageBackground: @gray-darker; 35 | @pageMiddleground: lighten(@gray-darker, 2.5%); 36 | @pageForeground: lighten(@gray-darker, 5%); 37 | @overlayBackground: @pageBackground; 38 | @highlightBackground: @gray; 39 | @linkColor: #fff; 40 | @linkHoverColor: #fff; 41 | @padding-base-vertical: 8px; 42 | @padding-large-vertical: 14px; 43 | @invertedDisabledTextColor: #ccc; 44 | @disabledBorderColor: rgba(0, 0, 0, 0.6); 45 | @positiveTextColor: #fff; 46 | @positiveBackgroundColor: #62c462; 47 | @infoTextColor: #fff; 48 | @infoBackgroundColor: @infoColor; 49 | @warningTextColor: #fff; 50 | @warningBackgroundColor: #f89406; 51 | @warningBorderColor: darken(spin(@warningBackgroundColor, -10), 3%); 52 | @negativeTextColor: #fff; 53 | @negativeBackgroundColor: #ee5f5b; 54 | @negativeBorderColor: darken(spin(@negativeBackgroundColor, -10), 3%); 55 | 56 | 57 | @darkTextColor : rgba(255, 255, 255, 0.85); 58 | @invertedMutedTextColor : rgba(0, 0, 0, 0.6); 59 | @invertedLightTextColor : rgba(0, 0, 0, 0.4); 60 | 61 | @invertedUnselectedTextColor : rgba(0, 0, 0, 0.4); 62 | @invertedHoveredTextColor : rgba(0, 0, 0, 0.8); 63 | @invertedPressedTextColor : rgba(0, 0, 0, 0.9); 64 | @invertedSelectedTextColor : rgba(0, 0, 0, 0.95); 65 | @invertedDisabledTextColor : rgba(0, 0, 0, 0.2); 66 | 67 | @inputPlaceholderColor: @gray-lighter; 68 | -------------------------------------------------------------------------------- /src/css/themes/dark/modules/accordion.variables: -------------------------------------------------------------------------------- 1 | @styledTitleColor: white; 2 | @styledBackground: @gray-light; 3 | -------------------------------------------------------------------------------- /src/css/themes/dark/modules/checkbox.overrides: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Theme Overrides 3 | *******************************/ 4 | 5 | @font-face { 6 | font-family: 'Checkbox'; 7 | src: 8 | url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype') 9 | ; 10 | } 11 | 12 | /* Checkmark */ 13 | .ui.checkbox label:after, 14 | .ui.checkbox .box:after { 15 | font-family: 'Checkbox'; 16 | } 17 | 18 | /* Checked */ 19 | .ui.checkbox input:checked ~ .box:after, 20 | .ui.checkbox input:checked ~ label:after { 21 | content: '\e800'; 22 | } 23 | 24 | /* Indeterminate */ 25 | .ui.checkbox input:indeterminate ~ .box:after, 26 | .ui.checkbox input:indeterminate ~ label:after { 27 | font-size: 12px; 28 | content: '\e801'; 29 | } 30 | 31 | 32 | /* UTF Reference 33 | .check:before { content: '\e800'; } 34 | .dash:before { content: '\e801'; } 35 | .plus:before { content: '\e802'; } 36 | */ 37 | -------------------------------------------------------------------------------- /src/css/themes/dark/modules/checkbox.variables: -------------------------------------------------------------------------------- 1 | // @sliderLineColor: @gray; 2 | @toggleLaneBackground: @gray; 3 | // @toggleOnLaneColor: @primaryColor; 4 | // @radioActiveBulletColor: @gray-base; 5 | // @radioFocusBulletColor: @gray-base; 6 | // @radioActiveFocusBulletColor: @gray-base; 7 | // @bulletColor: @gray-base; 8 | // @checkboxActiveFocusCheckColor: @gray-base; 9 | 10 | @checkboxBackground: @pageBackground; 11 | 12 | @checkboxActiveBackground: @pageBackground; 13 | @checkboxColor: @white; 14 | 15 | @checkboxFocusBackground: darken(@pageBackground, 2%); 16 | @checkboxActiveBackground: @pageBackground; 17 | @checkboxActiveFocusBackground: darken(@pageBackground, 2%); -------------------------------------------------------------------------------- /src/css/themes/dark/modules/dropdown.overrides: -------------------------------------------------------------------------------- 1 | .ui.active.search.dropdown input.search:focus + .text { 2 | color: @gray-dark !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/themes/dark/modules/dropdown.variables: -------------------------------------------------------------------------------- 1 | @menuBackground: @gray-dark; 2 | @itemColor: @textColor; 3 | @hoveredItemColor: #fff; 4 | @hoveredItemBackground: darken(@gray-dark, 3.5%); 5 | @activeItemColor: #fff; 6 | @activeItemBackground: lighten(@gray-dark, 1.5%); 7 | @selectedBackground: lighten(@gray-dark, 1.5%); 8 | @selectionTextUnderlayColor: white; 9 | @menuHeaderColor: @textColor; 10 | @pointingArrowBackground: darken(@gray-dark, 5%); 11 | @pointingArrowBoxShadow: none; 12 | @selectionTextColor: @textColor; 13 | @selectionVisibleTextColor: @white; 14 | -------------------------------------------------------------------------------- /src/css/themes/dark/modules/modal.variables: -------------------------------------------------------------------------------- 1 | @contentBackground: @pageBackground; 2 | @headerBorder: #e5e5e5; 3 | @background: @gray-dark; 4 | @headerBackground: @gray-dark; 5 | @actionBackground: @gray-dark; 6 | -------------------------------------------------------------------------------- /src/css/themes/dark/modules/popup.variables: -------------------------------------------------------------------------------- 1 | @background: lighten(@pageBackground, 3%); 2 | @headerBorder: darken(@gray-darker, 5%); 3 | @arrowBackground: lighten(@pageBackground, 3%); 4 | @borderColor: @gray; 5 | -------------------------------------------------------------------------------- /src/css/themes/dark/modules/progress.variables: -------------------------------------------------------------------------------- 1 | @background: darken(@pageBackground, 5%); 2 | -------------------------------------------------------------------------------- /src/css/themes/dark/views/card.variables: -------------------------------------------------------------------------------- 1 | @background: @pageForeground; 2 | @descriptionColor: white; 3 | @extraDivider: 1px solid rgba(255, 255, 255, 0.05); -------------------------------------------------------------------------------- /src/css/themes/dark/views/feed.variables: -------------------------------------------------------------------------------- 1 | @metadataActionColor: @gray-lighter; 2 | @iconLabelColor: @gray-lighter; 3 | -------------------------------------------------------------------------------- /src/css/themes/dark/views/statistic.variables: -------------------------------------------------------------------------------- 1 | @valueColor: @white; 2 | -------------------------------------------------------------------------------- /src/css/themes/light/globals/site.variables: -------------------------------------------------------------------------------- 1 | /*------------------- 2 | Page 3 | --------------------*/ 4 | @pageBackground : #F0F0F0; 5 | @pageForeground : @white; 6 | @pageMiddleground : #F9FAFB; 7 | @highlightBackground : #F5F5F5; 8 | @overlayBackground : @pageBackground; 9 | -------------------------------------------------------------------------------- /src/css/themes/light/modules/checkbox.overrides: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Theme Overrides 3 | *******************************/ 4 | 5 | @font-face { 6 | font-family: 'Checkbox'; 7 | src: 8 | url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype') 9 | ; 10 | } 11 | 12 | /* Checkmark */ 13 | .ui.checkbox label:after, 14 | .ui.checkbox .box:after { 15 | font-family: 'Checkbox'; 16 | } 17 | 18 | /* Checked */ 19 | .ui.checkbox input:checked ~ .box:after, 20 | .ui.checkbox input:checked ~ label:after { 21 | content: '\e800'; 22 | } 23 | 24 | /* Indeterminate */ 25 | .ui.checkbox input:indeterminate ~ .box:after, 26 | .ui.checkbox input:indeterminate ~ label:after { 27 | font-size: 12px; 28 | content: '\e801'; 29 | } 30 | 31 | 32 | /* UTF Reference 33 | .check:before { content: '\e800'; } 34 | .dash:before { content: '\e801'; } 35 | .plus:before { content: '\e802'; } 36 | */ 37 | -------------------------------------------------------------------------------- /src/css/zindex.less: -------------------------------------------------------------------------------- 1 | /* Notifications in the upper right corner */ 2 | .notifications { 3 | z-index: 100000; 4 | } 5 | /* Settings, welcome, choose-a-server ect. */ 6 | .overlay { 7 | z-index: 500; 8 | } 9 | /* Disconnect, drag-and-drop ect. */ 10 | .popup { 11 | z-index: 400; 12 | } 13 | /* Right click menu */ 14 | .context.menu { 15 | z-index: 300; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electorrent 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 |
20 |
{{statusText}}
21 |
22 |
23 | 24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 | 32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 |
46 | 47 |
48 | 49 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/lib/certificates.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const { app } = require('electron') 5 | 6 | const config = require('./config') 7 | const electorrent = require('./electorrent') 8 | 9 | const CERT_DIR = path.join(app.getPath('userData'), 'certs') 10 | 11 | function ensureDir() { 12 | try { 13 | if (!fs.existsSync(CERT_DIR)) { 14 | fs.mkdirSync(CERT_DIR) 15 | } 16 | } catch (e) { 17 | console.error(err) 18 | } 19 | } 20 | 21 | /* 22 | * Make sure the certificate directory exists 23 | */ 24 | ensureDir() 25 | 26 | function isEmpty(object) { 27 | for (var prop in object) { 28 | if (object.hasOwnProperty(prop)) 29 | return false 30 | } 31 | 32 | return true 33 | } 34 | 35 | function pemEncode(str, n) { 36 | if (!Buffer.isBuffer(str)) { 37 | str = Buffer.from(str) 38 | } 39 | str = str.toString("base64") 40 | 41 | var ret = [] 42 | 43 | for (var i = 1; i <= str.length; i++) { 44 | ret.push(str[i - 1]) 45 | var mod = i % n 46 | 47 | if (mod === 0) { 48 | ret.push('\n') 49 | } 50 | } 51 | 52 | var returnString = `-----BEGIN CERTIFICATE-----\n${ret.join('')}\n-----END CERTIFICATE-----` 53 | 54 | return returnString 55 | } 56 | 57 | function get(server, callback) { 58 | var options = { 59 | hostname: server.ip, 60 | port: server.port, 61 | path: server.path, 62 | agent: false, 63 | rejectUnauthorized: false, 64 | ciphers: 'ALL' 65 | } 66 | 67 | var req = https.get(options, function(res) { 68 | var certificate = res.socket.getPeerCertificate() 69 | if (isEmpty(certificate) || certificate === null) { 70 | callback(new Error('The website did not provide a certificate')) 71 | } else { 72 | let torrentWindow = electorrent.getWindow() 73 | torrentWindow.webContents.send('certificate-modal-node', certificate, server); 74 | callback(null, certificate) 75 | } 76 | }) 77 | 78 | req.on('error', function(e) { 79 | callback(e) 80 | }) 81 | 82 | req.end() 83 | } 84 | 85 | function installCertificate(cert, callback) { 86 | if (!cert.raw) { 87 | return callback(new Error('Could not install invalid certificate')) 88 | } 89 | 90 | const pemData = pemEncode(cert.raw, 64) 91 | const fingerprint = cert.fingerprint.split(":").join("").toLowerCase() 92 | 93 | const pemFilename = path.join(CERT_DIR, `${fingerprint}.crt`) 94 | fs.writeFile(pemFilename, pemData, (err) => callback(err, fingerprint)) 95 | } 96 | 97 | function loadCertificate(fingerprint) { 98 | const certPath = path.join(CERT_DIR, `${fingerprint}.crt`) 99 | if (!fs.existsSync(certPath)) { 100 | return 101 | } 102 | try { 103 | return fs.readFileSync(certPath) 104 | } catch (e) { 105 | return 106 | } 107 | } 108 | 109 | module.exports = { 110 | get: get, 111 | installCertificate: installCertificate, 112 | loadCertificate: loadCertificate, 113 | } -------------------------------------------------------------------------------- /src/lib/electorrent.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | 3 | // Reference to the main window of the application 4 | var mainWindow; 5 | 6 | exports.setWindow = function(newWindow) { 7 | mainWindow = newWindow; 8 | } 9 | 10 | exports.getWindow = function() { 11 | return mainWindow; 12 | } 13 | 14 | exports.isDevelopment = function() { 15 | try { 16 | if (electron.app.isPackaged) { 17 | return true; 18 | } 19 | return Number.parseInt(process.env.ELECTRON_IS_DEV, 10) === 1 20 | } catch (e) { 21 | return false 22 | } 23 | } -------------------------------------------------------------------------------- /src/lib/logger.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | const electron = require('electron'); 3 | const winston = require('winston'); 4 | const path = require('path'); 5 | const program = require('yargs').parse(process.argv); 6 | 7 | const {app} = electron; 8 | 9 | const loglevel = getLogLevel(); 10 | 11 | const logfile = path.join(app.getPath('userData'), 'logfile.log') 12 | const logger = new (winston.Logger)({ 13 | level: loglevel, 14 | transports: [ 15 | new (winston.transports.File)({ filename: logfile }) 16 | ] 17 | }); 18 | 19 | function getLogLevel() { 20 | if (program.debug){ 21 | return 'debug' 22 | } else if (program.verbose) { 23 | return 'verbose' 24 | } else { 25 | return 'info' 26 | } 27 | } 28 | 29 | module.exports = logger; 30 | -------------------------------------------------------------------------------- /src/lib/startup.js: -------------------------------------------------------------------------------- 1 | const {ProgId, ShellOption, Regedit} = require('electron-regedit') 2 | const {app} = require('electron') 3 | const path = require('path') 4 | const {spawn} = require('child_process') 5 | const Q = require('q') 6 | 7 | new ProgId({ 8 | description: 'Torrent File', 9 | friendlyAppName: true, 10 | icon: 'resources/torrentfile.ico', 11 | squirrel: true, 12 | extensions: ['torrent'] 13 | }) 14 | 15 | function executeSquirrelCommand(args) { 16 | let defer = Q.defer() 17 | var updateDotExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe'); 18 | var child = spawn(updateDotExe, args, { detached: true }); 19 | child.on('close', function(code) { 20 | if (code === 0) { 21 | defer.resolve(code) 22 | } else { 23 | defer.reject(code) 24 | } 25 | }); 26 | return defer.promise 27 | } 28 | 29 | function makeShortcut() { 30 | var target = path.basename(process.execPath); 31 | return executeSquirrelCommand(["--createShortcut", target]); 32 | } 33 | 34 | function removeShortcut() { 35 | var target = path.basename(process.execPath); 36 | return executeSquirrelCommand(["--removeShortcut", target]); 37 | } 38 | 39 | function checkSquirrel() { 40 | if (process.platform !== 'win32') { 41 | return false; 42 | } 43 | 44 | var squirrelCommand = process.argv[1]; 45 | switch (squirrelCommand) { 46 | case '--squirrel-install': 47 | case '--squirrel-updated': 48 | Q.all([ 49 | Regedit.installAll(), 50 | makeShortcut() 51 | ]).finally(() => app.quit()) 52 | return true; 53 | case '--squirrel-uninstall': 54 | Q.all([ 55 | Regedit.uninstallAll(), 56 | removeShortcut() 57 | ]).finally(() => app.quit()) 58 | return true; 59 | case '--squirrel-obsolete': 60 | app.quit(); 61 | return true; 62 | } 63 | } 64 | 65 | module.exports = checkSquirrel() -------------------------------------------------------------------------------- /src/lib/themes.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const DIR = path.join(__dirname, '../css/themes') 4 | 5 | function capitalize(string) { 6 | return string.charAt(0).toUpperCase() + string.slice(1); 7 | } 8 | 9 | function themes() { 10 | let themes = fs.readdirSync(DIR) 11 | .filter(function(file) { 12 | let stat = fs.statSync(path.join(DIR, file)) 13 | return stat.isFile() && path.extname(file) === '.css' 14 | }) 15 | 16 | return themes.map(function(theme) { 17 | return { 18 | css: path.join(DIR, theme), 19 | basename: path.basename(theme, '.css'), 20 | theme: capitalize(path.basename(theme, '.css')) 21 | } 22 | }) 23 | } 24 | 25 | module.exports = themes -------------------------------------------------------------------------------- /src/lib/torrents.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | const electron = require('electron'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | // Custom imports 7 | const electorrent = require('./electorrent'); 8 | 9 | // Electron modules 10 | const {dialog} = electron; 11 | 12 | function notify({ title = '', message = '', type = 'info'}) { 13 | var win = electorrent.getWindow(); 14 | if (!win) return; 15 | 16 | win.webContents.send('notify', { 17 | title: title, 18 | message: message, 19 | type: type 20 | }); 21 | } 22 | 23 | async function browse(askUploadOptions){ 24 | var win = electorrent.getWindow(); 25 | 26 | let result = await dialog.showOpenDialog(win, { 27 | title: 'Open Torrent File', 28 | buttonLabel: 'Add Torrent', 29 | filters: [ 30 | {name: 'Torrent', extensions: ['torrent']} 31 | ], 32 | properties: ['openFile', 'multiSelections'] 33 | }) 34 | 35 | if (!result.canceled) { 36 | processFiles(result.filePaths, askUploadOptions) 37 | } 38 | } 39 | 40 | function processFiles(filepaths, askUploadOptions) { 41 | var win = electorrent.getWindow(); 42 | 43 | if (!filepaths) return; 44 | 45 | var torrents = filepaths.filter(filterFiles); 46 | 47 | if (torrents.length === 0) { 48 | notify({ 49 | title: 'Oopsy Daisy!', 50 | message: 'Seems like you chose an incorrect file type!', 51 | type: 'negative' 52 | }) 53 | return; 54 | } 55 | 56 | torrents.forEach(function(file){ 57 | fs.readFile(file, (err, data) => { 58 | if (err) throw err; 59 | 60 | win.webContents.send('torrentfiles', data, path.basename(file), askUploadOptions); 61 | 62 | }); 63 | }) 64 | } 65 | 66 | function filterFiles(path){ 67 | return path.endsWith('.torrent'); 68 | } 69 | 70 | exports.browse = browse; 71 | exports.readFiles = processFiles 72 | -------------------------------------------------------------------------------- /src/lib/worker.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | class InstanceWorker { 5 | 6 | constructor(factory, worker) { 7 | this._worker = worker 8 | this._factory = factory 9 | this._instance = null 10 | this._worker.onmessage = this._onMessage.bind(this) 11 | this._worker.onmessageerror = this._onmessageerror.bind(this) 12 | this._worker.onerror = this._onerror.bind(this) 13 | this._callback = this._callback.bind(this) 14 | } 15 | 16 | _callback(id) { 17 | return function(err, value) { 18 | if (err) { 19 | this._worker.postMessage([id, this.__parse_error(err), null]) 20 | } else { 21 | this._worker.postMessage([id, null, this.__parse_safe(value)]) 22 | } 23 | }.bind(this) 24 | } 25 | 26 | __parse_safe(o) { 27 | return JSON.parse(JSON.stringify(o, (_,v) => v === undefined ? null : v)) 28 | } 29 | 30 | __parse_error(err) { 31 | let _err = {} 32 | let o = err 33 | while (o) { 34 | for (let k of Object.getOwnPropertyNames(o)) { 35 | if (typeof err[k] === 'string') { 36 | _err[k] = err[k] 37 | } 38 | } 39 | o = Object.getPrototypeOf(o) 40 | } 41 | return _err 42 | } 43 | 44 | instantiate() { 45 | this._instance = new this._factory(...arguments) 46 | let cb = arguments[arguments.length - 1] 47 | cb(null, true) 48 | } 49 | 50 | _onmessageerror(msg) { 51 | throw new Error("message error in worker thread") 52 | } 53 | 54 | _onerror(msg) { 55 | throw new Error("error in worker thread") 56 | } 57 | 58 | _onMessage(msg) { 59 | var data = msg.data 60 | 61 | if (data.constructor !== Array) { 62 | return self.postMessage([-1, "Invalid RPC call", null]) 63 | } 64 | 65 | var id = data[0] 66 | var call = data[1] 67 | var args = data.slice(2) 68 | 69 | let fn = null 70 | let target = null 71 | if (this[call] && typeof this[call] === 'function') { 72 | fn = this[call] 73 | target = this 74 | } else if (this._worker.hasOwnProperty(call) && typeof this._worker[call] === 'function') { 75 | fn = this._worker[call] 76 | target = this._worker 77 | } else if (this._instance && typeof this._instance[call] === 'function') { 78 | fn = this._instance[call] 79 | target = this._instance 80 | } else { 81 | this._worker.postMessage([id, `${call} is not a function`, null]) 82 | } 83 | 84 | if (fn) { 85 | fn.apply(target, [...args, this._callback(id)]) 86 | } 87 | } 88 | } 89 | 90 | 91 | module.exports = { 92 | InstanceWorker, 93 | } 94 | -------------------------------------------------------------------------------- /src/scripts/bittorrent/deluge/index.ts: -------------------------------------------------------------------------------- 1 | export { DelugeClient } from "./delugeservice" 2 | export { DelugeTorrent } from "./torrentd" -------------------------------------------------------------------------------- /src/scripts/bittorrent/deluge/torrentd.ts: -------------------------------------------------------------------------------- 1 | import {Torrent} from "../abstracttorrent"; 2 | 3 | export class DelugeTorrent extends Torrent { 4 | 5 | public state: string 6 | 7 | constructor(hash: string, data: Record) { 8 | super({ 9 | hash: hash, /* Hash (string): unique identifier for the torrent */ 10 | name: data.name, /* Name (string): the name of the torrent */ 11 | size: data.total_wanted, /* Size (integer): size of the file to be downloaded in bytes */ 12 | percent: data.progress*10, /* Percent (integer): completion in per-mille (100% = 1000) */ 13 | downloaded: data.total_done, /* Downloaded (integer): number of bytes */ 14 | uploaded: data.total_uploaded, /* Uploaded (integer): number of bytes */ 15 | ratio: data.ratio, /* Ratio (integer): integer i per-mille (1:1 = 1000) */ 16 | uploadSpeed: data.upload_payload_rate, /* Upload Speed (integer): bytes per second */ 17 | downloadSpeed: data.download_payload_rate, /* Download Speed (integer): bytes per second */ 18 | eta: data.eta, /* ETA (integer): second to completion */ 19 | label: undefined, /* Label (string): group/category identification */ 20 | peersConnected: data.num_peers, /* Peers Connected (integer): number of peers connected */ 21 | peersInSwarm: data.total_peers, /* Peers In Swarm (integer): number of peers in the swarm */ 22 | seedsConnected: data.num_seeds, /* Seeds Connected (integer): number of connected seeds */ 23 | seedsInSwarm: data.total_seeds, /* Seeds In Swarm (integer): number of connected seeds in swarm */ 24 | torrentQueueOrder: data.queue+1, /* Queue (integer): the number in the download queue */ 25 | statusMessage: data.state, /* Status (string): the current status of the torrent (e.g. downloading) */ 26 | dateAdded: data.time_added*1000, /* Date Added (integer): number of milliseconds unix time */ 27 | dateCompleted: undefined, /* Date Completed (integer): number of milliseconds unix time */ 28 | savePath: data.save_path, /* Save Path (string): the path at which the downloaded content is saved */ 29 | }); 30 | 31 | /* 32 | * Additional data that does not match the default scheme above 33 | * may be added as extra fields. This can be done in the manner below 34 | */ 35 | this.state = data.state 36 | } 37 | 38 | isStatusError(): boolean { 39 | return this.state === "Error" 40 | }; 41 | 42 | isStatusStopped(): boolean { 43 | return this.state === "Paused" 44 | }; 45 | 46 | isStatusQueued(): boolean { 47 | return this.state === "Queued" 48 | }; 49 | 50 | isStatusCompleted(): boolean { 51 | return this.percent === 1000 52 | }; 53 | 54 | isStatusDownloading(): boolean { 55 | return this.state === "Downloading" 56 | }; 57 | 58 | isStatusSeeding() { 59 | return this.state === "Seeding" 60 | }; 61 | 62 | isStatusPaused() { 63 | return false 64 | }; 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/scripts/bittorrent/index.ts: -------------------------------------------------------------------------------- 1 | export { Torrent } from "./abstracttorrent" 2 | export { TorrentClient } from "./torrentclient" 3 | 4 | // Client API implementations for bittorrent providers 5 | export { DelugeClient, DelugeTorrent } from "./deluge" 6 | export { QBittorrentClient, QBittorrentTorrent } from "./qbittorrent" 7 | export { RtorrentClient, RtorrentTorrent } from "./rtorrent" 8 | export { SynologyClient, SynologyTorrent } from "./synology" 9 | export { TransmissionClient, TransmissionTorrent } from "./transmission" 10 | export { UtorrentClient, UtorrentTorrent } from "./utorrent" 11 | -------------------------------------------------------------------------------- /src/scripts/bittorrent/qbittorrent/index.ts: -------------------------------------------------------------------------------- 1 | export { QBittorrentClient } from "./qbittorrentservice" 2 | export { QBittorrentTorrent } from "./torrentq" 3 | export { QBittorrentUploadOptions } from "./qbittorrentservice" -------------------------------------------------------------------------------- /src/scripts/bittorrent/qbittorrent/torrentq.ts: -------------------------------------------------------------------------------- 1 | import { Torrent } from '../abstracttorrent'; 2 | 3 | export class QBittorrentTorrent extends Torrent { 4 | 5 | // Field specific for qBittorrent 6 | state: string 7 | creationDate: string 8 | pieceSize: number 9 | comment: string 10 | totalWasted: number 11 | uploadedSession: number 12 | downloadedSession: number 13 | upLimit: number 14 | downLimit: number 15 | timeElapsed: number 16 | seedingTime: number 17 | connectionsLimit: number 18 | createdBy: number 19 | downAvgSpeed: number 20 | lastSeen: number 21 | peers: number 22 | havePieces: any 23 | totalPieces: number 24 | reannounce: string 25 | upSpeedAvg: number 26 | forceStart: boolean 27 | sequentialDownload: boolean 28 | 29 | 30 | constructor(hash: string, data: Record) { 31 | super({ 32 | hash: hash, 33 | name: data.name, 34 | size: data.size || data.total_size, 35 | percent: data.progress && (data.progress * 1000), 36 | downloaded: data.total_downloaded, 37 | uploaded: data.total_uploaded, 38 | ratio: data.share_ration || data.ratio, 39 | uploadSpeed: data.up_speed || data.upspeed, 40 | downloadSpeed: data.dl_speed || data.dlspeed, 41 | eta: data.eta, 42 | label: data.category || data.label, 43 | peersConnected: data.num_leechs, 44 | peersInSwarm: data.num_incomplete, 45 | seedsConnected: data.num_seeds, 46 | seedsInSwarm: data.num_complete, 47 | torrentQueueOrder: data.priority, 48 | statusMessage: undefined, // Not supplied 49 | dateAdded: (data.addition_date || data.added_on) * 1000 || undefined, 50 | dateCompleted: (data.completion_date || data.completion_on) * 1000 || undefined, 51 | savePath: data.save_path, 52 | }); 53 | 54 | this.state = data.state 55 | this.creationDate = data.creation_date; 56 | this.pieceSize = data.piece_size; 57 | this.comment = data.comment; 58 | this.totalWasted = data.total_wasted; 59 | this.uploadedSession = data.total_uploaded_session; 60 | this.downloadedSession = data.total_downloaded_session; 61 | this.upLimit = data.up_limit; 62 | this.downLimit = data.dl_limit; 63 | this.timeElapsed = data.time_elapsed; 64 | this.seedingTime = data.seeding_time; 65 | this.connectionsLimit = data.nb_connections_limit; 66 | this.createdBy = data.created_by; 67 | this.downAvgSpeed = data.dl_speed_avg; 68 | this.lastSeen = data.last_seen; 69 | this.peers = data.peers; 70 | this.havePieces = data.pieces_have; 71 | this.totalPieces = data.pieces_num; 72 | this.reannounce = data.reannounce; 73 | this.upSpeedAvg = data.up_speed_avg; 74 | this.forceStart = data.force_start; 75 | this.sequentialDownload = data.seq_dl; 76 | } 77 | 78 | getStatus(...statusOr: string[]) { 79 | return statusOr.includes(this.state); 80 | } 81 | 82 | isStatusError() { 83 | return this.getStatus('error', 'missingFiles'); 84 | }; 85 | isStatusStopped() { 86 | return this.getStatus('paused', 'pausedUP', 'pausedDL', 'stopped', 'stoppedUP', 'stoppedDL') && !this.isStatusCompleted(); 87 | }; 88 | isStatusQueued() { 89 | return this.getStatus('queuedUP', 'queuedDL', 'allocating'); 90 | }; 91 | isStatusCompleted() { 92 | return (this.percent === 1000) || this.getStatus('checkingUP', 'moving'); 93 | }; 94 | isStatusDownloading() { 95 | return this.getStatus('downloading', 'stalledDL', 'metaDL', 'forcedDL') || this.isStatusChecking(); 96 | }; 97 | isStatusSeeding() { 98 | return this.getStatus('uploading', 'stalledUP', 'forcedUP'); 99 | }; 100 | isStatusPaused() { 101 | /* qBittorrent only has started and stopped torrents */ 102 | return false; 103 | }; 104 | 105 | /* Additional custom states */ 106 | isStatusChecking() { 107 | return this.getStatus('checkingDL', 'checkingUP', 'checkingResumeData'); 108 | } 109 | 110 | manualStatusText() { 111 | if (this.isStatusChecking()) { 112 | return 'Checking'; 113 | } else { 114 | return super.manualStatusText(); 115 | } 116 | }; 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/scripts/bittorrent/rtorrent/index.ts: -------------------------------------------------------------------------------- 1 | export { RtorrentClient } from "./rtorrentservice" 2 | export { RtorrentTorrent } from "./torrentr" -------------------------------------------------------------------------------- /src/scripts/bittorrent/synology/index.ts: -------------------------------------------------------------------------------- 1 | export { SynologyClient } from "./synologyservice" 2 | export { SynologyTorrent } from "./synologytorrent" 3 | -------------------------------------------------------------------------------- /src/scripts/bittorrent/transmission/index.ts: -------------------------------------------------------------------------------- 1 | export { TransmissionClient } from "./transmissionservice" 2 | export { TransmissionTorrent } from "./torrentt" -------------------------------------------------------------------------------- /src/scripts/bittorrent/transmission/torrentt.ts: -------------------------------------------------------------------------------- 1 | import {Torrent} from "../abstracttorrent"; 2 | 3 | export class TransmissionTorrent extends Torrent { 4 | 5 | data: Record 6 | error: number 7 | trackers: string[] 8 | 9 | constructor(data: Record) { 10 | super({ 11 | hash: data.hashString, /* Hash (string): unique identifier for the torrent */ 12 | name: data.name, /* Name (string): the name of the torrent */ 13 | size: data.totalSize, /* Size (integer): size of the file to be downloaded in bytes */ 14 | percent: data.percentDone * 1000, /* Percent (integer): completion in per-mille (100% = 1000) */ 15 | downloaded: data.downloadedEver, /* Downloaded (integer): number of bytes */ 16 | uploaded: data.uploadedEver, /* Uploaded (integer): number of bytes */ 17 | ratio: data.uploadRatio, /* Ratio (integer): integer i per-mille (1:1 = 1000) */ 18 | uploadSpeed: data.rateUpload, /* Upload Speed (integer): bytes per second */ 19 | downloadSpeed: data.rateDownload, /* Download Speed (integer): bytes per second */ 20 | eta: data.eta, /* ETA (integer): second to completion */ 21 | label: data.comment, /* Label (string): group/category identification */ 22 | peersConnected: data.peersSendingToUs, /* Peers Connected (integer): number of peers connected */ 23 | peersInSwarm: data.peersConnected, /* Peers In Swarm (integer): number of peers in the swarm */ 24 | seedsConnected: data.peersGettingFromUs, /* Seeds Connected (integer): number of connected seeds */ 25 | seedsInSwarm: data.peersConnected, /* Seeds In Swarm (integer): number of connected seeds in swarm */ 26 | torrentQueueOrder: data.queuePosition, /* Queue (integer): the number in the download queue */ 27 | statusMessage: '', /* Status (string): the current status of the torrent (e.g. downloading) */ 28 | dateAdded: data.addedDate * 1000, /* Date Added (integer): number of milliseconds unix time */ 29 | dateCompleted: data.doneDate, /* Date Completed (integer): number of milliseconds unix time */ 30 | savePath: data.downloadDir, /* Save Path (string): the path at which the downloaded content is saved */ 31 | }); 32 | 33 | this.data = data; 34 | 35 | 36 | this.status = data.status; 37 | this.error = data.error; 38 | this.trackers = data.trackers.map((tracker: Record) => tracker.announce) 39 | 40 | // Extra Field: Recheck Progress aka Verifying. 41 | if(this.isStatusVerifying()) { 42 | this.percent = data.recheckProgress * 1000; 43 | } 44 | 45 | } 46 | 47 | isStatusVerifying(): boolean { 48 | return this.status === 2; 49 | }; 50 | 51 | isStatusError(): boolean { 52 | // Error = 0 means ok, 1 means tracker warn, 2 means tracker error, 3 local error. 53 | return this.error !== 0; 54 | }; 55 | 56 | isStatusStopped(): boolean { 57 | return this.status === 0 && this.percent !== 1000; 58 | }; 59 | 60 | isStatusQueued(): boolean { 61 | return 62 | }; 63 | 64 | isStatusCompleted(): boolean { 65 | return this.percent === 1000 && this.status === 0 && this.error === 0; 66 | }; 67 | 68 | isStatusDownloading(): boolean { 69 | return this.status === 4 || this.status === 2; 70 | }; 71 | 72 | isStatusSeeding(): boolean { 73 | return this.status === 6; 74 | }; 75 | 76 | isStatusPaused(): boolean { 77 | return ; 78 | }; 79 | 80 | 81 | statusText(): string { 82 | if (this.isStatusSeeding()){ 83 | return 'Seeding'; 84 | } else if (this.isStatusDownloading()){ 85 | return 'Downloading'; 86 | } else if (this.isStatusError()){ 87 | return 'Error'; 88 | } else if (this.isStatusStopped()){ 89 | return 'Stopped'; 90 | } else if (this.isStatusCompleted()){ 91 | return 'Completed'; 92 | } else if (this.isStatusPaused()){ 93 | return 'Paused'; 94 | } else { 95 | return 'Unknown'; 96 | } 97 | 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/scripts/bittorrent/transmission/transmissionconfig.ts: -------------------------------------------------------------------------------- 1 | export let fields = [ 2 | "activityDate", 3 | "addedDate", 4 | "bandwidthPriority", 5 | "comment", 6 | "corruptEver", 7 | "creator", 8 | "dateCreated", 9 | "desiredAvailable", 10 | "doneDate", 11 | "downloadDir", 12 | "downloadedEver", 13 | "downloadLimit", 14 | "downloadLimited", 15 | "error", 16 | "errorString", 17 | "eta", 18 | "etaIdle", 19 | "files", 20 | "fileStats", 21 | "hashString", 22 | "haveUnchecked", 23 | "haveValid", 24 | "honorsSessionLimits", 25 | "id", 26 | "isFinished", 27 | "isPrivate", 28 | "isStalled", 29 | "leftUntilDone", 30 | "magnetLink", 31 | "manualAnnounceTime", 32 | "maxConnectedPeers", 33 | "metadataPercentComplete", 34 | "name", 35 | "peer-limit", 36 | "peers", 37 | "peersConnected", 38 | "peersFrom", 39 | "peersGettingFromUs", 40 | "peersSendingToUs", 41 | "percentDone", 42 | "pieces", 43 | "pieceCount", 44 | "pieceSize", 45 | "priorities", 46 | "queuePosition", 47 | "rateDownload", 48 | "rateUpload", 49 | "recheckProgress", 50 | "secondsDownloading", 51 | "secondsSeeding", 52 | "seedIdleLimit", 53 | "seedIdleMode", 54 | "seedRatioLimit", 55 | "seedRatioMode", 56 | "sizeWhenDone", 57 | "startDate", 58 | "status", 59 | "trackers", 60 | "trackerStats", 61 | "totalSize", 62 | "torrentFile", 63 | "uploadedEver", 64 | "uploadLimit", 65 | "uploadLimited", 66 | "uploadRatio", 67 | "wanted", 68 | "webseeds", 69 | "webseedsSendingToUs", 70 | ]; 71 | 72 | -------------------------------------------------------------------------------- /src/scripts/bittorrent/utorrent/index.ts: -------------------------------------------------------------------------------- 1 | export { UtorrentClient } from "./utorrentservice" 2 | export { UtorrentTorrent } from "./torrentu" 3 | -------------------------------------------------------------------------------- /src/scripts/controllers/theme.ts: -------------------------------------------------------------------------------- 1 | export let themeController = ["$scope", "configService", function ($scope, $config) { 2 | let settings = $config.getAllSettings() 3 | $scope.theme = settings.ui.theme 4 | 5 | $scope.$on('new:settings', function(e, settings) { 6 | $scope.theme = settings.ui.theme || $scope.theme 7 | }) 8 | }]; 9 | -------------------------------------------------------------------------------- /src/scripts/controllers/welcome.ts: -------------------------------------------------------------------------------- 1 | export let welcomeController = ["$scope", "$timeout", "$bittorrent", "$btclients", "electron", "configService", "notificationService", "Server", function ($scope, $timeout, $bittorrent, $btclients, electron, config, $notify, Server) { 2 | 3 | $scope.connecting = false; 4 | $scope.btclients = $btclients; 5 | $scope.server = new Server() 6 | $scope.advanced = false 7 | 8 | function clearForm() { 9 | $scope.server = new Server() 10 | } 11 | 12 | $scope.connect = function() { 13 | $scope.connecting = true; 14 | 15 | $scope.server.connect().then(function() { 16 | return config.saveServer($scope.server) 17 | }).then(function(){ 18 | $scope.$emit('connect:server', $scope.server) 19 | clearForm() 20 | $notify.ok("Success!", "Hooray! Welcome to Electorrent") 21 | }).catch(function(err) { 22 | console.error(err); 23 | }).finally(function() { 24 | $scope.connecting = false; 25 | }) 26 | } 27 | 28 | $scope.setPath = function() { 29 | if ($scope.server.client) { 30 | $scope.server.setPath() 31 | } 32 | } 33 | 34 | function saveServer(ip, port, username, password, client){ 35 | let server = new Server(ip, port, username, password, client) 36 | 37 | $bittorrent.setServer(server) 38 | 39 | config.saveServer(server).then(function(){ 40 | $scope.$emit('show:torrents'); 41 | clearForm() 42 | $notify.ok("Success!", "Hooray! Welcome to Electorrent") 43 | }).catch(function(){ 44 | $notify.alert("Oops!", "Could not save settings?!") 45 | }) 46 | } 47 | 48 | }] 49 | -------------------------------------------------------------------------------- /src/scripts/directives/actionheader.ts: -------------------------------------------------------------------------------- 1 | 2 | export let actionHeader = [ "$rootScope", "$compile", "electron", 3 | function ($rootScope, $compile, electron) { 4 | var actionHeader = null; 5 | var toggleAble = []; 6 | 7 | return { 8 | restrict: "A", 9 | scope: { 10 | actions: "=", 11 | click: "=", 12 | labels: "=", 13 | bind: "=?", 14 | enabled: "=?", 15 | }, 16 | compile: compile, 17 | }; 18 | 19 | function compile(element) { 20 | actionHeader = element; 21 | return link; 22 | } 23 | 24 | function render(scope, element, attr?) { 25 | if (!scope.actions) return; 26 | 27 | toggleAble = []; 28 | 29 | // Remove existing dom 30 | actionHeader.empty(); 31 | 32 | // Insert new dom elements 33 | scope.actions.forEach(function (item) { 34 | if (item.type === "button") { 35 | appendButton(element, item, scope); 36 | } else if (item.type === "labels") { 37 | appendLabelsDropdown(element, item, scope); 38 | } else if (item.type === "dropdown") { 39 | appendDropdown(element, item, scope); 40 | } 41 | }); 42 | 43 | scope.$watch( 44 | function () { 45 | return scope.enabled; 46 | }, 47 | function (disable) { 48 | toggleActive(disable); 49 | } 50 | ); 51 | } 52 | 53 | function toggleActive(disable) { 54 | toggleAble.forEach(function (element) { 55 | if (disable) { 56 | element.addClass("disabled"); 57 | } else { 58 | element.removeClass("disabled"); 59 | } 60 | }); 61 | } 62 | 63 | function appendDropdown(list, item, scope) { 64 | var dropdown = angular.element(''); 65 | dropdown.addClass(item.color); 66 | addIcon(dropdown, "plus"); 67 | 68 | if (item.role) { 69 | dropdown.attr("data-role", item.role); 70 | } 71 | 72 | var text = angular.element(''); 73 | text.append(item.label); 74 | dropdown.append(text); 75 | 76 | var menu = angular.element(''); 77 | 78 | item.actions.forEach(function (action) { 79 | var option = angular.element('
'); 80 | option.append(action.label); 81 | 82 | option.bind("click", function () { 83 | scope.click(action.click, action.label); 84 | }); 85 | 86 | menu.append(option); 87 | }); 88 | 89 | dropdown.append(menu); 90 | 91 | $compile(dropdown)(scope); 92 | list.append(dropdown); 93 | } 94 | 95 | function appendButton(list, item, scope) { 96 | var button = angular.element(''); 97 | button.addClass(item.color); 98 | 99 | if (item.role) { 100 | button.attr("data-role", item.role); 101 | } 102 | 103 | addIcon(button, item.icon); 104 | 105 | button.append(item.label); 106 | 107 | button.bind("click", function () { 108 | scope.click(item.click, item.label); 109 | }); 110 | 111 | if (!item.alwaysActive) { 112 | toggleAble.push(button); 113 | } 114 | 115 | list.append(button); 116 | } 117 | 118 | function appendLabelsDropdown(list, item, scope) { 119 | var dropdown = angular.element( 120 | '' 121 | ); 122 | 123 | dropdown.attr("data-role", "labels"); 124 | 125 | scope.addLabel = function (label, create) { 126 | scope.click(item.click, item.label + " " + label, label, create); 127 | }; 128 | 129 | $compile(dropdown)(scope); 130 | list.append(dropdown); 131 | } 132 | 133 | function addIcon(item, iconName) { 134 | var icon = angular.element(""); 135 | icon.addClass("ui " + iconName + " icon"); 136 | item.append(icon); 137 | } 138 | 139 | function link(scope, element, attr) { 140 | scope.program = electron.program; 141 | 142 | render(scope, element, attr); 143 | 144 | // Bind show function to scope variable 145 | scope.bind = {}; 146 | 147 | scope.$watch( 148 | function () { 149 | return $rootScope.$btclient; 150 | }, 151 | function (client) { 152 | if (client) { 153 | render(scope, element, attr); 154 | } 155 | } 156 | ); 157 | } 158 | }, 159 | ]; 160 | 161 | -------------------------------------------------------------------------------- /src/scripts/directives/add-torrent-modal/add-torrent-modal.controller.ts: -------------------------------------------------------------------------------- 1 | import { IRootScopeService } from "angular"; 2 | import { TorrentUploadOptions } from "../../bittorrent/torrentclient"; 3 | import { ModalController } from "../modal/modal.controller"; 4 | import { AddTorrentModalScope } from "./add-torrent-modal.directive"; 5 | 6 | export class AddTorrentModalController { 7 | 8 | static $inject = ["$scope", "$rootScope"] 9 | 10 | static defaultTorrentUploadOptions: TorrentUploadOptions = { 11 | startTorrent: true, 12 | } 13 | 14 | scope: AddTorrentModalScope 15 | rootScope: IRootScopeService 16 | modalref: ModalController 17 | uploadOptions: TorrentUploadOptions 18 | isLoading: boolean 19 | 20 | constructor(scope: AddTorrentModalScope, rootScope: IRootScopeService) { 21 | this.scope = scope 22 | this.rootScope = rootScope 23 | this.isLoading = false 24 | this.uploadOptions = {} 25 | this.scope.$watch(() => { 26 | return this.scope.torrents && this.scope.torrents.length 27 | }, (newVal, oldVal) => { 28 | if (newVal > 0) { 29 | this.modalref.showModal() 30 | } else if (oldVal > 0 && newVal === 0) { 31 | this.modalref.hideModal() 32 | } 33 | }) 34 | } 35 | 36 | onShow() { 37 | this.uploadOptions = Object.assign({}, 38 | AddTorrentModalController.defaultTorrentUploadOptions 39 | ) 40 | } 41 | 42 | onHidden() { 43 | this.scope.torrents = [] 44 | } 45 | 46 | getCurrentTorrentUpload() { 47 | if (this.scope.torrents) { 48 | return this.scope.torrents.length && this.scope.torrents[0] 49 | } 50 | } 51 | 52 | discardCurrentTorrent() { 53 | this.scope.torrents.shift() 54 | if (this.scope.torrents.length === 0) { 55 | this.modalref.hideModal() 56 | } 57 | } 58 | 59 | async uploadCurrentTorrent() { 60 | try { 61 | this.isLoading = true 62 | let torrent = this.getCurrentTorrentUpload() 63 | if (torrent.type === 'file') { 64 | await this.performTorrentUpload(torrent.data, torrent.filename, this.uploadOptions) 65 | } else { 66 | await this.performTorrentURIUpload(torrent.uri, this.uploadOptions) 67 | } 68 | this.scope.torrents.shift() 69 | if (this.scope.torrents.length === 0) { 70 | this.modalref.hideModal() 71 | } 72 | } finally { 73 | this.isLoading = false 74 | } 75 | } 76 | 77 | async performTorrentURIUpload(uri: string, options: TorrentUploadOptions) { 78 | if (this.scope.uploadTorrentUrlAction) { 79 | await this.scope.uploadTorrentUrlAction(uri, options) 80 | } else { 81 | await this.rootScope.$btclient.addTorrentUrl(uri, options) 82 | } 83 | } 84 | 85 | async performTorrentUpload(torrent: Uint8Array, filename: string, options: TorrentUploadOptions) { 86 | if (this.scope.uploadTorrentAction) { 87 | await this.scope.uploadTorrentAction(torrent, filename, options) 88 | } else { 89 | await this.rootScope.$btclient.uploadTorrent(torrent, filename, options) 90 | } 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /src/scripts/directives/add-torrent-modal/add-torrent-modal.directive.ts: -------------------------------------------------------------------------------- 1 | import { IDirective, IDirectiveFactory, IScope } from "angular"; 2 | import { Torrent } from "../../bittorrent"; 3 | import { TorrentUploadOptions } from "../../bittorrent/torrentclient"; 4 | import { AddTorrentModalController } from "./add-torrent-modal.controller"; 5 | import html from "./add-torrent-modal.template.html" 6 | 7 | export type PendingTorrentUploadList = Array 8 | 9 | export type PendingTorrentUploadItem = PendingTorrentUploadFile|PendingTorrentUploadLink 10 | 11 | export interface PendingTorrentUploadFile { 12 | type: 'file', 13 | data: Uint8Array, 14 | filename: string, 15 | } 16 | 17 | export interface PendingTorrentUploadLink { 18 | type: 'link' 19 | uri: string 20 | } 21 | 22 | export interface AddTorrentModalScope extends IScope { 23 | torrents: PendingTorrentUploadList 24 | uploadTorrentAction: (torrent: Uint8Array, filename: string, options: TorrentUploadOptions) => Promise 25 | uploadTorrentUrlAction: (uri: string, options: TorrentUploadOptions) => Promise 26 | } 27 | 28 | export class AddTorrentModalDirective implements IDirective { 29 | 30 | template = html 31 | restrict = "E" 32 | scope = { 33 | torrents: '=', 34 | labels: '=', 35 | uploadTorrentAction: '<', 36 | uploadTorrentUrlAction: '<' 37 | } 38 | controller = AddTorrentModalController 39 | controllerAs = "ctl" 40 | 41 | static getInstance(): IDirectiveFactory { 42 | return () => new AddTorrentModalDirective() 43 | } 44 | } -------------------------------------------------------------------------------- /src/scripts/directives/add-torrent-modal/add-torrent-modal.template.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /src/scripts/directives/checkbox.ts: -------------------------------------------------------------------------------- 1 | 2 | export let toggleController = ['$scope', function($scope){ 3 | 4 | if (angular.isFunction($scope.checked)) { $scope.ngModel = !!$scope.checked(); } 5 | 6 | this.toggle = function() { 7 | if (angular.isFunction($scope.disabled) && $scope.disabled()) return; 8 | $scope.ngModel = !$scope.ngModel; 9 | } 10 | 11 | }] 12 | 13 | export let toggle = function() { 14 | 15 | function controller() { 16 | // var vm = this; 17 | // 18 | // // TODO: assert this is usefull ? 19 | // // if(angular.isUndefined(vm.ngModel)) { vm.ngModel = !!vm.ngModel; } 20 | // 21 | // if (angular.isFunction(vm.checked)) { vm.ngModel = !!vm.checked(); } 22 | // 23 | // vm.toggle = function() { 24 | // if (angular.isFunction(vm.disabled) && vm.disabled()) return; 25 | // vm.ngModel = !vm.ngModel; 26 | // } 27 | 28 | 29 | 30 | } 31 | 32 | function link() { 33 | 34 | } 35 | 36 | return { 37 | restrict: 'E', 38 | replace: true, 39 | transclude: true, 40 | scope: { 41 | ngChange: '=?ngChange', 42 | checked: '&?', 43 | disabled: '&?', 44 | ngModel: '=ngModel' 45 | }, 46 | controller: controller, 47 | controllerAs: 'vm', 48 | bindToController: true, 49 | require: 'ngModel', 50 | template: '
' + 51 | '' + 52 | '' + 53 | '
', 54 | link: link 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /src/scripts/directives/draganddrop.ts: -------------------------------------------------------------------------------- 1 | 2 | export let dragAndDrop = ['$rootScope', '$document', 'electron', function($rootScope, $document, electron) { 3 | return function(scope, element /*, attrs*/) { 4 | 5 | var dragging = 0; 6 | 7 | document.ondragover = document.ondrop = function(event) { 8 | event.preventDefault() 9 | } 10 | 11 | element.bind('click', function() { 12 | dragging = 0; 13 | $rootScope.$emit('show:draganddrop', false); 14 | }); 15 | 16 | element.bind('dragenter', function(event /*, data*/) { 17 | dragging++; 18 | 19 | $rootScope.$emit('show:draganddrop', true); 20 | 21 | event.stopPropagation(); 22 | event.preventDefault(); 23 | 24 | return false; 25 | }) 26 | 27 | element.bind('dragleave', function (event /*, data*/) { 28 | 29 | dragging--; 30 | 31 | if (dragging === 0) { 32 | $rootScope.$emit('show:draganddrop', false); 33 | } 34 | 35 | event.stopPropagation(); 36 | event.preventDefault(); 37 | 38 | return false; 39 | }) 40 | 41 | element.bind('drop', function(event /*, data*/) { 42 | var files = event.originalEvent.dataTransfer.files; 43 | var paths = []; 44 | 45 | for (var i = 0; i < files.length; i++) { 46 | paths.push(files.item(i).path); 47 | } 48 | 49 | let advancedKey = process.platform === 'darwin' ? event.altKey : event.ctrlKey 50 | 51 | electron.torrents.readFiles(paths, advancedKey); 52 | $rootScope.$emit('show:draganddrop', false); 53 | }); 54 | } 55 | }]; 56 | -------------------------------------------------------------------------------- /src/scripts/directives/dropdown.ts: -------------------------------------------------------------------------------- 1 | 2 | export let dropdown = [function() { 3 | return { 4 | restrict: 'A', 5 | link: link, 6 | scope: { 7 | ref: '=?', 8 | bind: '=?' 9 | } 10 | } 11 | 12 | function link(scope, element, attr) { 13 | 14 | let dropdown: any = $(element) 15 | 16 | dropdown.dropdown({ 17 | transition: "vertical flip", 18 | duration: 100, 19 | onChange: onChange, 20 | action: 'hide' 21 | }); 22 | 23 | if ('ref' in attr){ 24 | scope.ref = { 25 | clear: doAction(element, 'clear'), 26 | refresh: doAction(element, 'refresh'), 27 | setSelected: doAction(element, 'set selected'), 28 | getValue: doAction(element, 'get value') 29 | }; 30 | } 31 | 32 | scope.$watch(function() { 33 | return scope.bind; 34 | }, function(newValue) { 35 | if (newValue) { 36 | let dropdown: any = $(element) 37 | dropdown.dropdown('set selected', newValue); 38 | } 39 | }); 40 | 41 | function onChange(value /*, text, choice*/){ 42 | if (scope.bind){ 43 | scope.bind = value; 44 | } 45 | } 46 | 47 | } 48 | 49 | function doAction(element, action) { 50 | return function(param) { 51 | let dropdown: any = $(element) 52 | dropdown.dropdown(action, param); 53 | } 54 | } 55 | 56 | }]; 57 | 58 | export let dropItem = [function() { 59 | return { 60 | restrict: 'A', 61 | link: link 62 | } 63 | 64 | function link(scope, element, attr) { 65 | if (scope.bind === attr.value){ 66 | var dropdown: any = $(element).closest('.dropdown'); 67 | dropdown.dropdown('set selected', attr.value); 68 | } 69 | 70 | if (scope.$last) { 71 | scope.$emit('update:dropdown'); 72 | } 73 | } 74 | 75 | 76 | }]; 77 | -------------------------------------------------------------------------------- /src/scripts/directives/labelsdropdown.ts: -------------------------------------------------------------------------------- 1 | 2 | export let labelsDropdown = [ 3 | function () { 4 | return { 5 | restrict: "A", 6 | templateUrl: "./views/misc/labels.html", 7 | scope: { 8 | enabled: "=?", 9 | action: "=", 10 | labels: "=", 11 | }, 12 | link: function (scope) { 13 | scope.form = { label: "" }; 14 | 15 | scope.openNewLabelModal = function () { 16 | let modal: any = $("#newLabelModal") 17 | modal.modal("show"); 18 | }; 19 | 20 | scope.applyNewLabel = function (label) { 21 | console.log("Passed label:", label); 22 | console.log("New label:", scope.form.label); 23 | }; 24 | }, 25 | }; 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /src/scripts/directives/limit.ts: -------------------------------------------------------------------------------- 1 | 2 | export let limitBind = [function() { 3 | return { 4 | scope: false, 5 | controller: controller, 6 | bindToController: { 7 | limit: '=limitBind' 8 | }, 9 | link: link 10 | }; 11 | 12 | function link(scope, element, attr, ctrl) { 13 | ctrl.setContainer(element) 14 | 15 | $(window).on('resize', function() { 16 | scope.$apply(function() { 17 | ctrl.updateLimit() 18 | }) 19 | }) 20 | } 21 | 22 | function controller() { 23 | this.updateLimit = function(element, force) { 24 | if (element) { 25 | this.elementHeight = element.outerHeight() 26 | } 27 | var limit = Math.ceil(this.container.innerHeight() / this.elementHeight) 28 | if (limit > this.limit || force) { 29 | this.limit = limit 30 | } 31 | } 32 | 33 | this.setContainer = function(element) { 34 | this.container = element 35 | } 36 | } 37 | 38 | }]; 39 | 40 | export let limitSource = function() { 41 | return { 42 | scope: false, 43 | require: '^^limitBind', 44 | link: link 45 | }; 46 | 47 | function link(scope, element, attr, ctrl) { 48 | if (scope.$first) { 49 | ctrl.updateLimit(element, true) 50 | } 51 | } 52 | 53 | }; 54 | 55 | -------------------------------------------------------------------------------- /src/scripts/directives/modal.ts: -------------------------------------------------------------------------------- 1 | import { IDirectiveFactory } from "angular" 2 | 3 | export let modal: IDirectiveFactory = function() { 4 | return { 5 | templateUrl: template, 6 | replace: true, 7 | transclude: true, 8 | scope: { 9 | title: '@', 10 | btnOk: '@', 11 | btnCancel: '@', 12 | closable: '<', 13 | icon: '@', 14 | approve: '&', 15 | deny: '&', 16 | hidden: '&', 17 | show: '&', 18 | after: '=', 19 | data: '=', 20 | }, 21 | restrict: 'E', 22 | link: link 23 | }; 24 | 25 | function template(elem, attrs) { 26 | return attrs.templateUrl || 'some/path/default.html' 27 | } 28 | 29 | function link(scope, element/*, attrs*/) { 30 | var accepted = false 31 | 32 | let modal: any = $(element) 33 | 34 | modal.modal({ 35 | onDeny: function () { 36 | accepted = false 37 | return scope.deny() 38 | }, 39 | onApprove: function () { 40 | accepted = true 41 | return scope.approve() 42 | }, 43 | onHidden: function () { 44 | clearForm(element) 45 | scope.after && scope.after(accepted) 46 | return scope.hidden() 47 | }, 48 | onShow: function() { 49 | accepted = false 50 | scope.show() 51 | }, 52 | onVisible: function() { 53 | modal.modal('refresh') 54 | }, 55 | closable: !!scope.closable, 56 | duration: 150 57 | }); 58 | 59 | scope.applyAndClose = function() { 60 | if (scope.approve()) { 61 | modal.modal('hide') 62 | } 63 | } 64 | 65 | scope.$on("$destroy", function() { 66 | element.remove(); 67 | }); 68 | } 69 | 70 | function clearForm(element){ 71 | let form: any = $(element) 72 | form.form('clear'); 73 | form.find('.error.message').empty() 74 | } 75 | 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /src/scripts/directives/modal/modal.controller.ts: -------------------------------------------------------------------------------- 1 | import { IScope } from "angular"; 2 | 3 | export class ModalController { 4 | 5 | static $inject = ["$scope"] 6 | 7 | // jQuery element for Semantic UI modal 8 | modal: any 9 | 10 | constructor(scope: IScope) { 11 | this.modal = scope.modal 12 | 13 | } 14 | 15 | showModal() { 16 | this.modal.modal('show') 17 | } 18 | 19 | hideModal() { 20 | this.modal.modal('hide') 21 | } 22 | 23 | toggleModal() { 24 | this.modal.modal('toggle') 25 | } 26 | 27 | refreshModal() { 28 | this.modal.modal('refresh') 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/scripts/directives/modal/modal.directive.ts: -------------------------------------------------------------------------------- 1 | import { IAugmentedJQuery, IDirective, IDirectiveFactory, IScope } from "angular"; 2 | import { ModalController } from "./modal.controller"; 3 | import html from "./modal.template.html" 4 | 5 | export class ModalDirective implements IDirective { 6 | 7 | scope = { 8 | onShow: '&', 9 | onHidden: '&', 10 | } 11 | restrict = 'EA' 12 | //template = html 13 | controller = ModalController 14 | controllerAs = "modalctl" 15 | 16 | static getInstance(): IDirectiveFactory { 17 | return () => new ModalDirective() 18 | } 19 | 20 | link(scope: IScope, element: IAugmentedJQuery, attr: any, controller: ModalController) { 21 | var accepted = false 22 | 23 | let modal: any = $(element) 24 | 25 | controller.modal = modal 26 | 27 | modal.modal({ 28 | onDeny: function () { 29 | accepted = false 30 | }, 31 | onApprove: function () { 32 | accepted = true 33 | }, 34 | onHidden: function () { 35 | ModalDirective.clearForm(element) 36 | scope.after && scope.after(accepted) 37 | scope.onHidden && scope.onHidden() 38 | }, 39 | onShow: function() { 40 | accepted = false 41 | scope.onShow && scope.onShow() 42 | }, 43 | onVisible: function() { 44 | modal.modal('refresh') 45 | }, 46 | closable: !!scope.closable, 47 | duration: 150 48 | }); 49 | 50 | scope.applyAndClose = function() { 51 | if (scope.approve()) { 52 | modal.modal('hide') 53 | } 54 | } 55 | 56 | scope.$on("$destroy", function() { 57 | element.remove(); 58 | }); 59 | } 60 | 61 | static clearForm(element: IAugmentedJQuery) { 62 | let form: any = $(element) 63 | form.form('clear'); 64 | form.find('.error.message').empty() 65 | } 66 | 67 | } 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/scripts/directives/modal/modal.template.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/scripts/directives/progress.ts: -------------------------------------------------------------------------------- 1 | export let progress = ['$timeout', function($timeout) { 2 | return { 3 | scope: { 4 | torrent: '=progress', 5 | }, 6 | restrict: 'A', 7 | template: `
8 | 9 |
10 |
`, 11 | replace: true, 12 | link: link 13 | }; 14 | 15 | 16 | function link(scope, element /*, attrs*/ ) { 17 | var idle = true 18 | var bar = element.find('.bar') 19 | 20 | function updateProgress(newPercent?, oldPercent?) { 21 | if (scope.torrent.percent < 1000 || oldPercent < 1000) { 22 | bar.css('width', scope.torrent.getPercentStr()); 23 | if (idle) { 24 | $timeout(function() { 25 | bar.removeClass('idle') 26 | idle = false 27 | }) 28 | } 29 | } 30 | } 31 | 32 | scope.class = function(){ 33 | return scope.torrent.statusColor(); 34 | } 35 | 36 | scope.label = function(){ 37 | var label = scope.torrent.statusText(); 38 | if (scope.torrent.isStatusDownloading()){ 39 | label += (" " + scope.torrent.getPercentStr()); 40 | } 41 | return label; 42 | } 43 | 44 | scope.$watch(function() { 45 | return scope.torrent.percent; 46 | }, function(newPercent, oldPercent) { 47 | if (newPercent !== oldPercent) { 48 | updateProgress(newPercent, oldPercent) 49 | } 50 | }); 51 | 52 | updateProgress() 53 | } 54 | 55 | }]; 56 | -------------------------------------------------------------------------------- /src/scripts/directives/readyBroadcast.ts: -------------------------------------------------------------------------------- 1 | 2 | export let readyBroadcast = ['$rootScope', '$timeout', function($rootScope, $timeout) { 3 | return { 4 | restrict: 'A', 5 | link: link 6 | }; 7 | 8 | function link(/*scope, element, attr*/){ 9 | $timeout(function(){ 10 | $rootScope.$emit('ready'); 11 | }); 12 | } 13 | 14 | }]; 15 | 16 | -------------------------------------------------------------------------------- /src/scripts/directives/repeatdone.ts: -------------------------------------------------------------------------------- 1 | 2 | export let repeatDone = ['$timeout', function($timeout) { 3 | return function(scope, element, attrs) { 4 | if (scope.$last) { 5 | $timeout(function() { 6 | let callback = scope.$eval(attrs.repeatDone); 7 | if (angular.isFunction(callback)) callback() 8 | }, 0) 9 | } 10 | } 11 | }]; 12 | 13 | -------------------------------------------------------------------------------- /src/scripts/directives/rightclick.ts: -------------------------------------------------------------------------------- 1 | export let ngRightClick = function($parse) { 2 | function link(scope, element, attrs) { 3 | var fn = $parse(attrs.ngRightClick); 4 | element.bind('contextmenu', function(event) { 5 | scope.$apply(function() { 6 | event.preventDefault(); 7 | fn(scope, {$event:event}); 8 | }); 9 | }); 10 | }; 11 | return { 12 | link: link 13 | } 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /src/scripts/directives/search.ts: -------------------------------------------------------------------------------- 1 | 2 | export let search = ['$rootScope', '$document', function($rootScope, $document) { 3 | return { 4 | restrict: 'A', 5 | link: link 6 | }; 7 | 8 | function link(scope, element /*, attrs*/ ) { 9 | element.on('keyup', function(event) { 10 | if (event.keyCode === 27 /* Escape key */){ 11 | element.blur() 12 | } 13 | }) 14 | 15 | $rootScope.$on('search:torrent', () => { 16 | element.focus() 17 | element.select() 18 | }); 19 | } 20 | 21 | }]; 22 | 23 | -------------------------------------------------------------------------------- /src/scripts/directives/sorting.ts: -------------------------------------------------------------------------------- 1 | 2 | export let sorting = ['$window', function($window) { 3 | 4 | return { 5 | restrict: 'A', 6 | bindToController: true, 7 | scope: { 8 | mode: '=', 9 | sorting: '=' 10 | }, 11 | controller: controller, 12 | link: link 13 | }; 14 | 15 | function controller() { 16 | 17 | this.updateSettings = function() { 18 | this.sortKey = getSavedSortKey(this) 19 | this.sortOrder = getSavedSortOrder(this) 20 | } 21 | 22 | this.save = function(key, order) { 23 | $window.localStorage.setItem('sort_key.'+this.mode, key); 24 | $window.localStorage.setItem('sort_desc.'+this.mode, order); 25 | } 26 | 27 | this.updateSettings() 28 | 29 | } 30 | 31 | function link(scope, element, attr, ctrl) { 32 | function update(){ 33 | $(element).find('*[sort]').each(function(i, col){ 34 | var scope = angular.element(col).scope() 35 | scope.update() 36 | }) 37 | } 38 | 39 | scope.$watch(function() { 40 | return ctrl.mode 41 | }, function(newMode, oldMode) { 42 | if (newMode !== oldMode) { 43 | ctrl.updateSettings() 44 | update() 45 | } 46 | }) 47 | } 48 | 49 | function getSavedSortKey(ctrl) { 50 | let sortKey = $window.localStorage.getItem('sort_key.'+ctrl.mode); 51 | if (!sortKey || typeof sortKey !== 'string') { 52 | return 'dateAdded'; 53 | } else { 54 | return sortKey 55 | } 56 | } 57 | 58 | function getSavedSortOrder(ctrl) { 59 | let sortOrder = $window.localStorage.getItem('sort_desc.'+ctrl.mode); 60 | if (!sortOrder) { 61 | return true; 62 | } else { 63 | return (sortOrder === 'true'); 64 | } 65 | } 66 | 67 | }]; 68 | 69 | export let sort = [function() { 70 | 71 | return { 72 | restrict: 'A', 73 | require: '^^sorting', 74 | scope: false, 75 | link: link 76 | }; 77 | 78 | function link(scope, element, attr, ctrl) { 79 | scope.sort = scope.$eval(attr.sort); 80 | 81 | scope.update = function() { 82 | if (scope.sort === ctrl.sortKey) { 83 | setSortingArrow(scope, column, ctrl, ctrl.sortOrder); 84 | ctrl.sorting(scope.sort, ctrl.sortOrder); 85 | } 86 | } 87 | 88 | var column = $(element); 89 | column.append(''); 90 | bindSortAction(scope, column, ctrl); 91 | scope.update() 92 | } 93 | 94 | function bindSortAction(scope, element, ctrl) { 95 | var isDragging = false 96 | 97 | element.mousedown(function() { 98 | $(window).one('mousemove', function() { 99 | isDragging = true; 100 | }); 101 | }) 102 | 103 | element.mouseup(function() { 104 | var wasDragging = isDragging; 105 | isDragging = false; 106 | $(window).off("mousemove"); 107 | if(!wasDragging) { 108 | showSortingArrows(scope, element, ctrl); 109 | } 110 | }); 111 | } 112 | 113 | function showSortingArrows(scope, element, ctrl) { 114 | if(element.is('.sortdown, .sortup')) { 115 | element.toggleClass('sortdown sortup'); 116 | } else { 117 | if (ctrl.last) ctrl.last.removeClass('sortdown sortup'); 118 | element.addClass('sortdown') 119 | ctrl.last = element; 120 | } 121 | 122 | var desc = element.hasClass('sortdown'); 123 | ctrl.sorting(scope.sort, desc); 124 | ctrl.save(scope.sort, desc); 125 | } 126 | 127 | function setSortingArrow(scope, element, ctrl, sortDesc) { 128 | if (ctrl.last) ctrl.last.removeClass('sortdown sortup'); 129 | 130 | if (sortDesc === true) { 131 | element.addClass('sortdown') 132 | } else if (sortDesc === false) { 133 | element.addClass('sortup') 134 | } else { 135 | element.removeClass('sortdown sortup') 136 | } 137 | 138 | ctrl.last = element; 139 | } 140 | 141 | }]; 142 | -------------------------------------------------------------------------------- /src/scripts/directives/time.ts: -------------------------------------------------------------------------------- 1 | 2 | export let time = ['$timeout', '$filter', function($timeout, $filter) { 3 | 4 | const DAY = 60*60*24*1000 5 | const HOUR = 60*60*1000 6 | const MINUTE = 60*1000 7 | 8 | let filter = $filter('date') 9 | 10 | return { 11 | restrict: 'A', 12 | scope: { 13 | time: "=" 14 | }, 15 | compile: compile 16 | }; 17 | 18 | function compile(/*element, attr, ctrl*/) { 19 | return link; 20 | } 21 | 22 | function link(scope, element /*, attr, ctrl*/){ 23 | var timer 24 | 25 | element.bind('$destroy', function() { 26 | $timeout.cancel(timer) 27 | }) 28 | 29 | function startTimer(scope, element) { 30 | let next = nextUpdate(scope) 31 | 32 | if (next) { 33 | timer = $timeout(function() { 34 | updateTime(scope, element) 35 | startTimer(scope, element) 36 | }, next) 37 | } 38 | } 39 | 40 | updateTime(scope, element) 41 | startTimer(scope, element) 42 | } 43 | 44 | function updateTime(scope, element) { 45 | element.html(filter(scope.time)) 46 | } 47 | 48 | function nextUpdate(scope) { 49 | var date = new Date(scope.time) 50 | var diff = Math.abs(Date.now() - date.getTime()) 51 | 52 | if (diff > DAY) { 53 | return 54 | } else if (diff < HOUR) { 55 | return MINUTE 56 | } else if (diff < 6*HOUR) { 57 | return 15*MINUTE 58 | } else { 59 | return 30*MINUTE 60 | } 61 | 62 | } 63 | 64 | }]; 65 | 66 | -------------------------------------------------------------------------------- /src/scripts/directives/torrent-upload-form/torrent-upload-form.controller.ts: -------------------------------------------------------------------------------- 1 | import { ICompileService, IRootScopeService, IScope } from "angular"; 2 | import { TorrentUploadOptions, TorrentUploadOptionsEnable } from "../../bittorrent/torrentclient"; 3 | 4 | export class TorrentUploadFormController { 5 | 6 | static $inject = ["$scope", "$rootScope"] 7 | 8 | scope: IScope 9 | rootScope: IRootScopeService 10 | optionsEnabled: TorrentUploadOptionsEnable 11 | 12 | constructor(scope: IScope, rootScope: IRootScopeService) { 13 | this.scope = scope 14 | this.rootScope = rootScope 15 | this.onNewTorrentClient() 16 | 17 | scope.$watch(() => { 18 | return this.rootScope.$btclient 19 | }, () => { 20 | this.onNewTorrentClient() 21 | }) 22 | } 23 | 24 | onNewTorrentClient() { 25 | this.optionsEnabled = this.rootScope?.$btclient?.uploadOptionsEnable || {} 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/directives/torrent-upload-form/torrent-upload-form.directive.ts: -------------------------------------------------------------------------------- 1 | import { IAttributes, IAugmentedJQuery, ICompileService, IControllerProvider, IControllerService, IDirective, IDirectiveCompileFn, IDirectiveFactory, IDirectiveLinkFn, IDirectivePrePost, IRootElementService, IRootScopeService, IScope } from "angular"; 2 | import { link } from "fs"; 3 | import { TorrentUploadFormController } from "./torrent-upload-form.controller"; 4 | import html from "./torrent-upload-form.template.html" 5 | 6 | export interface TorrentUploadFormScope extends IScope { 7 | torrents: {data: Uint8Array, filename: string}[] 8 | } 9 | 10 | export abstract class Directive implements IDirective { 11 | 12 | static $inject: string[] 13 | 14 | compile?: IDirectiveCompileFn; 15 | controller?: any; 16 | controllerAs?: string; 17 | multiElement?: boolean; 18 | name?: string; 19 | priority?: number; 20 | require?: string | string[] | {[controller: string]: string}; 21 | restrict?: string; 22 | scope?: boolean | Object; 23 | template?: string | Function; 24 | templateNamespace?: string; 25 | templateUrl?: string | Function; 26 | terminal?: boolean; 27 | transclude?: boolean | string | {[slot: string]: string}; 28 | 29 | link?(scope?: IScope, element?: IAugmentedJQuery, attrs?: IAttributes, controller?: any): void 30 | } 31 | 32 | export class TorrentUploadFormDirective extends Directive { 33 | 34 | constructor() { 35 | super() 36 | this.template = html 37 | this.restrict = "E" 38 | this.scope = { 39 | options: "=", 40 | labels: "=", 41 | loading: "<", 42 | } 43 | this.controller = TorrentUploadFormController 44 | this.controllerAs = "ctl" 45 | } 46 | 47 | static getInstance() { 48 | return () => new TorrentUploadFormDirective() 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/scripts/directives/torrent-upload-form/torrent-upload-form.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 | {{ label }} 22 | 23 | 24 |
25 |
26 | 29 |
30 |
31 | 32 |
33 |
34 | 35 | Start torrent 36 | 37 |
38 | 39 |
40 | 41 | Skip hash check 42 | 43 |
44 | 45 |
46 | 47 | Sequential download 48 | 49 |
50 | 51 |
52 | 53 | First and last piece priority 54 | 55 |
56 |
57 | 58 |
59 |
60 |
61 |
62 | 63 | 64 |
65 |
66 |
67 |
68 | 69 | 70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 | -------------------------------------------------------------------------------- /src/scripts/directives/torrenttable.ts: -------------------------------------------------------------------------------- 1 | 2 | export let torrentBody = ['$document', '$compile', function($document, $compile) { 3 | return { 4 | restrict: 'A', 5 | controller: controller, 6 | bindToController: true, 7 | scope: { 8 | columns: "=" 9 | }, 10 | compile: compile 11 | }; 12 | 13 | function controller() { 14 | this.$template = "" 15 | this.$renderColumns = [] 16 | this.$rows = [] 17 | this.$link = function() {} 18 | 19 | this.subscribe = function(row) { 20 | this.$rows.push(row) 21 | } 22 | 23 | this.renderTemplate = function() { 24 | if (!this.columns) return 25 | this.$renderColumns = this.columns.filter(function(c) { 26 | return c.enabled 27 | }) 28 | let template = "" 29 | this.$renderColumns.forEach(function(c) { 30 | template = template + `${c.template}` 31 | }) 32 | 33 | this.$template = template 34 | this.$link = $compile(this.$template) 35 | } 36 | 37 | this.render = function() { 38 | this.renderTemplate() 39 | this.$rows.forEach(function(r) { 40 | r.render() 41 | }) 42 | } 43 | } 44 | 45 | function compile(/*element, attr, ctrl*/) { 46 | return link; 47 | } 48 | 49 | function link(scope, element, attr, ctrl){ 50 | ctrl.renderTemplate() 51 | 52 | scope.$watch(function() { 53 | return ctrl.columns 54 | }, function() { 55 | ctrl.render() 56 | }, true) 57 | } 58 | 59 | }]; 60 | 61 | export let torrentRow = [function() { 62 | return { 63 | restrict: 'A', 64 | require: '^^torrentBody', 65 | scope: false, 66 | compile: compile 67 | } 68 | 69 | function compile(/*element, attr, ctrl*/) { 70 | return link 71 | } 72 | 73 | function link(scope, element, attr, ctrl){ 74 | scope.render = function() { 75 | ctrl.$link(scope, function(clone) { 76 | element.append(clone) 77 | }) 78 | } 79 | scope.render() 80 | ctrl.subscribe(scope) 81 | } 82 | 83 | }]; 84 | 85 | -------------------------------------------------------------------------------- /src/scripts/filters/bytes.ts: -------------------------------------------------------------------------------- 1 | export let bytesFilter = function() { 2 | return function(bytes, decimals = 1) { 3 | if (!+bytes) return '0 B' 4 | 5 | const k = 1024 6 | const dm = +decimals < 0 ? 0 : +decimals 7 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 8 | 9 | const i = Math.floor(Math.log(bytes) / Math.log(k)) 10 | 11 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` 12 | }; 13 | }; 14 | 15 | export let speedFilter = ['bytesFilter', function(bytes) { 16 | return function(bytesPerSecond, torrent) { 17 | var display = true; 18 | 19 | if (torrent){ 20 | display = torrent.isStatusDownloading() 21 | } 22 | 23 | if (display){ 24 | return bytes(bytesPerSecond) + '/s'; 25 | } else { 26 | return ''; 27 | } 28 | } 29 | }] 30 | -------------------------------------------------------------------------------- /src/scripts/filters/dateFilter.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | 3 | export let dateFilter = function() { 4 | const BT_EPOCH = 994032000000 /* July 2nd 2001, release of bittorrent */ 5 | return function(epochtime) { 6 | if (!epochtime) return '' 7 | if (epochtime < BT_EPOCH) return '' 8 | return moment(epochtime).fromNow(); 9 | } 10 | }; 11 | 12 | export let etaFilter = function() { 13 | 14 | var MONTH_IN_SECONDS = 60*60*24*30 15 | 16 | return function(seconds) { 17 | if (!seconds || seconds < 1 || seconds > MONTH_IN_SECONDS) return '' 18 | return moment().to(moment().add(seconds, 'seconds'), true) 19 | }; 20 | }; 21 | 22 | export let releaseDateFilter = function() { 23 | return function(date) { 24 | if (!date){ 25 | return "Release date unknown" 26 | } 27 | return moment(date, moment.ISO_8601).format("MMMM Do YYYY, HH:mm"); 28 | } 29 | } 30 | 31 | angular.module("torrentApp").filter('epoch', function() { 32 | return function(epoch) { 33 | if (!epoch) { 34 | return 'Unknown date' 35 | } 36 | return moment(epoch*1000).format("MMMM Do YYYY, HH:mm"); 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /src/scripts/filters/torrentfilters.ts: -------------------------------------------------------------------------------- 1 | export let torrentQueueFilter = function() { 2 | return function(queue) { 3 | if (Number.isInteger(queue) && queue >= 0) { 4 | return queue 5 | } else { 6 | return '' 7 | } 8 | }; 9 | }; 10 | 11 | export let torrentRatioFilter = function() { 12 | function isNumeric(number) { 13 | return !isNaN(parseFloat(number)) && isFinite(number); 14 | } 15 | 16 | return function(ratio) { 17 | if (isNumeric(ratio)) { 18 | return parseFloat(ratio).toFixed(2) 19 | } else { 20 | return '' 21 | } 22 | } 23 | }; 24 | 25 | export let torrentTrackerFilter = function() { 26 | const URL_REGEX = /^[a-z]+:\/\/(?:[a-z0-9-]+\.)*((?:[a-z0-9-]+\.)[a-z]+)/ 27 | 28 | return function(tracker) { 29 | if (!tracker) return '' 30 | var match = tracker.match(URL_REGEX) 31 | if (!match) return '' 32 | return match[1] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/scripts/services/bittorrent.ts: -------------------------------------------------------------------------------- 1 | import { PendingTorrentUploadLink } from "../directives/add-torrent-modal/add-torrent-modal.directive"; 2 | 3 | export let bittorrentService = ['$rootScope', '$injector', '$btclients', 'notificationService', 'electron', function($rootScope, $injector, $btclients, $notify, electron){ 4 | 5 | this.getClient = function(name) { 6 | var client = $btclients[name]; 7 | if (client){ 8 | return client.service 9 | } else { 10 | throw new Error('Bittorrent client "' + name + '" not available'); 11 | } 12 | } 13 | 14 | this.changeClient = function(clientName) { 15 | var service = this.getClient(clientName); 16 | this.setClient(service); 17 | } 18 | 19 | this.setClient = function(service) { 20 | $rootScope.$btclient = service; 21 | console.info("Changed client to:", service.name || ""); 22 | } 23 | 24 | this.setServer = function(server) { 25 | $rootScope.$btclient = this.getClient(server.client) 26 | $rootScope.$server = server 27 | server.updateLastUsed() 28 | } 29 | 30 | this.getServer = function() { 31 | return $rootScope.$server 32 | } 33 | 34 | this.getServerCopy = function() { 35 | return angular.copy($rootScope.$server) 36 | } 37 | 38 | this.uploadFromClipboard = function(askUploadOptions: boolean) { 39 | var magnet = electron.clipboard.readText(); 40 | 41 | var protocol = ['magnet', 'http']; 42 | 43 | var supported = protocol.some(function(protocol) { 44 | return magnet.startsWith(protocol); 45 | }) 46 | 47 | if (!supported) { 48 | $notify.alert('Wait a minute?', 'Your clipboard doesn\'t contain a magnet link'); 49 | return; 50 | } 51 | 52 | let link: PendingTorrentUploadLink = { 53 | type: 'link', 54 | uri: magnet 55 | } 56 | 57 | $rootScope.$broadcast('torrents:add', link, askUploadOptions) 58 | } 59 | }]; 60 | -------------------------------------------------------------------------------- /src/scripts/services/column.ts: -------------------------------------------------------------------------------- 1 | type sortFunc = (a: any, b: any) => number 2 | 3 | export interface ColumnProps { 4 | name?: string 5 | enabled?: boolean 6 | attribute?: string 7 | template?: string 8 | sort?: sortFunc 9 | } 10 | 11 | export class Column implements ColumnProps { 12 | 13 | name: string 14 | attribute: string 15 | enabled: boolean 16 | template: string 17 | sort: sortFunc 18 | 19 | 20 | constructor(props: ColumnProps = {}) { 21 | Object.assign(this, Column.defaultProps) 22 | Object.assign(this, props) 23 | } 24 | 25 | static ALPHABETICAL = function(a: string, b: string) { 26 | var aLower = a.toLowerCase(); 27 | var bLower = b.toLowerCase(); 28 | return aLower.localeCompare(bLower); 29 | } 30 | 31 | static NUMERICAL = function(a: number, b: number){ 32 | return b - a; 33 | } 34 | 35 | static NATURAL_NUMBER_ASC = function(a: number, b: number){ 36 | if (a < 0) return 1 37 | if (b < 0) return -1 38 | return a - b 39 | } 40 | 41 | private static defaultProps: ColumnProps = { 42 | enabled: false, 43 | template: '', 44 | sort: Column.NUMERICAL, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/scripts/services/electron.ts: -------------------------------------------------------------------------------- 1 | export let electronService = [function() { 2 | var o: any = {}; 3 | 4 | // Get the Electron remote 5 | const remote = require('@electron/remote'); 6 | 7 | // Directly accesible modules 8 | o.ipc = require('electron').ipcRenderer; 9 | o.shell = require('electron').shell; 10 | 11 | //Remote moudles from main process 12 | o.remote = remote; 13 | o.app = remote.app; 14 | o.browserWindow = remote.BrowserWindow; 15 | o.clipboard = remote.clipboard; 16 | o.dialog = remote.dialog; 17 | o.menu = remote.Menu; 18 | o.menuItem = remote.MenuItem; 19 | o.nativeImage = remote.nativeImage; 20 | o.powerMonitor = remote.powerMonitor; 21 | o.protocol = remote.protocol; 22 | o.screen = remote.screen; 23 | o.tray = remote.shell; 24 | o.capturer = remote.capturer; 25 | o.autoUpdater = remote.autoUpdater; 26 | 27 | // Custom resources 28 | o.config = remote.require('./lib/config'); 29 | o.updater = remote.require('./lib/update'); 30 | o.is = remote.require('electron-is'); 31 | o.program = remote.require('yargs').argv; 32 | o.torrents = remote.require('./lib/torrents'); 33 | o.themes = remote.require('./lib/themes'); 34 | o.ca = remote.require('./lib/certificates') 35 | 36 | // Return object 37 | return o; 38 | }] 39 | -------------------------------------------------------------------------------- /src/scripts/services/httpFormService.ts: -------------------------------------------------------------------------------- 1 | export let httpFormService = [function() { 2 | 3 | // I prepare the request data for the form post. 4 | function transformRequest(data, getHeaders) { 5 | var headers = getHeaders(); 6 | headers["Content-Type"] = "application/x-www-form-urlencoded"; 7 | return(serializeData(data)); 8 | } 9 | 10 | // Return the factory value. 11 | return(transformRequest); 12 | 13 | function serializeData(data) { 14 | // If this is not an object, defer to native stringification. 15 | if(!angular.isObject(data)) { 16 | return((data === null) ? "" : data.toString()); 17 | } 18 | 19 | var buffer = []; 20 | // Serialize each key in the object. 21 | for(var name in data) { 22 | if(!data.hasOwnProperty(name)) { 23 | continue; 24 | } 25 | var value = data[name]; 26 | buffer.push(parseComponent(name, value)); 27 | } 28 | 29 | // Serialize the buffer and clean it up for transportation. 30 | return buffer.join("&") 31 | } 32 | 33 | function parseComponent(name, value) { 34 | return encodeURIComponent(name) + "=" + parseValue(value); 35 | } 36 | 37 | function parseValue(value){ 38 | if (Array.isArray(value)) { 39 | var encoded = value.map(encodeURIComponent); 40 | return encoded.join('|'); 41 | } else if (value !== null) { 42 | return encodeURIComponent(value) 43 | } else { 44 | return ""; 45 | } 46 | } 47 | }]; -------------------------------------------------------------------------------- /src/scripts/services/notification.ts: -------------------------------------------------------------------------------- 1 | 2 | export let notificationService = ["$rootScope", "electron", function ($rootScope, electron) { 3 | 4 | const ERR_SELF_SIGNED_CERT = 'DEPTH_ZERO_SELF_SIGNED_CERT' 5 | const ERR_TLS_CERT_ALTNAME_INVALID = 'ERR_TLS_CERT_ALTNAME_INVALID' 6 | const CERT_HAS_EXPIRED = 'CERT_HAS_EXPIRED' 7 | const UNABLE_TO_VERIFY_LEAF_SIGNATURE = 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' 8 | 9 | const ERR_CODES = { 10 | ERR_SELF_SIGNED_CERT: { 11 | title: 'Untrusted certificate', 12 | msg: 'Self signed certificate is not trusted with this server', 13 | }, 14 | ERR_TLS_CERT_ALTNAME_INVALID: { 15 | title: 'Certificate error', 16 | msg: 'The certificate is not useable with this server\ 17 | because the common name of the certificate does not match\ 18 | the hostname of the server', 19 | }, 20 | CERT_HAS_EXPIRED: { 21 | title: 'Certificate expired', 22 | msg: 'The certificate for this server has expired\ 23 | and is therefore not trusted', 24 | }, 25 | UNABLE_TO_VERIFY_LEAF_SIGNATURE: { 26 | title: 'Invalid certificate chain', 27 | msg: 'The certificate could not be verified because the certificate\ 28 | chain is invalid. Consolidate your webserver TLS configuration' 29 | } 30 | } 31 | 32 | var disableNotifications = false; 33 | 34 | this.disableAll = function () { 35 | disableNotifications = true; 36 | } 37 | 38 | this.enableAll = function () { 39 | disableNotifications = false; 40 | } 41 | 42 | this.alert = function (title, message) { 43 | sendNotification(title, message, "negative"); 44 | } 45 | 46 | this.warning = function (title, message) { 47 | sendNotification(title, message, "warning"); 48 | } 49 | 50 | this.ok = function (title, message) { 51 | sendNotification(title, message, "positive"); 52 | } 53 | 54 | function sendNotification(title, message, type) { 55 | if (disableNotifications) return; 56 | var notification = { 57 | title: title, 58 | message: message, 59 | type: type 60 | } 61 | $rootScope.$emit('notification', notification); 62 | } 63 | 64 | this.alertAuth = function (err) { 65 | if (typeof err === 'string') { 66 | this.alert('Connection problem', err) 67 | } else if (typeof err !== 'object') { 68 | this.alert("Connection problem", "The connection could not be established") 69 | } else if (err.status === -1) { 70 | this.alert("Connection problem", "The connection to the server timed out!") 71 | } else if (err.status === 401) { 72 | this.alert("Connection problem", "You entered an incorrent username/password") 73 | } else if (err.code && ERR_CODES.hasOwnProperty(err.code)) { 74 | this.alert(ERR_CODES[err.code].title, ERR_CODES[err.code].msg) 75 | } else { 76 | this.alert("Connection problem", "The connection could not be established") 77 | } 78 | } 79 | 80 | this.torrentComplete = function (torrent) { 81 | var torrentNotification = new Notification('Torrent Completed!', { 82 | body: torrent.decodedName, 83 | icon: 'img/electorrent-icon.png' 84 | }) 85 | 86 | torrentNotification.onclick = () => { 87 | console.info('Notification clicked') 88 | } 89 | } 90 | 91 | // Listen for incomming notifications from main process 92 | electron.ipc.on('notify', function (event, data) { 93 | $rootScope.$apply(function () { 94 | sendNotification(data.title, data.message, data.type || 'warning'); 95 | }) 96 | }) 97 | 98 | }]; 99 | -------------------------------------------------------------------------------- /src/scripts/services/remote.ts: -------------------------------------------------------------------------------- 1 | export let remoteService = ['$q', '$timeout', function($q, $timeout) { 2 | 3 | let _ID = 0 4 | 5 | class Remote { 6 | 7 | private _self: Remote 8 | private _prototype 9 | private _worker 10 | private _callbacks: Record 11 | 12 | constructor(prototype, worker) { 13 | this._self = this 14 | this._prototype = prototype 15 | this._worker = worker 16 | this._worker.onmessage = this._eventListener.bind(this) 17 | 18 | this._worker.onmessageerror = function() { 19 | throw new Error("message error from web worker remote") 20 | } 21 | this._worker.onerror = function() { 22 | throw new Error("error from web worker remote") 23 | } 24 | 25 | this._callbacks = {} 26 | 27 | for (var fn in this._prototype) { 28 | if (typeof this._prototype[fn] === 'function') { 29 | this._define(fn) 30 | } 31 | } 32 | 33 | this._define('instantiate') 34 | } 35 | 36 | _eventListener(msg) { 37 | var data = msg.data 38 | 39 | if (data.length !== 3) { 40 | return console.error("Invalid response from worker") 41 | } 42 | 43 | var id = data[0] 44 | var error = data[1] 45 | var value = data[2] 46 | 47 | if (!this._callbacks[id]) { 48 | return console.error("No callback found for worker") 49 | } 50 | 51 | this._callbacks[id](error, value) 52 | delete this._callbacks[id] 53 | } 54 | 55 | _error(errObj) { 56 | let msg = errObj.message && errObj.message.replace(/^Error: /, "") 57 | let err = new Error(msg || 'An unknown worker error occured') 58 | for (let k of Object.getOwnPropertyNames(errObj)) { 59 | err[k] = errObj[k] 60 | } 61 | return err 62 | } 63 | 64 | _define(func) { 65 | let self = this 66 | if (this[func]) { 67 | throw new Error("Duplicate method definition") 68 | } 69 | 70 | this[func] = function() { 71 | var defer = $q.defer() 72 | 73 | var timeout = setTimeout(function() { 74 | defer.reject(new Error("no answer from thread")) 75 | }, 10000) 76 | 77 | var id = _ID++ 78 | this._callbacks[id] = function(err, data) { 79 | clearTimeout(timeout) 80 | if (err) { 81 | defer.reject(self._error(err)) 82 | } else { 83 | defer.resolve(data) 84 | } 85 | } 86 | 87 | this._worker.postMessage([id, func, ...arguments]) 88 | 89 | return defer.promise 90 | } 91 | } 92 | } 93 | 94 | return Remote 95 | }]; 96 | -------------------------------------------------------------------------------- /src/scripts/workers/deluge.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Web worker for qBittorrent. 3 | */ 4 | const { InstanceWorker } = require('../../lib/worker') 5 | new InstanceWorker(require('@electorrent/node-deluge'), self) 6 | -------------------------------------------------------------------------------- /src/scripts/workers/qbittorrent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Web worker for qBittorrent. 3 | */ 4 | const { InstanceWorker } = require('../../lib/worker') 5 | new InstanceWorker(require('@electorrent/node-qbittorrent'), self) 6 | -------------------------------------------------------------------------------- /src/scripts/workers/rtorrent.js: -------------------------------------------------------------------------------- 1 | /* Web worker for rTorrent. 2 | * Script is used to load the rTorrent library into a web worker of offload 3 | * performance ciritical tasks to a new thread. 4 | */ 5 | const { InstanceWorker } = require('../../lib/worker') 6 | new InstanceWorker(require('@electorrent/node-rtorrent'), self) 7 | -------------------------------------------------------------------------------- /src/types/angular.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace angular { 2 | type TorrentClient = import("../scripts/bittorrent/torrentclient").TorrentClient 3 | 4 | interface IRootScopeService { 5 | $btclient: TorrentClient 6 | } 7 | } -------------------------------------------------------------------------------- /src/types/electorrent.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Electorrent { 2 | interface Testing123 { 3 | test: string 4 | } 5 | } -------------------------------------------------------------------------------- /src/types/electron.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessVersions { 3 | chrome: string; 4 | electron: string; 5 | } 6 | } -------------------------------------------------------------------------------- /src/types/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | //const angular: ng.IAngularStatic; 3 | } 4 | export { }; -------------------------------------------------------------------------------- /src/types/html.d.ts: -------------------------------------------------------------------------------- 1 | // Make imports for html files valid in typescript 2 | declare module '*.html' { 3 | const value: string; 4 | export default value 5 | } 6 | -------------------------------------------------------------------------------- /src/views/misc/labels.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/modals/addtorrent.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /src/views/modals/newlabel.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /src/views/modals/rename.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /src/views/modals/update.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /src/views/notifications.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 | 7 |
8 | {{notification.title}} 9 |
10 |

{{notification.message}}

11 |
12 |
13 |
14 | 15 | 16 | 21 | 22 | 23 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /src/views/servers.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

6 |
7 | Connect to Server 8 |
9 |

10 | 11 |
12 |
13 |
14 |
15 | 16 | {{server.getName()}} 17 |
18 |
19 | {{server.getNameAtAddress()}} 20 |
21 |
22 |
23 | Connect 24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /src/views/settings.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 | 51 |
52 | 62 |
63 | 64 |
65 | 66 |
67 | -------------------------------------------------------------------------------- /src/views/settings/about.html: -------------------------------------------------------------------------------- 1 | 2 |

About

3 | 4 |
5 |
6 |
7 | 8 |
9 | Application Version 10 |
{{appVersion}}
11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 | Node Version 19 |
{{nodeVersion}}
20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 | Chrome Version 28 |
{{chromeVersion}}
29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 | Electron Version 37 |
{{electronVersion}}
38 |
39 |
40 |
41 |
-------------------------------------------------------------------------------- /src/views/settings/connection.html: -------------------------------------------------------------------------------- 1 | 2 |

Connection

3 |
4 |
5 |
6 | 7 | 8 | 9 | {{client.name}} 10 | 11 | 12 |
13 |
14 | 15 | 16 | http 17 | https 18 | 19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 | 34 |
35 |
36 |
37 | 38 |
39 | 40 | 41 | 44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 | 52 | 53 |
54 |
55 |
56 | 57 |
58 | 59 | 60 |
61 |
62 |
63 |
-------------------------------------------------------------------------------- /src/views/settings/layout.html: -------------------------------------------------------------------------------- 1 | 2 |

Layout

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 24 | 25 | 26 | 27 |
OrderEnabledName
15 |
16 | 17 |
18 |
20 |
21 | 22 |
23 |
{{col.name}}
-------------------------------------------------------------------------------- /src/views/settings/servers.html: -------------------------------------------------------------------------------- 1 | 2 |

Servers

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 44 | 45 | 46 | 57 | 58 | 59 |
DefaultClientIP AddressProtocolPortUsernameActions
18 |
19 | 20 | 21 |
22 |
{{server.getName()}}{{server.ip}} 26 |
27 |
28 |
29 | {{server.proto}} 30 |
31 |
32 |
33 |
34 | {{server.proto}} 35 |
36 |
37 |
38 |
39 | {{server.proto}} 40 |
41 |
42 |
43 |
{{server.port}}{{server.user}} 47 | 50 | 53 | 56 |
60 | 61 | 67 | -------------------------------------------------------------------------------- /src/views/welcome.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |

7 |
8 | Welcome to Electorrent 9 |
10 |

11 |
18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | http 29 | 30 | 31 | https 32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 |
45 | 46 | 53 |
54 |
55 |
56 |
57 | 64 | 70 | {{client.name}} 71 | 72 | 73 |
74 |
75 |
76 |
77 | 78 | 79 |
80 |
81 |
82 |
83 | 84 | 92 |
93 |
94 |
95 |
96 | 97 | 98 |
99 |
100 |
101 | 104 |
105 |
106 |
107 |
108 |
109 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | data -------------------------------------------------------------------------------- /test/e2e/index.ts: -------------------------------------------------------------------------------- 1 | export { App, LoginOptions } from "./e2e_app" 2 | export { Torrent } from "./e2e_torrent" 3 | -------------------------------------------------------------------------------- /test/fixtures/deluge/deluge.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestSuite } from "../../testlib"; 2 | import { FeatureSet } from "../../testutil"; 3 | import { DelugeClient } from "../../../src/scripts/bittorrent" 4 | 5 | createTestSuite({ 6 | client: new DelugeClient(), 7 | fixture: "fixtures/deluge", 8 | version: "1", 9 | port: 8112, 10 | username: "admin", 11 | password: "deluge", 12 | stopLabel: "Paused", 13 | unsupportedFeatures: [ 14 | FeatureSet.Labels, 15 | FeatureSet.MagnetLinks, 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /test/fixtures/deluge/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | deluge: 5 | image: spritsail/deluge:${VERSION:-latest} 6 | ports: 7 | - 8112:8112 8 | tmpfs: 9 | - /downloads 10 | 11 | networks: 12 | default: 13 | name: electorrent_p2p 14 | external: true 15 | -------------------------------------------------------------------------------- /test/fixtures/qbittorrent/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | qbittorrent: 5 | image: linuxserver/qbittorrent:${VERSION:-latest} 6 | ports: 7 | - 8080:8080 8 | environment: 9 | - WEBUI_PORT=8080 10 | volumes: 11 | - ./qBittorrent.conf:/defaults/qBittorrent.conf 12 | tmpfs: 13 | - /downloads 14 | 15 | networks: 16 | default: 17 | name: electorrent_p2p 18 | external: true 19 | -------------------------------------------------------------------------------- /test/fixtures/qbittorrent/qBittorrent.conf: -------------------------------------------------------------------------------- 1 | [AutoRun] 2 | enabled=false 3 | program= 4 | 5 | [LegalNotice] 6 | Accepted=true 7 | 8 | [Preferences] 9 | Connection\UPnP=false 10 | Connection\PortRangeMin=6881 11 | Downloads\SavePath=/downloads/ 12 | Downloads\ScanDirsV2=@Variant(\0\0\0\x1c\0\0\0\0) 13 | Downloads\TempPath=/downloads/incomplete/ 14 | WebUI\Address=* 15 | WebUI\ServerDomains=* 16 | WebUI\Username=admin 17 | # Set default password to "adminadmin" 18 | WebUI\Password_PBKDF2="@ByteArray(lDSpDFHmT58el+iNKEbHUQ==:4q6atHknt54fYuXSxhakD9xgfTjCgSRxtQWk0x3V5N0EmKgki5pnA+/UrkLUGdQuvoGKIwi8b+fwqA8rZFrmyQ==)" -------------------------------------------------------------------------------- /test/fixtures/qbittorrent/qbittorrent.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestSuite } from "../../testlib"; 2 | 3 | import { QBittorrentClient } from "../../../src/scripts/bittorrent" 4 | 5 | createTestSuite({ 6 | client: new QBittorrentClient(), 7 | fixture: "fixtures/qbittorrent", 8 | version: "latest", 9 | port: 8080, 10 | username: "admin", 11 | password: "adminadmin", 12 | unsupportedFeatures: [], 13 | }); 14 | 15 | createTestSuite({ 16 | client: new QBittorrentClient(), 17 | fixture: "fixtures/qbittorrent", 18 | version: "5.1.0", 19 | port: 8080, 20 | username: "admin", 21 | password: "adminadmin", 22 | unsupportedFeatures: [], 23 | }); 24 | 25 | createTestSuite({ 26 | client: new QBittorrentClient(), 27 | fixture: "fixtures/qbittorrent", 28 | version: "4.6.7", 29 | port: 8080, 30 | username: "admin", 31 | password: "adminadmin", 32 | unsupportedFeatures: [], 33 | }); 34 | -------------------------------------------------------------------------------- /test/fixtures/rutorrent/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | rutorrent: 5 | image: linuxserver/rutorrent:${VERSION:-latest} 6 | ports: 7 | - 8080:80 8 | - 5000:5000 9 | - 51413:51413 10 | - 6881:6881/udp 11 | tmpfs: 12 | - /downloads 13 | - /config 14 | 15 | networks: 16 | default: 17 | name: electorrent_p2p 18 | external: true 19 | -------------------------------------------------------------------------------- /test/fixtures/rutorrent/rtorrent.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestSuite } from "../../testlib"; 2 | import { FeatureSet } from "../../testutil"; 3 | import { RtorrentClient } from "../../../src/scripts/bittorrent" 4 | 5 | createTestSuite({ 6 | client: new RtorrentClient(), 7 | fixture: "fixtures/rutorrent", 8 | port: 8080, 9 | proxyPort: 80, 10 | acceptHttpStatus: 200, 11 | username: "admin", 12 | password: "admin", 13 | unsupportedFeatures: [ 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /test/fixtures/transmission/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | transmission: 5 | image: linuxserver/transmission:${VERSION:-latest} 6 | environment: 7 | - USER=username 8 | - PASS=password 9 | ports: 10 | - 9091:9091 11 | 12 | networks: 13 | default: 14 | name: electorrent_p2p 15 | external: true 16 | 17 | -------------------------------------------------------------------------------- /test/fixtures/transmission/transmission.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestSuite } from "../../testlib"; 2 | import { FeatureSet } from "../../testutil"; 3 | import { TransmissionClient } from "../../../src/scripts/bittorrent" 4 | 5 | createTestSuite({ 6 | client: new TransmissionClient(), 7 | fixture: "fixtures/transmission", 8 | port: 9091, 9 | username: "username", 10 | password: "password", 11 | acceptHttpStatus: 401, 12 | unsupportedFeatures: [ 13 | FeatureSet.Labels, 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /test/fixtures/utorrent/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | utorrent: 5 | image: ekho/utorrent:${VERSION:-latest} 6 | ports: 7 | - 8080:8080 8 | tmpfs: 9 | - /data:uid=1001,gid=1001 10 | 11 | networks: 12 | default: 13 | name: electorrent_p2p 14 | external: true 15 | -------------------------------------------------------------------------------- /test/fixtures/utorrent/utorrent.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestSuite } from "../../testlib"; 2 | import { FeatureSet } from "../../testutil"; 3 | import { UtorrentClient } from "../../../src/scripts/bittorrent" 4 | 5 | createTestSuite({ 6 | client: new UtorrentClient(), 7 | fixture: "fixtures/utorrent", 8 | port: 8080, 9 | username: "admin", 10 | password: "", 11 | acceptHttpStatus: 400, 12 | unsupportedFeatures: [ 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /test/shared/app.hook.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '@wdio/globals' 2 | import { App } from "../e2e" 3 | 4 | /** 5 | * Mocha hooks to start and stop Electron application before and after each suite. This function register the 6 | * `before` and `after` hooks in Mocha. 7 | */ 8 | export function startApplicationHooks() { 9 | 10 | before(async function (this: Mocha.Context) { 11 | // Open the application 12 | await browser.reloadSession() 13 | this.timeout(10 * 1000) 14 | this.app = new App(); 15 | }) 16 | 17 | after(async function () { 18 | this.timeout(10 * 1000) 19 | }) 20 | 21 | } 22 | 23 | export async function restartApplication(context: Mocha.Context) { 24 | await browser.refresh() 25 | } -------------------------------------------------------------------------------- /test/shared/backend.hook.ts: -------------------------------------------------------------------------------- 1 | import compose, { IDockerComposeOptions } from "docker-compose"; 2 | import path from "path"; 3 | import { waitUntil } from "../testutil"; 4 | import { dockerComposeHooks } from "./compose.hook"; 5 | 6 | /** 7 | * Backend is a utility class to interact with the docker-compose containers runing the Bittorrent 8 | * backend being tested 9 | */ 10 | export class Backend { 11 | 12 | composeDir: string 13 | serviceName: string 14 | composeOptions: IDockerComposeOptions 15 | 16 | constructor(composeDir: string | string[]) { 17 | this.composeDir = Array.isArray(composeDir) ? path.join(...composeDir) : composeDir 18 | this.serviceName = path.basename(this.composeDir) 19 | this.composeOptions = { 20 | cwd: this.composeDir, 21 | } 22 | } 23 | 24 | async exec(command: string | string[]) { 25 | let result = await compose.exec(this.serviceName, command, this.composeOptions) 26 | if (result.exitCode !== 0) { 27 | throw new Error(`command for ${this.serviceName} container exited with code ${result.exitCode}`) 28 | } 29 | return result 30 | } 31 | 32 | async waitForExec(command: string | string[], timeout?: number, step?: number) { 33 | await waitUntil(async () => this.exec(command), timeout, step) 34 | } 35 | 36 | async pause() { 37 | await compose.pauseOne(this.serviceName, this.composeOptions) 38 | } 39 | 40 | async unpause() { 41 | await compose.unpauseOne(this.serviceName, this.composeOptions) 42 | } 43 | 44 | } 45 | 46 | /** 47 | * Start the backend bittorrent service being tested using docker-compose 48 | * @param composeDir directory containing docker-compose.yml 49 | * @param extraOpts extra arguments to docker-compose 50 | */ 51 | export function backendHooks(composeDir: string | string[], extraOpts?: IDockerComposeOptions) { 52 | dockerComposeHooks(composeDir, extraOpts) 53 | 54 | before(async function() { 55 | this.backend = new Backend(composeDir) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /test/shared/compose.hook.ts: -------------------------------------------------------------------------------- 1 | import compose, { IDockerComposeOptions } from "docker-compose" 2 | import path from "path" 3 | 4 | /** 5 | * Mocha hooks to start up and shut down a docker-compose service using the "before" and 6 | * "after" hooks. This means the services described in your docker-compose.yml file will 7 | * be running for the entire current mocha context. Before the docker-compose services 8 | * are started all existing containers are removed to ensure integrity of the test fixture 9 | * @param composeDir the directory containing your docker-compose.yml file 10 | * @param extraOpts additonal options to the docker-compose invocation 11 | */ 12 | export function dockerComposeHooks(composeDir: string | string[], extraOpts?: IDockerComposeOptions) { 13 | 14 | if (Array.isArray(composeDir)) { 15 | composeDir = path.join(...composeDir) 16 | } 17 | 18 | const composeOpts: IDockerComposeOptions = { 19 | cwd: composeDir, 20 | log: true, 21 | } 22 | 23 | before(async function() { 24 | this.timeout(60 * 1000) 25 | await compose.rm({ ...composeOpts, ...extraOpts}) 26 | await compose.upAll({ ...composeOpts, ...extraOpts, commandOptions: ['--build'] }) 27 | }) 28 | 29 | after(async function() { 30 | this.timeout(60 * 1000) 31 | if (!process.env.MOCHA_DOCKER_KEEP) { 32 | await compose.down({ ...composeOpts, ...extraOpts }) 33 | } 34 | }) 35 | } -------------------------------------------------------------------------------- /test/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { startApplicationHooks, restartApplication } from "./app.hook" 2 | export { dockerComposeHooks } from "./compose.hook" 3 | -------------------------------------------------------------------------------- /test/shared/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.25-alpine 2 | 3 | # Install openssl to generate x509 certs on startup 4 | RUN apk add openssl 5 | 6 | # Copy all configuraiton files 7 | COPY rootfs/ / 8 | -------------------------------------------------------------------------------- /test/shared/nginx/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | nginx: 5 | build: . 6 | ports: 7 | - 8443:443 8 | environment: 9 | - PROXY_HOST=${PROXY_HOST} 10 | - PROXY_PORT=${PROXY_PORT} 11 | 12 | networks: 13 | default: 14 | name: electorrent_p2p 15 | external: true 16 | -------------------------------------------------------------------------------- /test/shared/nginx/rootfs/docker-entrypoint.d/40-generate-x509.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes \ 6 | -subj "/C=US/ST=Test/L=Test/O=Test/CN=localhost" \ 7 | -out /etc/ssl/certs/dummy.crt \ 8 | -keyout /etc/ssl/private/dummy.key 9 | -------------------------------------------------------------------------------- /test/shared/nginx/rootfs/etc/nginx/templates/default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | # Redirect all traffic to SSL 5 | rewrite ^ https://$server_name$request_uri?; 6 | } 7 | 8 | server { 9 | listen 443 ssl default_server; 10 | 11 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 12 | ssl_ciphers HIGH:!aNULL:!MD5; 13 | 14 | ## Access and error logs. 15 | access_log /var/log/nginx/access.log; 16 | error_log /var/log/nginx/error.log info; 17 | 18 | ## Keep alive timeout set to a greater value for SSL/TLS. 19 | keepalive_timeout 75 75; 20 | 21 | ## See the keepalive_timeout directive in nginx.conf. 22 | ## Server certificate and key. 23 | ssl_certificate /etc/ssl/certs/dummy.crt; 24 | ssl_certificate_key /etc/ssl/private/dummy.key; 25 | ssl_session_timeout 5m; 26 | 27 | ## Strict Transport Security header for enhanced security. See 28 | ## http://www.chromium.org/sts. I've set it to 2 hours; set it to 29 | ## whichever age you want. 30 | add_header Strict-Transport-Security "max-age=7200"; 31 | 32 | location / { 33 | proxy_http_version 1.1; 34 | proxy_set_header Host ${PROXY_HOST}:${PROXY_PORT}; 35 | proxy_set_header X-Forwarded-Host $http_host; 36 | proxy_set_header X-Forwarded-For $remote_addr; 37 | proxy_set_header Referer ''; 38 | proxy_pass http://${PROXY_HOST}:${PROXY_PORT}; 39 | } 40 | } -------------------------------------------------------------------------------- /test/shared/opentracker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | # Common configuration for a peer in the p2p network 4 | x-peer: &peer 5 | build: peer 6 | networks: 7 | - p2p 8 | volumes: 9 | - ./data/shared:/shared 10 | tmpfs: 11 | - /srv 12 | environment: 13 | - P2P_PORT=2706 14 | command: ["/start", "leech"] 15 | 16 | 17 | services: 18 | # Opentracker (http://erdgeist.org/arts/software/opentracker/) 19 | # A local torrent tracker used in an ad-hoc torrent network to test 20 | # with real P2P traffic. 21 | tracker: 22 | hostname: tracker 23 | build: tracker 24 | ports: 25 | - 6969:6969 26 | - 6969:6969/udp 27 | networks: 28 | - p2p 29 | 30 | 31 | # A peer in the ad-hoc torrent network that announces torrents on the P2P 32 | # network using the local tracker (see above). Used to provide content to 33 | # the P2P network for testing file transmission. 34 | peer01: 35 | hostname: peer01 36 | <<: *peer 37 | ports: 38 | - 2706:2706 39 | command: ["/start", "seed"] 40 | 41 | # A peer that is leeching (downloading files) from other peers in the network 42 | peer02: 43 | hostname: peer02 44 | <<: *peer 45 | command: ["/start", "leech"] 46 | 47 | volumes: 48 | shared: 49 | driver_opts: 50 | type: tmpfs 51 | device: tmpfs 52 | 53 | networks: 54 | p2p: 55 | name: electorrent_p2p -------------------------------------------------------------------------------- /test/shared/opentracker/peer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | 3 | RUN apt-get update \ 4 | && apt-get install ctorrent \ 5 | && rm -rf /var/lib/apt/lists/* 6 | COPY ./rootfs / 7 | CMD ["/start"] 8 | -------------------------------------------------------------------------------- /test/shared/opentracker/peer/rootfs/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | # This script handles execution of two roles: a seeder and a leecher. 4 | # The seeder will generate files with random (/dev/urandom) content, the 5 | # corrosponding .torrent files and a checksum file. Files are shared with 6 | # other peers in the /shared folder. The leecher will download the content 7 | # using the shared .torrent file and check the content using the checksum file 8 | 9 | # The seeder role 10 | if [ "$1" == "seed" ]; then 11 | >2 echo "Peer acting as seeder" 12 | # Create a random test file 13 | dd if=/dev/urandom of=/srv/test-100k.bin bs=1M count=50 14 | # Delete any old torrent files 15 | rm -rf /shared/*.torrent 16 | # Create a torrent file and save it in the shared folder 17 | ctorrent -t -s /shared/test-100k.bin.torrent -u http://tracker:6969/announce /srv/test-100k.bin 18 | # Write a checksum file for the files to be seeded 19 | sha256sum /srv/*.bin | tee /shared/checksum 20 | # Seed the torrent file (and contents) 21 | ctorrent -P "$HOSTNAME" -p "$P2P_PORT" -U10 -D10 -s /srv/test-100k.bin /shared/test-100k.bin.torrent 22 | exit 0 23 | fi 24 | 25 | # The leecher role 26 | if [ "$1" == "leech" ]; then 27 | >2 echo "Peer acting a leecher" 28 | # Wait until files have been shared from seeder 29 | until [ -f /shared/checksum ]; do sleep 0.2; done 30 | # Make sure no files exists (will be downloaded shortly) 31 | test ! -e /srv/*.bin 32 | # Download files using the ctorrent client 33 | ctorrent /shared/test-100k.bin.torrent -s /srv/test-100k.bin -e0 -E0 34 | # Check that files a correct 35 | sha256sum -c /shared/checksum 36 | exit 0 37 | fi 38 | -------------------------------------------------------------------------------- /test/shared/opentracker/tracker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lednerb/opentracker-docker 2 | 3 | COPY ./rootfs / 4 | -------------------------------------------------------------------------------- /test/shared/opentracker/tracker/rootfs/etc/opentracker/opentracker.conf: -------------------------------------------------------------------------------- 1 | # opentracker config file 2 | # 3 | 4 | # I) Address opentracker will listen on, using both, tcp AND udp family 5 | # (note, that port 6969 is implicite if ommitted). 6 | # 7 | # If no listen option is given (here or on the command line), opentracker 8 | # listens on 0.0.0.0:6969 tcp and udp. 9 | # 10 | # The next variable determines if udp sockets are handled in the event 11 | # loop (set it to 0, the default) or are handled in blocking reads in 12 | # dedicated worker threads. You have to set this value before the 13 | # listen.tcp_udp or listen.udp statements before it takes effect, but you 14 | # can re-set it for each listen statement. Normally you should keep it at 15 | # the top of the config file. 16 | # 17 | # listen.udp.workers 4 18 | # 19 | # listen.tcp_udp 0.0.0.0 20 | # listen.tcp_udp 192.168.0.1:80 21 | # listen.tcp_udp 10.0.0.5:6969 22 | # 23 | # To only listen on tcp or udp family ports, list them this way: 24 | # 25 | # listen.tcp 0.0.0.0 26 | # listen.udp 192.168.0.1:6969 27 | # 28 | # Note, that using 0.0.0.0 for udp sockets may yield surprising results. 29 | # An answer packet sent on that socket will not necessarily have the 30 | # source address that the requesting client may expect, but any address 31 | # on that interface. 32 | # 33 | 34 | # II) If opentracker runs in a non-open mode, point it to files containing 35 | # all torrent hashes that it will serve (shell option -w) 36 | # 37 | # access.whitelist /etc/opentracker/whitelist.txt 38 | # 39 | # or, if opentracker was compiled to allow blacklisting (shell option -b) 40 | # 41 | # access.blacklist /etc/opentracker/blacklist.txt 42 | # 43 | # It is pointless and hence not possible to compile black AND white 44 | # listing, so choose one of those options at compile time. File format 45 | # is straight forward: "\n\n..." 46 | # 47 | # If you do not want to grant anyone access to your stats, enable the 48 | # WANT_RESTRICT_STATS option in Makefile and bless the ip addresses 49 | # allowed to fetch stats here. 50 | # 51 | # access.stats 192.168.0.23 52 | # 53 | # There is another way of hiding your stats. You can obfuscate the path 54 | # to them. Normally it is located at /stats but you can configure it to 55 | # appear anywhere on your tracker. 56 | # 57 | # access.stats_path stats 58 | 59 | # III) Live sync uses udp multicast packets to keep a cluster of opentrackers 60 | # synchronized. This option tells opentracker which port to listen for 61 | # incoming live sync packets. The ip address tells opentracker, on which 62 | # interface to join the multicast group, those packets will arrive. 63 | # (shell option -i 192.168.0.1 -s 9696), port 9696 is default. 64 | # 65 | # livesync.cluster.listen 192.168.0.1:9696 66 | # 67 | # Note that two udp sockets will be opened. One on ip address 0.0.0.0 68 | # port 9696, that will join the multicast group 224.0.42.23 for incoming 69 | # udp packets and one on ip address 192.168.0.1 port 9696 for outgoing 70 | # udp packets. 71 | # 72 | # As of now one and only one ip address must be given, if opentracker 73 | # was built with the WANT_SYNC_LIVE feature. 74 | # 75 | 76 | # IV) Sync between trackers running in a cluster is restricted to packets 77 | # coming from trusted ip addresses. While source ip verification is far 78 | # from perfect, the authors of opentracker trust in the correct 79 | # application of tunnels, filters and LAN setups (shell option -A). 80 | # 81 | # livesync.cluster.node_ip 192.168.0.4 82 | # livesync.cluster.node_ip 192.168.0.5 83 | # livesync.cluster.node_ip 192.168.0.6 84 | # 85 | # This is the admin ip address for old style (HTTP based) asynchronus 86 | # tracker syncing. 87 | # 88 | # batchsync.cluster.admin_ip 10.1.1.1 89 | # 90 | 91 | # V) Control privilege drop behaviour. 92 | # Put in the directory opentracker will chroot/chdir to. All black/white 93 | # list files must be put in that directory (shell option -d). 94 | # 95 | # 96 | # tracker.rootdir /usr/local/etc/opentracker 97 | # 98 | # Tell opentracker which user to setuid to. 99 | # 100 | # tracker.user nobody 101 | # 102 | 103 | # VI) opentracker can be told to answer to a "GET / HTTP"-request with a 104 | # redirect to another location (shell option -r). 105 | # 106 | tracker.redirect_url http://erdgeist.org/arts/software/opentracker/ 107 | -------------------------------------------------------------------------------- /test/testutil.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import readline from "readline" 3 | import { afterEach } from "mocha"; 4 | 5 | /** 6 | * Enum representing the different features of a bittorrent client to be tested 7 | */ 8 | export enum FeatureSet { 9 | Labels, 10 | MagnetLinks, 11 | } 12 | 13 | /** 14 | * Asks for a qestion is console and wait for answer 15 | * @param query Question to be asked 16 | */ 17 | export function askQuestion(query: string) { 18 | const rl = readline.createInterface({ 19 | input: process.stdin, 20 | output: process.stdout, 21 | }); 22 | return new Promise(resolve => rl.question(query, ans => { 23 | rl.close(); 24 | resolve(ans); 25 | })) 26 | } 27 | 28 | /** 29 | * Returns a promise that always resolves after set milliseconds 30 | * @param time milliseconds before promise is resolved 31 | */ 32 | export const sleep: (time: number) => Promise 33 | = (time: number) => new Promise(resolve => setTimeout(resolve, time)); 34 | 35 | /** 36 | * Continously performs http requests every `step` milliseconds for up to `timeout` milliseconds 37 | * until response has status code `statusCode`. If status code has not been observed within `timeout` 38 | * an exception is thrown 39 | */ 40 | export async function waitForHttp( 41 | { url, statusCode=200, timeout=30000, step=1000 }: 42 | { url: string, statusCode?: number, timeout?: number, step?: number }) 43 | { 44 | let timeSpent = 0; 45 | while (true) { 46 | if (timeSpent > timeout) { 47 | throw new Error(`Timeout waiting for ${url}`); 48 | } 49 | try { 50 | let res = await axios.get(url, { 51 | timeout: 1000, 52 | validateStatus: _ => true 53 | }) 54 | if (res.status === statusCode) { 55 | return; 56 | } 57 | } catch (err) { } 58 | await sleep(step) 59 | timeSpent += step 60 | } 61 | } 62 | 63 | /** 64 | * Perform a test function multiple times until it succeeds without an exception 65 | * @param fn function to test 66 | * @param timeout time to wait for `fn` to return successfully without an exception 67 | * @param step time between calls of `fn` 68 | * @returns whatever `fn` returns 69 | */ 70 | export async function waitUntil(fn: () => Promise, timeout?: number, step?: number) { 71 | let currentTime = 0 72 | let err: any 73 | timeout = timeout ? timeout : 5000 74 | step = step ? step : 500 75 | while (currentTime <= timeout) { 76 | try { 77 | return await fn() 78 | } catch (e) { 79 | err = e 80 | } 81 | await sleep(step) 82 | currentTime += step 83 | } 84 | throw err 85 | } 86 | 87 | /** 88 | * Sets up Mocha hooks for test execution. This function adds an `afterEach` hook 89 | * that performs specific actions based on the state of the test and environment variables. 90 | * 91 | * - If a test fails and the `MOCHA_HALT` environment variable is set to "1", the test execution 92 | * will halt and wait for user input before proceeding. 93 | * - If the `MOCHA_STEP` environment variable is set, the test execution will pause after each test 94 | * and wait for user input before proceeding. 95 | * 96 | * @example 97 | * setupMochaHooks(); 98 | * 99 | * Environment Variables: 100 | * - `MOCHA_HALT`: Set to "1" to halt execution on test failure and wait for user input. 101 | * - `MOCHA_STEP`: Set to pause execution after each test and wait for user input. 102 | */ 103 | export function setupMochaHooks() { 104 | afterEach(async function() { 105 | if (this.currentTest && this.currentTest.state === "failed") { 106 | if (process.env.MOCHA_HALT === "1") { 107 | // halt and wait until user decides to proceed (or very long timeout) 108 | this.timeout(Math.pow(2, 32)); 109 | await askQuestion("Test failed. Press any key to continue: "); 110 | } 111 | return; 112 | } 113 | if (process.env.MOCHA_STEP) { 114 | this.timeout(Math.pow(2, 32)); 115 | await askQuestion("Test paused. Press any key to continue: "); 116 | } 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node14/tsconfig.json", 3 | "include": [ 4 | "./" 5 | ], 6 | "compilerOptions": { 7 | "lib": [ 8 | "es6" 9 | ], 10 | "module": "commonjs", 11 | "target": "es2020", 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "noImplicitAny": false, 16 | "forceConsistentCasingInFileNames": true, 17 | "types": [ 18 | "node", 19 | "webdriverio/async", 20 | "@types/mocha", 21 | "@types/chai", 22 | "@types/chai-as-promised", 23 | ], 24 | "typeRoots": [ 25 | "types", 26 | "../node_modules/@types", 27 | "../node_modules/webdriverio/types" 28 | ] 29 | }, 30 | "exclude": [ 31 | "node_modules" 32 | ] 33 | } -------------------------------------------------------------------------------- /test/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Augment global mocha context with custom attributes 2 | declare namespace Mocha { 3 | interface Context { 4 | app: import("../e2e").App 5 | backend: import("../shared/backend.hook").Backend 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "noImplicitAny": false, 5 | "target": "es2017", 6 | "pretty": true, 7 | "module": "commonjs", 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "lib": [ 11 | "dom", 12 | "es5" 13 | ], 14 | "experimentalDecorators": true, 15 | "allowSyntheticDefaultImports": true, 16 | "esModuleInterop": true, 17 | "strict": false, 18 | "outDir": "./app", 19 | "typeRoots": [ 20 | "node_modules/@types", 21 | "src/types" 22 | ] 23 | }, 24 | "include": ["src/**/*.ts"], 25 | "exclude": ["node_modues"] 26 | } -------------------------------------------------------------------------------- /util/after-pack.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | const exec = util.promisify(require("child_process").exec); 3 | const path = require("path") 4 | 5 | exports.default = async function(context) { 6 | const electronPlatformNameLoweredCase = context.electronPlatformName.toLowerCase(); 7 | 8 | if (electronPlatformNameLoweredCase.startsWith("lin")) { 9 | const chromeSandbox = path.join(context.appOutDir, "chrome-sandbox"); 10 | console.log(`Changing permissions for ${chromeSandbox}`); 11 | await exec(`chmod 4755 ${chromeSandbox}`); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /util/mac_icon_gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | IMAGE=icon_512px.png 4 | 5 | mkdir electorrent.iconset 6 | cd electorrent.iconset 7 | convert -resize 16x16 ../$IMAGE icon_16x16.png 8 | convert -resize 32x32 ../$IMAGE icon_16x16@2x.png 9 | convert -resize 32x32 ../$IMAGE icon_32x32.png 10 | convert -resize 64x64 ../$IMAGE icon_32x32@2x.png 11 | convert -resize 128x128 ../$IMAGE icon_128x128.png 12 | convert -resize 256x256 ../$IMAGE icon_128x128@2x.png 13 | convert -resize 256x256 ../$IMAGE icon_256x256.png 14 | convert -resize 512x512 ../$IMAGE icon_256x256@2x.png 15 | convert -resize 512x512 ../$IMAGE icon_512x512.png 16 | convert -resize 1024x1024 ../$IMAGE icon_512x512@2x.png 17 | cd .. 18 | iconutil -o electorrent.icns -c icns electorrent.iconset -------------------------------------------------------------------------------- /vagrant/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # Environment configuration variables 6 | export GIT_URL="https://github.com/tympanix/Electorrent" 7 | export NODEJS_URL="https://deb.nodesource.com/setup_14.x" 8 | 9 | # Install common packages 10 | sudo apt-get update 11 | sudo apt-get install -y git xvfb build-essential ffmpeg 12 | 13 | # Install nodejs 14 | curl -fsSL "$NODEJS_URL" | sudo -E bash - 15 | sudo apt-get install -y nodejs 16 | 17 | # Install docker 18 | sudo apt-get install -y ca-certificates curl gnupg lsb-release 19 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --batch --keyserver --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 20 | echo \ 21 | "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ 22 | $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 23 | sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io 24 | sudo usermod -a -G docker $USER 25 | 26 | # Install docker-compose 27 | sudo curl -Lfs "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 28 | sudo chmod +x /usr/local/bin/docker-compose 29 | 30 | # Set up project source code and dependencies 31 | ( 32 | PROJ_NAME="$(basename $GIT_URL)" 33 | PROJ_DIR="$HOME/$PROJ_NAME" 34 | if [[ ! -d "$PROJ_DIR" ]]; then 35 | sudo rm -rf "$PROJ_DIR" && mkdir -p "$PROJ_DIR" 36 | git clone "$GIT_URL" "$PROJ_DIR" 37 | fi 38 | cd "$PROJ_DIR" 39 | npm install 40 | ) 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 6 | const nodeExternals = require('webpack-node-externals'); 7 | 8 | // Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up 9 | const defaultInclude = path.resolve(__dirname, 'src') 10 | 11 | module.exports = { 12 | devtool: 'source-map', 13 | entry: path.resolve(__dirname, "src/main.ts"), 14 | output: { 15 | path: path.resolve(__dirname, 'app'), 16 | filename: '[name].js', 17 | }, 18 | target: 'electron-renderer', 19 | mode: 'development', 20 | externals: [ 21 | /* Ignore import from bower packages */ 22 | { 23 | jquery: 'jQuery', 24 | jquery: '$', 25 | angular: 'angular', 26 | mousetrap: 'Mousetrap', 27 | fuse: 'Fuse', 28 | }, 29 | /* Ignore local app dependencies from runtime */ 30 | nodeExternals({ 31 | modulesDir: 'app/node_modules' 32 | }), 33 | ], 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.css$/, 38 | use: [ 39 | MiniCssExtractPlugin.loader, 40 | 'css-loader', 41 | 'postcss-loader' 42 | ], 43 | include: defaultInclude 44 | }, 45 | { 46 | test: /\.tsx?$/, 47 | use: 'ts-loader', 48 | include: defaultInclude, 49 | exclude: /node_modules/, 50 | }, 51 | // { 52 | // test: /\.jsx?$/, 53 | // use: [{ loader: 'babel-loader' }], 54 | // include: defaultInclude 55 | // }, 56 | { 57 | test: /\.(jpe?g|png|gif)$/, 58 | use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }], 59 | include: defaultInclude 60 | }, 61 | { 62 | test: /\.(eot|svg|ttf|woff|woff2)$/, 63 | use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }], 64 | include: defaultInclude 65 | }, 66 | { 67 | test: /\.html$/i, 68 | loader: "html-loader", 69 | }, 70 | ] 71 | }, 72 | resolve: { 73 | extensions: ['.ts', '.tsx', '.js', '.json'], 74 | modules: ['node_modules', 'src'] 75 | }, 76 | plugins: [ 77 | // new HtmlWebpackPlugin({ 78 | // template: path.resolve(__dirname, "src/index.html") 79 | // }), 80 | new MiniCssExtractPlugin({ 81 | // Options similar to the same options in webpackOptions.output 82 | // both options are optional 83 | filename: 'bundle.css', 84 | chunkFilename: '[id].css' 85 | }), 86 | // new webpack.DefinePlugin({ 87 | // 'process.env.NODE_ENV': JSON.stringify('production') 88 | // }), 89 | new webpack.ProvidePlugin({ 90 | 'window.jQuery': 'jquery', 91 | 'jQuery': 'jquery', 92 | '$': 'jquery', 93 | 'angular': 'angular', 94 | }), 95 | ], 96 | stats: { 97 | colors: true, 98 | children: false, 99 | chunks: false, 100 | modules: false 101 | } 102 | } 103 | --------------------------------------------------------------------------------