mirror of
https://github.com/VueTorrent/VueTorrent.git
synced 2024-11-24 18:36:14 +03:00
perf(search): Add tabs + reset results on new search (#958)
This commit is contained in:
parent
516f1d7913
commit
2de5f85798
3 changed files with 214 additions and 138 deletions
161
src/components/SearchEngine/SearchTab.vue
Normal file
161
src/components/SearchEngine/SearchTab.vue
Normal file
|
@ -0,0 +1,161 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card flat>
|
||||
<v-list-item class="searchCriterias">
|
||||
<v-row class="my-2">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="searchPattern"
|
||||
dense
|
||||
hide-details
|
||||
clearable
|
||||
:rules="[v => !!v || 'Search term is required']"
|
||||
label="Search pattern"
|
||||
@keydown.enter.prevent="runNewSearch"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="5" md="2">
|
||||
<v-select v-model="searchCategory" height="1" flat dense hide-details outlined :items="categories" label="Search category" />
|
||||
</v-col>
|
||||
<v-col cols="6" sm="5" md="2">
|
||||
<v-select v-model="searchPlugin" height="1" flat dense hide-details outlined :items="plugins" label="Search plugins" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2" class="d-flex align-center justify-center">
|
||||
<v-btn v-if="queryId === 0" class="mx-auto accent white--text elevation-0 px-4" @click="runNewSearch">
|
||||
{{ $t('search.runNewSearch') }}
|
||||
</v-btn>
|
||||
<v-btn v-else class="mx-auto warning white--text elevation-0 px-4" @click="stopSearch">
|
||||
{{ $t('search.stopSearch') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
|
||||
<v-card flat class="mt-5">
|
||||
<v-list-item class="searchResults">
|
||||
<v-data-table style="width: 100%" id="searchResultsTable" :headers="headers" :items="filteredResults" :search="resultFilter" :custom-filter="customFilter">
|
||||
<template v-slot:top>
|
||||
<v-row class="mt-2">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field v-model="resultFilter" dense hide-details label="Filter" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-slot:item.fileSize="{ item }">
|
||||
{{ item.fileSize | formatSize }}
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<span class="d-flex flex-row">
|
||||
<v-icon @click="downloadTorrent(item)">{{ mdiDownload }}</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
import {mapGetters} from 'vuex'
|
||||
import {mdiDownload} from '@mdi/js'
|
||||
import {SearchPlugin, SearchResult} from "@/types/qbit/models";
|
||||
import {General} from "@/mixins";
|
||||
import qbit from "@/services/qbit";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchTab",
|
||||
mixins: [General],
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{ text: 'Filename', value: 'fileName' },
|
||||
{ text: 'File Size', value: 'fileSize' },
|
||||
{ text: 'Seeders', value: 'nbSeeders' },
|
||||
{ text: 'Leechers', value: 'nbLeechers' },
|
||||
{ text: 'Site URL', value: 'siteUrl' },
|
||||
{ text: '', value: 'actions', sortable: false }
|
||||
],
|
||||
searchPattern: '',
|
||||
searchCategory: 'all',
|
||||
searchPlugin: 'enabled',
|
||||
queryId: 0,
|
||||
queryTimer: NaN as NodeJS.Timer,
|
||||
queryResults: [] as SearchResult[],
|
||||
resultFilter: '',
|
||||
mdiDownload
|
||||
}
|
||||
},
|
||||
async beforeDestroy() {
|
||||
await this.stopSearch()
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getSearchPlugins']),
|
||||
categories() {
|
||||
const cats = [
|
||||
{ text: 'Movies', value: 'movies' },
|
||||
{ text: 'TV shows', value: 'tv' },
|
||||
{ text: 'Music', value: 'music' },
|
||||
{ text: 'Games', value: 'games' },
|
||||
{ text: 'Anime', value: 'anime' },
|
||||
{ text: 'Software', value: 'software' },
|
||||
{ text: 'Pictures', value: 'pictures' },
|
||||
{ text: 'Books', value: 'books' }
|
||||
]
|
||||
cats.sort((a, b) => a.text.localeCompare(b.text))
|
||||
|
||||
return [{ text: 'All categories', value: 'all' }, ...cats]
|
||||
},
|
||||
plugins() {
|
||||
const plugins = [
|
||||
{ text: 'All plugins', value: 'all' },
|
||||
{ text: 'Only enabled', value: 'enabled' }
|
||||
]
|
||||
|
||||
this.getSearchPlugins().filter((plugin: SearchPlugin) => plugin.enabled).forEach((plugin: SearchPlugin) => plugins.push({ text: plugin.fullName, value: plugin.name }))
|
||||
|
||||
return plugins
|
||||
},
|
||||
filteredResults() {
|
||||
return this.queryResults
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
downloadTorrent(item: SearchResult) {
|
||||
this.createModal('AddModal', { initialMagnet: item.fileUrl })
|
||||
},
|
||||
async runNewSearch() {
|
||||
const searchJob = await qbit.startSearch(this.searchPattern, this.searchCategory, [this.searchPlugin])
|
||||
this.queryId = searchJob.id
|
||||
this.queryResults = []
|
||||
this.queryTimer = setInterval(() => this.refreshResults(), 1000)
|
||||
},
|
||||
async stopSearch() {
|
||||
if (this.queryId !== 0) await qbit.stopSearch(this.queryId)
|
||||
|
||||
this.queryId = 0
|
||||
clearInterval(this.queryTimer)
|
||||
},
|
||||
customFilter(value: any, search: string | null) {
|
||||
return value != null
|
||||
&& search != null
|
||||
&& typeof value === 'string'
|
||||
&& search.trim().toLowerCase().split(' ').every(i => value.toString().toLowerCase().indexOf(i) !== -1)
|
||||
},
|
||||
async refreshResults() {
|
||||
const response = await qbit.getSearchResults(this.queryId, this.queryResults.length)
|
||||
this.queryResults.push(...response.results)
|
||||
|
||||
if (response.status === 'Stopped') {
|
||||
this.queryId = 0
|
||||
await this.stopSearch()
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
|
@ -101,6 +101,7 @@
|
|||
},
|
||||
"search": {
|
||||
"title": "Search torrents",
|
||||
"tabHeaderTemplate": "Tab #$0",
|
||||
"runNewSearch": "Search",
|
||||
"stopSearch": "Stop"
|
||||
},
|
||||
|
|
|
@ -18,170 +18,85 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card flat>
|
||||
<v-list-item class="searchCriterias">
|
||||
<v-row class="my-2">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="searchPattern"
|
||||
dense
|
||||
hide-details
|
||||
clearable
|
||||
:rules="[v => !!v || 'Search term is required']"
|
||||
label="Search pattern"
|
||||
@keydown.enter.prevent="runNewSearch"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="5" md="2">
|
||||
<v-select v-model="searchCategory" height="1" flat dense hide-details outlined :items="categories" label="Search category" />
|
||||
</v-col>
|
||||
<v-col cols="6" sm="5" md="2">
|
||||
<v-select v-model="searchPlugin" height="1" flat dense hide-details outlined :items="plugins" label="Search plugins" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2" class="d-flex align-center justify-center">
|
||||
<v-btn v-if="queryId === 0" class="mx-auto accent white--text elevation-0 px-4" @click="runNewSearch">
|
||||
{{ $t('search.runNewSearch') }}
|
||||
</v-btn>
|
||||
<v-btn v-else class="mx-auto warning white--text elevation-0 px-4" @click="stopSearch">
|
||||
{{ $t('search.stopSearch') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
<v-row class="ma-0 pa-0">
|
||||
<v-container class="d-flex align-center justify-center ma-0 pa-0 primary" fluid>
|
||||
<v-tabs v-model="tab" align-with-title show-arrows background-color="primary" slider-color="white" class="overflow-auto">
|
||||
<v-tab v-for="t in tabs" :href="`#${t.value}`" class="white--text">
|
||||
<h4>{{ $t('search.tabHeaderTemplate').replace("$0", t.id) }}</h4>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-spacer />
|
||||
<v-btn icon @click="createNewTab" class="mr-1">
|
||||
<v-icon color="accent">{{ mdiPlusCircleOutline }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon @click="deleteTab" :disabled="tabs.length === 0" class="mx-1">
|
||||
<v-icon color="error">{{ mdiMinusCircleOutline }}</v-icon>
|
||||
</v-btn>
|
||||
</v-container>
|
||||
|
||||
<v-card flat class="mt-5">
|
||||
<v-list-item class="searchResults">
|
||||
<v-data-table style="width: 100%" id="searchResultsTable" :headers="headers" :items="filteredResults" :search="resultFilter" :custom-filter="customFilter">
|
||||
<template v-slot:top>
|
||||
<v-row class="mt-2">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field v-model="resultFilter" dense hide-details label="Filter" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-slot:item.fileSize="{ item }">
|
||||
{{ item.fileSize | formatSize }}
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<span class="search-actions">
|
||||
<v-icon @click="downloadTorrent(item)">{{ mdiDownload }}</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
<v-tabs-items v-model="tab" touchless class="full-width">
|
||||
<v-tab-item v-for="t in tabs" :key="t.id" eager :value="t.value">
|
||||
<SearchTab />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
import {mapGetters} from 'vuex'
|
||||
import {mdiClose, mdiDownload, mdiToyBrick} from '@mdi/js'
|
||||
import qbit from '@/services/qbit'
|
||||
import {General} from '@/mixins'
|
||||
import {SearchPlugin, SearchResult} from '@/types/qbit/models'
|
||||
import {mdiClose, mdiToyBrick, mdiPlusCircleOutline, mdiMinusCircleOutline} from '@mdi/js'
|
||||
import {FullScreenModal, General} from '@/mixins'
|
||||
import SearchTab from '@/components/SearchEngine/SearchTab.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SearchEngine',
|
||||
mixins: [General],
|
||||
components: {SearchTab},
|
||||
mixins: [General, FullScreenModal],
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{ text: 'Filename', value: 'fileName' },
|
||||
{ text: 'File Size', value: 'fileSize' },
|
||||
{ text: 'Seeders', value: 'nbSeeders' },
|
||||
{ text: 'Leechers', value: 'nbLeechers' },
|
||||
{ text: 'Site URL', value: 'siteUrl' },
|
||||
{ text: '', value: 'actions', sortable: false }
|
||||
],
|
||||
searchPattern: '',
|
||||
searchCategory: 'all',
|
||||
searchPlugin: 'enabled',
|
||||
queryId: 0,
|
||||
queryTimer: NaN as NodeJS.Timer,
|
||||
queryResults: [] as SearchResult[],
|
||||
resultFilter: '',
|
||||
tabs: [] as { id: number, value: string }[],
|
||||
tabCount: 0,
|
||||
mdiClose,
|
||||
mdiDownload,
|
||||
mdiToyBrick
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getSearchPlugins', 'getModals']),
|
||||
categories() {
|
||||
const cats = [
|
||||
{ text: 'Movies', value: 'movies' },
|
||||
{ text: 'TV shows', value: 'tv' },
|
||||
{ text: 'Music', value: 'music' },
|
||||
{ text: 'Games', value: 'games' },
|
||||
{ text: 'Anime', value: 'anime' },
|
||||
{ text: 'Software', value: 'software' },
|
||||
{ text: 'Pictures', value: 'pictures' },
|
||||
{ text: 'Books', value: 'books' }
|
||||
]
|
||||
cats.sort((a, b) => a.text.localeCompare(b.text))
|
||||
|
||||
return [{ text: 'All categories', value: 'all' }, ...cats]
|
||||
},
|
||||
plugins() {
|
||||
const plugins = [
|
||||
{ text: 'All plugins', value: 'all' },
|
||||
{ text: 'Only enabled', value: 'enabled' }
|
||||
]
|
||||
|
||||
this.getSearchPlugins().filter((plugin: SearchPlugin) => plugin.enabled).forEach((plugin: SearchPlugin) => plugins.push({ text: plugin.fullName, value: plugin.name }))
|
||||
|
||||
return plugins
|
||||
},
|
||||
filteredResults() {
|
||||
return this.queryResults
|
||||
mdiToyBrick,
|
||||
mdiPlusCircleOutline,
|
||||
mdiMinusCircleOutline
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.$store.dispatch('FETCH_SEARCH_PLUGINS')
|
||||
document.addEventListener('keydown', this.handleKeyboardShortcut)
|
||||
this.createNewTab()
|
||||
},
|
||||
async beforeDestroy() {
|
||||
await this.stopSearch()
|
||||
document.removeEventListener('keydown', this.handleKeyboardShortcut)
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getModals'])
|
||||
},
|
||||
methods: {
|
||||
openPluginManager() {
|
||||
this.createModal('SearchPluginManager')
|
||||
},
|
||||
createNewTab() {
|
||||
this.tabs.push({
|
||||
id: this.tabCount++,
|
||||
value: `tab-${this.tabCount}`
|
||||
})
|
||||
},
|
||||
deleteTab() {
|
||||
this.tabs.find((tab, index) => {
|
||||
if (tab.value === this.tab) {
|
||||
this.tabs.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.$router.back()
|
||||
},
|
||||
async runNewSearch() {
|
||||
const searchJob = await qbit.startSearch(this.searchPattern, this.searchCategory, [this.searchPlugin])
|
||||
this.queryId = searchJob.id
|
||||
this.queryTimer = setInterval(() => this.refreshResults(), 1000)
|
||||
},
|
||||
async stopSearch() {
|
||||
if (this.queryId !== 0) await qbit.stopSearch(this.queryId)
|
||||
|
||||
this.queryId = 0
|
||||
clearInterval(this.queryTimer)
|
||||
},
|
||||
async refreshResults() {
|
||||
const response = await qbit.getSearchResults(this.queryId, this.queryResults.length)
|
||||
this.queryResults.push(...response.results)
|
||||
|
||||
if (response.status === 'Stopped') {
|
||||
this.queryId = 0
|
||||
await this.stopSearch()
|
||||
}
|
||||
},
|
||||
downloadTorrent(item: SearchResult) {
|
||||
this.createModal('AddModal', { initialMagnet: item.fileUrl })
|
||||
},
|
||||
customFilter(value: any, search: string | null) {
|
||||
return value != null
|
||||
&& search != null
|
||||
&& typeof value === 'string'
|
||||
&& search.trim().toLowerCase().split(' ').every(i => value.toString().toLowerCase().indexOf(i) !== -1)
|
||||
},
|
||||
handleKeyboardShortcut(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && this.getModals().length === 0) {
|
||||
this.close()
|
||||
|
@ -192,8 +107,7 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue