webui start + basic torrent showing

This commit is contained in:
Daan Wijns 2020-05-20 09:59:12 +02:00
parent 402bb61a98
commit d1d01aafe4
25 changed files with 15035 additions and 14945 deletions

View file

@ -1,6 +0,0 @@
nodemodules
LICENSE.md
README.md
babel.config.js
.gitignore
.env

View file

@ -1,7 +0,0 @@
VUE_APP_DOMAIN=http://localhost:3001
VUE_APP_WEB_USER=test
VUE_APP_WEB_PASS=test
QBIT_USER=test
QBIT_PASS=test
QBIT_HOST=localhost:8080
PORT=3001

View file

@ -2,17 +2,17 @@ module.exports = {
env: {
browser: true,
commonjs: true,
es6: true,
es6: true
},
extends: ['plugin:vue/essential', 'airbnb-base'],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: {
ecmaVersion: 2018,
ecmaVersion: 2018
},
plugins: ['vue'],
plugins: ['vue', 'prettier'],
rules: {
semi: ['warn', 'never'],
'no-console': 0,
@ -24,6 +24,7 @@ module.exports = {
'no-underscore-dangle': 0,
'no-param-reassign': 0,
'no-unused-vars': 0,
'indent': 0
},
indent: 0,
'comma-dangle': 0
}
}

7
.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"tabWidth": 4,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "avoid"
}

View file

@ -1,18 +0,0 @@
# build stage
FROM node:10-slim as build-stage
# Create app directory
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install -D
COPY . .
RUN npm run build
# production stage
FROM node:10-slim as production-stage
WORKDIR /usr/src/app
COPY --from=build-stage /usr/src/app ./
RUN rm -r node_modules && rm -r src && npm install
#serve
EXPOSE 3001
CMD ["node", "server/server.js"]

175
README.md
View file

@ -1,220 +1,71 @@
# VueTorrent
The sleekest looking WEBUI for qBittorrent made with Vuejs!
A modern looking WEBUI for qBittorrent made with Vuejs & express!
(support for more clients coming in the future)
> Vue, Node, Express, qBitorrent
PS : This is NOT an alternate WEBUI, this is a webapp that interacts with your existing qBitorrent client.
## Update
now using this 'https://github.com/TheFlow95/node-qbittorrent-api-v2' qbittorrent-api
& deleting & adding don't quite work yet :/
### With the updates it has become too unstable to use unfortunately, I'll update it when I find a fully working library or maybe write one myself if I find the time.
> Vue, qBitorrent, Vuetify
## Screenshots
<p align="center">
<a href="https://i.imgur.com/vPBcrK4.png"><img src="https://i.imgur.com/vPBcrK4.png" title="Desktop" alt="Desktop Screenshot" ></a>
</p>
<p align="center">
<a href="https://i.imgur.com/SUOEyy9.png"><img src="https://i.imgur.com/SUOEyy9.png" title="Mobile" alt="Mobile Screenshot" width="320" height="540"></a>
</p>
## Installation
Easiest way is with docker-compose:
```
vuetorrent:
image: wdaan/vuetorrent
container_name: vuetorrent
restart: always
ports:
- "4000:3000"
environment:
- VUE_APP_WEB_USER=vuetr
- VUE_APP_WEB_PASS=vuetr
- QBIT_USER=qbit
- QBIT_PASS=qbit
- QBIT_HOST=https://qbit.example.com
```
With Docker run
```
docker run --name=vuetorrent -d --env VUE_APP_WEB_USER=vuetr --env VUE_APP_WEB_PASS=vuetr --env QBIT_USER=admin --env QBIT_PASS=adminadmin --env QBIT_HOST=http://10.0.0.10:8080 --restart unless-stopped -p 3000:3000 wdaan/vuetorrent:latest
```
- Download & Unzip the latest release
- Point your Alternate WEBUI location to it
## Development
- clone the repo
- npm install
FRONTEND : Git clone & npm run serve!
SERVER : npm run build & npm run start!
- npm run serve
## Features
- viewing sessions stats ( down / upload speed, session uploaded / downloaded )
- adding / removing / pausing / resuming torrents
- sorting by every property shown!
- mobile friendly! (maybe not for thousands of torrents...)
* mobile friendly! (maybe not for thousands of torrents...)
- works on QBittorrent V4.2 and later
## Contributing
I'll gladly accept help/pull requests & advice! (this is my first project of this nature, pls be kind 😛 ).
## FAQ
- **Why build this??**
- Why not? Most WebUI's look very dated and now it's no longer necessary to search for a remote control app!
* Why not? Most WebUI's look very dated and now it's no longer necessary to search for a remote control app!
## Support
Reach out to me at one of the following places!
- <a href="https://m.me/WijnsDaan" target="_blank">`Facebook Messenger`</a>
* Open up an issue 😛
- Open up an issue 😛
[<img src="https://cdn.buymeacoffee.com/buttons/lato-blue.png" alt="drawing" width="180"/>](https://www.buymeacoffee.com/wdaan 'Buy me a coffee')
## Credits
Dashboard design heavily inspired by: 'https://github.com/iamshaunjp/vuetify-playlist'
- Dashboard design heavily inspired by: '[Net Ninja - Vuetify](https://github.com/iamshaunjp/vuetify-playlist)'.
Also check out The Net Ninja's Youtube Channel.
---
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
* This repo '[CzBiX qb-web ](https://github.com/CzBiX/qb-web)'

272
package-lock.json generated
View file

@ -982,8 +982,7 @@
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
},
"@types/node": {
"version": "12.12.15",
@ -1460,6 +1459,12 @@
"yallist": "^2.1.2"
}
},
"prettier": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -1781,7 +1786,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@ -1870,6 +1874,11 @@
"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
"dev": true
},
"array-differ": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz",
"integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg=="
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -2023,6 +2032,11 @@
}
}
},
"arrify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="
},
"asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@ -2406,8 +2420,7 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base": {
"version": "0.11.2",
@ -2590,7 +2603,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -2927,7 +2939,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@ -3237,7 +3248,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
@ -3245,8 +3255,7 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-string": {
"version": "1.5.3",
@ -3349,8 +3358,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.2",
@ -4564,7 +4572,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
@ -4658,8 +4665,7 @@
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"eslint": {
"version": "6.8.0",
@ -5280,6 +5286,14 @@
}
}
},
"eslint-plugin-prettier": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz",
"integrity": "sha512-+HG5jmu/dN3ZV3T6eCD7a4BlAySdN7mLIbJYo0z1cFQuI+r2DiTJEFeF68ots93PsnrMxbzIZ2S/ieX+mkrBeQ==",
"requires": {
"prettier-linter-helpers": "^1.0.0"
}
},
"eslint-plugin-vue": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-5.2.3.tgz",
@ -5686,6 +5700,11 @@
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"dev": true
},
"fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w=="
},
"fast-glob": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
@ -6816,8 +6835,7 @@
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-symbols": {
"version": "1.0.0",
@ -7717,8 +7735,7 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isobject": {
"version": "3.0.1",
@ -8042,8 +8059,7 @@
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
@ -8267,8 +8283,7 @@
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
"merge2": {
"version": "1.3.0",
@ -8381,7 +8396,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -8452,6 +8466,11 @@
"run-queue": "^1.0.3"
}
},
"mri": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.5.tgz",
"integrity": "sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg=="
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
@ -8488,6 +8507,25 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
"multimatch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz",
"integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==",
"requires": {
"@types/minimatch": "^3.0.3",
"array-differ": "^3.0.0",
"array-union": "^2.1.0",
"arrify": "^2.0.1",
"minimatch": "^3.0.4"
},
"dependencies": {
"array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="
}
}
},
"mute-stream": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
@ -8897,7 +8935,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
@ -9885,10 +9922,17 @@
"dev": true
},
"prettier": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
"dev": true
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz",
"integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg=="
},
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"requires": {
"fast-diff": "^1.1.2"
}
},
"pretty-bytes": {
"version": "4.0.2",
@ -9906,6 +9950,160 @@
"utila": "~0.4"
}
},
"pretty-quick": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-2.0.1.tgz",
"integrity": "sha512-y7bJt77XadjUr+P1uKqZxFWLddvj3SKY6EU4BuQtMxmmEFSMpbN132pUWdSG1g1mtUfO0noBvn7wBf0BVeomHg==",
"requires": {
"chalk": "^2.4.2",
"execa": "^2.1.0",
"find-up": "^4.1.0",
"ignore": "^5.1.4",
"mri": "^1.1.4",
"multimatch": "^4.0.0"
},
"dependencies": {
"cross-spawn": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz",
"integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"execa": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz",
"integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==",
"requires": {
"cross-spawn": "^7.0.0",
"get-stream": "^5.0.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^3.0.0",
"onetime": "^5.1.0",
"p-finally": "^2.0.0",
"signal-exit": "^3.0.2",
"strip-final-newline": "^2.0.0"
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"requires": {
"pump": "^3.0.0"
}
},
"ignore": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
"integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A=="
},
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"requires": {
"p-locate": "^4.1.0"
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"npm-run-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz",
"integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==",
"requires": {
"path-key": "^3.0.0"
}
},
"onetime": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
"requires": {
"mimic-fn": "^2.1.0"
}
},
"p-finally": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
"integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw=="
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"requires": {
"p-limit": "^2.2.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"private": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
@ -9980,7 +10178,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@ -10847,8 +11044,7 @@
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"simple-swizzle": {
"version": "0.2.2",
@ -11436,8 +11632,7 @@
"strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
},
"strip-indent": {
"version": "2.0.0",
@ -11536,7 +11731,6 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@ -12404,6 +12598,11 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vue-toastification": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-1.7.1.tgz",
"integrity": "sha512-T0byaYmOgyTWnROjNBZ7P2aFmHu8RS69JM7TI5kuUC4LWji05xDnuXAj/KmPibakydQTHlgrU2CeGHDp+40PKQ=="
},
"vuetify": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-1.5.24.tgz",
@ -13330,8 +13529,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write": {
"version": "0.2.1",

View file

@ -6,7 +6,7 @@
"serve": "vue-cli-service serve",
"build": "./node_modules/\\@vue/cli-service/bin/vue-cli-service.js build",
"lint": "vue-cli-service lint",
"start": "nodemon server/server.js"
"format": "pretty-quick"
},
"dependencies": {
"apexcharts": "^3.17.0",
@ -14,18 +14,23 @@
"cors": "^2.8.5",
"date-fns": "^1.30.1",
"dotenv": "^8.2.0",
"eslint-plugin-prettier": "^3.1.3",
"express": "^4.17.1",
"filepond": "^4.13.0",
"filepond-plugin-file-validate-size": "^2.2.0",
"filepond-plugin-file-validate-type": "^1.2.4",
"filepond-plugin-image-preview": "^4.6.1",
"lodash": "^4.17.15",
"multer": "^1.4.2",
"prettier": "^2.0.5",
"pretty-quick": "^2.0.1",
"qbittorrent-api-v2": "^1.2.0",
"register-service-worker": "^1.7.1",
"vue": "^2.6.11",
"vue-apexcharts": "^1.5.2",
"vue-filepond": "^5.1.3",
"vue-router": "^3.1.6",
"vue-toastification": "^1.7.1",
"vuetify": "^1.5.24",
"vuex": "^3.1.3"
},

View file

@ -1,19 +0,0 @@
module.exports = class Stat {
constructor(data) {
if (data != undefined && data != null) {
this.status = data.connection_status;
this.downloaded = this.formatBytes(data.dl_info_data, 1);
this.uploaded = this.formatBytes(data.up_info_data, 1);
this.dlspeed = this.formatBytes(data.dl_info_speed, 1);
this.upspeed = this.formatBytes(data.up_info_speed, 1);
}
}
formatBytes(a, b) {
if (0 == a) return '0 Bytes';
var c = 1024,
d = b || 2,
e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
f = Math.floor(Math.log(a) / Math.log(c));
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f];
}
};

View file

@ -1,52 +0,0 @@
module.exports = class Torrent {
constructor(data) {
this.name = data.name
this.size = this.formatBytes(data.size)
this.birth = new Date(data.added_on * 1000).toLocaleString()
this.dlspeed = this.formatBytes(data.dlspeed, 1)
this.dloaded = this.formatBytes(data.downloaded)
this.upspeed = this.formatBytes(data.upspeed, 1)
this.uploaded = this.formatBytes(data.uploaded)
this.eta = `${new Date(data.eta).getHours()
}h ${
new Date(data.eta).getMinutes()
}min`
this.num_leechs = data.num_leechs
this.num_seeds = data.num_seeds
this.path = data.path === undefined ? '/downloads' : data.path
this.state = this.formatState(data.state)
// hash is used to identify
this.hash = data.hash
// available seeds
this.available_seeds = data.num_complete
this.available_peers = data.num_incomplete
}
formatState(state) {
switch (state) {
case 'pausedDL':
return 'paused'
case 'downloading':
return 'busy'
case 'stalledDL':
return 'fail'
case 'pausedUP':
return 'done'
case 'missingFiles':
return 'fail'
case 'stalledUP':
return 'done'
default:
return undefined
}
}
formatBytes(a, b) {
if (a == 0) return '0 Bytes'
const c = 1024
const d = b || 2
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const f = Math.floor(Math.log(a) / Math.log(c))
return `${parseFloat((a / Math.pow(c, f)).toFixed(d))} ${e[f]}`
}
}

View file

@ -1,101 +0,0 @@
const api = require('qbittorrent-api-v2')
const dotenv = require('dotenv')
const Torrent = require('./models/torrent.class.js')
const Stat = require('./models/stat.class.js')
dotenv.config()
const connection = api.connect(process.env.QBIT_HOST, process.env.QBIT_USER, process.env.QBIT_PASS)
class Qbit {
async get_all(prop) {
try {
const res = await (await connection).torrents('all', null, prop.name, String(prop.reverse), '20', null, null)
const torrents = []
res.forEach((el) => {
const t = new Torrent(el)
torrents.push(t)
})
return torrents
} catch (err) {
return `something went wrong:${err}`
}
}
async get_session_stats() {
try {
const res = await (await connection).transferInfo()
return new Stat(res)
} catch (err) {
return `something went wrong:${err}`
}
}
async pause_torrents(torrents) {
let _torrents = ''
torrents.forEach((el) => {
_torrents += `${el}|`
})
try {
return await (await connection).pauseTorrents(_torrents)
} catch (err) {
return `something went wrong:${err}`
}
}
async pause_all() {
try {
return await (await connection).pauseTorrents('all')
} catch (err) {
return `something went wrong:${err}`
}
}
async resume_torrents(torrents) {
let _torrents = ''
torrents.forEach((el) => {
_torrents += `${el}|`
})
console.log(_torrents)
try {
return await (await connection).resumeTorrents(_torrents)
} catch (err) {
return `something went wrong:${err}`
}
}
async resume_all() {
try {
return await (await connection).resumeTorrents('all')
} catch (err) {
return `something went wrong:${err}`
}
}
async remove_torrents(torrents) {
let _torrents = ''
torrents.forEach((el) => {
_torrents += `${el}|`
})
console.log(_torrents)
try {
return await (await connection).deleteTorrents(_torrents, 'true')
} catch (err) {
return `something went wrong:${err}`
}
}
async add(torrent) {
return new Promise((resolve, reject) => {
connection.add(torrent.path, null, null, (err, res) => {
resolve(res)
reject(err)
})
})
}
}
const qbit = new Qbit()
module.exports = qbit

View file

@ -1,127 +0,0 @@
const express = require('express')
const multer = require('multer')
const fs = require('fs')
const path = require('path')
const dotenv = require('dotenv')
const qbit = require('./qbit')
dotenv.config()
const PORT = process.env.PORT || 3000
const newest_torrent = {
name: '',
path: '',
}
const upload = multer({
dest: './src/tmp/',
})
// init express
const app = express()
app.use(express.json())
app.use(express.static('dist'))
// requests
// login
app.post('/login', (req, res) => {
console.log(req.body)
if (req.body.username !== process.env.VUE_APP_WEB_USER) {
return res.send('No such user')
} if (
req.body.username === process.env.VUE_APP_WEB_USER
&& req.body.password !== process.env.VUE_APP_WEB_PASS
) {
return res.send('Wrong password!')
} if (
req.body.username === process.env.VUE_APP_WEB_USER
&& req.body.password === process.env.VUE_APP_WEB_PASS
) {
return res.send('SUCCES')
}
return res.send('Something went wrong')
})
// get all torrents
// AAAND sort torrents
app.post('/all', async (req, res) => {
const torrents = await qbit.get_all(req.body)
res.set('Content-Type', 'application/json')
res.send(torrents)
})
// get session stats
app.get('/session', async (req, res) => {
const stats = await qbit.get_session_stats()
res.set('Content-Type', 'application/json')
res.send(stats)
})
// pause selected torrents
app.post('/pause', async (req, res) => {
// console.log(req.body);
const msg = await qbit.pause_torrents(req.body)
return res.send(msg)
})
// pause all torrents
app.post('/pause_all', async (req, res) => {
const msg = await qbit.pause_all()
return res.send(msg)
})
// resume selected torrents
app.post('/resume', async (req, res) => {
// console.log(req.body);
const msg = await qbit.resume_torrents(req.body)
return res.send(msg)
})
// resume all torrents
app.post('/resume_all', async (req, res) => {
const msg = await qbit.resume_all()
return res.send(msg)
})
// remove selected torrents
app.post('/remove', async (req, res) => {
// console.log(req.body);
const msg = await qbit.remove_torrents(req.body)
return res.send(msg)
})
// upload files to server
app.post('/upload', upload.single('file'), (req, res) => {
newest_torrent.name = req.file.filename
newest_torrent.path = req.file.path
return res.send('succes')
})
// add a torrent
app.post('/add', async (req, res) => {
const msg = await qbit.add(newest_torrent)
fs.unlinkSync(newest_torrent.path)
return res.send(msg)
})
// delete last uploaded file
app.delete('/upload', upload.single('file'), (req, res) => {
fs.unlinkSync(newest_torrent.path)
return res.send('deleted file')
})
app.listen(PORT, () => console.log(`Server listening on port ${PORT}!`))
// clear the tmp directory on every boot
const directory = path.resolve(`${__dirname}/tmp`)
fs.readdir(directory, (err, files) => {
if (err) console.log(err)
for (const file of files) {
fs.unlink(path.join(directory, file), (err) => {
if (err) console.log(err)
})
}
})

View file

View file

@ -1 +0,0 @@
Here will torrents files be stored temporarily

View file

@ -1,17 +1,5 @@
<template>
<v-app class="grey lighten-4">
<!--snackbar popup for torrent added -->
<v-snackbar :value="snackbar" :timeout="4000" top color="success">
<span>{{succes_msg}}</span>
<v-btn color="white" flat @click="snackbarClose">Close</v-btn>
</v-snackbar>
<!--snackbar popup for errors -->
<v-snackbar :value="snackbar_error" :timeout="4000" top color="error">
<span>{{error_msg}}</span>
<v-btn color="white" flat @click="snackbar_errorClose">Close</v-btn>
</v-snackbar>
<div v-if="authenticated">
<Navbar />
<v-content class="mx-4 mb-4">
@ -19,22 +7,32 @@
</v-content>
</div>
<v-container v-else fill-height>
<v-layout row wrap align-center class="justify-center" justify-center>
<div style="margin: 0 auto">
<v-layout
row
wrap
align-center
class="justify-center"
justify-center
>
<div style="margin: 0 auto;">
<Login />
</div>
</v-layout>
</v-container>
<v-spacer></v-spacer>
<p class="grey--text caption text-sm-center text-md-center text-xs-center">Made by Daan Wijns</p>
<p
class="grey--text caption text-sm-center text-md-center text-xs-center"
>
Made by Daan Wijns
</p>
</v-app>
</template>
<script>
import { mapState } from 'vuex'
import Navbar from './components/Navbar'
import Login from './components/Login'
import Navbar from './components/Navbar.vue'
import Login from './components/Login.vue'
import qbit from './services/qbit'
export default {
components: { Navbar, Login },
@ -43,21 +41,7 @@ export default {
return {}
},
computed: {
...mapState([
'authenticated',
'snackbar_error',
'error_msg',
'snackbar',
'succes_msg',
]),
},
methods: {
snackbar_errorClose() {
this.$store.state.snackbar_error = false
},
snackbarClose() {
this.$store.state.snackbar = false
},
},
...mapState(['authenticated', 'rid', 'mainData', 'preferences'])
}
}
</script>

View file

@ -17,6 +17,7 @@
v-model="username"
:rules="inputRules"
@keyup.enter.native="Login"
autocomplete="current email"
></v-text-field>
<v-text-field
flat
@ -28,6 +29,7 @@
v-model="password"
:rules="inputRules"
@keyup.enter.native="Login"
autocomplete="current password"
></v-text-field>
<v-spacer></v-spacer>
<v-card-actions class="justify-center">
@ -36,7 +38,8 @@
flat
@click="Login"
class="blue_accent white--text mx-0 mt-3"
>Login</v-btn>
>Login</v-btn
>
</v-card-actions>
</v-form>
</v-card-text>
@ -53,7 +56,7 @@ export default {
return {
username: '',
password: '',
inputRules: [(v) => v.length >= 1 || 'At least 1 character'],
inputRules: [v => v.length >= 1 || 'At least 1 character']
}
},
methods: {
@ -61,12 +64,12 @@ export default {
this.$store.state.loading = true
this.$store.dispatch('LOGIN', {
username: this.username,
password: this.password,
password: this.password
})
},
}
},
computed: {
...mapState(['loading']),
},
...mapState(['loading'])
}
}
</script>

View file

@ -2,8 +2,16 @@
<nav>
<!--title-->
<v-toolbar flat app>
<v-toolbar-side-icon @click="drawer = !drawer" class="grey--text"></v-toolbar-side-icon>
<v-toolbar-title :class="['grey--text', {'subheading ml-0': $vuetify.breakpoint.smAndDown}]">
<v-toolbar-side-icon
@click="drawer = !drawer"
class="grey--text"
></v-toolbar-side-icon>
<v-toolbar-title
:class="[
'grey--text',
{ 'subheading ml-0': $vuetify.breakpoint.smAndDown }
]"
>
<span class="font-weight-light">Vue</span>
<span>Torrent</span>
</v-toolbar-title>
@ -28,57 +36,100 @@
<v-navigation-drawer app v-model="drawer" class="primary allow-spacer">
<!--current download speeds -->
<v-flex class="mt-3">
<div class="secondary_lighter--text text-uppercase caption ml-4">current speed</div>
<div
class="secondary_lighter--text text-uppercase caption ml-4"
>
current speed
</div>
<v-card color="secondary" flat class="mr-2 ml-2">
<v-layout row wrap :class="`pa-3 project nav_download`">
<v-layout v-if="stats" row wrap :class="`pa-3 project nav_download`">
<v-icon color="download">keyboard_arrow_down</v-icon>
<span class="download--text title">
{{stats.dlspeed.substring(0, stats.dlspeed.indexOf(' '))}}
<span
class="font-weight-light caption"
>{{stats.dlspeed.substring(stats.dlspeed.indexOf(' '))}}</span>
{{
stats.dlspeed.substring(
0,
stats.dlspeed.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.dlspeed.substring(
stats.dlspeed.indexOf(' ')
)
}}</span>
</span>
<v-icon class="pl-5" color="upload">keyboard_arrow_up</v-icon>
<v-icon class="pl-5" color="upload"
>keyboard_arrow_up</v-icon
>
<span class="upload--text title">
{{stats.upspeed.substring(0, stats.upspeed.indexOf(' '))}}
<span
class="font-weight-light caption"
>{{stats.upspeed.substring(stats.upspeed.indexOf(' '))}}</span>
{{
stats.upspeed.substring(
0,
stats.upspeed.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.upspeed.substring(
stats.upspeed.indexOf(' ')
)
}}</span>
</span>
</v-layout>
</v-card>
<!--speeds graph -->
<div class="mt-4">
<apexchart ref="chart" type="line" :options="chartOptions" :series="series"></apexchart>
<apexchart
ref="chart"
type="line"
:options="chartOptions"
:series="series"
></apexchart>
</div>
<div class="mt-4"></div>
<div class="secondary_lighter--text text-uppercase caption ml-4">session stats</div>
<v-card flat color="secondary" class="mr-2 ml-2">
<div
class="secondary_lighter--text text-uppercase caption ml-4"
>
session stats
</div>
<v-card v-if="stats" flat color="secondary" class="mr-2 ml-2">
<v-layout row wrap :class="`pa-3 project nav_download`">
<v-flex md6>
<div class="download--text">Total downloaded</div>
</v-flex>
<v-flex md5 class="mr-2">
<span class="download--text title pl-3">
{{stats.downloaded.substring(0, stats.downloaded.indexOf(' '))}}
<span
class="font-weight-light caption"
>{{stats.downloaded.substring(stats.downloaded.indexOf(' '))}}</span>
{{
stats.downloaded.substring(
0,
stats.downloaded.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.downloaded.substring(
stats.downloaded.indexOf(' ')
)
}}</span>
</span>
</v-flex>
</v-layout>
</v-card>
<v-card flat color="secondary" class="mr-2 ml-2 mt-1">
<v-card v-if="stats" flat color="secondary" class="mr-2 ml-2 mt-1">
<v-layout row wrap :class="`pa-3 project nav_upload`">
<v-flex md6>
<div class="upload--text">Total uploaded</div>
</v-flex>
<v-flex md5 class="mr-2">
<span class="upload--text title pl-3">
{{stats.uploaded.substring(0, stats.uploaded.indexOf(' '))}}
<span
class="font-weight-light caption"
>{{stats.uploaded.substring(stats.uploaded.indexOf(' '))}}</span>
{{
stats.uploaded.substring(
0,
stats.uploaded.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.uploaded.substring(
stats.uploaded.indexOf(' ')
)
}}</span>
</span>
</v-flex>
</v-layout>
@ -89,13 +140,27 @@
<Settings />
<v-spacer></v-spacer>
<v-tooltip top v-if="paused">
<v-btn small fab flat class="mr-4" @click="startInterval" slot="activator">
<v-btn
small
fab
flat
class="mr-4"
@click="startInterval"
slot="activator"
>
<v-icon color="green_accent">play_arrow</v-icon>
</v-btn>
<span>Resumes connection to client</span>
</v-tooltip>
<v-tooltip top v-else>
<v-btn small fab flat class="mr-4" @click="clearInterval" slot="activator">
<v-btn
small
fab
flat
class="mr-4"
@click="clearInterval"
slot="activator"
>
<v-icon color="green_accent">pause</v-icon>
</v-btn>
<span>Pauses connection to client</span>
@ -119,26 +184,26 @@ export default {
paused: false,
links: [
{ icon: 'dashboard', text: 'Dashboard', route: '/' },
{ icon: 'settings', text: 'Settings', route: '/settings' },
{ icon: 'settings', text: 'Settings', route: '/settings' }
],
chartOptions: {
chart: {
sparkline: {
enabled: true,
enabled: true
},
animations: {
enabled: false,
dynamicAnimation: {
speed: 2000,
},
},
speed: 2000
}
}
},
colors: ['#00b3fa', '#64CEAA'],
stroke: {
show: true,
curve: 'smooth',
lineCap: 'round',
width: 4,
width: 4
},
fill: {
type: 'gradient',
@ -148,58 +213,51 @@ export default {
shadeIntensity: 0.5,
opacityFrom: 0.6,
opacityTo: 0.5,
stops: [0, 50, 100],
},
},
stops: [0, 50, 100]
}
}
},
series: [
{
name: 'upload',
type: 'area',
data: this.$store.state.upload_data,
data: this.$store.state.upload_data
},
{
name: 'download',
type: 'area',
data: this.$store.state.download_data,
},
],
data: this.$store.state.download_data
}
]
}
},
methods: {
...mapMutations(['REFRESH_TORRENTS', 'CLEAR_INTERVALS']),
clearInterval() {
this.$store.commit('CLEAR_INTERVALS')
this.$data.paused = !this.$data.paused
},
startInterval() {
this.$store.dispatch('REFRESH_TORRENTS')
this.$store.dispatch('REFRESH_SESSION_STATS')
this.$data.paused = !this.$data.paused
},
pauseTorrents() {
this.$store.dispatch('PAUSE_TORRENTS')
},
resumeTorrents() {
this.$store.dispatch('RESUME_TORRENTS')
},
removeTorrents() {
this.$store.dispatch('REMOVE_TORRENTS')
},
refreshTorrents() {
this.$store.state.init_torrents = false
this.$store.dispatch('REFRESH_TORRENTS')
},
closeSnackbar() {
this.$store.state.snackbar = false
},
},
created() {
this.$store.dispatch('REFRESH_SESSION_STATS')
}
},
computed: {
...mapState(['stats', 'snackbar_error', 'error_msg', 'snackbar']),
},
...mapGetters(['getStats']),
stats() {
return this.getStats()
}
}
}
</script>
<style>

View file

@ -1,20 +1,24 @@
import Vue from 'vue'
import './plugins/vuetify'
import VueApexCharts from 'vue-apexcharts'
import Toast from 'vue-toastification'
import App from './App.vue'
import router from './router'
import store from './services/store'
import './registerServiceWorker'
import 'vue-toastification/dist/index.css'
Vue.use(VueApexCharts)
Vue.component('apexchart', VueApexCharts)
Vue.use(Toast)
Vue.config.productionTip = false
new Vue({
router,
store,
render: (h) => h(App),
render: h => h(App)
}).$mount('#app')

21
src/models/sessionStat.js Normal file
View file

@ -0,0 +1,21 @@
export default class Stat {
constructor(data) {
if (data != undefined && data != null) {
this.status = data.connection_status
this.downloaded = this.formatBytes(data.dl_info_data, 1)
this.uploaded = this.formatBytes(data.up_info_data, 1)
this.dlspeed = this.formatBytes(data.dl_info_speed, 1)
this.upspeed = this.formatBytes(data.up_info_speed, 1)
}
}
formatBytes(a, b) {
if (a == 0) return '0 Bytes'
const c = 1024
const d = b || 2
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const f = Math.floor(Math.log(a) / Math.log(c))
return `${parseFloat((a / Math.pow(c, f)).toFixed(d))} ${e[f]}`
}
}

52
src/models/torrent.js Normal file
View file

@ -0,0 +1,52 @@
export default class Torrent {
constructor(data) {
this.id = data.id
this.name = data.name
this.size = this.formatBytes(data.size)
this.birth = new Date(data.added_on * 1000).toLocaleString()
this.dlspeed = this.formatBytes(data.dlspeed, 1)
this.dloaded = this.formatBytes(data.downloaded)
this.upspeed = this.formatBytes(data.upspeed, 1)
this.uploaded = this.formatBytes(data.uploaded)
this.eta = `${new Date(data.eta).getHours()}h ${new Date(
data.eta
).getMinutes()}min`
this.num_leechs = data.num_leechs
this.num_seeds = data.num_seeds
this.path = data.path === undefined ? '/downloads' : data.path
this.state = this.formatState(data.state)
// hash is used to identify
this.hash = data.hash
// available seeds
this.available_seeds = data.num_complete
this.available_peers = data.num_incomplete
}
formatState(state) {
switch (state) {
case 'pausedDL':
return 'paused'
case 'downloading':
return 'busy'
case 'stalledDL':
return 'fail'
case 'pausedUP':
return 'done'
case 'missingFiles':
return 'fail'
case 'stalledUP':
return 'done'
default:
return undefined
}
}
formatBytes(a, b) {
if (a == 0) return '0 Bytes'
const c = 1024
const d = b || 2
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const f = Math.floor(Math.log(a) / Math.log(c))
return `${parseFloat((a / Math.pow(c, f)).toFixed(d))} ${e[f]}`
}
}

View file

@ -1,62 +1,294 @@
const axios = require('axios')
import axios from 'axios'
class Qbit {
constructor() {
this._axios = axios.create({
timeout: 1000,
this.axios = axios.create({
baseURL: 'api/v2'
})
this.axios.defaults.headers.post['Content-Type'] =
'application/x-www-form-urlencoded'
}
getAppVersion() {
return this.axios.get('/app/version')
}
getApiVersion() {
return this.axios.get('/app/webapiVersion')
}
async login(params) {
const payload = new URLSearchParams(params)
const { data } = await this.axios.post('/auth/login', payload, {
validateStatus(status) {
return status === 200 || status === 403
}
})
return data
}
getGlobalTransferInfo() {
return this.axios.get('/transfer/info')
}
getAppPreferences() {
return this.axios.get('/app/preferences')
}
getMainData(rid) {
const params = {
rid
}
return this.axios.get('/sync/maindata', {
params
})
}
async getAll(sort) {
const res = await this._axios.post('/all', sort)
return res.data
addTorrents(params, torrents) {
let data
if (torrents) {
const formData = new FormData()
for (const [key, value] of Object.entries(params)) {
// eslint-disable-next-line
formData.append(key, value)
}
async get_sessions_stats() {
const res = await this._axios.get('/session')
return res.data
for (const torrent of torrents) {
formData.append('torrents', torrent)
}
async pause_torrents(torrents) {
const res = await this._axios.post('/pause', torrents)
return res.data
data = formData
} else {
data = new URLSearchParams(params)
}
return this.axios.post('/torrents/add', data).then(Api.handleResponse)
}
async pause_all() {
const res = await this._axios.post('/pause_all')
return res.data
switchToOldUi() {
const params = {
alternative_webui_enabled: false
}
async resume_torrents(torrents) {
const res = await this._axios.post('/resume', torrents)
return res.data
return this.setPreferences(params)
}
async resume_all() {
const res = await this._axios.post('/resume_all')
return res.data
}
async add_torrent(torrent) {
const res = await this._axios.post('/add', torrent)
return res
}
async remove_torrents(torrents) {
const res = await this._axios.post('/remove', torrents)
return res.data
}
async login(credentials) {
let timeout = false
const res = await this._axios.post('/login', credentials).catch((error) => {
if (error.code === 'ECONNABORTED') timeout = true
else throw error
setPreferences(params) {
const data = new URLSearchParams({
json: JSON.stringify(params)
})
return timeout ? 'timeout' : res.data
return this.axios.post('/app/setPreferences', data)
}
setTorrentFilePriority(hash, idList, priority) {
const idListStr = idList.join('|')
const params = {
hash,
id: idListStr,
priority
}
const data = new URLSearchParams(params)
return this.axios
.post('/torrents/filePrio', data)
.then(Api.handleResponse)
}
getLogs(lastId) {
const params = {
last_known_id: lastId
}
return this.axios
.get('/log/main', {
params
})
.then(Api.handleResponse)
}
toggleSpeedLimitsMode() {
return this.axios.post('/transfer/toggleSpeedLimitsMode')
}
deleteTorrents(hashes, deleteFiles) {
return this.actionTorrents('delete', hashes, { deleteFiles })
}
pauseTorrents(hashes) {
return this.actionTorrents('pause', hashes)
}
resumeTorrents(hashes) {
return this.actionTorrents('resume', hashes)
}
reannounceTorrents(hashes) {
return this.actionTorrents('reannounce', hashes)
}
recheckTorrents(hashes) {
return this.actionTorrents('recheck', hashes)
}
setTorrentsCategory(hashes, category) {
return this.actionTorrents('setCategory', hashes, { category })
}
getTorrentTracker(hash) {
const params = {
hash
}
return this.axios
.get('/torrents/trackers', {
params
})
.then(Api.handleResponse)
}
getTorrentPeers(hash, rid) {
const params = {
hash,
rid
}
return this.axios
.get('/sync/torrentPeers', {
params
})
.then(Api.handleResponse)
}
editTracker(hash, origUrl, newUrl) {
return this.actionTorrents('editTracker', [hash], { origUrl, newUrl })
}
setTorrentLocation(hashes, location) {
return this.actionTorrents('setLocation', hashes, { location })
}
getTorrentProperties(hash) {
const params = {
hash
}
return this.axios
.get('/torrents/properties', {
params
})
.then(Api.handleResponse)
}
getTorrentPieceStates(hash) {
const params = {
hash
}
return this.axios
.get('/torrents/pieceStates', {
params
})
.then(Api.handleResponse)
}
getTorrentFiles(hash) {
const params = {
hash
}
return this.axios
.get('/torrents/files', {
params
})
.then(Api.handleResponse)
}
getRssItems() {
const params = {
withData: true
}
return this.axios
.get('/rss/items', {
params
})
.then(Api.handleResponse)
}
addRssFeed(url, path = '') {
const params = {
url,
path
}
const data = new URLSearchParams(params)
return this.axios.post('/rss/addFeed', data).then(Api.handleResponse)
}
removeRssFeed(path) {
const params = {
path
}
const data = new URLSearchParams(params)
return this.axios.post('/rss/removeItem', data).then(Api.handleResponse)
}
refreshRssFeed(path) {
const params = {
itemPath: path
}
const data = new URLSearchParams(params)
return this.axios
.post('/rss/refreshItem', data)
.then(Api.handleResponse)
}
moveRssFeed(path, newPath) {
const params = {
itemPath: path,
destPath: newPath
}
const data = new URLSearchParams(params)
return this.axios.post('/rss/moveItem', data).then(Api.handleResponse)
}
getRssRules() {
return this.axios.get('/rss/rules').then(Api.handleResponse)
}
setRssRule(name, def) {
const params = {
ruleName: name,
ruleDef: JSON.stringify(def)
}
const data = new URLSearchParams(params)
return this.axios.post('/rss/setRule', data).then(Api.handleResponse)
}
removeRssRule(name) {
const params = {
ruleName: name
}
const data = new URLSearchParams(params)
return this.axios.post('/rss/removeRule', data).then(Api.handleResponse)
}
actionTorrents(action, hashes, extra) {
const params = {
hashes: hashes.join('|'),
...extra
}
const data = new URLSearchParams(params)
return this.axios
.post(`/torrents/${action}`, data)
.then(Api.handleResponse)
}
}
const qbit = new Qbit()
export default qbit
export default new Qbit()

64
src/services/qbitOld.js Normal file
View file

@ -0,0 +1,64 @@
const axios = require('axios')
class Qbit {
constructor() {
this._axios = axios.create({
timeout: 1000
})
}
async getAll(sort) {
const res = await this._axios.post('/all', sort)
return res.data
}
async get_sessions_stats() {
const res = await this._axios.get('/session')
return res.data
}
async pause_torrents(torrents) {
const res = await this._axios.post('/pause', torrents)
return res.data
}
async pause_all() {
const res = await this._axios.post('/pause_all')
return res.data
}
async resume_torrents(torrents) {
const res = await this._axios.post('/resume', torrents)
return res.data
}
async resume_all() {
const res = await this._axios.post('/resume_all')
return res.data
}
async add_torrent(torrent) {
const res = await this._axios.post('/add', torrent)
return res
}
async remove_torrents(torrents) {
const res = await this._axios.post('/remove', torrents)
return res.data
}
async login(credentials) {
let timeout = false
const res = await this._axios
.post('/login', credentials)
.catch(error => {
if (error.code === 'ECONNABORTED') timeout = true
else throw error
})
return timeout ? 'timeout' : res.data
}
}
const qbit = new Qbit()
export default qbit

View file

@ -1,5 +1,8 @@
import Vue from 'vue'
import Vuex from 'vuex'
import { cloneDeep, merge, map, groupBy, sortBy } from 'lodash'
import Torrent from '../models/torrent'
import Stat from '../models/sessionStat'
import qbit from './qbit'
@ -8,69 +11,26 @@ Vue.use(Vuex)
export default new Vuex.Store({
state: {
intervals: [],
stats: {
status: 'init',
dlspeed: '6 Mbps',
upspeed: '1 Mbps',
downloaded: '6.95 Gb',
uploaded: '1014 Mb',
},
stats: null,
upload_data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
download_data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
torrents: [],
init_torrents: false,
selected_torrents: [],
network_error: false,
snackbar_error: false,
error_msg: '',
snackbar: false,
succes_msg: '',
authenticated: false,
loading: false,
sort_options: { sort: 'name', reverse: false },
rid: 0,
mainData: undefined,
preferences: null,
pasteUrl: null
},
getters: {
CONTAINS_TORRENT: (state) => (hash) => state.selected_torrents.includes(hash),
CONTAINS_TORRENT: state => hash =>
state.selected_torrents.includes(hash)
},
mutations: {
REFRESH_TORRENTS: async (state) => {
const torrents = await qbit.getAll(state.sort_options).catch(() => {
state.network_error = true
state.error_msg = 'Lost connection with server, reload page'
state.snackbar_error = true
})
if (torrents) {
state.torrents = torrents.map((a) => ({ ...a }))
state.init_torrents = true
}
},
REFRESH_SESSION_STATS: async (state) => {
const _stats = await qbit.get_sessions_stats()
// push in array for graph
state.download_data.splice(0, 1)
if (_stats.dlspeed.indexOf('KB' > -1)) {
state.download_data.push(
_stats.dlspeed.substring(0, _stats.dlspeed.indexOf(' ')) / 1000,
)
} else {
state.download_data.push(
_stats.dlspeed(0, _stats.dlspeed.indexOf(' ')),
)
}
state.upload_data.splice(0, 1)
if (_stats.upspeed.indexOf('KB' > -1)) {
state.upload_data.push(
_stats.upspeed.substring(0, _stats.upspeed.indexOf(' ')) / 1000,
)
} else {
state.upload_data.push(
_stats.upspeed.substring(0, _stats.upspeed.indexOf(' ')),
)
}
state.stats = _stats
},
CLEAR_INTERVALS: (state) => {
if (state.intervals.length > 1) { state.intervals.forEach((el) => clearInterval(el)) }
REMOVE_INTERVALS: state => {
state.intervals.forEach(el => clearInterval(el))
},
ADD_SELECTED: (state, payload) => {
state.selected_torrents.push(payload)
@ -78,13 +38,13 @@ export default new Vuex.Store({
REMOVE_SELECTED: (state, payload) => {
state.selected_torrents.splice(
state.selected_torrents.indexOf(payload),
1,
1
)
},
RESET_SELECTED: (state) => {
RESET_SELECTED: state => {
state.selected_torrents = []
},
PAUSE_TORRENTS: async (state) => {
PAUSE_TORRENTS: async state => {
let res
if (state.selected_torrents.length === 0) {
res = await qbit.pause_all()
@ -92,7 +52,7 @@ export default new Vuex.Store({
res = await qbit.pause_torrents(state.selected_torrents)
}
},
RESUME_TORRENTS: async (state) => {
RESUME_TORRENTS: async state => {
let res
if (state.selected_torrents.length === 0) {
res = await qbit.resume_all()
@ -116,93 +76,45 @@ export default new Vuex.Store({
}, 4000)
}
},
REMOVE_TORRENTS: async (state) => {
REMOVE_TORRENTS: async state => {
if (state.selected_torrents.length !== 0) {
const res = await qbit.remove_torrents(state.selected_torrents)
}
},
LOGIN: async (state, payload) => {
const res = await qbit.login(payload)
if (res == 'timeout') {
if (res === 'Ok.') {
state.loading = false
state.snackbar_error = true
state.error_msg = 'Express server timed out!'
setTimeout(() => {
state.snackbar_error = false
}, 4000)
} else {
switch (res) {
case 'No such user':
state.snackbar_error = true
state.error_msg = 'No such user!'
setTimeout(() => {
state.snackbar_error = false
}, 4000)
break
case 'Wrong password!':
state.snackbar_error = true
state.error_msg = 'Wrong password!'
setTimeout(() => {
state.snackbar_error = false
}, 4000)
break
case 'SUCCES':
state.snackbar = true
state.succes_msg = 'Succesfully logged in!'
Vue.$toast.success('Successfully logged in!')
state.authenticated = true
setTimeout(() => {
state.snackbar = false
}, 4000)
break
default:
state.snackbar_error = true
state.error_msg = 'Something went wrong'
setTimeout(() => {
state.snackbar_error = false
}, 4000)
break
}
state.loading = false
}
},
updateMainData: async state => {
const rid = state.rid ? state.rid : undefined
const { data } = await qbit.getMainData(rid)
// torrents
state.torrents = []
for (const [key, value] of Object.entries(data.torrents)) {
state.torrents.push(new Torrent({ id: key, ...value }))
}
// download speed
state.stats = new Stat(data.server_state)
}
},
actions: {
REFRESH_TORRENTS: (context) => {
context.state.intervals[1] = setInterval(async () => {
context.commit('REFRESH_TORRENTS')
if (context.state.network_error) {
context.commit('CLEAR_INTERVALS')
}
INIT_INTERVALS: async (context) => {
context.state.intervals[0] = setInterval(() => {
context.commit('updateMainData')
}, 2000)
},
REFRESH_SESSION_STATS: (context) => {
context.state.intervals[0] = setInterval(async () => {
context.commit('REFRESH_SESSION_STATS')
}, 1000)
},
ADD_SELECTED: (context, payload) => {
context.commit('ADD_SELECTED', payload)
},
REMOVE_SELECTED: (context, payload) => {
context.commit('REMOVE_SELECTED', payload)
},
RESET_SELECTED: (context) => {
context.commit('RESET_SELECTED')
},
PAUSE_TORRENTS: (context) => {
context.commit('PAUSE_TORRENTS')
},
RESUME_TORRENTS: (context) => {
context.commit('RESUME_TORRENTS')
},
ADD_TORRENT: (context, payload) => {
context.commit('ADD_TORRENT', payload)
},
REMOVE_TORRENTS: (context) => {
context.commit('REMOVE_TORRENTS')
},
LOGIN: (context, payload) => {
LOGIN: async (context, payload) => {
context.commit('LOGIN', payload)
context.commit('updateMainData')
}
},
},
getters: {
getStats: state => () => state.stats
}
})

View file

@ -16,12 +16,7 @@
@keyup.enter.native="sortBy"
></v-text-field>
</v-flex>
<v-container v-if="!init_torrents" fill-height>
<div style="margin: 150px auto;">
<v-progress-circular :size="100" indeterminate color="green_accent"></v-progress-circular>
</div>
</v-container>
<div v-if="torrents.length === 0 && init_torrents" class="mt-5 text-xs-center">
<div v-if="torrents.length === 0" class="mt-5 text-xs-center">
<p class="grey--text">No active Torrents!</p>
</div>
<div v-else>
@ -31,7 +26,9 @@
v-for="torrent in torrents"
:key="torrent.name"
class="pointer"
:class=" containsTorrent(torrent.hash) ? 'grey lighten-3' : ''"
:class="
containsTorrent(torrent.hash) ? 'grey lighten-3' : ''
"
@click.native="selectTorrent(torrent.hash)"
>
<v-layout row wrap :class="`pa-3 project ${torrent.state}`">
@ -42,37 +39,65 @@
<v-flex xs6 sm1 md1 class="mr-2">
<div class="caption grey--text">Size</div>
<div>
{{ torrent.size.substring(0, torrent.size.indexOf(' '))}}
<span
class="caption grey--text"
>{{ torrent.size.substring(torrent.size.indexOf(' ')) }}</span>
{{
torrent.size.substring(
0,
torrent.size.indexOf(' ')
)
}}
<span class="caption grey--text">{{
torrent.size.substring(
torrent.size.indexOf(' ')
)
}}</span>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<div class="caption grey--text">Done</div>
<div>
{{ torrent.dloaded.substring(0, torrent.dloaded.indexOf(' ')) }}
<span
class="caption grey--text"
>{{ torrent.dloaded.substring(torrent.dloaded.indexOf(' ')) }}</span>
{{
torrent.dloaded.substring(
0,
torrent.dloaded.indexOf(' ')
)
}}
<span class="caption grey--text">{{
torrent.dloaded.substring(
torrent.dloaded.indexOf(' ')
)
}}</span>
</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
<div class="caption grey--text">Download</div>
<div>
{{ torrent.dlspeed.substring(0, torrent.dlspeed.indexOf(' ')) }}
<span
class="caption grey--text"
>{{ torrent.dlspeed.substring(torrent.dlspeed.indexOf(' ')) }}</span>
{{
torrent.dlspeed.substring(
0,
torrent.dlspeed.indexOf(' ')
)
}}
<span class="caption grey--text">{{
torrent.dlspeed.substring(
torrent.dlspeed.indexOf(' ')
)
}}</span>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<div class="caption grey--text">Upload</div>
<div>
{{ torrent.upspeed.substring(0, torrent.upspeed.indexOf(' ')) }}
<span
class="caption grey--text"
>{{ torrent.upspeed.substring(torrent.upspeed.indexOf(' ')) }}</span>
{{
torrent.upspeed.substring(
0,
torrent.upspeed.indexOf(' ')
)
}}
<span class="caption grey--text">{{
torrent.upspeed.substring(
torrent.upspeed.indexOf(' ')
)
}}</span>
</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
@ -83,16 +108,18 @@
<div class="caption grey--text">Peers</div>
<div>
{{ torrent.num_leechs }}
<span
class="grey--text caption"
>/{{torrent.available_peers}}</span>
<span class="grey--text caption"
>/{{ torrent.available_peers }}</span
>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<div class="caption grey--text">Seeds</div>
<div>
{{ torrent.num_seeds }}
<span class="grey--text caption">/{{torrent.available_seeds}}</span>
<span class="grey--text caption"
>/{{ torrent.available_seeds }}</span
>
</div>
</v-flex>
<v-flex xs4 sm12 md1>
@ -100,7 +127,8 @@
<v-chip
small
:class="`${torrent.state} white--text my-2 caption`"
>{{ torrent.state }}</v-chip>
>{{ torrent.state }}</v-chip
>
</div>
</v-flex>
<v-flex xs12 sm12 md12>
@ -125,17 +153,17 @@ import { mapState, mapMutations, mapGetters } from 'vuex'
export default {
data() {
return {
sort_input: '',
sort_input: ''
}
},
computed: {
...mapState(['torrents', 'init_torrents']),
...mapState(['mainData', 'torrents'])
},
methods: {
...mapMutations(['SORT_TORRENTS']),
sortBy() {
let name; let
reverse
let name
let reverse
// search if order was presented
const index = this.sort_input.indexOf(' ')
if (index > -1) {
@ -221,23 +249,16 @@ export default {
this.$store.state.sort_options = { name, reverse }
},
selectTorrent(hash) {
if (this.containsTorrent(hash)) {
this.$store.dispatch('REMOVE_SELECTED', hash)
} else {
this.$store.dispatch('ADD_SELECTED', hash)
}
},
containsTorrent(hash) {
return this.$store.getters.CONTAINS_TORRENT(hash)
},
resetSelected() {
this.$store.dispatch('RESET_SELECTED')
},
selectTorrent(hash) {},
containsTorrent(hash) {},
resetSelected() {}
},
created() {
this.$store.dispatch('REFRESH_TORRENTS')
this.$store.dispatch('INIT_INTERVALS')
},
beforeDestroy() {
this.$store.commit('REMOVE_INTERVALS')
}
}
</script>

View file

@ -1,5 +1,13 @@
module.exports = {
outputDir: 'dist/public',
publicPath: './',
devServer: {
proxy: 'http://localhost:3001/',
},
port: 8000,
proxy: {
'/api': {
target: 'http://127.0.0.1:8080'
}
}
}
}