sweeping changes to internals
This commit is contained in:
parent
cf8f971785
commit
0239e509de
19 changed files with 183 additions and 214 deletions
27
.vscode/launch.json
vendored
27
.vscode/launch.json
vendored
|
|
@ -5,18 +5,18 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Node: App.Vue",
|
||||
"name": "Node: Nuxt3",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"serve",
|
||||
"dev",
|
||||
"--",
|
||||
"--port",
|
||||
"3000"
|
||||
"8082"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeExecutable": "yarn",
|
||||
"type": "node",
|
||||
"cwd": "${workspaceFolder}/frontend",
|
||||
"cwd": "${workspaceFolder}/ui",
|
||||
"env": {
|
||||
"VUE_APP_BASE_URL": "http://localhost:8081",
|
||||
}
|
||||
|
|
@ -73,23 +73,6 @@
|
|||
}
|
||||
],
|
||||
"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",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ class Download:
|
|||
self.proc.start()
|
||||
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}")
|
||||
|
||||
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=}')
|
||||
|
||||
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
|
||||
|
||||
self.tmpfilename = status.get('tmpfilename')
|
||||
|
|
@ -349,7 +349,9 @@ class Download:
|
|||
|
||||
if self.info.status == 'error' and 'error' in status:
|
||||
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:
|
||||
total = status.get('total_bytes') or status.get('total_bytes_estimate')
|
||||
|
|
@ -368,4 +370,4 @@ class Download:
|
|||
self.info.file_size = None
|
||||
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}")
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ from ItemDTO import ItemDTO
|
|||
from DataStore import DataStore
|
||||
from Utils import Notifier, ObjectSerializer, calcDownloadPath, ExtractInfo, isDownloaded, mergeConfig
|
||||
from AsyncPool import AsyncPool
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
LOG = logging.getLogger('DownloadQueue')
|
||||
TYPE_DONE: str = 'done'
|
||||
|
|
@ -295,9 +294,9 @@ class DownloadQueue:
|
|||
await item.close()
|
||||
LOG.debug(f'Deleting from queue {itemMessage}')
|
||||
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)
|
||||
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}')
|
||||
|
||||
status[id] = 'ok'
|
||||
|
|
@ -319,7 +318,7 @@ class DownloadQueue:
|
|||
itemMessage = f"{id=} {item.info.id=} {item.info.title=}"
|
||||
LOG.debug(f'Deleting completed download {itemMessage}')
|
||||
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}')
|
||||
status[id] = 'ok'
|
||||
|
||||
|
|
@ -418,12 +417,12 @@ class DownloadQueue:
|
|||
self.queue.delete(key=id)
|
||||
|
||||
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.error = 'Canceled by user.'
|
||||
|
||||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class ItemDTO:
|
|||
url: str
|
||||
quality: str
|
||||
format: str
|
||||
preset: str = "default"
|
||||
folder: str
|
||||
download_dir: str = None
|
||||
temp_dir: str = None
|
||||
|
|
|
|||
|
|
@ -333,10 +333,10 @@ class Notifier:
|
|||
async def completed(self, dl: dict):
|
||||
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)
|
||||
|
||||
async def cleared(self, id: str):
|
||||
async def cleared(self, id: str, dl: dict = None):
|
||||
await self.emit('cleared', id)
|
||||
|
||||
async def error(self, dl: dict, message: str):
|
||||
|
|
|
|||
25
app/main.py
25
app/main.py
|
|
@ -717,19 +717,28 @@ class Main:
|
|||
await self.sio.emit('status', self.serializer.encode(status))
|
||||
|
||||
@self.sio.event
|
||||
async def cancel_items(sid, post: dict):
|
||||
ids = post.get('ids')
|
||||
identifier: str = post.get('identifier')
|
||||
|
||||
if not ids:
|
||||
async def item_cancel(sid, id: str):
|
||||
if not id:
|
||||
await self.notifier.warning('Invalid request.')
|
||||
return
|
||||
|
||||
status: dict[str, str] = {}
|
||||
status = await self.queue.cancel(ids)
|
||||
status.update({'identifier': identifier})
|
||||
status = await self.queue.cancel([id])
|
||||
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()
|
||||
async def archive_item(sid, data):
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ hr {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-hint {
|
||||
user-select: none;
|
||||
cursor: help;
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
* {
|
||||
unicode-bidi: plaintext;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<span class="icon">
|
||||
<i :class="showCompleted ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
|
||||
</span>
|
||||
<span>History <span v-if="hasItems">({{ getTotal }})</span></span>
|
||||
<span>History <span v-if="hasItems">({{ stateStore.count('history') }})</span></span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
<div class="column is-half-mobile">
|
||||
<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">
|
||||
<i class="fa-solid fa-trash-can" />
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
</div>
|
||||
|
||||
<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"
|
||||
:class="{ 'is-bordered-danger': item.status === 'error', 'is-bordered-info': item.live_in || item.is_live }">
|
||||
<header class="card-header has-tooltip">
|
||||
|
|
@ -127,7 +127,8 @@
|
|||
</span>
|
||||
</div>
|
||||
<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() }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -150,33 +151,26 @@
|
|||
</div>
|
||||
<div class="columns is-mobile is-multiline">
|
||||
<div class="column is-half-mobile" v-if="item.status != 'finished'">
|
||||
<a class="button is-warning is-fullwidth" v-tooltip="'Re-queue incomplete download.'"
|
||||
@click="reQueueItem(item)">
|
||||
<a class="button is-warning is-fullwidth" v-tooltip="'Re-queue item.'" @click="reQueueItem(item)">
|
||||
<span class="icon-text is-block">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-rotate-right" />
|
||||
</span>
|
||||
<span class="icon"><i class="fa-solid fa-rotate-right" /></span>
|
||||
<span>Re-queue</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<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">
|
||||
<i class="fa-solid fa-trash-can" />
|
||||
</span>
|
||||
<span class="icon"><i class="fa-solid fa-trash-can" /></span>
|
||||
<span>Remove</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<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.'"
|
||||
@click="$emit('archiveItem', 'completed', item)">
|
||||
@click="archiveItem(item)">
|
||||
<span class="icon-text is-block">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-box-archive" />
|
||||
</span>
|
||||
<span class="icon"><i class="fa-solid fa-box-archive" /></span>
|
||||
<span>Archive</span>
|
||||
</span>
|
||||
</a>
|
||||
|
|
@ -194,11 +188,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
</LateLoader>
|
||||
</div>
|
||||
|
||||
<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 has-text-success">
|
||||
<i class="fa-solid fa-circle-check" />
|
||||
|
|
@ -219,15 +213,14 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, computed, ref, watch, defineEmits } from 'vue';
|
||||
import moment from "moment";
|
||||
import moment from 'moment'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import LazyLoader from './LazyLoader'
|
||||
import { makeDownload, formatBytes, ucFirst } from '~/utils/index'
|
||||
import toast from '~/plugins/toast'
|
||||
|
||||
const emits = defineEmits(['deleteItem', 'addItem', 'playItem', 'archiveItem'])
|
||||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const socket = useSocketStore()
|
||||
|
||||
const selectedElms = ref([]);
|
||||
const masterSelectAll = ref(false);
|
||||
|
|
@ -257,7 +250,6 @@ const sortCompleted = computed(() => {
|
|||
|
||||
const hasSelected = computed(() => selectedElms.value.length > 0)
|
||||
const hasItems = computed(() => stateStore.count('history') > 0)
|
||||
const getTotal = computed(() => stateStore.count('history'));
|
||||
|
||||
const showMessage = (item) => {
|
||||
if (!item?.msg || item.msg === item?.error) {
|
||||
|
|
@ -295,18 +287,34 @@ const hasCompleted = computed(() => {
|
|||
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 state = confirm('Are you sure you want to clear all completed downloads?');
|
||||
if (false === state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = {};
|
||||
|
||||
for (const key in stateStore.history) {
|
||||
if (stateStore.history[key].status === 'finished') {
|
||||
keys[key] = stateStore.history[key]._id;
|
||||
stateStore.remove('history', key);
|
||||
if (ag(stateStore.get('history', key, {}), 'status') === 'finished') {
|
||||
socket.emit('item_delete', stateStore.history[key]._id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -318,7 +326,7 @@ const clearIncomplete = () => {
|
|||
|
||||
for (const key in stateStore.history) {
|
||||
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) {
|
||||
const item = stateStore.history[key];
|
||||
if (item.status !== 'finished') {
|
||||
emits('deleteItem', 'completed', key);
|
||||
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,
|
||||
});
|
||||
const item = stateStore.get('history', key, {});
|
||||
if ('finished' === item.status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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) => {
|
||||
emits('deleteItem', 'completed', item._id);
|
||||
emits('addItem', {
|
||||
const archiveItem = item => {
|
||||
if (!confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)) {
|
||||
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,
|
||||
format: item.format,
|
||||
quality: item.quality,
|
||||
|
|
|
|||
|
|
@ -128,13 +128,11 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useStorage, useEventBus } from '@vueuse/core'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const config = useConfigStore();
|
||||
const socket = useSocketStore();
|
||||
|
||||
const bus = useEventBus('item_added', 'task_edit');
|
||||
const emits = defineEmits(['addItem']);
|
||||
const toast = useToast();
|
||||
|
||||
const selectedFormat = useStorage('selectedFormat', 'any')
|
||||
const selectedPreset = useStorage('selectedPreset', 'default')
|
||||
|
|
@ -152,8 +150,7 @@ const addDownload = () => {
|
|||
addInProgress.value = true;
|
||||
socket.emit('add_url', {
|
||||
url: url.value,
|
||||
format: selectedFormat.value,
|
||||
quality: selectedQuality.value,
|
||||
preset: selectedPreset.value,
|
||||
folder: downloadPath.value,
|
||||
ytdlp_config: ytdlpConfig.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.')) {
|
||||
return;
|
||||
}
|
||||
selectedFormat.value = 'any';
|
||||
selectedQuality.value = '';
|
||||
selectedPreset.value = 'default';
|
||||
ytdlpConfig.value = '';
|
||||
ytdlpCookies.value = '';
|
||||
output_template.value = null;
|
||||
|
|
@ -174,34 +170,17 @@ const resetConfig = () => {
|
|||
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 { status, msg } = JSON.parse(data)
|
||||
|
||||
addInProgress.value = false
|
||||
|
||||
console.log(data)
|
||||
|
||||
if ('error' === status) {
|
||||
notification('error', 'Add error', msg, 5000)
|
||||
toast.error(msg)
|
||||
return
|
||||
}
|
||||
|
||||
notification('success', 'Add success', msg, 3000)
|
||||
await navigateTo('/')
|
||||
url.value = '';
|
||||
}
|
||||
|
||||
onMounted(() => socket.on('status', statusHandler))
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<div class="card">
|
||||
<header class="card-header has-tooltip" v-tooltip="item.title">
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
</LateLoader>
|
||||
</div>
|
||||
|
||||
<div class="content has-text-centered" v-if="!hasQueuedItems">
|
||||
|
|
@ -125,9 +125,9 @@
|
|||
<script setup>
|
||||
import moment from 'moment'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import LazyLoader from '~/components/LazyLoader'
|
||||
import { ucFirst } from '~/utils/index'
|
||||
|
||||
const toast = useToast();
|
||||
const config = useConfigStore();
|
||||
const stateStore = useStateStore();
|
||||
const socket = useSocketStore();
|
||||
|
|
@ -135,7 +135,6 @@ const socket = useSocketStore();
|
|||
const selectedElms = ref([]);
|
||||
const masterSelectAll = ref(false);
|
||||
const showQueue = useStorage('showQueue', true)
|
||||
const actionId = ref(null);
|
||||
|
||||
watch(masterSelectAll, (value) => {
|
||||
for (const key in stateStore.queue) {
|
||||
|
|
@ -149,8 +148,7 @@ watch(masterSelectAll, (value) => {
|
|||
})
|
||||
|
||||
const hasSelected = computed(() => selectedElms.value.length > 0)
|
||||
const hasQueuedItems = computed(() => stateStore.count('history') > 0)
|
||||
const getTotal = computed(() => stateStore.count('history'));
|
||||
const hasQueuedItems = computed(() => stateStore.count('queue') > 0)
|
||||
|
||||
const setIcon = item => {
|
||||
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})?`)) {
|
||||
return false;
|
||||
}
|
||||
deleteItem('queue', item._id);
|
||||
cancelItems(item._id);
|
||||
return true;
|
||||
}
|
||||
|
||||
const deleteItem = (type, item) => {
|
||||
const cancelItems = item => {
|
||||
const items = []
|
||||
|
||||
if (typeof item === 'object') {
|
||||
|
|
@ -241,51 +239,6 @@ const deleteItem = (type, item) => {
|
|||
return;
|
||||
}
|
||||
|
||||
actionId.value = makeId();
|
||||
|
||||
socket.emit('cancel_items', {
|
||||
identifier: actionId.value,
|
||||
ids: items,
|
||||
});
|
||||
items.forEach(id => socket.emit('item_cancel', id));
|
||||
}
|
||||
|
||||
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>
|
||||
|
|
|
|||
2
ui/composables/useToast.js
Normal file
2
ui/composables/useToast.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { useToast } from "vue-toastification";
|
||||
export default () => useToast()
|
||||
|
|
@ -21,12 +21,13 @@
|
|||
</div>
|
||||
|
||||
<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"><i class="fa-solid fa-plus" /></span>
|
||||
<span>Add</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" v-if="config.tasks.length > 0">
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
</div>
|
||||
</nav>
|
||||
|
||||
<NewDownload v-if="config.showForm" />
|
||||
<NuxtPage />
|
||||
|
||||
<div class="columns mt-3 is-mobile">
|
||||
|
|
@ -151,17 +153,4 @@ watch(selectedTheme, value => {
|
|||
})
|
||||
|
||||
const reloadPage = () => window.location.reload()
|
||||
const toggleForm = () => {
|
||||
console.log('emitting show_form');
|
||||
bus.emit('show_form', { foo: 'bar' })
|
||||
dEvent('show_form', { foo: 'bar' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.user-hint {
|
||||
user-select: none;
|
||||
cursor: help;
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ export default defineNuxtConfig({
|
|||
port: 8082,
|
||||
host: "0.0.0.0",
|
||||
},
|
||||
|
||||
css: [
|
||||
'vue-toastification/dist/index.css'
|
||||
],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
domain: '/',
|
||||
|
|
@ -16,7 +18,9 @@ export default defineNuxtConfig({
|
|||
version: '2.0.0',
|
||||
}
|
||||
},
|
||||
|
||||
build: {
|
||||
transpile: ['vue-toastification'],
|
||||
},
|
||||
app: {
|
||||
buildAssetsDir: "assets",
|
||||
head: {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@
|
|||
"nuxt3-notifications": "^1.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"plyr": "^3.7.8",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"socket.io-client": "^4.7.2"
|
||||
},
|
||||
"vue-toastification": "^2.0.0-rc.5"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,7 @@
|
|||
|
||||
<script setup>
|
||||
useHead({ title: 'Index' })
|
||||
|
||||
const config = useConfigStore()
|
||||
|
||||
</script>
|
||||
|
|
|
|||
9
ui/plugins/toast.js
Normal file
9
ui/plugins/toast.js
Normal 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
|
||||
}))
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { io } from "socket.io-client";
|
||||
import { notification, ag } from "~/utils/index"
|
||||
import { ag } from "~/utils/index"
|
||||
|
||||
export const useSocketStore = defineStore('socket', () => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const toast = useToast()
|
||||
|
||||
const socket = ref(null);
|
||||
const isConnected = ref(false);
|
||||
|
|
@ -31,31 +32,34 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
socket.value.on('added', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
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 => {
|
||||
const [item, error] = JSON.parse(stream);
|
||||
notification('error', `${item?.id}: Error: ${error}`);
|
||||
toast.error(`${item?.id}: Error: ${error}`);
|
||||
});
|
||||
|
||||
socket.value.on('completed', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
|
||||
console.log(item)
|
||||
if (true === stateStore.has('queue', item._id)) {
|
||||
stateStore.move('queue', 'history', item._id);
|
||||
return
|
||||
stateStore.remove('queue', item._id);
|
||||
}
|
||||
stateStore.add('history', item);
|
||||
|
||||
stateStore.add('history', item._id, item);
|
||||
});
|
||||
|
||||
socket.value.on('canceled', stream => {
|
||||
const id = JSON.parse(stream);
|
||||
|
||||
if (true !== stateStore.has('queue', id)) {
|
||||
console.log(stream)
|
||||
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)) {
|
||||
stateStore.remove('queue', id);
|
||||
|
|
@ -64,9 +68,12 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
|
||||
socket.value.on('cleared', stream => {
|
||||
const id = JSON.parse(stream);
|
||||
|
||||
if (true !== stateStore.has('history', id)) {
|
||||
console.log(stream)
|
||||
return
|
||||
}
|
||||
|
||||
stateStore.remove('history', id);
|
||||
});
|
||||
|
||||
|
|
|
|||
45
ui/yarn.lock
45
ui/yarn.lock
|
|
@ -1683,13 +1683,13 @@ braces@^3.0.3, braces@~3.0.2:
|
|||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.23.3, browserslist@^4.24.0:
|
||||
version "4.24.2"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
|
||||
integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2"
|
||||
integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001669"
|
||||
electron-to-chromium "^1.5.41"
|
||||
node-releases "^2.0.18"
|
||||
caniuse-lite "^1.0.30001688"
|
||||
electron-to-chromium "^1.5.73"
|
||||
node-releases "^2.0.19"
|
||||
update-browserslist-db "^1.1.1"
|
||||
|
||||
buffer-crc32@^1.0.0:
|
||||
|
|
@ -1750,7 +1750,7 @@ caniuse-api@^3.0.0:
|
|||
lodash.memoize "^4.1.2"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz#f9d3ede749f083ce0db4c13db9d828adaf2e8d0a"
|
||||
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"
|
||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||
|
||||
electron-to-chromium@^1.5.41:
|
||||
electron-to-chromium@^1.5.73:
|
||||
version "1.5.73"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz#f32956ce40947fa3c8606726a96cd8fb5bb5f720"
|
||||
integrity sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==
|
||||
|
|
@ -2864,10 +2864,10 @@ is-binary-path@~2.1.0:
|
|||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-core-module@^2.13.0:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37"
|
||||
integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
|
||||
is-core-module@^2.16.0:
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.0.tgz#6c01ffdd5e33c49c1d2abfa93334a85cb56bd81c"
|
||||
integrity sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==
|
||||
dependencies:
|
||||
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"
|
||||
integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
|
||||
|
||||
node-releases@^2.0.18:
|
||||
node-releases@^2.0.19:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
||||
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
||||
|
|
@ -4182,11 +4182,11 @@ resolve-from@^5.0.0:
|
|||
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
|
||||
|
||||
resolve@^1.22.1:
|
||||
version "1.22.8"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
|
||||
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
|
||||
version "1.22.9"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.9.tgz#6da76e4cdc57181fa4471231400e8851d0a924f3"
|
||||
integrity sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==
|
||||
dependencies:
|
||||
is-core-module "^2.13.0"
|
||||
is-core-module "^2.16.0"
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
|
|
@ -4640,9 +4640,9 @@ terser@^5.17.4:
|
|||
source-map-support "~0.5.20"
|
||||
|
||||
text-decoder@^1.1.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.2.tgz#63dd2401c43895cecb292e2407db184b50ad60ac"
|
||||
integrity sha512-/MDslo7ZyWTA2vnk1j7XoDVfXsGk3tp+zFEJHJGm0UjIlQifonVFwlVbQDFh8KJzTBnT8ie115TYqir6bclddA==
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65"
|
||||
integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==
|
||||
dependencies:
|
||||
b4a "^1.6.4"
|
||||
|
||||
|
|
@ -5054,6 +5054,11 @@ vue-router@^4.3.0, vue-router@^4.4.5:
|
|||
dependencies:
|
||||
"@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:
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
|
||||
|
|
|
|||
Loading…
Reference in a new issue