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 kind: FieldType = FieldType.TEXT
"""The kind of the field. i.e. string, bool""" """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 = "" value: str = ""
"""The default value of the field, It's currently unused.""" """The default value of the field, It's currently unused."""
@ -254,6 +260,10 @@ class DLFields(metaclass=Singleton):
msg = "Value must be a string." msg = "Value must be a string."
raise ValueError(msg) 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): if not isinstance(item.get("extras", {}), dict):
msg = "Extras must be a dictionary." msg = "Extras must be a dictionary."
raise ValueError(msg) # noqa: TRY004 raise ValueError(msg) # noqa: TRY004

View file

@ -9,6 +9,12 @@
<section class="modal-card-body"> <section class="modal-card-body">
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable"> <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="is-pulled-right">
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control"> <p class="control">
@ -24,9 +30,13 @@
</p> </p>
</div> </div>
</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>
<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="box">
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-12 is-12-mobile"> <div class="column is-12 is-12-mobile">
@ -38,7 +48,10 @@
<div class="column is-6 is-12-mobile"> <div class="column is-6 is-12-mobile">
<div class="field"> <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 }"> <div class="select is-fullwidth" :class="{ 'is-loading': isLoading }">
<select v-model="item.kind" :disabled="isLoading" class="is-capitalized"> <select v-model="item.kind" :disabled="isLoading" class="is-capitalized">
<option v-for="kind in Object.values(FieldTypes)" :key="`kind-${kind}`" :value="kind" <option v-for="kind in Object.values(FieldTypes)" :key="`kind-${kind}`" :value="kind"
@ -46,13 +59,16 @@
</select> </select>
</div> </div>
<span class="help is-bold"> <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> </span>
</div> </div>
</div> </div>
<div class="column is-6 is-12-mobile"> <div class="column is-6 is-12-mobile">
<div class="field"> <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" /> <input type="text" v-model="item.name" class="input" :disabled="isLoading" />
<span class="help is-bold"> <span class="help is-bold">
The name of the field, it will be shown in the UI. The name of the field, it will be shown in the UI.
@ -61,7 +77,10 @@
</div> </div>
<div class="column is-6 is-12-mobile"> <div class="column is-6 is-12-mobile">
<div class="field"> <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" /> <input type="text" v-model="item.field" class="input" :disabled="isLoading" />
<span class="help is-bold"> <span class="help is-bold">
The long form of yt-dlp option name, e.g. <code>--no-overwrites</code> not <code>-w</code>. The long form of yt-dlp option name, e.g. <code>--no-overwrites</code> not <code>-w</code>.
@ -70,7 +89,10 @@
</div> </div>
<div class="column is-6 is-12-mobile"> <div class="column is-6 is-12-mobile">
<div class="field"> <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" /> <input type="text" v-model="item.description" class="input" :disabled="isLoading" />
<span class="help is-bold"> <span class="help is-bold">
A short description of the field, it will be shown in the UI. A short description of the field, it will be shown in the UI.
@ -78,34 +100,36 @@
</div> </div>
</div> </div>
<!--
<div class="column is-6 is-12-mobile"> <div class="column is-6 is-12-mobile">
<div class="field"> <div class="field">
<label class="label is-unselectable" :for="`p-${index}`">Persist the value?</label> <label class="label">
<span class="icon"><i class="fas fa-sort-numeric-up" /></span>
<input :id="`p-${index}`" type="checkbox" v-model="item.extras.persist" :disabled="isLoading" <span>Field Order</span>
class="switch is-success" />
<label class="label is-unselectable" :for="`p-${index}`">
{{ item.extras?.persist ? 'Yes' : 'No' }}
</label> </label>
<span class="help is-bold">If enabled the value will persist.</span> <input type="number" v-model="item.order" class="input" :disabled="isLoading" />
</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" />
<span class="help is-bold"> <span class="help is-bold">
This field will be used in the future to store values for select type field. The order of the field, used to sort the fields in the UI. Lower numbers will appear first.
</span> </span>
</div> </div>
</div> -->
</div>
</div>
</div> </div>
<div class="column is-6 is-12-mobile">
<div class="field">
<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">
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 class="column is-12"> <div class="column is-12">
<Message message_class="has-background-warning-90 has-text-dark" v-if="items.length === 0"> <Message message_class="has-background-warning-90 has-text-dark" v-if="items.length === 0">
<span class="icon"> <span class="icon">
@ -211,10 +235,11 @@ const addNewField = () => items.value.push({
kind: 'string', kind: 'string',
field: '', field: '',
value: '', value: '',
icon: '',
order: items.value.reduce((max, item) => Math.max(max, item.order || 1), 0) + 1,
extras: {} extras: {}
}) })
const deleteField = (index: number) => items.value.splice(index, 1) const deleteField = (index: number) => items.value.splice(index, 1)
const validateItem = (item: DLField, index: number): boolean => { const validateItem = (item: DLField, index: number): boolean => {
@ -222,12 +247,17 @@ const validateItem = (item: DLField, index: number): boolean => {
const requiredFields = ['name', 'field', 'kind', 'description'] const requiredFields = ['name', 'field', 'kind', 'description']
for (const field of requiredFields) { 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.`) toast.error(`${item.name || index}: Field ${field} is required.`)
return false 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)) { if (!Object.values(FieldTypes).includes(item.kind)) {
toast.error(`${item.name || index}: Invalid field type: ${item.kind}`) toast.error(`${item.name || index}: Invalid field type: ${item.kind}`)
return false return false
@ -242,4 +272,5 @@ const validateItem = (item: DLField, index: number): boolean => {
} }
onMounted(async () => await loadContent()) onMounted(async () => await loadContent())
const sortedDLFields = computed(() => items.value.sort((a, b) => (a.order || 0) - (b.order || 0)))
</script> </script>

View file

@ -149,10 +149,11 @@
</DLInput> </DLInput>
</div> </div>
<template v-if="config.dl_fields.length > 0"> <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}`"> :key="fi.id || `dlf-${index}`">
<DLInput :id="fi?.id || `dlf-${index}`" :type="fi.kind" :description="fi.description" :label="fi.name" <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> </div>
</template> </template>
<div class="column is-12 is-hidden-tablet"> <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 addInProgress = ref<boolean>(false)
const showFields = 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', { const form = useStorage<item_request>('local_config_v1', {
id: null, id: null,
@ -278,12 +279,33 @@ const dialog_confirm = ref({
const FORCE_FLAG = '--no-download-archive' const FORCE_FLAG = '--no-download-archive'
const addDownload = async () => { const addDownload = async () => {
if (form.value?.cli && '' !== form.value.cli) { let form_cli = (form.value?.cli || '').trim()
const options = await convertOptions(form.value.cli)
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) { if (null === options) {
return return
} }
} }
}
const request_data = [] as Array<item_request> const request_data = [] as Array<item_request>
@ -298,7 +320,7 @@ const addDownload = async () => {
folder: config.app.basic_mode ? null : form.value.folder, folder: config.app.basic_mode ? null : form.value.folder,
template: config.app.basic_mode ? null : form.value.template, template: config.app.basic_mode ? null : form.value.template,
cookies: config.app.basic_mode ? '' : form.value.cookies, 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 auto_start: config.app.basic_mode ? true : auto_start.value
} as item_request } as item_request
@ -323,16 +345,21 @@ const addDownload = async () => {
return return
} }
let had_errors = false
data.forEach((item: Record<string, any>) => { data.forEach((item: Record<string, any>) => {
if (false !== item.status) { if (false !== item.status) {
return return
} }
toast.error(`Error: ${item.msg || 'Failed to add download.'}`) toast.error(`Error: ${item.msg || 'Failed to add download.'}`)
had_errors = true
}) })
if (false === had_errors) {
form.value.url = '' form.value.url = ''
emitter('clear_form') emitter('clear_form')
} }
}
catch (e: any) { catch (e: any) {
console.error(e) console.error(e)
toast.error(`Error: ${e.message}`) toast.error(`Error: ${e.message}`)
@ -357,6 +384,7 @@ const reset_config = () => {
folder: '', folder: '',
extras: {}, extras: {},
} as item_request } as item_request
dlFields.value = {}
showAdvanced.value = false 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> </script>

View file

@ -16,6 +16,12 @@ type DLField = {
/** The kind of the field. i.e. string, bool */ /** The kind of the field. i.e. string, bool */
kind: DLFieldType; 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 */ /** The default value of the field, It's currently unused */
value: string; value: string;