Merge pull request #528 from arabcoders/dev

Refactor: improve filtering capabilities in downloads page
This commit is contained in:
Abdulmohsen 2025-12-31 21:37:28 +03:00 committed by GitHub
commit b8934d40eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 119 additions and 21 deletions

View file

@ -415,6 +415,20 @@
<Message message_class="is-warning" title="Filter results" icon="fas fa-search" :useClose="true"
@close="() => emitter('clear_search')" v-if="query" :newStyle="true">
<span class="is-block">No results found for '<span class="is-underlined is-bold">{{ query }}</span>'.</span>
<hr>
<p>
You can search using any value shown in the items <code><span class="icon"><i
class="fa-solid fa-info-circle" /></span> Local Information</code>. You can also do a targeted search
using <code><u>key</u>:value</code>.
</p>
<h5>Examples:</h5>
<ul>
<li><code>youtube.com</code> - items containing that text</li>
<li><code>is_live:true</code> - only live downloads</li>
<li><code>source_name:task_name</code> - items added by the specified task.</li>
</ul>
</Message>
<Message message_class="is-primary" title="No items" icon="fas fa-exclamation-triangle"
v-else-if="socket.isConnected" :new-style="true">
@ -463,6 +477,7 @@ import moment from 'moment'
import { useStorage, useIntersectionObserver } from '@vueuse/core'
import type { StoreItem } from '~/types/store'
import { useConfirm } from '~/composables/useConfirm'
import { deepIncludes } from '~/utils'
const emitter = defineEmits<{
(e: 'getInfo', url: string, preset: string, cli: string): void
@ -578,9 +593,7 @@ const filterItem = (item: StoreItem) => {
if (!q) {
return true
}
return Object.values(item).some(v =>
typeof v === 'string' && v.toLowerCase().includes(q)
)
return deepIncludes(item, q, new WeakSet())
}
watch(masterSelectAll, (value) => {

View file

@ -284,6 +284,20 @@
<Message message_class="is-warning" title="Filter results" :newStyle="true" icon="fas fa-search" :useClose="true"
@close="() => emitter('clear_search')" v-if="query">
<span class="is-block">No results found for '<span class="is-underlined is-bold">{{ query }}</span>'.</span>
<hr>
<p>
You can search using any value shown in the items <code><span class="icon"><i
class="fa-solid fa-info-circle" /></span> Local Information</code>. You can also do a targeted search
using <code><u>key</u>:value</code>.
</p>
<h5>Examples:</h5>
<ul>
<li><code>youtube.com</code> - items containing that text</li>
<li><code>is_live:true</code> - only live downloads</li>
<li><code>source_name:task_name</code> - items added by the specified task.</li>
</ul>
</Message>
<Message message_class="is-info" title="No items" icon="fas fa-exclamation-triangle" :useClose="false"
:newStyle="true" v-else>
@ -306,6 +320,7 @@ import moment from 'moment'
import { useStorage } from '@vueuse/core'
import type { StoreItem } from '~/types/store'
import { useConfirm } from '~/composables/useConfirm'
import { deepIncludes } from '~/utils'
const emitter = defineEmits<{
(e: 'getInfo', url: string, preset: string, cli: string): void
@ -349,9 +364,7 @@ const filteredItems = computed<StoreItem[]>(() => {
if (!q) {
return Object.values(stateStore.queue)
}
return Object.values(stateStore.queue).filter((i: StoreItem) =>
Object.values(i).some(v => typeof v === 'string' && v.toLowerCase().includes(q))
)
return Object.values(stateStore.queue).filter((i: StoreItem) => deepIncludes(i, q, new WeakSet()))
})
const hasSelected = computed(() => 0 < selectedElms.value.length)

View file

@ -719,10 +719,82 @@ const shortPath = (path: string, prefix: string = '...'): string => {
return `${prefix}/${parts.at(-1)}${hasTrailingSlash ? '/' : ''}`;
}
/**
* Recursively test if a value (including nested objects/arrays) contains a query string.
* - Plain queries match keys or values (case-insensitive).
* - key:value queries require the value to be under a matching key in the path.
*
* @param value - Value to search within.
* @param query - Raw query string.
* @param seen - Optional WeakSet to prevent circular reference loops.
* @param kv - Internal: parsed key/value pair when using key:value mode.
* @param keyMatched - Internal: whether current recursion path matched the key.
*/
const deepIncludes = (
value: unknown,
query: string,
seen: WeakSet<object> = new WeakSet(),
kv: { key: string; val: string } | null = null,
keyMatched: boolean = false
): boolean => {
const normalized = query.trim().toLowerCase()
if (!normalized || null === value || undefined === value) {
return false
}
const pair = kv ?? (() => {
const idx = normalized.indexOf(':')
if (idx <= 0 || idx >= normalized.length - 1) {
return null
}
const key = normalized.slice(0, idx).trim()
const val = normalized.slice(idx + 1).trim()
if (!key || !val) {
return null
}
return { key, val }
})()
const matchPrimitive = (val: unknown, q: string): boolean => String(val).toLowerCase().includes(q)
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
if (!pair) {
return matchPrimitive(value, normalized)
}
return keyMatched && matchPrimitive(value, pair.val)
}
if (Array.isArray(value)) {
return value.some(entry => deepIncludes(entry, normalized, seen, pair, keyMatched))
}
if ('object' === typeof value) {
const obj = value as Record<string, unknown>
if (seen.has(obj)) {
return false
}
seen.add(obj)
for (const [key, val] of Object.entries(obj)) {
const keyLower = key.toLowerCase()
if (!pair && keyLower.includes(normalized)) {
return true
}
const nextKeyMatched = pair ? keyMatched || keyLower.includes(pair.key) : keyMatched
if (deepIncludes(val, normalized, seen, pair, nextKeyMatched)) {
return true
}
}
}
return false
}
export {
separators, convertCliOptions, getSeparatorsName, iTrim, eTrim, sTrim, ucFirst,
getValue, ag, ag_set, awaitElement, r, copyText, dEvent, makePagination, encodePath,
request, removeANSIColors, dec2hex, makeId, basename, dirname, getQueryParams,
makeDownload, formatBytes, has_data, toggleClass, cleanObject, uri, formatTime,
sleep, awaiter, encode, decode, disableOpacity, enableOpacity, stripPath, shortPath
sleep, awaiter, encode, decode, disableOpacity, enableOpacity, stripPath, shortPath, deepIncludes
}

View file

@ -1267,8 +1267,8 @@ packages:
'@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
'@rolldown/pluginutils@1.0.0-beta.57':
resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==}
'@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5':
resolution: {integrity: sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ==}
'@rollup/plugin-alias@5.1.1':
resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==}
@ -1467,8 +1467,8 @@ packages:
resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==}
engines: {node: '>=18'}
'@sindresorhus/is@7.1.1':
resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==}
'@sindresorhus/is@7.2.0':
resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==}
engines: {node: '>=18'}
'@sindresorhus/merge-streams@4.0.0':
@ -4087,8 +4087,8 @@ packages:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'}
ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
ts-api-utils@2.3.0:
resolution: {integrity: sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
@ -5793,14 +5793,14 @@ snapshots:
'@poppinss/dumper@0.6.5':
dependencies:
'@poppinss/colors': 4.1.6
'@sindresorhus/is': 7.1.1
'@sindresorhus/is': 7.2.0
supports-color: 10.2.2
'@poppinss/exception@1.2.3': {}
'@rolldown/pluginutils@1.0.0-beta.53': {}
'@rolldown/pluginutils@1.0.0-beta.57': {}
'@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5': {}
'@rollup/plugin-alias@5.1.1(rollup@4.54.0)':
optionalDependencies:
@ -5933,7 +5933,7 @@ snapshots:
'@sindresorhus/base62@1.0.0': {}
'@sindresorhus/is@7.1.1': {}
'@sindresorhus/is@7.2.0': {}
'@sindresorhus/merge-streams@4.0.0': {}
@ -5993,7 +5993,7 @@ snapshots:
eslint: 9.39.2(jiti@2.6.1)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.9.3)
ts-api-utils: 2.3.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@ -6035,7 +6035,7 @@ snapshots:
'@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.2(jiti@2.6.1)
ts-api-utils: 2.1.0(typescript@5.9.3)
ts-api-utils: 2.3.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@ -6052,7 +6052,7 @@ snapshots:
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
ts-api-utils: 2.1.0(typescript@5.9.3)
ts-api-utils: 2.3.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@ -6162,7 +6162,7 @@ snapshots:
'@babel/core': 7.28.5
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5)
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
'@rolldown/pluginutils': 1.0.0-beta.57
'@rolldown/pluginutils': 1.0.0-beta.9-commit.d91dfb5
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5)
vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
vue: 3.5.26(typescript@5.9.3)
@ -8890,7 +8890,7 @@ snapshots:
punycode: 2.3.1
optional: true
ts-api-utils@2.1.0(typescript@5.9.3):
ts-api-utils@2.3.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3