-
+
+
-
@@ -65,6 +67,7 @@ import { FitAddon } from '@xterm/addon-fit'
import { useStorage } from '@vueuse/core'
import { disableOpacity, enableOpacity } from '~/utils'
import InputAutocomplete from '~/components/InputAutocomplete.vue'
+import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { AutoCompleteOptions } from '~/types/autocomplete'
const config = useConfigStore()
@@ -75,6 +78,8 @@ const terminal = ref
()
const terminalFit = ref()
const command = ref('')
const terminal_window = useTemplateRef('terminal_window')
+const commandInput = ref | null>(null)
+const commandTextarea = ref | null>(null)
const isLoading = ref(false)
const storedCommand = useStorage('console_command', '')
@@ -82,6 +87,9 @@ const ytDlpOptions = computed(() => config.ytdlp_options.fl
.map(flag => ({ value: flag, description: opt.description || '' }))
))
+const hasValidCommand = computed(() => command.value && command.value.trim().length > 0)
+const isMultiLineInput = computed(() => !!command.value && command.value.includes('\n'))
+
watch(() => isLoading.value, async value => {
if (value) {
return
@@ -99,6 +107,61 @@ watch(() => config.app.console_enabled, async () => {
await navigateTo('/')
})
+const handleKeyDown = async (event: KeyboardEvent): Promise => {
+ const target = event.target as HTMLInputElement | HTMLTextAreaElement
+ const isTextarea = 'TEXTAREA' === target.tagName
+
+ if (event.key !== 'Enter') {
+ return
+ }
+
+ if (((event.ctrlKey && isTextarea) || !isTextarea) && hasValidCommand.value) {
+ event.preventDefault()
+ runCommand()
+ return
+ }
+
+ if (event.shiftKey && !isTextarea) {
+ event.preventDefault()
+ const cursorPos = target.selectionStart || command.value.length
+ command.value = command.value.substring(0, cursorPos) + '\n' + command.value.substring(target.selectionEnd || cursorPos)
+ await nextTick()
+ if (commandTextarea.value) {
+ const textarea = commandTextarea.value.$el?.querySelector('textarea') as HTMLTextAreaElement
+ if (textarea) {
+ textarea.setSelectionRange(cursorPos + 1, cursorPos + 1)
+ textarea.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 = command.value || ''
+ const start = target.selectionStart || currentValue.length
+ const end = target.selectionEnd || currentValue.length
+ command.value = currentValue.substring(0, start) + pastedText + currentValue.substring(end)
+ await nextTick()
+
+ if (!commandTextarea.value) {
+ return
+ }
+
+ const textarea = commandTextarea.value.$el?.querySelector('textarea') as HTMLTextAreaElement
+ if (textarea) {
+ const newPos = start + pastedText.length
+ textarea.setSelectionRange(newPos, newPos)
+ textarea.focus()
+ }
+}
+
const handle_event = () => {
if (!terminal.value) {
return
@@ -135,7 +198,7 @@ const ensureTerminal = async () => {
}
const runCommand = async () => {
- if ('' === command.value) {
+ if (!hasValidCommand.value) {
return
}
@@ -145,22 +208,24 @@ const runCommand = async () => {
return
}
- if (command.value.startsWith('yt-dlp')) {
- command.value = command.value.replace(/^yt-dlp/, '').trim()
+ let cmd = command.value.trim().replace(/\n/g, ' ').trim()
+
+ if (cmd.startsWith('yt-dlp')) {
+ cmd = cmd.replace(/^yt-dlp/, '').trim()
await nextTick()
- if ('' === command.value) {
+ if ('' === cmd) {
return
}
}
await ensureTerminal()
- if ('clear' === command.value) {
+ if ('clear' === cmd) {
clearOutput(true)
return
}
- socket.emit('cli_post', command.value)
+ socket.emit('cli_post', cmd)
isLoading.value = true
terminal.value?.writeln(`user@YTPTube ~`)
terminal.value?.writeln(`$ yt-dlp ${command.value}`)
@@ -179,25 +244,29 @@ const clearOutput = async (withCommand: boolean = false) => {
focusInput()
}
-const focusInput = () => {
- const inputElement = document.getElementById('command') as HTMLInputElement
- if (inputElement) {
- inputElement.focus()
+const focusInput = async () => {
+ await nextTick()
+ let elm;
+ if (isMultiLineInput.value) {
+ elm = commandTextarea.value?.$el?.querySelector('textarea') as HTMLTextAreaElement
+ } else {
+ elm = commandInput.value?.$el?.querySelector('input') as HTMLInputElement
}
+
+ elm?.focus()
}
const writer = (s: string) => {
if (!terminal.value) {
return
}
-
- const json = JSON.parse(s)
-
- terminal.value.writeln(json.data.line)
+ terminal.value.writeln(JSON.parse(s).data.line)
}
const loader = () => isLoading.value = false
+watch(isMultiLineInput, () => focusInput())
+
onMounted(async () => {
document.addEventListener('resize', handle_event);
focusInput()
@@ -213,7 +282,6 @@ onMounted(async () => {
if (storedCommand.value) {
command.value = storedCommand.value
await nextTick()
- runCommand()
}
})
diff --git a/ui/app/stores/NotificationStore.ts b/ui/app/stores/NotificationStore.ts
index 3bbc2225..5ef9b276 100644
--- a/ui/app/stores/NotificationStore.ts
+++ b/ui/app/stores/NotificationStore.ts
@@ -3,8 +3,8 @@ import { useStorage } from '@vueuse/core'
import type { notification, notificationType } from '~/composables/useNotification'
const _map: Record = {
- 'error': { level: 3, color: 'is-danger', icon: 'fas fa-circle-exclamation' },
- 'warning': { level: 2, color: 'is-warning', icon: 'fas fa-triangle-exclamation' },
+ 'error': { level: 3, color: 'is-danger', icon: 'fas fa-triangle-exclamation' },
+ 'warning': { level: 2, color: 'is-warning', icon: 'fas fa-circle-exclamation' },
'success': { level: 1, color: 'is-primary', icon: 'fas fa-circle-check' },
'info': { level: 0, color: 'is-info', icon: 'fas fa-circle-info' }
}