[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 class="column is-6-tablet is-12-mobile">
|
||||
<DLInput id="ytdlpCookies" type="text" label="Cookies" v-model="form.cookies"
|
||||
icon="fa-solid fa-cookie" :disabled="!socket.isConnected || addInProgress"
|
||||
:placeholder="getDefault('cookies', '')">
|
||||
<DLInput id="ytdlpCookies" type="text" label="Cookies" v-model="form.cookies" icon="fa-solid fa-cookie"
|
||||
:disabled="!socket.isConnected || addInProgress" :placeholder="getDefault('cookies', '')">
|
||||
<template #help>
|
||||
<span class="help is-bold">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
|
|
@ -173,6 +172,12 @@
|
|||
<span>yt-dlp Information</span>
|
||||
</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" />
|
||||
<NuxtLink class="dropdown-item" @click="resetConfig">
|
||||
<span class="icon has-text-danger"><i class="fa-solid fa-rotate-left" /></span>
|
||||
|
|
@ -199,6 +204,14 @@
|
|||
</button>
|
||||
</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">
|
||||
<button type="button" class="button is-danger" @click="resetConfig"
|
||||
:disabled="!!(!socket.isConnected || form?.id)" v-tooltip="'Reset local settings'">
|
||||
|
|
@ -216,9 +229,6 @@
|
|||
<datalist id="folders" v-if="config?.folders">
|
||||
<option v-for="dir in config.folders" :key="dir" :value="dir" />
|
||||
</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" />
|
||||
<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 TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
|
||||
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 emitter = defineEmits<{
|
||||
|
|
@ -242,12 +254,14 @@ const emitter = defineEmits<{
|
|||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const toast = useNotification()
|
||||
const dialog = useDialog()
|
||||
|
||||
const showAdvanced = useStorage<boolean>('show_advanced', false)
|
||||
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
|
||||
const auto_start = useStorage<boolean>('auto_start', true)
|
||||
const show_description = useStorage<boolean>('show_description', true)
|
||||
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
||||
const storedCommand = useStorage<string>('console_command', '')
|
||||
|
||||
const addInProgress = ref<boolean>(false)
|
||||
const showFields = ref<boolean>(false)
|
||||
|
|
@ -275,25 +289,25 @@ const dialog_confirm = ref({
|
|||
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 () => {
|
||||
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) {
|
||||
const joined = []
|
||||
for (const [key, value] of Object.entries(dlFields.value)) {
|
||||
if (false === is_valid(key)) {
|
||||
if (false === is_valid_dl_field(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -382,13 +396,16 @@ const addDownload = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const resetConfig = () => {
|
||||
dialog_confirm.value.visible = true
|
||||
dialog_confirm.value.message = `Reset local configuration?`
|
||||
dialog_confirm.value.confirm = reset_config
|
||||
}
|
||||
const resetConfig = async () => {
|
||||
const { status } = await dialog.confirmDialog({
|
||||
title: 'Confirm Action',
|
||||
message: `Reset local configuration?`,
|
||||
confirmColor: 'is-danger',
|
||||
})
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
|
||||
const reset_config = () => {
|
||||
form.value = {
|
||||
url: '',
|
||||
preset: config.app.default_preset,
|
||||
|
|
@ -399,9 +416,7 @@ const reset_config = () => {
|
|||
extras: {},
|
||||
} as item_request
|
||||
dlFields.value = {}
|
||||
|
||||
showAdvanced.value = false
|
||||
|
||||
toast.success('Local configuration has been reset.')
|
||||
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 filter_presets = (flag: boolean = true) => config.presets.filter(item => item.default === flag)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
import '@xterm/xterm/css/xterm.css'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { disableOpacity, enableOpacity } from '~/utils'
|
||||
import InputAutocomplete from '~/components/InputAutocomplete.vue'
|
||||
import type { AutoCompleteOptions } from '~/types/autocomplete'
|
||||
|
|
@ -75,6 +76,7 @@ const terminalFit = ref<FitAddon>()
|
|||
const command = ref<string>('')
|
||||
const terminal_window = useTemplateRef<HTMLDivElement>('terminal_window')
|
||||
const isLoading = ref<boolean>(false)
|
||||
const storedCommand = useStorage<string>('console_command', '')
|
||||
|
||||
const ytDlpOptions = computed<AutoCompleteOptions>(() => config.ytdlp_options.flatMap(opt => opt.flags
|
||||
.map(flag => ({ value: flag, description: opt.description || '' }))
|
||||
|
|
@ -104,6 +106,34 @@ const handle_event = () => {
|
|||
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 () => {
|
||||
if ('' === command.value) {
|
||||
return
|
||||
|
|
@ -123,24 +153,7 @@ const runCommand = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (!terminal.value) {
|
||||
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();
|
||||
}
|
||||
await ensureTerminal()
|
||||
|
||||
if ('clear' === command.value) {
|
||||
clearOutput(true)
|
||||
|
|
@ -149,8 +162,9 @@ const runCommand = async () => {
|
|||
|
||||
socket.emit('cli_post', command.value)
|
||||
isLoading.value = true
|
||||
terminal.value.writeln(`user@YTPTube ~`)
|
||||
terminal.value.writeln(`$ yt-dlp ${command.value}`)
|
||||
terminal.value?.writeln(`user@YTPTube ~`)
|
||||
terminal.value?.writeln(`$ yt-dlp ${command.value}`)
|
||||
storedCommand.value = ''
|
||||
}
|
||||
|
||||
const clearOutput = async (withCommand: boolean = false) => {
|
||||
|
|
@ -166,7 +180,6 @@ const clearOutput = async (withCommand: boolean = false) => {
|
|||
}
|
||||
|
||||
const focusInput = () => {
|
||||
// Focus the InputAutocomplete component's input field
|
||||
const inputElement = document.getElementById('command') as HTMLInputElement
|
||||
if (inputElement) {
|
||||
inputElement.focus()
|
||||
|
|
@ -192,7 +205,16 @@ onMounted(async () => {
|
|||
socket.off('cli_output', writer)
|
||||
socket.on('cli_close', loader)
|
||||
socket.on('cli_output', writer)
|
||||
|
||||
disableOpacity()
|
||||
|
||||
await ensureTerminal()
|
||||
|
||||
if (storedCommand.value) {
|
||||
command.value = storedCommand.value
|
||||
await nextTick()
|
||||
runCommand()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
event.forEach(e => socket.value?.off(e, callback));
|
||||
}
|
||||
|
||||
const getSessionId = (): string | null => socket.value?.id || null
|
||||
|
||||
const reconnect = () => {
|
||||
if (true === isConnected.value) {
|
||||
return;
|
||||
|
|
@ -220,6 +222,7 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
connect, reconnect, disconnect,
|
||||
on, off, emit,
|
||||
socket, isConnected,
|
||||
getSessionId,
|
||||
connectionStatus: readonly(connectionStatus) as Readonly<Ref<connectionStatus>>,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue