minor design update for index page.

This commit is contained in:
arabcoders 2025-06-21 20:47:18 +03:00
parent a188c20099
commit a2f5c770c6
3 changed files with 173 additions and 87 deletions

View file

@ -234,7 +234,7 @@
<div class="columns is-multiline" v-else>
<LateLoader :unrender="true" :min-height="showThumbnails ? 410 : 210" class="column is-6"
v-for="item in sortCompleted" :key="item._id">
v-for="item in filteredItems(sortCompleted)" :key="item._id">
<div class="card is-flex is-full-height is-flex-direction-column"
:class="{ 'is-bordered-danger': item.status === 'error', 'is-bordered-info': item.live_in || item.is_live }">
<header class="card-header">
@ -286,18 +286,8 @@
</template>
</figure>
</div>
<div class="card-content is-flex-grow-1">
<div class="card-content">
<div class="columns is-mobile is-multiline">
<div class="column is-12" v-if="item.error">
<div class="is-text-overflow is-pointer" @click="toggle_class($event)">
<span class="has-text-danger">{{ item.error }}</span>
</div>
</div>
<div class="column is-12" v-if="showMessage(item)">
<div class="is-text-overflow is-pointer" @click="toggle_class($event)">
<span class="has-text-danger">{{ item.msg }}</span>
</div>
</div>
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
<span class="icon-text">
<span class="icon" :class="setIconColor(item)"><i :class="[setIcon(item), is_queued(item)]" /></span>
@ -329,14 +319,6 @@
</span>
</a>
</div>
<div class="column is-half-mobile">
<a class="button is-danger is-fullwidth" @click="removeItem(item)">
<span class="icon-text is-block">
<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="item.filename && item.status === 'finished'">
<a class="button is-link is-fullwidth" :href="makeDownload(config, item)"
@ -347,7 +329,17 @@
</span>
</a>
</div>
<div class="column" v-if="item.url && !config.app.basic_mode">
<div class="column is-half-mobile">
<a class="button is-danger is-fullwidth" @click="removeItem(item)">
<span class="icon-text is-block">
<span class="icon"><i class="fa-solid fa-trash-can" /></span>
<span>Remove</span>
</span>
</a>
</div>
<div class="column">
<Dropdown icons="fa-solid fa-cogs" label="Actions">
<template v-if="'finished' === item.status && item.filename">
<NuxtLink @click="playVideo(item)" class="dropdown-item">
@ -365,12 +357,13 @@
<hr class="dropdown-divider" />
</template>
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)">
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)" v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>yt-dlp Information</span>
</NuxtLink>
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)">
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)"
v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>Local Information</span>
</NuxtLink>
@ -383,7 +376,7 @@
</NuxtLink>
</template>
<template v-if="'finished' !== item.status && config.app?.keep_archive">
<template v-if="'finished' !== item.status && config.app?.keep_archive && !config.app.basic_mode">
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item has-text-danger" @click="addArchiveDialog(item)">
<span class="icon"><i class="fa-solid fa-box-archive" /></span>
@ -391,7 +384,8 @@
</NuxtLink>
</template>
<template v-if="'finished' === item.status && item.filename && config.app?.keep_archive">
<template
v-if="'finished' === item.status && item.filename && config.app?.keep_archive && !config.app.basic_mode">
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item" @click="removeFromArchiveDialog(item)">
<span class="icon"><i class="fa-solid fa-box-archive" /></span>
@ -400,6 +394,19 @@
</template>
</Dropdown>
</div>
</div>
<div class="columns is-mobile is-multiline" v-if="item.error || showMessage(item)">
<div class="column is-12" v-if="item.error">
<div class="is-text-overflow is-pointer" @click="toggle_class($event)">
<span class="has-text-danger">{{ item.error }}</span>
</div>
</div>
<div class="column is-12" v-if="showMessage(item)">
<div class="is-text-overflow is-pointer" @click="toggle_class($event)">
<span class="has-text-danger">{{ item.msg }}</span>
</div>
</div>
</div>
</div>
</div>
@ -408,8 +415,12 @@
<div class="columns is-multiline" v-if="!hasItems">
<div class="column is-12">
<Message message_class="has-background-warning-90 has-text-dark" title="No results for downloaded items."
icon="fas fa-search" :useClose="true" @close="() => emitter('clear_search')" v-if="query">
<span class="is-block">No results found for '<span class="is-underlined is-bold">{{ query }}</span>'.</span>
</Message>
<Message message_class="has-background-success-90 has-text-dark" title="No records in history."
icon="fas fa-circle-check" v-if="socket.isConnected" />
icon="fas fa-circle-check" v-else-if="socket.isConnected" />
<Message message_class="has-background-info-90 has-text-dark" title="Connecting.." icon="fas fa-spinner fa-spin"
v-else />
</div>
@ -446,12 +457,17 @@ import { getEmbedable, isEmbedable } from '~/utils/embedable'
import { formatBytes, makeDownload, uri } from '~/utils/index'
import Dropdown from './Dropdown.vue'
const emitter = defineEmits(['getInfo', 'add_new', 'getItemInfo'])
const emitter = defineEmits(['getInfo', 'add_new', 'getItemInfo', 'clear_search'])
const props = defineProps({
thumbnails: {
type: Boolean,
default: true
},
query: {
type: String,
required: false,
default: '',
}
})
@ -461,34 +477,42 @@ const socket = useSocketStore()
const toast = useNotification()
const box = useConfirm()
const selectedElms = ref([])
const masterSelectAll = ref(false)
const showCompleted = useStorage('showCompleted', true)
const hideThumbnail = useStorage('hideThumbnailHistory', false)
const direction = useStorage('sortCompleted', 'desc')
const display_style = useStorage('display_style', 'cards')
const table_container = ref(false)
const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.85)
const selectedElms = ref([])
const masterSelectAll = ref(false)
const table_container = ref(false)
const embed_url = ref('')
const video_item = ref(null)
const dialog_confirm = ref({
visible: false,
title: 'Confirm Action',
confirm: (opts) => { },
confirm: () => { },
message: '',
options: [
{ key: 'remove_history', label: 'Also, Remove from history.' },
],
})
const showThumbnails = computed(() => props.thumbnails && !hideThumbnail.value)
const playVideo = item => video_item.value = item
const closeVideo = () => video_item.value = null
const showThumbnails = computed(() => props.thumbnails && !hideThumbnail.value)
const filteredItems = items => !props.query ? items : items.filter(filterItem)
const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.85)
const filterItem = item => {
const q = props.query?.toLowerCase()
if (!q) {
return true
}
return Object.values(item).some(v => typeof v === 'string' && v.toLowerCase().includes(q))
}
watch(masterSelectAll, (value) => {
for (const key in stateStore.history) {
@ -512,7 +536,7 @@ const sortCompleted = computed(() => {
})
const hasSelected = computed(() => selectedElms.value.length > 0)
const hasItems = computed(() => stateStore.count('history') > 0)
const hasItems = computed(() => filteredItems(sortCompleted.value).length > 0)
const showMessage = (item) => {
if (!item?.msg || item.msg === item?.error) {

View file

@ -4,12 +4,12 @@
<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>Queue <span v-if="hasQueuedItems">({{ filteredItems.length }})</span></span>
</span>
</h1>
<div v-if="showQueue">
<div class="columns is-multiline is-mobile has-text-centered" v-if="hasQueuedItems">
<div class="columns is-multiline is-mobile has-text-centered" v-if="filteredItems.length > 0">
<div class="column is-half-mobile" v-if="'cards' === display_style">
<button type="button" class="button is-fullwidth is-ghost" @click="masterSelectAll = !masterSelectAll">
<span class="icon-text is-block">
@ -34,8 +34,8 @@
</div>
<div class="columns is-multiline" v-if="'list' === display_style">
<div class="column is-12" v-if="hasQueuedItems">
<div class="table-container is-responsive">
<div class="column is-12" v-if="filteredItems.length > 0">
<div :class="{ 'table-container': table_container }">
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 1300px; table-layout: fixed;">
<thead>
@ -58,7 +58,7 @@
</tr>
</thead>
<tbody>
<tr v-for="item in stateStore.queue" :key="item._id">
<tr v-for="item in filteredItems" :key="item._id">
<td class="has-text-centered is-vcentered">
<label class="checkbox is-block">
<input class="completed-checkbox" type="checkbox" v-model="selectedElms"
@ -105,26 +105,34 @@
:data-datetime="item.datetime" v-rtime="item.datetime" />
</td>
<td class="is-vcentered is-items-center">
<div class="field is-grouped is-grouped-centered">
<div class="control" v-if="isEmbedable(item.url)">
<button @click="() => embed_url = getEmbedable(item.url)" v-tooltip="'Play video'"
class="button is-danger is-small">
<Dropdown icons="fa-solid fa-cogs" @open_state="s => table_container = !s"
:button_classes="'is-small'" label="Actions">
<template v-if="isEmbedable(item.url)">
<NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url)">
<span class="icon"><i class="fa-solid fa-play" /></span>
</button>
</div>
<div class="control">
<button class="button is-warning is-small" @click="confirmCancel(item);"
v-tooltip="'Cancel download'">
<span class="icon"><i class="fa-solid fa-eject" /></span>
</button>
</div>
<div class="control" v-if="item.url && !config.app.basic_mode">
<button class="button is-info is-small" @click="emitter('getInfo', item.url)"
v-tooltip="'Show video information'">
<span class="icon"><i class="fa-solid fa-info" /></span>
</button>
</div>
</div>
<span>Play video</span>
</NuxtLink>
<hr class="dropdown-divider" />
</template>
<NuxtLink class="dropdown-item has-text-warning" @click="confirmCancel(item);">
<span class="icon"><i class="fa-solid fa-eject" /></span>
<span>Cancel Item</span>
</NuxtLink>
<hr class="dropdown-divider" v-if="!config.app.basic_mode" />
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)" v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>yt-dlp Information</span>
</NuxtLink>
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)"
v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>Local Information</span>
</NuxtLink>
</Dropdown>
</td>
</tr>
</tbody>
@ -135,8 +143,8 @@
<div class="columns is-multiline" v-else>
<LateLoader :unrender="true" :min-height="showThumbnails ? 475 : 265" class="column is-6"
v-for="item in stateStore.queue" :key="item._id">
<div class="card is-flex is-full-height is-flex-direction-column">
v-for="item in filteredItems" :key="item._id">
<div class="card">
<header class="card-header">
<div class="card-header-title is-text-overflow is-block" v-tooltip="item.title">
<NuxtLink target="_blank" :href="item.url">{{ item.title }}</NuxtLink>
@ -149,11 +157,6 @@
{{ formatTime(item.extras.duration) }}
</span>
</div>
<div class="control" v-if="!showThumbnails && isEmbedable(item.url)">
<NuxtLink @click="embed_url = getEmbedable(item.url)" v-tooltip="'Play video'">
<span class="icon has-text-danger"><i class="fa-solid fa-play" /></span>
</NuxtLink>
</div>
<div class="control">
<button @click="hideThumbnail = !hideThumbnail" v-if="thumbnails">
<span class="icon"><i class="fa-solid"
@ -184,7 +187,7 @@
</template>
</figure>
</div>
<div class="card-content is-flex-grow-1">
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-12">
<div class="progress-bar is-unselectable">
@ -217,11 +220,27 @@
<span>Cancel</span>
</button>
</div>
<div class="column is-half-mobile" v-if="item.url && !config.app.basic_mode">
<button class="button is-info is-fullwidth" @click="emitter('getInfo', item.url)">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Information</span>
</button>
<div class="column is-half-mobile">
<Dropdown icons="fa-solid fa-cogs" @open_state="s => table_container = !s" label="Actions">
<template v-if="isEmbedable(item.url)">
<NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url)">
<span class="icon"><i class="fa-solid fa-play" /></span>
<span>Play video</span>
</NuxtLink>
<hr class="dropdown-divider" />
</template>
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)" v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>yt-dlp Information</span>
</NuxtLink>
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)"
v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>Local Information</span>
</NuxtLink>
</Dropdown>
</div>
</div>
</div>
@ -229,6 +248,14 @@
</LateLoader>
</div>
<div class="columns is-multiline" v-if="hasQueuedItems && filteredItems.length < 1 && query">
<div class="column is-12">
<Message message_class="has-background-warning-90 has-text-dark" title="No results for queued items"
icon="fas fa-search" :useClose="true" @close="() => emitter('clear_search')" v-if="query">
<span class="is-block">No results found for '<span class="is-underlined is-bold">{{ query }}</span>'.</span>
</Message>
</div>
</div>
<div class="modal is-active" v-if="embed_url">
<div class="modal-background" @click="embed_url = ''"></div>
@ -246,11 +273,16 @@ import { useStorage } from '@vueuse/core'
import { ucFirst } from '~/utils/index'
import { isEmbedable, getEmbedable } from '~/utils/embedable'
const emitter = defineEmits(['getInfo'])
const emitter = defineEmits(['getInfo', 'clear_search', 'getItemInfo'])
const props = defineProps({
thumbnails: {
type: Boolean,
default: true
},
query: {
type: String,
required: false,
default: ''
}
})
const config = useConfigStore()
@ -258,18 +290,19 @@ const stateStore = useStateStore()
const socket = useSocketStore()
const box = useConfirm()
const selectedElms = ref([])
const masterSelectAll = ref(false)
const showQueue = useStorage('showQueue', true)
const hideThumbnail = useStorage('hideThumbnailQueue', false)
const display_style = useStorage('display_style', 'cards')
const showThumbnails = computed(() => props.thumbnails && !hideThumbnail.value)
const embed_url = ref('')
const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.85)
const selectedElms = ref([])
const masterSelectAll = ref(false)
const embed_url = ref('')
const table_container = ref(false)
const showThumbnails = computed(() => props.thumbnails && !hideThumbnail.value)
watch(masterSelectAll, (value) => {
for (const key in stateStore.queue) {
const element = stateStore.queue[key]
@ -281,6 +314,14 @@ watch(masterSelectAll, (value) => {
}
})
const filteredItems = computed(() => {
const q = props.query?.toLowerCase();
if (!q) {
return Object.values(stateStore.queue)
}
return Object.values(stateStore.queue).filter(i => Object.values(i).some(v => typeof v === 'string' && v.toLowerCase().includes(q)));
});
const hasSelected = computed(() => selectedElms.value.length > 0)
const hasQueuedItems = computed(() => stateStore.count('queue') > 0)

View file

@ -11,6 +11,19 @@
<div class="is-pulled-right" v-if="socket.isConnected">
<div class="field is-grouped">
<p class="control has-icons-left" v-if="toggleFilter">
<input type="search" v-model.lazy="query" class="input" id="filter"
placeholder="Filter displayed content">
<span class="icon is-left"><i class="fas fa-filter" /></span>
</p>
<p class="control">
<button class="button is-danger is-light" v-tooltip.bottom="'Filter'"
@click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter" /></span>
</button>
</p>
<p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode">
<button class="button is-warning" @click="pauseDownload" v-if="false === config.paused"
v-tooltip.bottom="'Pause non-active downloads.'">
@ -49,10 +62,11 @@
<NewDownload v-if="config.showForm || config.app.basic_mode" @getInfo="url => get_info = url" :item="item_form"
@clear_form="item_form = {}" @remove_archive="" />
<Queue @getInfo="(url: string) => view_info(url, false)" :thumbnails="show_thumbnail"
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" />
<History @getInfo="(url: string) => view_info(url, false)" @add_new="item => toNewDownload(item)"
:thumbnails="show_thumbnail" @getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" />
<Queue @getInfo="(url: string) => view_info(url, false)" :thumbnails="show_thumbnail" :query="query"
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" />
<History @getInfo="(url: string) => view_info(url, false)" @add_new="item => toNewDownload(item)" :query="query"
:thumbnails="show_thumbnail" @getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)"
@clear_search="query = ''" />
<GetInfo v-if="get_info" :link="get_info" :useUrl="get_info_use_url" @closeModel="close_info()" />
</div>
</template>
@ -64,15 +78,22 @@ const config = useConfigStore()
const stateStore = useStateStore()
const socket = useSocketStore()
const box = useConfirm()
const get_info = ref<string>('')
const get_info_use_url = ref<boolean>(false)
const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.85)
const display_style = useStorage<string>('display_style', 'cards')
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const get_info = ref<string>('')
const get_info_use_url = ref<boolean>(false)
const item_form = ref({})
const query = ref()
const toggleFilter = ref(false)
watch(toggleFilter, () => {
if (!toggleFilter.value) {
query.value = ''
}
});
onMounted(() => {
if (!config.app.ui_update_title) {