torrent right click menu

This commit is contained in:
Daan Wijns 2020-05-23 10:13:26 +02:00
parent 04975e1ce4
commit 7819abb1e7
10 changed files with 295 additions and 207 deletions

View file

@ -20,28 +20,28 @@ The sleekest looking WEBUI for qBittorrent made with Vuejs!
## Installation
- Download & Unzip the latest release
- Download & Unzip the latest release
- Point your Alternate WEBUI location to it
- Point your Alternate WEBUI location to it
## Development
- clone the repo
- clone the repo
- npm install
- npm install
- npm run serve
- npm run serve
## Features
- viewing sessions stats ( down / upload speed, session uploaded / downloaded )
- adding / removing / pausing / resuming torrents
- viewing sessions status ( down / upload speed, session uploaded / downloaded )
- adding / removing / pausing / resuming torrents
- sorting by every property shown!
- 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
- works on QBittorrent V4.2 and later
## Contributing
@ -49,23 +49,23 @@ I'll gladly accept help/pull requests & advice! (this is my first project of thi
## FAQ
- **Why build this??**
- **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>
- <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')
[<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: '[Net Ninja - Vuetify](https://github.com/iamshaunjp/vuetify-playlist)'.
Also check out The Net Ninja's Youtube Channel.
- Dashboard design heavily inspired by: '[Net Ninja - Vuetify](https://github.com/iamshaunjp/vuetify-playlist)'.
Also check out The Net Ninja's Youtube Channel.
* This repo '[CzBiX qb-web ](https://github.com/CzBiX/qb-web)'
* This repo '[CzBiX qb-web ](https://github.com/CzBiX/qb-web)'

20
package-lock.json generated
View file

@ -6455,8 +6455,7 @@
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml": {
"version": "3.13.1",
@ -6763,7 +6762,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
@ -10789,6 +10787,22 @@
}
}
},
"vue-clickaway": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/vue-clickaway/-/vue-clickaway-2.2.2.tgz",
"integrity": "sha512-25SpjXKetL06GLYoLoC8pqAV6Cur9cQ//2g35GRFBV4FgoljbZZjTINR8g2NuVXXDMLSUXaKx5dutgO4PaDE7A==",
"requires": {
"loose-envify": "^1.2.0"
}
},
"vue-context": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vue-context/-/vue-context-5.1.0.tgz",
"integrity": "sha512-PcKbc9mjiUt4fswRR/oA/IJc5oXHjpnKIcATWZcBTDk7CzTvLznkIjq6pGFI8vUtGzABxVMvPq93dpBSRfinxg==",
"requires": {
"vue-clickaway": "^2.2.2"
}
},
"vue-eslint-parser": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.1.0.tgz",

View file

@ -15,6 +15,7 @@
"register-service-worker": "^1.7.1",
"vue": "^2.6.11",
"vue-apexcharts": "^1.5.3",
"vue-context": "^5.1.0",
"vue-observe-visibility": "^0.4.6",
"vue-router": "^3.2.0",
"vue-toastification": "^1.7.1",

View file

@ -45,7 +45,7 @@
<!--navigation drawer itself -->
<v-navigation-drawer app v-model="drawer" class="primary" style="position:fixed;">
<!--current download speeds -->
<v-flex class="mt-3" v-if="stats">
<v-flex class="mt-3" v-if="status">
<div
class="secondary_lighter--text text-uppercase caption ml-4"
>
@ -56,14 +56,14 @@
<v-icon color="download">keyboard_arrow_down</v-icon>
<span class="download--text title">
{{
stats.dlspeed.substring(
status.dlspeed.substring(
0,
stats.dlspeed.indexOf(' ')
status.dlspeed.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.dlspeed.substring(
stats.dlspeed.indexOf(' ')
status.dlspeed.substring(
status.dlspeed.indexOf(' ')
)
}}</span>
</span>
@ -72,14 +72,14 @@
>
<span class="upload--text title">
{{
stats.upspeed.substring(
status.upspeed.substring(
0,
stats.upspeed.indexOf(' ')
status.upspeed.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.upspeed.substring(
stats.upspeed.indexOf(' ')
status.upspeed.substring(
status.upspeed.indexOf(' ')
)
}}</span>
</span>
@ -98,7 +98,7 @@
<div
class="secondary_lighter--text text-uppercase caption ml-4"
>
session stats
session status
</div>
<v-card flat color="secondary" class="mr-2 ml-2">
<v-layout row wrap class="pa-3 project nav_download mx-auto">
@ -108,14 +108,14 @@
<v-flex md5 class="ml-4">
<span class="download--text title">
{{
stats.downloaded.substring(
status.downloaded.substring(
0,
stats.downloaded.indexOf(' ')
status.downloaded.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.downloaded.substring(
stats.downloaded.indexOf(' ')
status.downloaded.substring(
status.downloaded.indexOf(' ')
)
}}</span>
</span>
@ -130,14 +130,14 @@
<v-flex md5 class="ml-4">
<span class="upload--text title">
{{
stats.uploaded.substring(
status.uploaded.substring(
0,
stats.uploaded.indexOf(' ')
status.uploaded.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.uploaded.substring(
stats.uploaded.indexOf(' ')
status.uploaded.substring(
status.uploaded.indexOf(' ')
)
}}</span>
</span>
@ -153,14 +153,14 @@
<v-flex md5 class="ml-4">
<span class="upload--text title">
{{
stats.freeDiskSpace.substring(
status.freeDiskSpace.substring(
0,
stats.freeDiskSpace.indexOf(' ')
status.freeDiskSpace.indexOf(' ')
)
}}
<span class="font-weight-light caption">{{
stats.freeDiskSpace.substring(
stats.freeDiskSpace.indexOf(' ')
status.freeDiskSpace.substring(
status.freeDiskSpace.indexOf(' ')
)
}}</span>
</span>
@ -294,13 +294,13 @@ export default {
}
},
computed: {
...mapState(['stats', 'selected_torrents']),
...mapGetters(['getTheme', 'getStats']),
...mapState(['status', 'selected_torrents']),
...mapGetters(['getTheme', 'getStatus']),
theme() {
return this.getTheme() ? 'Dark' : 'Light'
},
altSpeed(){
return this.getStats().altSpeed
return this.getStatus().altSpeed
}
},
created() {

View file

@ -1,172 +1,170 @@
<template>
<v-card ripple flat class="pointer torrent"
<v-card
ripple
flat
class="pointer torrent"
:class="containsTorrent(torrent.hash) ? 'torrent_selected' : ''"
@click.native="selectTorrent(torrent.hash)">
<v-layout row wrap :class="`pa-4 ml-0 project ${torrent.state}`">
<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.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>
</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>
</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>
</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 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
>
</div>
</v-flex>
<v-flex xs4 sm12 md1>
<div class="right">
<v-chip
small
:class="
`${torrent.state} white--text my-2 caption`
"
>{{ torrent.state }}</v-chip
>
</div>
</v-flex>
<v-flex xs12 sm12 md12>
<v-progress-linear
height="3"
color="cyan darken-1"
background-color="cyan lighten-3"
:value="(torrent.dloaded / torrent.size) * 100"
></v-progress-linear>
</v-flex>
</v-layout>
<v-divider></v-divider>
</v-card>
@click.native="selectTorrent(torrent.hash)"
>
<v-tooltip top>
<template v-slot:activator="{ on }">
<v-layout
@contextmenu.prevent="$refs.menu.open"
v-on="on"
row
wrap
:class="`pa-4 ml-0 project ${torrent.state}`"
>
<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.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>
</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>
</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>
</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 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
>
</div>
</v-flex>
<v-flex xs4 sm12 md1>
<div class="right">
<v-chip
small
:class="`${torrent.state} white--text my-2 caption`"
>{{ torrent.state }}</v-chip
>
</div>
</v-flex>
<v-flex xs12 sm12 md12>
<v-progress-linear
height="3"
color="cyan darken-1"
background-color="cyan lighten-3"
:value="(torrent.dloaded / torrent.size) * 100"
></v-progress-linear>
</v-flex>
</v-layout>
</template>
<span>{{ torrent.name }}</span>
</v-tooltip>
<v-divider></v-divider>
<vue-context ref="menu">
<torrentRightClickMenu :hash="torrent.hash" />
</vue-context>
</v-card>
</template>
<script>
import { VueContext } from "vue-context";
import torrentRightClickMenu from "@/components/Torrent/torrentRightClickMenu.vue";
export default {
name:'Torrent',
props: {
torrent: Object
},
methods: {selectTorrent(hash) {
if (this.containsTorrent(hash)) {
this.$store.commit('SET_SELECTED', {type:"remove", hash})
name: "Torrent",
components: {
VueContext,
torrentRightClickMenu,
},
props: {
torrent: Object,
},
methods: {
selectTorrent(hash) {
if (this.containsTorrent(hash)) {
this.$store.commit("SET_SELECTED", { type: "remove", hash });
} else {
this.$store.commit('SET_SELECTED', {type:"add", hash})
this.$store.commit("SET_SELECTED", { type: "add", hash });
}
},
containsTorrent(hash) {
return this.$store.getters.containsTorrent(hash)
},}
}
},
containsTorrent(hash) {
return this.$store.getters.containsTorrent(hash);
},
},
};
</script>
<style>
.project.done {
border-left: 4px solid #3cd1c2;
border-left: 4px solid #3cd1c2;
}
.project.busy {
border-left: 4px solid #ffaa2c;
border-left: 4px solid #ffaa2c;
}
.project.fail {
border-left: 4px solid #f83e70;
border-left: 4px solid #f83e70;
}
.project.paused {
border-left: 4px solid #cfd8dc;
border-left: 4px solid #cfd8dc;
}
.v-chip.done {
background: #3cd1c2 !important;
background: #3cd1c2 !important;
}
.v-chip.busy {
background: #ffaa2c !important;
background: #ffaa2c !important;
}
.v-chip.fail {
background: #f83e70 !important;
background: #f83e70 !important;
}
.v-chip.paused {
background: #cfd8dc !important;
background: #cfd8dc !important;
}
.pointer {
cursor: pointer;
cursor: pointer;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View file

@ -0,0 +1,69 @@
<template>
<v-card
elevation="20"
width="200"
style="position:absolute; top: 50%; left:50% ;z-index: 10; overflow: show"
:dark="dark"
>
<v-list dense rounded>
<v-list-item @click="resume" link>
<v-icon>play_arrow</v-icon>
<v-list-item-title class="ml-2" style="font-size:15px;">Resume</v-list-item-title>
</v-list-item>
<v-list-item @click="pause" link>
<v-icon>pause</v-icon>
<v-list-item-title class="ml-2" style="font-size:15px;">Pause</v-list-item-title>
</v-list-item>
<v-divider/>
<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>
</v-list-item>
<v-divider/>
<v-list-item @click="deleteWithoutFiles" link>
<v-icon color="red">delete</v-icon>
<v-list-item-title class="ml-2" style="font-size:15px; color:red">Delete</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteWithFiles" link>
<v-icon color="red">delete</v-icon>
<v-list-item-title class="ml-2" style="font-size:15px; color:red;">Delete with files</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</template>
<script>
import qbit from '@/services/qbit'
export default {
name: 'torrentRightClickMenu',
props: {
hash: String
},
methods: {
resume(){
qbit.resumeTorrents([this.hash])
},
pause(){
qbit.pauseTorrents([this.hash])
},
reannounce(){
qbit.reannounceTorrents([this.hash])
},
deleteWithoutFiles(){
qbit.deleteTorrents([this.hash], false)
},
deleteWithFiles(){
qbit.deleteTorrents([this.hash], true)
}
},
computed: {
dark() {
return this.$vuetify.dark
}
}
}
</script>
<style>
</style>

View file

@ -1,4 +1,4 @@
export default class Stat {
export default class Status {
constructor(data) {
if (data != undefined && data != null) {
this.status = data.connection_status

View file

@ -2,7 +2,7 @@ import Vue from 'vue'
import Router from 'vue-router'
import Dashboard from '@/views/Dashboard.vue'
import Login from '@/views/Login.vue'
import store from '@/store'
import {isAuthenticated} from '@/services/auth.js'
Vue.use(Router)
@ -28,12 +28,12 @@ const router = new Router({
})
router.beforeEach((to, from, next) => {
router.beforeEach(async(to, from, next) => {
const isPublic = to.matched.some(record => record.meta.public)
const onlyWhenLoggedOut = to.matched.some(record => record.meta.onlyWhenLoggedOut)
const loggedIn = store.state.authenticated
const authenticated = await isAuthenticated();
if (!isPublic && !loggedIn) {
if (!isPublic && !authenticated) {
return next({
path:'/login',
query: {redirect: to.fullPath} // Store the full path to redirect the user to after login
@ -41,7 +41,7 @@ router.beforeEach((to, from, next) => {
}
// Do not allow user to visit login page or register page if they are logged in
if (loggedIn && onlyWhenLoggedOut) {
if (authenticated && onlyWhenLoggedOut) {
return next('/')
}

6
src/services/auth.js Normal file
View file

@ -0,0 +1,6 @@
import qbit from '@/services/qbit'
export async function isAuthenticated(){
const res = await qbit.login()
return res === 'Ok.'
}

View file

@ -3,7 +3,7 @@ import Vuex from 'vuex'
import VuexPersist from 'vuex-persist'
import Torrent from '../models/torrent'
import Stat from '../models/sessionStat'
import Status from '../models/Status'
import qbit from '../services/qbit'
const vuexPersist = new VuexPersist({
@ -18,7 +18,7 @@ export default new Vuex.Store({
state: {
darkTheme: false,
intervals: [],
stats: null,
status: 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: [],
@ -43,7 +43,7 @@ export default new Vuex.Store({
getTheme: state => () => state.darkTheme,
getModalState: state => name => state.modals[name.toLowerCase()],
getSettings: state => () => state.settings,
getStats: state => () => state.stats
getStatus: state => () => state.status
},
mutations: {
@ -88,37 +88,37 @@ export default new Vuex.Store({
state.torrents.push(new Torrent({ hash: key, ...value }))
}
// stats
state.stats = new Stat(data.server_state)
// status
state.status = new Status(data.server_state)
// graph
state.download_data.splice(0, 1)
if (state.stats.dlspeed.indexOf('KB' > -1)) {
if (state.status.dlspeed.indexOf('KB' > -1)) {
state.download_data.push(
state.stats.dlspeed.substring(
state.status.dlspeed.substring(
0,
state.stats.dlspeed.indexOf(' ')
state.status.dlspeed.indexOf(' ')
) / 1000
)
} else {
state.download_data.push(
state.stats.dlspeed(0, state.stats.dlspeed.indexOf(' '))
state.status.dlspeed(0, state.status.dlspeed.indexOf(' '))
)
}
state.upload_data.splice(0, 1)
if (state.stats.upspeed.indexOf('KB' > -1)) {
if (state.status.upspeed.indexOf('KB' > -1)) {
state.upload_data.push(
state.stats.upspeed.substring(
state.status.upspeed.substring(
0,
state.stats.upspeed.indexOf(' ')
state.status.upspeed.indexOf(' ')
) / 1000
)
} else {
state.upload_data.push(
state.stats.upspeed.substring(
state.status.upspeed.substring(
0,
state.stats.upspeed.indexOf(' ')
state.status.upspeed.indexOf(' ')
)
)
}