feat: Add tag support in Dashboard, TorrentDetail and Add Modal (#570)

This commit is contained in:
Rémi Marseault 2022-12-30 11:02:45 +01:00 committed by GitHub
parent e60e1ee136
commit 0a175ccb43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 185 additions and 58 deletions

View file

@ -56,7 +56,7 @@
</template>
</v-file-input>
<v-textarea
v-if="files.length == 0"
v-if="files.length === 0"
v-model="urls"
style="max-height: 200px; overflow-x: hidden; overflow-y: auto"
:label="$t('url')"
@ -72,7 +72,8 @@
</v-col>
</v-row>
<v-combobox v-model="category" :items="availableCategories" clearable :label="$t('category')" item-text="name" :prepend-icon="mdiTag" @input="categoryChanged" />
<v-combobox v-model="tags" :items="availableTags" clearable :label="$t('tags')" :prepend-icon="mdiTag" multiple chips />
<v-combobox v-model="category" :items="availableCategories" clearable :label="$t('category')" item-text="name" :prepend-icon="mdiShape" @input="categoryChanged" />
<v-text-field
v-model="directory"
@ -141,7 +142,7 @@
<script>
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { mdiCloudUpload, mdiFolder, mdiTag, mdiPaperclip, mdiLink, mdiClose } from '@mdi/js'
import { mdiCloudUpload, mdiFolder, mdiTag, mdiShape, mdiPaperclip, mdiLink, mdiClose } from '@mdi/js'
import { FullScreenModal, Modal } from '@/mixins'
export default {
@ -155,6 +156,7 @@ export default {
showWrapDrag: false,
files: [],
category: null,
tags: [],
directory: '',
start: true,
skip_checking: false,
@ -178,13 +180,14 @@ export default {
mdiCloudUpload,
mdiFolder,
mdiTag,
mdiShape,
mdiPaperclip,
mdiLink,
mdiClose
}
},
computed: {
...mapGetters(['getSettings', 'getCategories']),
...mapGetters(['getSettings', 'getCategories', 'getAvailableTags']),
validFile() {
return this.Files.length > 0
},
@ -201,12 +204,15 @@ export default {
},
availableCategories() {
return this.getCategories()
},
availableTags() {
return this.getAvailableTags()
}
},
created() {
this.urls = this.initialMagnet
this.setSettings()
if (this.openSuddenly == true) {
if (this.openSuddenly === true) {
this.dTransition = 'none'
}
},
@ -217,6 +223,7 @@ export default {
async setSettings() {
await this.$store.dispatch('FETCH_SETTINGS')
await this.$store.commit('FETCH_CATEGORIES')
await this.$store.commit('FETCH_TAGS')
const settings = this.getSettings()
this.start = !settings.start_paused_enabled
this.autoTMM = settings.auto_tmm_enabled
@ -259,6 +266,7 @@ export default {
if (this.files.length) torrents.push(...this.files)
if (this.urls) params.urls = this.urls
if (this.category) params.category = this.category.name
if (this.tags) params.tags = this.tags.join(',')
if (!this.autoTMM) params.savepath = this.directory
qbit.addTorrents(params, torrents)
@ -275,6 +283,7 @@ export default {
this.url = null
this.files = []
this.category = null
this.tags = []
this.directory = this.savepath
this.skip_checking = null
},

View file

@ -37,12 +37,16 @@ export default {
rules: [v => !!v || 'Tag is required'],
valid: false
}),
created() {
this.$store.commit('FETCH_TAGS')
},
methods: {
create() {
qbit.createTag(this.tagname)
this.cancel()
},
cancel() {
this.$store.commit('FETCH_TAGS')
this.dialog = false
}
}

View file

@ -39,6 +39,25 @@
@input="setCategory"
/>
</div>
<div id="tag_filter">
<label class="white--text text-uppercase font-weight-medium caption ml-4">
{{ $t('tags') }}
</label>
<v-select
aria-label="tag_filter"
:value="selectedTag"
flat
solo
class="ml-2 mr-2"
:label="$t('tag')"
:items="availableTags"
item-text="name"
color="download"
item-color="download"
background-color="secondary"
@input="setTag"
/>
</div>
<div id="tracker_filter" v-if="showTrackerFilter">
<label class="white--text text-uppercase font-weight-medium caption ml-4"> Tracker </label>
<v-select
@ -67,10 +86,11 @@ export default {
data: () => ({
selectedState: null,
selectedCategory: null,
selectedTag: null,
selectedTracker: null
}),
computed: {
...mapGetters(['getCategories', 'getTrackers']),
...mapGetters(['getCategories', 'getAvailableTags', 'getTrackers']),
...mapState(['sort_options']),
options() {
return [
@ -107,6 +127,13 @@ export default {
return categories
},
availableTags() {
return [
{name: 'All', value: null},
{name: 'Untagged', value: ''},
...this.getAvailableTags()
]
},
availableTrackers() {
const trackers = [
{ name: 'All', value: null },
@ -135,12 +162,14 @@ export default {
this.$store.commit('UPDATE_SORT_OPTIONS', {
filter: this.selectedState,
category: this.selectedCategory,
tag: this.selectedTag,
tracker: this.selectedTracker
})
},
loadFilter() {
this.selectedState = this.$store.state.sort_options.filter
this.selectedCategory = this.$store.state.sort_options.category
this.selectedTag = this.$store.state.sort_options.tag
this.selectedTracker = this.$store.state.sort_options.tracker
},
setState(value) {
@ -151,6 +180,10 @@ export default {
this.selectedCategory = value
this.commitFilter()
},
setTag(value) {
this.selectedTag = value
this.commitFilter()
},
setTracker(value) {
this.selectedTracker = value
this.commitFilter()

View file

@ -68,7 +68,6 @@ export default {
hash: String
},
data: () => ({
selectedCategory: null,
mdiDelete,
mdiPencil
}),
@ -83,13 +82,12 @@ export default {
},
created() {
this.$store.commit('FETCH_CATEGORIES')
this.$store.commit('FETCH_TAGS')
},
methods: {
activeMethod() {
this.$store.commit('FETCH_CATEGORIES')
},
deleteTag(item) {
qbit.deleteTag(item)
this.$store.commit('FETCH_TAGS')
},
createTag() {
this.createModal('CreateTagDialog')
@ -101,6 +99,10 @@ export default {
qbit.deleteCategory(category.name)
this.$store.commit('FETCH_CATEGORIES')
},
deleteTag(item) {
qbit.deleteTag(item)
this.$store.commit('FETCH_TAGS')
},
editCategory(cat) {
this.createModal('CreateCategoryDialog', { initialCategory: cat })
}

View file

@ -4,7 +4,7 @@
{{ $t('tags') }}
</div>
<v-row wrap class="ma-0">
<v-chip v-for="tag in torrent.tags" :key="tag" small :class="theme === 'light' ? 'white--text' : 'black--text'" class="download caption mb-1 mx-1">
<v-chip v-for="tag in torrent.tags" :key="tag" small class="tags white--text caption mb-1 mx-1">
{{ tag }}
</v-chip>
</v-row>
@ -18,3 +18,9 @@ export default {
props: ['torrent']
}
</script>
<style>
.tags {
background-color: #048b9a !important;
}
</style>

View file

@ -5,13 +5,16 @@
{{ torrent.name }}
</span>
</v-flex>
<v-flex xs12 row class="ma-1 mt-0">
<v-chip small class="caption white--text mr-2" :class="torrent.state.toLowerCase()" style="height: 20px">
<v-flex xs12 row class="ma-1 mt-0 chipgap">
<v-chip small class="caption white--text" :class="torrent.state.toLowerCase()" style="height: 20px">
{{ torrent.state }}
</v-chip>
<v-chip v-if="torrent.category" small class="upload caption white--text" style="height: 20px">
{{ torrent.category }}
</v-chip>
<v-chip v-if="torrent.tags" v-for="tag in torrent.tags" small class="tags caption white--text" style="height: 20px">
{{ tag }}
</v-chip>
</v-flex>
<v-flex xs12 class="pa-0 ma-1 row">
<span class="body-2"> {{ torrent.dloaded | getDataValue }} </span>
@ -70,3 +73,12 @@ export default {
})
}
</script>
<style>
.chipgap {
gap: 8px;
}
.tags {
background-color: #048b9a !important;
}
</style>

View file

@ -113,6 +113,35 @@
</v-list-item>
</v-list>
</v-menu>
<v-menu v-if="availableTags.length > 0" :open-on-hover="!touchmode" top offset-x :transition="isRightside ? 'slide-x-reverse-transition' : 'slide-x-transition'" :left="isRightside">
<template #activator="{ on }">
<v-list-item link v-on="on">
<v-icon>{{ mdiTag }}</v-icon>
<v-list-item-title class="ml-2 list-item__title">
{{ $t('rightClick.tags') | titleCase }}
</v-list-item-title>
<v-list-item-action>
<v-icon>{{ mdiChevronRight }}</v-icon>
</v-list-item-action>
</v-list-item>
</template>
<v-list>
<v-list-item v-for="(tag, index) in availableTags" :key="index" link @click="setTag(tag)">
<v-icon>
{{ torrent.tags !== null && torrent.tags.includes(tag) ? mdiCheckboxMarked : mdiCheckboxBlankOutline }}
</v-icon>
<v-list-item-title class="ml-2 list-item__title">
{{ tag }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-list-item v-else>
<v-icon>{{ mdiTagOff }}</v-icon>
<v-list-item-title class="ml-2 list-item__title">
{{ $t('rightClick.notags') | titleCase }}
</v-list-item-title>
</v-list-item>
<v-menu :open-on-hover="!touchmode" top offset-x :transition="isRightside ? 'slide-x-reverse-transition' : 'slide-x-transition'" :left="isRightside">
<template #activator="{ on }">
<v-list-item link v-on="on">
@ -210,35 +239,37 @@
</template>
<script>
import { mapGetters, mapState } from 'vuex'
import {mapGetters, mapState} from 'vuex'
import qbit from '@/services/qbit'
import { General, TorrentSelect } from '@/mixins'
import {General, TorrentSelect} from '@/mixins'
import {
mdiBullhorn,
mdiPlaylistCheck,
mdiArrowUp,
mdiArrowDown,
mdiPriorityLow,
mdiInformation,
mdiRenameBox,
mdiFolder,
mdiDelete,
mdiAccountGroup,
mdiPlay,
mdiPause,
mdiSelect,
mdiPriorityHigh,
mdiChevronRight,
mdiFastForward,
mdiShape,
mdiHeadCog,
mdiCheckboxMarked,
mdiArrowDown,
mdiArrowUp,
mdiBullhorn,
mdiCheckboxBlankOutline,
mdiSpeedometerSlow,
mdiChevronUp,
mdiCheckboxMarked,
mdiChevronDown,
mdiChevronRight,
mdiChevronUp,
mdiContentCopy,
mdiMagnet
mdiDelete,
mdiFastForward,
mdiFolder,
mdiHeadCog,
mdiInformation,
mdiMagnet,
mdiPause,
mdiPlay,
mdiPlaylistCheck,
mdiPriorityHigh,
mdiPriorityLow,
mdiRenameBox,
mdiSelect,
mdiShape,
mdiSpeedometerSlow,
mdiTag,
mdiTagOff
} from '@mdi/js'
export default {
@ -273,6 +304,8 @@ export default {
mdiBullhorn,
mdiChevronRight,
mdiShape,
mdiTag,
mdiTagOff,
mdiHeadCog,
mdiCheckboxMarked,
mdiCheckboxBlankOutline,
@ -283,7 +316,7 @@ export default {
}
},
computed: {
...mapGetters(['getCategories']),
...mapGetters(['getCategories', 'getAvailableTags']),
...mapState(['selected_torrents']),
availableCategories() {
const categories = [{ name: 'None', value: '' }]
@ -295,6 +328,9 @@ export default {
return categories
},
availableTags() {
return this.getAvailableTags()
},
hashes() {
if (this.multiple) return this.selected_torrents
@ -357,6 +393,18 @@ export default {
setCategory(cat) {
qbit.setCategory(this.hashes, cat)
},
setTag(tag) {
if (this.torrent.tags && this.torrent.tags.includes(tag))
return this.removeTag(tag)
else
return this.addTag(tag)
},
addTag(tag) {
qbit.addTorrentTag(this.hashes, tag)
},
removeTag(tag) {
qbit.removeTorrentTag(this.hashes, tag)
},
toggleSeq() {
qbit.toggleSequentialDownload(this.hashes)
},

View file

@ -4,8 +4,8 @@
<v-col cols="12" md="6">
<v-subheader>{{ $t('modals.detail.pageTagsAndCategories.subHeaderTag') }}</v-subheader>
<v-list-item-group :value="activeTags" active-class="accent--text" multiple>
<template v-for="(item, index) in availableTags">
<v-list-item :key="item.title" @click="addTag(item)">
<template v-for="(item, index) in availableTags" :key="item.title">
<v-list-item link @click="addTag(item)">
<v-list-item-content>
<v-list-item-title v-text="item" />
</v-list-item-content>
@ -18,8 +18,8 @@
<v-col cols="12" md="6">
<v-subheader>{{ $t('modals.detail.pageTagsAndCategories.subHeaderCategories') }}</v-subheader>
<v-list-item-group :value="activeCategory" active-class="accent--text">
<template v-for="(item, index) in availableCategories">
<v-list-item :key="item.title" @click="setCategory(item)">
<template v-for="(item, index) in availableCategories" :key="item.title">
<v-list-item link @click="setCategory(item)">
<v-list-item-content>
<v-list-item-title v-text="item.name" />
</v-list-item-content>
@ -74,22 +74,25 @@ export default {
},
created() {
this.$store.commit('FETCH_CATEGORIES')
this.$store.commit('FETCH_TAGS')
},
methods: {
addTag(tag) {
if (this.activeTags.includes(this.availableTags.indexOf(tag))) {
return this.deleteTag(tag)
}
qbit.addTorrentTag(this.hash, tag)
return qbit.addTorrentTag([this.hash], tag)
},
deleteTag(tag) {
qbit.removeTorrentTag(this.hash, tag)
qbit.removeTorrentTag([this.hash], tag)
},
setCategory(cat) {
if (this.torrent.category === cat.name) {
return qbit.setCategory([this.hash], '')
return this.deleteCategory()
}
qbit.setCategory([this.hash], cat.name)
return qbit.setCategory([this.hash], cat.name)
},
deleteCategory() {
qbit.setCategory([this.hash], '')

View file

@ -2,6 +2,6 @@ import Content from './Content.vue'
import Info from './Info.vue'
import DetailPeers from './DetailPeers.vue'
import Trackers from './Trackers.vue'
import TagsAndCategories from './TorrentTagsAndCategories.vue'
import TorrentTagsAndCategories from './TorrentTagsAndCategories.vue'
export { Content, Info, DetailPeers, Trackers, TagsAndCategories }
export { Content, Info, DetailPeers, Trackers, TorrentTagsAndCategories }

View file

@ -430,6 +430,8 @@ const locale = {
decrease: 'decrease'
},
category: 'set category',
tags: 'set tags',
notags: 'no tags',
limit: 'set limit',
copy: 'copy',
info: 'show info'

View file

@ -368,6 +368,8 @@ const locale = {
decrease: 'Diminuer'
},
category: 'Définir la catégorie',
tags: 'Définir les tags',
notags: 'Aucun tag',
limit: 'Définir la limite',
copy: 'copier',
info: 'afficher les informations'

View file

@ -101,7 +101,8 @@ export class QBitApi {
reverse: payload.reverse,
hashes: payload.hashes ? payload.hashes.join('|') : null,
filter: payload.filter ? payload.filter : null,
category: payload.category !== null ? payload.category : null
category: payload.category !== null ? payload.category : null,
tag: payload.tag !== null ? payload.tag : null
}
// clean
@ -141,7 +142,7 @@ export class QBitApi {
}
getAvailableTags() {
return this.axios.get('/torrents/tags')
return this.axios.get('/torrents/tags').then(res => res.data)
}
getTorrentProperties(hash) {
@ -384,16 +385,16 @@ export class QBitApi {
}
/** Begin Torrent Tags **/
removeTorrentTag(hash, tag) {
removeTorrentTag(hashes, tag) {
return this.execute('post', '/torrents/removeTags', {
hashes: hash,
hashes: hashes.join('|'),
tags: tag
})
}
addTorrentTag(hash, tag) {
addTorrentTag(hashes, tag) {
return this.execute('post', '/torrents/addTags ', {
hashes: hash,
hashes: hashes.join('|'),
tags: tag
})
}

View file

@ -17,6 +17,7 @@ export default {
context.commit('updateMainData')
context.commit('FETCH_SETTINGS')
context.commit('FETCH_CATEGORIES')
context.commit('FETCH_TAGS')
return true
}

View file

@ -49,6 +49,7 @@ export default new Vuex.Store({
hashes: [],
filter: null,
category: null,
tag: null,
tracker: null
},
rid: 0,

View file

@ -1,6 +1,6 @@
import qbit from '../services/qbit'
import { DocumentTitle, Tags, Trackers, Torrents, Graph, ServerStatus } from '@/actions'
import { setLanguage } from '../plugins/i18n'
import { setLanguage } from '@/plugins/i18n'
export default {
SET_APP_VERSION(state, version) {
@ -65,13 +65,15 @@ export default {
FETCH_SETTINGS: async (state, settings) => {
state.settings = settings
},
UPDATE_SORT_OPTIONS: (state, { hashes = [], filter = null, category = null, tracker = null }) => {
UPDATE_SORT_OPTIONS: (state, { hashes = [], filter = null, category = null, tag = null, tracker = null }) => {
state.sort_options.hashes = hashes
state.sort_options.filter = filter
state.sort_options.category = category
state.sort_options.tag = tag
state.sort_options.tracker = tracker
},
FETCH_CATEGORIES: async state => (state.categories = Object.values(await qbit.getCategories())),
FETCH_TAGS: async state => (state.tags = await qbit.getAvailableTags()),
FETCH_FEEDS: async state => (state.rss.feeds = await qbit.getFeeds()),
FETCH_RULES: async state => (state.rss.rules = await qbit.getRules()),
FETCH_SEARCH_PLUGINS: async state => (state.searchPlugins = await qbit.getSearchPlugins()),

View file

@ -287,6 +287,7 @@ export default {
created() {
this.$store.dispatch('INIT_INTERVALS')
this.$store.commit('FETCH_CATEGORIES')
this.$store.commit('FETCH_TAGS')
if (this.input) this.searchFilterEnabled = true
},
beforeDestroy() {

View file

@ -50,7 +50,7 @@
<Content :is-active="tab === 'content'" :hash="hash" />
</v-tab-item>
<v-tab-item eager value="tagsAndCategories">
<TagsAndCategories v-if="torrent" :torrent="torrent" :is-active="tab === 'tagsAndCategories'" :hash="hash" />
<TorrentTagsAndCategories v-if="torrent" :torrent="torrent" :is-active="tab === 'tagsAndCategories'" :hash="hash" />
</v-tab-item>
</v-tabs-items>
</v-card-text>
@ -60,12 +60,12 @@
<script>
import { mapGetters } from 'vuex'
import { Content, Info, DetailPeers, Trackers, TagsAndCategories } from '../components/TorrentDetail/Tabs'
import { Content, Info, DetailPeers, Trackers, TorrentTagsAndCategories } from '../components/TorrentDetail/Tabs'
import { mdiClose } from '@mdi/js'
export default {
name: 'TorrentDetail',
components: { Content, Info, DetailPeers, Trackers, TagsAndCategories },
components: { Content, Info, DetailPeers, Trackers, TorrentTagsAndCategories },
data() {
return {
tab: null,