refactor: minor fixes for better mobile view.

This commit is contained in:
arabcoders 2026-03-26 17:13:58 +03:00
parent 218832a493
commit cdca27d4b9
13 changed files with 208 additions and 77 deletions

View file

@ -16,7 +16,7 @@
{{ selectedElms.length }}
</UBadge>
<UDropdownMenu :items="bulkActionGroups">
<UDropdownMenu :items="bulkActionGroups" :modal="false">
<UButton
color="neutral"
variant="outline"
@ -52,7 +52,7 @@
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-325 w-full text-sm">
<table class="min-w-325 max-w-455 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">
<th class="w-[5%]">
@ -63,11 +63,11 @@
/>
</button>
</th>
<th class="w-full text-left">Title</th>
<th class="w-[15%]">Status</th>
<th class="w-[15%]">Created</th>
<th class="w-[10%]">Size/Starts</th>
<th class="w-[1%]">Actions</th>
<th class="text-left">Title</th>
<th class="w-[7%]">Status</th>
<th class="w-[7%]">Created</th>
<th class="w-[7%]">Size/Starts</th>
<th class="w-[8%]">Actions</th>
</tr>
</thead>
@ -85,8 +85,8 @@
</label>
</td>
<td class="px-3 py-3 align-top">
<div class="flex items-start justify-between gap-3">
<td class="w-0 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
:text="
@ -159,16 +159,16 @@
<p
v-if="item.error"
class="is-text-overflow mt-2 cursor-pointer text-sm text-error"
@click="toggle_class($event)"
:class="messageClass(item._id, 'error', 'list', 'mt-2')"
@click="toggleMessage(item._id, 'error', 'list')"
>
{{ item.error }}
</p>
<p
v-if="showMessage(item)"
class="is-text-overflow mt-1 cursor-pointer text-sm text-error"
@click="toggle_class($event)"
:class="messageClass(item._id, 'msg', 'list', 'mt-1')"
@click="toggleMessage(item._id, 'msg', 'list')"
>
{{ item.msg }}
</p>
@ -244,7 +244,7 @@
@click="() => void removeItem(item)"
/>
<UDropdownMenu v-if="item.url" :items="itemActionGroups(item)">
<UDropdownMenu v-if="item.url" :items="itemActionGroups(item)" :modal="false">
<UButton
color="neutral"
variant="outline"
@ -389,7 +389,7 @@
</figure>
</div>
<div class="grid gap-2 text-sm sm:auto-cols-fr sm:grid-flow-col">
<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"
>
@ -440,7 +440,7 @@
</div>
</div>
<div class="grid gap-2 sm:grid-cols-3">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
v-if="!item.filename"
color="warning"
@ -475,7 +475,7 @@
{{ config.app.remove_files ? 'Remove' : 'Clear' }}
</UButton>
<UDropdownMenu :items="itemActionGroups(item)" class="w-full">
<UDropdownMenu :items="itemActionGroups(item)" :modal="false" class="w-full">
<UButton
color="neutral"
variant="outline"
@ -494,16 +494,16 @@
>
<p
v-if="item.error"
class="is-text-overflow cursor-pointer text-sm text-error"
@click="toggle_class($event)"
:class="messageClass(item._id, 'error', 'card')"
@click="toggleMessage(item._id, 'error', 'card')"
>
{{ item.error }}
</p>
<p
v-if="showMessage(item)"
class="is-text-overflow cursor-pointer text-sm text-error"
@click="toggle_class($event)"
:class="messageClass(item._id, 'msg', 'card')"
@click="toggleMessage(item._id, 'msg', 'card')"
>
{{ item.msg }}
</p>
@ -652,6 +652,7 @@ const masterSelectAll = ref(false);
const embed_url = ref('');
const video_item = ref<StoreItem | null>(null);
const loadMoreTrigger = ref<HTMLElement | null>(null);
const expandedMessages = reactive<Record<string, Set<string>>>({});
const paginationInfo = computed(() => stateStore.getPagination());
@ -1277,10 +1278,45 @@ const downloadSelected = async () => {
}
};
const toggle_class = (e: Event) =>
['is-text-overflow', 'is-word-break'].forEach((c) =>
(e.currentTarget as HTMLElement).classList.toggle(c),
);
const toggleMessage = (itemId: string, field: 'error' | 'msg', view: 'list' | 'card') => {
const key = `${itemId}:${view}`;
if (!expandedMessages[key]) {
expandedMessages[key] = new Set();
}
if (expandedMessages[key].has(field)) {
expandedMessages[key].delete(field);
return;
}
expandedMessages[key].add(field);
};
const isMessageExpanded = (itemId: string, field: 'error' | 'msg', view: 'list' | 'card') =>
expandedMessages[`${itemId}:${view}`]?.has(field) ?? false;
const messageClass = (
itemId: string,
field: 'error' | 'msg',
view: 'list' | 'card',
spacingClass = '',
) => {
const expanded = isMessageExpanded(itemId, field, view);
const base = ['cursor-pointer', 'text-sm', 'text-error'];
if (spacingClass) {
base.push(spacingClass);
}
if ('card' === view) {
base.push(expanded ? 'whitespace-pre-wrap break-words' : 'line-clamp-2 break-words');
return base;
}
base.push(expanded ? 'whitespace-pre-wrap break-words' : 'block max-w-full truncate');
return base;
};
const removeFromArchiveDialog = async (item: StoreItem): Promise<void> => {
const options = [

View file

@ -375,7 +375,7 @@
<div
class="flex flex-wrap items-center justify-between gap-2 border-t border-default pt-4"
>
<UDropdownMenu class="sm:hidden" :items="mobileActionGroups">
<UDropdownMenu class="sm:hidden" :items="mobileActionGroups" :modal="false">
<UButton
color="neutral"
variant="outline"

View file

@ -29,7 +29,7 @@
{{ selectedElms.length }}
</UBadge>
<UDropdownMenu :items="bulkActionGroups">
<UDropdownMenu :items="bulkActionGroups" :modal="false">
<UButton
color="neutral"
variant="outline"
@ -225,7 +225,7 @@
@click="() => void pauseItem(item)"
/>
<UDropdownMenu :items="itemActionGroups(item)">
<UDropdownMenu :items="itemActionGroups(item)" :modal="false">
<UButton
color="neutral"
variant="outline"
@ -380,7 +380,7 @@
</div>
</div>
<div class="grid gap-2 text-sm sm:auto-cols-fr sm:grid-flow-col">
<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"
>
@ -417,7 +417,7 @@
</div>
</div>
<div class="grid gap-2 sm:auto-cols-fr sm:grid-flow-col">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
color="warning"
variant="outline"
@ -450,7 +450,7 @@
Pause
</UButton>
<UDropdownMenu :items="itemActionGroups(item)" class="w-full">
<UDropdownMenu :items="itemActionGroups(item)" :modal="false" class="w-full">
<UButton
color="neutral"
variant="outline"

View file

@ -8,11 +8,21 @@
@update:open="(open) => !open && emitter('close')"
>
<template #header>
<div class="flex items-center justify-between gap-3">
<div>
<div class="flex w-full items-start gap-3">
<div class="min-w-0 flex-1">
<p class="text-base font-semibold text-highlighted">WebUI Settings</p>
<p class="text-sm text-toned">Adjust interface behavior and download defaults.</p>
<p class="text-sm text-toned">Adjust interface behavior.</p>
</div>
<UButton
color="neutral"
variant="ghost"
size="sm"
square
icon="i-lucide-x"
class="ml-auto shrink-0 sm:hidden"
@click="emitter('close')"
/>
</div>
</template>

View file

@ -27,7 +27,7 @@ const DOCS_ENTRIES: DocsEntry[] = [
{
id: 'faq',
title: 'FAQ',
description: 'Answers for setup details, task handlers, and common issues.',
description: 'Frequently asked questions about the project and troubleshooting.',
file: 'FAQ.md',
route: '/docs/faq',
slug: ['faq'],
@ -77,6 +77,7 @@ const getDocsNavigationEntries = () =>
DOCS_ENTRIES.map((entry) => ({
id: entry.id,
label: entry.navLabel,
description: entry.description,
icon: entry.icon,
to: entry.route,
}));

View file

@ -139,6 +139,16 @@
<template #left>
<div class="flex items-center gap-2">
<UDashboardSidebarToggle class="lg:hidden" />
<UButton
to="/"
color="neutral"
variant="ghost"
size="sm"
icon="i-lucide-house"
class="lg:hidden"
>
Home
</UButton>
<UDashboardSidebarCollapse class="hidden lg:inline-flex" />
</div>
</template>
@ -480,6 +490,7 @@ import type { version_check } from '~/types';
type NavEntry = {
id: string;
label: string;
description?: string;
icon: string;
to?: string;
children?: NavEntry[];
@ -544,21 +555,65 @@ const makeNavigationItem = (item: NavEntry): NavigationMenuItem => ({
const docsNavigationEntries = getDocsNavigationEntries();
const allNavItems = computed<NavEntry[]>(() => [
{ id: 'downloads', label: 'Downloads', icon: 'i-lucide-download', to: '/' },
{ id: 'files', label: 'Files', icon: 'i-lucide-folder-tree', to: '/browser' },
{ id: 'presets', label: 'Presets', icon: 'i-lucide-sliders-horizontal', to: '/presets' },
{ id: 'custom-fields', label: 'Custom Fields', icon: 'i-lucide-braces', to: '/dl_fields' },
{ id: 'conditions', label: 'Conditions', icon: 'i-lucide-filter', to: '/conditions' },
{ id: 'notifications', label: 'Notifications', icon: 'i-lucide-bell', to: '/notifications' },
{
id: 'downloads',
label: 'Downloads',
description: 'Queued and completed downloads list.',
icon: 'i-lucide-download',
to: '/',
},
{
id: 'files',
label: 'Files',
description: 'Browse downloaded files.',
icon: 'i-lucide-folder-tree',
to: '/browser',
},
{
id: 'presets',
label: 'Presets',
description:
'Presets are pre-defined command options for yt-dlp that you want to apply to given download.',
icon: 'i-lucide-sliders-horizontal',
to: '/presets',
},
{
id: 'custom-fields',
label: 'Custom Fields',
description: 'Custom fields allow you to add new fields to the download form.',
icon: 'i-lucide-braces',
to: '/dl_fields',
},
{
id: 'conditions',
label: 'Conditions',
description: 'Run yt-dlp custom match filter on returned info and apply options.',
icon: 'i-lucide-filter',
to: '/conditions',
},
{
id: 'notifications',
label: 'Notifications',
description: 'Send notifications to your webhooks based on specified events or presets.',
icon: 'i-lucide-bell',
to: '/notifications',
},
{
id: 'tasks',
label: 'Tasks',
icon: 'i-lucide-list-todo',
children: [
{ id: 'tasks-list', label: 'Tasks', icon: 'i-lucide-list-todo', to: '/tasks' },
{
id: 'tasks-list',
label: 'Tasks',
description: 'Queue playlist/channels for automatic download at specified intervals.',
icon: 'i-lucide-list-todo',
to: '/tasks',
},
{
id: 'task-definitions',
label: 'Task Definitions',
description: 'Create definitions to turn any website into a downloadable feed of links.',
icon: 'i-lucide-workflow',
to: '/task_definitions',
},
@ -570,10 +625,26 @@ const allNavItems = computed<NavEntry[]>(() => [
icon: 'i-lucide-wrench',
children: [
...(config.app?.file_logging
? [{ id: 'logs', label: 'Logs', icon: 'i-lucide-file-text', to: '/logs' }]
? [
{
id: 'logs',
label: 'Logs',
description: 'Scroll near the top to load older logs.',
icon: 'i-lucide-file-text',
to: '/logs',
},
]
: []),
...(config.app.console_enabled
? [{ id: 'console', label: 'Console', icon: 'i-lucide-terminal', to: '/console' }]
? [
{
id: 'console',
label: 'Console',
description: 'Run yt-dlp commands directly in a non-interactive session.',
icon: 'i-lucide-terminal',
to: '/console',
},
]
: []),
],
},
@ -583,7 +654,14 @@ const allNavItems = computed<NavEntry[]>(() => [
icon: 'i-lucide-book-open',
children: [
...docsNavigationEntries,
{ id: 'changelog', label: 'Changelog', icon: 'i-lucide-list', to: '/changelog' },
{
id: 'changelog',
label: 'Changelog',
description:
'Latest project changes, loaded remotely when available and falling back to the bundled changelog file.',
icon: 'i-lucide-list',
to: '/changelog',
},
],
},
]);
@ -674,6 +752,7 @@ const routeSearchGroups = computed(() => [
? [
{
label: item.label,
description: item.description,
icon: item.icon,
suffix: item.to,
onSelect: () => handleRouteSelect(item),
@ -683,6 +762,7 @@ const routeSearchGroups = computed(() => [
const children = (item.children || []).map((child) => ({
label: child.label,
description: child.description,
icon: child.icon,
suffix: child.to || '',
onSelect: () => handleRouteSelect(child),
@ -698,18 +778,30 @@ const routeSearchGroups = computed(() => [
config.paused
? {
label: 'Resume Downloads',
description: 'Resume the global download queue so pending items can start again.',
description: 'Resume globally paused downloads.',
icon: 'i-lucide-play',
onSelect: () => void resumeDownloads(),
}
: {
label: 'Pause Downloads',
description: 'Pause pending downloads without stopping items already in progress.',
description: 'Globally pause all non-active downloads.',
icon: 'i-lucide-pause',
onSelect: () => void pauseDownloads(),
},
],
},
{
id: 'preferences',
label: 'Preferences',
items: [
{
label: 'WebUI Settings',
description: 'Adjust interface behavior and download defaults.',
icon: 'i-lucide-settings-2',
onSelect: () => void openSettings(),
},
],
},
]);
const closeRouteSearch = async (): Promise<void> => {
@ -723,19 +815,6 @@ const closeRouteSearch = async (): Promise<void> => {
const pauseDownloads = async (): Promise<void> => {
await closeRouteSearch();
const { status } = await confirmDialog({
title: 'Pause Downloads',
confirmText: 'Pause',
cancelText: 'Cancel',
confirmColor: 'warning',
message: 'Are you sure you want to pause all non-active downloads?',
});
if (!status) {
return;
}
await request('/api/system/pause', { method: 'POST' });
};
@ -744,6 +823,11 @@ const resumeDownloads = async (): Promise<void> => {
await request('/api/system/resume', { method: 'POST' });
};
const openSettings = async (): Promise<void> => {
await closeRouteSearch();
show_settings.value = true;
};
const syncShellModeClass = () => {
const html = document.documentElement;
html.classList.toggle('simple-mode', simpleMode.value);

View file

@ -84,7 +84,7 @@
<span v-if="!isMobile">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
</UButton>
<UDropdownMenu v-if="hasItems" :items="sortGroups">
<UDropdownMenu v-if="hasItems" :items="sortGroups" :modal="false">
<UButton
color="neutral"
variant="outline"
@ -364,7 +364,7 @@
</div>
</template>
<div class="grid grid-cols-3 gap-2 text-sm">
<div class="flex flex-wrap gap-2 text-sm *:min-w-32 *:flex-1">
<div
class="min-w-0 rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-default"
>
@ -386,7 +386,7 @@
</div>
</div>
<div v-if="controlEnabled" class="mt-auto grid grid-cols-3 gap-2 pt-1">
<div v-if="controlEnabled" class="mt-auto flex flex-wrap gap-2 pt-1 *:min-w-32 *:flex-1">
<UButton
color="warning"
variant="outline"

View file

@ -203,7 +203,7 @@
<span :class="expandClass(cond.id, 'name')">{{ cond.name }}</span>
</button>
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<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"
@ -293,7 +293,7 @@
</button>
</div>
<div class="mt-auto grid gap-2 pt-2 sm:grid-cols-2">
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
<UButton
color="warning"
variant="outline"

View file

@ -177,7 +177,7 @@
<div class="min-w-0 flex-1 space-y-2">
<div class="truncate text-sm font-semibold text-highlighted">{{ field.name }}</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<span
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
>
@ -227,7 +227,7 @@
</div>
</div>
<div class="mt-auto grid gap-2 pt-2 sm:grid-cols-2">
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
<UButton
color="warning"
variant="outline"

View file

@ -275,7 +275,7 @@
</div>
</div>
<div class="mt-auto grid gap-2 pt-2 sm:grid-cols-2">
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
<UButton
color="warning"
variant="outline"

View file

@ -186,7 +186,7 @@
/>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<span
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
:class="item.cookies ? 'border-info/40 text-info' : ''"
@ -247,7 +247,7 @@
</button>
</div>
<div class="mt-auto grid gap-2 pt-2 sm:grid-cols-2">
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
<UButton
color="warning"
variant="outline"

View file

@ -214,7 +214,7 @@
{{ definition.name || '(Unnamed definition)' }}
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<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"
@ -283,7 +283,7 @@
</div>
</div>
<div class="mt-auto grid gap-2 pt-2 sm:grid-cols-2">
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
<UButton
color="warning"
variant="outline"

View file

@ -95,7 +95,7 @@
{{ selectedElms.length }}
</UBadge>
<UDropdownMenu :items="bulkActionGroups">
<UDropdownMenu :items="bulkActionGroups" :modal="false">
<UButton
color="neutral"
variant="outline"
@ -308,7 +308,7 @@
<span v-if="!isMobile">Delete</span>
</UButton>
<UDropdownMenu :items="itemActionGroups(item)">
<UDropdownMenu :items="itemActionGroups(item)" :modal="false">
<UButton
color="neutral"
variant="outline"
@ -391,7 +391,7 @@
</template>
<div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<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"
@ -500,7 +500,7 @@
</div>
</div>
<div class="mt-auto grid gap-2 pt-2 sm:grid-cols-3">
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
<UButton
color="warning"
variant="outline"
@ -521,7 +521,7 @@
Delete
</UButton>
<UDropdownMenu :items="itemActionGroups(item)">
<UDropdownMenu :items="itemActionGroups(item)" :modal="false">
<UButton
color="neutral"
variant="outline"