more updates for new UI

This commit is contained in:
ArabCoders 2024-12-14 14:57:23 +03:00
parent 9fc047dba3
commit c61fd133d8
12 changed files with 351 additions and 197 deletions

2
.vscode/launch.json vendored
View file

@ -84,7 +84,7 @@
"--port",
"8082"
],
"runtimeExecutable": "npm",
"runtimeExecutable": "yarn",
"type": "node",
"cwd": "${workspaceFolder}/ui",
"env": {

View file

@ -224,8 +224,6 @@ import moment from "moment";
import { useStorage } from '@vueuse/core'
import LazyLoader from './LazyLoader'
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 config = useConfigStore()

View file

@ -6,7 +6,7 @@
<div class="column is-12">
<div class="control has-icons-left">
<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">
<i class="fa-solid fa-link" />
</span>
@ -19,9 +19,9 @@
</div>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select id="preset" class="is-fullwidth" :disabled="!config.isConnected" v-model="selectedPreset">
<option v-for="item in downloadFormats" :key="item.id" :value="item.id">
{{ item.text }}
<select id="preset" class="is-fullwidth" :disabled="!socket.isConnected" v-model="selectedPreset">
<option v-for="item in config.presets" :key="item.name" :value="item.format">
{{ item.name }}
</option>
</select>
</div>
@ -35,27 +35,23 @@
</div>
<div class="control is-expanded">
<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 class="column">
<button type="submit" class="button is-primary" @click="addDownload"
:class="{ 'is-loading': !config.isConnected || addInProgress }"
:disabled="!config.isConnected || addInProgress || !url">
<span class="icon">
<i class="fa-solid fa-plus" />
</span>
<span>Add Link</span>
:class="{ 'is-loading': !socket.isConnected || addInProgress }"
:disabled="!socket.isConnected || addInProgress || !url">
<span class="icon"><i class="fa-solid fa-plus" /></span>
<span>Add</span>
</button>
</div>
<div class="column">
<button type="submit" class="button is-info" @click="showAdvanced = !showAdvanced"
v-tooltip="'Show advanced options'" :class="{ 'is-loading': !config.isConnected }"
:disabled="!config.isConnected">
<span class="icon">
<i class="fa-solid fa-cog" />
</span>
v-tooltip="'Show advanced options'" :class="{ 'is-loading': !socket.isConnected }"
:disabled="!socket.isConnected">
<span class="icon"><i class="fa-solid fa-cog" /></span>
</button>
</div>
</div>
@ -84,7 +80,7 @@
</label>
<div class="control">
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig"
:disabled="!config.isConnected"></textarea>
:disabled="!socket.isConnected"></textarea>
</div>
<span class="subtitle is-6 has-text-info">
Some config fields are ignored like cookiefile, path, and output_format etc.
@ -101,7 +97,7 @@
</label>
<div class="control">
<textarea class="textarea" id="ytdlpCookies" v-model="ytdlpCookies"
:disabled="!config.isConnected"></textarea>
:disabled="!socket.isConnected"></textarea>
</div>
<span class="subtitle is-6 has-text-info">
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="field">
<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.'">
<span class="icon">
<i class="fa-solid fa-trash" />
@ -132,15 +128,16 @@
</template>
<script setup>
import { defineEmits, defineProps, onMounted, ref } from 'vue'
import { useStorage, useEventBus } from '@vueuse/core'
import { useConfigStore } from '~/store/ConfigStore';
const config = useConfigStore();
const socket = useSocketStore();
const bus = useEventBus('item_added', 'task_edit');
const emits = defineEmits(['addItem']);
const selectedFormat = useStorage('selectedFormat', 'any')
const selectedPreset = useStorage('selectedPreset', 'default')
const selectedQuality = useStorage('selectedQuality', '')
const ytdlpConfig = useStorage('ytdlp_config', '')
const ytdlpCookies = useStorage('ytdlp_cookies', '')

View file

@ -102,7 +102,7 @@
</div>
<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 has-text-success">
<i class="fa-solid fa-circle-check" />
@ -127,13 +127,12 @@ import { defineProps, defineEmits, ref, watch, computed } from 'vue';
import moment from "moment";
import { useStorage } from '@vueuse/core'
import LazyLoader from './LazyLoader'
import { useConfigStore } from '~/store/ConfigStore';
import { useStateStore } from '~/store/StateStore';
const emit = defineEmits(['deleteItem']);
const config = useConfigStore();
const stateStore = useStateStore();
const socket = useSocketStore();
const selectedElms = ref([]);
const masterSelectAll = ref(false);

View file

@ -49,6 +49,5 @@ defineProps({
required: true
}
})
const showStacks = ref(false)
</script>

View file

@ -1,87 +1,77 @@
<template>
<div id="main_container" class="container" :class="{ 'is-max-fullwidth': expandContainer }">
<nav class="navbar is-dark mb-5">
<div id="main_container" class="container">
<nav class="navbar is-mobile is-dark">
<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"><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>
</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 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-end pr-3">
<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"
<div class="navbar-item">
<button v-tooltip.bottom="'Toggle Add Form'" class="button is-dark has-tooltip-bottom"
@click="config.showForm = !config.showForm">
<span class="icon"><i class="fa-solid fa-plus" /></span>
<span class="is-hidden-mobile">Add</span>
</button>
</div>
<span class="icon"><i class="fa-solid fa-plus" /></span>
<span>Add</span>
</button>
</div>
<div class="navbar-item" v-if="config.tasks.length > 0">
<button v-tooltip.bottom="'Toggle Tasks'" class="button is-dark has-tooltip-bottom"
@click="config.showTasks = !config.showTasks">
<span class="icon">
<i class="fa-solid fa-tasks" />
</span>
<span class="is-hidden-mobile">Tasks</span>
</button>
</div>
<div class="navbar-item" v-if="config.tasks.length > 0">
<button v-tooltip.bottom="'Toggle Tasks'" class="button is-dark has-tooltip-bottom"
@click="config.showTasks = !config.showTasks">
<span class="icon">
<i class="fa-solid fa-tasks" />
</span>
<span class="is-hidden-mobile">Tasks</span>
</button>
</div>
<div class="navbar-item">
<button class="button is-dark" @click="selectedTheme = 'light'" v-if="'dark' === selectedTheme"
v-tooltip="'Switch to light theme'">
<span class="icon has-text-warning"><i class="fas fa-sun"></i></span>
</button>
<button class="button is-dark" @click="selectedTheme = 'dark'" v-if="'light' === selectedTheme"
v-tooltip="'Switch to dark theme'">
<span class="icon"><i class="fas fa-moon"></i></span>
</button>
</div>
<div class="navbar-item">
<button class="button is-dark" @click="reloadPage">
<span class="icon"><i class="fas fa-refresh"></i></span>
</button>
</div>
<div class="navbar-item">
<button class="button is-dark" @click="selectedTheme = 'light'" v-if="'dark' === selectedTheme"
v-tooltip="'Switch to light theme'">
<span class="icon has-text-warning"><i class="fas fa-sun"></i></span>
</button>
<button class="button is-dark" @click="selectedTheme = 'dark'" v-if="'light' === selectedTheme"
v-tooltip="'Switch to dark theme'">
<span class="icon"><i class="fas fa-moon"></i></span>
</button>
</div>
<div class="navbar-item">
<button class="button is-dark" @click="reloadPage">
<span class="icon"><i class="fas fa-refresh"></i></span>
</button>
</div>
</div>
</nav>
<NuxtPage />
<div class="columns is-multiline is-mobile mt-3">
<div class="column is-12 is-hidden-tablet has-text-centered">
<a href="#top" id="bottom" class="button">
<span class="icon"><i class="fas fa-arrow-up"></i>&nbsp;</span>
<span>Go to Top</span>
</a>
<div class="columns mt-3 is-mobile">
<div class="column is-8-mobile">
<div class="has-text-left" v-if="config.app?.version">
© {{ Year }} - <a href="https://github.com/ArabCoders/ytptube" target="_blank">YTPTube</a>
<span class="is-hidden-mobile">&nbsp;({{ config.app?.version || 'unknown' }})</span>
- <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>
<span class="is-hidden-mobile">&nbsp;({{ config?.app.ytdlp_version || 'unknown' }})</span>
</div>
</div>
<div class="column is-6 is-9-mobile has-text-left"></div>
<div class="column is-6 is-4-mobile has-text-right">
v{{ VERSION }}
<div class="column is-4-mobile" v-if="config.app?.started">
<div class="has-text-right">
<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>
@ -95,15 +85,13 @@ import 'assets/css/bulma.css'
import 'assets/css/style.css'
import 'assets/css/all.css'
import { useStorage } from '@vueuse/core'
import { useConfigStore } from '~/store/ConfigStore'
import moment from "moment";
const runtimeConfig = useRuntimeConfig()
const config = useConfigStore()
const Year = new Date().getFullYear()
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 socket = useSocketStore()
const config = useConfigStore()
const applyPreferredColorScheme = scheme => {
for (let s = 0; s < document.styleSheets.length; s++) {
@ -167,3 +155,11 @@ const toggleForm = () => {
dEvent('show_form', { foo: 'bar' })
}
</script>
<style>
.user-hint {
user-select: none;
cursor: help;
border-bottom: 1px dotted;
}
</style>

View file

@ -12,7 +12,7 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
domain: '/',
wss: ':8081',
wss: ':8081/',
version: '2.0.0',
}
},

157
ui/pages/console.vue Normal file
View 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>

View file

@ -7,18 +7,9 @@
</template>
<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 config = useConfigStore()
const runtimeConfig = useRuntimeConfig()
const socket = ref()
const cli_output = ref([])
const cli_isLoading = ref(false)
const stateStore = useStateStore()
const socket = useSocketStore()
bus.on((event, data) => {
console.log({ e: event, d: data })
@ -34,86 +25,4 @@ bus.on((event, data) => {
})
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>

View file

@ -1,5 +1,3 @@
import { defineStore } from 'pinia';
const CONFIG_KEYS = {
isConnected: false,
showForm: false,
@ -12,6 +10,12 @@ const CONFIG_KEYS = {
keep_archive: false,
output_template: '',
},
presets: [
{
name: 'Default - Use Predefined yt-dlp Format',
format: 'default',
}
],
directories: [],
};

97
ui/stores/SocketStore.js Normal file
View 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 };
});

View file

@ -1,5 +1,3 @@
import { defineStore } from 'pinia';
export const useStateStore = defineStore('state', () => {
const state = reactive({
queue: {},