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
![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",
"version": "0.5.4",
"version": "0.5.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -3634,6 +3634,12 @@
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -6352,6 +6358,12 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
@ -7595,6 +7607,15 @@
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@ -17366,9 +17387,9 @@
"integrity": "sha512-sT6tuVTLBwfH3TA7azecDNS/W70bmz14ZJI7aE7QIqcG9I6OywyH7x3hcOeY1v1DxttI8Svc5RuYj4Dd+A5F4g=="
},
"vue-cli-plugin-vuetify": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.9.tgz",
"integrity": "sha512-J4fzpz27OmCCAA3CI56ulYsUrZ859dQAh58Z9XZilY03kd/M+svLlPkK45cBIrGGfjSqQ40oyWezA3NiPBEG8g==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.1.0.tgz",
"integrity": "sha512-cvJR2+6U1PS4UUP7NnuylWfxM3LrzKnusOgrCZUyzr5abyDxf/t0TZy5EqfJwAa9/TsIO0W4gOoaoy/f4Yw0aQ==",
"dev": true,
"requires": {
"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": {
"version": "3.4.9",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
"integrity": "sha512-CGAKWN44RqXW06oC+u4mPgHLQQi2t6vLD/JbGRDAXm0YpMv0bgpKuU5bBd7AvMgfTz9kXVRIWKHqRwGEb8xFkA=="
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.1.tgz",
"integrity": "sha512-RRQNLT8Mzr8z7eL4p7BtKvRaTSGdCbTy2+Mm5HTJvLGYSSeG9gDzNasJPP/yOYKLy+/cLG/ftrqq5fvkFwBJEw=="
},
"vue-style-loader": {
"version": "4.1.2",
@ -17597,19 +17613,72 @@
"integrity": "sha512-i2/Df0U0sedlaCbft4NMbna7WXbTCBhKVYTMjBrLVzrYTTWqzSO7ZCxLuDRY7MjwQhn7AOec7ent9U/NyIICqA=="
},
"vuetify-loader": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.6.0.tgz",
"integrity": "sha512-1bx3YeZ712dT1+QMX+XSFlP0O5k5O5Ui9ysBBmUZ9bWkAEHWZJQI9soI+qG5qmeFxUC0L9QYMCIKP0hOL/pf3Q==",
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.7.1.tgz",
"integrity": "sha512-zRfgNxi/SeE8Nh4Vhw3aIJftYrcJWd3PqPn8+cB/F9CgBVhJo5qp2BuFL70k33G1kTaBvcjYgM+vZc9nvvU3xg==",
"dev": true,
"requires": {
"file-loader": "^4.0.0",
"loader-utils": "^1.2.0"
"decache": "^4.6.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": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz",
"integrity": "sha512-W74OO2vCJPs9/YjNjW8lLbj+jzT24waTo2KShI8jLvJW8OaIkgb3wuAMA7D+ZiUxDOx3ubwSZTaJBip9G8a3aQ=="
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
"integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw=="
},
"vuex-persist": {
"version": "3.1.3",

View file

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

View file

@ -11,16 +11,16 @@
{{ icon }}
</v-icon>
</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">
{{ value | getDataValue(2) }}
{{ value | getSpeedValue }}
</span>
</v-flex>
<v-flex
xs3
xs4
class="caption robot-mono text-right mt-1"
>
<span class="speedUnits" data-testid="SpeedCard-unit">
<span data-testid="SpeedCard-unit">
{{ value | getDataUnit(1) }}/s
</span>
</v-flex>
@ -31,6 +31,16 @@
<script>
export default {
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']
}
</script>
@ -40,8 +50,4 @@ export default {
padding: 32px 16px !important;
font-size: 1.05em;
}
.speedUnits {
font-size: .8em !important;
}
</style>

View file

@ -13,7 +13,7 @@
<v-flex md5 class="ml-4">
<span data-testid="StorageCard-Wrapper" :class="color + '--text title'">
<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 }}
</span>
</span>

View file

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

View file

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

View file

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

View file

@ -1,60 +1,5 @@
<template>
<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>
<template #activator="{ on }">
<v-btn
@ -74,24 +19,6 @@
</template>
<span> Add Torrent</span>
</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
@ -128,6 +55,43 @@
</template>
<span>Pause Selected Torrents</span>
</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>
<template #activator="{ on }">
<v-btn
@ -193,14 +157,6 @@ export default {
},
addModal(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
row
wrap
class="ma-0 pa-4 ml-0 "
class="ma-0 px-4 py-2 ml-0 "
:class="style"
>
<v-flex xs12>

View file

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

View file

@ -85,7 +85,7 @@ export function networkSize(size) {
Vue.filter('networkSize', networkSize)
function getDataUnit(a, b) {
if (a == 0) return 'B'
if (!a) return 'B'
const c = 1024
const e = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
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.seq_dl = data.seq_dl
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)
}

View file

@ -211,6 +211,18 @@ class Qbit {
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) {
return this.torrentAction('reannounce', hashes)
}
@ -269,10 +281,10 @@ class Qbit {
}
/** Torrent Priority **/
setTorrentPriority(hash, priority) {
setTorrentPriority(hashes, priority) {
if (['increasePrio', 'decreasePrio', 'topPrio', 'bottomPrio'].includes(priority)) {
return this.execute('post', `/torrents/${priority}`, {
hashes: hash
hashes: hashes.join('|')
})
}
}

View file

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

View file

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

View file

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

View file

@ -18,12 +18,26 @@ describe('SpeedCard.vue', () => {
expect(wrapper.find('[data-testid="SpeedCard-icon"]').exists()).toBe(true)
})
it('should render value and unit & be formatted', () => {
const wrapper = setup(SpeedCard, { value: 10000 })
it('should render with 0 as value', () => {
const wrapper = setup(SpeedCard)
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"]').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')
})
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')
})
})