Feature categories (#35)

This commit is contained in:
Daan Wijns 2020-09-12 15:37:40 +02:00 committed by GitHub
parent 879cdc3420
commit 28e716ff29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 810 additions and 320 deletions

54
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "vuetorrent",
"version": "0.1.3",
"version": "0.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -845,9 +845,9 @@
}
},
"@babel/polyfill": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.10.4.tgz",
"integrity": "sha512-8BYcnVqQ5kMD2HXoHInBH7H1b/uP3KdnwCYXOqFnXqguOyuu443WXusbIUbWEfY3Z0Txk0M1uG/8YuAMhNl6zg==",
"version": "7.11.5",
"resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.11.5.tgz",
"integrity": "sha512-FunXnE0Sgpd61pKSj2OSOs1D44rKTD3pGOfGilZ6LGrrIH0QEtJlTjqOqdF8Bs98JmjfGhni2BBkTfv9KcKJ9g==",
"requires": {
"core-js": "^2.6.5",
"regenerator-runtime": "^0.13.4"
@ -1998,9 +1998,9 @@
}
},
"apexcharts": {
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.20.0.tgz",
"integrity": "sha512-DuQ9SlFPJBJwamYudzwf/Z6KMaIRUhnVBQk/TBa3mXzN2SwS3GgGz7V39OS1GfcPlPUTTy8vXv91M8pYmfFkCg==",
"version": "3.20.1",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.20.1.tgz",
"integrity": "sha512-86WedRPcIs45gdcVC+na0SDGIYcH378Z+TmOAyXYs4vwqjvbYyzA9VGFN2UorLgXHIV4RJm4kFJXdIBYh3aDiA==",
"requires": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
@ -3909,9 +3909,9 @@
}
},
"dayjs": {
"version": "1.8.34",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.34.tgz",
"integrity": "sha512-Olb+E6EoMvdPmAMq2QoucuyZycKHjTlBXmRx8Ada+wGtq4SIXuDCdtoaX4KkK0yjf1fJLnwXQURr8gQKWKaybw=="
"version": "1.8.35",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.35.tgz",
"integrity": "sha512-isAbIEenO4ilm6f8cpqvgjZCsuerDAz2Kb7ri201AiNn58aqXuaLJEnCtfIMdCvERZHNGRY5lDMTr/jdAnKSWQ=="
},
"de-indent": {
"version": "1.0.2",
@ -6950,8 +6950,7 @@
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash.defaultsdeep": {
"version": "4.6.1",
@ -9748,6 +9747,14 @@
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
"request-promise-core": {
@ -10533,6 +10540,14 @@
"faye-websocket": "^0.10.0",
"uuid": "^3.4.0",
"websocket-driver": "0.6.5"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
"sockjs-client": {
@ -11692,10 +11707,9 @@
"dev": true
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
},
"v8-compile-cache": {
"version": "2.1.0",
@ -12529,6 +12543,14 @@
"requires": {
"ansi-colors": "^3.0.0",
"uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
"webpack-merge": {

View file

@ -9,12 +9,14 @@
"format": "pretty-quick"
},
"dependencies": {
"@babel/polyfill": "^7.10.4",
"apexcharts": "^3.19.3",
"@babel/polyfill": "^7.11.5",
"apexcharts": "^3.20.1",
"axios": "^0.19.2",
"core-js": "^3.6.4",
"dayjs": "^1.8.29",
"dayjs": "^1.8.35",
"lodash": "^4.17.20",
"register-service-worker": "^1.7.1",
"uuid": "^8.3.0",
"vue": "^2.6.12",
"vue-apexcharts": "^1.6.0",
"vue-async-computed": "^3.9.0",

View file

@ -1,8 +1,11 @@
<template>
<v-app :style="{ backgroundColor: background }">
<AddModal />
<SettingsModal />
<SearchModal />
<component
v-for="modal in modals"
:key="modal.guid"
:is="modal.component"
v-bind="{ guid: modal.guid, ...modal.props }"
/>
<Navbar v-if="isAuthenticated" />
<v-main fill-height fill-width>
<router-view></router-view>
@ -13,11 +16,10 @@
<script>
import { mapState, mapGetters } from 'vuex'
import Navbar from '@/components/Navbar.vue'
import SettingsModal from '@/components/Modals/SettingsModal/SettingsModal'
import { isAuthenticated } from '@/services/auth.js'
export default {
components: { Navbar, SettingsModal },
components: { Navbar },
name: 'App',
data() {
return {}
@ -28,8 +30,12 @@ export default {
}
},
computed: {
...mapState(['rid', 'mainData', 'preferences']),
...mapGetters(['getTheme']),
...mapState(['rid', 'mainData', 'preferences', 'modals']),
...mapGetters([
'getTheme',
'getDynamicComponent',
'getDynamicComponent'
]),
theme() {
return this.getTheme() ? 'dark' : 'light'
},

View file

@ -119,7 +119,7 @@ export default {
this.resetForm()
this.$store.commit('TOGGLE_MODAL', 'addmodal')
this.$store.commit('DELETE_MODAL', this.guid)
}
},
resetForm() {

View file

@ -164,7 +164,7 @@ export default {
qbit.addTorrents(params)
},
close() {
this.$store.commit('TOGGLE_MODAL', 'SearchModal')
this.$store.commit('DELETE_MODAL', this.guid)
}
},
computed: {

View file

@ -15,6 +15,7 @@
<v-tab href="#bittorrent">BitTorrent</v-tab>
<v-tab href="#webui">WebUI</v-tab>
<v-tab href="#vuetorrent">VueTorrent</v-tab>
<v-tab href="#tagsAndCategories">Tags & Categories</v-tab>
</v-tabs>
<perfect-scrollbar>
<v-tabs-items
@ -34,11 +35,16 @@
<v-tab-item value="vuetorrent">
<VueTorrent :is-active="tab === 'vuetorrent'" />
</v-tab-item>
<v-tab-item value="tagsAndCategories">
<TagsAndCategories
:is-active="tab === 'tagsAndCategories'"
/>
</v-tab-item>
</v-tabs-items>
</perfect-scrollbar>
</div>
<v-card-actions class="d-flex justify-center">
<v-btn color="success" @click="save_settings">Save</v-btn>
<v-btn color="success" @click="saveSettings">Save</v-btn>
<v-fab-transition v-if="phoneLayout">
<v-btn
@click="close"
@ -62,12 +68,18 @@ import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { Modal, FullScreenModal } from '@/mixins'
import { WebUI, BitTorrent, Downloads, VueTorrent } from './Tabs'
import {
WebUI,
BitTorrent,
Downloads,
VueTorrent,
TagsAndCategories
} from './Tabs'
export default {
name: 'SettingsModal',
mixins: [Modal, FullScreenModal],
components: { WebUI, BitTorrent, Downloads, VueTorrent },
components: { WebUI, BitTorrent, Downloads, VueTorrent, TagsAndCategories },
data() {
return {
tab: null,
@ -77,12 +89,14 @@ export default {
},
methods: {
close() {
this.$store.commit('TOGGLE_MODAL', 'SettingsModal')
this.deleteModal()
},
save_settings() {
saveSettings() {
qbit.setPreferences(this.getSettings()).then(() => {
Vue.$toast.success('Settings saved successfully!')
})
this.$store.commit('FETCH_SETTINGS')
this.close()
}
},
computed: {
@ -93,11 +107,6 @@ export default {
dialogHeight() {
return this.phoneLayout ? '79vh' : '70vh'
}
},
watch: {
dialog(visible) {
!visible ? (this.tab = null) : this.$store.commit('SET_SETTINGS')
}
}
}
</script>

View file

@ -72,7 +72,7 @@
import SettingsTab from '@/mixins/SettingsTab'
export default {
name: 'BitTorrent',
name: 'Downloads',
mixins: [SettingsTab]
}
</script>

View file

@ -0,0 +1,124 @@
<template>
<v-card flat>
<v-card-text
class="mx-auto mt-5"
style="font-size: 1.1em; max-height: 500px; min-height: 300px"
>
<v-layout class="mx-auto" row wrap>
<v-flex xs12 sm12>
<h3>Available Tags:</h3>
</v-flex>
<v-flex xs12 sm12 class="mt-3">
<v-chip
v-for="tag in availableTags"
:key="tag"
small
class="download white--text caption mx-2"
style="font-size: 0.95em !important"
>
{{ tag }}
</v-chip>
</v-flex>
</v-layout>
<v-card-actions class="justify-center pb-5">
<v-btn text class="error white--text mt-3" @click="deleteTag"
>Delete</v-btn
>
<v-btn
text
class="green_accent white--text mt-3"
@click="createTag"
>Create new</v-btn
>
</v-card-actions>
<v-layout class="mx-auto" row wrap>
<v-flex xs12 sm12>
<h3>Available Categories:</h3>
</v-flex>
<v-flex xs12 sm12 class="mt-3">
<v-chip
v-for="cat in availableCategories"
:key="cat.name"
small
class="upload white--text caption mx-2"
style="font-size: 0.95em !important"
@click="editCategory(cat)"
@click:close="editCategory(cat)"
>
{{ cat.name }}
</v-chip>
</v-flex>
</v-layout>
<v-card-actions class="justify-center pb-5">
<v-btn
text
class="error white--text mt-3"
@click="deleteCategory"
>Delete</v-btn
>
<v-btn
text
class="green_accent white--text mt-3"
@click="createCategory"
>Create new</v-btn
>
</v-card-actions>
</v-card-text>
</v-card>
</template>
<script>
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { Tab, General } from '@/mixins'
export default {
name: 'TagsAndCategories',
mixins: [Tab, General],
props: {
hash: String
},
data: () => ({
selectedCategory: null
}),
computed: {
...mapGetters(['getTorrent', 'getAvailableTags', 'getCategories']),
availableTags() {
return this.getAvailableTags()
},
availableCategories() {
return this.getCategories()
}
},
methods: {
activeMethod() {
this.fetchCategories()
},
async fetchCategories() {
const { data } = await qbit.getCategories()
this.categories = data
},
deleteTag() {
this.createModal('DeleteTagDialog')
},
createTag() {
this.createModal('CreateTagDialog')
},
createCategory() {
this.createModal('CreateCategoryDialog')
},
deleteCategory() {
this.createModal('DeleteCategoryDialog')
},
editCategory(cat) {
this.createModal('CreateCategoryDialog', { initialCategory: cat })
}
},
created() {
this.$store.commit('FETCH_CATEGORIES')
}
}
</script>

View file

@ -1,6 +1,7 @@
import WebUI from '@/components/Modals/SettingsModal/Tabs/WebUI.vue'
import BitTorrent from '@/components/Modals/SettingsModal/Tabs/BitTorrent.vue'
import Downloads from '@/components/Modals/SettingsModal/Tabs/Downloads.vue'
import VueTorrent from '@/components/Modals/SettingsModal/Tabs/VueTorrent.vue'
import WebUI from './WebUI.vue'
import BitTorrent from './BitTorrent.vue'
import Downloads from './Downloads.vue'
import VueTorrent from './VueTorrent.vue'
import TagsAndCategories from './TagsAndCategories.vue'
export { WebUI, BitTorrent, Downloads, VueTorrent }
export { WebUI, BitTorrent, Downloads, VueTorrent, TagsAndCategories }

View file

@ -0,0 +1,117 @@
<template>
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-container style="min-height: 200px" :class="`pa-0 project done`">
<v-card-title class="justify-center">
<h2>Create New Category</h2>
</v-card-title>
<v-form ref="categoryForm" class="px-6 mt-3">
<v-container>
<v-text-field
class="mx-auto"
style="max-width: 200px"
v-model="category.name"
:rules="nameRules"
:counter="15"
label="Category name"
required
></v-text-field>
<v-text-field
class="mx-auto"
style="max-width: 200px"
v-model="category.savePath"
:rules="PathRules"
:counter="40"
label="Path"
required
></v-text-field>
</v-container>
</v-form>
</v-container>
<v-card-actions class="justify-center pb-5 project done">
<v-btn text @click="cancel" class="error white--text mt-3"
>Cancel</v-btn
>
<v-btn
v-if="!hasInitialCategory"
text
@click="create"
class="green_accent white--text mt-3"
>Save</v-btn
>
<v-btn
v-else
text
@click="edit"
class="green_accent white--text mt-3"
>Edit</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { Modal } from '@/mixins'
import Vue from 'vue'
export default {
name: 'createNewCategoryDialog',
props: {
initialCategory: Object
},
mixins: [Modal],
computed: {
...mapGetters(['getSelectedCategory']),
hasInitialCategory() {
return (
this.initialCategory &&
this.initialCategory.name &&
this.initialCategory.savePath
)
}
},
data: () => ({
nameRules: [
v => !!v || 'Category name is required',
v =>
(v && v.length <= 15) ||
'Category name must be less than 15 characters'
],
PathRules: [
v => !!v || 'Path is required',
v => (v && v.length <= 40) || 'Path must be less than 40 characters'
],
category: { name: '', savePath: '' }
}),
methods: {
create() {
qbit.createCategory(this.category)
this.cancel()
},
cancel() {
this.category.name = ''
this.category.savePath = ''
this.$refs.categoryForm.reset()
this.$store.commit('FETCH_CATEGORIES')
this.deleteModal()
},
edit() {
qbit.editCategory(this.category)
Vue.$toast.success('Category edited successfully!')
this.cancel()
}
},
created() {
this.$store.commit('FETCH_CATEGORIES')
if (this.hasInitialCategory) {
this.category = this.initialCategory
}
}
}
</script>
<style></style>

View file

@ -1,10 +1,7 @@
<template>
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-container
style="min-height: 200px;"
:class="`pa-0 project done`"
>
<v-container style="min-height: 200px" :class="`pa-0 project done`">
<v-card-title class="justify-center">
<h2>Create New Tag</h2>
</v-card-title>
@ -13,7 +10,7 @@
<v-container>
<v-text-field
class="mx-auto"
style="max-width: 200px;"
style="max-width: 200px"
v-model="tagname"
:rules="rules"
:counter="10"
@ -40,11 +37,10 @@
<script>
import qbit from '@/services/qbit'
import { Modal } from '@/mixins'
export default {
name: 'createNewTagDialog',
props: {
dialog: Boolean
},
name: 'createTagDialog',
mixins: [Modal],
data: () => ({
tagname: '',
rules: [
@ -59,7 +55,7 @@ export default {
},
cancel() {
this.tagname = ''
this.$emit('close')
this.deleteModal()
}
}
}

View file

@ -0,0 +1,70 @@
<template>
<v-dialog :value="dialog" max-width="600px">
<v-card>
<v-container style="min-height: 200px" :class="`pa-0 project done`">
<v-card-title class="justify-center">
<h2>Delete Category</h2>
</v-card-title>
<v-list
rounded
v-if="categories"
class="text-center mx-auto"
style="max-width: 200px"
>
<v-list-item
@click="deleteCategory(t)"
v-for="(t, i) in categories"
:key="i"
>
<v-list-item-content>
<v-list-item-title
v-text="t.name"
></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
<v-card-subtitle
class="text-center mx-auto"
style="font-size: 1.5em; margin-top: 20px"
v-else
>
No categories found
</v-card-subtitle>
</v-container>
<v-card-actions class="justify-center pb-5 project done">
<v-btn text @click="cancel" class="error white--text mt-3"
>Close</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import qbit from '@/services/qbit'
import { Modal } from '@/mixins'
import { mapGetters } from 'vuex'
export default {
name: 'DeleteCategoryDialog',
mixins: [Modal],
computed: {
...mapGetters(['getCategories']),
categories() {
return this.getCategories()
}
},
methods: {
deleteCategory(cat) {
qbit.deleteCategory(cat.name)
this.cancel()
},
cancel() {
this.$store.commit('FETCH_CATEGORIES')
this.deleteModal()
}
}
}
</script>
<style></style>

View file

@ -1,23 +1,20 @@
<template>
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-container
style="min-height: 200px;"
:class="`pa-0 project done`"
>
<v-container style="min-height: 200px" :class="`pa-0 project done`">
<v-card-title class="justify-center">
<h2>Delete Tag</h2>
</v-card-title>
<v-list
rounded
v-if="tags"
v-if="availableTags && availableTags.length"
class="text-center mx-auto"
style="max-width: 200px;"
style="max-width: 200px"
>
<v-list-item
@click="deleteTag(t)"
v-for="(t, i) in tags"
v-for="(t, i) in availableTags"
:key="i"
>
<v-list-item-content>
@ -25,6 +22,13 @@
</v-list-item-content>
</v-list-item>
</v-list>
<v-card-subtitle
class="text-center mx-auto"
style="font-size: 1.5em; margin-top: 20px"
v-else
>
No tags found
</v-card-subtitle>
</v-container>
<v-card-actions class="justify-center pb-5 project done">
<v-btn text @click="cancel" class="error white--text mt-3"
@ -37,11 +41,16 @@
<script>
import qbit from '@/services/qbit'
import { Modal } from '@/mixins'
import { mapGetters } from 'vuex'
export default {
name: 'DeleteTagDialog',
props: {
dialog: Boolean,
tags: Array
mixins: [Modal],
computed: {
...mapGetters(['getTorrent', 'getAvailableTags']),
availableTags() {
return this.getAvailableTags()
}
},
methods: {
deleteTag(tag) {
@ -49,8 +58,7 @@ export default {
this.cancel()
},
cancel() {
this.tagname = ''
this.$emit('close')
this.deleteModal()
}
}
}

View file

@ -0,0 +1,11 @@
import CreateNewTagDialog from './CreateTagDialog.vue'
import DeleteTagDialog from './DeleteTagDialog.vue'
import CreateNewCategoryDialog from './CreateCategoryDialog.vue'
import DeleteCategoryDialog from './DeleteCategoryDialog'
export {
CreateNewTagDialog,
DeleteTagDialog,
CreateNewCategoryDialog,
DeleteCategoryDialog
}

View file

@ -1,7 +1,7 @@
<template>
<v-card flat>
<perfect-scrollbar>
<v-card-text style="max-height: 500px; min-height: 400px;">
<v-card-text style="max-height: 500px; min-height: 400px">
<v-treeview
v-model="tree"
:items="fileTree"
@ -68,5 +68,3 @@ export default {
}
}
</script>
<style></style>

View file

@ -1,6 +1,6 @@
<template>
<v-card flat>
<v-card-text class="pa-0" style="font-size: 1.1em;">
<v-card-text class="pa-0" style="font-size: 1.1em">
<v-simple-table>
<tbody>
<tr>
@ -9,7 +9,7 @@
{{ torrent.name }}
</td>
</tr>
<tr style="margin-top: 10px !important;">
<tr style="margin-top: 10px !important">
<td class="grey--text">hash</td>
<td class="torrentmodaltext--text">
{{ torrent.hash }}

View file

@ -0,0 +1,150 @@
<template>
<v-card flat>
<v-card-text
class="mx-auto mt-5"
style="font-size: 1.1em; max-height: 500px; min-height: 300px"
>
<v-row>
<v-col :cols="12" :lg="6" :md="6" :sm="12">
<v-layout class="mx-auto" row wrap>
<v-flex xs12 sm12>
<h3>Available Tags:</h3>
</v-flex>
<v-flex class="mt-3 d-flex justify-center" xs12 sm12>
<v-chip
v-for="tag in availableTags"
:key="tag"
small
class="download white--text caption mx-2"
style="font-size: 0.95em !important"
@click="addTag(tag)"
>
{{ tag }}
</v-chip>
</v-flex>
</v-layout>
<v-layout class="mx-auto mt-12" row wrap>
<v-flex xs12 sm12>
<h3>Current Tags:</h3>
</v-flex>
<v-flex class="mt-3 d-flex justify-center" xs12 sm12>
<div v-if="torrent.tags">
<v-chip
v-for="tag in torrent.tags"
:key="tag"
small
close
class="download white--text caption mx-2"
style="font-size: 0.95em !important"
@click="deleteTag(tag)"
@click:close="deleteTag(tag)"
>{{ tag }}</v-chip
>
</div>
<div v-else>None</div>
</v-flex>
</v-layout>
</v-col>
<v-col :cols="12" :lg="6" :md="6" :sm="12">
<v-layout
class="mx-auto"
:class="
this.$vuetify.breakpoint.smAndDown ? 'mt-12' : ''
"
row
wrap
>
<v-flex xs12 sm12>
<h3>Available Categories:</h3>
</v-flex>
<v-flex class="mt-3 d-flex justify-center" xs12 sm12>
<v-chip
v-for="cat in availableCategories"
:key="cat.name"
small
class="upload white--text caption mx-2"
style="font-size: 0.95em !important"
@click="setCategory(cat.name)"
>
{{ cat.name }}
</v-chip>
</v-flex>
</v-layout>
<v-layout class="mx-auto mt-12" row wrap>
<v-flex xs12 sm12>
<h3>Current Category:</h3>
</v-flex>
<v-flex class="mt-3 d-flex justify-center" xs12 sm12>
<v-chip
v-if="torrent.category"
small
close
class="upload white--text caption mx-2"
style="font-size: 0.95em !important"
@click="deleteCategory"
@click:close="deleteCategory"
>{{ torrent.category }}</v-chip
>
<div v-else>None</div>
</v-flex>
</v-layout>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import { difference } from 'lodash'
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { Tab } from '@/mixins'
export default {
name: 'TorrentTagsAndCategories',
props: {
hash: String
},
mixins: [Tab],
data: () => ({
categories: []
}),
computed: {
...mapGetters(['getTorrent', 'getAvailableTags', 'getCategories']),
torrent() {
return this.getTorrent(this.hash)
},
availableTags() {
let availableTags = this.getAvailableTags()
let currentTags = this.getTorrent(this.hash).tags
return difference(availableTags, currentTags)
},
availableCategories() {
return this.getCategories()
}
},
methods: {
addTag(tag) {
qbit.addTorrentTag(this.hash, tag)
},
deleteTag(tag) {
qbit.removeTorrentTag(this.hash, tag)
},
setCategory(cat) {
qbit.setCategory(this.hash, cat)
},
deleteCategory() {
qbit.setCategory(this.hash, '')
}
},
created() {
this.$store.commit('FETCH_CATEGORIES')
}
}
</script>
<style scoped>
h3 {
text-align: center;
}
</style>

View file

@ -5,7 +5,7 @@
:headers="headers"
:items="trackers"
:hide-default-footer="true"
style="max-height: 500px; min-height: 400px;"
style="max-height: 500px; min-height: 400px"
>
<template v-slot:item="row">
<tr>

View file

@ -0,0 +1,7 @@
import Content from '@/components/Modals/TorrentDetailModal/Tabs/Content'
import Info from '@/components/Modals/TorrentDetailModal/Tabs/Info'
import Peers from '@/components/Modals/TorrentDetailModal/Tabs/Peers'
import Trackers from '@/components/Modals/TorrentDetailModal/Tabs/Trackers'
import TagsAndCategories from '@/components/Modals/TorrentDetailModal/Tabs/TorrentTagsAndCategories'
export { Content, Info, Peers, Trackers, TagsAndCategories }

View file

@ -21,7 +21,7 @@
<v-tab href="#trackers">Trackers</v-tab>
<v-tab href="#peers">Peers</v-tab>
<v-tab href="#content">Content</v-tab>
<v-tab href="#tags">Tags</v-tab>
<v-tab href="#tagsAndCategories">Tags & Categories</v-tab>
</v-tabs>
<v-tabs-items v-model="tab" touchless>
<v-tab-item value="info">
@ -39,8 +39,11 @@
<v-tab-item value="content">
<Content :is-active="tab === 'content'" :hash="hash" />
</v-tab-item>
<v-tab-item value="tags">
<Tags :is-active="tab === 'tags'" :hash="hash" />
<v-tab-item value="tagsAndCategories">
<TagsAndCategories
:is-active="tab === 'tagsAndCategories'"
:hash="hash"
/>
</v-tab-item>
</v-tabs-items>
</div>
@ -57,12 +60,15 @@
import { mapGetters } from 'vuex'
import { Modal, FullScreenModal } from '@/mixins'
import { Content, Info, Peers, Trackers, Tags } from './Tabs'
import { Content, Info, Peers, Trackers, TagsAndCategories } from './Tabs'
export default {
name: 'TorrentDetailModal',
mixins: [Modal, FullScreenModal],
components: { Content, Info, Peers, Trackers, Tags },
components: { Content, Info, Peers, Trackers, TagsAndCategories },
props: {
hash: String
},
data() {
return {
tab: null,
@ -72,14 +78,11 @@ export default {
},
methods: {
close() {
this.$store.commit('TOGGLE_MODAL', 'TorrentDetailModal')
this.deleteModal()
}
},
computed: {
...mapGetters(['getTorrent']),
hash() {
return this.$store.state.selectedDetailTorrent
},
torrent() {
return this.getTorrent(this.hash)
}

View file

@ -26,7 +26,7 @@
fab
color="grey"
class="mr-0 ml-0"
@click="toggleModal('searchmodal')"
@click="addModal('SearchModal')"
>
<v-icon color="grey">search</v-icon>
</v-btn>
@ -36,7 +36,7 @@
fab
color="grey"
class="mr-0 ml-0"
@click="toggleModal('addmodal')"
@click="addModal('AddModal')"
>
<v-icon color="grey">add</v-icon>
</v-btn>
@ -54,7 +54,7 @@
fab
text
class="mr-0 ml-0"
@click="toggleModal('settingsmodal')"
@click="addModal('SettingsModal')"
>
<v-icon color="grey">settings</v-icon>
</v-btn>
@ -64,7 +64,7 @@
app
v-model="drawer"
class="primary"
style="position: fixed;"
style="position: fixed"
disable-resize-watcher
>
<!--current download speeds -->
@ -82,36 +82,18 @@
>
<v-icon color="download">keyboard_arrow_down</v-icon>
<span class="download--text title">
{{
status.dlspeed.substring(
0,
status.dlspeed.indexOf(' ')
)
}}
{{ status.dlspeed | getDataValue }}
<span class="font-weight-light caption">
{{
status.dlspeed.substring(
status.dlspeed.indexOf(' ')
)
}}
{{ status.dlspeed | getDataUnit }}
</span>
</span>
<v-icon class="pl-5" color="upload"
>keyboard_arrow_up</v-icon
>
<span class="upload--text title">
{{
status.upspeed.substring(
0,
status.upspeed.indexOf(' ')
)
}}
{{ status.upspeed | getDataValue }}
<span class="font-weight-light caption">
{{
status.upspeed.substring(
status.upspeed.indexOf(' ')
)
}}
{{ status.upspeed | getDataUnit }}
</span>
</span>
</v-layout>
@ -139,7 +121,7 @@
>
<v-flex md6>
<div
style="font-size: 0.95em; margin-top: 6px;"
style="font-size: 0.95em; margin-top: 6px"
class="download--text"
>
Downloaded
@ -147,18 +129,9 @@
</v-flex>
<v-flex md5 class="ml-4">
<span class="download--text title">
{{
status.downloaded.substring(
0,
status.downloaded.indexOf(' ')
)
}}
{{ status.downloaded | getDataValue }}
<span class="font-weight-light caption">
{{
status.downloaded.substring(
status.downloaded.indexOf(' ')
)
}}
{{ status.downloaded | getDataUnit }}
</span>
</span>
</v-flex>
@ -168,7 +141,7 @@
<v-layout row wrap class="pa-3 project nav_upload mx-auto">
<v-flex md6>
<div
style="font-size: 0.95em; margin-top: 6px;"
style="font-size: 0.95em; margin-top: 6px"
class="upload--text"
>
Uploaded
@ -176,18 +149,9 @@
</v-flex>
<v-flex md5 class="ml-4">
<span class="upload--text title">
{{
status.uploaded.substring(
0,
status.uploaded.indexOf(' ')
)
}}
{{ status.uploaded | getDataValue }}
<span class="font-weight-light caption">
{{
status.uploaded.substring(
status.uploaded.indexOf(' ')
)
}}
{{ status.uploaded | getDataUnit }}
</span>
</span>
</v-flex>
@ -197,14 +161,14 @@
<v-card
v-if="webuiSettings.showFreeSpace"
flat
style="margin-top: 30px;"
style="margin-top: 30px"
color="secondary"
class="ml-2 mr-2"
>
<v-layout row wrap class="pa-3 project nav_upload mx-auto">
<v-flex md6>
<div
style="font-size: 0.95em; margin-top: 6px;"
style="font-size: 0.95em; margin-top: 6px"
class="upload--text"
>
Free Space
@ -212,18 +176,9 @@
</v-flex>
<v-flex md5 class="ml-4">
<span class="upload--text title">
{{
status.freeDiskSpace.substring(
0,
status.freeDiskSpace.indexOf(' ')
)
}}
{{ status.freeDiskSpace | getDataValue }}
<span class="font-weight-light caption">
{{
status.freeDiskSpace.substring(
status.freeDiskSpace.indexOf(' ')
)
}}
{{ status.freeDiskSpace | getDataUnit }}
</span>
</span>
</v-flex>
@ -293,9 +248,12 @@
import { mapMutations, mapState, mapGetters } from 'vuex'
import VueApexCharts from 'vue-apexcharts'
import qbit from '@/services/qbit'
import { General } from '@/mixins'
export default {
name: 'Navbar',
components: { apexcharts: VueApexCharts },
mixins: [General],
data() {
return {
drawer: false,
@ -331,7 +289,13 @@ export default {
}
},
tooltip: {
theme: 'light'
theme: 'light',
x: {
formatter: value => {
let val = 32 - value * 2
return val + ' seconds ago'
}
}
}
},
chartInterval: null
@ -351,8 +315,8 @@ export default {
updateChart() {
this.$refs.chart.updateSeries(this.series, true)
},
toggleModal(name) {
this.$store.commit('TOGGLE_MODAL', name)
addModal(name) {
this.createModal(name)
},
toggleTheme() {
this.$store.commit('TOGGLE_THEME')

View file

@ -23,36 +23,36 @@
<v-flex xs6 sm1 md1 class="mr-2">
<div class="caption grey--text">Size</div>
<div>
{{ torrent.size | getNumber }}
{{ torrent.size | getDataValue }}
<span class="caption grey--text">{{
torrent.size | getUnit
torrent.size | getDataUnit
}}</span>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<div class="caption grey--text">Done</div>
<div>
{{ torrent.dloaded | getNumber }}
{{ torrent.dloaded | getDataValue }}
<span class="caption grey--text">{{
torrent.dloaded | getUnit
torrent.dloaded | getDataUnit
}}</span>
</div>
</v-flex>
<v-flex xs6 sm1 md1 class="mr-2">
<div class="caption grey--text">Download</div>
<div>
{{ torrent.dlspeed | getNumber }}
{{ torrent.dlspeed | getDataValue }}
<span class="caption grey--text">{{
torrent.dlspeed | getUnit
torrent.dlspeed | getDataUnit
}}</span>
</div>
</v-flex>
<v-flex xs5 sm1 md1 class="mr-2">
<div class="caption grey--text">Upload</div>
<div>
{{ torrent.upspeed | getNumber }}
{{ torrent.upspeed | getDataValue }}
<span class="caption grey--text">{{
torrent.upspeed | getUnit
torrent.upspeed | getDataUnit
}}</span>
</div>
</v-flex>
@ -90,11 +90,22 @@
</div>
</v-flex>
<!-- labels -->
<v-flex v-for="tag in torrent.tags" :key="tag" xs3 sm1 md1>
<v-flex
v-for="tag in torrent.tags.slice(0, 3)"
:key="tag"
xs3
sm1
md1
>
<v-chip small class="download white--text my-2 caption">
{{ tag }}
</v-chip>
</v-flex>
<v-flex v-if="torrent.category" xs3 sm1 md1>
<v-chip small class="upload white--text my-2 caption">
{{ torrent.category }}
</v-chip>
</v-flex>
<v-flex xs12 sm12 md12>
<v-progress-linear
height="3"
@ -118,15 +129,30 @@
<script>
import { VueContext } from 'vue-context'
import TorrentRightClickMenu from '@/components/Torrent/TorrentRightClickMenu.vue'
import { General } from '@/mixins'
export default {
name: 'Torrent',
components: {
VueContext,
TorrentRightClickMenu
},
mixins: [General],
props: {
torrent: Object
},
computed: {
chips() {
let chips = []
if (this.torrent.category.length > 0) {
chips.push(this.torrent.category)
}
return chips
}
},
methods: {
selectTorrent(hash) {
if (this.containsTorrent(hash)) {
@ -139,19 +165,10 @@ export default {
return this.$store.getters.containsTorrent(hash)
},
showInfo(hash) {
this.$store.commit('TOGGLE_MODAL', 'TorrentDetailModal')
this.$store.commit('SET_SELECTED_TORRENT_DETAIL', hash)
this.createModal('TorrentDetailModal', { hash })
}
},
filters: {
getUnit(value) {
if (!value) return ''
return value.substring(value.indexOf(' '))
},
getNumber(value) {
if (!value) return ''
return value.substring(0, value.indexOf(' '))
},
formatEta(value, options) {
const minute = 60
const hour = minute * 60

View file

@ -14,27 +14,27 @@
<v-list dense rounded>
<v-list-item @click="showInfo" link>
<v-icon>info</v-icon>
<v-list-item-title class="ml-2" style="font-size: 15px;"
<v-list-item-title class="ml-2" style="font-size: 15px"
>Show Info</v-list-item-title
>
</v-list-item>
<v-divider />
<v-list-item @click="resume" link>
<v-icon>play_arrow</v-icon>
<v-list-item-title class="ml-2" style="font-size: 15px;"
<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;"
<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;"
<v-list-item-title class="ml-2" style="font-size: 15px"
>reannounce</v-list-item-title
>
</v-list-item>
@ -43,7 +43,7 @@
<v-icon color="red">delete</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 15px; color: red;"
style="font-size: 15px; color: red"
>Delete</v-list-item-title
>
</v-list-item>
@ -51,7 +51,7 @@
<v-icon color="red">delete</v-icon>
<v-list-item-title
class="ml-2"
style="font-size: 15px; color: red;"
style="font-size: 15px; color: red"
>Delete with files</v-list-item-title
>
</v-list-item>
@ -61,8 +61,10 @@
<script>
import qbit from '@/services/qbit'
import { General } from '@/mixins'
export default {
name: 'TorrentRightClickMenu',
mixins: [General],
props: {
hash: String
},
@ -83,8 +85,7 @@ export default {
qbit.deleteTorrents([this.hash], true)
},
showInfo() {
this.$store.commit('TOGGLE_MODAL', 'TorrentDetailModal')
this.$store.commit('SET_SELECTED_TORRENT_DETAIL', this.hash)
this.createModal('TorrentDetailModal', { hash: this.hash })
}
},
computed: {

View file

@ -1,105 +0,0 @@
<template>
<v-card flat>
<v-card-text
class="mx-auto mt-5"
style="font-size: 1.1em; max-height: 500px; min-height: 300px;"
>
<v-layout class="mx-auto" row wrap>
<v-flex xs12 sm12>
<h3>Available Tags:</h3>
</v-flex>
<v-flex xs12 sm12 class="mt-3">
<v-chip
v-for="tag in availableTags"
:key="tag"
small
class="download white--text caption mx-2"
style="font-size: 0.95em !important;"
@click="addTag(tag)"
>
{{ tag }}
</v-chip>
</v-flex>
</v-layout>
<v-layout class="mx-auto mt-12" row wrap>
<v-flex xs12 sm12>
<h3>Current Tags:</h3>
</v-flex>
<v-flex xs12 sm12 class="mt-3">
<v-chip
v-for="tag in torrent.tags"
:key="tag"
small
close
class="download white--text caption mx-2"
style="font-size: 0.95em !important;"
@click="deleteTag(tag)"
@click:close="deleteTag(tag)"
>{{ tag }}</v-chip
>
</v-flex>
</v-layout>
</v-card-text>
<v-card-actions class="justify-center pb-5">
<v-btn
text
class="error white--text mt-3"
@click="DeleteDialog = true"
>Delete</v-btn
>
<v-btn
text
class="green_accent white--text mt-3"
@click="CreateNewDialog = true"
>Create new</v-btn
>
</v-card-actions>
<CreateNewTagDialog
:dialog="CreateNewDialog"
@close="CreateNewDialog = false"
/>
<DeleteTagDialog
:dialog="DeleteDialog"
@close="DeleteDialog = false"
:tags="availableTags"
/>
</v-card>
</template>
<script>
import { difference } from 'lodash'
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import CreateNewTagDialog from './CreateNewTagDialog'
import DeleteTagDialog from './DeleteTagDialog'
export default {
name: 'Tags',
components: { CreateNewTagDialog, DeleteTagDialog },
props: {
hash: String
},
data: () => ({
CreateNewDialog: false,
DeleteDialog: false
}),
computed: {
...mapGetters(['getTorrent', 'getAvailableTags']),
torrent() {
return this.getTorrent(this.hash)
},
availableTags() {
let availableTags = this.getAvailableTags()
let currentTags = this.getTorrent(this.hash).tags
return difference(availableTags, currentTags)
}
},
methods: {
addTag(tag) {
qbit.addTorrentTag(this.hash, tag)
},
deleteTag(tag) {
qbit.removeTorrentTag(this.hash, tag)
}
}
}
</script>

View file

@ -1,7 +0,0 @@
import Content from '@/components/TorrentDetailModal/Tabs/Content'
import Info from '@/components/TorrentDetailModal/Tabs/Info'
import Peers from '@/components/TorrentDetailModal/Tabs/Peers'
import Trackers from '@/components/TorrentDetailModal/Tabs/Trackers'
import Tags from '@/components/TorrentDetailModal/Tabs/Tags'
export { Content, Info, Peers, Trackers, Tags }

View file

@ -80,3 +80,17 @@ export function networkSize(size) {
}
Vue.filter('networkSize', networkSize)
function getDataUnit(value) {
if (!value) return ''
return value.substring(value.indexOf(' '))
}
Vue.filter('getDataUnit', getDataUnit)
function getDataValue(value) {
if (!value) return ''
return value.substring(0, value.indexOf(' '))
}
Vue.filter('getDataValue', getDataValue)

14
src/mixins/General.js Normal file
View file

@ -0,0 +1,14 @@
import { v4 as uuidv4 } from 'uuid'
export default {
methods: {
createModal(name, props) {
let component = {
component: name,
props,
guid: uuidv4()
}
this.$store.commit('ADD_MODAL', component)
}
}
}

View file

@ -1,14 +1,23 @@
import { mapGetters } from 'vuex'
export default {
props: ['guid'],
computed: {
...mapGetters(['getModalState']),
dialog: {
get() {
return this.getModalState(this.$options.name)
return this.getModalState(this.guid)
},
set() {
this.$store.commit('TOGGLE_MODAL', this.$options.name)
this.deleteModal()
}
}
},
methods: {
deleteModal() {
setTimeout(() => this.$store.commit('DELETE_MODAL', this.guid), 100)
}
},
beforeDestroy() {
this.deleteModal()
}
}

13
src/mixins/Tab.js Normal file
View file

@ -0,0 +1,13 @@
export default {
props: {
hash: String,
isActive: Boolean
},
watch: {
isActive(active) {
if (active) {
this.activeMethod()
}
}
}
}

View file

@ -1,5 +1,7 @@
import FullScreenModal from '@/mixins/FullScreenModal'
import Modal from '@/mixins/Modal'
import SettingsTab from '@/mixins/SettingsTab'
import FullScreenModal from './FullScreenModal'
import Modal from './Modal'
import SettingsTab from './SettingsTab'
import Tab from './Tab'
import General from './General'
export { FullScreenModal, Modal, SettingsTab }
export { FullScreenModal, Modal, SettingsTab, Tab, General }

View file

@ -21,6 +21,7 @@ export default class Torrent {
this.progress = data.progress * 100
this.ratio = Math.round(data.ratio * 100)
this.tags = data.tags.length > 0 ? data.tags.split(',') : null
this.category = data.category
}
formatState(state) {

View file

@ -40,6 +40,14 @@ class Qbit {
return this.axios.get('/app/preferences')
}
setPreferences(params) {
const data = new URLSearchParams({
json: JSON.stringify(params)
})
return this.axios.post('/app/setPreferences', data)
}
getMainData(rid) {
const params = {
rid
@ -78,14 +86,6 @@ class Qbit {
return this.setPreferences(params)
}
setPreferences(params) {
const data = new URLSearchParams({
json: JSON.stringify(params)
})
return this.axios.post('/app/setPreferences', data)
}
setTorrentFilePriority(hash, idList, priority) {
const idListStr = idList.join('|')
const params = {
@ -253,6 +253,50 @@ class Qbit {
return this.axios.post('/torrents/deleteTags ', data)
}
// Begin Categories
getCategories() {
return this.axios.get('/torrents/categories')
}
deleteCategory(cat) {
const params = {
categories: cat
}
const data = new URLSearchParams(params)
return this.axios.post('/torrents/removeCategories ', data)
}
createCategory(cat) {
const params = {
category: cat.name,
savePath: cat.savePath
}
const data = new URLSearchParams(params)
return this.axios.post('/torrents/createCategory ', data)
}
setCategory(hash, cat) {
const params = {
hashes: hash,
category: cat
}
const data = new URLSearchParams(params)
return this.axios.post('/torrents/setCategory ', data)
}
editCategory(cat) {
const params = {
category: cat.name,
savePath: cat.savePath
}
const data = new URLSearchParams(params)
return this.axios.post('/torrents/editCategory ', data)
}
// End Categories
actionTorrents(action, hashes, extra) {
const params = {
hashes: hashes.join('|'),

View file

@ -14,7 +14,7 @@ export default {
Vue.$toast.success('Successfully logged in!')
context.commit('LOGIN', true)
context.commit('updateMainData')
context.commit('SET_SETTINGS')
context.commit('FETCH_SETTINGS')
return true
}
Vue.$toast.error('Log in failed 😕')

View file

@ -1,11 +1,14 @@
export default {
containsTorrent: state => hash => state.selected_torrents.includes(hash),
getTheme: state => () => state.webuiSettings.darkTheme,
getModalState: state => name => state.modals[name.toLowerCase()],
getModalState: state => guid =>
state.modals.filter(m => m.guid === guid)[0],
getSettings: state => () => state.settings,
getStatus: state => () => state.status,
getTorrent: state => hash =>
state.torrents.filter(el => el.hash === hash)[0],
getWebuiSettings: state => () => state.webuiSettings,
getAvailableTags: state => () => state.status.tags
getAvailableTags: state => () => state.status.tags,
getCategories: state => () => state.categories,
getModals: state => () => state.modals
}

View file

@ -31,19 +31,13 @@ export default new Vuex.Store({
},
rid: 0,
pasteUrl: null,
modals: {
addmodal: false,
deletemodal: false,
settingsmodal: false,
olduimodal: false,
torrentdetailmodal: false
},
modals: [],
settings: {},
webuiSettings: {
darkTheme: false,
showFreeSpace: true
},
selectedDetailTorrent: null
categories: []
},
getters: {
...getters

View file

@ -6,8 +6,11 @@ export default {
REMOVE_INTERVALS: state => {
state.intervals.forEach(el => clearInterval(el))
},
TOGGLE_MODAL(state, modal) {
state.modals[modal.toLowerCase()] = !state.modals[modal.toLowerCase()]
ADD_MODAL(state, modal) {
state.modals.push(modal)
},
DELETE_MODAL(state, guid) {
state.modals = state.modals.filter(m => m.guid !== guid)
},
SET_SELECTED: (state, payload) => {
if (payload.type === 'add') state.selected_torrents.push(payload.hash)
@ -50,17 +53,18 @@ export default {
state.torrents.push(new Torrent({ hash: key, ...value }))
}
},
SET_SETTINGS: async state => {
FETCH_SETTINGS: async state => {
const { data } = await qbit.getAppPreferences()
state.settings = data
},
SET_SELECTED_TORRENT_DETAIL: (state, hash) => {
state.selectedDetailTorrent = hash
},
UPDATE_SORT_OPTIONS: (state, payload) => {
state.sort_options.sort = payload.name
state.sort_options.reverse = payload.reverse
state.sort_options.hashes = payload.hashes ? payload.hashes : null
state.sort_options.filter = payload.filter ? payload.filter : null
},
FETCH_CATEGORIES: async state => {
const { data } = await qbit.getCategories()
state.categories = data
}
}

View file

@ -31,20 +31,18 @@
</div>
</div>
</v-container>
<TorrentDetailModal />
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import Torrent from '@/components/Torrent'
import TorrentDetailModal from '@/components/TorrentDetailModal/TorrentDetailModal'
import { getPropName, sortOrFilter, filterOption } from '@/helpers'
export default {
name: 'Dashboard',
components: { Torrent, TorrentDetailModal },
components: { Torrent },
data() {
return {
sort_input: ''