ytptube/ui/components/Queue.vue
2024-12-14 16:42:31 +03:00

291 lines
8.8 KiB
Vue

<template>
<h1 class="mt-3 is-size-3 is-clickable is-unselectable" @click="showQueue = !showQueue">
<span class="icon-text title is-4">
<span class="icon">
<i class="fas" :class="showQueue ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
</span>
<span>Queue <span v-if="hasQueuedItems">({{ stateStore.count('queue') }})</span></span>
</span>
</h1>
<div v-if="showQueue">
<div class="columns is-multiline is-mobile has-text-centered" v-if="hasQueuedItems">
<div class="column is-half-mobile">
<button type="button" class="button is-fullwidth is-ghost" @click="masterSelectAll = !masterSelectAll">
<span class="icon-text is-block">
<span class="icon">
<i class="fas" :class="!masterSelectAll ? 'fa-regular fa-square-check' : 'fa-regular fa-square'" />
</span>
<span v-if="!masterSelectAll">Select All</span>
<span v-else>Unselect All</span>
</span>
</button>
</div>
<div class="column is-half-mobile">
<button type="button" class="button is-fullwidth is-danger" :disabled="!hasSelected"
@click="$emit('deleteItem', 'queue', selectedElms); selectedElms = []">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-trash-can" />
</span>
<span>Cancel Selected</span>
</span>
</button>
</div>
</div>
<div class="columns is-multiline">
<LazyLoader :unrender="true" :min-height="265" class="column is-6" v-for="item in stateStore.history"
:key="item._id">
<div class="card">
<header class="card-header has-tooltip" v-tooltip="item.title">
<div class="card-header-title has-text-centered is-text-overflow is-block">
{{ item.title }}
</div>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-12">
<div class="progress-bar is-round">
<div class="progress-percentage">{{ updateProgress(item) }}</div>
<div class="progress" :style="{ width: percentPipe(item.percent) + '%' }"></div>
</div>
</div>
<div class="column is-half-mobile has-text-centered">
<span class="icon-text">
<span class="icon" :class="{ 'has-text-success': item.status == 'downloading' }">
<i class="fas" :class="setIcon(item)" />
</span>
<span v-if="item.status == 'downloading' && item.is_live">Live Streaming</span>
<span v-else>{{ ucFirst(item.status) }}</span>
</span>
</div>
<div class="column is-half-mobile has-text-centered">
<span :data-datetime="item.datetime"
v-tooltip="moment(item.datetime).format('MMMM Do YYYY, h:mm:ss a')">
{{ moment(item.datetime).fromNow() }}
</span>
</div>
<div class="column is-half-mobile has-text-centered">
<label class="checkbox is-block">
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
:value="item._id">
Select
</label>
</div>
</div>
<div class="columns is-multiline is-mobile">
<div class="column is-half-mobile">
<button class="button is-danger is-fullwidth" @click="confirmCancel(item);">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-trash-can" />
</span>
<span>Cancel</span>
</span>
</button>
</div>
<div class="column is-half-mobile">
<a referrerpolicy="no-referrer" class="button is-link is-fullwidth" target="_blank" :href="item.url">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-up-right-from-square" />
</span>
<span>Visit Link</span>
</span>
</a>
</div>
</div>
</div>
</div>
</LazyLoader>
</div>
<div class="content has-text-centered" v-if="!hasQueuedItems">
<p v-if="socket.isConnected">
<span class="icon-text">
<span class="icon has-text-success">
<i class="fa-solid fa-circle-check" />
</span>
<span>No queued items.</span>
</span>
</p>
<p v-else>
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-spinner fa-spin" />
</span>
<span>Connecting...</span>
</span>
</p>
</div>
</div>
</template>
<script setup>
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import LazyLoader from '~/components/LazyLoader'
import { ucFirst } from '~/utils/index'
const config = useConfigStore();
const stateStore = useStateStore();
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) {
const element = stateStore.queue[key];
if (value) {
selectedElms.value.push(element._id);
} else {
selectedElms.value = [];
}
}
})
const hasSelected = computed(() => selectedElms.value.length > 0)
const hasQueuedItems = computed(() => stateStore.count('history') > 0)
const getTotal = computed(() => stateStore.count('history'));
const setIcon = item => {
if (item.status === 'downloading' && item.is_live) {
return 'fa-solid fa-globe';
}
if (item.status === 'downloading') {
return 'fa-solid fa-circle-check';
}
return 'fa-solid fa-spinner fa-spin';
}
const ETAPipe = value => {
if (value === null || 0 === value) {
return 'Live';
}
if (value < 60) {
return `${Math.round(value)}s`;
}
if (value < 3600) {
return `${Math.floor(value / 60)}m ${Math.round(value % 60)}s`;
}
const hours = Math.floor(value / 3600)
const minutes = value % 3600
return `${hours}h ${Math.floor(minutes / 60)}m ${Math.round(minutes % 60)}s`;
}
const speedPipe = value => {
if (value === null || 0 === value) {
return '0KB/s';
}
const k = 1024;
const dm = 2;
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s', 'EB/s', 'ZB/s', 'YB/s'];
const i = Math.floor(Math.log(value) / Math.log(k));
return parseFloat((value / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
const percentPipe = value => {
if (value === null || 0 === value) {
return '00.00';
}
return parseFloat(value).toFixed(2);
}
const updateProgress = (item) => {
let string = '';
if (item.status == 'preparing') {
return 'Preparing';
}
if (item.status != null) {
string += item.percent && !item.is_live ? percentPipe(item.percent) + '%' : 'Live';
}
string += item.speed ? ' - ' + speedPipe(item.speed) : ' - Waiting..';
if (item.status != null && item.eta) {
string += ' - ' + ETAPipe(item.eta);
}
return string;
}
const confirmCancel = item => {
if (true !== confirm(`Are you sure you want to cancel (${item.title})?`)) {
return false;
}
deleteItem('queue', item._id);
return true;
}
const deleteItem = (type, item) => {
const items = []
if (typeof item === 'object') {
for (const key in item) {
items.push(item[key]);
}
} else {
items.push(item);
}
if (items.length < 0) {
return;
}
actionId.value = makeId();
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>