This commit is contained in:
Daan Wijns 2021-01-29 11:42:20 +01:00 committed by GitHub
parent c4329ce2b9
commit 2e334ca909
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 507 additions and 185 deletions

View file

@ -6,7 +6,7 @@ The sleekest looking WebUI for qBittorrent made with Vue.js!
## Screenshots ## Screenshots
![Desktop screenshot](https://imgur.com/BgqO5Zp.png) ![Desktop screenshot](https://imgur.com/IUkaDnI.png)
| | | | | | | |
| :--------------------------------: | :--------------------------------: | :--------------------------------: | | :--------------------------------: | :--------------------------------: | :--------------------------------: |

109
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "vuetorrent", "name": "vuetorrent",
"version": "0.5.4", "version": "0.5.5",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -3634,6 +3634,12 @@
"jest-diff": "^24.3.0" "jest-diff": "^24.3.0"
} }
}, },
"@types/json-schema": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
"dev": true
},
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -6352,6 +6358,12 @@
"caller-callsite": "^2.0.0" "caller-callsite": "^2.0.0"
} }
}, },
"callsite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
"dev": true
},
"callsites": { "callsites": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
@ -7595,6 +7607,15 @@
"ms": "^2.1.1" "ms": "^2.1.1"
} }
}, },
"decache": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/decache/-/decache-4.6.0.tgz",
"integrity": "sha512-PppOuLiz+DFeaUvFXEYZjLxAkKiMYH/do/b/MxpDe/8AgKBi5GhZxridoVIbBq72GDbL36e4p0Ce2jTGUwwU+w==",
"dev": true,
"requires": {
"callsite": "^1.0.0"
}
},
"decamelize": { "decamelize": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@ -17366,9 +17387,9 @@
"integrity": "sha512-sT6tuVTLBwfH3TA7azecDNS/W70bmz14ZJI7aE7QIqcG9I6OywyH7x3hcOeY1v1DxttI8Svc5RuYj4Dd+A5F4g==" "integrity": "sha512-sT6tuVTLBwfH3TA7azecDNS/W70bmz14ZJI7aE7QIqcG9I6OywyH7x3hcOeY1v1DxttI8Svc5RuYj4Dd+A5F4g=="
}, },
"vue-cli-plugin-vuetify": { "vue-cli-plugin-vuetify": {
"version": "2.0.9", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.9.tgz", "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.1.0.tgz",
"integrity": "sha512-J4fzpz27OmCCAA3CI56ulYsUrZ859dQAh58Z9XZilY03kd/M+svLlPkK45cBIrGGfjSqQ40oyWezA3NiPBEG8g==", "integrity": "sha512-cvJR2+6U1PS4UUP7NnuylWfxM3LrzKnusOgrCZUyzr5abyDxf/t0TZy5EqfJwAa9/TsIO0W4gOoaoy/f4Yw0aQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"null-loader": "^3.0.0", "null-loader": "^3.0.0",
@ -17524,15 +17545,10 @@
} }
} }
}, },
"vue-observe-visibility": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz",
"integrity": "sha512-xo0CEVdkjSjhJoDdLSvoZoQrw/H2BlzB5jrCBKGZNXN2zdZgMuZ9BKrxXDjNP2AxlcCoKc8OahI3F3r3JGLv2Q=="
},
"vue-router": { "vue-router": {
"version": "3.4.9", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.1.tgz",
"integrity": "sha512-CGAKWN44RqXW06oC+u4mPgHLQQi2t6vLD/JbGRDAXm0YpMv0bgpKuU5bBd7AvMgfTz9kXVRIWKHqRwGEb8xFkA==" "integrity": "sha512-RRQNLT8Mzr8z7eL4p7BtKvRaTSGdCbTy2+Mm5HTJvLGYSSeG9gDzNasJPP/yOYKLy+/cLG/ftrqq5fvkFwBJEw=="
}, },
"vue-style-loader": { "vue-style-loader": {
"version": "4.1.2", "version": "4.1.2",
@ -17597,19 +17613,72 @@
"integrity": "sha512-i2/Df0U0sedlaCbft4NMbna7WXbTCBhKVYTMjBrLVzrYTTWqzSO7ZCxLuDRY7MjwQhn7AOec7ent9U/NyIICqA==" "integrity": "sha512-i2/Df0U0sedlaCbft4NMbna7WXbTCBhKVYTMjBrLVzrYTTWqzSO7ZCxLuDRY7MjwQhn7AOec7ent9U/NyIICqA=="
}, },
"vuetify-loader": { "vuetify-loader": {
"version": "1.6.0", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.6.0.tgz", "resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.7.1.tgz",
"integrity": "sha512-1bx3YeZ712dT1+QMX+XSFlP0O5k5O5Ui9ysBBmUZ9bWkAEHWZJQI9soI+qG5qmeFxUC0L9QYMCIKP0hOL/pf3Q==", "integrity": "sha512-zRfgNxi/SeE8Nh4Vhw3aIJftYrcJWd3PqPn8+cB/F9CgBVhJo5qp2BuFL70k33G1kTaBvcjYgM+vZc9nvvU3xg==",
"dev": true, "dev": true,
"requires": { "requires": {
"file-loader": "^4.0.0", "decache": "^4.6.0",
"loader-utils": "^1.2.0" "file-loader": "^6.2.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true
},
"file-loader": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
"dev": true,
"requires": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
}
} }
}, },
"vuex": { "vuex": {
"version": "3.6.0", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
"integrity": "sha512-W74OO2vCJPs9/YjNjW8lLbj+jzT24waTo2KShI8jLvJW8OaIkgb3wuAMA7D+ZiUxDOx3ubwSZTaJBip9G8a3aQ==" "integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw=="
}, },
"vuex-persist": { "vuex-persist": {
"version": "3.1.3", "version": "3.1.3",

View file

@ -1,6 +1,6 @@
{ {
"name": "vuetorrent", "name": "vuetorrent",
"version": "0.5.4", "version": "0.5.5",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@ -24,12 +24,12 @@
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-apexcharts": "^1.6.0", "vue-apexcharts": "^1.6.0",
"vue-context": "^5.2.0", "vue-context": "^5.2.0",
"vue-router": "^3.4.9", "vue-router": "^3.5.1",
"vue-toastification": "^1.7.11", "vue-toastification": "^1.7.11",
"vue2-perfect-scrollbar": "^1.5.0", "vue2-perfect-scrollbar": "^1.5.0",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuetify": "^2.4.3", "vuetify": "^2.4.3",
"vuex": "^3.6.0", "vuex": "^3.6.2",
"vuex-persist": "^3.1.3" "vuex-persist": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
@ -51,9 +51,9 @@
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"sass": "^1.32.5", "sass": "^1.32.5",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"vue-cli-plugin-vuetify": "^2.0.9", "vue-cli-plugin-vuetify": "^2.1.0",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",
"vuetify-loader": "^1.6.0" "vuetify-loader": "^1.7.1"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View file

@ -11,16 +11,16 @@
{{ icon }} {{ icon }}
</v-icon> </v-icon>
</v-flex> </v-flex>
<v-flex xs7 class="text-center font-weight-bold robot-mono"> <v-flex xs6 class="text-center font-weight-bold robot-mono">
<span data-testid="SpeedCard-value"> <span data-testid="SpeedCard-value">
{{ value | getDataValue(2) }} {{ value | getSpeedValue }}
</span> </span>
</v-flex> </v-flex>
<v-flex <v-flex
xs3 xs4
class="caption robot-mono text-right mt-1" class="caption robot-mono text-right mt-1"
> >
<span class="speedUnits" data-testid="SpeedCard-unit"> <span data-testid="SpeedCard-unit">
{{ value | getDataUnit(1) }}/s {{ value | getDataUnit(1) }}/s
</span> </span>
</v-flex> </v-flex>
@ -31,6 +31,16 @@
<script> <script>
export default { export default {
name: 'SpeedCard', name: 'SpeedCard',
filters: {
getSpeedValue(value) {
if (!value) return '0'
const c = 1024
const d = value > 1048576 ? 2 : 0 // 2 decimals when MB
const f = Math.floor(Math.log(value) / Math.log(c))
return `${parseFloat((value / Math.pow(c, f)).toFixed(d))}`
}
},
props: ['color', 'icon', 'value'] props: ['color', 'icon', 'value']
} }
</script> </script>
@ -40,8 +50,4 @@ export default {
padding: 32px 16px !important; padding: 32px 16px !important;
font-size: 1.05em; font-size: 1.05em;
} }
.speedUnits {
font-size: .8em !important;
}
</style> </style>

View file

@ -13,7 +13,7 @@
<v-flex md5 class="ml-4"> <v-flex md5 class="ml-4">
<span data-testid="StorageCard-Wrapper" :class="color + '--text title'"> <span data-testid="StorageCard-Wrapper" :class="color + '--text title'">
<span data-testid="StorageCard-value"> {{ value | getDataValue(2) }} </span> <span data-testid="StorageCard-value"> {{ value | getDataValue(2) }} </span>
<span data-testid="StorageCard-unit" class="font-weight-light caption"> <span data-testid="StorageCard-unit" class="caption">
{{ value | getDataUnit }} {{ value | getDataUnit }}
</span> </span>
</span> </span>

View file

@ -8,7 +8,7 @@
<v-card style="overflow: hidden !important"> <v-card style="overflow: hidden !important">
<v-container :style="{ height: phoneLayout ? '100vh' : '' }"> <v-container :style="{ height: phoneLayout ? '100vh' : '' }">
<v-card-title class="pb-0 justify-center"> <v-card-title class="pb-0 justify-center">
<h2>Change Torrent Location</h2> <h2>Change Location</h2>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<div> <div>
@ -18,13 +18,13 @@
<v-text-field <v-text-field
v-model="torrent.name" v-model="torrent.name"
label="Torrent Name" label="Torrent Name"
prepend-icon="insert_drive_file" :prepend-icon="mdiFile"
readonly readonly
/> />
<v-text-field <v-text-field
v-model="newPath" v-model="newPath"
label="Directory" label="Directory"
prepend-icon="folder" :prepend-icon="mdiFolder"
@keydown.enter="setLocation" @keydown.enter="setLocation"
/> />
</v-col> </v-col>
@ -49,7 +49,7 @@
right right
@click="close" @click="close"
> >
<v-icon>close</v-icon> <v-icon>{{ mdiClose }}</v-icon>
</v-btn> </v-btn>
</v-fab-transition> </v-fab-transition>
</v-card> </v-card>
@ -58,9 +58,10 @@
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { mdiFile, mdiFolder, mdiClose } from '@mdi/js'
import { Modal, FullScreenModal } from '@/mixins' import { Modal, FullScreenModal } from '@/mixins'
import qbit from '@/services/qbit' import qbit from '@/services/qbit'
export default { export default {
name: 'ChangeLocationModal', name: 'ChangeLocationModal',
mixins: [Modal, FullScreenModal], mixins: [Modal, FullScreenModal],
@ -69,7 +70,8 @@ export default {
}, },
data() { data() {
return { return {
newPath: '' newPath: '',
mdiFile, mdiFolder, mdiClose
} }
}, },
computed: { computed: {

View file

@ -12,7 +12,10 @@
:key="t.hash" :key="t.hash"
> >
<v-list-item-content> <v-list-item-content>
<v-list-item-title class="truncate" v-text="t.name" /> <v-list-item-title
class="text-wrap"
v-text="t.name"
/>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -20,21 +23,21 @@
<v-card-actions class="justify-center pb-5"> <v-card-actions class="justify-center pb-5">
<v-btn <v-btn
text text
class="error white--text mt-3" class="accent white--text mt-3"
@click="close()" @click="close()"
> >
Cancel Cancel
</v-btn> </v-btn>
<v-btn <v-btn
text text
class="accent white--text mt-3" class="error white--text mt-3"
@click="deleteWithoutFiles()" @click="deleteWithoutFiles()"
> >
Delete Delete
</v-btn> </v-btn>
<v-btn <v-btn
text text
class="accent white--text mt-3" class="error white--text mt-3"
@click="deleteWithFiles()" @click="deleteWithFiles()"
> >
Delete with files Delete with files

View file

@ -8,7 +8,7 @@
<v-card style="overflow: hidden !important"> <v-card style="overflow: hidden !important">
<v-container :style="{ height: phoneLayout ? '100vh' : '' }"> <v-container :style="{ height: phoneLayout ? '100vh' : '' }">
<v-card-title class="pb-0 justify-center"> <v-card-title class="pb-0 justify-center">
<h2>Rename Torrent</h2> <h2>Rename</h2>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<div> <div>
@ -18,7 +18,7 @@
<v-text-field <v-text-field
v-model="name" v-model="name"
label="Torrent Name" label="Torrent Name"
prepend-icon="insert_drive_file" :prepend-icon="mdiFile"
/> />
</v-col> </v-col>
</v-row> </v-row>
@ -42,7 +42,7 @@
right right
@click="close" @click="close"
> >
<v-icon>close</v-icon> <v-icon>{{ mdiClose }}</v-icon>
</v-btn> </v-btn>
</v-fab-transition> </v-fab-transition>
</v-card> </v-card>
@ -51,6 +51,7 @@
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { mdiFile, mdiClose } from '@mdi/js'
import { Modal, FullScreenModal } from '@/mixins' import { Modal, FullScreenModal } from '@/mixins'
import qbit from '@/services/qbit' import qbit from '@/services/qbit'
export default { export default {
@ -61,7 +62,8 @@ export default {
}, },
data() { data() {
return { return {
name: '' name: '',
mdiFile, mdiClose
} }
}, },
computed: { computed: {

View file

@ -0,0 +1,112 @@
<template>
<v-dialog
v-model="dialog"
scrollable
max-width="500px"
:fullscreen="phoneLayout"
>
<v-card style="overflow: hidden !important">
<v-container :style="{ height: phoneLayout ? '100vh' : '' }">
<v-card-title class="pb-0 justify-center">
<h2 class="text-capitalize">
Limit {{ mode }}
</h2>
</v-card-title>
<v-card-text>
<div>
<v-container>
<v-row>
<v-col>
<v-text-field
v-model="limit"
label="Speed Limit"
:prepend-icon="mdiSpeedometer"
suffix="KB/s"
clearable
/>
</v-col>
</v-row>
</v-container>
</div>
</v-card-text>
<div>
<v-card-actions class="justify-center">
<v-btn color="success" @click="setLimit">
Save
</v-btn>
</v-card-actions>
</div>
</v-container>
<v-fab-transition v-if="phoneLayout">
<v-btn
color="red"
dark
absolute
bottom
right
@click="close"
>
<v-icon>{{ mdiClose }}</v-icon>
</v-btn>
</v-fab-transition>
</v-card>
</v-dialog>
</template>
<script>
import { mapGetters } from 'vuex'
import { mdiSpeedometer, mdiClose } from '@mdi/js'
import { Modal, FullScreenModal } from '@/mixins'
import qbit from '@/services/qbit'
export default {
name: 'SpeedLimitModal',
mixins: [Modal, FullScreenModal],
props: {
mode: String,
hash: String
},
data() {
return {
limit: '',
mdiSpeedometer, mdiClose
}
},
computed: {
...mapGetters(['getTorrent']),
torrent() {
return this.getTorrent(this.hash)
}
},
created() {
switch (this.mode) {
case 'download':
this.limit = this.torrent.dl_limit / 1024
break
case 'upload':
this.limit = this.torrent.up_limit / 1024
break
default:
break
}
},
methods: {
setLimit() {
switch (this.mode) {
case 'download':
qbit.setDownloadLimit([this.hash], this.limit * 1024 ?? -1)
break
case 'upload':
qbit.setUploadLimit([this.hash], this.limit * 1024 ?? -1)
break
default:
break
}
this.close()
},
close() {
this.$store.commit('DELETE_MODAL', this.guid)
}
}
}
</script>

View file

@ -170,6 +170,30 @@
{{ torrent.auto_tmm }} {{ torrent.auto_tmm }}
</td> </td>
</tr> </tr>
<tr>
<td class="grey--text">
Share Limit
</td>
<td>
{{ torrent.ratio_limit }}
</td>
</tr>
<tr>
<td class="grey--text">
Download Limit
</td>
<td>
{{ torrent.dl_limit | getDataValue }} {{ torrent.dl_limit | getDataUnit }}/s
</td>
</tr>
<tr>
<td class="grey--text">
Upload Limit
</td>
<td>
{{ torrent.up_limit | getDataValue }} {{ torrent.up_limit | getDataUnit }}/s
</td>
</tr>
</tbody> </tbody>
</v-simple-table> </v-simple-table>
</v-card-text> </v-card-text>

View file

@ -1,60 +1,5 @@
<template> <template>
<div :class="mobile ? '' : 'flex-shrink-0 ml-0'"> <div :class="mobile ? '' : 'flex-shrink-0 ml-0'">
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
:text="!mobile"
small
fab
class="mr-0 ml-0"
aria-label="Select Mode"
v-on="on"
@click="toggleSelectMode()"
>
<v-icon color="grey">
{{ $store.state.selectMode ? mdiCheckboxMarked : mdiCheckboxBlankOutline }}
</v-icon>
</v-btn>
</template>
<span>Select Mode</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
:text="!mobile"
small
fab
class="mr-0 ml-0"
aria-label="Sort Torrents"
v-on="on"
@click="addModal('SortModal')"
>
<v-icon color="grey">
{{ mdiSort }}
</v-icon>
</v-btn>
</template>
<span>Sort Torrents</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
:text="!mobile"
small
fab
color="grey--text"
class="mr-0 ml-0"
aria-label="Search New Torrent"
v-on="on"
@click="addModal('SearchModal')"
>
<v-icon color="grey">
{{ mdiSearchWeb }}
</v-icon>
</v-btn>
</template>
<span>Search new Torrent</span>
</v-tooltip>
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-btn <v-btn
@ -74,24 +19,6 @@
</template> </template>
<span> Add Torrent</span> <span> Add Torrent</span>
</v-tooltip> </v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
small
fab
:text="!mobile"
class="mr-0 ml-0"
aria-label="Remove Selected Torrents"
v-on="on"
@click="removeTorrents"
>
<v-icon color="grey">
{{ mdiDelete }}
</v-icon>
</v-btn>
</template>
<span>Remove Selected Torrents</span>
</v-tooltip>
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-btn <v-btn
@ -128,6 +55,43 @@
</template> </template>
<span>Pause Selected Torrents</span> <span>Pause Selected Torrents</span>
</v-tooltip> </v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
small
fab
:text="!mobile"
class="mr-0 ml-0"
aria-label="Remove Selected Torrents"
v-on="on"
@click="removeTorrents"
>
<v-icon color="grey">
{{ mdiDelete }}
</v-icon>
</v-btn>
</template>
<span>Remove Selected Torrents</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
:text="!mobile"
small
fab
color="grey--text"
class="mr-0 ml-0"
aria-label="Search New Torrent"
v-on="on"
@click="addModal('SearchModal')"
>
<v-icon color="grey">
{{ mdiSearchWeb }}
</v-icon>
</v-btn>
</template>
<span>Search new Torrent</span>
</v-tooltip>
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-btn <v-btn
@ -193,14 +157,6 @@ export default {
}, },
addModal(name) { addModal(name) {
this.createModal(name) this.createModal(name)
},
toggleSelectMode() {
if (this.$store.state.selectMode) {
this.$store.state.selected_torrents = []
return this.$store.state.selectMode = false
}
this.$store.state.selectMode = true
} }
} }
} }

View file

@ -2,7 +2,7 @@
<v-layout <v-layout
row row
wrap wrap
class="ma-0 pa-4 ml-0 " class="ma-0 px-4 py-2 ml-0 "
:class="style" :class="style"
> >
<v-flex xs12> <v-flex xs12>

View file

@ -193,13 +193,42 @@
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
<v-menu
v-if="!multiple"
open-on-hover
top
>
<template #activator="{ on }">
<v-list-item link v-on="on">
<v-icon>{{ mdiSpeedometerSlow }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Set Limit
<v-icon>{{ mdiChevronRight }}</v-icon>
</v-list-item-title>
</v-list-item>
</template>
<v-list dense rounded>
<v-list-item @click="setLimit('download')">
<v-icon>{{ mdiChevronDown }}</v-icon>
<v-list-item-title class="ml-2">
Download
</v-list-item-title>
</v-list-item>
<v-list-item @click="setLimit('upload')">
<v-icon>{{ mdiChevronUp }}</v-icon>
<v-list-item-title class="ml-2">
Upload
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-divider v-if="!multiple" /> <v-divider v-if="!multiple" />
<v-list-item v-if="!multiple" link @click="showInfo"> <v-list-item v-if="!multiple" link @click="showInfo">
<v-icon>{{ mdiInformation }}</v-icon> <v-icon>{{ mdiInformation }}</v-icon>
<v-list-item-title <v-list-item-title class="ml-2">
class="ml-2"
style="font-size: 1em"
>
Show Info Show Info
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
@ -222,7 +251,8 @@ import {
mdiBullhorn, mdiPlaylistCheck, mdiArrowUp, mdiArrowDown, mdiPriorityLow, mdiBullhorn, mdiPlaylistCheck, mdiArrowUp, mdiArrowDown, mdiPriorityLow,
mdiInformation, mdiDeleteForever, mdiRenameBox, mdiFolder, mdiDelete, mdiInformation, mdiDeleteForever, mdiRenameBox, mdiFolder, mdiDelete,
mdiPlay, mdiPause, mdiSelect, mdiPriorityHigh, mdiChevronRight, mdiPlay, mdiPause, mdiSelect, mdiPriorityHigh, mdiChevronRight,
mdiFastForward, mdiShape, mdiHeadCog, mdiCheckboxMarked, mdiCheckboxBlankOutline mdiFastForward, mdiShape, mdiHeadCog, mdiCheckboxMarked, mdiCheckboxBlankOutline,
mdiSpeedometerSlow, mdiChevronUp, mdiChevronDown
} from '@mdi/js' } from '@mdi/js'
export default { export default {
@ -241,7 +271,8 @@ export default {
mdiDelete, mdiPlay, mdiPause, mdiSelect, mdiFastForward, mdiDelete, mdiPlay, mdiPause, mdiSelect, mdiFastForward,
mdiFolder, mdiRenameBox, mdiDeleteForever, mdiInformation, mdiFolder, mdiRenameBox, mdiDeleteForever, mdiInformation,
mdiPlaylistCheck, mdiPriorityHigh, mdiBullhorn, mdiChevronRight, mdiPlaylistCheck, mdiPriorityHigh, mdiBullhorn, mdiChevronRight,
mdiShape, mdiHeadCog, mdiCheckboxMarked, mdiCheckboxBlankOutline mdiShape, mdiHeadCog, mdiCheckboxMarked, mdiCheckboxBlankOutline,
mdiSpeedometerSlow, mdiChevronUp, mdiChevronDown
}), }),
computed: { computed: {
...mapGetters(['getCategories']), ...mapGetters(['getCategories']),
@ -293,7 +324,10 @@ export default {
this.createModal('TorrentDetailModal', { hash: this.torrent.hash }) this.createModal('TorrentDetailModal', { hash: this.torrent.hash })
}, },
setPriority(priority) { setPriority(priority) {
qbit.setTorrentPriority(this.hash, priority) qbit.setTorrentPriority(this.hashes, priority)
},
setLimit(mode) {
this.createModal('SpeedLimitModal', { hash: this.torrent.hash, mode })
}, },
forceResume() { forceResume() {
qbit.forceStartTorrents(this.hashes) qbit.forceStartTorrents(this.hashes)

View file

@ -85,7 +85,7 @@ export function networkSize(size) {
Vue.filter('networkSize', networkSize) Vue.filter('networkSize', networkSize)
function getDataUnit(a, b) { function getDataUnit(a, b) {
if (a == 0) return 'B' if (!a) return 'B'
const c = 1024 const c = 1024
const e = ['B', '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)) const f = Math.floor(Math.log(a) / Math.log(c))

View file

@ -26,6 +26,9 @@ export default class Torrent {
this.f_l_piece_prio = data.f_l_piece_prio this.f_l_piece_prio = data.f_l_piece_prio
this.seq_dl = data.seq_dl this.seq_dl = data.seq_dl
this.auto_tmm = data.auto_tmm this.auto_tmm = data.auto_tmm
this.dl_limit = data.dl_limit
this.up_limit = data.up_limit
this.ratio_limit = data.ratio_limit
Object.freeze(this) Object.freeze(this)
} }

View file

@ -211,6 +211,18 @@ class Qbit {
return this.torrentAction('setAutoManagement', hashes, { enable }) return this.torrentAction('setAutoManagement', hashes, { enable })
} }
setDownloadLimit(hashes, limit) {
return this.torrentAction('setDownloadLimit', hashes, { limit })
}
setUploadLimit(hashes, limit) {
return this.torrentAction('setUploadLimit', hashes, { limit })
}
setShareLimit(hashes, ratioLimit, seedingTimeLimit) {
return this.torrentAction('setShareLimits', hashes, { ratioLimit, seedingTimeLimit })
}
reannounceTorrents(hashes) { reannounceTorrents(hashes) {
return this.torrentAction('reannounce', hashes) return this.torrentAction('reannounce', hashes)
} }
@ -269,10 +281,10 @@ class Qbit {
} }
/** Torrent Priority **/ /** Torrent Priority **/
setTorrentPriority(hash, priority) { setTorrentPriority(hashes, priority) {
if (['increasePrio', 'decreasePrio', 'topPrio', 'bottomPrio'].includes(priority)) { if (['increasePrio', 'decreasePrio', 'topPrio', 'bottomPrio'].includes(priority)) {
return this.execute('post', `/torrents/${priority}`, { return this.execute('post', `/torrents/${priority}`, {
hashes: hash hashes: hashes.join('|')
}) })
} }
} }

View file

@ -95,14 +95,17 @@ export default {
UPDATE_SORT_OPTIONS: (state, { UPDATE_SORT_OPTIONS: (state, {
reverse = false, reverse = false,
hashes = [], hashes = [],
filter = null, category = null, filter = null,
tracker = null category = null,
tracker = null,
sort = null
}) => { }) => {
state.sort_options.reverse = reverse state.sort_options.reverse = reverse
state.sort_options.hashes = hashes state.sort_options.hashes = hashes
state.sort_options.filter = filter state.sort_options.filter = filter
state.sort_options.category = category state.sort_options.category = category
state.sort_options.tracker = tracker state.sort_options.tracker = tracker
state.sort_options.sort = sort
}, },
FETCH_CATEGORIES: async state => state.categories = Object.values(await (qbit.getCategories())), FETCH_CATEGORIES: async state => state.categories = Object.values(await (qbit.getCategories())),
FETCH_SEARCH_PLUGINS: async state => state.searchPlugins = await qbit.getSearchPlugins(), FETCH_SEARCH_PLUGINS: async state => state.searchPlugins = await qbit.getSearchPlugins(),

View file

@ -7,7 +7,7 @@ $upload: #00b3fa;
$torrent-done: #3cd1c2; $torrent-done: #3cd1c2;
$torrent-downloading: #5bb974; $torrent-downloading: #5bb974;
$torrent-fail: #f83e70; $torrent-fail: #f83e70;
$torrent-paused: #cfd8dc; $torrent-paused: #9CA3AF;
$torrent-queued: #2e5eaa; $torrent-queued: #2e5eaa;
$torrent-seeding: #4ecde6; $torrent-seeding: #4ecde6;
$torrent-checking: #ff7043; $torrent-checking: #ff7043;

View file

@ -1,36 +1,105 @@
<template> <template>
<div class="px-1 px-sm-5 pt-4 background" @click.self="resetSelected"> <div class="px-1 px-sm-5 pt-4 background" @click.self="resetSelected">
<v-row no-gutters class="grey--text"> <v-row
no-gutters
class="grey--text"
align="center"
justify="center"
>
<v-col> <v-col>
<h1 style="font-size: 1.6em !important" class="subtitle-1 ml-2"> <h1 style="font-size: 1.6em !important" class="subtitle-1 ml-2">
Dashboard Dashboard
</h1> </h1>
</v-col> </v-col>
<v-col> <v-col class="align-center justify-center">
<p style="float: right; font-size: 0.8em" class="mr-2 text-uppercase"> <span style="float: right; font-size: 0.8em" class="mr-2 text-uppercase">
{{ torrentCountString }} {{ torrentCountString }}
</p> </span>
</v-col> </v-col>
</v-row> </v-row>
<div class="my-2 px-2" @click.self="resetSelected"> <div class="my-2 px-2" @click.self="resetSelected">
<v-flex <v-row class="my-2 mx-1" @click.self="resetSelected">
xs12 <v-expand-x-transition>
sm6 <v-card
md3 v-show="searchFilterEnabled"
@click.self="resetSelected" id="searchFilter"
> flat
<v-text-field xs7
v-model="input" md3
flat class="ma-0 pa-0 mt-1 transparent"
label="Filter" >
outlined <v-text-field
clearable v-model="input"
solo flat
:append-outer-icon="mdiFilter" label="Search"
@click:clear="resetInput()" dense
/> outlined
</v-flex> clearable
solo
height="50px"
width="100px"
@click:clear="resetInput()"
/>
</v-card>
</v-expand-x-transition>
<v-row style="margin-top: 10px" class="mb-1 mx-1">
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
text
small
fab
class="mr-0 ml-0"
aria-label="Select Mode"
v-on="on"
@click="searchFilterEnabled = !searchFilterEnabled"
>
<v-icon color="grey">
{{ mdiFilter }}
</v-icon>
</v-btn>
</template>
<span>Toggle Search Filter</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
text
small
fab
class="mr-0 ml-0"
aria-label="Select Mode"
v-on="on"
@click="toggleSelectMode()"
>
<v-icon color="grey">
{{ $store.state.selectMode ? mdiCheckboxMarked : mdiCheckboxBlankOutline }}
</v-icon>
</v-btn>
</template>
<span>Select Mode</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
text
small
fab
class="mr-0 ml-0"
aria-label="Sort Torrents"
v-on="on"
@click="addModal('SortModal')"
>
<v-icon color="grey">
{{ mdiSort }}
</v-icon>
</v-btn>
</template>
<span>Sort Torrents</span>
</v-tooltip>
</v-row>
</v-row>
<div v-if="torrents.length === 0" class="mt-5 text-xs-center"> <div v-if="torrents.length === 0" class="mt-5 text-xs-center">
<p class="grey--text"> <p class="grey--text">
@ -42,17 +111,22 @@
<v-list-item <v-list-item
v-for="(torrent, index) in paginatedData" v-for="(torrent, index) in paginatedData"
:key="torrent.hash" :key="torrent.hash"
class="pa-0 mb-1" class="pa-0"
:class="isMobile ? 'mb-1' : 'mb-2'"
@contextmenu.prevent="$refs.menu.open($event, { torrent })" @contextmenu.prevent="$refs.menu.open($event, { torrent })"
> >
<template #default> <template #default>
<v-list-item-action v-if="selectMode"> <v-expand-x-transition>
<v-checkbox <v-card v-show="selectMode" flat class="transparent">
color="grey" <v-list-item-action>
:input-value="selected_torrents.indexOf(torrent.hash) !== -1" <v-checkbox
@click="selectTorrent(torrent.hash)" color="grey"
/> :input-value="selected_torrents.indexOf(torrent.hash) !== -1"
</v-list-item-action> @click="selectTorrent(torrent.hash)"
/>
</v-list-item-action>
</v-card>
</v-expand-x-transition>
<v-list-item-content class="pa-0"> <v-list-item-content class="pa-0">
<Torrent :torrent="torrent" /> <Torrent :torrent="torrent" />
<v-divider <v-divider
@ -86,7 +160,7 @@
<script> <script>
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { mdiFilter } from '@mdi/js' import { mdiFilter, mdiCheckboxMarked, mdiCheckboxBlankOutline, mdiSort } from '@mdi/js'
import { VueContext } from 'vue-context' import { VueContext } from 'vue-context'
import 'vue-context/src/sass/vue-context.scss' import 'vue-context/src/sass/vue-context.scss'
@ -103,8 +177,9 @@ export default {
data() { data() {
return { return {
input: '', input: '',
searchFilterEnabled: false,
pageNumber: 1, pageNumber: 1,
mdiFilter mdiFilter, mdiCheckboxBlankOutline, mdiCheckboxMarked, mdiSort
} }
}, },
computed: { computed: {
@ -176,6 +251,17 @@ export default {
toTop() { toTop() {
this.$vuetify.goTo(0) this.$vuetify.goTo(0)
}, },
toggleSelectMode() {
if (this.$store.state.selectMode) {
this.$store.state.selected_torrents = []
return this.$store.state.selectMode = false
}
this.$store.state.selectMode = true
},
addModal(name) {
this.createModal(name)
},
handleKeyboardShortcut(e) { handleKeyboardShortcut(e) {
// 'ctrl + A' => select torrents // 'ctrl + A' => select torrents
if (e.keyCode === 65 && e.ctrlKey) { if (e.keyCode === 65 && e.ctrlKey) {
@ -204,13 +290,9 @@ export default {
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss">
.v-context { #searchFilter .v-text-field__details {
&, display: none;
& ul {
border-radius: 0.3rem;
padding: 0;
}
} }
</style> </style>

View file

@ -18,12 +18,26 @@ describe('SpeedCard.vue', () => {
expect(wrapper.find('[data-testid="SpeedCard-icon"]').exists()).toBe(true) expect(wrapper.find('[data-testid="SpeedCard-icon"]').exists()).toBe(true)
}) })
it('should render value and unit & be formatted', () => { it('should render with 0 as value', () => {
const wrapper = setup(SpeedCard, { value: 10000 }) const wrapper = setup(SpeedCard)
expect(wrapper.find('[data-testid="SpeedCard-value"]').exists()).toBe(true) expect(wrapper.find('[data-testid="SpeedCard-value"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="SpeedCard-value"]').text()).toBe('9.77') expect(wrapper.find('[data-testid="SpeedCard-value"]').text()).toBe('0')
expect(wrapper.find('[data-testid="SpeedCard-unit"]').exists()).toBe(true) expect(wrapper.find('[data-testid="SpeedCard-unit"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="SpeedCard-unit"]').text()).toBe('B/s')
})
it('should render value (0 decimals) and unit & be formatted', () => {
const wrapper = setup(SpeedCard, { value: 26823 })
expect(wrapper.find('[data-testid="SpeedCard-value"]').text()).toBe('26')
expect(wrapper.find('[data-testid="SpeedCard-unit"]').text()).toBe('KB/s') expect(wrapper.find('[data-testid="SpeedCard-unit"]').text()).toBe('KB/s')
}) })
it('should render value (2 decimals) and unit & be formatted', () => {
const wrapper = setup(SpeedCard, { value: 10899700 })
expect(wrapper.find('[data-testid="SpeedCard-value"]').text()).toBe('10.39')
expect(wrapper.find('[data-testid="SpeedCard-unit"]').text()).toBe('MB/s')
})
}) })