Merge pull request #528 from arabcoders/dev
Refactor: improve filtering capabilities in downloads page
This commit is contained in:
commit
b8934d40eb
4 changed files with 119 additions and 21 deletions
|
|
@ -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 item’s <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) => {
|
||||
|
|
|
|||
|
|
@ -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 item’s <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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue