Added autocomplete for cli options

This commit is contained in:
arabcoders 2025-08-20 16:19:43 +03:00
parent f31ea55ca0
commit a0533a0861
19 changed files with 724 additions and 387 deletions

View file

@ -358,9 +358,11 @@ div.is-centered {
justify-content: center;
}
.modal-content-max {
width: max-content;
width: calc(100% - 20px);
max-width: 1344px;
min-width: 320px;
box-sizing: border-box;
padding: 0;
margin: 0;
margin: 0 auto;
}

View file

@ -90,22 +90,20 @@
<div class="column is-12">
<div class="field">
<label class="label is-inline" for="cli_options">
<label class="label is-unselectable" for="cli_options">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>
Command options for yt-dlp -
<NuxtLink @click="showOptions = true" v-text="'View Options'" />
</span>
<span>Command options for yt-dlp</span>
</label>
<div class="control">
<textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress"
placeholder="command options to use, e.g. --proxy 1.2.3.4:3128" />
</div>
<TextareaAutocomplete id="cli_options" v-model="form.cli" :options="ytDlpOpt"
:disabled="addInProgress" />
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Not all options are supported <NuxtLink target="_blank"
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
ignored</NuxtLink>. Use with caution.</span>
<span>
<NuxtLink @click="showOptions = true" v-text="'View all options'" />. Not all options are
supported <NuxtLink target="_blank"
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some
are ignored</NuxtLink>. Use with caution.
</span>
</span>
</div>
</div>
@ -211,6 +209,8 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { AutoCompleteOptions } from '~/types/autocomplete';
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
const emitter = defineEmits<{
@ -227,6 +227,7 @@ const props = defineProps<{
const toast = useNotification()
const showImport = useStorage('showImport', false)
const box = useConfirm()
const config = useConfigStore()
const form = reactive<ConditionItem>(JSON.parse(JSON.stringify(props.item)))
const import_string = ref('')
@ -238,6 +239,15 @@ const test_data = ref<{
data: { status: boolean | null, data: Record<string, any> }
}>({ show: false, url: '', in_progress: false, changed: false, data: { status: null, data: {} } })
const showOptions = ref<boolean>(false)
const ytDlpOpt = ref<AutoCompleteOptions>([])
watch(() => config.ytdlp_options, newOptions => ytDlpOpt.value = newOptions
.filter(opt => !opt.ignored)
.flatMap(opt => opt.flags
.filter(flag => flag.startsWith('--'))
.map(flag => ({ value: flag, description: opt.description || '' }))),
{ immediate: true }
)
watch(() => form.filter, () => test_data.value.changed = true)

View file

@ -81,7 +81,8 @@
<span class="icon"><i class="fas fa-terminal" /></span>
<span>Associated yt-dlp option</span>
</label>
<input type="text" v-model="item.field" class="input" :disabled="isLoading" />
<InputAutocomplete v-model="item.field" :options="ytDlpOptions" :disabled="isLoading"
placeholder="Type or select a yt-dlp option" />
<span class="help is-bold">
The long form of yt-dlp option name, e.g. <code>--no-overwrites</code> not <code>-w</code>.
</span>
@ -164,15 +165,20 @@
<script setup lang="ts">
import { defineEmits, ref } from 'vue'
import type { DLField } from '~/types/dl_fields'
import InputAutocomplete from '~/components/InputAutocomplete.vue'
import { disableOpacity, enableOpacity } from '~/utils'
import type { DLField } from '~/types/dl_fields'
import type { AutoCompleteOptions } from '~/types/autocomplete'
const emitter = defineEmits<{ (e: 'cancel'): void }>()
const toast = useNotification()
const isLoading = ref<boolean>(false)
const items = ref<DLField[]>([])
const config = useConfigStore()
const ytDlpOptions = ref<AutoCompleteOptions>([])
const FieldTypes = {
STRING: 'string',
@ -274,6 +280,12 @@ const validateItem = (item: DLField, index: number): boolean => {
const sortedDLFields = computed(() => items.value.sort((a, b) => (a.order || 0) - (b.order || 0)))
watch(() => config.ytdlp_options, newOptions => ytDlpOptions.value = newOptions
.filter(opt => !opt.ignored).flatMap(opt => opt.flags
.filter(flag => flag.startsWith('--'))
.map(flag => ({ value: flag, description: opt.description || '' }))),
{ immediate: true }
)
onMounted(async () => {
disableOpacity()
await loadContent()

View file

@ -2,13 +2,14 @@
code {
color: var(--bulma-code) !important
}
</style>
<template>
<div>
<div class="modal is-active" v-if="false === externalModel">
<div class="modal-background" @click="emitter('closeModel')"></div>
<div class="modal-content" style="width:60vw;">
<div class="modal-content modal-content-max">
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
@ -25,7 +26,7 @@ code {
</div>
<button class="modal-close is-large" aria-label="close" @click="emitter('closeModel')"></button>
</div>
<div style="width:70vw; height: 80vh;" v-else>
<div class="modal-content-max" style="height: 80vh;" v-else>
<div class="content p-0 m-0" style="position: relative">
<pre><code class="p-4 is-block" v-text="data" /></pre>
<button class="button m-4" @click="() => copyText(JSON.stringify(data, null, 4))"

View file

@ -0,0 +1,133 @@
<template>
<div class="dropdown" :class="{ 'is-active': showList && filteredOptions.length }" style="width:100%;">
<div class="control" style="width:100%;">
<input v-model="model" @focus="onFocus" @blur="hideList" @keydown="handleKeydown" @input="onInput" class="input"
:placeholder="placeholder" autocomplete="new-password" style="width:100%; position:relative; z-index:2;"
:disabled="disabled" />
</div>
<div class="dropdown-menu" role="menu" style="width:100%; z-index:3;">
<div class="dropdown-content" style="width:100%; max-height:10em; overflow-y:auto;">
<a v-for="(option, idx) in filteredOptions" :key="option.value" @mousedown.prevent="selectOption(option.value)"
:class="['dropdown-item', { 'is-active': idx === highlightedIndex }]"
style="display:flex; justify-content:space-between;" :ref="el => setDropdownItemRef(el, idx)">
<span class="has-text-weight-bold">{{ option.value }}</span>
<abbr class="has-text-grey-light is-text-overflow" :title="option.description" style="margin-left:1em;">
{{ option.description }}
</abbr>
</a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed, defineModel, defineProps, nextTick } from 'vue'
import type { AutoCompleteOptions } from '~/types/autocomplete'
const props = defineProps<{
options: AutoCompleteOptions
placeholder?: string
disabled?: boolean
}>()
const model = defineModel<string>()
const onInput = () => {
if (false === showList.value) {
showList.value = true
}
highlightedIndex.value = filteredOptions.value.length ? 0 : -1
}
const showList = ref(false)
const highlightedIndex = ref(-1)
const dropdownItemRefs = ref<(HTMLElement | null)[]>([])
const filteredOptions = computed(() => {
if (!model.value) {
return props.options
}
const val = model.value.toLowerCase()
const startsWithFlag = []
const includesFlag = []
const includesDesc = []
for (const opt of props.options) {
const flag = opt.value.toLowerCase()
const desc = opt.description.toLowerCase()
if (flag.startsWith(val)) {
startsWithFlag.push(opt)
} else if (flag.includes(val)) {
includesFlag.push(opt)
} else if (desc.includes(val)) {
includesDesc.push(opt)
}
}
return [...startsWithFlag, ...includesFlag, ...includesDesc]
})
const selectOption = (val: string) => {
model.value = val
showList.value = false
highlightedIndex.value = -1
}
const hideList = () => {
setTimeout(() => {
showList.value = false
highlightedIndex.value = -1
dropdownItemRefs.value = []
}, 100)
}
const onFocus = () => {
showList.value = true
highlightedIndex.value = filteredOptions.value.length ? 0 : -1
}
const setDropdownItemRef = (el: Element | ComponentPublicInstance | null, idx: number) => {
dropdownItemRefs.value[idx] = el instanceof HTMLElement ? el : null
}
watch(filteredOptions, () => dropdownItemRefs.value = Array(filteredOptions.value.length).fill(null))
const handleKeydown = (e: KeyboardEvent) => {
if (!filteredOptions.value.length) {
return
}
const pageSize = 5
if (e.key === 'ArrowDown') {
e.preventDefault()
highlightedIndex.value = Math.min(highlightedIndex.value + 1, filteredOptions.value.length - 1)
nextTick(() => scrollHighlightedIntoView())
} else if (e.key === 'ArrowUp') {
e.preventDefault()
highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0)
nextTick(() => scrollHighlightedIntoView())
} else if (e.key === 'PageDown') {
e.preventDefault()
highlightedIndex.value = Math.min(highlightedIndex.value + pageSize, filteredOptions.value.length - 1)
nextTick(() => scrollHighlightedIntoView())
} else if (e.key === 'PageUp') {
e.preventDefault()
highlightedIndex.value = Math.max(highlightedIndex.value - pageSize, 0)
nextTick(() => scrollHighlightedIntoView())
} else if (e.key === 'Enter' || e.key === 'Tab') {
const selected = highlightedIndex.value >= 0 && highlightedIndex.value < filteredOptions.value.length ?
filteredOptions.value[highlightedIndex.value] : undefined
if (selected) {
e.preventDefault()
selectOption(selected.value)
}
}
}
function scrollHighlightedIntoView() {
const el = dropdownItemRefs.value[highlightedIndex.value]
if (!el) {
return
}
el.scrollIntoView({ block: 'nearest' })
}
</script>

View file

@ -116,23 +116,22 @@
</div>
<div class="column is-6-tablet is-12-mobile">
<DLInput id="cli_options" type="text" label="Command options for yt-dlp" v-model="form.cli"
icon="fa-solid fa-terminal" :disabled="!socket.isConnected || addInProgress">
<template #title>
<div class="field">
<label class="label is-unselectable" for="cli_options">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Command options for yt-dlp -
<NuxtLink @click="showOptions = true" v-text="'View Options'" />
<span>Command options for yt-dlp</span>
</label>
<TextareaAutocomplete id="cli_options" v-model="form.cli" :options="ytDlpOpt" />
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
<NuxtLink @click="showOptions = true" v-text="'View all options'" />. Not all options are supported
<NuxtLink target="_blank"
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
ignored</NuxtLink>. Use with caution.
</span>
</template>
<template #help>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Not all options are supported <NuxtLink target="_blank"
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
ignored</NuxtLink>. Use with caution.</span>
</span>
</template>
</DLInput>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
@ -241,7 +240,9 @@
<script setup lang="ts">
import 'assets/css/bulma-switch.css'
import { useStorage } from '@vueuse/core'
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { item_request } from '~/types/item'
import type { AutoCompleteOptions } from '~/types/autocomplete';
const props = defineProps<{ item?: Partial<item_request> }>()
const emitter = defineEmits<{
@ -263,6 +264,7 @@ const addInProgress = ref<boolean>(false)
const showFields = ref<boolean>(false)
const showOptions = ref<boolean>(false)
const dlFieldsExtra = ['--no-download-archive']
const ytDlpOpt = ref<AutoCompleteOptions>([])
const form = useStorage<item_request>('local_config_v1', {
id: null,
@ -445,6 +447,14 @@ onUpdated(async () => {
}
})
watch(() => config.ytdlp_options, newOptions => ytDlpOpt.value = newOptions
.filter(opt => !opt.ignored)
.flatMap(opt => opt.flags
.filter(flag => flag.startsWith('--'))
.map(flag => ({ value: flag, description: opt.description || '' }))),
{ immediate: true }
)
onMounted(async () => {
await nextTick()

View file

@ -143,22 +143,22 @@
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="column is-12">
<div class="field">
<label class="label is-inline" for="cli_options">
<span>Command options for yt-dlp -
<NuxtLink @click="showOptions = true" v-text="'View Options'" />
</span>
<label class="label is-unselectable" for="cli_options">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Command options for yt-dlp</span>
</label>
<div class="control">
<textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress"
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail" />
</div>
<TextareaAutocomplete id="cli_options" v-model="form.cli" :options="ytDlpOpt"
:disabled="addInProgress" />
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Not all options are supported <NuxtLink target="_blank"
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
ignored</NuxtLink>. Use with caution.</span>
<span>
<NuxtLink @click="showOptions = true" v-text="'View all options'" />. Not all options are
supported <NuxtLink target="_blank"
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some
are ignored</NuxtLink>. Use with caution.
</span>
</span>
</div>
</div>
@ -184,7 +184,7 @@
</div>
</div>
<div class="column">
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="description">
<span class="icon"><i class="fa-solid fa-comment" /></span>
@ -235,6 +235,7 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import type { AutoCompleteOptions } from '~/types/autocomplete';
import type { Preset, PresetImport } from '~/types/presets'
const emitter = defineEmits<{
@ -256,6 +257,15 @@ const import_string = ref<string>('')
const showImport = useStorage<boolean>('showImport', false)
const selected_preset = ref<string>('')
const showOptions = ref<boolean>(false)
const ytDlpOpt = ref<AutoCompleteOptions>([])
watch(() => config.ytdlp_options, newOptions => ytDlpOpt.value = newOptions
.filter(opt => !opt.ignored)
.flatMap(opt => opt.flags
.filter(flag => flag.startsWith('--'))
.map(flag => ({ value: flag, description: opt.description || '' }))),
{ immediate: true }
)
const checkInfo = async (): Promise<void> => {
for (const key of ['name']) {

View file

@ -232,22 +232,20 @@
<div class="column is-12">
<div class="field">
<label class="label is-inline" for="cli_options">
<label class="label is-unselectable" for="cli_options">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Command options for yt-dlp -
<NuxtLink @click="showOptions = true" v-text="'View Options'" />
</span>
<span>Command options for yt-dlp</span>
</label>
<div class="control">
<textarea type="text" class="textarea is-pre" v-model="form.cli" id="cli_options"
:disabled="addInProgress"
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail" />
</div>
<TextareaAutocomplete id="cli_options" v-model="form.cli" :options="ytDlpOpt"
:disabled="addInProgress" />
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Not all options are supported <NuxtLink target="_blank"
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
ignored</NuxtLink>. Use with caution.</span>
<span>
<NuxtLink @click="showOptions = true" v-text="'View all options'" />. Not all options are
supported <NuxtLink target="_blank"
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some
are ignored</NuxtLink>. Use with caution.
</span>
</span>
</div>
</div>
@ -307,6 +305,8 @@
import 'assets/css/bulma-switch.css'
import { useStorage } from '@vueuse/core'
import { CronExpressionParser } from 'cron-parser'
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { AutoCompleteOptions } from '~/types/autocomplete'
import type { exported_task, task_item } from '~/types/tasks'
const props = defineProps<{
@ -328,11 +328,20 @@ const showImport = useStorage('showImport', false)
const convertInProgress = ref<boolean>(false)
const import_string = ref<string>('')
const showOptions = ref<boolean>(false)
const ytDlpOpt = ref<AutoCompleteOptions>([])
const CHANNEL_REGEX = /^https?:\/\/(?:www\.)?youtube\.com\/(?:(?:channel\/(?<channelId>UC[0-9A-Za-z_-]{22}))|(?:c\/(?<customName>[A-Za-z0-9_-]+))|(?:user\/(?<userName>[A-Za-z0-9_-]+))|(?:@(?<handle>[A-Za-z0-9_-]+)))(?<suffix>\/.*)?\/?$/
const form = reactive<task_item>({ ...props.task })
watch(() => config.ytdlp_options, newOptions => ytDlpOpt.value = newOptions
.filter(opt => !opt.ignored)
.flatMap(opt => opt.flags
.filter(flag => flag.startsWith('--'))
.map(flag => ({ value: flag, description: opt.description || '' }))),
{ immediate: true }
)
onMounted(() => {
if (!props.task?.preset || '' === props.task.preset) {
form.preset = toRaw(config.app.default_preset)

View file

@ -0,0 +1,136 @@
<template>
<div class="dropdown" :class="{ 'is-active': showList && filteredOptions.length }" style="width:100%;">
<div class="control" style="width:100%;">
<textarea v-model="localValue" @input="onInput" @focus="showList = true" @blur="hideList" @keydown="onKeydown"
class="textarea" :placeholder="placeholder" autocomplete="off" style="width:100%; position:relative; z-index:2;"
rows="4" :disabled="disabled" />
</div>
<div class="dropdown-menu" role="menu" style="width:100%; z-index:3;">
<div class="dropdown-content" style="width:100%; max-height:11em; overflow-y:auto;">
<a v-for="(option, idx) in filteredOptions" :key="option.value" @mousedown.prevent="appendFlag(option.value)"
:class="['dropdown-item', { 'is-active': idx === highlightedIndex }]"
style="display:flex; justify-content:space-between;" ref="dropdownItems">
<span class="has-text-weight-bold">{{ option.value }}</span>
<span class="has-text-grey-light is-text-overflow" style="margin-left:1em;">{{ option.description }}</span>
</a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineModel, defineProps, watch, nextTick } from 'vue'
import type { AutoCompleteOptions } from '~/types/autocomplete'
const props = defineProps<{
options: AutoCompleteOptions
placeholder?: string
disabled?: boolean
}>()
const model = defineModel<string>()
const localValue = ref(model.value || '')
const showList = ref(false)
const highlightedIndex = ref(-1)
const dropdownItems = ref<HTMLElement[]>([])
watch(model, val => localValue.value = val || '')
watch(localValue, val => model.value = val)
const filteredOptions = computed(() => {
const lastWord = localValue.value.split(/\s+/).pop() || ''
if (!lastWord) {
return props.options
}
const val = lastWord.toLowerCase()
const startsWithFlag = []
const includesFlag = []
const includesDesc = []
for (const opt of props.options) {
const flag = opt.value.toLowerCase()
const desc = opt.description.toLowerCase()
if (flag.startsWith(val)) {
startsWithFlag.push(opt)
} else if (flag.includes(val)) {
includesFlag.push(opt)
} else if (desc.includes(val)) {
includesDesc.push(opt)
}
}
return [...startsWithFlag, ...includesFlag, ...includesDesc]
})
const appendFlag = (flag: string) => {
// Only replace the last word if the cursor is at the end and the last word is not followed by a space
const value = localValue.value
const cursorAtEnd = true // textarea autocomplete only works at end
const match = value.match(/(\S+)$/)
if (match && cursorAtEnd && !value.endsWith(' ')) {
// Replace last word
localValue.value = value.slice(0, match.index) + flag
} else {
// Just append
localValue.value += (value && !value.endsWith(' ') ? ' ' : '') + flag
}
showList.value = false
highlightedIndex.value = -1
}
const onInput = () => {
showList.value = true
highlightedIndex.value = filteredOptions.value.length ? 0 : -1
}
const hideList = () => setTimeout(() => { showList.value = false; highlightedIndex.value = -1 }, 100)
const onKeydown = (e: KeyboardEvent) => {
if (!showList.value || !filteredOptions.value.length) {
return
}
const pageSize = 5
if (e.key === 'ArrowDown') {
e.preventDefault()
highlightedIndex.value = Math.min(highlightedIndex.value + 1, filteredOptions.value.length - 1)
nextTick(() => {
const el = dropdownItems.value[highlightedIndex.value]
if (el) el.scrollIntoView({ block: 'nearest' })
})
} else if (e.key === 'ArrowUp') {
e.preventDefault()
highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0)
nextTick(() => {
const el = dropdownItems.value[highlightedIndex.value]
if (el) el.scrollIntoView({ block: 'nearest' })
})
} else if (e.key === 'PageDown') {
e.preventDefault()
highlightedIndex.value = Math.min(highlightedIndex.value + pageSize, filteredOptions.value.length - 1)
nextTick(() => {
const el = dropdownItems.value[highlightedIndex.value]
if (el) el.scrollIntoView({ block: 'nearest' })
})
} else if (e.key === 'PageUp') {
e.preventDefault()
highlightedIndex.value = Math.max(highlightedIndex.value - pageSize, 0)
nextTick(() => {
const el = dropdownItems.value[highlightedIndex.value]
if (el) el.scrollIntoView({ block: 'nearest' })
})
} else if (e.key === 'Enter' || e.key === 'Tab') {
const selected = highlightedIndex.value >= 0 && highlightedIndex.value < filteredOptions.value.length ?
filteredOptions.value[highlightedIndex.value] : undefined
if (selected) {
e.preventDefault()
appendFlag(selected.value)
}
}
// Keep dropdownItems ref in sync with rendered items
watch(filteredOptions, () => nextTick(() => {
const items = document.querySelectorAll('.dropdown-item')
dropdownItems.value = Array.from(items) as HTMLElement[]
}))
}
</script>

View file

@ -153,13 +153,7 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
type YTDLPOption = {
flags: string[],
description: string | null,
group: string | null,
ignored: boolean,
}
import type { YTDLPOption } from '~/types/ytdlp'
const isLoading = ref(false)
const options = ref<YTDLPOption[]>([])

View file

@ -139,6 +139,7 @@ import 'assets/css/all.css'
import { useStorage } from '@vueuse/core'
import moment from 'moment'
import * as Sentry from '@sentry/nuxt'
import type { YTDLPOption } from '~/types/ytdlp'
const Year = new Date().getFullYear()
const selectedTheme = useStorage('theme', 'auto')
@ -235,13 +236,20 @@ watch(() => config.app.sentry_dsn, dsn => {
onMounted(async () => {
try {
await handleImage(bg_enable.value)
} catch (e) {
}
} catch (e) { }
try {
const opts = await request('/api/yt-dlp/options')
if (!opts.ok) {
return
}
const data: Array<YTDLPOption> = await opts.json()
config.ytdlp_options = data
} catch (e) { }
try {
applyPreferredColorScheme(selectedTheme.value)
} catch (e) {
}
} catch (e) { }
})
watch(selectedTheme, value => {

View file

@ -339,7 +339,7 @@
</div>
</div>
<div class="columns is-multiline" v-if="tasks && tasks.length > 0">
<div class="columns is-multiline" v-if="!toggleForm && tasks && tasks.length > 0">
<div class="column is-12">
<Message title="Tips" class="is-info is-background-info-80" icon="fas fa-info-circle">
<span>

View file

@ -41,6 +41,7 @@ export const useConfigStore = defineStore('config', () => {
],
dl_fields: [],
folders: [],
ytdlp_options: [],
paused: false,
is_loaded: false,
});

9
ui/app/types/autocomplete.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
interface Option {
value: string
description: string
}
type AutoCompleteOptions = Option[]
export type { Option, AutoCompleteOptions }

View file

@ -1,3 +1,4 @@
import type { YTDLPOption } from './ytdlp';
import type { DLField } from "./dl_fields"
type AppConfig = {
@ -77,6 +78,8 @@ type ConfigState = {
dl_fields: DLField[]
/** List of folders where files can be saved */
folders: string[]
/** List of yt-dlp options */
ytdlp_options: YTDLPOption[]
/** Indicates if downloads are currently paused */
paused: boolean
/** Indicates if the configuration has been loaded */

8
ui/app/types/ytdlp.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
type YTDLPOption = {
flags: string[],
description: string | null,
group: string | null,
ignored: boolean,
}
export type { YTDLPOption };

View file

@ -13,8 +13,8 @@
"dependencies": {
"@pinia/nuxt": "^0.11.2",
"@sentry/nuxt": "^10.5.0",
"@vueuse/core": "^13.6.0",
"@vueuse/nuxt": "^13.6.0",
"@vueuse/core": "^13.7.0",
"@vueuse/nuxt": "^13.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"cron-parser": "^5.3.0",

File diff suppressed because it is too large Load diff

12
uv.lock
View file

@ -1363,7 +1363,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.32.4"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@ -1371,9 +1371,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 }
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 },
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 },
]
[[package]]
@ -1572,11 +1572,11 @@ wheels = [
[[package]]
name = "yt-dlp"
version = "2025.8.11"
version = "2025.8.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/72/de4a7f9bbfef886c7f0790b8246585310f155e4a6589dd38d846efa932e9/yt_dlp-2025.8.11.tar.gz", hash = "sha256:dc7c120a367fe55e0f711613dc80ea29d3a4e0ed8d66104cebfbe3d36e81fdfc", size = 3045769 }
sdist = { url = "https://files.pythonhosted.org/packages/c8/af/d3c81af35ae2aef148d0ff78f001650ce5a7ca73fbd3b271eb9aab4c56ee/yt_dlp-2025.8.20.tar.gz", hash = "sha256:da873bcf424177ab5c3b701fa94ea4cdac17bf3aec5ef37b91f530c90def7bcf", size = 3037484 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/dc/4400fb3e8bccdbd0f6ecf36a8a9aea3290ad98a1c9d19664a5c92d7b2e5d/yt_dlp-2025.8.11-py3-none-any.whl", hash = "sha256:f115d2246c1ab5737772bd4845be057eebb91c0d95125a7577d92288351df7d0", size = 3281677 },
{ url = "https://files.pythonhosted.org/packages/33/e8/ebd888100684c10799897296e3061c19ba5559b641f8da218bf48229a815/yt_dlp-2025.8.20-py3-none-any.whl", hash = "sha256:073c97e2a3f9cd0fa6a76142c4ef46ca62b2575c37eaf80d8c3718fd6f3277eb", size = 3266841 },
]
[[package]]