This commit is contained in:
Daan Wijns 2020-10-18 12:12:16 +02:00 committed by GitHub
parent 403c7d5aea
commit af923e6648
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 915 additions and 406 deletions

View file

@ -1,6 +1,6 @@
# VueTorrent
The sleekest looking WEBUI for qBittorrent made with Vuejs!
The sleekest looking WEBUI for qBittorrent made with Vuejs! Forked from [WDaan/VueTorrent](https://github.com/WDaan/VueTorrent)
> Vue, qBitorrent, Vuetify
@ -8,22 +8,25 @@ The sleekest looking WEBUI for qBittorrent made with Vuejs!
<p align="center">
<a href="https://imgur.com/kEqGvem.jng"><img src="https://imgur.com/kEqGvem.jpg" title="Desktop" alt="Desktop Screenshot" ></a>
<a href="https://imgur.com/hpjuVYb.png"><img src="https://imgur.com/hpjuVYb.png" title="Desktop" alt="Desktop Screenshot" ></a>
</p>
| | | |
| :------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------: |
| <img width="1604" alt="screen shot 2017-08-07 at 12 18 15 pm" src="https://imgur.com/eN8qAM9.png"> | <img width="1604" alt="screen shot 2017-08-07 at 12 18 15 pm" src="https://imgur.com/T1A2Bng.png"> | <img width="1604" alt="screen shot 2017-08-07 at 12 18 15 pm" src="https://imgur.com/V83NMPg.png"> |
| <img width="1604" alt="screen shot 2017-08-07 at 12 18 15 pm" src="https://imgur.com/Zcm98H3.png"> | <img width="1604" alt="screen shot 2017-08-07 at 12 18 15 pm" src="https://imgur.com/OujrH0f.png"> | <img width="1604" alt="screen shot 2017-08-07 at 12 18 15 pm" src="https://imgur.com/OkukwYY.png"> |
| <img width="1604" alt="screen shot 2017-015 pm" src="https://imgur.com/QYpNCXs.png"> | <img width="1604" alt="screen shot 2017-08-07 at 12 18 15 pm" src="https://imgur.com/6j5wxhl.png"> | <img width="1604" alt="screen shot 2017-08-07 at 12 18 15 pm" src="https://imgur.com/jnzDKjW.png"> |
<p align="center">
<a href="https://imgur.com/kEqGvem.jpg"><img src="https://imgur.com/c5i63Yz.jpg" title="Desktop" width="300" alt="Mobile" >
<a href="https://imgur.com/U3mes8r.png"><img src="https://imgur.com/U3mes8r.png" title="Desktop" width="300" alt="Mobile" >
</a>
</p>
## Installation
- ### manual
- Visit the releases page!
- Download the latest release.zip
@ -32,6 +35,12 @@ The sleekest looking WEBUI for qBittorrent made with Vuejs!
- Point your Alternate WEBUI location to the 'vuetorrent' folder
- ### 'automatic'
- head over to the 'latest_release' branch
- clone it
- pull every once in a while
## Development
- clone the repo
@ -44,11 +53,15 @@ The sleekest looking WEBUI for qBittorrent made with Vuejs!
- viewing sessions status ( down / upload speed, session uploaded / downloaded )
- adding / removing / pausing / resuming torrents
- adding / removing / pausing / resuming / renaming torrents
- selectively downloading torrents
- choosing / renaming torrent files
- filtering powered by Fuse.js!
- mobile friendly! (maybe not for thousands of torrents...)
- mobile friendly! (can be installed as a PWA)
- torrent info / trackers / peers / content
@ -62,20 +75,10 @@ The sleekest looking WEBUI for qBittorrent made with Vuejs!
## 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!
I'll gladly accept help/pull requests & advice!
## 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 😛
[<img src="https://cdn.buymeacoffee.com/buttons/lato-blue.png" alt="drawing" width="180"/>](https://www.buymeacoffee.com/wdaan 'Buy me a coffee')
@ -84,4 +87,6 @@ Reach out to me at one of the following places!
- Dashboard design heavily inspired by: '[Net Ninja - Vuetify](https://github.com/iamshaunjp/vuetify-playlist)'.
* This repo '[CzBiX qb-web ](https://github.com/CzBiX/qb-web)'
- This repo '[CzBiX qb-web ](https://github.com/CzBiX/qb-web)'
- Muertocaloh's [fork](https://github.com/muertocaloh/VueTorrent)

109
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "vuetorrent",
"version": "0.3.7",
"version": "0.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -845,9 +845,9 @@
}
},
"@babel/polyfill": {
"version": "7.11.5",
"resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.11.5.tgz",
"integrity": "sha512-FunXnE0Sgpd61pKSj2OSOs1D44rKTD3pGOfGilZ6LGrrIH0QEtJlTjqOqdF8Bs98JmjfGhni2BBkTfv9KcKJ9g==",
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz",
"integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==",
"requires": {
"core-js": "^2.6.5",
"regenerator-runtime": "^0.13.4"
@ -1414,20 +1414,20 @@
}
},
"@vue/cli-plugin-pwa": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-pwa/-/cli-plugin-pwa-4.5.6.tgz",
"integrity": "sha512-jMTBo9oR3mkwcqFbtgbKgfuYLZoivDoH5KEwqOkzqamuapOUazAbmlrad0XSF92MKcF8XxWrdZjsEsD/TshDPw==",
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-pwa/-/cli-plugin-pwa-4.5.7.tgz",
"integrity": "sha512-mOaEgoLCT2yE8Pdvlz8LhXKqIs3w4xJjDr2dLrOrxh0+OhSpOHJdJ3yHswlgvkxgg0/FGS6t8haj0DfInQ+fYg==",
"dev": true,
"requires": {
"@vue/cli-shared-utils": "^4.5.6",
"@vue/cli-shared-utils": "^4.5.7",
"webpack": "^4.0.0",
"workbox-webpack-plugin": "^4.3.1"
},
"dependencies": {
"@vue/cli-shared-utils": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.6.tgz",
"integrity": "sha512-p6ePDlEa7Xc0GEt99KDOCwPZtR7UnoEaZLMfwPYU5LAWkdCmtAw8HPAY/WWcjtoiaAkY4k9tz7ZehQasZ9mJxg==",
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.7.tgz",
"integrity": "sha512-oicFfx9PvgupxN/LW0s2ktdn1U6bBu8J4lPcW2xj6TtTWUkkxwzis4Tm+XOvgvZnu44+d7216y0Y4TX90q645w==",
"dev": true,
"requires": {
"@hapi/joi": "^15.0.1",
@ -2123,9 +2123,9 @@
}
},
"apexcharts": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.21.0.tgz",
"integrity": "sha512-yeulUZCTG57swbJ5oIJIjgfRdIsvmC/2WJanrZxNGhjtZf2B9NaT95pEtbrml1BILJKtMn4VbpXVZp+8Tzmydg==",
"version": "3.22.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.22.0.tgz",
"integrity": "sha512-DDh2eXnAEA8GoKU/hdicOaS2jzGehXwv8Bj1djYYudkeQzEdglFoWsVyIxff+Ds7+aUtVAJzd/9ythZuyyIbXQ==",
"requires": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
@ -2584,6 +2584,16 @@
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@ -3128,9 +3138,9 @@
"dev": true
},
"chokidar": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
"integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz",
"integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
@ -3140,7 +3150,7 @@
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.4.0"
"readdirp": "~3.5.0"
},
"dependencies": {
"braces": {
@ -3167,6 +3177,15 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
}
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -4084,9 +4103,9 @@
}
},
"dayjs": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.1.tgz",
"integrity": "sha512-01NCTBg8cuMJG1OQc6PR7T66+AFYiPwgDvdJmvJBn29NGzIG+DIFxPLNjHzwz3cpFIvG+NcwIjP9hSaPVoOaDg=="
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.3.tgz",
"integrity": "sha512-V+1SyIvkS+HmNbN1G7A9+ERbFTV9KTXu6Oor98v2xHmzzpp52OIJhQuJSTywWuBY5pyAEmlwbCi1Me87n/SLOw=="
},
"de-indent": {
"version": "1.0.2",
@ -5503,6 +5522,13 @@
"schema-utils": "^2.5.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"filesize": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
@ -7867,9 +7893,9 @@
}
},
"node-forge": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"dev": true
},
"node-gyp": {
@ -9655,6 +9681,7 @@
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
"dev": true,
"optional": true,
"requires": {
"picomatch": "^2.2.1"
}
@ -10092,9 +10119,9 @@
"dev": true
},
"sass": {
"version": "1.26.11",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz",
"integrity": "sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==",
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.27.0.tgz",
"integrity": "sha512-0gcrER56OkzotK/GGwgg4fPrKuiFlPNitO7eUJ18Bs+/NBlofJfMxmxqpqJxjae9vu0Wq8TZzrSyxZal00WDig==",
"dev": true,
"requires": {
"chokidar": ">=2.0.0 <4.0.0"
@ -10303,12 +10330,12 @@
"dev": true
},
"selfsigned": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
"integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
"integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
"dev": true,
"requires": {
"node-forge": "0.9.0"
"node-forge": "^0.10.0"
}
},
"semver": {
@ -11911,9 +11938,9 @@
"dev": true
},
"uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg=="
},
"v8-compile-cache": {
"version": "2.1.1",
@ -12063,9 +12090,9 @@
"integrity": "sha512-xo0CEVdkjSjhJoDdLSvoZoQrw/H2BlzB5jrCBKGZNXN2zdZgMuZ9BKrxXDjNP2AxlcCoKc8OahI3F3r3JGLv2Q=="
},
"vue-router": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.5.tgz",
"integrity": "sha512-ioRY5QyDpXM9TDjOX6hX79gtaMXSVDDzSlbIlyAmbHNteIL81WIVB2e+jbzV23vzxtoV0krdS2XHm+GxFg+Nxg=="
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.7.tgz",
"integrity": "sha512-CbHXue5BLrDivOk5O4eZ0WT4Yj8XwdXa4kCnsEIOzYUPF/07ZukayA2jGxDCJxLc9SgVQX9QX0OuGOwGlVB4Qg=="
},
"vue-style-loader": {
"version": "4.1.2",
@ -12117,9 +12144,9 @@
}
},
"vuetify": {
"version": "2.3.12",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.3.12.tgz",
"integrity": "sha512-FSt1pzpf0/Lh0xuctAPB7RiLbUl7bzVc7ejbXLLhfmgm7zD7yabuhVYuyVda/SzokjZMGS3j1lNu2lLfdrz0oQ=="
"version": "2.3.14",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.3.14.tgz",
"integrity": "sha512-1Ys1MreJQOL/Ddp3YotBi1SlC2+1A0/RVkDXX3Azspt8incPdAnNB0JyChHiJ/TM+L+KSA7T4EXF9YDrCWENmg=="
},
"vuex": {
"version": "3.5.1",
@ -12283,6 +12310,7 @@
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
@ -12591,6 +12619,7 @@
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},

View file

@ -1,6 +1,6 @@
{
"name": "vuetorrent",
"version": "0.3.7",
"version": "0.4.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@ -8,30 +8,30 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@babel/polyfill": "^7.11.5",
"apexcharts": "^3.21.0",
"@babel/polyfill": "^7.12.1",
"apexcharts": "^3.22.0",
"axios": "^0.19.2",
"core-js": "^3.6.4",
"dayjs": "^1.9.1",
"dayjs": "^1.9.3",
"fuse.js": "^6.4.1",
"lodash": "^4.17.20",
"register-service-worker": "^1.7.1",
"uuid": "^8.3.0",
"uuid": "^8.3.1",
"vue": "^2.6.12",
"vue-apexcharts": "^1.6.0",
"vue-context": "^5.2.0",
"vue-observe-visibility": "^0.4.6",
"vue-router": "^3.4.5",
"vue-router": "^3.4.7",
"vue-toastification": "^1.7.8",
"vue2-perfect-scrollbar": "^1.5.0",
"vuetify": "^2.3.12",
"vuetify": "^2.3.14",
"vuex": "^3.5.1",
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-eslint": "~4.3.0",
"@vue/cli-plugin-pwa": "^4.5.6",
"@vue/cli-plugin-pwa": "^4.5.7",
"@vue/cli-service": "~4.3.0",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
@ -40,7 +40,7 @@
"eslint-plugin-vue": "^6.2.2",
"fibers": "^5.0.0",
"node-sass": "^4.14.1",
"sass": "^1.26.11",
"sass": "^1.27.0",
"sass-loader": "^8.0.2",
"vue-cli-plugin-vuetify": "^2.0.7",
"vue-template-compiler": "^2.6.12"

View file

@ -4,7 +4,7 @@ build_and_copy(){
npm run build
VERSION=$(jq -r .version package.json)
mkdir ../vuetorrent-release
git clone git@github.com:WDaan/VueTorrent.git ../vuetorrent-release
git clone https://github.com/wdaan/VueTorrent.git ../vuetorrent-release
cd ../vuetorrent-release
git checkout latest-release
git pull

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -28,7 +28,9 @@ export default {
methods: {
async checkAuthenticated() {
const res = await qbit.login()
this.$store.commit('LOGIN', res === 'Ok.')
const authenticated = res === 'Ok.'
this.$store.commit('LOGIN', authenticated)
if (!authenticated && this.$router.currentRoute.name !== 'login') this.$router.push('login')
}
},
computed: {

View file

@ -1,4 +1,5 @@
@import '~vuetify/src/styles/styles.sass';
@import '../styles/variables.scss';
@mixin dialog-title {
@include theme(v-card) using($material) {
@ -18,58 +19,58 @@
}
.sideborder.done {
border-left: 4px solid #3cd1c2;
border-left: 4px solid #{$torrent-done};
}
.sideborder.busy {
border-left: 4px solid #ffaa2c;
.sideborder.downloading {
border-left: 4px solid #{$torrent-downloading};
}
.sideborder.fail {
border-left: 4px solid #f83e70;
border-left: 4px solid #{$torrent-fail};
}
.sideborder.paused {
border-left: 4px solid #cfd8dc;
border-left: 4px solid #{$torrent-paused};
}
.sideborder.queued {
border-left: 4px solid #2e5eaa;
border-left: 4px solid #{$torrent-queued};
}
.sideborder.seeding {
border-left: 4px solid #26a69a;
border-left: 4px solid #{$torrent-seeding};
}
.sideborder.checking {
border-left: 4px solid #ff7043;
border-left: 4px solid #{$torrent-checking};
}
.sideborder.stalled {
border-left: 4px solid #81c784;
border-left: 4px solid #{$torrent-stalled};
}
.sideborder.metadata {
border-left: 4px solid #7e57c2;
border-left: 4px solid #{$torrent-metadata};
}
.v-chip.done {
background: #3cd1c2 !important;
background: #{$torrent-done} !important;
}
.v-chip.busy {
background: #ffaa2c !important;
.v-chip.downloading {
background: #{$torrent-downloading} !important;
}
.v-chip.fail {
background: #f83e70 !important;
background: #{$torrent-fail}!important;
}
.v-chip.paused {
background: #cfd8dc !important;
background: #{$torrent-paused} !important;
}
.v-chip.queued {
background: #2e5eaa !important;
background: #{$torrent-queued} !important;
}
.v-chip.seeding {
background: #26a69a !important;
background: #{$torrent-seeding} !important;
}
.v-chip.checking {
background: #ff7043 !important;
background: #{$torrent-checking} !important;
}
.v-chip.stalled {
background: #81c784 !important;
background: #{$torrent-stalled} !important;
}
.v-chip.metadata {
background: #7e57c2 !important;
background: #{$torrent-metadata} !important;
}
.noselect {
-webkit-touch-callout: none; /* iOS Safari */

View file

@ -11,6 +11,7 @@
<v-row no-gutters>
<v-col ref="fileZone">
<v-file-input
v-if="!url"
v-model="files"
color="deep-purple accent-4"
counter
@ -44,6 +45,7 @@
</template>
</v-file-input>
<v-text-field
v-if="files.length == 0"
label="URL"
prepend-icon="mdi-link"
:rows="
@ -62,19 +64,29 @@
clearable
label="Category"
prepend-icon="tag"
@input="categoryChanged"
></v-combobox>
<v-text-field
:disabled="autoTMM"
v-model="directory"
:placeholder="savepath"
label="Download Directory"
prepend-icon="folder"
></v-text-field>
<v-row no-gutters>
<v-flex xs12 sm6>
<v-checkbox
v-model="autoTMM"
label="Automatic Torrent Management"
></v-checkbox>
</v-flex>
<v-flex xs12 sm6>
<v-checkbox
v-model="skip_checking"
label="Skip hash check"
></v-checkbox>
</v-flex>
</v-row>
</v-container>
</v-form>
</v-card-text>
@ -108,6 +120,7 @@ export default {
files: [],
category: null,
directory: '',
autoTMM: true,
skip_checking: false,
inputRules: [
v =>
@ -125,14 +138,11 @@ export default {
submit() {
if (this.files.length || this.url) {
let torrents = []
let params = { urls: null, autoTMM: true }
let params = { urls: null, autoTMM: this.autoTMM }
if (this.files.length) torrents.push(...this.files)
if (this.url) params.urls = this.url
if (this.category) params.category = this.category
if (this.directory) {
params.savepath = this.directory
params.autoTMM = false
}
if (!this.autoTMM) params.savepath = this.directory
if (this.skip_checking) params.skip_checking = this.skip_checking
qbit.addTorrents(params, torrents)
@ -142,6 +152,9 @@ export default {
this.$store.commit('DELETE_MODAL', this.guid)
}
},
categoryChanged() {
if (this.autoTMM) this.directory = this.savepath
},
resetForm() {
this.url = null
this.files = []
@ -162,8 +175,8 @@ export default {
let savePath = this.getSettings().save_path
if (this.category) {
savePath += this.category
let category_path = this.getCategories()[this.category].savePath
if (category_path) savePath = category_path
let category = this.getCategories()[this.category]
if (category && category.savePath) savePath = category.savePath
}
return savePath
},
@ -174,6 +187,7 @@ export default {
created() {
this.$store.commit('FETCH_SETTINGS')
this.$store.commit('FETCH_CATEGORIES')
this.directory = this.savepath
}
}
</script>

View file

@ -0,0 +1,88 @@
<template>
<v-dialog
v-model="dialog"
scrollable
:width="dialogWidth"
:fullscreen="phoneLayout"
>
<v-card style="overflow: hidden !important">
<v-container :style="{ height: phoneLayout ? '100vh' : '' }">
<v-card-title class="pb-0 justify-center">
<h2>Change Torrent Location</h2>
</v-card-title>
<v-card-text>
<div>
<v-container>
<v-row>
<v-col>
<v-text-field
label="Torrent Name"
prepend-icon="insert_drive_file"
readonly
v-model="torrent.name"
/>
<v-text-field
label="Directory"
prepend-icon="folder"
v-model="newPath"
v-on:keydown.enter="setLocation"
/>
</v-col>
</v-row>
</v-container>
</div>
</v-card-text>
<div>
<v-card-actions class="justify-center">
<v-btn color="success" @click="setLocation">Save</v-btn>
</v-card-actions>
</div>
</v-container>
<v-fab-transition v-if="phoneLayout">
<v-btn @click="close" color="red" dark absolute bottom right>
<v-icon>close</v-icon>
</v-btn>
</v-fab-transition>
</v-card>
</v-dialog>
</template>
<script>
import { mapGetters } from 'vuex'
import { Modal, FullScreenModal } from '@/mixins'
import qbit from '@/services/qbit'
export default {
name: 'ChangeLocationModal',
mixins: [Modal, FullScreenModal],
props: {
hash: String
},
data() {
return {
newPath: ''
}
},
computed: {
...mapGetters(['getTorrent']),
dialogWidth() {
return this.phoneLayout ? '100%' : '750px'
},
torrent() {
return this.getTorrent(this.hash)
}
},
methods: {
setLocation() {
qbit.setTorrentLocation([this.hash], this.newPath)
this.close()
},
close() {
this.$store.commit('DELETE_MODAL', this.guid)
}
},
created() {
this.newPath = this.torrent.savePath
}
}
</script>

View file

@ -10,7 +10,7 @@
<v-card-title class="pb-0 justify-center primary">
<h2 class="white--text">Settings</h2>
</v-card-title>
<v-tabs v-model="tab" background-color="primary" center-active>
<v-tabs v-model="tab" background-color="primary" dark fixed-tabs>
<v-tab href="#downloads">Downloads</v-tab>
<v-tab href="#bittorrent">BitTorrent</v-tab>
<v-tab href="#webui">WebUI</v-tab>

View file

@ -51,6 +51,29 @@
Global Remove/Resume/Pause Buttons</template
>
</v-switch>
<v-switch
class="v-input--reverse v-input--expand pa-0 ma-0"
inset
v-model="denseDashboard"
color="green_accent"
>
<template #label>
Dense version of the dasbhoard</template
>
</v-switch>
<v-row dense>
<v-col cols="10" sm="10" md="10">
<p class="subtitle-1">Pagination size:</p>
</v-col>
<v-col cols="2" sm="2" md="2">
<v-select
class="pa-0 ma-0"
color="green_accent"
:items="paginationSizes"
v-model="paginationSize"
></v-select>
</v-col>
</v-row>
<v-row dense>
<v-col cols="10" sm="10" md="11">
<p class="subtitle-1">Current Version:</p>
@ -71,6 +94,11 @@
import { mapState, mapGetters } from 'vuex'
export default {
name: 'VueTorrent',
data() {
return {
paginationSizes: [5, 15, 30, 50]
}
},
computed: {
...mapState(['webuiSettings']),
...mapGetters(['getAppVersion']),
@ -114,6 +142,22 @@ export default {
this.webuiSettings.showGlobalRemoveResumePause = val
}
},
denseDashboard: {
get() {
return this.webuiSettings.denseDashboard
},
set(val) {
this.webuiSettings.denseDashboard = val
}
},
paginationSize: {
get() {
return this.webuiSettings.paginationSize
},
set(val) {
this.webuiSettings.paginationSize = val
}
},
version() {
return this.getAppVersion()
}

View file

@ -46,10 +46,10 @@ export default {
mixins: [Modal],
data() {
return {
sortProperty: { value: '', name: 'Default' },
reverse: false,
sortProperty: { value: 'added_on', name: 'Default' },
reverse: true,
options: [
{ value: 'default', name: 'Default' },
{ value: 'added_on', name: 'Default' },
{ value: 'availability', name: 'Availability' },
{ value: 'category', name: 'Category' },
{ value: 'completed', name: 'Completed' },

View file

@ -3,7 +3,7 @@
<v-card>
<v-container style="min-height: 200px" :class="`pa-0 project done`">
<v-card-title class="justify-center">
<h2>Create New Category</h2>
<h2>{{ hasInitialCategory ? 'Edit' : 'Create New' }} Category</h2>
</v-card-title>
<v-form ref="categoryForm" class="px-6 mt-3">
@ -69,8 +69,7 @@ export default {
hasInitialCategory() {
return (
this.initialCategory &&
this.initialCategory.name &&
this.initialCategory.savePath
this.initialCategory.name
)
}
},

View file

@ -3,11 +3,13 @@
<perfect-scrollbar>
<v-card-text style="max-height: 500px; min-height: 400px">
<v-treeview
v-model="tree"
v-model="selected"
:items="fileTree"
:open.sync="opened"
activatable
item-key="name"
open-on-click
selectable
item-key="fullName"
open-all
>
<template v-slot:prepend="{ item, open }">
<v-icon v-if="!item.icon">
@ -15,6 +17,14 @@
</v-icon>
<v-icon v-else>{{ item.icon }}</v-icon>
</template>
<template v-slot:label="{ item }">
<span v-if="!item.editing">{{item.name}}</span>
<v-text-field
autofocus
v-if="item.editing"
v-model="item.newName"
/>
</template>
<template v-slot:append="{ item }">
<span v-if="!item.icon"
>{{ item.children.length }} Files</span
@ -22,6 +32,33 @@
<div v-else>
<span>[{{ item.size }}]</span>
<span class="ml-4">{{ item.progress }}%</span>
<v-btn
v-if="!item.editing"
class="mb-2 ml-4"
x-small
fab
@click="edit(item)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn
v-if="item.editing"
class="mb-2 ml-4"
x-small
fab
@click="renameFile(item)"
>
<v-icon>save</v-icon>
</v-btn>
<v-btn
v-if="item.editing"
class="mb-2 ml-2"
x-small
fab
@click="togleEditing(item)"
>
<v-icon>close</v-icon>
</v-btn>
</div>
</template>
</v-treeview>
@ -41,7 +78,8 @@ export default {
},
data() {
return {
tree: [],
opened: null,
selected: [],
treeData: null
}
},
@ -56,7 +94,42 @@ export default {
methods: {
async getTorrentFiles() {
const { data } = await qbit.getTorrentFiles(this.hash)
data.forEach((d, i) => {
d.id = i
d.name = d.name.replace('.unwanted/', '')
})
this.treeData = data
},
async changeFilePriorities(newValue, oldValue) {
if (newValue.length == oldValue.length) return
const filesToExclude = oldValue.filter(f => !newValue.includes(f))
.map(name => this.treeData.find(f => f.name === name))
.filter(f => f.priority !== 0)
.map(f => f.id)
const filesToInclude = newValue.filter(f => !oldValue.includes(f))
.map(name => this.treeData.find(f => f.name === name))
.filter(f => f.priority === 0)
.map(f => f.id)
if (filesToExclude.length)
await qbit.setTorrentFilePriority(this.hash, filesToExclude, 0)
if (filesToInclude.length)
await qbit.setTorrentFilePriority(this.hash, filesToInclude, 1)
if (filesToExclude.length || filesToInclude.length)
await this.getTorrentFiles()
},
togleEditing(item) {
item.editing = !item.editing
},
edit(item){
item.newName = item.name
this.togleEditing(item)
},
renameFile(item) {
qbit.renameFile(this.hash, item.id, item.newName)
item.name = item.newName
this.togleEditing(item)
}
},
watch: {
@ -64,10 +137,21 @@ export default {
if (active) {
this.getTorrentFiles()
}
},
selected(newValue, oldValue) {
this.changeFilePriorities(newValue, oldValue)
}
},
created() {
this.getTorrentFiles()
this.getTorrentFiles().then(() => {
this.opened = [].concat(
...this.treeData.map(file => file.name.split('/'))
.filter(f => f.splice(-1, 1)))
.filter((f, index, self) => index === self.indexOf(f)
)
this.selected = this.treeData.filter(file => file.priority !== 0)
.map(file => file.name)
})
}
}
</script>

View file

@ -2,8 +2,8 @@
<v-card flat>
<perfect-scrollbar>
<v-card-text
class="pa-0"
style="font-size: 1.1em; max-height: 500px; min-height: 400px"
style="font-size: 1.1em; min-height: 400px;"
:style="{ maxHeight: phoneLayout ? '' : '500px' }"
>
<v-simple-table>
<tbody>
@ -13,6 +13,12 @@
{{ torrent.name }}
</td>
</tr>
<tr>
<td class="grey--text">Directory</td>
<td class="torrentmodaltext--text">
{{ torrent.savePath }}
</td>
</tr>
<tr style="margin-top: 10px !important">
<td class="grey--text">hash</td>
<td class="torrentmodaltext--text">
@ -32,13 +38,25 @@
</td>
</tr>
<tr>
<td class="grey--text">Download</td>
<td class="grey--text">Uploaded:</td>
<td class="torrentmodaltext--text">
{{ torrent.uploaded }}
</td>
</tr>
<tr>
<td class="grey--text">Ratio</td>
<td class="torrentmodaltext--text">
{{ torrent.ratio }}
</td>
</tr>
<tr>
<td class="grey--text">Download Speed</td>
<td class="torrentmodaltext--text">
{{ torrent.dlspeed }}
</td>
</tr>
<tr>
<td class="grey--text">Upload</td>
<td class="grey--text">Upload Speed</td>
<td class="torrentmodaltext--text">
{{ torrent.upspeed }}
</td>
@ -68,39 +86,21 @@
</td>
</tr>
<tr>
<td class="grey--text">Ratio</td>
<td class="grey--text">Added on</td>
<td class="torrentmodaltext--text">
{{ torrent.ratio }}%
{{ torrent.added_on }}
</td>
</tr>
<tr>
<td class="grey--text">Tags</td>
<td v-if="torrent.tags">
{{ torrent.tags.join(',') }}
</td>
<td v-else>None</td>
</tr>
<tr>
<td class="grey--text">Status</td>
<v-chip
small
:class="`${torrent.state} white--text my-2 caption`"
:class="`${torrent.state.toLowerCase()} white--text my-2 caption`"
>{{ torrent.state }}</v-chip
>
</tr>
</tbody>
</v-simple-table>
<v-flex class="pt-3 pb-4">
<v-progress-linear
height="5"
stream
rounded
color="cyan darken-1"
background-color="cyan lighten-3"
:buffer-value="torrent.progress"
></v-progress-linear>
</v-flex>
</v-card-text>
</perfect-scrollbar>
</v-card>
@ -117,6 +117,9 @@ export default {
...mapGetters(['getTorrent']),
torrent() {
return this.getTorrent(this.hash)
},
phoneLayout() {
return this.$vuetify.breakpoint.xsOnly
}
}
}

View file

@ -16,7 +16,7 @@
<v-card-title class="pb-0 justify-center primary">
<h2 class="white--text">Torrent Detail</h2>
</v-card-title>
<v-tabs v-model="tab" background-color="primary" center-active>
<v-tabs v-model="tab" background-color="primary" dark fixed-tabs>
<v-tab href="#info">Info</v-tab>
<v-tab href="#trackers">Trackers</v-tab>
<v-tab href="#peers">Peers</v-tab>

View file

@ -92,7 +92,7 @@ export default {
},
data() {
return {
drawer: false
drawer: this.$vuetify.breakpoint.mdAndUp
}
},

View file

@ -93,6 +93,7 @@ export default {
},
removeTorrents() {
qbit.deleteTorrents(this.selected_torrents, false)
this.$store.commit('RESET_SELECTED')
},
addModal(name) {
this.createModal(name)

View file

@ -7,14 +7,12 @@
@click.native="selectTorrent(torrent.hash)"
@dblclick.prevent="showInfo(torrent.hash)"
>
<v-tooltip top>
<template v-slot:activator="{ on }">
<v-layout v-on="on" row wrap :class="style">
<v-flex xs12 sm2 md3>
<v-layout row wrap :class="style">
<v-flex xs12 class="mb-4">
<div class="caption grey--text">Torrent title</div>
<div class="truncate">{{ torrent.name }}</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
<v-flex xs6 sm1 md1>
<div class="caption grey--text">Size</div>
<div>
{{ torrent.size | getDataValue }}
@ -23,16 +21,33 @@
}}</span>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<v-flex xs12 sm1 md1 class="mr-4">
<div class="caption grey--text">Done</div>
<v-progress-linear
v-model="torrent.progress"
height="20"
:style="phoneLayout ? '' : 'width: 80%;'"
:color="`torrent-${state}-color`" >
<span
class="caption"
>
{{ torrent.progress }}%
</span>
</v-progress-linear>
</v-flex>
<v-flex xs6 sm1 md1>
<div class="caption grey--text">Ratio</div>
<div>
{{ torrent.dloaded | getDataValue }}
<span class="caption grey--text">{{
torrent.dloaded | getDataUnit
}}</span>
{{ torrent.ratio }}
</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
<v-flex xs6 sm1 md1>
<div class="caption grey--text">ETA</div>
<div>
{{ torrent.eta }}
</div>
</v-flex>
<v-flex xs6 sm1 md1>
<div class="caption grey--text">Download</div>
<div>
{{ torrent.dlspeed | getDataValue }}
@ -41,7 +56,7 @@
}}</span>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<v-flex xs6 sm1 md1>
<div class="caption grey--text">Upload</div>
<div>
{{ torrent.upspeed | getDataValue }}
@ -50,13 +65,7 @@
}}</span>
</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
<div class="caption grey--text">ETA</div>
<div>
{{ torrent.eta | formatEta({ dayLimit: 100 }) }}
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<v-flex xs6 sm1 md1>
<div class="caption grey--text">Peers</div>
<div>
{{ torrent.num_leechs }}
@ -65,7 +74,7 @@
>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<v-flex xs6 sm1 md1>
<div class="caption grey--text">Seeds</div>
<div>
{{ torrent.num_seeds }}
@ -74,165 +83,53 @@
>
</div>
</v-flex>
<v-flex xs4 sm12 md1>
<div class="right">
<v-flex xs6 sm1 md1 :class="phoneLayout ? '' : 'mr-4'">
<div class="caption grey--text">Status</div>
<v-chip
small
class="my-2 caption"
class="caption"
:class="
theme === 'light'
? `${torrent.state} white--text `
: `${torrent.state} black--text`
"
>{{ torrent.state }}</v-chip
>
</div>
? `${state} white--text `
: `${state} black--text`">
{{ torrent.state }}
</v-chip>
</v-flex>
<!-- labels -->
<v-flex v-for="tag in torrent.tags" :key="tag" xs3 sm1 md1>
<v-chip
<!-- Category -->
<v-flex v-if="torrent.category" xs4 sm1 md1>
<div class="caption grey--text">Category</div>
<v-chip small class="upload white--text caption">
{{ torrent.category }}
</v-chip>
</v-flex>
<!-- Tags -->
<v-flex xs12 sm1>
<div class="caption grey--text">Tags</div>
<v-row wrap class="ma-0">
<v-chip v-for="tag in torrent.tags" :key="tag"
small
:class="
theme === 'light'
? 'white--text'
: 'black--text'
"
class="download my-2 caption"
class="download caption mb-1 mx-1"
>
{{ tag }}
</v-chip>
</v-flex>
<v-flex v-if="torrent.category" xs3 sm1 md1>
<v-chip small class="upload white--text my-2 caption">
{{ torrent.category }}
</v-chip>
</v-flex>
<v-flex xs12 sm12 md12>
<v-progress-linear
height="3"
rounded
color="cyan darken-1"
background-color="cyan lighten-3"
:value="torrent.progress"
></v-progress-linear>
</v-row>
</v-flex>
</v-layout>
</template>
<span>{{ torrent.name }}</span>
</v-tooltip>
<v-divider v-if="index !== length"></v-divider>
</v-card>
</template>
<script>
import { General } from '@/mixins'
import { mapGetters } from 'vuex'
import { General, Torrent } from '@/mixins'
export default {
name: 'Torrent',
mixins: [General],
props: {
torrent: Object,
index: Number,
length: Number
},
computed: {
chips() {
let chips = []
if (this.torrent.category.length > 0) {
chips.push(this.torrent.category)
}
return chips
},
...mapGetters(['getTheme']),
theme() {
return this.getTheme() ? 'dark' : 'light'
},
style() {
let base = `pa-4 ml-0 sideborder ${this.torrent.state} `
if (this.index === this.length) base += ' bottomBorderRadius'
if (this.index === 0) base += ' topBorderRadius'
return base
}
},
methods: {
selectTorrent(hash) {
if (this.containsTorrent(hash)) {
this.$store.commit('SET_SELECTED', { type: 'remove', hash })
} else {
this.$store.commit('SET_SELECTED', { type: 'add', hash })
}
},
containsTorrent(hash) {
return this.$store.getters.containsTorrent(hash)
},
showInfo(hash) {
this.createModal('TorrentDetailModal', { hash })
}
},
filters: {
formatEta(value, options) {
const minute = 60
const hour = minute * 60
const day = hour * 24
const year = day * 365
const durations = [year, day, hour, minute, 1]
const units = 'ydhms'
let index = 0
let unitSize = 0
const parts = []
const defaultOptions = {
maxUnitSize: 2,
dayLimit: 0,
minUnit: 0
}
const opt = options
? Object.assign(defaultOptions, options)
: defaultOptions
if (opt.dayLimit && value >= opt.dayLimit * day) {
return '∞'
}
while (
(!opt.maxUnitSize || unitSize !== opt.maxUnitSize) &&
index !== durations.length
) {
const duration = durations[index]
if (value < duration) {
index++
continue
} else if (
opt.minUnit &&
durations.length - index <= opt.minUnit
) {
break
}
const result = Math.floor(value / duration)
parts.push(result + units[index])
value %= duration
index++
unitSize++
}
if (!parts.length) {
return '0' + units[durations.length - 1 - opt.minUnit]
}
return parts.join(' ')
}
}
mixins: [General, Torrent]
}
</script>

View file

@ -20,10 +20,16 @@
>
</v-list-item>
<v-divider />
<v-list-item @click="directory" link>
<v-icon>folder</v-icon>
<v-list-item-title class="ml-2" style="font-size: 15px"
>Change location</v-list-item-title
>
</v-list-item>
<v-list-item @click="reannounce" link>
<v-icon>record_voice_over</v-icon>
<v-list-item-title class="ml-2" style="font-size: 15px"
>reannounce</v-list-item-title
>Reannounce</v-list-item-title
>
</v-list-item>
<v-divider />
@ -58,6 +64,9 @@ export default {
pause() {
qbit.pauseTorrents([this.hash])
},
directory() {
this.createModal('ChangeLocationModal', { hash: this.hash })
},
reannounce() {
qbit.reannounceTorrents([this.hash])
},

View file

@ -0,0 +1,135 @@
<template>
<v-card
ripple
flat
class="pointer torrent noselect"
:class="{ torrent_selected: containsTorrent(torrent.hash) }"
@click.native="selectTorrent(torrent.hash)"
@dblclick.prevent="showInfo(torrent.hash)"
>
<v-tooltip top>
<template v-slot:activator="{ on }">
<v-layout v-on="on" row wrap :class="style">
<v-flex xs12 sm2 md3>
<div class="caption grey--text">Torrent title</div>
<div class="truncate">{{ torrent.name }}</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
<div class="caption grey--text">Size</div>
<div>
{{ torrent.size | getDataValue }}
<span class="caption grey--text">{{
torrent.size | getDataUnit
}}</span>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<div class="caption grey--text">Done</div>
<div>
{{ torrent.progress }}<span class="grey--text">% </span>
</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
<div class="caption grey--text">Download</div>
<div>
{{ torrent.dlspeed | getDataValue }}
<span class="caption grey--text">{{
torrent.dlspeed | getDataUnit
}}</span>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<div class="caption grey--text">Upload</div>
<div>
{{ torrent.upspeed | getDataValue }}
<span class="caption grey--text">{{
torrent.upspeed | getDataUnit
}}</span>
</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
<div class="caption grey--text">ETA</div>
<div>
{{ torrent.eta }}
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<div class="caption grey--text">Peers</div>
<div>
{{ torrent.num_leechs }}
<span class="grey--text caption"
>/{{ torrent.available_peers }}</span
>
</div>
</v-flex>
<v-flex xs6 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
>
</div>
</v-flex>
<v-flex xs5 sm1>
<div class="caption grey--text">Status</div>
<v-chip
small
class="caption"
:class="
theme === 'light'
? `${state} white--text `
: `${state} black--text`">
{{ torrent.state }}
</v-chip>
</v-flex>
<!-- Category -->
<v-flex v-if="torrent.category" class="mr-2" xs6 sm1 md1>
<div class="caption grey--text">Category</div>
<v-chip small class="upload white--text caption">
{{ torrent.category }}
</v-chip>
</v-flex>
<!-- Tags -->
<v-flex xs5 sm4>
<div class="caption grey--text">Tags</div>
<v-row wrap class="ma-0">
<v-chip v-for="tag in torrent.tags" :key="tag"
small
:class="
theme === 'light'
? 'white--text'
: 'black--text'
"
class="download caption mb-1 mx-1"
>
{{ tag }}
</v-chip>
</v-row>
</v-flex>
</v-layout>
</template>
<span>{{ torrent.name }}</span>
</v-tooltip>
<v-divider v-if="index !== length"></v-divider>
</v-card>
</template>
<script>
import { General, Torrent } from '@/mixins'
export default {
name: 'TorrentDense',
mixins: [General, Torrent]
}
</script>
<style lang="scss" scoped>
.topBorderRadius {
border-top-left-radius: 3px;
}
.bottomBorderRadius {
border-bottom-left-radius: 3px;
}
</style>

View file

@ -54,7 +54,7 @@ export function treeify(paths) {
let level = { result }
paths.forEach(path => {
path.name.split('/').reduce((r, name, i, a) => {
path.name.split('/').reduce((r, name) => {
if (!r[name]) {
r[name] = { result: [] }
r.result.push(createFile(path, name, r[name].result))
@ -81,7 +81,9 @@ export function treeify(paths) {
function createFile(data, name, children) {
return {
id: data.id,
name: name,
fullName: data.name,
progress: Math.round(data.progress * 100),
size: formatBytes(data.size),
icon: getIconForFileType(name.split('.').pop()),
@ -92,6 +94,7 @@ function createFile(data, name, children) {
function createFolder(name, children) {
return {
name: name,
fullName: name,
type: 'directory',
children: children
}

42
src/mixins/Torrent.js Normal file
View file

@ -0,0 +1,42 @@
import {mapGetters} from 'vuex'
export default {
props: {
torrent: Object,
index: Number,
length: Number
},
computed: {
...mapGetters(['getTheme']),
theme() {
return this.getTheme() ? 'dark' : 'light'
},
state() {
return this.torrent.state.toLowerCase()
},
style() {
let base = `pa-4 ml-0 sideborder ${this.state} `
if (this.index === this.length) base += ' bottomBorderRadius'
if (this.index === 0) base += ' topBorderRadius'
return base
},
phoneLayout() {
return this.$vuetify.breakpoint.xsOnly
}
},
methods: {
selectTorrent(hash) {
if (this.containsTorrent(hash)) {
this.$store.commit('SET_SELECTED', { type: 'remove', hash })
} else {
this.$store.commit('SET_SELECTED', { type: 'add', hash })
}
},
containsTorrent(hash) {
return this.$store.getters.containsTorrent(hash)
},
showInfo(hash) {
this.createModal('TorrentDetailModal', { hash })
}
}
}

View file

@ -3,5 +3,6 @@ import Modal from './Modal'
import SettingsTab from './SettingsTab'
import Tab from './Tab'
import General from './General'
import Torrent from './Torrent'
export { FullScreenModal, Modal, SettingsTab, Tab, General }
export { FullScreenModal, Modal, SettingsTab, Tab, General, Torrent }

View file

@ -2,68 +2,127 @@ export default 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.added_on = new Date(data.added_on * 1000).toLocaleString()
this.dlspeed = this.formatBytes(data.dlspeed, 1) + '/s'
this.dloaded = this.formatBytes(data.completed)
this.upspeed = this.formatBytes(data.upspeed, 1)
this.upspeed = this.formatBytes(data.upspeed, 1) + '/s'
this.uploaded = this.formatBytes(data.uploaded)
this.eta = data.eta
this.eta = this.formatEta(data.eta)
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)
this.state = this.formatState(data)
// hash is used to identify
this.hash = data.hash
// available seeds
this.available_seeds = data.num_complete
this.available_peers = data.num_incomplete
this.savePath = data.save_path
this.progress = data.progress * 100
this.ratio = Math.round(data.ratio * 100)
this.progress = Math.round(data.progress * 10000) / 100
this.ratio = Math.round(data.ratio * 100) / 100
this.tags = data.tags.length > 0 ? data.tags.split(',') : null
this.category = data.category
}
formatState(state) {
switch (state) {
formatState(item) {
if (!item.tracker) return 'Fail'
switch (item.state) {
case 'forceDL':
case 'downloading':
return 'busy'
return 'Downloading'
case 'metaDL':
return 'metadata'
return 'Metadata'
case 'forcedUP':
case 'uploading':
case 'stalledUP':
return 'seeding'
return 'Seeding'
case 'pausedDL':
return 'paused'
return 'Paused'
case 'pausedUP':
return 'done'
return 'Done'
case 'queuedDL':
case 'queuedUP':
return 'queued'
return 'Queued'
case 'allocating':
case 'checkingDL':
case 'checkingUP':
case 'checkingResumeData':
case 'moving':
return 'checking'
return 'Checking'
case 'unknown':
case 'missingFiles':
return 'fail'
return 'Fail'
case 'stalledDL':
return 'stalled'
return 'Stalled'
default:
return 'fail'
return 'Fail'
}
}
formatBytes(a, b) {
if (a == 0) return '0 Bytes'
if (a == 0) return '0 B'
const c = 1024
const d = b || 2
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const e = ['B', '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]}`
}
formatEta(value) {
let options = { dayLimit: 100 }
const minute = 60
const hour = minute * 60
const day = hour * 24
const year = day * 365
const durations = [year, day, hour, minute, 1]
const units = 'ydhms'
let index = 0
let unitSize = 0
const parts = []
const defaultOptions = {
maxUnitSize: 2,
dayLimit: 0,
minUnit: 0
}
const opt = options
? Object.assign(defaultOptions, options)
: defaultOptions
if (opt.dayLimit && value >= opt.dayLimit * day) {
return '∞'
}
while (
(!opt.maxUnitSize || unitSize !== opt.maxUnitSize) &&
index !== durations.length
) {
const duration = durations[index]
if (value < duration) {
index++
continue
} else if (
opt.minUnit &&
durations.length - index <= opt.minUnit
) {
break
}
const result = Math.floor(value / duration)
parts.push(result + units[index])
value %= duration
index++
unitSize++
}
if (!parts.length) {
return '0' + units[durations.length - 1 - opt.minUnit]
}
return parts.join(' ')
}
}

View file

@ -3,6 +3,7 @@ import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import colors from 'vuetify/lib/util/colors'
import variables from '../styles/variables.scss'
Vue.use(Vuetify)
@ -28,7 +29,8 @@ export default new Vuetify({
background: colors.grey.lighten4,
search: colors.grey.darken1,
torrentmodaltext: colors.grey.darken4,
select: colors.grey.lighten4
select: colors.grey.lighten4,
...variables
},
dark: {
primary: '#35495e',
@ -45,7 +47,8 @@ export default new Vuetify({
background: colors.grey.darken4,
search: colors.grey.darken3,
torrentmodaltext: colors.grey.lighten4,
select: colors.grey.darken3
select: colors.grey.darken3,
...variables
}
}
}

View file

@ -214,6 +214,16 @@ class Qbit {
})
}
renameFile(hash, id, name) {
const params = {
hash,
id,
name
}
const data = new URLSearchParams(params)
return this.axios.post('/torrents/renameFile', data)
}
getAvailableTags() {
return this.axios.get('/torrents/tags')
}
@ -299,6 +309,7 @@ class Qbit {
// End Categories
actionTorrents(action, hashes, extra) {
if (action == 'delete' && !hashes.length) return
const params = {
hashes: hashes.length ? hashes.join('|') : 'all',
...extra

View file

@ -31,8 +31,8 @@ export default new Vuex.Store({
selected_torrents: [],
authenticated: false,
sort_options: {
sort: 'name',
reverse: false,
sort: 'default',
reverse: true,
hashes: [],
filter: null
},
@ -46,7 +46,9 @@ export default new Vuex.Store({
showSpeedGraph: true,
showSessionStat: true,
showCurrentSpeed: true,
showGlobalRemoveResumePause: true
showGlobalRemoveResumePause: true,
denseDashboard: true,
paginationSize: 15
},
categories: [],
filteredTorrentsCount: 0

View file

@ -61,10 +61,10 @@ export default {
state.settings = data
},
UPDATE_SORT_OPTIONS: (state, payload) => {
state.sort_options.sort = payload.name ? payload.name : null
state.sort_options.reverse = payload.reverse ? payload.reverse : null
state.sort_options.hashes = payload.hashes ? payload.hashes : null
state.sort_options.filter = payload.filter ? payload.filter : null
state.sort_options.sort = payload.name ? payload.name : state.sort_options.sort
state.sort_options.reverse = payload.reverse ? payload.reverse : state.sort_options.reverse
state.sort_options.hashes = payload.hashes ? payload.hashes : state.sort_options.hashes
state.sort_options.filter = payload.filter ? payload.filter : state.sort_options.filter
state.sort_options.category =
payload.category !== null ? payload.category : null
},

21
src/styles/variables.scss Normal file
View file

@ -0,0 +1,21 @@
$torrent-done: #3cd1c2;
$torrent-downloading: #ffaa2c;
$torrent-fail: #f83e70;
$torrent-paused: #cfd8dc;
$torrent-queued: #2e5eaa;
$torrent-seeding: #26a69a;
$torrent-checking: #ff7043;
$torrent-stalled: #81c784;
$torrent-metadata: #7e57c2;
:export {
torrent-done-color: $torrent-done;
torrent-downloading-color: $torrent-downloading;
torrent-fail-color: $torrent-fail;
torrent-paused-color: $torrent-paused;
torrent-queued-color: $torrent-queued;
torrent-seeding-color: $torrent-seeding;
torrent-checking-color: $torrent-checking;
torrent-stalled-color: $torrent-stalled;
torrent-metadata-color: $torrent-metadata;
}

View file

@ -10,9 +10,9 @@
</p>
</h1>
<v-container
<div
color="background"
class="my-4 pt-5 pa-0"
class="my-4 pt-5 px-8"
@click.self="resetSelected"
>
<v-flex xs12 sm6 md3 @click.self="resetSelected">
@ -23,6 +23,7 @@
clearable
solo
color="search"
@click:clear="resetInput()"
v-model="input"
></v-text-field>
</v-flex>
@ -32,10 +33,21 @@
<div v-else>
<div
@contextmenu.prevent="$refs.menu.open($event, { torrent })"
v-for="(torrent, index) in torrents"
v-for="(torrent, index) in paginatedData"
:key="torrent.hash"
>
<Torrent
<Torrent v-if="!denseDashboard"
:class="{
topBorderRadius: index === 0,
noBorderRadius:
index !== 0 && index !== torrent.length - 1,
bottomBorderRadius: index === torrents.length - 1
}"
:torrent="torrent"
:index="index"
:length="torrents.length - 1"
/>
<TorrentDense v-if="denseDashboard"
:class="{
topBorderRadius: index === 0,
noBorderRadius:
@ -47,8 +59,20 @@
:length="torrents.length - 1"
/>
</div>
</div>
<v-row v-if="pageCount > 1" xs12 justify="center">
<v-col>
<v-container>
<v-pagination
v-model="pageNumber"
:length="pageCount"
:total-visible="7"
@input="toTop"
></v-pagination>
</v-container>
</v-col>
</v-row>
</div>
</div>
<vue-context ref="menu" v-slot="{ data }">
<TorrentRightClickMenu v-if="data" :hash="data.torrent.hash" />
</vue-context>
@ -58,6 +82,7 @@
<script>
import { mapState, mapGetters } from 'vuex'
import Torrent from '@/components/Torrent'
import TorrentDense from '@/components/TorrentDense'
import Fuse from 'fuse.js'
import { VueContext } from 'vue-context'
import 'vue-context/src/sass/vue-context.scss'
@ -65,17 +90,18 @@ import TorrentRightClickMenu from '@/components/Torrent/TorrentRightClickMenu.vu
export default {
name: 'Dashboard',
components: { Torrent, VueContext, TorrentRightClickMenu },
components: { Torrent, TorrentDense, VueContext, TorrentRightClickMenu },
data() {
return {
input: ''
input: '',
pageNumber: 1
}
},
computed: {
...mapState(['mainData']),
...mapGetters(['getTorrents', 'getTorrentCountString']),
...mapGetters(['getTorrents', 'getTorrentCountString', 'getWebuiSettings']),
torrents() {
if (this.input.length === 0) return this.getTorrents()
if (!this.input || !this.input.length) return this.getTorrents()
const options = {
threshold: 0.3,
@ -92,13 +118,35 @@ export default {
const fuse = new Fuse(this.getTorrents(), options)
return fuse.search(this.input).map(el => el.item)
},
paginationSize() {
return this.getWebuiSettings().paginationSize
},
pageCount(){
let l = this.torrents.length,
s = this.paginationSize
return Math.ceil(l/s)
},
paginatedData(){
const start = (this.pageNumber - 1) * this.paginationSize,
end = start + this.paginationSize
return this.torrents.slice(start, end)
},
torrentCountString() {
return this.getTorrentCountString()
},
denseDashboard(){
return this.getWebuiSettings().denseDashboard
}
},
methods: {
resetSelected() {
this.$store.commit('RESET_SELECTED')
},
resetInput(){
this.input = ''
},
toTop () {
this.$vuetify.goTo(0)
}
},
created() {

View file

@ -1,5 +1,13 @@
const webpack = require('webpack')
module.exports = {
chainWebpack: config => {
config
.plugin('html')
.tap(args => {
args[0].title = 'VueTorrent'
return args
})
},
outputDir: 'vuetorrent/public',
publicPath: './',
transpileDependencies: ['vuetify'],