wip
This commit is contained in:
parent
e7fd60010b
commit
bb246cc6f5
15 changed files with 968 additions and 725 deletions
|
|
@ -1,12 +1,15 @@
|
|||
<template>
|
||||
<div class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div v-if="hasItems" class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div
|
||||
v-if="hasItems"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:icon="masterSelectAll ? 'i-lucide-square' : 'i-lucide-check'"
|
||||
:icon="masterSelectAll ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
{{ masterSelectAll ? 'Unselect' : 'Select' }}
|
||||
|
|
@ -34,9 +37,10 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="direction === 'desc' ? 'i-lucide-arrow-down-a-z' : 'i-lucide-arrow-up-a-z'"
|
||||
square
|
||||
@click="toggleDirection"
|
||||
/>
|
||||
>
|
||||
<span>Sort</span>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
|
|
@ -48,17 +52,24 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
v-if="'list' === display_style && hasItems"
|
||||
v-if="'list' === contentStyle && hasItems"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-190 table-fixed w-full text-sm">
|
||||
<table class="min-w-210 table-fixed w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer"
|
||||
:aria-label="masterSelectAll ? 'Unselect all items' : 'Select all items'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
<UIcon
|
||||
:name="masterSelectAll ? 'i-lucide-square' : 'i-lucide-check'"
|
||||
:name="masterSelectAll ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
|
|
@ -67,13 +78,17 @@
|
|||
<th class="w-32 whitespace-nowrap">Status</th>
|
||||
<th class="w-36 whitespace-nowrap">Created</th>
|
||||
<th class="w-36 whitespace-nowrap">Size/Starts</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
<th class="w-80 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="item in displayedItems" :key="item._id" class="align-top hover:bg-muted/20">
|
||||
<td class="px-3 py-3 text-center align-top">
|
||||
<tr
|
||||
v-for="item in displayedItems"
|
||||
:key="item._id"
|
||||
class="align-top transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="border-r border-default/60 px-3 py-3 text-center align-top">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
:id="`checkbox-${item._id}`"
|
||||
|
|
@ -85,7 +100,7 @@
|
|||
</label>
|
||||
</td>
|
||||
|
||||
<td class="w-0 px-3 py-3 align-top">
|
||||
<td class="w-0 border-r border-default/60 px-3 py-3 align-top">
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<UTooltip
|
||||
|
|
@ -174,7 +189,7 @@
|
|||
</p>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 text-center align-top text-sm">
|
||||
<td class="border-r border-default/60 px-3 py-3 text-center align-top text-sm">
|
||||
<div class="inline-flex items-center gap-2 text-default">
|
||||
<span class="inline-flex items-center">
|
||||
<UIcon
|
||||
|
|
@ -186,13 +201,17 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 text-center align-top text-sm text-toned whitespace-nowrap">
|
||||
<td
|
||||
class="border-r border-default/60 px-3 py-3 text-center align-top text-sm text-toned whitespace-nowrap"
|
||||
>
|
||||
<UTooltip :text="moment(item.datetime).format('YYYY-M-DD H:mm Z')">
|
||||
<span :date-datetime="item.datetime" v-rtime="item.datetime" />
|
||||
</UTooltip>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 text-center align-top text-sm text-toned whitespace-nowrap">
|
||||
<td
|
||||
class="border-r border-default/60 px-3 py-3 text-center align-top text-sm text-toned whitespace-nowrap"
|
||||
>
|
||||
<template
|
||||
v-if="'not_live' === item.status && (item.live_in || item.extras?.release_in)"
|
||||
>
|
||||
|
|
@ -211,38 +230,41 @@
|
|||
</template>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-top whitespace-nowrap">
|
||||
<td class="w-80 px-3 py-3 align-top whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<UButton
|
||||
v-if="showRetryAction(item)"
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-rotate-cw"
|
||||
square
|
||||
@click="() => void retryItem(item, true)"
|
||||
/>
|
||||
>
|
||||
Retry
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="item.filename"
|
||||
color="info"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-download"
|
||||
square
|
||||
external
|
||||
:href="makeDownload(config, item)"
|
||||
:download="item.filename?.split('/').reverse()[0]"
|
||||
/>
|
||||
>
|
||||
Download
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
square
|
||||
@click="() => void removeItem(item)"
|
||||
/>
|
||||
>
|
||||
Remove
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu v-if="item.url" :items="itemActionGroups(item)" :modal="false">
|
||||
<UButton
|
||||
|
|
@ -273,8 +295,12 @@
|
|||
>
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden border"
|
||||
:class="historyCardClass(item)"
|
||||
:ui="{ body: 'flex flex-1 flex-col gap-4 p-4', header: 'p-4 pb-3', root: 'bg-default' }"
|
||||
:ui="{
|
||||
body: 'flex flex-1 flex-col gap-4 p-4',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
header: 'p-4 pb-3',
|
||||
root: 'bg-default',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
|
||||
|
|
@ -390,102 +416,85 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-sm *:min-w-32 *:flex-1">
|
||||
<div
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-default"
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-default transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'status')"
|
||||
>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon
|
||||
:name="setIcon(item)"
|
||||
:class="[setIconColor(item), isQueuedAnimation(item), 'size-4 shrink-0']"
|
||||
/>
|
||||
<span>{{ setStatus(item) }}</span>
|
||||
<span :class="['min-w-0 text-center', expandClass(item._id, 'status')]">
|
||||
{{ setStatus(item) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-default"
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-default transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'preset')"
|
||||
>
|
||||
<UTooltip :text="`Preset: ${item.preset}`">
|
||||
<span class="block min-w-0 truncate">{{ item.preset }}</span>
|
||||
</UTooltip>
|
||||
</div>
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-4 shrink-0 text-toned" />
|
||||
<span :class="['min-w-0 text-center', expandClass(item._id, 'preset')]">
|
||||
{{ item.preset }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
<button
|
||||
v-if="'not_live' === item.status && (item.live_in || item.extras?.release_in)"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-toned"
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-toned transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'retry_at')"
|
||||
>
|
||||
<UTooltip
|
||||
:text="`Retry at: ${moment(item.live_in || item.extras?.release_in).format('YYYY-M-DD H:mm Z')}`"
|
||||
>
|
||||
<span
|
||||
:date-datetime="item.live_in || item.extras?.release_in"
|
||||
v-rtime="item.live_in || item.extras?.release_in"
|
||||
/>
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon name="i-lucide-calendar" class="size-4 shrink-0 text-toned" />
|
||||
<span
|
||||
:class="['min-w-0 text-center', expandClass(item._id, 'retry_at')]"
|
||||
:date-datetime="item.live_in || item.extras?.release_in"
|
||||
v-rtime="item.live_in || item.extras?.release_in"
|
||||
/>
|
||||
</span>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-toned"
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-toned transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'datetime')"
|
||||
>
|
||||
<UTooltip :text="moment(item.datetime).format('YYYY-M-DD H:mm Z')">
|
||||
<span :date-datetime="item.datetime" v-rtime="item.datetime" />
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon name="i-lucide-clock-3" class="size-4 shrink-0 text-toned" />
|
||||
<span
|
||||
:class="['min-w-0 text-center', expandClass(item._id, 'datetime')]"
|
||||
:date-datetime="item.datetime"
|
||||
v-rtime="item.datetime"
|
||||
/>
|
||||
</span>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
<button
|
||||
v-if="item.file_size"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-toned"
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-toned transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'size')"
|
||||
>
|
||||
{{ formatBytes(item.file_size) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
v-if="showRetryAction(item)"
|
||||
color="warning"
|
||||
variant="outline"
|
||||
icon="i-lucide-rotate-cw"
|
||||
class="w-full justify-center"
|
||||
@click="() => void retryItem(item, false)"
|
||||
>
|
||||
Retry
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="item.filename"
|
||||
color="info"
|
||||
variant="outline"
|
||||
icon="i-lucide-download"
|
||||
class="w-full justify-center"
|
||||
external
|
||||
:href="makeDownload(config, item)"
|
||||
:download="item.filename?.split('/').reverse()[0]"
|
||||
>
|
||||
Download
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void removeItem(item)"
|
||||
>
|
||||
{{ config.app.remove_files ? 'Remove' : 'Clear' }}
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu :items="itemActionGroups(item)" :modal="false" class="w-full">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-settings-2"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
class="w-full justify-center"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon name="i-lucide-hard-drive" class="size-4 shrink-0 text-toned" />
|
||||
<span :class="['min-w-0 text-center', expandClass(item._id, 'size')]">
|
||||
{{ formatBytes(item.file_size) }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -508,6 +517,56 @@
|
|||
{{ item.msg }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
v-if="showRetryAction(item)"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-rotate-cw"
|
||||
class="w-full justify-center"
|
||||
@click="() => void retryItem(item, false)"
|
||||
>
|
||||
Retry
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="item.filename"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-download"
|
||||
class="w-full justify-center"
|
||||
external
|
||||
:href="makeDownload(config, item)"
|
||||
:download="item.filename?.split('/').reverse()[0]"
|
||||
>
|
||||
Download
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void removeItem(item)"
|
||||
>
|
||||
{{ config.app.remove_files ? 'Remove' : 'Clear' }}
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu :items="itemActionGroups(item)" :modal="false" class="w-full">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-settings-2"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
class="w-full justify-center"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</LateLoader>
|
||||
</div>
|
||||
|
|
@ -614,6 +673,7 @@
|
|||
import moment from 'moment';
|
||||
import { useStorage, useIntersectionObserver } from '@vueuse/core';
|
||||
import { useDialog } from '~/composables/useDialog';
|
||||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import type { StoreItem } from '~/types/store';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
import { deepIncludes, getPath, getImage, isDownloadSkipped } from '~/utils';
|
||||
|
|
@ -637,11 +697,13 @@ const socket = useSocketStore();
|
|||
const toast = useNotification();
|
||||
const box = useConfirm();
|
||||
const { confirmDialog } = useDialog();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
||||
const showCompleted = useStorage<boolean>('showCompleted', true);
|
||||
const hideThumbnail = useStorage<boolean>('hideThumbnailHistory', false);
|
||||
const direction = useStorage<'asc' | 'desc'>('sortCompleted', 'desc');
|
||||
const display_style = useStorage<'grid' | 'list'>('display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
const bg_enable = useStorage<boolean>('random_bg', true);
|
||||
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95);
|
||||
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1');
|
||||
|
|
@ -654,6 +716,10 @@ const video_item = ref<StoreItem | null>(null);
|
|||
const loadMoreTrigger = ref<HTMLElement | null>(null);
|
||||
const expandedMessages = reactive<Record<string, Set<string>>>({});
|
||||
|
||||
const contentStyle = computed<'grid' | 'list'>(() =>
|
||||
isMobile.value ? 'grid' : display_style.value,
|
||||
);
|
||||
|
||||
const paginationInfo = computed(() => stateStore.getPagination());
|
||||
|
||||
// Function to load more history items
|
||||
|
|
@ -818,18 +884,6 @@ const getGridImage = (item: StoreItem): string => getImage(config.app.download_p
|
|||
|
||||
const showRetryAction = (item: StoreItem): boolean => !item.filename && !isDownloadSkipped(item);
|
||||
|
||||
const historyCardClass = (item: StoreItem): string => {
|
||||
if (item.status === 'error') {
|
||||
return 'border-error';
|
||||
}
|
||||
|
||||
if (item.live_in || item.is_live) {
|
||||
return 'border-info';
|
||||
}
|
||||
|
||||
return 'border-default';
|
||||
};
|
||||
|
||||
const bulkActionGroups = computed(() => {
|
||||
const groups: Array<Array<Record<string, unknown>>> = [
|
||||
[
|
||||
|
|
@ -842,7 +896,6 @@ const bulkActionGroups = computed(() => {
|
|||
{
|
||||
label: config.app.remove_files ? 'Remove' : 'Clear',
|
||||
icon: 'i-lucide-trash',
|
||||
color: 'error',
|
||||
disabled: !hasSelected.value,
|
||||
onSelect: () => void deleteSelectedItems(),
|
||||
},
|
||||
|
|
@ -869,7 +922,6 @@ const bulkActionGroups = computed(() => {
|
|||
{
|
||||
label: 'Retry Incomplete',
|
||||
icon: 'i-lucide-rotate-cw',
|
||||
color: 'warning',
|
||||
onSelect: () => void retryIncomplete(),
|
||||
},
|
||||
);
|
||||
|
|
@ -897,7 +949,6 @@ const itemActionGroups = (item: StoreItem) => {
|
|||
mediaActions.push({
|
||||
label: 'Retry download',
|
||||
icon: 'i-lucide-rotate-cw',
|
||||
color: 'warning',
|
||||
onSelect: () => void retryItem(item, true),
|
||||
});
|
||||
}
|
||||
|
|
@ -905,14 +956,12 @@ const itemActionGroups = (item: StoreItem) => {
|
|||
mediaActions.push({
|
||||
label: 'Generate NFO',
|
||||
icon: 'i-lucide-file-code-2',
|
||||
color: 'info',
|
||||
onSelect: () => void generateNfo(item),
|
||||
});
|
||||
} else if (isEmbedable(item.url)) {
|
||||
mediaActions.push({
|
||||
label: 'Play video',
|
||||
icon: 'i-lucide-play',
|
||||
color: 'error',
|
||||
onSelect: () => {
|
||||
embed_url.value = getEmbedable(item.url) as string;
|
||||
},
|
||||
|
|
@ -946,7 +995,6 @@ const itemActionGroups = (item: StoreItem) => {
|
|||
{
|
||||
label: 'Add to archive',
|
||||
icon: 'i-lucide-archive',
|
||||
color: 'error',
|
||||
onSelect: () => addArchiveDialog(item),
|
||||
},
|
||||
]);
|
||||
|
|
@ -957,7 +1005,6 @@ const itemActionGroups = (item: StoreItem) => {
|
|||
{
|
||||
label: 'Remove from archive',
|
||||
icon: 'i-lucide-archive-x',
|
||||
color: 'error',
|
||||
onSelect: () => removeFromArchiveDialog(item),
|
||||
},
|
||||
]);
|
||||
|
|
@ -1113,7 +1160,7 @@ const setStatus = (item: StoreItem) => {
|
|||
if (item.extras?.is_premiere) {
|
||||
return 'Premiere';
|
||||
}
|
||||
return display_style.value === 'grid' ? 'Stream' : 'Live';
|
||||
return 'Live';
|
||||
}
|
||||
if ('skip' === item.status) {
|
||||
return 'Skipped';
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@
|
|||
</UButton>
|
||||
</div>
|
||||
|
||||
<div v-if="hasItems" class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div
|
||||
v-if="hasItems"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:icon="masterSelectAll ? 'i-lucide-square' : 'i-lucide-check'"
|
||||
:icon="masterSelectAll ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
{{ masterSelectAll ? 'Unselect' : 'Select' }}
|
||||
|
|
@ -44,17 +47,24 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="'list' === display_style && hasItems"
|
||||
v-if="'list' === contentStyle && hasItems"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-190 table-fixed w-full text-sm">
|
||||
<table class="min-w-210 table-fixed w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer"
|
||||
:aria-label="masterSelectAll ? 'Unselect all items' : 'Select all items'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
<UIcon
|
||||
:name="masterSelectAll ? 'i-lucide-square' : 'i-lucide-check'"
|
||||
:name="masterSelectAll ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
|
|
@ -63,13 +73,17 @@
|
|||
<th class="w-56">Progress</th>
|
||||
<th class="w-32 whitespace-nowrap">Status</th>
|
||||
<th class="w-36 whitespace-nowrap">Created</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
<th class="w-80 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="item in displayedItems" :key="item._id" class="align-top hover:bg-muted/20">
|
||||
<td class="px-3 py-3 text-center align-top">
|
||||
<tr
|
||||
v-for="item in displayedItems"
|
||||
:key="item._id"
|
||||
class="align-top transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="border-r border-default/60 px-3 py-3 text-center align-top">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
:id="`checkbox-${item._id}`"
|
||||
|
|
@ -81,7 +95,7 @@
|
|||
</label>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 align-top">
|
||||
<td class="border-r border-default/60 px-3 py-3 align-top">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<UTooltip :text="`[${item.preset}] - ${item.title}`">
|
||||
|
|
@ -153,7 +167,7 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="w-56 px-3 py-3 align-top">
|
||||
<td class="w-56 border-r border-default/60 px-3 py-3 align-top">
|
||||
<div
|
||||
class="queue-progress queue-progress--compact w-56 rounded-md border border-default bg-muted/20"
|
||||
>
|
||||
|
|
@ -180,7 +194,7 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 text-center align-top text-sm">
|
||||
<td class="border-r border-default/60 px-3 py-3 text-center align-top text-sm">
|
||||
<div class="inline-flex items-center gap-2 text-default whitespace-nowrap">
|
||||
<span class="inline-flex items-center">
|
||||
<UIcon
|
||||
|
|
@ -192,44 +206,47 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 text-center align-top text-sm text-toned whitespace-nowrap">
|
||||
<td
|
||||
class="border-r border-default/60 px-3 py-3 text-center align-top text-sm text-toned whitespace-nowrap"
|
||||
>
|
||||
<UTooltip :text="moment(item.datetime).format('MMMM Do YYYY, h:mm:ss a')">
|
||||
<span :data-datetime="item.datetime" v-rtime="item.datetime" />
|
||||
</UTooltip>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-top whitespace-nowrap">
|
||||
<td class="w-80 px-3 py-3 align-top whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-circle-off"
|
||||
square
|
||||
:title="item.is_live ? 'Stop Stream' : 'Cancel Download'"
|
||||
:aria-label="item.is_live ? 'Stop Stream' : 'Cancel Download'"
|
||||
@click="() => void confirmCancel(item)"
|
||||
/>
|
||||
>
|
||||
{{ item.is_live ? 'Stop Stream' : 'Cancel' }}
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="canStartItem(item)"
|
||||
color="success"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-circle-play"
|
||||
square
|
||||
@click="() => void startItem(item)"
|
||||
/>
|
||||
>
|
||||
Start
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="canPauseItem(item)"
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-pause"
|
||||
square
|
||||
@click="() => void pauseItem(item)"
|
||||
/>
|
||||
>
|
||||
Pause
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu :items="itemActionGroups(item)" :modal="false">
|
||||
<UButton
|
||||
|
|
@ -260,8 +277,12 @@
|
|||
>
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden border"
|
||||
:class="queueCardClass(item)"
|
||||
:ui="{ body: 'flex flex-1 flex-col gap-4 p-4', header: 'p-4 pb-3', root: 'bg-default' }"
|
||||
:ui="{
|
||||
body: 'flex flex-1 flex-col gap-4 p-4',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
header: 'p-4 pb-3',
|
||||
root: 'bg-default',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
|
||||
|
|
@ -387,87 +408,114 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-sm *:min-w-32 *:flex-1">
|
||||
<div
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-default"
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-default transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'status')"
|
||||
>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon
|
||||
:name="setIcon(item)"
|
||||
:class="[setIconColor(item), setIconAnimation(item), 'size-4 shrink-0']"
|
||||
/>
|
||||
<span>{{ setStatus(item) }}</span>
|
||||
<span :class="['min-w-0 text-center', expandClass(item._id, 'status')]">
|
||||
{{ setStatus(item) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-default"
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-default transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'preset')"
|
||||
>
|
||||
<UTooltip :text="`Preset: ${item.preset}`">
|
||||
<span class="block min-w-0 truncate">{{ item.preset }}</span>
|
||||
</UTooltip>
|
||||
</div>
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-4 shrink-0 text-toned" />
|
||||
<span :class="['min-w-0 text-center', expandClass(item._id, 'preset')]">
|
||||
{{ item.preset }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-toned"
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-toned transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'datetime')"
|
||||
>
|
||||
<UTooltip :text="moment(item.datetime).format('MMMM Do YYYY, h:mm:ss a')">
|
||||
<span :data-datetime="item.datetime" v-rtime="item.datetime" />
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon name="i-lucide-clock-3" class="size-4 shrink-0 text-toned" />
|
||||
<span
|
||||
:class="['min-w-0 text-center', expandClass(item._id, 'datetime')]"
|
||||
:data-datetime="item.datetime"
|
||||
v-rtime="item.datetime"
|
||||
/>
|
||||
</span>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
<button
|
||||
v-if="item.downloaded_bytes"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-toned"
|
||||
type="button"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2 text-toned transition hover:border-primary hover:text-default"
|
||||
@click="toggleExpand(item._id, 'size')"
|
||||
>
|
||||
{{ formatBytes(item.downloaded_bytes) }}
|
||||
</div>
|
||||
<span class="inline-flex w-full items-center justify-center gap-2">
|
||||
<UIcon name="i-lucide-hard-drive" class="size-4 shrink-0 text-toned" />
|
||||
<span :class="['min-w-0 text-center', expandClass(item._id, 'size')]">
|
||||
{{ formatBytes(item.downloaded_bytes) }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
icon="i-lucide-circle-off"
|
||||
class="w-full justify-center"
|
||||
@click="() => void confirmCancel(item)"
|
||||
>
|
||||
{{ item.is_live ? 'Stop' : 'Cancel' }}
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="canStartItem(item)"
|
||||
color="success"
|
||||
variant="outline"
|
||||
icon="i-lucide-circle-play"
|
||||
class="w-full justify-center"
|
||||
@click="() => void startItem(item)"
|
||||
>
|
||||
Start
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="canPauseItem(item)"
|
||||
color="warning"
|
||||
variant="outline"
|
||||
icon="i-lucide-pause"
|
||||
class="w-full justify-center"
|
||||
@click="() => void pauseItem(item)"
|
||||
>
|
||||
Pause
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu :items="itemActionGroups(item)" :modal="false" class="w-full">
|
||||
<template #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-settings-2"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
icon="i-lucide-circle-off"
|
||||
class="w-full justify-center"
|
||||
@click="() => void confirmCancel(item)"
|
||||
>
|
||||
Actions
|
||||
{{ item.is_live ? 'Stop Stream' : 'Cancel' }}
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-if="canStartItem(item)"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-circle-play"
|
||||
class="w-full justify-center"
|
||||
@click="() => void startItem(item)"
|
||||
>
|
||||
Start
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="canPauseItem(item)"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pause"
|
||||
class="w-full justify-center"
|
||||
@click="() => void pauseItem(item)"
|
||||
>
|
||||
Pause
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu :items="itemActionGroups(item)" :modal="false" class="w-full">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-settings-2"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
class="w-full justify-center"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</LateLoader>
|
||||
</div>
|
||||
|
|
@ -540,6 +588,7 @@
|
|||
<script setup lang="ts">
|
||||
import moment from 'moment';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import type { StoreItem } from '~/types/store';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
import { deepIncludes, getPath, getImage } from '~/utils';
|
||||
|
|
@ -560,9 +609,11 @@ const stateStore = useStateStore();
|
|||
const socket = useSocketStore();
|
||||
const box = useConfirm();
|
||||
const toast = useNotification();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
||||
const hideThumbnail = useStorage<boolean>('hideThumbnailQueue', false);
|
||||
const display_style = useStorage<'grid' | 'list'>('display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
const bg_enable = useStorage<boolean>('random_bg', true);
|
||||
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95);
|
||||
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1');
|
||||
|
|
@ -576,6 +627,10 @@ const autoRefreshInterval = ref<NodeJS.Timeout | null>(null);
|
|||
const autoRefreshEnabled = useStorage<boolean>('queue_auto_refresh', true);
|
||||
const autoRefreshDelay = useStorage<number>('queue_auto_refresh_delay', 10000);
|
||||
|
||||
const contentStyle = computed<'grid' | 'list'>(() =>
|
||||
isMobile.value ? 'grid' : display_style.value,
|
||||
);
|
||||
|
||||
const showThumbnails = computed(() => Boolean(props.thumbnails) && !hideThumbnail.value);
|
||||
|
||||
const refreshQueue = async () => {
|
||||
|
|
@ -706,22 +761,6 @@ const getListImage = (item: StoreItem): string =>
|
|||
|
||||
const getGridImage = (item: StoreItem): string => getImage(config.app.download_path, item) || '';
|
||||
|
||||
const queueCardClass = (item: StoreItem): string => {
|
||||
if ('postprocessing' === item.status) {
|
||||
return 'border-info';
|
||||
}
|
||||
|
||||
if (['downloading', 'started'].includes(item.status || '')) {
|
||||
return 'border-success';
|
||||
}
|
||||
|
||||
if (!item.auto_start || (null === item.status && true === config.paused)) {
|
||||
return 'border-warning';
|
||||
}
|
||||
|
||||
return 'border-default';
|
||||
};
|
||||
|
||||
const canStartItem = (item: StoreItem): boolean => !item.auto_start && !item.status;
|
||||
|
||||
const canPauseItem = (item: StoreItem): boolean => item.auto_start && !item.status;
|
||||
|
|
@ -739,7 +778,6 @@ const bulkActionGroups = computed(() => {
|
|||
groups[0]?.push({
|
||||
label: 'Start',
|
||||
icon: 'i-lucide-circle-play',
|
||||
color: 'success',
|
||||
disabled: !hasSelected.value,
|
||||
onSelect: () => void startItems(),
|
||||
});
|
||||
|
|
@ -749,7 +787,6 @@ const bulkActionGroups = computed(() => {
|
|||
groups[0]?.push({
|
||||
label: 'Pause',
|
||||
icon: 'i-lucide-pause',
|
||||
color: 'warning',
|
||||
disabled: !hasSelected.value,
|
||||
onSelect: () => void pauseSelected(),
|
||||
});
|
||||
|
|
@ -758,7 +795,6 @@ const bulkActionGroups = computed(() => {
|
|||
groups[0]?.push({
|
||||
label: selectedLiveOnly ? 'Stop' : 'Cancel',
|
||||
icon: 'i-lucide-circle-off',
|
||||
color: 'warning',
|
||||
disabled: !hasSelected.value,
|
||||
onSelect: () => void cancelSelected(),
|
||||
});
|
||||
|
|
@ -774,7 +810,6 @@ const itemActionGroups = (item: StoreItem) => {
|
|||
primaryActions.push({
|
||||
label: 'Play video',
|
||||
icon: 'i-lucide-play',
|
||||
color: 'error',
|
||||
onSelect: () => {
|
||||
embed_url.value = getEmbedable(item.url) as string;
|
||||
},
|
||||
|
|
@ -784,7 +819,6 @@ const itemActionGroups = (item: StoreItem) => {
|
|||
primaryActions.push({
|
||||
label: item.is_live ? 'Stop Stream' : 'Cancel Download',
|
||||
icon: 'i-lucide-circle-off',
|
||||
color: 'warning',
|
||||
onSelect: () => void confirmCancel(item),
|
||||
});
|
||||
|
||||
|
|
@ -792,7 +826,6 @@ const itemActionGroups = (item: StoreItem) => {
|
|||
primaryActions.push({
|
||||
label: 'Start Download',
|
||||
icon: 'i-lucide-circle-play',
|
||||
color: 'success',
|
||||
onSelect: () => void startItem(item),
|
||||
});
|
||||
}
|
||||
|
|
@ -801,7 +834,6 @@ const itemActionGroups = (item: StoreItem) => {
|
|||
primaryActions.push({
|
||||
label: 'Pause Download',
|
||||
icon: 'i-lucide-pause',
|
||||
color: 'warning',
|
||||
onSelect: () => void pauseItem(item),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,7 +367,9 @@
|
|||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-215 table-fixed w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-left [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-40">
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<UIcon name="i-lucide-key" class="size-3.5 text-toned" />
|
||||
|
|
@ -409,7 +411,7 @@
|
|||
<tr
|
||||
v-for="(field, index) in guiState.fields"
|
||||
:key="`${index}-${field.key}`"
|
||||
class="align-top"
|
||||
class="align-top [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="px-3 py-3">
|
||||
<UInput
|
||||
|
|
|
|||
|
|
@ -121,7 +121,9 @@
|
|||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-180 w-full table-auto text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-left [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-80 whitespace-nowrap">Flags</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
|
@ -130,7 +132,7 @@
|
|||
<tr
|
||||
v-for="opt in group.items"
|
||||
:key="opt.flags.join('|')"
|
||||
class="align-top hover:bg-muted/20"
|
||||
class="align-top transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="w-80 px-3 py-3 align-top">
|
||||
<div class="flex items-start gap-2">
|
||||
|
|
@ -185,7 +187,9 @@
|
|||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-215 w-full table-auto text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-left [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-80 whitespace-nowrap">Flags</th>
|
||||
<th class="w-36 whitespace-nowrap">Group</th>
|
||||
<th>Description</th>
|
||||
|
|
@ -195,7 +199,7 @@
|
|||
<tr
|
||||
v-for="opt in visible"
|
||||
:key="opt.flags.join('|')"
|
||||
class="align-top hover:bg-muted/20"
|
||||
class="align-top transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="w-80 px-3 py-3 align-top">
|
||||
<div class="flex items-start gap-2">
|
||||
|
|
|
|||
|
|
@ -552,11 +552,33 @@ const navigationUi = (collapsed: boolean) => ({
|
|||
linkLabel: collapsed ? 'hidden' : 'truncate',
|
||||
});
|
||||
|
||||
const navEntryPath = (item: NavEntry): string => (item.to ? item.to.split(/[?#]/)[0] || '/' : '');
|
||||
|
||||
const isNavEntryActive = (item: NavEntry): boolean => {
|
||||
if (item.id === 'downloads') {
|
||||
return route.path === '/' && route.hash !== '#history';
|
||||
}
|
||||
|
||||
if (item.id === 'history') {
|
||||
return route.path === '/' && route.hash === '#history';
|
||||
}
|
||||
|
||||
const path = navEntryPath(item);
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return path === '/'
|
||||
? route.path === '/'
|
||||
: route.path === path || route.path.startsWith(`${path}/`);
|
||||
};
|
||||
|
||||
const makeNavigationItem = (item: NavEntry): NavigationMenuItem => ({
|
||||
label: item.label,
|
||||
icon: item.icon,
|
||||
to: item.to,
|
||||
value: item.id,
|
||||
active: isNavEntryActive(item),
|
||||
});
|
||||
|
||||
const docsNavigationEntries = getDocsNavigationEntries();
|
||||
|
|
@ -565,10 +587,17 @@ const allNavItems = computed<NavEntry[]>(() => [
|
|||
{
|
||||
id: 'downloads',
|
||||
label: 'Downloads',
|
||||
description: 'Queued and completed downloads list.',
|
||||
description: 'Manage queued, active, and completed downloads in one workspace.',
|
||||
icon: 'i-lucide-download',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
id: 'history',
|
||||
label: 'History',
|
||||
description: 'Jump to the history section on the downloads page.',
|
||||
icon: 'i-lucide-history',
|
||||
to: '/#history',
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
label: 'Files',
|
||||
|
|
@ -674,11 +703,14 @@ const allNavItems = computed<NavEntry[]>(() => [
|
|||
]);
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
const path = route.path;
|
||||
if (route.path === '/') {
|
||||
return 'Downloads';
|
||||
}
|
||||
|
||||
const flat = allNavItems.value.flatMap((item) => [item, ...(item.children || [])]);
|
||||
const match = flat
|
||||
.filter((item) => item.to && (item.to === '/' ? path === '/' : path.startsWith(item.to)))
|
||||
.sort((left, right) => (right.to?.length || 0) - (left.to?.length || 0))[0];
|
||||
.filter((item) => isNavEntryActive(item))
|
||||
.sort((left, right) => navEntryPath(right).length - navEntryPath(left).length)[0];
|
||||
return match?.label || 'YTPTube';
|
||||
});
|
||||
|
||||
|
|
@ -720,8 +752,8 @@ const sidebarSections = computed<Array<SidebarSection>>(() => {
|
|||
id: 'downloads',
|
||||
label: 'Downloads',
|
||||
items:
|
||||
topLevelItems(['downloads', 'files']).length > 0
|
||||
? [topLevelItems(['downloads', 'files'])]
|
||||
topLevelItems(['downloads', 'history', 'files']).length > 0
|
||||
? [topLevelItems(['downloads', 'history', 'files'])]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -732,10 +764,10 @@ const sidebarSections = computed<Array<SidebarSection>>(() => {
|
|||
{
|
||||
id: 'configuration',
|
||||
label: 'Configuration',
|
||||
items: [
|
||||
topLevelItems(['presets', 'custom-fields']),
|
||||
topLevelItems(['conditions', 'notifications']),
|
||||
].filter((group) => group.length > 0),
|
||||
items:
|
||||
topLevelItems(['presets', 'custom-fields', 'conditions', 'notifications']).length > 0
|
||||
? [topLevelItems(['presets', 'custom-fields', 'conditions', 'notifications'])]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
icon="i-lucide-folder-plus"
|
||||
@click="() => void handleCreateDirectory()"
|
||||
>
|
||||
<span v-if="!isMobile">New Folder</span>
|
||||
<span>New Folder</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -79,9 +79,10 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
|
||||
class="hidden sm:inline-flex"
|
||||
@click="toggleDisplayStyle"
|
||||
>
|
||||
<span v-if="!isMobile">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
|
||||
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu v-if="hasItems" :items="sortGroups" :modal="false">
|
||||
|
|
@ -92,7 +93,7 @@
|
|||
icon="i-lucide-arrow-up-down"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
>
|
||||
<span v-if="!isMobile">Sort</span>
|
||||
<span>Sort</span>
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
|
||||
|
|
@ -105,7 +106,7 @@
|
|||
:disabled="isLoading"
|
||||
@click="() => void reloadContent(browserPath)"
|
||||
>
|
||||
<span v-if="!isMobile">Reload</span>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -119,9 +120,9 @@
|
|||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:icon="masterSelectAll ? 'i-lucide-square' : 'i-lucide-check'"
|
||||
:icon="masterSelectAll ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
:disabled="isLoading || filteredItems.length < 1"
|
||||
@click="masterSelectAll = !masterSelectAll"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
{{ masterSelectAll ? 'Unselect' : 'Select' }}
|
||||
</UButton>
|
||||
|
|
@ -130,27 +131,17 @@
|
|||
{{ selectedElms.length }}
|
||||
</UBadge>
|
||||
|
||||
<UButton
|
||||
color="info"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-arrow-right-left"
|
||||
:disabled="!hasSelected || isLoading"
|
||||
@click="() => void handleMoveSelected()"
|
||||
>
|
||||
Move
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-trash"
|
||||
:disabled="!hasSelected || isLoading"
|
||||
@click="() => void handleDeleteSelected()"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
<UDropdownMenu :items="bulkActionGroups" :modal="false">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-list"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
|
|
@ -167,14 +158,28 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
v-if="display_style === 'list' && hasItems"
|
||||
v-if="contentStyle === 'list' && hasItems"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-345 w-full text-sm">
|
||||
<table class="min-w-360 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<th v-if="controlEnabled" class="w-16">Select</th>
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th v-if="controlEnabled" class="w-16">
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer"
|
||||
:aria-label="masterSelectAll ? 'Unselect all items' : 'Select all items'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
<UIcon
|
||||
:name="masterSelectAll ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
<th :class="controlEnabled ? 'w-20' : 'w-24'">
|
||||
#
|
||||
<UIcon
|
||||
|
|
@ -207,12 +212,16 @@
|
|||
class="ml-1 inline-flex size-3.5"
|
||||
/>
|
||||
</th>
|
||||
<th v-if="controlEnabled" class="w-80 whitespace-nowrap">Actions</th>
|
||||
<th v-if="controlEnabled" class="w-96 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="item in filteredItems" :key="item.path" class="hover:bg-muted/20">
|
||||
<tr
|
||||
v-for="item in filteredItems"
|
||||
:key="item.path"
|
||||
class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td v-if="controlEnabled" class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
|
|
@ -245,19 +254,6 @@
|
|||
</a>
|
||||
</UTooltip>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-if="item.type === 'file'"
|
||||
color="info"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-download"
|
||||
class="shrink-0"
|
||||
external
|
||||
:href="downloadHref(item)"
|
||||
:download="downloadName(item)"
|
||||
square
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
|
@ -271,10 +267,23 @@
|
|||
</UTooltip>
|
||||
</td>
|
||||
|
||||
<td v-if="controlEnabled" class="w-80 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<td v-if="controlEnabled" class="w-96 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="warning"
|
||||
v-if="item.type === 'file'"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-download"
|
||||
class="shrink-0"
|
||||
external
|
||||
:href="downloadHref(item)"
|
||||
:download="downloadName(item)"
|
||||
>
|
||||
Download
|
||||
</UButton>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-pencil"
|
||||
|
|
@ -284,7 +293,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="info"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-arrow-right-left"
|
||||
|
|
@ -294,7 +303,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
|
|
@ -315,7 +324,11 @@
|
|||
v-for="item in filteredItems"
|
||||
:key="item.path"
|
||||
class="flex h-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
:ui="{
|
||||
header: 'p-4 pb-3',
|
||||
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
|
|
@ -340,18 +353,6 @@
|
|||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<UButton
|
||||
v-if="item.type === 'file'"
|
||||
color="info"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-download"
|
||||
external
|
||||
:href="downloadHref(item)"
|
||||
:download="downloadName(item)"
|
||||
square
|
||||
/>
|
||||
|
||||
<label v-if="controlEnabled" class="inline-flex cursor-pointer items-center px-1">
|
||||
<input
|
||||
v-model="selectedElms"
|
||||
|
|
@ -386,40 +387,56 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="controlEnabled" class="mt-auto flex flex-wrap gap-2 pt-1 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="() => void handleAction('rename', item)"
|
||||
>
|
||||
Rename
|
||||
</UButton>
|
||||
<template v-if="controlEnabled" #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
v-if="item.type === 'file'"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-download"
|
||||
external
|
||||
:href="downloadHref(item)"
|
||||
:download="downloadName(item)"
|
||||
class="w-full justify-center"
|
||||
>
|
||||
Download
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="info"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-arrow-right-left"
|
||||
class="w-full justify-center"
|
||||
@click="() => void handleAction('move', item)"
|
||||
>
|
||||
Move
|
||||
</UButton>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="() => void handleAction('rename', item)"
|
||||
>
|
||||
Rename
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void handleAction('delete', item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-arrow-right-left"
|
||||
class="w-full justify-center"
|
||||
@click="() => void handleAction('move', item)"
|
||||
>
|
||||
Move
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void handleAction('delete', item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
|
|
@ -521,9 +538,9 @@ const toast = useNotification();
|
|||
const config = useConfigStore();
|
||||
const dialog = useDialog();
|
||||
const browser = useBrowser();
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
|
||||
const display_style = useStorage<string>('browser_display_style', 'list');
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
const show_filter = ref(false);
|
||||
const localSearch = ref('');
|
||||
const searchInput = ref<HTMLInputElement | null>(null);
|
||||
|
|
@ -539,6 +556,9 @@ const sort_order = browser.sort_order;
|
|||
const filteredItems = browser.filteredItems;
|
||||
|
||||
const controlEnabled = computed(() => Boolean(config.app.browser_control_enabled));
|
||||
const contentStyle = computed<'list' | 'grid'>(() =>
|
||||
isMobile.value ? 'grid' : 'list' === display_style.value ? 'list' : 'grid',
|
||||
);
|
||||
const hasItems = computed(() => filteredItems.value.length > 0);
|
||||
const hasSelected = computed(() => selectedElms.value.length > 0);
|
||||
const displayedItemPaths = computed(() => filteredItems.value.map((item) => item.path));
|
||||
|
|
@ -567,6 +587,25 @@ const sortGroups = computed<DropdownMenuItem[][]>(() => [
|
|||
})),
|
||||
]);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
label: 'Move Selected',
|
||||
icon: 'i-lucide-arrow-right-left',
|
||||
color: 'primary',
|
||||
disabled: !hasSelected.value || isLoading.value,
|
||||
onSelect: () => void handleMoveSelected(),
|
||||
},
|
||||
{
|
||||
label: 'Delete Selected',
|
||||
icon: 'i-lucide-trash',
|
||||
color: 'error',
|
||||
disabled: !hasSelected.value || isLoading.value,
|
||||
onSelect: () => void handleDeleteSelected(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const initialPath = (() => {
|
||||
const slug = route.params.slug;
|
||||
if (Array.isArray(slug) && slug.length > 0) {
|
||||
|
|
@ -917,6 +956,10 @@ const toggleDisplayStyle = (): void => {
|
|||
display_style.value = display_style.value === 'list' ? 'grid' : 'list';
|
||||
};
|
||||
|
||||
const toggleMasterSelection = (): void => {
|
||||
masterSelectAll.value = !masterSelectAll.value;
|
||||
};
|
||||
|
||||
const handleCreateDirectory = async (): Promise<void> => {
|
||||
if (!controlEnabled.value) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilter = !toggleFilter"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<USwitch
|
||||
|
|
@ -79,17 +79,25 @@
|
|||
Installed
|
||||
</UBadge>
|
||||
</div>
|
||||
|
||||
<p v-if="log.date" class="text-xs text-toned">
|
||||
<UTooltip :text="`Release Date: ${log.date}`">
|
||||
<span>{{ moment(log.date).fromNow() }}</span>
|
||||
</UTooltip>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UBadge color="neutral" variant="soft" size="sm">
|
||||
{{ log.commits?.length || 0 }} commits
|
||||
</UBadge>
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 text-xs text-toned">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list" class="size-3.5" />
|
||||
<span>{{ log.commits?.length || 0 }} commits</span>
|
||||
</span>
|
||||
|
||||
<UTooltip v-if="log.date" :text="`Release Date: ${log.date}`">
|
||||
<span
|
||||
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-calendar-days" class="size-3.5" />
|
||||
<span>{{ moment(log.date).fromNow() }}</span>
|
||||
</span>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 border-t border-default pt-4">
|
||||
|
|
@ -98,35 +106,48 @@
|
|||
:key="commit.sha"
|
||||
class="flex flex-col gap-2 rounded-md border border-default bg-muted/20 px-3 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<p class="min-w-0 flex-1 text-sm text-default">
|
||||
<div class="min-w-0">
|
||||
<NuxtLink
|
||||
:to="`${REPO}/commit/${commit.full_sha}`"
|
||||
target="_blank"
|
||||
class="block min-w-0 text-sm text-default hover:underline"
|
||||
>
|
||||
<span class="font-semibold text-highlighted">
|
||||
{{ ucFirst(commit.message).replace(/\.$/, '') }}.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<NuxtLink
|
||||
:to="`${REPO}/commit/${commit.full_sha}`"
|
||||
target="_blank"
|
||||
class="text-xs font-medium text-primary hover:underline"
|
||||
>
|
||||
{{ commit.sha }}
|
||||
</NuxtLink>
|
||||
|
||||
<UIcon
|
||||
v-if="commit.full_sha === app_sha"
|
||||
name="i-lucide-check"
|
||||
class="size-4 text-success"
|
||||
/>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-toned">
|
||||
<span>{{ commit.author }}</span>
|
||||
<UTooltip :text="`SHA: ${commit.full_sha} - Date: ${commit.date}`">
|
||||
<span>{{ moment(commit.date).fromNow() }}</span>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-user" class="size-3.5" />
|
||||
{{ commit.author }}
|
||||
</span>
|
||||
<UTooltip :text="`Date: ${commit.date}`">
|
||||
<span
|
||||
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-clock-3" class="size-3.5" />
|
||||
{{ moment(commit.date).fromNow() }}
|
||||
</span>
|
||||
</UTooltip>
|
||||
<UTooltip :text="`SHA: ${commit.full_sha}`">
|
||||
<span
|
||||
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1 font-medium"
|
||||
>
|
||||
<UIcon name="i-lucide-git-commit-horizontal" class="size-3.5" />
|
||||
{{ commit.sha }}
|
||||
</span>
|
||||
</UTooltip>
|
||||
<span
|
||||
v-if="commit.full_sha === app_sha"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 font-medium"
|
||||
>
|
||||
<UIcon name="i-lucide-check" class="size-3.5 text-success" />
|
||||
<span>Installed</span>
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
@ -170,7 +191,6 @@ import { request, ucFirst, uri } from '~/utils';
|
|||
|
||||
const toast = useNotification();
|
||||
const config = useConfigStore();
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
|
||||
useHead({ title: 'CHANGELOG' });
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilterPanel"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
icon="i-lucide-plus"
|
||||
@click="openCreate"
|
||||
>
|
||||
<span v-if="!isMobile">New Condition</span>
|
||||
<span>New Condition</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -56,9 +56,10 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
|
||||
class="hidden sm:inline-flex"
|
||||
@click="toggleDisplayStyle"
|
||||
>
|
||||
<span v-if="!isMobile">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
|
||||
<span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span v-if="!isMobile">Reload</span>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -120,13 +121,15 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayStyle === 'list' && filteredItems.length > 0"
|
||||
v-if="contentStyle === 'list' && filteredItems.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-225 w-full text-sm">
|
||||
<table class="min-w-235 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
|
|
@ -136,12 +139,16 @@
|
|||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Condition</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
<th class="w-48 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="cond in filteredItems" :key="cond.id" class="hover:bg-muted/20">
|
||||
<tr
|
||||
v-for="cond in filteredItems"
|
||||
:key="cond.id"
|
||||
class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
|
|
@ -183,7 +190,7 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<td class="w-48 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
|
|
@ -192,7 +199,7 @@
|
|||
icon="i-lucide-file-up"
|
||||
@click="exportItem(cond)"
|
||||
>
|
||||
<span v-if="!isMobile">Export</span>
|
||||
<span class="hidden sm:inline">Export</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -202,7 +209,7 @@
|
|||
icon="i-lucide-pencil"
|
||||
@click="editItem(cond)"
|
||||
>
|
||||
<span v-if="!isMobile">Edit</span>
|
||||
<span class="hidden sm:inline">Edit</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -212,7 +219,7 @@
|
|||
icon="i-lucide-trash"
|
||||
@click="() => void deleteItem(cond)"
|
||||
>
|
||||
<span v-if="!isMobile">Delete</span>
|
||||
<span class="hidden sm:inline">Delete</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -226,7 +233,11 @@
|
|||
<div v-for="cond in filteredItems" :key="cond.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
:ui="{
|
||||
header: 'p-4 pb-3',
|
||||
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
|
|
@ -251,7 +262,7 @@
|
|||
square
|
||||
@click="exportItem(cond)"
|
||||
>
|
||||
<span class="hidden sm:inline">Export Condition</span>
|
||||
<span>Export Condition</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
|
|
@ -366,27 +377,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(cond)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
<template #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(cond)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(cond)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(cond)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -490,8 +503,8 @@ import { cleanObject, copyText, encode } from '~/utils';
|
|||
type ConditionItemWithUI = Condition & { raw?: boolean };
|
||||
|
||||
const box = useConfirm();
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const displayStyle = useStorage<'list' | 'grid'>('conditions_display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const conditions = useConditions();
|
||||
const route = useRoute();
|
||||
|
|
@ -534,6 +547,9 @@ const allSelected = computed(
|
|||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
const contentStyle = computed<'list' | 'grid'>(() =>
|
||||
isMobile.value ? 'grid' : displayStyle.value,
|
||||
);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@
|
|||
<tr
|
||||
v-for="(cmd, index) in historyEntries"
|
||||
:key="`${index}-${cmd}`"
|
||||
class="hover:bg-muted/20"
|
||||
class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="px-3 py-3 align-middle">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilterPanel"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
icon="i-lucide-plus"
|
||||
@click="openCreate"
|
||||
>
|
||||
<span v-if="!isMobile">New Field</span>
|
||||
<span>New Field</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -56,9 +56,10 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
|
||||
class="hidden sm:inline-flex"
|
||||
@click="toggleDisplayStyle"
|
||||
>
|
||||
<span v-if="!isMobile">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
|
||||
<span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span v-if="!isMobile">Reload</span>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -120,13 +121,15 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayStyle === 'list' && filteredItems.length > 0"
|
||||
v-if="contentStyle === 'list' && filteredItems.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-225 w-full text-sm">
|
||||
<table class="min-w-235 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
|
|
@ -136,12 +139,16 @@
|
|||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Field</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
<th class="w-48 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="field in filteredItems" :key="field.id" class="hover:bg-muted/20">
|
||||
<tr
|
||||
v-for="field in filteredItems"
|
||||
:key="field.id"
|
||||
class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
|
|
@ -182,7 +189,7 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<td class="w-48 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
|
|
@ -191,7 +198,7 @@
|
|||
icon="i-lucide-file-up"
|
||||
@click="exportItem(field)"
|
||||
>
|
||||
<span v-if="!isMobile">Export</span>
|
||||
Export
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -201,7 +208,7 @@
|
|||
icon="i-lucide-pencil"
|
||||
@click="editItem(field)"
|
||||
>
|
||||
<span v-if="!isMobile">Edit</span>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -211,7 +218,7 @@
|
|||
icon="i-lucide-trash"
|
||||
@click="() => void deleteItem(field)"
|
||||
>
|
||||
<span v-if="!isMobile">Delete</span>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -225,7 +232,11 @@
|
|||
<div v-for="field in filteredItems" :key="field.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
:ui="{
|
||||
header: 'p-4 pb-3',
|
||||
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
|
|
@ -250,7 +261,7 @@
|
|||
square
|
||||
@click="exportItem(field)"
|
||||
>
|
||||
<span class="hidden sm:inline">Export</span>
|
||||
<span>Export Field</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
|
|
@ -318,27 +329,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(field)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
<template #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(field)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(field)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(field)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -411,9 +424,9 @@ import type { APIResponse } from '~/types/responses';
|
|||
import { copyText, encode } from '~/utils';
|
||||
|
||||
const box = useConfirm();
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const displayStyle = useStorage<'list' | 'grid'>('dl_fields_display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
const dlFields = useDlFields();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
|
@ -453,6 +466,9 @@ const allSelected = computed(
|
|||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
const contentStyle = computed<'list' | 'grid'>(() =>
|
||||
isMobile.value ? 'grid' : displayStyle.value,
|
||||
);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="space-y-6">
|
||||
<UPageHeader
|
||||
title="Downloads"
|
||||
description="Queued and completed downloads are displayed here."
|
||||
description="Manage queued, active, and completed downloads in one workspace."
|
||||
:ui="{
|
||||
root: 'border-b border-default py-4',
|
||||
headline: 'hidden',
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilter = !toggleFilter"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
icon="i-lucide-pause"
|
||||
@click="() => void pauseDownload()"
|
||||
>
|
||||
<span v-if="!isMobile">Pause</span>
|
||||
<span>Pause</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
icon="i-lucide-play"
|
||||
@click="() => void resumeDownload()"
|
||||
>
|
||||
<span v-if="!isMobile">Resume</span>
|
||||
<span>Resume</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
icon="i-lucide-plus"
|
||||
@click="config.showForm = !config.showForm"
|
||||
>
|
||||
<span v-if="!isMobile">Add</span>
|
||||
<span>Add</span>
|
||||
</UButton>
|
||||
</template>
|
||||
|
||||
|
|
@ -81,16 +81,17 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
|
||||
class="hidden sm:inline-flex"
|
||||
@click="changeDisplay"
|
||||
>
|
||||
<span v-if="!isMobile">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
|
||||
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
|
||||
</UButton>
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
<div v-if="config.showForm" class="page-form-wrap">
|
||||
<div v-if="config.showForm" ref="formSection" class="page-form-wrap scroll-mt-24">
|
||||
<NewDownload
|
||||
:item="item_form"
|
||||
@clear_form="item_form = {}"
|
||||
|
|
@ -100,63 +101,68 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<UPageCard
|
||||
variant="outline"
|
||||
:ui="{
|
||||
root: 'w-full min-w-0 max-w-full bg-default',
|
||||
container: 'w-full min-w-0 max-w-full p-0 sm:p-0',
|
||||
wrapper: 'w-full min-w-0 items-stretch',
|
||||
body: 'w-full min-w-0 max-w-full p-0',
|
||||
}"
|
||||
>
|
||||
<template #body>
|
||||
<UTabs
|
||||
v-model="activeTab"
|
||||
:items="tabItems"
|
||||
value-key="value"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
size="md"
|
||||
:content="false"
|
||||
:ui="{
|
||||
root: 'flex-col gap-4',
|
||||
list: 'border-b border-default px-2 sm:px-4',
|
||||
trigger: 'justify-start rounded-none px-3 py-3 text-sm font-medium',
|
||||
trailingBadge: 'ml-1',
|
||||
}"
|
||||
@update:model-value="(value) => void setActiveTab(value as TabType)"
|
||||
/>
|
||||
<UEmpty
|
||||
v-if="!hasDownloadsContent"
|
||||
icon="i-lucide-inbox"
|
||||
title="No downloads yet"
|
||||
description="Add a URL to get started."
|
||||
class="rounded-lg border border-dashed border-default bg-muted/10 py-10"
|
||||
/>
|
||||
|
||||
<div class="w-full min-w-0 max-w-full px-0 pt-2">
|
||||
<div v-show="'queue' === activeTab" class="w-full min-w-0 max-w-full">
|
||||
<Queue
|
||||
:thumbnails="show_thumbnail"
|
||||
:query="query"
|
||||
@getInfo="
|
||||
(url: string, preset: string = '', cli: string = '') =>
|
||||
view_info(url, false, preset, cli)
|
||||
"
|
||||
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)"
|
||||
@clear_search="query = ''"
|
||||
/>
|
||||
<div v-else class="space-y-8">
|
||||
<section id="queue" class="scroll-mt-24 space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-default pb-3">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-download" class="size-4 text-toned" />
|
||||
<h2>Queue</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-toned">Active and queued downloads.</p>
|
||||
</div>
|
||||
|
||||
<div v-show="'history' === activeTab" class="w-full min-w-0 max-w-full">
|
||||
<History
|
||||
:query="query"
|
||||
:thumbnails="show_thumbnail"
|
||||
@getInfo="
|
||||
(url: string, preset: string = '', cli: string = '') =>
|
||||
view_info(url, false, preset, cli)
|
||||
"
|
||||
@add_new="(item: item_request) => toNewDownload(item)"
|
||||
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)"
|
||||
@clear_search="query = ''"
|
||||
/>
|
||||
</div>
|
||||
<UBadge color="info" variant="soft" size="sm">{{ queueCount }}</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
</UPageCard>
|
||||
|
||||
<Queue
|
||||
:thumbnails="show_thumbnail"
|
||||
:query="query"
|
||||
@getInfo="
|
||||
(url: string, preset: string = '', cli: string = '') =>
|
||||
view_info(url, false, preset, cli)
|
||||
"
|
||||
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)"
|
||||
@clear_search="query = ''"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section id="history" class="scroll-mt-24 space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-default pb-3">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-history" class="size-4 text-toned" />
|
||||
<h2>History</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-toned">Completed, skipped, and failed downloads.</p>
|
||||
</div>
|
||||
|
||||
<UBadge color="neutral" variant="soft" size="sm">{{ historyCount }}</UBadge>
|
||||
</div>
|
||||
|
||||
<History
|
||||
:query="query"
|
||||
:thumbnails="show_thumbnail"
|
||||
@getInfo="
|
||||
(url: string, preset: string = '', cli: string = '') =>
|
||||
view_info(url, false, preset, cli)
|
||||
"
|
||||
@add_new="(item: item_request) => void toNewDownload(item)"
|
||||
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)"
|
||||
@clear_search="query = ''"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<GetInfo
|
||||
v-if="info_view.url"
|
||||
|
|
@ -178,15 +184,14 @@ import type { StoreItem } from '~/types/store';
|
|||
const config = useConfigStore();
|
||||
const stateStore = useStateStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { confirmDialog } = useDialog();
|
||||
|
||||
const bg_enable = useStorage<boolean>('random_bg', true);
|
||||
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95);
|
||||
const display_style = useStorage<string>('display_style', 'grid');
|
||||
const show_thumbnail = useStorage<boolean>('show_thumbnail', true);
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
|
||||
const formSection = ref<HTMLElement | null>(null);
|
||||
const info_view = ref({
|
||||
url: '',
|
||||
preset: '',
|
||||
|
|
@ -197,33 +202,7 @@ const item_form = ref<item_request | object>({});
|
|||
const query = ref('');
|
||||
const toggleFilter = ref(false);
|
||||
|
||||
type TabType = 'queue' | 'history';
|
||||
|
||||
const activeTab = ref<TabType>('queue');
|
||||
|
||||
const getInitialTab = (): TabType => {
|
||||
const tabParam = route.query.tab as string;
|
||||
return tabParam === 'queue' || tabParam === 'history' ? tabParam : 'queue';
|
||||
};
|
||||
|
||||
const setActiveTab = async (tab: TabType): Promise<void> => {
|
||||
activeTab.value = tab;
|
||||
await router.push({ query: { ...route.query, tab }, replace: true });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
(newTab) => {
|
||||
if (!['queue', 'history'].includes(newTab as string)) {
|
||||
return;
|
||||
}
|
||||
activeTab.value = newTab as TabType;
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
const route = useRoute();
|
||||
|
||||
if (route.query?.simple !== undefined) {
|
||||
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false);
|
||||
simpleMode.value = ['true', '1', 'yes', 'on'].includes(route.query.simple as string);
|
||||
|
|
@ -233,31 +212,14 @@ onMounted(async () => {
|
|||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
activeTab.value = getInitialTab();
|
||||
useHead({ title: getTitle() });
|
||||
});
|
||||
|
||||
const queueCount = computed(() => stateStore.count('queue'));
|
||||
const historyCount = computed(() => stateStore.count('history'));
|
||||
|
||||
const tabItems = computed(() => [
|
||||
{
|
||||
label: 'Downloads',
|
||||
icon: 'i-lucide-download',
|
||||
value: 'queue',
|
||||
badge: { label: String(queueCount.value), color: 'info' as const, variant: 'soft' as const },
|
||||
},
|
||||
{
|
||||
label: 'History',
|
||||
icon: 'i-lucide-history',
|
||||
value: 'history',
|
||||
badge: {
|
||||
label: String(historyCount.value),
|
||||
color: 'primary' as const,
|
||||
variant: 'soft' as const,
|
||||
},
|
||||
},
|
||||
]);
|
||||
const hasDownloadsContent = computed(
|
||||
() => queueCount.value > 0 || historyCount.value > 0 || query.value.trim().length > 0,
|
||||
);
|
||||
|
||||
watch(toggleFilter, () => {
|
||||
if (!toggleFilter.value) {
|
||||
|
|
@ -356,6 +318,8 @@ const toNewDownload = async (item: item_request | Partial<StoreItem>) => {
|
|||
|
||||
await nextTick();
|
||||
config.showForm = true;
|
||||
await nextTick();
|
||||
formSection.value?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilterPanel"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
icon="i-lucide-plus"
|
||||
@click="openCreate"
|
||||
>
|
||||
<span v-if="!isMobile">New Notification</span>
|
||||
<span>New Notification</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
:disabled="sendingTest"
|
||||
@click="() => void sendTest()"
|
||||
>
|
||||
<span v-if="!isMobile">Send Test</span>
|
||||
<span>Send Test</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -69,9 +69,10 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
|
||||
class="hidden sm:inline-flex"
|
||||
@click="toggleDisplayStyle"
|
||||
>
|
||||
<span v-if="!isMobile">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
|
||||
<span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -84,7 +85,7 @@
|
|||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span v-if="!isMobile">Reload</span>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -133,13 +134,15 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayStyle === 'list' && filteredTargets.length > 0"
|
||||
v-if="contentStyle === 'list' && filteredTargets.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-225 w-full text-sm">
|
||||
<table class="min-w-235 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
|
|
@ -149,12 +152,16 @@
|
|||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Targets</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
<th class="w-48 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="item in filteredTargets" :key="item.id" class="hover:bg-muted/20">
|
||||
<tr
|
||||
v-for="item in filteredTargets"
|
||||
:key="item.id"
|
||||
class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
|
|
@ -220,7 +227,7 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<td class="w-48 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
|
|
@ -229,7 +236,7 @@
|
|||
icon="i-lucide-file-up"
|
||||
@click="exportItem(item)"
|
||||
>
|
||||
<span v-if="!isMobile">Export</span>
|
||||
<span class="hidden sm:inline">Export</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -239,7 +246,7 @@
|
|||
icon="i-lucide-pencil"
|
||||
@click="editItem(item)"
|
||||
>
|
||||
<span v-if="!isMobile">Edit</span>
|
||||
<span class="hidden sm:inline">Edit</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -249,7 +256,7 @@
|
|||
icon="i-lucide-trash"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
<span v-if="!isMobile">Delete</span>
|
||||
<span class="hidden sm:inline">Delete</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -263,7 +270,11 @@
|
|||
<div v-for="item in filteredTargets" :key="item.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
:ui="{
|
||||
header: 'p-4 pb-3',
|
||||
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
|
|
@ -291,7 +302,7 @@
|
|||
square
|
||||
@click="exportItem(item)"
|
||||
>
|
||||
<span class="hidden sm:inline">Export Target</span>
|
||||
<span>Export Target</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
|
|
@ -416,27 +427,29 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(item)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
<template #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(item)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -544,12 +557,12 @@ import type { notification } from '~/types/notification';
|
|||
const toast = useNotification();
|
||||
const box = useConfirm();
|
||||
const { confirmDialog } = useDialog();
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const displayStyleState = useStorage<'list' | 'grid' | 'cards'>(
|
||||
'notification_display_style',
|
||||
'cards',
|
||||
);
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
|
||||
const notificationsStore = useNotifications();
|
||||
const notifications = notificationsStore.notifications;
|
||||
|
|
@ -574,6 +587,9 @@ const massDelete = ref(false);
|
|||
const displayStyle = computed<'list' | 'grid'>(() =>
|
||||
displayStyleState.value === 'list' ? 'list' : 'grid',
|
||||
);
|
||||
const contentStyle = computed<'list' | 'grid'>(() =>
|
||||
isMobile.value ? 'grid' : displayStyle.value,
|
||||
);
|
||||
|
||||
const modalTitle = computed(() =>
|
||||
targetRef.value ? `Edit - ${target.value.name}` : 'Add new notification target',
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilterPanel"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
icon="i-lucide-plus"
|
||||
@click="editor.openCreate()"
|
||||
>
|
||||
<span v-if="!isMobile">New Preset</span>
|
||||
<span>New Preset</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -56,9 +56,10 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
|
||||
class="hidden sm:inline-flex"
|
||||
@click="toggleDisplayStyle"
|
||||
>
|
||||
<span v-if="!isMobile">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
|
||||
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span v-if="!isMobile">Reload</span>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -112,13 +113,15 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="display_style === 'list' && filteredPresets.length > 0"
|
||||
v-if="contentStyle === 'list' && filteredPresets.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-190 w-full text-sm">
|
||||
<table class="min-w-200 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
|
|
@ -128,12 +131,16 @@
|
|||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Preset</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
<th class="w-48 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="item in filteredPresets" :key="item.id" class="hover:bg-muted/20">
|
||||
<tr
|
||||
v-for="item in filteredPresets"
|
||||
:key="item.id"
|
||||
class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
|
|
@ -160,7 +167,7 @@
|
|||
class="size-3.5"
|
||||
:class="item.cookies ? 'text-success' : ''"
|
||||
/>
|
||||
<span>{{ item.cookies ? 'Has cookies' : 'No cookies' }}</span>
|
||||
<span>Cookies: {{ item.cookies ? 'Configured' : 'Not set' }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
|
|
@ -174,7 +181,7 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<td class="w-48 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
|
|
@ -183,7 +190,7 @@
|
|||
icon="i-lucide-file-up"
|
||||
@click="exportItem(item)"
|
||||
>
|
||||
<span v-if="!isMobile">Export</span>
|
||||
<span class="hidden sm:inline">Export</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -193,7 +200,7 @@
|
|||
icon="i-lucide-pencil"
|
||||
@click="editor.openEdit(item)"
|
||||
>
|
||||
<span v-if="!isMobile">Edit</span>
|
||||
<span class="hidden sm:inline">Edit</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -203,7 +210,7 @@
|
|||
icon="i-lucide-trash"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
<span v-if="!isMobile">Delete</span>
|
||||
<span class="hidden sm:inline">Delete</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -217,7 +224,11 @@
|
|||
<div v-for="item in filteredPresets" :key="item.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
:ui="{
|
||||
header: 'p-4 pb-3',
|
||||
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
|
|
@ -244,7 +255,7 @@
|
|||
square
|
||||
@click="exportItem(item)"
|
||||
>
|
||||
<span class="hidden sm:inline">Export Preset</span>
|
||||
<span>Export Preset</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
|
|
@ -348,27 +359,29 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editor.openEdit(item)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
<template #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editor.openEdit(item)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -461,7 +474,7 @@ const { confirmDialog } = useDialog();
|
|||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
||||
const display_style = useStorage<string>('preset_display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
|
||||
const query = ref('');
|
||||
const showFilter = ref(false);
|
||||
|
|
@ -496,6 +509,9 @@ const allSelected = computed(
|
|||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
const contentStyle = computed<'list' | 'grid'>(() =>
|
||||
isMobile.value ? 'grid' : 'list' === display_style.value ? 'list' : 'grid',
|
||||
);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilterPanel"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
icon="i-lucide-search"
|
||||
@click="inspect = true"
|
||||
>
|
||||
<span v-if="!isMobile">Inspect</span>
|
||||
<span>Inspect</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
icon="i-lucide-plus"
|
||||
@click="openCreate"
|
||||
>
|
||||
<span v-if="!isMobile">New Definition</span>
|
||||
<span>New Definition</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -65,9 +65,10 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
|
||||
class="hidden sm:inline-flex"
|
||||
@click="toggleDisplayStyle"
|
||||
>
|
||||
<span v-if="!isMobile">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
|
||||
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span v-if="!isMobile">Reload</span>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -129,13 +130,15 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="display_style === 'list' && filteredDefinitions.length > 0"
|
||||
v-if="contentStyle === 'list' && filteredDefinitions.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-245 w-full text-sm">
|
||||
<table class="min-w-255 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
|
|
@ -147,7 +150,7 @@
|
|||
<th class="w-full text-left">Definition</th>
|
||||
<th class="w-28 whitespace-nowrap">Priority</th>
|
||||
<th class="w-36 whitespace-nowrap">Updated</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
<th class="w-48 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
|
@ -155,7 +158,7 @@
|
|||
<tr
|
||||
v-for="definition in filteredDefinitions"
|
||||
:key="definition.id"
|
||||
class="hover:bg-muted/20"
|
||||
class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
|
|
@ -198,7 +201,9 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 text-center align-middle">{{ definition.priority }}</td>
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
{{ definition.priority }}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 text-center align-middle whitespace-nowrap">
|
||||
<UTooltip :text="moment(definition.updated_at).format('YYYY-M-DD H:mm Z')">
|
||||
|
|
@ -210,7 +215,7 @@
|
|||
</UTooltip>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<td class="w-48 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
|
|
@ -219,7 +224,7 @@
|
|||
icon="i-lucide-file-up"
|
||||
@click="() => void exportDefinition(definition)"
|
||||
>
|
||||
<span v-if="!isMobile">Export</span>
|
||||
<span class="hidden sm:inline">Export</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -229,7 +234,7 @@
|
|||
icon="i-lucide-pencil"
|
||||
@click="() => void openEdit(definition)"
|
||||
>
|
||||
<span v-if="!isMobile">Edit</span>
|
||||
<span class="hidden sm:inline">Edit</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -239,7 +244,7 @@
|
|||
icon="i-lucide-trash"
|
||||
@click="() => void remove(definition)"
|
||||
>
|
||||
<span v-if="!isMobile">Delete</span>
|
||||
<span class="hidden sm:inline">Delete</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -260,7 +265,11 @@
|
|||
>
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
:ui="{
|
||||
header: 'p-4 pb-3',
|
||||
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
|
|
@ -287,7 +296,7 @@
|
|||
square
|
||||
@click="() => void exportDefinition(definition)"
|
||||
>
|
||||
<span class="hidden sm:inline">Export Definition</span>
|
||||
<span>Export Definition</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
|
|
@ -340,27 +349,29 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="() => void openEdit(definition)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
<template #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="() => void openEdit(definition)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void remove(definition)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void remove(definition)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -473,7 +484,6 @@ const DEFAULT_DEFINITION: TaskDefinitionDocument = {
|
|||
},
|
||||
};
|
||||
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
||||
const taskDefs = useTaskDefinitionsComposable();
|
||||
|
|
@ -500,6 +510,7 @@ const workingDefinition = ref<TaskDefinitionDocument | null>(null);
|
|||
const workingId = ref<number | null>(null);
|
||||
const inspect = ref(false);
|
||||
const display_style = useStorage<'list' | 'grid'>('task-definitions:display', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
|
||||
const query = ref('');
|
||||
const showFilter = ref(false);
|
||||
|
|
@ -541,6 +552,9 @@ const allSelected = computed(
|
|||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
const contentStyle = computed<'list' | 'grid'>(() =>
|
||||
isMobile.value ? 'grid' : display_style.value,
|
||||
);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
icon="i-lucide-filter"
|
||||
@click="toggleFilterPanel"
|
||||
>
|
||||
<span v-if="!isMobile">Filter</span>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
icon="i-lucide-plus"
|
||||
@click="openCreateForm"
|
||||
>
|
||||
<span v-if="!isMobile">New Task</span>
|
||||
<span>New Task</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -56,9 +56,10 @@
|
|||
variant="outline"
|
||||
size="sm"
|
||||
:icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
|
||||
class="hidden sm:inline-flex"
|
||||
@click="toggleDisplayStyle"
|
||||
>
|
||||
<span v-if="!isMobile">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
|
||||
<span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span v-if="!isMobile">Reload</span>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -112,13 +113,15 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayStyle === 'list' && filteredTasks.length > 0"
|
||||
v-if="contentStyle === 'list' && filteredTasks.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
>
|
||||
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table class="min-w-190 table-fixed w-full text-sm">
|
||||
<table class="min-w-210 table-fixed w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<tr
|
||||
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
|
||||
>
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
|
|
@ -128,13 +131,17 @@
|
|||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Task</th>
|
||||
<th class="w-100 whitespace-nowrap">Timer</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
<th class="w-50 whitespace-nowrap">Timer</th>
|
||||
<th class="w-75 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="item in filteredTasks" :key="item.id" class="align-top hover:bg-muted/20">
|
||||
<tr
|
||||
v-for="item in filteredTasks"
|
||||
:key="item.id"
|
||||
class="align-top transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
|
||||
>
|
||||
<td class="px-3 py-3 text-center align-top">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
|
|
@ -224,9 +231,8 @@
|
|||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-3.5" />
|
||||
Preset:
|
||||
<span class="capitalize">
|
||||
{{ item.preset ?? config.app.default_preset }}
|
||||
Preset: {{ item.preset ?? config.app.default_preset }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -273,7 +279,7 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-top whitespace-nowrap">
|
||||
<td class="w-56 px-3 py-3 align-top whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
|
|
@ -282,7 +288,7 @@
|
|||
icon="i-lucide-pencil"
|
||||
@click="editItem(item)"
|
||||
>
|
||||
<span v-if="!isMobile">Edit</span>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
|
|
@ -292,7 +298,7 @@
|
|||
icon="i-lucide-trash"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
<span v-if="!isMobile">Delete</span>
|
||||
Delete
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu :items="itemActionGroups(item)" :modal="false">
|
||||
|
|
@ -303,7 +309,7 @@
|
|||
icon="i-lucide-settings-2"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
>
|
||||
<span v-if="!isMobile">Actions</span>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
|
@ -321,7 +327,11 @@
|
|||
<div v-for="item in filteredTasks" :key="item.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
:ui="{
|
||||
header: 'p-4 pb-3',
|
||||
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
|
||||
footer: 'border-t border-default px-4 py-4',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
|
|
@ -377,7 +387,7 @@
|
|||
square
|
||||
@click="() => void exportItem(item)"
|
||||
>
|
||||
<span class="hidden sm:inline">Export Task</span>
|
||||
<span>Export Task</span>
|
||||
</UButton>
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
|
|
@ -392,10 +402,10 @@
|
|||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<div class="grid grid-cols-2 gap-2 text-xs text-toned sm:flex sm:flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
class="flex min-w-0 w-full items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default sm:w-auto sm:flex-none sm:shrink-0 sm:whitespace-nowrap"
|
||||
@click="() => void toggleFlag(item, 'enabled')"
|
||||
>
|
||||
<UIcon
|
||||
|
|
@ -408,7 +418,7 @@
|
|||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
class="flex min-w-0 w-full items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default sm:w-auto sm:flex-none sm:shrink-0 sm:whitespace-nowrap"
|
||||
@click="() => void toggleFlag(item, 'auto_start')"
|
||||
>
|
||||
<UIcon
|
||||
|
|
@ -421,7 +431,7 @@
|
|||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
class="flex min-w-0 w-full items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default sm:w-auto sm:flex-none sm:shrink-0 sm:whitespace-nowrap"
|
||||
@click="() => void toggleFlag(item, 'handler_enabled')"
|
||||
>
|
||||
<UIcon
|
||||
|
|
@ -432,12 +442,18 @@
|
|||
<span>Handler: {{ item.handler_enabled !== false ? 'On' : 'Off' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-1 rounded-md border border-default px-2 py-1 text-left transition hover:border-primary hover:text-default sm:flex-1"
|
||||
@click="toggleExpand(item.id, 'preset')"
|
||||
>
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-3.5" />
|
||||
<span>Preset: {{ item.preset ?? config.app.default_preset }}</span>
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span :class="['min-w-0 capitalize', expandClass(item.id, 'preset')]">
|
||||
Preset: {{ item.preset ?? config.app.default_preset }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
|
|
@ -541,39 +557,41 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(item)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu :items="itemActionGroups(item)" :modal="false">
|
||||
<template #footer>
|
||||
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-settings-2"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(item)"
|
||||
>
|
||||
Actions
|
||||
Edit
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
|
||||
<UDropdownMenu :items="itemActionGroups(item)" :modal="false">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-settings-2"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
class="w-full justify-center"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -702,7 +720,7 @@ const { confirmDialog } = useDialog();
|
|||
const sessionCache = useSessionCache();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const display_style = useStorage<'list' | 'grid' | 'cards'>('tasks_display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
|
||||
const tasksComposable = useTasks();
|
||||
const {
|
||||
|
|
@ -744,6 +762,9 @@ const taskHandlerSupport = ref<Record<string, boolean>>(sessionCache.get(CACHE_K
|
|||
const displayStyle = computed<'list' | 'grid'>(() =>
|
||||
display_style.value === 'list' ? 'list' : 'grid',
|
||||
);
|
||||
const contentStyle = computed<'list' | 'grid'>(() =>
|
||||
isMobile.value ? 'grid' : displayStyle.value,
|
||||
);
|
||||
|
||||
const editorSessionId = ref(0);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue