diff --git a/ui/app/components/History.vue b/ui/app/components/History.vue index ebf9be3a..7e97625f 100644 --- a/ui/app/components/History.vue +++ b/ui/app/components/History.vue @@ -415,6 +415,20 @@ No results found for '{{ query }}'. +
+

+ You can search using any value shown in the item’s Local Information. You can also do a targeted search + using key:value. +

+ +
Examples:
+ +
@@ -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) => { diff --git a/ui/app/components/Queue.vue b/ui/app/components/Queue.vue index d2aafa29..1fd6beb8 100644 --- a/ui/app/components/Queue.vue +++ b/ui/app/components/Queue.vue @@ -284,6 +284,20 @@ No results found for '{{ query }}'. +
+

+ You can search using any value shown in the item’s Local Information. You can also do a targeted search + using key:value. +

+ +
Examples:
+ +
    +
  • youtube.com - items containing that text
  • +
  • is_live:true - only live downloads
  • +
  • source_name:task_name - items added by the specified task.
  • +
@@ -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(() => { 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) diff --git a/ui/app/utils/index.ts b/ui/app/utils/index.ts index 420c466f..7ab90325 100644 --- a/ui/app/utils/index.ts +++ b/ui/app/utils/index.ts @@ -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 = 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 + 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 } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index d6383228..f05195ac 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -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