├── .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 |
24 |
--------------------------------------------------------------------------------
/src/css/fonts/icons/icon_downloadstation.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/src/css/fonts/icons/icon_qbittorrent.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 |
--------------------------------------------------------------------------------
/src/css/fonts/icons/icon_rtorrent.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
--------------------------------------------------------------------------------
/src/css/fonts/icons/icon_transmission.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
32 |
--------------------------------------------------------------------------------
/src/css/fonts/icons/icon_utorrent.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
36 |
37 |
38 |
41 |
42 |
43 |
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 |
2 |
3 |
9 |
10 |
16 |
17 |
18 |
19 |
22 |
26 |
27 |
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 |
2 |
3 |
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: ``,
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 |
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 |
2 |
3 |
14 |
15 |
16 |
23 |
24 |
--------------------------------------------------------------------------------
/src/views/modals/addtorrent.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
14 |
15 |
16 |
25 |
26 |
--------------------------------------------------------------------------------
/src/views/modals/newlabel.html:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/src/views/modals/rename.html:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/src/views/modals/update.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
34 |
35 |
--------------------------------------------------------------------------------
/src/views/notifications.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
{{notification.message}}
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/views/servers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
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 |
34 |
35 |
38 |
39 |
42 |
43 |
46 |
47 |
50 |
51 |
52 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/views/settings/about.html:
--------------------------------------------------------------------------------
1 |
2 | About
3 |
4 |
5 |
6 |
13 |
14 |
15 |
22 |
23 |
24 |
31 |
32 |
33 |
40 |
41 |
--------------------------------------------------------------------------------
/src/views/settings/connection.html:
--------------------------------------------------------------------------------
1 |
2 | Connection
3 |
--------------------------------------------------------------------------------
/src/views/settings/layout.html:
--------------------------------------------------------------------------------
1 |
2 | Layout
3 |
4 |
--------------------------------------------------------------------------------
/src/views/settings/servers.html:
--------------------------------------------------------------------------------
1 |
2 | Servers
3 |
4 |
5 |
6 | Default |
7 | Client |
8 | IP Address |
9 | Protocol |
10 | Port |
11 | Username |
12 | Actions |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | |
23 | {{server.getName()}} |
24 | {{server.ip}} |
25 |
26 |
27 |
28 |
29 | {{server.proto}}
30 |
31 |
32 |
33 |
34 | {{server.proto}}
35 |
36 |
37 |
38 |
39 | {{server.proto}}
40 |
41 |
42 |
43 | |
44 | {{server.port}} |
45 | {{server.user}} |
46 |
47 |
50 |
53 |
56 | |
57 |
58 |
59 |
60 |
61 |
67 |
--------------------------------------------------------------------------------
/src/views/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------