Merge pull request #504 from arabcoders/dev
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled
QoL: Support newlines as seperator for URLs.
This commit is contained in:
commit
af84a03ad1
5 changed files with 289 additions and 55 deletions
2
FAQ.md
2
FAQ.md
|
|
@ -479,7 +479,7 @@ This setup should work for at VAAPI encoding in `x86_64` containers.
|
|||
> Your `video`, `render` group id might be different from mine, you can run the follow command in docker host server to get the group ids for both groups.
|
||||
|
||||
```bash
|
||||
$ cat /etc/group | grep -E 'render|video'
|
||||
cat /etc/group | grep -E 'render|video'
|
||||
|
||||
video:x:44:your_docker_username
|
||||
render:x:105:your_docker_username
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ services:
|
|||
> If you have low RAM, remove the `tmpfs` and mount a disk-based directory to `/tmp` instead. See [FAQ](FAQ.md#getting-no-space-left-on-device-error) for more information.
|
||||
|
||||
```bash
|
||||
$ mkdir -p ./{config,downloads} && docker compose -f compose.yaml up -d
|
||||
mkdir -p ./{config,downloads} && docker compose -f compose.yaml up -d
|
||||
```
|
||||
|
||||
Then you can access the WebUI at `http://localhost:8081`.
|
||||
|
|
|
|||
|
|
@ -5,18 +5,26 @@
|
|||
<div class="box">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-12">
|
||||
<label class="label is-inline is-unselectable" for="url">
|
||||
<label class=" label is-inline is-unselectable" for="url">
|
||||
<span class="icon"><i class="fa-solid fa-link" /></span>
|
||||
URLs separated by <span class="is-bold">{{ getSeparatorsName(separator) }}</span>
|
||||
<span class="has-tooltip" v-tooltip="'Use Shift+Enter to switch to multiline input mode.'">
|
||||
URLs separated by newlines or <span class="is-bold is-lowercase">{{ getSeparatorsName(separator)
|
||||
}}</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<input type="text" class="input" id="url" placeholder="URLs to download"
|
||||
:disabled="!socket.isConnected || addInProgress" v-model="form.url">
|
||||
<textarea v-if="isMultiLineInput" ref="urlTextarea" class="textarea" id="url"
|
||||
:disabled="!socket.isConnected || addInProgress" v-model="form.url" @keydown="handleKeyDown"
|
||||
@input="adjustTextareaHeight"
|
||||
style="resize: none; overflow-y: auto; min-height: 38px; max-height: 300px;" />
|
||||
<input v-else type="text" class="input" id="url" placeholder="URLs to download"
|
||||
:disabled="!socket.isConnected || addInProgress" v-model="form.url" @keydown="handleKeyDown"
|
||||
@paste="handlePaste">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary" :class="{ 'is-loading': addInProgress }"
|
||||
:disabled="!socket.isConnected || addInProgress || !form?.url">
|
||||
:disabled="!socket.isConnected || addInProgress || !hasValidUrl">
|
||||
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
||||
<span>Add</span>
|
||||
</button>
|
||||
|
|
@ -209,7 +217,7 @@
|
|||
|
||||
<div class="control" v-if="config.app.console_enabled" v-tooltip="'Run directly in console'">
|
||||
<button type="button" class="button is-warning" @click="runCliCommand"
|
||||
:disabled="!socket.isConnected || !form?.url">
|
||||
:disabled="!socket.isConnected || !hasValidUrl">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -217,15 +225,15 @@
|
|||
<div class="control">
|
||||
<button type="button" class="button is-info"
|
||||
v-tooltip="'Get yt-dlp information for the provided URL.'"
|
||||
@click="emitter('getInfo', form.url, form.preset, form.cli)"
|
||||
:disabled="!socket.isConnected || addInProgress || !form?.url || multiURLs">
|
||||
@click="emitter('getInfo', splitUrls(form.url || '')[0] || '', form.preset, form.cli)"
|
||||
:disabled="!socket.isConnected || addInProgress || !hasValidUrl">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control" v-if="config.app.console_enabled">
|
||||
<button type="button" class="button is-success" @click="testDownloadOptions"
|
||||
:disabled="!socket.isConnected || !form?.url" v-tooltip="'Show compiled yt-dlp options.'">
|
||||
:disabled="!socket.isConnected || !hasValidUrl" v-tooltip="'Show compiled yt-dlp options.'">
|
||||
<span class="icon"><i class="fa-solid fa-flask" /></span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -291,6 +299,7 @@ const testResultsData = ref<any>(null)
|
|||
const dlFieldsExtra = ['--no-download-archive', '--no-continue']
|
||||
const ytDlpOpt = ref<AutoCompleteOptions>([])
|
||||
const cookiesDropzoneRef = ref<InstanceType<typeof TextDropzone> | null>(null)
|
||||
const urlTextarea = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const form = useStorage<item_request>('local_config_v1', {
|
||||
id: null,
|
||||
|
|
@ -304,14 +313,6 @@ const form = useStorage<item_request>('local_config_v1', {
|
|||
}) as Ref<item_request>
|
||||
|
||||
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<string> => {
|
||||
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<item_request>
|
||||
|
||||
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<void> => {
|
|||
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(/(?<!\S)(-f|--format)(=|\s)(\S+)/))
|
||||
|
||||
const filter_presets = (flag: boolean = true) => 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()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
.terminal {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
|
|
@ -39,15 +48,17 @@
|
|||
<section class="card-content p-1 m-1">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<InputAutocomplete v-model="command" :options="ytDlpOptions" :disabled="isLoading" placeholder="--help"
|
||||
id="command" @keydown.enter="runCommand" :multiple="true" :allowShortFlags="true" />
|
||||
<TextareaAutocomplete v-if="isMultiLineInput" ref="commandTextarea" v-model="command"
|
||||
:options="ytDlpOptions" :disabled="isLoading" placeholder="--help" @keydown="handleKeyDown" />
|
||||
<InputAutocomplete v-else v-model="command" ref="commandInput" :options="ytDlpOptions"
|
||||
:disabled="isLoading" placeholder="--help" @keydown="handleKeyDown" @paste="handlePaste"
|
||||
:multiple="true" :allowShortFlags="true" />
|
||||
</div>
|
||||
<p class="control">
|
||||
<button class="button is-primary" type="button" :disabled="isLoading || '' === command"
|
||||
<button class="button is-primary" type="button" :disabled="isLoading || !hasValidCommand"
|
||||
@click="runCommand">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-spinner fa-spin" v-if="isLoading" />
|
||||
<i class="fa-solid fa-paper-plane" v-else />
|
||||
<i class="fa-solid" :class="isLoading ? 'fa-spinner fa-spin' : 'fa-paper-plane'" />
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
|
|
@ -55,6 +66,48 @@
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-pointer is-unselectable" @click="isHistoryCollapsed = !isHistoryCollapsed">
|
||||
<span class="icon"><i class="fa-solid fa-clock-rotate-left" /></span>
|
||||
<span class="ml-2">Commands History</span>
|
||||
</p>
|
||||
<p class="card-header-icon">
|
||||
<span v-tooltip.top="'Clear command history'" class="icon" @click="clearHistory()">
|
||||
<i class="fa-solid fa-trash" />
|
||||
</span>
|
||||
<span v-tooltip.top="isHistoryCollapsed ? 'Expand' : 'Collapse'" class="icon ml-2"
|
||||
@click="isHistoryCollapsed = !isHistoryCollapsed">
|
||||
<i class="fa-solid" :class="isHistoryCollapsed ? 'fa-chevron-down' : 'fa-chevron-up'" />
|
||||
</span>
|
||||
</p>
|
||||
</header>
|
||||
<div v-show="!isHistoryCollapsed" class="card-content p-2">
|
||||
<Message :newStyle="true" class="is-info" v-if="commandHistory.length < 1">
|
||||
Commands history is empty.
|
||||
</Message>
|
||||
<div class="table-container" v-if="commandHistory.length > 0">
|
||||
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
|
||||
style="min-width: 850px; table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr v-for="(cmd, index) in commandHistory" :key="index">
|
||||
<td>
|
||||
<code class="is-family-monospace is-text-overflow is-pointer is-block" @click="loadCommand(cmd)">
|
||||
{{ cmd.replace(/\n/g, ' ') }}
|
||||
</code>
|
||||
</td>
|
||||
<td style="width: 40px; text-align: center;">
|
||||
<button class="delete" @click="removeFromHistory(index)" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -65,27 +118,41 @@ 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 { useDialog } from '~/composables/useDialog'
|
||||
import type { AutoCompleteOptions } from '~/types/autocomplete'
|
||||
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const toast = useNotification()
|
||||
const dialog = useDialog()
|
||||
|
||||
const terminal = ref<Terminal>()
|
||||
const terminalFit = ref<FitAddon>()
|
||||
const command = ref<string>('')
|
||||
const terminal_window = useTemplateRef<HTMLDivElement>('terminal_window')
|
||||
const commandInput = ref<InstanceType<typeof InputAutocomplete> | null>(null)
|
||||
const commandTextarea = ref<InstanceType<typeof TextareaAutocomplete> | null>(null)
|
||||
const isLoading = ref<boolean>(false)
|
||||
const storedCommand = useStorage<string>('console_command', '')
|
||||
const commandHistory = useStorage<string[]>('console_command_history', [])
|
||||
const isHistoryCollapsed = useStorage<boolean>('console_history_collapsed', false)
|
||||
const MAX_HISTORY_ITEMS = 50
|
||||
|
||||
const ytDlpOptions = computed<AutoCompleteOptions>(() => config.ytdlp_options.flatMap(opt => opt.flags
|
||||
.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
|
||||
}
|
||||
if (command.value.trim()) {
|
||||
addToHistory(command.value.trim())
|
||||
}
|
||||
command.value = ''
|
||||
await nextTick();
|
||||
focusInput()
|
||||
|
|
@ -99,6 +166,61 @@ watch(() => config.app.console_enabled, async () => {
|
|||
await navigateTo('/')
|
||||
})
|
||||
|
||||
const handleKeyDown = async (event: KeyboardEvent): Promise<void> => {
|
||||
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<void> => {
|
||||
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 +257,7 @@ const ensureTerminal = async () => {
|
|||
}
|
||||
|
||||
const runCommand = async () => {
|
||||
if ('' === command.value) {
|
||||
if (!hasValidCommand.value) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -145,22 +267,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 +303,56 @@ 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 addToHistory = (cmd: string) => {
|
||||
commandHistory.value = [cmd, ...commandHistory.value.filter(h => h !== cmd)].slice(0, MAX_HISTORY_ITEMS)
|
||||
}
|
||||
|
||||
const loadCommand = async (cmd: string) => {
|
||||
command.value = cmd
|
||||
await nextTick()
|
||||
focusInput()
|
||||
}
|
||||
|
||||
const clearHistory = async () => {
|
||||
if (commandHistory.value.length === 0) {
|
||||
return
|
||||
}
|
||||
const { status } = await dialog.confirmDialog({
|
||||
title: 'Confirm Action',
|
||||
message: `Clear commands history?`,
|
||||
confirmColor: 'is-danger',
|
||||
})
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
commandHistory.value = []
|
||||
}
|
||||
|
||||
const removeFromHistory = (index: number) => commandHistory.value = commandHistory.value.filter((_, i) => i !== index)
|
||||
|
||||
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 +368,6 @@ onMounted(async () => {
|
|||
if (storedCommand.value) {
|
||||
command.value = storedCommand.value
|
||||
await nextTick()
|
||||
runCommand()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { useStorage } from '@vueuse/core'
|
|||
import type { notification, notificationType } from '~/composables/useNotification'
|
||||
|
||||
const _map: Record<notificationType, { level: number; color: string; icon: string }> = {
|
||||
'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' }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue