+ :disabled="!socket.isConnected || addInProgress || !hasValidUrl">
Add
@@ -209,7 +217,7 @@
+ :disabled="!socket.isConnected || !hasValidUrl">
@@ -217,15 +225,15 @@
+ @click="emitter('getInfo', splitUrls(form.url || '')[0] || '', form.preset, form.cli)"
+ :disabled="!socket.isConnected || addInProgress || !hasValidUrl">
+ :disabled="!socket.isConnected || !hasValidUrl" v-tooltip="'Show compiled yt-dlp options.'">
@@ -291,6 +299,7 @@ const testResultsData = ref
(null)
const dlFieldsExtra = ['--no-download-archive', '--no-continue']
const ytDlpOpt = ref([])
const cookiesDropzoneRef = ref | null>(null)
+const urlTextarea = ref(null)
const form = useStorage('local_config_v1', {
id: null,
@@ -304,14 +313,6 @@ const form = useStorage('local_config_v1', {
}) as Ref
-const dialog_confirm = ref({
- visible: false,
- title: 'Confirm Action',
- confirm: () => { },
- message: '',
- options: [],
-})
-
const is_valid_dl_field = (dl_field: string): boolean => {
if (dlFieldsExtra.includes(dl_field)) {
return true
@@ -324,6 +325,81 @@ const is_valid_dl_field = (dl_field: string): boolean => {
return false;
}
+const adjustTextareaHeight = async (): Promise => {
+ await nextTick()
+ if (urlTextarea.value) {
+ urlTextarea.value.style.height = 'auto'
+ const newHeight = Math.min(urlTextarea.value.scrollHeight, 300)
+ urlTextarea.value.style.height = `${newHeight}px`
+ }
+}
+
+const handleKeyDown = async (event: KeyboardEvent): Promise => {
+ const target = event.target as HTMLInputElement | HTMLTextAreaElement
+ const isTextarea = target.tagName === 'TEXTAREA'
+ if (event.key !== 'Enter') {
+ return
+ }
+
+ if (event.ctrlKey && isTextarea && !hasValidUrl.value) {
+ event.preventDefault()
+ addDownload()
+ return
+ }
+
+ if (event.shiftKey && !isTextarea) {
+ event.preventDefault()
+ const cursorPos = target.selectionStart || form.value.url.length
+ form.value.url = form.value.url.substring(0, cursorPos) + '\n' + form.value.url.substring(target.selectionEnd || cursorPos)
+
+ await nextTick()
+
+ if (urlTextarea.value) {
+ await adjustTextareaHeight()
+ urlTextarea.value.setSelectionRange(cursorPos + 1, cursorPos + 1)
+ urlTextarea.value.focus()
+ }
+ }
+}
+
+const handlePaste = async (event: ClipboardEvent): Promise => {
+ const pastedText = event.clipboardData?.getData('text') || ''
+ if (!pastedText.includes('\n')) {
+ return
+ }
+
+ event.preventDefault()
+
+ const target = event.target as HTMLInputElement
+ const currentValue = form.value.url || ''
+ const start = target.selectionStart || currentValue.length
+ const end = target.selectionEnd || currentValue.length
+ form.value.url = currentValue.substring(0, start) + pastedText + currentValue.substring(end)
+
+ await nextTick()
+
+ if (urlTextarea.value) {
+ await adjustTextareaHeight()
+ const newPos = start + pastedText.length
+ urlTextarea.value.setSelectionRange(newPos, newPos)
+ urlTextarea.value.focus()
+ }
+}
+
+const splitUrls = (urlString: string): Array => {
+ const lines = urlString.split('\n')
+ const urls: string[] = []
+
+ lines.forEach(line => line.split(separator.value).forEach(url => {
+ const trimmed = url.trim()
+ if (trimmed) {
+ urls.push(trimmed)
+ }
+ }))
+
+ return urls
+}
+
const addDownload = async () => {
let form_cli = (form.value?.cli || '').trim()
@@ -361,11 +437,7 @@ const addDownload = async () => {
const request_data = [] as Array
- form.value.url.split(separator.value).forEach(async (url: string) => {
- if (!url.trim()) {
- return
- }
-
+ splitUrls(form.value.url).forEach(async (url: string) => {
const data = {
url: url,
preset: form.value.preset || config.app.default_preset,
@@ -441,7 +513,6 @@ const resetConfig = async () => {
dlFields.value = {}
showAdvanced.value = false
toast.success('Local configuration has been reset.')
- dialog_confirm.value.visible = false
}
const convertOptions = async (args: string) => {
@@ -509,7 +580,11 @@ onMounted(async () => {
await nextTick()
if (!separators.some(s => s.value === separator.value)) {
- separator.value = separators[0]?.value ?? '.'
+ separator.value = separators[0]?.value ?? ','
+ }
+
+ if (isMultiLineInput.value && urlTextarea.value) {
+ await adjustTextareaHeight()
}
})
@@ -565,7 +640,7 @@ const runCliCommand = async (): Promise => {
const resp = await request('/api/yt-dlp/command', {
method: 'POST',
body: JSON.stringify({
- url: form.value.url,
+ url: splitUrls(form.value.url).join(' '),
preset: form.value.preset,
folder: form.value.folder,
cookies: form.value.cookies,
@@ -657,6 +732,7 @@ const CloseTestResults = () => {
testResultsData.value = null
}
+const isMultiLineInput = computed(() => !!form.value.url && form.value.url.includes('\n'))
const hasFormatInConfig = computed((): boolean => !!form.value.cli?.match(/(? config.presets.filter(item => item.default === flag)
@@ -693,13 +769,17 @@ const getDefault = (type: 'cookies' | 'cli' | 'template' | 'folder', ret: string
return ret
}
-// eslint-disable-next-line vue/no-side-effects-in-computed-properties
-const sortedDLFields = computed(() => config.dl_fields.sort((a, b) => (a.order || 0) - (b.order || 0)))
+const sortedDLFields = computed(() => [...config.dl_fields].sort((a, b) => (a.order || 0) - (b.order || 0)))
+const hasValidUrl = computed(() => form.value.url && form.value.url.trim().length > 0)
-const multiURLs = computed(() => {
- if (!form.value.url) {
- return false
+watch(isMultiLineInput, async newValue => {
+ await nextTick()
+ if (newValue) {
+ await adjustTextareaHeight()
+ urlTextarea.value?.focus()
+ return
}
- return form.value.url.split(separator.value).filter((u: string) => u.trim()).length > 1
+ const inputElement = document.getElementById('url') as HTMLInputElement
+ inputElement?.focus()
})
diff --git a/ui/app/pages/console.vue b/ui/app/pages/console.vue
index e8e2b035..dcaaae5a 100644
--- a/ui/app/pages/console.vue
+++ b/ui/app/pages/console.vue
@@ -2,6 +2,15 @@
.terminal {
padding-left: 10px;
}
+
+.history-item {
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.history-item:hover {
+ background-color: #f5f5f5;
+}
@@ -39,15 +48,17 @@
-
+
+
-
-
-
+
@@ -55,6 +66,48 @@