[FEAT] When console enabled show button to run commands from newDownload into console.
This commit is contained in:
parent
444dc72e8f
commit
b888e67877
3 changed files with 167 additions and 49 deletions
|
|
@ -137,9 +137,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<DLInput id="ytdlpCookies" type="text" label="Cookies" v-model="form.cookies"
|
<DLInput id="ytdlpCookies" type="text" label="Cookies" v-model="form.cookies" icon="fa-solid fa-cookie"
|
||||||
icon="fa-solid fa-cookie" :disabled="!socket.isConnected || addInProgress"
|
:disabled="!socket.isConnected || addInProgress" :placeholder="getDefault('cookies', '')">
|
||||||
:placeholder="getDefault('cookies', '')">
|
|
||||||
<template #help>
|
<template #help>
|
||||||
<span class="help is-bold">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
|
@ -173,6 +172,12 @@
|
||||||
<span>yt-dlp Information</span>
|
<span>yt-dlp Information</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
|
<hr class="dropdown-divider" v-if="config.app.console_enabled" />
|
||||||
|
<NuxtLink class="dropdown-item" @click="runCliCommand" v-if="config.app.console_enabled">
|
||||||
|
<span class="icon has-text-warning"><i class="fa-solid fa-terminal" /></span>
|
||||||
|
<span>Run CLI</span>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
<hr class="dropdown-divider" />
|
<hr class="dropdown-divider" />
|
||||||
<NuxtLink class="dropdown-item" @click="resetConfig">
|
<NuxtLink class="dropdown-item" @click="resetConfig">
|
||||||
<span class="icon has-text-danger"><i class="fa-solid fa-rotate-left" /></span>
|
<span class="icon has-text-danger"><i class="fa-solid fa-rotate-left" /></span>
|
||||||
|
|
@ -199,6 +204,14 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="control" v-if="config.app.console_enabled">
|
||||||
|
<button type="button" class="button is-warning" @click="runCliCommand"
|
||||||
|
:disabled="!socket.isConnected || !form?.url">
|
||||||
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
|
<span>Run CLI</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="button" class="button is-danger" @click="resetConfig"
|
<button type="button" class="button is-danger" @click="resetConfig"
|
||||||
:disabled="!!(!socket.isConnected || form?.id)" v-tooltip="'Reset local settings'">
|
:disabled="!!(!socket.isConnected || form?.id)" v-tooltip="'Reset local settings'">
|
||||||
|
|
@ -216,9 +229,6 @@
|
||||||
<datalist id="folders" v-if="config?.folders">
|
<datalist id="folders" v-if="config?.folders">
|
||||||
<option v-for="dir in config.folders" :key="dir" :value="dir" />
|
<option v-for="dir in config.folders" :key="dir" :value="dir" />
|
||||||
</datalist>
|
</datalist>
|
||||||
<ConfirmDialog v-if="dialog_confirm.visible" :visible="dialog_confirm.visible" :title="dialog_confirm.title"
|
|
||||||
:message="dialog_confirm.message" :options="dialog_confirm.options" @confirm="dialog_confirm.confirm"
|
|
||||||
@cancel="() => dialog_confirm.visible = false" />
|
|
||||||
|
|
||||||
<DLFields v-if="showFields" @cancel="() => showFields = false" />
|
<DLFields v-if="showFields" @cancel="() => showFields = false" />
|
||||||
<Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'">
|
<Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'">
|
||||||
|
|
@ -232,7 +242,9 @@ import 'assets/css/bulma-switch.css'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
|
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
|
||||||
import type { item_request } from '~/types/item'
|
import type { item_request } from '~/types/item'
|
||||||
import type { AutoCompleteOptions } from '~/types/autocomplete';
|
import type { AutoCompleteOptions } from '~/types/autocomplete'
|
||||||
|
import { navigateTo } from '#app'
|
||||||
|
import { useDialog } from '~/composables/useDialog'
|
||||||
|
|
||||||
const props = defineProps<{ item?: Partial<item_request> }>()
|
const props = defineProps<{ item?: Partial<item_request> }>()
|
||||||
const emitter = defineEmits<{
|
const emitter = defineEmits<{
|
||||||
|
|
@ -242,12 +254,14 @@ const emitter = defineEmits<{
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const showAdvanced = useStorage<boolean>('show_advanced', false)
|
const showAdvanced = useStorage<boolean>('show_advanced', false)
|
||||||
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
|
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
|
||||||
const auto_start = useStorage<boolean>('auto_start', true)
|
const auto_start = useStorage<boolean>('auto_start', true)
|
||||||
const show_description = useStorage<boolean>('show_description', true)
|
const show_description = useStorage<boolean>('show_description', true)
|
||||||
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
||||||
|
const storedCommand = useStorage<string>('console_command', '')
|
||||||
|
|
||||||
const addInProgress = ref<boolean>(false)
|
const addInProgress = ref<boolean>(false)
|
||||||
const showFields = ref<boolean>(false)
|
const showFields = ref<boolean>(false)
|
||||||
|
|
@ -275,25 +289,25 @@ const dialog_confirm = ref({
|
||||||
options: [],
|
options: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const is_valid_dl_field = (dl_field: string): boolean => {
|
||||||
|
if (dlFieldsExtra.includes(dl_field)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.dl_fields && config.dl_fields.length > 0) {
|
||||||
|
return config.dl_fields.some(f => f.field === dl_field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const addDownload = async () => {
|
const addDownload = async () => {
|
||||||
let form_cli = (form.value?.cli || '').trim()
|
let form_cli = (form.value?.cli || '').trim()
|
||||||
|
|
||||||
const is_valid = (dl_field: string): boolean => {
|
|
||||||
if (dlFieldsExtra.includes(dl_field)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.dl_fields && config.dl_fields.length > 0) {
|
|
||||||
return config.dl_fields.some(f => f.field === dl_field)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dlFields.value && Object.keys(dlFields.value).length > 0) {
|
if (dlFields.value && Object.keys(dlFields.value).length > 0) {
|
||||||
const joined = []
|
const joined = []
|
||||||
for (const [key, value] of Object.entries(dlFields.value)) {
|
for (const [key, value] of Object.entries(dlFields.value)) {
|
||||||
if (false === is_valid(key)) {
|
if (false === is_valid_dl_field(key)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -382,13 +396,16 @@ const addDownload = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetConfig = () => {
|
const resetConfig = async () => {
|
||||||
dialog_confirm.value.visible = true
|
const { status } = await dialog.confirmDialog({
|
||||||
dialog_confirm.value.message = `Reset local configuration?`
|
title: 'Confirm Action',
|
||||||
dialog_confirm.value.confirm = reset_config
|
message: `Reset local configuration?`,
|
||||||
}
|
confirmColor: 'is-danger',
|
||||||
|
})
|
||||||
|
if (!status) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const reset_config = () => {
|
|
||||||
form.value = {
|
form.value = {
|
||||||
url: '',
|
url: '',
|
||||||
preset: config.app.default_preset,
|
preset: config.app.default_preset,
|
||||||
|
|
@ -399,9 +416,7 @@ const reset_config = () => {
|
||||||
extras: {},
|
extras: {},
|
||||||
} as item_request
|
} as item_request
|
||||||
dlFields.value = {}
|
dlFields.value = {}
|
||||||
|
|
||||||
showAdvanced.value = false
|
showAdvanced.value = false
|
||||||
|
|
||||||
toast.success('Local configuration has been reset.')
|
toast.success('Local configuration has been reset.')
|
||||||
dialog_confirm.value.visible = false
|
dialog_confirm.value.visible = false
|
||||||
}
|
}
|
||||||
|
|
@ -475,6 +490,84 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const runCliCommand = async (): Promise<void> => {
|
||||||
|
if (!form.value.url) {
|
||||||
|
toast.warning('Please enter a URL first')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const {status} = await dialog.confirmDialog({
|
||||||
|
title: 'Run CLI Command',
|
||||||
|
message: `This will generate a yt-dlp command and run it in the console. Continue?`,
|
||||||
|
confirmColor: 'is-warning',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let form_cli = (form.value?.cli || '').trim()
|
||||||
|
|
||||||
|
if (dlFields.value && Object.keys(dlFields.value).length > 0) {
|
||||||
|
const joined = []
|
||||||
|
for (const [key, value] of Object.entries(dlFields.value)) {
|
||||||
|
if (false === is_valid_dl_field(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([undefined, null, '', false].includes(value as any)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyRegex = new RegExp(`(^|\\s)${key}(\\s|$)`);
|
||||||
|
if (form_cli && keyRegex.test(form_cli)) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await request('/api/yt-dlp/command', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: form.value.url,
|
||||||
|
preset: form.value.preset,
|
||||||
|
folder: form.value.folder,
|
||||||
|
cookies: form.value.cookies,
|
||||||
|
template: form.value.template,
|
||||||
|
cli: form_cli,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const json = await resp.json() as { command?: string; error?: string }
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
toast.error(`Error: ${json.error || 'Failed to generate command.'}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storedCommand.value = json.command
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
await navigateTo('/console')
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Failed to create and navigate to command')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hasFormatInConfig = computed((): boolean => !!form.value.cli?.match(/(?<!\S)(-f|--format)(=|\s)(\S+)/))
|
const hasFormatInConfig = computed((): boolean => !!form.value.cli?.match(/(?<!\S)(-f|--format)(=|\s)(\S+)/))
|
||||||
|
|
||||||
const filter_presets = (flag: boolean = true) => config.presets.filter(item => item.default === flag)
|
const filter_presets = (flag: boolean = true) => config.presets.filter(item => item.default === flag)
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@
|
||||||
import '@xterm/xterm/css/xterm.css'
|
import '@xterm/xterm/css/xterm.css'
|
||||||
import { Terminal } from '@xterm/xterm'
|
import { Terminal } from '@xterm/xterm'
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
import { disableOpacity, enableOpacity } from '~/utils'
|
import { disableOpacity, enableOpacity } from '~/utils'
|
||||||
import InputAutocomplete from '~/components/InputAutocomplete.vue'
|
import InputAutocomplete from '~/components/InputAutocomplete.vue'
|
||||||
import type { AutoCompleteOptions } from '~/types/autocomplete'
|
import type { AutoCompleteOptions } from '~/types/autocomplete'
|
||||||
|
|
@ -75,6 +76,7 @@ const terminalFit = ref<FitAddon>()
|
||||||
const command = ref<string>('')
|
const command = ref<string>('')
|
||||||
const terminal_window = useTemplateRef<HTMLDivElement>('terminal_window')
|
const terminal_window = useTemplateRef<HTMLDivElement>('terminal_window')
|
||||||
const isLoading = ref<boolean>(false)
|
const isLoading = ref<boolean>(false)
|
||||||
|
const storedCommand = useStorage<string>('console_command', '')
|
||||||
|
|
||||||
const ytDlpOptions = computed<AutoCompleteOptions>(() => config.ytdlp_options.flatMap(opt => opt.flags
|
const ytDlpOptions = computed<AutoCompleteOptions>(() => config.ytdlp_options.flatMap(opt => opt.flags
|
||||||
.map(flag => ({ value: flag, description: opt.description || '' }))
|
.map(flag => ({ value: flag, description: opt.description || '' }))
|
||||||
|
|
@ -104,6 +106,34 @@ const handle_event = () => {
|
||||||
terminalFit.value?.fit()
|
terminalFit.value?.fit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ensureTerminal = async () => {
|
||||||
|
if (terminal.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.value = new Terminal({
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
cursorBlink: false,
|
||||||
|
cursorStyle: 'underline',
|
||||||
|
cols: 108,
|
||||||
|
rows: 10,
|
||||||
|
disableStdin: true,
|
||||||
|
scrollback: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
terminalFit.value = new FitAddon()
|
||||||
|
terminal.value.loadAddon(terminalFit.value)
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
if (terminal_window.value) {
|
||||||
|
terminal.value.open(terminal_window.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
terminalFit.value.fit();
|
||||||
|
}
|
||||||
|
|
||||||
const runCommand = async () => {
|
const runCommand = async () => {
|
||||||
if ('' === command.value) {
|
if ('' === command.value) {
|
||||||
return
|
return
|
||||||
|
|
@ -123,24 +153,7 @@ const runCommand = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!terminal.value) {
|
await ensureTerminal()
|
||||||
terminal.value = new Terminal({
|
|
||||||
fontSize: 14,
|
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
|
||||||
cursorBlink: false,
|
|
||||||
cursorStyle: 'underline',
|
|
||||||
cols: 108,
|
|
||||||
rows: 10,
|
|
||||||
disableStdin: true,
|
|
||||||
scrollback: 1000,
|
|
||||||
})
|
|
||||||
terminalFit.value = new FitAddon()
|
|
||||||
terminal.value.loadAddon(terminalFit.value)
|
|
||||||
if (terminal_window.value) {
|
|
||||||
terminal.value.open(terminal_window.value)
|
|
||||||
}
|
|
||||||
terminalFit.value.fit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('clear' === command.value) {
|
if ('clear' === command.value) {
|
||||||
clearOutput(true)
|
clearOutput(true)
|
||||||
|
|
@ -149,8 +162,9 @@ const runCommand = async () => {
|
||||||
|
|
||||||
socket.emit('cli_post', command.value)
|
socket.emit('cli_post', command.value)
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
terminal.value.writeln(`user@YTPTube ~`)
|
terminal.value?.writeln(`user@YTPTube ~`)
|
||||||
terminal.value.writeln(`$ yt-dlp ${command.value}`)
|
terminal.value?.writeln(`$ yt-dlp ${command.value}`)
|
||||||
|
storedCommand.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearOutput = async (withCommand: boolean = false) => {
|
const clearOutput = async (withCommand: boolean = false) => {
|
||||||
|
|
@ -166,7 +180,6 @@ const clearOutput = async (withCommand: boolean = false) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
// Focus the InputAutocomplete component's input field
|
|
||||||
const inputElement = document.getElementById('command') as HTMLInputElement
|
const inputElement = document.getElementById('command') as HTMLInputElement
|
||||||
if (inputElement) {
|
if (inputElement) {
|
||||||
inputElement.focus()
|
inputElement.focus()
|
||||||
|
|
@ -192,7 +205,16 @@ onMounted(async () => {
|
||||||
socket.off('cli_output', writer)
|
socket.off('cli_output', writer)
|
||||||
socket.on('cli_close', loader)
|
socket.on('cli_close', loader)
|
||||||
socket.on('cli_output', writer)
|
socket.on('cli_output', writer)
|
||||||
|
|
||||||
disableOpacity()
|
disableOpacity()
|
||||||
|
|
||||||
|
await ensureTerminal()
|
||||||
|
|
||||||
|
if (storedCommand.value) {
|
||||||
|
command.value = storedCommand.value
|
||||||
|
await nextTick()
|
||||||
|
runCommand()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ export const useSocketStore = defineStore('socket', () => {
|
||||||
event.forEach(e => socket.value?.off(e, callback));
|
event.forEach(e => socket.value?.off(e, callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSessionId = (): string | null => socket.value?.id || null
|
||||||
|
|
||||||
const reconnect = () => {
|
const reconnect = () => {
|
||||||
if (true === isConnected.value) {
|
if (true === isConnected.value) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -220,6 +222,7 @@ export const useSocketStore = defineStore('socket', () => {
|
||||||
connect, reconnect, disconnect,
|
connect, reconnect, disconnect,
|
||||||
on, off, emit,
|
on, off, emit,
|
||||||
socket, isConnected,
|
socket, isConnected,
|
||||||
|
getSessionId,
|
||||||
connectionStatus: readonly(connectionStatus) as Readonly<Ref<connectionStatus>>,
|
connectionStatus: readonly(connectionStatus) as Readonly<Ref<connectionStatus>>,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue