more updates for new UI
This commit is contained in:
parent
9fc047dba3
commit
c61fd133d8
12 changed files with 351 additions and 197 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -84,7 +84,7 @@
|
||||||
"--port",
|
"--port",
|
||||||
"8082"
|
"8082"
|
||||||
],
|
],
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "yarn",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"cwd": "${workspaceFolder}/ui",
|
"cwd": "${workspaceFolder}/ui",
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
|
|
@ -224,8 +224,6 @@ import moment from "moment";
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import LazyLoader from './LazyLoader'
|
import LazyLoader from './LazyLoader'
|
||||||
import { makeDownload, formatBytes, ucFirst } from '~/utils/index'
|
import { makeDownload, formatBytes, ucFirst } from '~/utils/index'
|
||||||
import { useConfigStore } from '~/store/ConfigStore';
|
|
||||||
import { useStateStore } from '~/store/StateStore';
|
|
||||||
|
|
||||||
const emits = defineEmits(['deleteItem', 'addItem', 'playItem', 'archiveItem'])
|
const emits = defineEmits(['deleteItem', 'addItem', 'playItem', 'archiveItem'])
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<input type="url" class="input" id="url" placeholder="Video or playlist link"
|
<input type="url" class="input" id="url" placeholder="Video or playlist link"
|
||||||
:disabled="!config.isConnected || addInProgress" v-model="url">
|
:disabled="!socket.isConnected || addInProgress" v-model="url">
|
||||||
<span class="icon is-small is-left">
|
<span class="icon is-small is-left">
|
||||||
<i class="fa-solid fa-link" />
|
<i class="fa-solid fa-link" />
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select id="preset" class="is-fullwidth" :disabled="!config.isConnected" v-model="selectedPreset">
|
<select id="preset" class="is-fullwidth" :disabled="!socket.isConnected" v-model="selectedPreset">
|
||||||
<option v-for="item in downloadFormats" :key="item.id" :value="item.id">
|
<option v-for="item in config.presets" :key="item.name" :value="item.format">
|
||||||
{{ item.text }}
|
{{ item.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -35,27 +35,23 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<input type="text" class="input is-fullwidth" id="path" v-model="downloadPath" placeholder="Default"
|
<input type="text" class="input is-fullwidth" id="path" v-model="downloadPath" placeholder="Default"
|
||||||
:disabled="!config.isConnected" list="directories">
|
:disabled="!socket.isConnected" list="directories">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button type="submit" class="button is-primary" @click="addDownload"
|
<button type="submit" class="button is-primary" @click="addDownload"
|
||||||
:class="{ 'is-loading': !config.isConnected || addInProgress }"
|
:class="{ 'is-loading': !socket.isConnected || addInProgress }"
|
||||||
:disabled="!config.isConnected || addInProgress || !url">
|
:disabled="!socket.isConnected || addInProgress || !url">
|
||||||
<span class="icon">
|
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
||||||
<i class="fa-solid fa-plus" />
|
<span>Add</span>
|
||||||
</span>
|
|
||||||
<span>Add Link</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button type="submit" class="button is-info" @click="showAdvanced = !showAdvanced"
|
<button type="submit" class="button is-info" @click="showAdvanced = !showAdvanced"
|
||||||
v-tooltip="'Show advanced options'" :class="{ 'is-loading': !config.isConnected }"
|
v-tooltip="'Show advanced options'" :class="{ 'is-loading': !socket.isConnected }"
|
||||||
:disabled="!config.isConnected">
|
:disabled="!socket.isConnected">
|
||||||
<span class="icon">
|
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||||
<i class="fa-solid fa-cog" />
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -84,7 +80,7 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig"
|
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig"
|
||||||
:disabled="!config.isConnected"></textarea>
|
:disabled="!socket.isConnected"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<span class="subtitle is-6 has-text-info">
|
<span class="subtitle is-6 has-text-info">
|
||||||
Some config fields are ignored like cookiefile, path, and output_format etc.
|
Some config fields are ignored like cookiefile, path, and output_format etc.
|
||||||
|
|
@ -101,7 +97,7 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" id="ytdlpCookies" v-model="ytdlpCookies"
|
<textarea class="textarea" id="ytdlpCookies" v-model="ytdlpCookies"
|
||||||
:disabled="!config.isConnected"></textarea>
|
:disabled="!socket.isConnected"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<span class="subtitle is-6 has-text-info">
|
<span class="subtitle is-6 has-text-info">
|
||||||
Use something like <a class="has-text-danger" href="https://github.com/jrie/flagCookies">flagCookies</a>
|
Use something like <a class="has-text-danger" href="https://github.com/jrie/flagCookies">flagCookies</a>
|
||||||
|
|
@ -112,7 +108,7 @@
|
||||||
<div class="column is-12 has-text-right">
|
<div class="column is-12 has-text-right">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="submit" class="button is-danger" @click="resetConfig" :disabled="!config.isConnected"
|
<button type="submit" class="button is-danger" @click="resetConfig" :disabled="!socket.isConnected"
|
||||||
v-tooltip="'This configuration are stored locally in your browser.'">
|
v-tooltip="'This configuration are stored locally in your browser.'">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa-solid fa-trash" />
|
<i class="fa-solid fa-trash" />
|
||||||
|
|
@ -132,15 +128,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineEmits, defineProps, onMounted, ref } from 'vue'
|
|
||||||
import { useStorage, useEventBus } from '@vueuse/core'
|
import { useStorage, useEventBus } from '@vueuse/core'
|
||||||
import { useConfigStore } from '~/store/ConfigStore';
|
|
||||||
|
|
||||||
const config = useConfigStore();
|
const config = useConfigStore();
|
||||||
|
const socket = useSocketStore();
|
||||||
|
|
||||||
const bus = useEventBus('item_added', 'task_edit');
|
const bus = useEventBus('item_added', 'task_edit');
|
||||||
const emits = defineEmits(['addItem']);
|
const emits = defineEmits(['addItem']);
|
||||||
|
|
||||||
const selectedFormat = useStorage('selectedFormat', 'any')
|
const selectedFormat = useStorage('selectedFormat', 'any')
|
||||||
|
const selectedPreset = useStorage('selectedPreset', 'default')
|
||||||
const selectedQuality = useStorage('selectedQuality', '')
|
const selectedQuality = useStorage('selectedQuality', '')
|
||||||
const ytdlpConfig = useStorage('ytdlp_config', '')
|
const ytdlpConfig = useStorage('ytdlp_config', '')
|
||||||
const ytdlpCookies = useStorage('ytdlp_cookies', '')
|
const ytdlpCookies = useStorage('ytdlp_cookies', '')
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content has-text-centered" v-if="!hasQueuedItems">
|
<div class="content has-text-centered" v-if="!hasQueuedItems">
|
||||||
<p v-if="config.isConnected">
|
<p v-if="socket.isConnected">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon has-text-success">
|
<span class="icon has-text-success">
|
||||||
<i class="fa-solid fa-circle-check" />
|
<i class="fa-solid fa-circle-check" />
|
||||||
|
|
@ -127,13 +127,12 @@ import { defineProps, defineEmits, ref, watch, computed } from 'vue';
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import LazyLoader from './LazyLoader'
|
import LazyLoader from './LazyLoader'
|
||||||
import { useConfigStore } from '~/store/ConfigStore';
|
|
||||||
import { useStateStore } from '~/store/StateStore';
|
|
||||||
|
|
||||||
const emit = defineEmits(['deleteItem']);
|
const emit = defineEmits(['deleteItem']);
|
||||||
|
|
||||||
const config = useConfigStore();
|
const config = useConfigStore();
|
||||||
const stateStore = useStateStore();
|
const stateStore = useStateStore();
|
||||||
|
const socket = useSocketStore();
|
||||||
|
|
||||||
const selectedElms = ref([]);
|
const selectedElms = ref([]);
|
||||||
const masterSelectAll = ref(false);
|
const masterSelectAll = ref(false);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,5 @@ defineProps({
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const showStacks = ref(false)
|
const showStacks = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,77 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="main_container" class="container" :class="{ 'is-max-fullwidth': expandContainer }">
|
<div id="main_container" class="container">
|
||||||
<nav class="navbar is-dark mb-5">
|
<nav class="navbar is-mobile is-dark">
|
||||||
<div class="navbar-brand pl-5">
|
<div class="navbar-brand pl-5">
|
||||||
<NuxtLink class="navbar-item" to="/">
|
<NuxtLink class="navbar-item has-tooltip-bottom" to="/"
|
||||||
|
v-tooltip="socket.isConnected ? 'Connected' : 'Connecting'">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon"><i class="fas fa-home"></i></span>
|
<span class="icon"><i class="fas fa-home"></i></span>
|
||||||
<span>Home</span>
|
<span :class="socket.isConnected ? 'has-text-success' : 'has-text-danger'"><b>YTPTube</b></span>
|
||||||
</span>
|
</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<a class="navbar-item is-hidden-tablet" id="top" href="#bottom">
|
|
||||||
<span class="icon"><i class="fas fa-arrow-down"></i></span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<button class="navbar-burger burger" @click="showMenu = !showMenu">
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="navbar-end is-flex">
|
||||||
|
<div class="navbar-item">
|
||||||
|
<NuxtLink class="button is-dark has-tooltip-bottom" to="/console">
|
||||||
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
|
<span>Terminal</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="navbar-menu is-unselectable" :class="{ 'is-active': showMenu }">
|
<div class="navbar-item">
|
||||||
<div class="navbar-end pr-3">
|
<button v-tooltip.bottom="'Toggle Add Form'" class="button is-dark has-tooltip-bottom"
|
||||||
|
|
||||||
<div class="navbar-item">
|
|
||||||
<button v-tooltip.bottom="'Toggle Console'" class="button is-dark has-tooltip-bottom"
|
|
||||||
@click="config.showConsole = !config.showConsole">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="fa-solid fa-terminal" />
|
|
||||||
</span>
|
|
||||||
<span class="is-hidden-mobile">Console</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="navbar-item">
|
|
||||||
<button v-tooltip.bottom="'Toggle Add Form'" class="button is-dark has-tooltip-bottom"
|
|
||||||
@click="config.showForm = !config.showForm">
|
@click="config.showForm = !config.showForm">
|
||||||
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
||||||
<span class="is-hidden-mobile">Add</span>
|
<span>Add</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item" v-if="config.tasks.length > 0">
|
<div class="navbar-item" v-if="config.tasks.length > 0">
|
||||||
<button v-tooltip.bottom="'Toggle Tasks'" class="button is-dark has-tooltip-bottom"
|
<button v-tooltip.bottom="'Toggle Tasks'" class="button is-dark has-tooltip-bottom"
|
||||||
@click="config.showTasks = !config.showTasks">
|
@click="config.showTasks = !config.showTasks">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa-solid fa-tasks" />
|
<i class="fa-solid fa-tasks" />
|
||||||
</span>
|
</span>
|
||||||
<span class="is-hidden-mobile">Tasks</span>
|
<span class="is-hidden-mobile">Tasks</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<button class="button is-dark" @click="selectedTheme = 'light'" v-if="'dark' === selectedTheme"
|
<button class="button is-dark" @click="selectedTheme = 'light'" v-if="'dark' === selectedTheme"
|
||||||
v-tooltip="'Switch to light theme'">
|
v-tooltip="'Switch to light theme'">
|
||||||
<span class="icon has-text-warning"><i class="fas fa-sun"></i></span>
|
<span class="icon has-text-warning"><i class="fas fa-sun"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button is-dark" @click="selectedTheme = 'dark'" v-if="'light' === selectedTheme"
|
<button class="button is-dark" @click="selectedTheme = 'dark'" v-if="'light' === selectedTheme"
|
||||||
v-tooltip="'Switch to dark theme'">
|
v-tooltip="'Switch to dark theme'">
|
||||||
<span class="icon"><i class="fas fa-moon"></i></span>
|
<span class="icon"><i class="fas fa-moon"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<button class="button is-dark" @click="reloadPage">
|
<button class="button is-dark" @click="reloadPage">
|
||||||
<span class="icon"><i class="fas fa-refresh"></i></span>
|
<span class="icon"><i class="fas fa-refresh"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
|
||||||
<div class="columns is-multiline is-mobile mt-3">
|
<div class="columns mt-3 is-mobile">
|
||||||
<div class="column is-12 is-hidden-tablet has-text-centered">
|
<div class="column is-8-mobile">
|
||||||
<a href="#top" id="bottom" class="button">
|
<div class="has-text-left" v-if="config.app?.version">
|
||||||
<span class="icon"><i class="fas fa-arrow-up"></i> </span>
|
© {{ Year }} - <a href="https://github.com/ArabCoders/ytptube" target="_blank">YTPTube</a>
|
||||||
<span>Go to Top</span>
|
<span class="is-hidden-mobile"> ({{ config.app?.version || 'unknown' }})</span>
|
||||||
</a>
|
- <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>
|
||||||
|
<span class="is-hidden-mobile"> ({{ config?.app.ytdlp_version || 'unknown' }})</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6 is-9-mobile has-text-left"></div>
|
<div class="column is-4-mobile" v-if="config.app?.started">
|
||||||
<div class="column is-6 is-4-mobile has-text-right">
|
<div class="has-text-right">
|
||||||
v{{ VERSION }}
|
<span class="user-hint"
|
||||||
|
v-tooltip="'App Started: ' + moment.unix(config.app?.started).format('YYYY-M-DD H:mm Z')">
|
||||||
|
{{ moment.unix(config.app?.started).fromNow() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -95,15 +85,13 @@ import 'assets/css/bulma.css'
|
||||||
import 'assets/css/style.css'
|
import 'assets/css/style.css'
|
||||||
import 'assets/css/all.css'
|
import 'assets/css/all.css'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { useConfigStore } from '~/store/ConfigStore'
|
import moment from "moment";
|
||||||
|
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const Year = new Date().getFullYear()
|
||||||
const config = useConfigStore()
|
|
||||||
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')())
|
||||||
const showMenu = ref(false)
|
|
||||||
const VERSION = ref(runtimeConfig.public.version)
|
|
||||||
const expandContainer = ref(false)
|
|
||||||
const bus = useEventBus('show_form')
|
const bus = useEventBus('show_form')
|
||||||
|
const socket = useSocketStore()
|
||||||
|
const config = useConfigStore()
|
||||||
|
|
||||||
const applyPreferredColorScheme = scheme => {
|
const applyPreferredColorScheme = scheme => {
|
||||||
for (let s = 0; s < document.styleSheets.length; s++) {
|
for (let s = 0; s < document.styleSheets.length; s++) {
|
||||||
|
|
@ -167,3 +155,11 @@ const toggleForm = () => {
|
||||||
dEvent('show_form', { foo: 'bar' })
|
dEvent('show_form', { foo: 'bar' })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.user-hint {
|
||||||
|
user-select: none;
|
||||||
|
cursor: help;
|
||||||
|
border-bottom: 1px dotted;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export default defineNuxtConfig({
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
domain: '/',
|
domain: '/',
|
||||||
wss: ':8081',
|
wss: ':8081/',
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
157
ui/pages/console.vue
Normal file
157
ui/pages/console.vue
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<template>
|
||||||
|
<div class="mt-1 columns is-multiline">
|
||||||
|
<div class="column is-12 is-clearfix">
|
||||||
|
<h1 class="title is-4">Terminal</h1>
|
||||||
|
<div class="subtitle is-6">
|
||||||
|
You can use this terminal window to execute non-interactive commands. The interface is jailed to the
|
||||||
|
<code>yt-dlp</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-12">
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">
|
||||||
|
<span class="icon"><i class="fa-solid fa-terminal" /></span> Console Output
|
||||||
|
</p>
|
||||||
|
<p class="card-header-icon">
|
||||||
|
<span v-tooltip.top="'Clear console window'" class="icon" @click="clearOutput">
|
||||||
|
<i class="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">
|
||||||
|
<span class="icon-text input is-unselectable">
|
||||||
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
|
<span>yt-dlp</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<input type="text" class="input" v-model="command" placeholder="--help" autocomplete="off"
|
||||||
|
ref="command_input" @keydown.enter="runCommand" :disabled="isLoading" id="command">
|
||||||
|
</div>
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-primary" type="button" :disabled="isLoading || '' === command"
|
||||||
|
@click="runCommand">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa-solid fa-spinner" spin v-if="isLoading" />
|
||||||
|
<i class="fa-solid fa-paper-plane" v-else />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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 isLoading = ref(false)
|
||||||
|
|
||||||
|
const socket = useSocketStore()
|
||||||
|
|
||||||
|
watch(() => isLoading.value, async value => {
|
||||||
|
if (value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command.value = ''
|
||||||
|
await nextTick();
|
||||||
|
focusInput()
|
||||||
|
}, { immediate: 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
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('cli_post', command.value)
|
||||||
|
isLoading.value = true
|
||||||
|
terminal.value.writeln(`user@YTPTube ~`)
|
||||||
|
terminal.value.writeln(`$ yt-dlp ${command.value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearOutput = async (withCommand = false) => {
|
||||||
|
if (terminal.value) {
|
||||||
|
terminal.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === withCommand) {
|
||||||
|
command.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
focusInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
if (!command_input.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command_input.value.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => window.removeEventListener('resize', reSizeTerminal));
|
||||||
|
|
||||||
|
const writer = s => {
|
||||||
|
if (!terminal.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
terminal.value.writeln(s.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = () => isLoading.value = false
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.addEventListener('resize', reSizeTerminal);
|
||||||
|
focusInput()
|
||||||
|
socket.off('cli_close', loader)
|
||||||
|
socket.off('cli_output', writer)
|
||||||
|
socket.on('cli_close', loader)
|
||||||
|
socket.on('cli_output', writer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -7,18 +7,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { io } from "socket.io-client"
|
|
||||||
import { useConfigStore } from "~/store/ConfigStore"
|
|
||||||
import { useStateStore } from "~/store/StateStore"
|
|
||||||
|
|
||||||
const bus = useEventBus('item_added', 'show_form', 'task_edit')
|
const bus = useEventBus('item_added', 'show_form', 'task_edit')
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
|
const socket = useSocketStore()
|
||||||
const runtimeConfig = useRuntimeConfig()
|
|
||||||
const socket = ref()
|
|
||||||
const cli_output = ref([])
|
|
||||||
const cli_isLoading = ref(false)
|
|
||||||
const stateStore = useStateStore()
|
|
||||||
|
|
||||||
bus.on((event, data) => {
|
bus.on((event, data) => {
|
||||||
console.log({ e: event, d: data })
|
console.log({ e: event, d: data })
|
||||||
|
|
@ -34,86 +25,4 @@ bus.on((event, data) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
useHead({ title: 'Index' })
|
useHead({ title: 'Index' })
|
||||||
onMounted(() => {
|
|
||||||
socket.value = io(runtimeConfig.public.wss, {
|
|
||||||
path: document.location.pathname + 'socket.io',
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.value.on('connect', () => config.isConnected = true);
|
|
||||||
socket.value.on('disconnect', () => config.isConnected = false);
|
|
||||||
|
|
||||||
socket.value.on('initial_data', stream => {
|
|
||||||
const initialData = JSON.parse(stream)
|
|
||||||
|
|
||||||
config.setAll({
|
|
||||||
app: initialData['config'],
|
|
||||||
tasks: initialData['tasks'],
|
|
||||||
directories: initialData['directories'],
|
|
||||||
})
|
|
||||||
stateStore.addAll('queue', initialData['queue'] ?? {})
|
|
||||||
stateStore.addAll('history', initialData['done'] ?? {})
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.value.on('added', stream => {
|
|
||||||
const item = JSON.parse(stream);
|
|
||||||
stateStore.add('queue', item);
|
|
||||||
toast.success(`Item queued successfully: ${stateStore.get('queue', item._id)?.title}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on('error', stream => {
|
|
||||||
const [item, error] = JSON.parse(stream);
|
|
||||||
toast.error(`${item?.id}: Error: ${error}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on('completed', stream => {
|
|
||||||
const item = JSON.parse(stream);
|
|
||||||
if (true === stateStore.has('queue', item._id)) {
|
|
||||||
stateStore.move('queue', 'history', item._id);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stateStore.add('history', item);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on('canceled', stream => {
|
|
||||||
const id = JSON.parse(stream);
|
|
||||||
|
|
||||||
if (true !== stateStore.has('queue', id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.info('Download canceled: ' + stateStore.get('queue', id)?.title);
|
|
||||||
|
|
||||||
if (true === stateStore.has('queue', id)) {
|
|
||||||
stateStore.remove('queue', id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on('cleared', stream => {
|
|
||||||
const id = JSON.parse(stream);
|
|
||||||
if (true !== stateStore.has('history', id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stateStore.remove('history', id);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on("updated", stream => {
|
|
||||||
const data = JSON.parse(stream);
|
|
||||||
let dl = stateStore.get('queue', data._id, {});
|
|
||||||
data.deleting = dl?.deleting;
|
|
||||||
stateStore.update('queue', data._id, data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on("update", stream => {
|
|
||||||
const data = JSON.parse(stream);
|
|
||||||
if (true === stateStore.has('history', data._id)) {
|
|
||||||
stateStore.update('history', data._id, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on('cli_close', () => cli_isLoading.value = false);
|
|
||||||
socket.value.on('cli_output', s => cli_output.value.push(s));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
const CONFIG_KEYS = {
|
const CONFIG_KEYS = {
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
showForm: false,
|
showForm: false,
|
||||||
|
|
@ -12,6 +10,12 @@ const CONFIG_KEYS = {
|
||||||
keep_archive: false,
|
keep_archive: false,
|
||||||
output_template: '',
|
output_template: '',
|
||||||
},
|
},
|
||||||
|
presets: [
|
||||||
|
{
|
||||||
|
name: 'Default - Use Predefined yt-dlp Format',
|
||||||
|
format: 'default',
|
||||||
|
}
|
||||||
|
],
|
||||||
directories: [],
|
directories: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
97
ui/stores/SocketStore.js
Normal file
97
ui/stores/SocketStore.js
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { io } from "socket.io-client";
|
||||||
|
|
||||||
|
export const useSocketStore = defineStore('socket', () => {
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const config = useConfigStore()
|
||||||
|
const stateStore = useStateStore()
|
||||||
|
|
||||||
|
const socket = ref(null);
|
||||||
|
const isConnected = ref(false);
|
||||||
|
|
||||||
|
const connect = () => {
|
||||||
|
socket.value = io(runtimeConfig.public.wss)
|
||||||
|
|
||||||
|
socket.value.on('connect', () => isConnected.value = true);
|
||||||
|
socket.value.on('disconnect', () => isConnected.value = false);
|
||||||
|
|
||||||
|
socket.value.on('initial_data', stream => {
|
||||||
|
const initialData = JSON.parse(stream)
|
||||||
|
|
||||||
|
config.setAll({
|
||||||
|
app: initialData['config'],
|
||||||
|
tasks: initialData['tasks'],
|
||||||
|
directories: initialData['directories'],
|
||||||
|
})
|
||||||
|
|
||||||
|
stateStore.addAll('queue', initialData['queue'] ?? {})
|
||||||
|
stateStore.addAll('history', initialData['done'] ?? {})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('added', stream => {
|
||||||
|
const item = JSON.parse(stream);
|
||||||
|
stateStore.add('queue', item);
|
||||||
|
toast.success(`Item queued successfully: ${stateStore.get('queue', item._id)?.title}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('error', stream => {
|
||||||
|
const [item, error] = JSON.parse(stream);
|
||||||
|
toast.error(`${item?.id}: Error: ${error}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('completed', stream => {
|
||||||
|
const item = JSON.parse(stream);
|
||||||
|
if (true === stateStore.has('queue', item._id)) {
|
||||||
|
stateStore.move('queue', 'history', item._id);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stateStore.add('history', item);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('canceled', stream => {
|
||||||
|
const id = JSON.parse(stream);
|
||||||
|
|
||||||
|
if (true !== stateStore.has('queue', id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.info('Download canceled: ' + stateStore.get('queue', id)?.title);
|
||||||
|
|
||||||
|
if (true === stateStore.has('queue', id)) {
|
||||||
|
stateStore.remove('queue', id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('cleared', stream => {
|
||||||
|
const id = JSON.parse(stream);
|
||||||
|
if (true !== stateStore.has('history', id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stateStore.remove('history', id);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on("updated", stream => {
|
||||||
|
const data = JSON.parse(stream);
|
||||||
|
let dl = stateStore.get('queue', data._id, {});
|
||||||
|
data.deleting = dl?.deleting;
|
||||||
|
stateStore.update('queue', data._id, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on("update", stream => {
|
||||||
|
const data = JSON.parse(stream);
|
||||||
|
if (true === stateStore.has('history', data._id)) {
|
||||||
|
stateStore.update('history', data._id, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const on = (event, callback) => socket.value.on(event, callback);
|
||||||
|
const off = (event, callback) => socket.value.off(event, callback);
|
||||||
|
const emit = (event, data) => socket.value.emit(event, data);
|
||||||
|
|
||||||
|
if (false === isConnected.value) {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { connect, on, off, emit, socket, isConnected };
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
export const useStateStore = defineStore('state', () => {
|
export const useStateStore = defineStore('state', () => {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
queue: {},
|
queue: {},
|
||||||
Loading…
Reference in a new issue