This commit is contained in:
Daan Wijns 2021-01-27 13:24:23 +01:00 committed by GitHub
parent 8a8b7e9d62
commit c4329ce2b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 6331 additions and 782 deletions

View file

@ -69,5 +69,16 @@ module.exports = {
'arrow-spacing': ['error', { before: true, after: true }],
'no-multi-spaces': ['error'],
'newline-before-return': ['error']
}
}
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}

25
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Test Core Components
on:
push
jobs:
test-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install & test
uses: actions/setup-node@master
- run: npm ci
- run: npm run test:unit

5
babel.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

8
jest.config.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
moduleFileExtensions: ['js', 'json', 'vue'],
transform: {
'.*\\.(vue)$': 'vue-jest',
'^.+\\.js$': '<rootDir>/node_modules/babel-jest'
}
}

6322
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,61 +1,64 @@
{
"name": "vuetorrent",
"version": "0.5.3",
"private": true,
"scripts": {
"start": "npm run serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"apexcharts": "^3.23.1",
"axios": "^0.21.1",
"core-js": "^3.8.3",
"dayjs": "^1.10.4",
"fuse.js": "^6.4.6",
"lodash": "^4.17.20",
"register-service-worker": "^1.7.2",
"typeface-roboto": "^1.1.13",
"typeface-roboto-mono": "^1.1.13",
"uuid": "^8.3.2",
"vue": "^2.6.12",
"vue-apexcharts": "^1.6.0",
"vue-context": "^5.2.0",
"vue-observe-visibility": "^0.4.6",
"vue-router": "^3.4.9",
"vue-toastification": "^1.7.11",
"vue2-perfect-scrollbar": "^1.5.0",
"vuedraggable": "^2.24.3",
"vuetify": "^2.4.3",
"vuex": "^3.6.0",
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"@mdi/js": "^5.9.55",
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-eslint": "~4.3.0",
"@vue/cli-plugin-pwa": "^4.5.11",
"@vue/cli-service": "~4.3.0",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.5.0",
"fibers": "^5.0.0",
"node-sass": "^4.14.1",
"sass": "^1.32.5",
"sass-loader": "^8.0.2",
"vue-cli-plugin-vuetify": "^2.0.9",
"vue-template-compiler": "^2.6.12",
"vuetify-loader": "^1.6.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie <= 10"
]
"name": "vuetorrent",
"version": "0.5.4",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit --setupTestFrameworkScriptFile=./tests/setup.js",
"lint": "vue-cli-service lint",
"start": "npm run serve"
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"apexcharts": "^3.23.1",
"axios": "^0.21.1",
"core-js": "^3.8.3",
"dayjs": "^1.10.4",
"fuse.js": "^6.4.6",
"lodash": "^4.17.20",
"register-service-worker": "^1.7.2",
"typeface-roboto": "^1.1.13",
"typeface-roboto-mono": "^1.1.13",
"uuid": "^8.3.2",
"vue": "^2.6.12",
"vue-apexcharts": "^1.6.0",
"vue-context": "^5.2.0",
"vue-router": "^3.4.9",
"vue-toastification": "^1.7.11",
"vue2-perfect-scrollbar": "^1.5.0",
"vuedraggable": "^2.24.3",
"vuetify": "^2.4.3",
"vuex": "^3.6.0",
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"@mdi/js": "^5.9.55",
"@vue/babel-preset-app": "^4.5.11",
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-eslint": "~4.3.0",
"@vue/cli-plugin-pwa": "^4.5.11",
"@vue/cli-plugin-unit-jest": "^4.5.11",
"@vue/cli-service": "~4.3.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "^1.0.3",
"babel-eslint": "^10.1.0",
"eslint": "^7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.5.0",
"fibers": "^5.0.0",
"node-sass": "^4.14.1",
"sass": "^1.32.5",
"sass-loader": "^8.0.2",
"vue-cli-plugin-vuetify": "^2.0.9",
"vue-template-compiler": "^2.6.12",
"vuetify-loader": "^1.6.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie <= 10"
]
}

View file

@ -41,6 +41,7 @@ export default {
const res = await qbit.login()
const authenticated = res === 'Ok.'
this.$store.commit('LOGIN', authenticated)
this.$store.commit('updateMainData')
if (
!authenticated &&
!this.$router.currentRoute.name.includes('login')

View file

@ -3,15 +3,16 @@
flat
color="secondary"
class="speedCard"
data-testid="SpeedCard"
>
<v-layout row :class="color + '--text'">
<v-flex xs2>
<v-icon :color="color" size="16px">
<v-flex v-if="icon" xs2>
<v-icon data-testid="SpeedCard-icon" :color="color" size="16px">
{{ icon }}
</v-icon>
</v-flex>
<v-flex xs7 class="text-center font-weight-bold robot-mono">
<span>
<span data-testid="SpeedCard-value">
{{ value | getDataValue(2) }}
</span>
</v-flex>
@ -19,7 +20,7 @@
xs3
class="caption robot-mono text-right mt-1"
>
<span class="speedUnits">
<span class="speedUnits" data-testid="SpeedCard-unit">
{{ value | getDataUnit(1) }}/s
</span>
</v-flex>

View file

@ -5,14 +5,15 @@
<div
style="margin-top: 6px"
:class="color + '--text'"
data-testid="StorageCard-label"
>
{{ label }}
</div>
</v-flex>
<v-flex md5 class="ml-4">
<span :class="color + '--text title'">
{{ value | getDataValue(2) }}
<span class="font-weight-light caption">
<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">
{{ value | getDataUnit }}
</span>
</span>

View file

@ -129,6 +129,9 @@ export default {
return this.getSearchPlugins().filter(p => p.enabled)
}
},
created() {
this.$store.commit('FETCH_SEARCH_PLUGINS')
},
methods: {
async startSearch() {
if (this.searchForm.pattern.length && !this.search.interval) {

View file

@ -11,7 +11,7 @@
<td class="grey--text">
Torrent title
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.name }}
</td>
</tr>
@ -19,7 +19,7 @@
<td class="grey--text">
Directory
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.savePath }}
</td>
</tr>
@ -27,7 +27,7 @@
<td class="grey--text">
hash
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.hash }}
</td>
</tr>
@ -35,7 +35,7 @@
<td class="grey--text">
Size
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.size | getDataValue }}
{{ torrent.size | getDataUnit(1) }}
</td>
@ -44,7 +44,7 @@
<td class="grey--text">
Downloaded:
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.dloaded | getDataValue }}
{{ torrent.dloaded | getDataUnit(1) }}
</td>
@ -53,7 +53,7 @@
<td class="grey--text">
Uploaded:
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.uploaded | getDataValue }}
{{ torrent.uploaded | getDataUnit(1) }}
</td>
@ -62,7 +62,7 @@
<td class="grey--text">
Ratio
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.ratio }}
</td>
</tr>
@ -70,7 +70,7 @@
<td class="grey--text">
Download Speed
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.dlspeed | getDataValue }}
{{ torrent.dlspeed | getDataUnit(1) }}
</td>
@ -79,7 +79,7 @@
<td class="grey--text">
Upload Speed
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.upspeed | getDataValue }}
{{ torrent.upspeed | getDataUnit(1) }}
</td>
@ -88,7 +88,7 @@
<td class="grey--text">
ETA
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.eta }}
</td>
</tr>
@ -96,7 +96,7 @@
<td class="grey--text">
Peers
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.num_leechs
}}<span class="grey--text">/{{ torrent.available_peers }}</span>
</td>
@ -105,7 +105,7 @@
<td class="grey--text">
Seeds
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.num_seeds
}}<span class="grey--text">/{{ torrent.available_seeds }}</span>
</td>
@ -114,26 +114,10 @@
<td class="grey--text">
Added on
</td>
<td class="torrentmodaltext--text">
<td>
{{ torrent.added_on }}
</td>
</tr>
<tr v-if="torrent.tracker">
<td class="grey--text">
Tracker
</td>
<td class="torrentmodaltext--text">
{{ torrent.tracker }}
</td>
</tr>
<tr v-if="torrent.comment">
<td class="grey--text">
Comment
</td>
<td class="torrentmodaltext--text">
{{ torrent.comment }}
</td>
</tr>
<tr>
<td class="grey--text">
Status
@ -145,6 +129,47 @@
{{ torrent.state }}
</v-chip>
</tr>
<tr v-if="torrent.tracker">
<td class="grey--text">
Tracker
</td>
<td>
{{ torrent.tracker }}
</td>
</tr>
<tr v-if="torrent.comment">
<td class="grey--text">
Comment
</td>
<td>
{{ torrent.comment }}
</td>
</tr>
<tr>
<td class="grey--text">
First/Last Piece Priority
</td>
<td>
{{ torrent.f_l_piece_prio }}
</td>
</tr>
<tr>
<td class="grey--text">
Sequential Download
</td>
<td>
{{ torrent.seq_dl }}
</td>
</tr>
<tr>
<td class="grey--text">
Auto TMM
</td>
<td>
{{ torrent.auto_tmm }}
</td>
</tr>
</tbody>
</v-simple-table>
</v-card-text>

View file

@ -152,5 +152,10 @@ export default {
#app .v-select .v-text-field__details {
display: none;
}
#app .v-select .v-select__selection {
padding: 16px 0;
margin: 0;
}
</style>

View file

@ -0,0 +1,21 @@
<template>
<v-flex xs6 sm1 md1>
<div class="caption grey--text">
Downloaded
</div>
<div>
{{ torrent.dloaded | getDataValue(2) }}
<span class="caption grey--text">
{{
torrent.dloaded | getDataUnit(1)
}}
</span>
</div>
</v-flex>
</template>
<script>
export default {
name: 'Downloaded',
props: ['torrent']
}
</script>

View file

@ -6,13 +6,14 @@
class="mr-4"
>
<div class="caption grey--text">
Done
Progress
</div>
<v-progress-linear
:value="torrent.progress"
height="20"
:style="phoneLayout ? '' : 'width: 80%;'"
:color="`torrent-${state}-color`"
rounded
>
<span
class="caption white--text"

View file

@ -2,14 +2,15 @@
<v-flex
xs6
sm1
md2
md1
class="mr-4"
>
<div class="caption grey--text">
Status
</div>
<v-chip
small
class="caption white--text"
class="caption white--text px-2"
:class="state"
>
{{ torrent.state }}

View file

@ -1,6 +1,7 @@
import Size from './Size'
import Progress from './Progress'
import Download from './Download'
import Downloaded from './Downloaded'
import Ratio from './Ratio'
import Upload from './Upload'
import ETA from './ETA'
@ -27,5 +28,6 @@ export {
Tags,
AddedOn,
Uploaded,
UploadedSession
UploadedSession,
Downloaded
}

View file

@ -1,170 +0,0 @@
<template>
<v-list class="py-1">
<v-list-item link @click="resume">
<v-icon>{{ mdiPlay }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Resume
</v-list-item-title>
</v-list-item>
<v-list-item link @click="forceResume">
<v-icon>{{ mdiFastForward }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Force Resume
</v-list-item-title>
</v-list-item>
<v-list-item link @click="pause">
<v-icon>{{ mdiPause }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Pause
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item link @click="deleteWithoutFiles">
<v-icon color="red">
{{ mdiDelete }}
</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em; color: red"
>
Delete
</v-list-item-title>
</v-list-item>
<v-list-item link @click="deleteWithFiles">
<v-icon color="red">
{{ mdiDeleteForever }}
</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em; color: red"
>
Delete with files
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item link @click="recheck">
<v-icon>{{ mdiPlaylistCheck }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Force recheck
</v-list-item-title>
</v-list-item>
<v-list-item link @click="reannounce">
<v-icon>{{ mdiBullhorn }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Force reannounce
</v-list-item-title>
</v-list-item>
<v-divider />
<v-menu
open-on-hover
top
>
<template #activator="{ on }">
<v-list-item link v-on="on">
<v-icon>{{ mdiShape }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Set Category
<v-icon>{{ mdiChevronRight }}</v-icon>
</v-list-item-title>
</v-list-item>
</template>
<v-list dense rounded>
<v-list-item
v-for="(item, index) in availableCategories"
:key="index"
link
@click="setCategory(item.value)"
>
<v-list-item-title class="ml-2" style="font-size: 12px">
{{ item.name }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-list>
</template>
<script>
import qbit from '@/services/qbit'
import { General, TorrentSelect } from '@/mixins'
import {
mdiBullhorn, mdiArrowUp, mdiArrowDown, mdiPriorityLow,
mdiDeleteForever, mdiDelete, mdiPlaylistCheck, mdiShape,
mdiPlay, mdiPause, mdiSelect, mdiPriorityHigh, mdiFastForward,
mdiChevronRight
} from '@mdi/js'
import { mapGetters, mapState } from 'vuex'
export default {
name: 'TorrentMultipleRightClickMenu',
mixins: [General, TorrentSelect],
data: () => ({
priority_options: [
{ name: 'top', icon: mdiPriorityHigh, action: 'topPrio' },
{ name: 'increase', icon: mdiArrowUp, action: 'increasePrio' },
{ name: 'decrease', icon: mdiArrowDown, action: 'decreasePrio' },
{ name: 'bottom', icon: mdiPriorityLow, action: 'bottomPrio' }
],
mdiDelete, mdiPlay, mdiPause, mdiSelect, mdiFastForward,
mdiDeleteForever, mdiBullhorn, mdiPlaylistCheck, mdiShape,
mdiChevronRight
}),
computed: {
...mapState(['selected_torrents']),
...mapGetters(['getCategories']),
availableCategories() {
const categories = [
{ name: 'None', value: '' }]
categories.push(...this.getCategories().map(c => {
return { name: c.name, value: c.name }
}))
return categories
}
},
methods: {
resume() {
qbit.resumeTorrents(this.selected_torrents)
},
pause() {
qbit.pauseTorrents(this.selected_torrents)
},
reannounce() {
qbit.reannounceTorrents(this.selected_torrents)
},
deleteWithoutFiles() {
qbit.deleteTorrents(this.selected_torrents, false)
},
deleteWithFiles() {
qbit.deleteTorrents(this.selected_torrents, true)
},
recheck() {
qbit.recheckTorrents(this.selected_torrents)
},
forceResume() {
qbit.forceStartTorrents(this.selected_torrents)
},
setCategory(cat) {
qbit.setCategory(this.selected_torrents, cat)
}
}
}
</script>

View file

@ -52,42 +52,88 @@
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item link @click="location">
<v-icon>{{ mdiFolder }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Change location
</v-list-item-title>
</v-list-item>
<v-list-item link @click="rename">
<v-icon>{{ mdiRenameBox }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Rename
</v-list-item-title>
</v-list-item>
<v-list-item link @click="recheck">
<v-icon>{{ mdiPlaylistCheck }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Force recheck
</v-list-item-title>
</v-list-item>
<v-list-item link @click="reannounce">
<v-icon>{{ mdiBullhorn }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Force reannounce
</v-list-item-title>
</v-list-item>
<v-menu
open-on-hover
top
>
<template #activator="{ on }">
<v-list-item link v-on="on">
<v-icon>{{ mdiHeadCog }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Advanced
<v-icon>{{ mdiChevronRight }}</v-icon>
</v-list-item-title>
</v-list-item>
</template>
<v-list dense rounded>
<v-list-item v-if="!multiple" link @click="location">
<v-icon>{{ mdiFolder }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Change location
</v-list-item-title>
</v-list-item>
<v-list-item v-if="!multiple" link @click="rename">
<v-icon>{{ mdiRenameBox }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Rename
</v-list-item-title>
</v-list-item>
<v-list-item link @click="recheck">
<v-icon>{{ mdiPlaylistCheck }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Force recheck
</v-list-item-title>
</v-list-item>
<v-list-item link @click="reannounce">
<v-icon>{{ mdiBullhorn }}</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Force reannounce
</v-list-item-title>
</v-list-item>
<v-list-item v-if="!multiple" link @click="toggleSeq">
<v-icon> {{ torrent.seq_dl ? mdiCheckboxMarked : mdiCheckboxBlankOutline }} </v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Sequential Download
</v-list-item-title>
</v-list-item>
<v-list-item v-if="!multiple" link @click="toggleFL">
<v-icon> {{ torrent.f_l_piece_prio ? mdiCheckboxMarked : mdiCheckboxBlankOutline }} </v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
First/Last priority
</v-list-item-title>
</v-list-item>
<v-list-item v-if="!multiple" link @click="toggleAutoTMM">
<v-icon> {{ torrent.auto_tmm ? mdiCheckboxMarked : mdiCheckboxBlankOutline }} </v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 1em"
>
Auto TMM
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu
open-on-hover
top
@ -147,8 +193,8 @@
</v-list-item>
</v-list>
</v-menu>
<v-divider />
<v-list-item link @click="showInfo">
<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"
@ -157,7 +203,7 @@
Show Info
</v-list-item-title>
</v-list-item>
<v-list-item link @click="selectTorrent(hash)">
<v-list-item v-if="!multiple" link @click="selectTorrent(hash)">
<v-icon>{{ mdiSelect }}</v-icon>
<v-list-item-title
class="ml-2"
@ -169,20 +215,21 @@
</template>
<script>
import { mapGetters, mapState } from 'vuex'
import qbit from '@/services/qbit'
import { General, TorrentSelect } from '@/mixins'
import {
mdiBullhorn, mdiPlaylistCheck, mdiArrowUp, mdiArrowDown, mdiPriorityLow,
mdiInformation, mdiDeleteForever, mdiRenameBox, mdiFolder, mdiDelete,
mdiPlay, mdiPause, mdiSelect, mdiPriorityHigh, mdiChevronRight,
mdiFastForward, mdiShape
mdiFastForward, mdiShape, mdiHeadCog, mdiCheckboxMarked, mdiCheckboxBlankOutline
} from '@mdi/js'
import { mapGetters } from 'vuex'
export default {
name: 'TorrentRightClickMenu',
mixins: [General, TorrentSelect],
props: {
hash: String
torrent: Object
},
data: () => ({
priority_options: [
@ -194,10 +241,11 @@ export default {
mdiDelete, mdiPlay, mdiPause, mdiSelect, mdiFastForward,
mdiFolder, mdiRenameBox, mdiDeleteForever, mdiInformation,
mdiPlaylistCheck, mdiPriorityHigh, mdiBullhorn, mdiChevronRight,
mdiShape
mdiShape, mdiHeadCog, mdiCheckboxMarked, mdiCheckboxBlankOutline
}),
computed: {
...mapGetters(['getCategories']),
...mapState(['selected_torrents']),
availableCategories() {
const categories = [
{ name: 'None', value: '' }]
@ -206,44 +254,61 @@ export default {
}))
return categories
},
hashes() {
if (this.multiple) return this.selected_torrents
return [this.torrent.hash]
},
multiple() {
return this.selected_torrents.length
}
},
methods: {
resume() {
qbit.resumeTorrents([this.hash])
qbit.resumeTorrents(this.hashes)
},
pause() {
qbit.pauseTorrents([this.hash])
qbit.pauseTorrents(this.hashes)
},
location() {
this.createModal('ChangeLocationModal', { hash: this.hash })
this.createModal('ChangeLocationModal', { hash: this.torrent.hash })
},
rename() {
this.createModal('RenameModal', { hash: this.hash })
this.createModal('RenameModal', { hash: this.torrent.hash })
},
reannounce() {
qbit.reannounceTorrents([this.hash])
qbit.reannounceTorrents(this.hashes)
},
deleteWithoutFiles() {
qbit.deleteTorrents([this.hash], false)
qbit.deleteTorrents(this.hashes, false)
},
deleteWithFiles() {
qbit.deleteTorrents([this.hash], true)
qbit.deleteTorrents(this.hashes, true)
},
recheck() {
qbit.recheckTorrents([this.hash])
qbit.recheckTorrents(this.hashes)
},
showInfo() {
this.createModal('TorrentDetailModal', { hash: this.hash })
this.createModal('TorrentDetailModal', { hash: this.torrent.hash })
},
setPriority(priority) {
qbit.setTorrentPriority(this.hash, priority)
},
forceResume() {
qbit.forceStartTorrents([this.hash])
qbit.forceStartTorrents(this.hashes)
},
setCategory(cat) {
qbit.setCategory([this.hash], cat)
qbit.setCategory(this.hashes, cat)
},
toggleSeq() {
qbit.toggleSequentialDownload(this.hashes)
},
toggleFL() {
qbit.toggleFirstLastPiecePriority(this.hashes)
},
toggleAutoTMM() {
qbit.setAutoTMM(this.hashes, !this.torrent.auto_tmm)
}
}
}

View file

@ -4,6 +4,7 @@ import '@/registerServiceWorker'
import router from '@/router'
import store from '@/store'
import '@babel/polyfill'
import vuetify from './plugins/vuetify'
import 'typeface-roboto'
import 'typeface-roboto-mono'
@ -12,13 +13,8 @@ import filters from '@/filters'
import styles from '@/styles/styles.scss'
/* eslint-enable no-unused-vars */
import VueObserveVisibility from 'vue-observe-visibility'
Vue.use(VueObserveVisibility)
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
import vuetify from './plugins/vuetify'
Vue.use(Toast, {
maxToasts: 5,
timeout: 2000
@ -26,7 +22,6 @@ Vue.use(Toast, {
import PerfectScrollbar from 'vue2-perfect-scrollbar'
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
Vue.use(PerfectScrollbar)
import './registerServiceWorker'

View file

@ -23,6 +23,10 @@ export default class Torrent {
this.category = data.category
this.tracker = data.tracker
this.comment = data.comment
this.f_l_piece_prio = data.f_l_piece_prio
this.seq_dl = data.seq_dl
this.auto_tmm = data.auto_tmm
Object.freeze(this)
}

View file

@ -1,6 +1,5 @@
import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import Vuetify from 'vuetify/lib'
import colors from 'vuetify/lib/util/colors'
import variables from '@/styles/colors.scss'

View file

@ -199,6 +199,18 @@ class Qbit {
return this.torrentAction('setForceStart', hashes, { value: true })
}
toggleSequentialDownload(hashes) {
return this.torrentAction('toggleSequentialDownload', hashes)
}
toggleFirstLastPiecePriority(hashes) {
return this.torrentAction('toggleFirstLastPiecePrio', hashes)
}
setAutoTMM(hashes, enable) {
return this.torrentAction('setAutoManagement', hashes, { enable })
}
reannounceTorrents(hashes) {
return this.torrentAction('reannounce', hashes)
}

View file

@ -8,8 +8,7 @@ const vuexPersist = new VuexPersist({
reducer: state => ({
sort_options: state.sort_options,
webuiSettings: state.webuiSettings,
authenticated: state.authenticated,
torrents: state.torrents
authenticated: state.authenticated
})
})
@ -64,6 +63,7 @@ export default new Vuex.Store({
busyTorrentProperties: [
{ name: 'Size', active: true },
{ name: 'Progress', active: true },
{ name: 'Downloaded', active: true },
{ name: 'Download', active: true },
{ name: 'Upload', active: true },
{ name: 'ETA', active: true },
@ -78,6 +78,7 @@ export default new Vuex.Store({
doneTorrentProperties: [
{ name: 'Size', active: true },
{ name: 'Progress', active: true },
{ name: 'Downloaded', active: true },
{ name: 'Download', active: true },
{ name: 'Upload', active: true },
{ name: 'ETA', active: true },

View file

@ -92,13 +92,17 @@ export default {
const { data } = await qbit.getAppPreferences()
state.settings = data
},
UPDATE_SORT_OPTIONS: (state, payload) => {
state.sort_options.sort = payload.name ? payload.name : state.sort_options.sort
state.sort_options.reverse = payload.reverse
state.sort_options.hashes = payload.hashes ? payload.hashes : state.sort_options.hashes
state.sort_options.filter = payload.filter
state.sort_options.category = payload.category
state.sort_options.tracker = payload.tracker
UPDATE_SORT_OPTIONS: (state, {
reverse = false,
hashes = [],
filter = null, category = null,
tracker = 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
},
FETCH_CATEGORIES: async state => state.categories = Object.values(await (qbit.getCategories())),
FETCH_SEARCH_PLUGINS: async state => state.searchPlugins = await qbit.getSearchPlugins(),

View file

@ -78,8 +78,7 @@
</div>
</div>
<vue-context ref="menu" v-slot="{ data }">
<TorrentRightClickMenu v-if="data && !selected_torrents.length" :hash="data.torrent.hash" />
<TorrentMultipleRightClickMenu v-if="data && selected_torrents.length" />
<TorrentRightClickMenu v-if="data" :torrent="data.torrent" />
</vue-context>
</div>
</template>
@ -94,13 +93,12 @@ import 'vue-context/src/sass/vue-context.scss'
import Torrent from '@/components/Torrent/Torrent'
import TorrentRightClickMenu from '@/components/Torrent/TorrentRightClickMenu.vue'
import TorrentMultipleRightClickMenu from '@/components/Torrent/TorrentMultipleRightClickMenu.vue'
import { TorrentSelect, General } from '@/mixins'
export default {
name: 'Dashboard',
components: { Torrent, VueContext, TorrentRightClickMenu, TorrentMultipleRightClickMenu },
components: { Torrent, VueContext, TorrentRightClickMenu },
mixins: [TorrentSelect, General],
data() {
return {

13
tests/helpers.js Normal file
View file

@ -0,0 +1,13 @@
import { createLocalVue, mount } from '@vue/test-utils'
import Vuetify from 'vuetify'
export function setup(component, propsData) {
const localVue = createLocalVue() // because of vuetify, we should use a localVue instance
const vuetify = new Vuetify()
return mount(component, {
localVue,
vuetify,
propsData
})
}

7
tests/setup.js Normal file
View file

@ -0,0 +1,7 @@
import Vue from 'vue'
import Vuetify from 'vuetify'
// eslint-disable-next-line no-unused-vars
import filters from '@/filters'
Vue.use(Vuetify)
Vue.config.productionTip = false

View file

@ -0,0 +1,29 @@
import { setup } from '../helpers'
import SpeedCard from '@/components/Core/SpeedCard.vue'
import { mdiChevronDown } from '@mdi/js'
describe('SpeedCard.vue', () => {
it('should render the card', () => {
const wrapper = setup(SpeedCard)
expect(wrapper.find('[data-testid="SpeedCard"]').exists()).toBe(true)
})
it('shouldn\'t render the icon', () => {
const wrapper = setup(SpeedCard)
expect(wrapper.find('[data-testid="SpeedCard-icon"]').exists()).toBe(false)
})
it('should render the icon', () => {
const wrapper = setup(SpeedCard, { icon: mdiChevronDown })
expect(wrapper.find('[data-testid="SpeedCard-icon"]').exists()).toBe(true)
})
it('should render value and unit & be formatted', () => {
const wrapper = setup(SpeedCard, { value: 10000 })
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-unit"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="SpeedCard-unit"]').text()).toBe('KB/s')
})
})

View file

@ -0,0 +1,27 @@
import { setup } from '../helpers'
import StorageCard from '@/components/Core/StorageCard'
describe('StorageCard.vue', () => {
it('should render the label', () => {
const label = 'Downloaded'
const wrapper = setup(StorageCard, { label })
expect(wrapper.find('[data-testid="StorageCard-label"]').text()).toEqual(label)
})
it('should render value and unit & be formatted', () => {
const wrapper = setup(StorageCard, { value: 10000 })
expect(wrapper.find('[data-testid="StorageCard-value"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="StorageCard-value"]').text()).toBe('9.77')
expect(wrapper.find('[data-testid="StorageCard-unit"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="StorageCard-unit"]').text()).toBe('KB')
})
it('text should have the passed-in color', () => {
const color = 'download'
const wrapper = setup(StorageCard, { color })
expect(wrapper.find('[data-testid="StorageCard-label"]').classes()).toContain(color + '--text')
expect(wrapper.find('[data-testid="StorageCard-Wrapper"]').classes()).toContain(color + '--text')
})
})

View file

@ -5,7 +5,7 @@ module.exports = {
.plugin('html')
.tap(args => {
args[0].title = 'VueTorrent'
return args
})
},