working version of custom fields.

This commit is contained in:
arabcoders 2025-08-07 23:03:34 +03:00
parent 45401639ad
commit 00fc97c570
4 changed files with 109 additions and 32 deletions

View file

@ -70,6 +70,12 @@ class DLField:
kind: FieldType = FieldType.TEXT
"""The kind of the field. i.e. string, bool"""
icon: str = ""
"""The icon of the field, it can be a font-awesome icon"""
order: int = 0
"""The order of the field, used to sort the fields in the UI."""
value: str = ""
"""The default value of the field, It's currently unused."""
@ -254,6 +260,10 @@ class DLFields(metaclass=Singleton):
msg = "Value must be a string."
raise ValueError(msg)
if item.get("order") is not None and not isinstance(item.get("order"), int):
msg = "Order must be an integer."
raise ValueError(msg)
if not isinstance(item.get("extras", {}), dict):
msg = "Extras must be a dictionary."
raise ValueError(msg) # noqa: TRY004

View file

@ -9,6 +9,12 @@
<section class="modal-card-body">
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span class="title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-plus" /></span>
<span>Custom Fields</span>
</span>
</span>
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
@ -24,9 +30,13 @@
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">The idea behind the custom fields is to allow you to add new fields to the download
form to automate some of the yt-dlp options.</span>
</div>
</div>
<div class="column is-12" v-for="(item, index) in items" :key="item.id || index">
<div class="column is-12" v-for="(item, index) in sortedDLFields" :key="item.id || index">
<div class="box">
<div class="columns is-multiline">
<div class="column is-12 is-12-mobile">
@ -38,7 +48,10 @@
<div class="column is-6 is-12-mobile">
<div class="field">
<label class="label">Field Type</label>
<label class="label">
<span class="icon"><i class="fas fa-cog" /></span>
<span>Field Type</span>
</label>
<div class="select is-fullwidth" :class="{ 'is-loading': isLoading }">
<select v-model="item.kind" :disabled="isLoading" class="is-capitalized">
<option v-for="kind in Object.values(FieldTypes)" :key="`kind-${kind}`" :value="kind"
@ -46,13 +59,16 @@
</select>
</div>
<span class="help is-bold">
Field Type.
Field Type. String is a single line input, Text is a multi-line input, Bool is a checkbox.
</span>
</div>
</div>
<div class="column is-6 is-12-mobile">
<div class="field">
<label class="label">Field Name</label>
<label class="label">
<span class="icon"><i class="fas fa-font" /></span>
<span>Field Name</span>
</label>
<input type="text" v-model="item.name" class="input" :disabled="isLoading" />
<span class="help is-bold">
The name of the field, it will be shown in the UI.
@ -61,7 +77,10 @@
</div>
<div class="column is-6 is-12-mobile">
<div class="field">
<label class="label">Associated yt-dlp option</label>
<label class="label">
<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" />
<span class="help is-bold">
The long form of yt-dlp option name, e.g. <code>--no-overwrites</code> not <code>-w</code>.
@ -70,7 +89,10 @@
</div>
<div class="column is-6 is-12-mobile">
<div class="field">
<label class="label">Description</label>
<label class="label">
<span class="icon"><i class="fas fa-info-circle" /></span>
<span>Field Description</span>
</label>
<input type="text" v-model="item.description" class="input" :disabled="isLoading" />
<span class="help is-bold">
A short description of the field, it will be shown in the UI.
@ -78,34 +100,36 @@
</div>
</div>
<!--
<div class="column is-6 is-12-mobile">
<div class="field">
<label class="label is-unselectable" :for="`p-${index}`">Persist the value?</label>
<input :id="`p-${index}`" type="checkbox" v-model="item.extras.persist" :disabled="isLoading"
class="switch is-success" />
<label class="label is-unselectable" :for="`p-${index}`">
{{ item.extras?.persist ? 'Yes' : 'No' }}
<label class="label">
<span class="icon"><i class="fas fa-sort-numeric-up" /></span>
<span>Field Order</span>
</label>
<span class="help is-bold">If enabled the value will persist.</span>
<input type="number" v-model="item.order" class="input" :disabled="isLoading" />
<span class="help is-bold">
The order of the field, used to sort the fields in the UI. Lower numbers will appear first.
</span>
</div>
</div>
<div class="column is-6 is-12-mobile">
<div class="field">
<label class="label">Value</label>
<input type="text" v-model="item.value" class="input" :disabled="true" />
<label class="label">
<span class="icon"><i class="fas fa-image" /></span>
<span>Field Icon</span>
</label>
<input type="text" v-model="item.icon" class="input" :disabled="isLoading" />
<span class="help is-bold">
This field will be used in the future to store values for select type field.
The icon of the field, must be from <NuxtLink
href="https://fontawesome.com/icons/image?f=classic&s=solid" target="_blank">
font-awesome</NuxtLink> icon. e.g. <code>fa-solid fa-image</code>. Leave empty for no icon.
</span>
</div>
</div> -->
</div>
</div>
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-warning-90 has-text-dark" v-if="items.length === 0">
<span class="icon">
@ -211,10 +235,11 @@ const addNewField = () => items.value.push({
kind: 'string',
field: '',
value: '',
icon: '',
order: items.value.reduce((max, item) => Math.max(max, item.order || 1), 0) + 1,
extras: {}
})
const deleteField = (index: number) => items.value.splice(index, 1)
const validateItem = (item: DLField, index: number): boolean => {
@ -222,12 +247,17 @@ const validateItem = (item: DLField, index: number): boolean => {
const requiredFields = ['name', 'field', 'kind', 'description']
for (const field of requiredFields) {
if (!item[field as keyof DLField] || item[field as keyof DLField]?.trim() === '') {
if (!item[field as keyof DLField]) {
toast.error(`${item.name || index}: Field ${field} is required.`)
return false
}
}
if (!item.order || item.order < 1) {
toast.error(`${item.name || index}: Order must be a positive number.`)
return false
}
if (!Object.values(FieldTypes).includes(item.kind)) {
toast.error(`${item.name || index}: Invalid field type: ${item.kind}`)
return false
@ -242,4 +272,5 @@ const validateItem = (item: DLField, index: number): boolean => {
}
onMounted(async () => await loadContent())
const sortedDLFields = computed(() => items.value.sort((a, b) => (a.order || 0) - (b.order || 0)))
</script>

View file

@ -149,10 +149,11 @@
</DLInput>
</div>
<template v-if="config.dl_fields.length > 0">
<div class="column is-6-tablet is-12-mobile" v-for="(fi, index) in config.dl_fields"
<div class="column is-6-tablet is-12-mobile" v-for="(fi, index) in sortedDLFields"
:key="fi.id || `dlf-${index}`">
<DLInput :id="fi?.id || `dlf-${index}`" :type="fi.kind" :description="fi.description" :label="fi.name"
v-model="dlFields[fi.field]" :field="fi.field" />
:icon="fi.icon" v-model="dlFields[fi.field]" :field="fi.field"
:disabled="!socket.isConnected || addInProgress" />
</div>
</template>
<div class="column is-12 is-hidden-tablet">
@ -254,7 +255,7 @@ const show_description = useStorage<boolean>('show_description', true)
const addInProgress = ref<boolean>(false)
const showFields = ref<boolean>(false)
const dlFields: Record<string, any> = ref({})
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
const form = useStorage<item_request>('local_config_v1', {
id: null,
@ -278,10 +279,31 @@ const dialog_confirm = ref({
const FORCE_FLAG = '--no-download-archive'
const addDownload = async () => {
if (form.value?.cli && '' !== form.value.cli) {
const options = await convertOptions(form.value.cli)
if (null === options) {
return
let form_cli = (form.value?.cli || '').trim()
if (false === config.app.basic_mode) {
if (dlFields.value && Object.keys(dlFields.value).length > 0 && config.dl_fields && config.dl_fields.length > 0) {
const joined = []
for (const [key, value] of Object.entries(dlFields.value)) {
if (!config.dl_fields.some(f => f.field === key)) {
continue
}
if ([undefined, null, '', false].includes(value as any)) {
continue
}
joined.push(true === value ? `${key}` : `${key} ${value}`)
}
if (joined.length > 0) {
form_cli = form_cli ? `${form_cli} ${joined.join(' ')}` : joined.join(' ')
}
}
if (form_cli && form_cli.trim()) {
const options = await convertOptions(form_cli)
if (null === options) {
return
}
}
}
@ -298,7 +320,7 @@ const addDownload = async () => {
folder: config.app.basic_mode ? null : form.value.folder,
template: config.app.basic_mode ? null : form.value.template,
cookies: config.app.basic_mode ? '' : form.value.cookies,
cli: config.app.basic_mode ? null : form.value.cli,
cli: config.app.basic_mode ? null : form_cli,
auto_start: config.app.basic_mode ? true : auto_start.value
} as item_request
@ -323,15 +345,20 @@ const addDownload = async () => {
return
}
let had_errors = false
data.forEach((item: Record<string, any>) => {
if (false !== item.status) {
return
}
toast.error(`Error: ${item.msg || 'Failed to add download.'}`)
had_errors = true
})
form.value.url = ''
emitter('clear_form')
if (false === had_errors) {
form.value.url = ''
emitter('clear_form')
}
}
catch (e: any) {
console.error(e)
@ -357,6 +384,7 @@ const reset_config = () => {
folder: '',
extras: {},
} as item_request
dlFields.value = {}
showAdvanced.value = false
@ -484,4 +512,6 @@ const forceDownload = computed({
}
},
})
const sortedDLFields = computed(() => config.dl_fields.sort((a, b) => (a.order || 0) - (b.order || 0)))
</script>

View file

@ -16,6 +16,12 @@ type DLField = {
/** The kind of the field. i.e. string, bool */
kind: DLFieldType;
/** The icon of the field, it can be a font-awesome icon */
icon?: string;
/** The order of the field, used to sort the fields in the UI. */
order: number = 1;
/** The default value of the field, It's currently unused */
value: string;