perf(search): Add tabs + reset results on new search (#958)

This commit is contained in:
Rémi Marseault 2023-07-13 15:47:14 +02:00 committed by GitHub
parent 516f1d7913
commit 2de5f85798
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 138 deletions

View 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>

View file

@ -101,6 +101,7 @@
},
"search": {
"title": "Search torrents",
"tabHeaderTemplate": "Tab #$0",
"runNewSearch": "Search",
"stopSearch": "Stop"
},

View file

@ -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>