sweeping changes to internals

This commit is contained in:
ArabCoders 2024-12-14 19:27:13 +03:00
parent cf8f971785
commit 0239e509de
19 changed files with 183 additions and 214 deletions

27
.vscode/launch.json vendored
View file

@ -5,18 +5,18 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Node: App.Vue", "name": "Node: Nuxt3",
"request": "launch", "request": "launch",
"runtimeArgs": [ "runtimeArgs": [
"run", "run",
"serve", "dev",
"--", "--",
"--port", "--port",
"3000" "8082"
], ],
"runtimeExecutable": "npm", "runtimeExecutable": "yarn",
"type": "node", "type": "node",
"cwd": "${workspaceFolder}/frontend", "cwd": "${workspaceFolder}/ui",
"env": { "env": {
"VUE_APP_BASE_URL": "http://localhost:8081", "VUE_APP_BASE_URL": "http://localhost:8081",
} }
@ -73,23 +73,6 @@
} }
], ],
"justMyCode": true "justMyCode": true
},
{
"name": "Node: NuxtJS",
"request": "launch",
"runtimeArgs": [
"run",
"dev",
"--",
"--port",
"8082"
],
"runtimeExecutable": "yarn",
"type": "node",
"cwd": "${workspaceFolder}/ui",
"env": {
"VUE_APP_BASE_URL": "http://localhost:8081",
}
} }
] ]
} }

View file

@ -201,7 +201,7 @@ class Download:
self.proc.start() self.proc.start()
self.info.status = 'preparing' self.info.status = 'preparing'
asyncio.create_task(self.notifier.updated(self.info), name=f"notifier-{self.id}") asyncio.create_task(self.notifier.updated(dl=self.info), name=f"notifier-{self.id}")
asyncio.create_task(self.progress_update(), name=f"update-{self.id}") asyncio.create_task(self.progress_update(), name=f"update-{self.id}")
return await asyncio.get_running_loop().run_in_executor(None, self.proc.join) return await asyncio.get_running_loop().run_in_executor(None, self.proc.join)
@ -329,7 +329,7 @@ class Download:
LOG.debug(f'Status Update: {self.info._id=} {status=}') LOG.debug(f'Status Update: {self.info._id=} {status=}')
if isinstance(status, str): if isinstance(status, str):
asyncio.create_task(self.notifier.updated(self.info), name=f"notifier-u-{self.id}") asyncio.create_task(self.notifier.updated(dl=self.info), name=f"notifier-u-{self.id}")
continue continue
self.tmpfilename = status.get('tmpfilename') self.tmpfilename = status.get('tmpfilename')
@ -349,7 +349,9 @@ class Download:
if self.info.status == 'error' and 'error' in status: if self.info.status == 'error' and 'error' in status:
self.info.error = status.get('error') self.info.error = status.get('error')
asyncio.create_task(self.notifier.error(self.info, self.info.error), name=f"notifier-e-{self.id}") asyncio.create_task(
self.notifier.error(dl=self.info, message=self.info.error),
name=f"notifier-e-{self.id}")
if 'downloaded_bytes' in status: if 'downloaded_bytes' in status:
total = status.get('total_bytes') or status.get('total_bytes_estimate') total = status.get('total_bytes') or status.get('total_bytes_estimate')
@ -368,4 +370,4 @@ class Download:
self.info.file_size = None self.info.file_size = None
pass pass
asyncio.create_task(self.notifier.updated(self.info), name=f"notifier-u-{self.id}") asyncio.create_task(self.notifier.updated(dl=self.info), name=f"notifier-u-{self.id}")

View file

@ -12,7 +12,6 @@ from ItemDTO import ItemDTO
from DataStore import DataStore from DataStore import DataStore
from Utils import Notifier, ObjectSerializer, calcDownloadPath, ExtractInfo, isDownloaded, mergeConfig from Utils import Notifier, ObjectSerializer, calcDownloadPath, ExtractInfo, isDownloaded, mergeConfig
from AsyncPool import AsyncPool from AsyncPool import AsyncPool
from concurrent.futures import ThreadPoolExecutor
LOG = logging.getLogger('DownloadQueue') LOG = logging.getLogger('DownloadQueue')
TYPE_DONE: str = 'done' TYPE_DONE: str = 'done'
@ -295,9 +294,9 @@ class DownloadQueue:
await item.close() await item.close()
LOG.debug(f'Deleting from queue {itemMessage}') LOG.debug(f'Deleting from queue {itemMessage}')
self.queue.delete(id) self.queue.delete(id)
asyncio.create_task(self.notifier.canceled(id), name=f'notifier-c-{id}') asyncio.create_task(self.notifier.canceled(id=id, dl=item), name=f'notifier-c-{id}')
self.done.put(item) self.done.put(item)
asyncio.create_task(self.notifier.completed(item), name=f'notifier-d-{id}') asyncio.create_task(self.notifier.completed(dl=item), name=f'notifier-d-{id}')
LOG.info(f'Deleted from queue {itemMessage}') LOG.info(f'Deleted from queue {itemMessage}')
status[id] = 'ok' status[id] = 'ok'
@ -319,7 +318,7 @@ class DownloadQueue:
itemMessage = f"{id=} {item.info.id=} {item.info.title=}" itemMessage = f"{id=} {item.info.id=} {item.info.title=}"
LOG.debug(f'Deleting completed download {itemMessage}') LOG.debug(f'Deleting completed download {itemMessage}')
self.done.delete(id) self.done.delete(id)
asyncio.create_task(self.notifier.cleared(id), name=f'notifier-c-{id}') asyncio.create_task(self.notifier.cleared(id, dl=item), name=f'notifier-c-{id}')
LOG.info(f'Deleted completed download {itemMessage}') LOG.info(f'Deleted completed download {itemMessage}')
status[id] = 'ok' status[id] = 'ok'
@ -418,12 +417,12 @@ class DownloadQueue:
self.queue.delete(key=id) self.queue.delete(key=id)
if entry.is_canceled() is True: if entry.is_canceled() is True:
asyncio.create_task(self.notifier.canceled(id), name=f'notifier-c-{id}') asyncio.create_task(self.notifier.canceled(id, dl=entry.info), name=f'notifier-c-{id}')
entry.info.status = 'canceled' entry.info.status = 'canceled'
entry.info.error = 'Canceled by user.' entry.info.error = 'Canceled by user.'
self.done.put(value=entry) self.done.put(value=entry)
asyncio.create_task(self.notifier.completed(entry.info), name=f'notifier-d-{id}') asyncio.create_task(self.notifier.completed(dl=entry.info), name=f'notifier-d-{id}')
self.event.set() self.event.set()

View file

@ -15,6 +15,7 @@ class ItemDTO:
url: str url: str
quality: str quality: str
format: str format: str
preset: str = "default"
folder: str folder: str
download_dir: str = None download_dir: str = None
temp_dir: str = None temp_dir: str = None

View file

@ -333,10 +333,10 @@ class Notifier:
async def completed(self, dl: dict): async def completed(self, dl: dict):
await self.emit('completed', dl) await self.emit('completed', dl)
async def canceled(self, id: str): async def canceled(self, id: str, dl: dict = None):
await self.emit('canceled', id) await self.emit('canceled', id)
async def cleared(self, id: str): async def cleared(self, id: str, dl: dict = None):
await self.emit('cleared', id) await self.emit('cleared', id)
async def error(self, dl: dict, message: str): async def error(self, dl: dict, message: str):

View file

@ -717,19 +717,28 @@ class Main:
await self.sio.emit('status', self.serializer.encode(status)) await self.sio.emit('status', self.serializer.encode(status))
@self.sio.event @self.sio.event
async def cancel_items(sid, post: dict): async def item_cancel(sid, id: str):
ids = post.get('ids') if not id:
identifier: str = post.get('identifier')
if not ids:
await self.notifier.warning('Invalid request.') await self.notifier.warning('Invalid request.')
return return
status: dict[str, str] = {} status: dict[str, str] = {}
status = await self.queue.cancel(ids) status = await self.queue.cancel([id])
status.update({'identifier': identifier}) status.update({'identifier': id})
await self.sio.emit('cancel_items', self.serializer.encode(status)) await self.sio.emit('item_cancel', self.serializer.encode(status))
@self.sio.event
async def item_delete(sid, id: str):
if not id:
await self.notifier.warning('Invalid request.')
return
status: dict[str, str] = {}
status = await self.queue.clear([id])
status.update({'identifier': id})
await self.sio.emit('item_delete', self.serializer.encode(status))
@self.sio.event() @self.sio.event()
async def archive_item(sid, data): async def archive_item(sid, data):

View file

@ -46,6 +46,12 @@ hr {
white-space: nowrap; white-space: nowrap;
} }
.user-hint {
user-select: none;
cursor: help;
border-bottom: 1px dotted;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
* { * {
unicode-bidi: plaintext; unicode-bidi: plaintext;

View file

@ -4,7 +4,7 @@
<span class="icon"> <span class="icon">
<i :class="showCompleted ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" /> <i :class="showCompleted ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
</span> </span>
<span>History <span v-if="hasItems">({{ getTotal }})</span></span> <span>History <span v-if="hasItems">({{ stateStore.count('history') }})</span></span>
</span> </span>
</h1> </h1>
@ -24,7 +24,7 @@
</div> </div>
<div class="column is-half-mobile"> <div class="column is-half-mobile">
<button type="button" class="button is-fullwidth is-danger" :disabled="!hasSelected" <button type="button" class="button is-fullwidth is-danger" :disabled="!hasSelected"
@click="$emit('deleteItem', 'completed', selectedElms); selectedElms = []"> @click="deleteSelectedItems">
<span class="icon-text is-block"> <span class="icon-text is-block">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-trash-can" /> <i class="fa-solid fa-trash-can" />
@ -75,7 +75,7 @@
</div> </div>
<div class="columns is-multiline"> <div class="columns is-multiline">
<LazyLoader :unrender="true" :min-height="210" class="column is-6" v-for="item in sortCompleted" :key="item._id"> <LateLoader :unrender="true" :min-height="210" class="column is-6" v-for="item in sortCompleted" :key="item._id">
<div class="card" <div class="card"
:class="{ 'is-bordered-danger': item.status === 'error', 'is-bordered-info': item.live_in || item.is_live }"> :class="{ 'is-bordered-danger': item.status === 'error', 'is-bordered-info': item.live_in || item.is_live }">
<header class="card-header has-tooltip"> <header class="card-header has-tooltip">
@ -127,7 +127,8 @@
</span> </span>
</div> </div>
<div class="column is-half-mobile has-text-centered"> <div class="column is-half-mobile has-text-centered">
<span :date-datetime="item.datetime" v-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')"> <span class="user-hint" :date-datetime="item.datetime"
v-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')">
{{ moment(item.datetime).fromNow() }} {{ moment(item.datetime).fromNow() }}
</span> </span>
</div> </div>
@ -150,33 +151,26 @@
</div> </div>
<div class="columns is-mobile is-multiline"> <div class="columns is-mobile is-multiline">
<div class="column is-half-mobile" v-if="item.status != 'finished'"> <div class="column is-half-mobile" v-if="item.status != 'finished'">
<a class="button is-warning is-fullwidth" v-tooltip="'Re-queue incomplete download.'" <a class="button is-warning is-fullwidth" v-tooltip="'Re-queue item.'" @click="reQueueItem(item)">
@click="reQueueItem(item)">
<span class="icon-text is-block"> <span class="icon-text is-block">
<span class="icon"> <span class="icon"><i class="fa-solid fa-rotate-right" /></span>
<i class="fa-solid fa-rotate-right" />
</span>
<span>Re-queue</span> <span>Re-queue</span>
</span> </span>
</a> </a>
</div> </div>
<div class="column is-half-mobile"> <div class="column is-half-mobile">
<a class="button is-danger is-fullwidth" @click="$emit('deleteItem', 'completed', item._id)"> <a class="button is-danger is-fullwidth" @click="socket.emit('item_delete', item._id)">
<span class="icon-text is-block"> <span class="icon-text is-block">
<span class="icon"> <span class="icon"><i class="fa-solid fa-trash-can" /></span>
<i class="fa-solid fa-trash-can" />
</span>
<span>Remove</span> <span>Remove</span>
</span> </span>
</a> </a>
</div> </div>
<div class="column is-half-mobile" v-if="config.app?.keep_archive && item.status != 'finished'"> <div class="column is-half-mobile" v-if="config.app?.keep_archive && item.status != 'finished'">
<a class="button is-danger is-light is-fullwidth" v-tooltip="'Add link to archive.'" <a class="button is-danger is-light is-fullwidth" v-tooltip="'Add link to archive.'"
@click="$emit('archiveItem', 'completed', item)"> @click="archiveItem(item)">
<span class="icon-text is-block"> <span class="icon-text is-block">
<span class="icon"> <span class="icon"><i class="fa-solid fa-box-archive" /></span>
<i class="fa-solid fa-box-archive" />
</span>
<span>Archive</span> <span>Archive</span>
</span> </span>
</a> </a>
@ -194,11 +188,11 @@
</div> </div>
</div> </div>
</div> </div>
</LazyLoader> </LateLoader>
</div> </div>
<div class="content has-text-centered" v-if="!hasItems"> <div class="content has-text-centered" v-if="!hasItems">
<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" />
@ -219,15 +213,14 @@
</template> </template>
<script setup> <script setup>
import { defineProps, computed, ref, watch, defineEmits } 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 { makeDownload, formatBytes, ucFirst } from '~/utils/index' import { makeDownload, formatBytes, ucFirst } from '~/utils/index'
import toast from '~/plugins/toast'
const emits = defineEmits(['deleteItem', 'addItem', 'playItem', 'archiveItem'])
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);
@ -257,7 +250,6 @@ const sortCompleted = computed(() => {
const hasSelected = computed(() => selectedElms.value.length > 0) const hasSelected = computed(() => selectedElms.value.length > 0)
const hasItems = computed(() => stateStore.count('history') > 0) const hasItems = computed(() => stateStore.count('history') > 0)
const getTotal = computed(() => stateStore.count('history'));
const showMessage = (item) => { const showMessage = (item) => {
if (!item?.msg || item.msg === item?.error) { if (!item?.msg || item.msg === item?.error) {
@ -295,18 +287,34 @@ const hasCompleted = computed(() => {
return false; return false;
}) })
const deleteSelectedItems = () => {
if (selectedElms.value.length < 1) {
toast.error('No items selected.');
return;
}
if (false === confirm('Are you sure you want to delete selected items?')) {
return;
}
for (const key in selectedElms.value) {
const item = stateStore.history[selectedElms.value[key]];
if (item.status === 'finished') {
archiveItem(item);
}
socket.emit('item_delete', item._id);
}
}
const clearCompleted = () => { const clearCompleted = () => {
const state = confirm('Are you sure you want to clear all completed downloads?'); const state = confirm('Are you sure you want to clear all completed downloads?');
if (false === state) { if (false === state) {
return; return;
} }
const keys = {};
for (const key in stateStore.history) { for (const key in stateStore.history) {
if (stateStore.history[key].status === 'finished') { if (ag(stateStore.get('history', key, {}), 'status') === 'finished') {
keys[key] = stateStore.history[key]._id; socket.emit('item_delete', stateStore.history[key]._id);
stateStore.remove('history', key);
} }
} }
} }
@ -318,7 +326,7 @@ const clearIncomplete = () => {
for (const key in stateStore.history) { for (const key in stateStore.history) {
if (stateStore.history[key].status !== 'finished') { if (stateStore.history[key].status !== 'finished') {
stateStore.remove('history', key); socket.emit('item_delete', stateStore.history[key]._id);
} }
} }
} }
@ -349,25 +357,34 @@ const requeueIncomplete = () => {
} }
for (const key in stateStore.history) { for (const key in stateStore.history) {
const item = stateStore.history[key]; const item = stateStore.get('history', key, {});
if (item.status !== 'finished') { if ('finished' === item.status) {
emits('deleteItem', 'completed', key); continue;
emits('addItem', {
url: item.url,
format: item.format,
quality: item.quality,
folder: item.folder,
ytdlp_config: item.ytdlp_config,
ytdlp_cookies: item.ytdlp_cookies,
output_template: item.output_template,
});
} }
emits('deleteItem', 'completed', key);
emits('addItem', {
url: item.url,
format: item.preset,
folder: item.folder,
ytdlp_config: item.ytdlp_config,
ytdlp_cookies: item.ytdlp_cookies,
output_template: item.output_template,
});
} }
} }
const reQueueItem = (item) => { const archiveItem = item => {
emits('deleteItem', 'completed', item._id); if (!confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)) {
emits('addItem', { return
}
socket.emit('archive_item', item);
socket.emit('item_delete', item._id);
}
const reQueueItem = item => {
socket.emit('item_delete', item._id)
socket.emit('add_url', {
url: item.url, url: item.url,
format: item.format, format: item.format,
quality: item.quality, quality: item.quality,

View file

@ -128,13 +128,11 @@
</template> </template>
<script setup> <script setup>
import { useStorage, useEventBus } from '@vueuse/core' import { useStorage } from '@vueuse/core'
const config = useConfigStore(); const config = useConfigStore();
const socket = useSocketStore(); const socket = useSocketStore();
const toast = useToast();
const bus = useEventBus('item_added', 'task_edit');
const emits = defineEmits(['addItem']);
const selectedFormat = useStorage('selectedFormat', 'any') const selectedFormat = useStorage('selectedFormat', 'any')
const selectedPreset = useStorage('selectedPreset', 'default') const selectedPreset = useStorage('selectedPreset', 'default')
@ -152,8 +150,7 @@ const addDownload = () => {
addInProgress.value = true; addInProgress.value = true;
socket.emit('add_url', { socket.emit('add_url', {
url: url.value, url: url.value,
format: selectedFormat.value, preset: selectedPreset.value,
quality: selectedQuality.value,
folder: downloadPath.value, folder: downloadPath.value,
ytdlp_config: ytdlpConfig.value, ytdlp_config: ytdlpConfig.value,
ytdlp_cookies: ytdlpCookies.value, ytdlp_cookies: ytdlpCookies.value,
@ -165,8 +162,7 @@ const resetConfig = () => {
if (!confirm('Are you sure you want to reset the local configuration? this will NOT delete any downloads or server configuration.')) { if (!confirm('Are you sure you want to reset the local configuration? this will NOT delete any downloads or server configuration.')) {
return; return;
} }
selectedFormat.value = 'any'; selectedPreset.value = 'default';
selectedQuality.value = '';
ytdlpConfig.value = ''; ytdlpConfig.value = '';
ytdlpCookies.value = ''; ytdlpCookies.value = '';
output_template.value = null; output_template.value = null;
@ -174,34 +170,17 @@ const resetConfig = () => {
downloadPath.value = ''; downloadPath.value = '';
} }
bus.on((event, data) => {
const allowedEvents = ['item_added'];
if (!allowedEvents.includes(event)) {
return;
}
if ('item_added' === event) {
if (data?.status === 'ok') {
url.value = '';
}
addInProgress.value = false;
}
});
const statusHandler = async data => { const statusHandler = async data => {
const { status, msg } = JSON.parse(data) const { status, msg } = JSON.parse(data)
addInProgress.value = false addInProgress.value = false
console.log(data)
if ('error' === status) { if ('error' === status) {
notification('error', 'Add error', msg, 5000) toast.error(msg)
return return
} }
notification('success', 'Add success', msg, 3000) url.value = '';
await navigateTo('/')
} }
onMounted(() => socket.on('status', statusHandler)) onMounted(() => socket.on('status', statusHandler))

View file

@ -35,7 +35,7 @@
</div> </div>
<div class="columns is-multiline"> <div class="columns is-multiline">
<LazyLoader :unrender="true" :min-height="265" class="column is-6" v-for="item in stateStore.history" <LateLoader :unrender="true" :min-height="265" class="column is-6" v-for="item in stateStore.queue"
:key="item._id"> :key="item._id">
<div class="card"> <div class="card">
<header class="card-header has-tooltip" v-tooltip="item.title"> <header class="card-header has-tooltip" v-tooltip="item.title">
@ -98,7 +98,7 @@
</div> </div>
</div> </div>
</div> </div>
</LazyLoader> </LateLoader>
</div> </div>
<div class="content has-text-centered" v-if="!hasQueuedItems"> <div class="content has-text-centered" v-if="!hasQueuedItems">
@ -125,9 +125,9 @@
<script setup> <script setup>
import moment from 'moment' import moment from 'moment'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import LazyLoader from '~/components/LazyLoader'
import { ucFirst } from '~/utils/index' import { ucFirst } from '~/utils/index'
const toast = useToast();
const config = useConfigStore(); const config = useConfigStore();
const stateStore = useStateStore(); const stateStore = useStateStore();
const socket = useSocketStore(); const socket = useSocketStore();
@ -135,7 +135,6 @@ const socket = useSocketStore();
const selectedElms = ref([]); const selectedElms = ref([]);
const masterSelectAll = ref(false); const masterSelectAll = ref(false);
const showQueue = useStorage('showQueue', true) const showQueue = useStorage('showQueue', true)
const actionId = ref(null);
watch(masterSelectAll, (value) => { watch(masterSelectAll, (value) => {
for (const key in stateStore.queue) { for (const key in stateStore.queue) {
@ -149,8 +148,7 @@ watch(masterSelectAll, (value) => {
}) })
const hasSelected = computed(() => selectedElms.value.length > 0) const hasSelected = computed(() => selectedElms.value.length > 0)
const hasQueuedItems = computed(() => stateStore.count('history') > 0) const hasQueuedItems = computed(() => stateStore.count('queue') > 0)
const getTotal = computed(() => stateStore.count('history'));
const setIcon = item => { const setIcon = item => {
if (item.status === 'downloading' && item.is_live) { if (item.status === 'downloading' && item.is_live) {
@ -222,11 +220,11 @@ const confirmCancel = item => {
if (true !== confirm(`Are you sure you want to cancel (${item.title})?`)) { if (true !== confirm(`Are you sure you want to cancel (${item.title})?`)) {
return false; return false;
} }
deleteItem('queue', item._id); cancelItems(item._id);
return true; return true;
} }
const deleteItem = (type, item) => { const cancelItems = item => {
const items = [] const items = []
if (typeof item === 'object') { if (typeof item === 'object') {
@ -241,51 +239,6 @@ const deleteItem = (type, item) => {
return; return;
} }
actionId.value = makeId(); items.forEach(id => socket.emit('item_cancel', id));
socket.emit('cancel_items', {
identifier: actionId.value,
ids: items,
});
} }
const cancelHandler = async ev => {
if (!actionId.value) {
return;
}
const data = JSON.parse(ev)
if (data.identifier !== actionId.value) {
console.log('Invalid action identifier', data);
return;
}
if ('error' === data.status) {
notification('error', 'Failed to cancel item/s.');
return;
}
for (const key in items) {
const itemId = items[key];
if (itemId in json && json[itemId] === 'ok') {
if (true === stateStore.has('history', itemId)) {
stateStore.remove('history', itemId);
}
if (true === stateStore.has('queue', itemId)) {
notification('info', 'Download canceled: ' + ag(stateStore.get(itemId), 'title'));
stateStore.remove('queue', itemId);
}
}
}
}
onMounted(() => {
socket.on('cancel_items', cancelHandler)
})
onUnmounted(() => {
socket.off('cancel_items', cancelHandler)
})
</script> </script>

View file

@ -0,0 +1,2 @@
import { useToast } from "vue-toastification";
export default () => useToast()

View file

@ -21,12 +21,13 @@
</div> </div>
<div class="navbar-item"> <div class="navbar-item">
<NuxtLink v-tooltip.bottom="'Toggle Add Form'" class="button is-dark has-tooltip-bottom" to="/add"> <button v-tooltip.bottom="'Toggle Add Form'" class="button is-dark has-tooltip-bottom"
@click="config.showForm = !config.showForm">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fa-solid fa-plus" /></span> <span class="icon"><i class="fa-solid fa-plus" /></span>
<span>Add</span> <span>Add</span>
</span> </span>
</NuxtLink> </button>
</div> </div>
<div class="navbar-item" v-if="config.tasks.length > 0"> <div class="navbar-item" v-if="config.tasks.length > 0">
@ -56,6 +57,7 @@
</div> </div>
</nav> </nav>
<NewDownload v-if="config.showForm" />
<NuxtPage /> <NuxtPage />
<div class="columns mt-3 is-mobile"> <div class="columns mt-3 is-mobile">
@ -151,17 +153,4 @@ watch(selectedTheme, value => {
}) })
const reloadPage = () => window.location.reload() const reloadPage = () => window.location.reload()
const toggleForm = () => {
console.log('emitting show_form');
bus.emit('show_form', { foo: 'bar' })
dEvent('show_form', { foo: 'bar' })
}
</script> </script>
<style>
.user-hint {
user-select: none;
cursor: help;
border-bottom: 1px dotted;
}
</style>

View file

@ -8,7 +8,9 @@ export default defineNuxtConfig({
port: 8082, port: 8082,
host: "0.0.0.0", host: "0.0.0.0",
}, },
css: [
'vue-toastification/dist/index.css'
],
runtimeConfig: { runtimeConfig: {
public: { public: {
domain: '/', domain: '/',
@ -16,7 +18,9 @@ export default defineNuxtConfig({
version: '2.0.0', version: '2.0.0',
} }
}, },
build: {
transpile: ['vue-toastification'],
},
app: { app: {
buildAssetsDir: "assets", buildAssetsDir: "assets",
head: { head: {

View file

@ -23,9 +23,10 @@
"nuxt3-notifications": "^1.2.0", "nuxt3-notifications": "^1.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"plyr": "^3.7.8", "plyr": "^3.7.8",
"socket.io-client": "^4.7.2",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0", "vue-router": "^4.3.0",
"socket.io-client": "^4.7.2" "vue-toastification": "^2.0.0-rc.5"
}, },
"devDependencies": {} "devDependencies": {}
} }

View file

@ -7,4 +7,7 @@
<script setup> <script setup>
useHead({ title: 'Index' }) useHead({ title: 'Index' })
const config = useConfigStore()
</script> </script>

9
ui/plugins/toast.js Normal file
View file

@ -0,0 +1,9 @@
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
export default defineNuxtPlugin(nuxtApp => nuxtApp.vueApp.use(Toast, {
transition: "Vue-Toastification__bounce",
//position: "bottom-right",
maxToasts: 5,
newestOnTop: true
}))

View file

@ -1,10 +1,11 @@
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import { notification, ag } from "~/utils/index" import { ag } from "~/utils/index"
export const useSocketStore = defineStore('socket', () => { export const useSocketStore = defineStore('socket', () => {
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const config = useConfigStore() const config = useConfigStore()
const stateStore = useStateStore() const stateStore = useStateStore()
const toast = useToast()
const socket = ref(null); const socket = ref(null);
const isConnected = ref(false); const isConnected = ref(false);
@ -31,31 +32,34 @@ export const useSocketStore = defineStore('socket', () => {
socket.value.on('added', stream => { socket.value.on('added', stream => {
const item = JSON.parse(stream); const item = JSON.parse(stream);
stateStore.add('queue', item._id, item); stateStore.add('queue', item._id, item);
notification('success', `Item queued successfully: ${ag(stateStore.get('queue', item._id, {}), 'title')}`); toast.success(`Item queued successfully: ${ag(stateStore.get('queue', item._id, {}), 'title')}`);
}); });
socket.value.on('error', stream => { socket.value.on('error', stream => {
const [item, error] = JSON.parse(stream); const [item, error] = JSON.parse(stream);
notification('error', `${item?.id}: Error: ${error}`); toast.error(`${item?.id}: Error: ${error}`);
}); });
socket.value.on('completed', stream => { socket.value.on('completed', stream => {
const item = JSON.parse(stream); const item = JSON.parse(stream);
console.log(item)
if (true === stateStore.has('queue', item._id)) { if (true === stateStore.has('queue', item._id)) {
stateStore.move('queue', 'history', item._id); stateStore.remove('queue', item._id);
return
} }
stateStore.add('history', item);
stateStore.add('history', item._id, item);
}); });
socket.value.on('canceled', stream => { socket.value.on('canceled', stream => {
const id = JSON.parse(stream); const id = JSON.parse(stream);
if (true !== stateStore.has('queue', id)) { if (true !== stateStore.has('queue', id)) {
console.log(stream)
return return
} }
notification('info', `Download canceled: ${ag(stateStore.get('queue', id, {}), 'title')}`); toast.info(`Download canceled: ${ag(stateStore.get('queue', id, {}), 'title')}`);
if (true === stateStore.has('queue', id)) { if (true === stateStore.has('queue', id)) {
stateStore.remove('queue', id); stateStore.remove('queue', id);
@ -64,9 +68,12 @@ export const useSocketStore = defineStore('socket', () => {
socket.value.on('cleared', stream => { socket.value.on('cleared', stream => {
const id = JSON.parse(stream); const id = JSON.parse(stream);
if (true !== stateStore.has('history', id)) { if (true !== stateStore.has('history', id)) {
console.log(stream)
return return
} }
stateStore.remove('history', id); stateStore.remove('history', id);
}); });

View file

@ -1683,13 +1683,13 @@ braces@^3.0.3, braces@~3.0.2:
fill-range "^7.1.1" fill-range "^7.1.1"
browserslist@^4.0.0, browserslist@^4.23.3, browserslist@^4.24.0: browserslist@^4.0.0, browserslist@^4.23.3, browserslist@^4.24.0:
version "4.24.2" version "4.24.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2"
integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==
dependencies: dependencies:
caniuse-lite "^1.0.30001669" caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.41" electron-to-chromium "^1.5.73"
node-releases "^2.0.18" node-releases "^2.0.19"
update-browserslist-db "^1.1.1" update-browserslist-db "^1.1.1"
buffer-crc32@^1.0.0: buffer-crc32@^1.0.0:
@ -1750,7 +1750,7 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2" lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688:
version "1.0.30001688" version "1.0.30001688"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz#f9d3ede749f083ce0db4c13db9d828adaf2e8d0a" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz#f9d3ede749f083ce0db4c13db9d828adaf2e8d0a"
integrity sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA== integrity sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==
@ -2248,7 +2248,7 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.41: electron-to-chromium@^1.5.73:
version "1.5.73" version "1.5.73"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz#f32956ce40947fa3c8606726a96cd8fb5bb5f720" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz#f32956ce40947fa3c8606726a96cd8fb5bb5f720"
integrity sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg== integrity sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==
@ -2864,10 +2864,10 @@ is-binary-path@~2.1.0:
dependencies: dependencies:
binary-extensions "^2.0.0" binary-extensions "^2.0.0"
is-core-module@^2.13.0: is-core-module@^2.16.0:
version "2.15.1" version "2.16.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.0.tgz#6c01ffdd5e33c49c1d2abfa93334a85cb56bd81c"
integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== integrity sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==
dependencies: dependencies:
hasown "^2.0.2" hasown "^2.0.2"
@ -3468,7 +3468,7 @@ node-gyp-build@^4.2.2:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8"
integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
node-releases@^2.0.18: node-releases@^2.0.19:
version "2.0.19" version "2.0.19"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
@ -4182,11 +4182,11 @@ resolve-from@^5.0.0:
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve@^1.22.1: resolve@^1.22.1:
version "1.22.8" version "1.22.9"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.9.tgz#6da76e4cdc57181fa4471231400e8851d0a924f3"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== integrity sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==
dependencies: dependencies:
is-core-module "^2.13.0" is-core-module "^2.16.0"
path-parse "^1.0.7" path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0" supports-preserve-symlinks-flag "^1.0.0"
@ -4640,9 +4640,9 @@ terser@^5.17.4:
source-map-support "~0.5.20" source-map-support "~0.5.20"
text-decoder@^1.1.0: text-decoder@^1.1.0:
version "1.2.2" version "1.2.3"
resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.2.tgz#63dd2401c43895cecb292e2407db184b50ad60ac" resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65"
integrity sha512-/MDslo7ZyWTA2vnk1j7XoDVfXsGk3tp+zFEJHJGm0UjIlQifonVFwlVbQDFh8KJzTBnT8ie115TYqir6bclddA== integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==
dependencies: dependencies:
b4a "^1.6.4" b4a "^1.6.4"
@ -5054,6 +5054,11 @@ vue-router@^4.3.0, vue-router@^4.4.5:
dependencies: dependencies:
"@vue/devtools-api" "^6.6.4" "@vue/devtools-api" "^6.6.4"
vue-toastification@^2.0.0-rc.5:
version "2.0.0-rc.5"
resolved "https://registry.yarnpkg.com/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz#92798604d806ae473cfb76ed776fae294280f8f8"
integrity sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==
vue@^3.4.21, vue@^3.5.13: vue@^3.4.21, vue@^3.5.13:
version "3.5.13" version "3.5.13"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a" resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"