refactor(ui): more consistency changes.

This commit is contained in:
arabcoders 2026-04-19 21:22:29 +03:00
parent e644ea6357
commit fc5cf84dd2
17 changed files with 704 additions and 928 deletions

View file

@ -475,7 +475,9 @@
: 'i-lucide-circle-help' : 'i-lucide-circle-help'
" "
title="Filter Status" title="Filter Status"
:description="logicStatusText" :description="
testData.data.status === null ? 'Not tested' : logicTest ? 'Matched' : 'Not matched'
"
/> />
<UFormField :ui="fieldUi"> <UFormField :ui="fieldUi">
@ -673,14 +675,6 @@ const logicTest = computed(() => {
} }
}); });
const logicStatusText = computed(() => {
if (testData.value.data.status === null) {
return 'Not tested';
}
return logicTest.value ? 'Matched' : 'Not matched';
});
const checkInfo = async (): Promise<void> => { const checkInfo = async (): Promise<void> => {
for (const key of ['name', 'filter'] as const) { for (const key of ['name', 'filter'] as const) {
if (!form[key]) { if (!form[key]) {

View file

@ -5,7 +5,7 @@
> >
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="inline-flex min-w-0 items-center gap-2 text-sm font-semibold text-default"> <div class="inline-flex min-w-0 items-center gap-2 text-sm font-semibold text-default">
<UIcon v-if="resolvedIcon" :name="resolvedIcon" class="size-4 shrink-0 text-toned" /> <UIcon v-if="props.icon" :name="props.icon" class="size-4 shrink-0 text-toned" />
<UTooltip :text="field ? `yt-dlp option: ${field}` : undefined"> <UTooltip :text="field ? `yt-dlp option: ${field}` : undefined">
<span class="truncate" :class="{ 'has-tooltip': field }"> <span class="truncate" :class="{ 'has-tooltip': field }">
{{ label }} {{ label }}
@ -52,7 +52,7 @@
</template> </template>
<template v-else> <template v-else>
<span class="inline-flex items-center gap-2 font-semibold"> <span class="inline-flex items-center gap-2 font-semibold">
<UIcon v-if="resolvedIcon" :name="resolvedIcon" class="size-4 text-toned" /> <UIcon v-if="props.icon" :name="props.icon" class="size-4 text-toned" />
<UTooltip :text="field ? `yt-dlp option: ${field}` : undefined"> <UTooltip :text="field ? `yt-dlp option: ${field}` : undefined">
<span :class="{ 'has-tooltip': field }"> <span :class="{ 'has-tooltip': field }">
{{ label }} {{ label }}
@ -142,14 +142,6 @@ const boolModel = computed({
}, },
}); });
const resolvedIcon = computed(() => {
if (!props.icon) {
return '';
}
return props.icon;
});
const fieldUi = computed(() => ({ const fieldUi = computed(() => ({
container: props.compact ? 'space-y-1.5' : 'space-y-2', container: props.compact ? 'space-y-1.5' : 'space-y-2',
description: props.compact ? 'text-xs text-toned' : 'text-sm text-toned', description: props.compact ? 'text-xs text-toned' : 'text-sm text-toned',

View file

@ -43,7 +43,7 @@
<template v-if="'alert' === state.current?.type"> <template v-if="'alert' === state.current?.type">
<UButton <UButton
id="primaryButton" id="primaryButton"
:color="resolveConfirmColor(state.current?.opts.confirmColor)" :color="state.current?.opts.confirmColor ?? 'primary'"
@click="onEnter" @click="onEnter"
> >
{{ state.current?.opts.confirmText ?? 'OK' }} {{ state.current?.opts.confirmText ?? 'OK' }}
@ -53,7 +53,7 @@
<template v-else-if="'confirm' === state.current?.type || 'prompt' === state.current?.type"> <template v-else-if="'confirm' === state.current?.type || 'prompt' === state.current?.type">
<UButton <UButton
id="primaryButton" id="primaryButton"
:color="resolveConfirmColor(state.current?.opts.confirmColor)" :color="state.current?.opts.confirmColor ?? 'primary'"
:disabled=" :disabled="
'prompt' === state.current?.type && 'prompt' === state.current?.type &&
localInput === (state.current?.opts as PromptOptions)?.initial localInput === (state.current?.opts as PromptOptions)?.initial
@ -121,8 +121,6 @@ const focusInput = async () => {
requestAnimationFrame(focusPrimary); requestAnimationFrame(focusPrimary);
}; };
const resolveConfirmColor = (color?: ConfirmOptions['confirmColor']) => color ?? 'primary';
const onCancel = () => cancel(); const onCancel = () => cancel();
const onEnter = () => const onEnter = () =>
confirm('confirm' === state.current?.type ? selected.value : localInput.value); confirm('confirm' === state.current?.type ? selected.value : localInput.value);

View file

@ -32,15 +32,24 @@
</UDropdownMenu> </UDropdownMenu>
</div> </div>
<UButton <div class="flex flex-wrap items-center gap-2">
color="neutral" <UBadge color="neutral" variant="soft" size="sm">
variant="outline" <span class="inline-flex items-center gap-1.5">
size="sm" <UIcon name="i-lucide-history" class="size-3.5" />
:icon="direction === 'desc' ? 'i-lucide-arrow-down-a-z' : 'i-lucide-arrow-up-a-z'" <span>Total: {{ stateStore.count('history') }}</span>
@click="toggleDirection" </span>
> </UBadge>
<span>Sort</span>
</UButton> <UButton
color="neutral"
variant="outline"
size="sm"
:icon="direction === 'desc' ? 'i-lucide-arrow-down-a-z' : 'i-lucide-arrow-up-a-z'"
@click="toggleDirection"
>
<span>Sort</span>
</UButton>
</div>
</div> </div>
<UAlert <UAlert
@ -599,15 +608,6 @@
<li><code>source_name:task_name</code> - items added by the specified task.</li> <li><code>source_name:task_name</code> - items added by the specified task.</li>
</ul> </ul>
</div> </div>
<UButton
color="neutral"
variant="outline"
size="sm"
@click="() => emitter('clear_search')"
>
Clear filter
</UButton>
</div> </div>
</template> </template>
</UAlert> </UAlert>

View file

@ -202,6 +202,24 @@
border-radius: 0.9rem; border-radius: 0.9rem;
} }
.docs-markdown img.docs-markdown__image--inline {
display: inline-block;
max-width: none;
vertical-align: middle;
border: 0;
border-radius: 0;
}
.docs-markdown img.docs-markdown__image--badge {
height: 1.25rem;
}
.docs-markdown img.docs-markdown__image--large {
width: auto;
min-width: min(100%, 24rem);
margin-inline: auto;
}
.markdown-alert { .markdown-alert {
--markdown-alert-accent: color-mix(in oklab, var(--ui-border-accented) 70%, transparent); --markdown-alert-accent: color-mix(in oklab, var(--ui-border-accented) 70%, transparent);
margin-top: 1rem; margin-top: 1rem;
@ -294,6 +312,18 @@ const content = ref('');
const error = ref(''); const error = ref('');
const isLoading = ref(true); const isLoading = ref(true);
const isInlineMarkdownImage = (href: string): boolean => {
return /(?:badge\.svg|shields\.io|ghcr-badge|\.svg(?:[?#].*)?$)/i.test(href);
};
const getMarkdownImageClasses = (href: string): string[] => {
if (isInlineMarkdownImage(href)) {
return ['docs-markdown__image--inline', 'docs-markdown__image--badge'];
}
return ['docs-markdown__image--large'];
};
const createMarkdownParser = () => { const createMarkdownParser = () => {
const parser = new Marked(); const parser = new Marked();
@ -370,7 +400,9 @@ const createMarkdownParser = () => {
const refPolicy = ' referrerpolicy="no-referrer"'; const refPolicy = ' referrerpolicy="no-referrer"';
const crossorigin = token._isExternalImage ? ' crossorigin="anonymous"' : ''; const crossorigin = token._isExternalImage ? ' crossorigin="anonymous"' : '';
const loading = ' loading="lazy"'; const loading = ' loading="lazy"';
return `<img src="${token.href || ''}"${alt}${title}${refPolicy}${crossorigin}${loading} />`; const src = token.href || '';
const classes = getMarkdownImageClasses(src).join(' ');
return `<img src="${src}" class="${classes}"${alt}${title}${refPolicy}${crossorigin}${loading} />`;
}, },
}, },
}); });

View file

@ -44,6 +44,15 @@
</UButton> </UButton>
</UDropdownMenu> </UDropdownMenu>
</div> </div>
<div class="flex flex-wrap items-center gap-2">
<UBadge color="neutral" variant="soft" size="sm">
<span class="inline-flex items-center gap-1.5">
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
<span>Total: {{ stateStore.count('queue') }}</span>
</span>
</UBadge>
</div>
</div> </div>
<div <div
@ -548,15 +557,6 @@
<li><code>source_name:task_name</code> - items added by the specified task.</li> <li><code>source_name:task_name</code> - items added by the specified task.</li>
</ul> </ul>
</div> </div>
<UButton
color="neutral"
variant="outline"
size="sm"
@click="() => emitter('clear_search')"
>
Clear filter
</UButton>
</div> </div>
</template> </template>
</UAlert> </UAlert>

View file

@ -44,80 +44,74 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton color="neutral"
color="neutral" :variant="show_filter ? 'soft' : 'outline'"
:variant="show_filter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilter"
@click="toggleFilter" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<UButton <UButton
v-if="controlEnabled" v-if="controlEnabled"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-folder-plus" icon="i-lucide-folder-plus"
@click="() => void handleCreateDirectory()" @click="() => void handleCreateDirectory()"
> >
<span>New Folder</span> <span>New Folder</span>
</UButton> </UButton>
<UButton
color="neutral"
variant="outline"
size="sm"
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex"
@click="toggleDisplayStyle"
>
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
</UButton>
<UDropdownMenu v-if="hasItems" :items="sortGroups" :modal="false">
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'" icon="i-lucide-arrow-up-down"
class="hidden sm:inline-flex" trailing-icon="i-lucide-chevron-down"
@click="toggleDisplayStyle"
> >
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span> <span>Sort</span>
</UButton> </UButton>
</UDropdownMenu>
<UDropdownMenu v-if="hasItems" :items="sortGroups" :modal="false"> <UButton
<UButton color="neutral"
color="neutral" variant="outline"
variant="outline" size="sm"
size="sm" icon="i-lucide-refresh-cw"
icon="i-lucide-arrow-up-down" :loading="isLoading"
trailing-icon="i-lucide-chevron-down" :disabled="isLoading"
> @click="() => void reloadContent(browserPath)"
<span>Sort</span> >
</UButton> <span>Reload</span>
</UDropdownMenu> </UButton>
<UButton <UInput
color="neutral" v-if="show_filter"
variant="outline" id="search"
size="sm" ref="searchInput"
icon="i-lucide-refresh-cw" v-model.lazy="localSearch"
:loading="isLoading" type="search"
:disabled="isLoading" placeholder="Filter"
@click="() => void reloadContent(browserPath)" icon="i-lucide-filter"
> size="sm"
<span>Reload</span> class="order-last w-full sm:order-first sm:w-80"
</UButton> />
</div>
<div v-if="show_filter" class="relative w-full xl:w-80">
<span
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned"
>
<UIcon name="i-lucide-filter" class="size-4" />
</span>
<input
id="search"
ref="searchInput"
v-model.lazy="localSearch"
type="search"
placeholder="Filter"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
@ -458,10 +452,6 @@
title="No results" title="No results"
:description="`No results found for '${localSearch}'.`" :description="`No results found for '${localSearch}'.`"
/> />
<UButton color="neutral" variant="outline" size="sm" @click="clearFilter"
>Clear filter</UButton
>
</div> </div>
<UAlert <UAlert
@ -554,7 +544,7 @@ const display_style = useStorage<string>('browser_display_style', 'list');
const isMobile = useMediaQuery({ maxWidth: 639 }); const isMobile = useMediaQuery({ maxWidth: 639 });
const show_filter = ref(false); const show_filter = ref(false);
const localSearch = ref(''); const localSearch = ref('');
const searchInput = ref<HTMLInputElement | null>(null); const searchInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const items = browser.items; const items = browser.items;
const browserPath = browser.path; const browserPath = browser.path;
@ -943,7 +933,7 @@ const toggleFilter = async (): Promise<void> => {
} }
await nextTick(); await nextTick();
searchInput.value?.focus(); searchInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
}; };
const toggleDisplayStyle = (): void => { const toggleDisplayStyle = (): void => {

View file

@ -21,43 +21,36 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton v-if="logs.length > 0"
v-if="logs.length > 0" color="neutral"
color="neutral" :variant="toggleFilter ? 'soft' : 'outline'"
:variant="toggleFilter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilter = !toggleFilter"
@click="toggleFilter = !toggleFilter" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<USwitch <USwitch
v-model="latestOnly" v-model="latestOnly"
color="primary" color="primary"
size="sm" size="sm"
:label="latestOnly ? 'Latest Only' : 'All Loaded'" :label="latestOnly ? 'Latest Only' : 'All Loaded'"
:ui="{ root: 'items-center gap-2', wrapper: 'ms-0 text-xs text-toned' }" :ui="{ root: 'items-center gap-2', wrapper: 'ms-0 text-xs text-toned' }"
/> />
</div>
<div v-if="toggleFilter && logs.length > 0" class="relative w-full xl:w-80"> <UInput
<span v-if="toggleFilter && logs.length > 0"
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned" id="filter"
> v-model.lazy="query"
<UIcon name="i-lucide-filter" class="size-4" /> type="search"
</span> placeholder="Filter changelog entries"
icon="i-lucide-filter"
<input size="sm"
id="filter" class="order-last w-full sm:order-first sm:w-80"
v-model.lazy="query" />
type="search"
placeholder="Filter changelog entries"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
@ -176,10 +169,6 @@
:description="`No changelog entries found for the query: ${query}.`" :description="`No changelog entries found for the query: ${query}.`"
/> />
<UButton v-if="query" color="neutral" variant="outline" size="sm" @click="query = ''">
Clear filter
</UButton>
<UAlert <UAlert
v-else v-else
color="warning" color="warning"

View file

@ -21,70 +21,64 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton v-if="items.length > 0"
v-if="items.length > 0" color="neutral"
color="neutral" :variant="showFilter ? 'soft' : 'outline'"
:variant="showFilter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilterPanel"
@click="toggleFilterPanel" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-plus" icon="i-lucide-plus"
@click="openCreate" @click="openCreate"
> >
<span>New Condition</span> <span>New Condition</span>
</UButton> </UButton>
<UButton <UButton
v-if="items.length > 0" v-if="items.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
:icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'" :icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex" class="hidden sm:inline-flex"
@click="toggleDisplayStyle" @click="toggleDisplayStyle"
> >
<span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span> <span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
</UButton> </UButton>
<UButton <UButton
v-if="items.length > 0" v-if="items.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-refresh-cw" icon="i-lucide-refresh-cw"
:loading="isLoading" :loading="isLoading"
:disabled="isLoading" :disabled="isLoading"
@click="() => void reloadContent()" @click="() => void reloadContent()"
> >
<span>Reload</span> <span>Reload</span>
</UButton> </UButton>
</div>
<div v-if="showFilter && items.length > 0" class="relative w-full xl:w-80"> <UInput
<span v-if="showFilter && items.length > 0"
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned" id="filter"
> ref="filterInput"
<UIcon name="i-lucide-filter" class="size-4" /> v-model="query"
</span> type="search"
<input placeholder="Filter displayed content"
id="filter" icon="i-lucide-filter"
ref="filterInput" size="sm"
v-model="query" class="order-last w-full sm:order-first sm:w-80"
type="search" />
placeholder="Filter displayed content"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
@ -432,10 +426,6 @@
title="No Results" title="No Results"
:description="`No results found for the query: ${query}. Please try a different search term.`" :description="`No results found for the query: ${query}. Please try a different search term.`"
/> />
<UButton color="neutral" variant="outline" size="sm" @click="query = ''">
Clear filter
</UButton>
</div> </div>
<UAlert <UAlert
@ -479,8 +469,8 @@
<UModal <UModal
v-if="editorOpen" v-if="editorOpen"
:open="editorOpen" :open="editorOpen"
:title="modalTitle" :title="itemRef ? `Edit - ${item.name || 'Condition'}` : 'Add new condition'"
:description="modalDescription" description="Run yt-dlp custom match filter on returned info. and apply options."
:dismissible="!conditions.addInProgress.value" :dismissible="!conditions.addInProgress.value"
:ui="{ content: 'w-full sm:max-w-6xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }" :ui="{ content: 'w-full sm:max-w-6xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }"
@update:open="handleEditorOpenChange" @update:open="handleEditorOpenChange"
@ -534,7 +524,7 @@ const editorOpen = ref(false);
const editorDirty = ref(false); const editorDirty = ref(false);
const query = ref(''); const query = ref('');
const showFilter = ref(false); const showFilter = ref(false);
const filterInput = ref<HTMLInputElement | null>(null); const filterInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const selectedIds = ref<number[]>([]); const selectedIds = ref<number[]>([]);
const massDelete = ref(false); const massDelete = ref(false);
@ -576,12 +566,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
], ],
]); ]);
const modalTitle = computed(() =>
itemRef.value ? `Edit - ${item.value.name}` : 'Add new condition',
);
const modalDescription = computed(
() => 'Run yt-dlp custom match filter on returned info. and apply options.',
);
const modalKey = computed( const modalKey = computed(
() => `${itemRef.value ?? 'new'}-${editorOpen.value ? 'open' : 'closed'}`, () => `${itemRef.value ?? 'new'}-${editorOpen.value ? 'open' : 'closed'}`,
); );
@ -639,7 +623,7 @@ const toggleFilterPanel = async (): Promise<void> => {
} }
await nextTick(); await nextTick();
filterInput.value?.focus(); filterInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
}; };
const loadContent = async (pageNumber = 1): Promise<void> => { const loadContent = async (pageNumber = 1): Promise<void> => {

View file

@ -1,5 +1,5 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="flex min-h-0 w-full min-w-0 max-w-full flex-1 flex-col gap-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div class="flex min-w-0 items-start gap-3"> <div class="flex min-w-0 items-start gap-3">
<span <span
@ -17,37 +17,7 @@
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
<UBadge :color="sessionStatusColor" variant="soft" size="sm">
<span class="inline-flex items-center gap-1.5">
<UIcon
:name="sessionStatusIcon"
class="size-3.5"
:class="sessionStatusSpinning ? 'animate-spin' : ''"
/>
<span>{{ sessionStatusLabel }}</span>
</span>
</UBadge>
<UBadge v-if="hasActiveSession" color="neutral" variant="outline" size="sm">
Session {{ shortSessionId }}
</UBadge>
<UBadge
v-if="sessionExitCode !== null && !isLoading"
:color="exitCodeBadgeColor"
variant="outline"
size="sm"
>
Exit {{ sessionExitCode }}
</UBadge>
<UBadge v-if="commandHistory.length > 0" color="neutral" variant="outline" size="sm">
{{ commandHistory.length }} saved
</UBadge>
</div>
<p class="max-w-3xl text-sm text-toned">{{ sessionStatusDescription }}</p>
</div> </div>
</div> </div>
@ -75,13 +45,15 @@
</div> </div>
</div> </div>
<UPageCard variant="naked" :ui="pageCardUi"> <UPageCard variant="naked" :ui="pageCardUi" class="flex min-h-0 flex-1">
<template #body> <template #body>
<div class="space-y-4"> <div class="flex min-h-0 flex-1 flex-col gap-4">
<div class="overflow-hidden rounded-xl border border-default bg-neutral-950/95 shadow-sm"> <div
class="flex min-h-72 min-w-0 flex-1 overflow-hidden rounded-sm border border-default bg-neutral-950/95 shadow-sm"
>
<div <div
ref="terminal_window" ref="terminal_window"
class="terminal-host min-h-[55vh] max-h-[55vh] overflow-hidden" class="terminal-host h-full min-h-0 w-full overflow-hidden"
/> />
</div> </div>
@ -92,14 +64,25 @@
<div class="min-w-0 flex-1 space-y-1"> <div class="min-w-0 flex-1 space-y-1">
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
<div <div
class="flex min-w-0 items-center gap-2 text-sm font-semibold text-highlighted" class="flex min-w-0 flex-wrap items-center gap-2 text-sm font-semibold text-highlighted"
> >
<UIcon name="i-lucide-send" class="size-4 shrink-0 text-toned" /> <UIcon name="i-lucide-send" class="size-4 shrink-0 text-toned" />
<span>Command</span> <span>Command</span>
<UBadge :color="sessionStatusColor" variant="soft" size="sm">
<span class="inline-flex items-center gap-1.5">
<UIcon
:name="sessionStatusIcon"
class="size-3.5"
:class="sessionStatusSpinning ? 'animate-spin' : ''"
/>
<span>{{ sessionStatusLabel }}</span>
</span>
</UBadge>
</div> </div>
<UBadge :color="inputModeColor" variant="soft" size="sm"> <UBadge :color="isMultiLineInput ? 'info' : 'neutral'" variant="soft" size="sm">
{{ inputModeLabel }} {{ isMultiLineInput ? 'Multi-line' : 'Single-line' }}
</UBadge> </UBadge>
</div> </div>
@ -120,37 +103,13 @@
:description="sessionError" :description="sessionError"
/> />
<UAlert
v-if="hasYtDlpPrefix"
color="warning"
variant="soft"
icon="i-lucide-triangle-alert"
title="Remove the yt-dlp prefix"
>
<template #description>
<p class="text-sm text-default">
Enter only the URLs and flags. This page already runs commands as
<code>yt-dlp &lt;your command&gt;</code>.
</p>
</template>
</UAlert>
<UAlert <UAlert
v-if="sessionStatus === 'reconnecting'" v-if="sessionStatus === 'reconnecting'"
color="warning" color="warning"
variant="soft" variant="soft"
icon="i-lucide-rotate-cw" icon="i-lucide-rotate-cw"
title="Reconnecting to the live stream" title="Reconnecting to the command stream"
description="The command is still tracked in the background and the page is trying to reattach automatically." description="The connection was lost. Attempting to reconnect and restore the stream."
/>
<UAlert
v-if="showExpiredAlert"
color="warning"
variant="soft"
icon="i-lucide-clock-3"
title="Session expired"
description="The saved transcript has aged out, but the last command was restored so you can run it again."
/> />
<UAlert <UAlert
@ -159,16 +118,7 @@
variant="soft" variant="soft"
icon="i-lucide-circle-off" icon="i-lucide-circle-off"
title="Session interrupted" title="Session interrupted"
description="The last command did not finish cleanly. Inspect the transcript above or rerun the command below." description="The command execution was interrupted."
/>
<UAlert
v-if="showNonZeroExitAlert"
color="warning"
variant="soft"
icon="i-lucide-badge-alert"
:title="`Command exited with code ${sessionExitCode}`"
description="The transcript is preserved so you can review the output or run the command again."
/> />
<div class="grid gap-3 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-end"> <div class="grid gap-3 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-end">
@ -180,8 +130,8 @@
class="console-input" class="console-input"
:options="ytDlpOptions" :options="ytDlpOptions"
:disabled="isStartBlocked" :disabled="isStartBlocked"
:icon="commandInputIcon" :icon="isLoading ? 'i-lucide-loader-circle' : 'i-lucide-terminal'"
:icon-class="commandInputIconClass" :icon-class="isLoading ? 'animate-spin' : ''"
placeholder="--help" placeholder="--help"
:rows="5" :rows="5"
@keydown="handleKeyDown" @keydown="handleKeyDown"
@ -194,8 +144,8 @@
class="console-input" class="console-input"
:options="ytDlpOptions" :options="ytDlpOptions"
:disabled="isStartBlocked" :disabled="isStartBlocked"
:icon="commandInputIcon" :icon="isLoading ? 'i-lucide-loader-circle' : 'i-lucide-terminal'"
:icon-class="commandInputIconClass" :icon-class="isLoading ? 'animate-spin' : ''"
placeholder="--help" placeholder="--help"
:multiple="true" :multiple="true"
:allowShortFlags="true" :allowShortFlags="true"
@ -348,11 +298,19 @@
} }
.terminal-host :deep(.xterm) { .terminal-host :deep(.xterm) {
height: 100%;
padding: 0.75rem !important; padding: 0.75rem !important;
} }
.terminal-host :deep(.xterm-viewport) { .terminal-host :deep(.xterm-viewport) {
-ms-overflow-style: none;
background-color: transparent !important; background-color: transparent !important;
scrollbar-width: none;
}
.terminal-host :deep(.xterm-viewport::-webkit-scrollbar) {
width: 0;
height: 0;
} }
</style> </style>
@ -386,8 +344,8 @@ const dialog = useDialog();
const consoleSession = useConsoleSession(); const consoleSession = useConsoleSession();
const pageShell = requirePageShell('console'); const pageShell = requirePageShell('console');
const terminal = ref<Terminal | null>(null); const terminal = shallowRef<Terminal | null>(null);
const terminalFit = ref<FitAddon | null>(null); const terminalFit = shallowRef<FitAddon | null>(null);
const command = ref(''); const command = ref('');
const manualReconnectPending = ref(false); const manualReconnectPending = ref(false);
const cancelPending = ref(false); const cancelPending = ref(false);
@ -398,10 +356,10 @@ const storedCommand = useStorage<string>('console_command', '');
const commandHistory = useStorage<string[]>('console_command_history', []); const commandHistory = useStorage<string[]>('console_command_history', []);
const pageCardUi = { const pageCardUi = {
root: 'w-full bg-transparent', root: 'flex min-h-0 flex-1 w-full bg-transparent',
container: 'w-full p-0 sm:p-0', container: 'flex min-h-0 flex-1 w-full p-0 sm:p-0',
wrapper: 'w-full items-stretch', wrapper: 'flex min-h-0 flex-1 w-full items-stretch',
body: 'w-full', body: 'flex min-h-0 flex-1 w-full flex-col',
}; };
const historyCardUi = { const historyCardUi = {
@ -419,11 +377,9 @@ const sessionStatus = computed(() => consoleSession.state.value.status);
const sessionError = computed(() => consoleSession.state.value.error); const sessionError = computed(() => consoleSession.state.value.error);
const sessionExitCode = computed(() => consoleSession.state.value.exitCode); const sessionExitCode = computed(() => consoleSession.state.value.exitCode);
const hasActiveSession = computed(() => Boolean(consoleSession.state.value.sessionId)); const hasActiveSession = computed(() => Boolean(consoleSession.state.value.sessionId));
const shortSessionId = computed(() => consoleSession.state.value.sessionId?.slice(0, 8) ?? '');
const displayCommand = computed(() => command.value.trim().replace(/^yt-dlp\b\s*/i, '')); const displayCommand = computed(() => command.value.trim().replace(/^yt-dlp\b\s*/i, ''));
const runnableCommand = computed(() => displayCommand.value.replace(/\n/g, ' ').trim()); const runnableCommand = computed(() => displayCommand.value.replace(/\n/g, ' ').trim());
const hasValidCommand = computed(() => Boolean(runnableCommand.value)); const hasValidCommand = computed(() => Boolean(runnableCommand.value));
const hasYtDlpPrefix = computed(() => /^yt-dlp\b/i.test(command.value.trim()));
const isLoading = computed(() => consoleSession.isLoading.value); const isLoading = computed(() => consoleSession.isLoading.value);
const isMultiLineInput = computed(() => Boolean(command.value && command.value.includes('\n'))); const isMultiLineInput = computed(() => Boolean(command.value && command.value.includes('\n')));
const historyEntries = computed(() => commandHistory.value); const historyEntries = computed(() => commandHistory.value);
@ -433,22 +389,6 @@ const showCancelButton = computed(() =>
); );
const isStartBlocked = computed(() => isLoading.value); const isStartBlocked = computed(() => isLoading.value);
const canStartCommand = computed(() => !isStartBlocked.value && hasValidCommand.value); const canStartCommand = computed(() => !isStartBlocked.value && hasValidCommand.value);
const showExpiredAlert = computed(
() => sessionStatus.value === 'expired' && Boolean(consoleSession.state.value.command),
);
const showNonZeroExitAlert = computed(
() =>
!isLoading.value && typeof sessionExitCode.value === 'number' && sessionExitCode.value !== 0,
);
const inputModeLabel = computed(() => (isMultiLineInput.value ? 'Multi-line' : 'Single-line'));
const inputModeColor = computed(() => (isMultiLineInput.value ? 'info' : 'neutral'));
const commandInputIcon = computed(() =>
isLoading.value ? 'i-lucide-loader-circle' : 'i-lucide-terminal',
);
const commandInputIconClass = computed(() => (isLoading.value ? 'animate-spin' : ''));
const exitCodeBadgeColor = computed(() => {
return sessionExitCode.value === 0 ? 'success' : 'error';
});
const runButtonLabel = computed(() => { const runButtonLabel = computed(() => {
if (runnableCommand.value === 'clear') { if (runnableCommand.value === 'clear') {
return 'Clear output'; return 'Clear output';
@ -483,7 +423,7 @@ const sessionStatusLabel = computed(() => {
return 'Reconnecting'; return 'Reconnecting';
case 'finished': case 'finished':
return 'Finished'; return sessionExitCode.value === 0 ? 'Finished' : 'Failed';
case 'interrupted': case 'interrupted':
return 'Interrupted'; return 'Interrupted';
@ -510,7 +450,7 @@ const sessionStatusColor = computed(() => {
return 'warning'; return 'warning';
case 'finished': case 'finished':
return 'success'; return sessionExitCode.value === 0 ? 'success' : 'error';
case 'error': case 'error':
return 'error'; return 'error';
@ -527,7 +467,7 @@ const sessionStatusIcon = computed(() => {
return 'i-lucide-loader-circle'; return 'i-lucide-loader-circle';
case 'finished': case 'finished':
return 'i-lucide-circle-check'; return sessionExitCode.value === 0 ? 'i-lucide-circle-check' : 'i-lucide-triangle-alert';
case 'interrupted': case 'interrupted':
return 'i-lucide-circle-off'; return 'i-lucide-circle-off';
@ -543,38 +483,6 @@ const sessionStatusIcon = computed(() => {
} }
}); });
const sessionStatusSpinning = computed(() => ACTIVE_SESSION_STATUSES.includes(sessionStatus.value)); const sessionStatusSpinning = computed(() => ACTIVE_SESSION_STATUSES.includes(sessionStatus.value));
const sessionStatusDescription = computed(() => {
switch (sessionStatus.value) {
case 'starting':
case 'running':
return 'The command keeps running even if you reload or navigate away.';
case 'reconnecting':
return 'The command is still tracked in the background while the page reconnects to the stream.';
case 'finished':
return typeof sessionExitCode.value === 'number' && sessionExitCode.value !== 0
? `The last command finished with exit code ${sessionExitCode.value}.`
: 'The last command completed and its transcript is still available.';
case 'interrupted':
return 'The last command was interrupted before it completed.';
case 'expired':
return 'The transcript expired, but you can rerun the restored command below.';
case 'error':
return typeof sessionExitCode.value === 'number'
? `The last command failed with exit code ${sessionExitCode.value}.`
: hasActiveSession.value
? 'The live stream ran into a problem while the page was attached to the session.'
: 'The command request failed before a durable session could be restored.';
default:
return 'Run yt-dlp commands directly in a non-interactive session.';
}
});
watch(command, (value) => { watch(command, (value) => {
storedCommand.value = value; storedCommand.value = value;
}); });

View file

@ -21,70 +21,64 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton v-if="items.length > 0"
v-if="items.length > 0" color="neutral"
color="neutral" :variant="showFilter ? 'soft' : 'outline'"
:variant="showFilter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilterPanel"
@click="toggleFilterPanel" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-plus" icon="i-lucide-plus"
@click="openCreate" @click="openCreate"
> >
<span>New Field</span> <span>New Field</span>
</UButton> </UButton>
<UButton <UButton
v-if="items.length > 0" v-if="items.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
:icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'" :icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex" class="hidden sm:inline-flex"
@click="toggleDisplayStyle" @click="toggleDisplayStyle"
> >
<span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span> <span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
</UButton> </UButton>
<UButton <UButton
v-if="items.length > 0" v-if="items.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-refresh-cw" icon="i-lucide-refresh-cw"
:loading="isLoading" :loading="isLoading"
:disabled="isLoading" :disabled="isLoading"
@click="() => void reloadContent()" @click="() => void reloadContent()"
> >
<span>Reload</span> <span>Reload</span>
</UButton> </UButton>
</div>
<div v-if="showFilter && items.length > 0" class="relative w-full xl:w-80"> <UInput
<span v-if="showFilter && items.length > 0"
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned" id="filter"
> ref="filterInput"
<UIcon name="i-lucide-filter" class="size-4" /> v-model="query"
</span> type="search"
<input placeholder="Filter displayed content"
id="filter" icon="i-lucide-filter"
ref="filterInput" size="sm"
v-model="query" class="order-last w-full sm:order-first sm:w-80"
type="search" />
placeholder="Filter displayed content"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
@ -384,10 +378,6 @@
title="No Results" title="No Results"
:description="`No results found for the query: ${query}. Please try a different search term.`" :description="`No results found for the query: ${query}. Please try a different search term.`"
/> />
<UButton color="neutral" variant="outline" size="sm" @click="query = ''">
Clear filter
</UButton>
</div> </div>
<UAlert <UAlert
@ -402,8 +392,8 @@
<UModal <UModal
v-if="editorOpen" v-if="editorOpen"
:open="editorOpen" :open="editorOpen"
:title="modalTitle" :title="itemRef ? `Edit - ${item.name || 'Field'}` : 'Add new field'"
:description="modalDescription" description="Custom fields allow you to add new fields to the download form."
:dismissible="!dlFields.addInProgress.value" :dismissible="!dlFields.addInProgress.value"
:ui="{ content: 'w-full sm:max-w-5xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }" :ui="{ content: 'w-full sm:max-w-5xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }"
@update:open="handleEditorOpenChange" @update:open="handleEditorOpenChange"
@ -455,7 +445,7 @@ const editorOpen = ref(false);
const editorDirty = ref(false); const editorDirty = ref(false);
const query = ref(''); const query = ref('');
const showFilter = ref(false); const showFilter = ref(false);
const filterInput = ref<HTMLInputElement | null>(null); const filterInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const selectedIds = ref<number[]>([]); const selectedIds = ref<number[]>([]);
const massDelete = ref(false); const massDelete = ref(false);
@ -495,10 +485,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
], ],
]); ]);
const modalTitle = computed(() => (itemRef.value ? `Edit - ${item.value.name}` : 'Add new field'));
const modalDescription = computed(
() => 'Custom fields allow you to add new fields to the download form.',
);
const modalKey = computed( const modalKey = computed(
() => `${itemRef.value ?? 'new'}-${editorOpen.value ? 'open' : 'closed'}`, () => `${itemRef.value ?? 'new'}-${editorOpen.value ? 'open' : 'closed'}`,
); );
@ -556,7 +542,7 @@ const toggleFilterPanel = async (): Promise<void> => {
} }
await nextTick(); await nextTick();
filterInput.value?.focus(); filterInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
}; };
const loadContent = async (pageNumber = 1): Promise<void> => { const loadContent = async (pageNumber = 1): Promise<void> => {

View file

@ -21,61 +21,59 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton color="neutral"
color="neutral" :variant="toggleFilter ? 'soft' : 'outline'"
:variant="toggleFilter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilter = !toggleFilter"
@click="toggleFilter = !toggleFilter" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<UButton <UButton
v-if="false === config.paused" v-if="false === config.paused"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-pause" icon="i-lucide-pause"
@click="() => void pauseDownload()" @click="() => void pauseDownload()"
> >
<span>Pause</span> <span>Pause</span>
</UButton> </UButton>
<UButton <UButton
v-else v-else
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-play" icon="i-lucide-play"
@click="() => void resumeDownload()" @click="() => void resumeDownload()"
> >
<span>Resume</span> <span>Resume</span>
</UButton> </UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-plus" icon="i-lucide-plus"
@click="config.showForm = !config.showForm" @click="config.showForm = !config.showForm"
> >
<span>Add</span> <span>Add</span>
</UButton> </UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'" :icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex" class="hidden sm:inline-flex"
@click="changeDisplay" @click="changeDisplay"
> >
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span> <span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
</UButton> </UButton>
</div>
<UInput <UInput
v-if="toggleFilter" v-if="toggleFilter"
@ -85,7 +83,7 @@
placeholder="Filter displayed content" placeholder="Filter displayed content"
icon="i-lucide-filter" icon="i-lucide-filter"
size="sm" size="sm"
class="w-full xl:w-80" class="order-last w-full sm:order-first sm:w-80"
/> />
</div> </div>
</div> </div>
@ -123,17 +121,26 @@
</section> </section>
<section id="history" class="scroll-mt-24 space-y-4"> <section id="history" class="scroll-mt-24 space-y-4">
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-default pb-3"> <div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div class="space-y-1.5"> <div class="flex min-w-0 items-center gap-3">
<div class="flex items-center gap-2 text-base font-semibold text-highlighted"> <span
<UIcon :name="historyShell.icon" class="size-4 text-toned" /> class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
<h2>{{ historyShell.pageLabel }}</h2> >
<UIcon :name="historyShell.icon" class="size-5" />
</span>
<div class="min-w-0 space-y-2">
<div
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ historyShell.sectionLabel }}</span>
<span>/</span>
<span>{{ historyShell.pageLabel }}</span>
</div>
<p class="max-w-3xl text-sm text-toned">{{ historyShell.description }}</p>
</div> </div>
<p class="text-sm text-toned">{{ historyShell.description }}</p>
</div> </div>
<UBadge color="neutral" variant="soft" size="sm">{{ historyCount }}</UBadge>
</div> </div>
<History <History
@ -205,9 +212,8 @@ onMounted(async () => {
}); });
const queueCount = computed(() => stateStore.count('queue')); const queueCount = computed(() => stateStore.count('queue'));
const historyCount = computed(() => stateStore.count('history'));
const hasDownloadsContent = computed( const hasDownloadsContent = computed(
() => queueCount.value > 0 || historyCount.value > 0 || query.value.trim().length > 0, () => queueCount.value > 0 || stateStore.count('history') > 0 || query.value.trim().length > 0,
); );
watch(toggleFilter, () => { watch(toggleFilter, () => {

View file

@ -25,104 +25,61 @@
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
</div> </div>
<UBadge v-if="loading" color="info" variant="soft" size="sm">Loading history</UBadge>
<UBadge color="neutral" variant="soft" size="sm">
{{ filteredLogs.length }} shown
</UBadge>
<UBadge
v-if="logs.length !== filteredLogs.length"
color="neutral"
variant="outline"
size="sm"
>
{{ logs.length }} loaded
</UBadge>
<UBadge v-if="hasActiveFilter" color="warning" variant="soft" size="sm">
{{ matchCount }} matches
</UBadge>
<UBadge v-if="reachedEnd && !hasActiveFilter" color="neutral" variant="soft" size="sm">
Start of file loaded
</UBadge>
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p> <p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton v-if="!autoScroll"
v-if="!autoScroll" color="neutral"
color="neutral" variant="outline"
variant="outline" size="sm"
size="sm" icon="i-lucide-arrow-down"
icon="i-lucide-arrow-down" @click="scrollToBottom(false)"
@click="scrollToBottom(false)" >
> Jump to Live Tail
Jump to Live Tail </UButton>
</UButton>
<UButton <UButton
color="neutral" color="neutral"
:variant="toggleFilter ? 'soft' : 'outline'" :variant="toggleFilter ? 'soft' : 'outline'"
size="sm" size="sm"
icon="i-lucide-filter" icon="i-lucide-filter"
@click="toggleFilter = !toggleFilter" @click="toggleFilter = !toggleFilter"
> >
Filter Filter
</UButton> </UButton>
<UButton <UButton
color="neutral" color="neutral"
:variant="textWrap ? 'soft' : 'outline'" :variant="textWrap ? 'soft' : 'outline'"
size="sm" size="sm"
icon="i-lucide-wrap-text" icon="i-lucide-wrap-text"
:aria-pressed="textWrap" :aria-pressed="textWrap"
:title="textWrap ? 'Wrap lines enabled' : 'Wrap lines disabled'" :title="textWrap ? 'Wrap lines enabled' : 'Wrap lines disabled'"
:class="[ :class="['transition-all', textWrap ? '-translate-y-px ring ring-default shadow-xs' : '']"
'transition-all', @click="textWrap = !textWrap"
textWrap ? '-translate-y-px ring ring-default shadow-xs' : '', >
]" Wrap lines
@click="textWrap = !textWrap" </UButton>
>
Wrap lines
</UButton>
</div>
<div v-if="toggleFilter || query" class="relative w-full xl:w-80"> <UInput
<span v-if="toggleFilter || query"
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned" id="filter"
> v-model.lazy="query"
<UIcon name="i-lucide-filter" class="size-4" /> type="search"
</span> placeholder="Filter displayed content"
icon="i-lucide-filter"
<input size="sm"
id="filter" class="order-last w-full sm:order-first sm:w-80"
v-model.lazy="query" />
type="search"
placeholder="Filter displayed content"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
<UPageCard variant="naked" :ui="pageCardUi"> <UPageCard variant="naked" :ui="pageCardUi">
<template #body> <template #body>
<div class="w-full min-w-0 max-w-full space-y-3"> <div class="w-full min-w-0 max-w-full space-y-3">
<div class="flex flex-wrap items-center justify-end gap-2 text-xs text-toned">
<span v-if="searchTerm">
Query: <code>{{ searchTerm }}</code>
</span>
<span v-if="filterContext > 0">
Context: <code>{{ filterContext }}</code>
</span>
</div>
<div class="w-full min-w-0 max-w-full overflow-hidden"> <div class="w-full min-w-0 max-w-full overflow-hidden">
<div <div
ref="logContainer" ref="logContainer"
@ -164,24 +121,24 @@
</article> </article>
</div> </div>
<div <div v-else class="space-y-3 font-sans text-sm leading-normal">
v-else <UAlert
class="rounded-xl border border-default bg-muted/20 px-4 py-6 text-center" v-if="query"
> color="warning"
<div class="space-y-2"> variant="soft"
<p class="text-sm font-semibold text-highlighted"> icon="i-lucide-search"
{{ title="No Results"
hasActiveFilter :description="`No log lines found for the query: ${query}. Please try a different search term.`"
? 'No log lines match the current filter.' />
: 'No log lines are available yet.'
}}
</p>
<p v-if="hasActiveFilter" class="text-xs text-toned"> <UAlert
Try a different term or clear <code>{{ query }}</code v-else
>. color="warning"
</p> variant="soft"
</div> icon="i-lucide-circle-alert"
title="No log lines"
description="No log lines are available yet."
/>
</div> </div>
<div ref="bottomMarker" /> <div ref="bottomMarker" />
@ -320,8 +277,6 @@ const filteredLogs = computed<FilteredLogEntry[]>(() => {
return result; return result;
}); });
const matchCount = computed(() => filteredLogs.value.filter((entry) => entry.isMatch).length);
const fetchLogs = async (): Promise<void> => { const fetchLogs = async (): Promise<void> => {
loading.value = true; loading.value = true;

View file

@ -21,83 +21,77 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton v-if="notifications.length > 0"
v-if="notifications.length > 0" color="neutral"
color="neutral" :variant="showFilter ? 'soft' : 'outline'"
:variant="showFilter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilterPanel"
@click="toggleFilterPanel" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-plus" icon="i-lucide-plus"
@click="openCreate" @click="openCreate"
> >
<span>New Notification</span> <span>New Notification</span>
</UButton> </UButton>
<UButton <UButton
v-if="notifications.length > 0" v-if="notifications.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-send" icon="i-lucide-send"
:loading="sendingTest" :loading="sendingTest"
:disabled="sendingTest" :disabled="sendingTest"
@click="() => void sendTest()" @click="() => void sendTest()"
> >
<span>Send Test</span> <span>Send Test</span>
</UButton> </UButton>
<UButton <UButton
v-if="notifications.length > 0" v-if="notifications.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
:icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'" :icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex" class="hidden sm:inline-flex"
@click="toggleDisplayStyle" @click="toggleDisplayStyle"
> >
<span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span> <span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
</UButton> </UButton>
<UButton <UButton
v-if="notifications.length > 0" v-if="notifications.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-refresh-cw" icon="i-lucide-refresh-cw"
:loading="isLoading" :loading="isLoading"
:disabled="isLoading" :disabled="isLoading"
@click="() => void reloadContent()" @click="() => void reloadContent()"
> >
<span>Reload</span> <span>Reload</span>
</UButton> </UButton>
</div>
<div v-if="showFilter && notifications.length > 0" class="relative w-full xl:w-80"> <UInput
<span v-if="showFilter && notifications.length > 0"
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned" id="filter"
> ref="filterInput"
<UIcon name="i-lucide-filter" class="size-4" /> v-model="query"
</span> type="search"
<input placeholder="Filter displayed content"
id="filter" icon="i-lucide-filter"
ref="filterInput" size="sm"
v-model="query" class="order-last w-full sm:order-first sm:w-80"
type="search" />
placeholder="Filter displayed content"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
@ -482,10 +476,6 @@
title="No Results" title="No Results"
:description="`No results found for the query: ${query}. Please try a different search term.`" :description="`No results found for the query: ${query}. Please try a different search term.`"
/> />
<UButton color="neutral" variant="outline" size="sm" @click="query = ''">
Clear filter
</UButton>
</div> </div>
<UAlert <UAlert
@ -532,8 +522,8 @@
<UModal <UModal
v-if="editorOpen" v-if="editorOpen"
:open="editorOpen" :open="editorOpen"
:title="modalTitle" :title="targetRef ? `Edit - ${target.name}` : 'Add new notification target'"
:description="modalDescription" description="Send notifications to your webhooks based on specified events or presets."
:dismissible="!addInProgress" :dismissible="!addInProgress"
:ui="{ content: 'w-full sm:max-w-5xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }" :ui="{ content: 'w-full sm:max-w-5xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }"
@update:open="handleEditorOpenChange" @update:open="handleEditorOpenChange"
@ -593,7 +583,7 @@ const editorDirty = ref(false);
const sendingTest = ref(false); const sendingTest = ref(false);
const query = ref(''); const query = ref('');
const showFilter = ref(false); const showFilter = ref(false);
const filterInput = ref<HTMLInputElement | null>(null); const filterInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const selectedIds = ref<number[]>([]); const selectedIds = ref<number[]>([]);
const massDelete = ref(false); const massDelete = ref(false);
@ -604,12 +594,6 @@ const contentStyle = computed<'list' | 'grid'>(() =>
isMobile.value ? 'grid' : displayStyle.value, isMobile.value ? 'grid' : displayStyle.value,
); );
const modalTitle = computed(() =>
targetRef.value ? `Edit - ${target.value.name}` : 'Add new notification target',
);
const modalDescription = computed(
() => 'Send notifications to your webhooks based on specified events or presets.',
);
const modalKey = computed( const modalKey = computed(
() => `${targetRef.value ?? 'new'}-${editorOpen.value ? 'open' : 'closed'}`, () => `${targetRef.value ?? 'new'}-${editorOpen.value ? 'open' : 'closed'}`,
); );
@ -699,7 +683,7 @@ const toggleFilterPanel = async (): Promise<void> => {
} }
await nextTick(); await nextTick();
filterInput.value?.focus(); filterInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
}; };
const loadContent = async (pageNumber = page.value): Promise<void> => { const loadContent = async (pageNumber = page.value): Promise<void> => {

View file

@ -21,69 +21,63 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton v-if="presetsNoDefault.length > 0"
v-if="presetsNoDefault.length > 0" color="neutral"
color="neutral" :variant="showFilter ? 'soft' : 'outline'"
:variant="showFilter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilterPanel"
@click="toggleFilterPanel" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-plus" icon="i-lucide-plus"
@click="editor.openCreate()" @click="editor.openCreate()"
> >
<span>New Preset</span> <span>New Preset</span>
</UButton> </UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'" :icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex" class="hidden sm:inline-flex"
@click="toggleDisplayStyle" @click="toggleDisplayStyle"
> >
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span> <span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
</UButton> </UButton>
<UButton <UButton
v-if="presets.length > 0" v-if="presets.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-refresh-cw" icon="i-lucide-refresh-cw"
:loading="isLoading" :loading="isLoading"
:disabled="isLoading" :disabled="isLoading"
@click="() => void reloadContent()" @click="() => void reloadContent()"
> >
<span>Reload</span> <span>Reload</span>
</UButton> </UButton>
</div>
<div v-if="showFilter && presetsNoDefault.length > 0" class="relative w-full xl:w-80"> <UInput
<span v-if="showFilter && presetsNoDefault.length > 0"
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned" id="filter"
> ref="filterInput"
<UIcon name="i-lucide-filter" class="size-4" /> v-model="query"
</span> type="search"
<input placeholder="Filter displayed content"
id="filter" icon="i-lucide-filter"
ref="filterInput" size="sm"
v-model="query" class="order-last w-full sm:order-first sm:w-80"
type="search" />
placeholder="Filter displayed content"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
@ -413,10 +407,6 @@
title="No Results" title="No Results"
:description="`No results found for the query: ${query}. Please try a different search term.`" :description="`No results found for the query: ${query}. Please try a different search term.`"
/> />
<UButton color="neutral" variant="outline" size="sm" @click="query = ''"
>Clear filter</UButton
>
</div> </div>
<UAlert <UAlert
@ -490,7 +480,7 @@ const isMobile = useMediaQuery({ maxWidth: 639 });
const query = ref(''); const query = ref('');
const showFilter = ref(false); const showFilter = ref(false);
const filterInput = ref<HTMLInputElement | null>(null); const filterInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const selectedIds = ref<number[]>([]); const selectedIds = ref<number[]>([]);
const massDelete = ref(false); const massDelete = ref(false);
@ -562,7 +552,7 @@ const toggleFilterPanel = async (): Promise<void> => {
} }
await nextTick(); await nextTick();
filterInput.value?.focus(); filterInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
}; };
const reloadContent = async (): Promise<void> => { const reloadContent = async (): Promise<void> => {

View file

@ -21,78 +21,72 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton v-if="definitions.length > 0"
v-if="definitions.length > 0" color="neutral"
color="neutral" :variant="showFilter ? 'soft' : 'outline'"
:variant="showFilter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilterPanel"
@click="toggleFilterPanel" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-search" icon="i-lucide-search"
@click="inspect = true" @click="inspect = true"
> >
<span>Inspect</span> <span>Inspect</span>
</UButton> </UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-plus" icon="i-lucide-plus"
@click="openCreate" @click="openCreate"
> >
<span>New Definition</span> <span>New Definition</span>
</UButton> </UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'" :icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex" class="hidden sm:inline-flex"
@click="toggleDisplayStyle" @click="toggleDisplayStyle"
> >
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span> <span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
</UButton> </UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-refresh-cw" icon="i-lucide-refresh-cw"
:loading="isLoading" :loading="isLoading"
:disabled="isLoading" :disabled="isLoading"
@click="() => void reloadContent()" @click="() => void reloadContent()"
> >
<span>Reload</span> <span>Reload</span>
</UButton> </UButton>
</div>
<div v-if="showFilter && definitions.length > 0" class="relative w-full xl:w-80"> <UInput
<span v-if="showFilter && definitions.length > 0"
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned" id="filter"
> ref="filterInput"
<UIcon name="i-lucide-filter" class="size-4" /> v-model="query"
</span> type="search"
<input placeholder="Filter displayed content"
id="filter" icon="i-lucide-filter"
ref="filterInput" size="sm"
v-model="query" class="order-last w-full sm:order-first sm:w-80"
type="search" />
placeholder="Filter displayed content"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
@ -404,10 +398,6 @@
title="No Results" title="No Results"
:description="`No results found for the query: ${query}. Please try a different search term.`" :description="`No results found for the query: ${query}. Please try a different search term.`"
/> />
<UButton color="neutral" variant="outline" size="sm" @click="query = ''"
>Clear filter</UButton
>
</div> </div>
<UAlert <UAlert
@ -422,8 +412,16 @@
<UModal <UModal
v-if="isEditorOpen" v-if="isEditorOpen"
:open="isEditorOpen" :open="isEditorOpen"
:title="editorTitle" :title="
:description="editorDescription" editorMode === 'create'
? 'Create Task Definition'
: `Edit - ${currentSummary?.name || 'Task Definition'}`
"
:description="
editorLoading
? 'Loading full definition before editing.'
: 'Use the GUI editor when it fits, or switch to advanced JSON for full control.'
"
:dismissible="!editorLoading && !editorSubmitting" :dismissible="!editorLoading && !editorSubmitting"
:ui="{ content: 'w-full sm:max-w-7xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }" :ui="{ content: 'w-full sm:max-w-7xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }"
@update:open="handleEditorOpenChange" @update:open="handleEditorOpenChange"
@ -528,7 +526,7 @@ const isMobile = useMediaQuery({ maxWidth: 639 });
const query = ref(''); const query = ref('');
const showFilter = ref(false); const showFilter = ref(false);
const filterInput = ref<HTMLInputElement | null>(null); const filterInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const hideImportByDefault = ref(false); const hideImportByDefault = ref(false);
const selectedIds = ref<number[]>([]); const selectedIds = ref<number[]>([]);
const massDelete = ref(false); const massDelete = ref(false);
@ -590,20 +588,6 @@ const currentSummary = computed<TaskDefinitionSummary | undefined>(() => {
return definitions.value.find((item) => item.id === workingId.value); return definitions.value.find((item) => item.id === workingId.value);
}); });
const editorTitle = computed(() => {
return editorMode.value === 'create'
? 'Create Task Definition'
: `Edit - ${currentSummary.value?.name || 'Task Definition'}`;
});
const editorDescription = computed(() => {
if (editorLoading.value) {
return 'Loading full definition before editing.';
}
return 'Use the GUI editor when it fits, or switch to advanced JSON for full control.';
});
const showImportByDefault = computed( const showImportByDefault = computed(
() => editorMode.value === 'create' && !hideImportByDefault.value, () => editorMode.value === 'create' && !hideImportByDefault.value,
); );
@ -655,7 +639,7 @@ const toggleFilterPanel = async (): Promise<void> => {
} }
await nextTick(); await nextTick();
filterInput.value?.focus(); filterInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
}; };
const reloadContent = async (): Promise<void> => { const reloadContent = async (): Promise<void> => {

View file

@ -21,69 +21,63 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-3 xl:items-end"> <div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<div class="flex flex-wrap gap-2 xl:justify-end"> <UButton
<UButton v-if="tasks.length > 0"
v-if="tasks.length > 0" color="neutral"
color="neutral" :variant="showFilter ? 'soft' : 'outline'"
:variant="showFilter ? 'soft' : 'outline'" size="sm"
size="sm" icon="i-lucide-filter"
icon="i-lucide-filter" @click="toggleFilterPanel"
@click="toggleFilterPanel" >
> <span>Filter</span>
<span>Filter</span> </UButton>
</UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-plus" icon="i-lucide-plus"
@click="openCreateForm" @click="openCreateForm"
> >
<span>New Task</span> <span>New Task</span>
</UButton> </UButton>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
:icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'" :icon="displayStyle === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex" class="hidden sm:inline-flex"
@click="toggleDisplayStyle" @click="toggleDisplayStyle"
> >
<span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span> <span class="hidden sm:inline">{{ displayStyle === 'list' ? 'List' : 'Grid' }}</span>
</UButton> </UButton>
<UButton <UButton
v-if="tasks.length > 0" v-if="tasks.length > 0"
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-refresh-cw" icon="i-lucide-refresh-cw"
:loading="isLoading" :loading="isLoading"
:disabled="isLoading" :disabled="isLoading"
@click="() => void reloadContent()" @click="() => void reloadContent()"
> >
<span>Reload</span> <span>Reload</span>
</UButton> </UButton>
</div>
<div v-if="showFilter && tasks.length > 0" class="relative w-full xl:w-80"> <UInput
<span v-if="showFilter && tasks.length > 0"
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-toned" id="filter"
> ref="filterInput"
<UIcon name="i-lucide-filter" class="size-4" /> v-model="query"
</span> type="search"
<input placeholder="Filter displayed content"
id="filter" icon="i-lucide-filter"
ref="filterInput" size="sm"
v-model="query" class="order-last w-full sm:order-first sm:w-80"
type="search" />
placeholder="Filter displayed content"
class="w-full rounded-md border border-default bg-elevated py-2 pr-3 pl-9 text-sm text-default outline-none transition focus:border-primary"
/>
</div>
</div> </div>
</div> </div>
@ -623,10 +617,6 @@
title="No Results" title="No Results"
:description="`No results found for the query: ${query}. Please try a different search term.`" :description="`No results found for the query: ${query}. Please try a different search term.`"
/> />
<UButton color="neutral" variant="outline" size="sm" @click="query = ''">
Clear filter
</UButton>
</div> </div>
<UAlert <UAlert
@ -672,8 +662,12 @@
<UModal <UModal
v-if="toggleForm" v-if="toggleForm"
:open="toggleForm" :open="toggleForm"
:title="editorTitle" :title="taskRef ? `Edit - ${task.name || 'Task'}` : 'Add new task'"
:description="editorDescription" :description="
taskRef
? 'Update the settings of your automated download task'
: 'Create an automated download task'
"
:dismissible="!addInProgress" :dismissible="!addInProgress"
:ui="{ content: 'w-full sm:max-w-7xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }" :ui="{ content: 'w-full sm:max-w-7xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }"
@update:open="handleEditorOpenChange" @update:open="handleEditorOpenChange"
@ -767,7 +761,7 @@ const massDelete = ref(false);
const inspectTask = ref<Task | null>(null); const inspectTask = ref<Task | null>(null);
const query = ref(''); const query = ref('');
const showFilter = ref(false); const showFilter = ref(false);
const filterInput = ref<HTMLInputElement | null>(null); const filterInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const CACHE_KEY = 'tasks:handler_support'; const CACHE_KEY = 'tasks:handler_support';
const taskHandlerSupport = ref<Record<string, boolean>>(sessionCache.get(CACHE_KEY) || {}); const taskHandlerSupport = ref<Record<string, boolean>>(sessionCache.get(CACHE_KEY) || {});
@ -780,16 +774,6 @@ const contentStyle = computed<'list' | 'grid'>(() =>
const editorSessionId = ref(0); const editorSessionId = ref(0);
const editorTitle = computed(() => {
return taskRef.value ? `Edit - ${task.value.name}` : 'Add new task';
});
const editorDescription = computed(() => {
return taskRef.value
? 'Update the settings of your automated download task'
: 'Create an automated download task';
});
const formKey = computed(() => `${taskRef.value ?? 'new'}:${editorSessionId.value}`); const formKey = computed(() => `${taskRef.value ?? 'new'}:${editorSessionId.value}`);
const discardEditor = (): void => { const discardEditor = (): void => {
@ -885,7 +869,7 @@ const toggleFilterPanel = async (): Promise<void> => {
} }
await nextTick(); await nextTick();
filterInput.value?.focus(); filterInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
}; };
const toggleMasterSelection = (): void => { const toggleMasterSelection = (): void => {