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>
</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" />
<input type="number" v-model="item.order" 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 order of the field, used to sort the fields in the UI. Lower numbers will appear first.
</span>
</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">
<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,12 +279,33 @@ 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)
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
}
}
}
const request_data = [] as Array<item_request>
@ -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,16 +345,21 @@ 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
})
if (false === had_errors) {
form.value.url = ''
emitter('clear_form')
}
}
catch (e: any) {
console.error(e)
toast.error(`Error: ${e.message}`)
@ -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;