commit
948235a0e5
9 changed files with 369 additions and 116 deletions
6
Pipfile.lock
generated
6
Pipfile.lock
generated
|
|
@ -1098,12 +1098,12 @@
|
||||||
},
|
},
|
||||||
"yt-dlp": {
|
"yt-dlp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2b0c86b579d4a044eaf3c4b00e3d7b24d82e6e26869fa11c288ea4395b387f41",
|
"sha256:b50a595abde523b5cc84d788f97e69c642503bd673ba740f709ebf65b5ec6592",
|
||||||
"sha256:4f76b48244c783e6ac06e8d7627bcf62cbeb4f6d79ba7e3cfc8249e680d4e691"
|
"sha256:e19f00f9e55e90bca1c94bcaf809aa33e51634be9f0de2df84a72d3206934f94"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2024.7.2"
|
"version": "==2024.7.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
|
|
||||||
69
app/main.py
69
app/main.py
|
|
@ -140,7 +140,7 @@ class Main:
|
||||||
|
|
||||||
# get directory listing
|
# get directory listing
|
||||||
dlDir: str = self.config.download_path
|
dlDir: str = self.config.download_path
|
||||||
data['directories'] = [ name for name in os.listdir(dlDir) if os.path.isdir(os.path.join(dlDir, name)) ]
|
data['directories'] = [name for name in os.listdir(dlDir) if os.path.isdir(os.path.join(dlDir, name))]
|
||||||
|
|
||||||
await self.sio.emit('initial_data', self.serializer.encode(data), to=sid)
|
await self.sio.emit('initial_data', self.serializer.encode(data), to=sid)
|
||||||
|
|
||||||
|
|
@ -500,36 +500,55 @@ class Main:
|
||||||
@self.sio.event()
|
@self.sio.event()
|
||||||
async def cli_post(sid, data):
|
async def cli_post(sid, data):
|
||||||
if not data:
|
if not data:
|
||||||
|
await self.sio.emit('cli_close', {'exitcode': 0})
|
||||||
return
|
return
|
||||||
|
|
||||||
async def _read_stream(streamType, stream):
|
proc = None
|
||||||
while True:
|
|
||||||
line = await stream.readline()
|
|
||||||
if line:
|
|
||||||
await self.sio.emit('cli_output', {
|
|
||||||
'type': streamType,
|
|
||||||
'line': line.decode('utf-8')
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
proc = await asyncio.subprocess.create_subprocess_exec(
|
try:
|
||||||
'yt-dlp', data,
|
async def _read_stream(streamType, stream):
|
||||||
cwd=self.config.download_path,
|
while True:
|
||||||
stdin=asyncio.subprocess.DEVNULL,
|
line = await stream.readline()
|
||||||
stdout=asyncio.subprocess.PIPE,
|
if line:
|
||||||
stderr=asyncio.subprocess.PIPE
|
await self.sio.emit('cli_output', {
|
||||||
)
|
'type': streamType,
|
||||||
|
'line': line.decode('utf-8')
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
await asyncio.gather(
|
LOG.info(f'CLI: {data}')
|
||||||
_read_stream('stdout', proc.stdout),
|
proc = await asyncio.subprocess.create_subprocess_exec(
|
||||||
_read_stream('stderr', proc.stderr),
|
'yt-dlp', data,
|
||||||
)
|
cwd=self.config.download_path,
|
||||||
|
stdin=asyncio.subprocess.DEVNULL,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
env=os.environ.copy().update({
|
||||||
|
"TERM": "xterm-256color",
|
||||||
|
"LANG": "en_US.UTF-8",
|
||||||
|
"SHELL": "/bin/bash",
|
||||||
|
"LC_ALL": "en_US.UTF-8",
|
||||||
|
"PWD": self.config.download_path,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
while proc.returncode is None:
|
await asyncio.gather(
|
||||||
await asyncio.sleep(0.1)
|
_read_stream('stdout', proc.stdout),
|
||||||
|
_read_stream('stderr', proc.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
await self.sio.emit('cli_close', {'exitcode': proc.returncode})
|
while proc.returncode is None:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
await self.sio.emit('cli_close', {'exitcode': proc.returncode})
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f'CLI Error: {str(e)}')
|
||||||
|
await self.sio.emit('cli_output', {
|
||||||
|
'type': 'stderr',
|
||||||
|
'line': str(e),
|
||||||
|
})
|
||||||
|
await self.sio.emit('cli_close', {'exitcode': -1})
|
||||||
|
|
||||||
@self.sio.event()
|
@self.sio.event()
|
||||||
async def connect(sid, environ):
|
async def connect(sid, environ):
|
||||||
|
|
|
||||||
58
frontend/package-lock.json
generated
58
frontend/package-lock.json
generated
|
|
@ -13,6 +13,8 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.5",
|
"@fortawesome/vue-fontawesome": "^3.0.5",
|
||||||
"@vueuse/core": "^10.6.1",
|
"@vueuse/core": "^10.6.1",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"cron-parser": "^4.9.0",
|
"cron-parser": "^4.9.0",
|
||||||
"floating-vue": "^5.0.2",
|
"floating-vue": "^5.0.2",
|
||||||
|
|
@ -3477,6 +3479,19 @@
|
||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-fit": {
|
||||||
|
"version": "0.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||||
|
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/xterm": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
|
||||||
|
},
|
||||||
"node_modules/@xtuc/ieee754": {
|
"node_modules/@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||||
|
|
@ -5393,14 +5408,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io-client": {
|
"node_modules/engine.io-client": {
|
||||||
"version": "6.5.3",
|
"version": "6.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
|
||||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
"debug": "~4.3.1",
|
"debug": "~4.3.1",
|
||||||
"engine.io-parser": "~5.2.1",
|
"engine.io-parser": "~5.2.1",
|
||||||
"ws": "~8.11.0",
|
"ws": "~8.17.1",
|
||||||
"xmlhttprequest-ssl": "~2.0.0"
|
"xmlhttprequest-ssl": "~2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -11428,9 +11443,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-bundle-analyzer/node_modules/ws": {
|
"node_modules/webpack-bundle-analyzer/node_modules/ws": {
|
||||||
"version": "7.5.9",
|
"version": "7.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.3.0"
|
"node": ">=8.3.0"
|
||||||
|
|
@ -11650,27 +11665,6 @@
|
||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-dev-server/node_modules/ws": {
|
|
||||||
"version": "8.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
|
||||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack-merge": {
|
"node_modules/webpack-merge": {
|
||||||
"version": "5.10.0",
|
"version": "5.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
|
||||||
|
|
@ -11844,15 +11838,15 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.11.0",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"bufferutil": "^4.0.1",
|
"bufferutil": "^4.0.1",
|
||||||
"utf-8-validate": "^5.0.2"
|
"utf-8-validate": ">=5.0.2"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"bufferutil": {
|
"bufferutil": {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@
|
||||||
"plyr": "^3.7.8",
|
"plyr": "^3.7.8",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"vue": "^3.4.27",
|
"vue": "^3.4.27",
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
"vue-toastification": "^2.0.0-rc.5",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/xterm": "^5.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.16",
|
"@babel/core": "^7.12.16",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<PageHeader :config="config" @toggleForm="addForm = !addForm" @toggleTasks="showTasks = !showTasks"
|
<PageHeader :config="config" @toggleForm="addForm = !addForm" @toggleTasks="showTasks = !showTasks"
|
||||||
@toggleConsole="showConsole = !showConsole" @reload="reloadWindow" />
|
@toggleConsole="showConsole = !showConsole" @toggleConsole2="showConsole2 = !showConsole2" @reload="reloadWindow" />
|
||||||
|
|
||||||
<CliConsole v-if="showConsole" @runCommand="runCommand" :cli_output="cli_output" :isLoading="cli_isLoading"
|
<CliConsole v-if="showConsole" @runCommand="runCommand" :cli_output="cli_output" :isLoading="cli_isLoading"
|
||||||
@cli_clear="cli_output = []" />
|
@cli_clear="cli_output = []" />
|
||||||
|
|
@ -36,7 +36,7 @@ import VideoPlayer from './components/Video-Player'
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { useStorage, useEventBus } from '@vueuse/core'
|
import { useStorage, useEventBus } from '@vueuse/core'
|
||||||
import CliConsole from './components/CLI-Console.vue'
|
import CliConsole from './components/CLI-Console'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const bus = useEventBus('item_added', 'show_form', 'task_edit')
|
const bus = useEventBus('item_added', 'show_form', 'task_edit')
|
||||||
|
|
@ -56,11 +56,11 @@ const showTasks = useStorage('showTasks', false)
|
||||||
const cli_output = ref([])
|
const cli_output = ref([])
|
||||||
const cli_isLoading = ref(false)
|
const cli_isLoading = ref(false)
|
||||||
const showConsole = ref(false)
|
const showConsole = ref(false)
|
||||||
|
const showConsole2 = ref(false)
|
||||||
|
|
||||||
const runCommand = (args) => {
|
const runCommand = (args) => {
|
||||||
cli_output.value = [];
|
cli_output.value = [];
|
||||||
cli_isLoading.value = true;
|
cli_isLoading.value = true;
|
||||||
console.log(args)
|
|
||||||
socket.value.emit('cli_post', args);
|
socket.value.emit('cli_post', args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,8 +142,8 @@ onMounted(() => {
|
||||||
|
|
||||||
socket.value.on('cli_close', () => cli_isLoading.value = false);
|
socket.value.on('cli_close', () => cli_isLoading.value = false);
|
||||||
socket.value.on('cli_output', stream => {
|
socket.value.on('cli_output', stream => {
|
||||||
console.log(stream)
|
|
||||||
cli_output.value.push(stream)
|
cli_output.value.push(stream)
|
||||||
|
console.log(stream);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -212,7 +212,7 @@ const addItem = (item) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const playItem = (item) => {
|
const playItem = item => {
|
||||||
let baseDir = 'm3u8/';
|
let baseDir = 'm3u8/';
|
||||||
|
|
||||||
if (item.folder) {
|
if (item.folder) {
|
||||||
|
|
|
||||||
|
|
@ -7,63 +7,157 @@
|
||||||
command.
|
command.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="column is-12">
|
||||||
|
|
||||||
<div class="column is-12">
|
<div class="card">
|
||||||
<form @submit.prevent="$emit('runCommand', command)">
|
<header class="card-header">
|
||||||
<div class="field">
|
<p class="card-header-title">
|
||||||
<div class="field-body">
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-terminal" /></span> Terminal
|
||||||
<div class="field is-grouped">
|
</p>
|
||||||
<p class="control is-expanded has-icons-left">
|
<p class="card-header-icon">
|
||||||
<input type="text" class="input" v-model="command" placeholder="--help" autocomplete="off" autofocus
|
<span class="icon" @click="clearOutput"><font-awesome-icon icon="fa-solid fa-broom" /></span>
|
||||||
:disabled="props.isLoading">
|
</p>
|
||||||
<span class="icon is-left">
|
</header>
|
||||||
<font-awesome-icon icon="fa-solid fa-terminal" />
|
<section class="card-content p-0 m-0">
|
||||||
</span>
|
<div ref="terminal_window" style="min-height: 60vh;max-height:70vh;" />
|
||||||
</p>
|
</section>
|
||||||
<p class="control">
|
<section class="card-content p-1 m-1">
|
||||||
<button class="button is-primary" type="submit" :disabled="props.isLoading"
|
<div class="field is-grouped">
|
||||||
:class="{ 'is-loading': props.isLoading }">
|
<div class="control is-expanded has-icons-left">
|
||||||
<span class="icon-text">
|
<input type="text" class="input" v-model="command" placeholder="ls -la" autocomplete="off"
|
||||||
<span class="icon"><font-awesome-icon icon="fa-solid fa-server" /></span>
|
ref="command_input" @keydown.enter="runCommand" :disabled="props.isLoading" id="command">
|
||||||
<span>Run</span>
|
<span class="icon is-left"><font-awesome-icon icon="fa-solid fa-terminal" /></span>
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<p class="control">
|
|
||||||
<button class="button is-info" type="button" v-tooltip="'Clear output'" @click="$emit('cli_clear')">
|
|
||||||
<span class="icon-text">
|
|
||||||
<span class="icon"><font-awesome-icon icon="fa-solid fa-broom" /></span>
|
|
||||||
<span>Clear</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-primary" type="button" :disabled="props.isLoading || '' === command"
|
||||||
|
:class="{ 'is-loading': props.isLoading }" @click="runCommand">
|
||||||
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-paper-plane" /></span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="column is-12">
|
|
||||||
<pre ref="outputConsole"
|
|
||||||
style="min-height: 60vh;max-height:65vh; overflow-y: scroll"><code><div v-for="(item, index) in props.cli_output" :key="'log_line-' + index" :class="{ 'has-text-danger': 'stderr' === item.type }">{{ item.line }}</div></code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineEmits, defineProps, ref } from 'vue'
|
import { ref, watch, defineProps, onMounted, defineEmits, onUnmounted, nextTick } from 'vue'
|
||||||
|
import "@xterm/xterm/css/xterm.css"
|
||||||
|
import { Terminal } from "@xterm/xterm"
|
||||||
|
import { FitAddon } from "@xterm/addon-fit"
|
||||||
|
|
||||||
const props = defineProps({
|
const emitter = defineEmits(['runCommand', 'cli_clear']);
|
||||||
cli_output: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
isLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
defineEmits(['runCommand','cli_clear']);
|
const terminal = ref()
|
||||||
|
const terminalFit = ref()
|
||||||
|
|
||||||
const command = ref('')
|
const command = ref('')
|
||||||
|
const terminal_window = ref()
|
||||||
|
const command_input = ref()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
cli_output: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.isLoading, async value => {
|
||||||
|
if (value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command.value = ''
|
||||||
|
await nextTick();
|
||||||
|
focusInput()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(() => props.cli_output, v => {
|
||||||
|
if (!terminal.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!v.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- display last output
|
||||||
|
const data = v.slice(-1)
|
||||||
|
terminal.value.writeln(data[0].line)
|
||||||
|
|
||||||
|
}, {
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const reSizeTerminal = () => {
|
||||||
|
if (!terminal.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
terminalFit.value.fit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const runCommand = async () => {
|
||||||
|
if ('' === command.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!terminal.value) {
|
||||||
|
terminal.value = new Terminal({
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
cursorBlink: false,
|
||||||
|
cursorStyle: 'underline',
|
||||||
|
cols: 108,
|
||||||
|
rows: 10,
|
||||||
|
buffer: 1000,
|
||||||
|
disableStdin: true,
|
||||||
|
scrollback: 1000,
|
||||||
|
})
|
||||||
|
terminalFit.value = new FitAddon()
|
||||||
|
terminal.value.loadAddon(terminalFit.value)
|
||||||
|
terminal.value.open(terminal_window.value)
|
||||||
|
terminalFit.value.fit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('clear' === command.value) {
|
||||||
|
clearOutput(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter('runCommand', command.value)
|
||||||
|
terminal.value.writeln(`~ ${command.value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearOutput = async (withCommand = false) => {
|
||||||
|
if (terminal.value) {
|
||||||
|
terminal.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter('cli_clear', {})
|
||||||
|
if (true === withCommand) {
|
||||||
|
command.value = ''
|
||||||
|
}
|
||||||
|
focusInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.addEventListener('resize', reSizeTerminal);
|
||||||
|
focusInput()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
if (!command_input.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command_input.value.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => window.removeEventListener('resize', reSizeTerminal));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
142
frontend/src/components/CLI-Terminal.vue
Normal file
142
frontend/src/components/CLI-Terminal.vue
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
<template>
|
||||||
|
<div class="mt-1 columns is-multiline">
|
||||||
|
<div class="column is-12 is-clearfix">
|
||||||
|
<h1 class="title is-4">Console</h1>
|
||||||
|
<div class="subtitle is-6">
|
||||||
|
You can execute <strong>non-interactive</strong> commands here. The interface jailed to the <code>yt-dlp</code>
|
||||||
|
command.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-12">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">
|
||||||
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-terminal" /></span> Terminal
|
||||||
|
</p>
|
||||||
|
<p class="card-header-icon">
|
||||||
|
<span class="icon" @click="clearOutput"><font-awesome-icon icon="fa-solid fa-broom" /></span>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<section class="card-content p-0 m-0">
|
||||||
|
<div ref="terminal_window" style="min-height: 60vh;max-height:70vh;" />
|
||||||
|
</section>
|
||||||
|
<section class="card-content p-1 m-1">
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<div class="control is-expanded has-icons-left">
|
||||||
|
<input type="text" class="input" v-model="command" placeholder="ls -la" autocomplete="off"
|
||||||
|
ref="command_input" @keydown.enter="runCommand" :disabled="props.isLoading" id="command">
|
||||||
|
<span class="icon is-left"><font-awesome-icon icon="fa-solid fa-terminal" /></span>
|
||||||
|
</div>
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-primary" type="button" :disabled="props.isLoading || '' === command"
|
||||||
|
:class="{ 'is-loading': props.isLoading }" @click="runCommand">
|
||||||
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-paper-plane" /></span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, defineProps, onMounted, defineEmits, onUnmounted, nextTick } from 'vue'
|
||||||
|
import "@xterm/xterm/css/xterm.css"
|
||||||
|
import { Terminal } from "@xterm/xterm"
|
||||||
|
import { FitAddon } from "@xterm/addon-fit"
|
||||||
|
|
||||||
|
const emitter = defineEmits(['runCommand', 'cli_clear']);
|
||||||
|
|
||||||
|
const terminal = ref()
|
||||||
|
const terminalFit = ref()
|
||||||
|
|
||||||
|
const command = ref('')
|
||||||
|
const terminal_window = ref()
|
||||||
|
const command_input = ref()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
cli_output: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.isLoading, async value => {
|
||||||
|
if (value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command.value = ''
|
||||||
|
await nextTick();
|
||||||
|
command_input.value.focus()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(() => props.cli_output, v => {
|
||||||
|
if (!terminal.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!v.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- display last output
|
||||||
|
const data = v.slice(-1)
|
||||||
|
terminal.value.writeln(data[0].line)
|
||||||
|
|
||||||
|
}, {
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const reSizeTerminal = () => {
|
||||||
|
if (!terminal.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
terminalFit.value.fit()
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
terminalFit.value = new FitAddon()
|
||||||
|
terminal.value.loadAddon(terminalFit.value)
|
||||||
|
terminal.value.open(terminal_window.value)
|
||||||
|
terminalFit.value.fit();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter('runCommand', command.value)
|
||||||
|
terminal.value.writeln(`~ ${command.value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearOutput = async () => {
|
||||||
|
if (terminal.value) {
|
||||||
|
terminal.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter('cli_clear', {})
|
||||||
|
command_input.value.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.addEventListener("resize", reSizeTerminal);
|
||||||
|
command_input.value.focus()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => window.removeEventListener("resize", reSizeTerminal));
|
||||||
|
</script>
|
||||||
|
|
@ -51,7 +51,7 @@ import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')());
|
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')());
|
||||||
|
|
||||||
defineEmits(['toggleForm', 'toggleTasks'])
|
defineEmits(['toggleForm', 'toggleTasks', 'toggleConsole', 'reload'])
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
config: {
|
config: {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ import {
|
||||||
faTerminal,
|
faTerminal,
|
||||||
faBroom,
|
faBroom,
|
||||||
faServer,
|
faServer,
|
||||||
faPowerOff
|
faPowerOff,
|
||||||
|
faPaperPlane
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-svg-icons'
|
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
@ -22,7 +23,8 @@ import 'floating-vue/dist/style.css'
|
||||||
|
|
||||||
library.add(faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload,
|
library.add(faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload,
|
||||||
faUpRightFromSquare, faSquare, faSquareCheck, faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar, faArrowUpAZ,
|
faUpRightFromSquare, faSquare, faSquareCheck, faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar, faArrowUpAZ,
|
||||||
faArrowDownAZ, faEject, faGlobe, faMoon, faSun, faTerminal, faBroom, faServer, faPowerOff)
|
faArrowDownAZ, faEject, faGlobe, faMoon, faSun, faTerminal, faBroom, faServer, faPowerOff, faBroom, faServer,
|
||||||
|
faPowerOff, faPaperPlane)
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue