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'
"
title="Filter Status"
:description="logicStatusText"
:description="
testData.data.status === null ? 'Not tested' : logicTest ? 'Matched' : 'Not matched'
"
/>
<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> => {
for (const key of ['name', 'filter'] as const) {
if (!form[key]) {

View file

@ -5,7 +5,7 @@
>
<div class="min-w-0 flex-1">
<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">
<span class="truncate" :class="{ 'has-tooltip': field }">
{{ label }}
@ -52,7 +52,7 @@
</template>
<template v-else>
<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">
<span :class="{ 'has-tooltip': field }">
{{ label }}
@ -142,14 +142,6 @@ const boolModel = computed({
},
});
const resolvedIcon = computed(() => {
if (!props.icon) {
return '';
}
return props.icon;
});
const fieldUi = computed(() => ({
container: props.compact ? 'space-y-1.5' : 'space-y-2',
description: props.compact ? 'text-xs text-toned' : 'text-sm text-toned',

View file

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

View file

@ -32,15 +32,24 @@
</UDropdownMenu>
</div>
<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 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-history" class="size-3.5" />
<span>Total: {{ stateStore.count('history') }}</span>
</span>
</UBadge>
<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>
<UAlert
@ -599,15 +608,6 @@
<li><code>source_name:task_name</code> - items added by the specified task.</li>
</ul>
</div>
<UButton
color="neutral"
variant="outline"
size="sm"
@click="() => emitter('clear_search')"
>
Clear filter
</UButton>
</div>
</template>
</UAlert>

View file

@ -202,6 +202,24 @@
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-accent: color-mix(in oklab, var(--ui-border-accented) 70%, transparent);
margin-top: 1rem;
@ -294,6 +312,18 @@ const content = ref('');
const error = ref('');
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 parser = new Marked();
@ -370,7 +400,9 @@ const createMarkdownParser = () => {
const refPolicy = ' referrerpolicy="no-referrer"';
const crossorigin = token._isExternalImage ? ' crossorigin="anonymous"' : '';
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>
</UDropdownMenu>
</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
@ -548,15 +557,6 @@
<li><code>source_name:task_name</code> - items added by the specified task.</li>
</ul>
</div>
<UButton
color="neutral"
variant="outline"
size="sm"
@click="() => emitter('clear_search')"
>
Clear filter
</UButton>
</div>
</template>
</UAlert>

View file

@ -44,80 +44,74 @@
</div>
</div>
<div class="flex flex-col gap-3 xl:items-end">
<div class="flex flex-wrap gap-2 xl:justify-end">
<UButton
color="neutral"
:variant="show_filter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilter"
>
<span>Filter</span>
</UButton>
<div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<UButton
color="neutral"
:variant="show_filter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilter"
>
<span>Filter</span>
</UButton>
<UButton
v-if="controlEnabled"
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-folder-plus"
@click="() => void handleCreateDirectory()"
>
<span>New Folder</span>
</UButton>
<UButton
v-if="controlEnabled"
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-folder-plus"
@click="() => void handleCreateDirectory()"
>
<span>New Folder</span>
</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
color="neutral"
variant="outline"
size="sm"
:icon="display_style === 'list' ? 'i-lucide-list' : 'i-lucide-grid-2x2'"
class="hidden sm:inline-flex"
@click="toggleDisplayStyle"
icon="i-lucide-arrow-up-down"
trailing-icon="i-lucide-chevron-down"
>
<span class="hidden sm:inline">{{ display_style === 'list' ? 'List' : 'Grid' }}</span>
<span>Sort</span>
</UButton>
</UDropdownMenu>
<UDropdownMenu v-if="hasItems" :items="sortGroups" :modal="false">
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-arrow-up-down"
trailing-icon="i-lucide-chevron-down"
>
<span>Sort</span>
</UButton>
</UDropdownMenu>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent(browserPath)"
>
<span>Reload</span>
</UButton>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent(browserPath)"
>
<span>Reload</span>
</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>
<UInput
v-if="show_filter"
id="search"
ref="searchInput"
v-model.lazy="localSearch"
type="search"
placeholder="Filter"
icon="i-lucide-filter"
size="sm"
class="order-last w-full sm:order-first sm:w-80"
/>
</div>
</div>
@ -458,10 +452,6 @@
title="No results"
:description="`No results found for '${localSearch}'.`"
/>
<UButton color="neutral" variant="outline" size="sm" @click="clearFilter"
>Clear filter</UButton
>
</div>
<UAlert
@ -554,7 +544,7 @@ 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);
const searchInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const items = browser.items;
const browserPath = browser.path;
@ -943,7 +933,7 @@ const toggleFilter = async (): Promise<void> => {
}
await nextTick();
searchInput.value?.focus();
searchInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
};
const toggleDisplayStyle = (): void => {

View file

@ -21,43 +21,36 @@
</div>
</div>
<div class="flex flex-col gap-3 xl:items-end">
<div class="flex flex-wrap gap-2 xl:justify-end">
<UButton
v-if="logs.length > 0"
color="neutral"
:variant="toggleFilter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilter = !toggleFilter"
>
<span>Filter</span>
</UButton>
<div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<UButton
v-if="logs.length > 0"
color="neutral"
:variant="toggleFilter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilter = !toggleFilter"
>
<span>Filter</span>
</UButton>
<USwitch
v-model="latestOnly"
color="primary"
size="sm"
:label="latestOnly ? 'Latest Only' : 'All Loaded'"
:ui="{ root: 'items-center gap-2', wrapper: 'ms-0 text-xs text-toned' }"
/>
</div>
<USwitch
v-model="latestOnly"
color="primary"
size="sm"
:label="latestOnly ? 'Latest Only' : 'All Loaded'"
:ui="{ root: 'items-center gap-2', wrapper: 'ms-0 text-xs text-toned' }"
/>
<div v-if="toggleFilter && logs.length > 0" 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="filter"
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>
<UInput
v-if="toggleFilter && logs.length > 0"
id="filter"
v-model.lazy="query"
type="search"
placeholder="Filter changelog entries"
icon="i-lucide-filter"
size="sm"
class="order-last w-full sm:order-first sm:w-80"
/>
</div>
</div>
@ -176,10 +169,6 @@
: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
v-else
color="warning"

View file

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

View file

@ -1,5 +1,5 @@
<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 min-w-0 items-start gap-3">
<span
@ -17,37 +17,7 @@
<span>{{ pageShell.pageLabel }}</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<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>
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
</div>
</div>
@ -75,13 +45,15 @@
</div>
</div>
<UPageCard variant="naked" :ui="pageCardUi">
<UPageCard variant="naked" :ui="pageCardUi" class="flex min-h-0 flex-1">
<template #body>
<div class="space-y-4">
<div class="overflow-hidden rounded-xl border border-default bg-neutral-950/95 shadow-sm">
<div class="flex min-h-0 flex-1 flex-col gap-4">
<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
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>
@ -92,14 +64,25 @@
<div class="min-w-0 flex-1 space-y-1">
<div class="flex flex-wrap items-center justify-between gap-3">
<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" />
<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>
<UBadge :color="inputModeColor" variant="soft" size="sm">
{{ inputModeLabel }}
<UBadge :color="isMultiLineInput ? 'info' : 'neutral'" variant="soft" size="sm">
{{ isMultiLineInput ? 'Multi-line' : 'Single-line' }}
</UBadge>
</div>
@ -120,37 +103,13 @@
: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
v-if="sessionStatus === 'reconnecting'"
color="warning"
variant="soft"
icon="i-lucide-rotate-cw"
title="Reconnecting to the live stream"
description="The command is still tracked in the background and the page is trying to reattach automatically."
/>
<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."
title="Reconnecting to the command stream"
description="The connection was lost. Attempting to reconnect and restore the stream."
/>
<UAlert
@ -159,16 +118,7 @@
variant="soft"
icon="i-lucide-circle-off"
title="Session interrupted"
description="The last command did not finish cleanly. Inspect the transcript above or rerun the command below."
/>
<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."
description="The command execution was interrupted."
/>
<div class="grid gap-3 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-end">
@ -180,8 +130,8 @@
class="console-input"
:options="ytDlpOptions"
:disabled="isStartBlocked"
:icon="commandInputIcon"
:icon-class="commandInputIconClass"
:icon="isLoading ? 'i-lucide-loader-circle' : 'i-lucide-terminal'"
:icon-class="isLoading ? 'animate-spin' : ''"
placeholder="--help"
:rows="5"
@keydown="handleKeyDown"
@ -194,8 +144,8 @@
class="console-input"
:options="ytDlpOptions"
:disabled="isStartBlocked"
:icon="commandInputIcon"
:icon-class="commandInputIconClass"
:icon="isLoading ? 'i-lucide-loader-circle' : 'i-lucide-terminal'"
:icon-class="isLoading ? 'animate-spin' : ''"
placeholder="--help"
:multiple="true"
:allowShortFlags="true"
@ -348,11 +298,19 @@
}
.terminal-host :deep(.xterm) {
height: 100%;
padding: 0.75rem !important;
}
.terminal-host :deep(.xterm-viewport) {
-ms-overflow-style: none;
background-color: transparent !important;
scrollbar-width: none;
}
.terminal-host :deep(.xterm-viewport::-webkit-scrollbar) {
width: 0;
height: 0;
}
</style>
@ -386,8 +344,8 @@ const dialog = useDialog();
const consoleSession = useConsoleSession();
const pageShell = requirePageShell('console');
const terminal = ref<Terminal | null>(null);
const terminalFit = ref<FitAddon | null>(null);
const terminal = shallowRef<Terminal | null>(null);
const terminalFit = shallowRef<FitAddon | null>(null);
const command = ref('');
const manualReconnectPending = ref(false);
const cancelPending = ref(false);
@ -398,10 +356,10 @@ const storedCommand = useStorage<string>('console_command', '');
const commandHistory = useStorage<string[]>('console_command_history', []);
const pageCardUi = {
root: 'w-full bg-transparent',
container: 'w-full p-0 sm:p-0',
wrapper: 'w-full items-stretch',
body: 'w-full',
root: 'flex min-h-0 flex-1 w-full bg-transparent',
container: 'flex min-h-0 flex-1 w-full p-0 sm:p-0',
wrapper: 'flex min-h-0 flex-1 w-full items-stretch',
body: 'flex min-h-0 flex-1 w-full flex-col',
};
const historyCardUi = {
@ -419,11 +377,9 @@ const sessionStatus = computed(() => consoleSession.state.value.status);
const sessionError = computed(() => consoleSession.state.value.error);
const sessionExitCode = computed(() => consoleSession.state.value.exitCode);
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 runnableCommand = computed(() => displayCommand.value.replace(/\n/g, ' ').trim());
const hasValidCommand = computed(() => Boolean(runnableCommand.value));
const hasYtDlpPrefix = computed(() => /^yt-dlp\b/i.test(command.value.trim()));
const isLoading = computed(() => consoleSession.isLoading.value);
const isMultiLineInput = computed(() => Boolean(command.value && command.value.includes('\n')));
const historyEntries = computed(() => commandHistory.value);
@ -433,22 +389,6 @@ const showCancelButton = computed(() =>
);
const isStartBlocked = computed(() => isLoading.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(() => {
if (runnableCommand.value === 'clear') {
return 'Clear output';
@ -483,7 +423,7 @@ const sessionStatusLabel = computed(() => {
return 'Reconnecting';
case 'finished':
return 'Finished';
return sessionExitCode.value === 0 ? 'Finished' : 'Failed';
case 'interrupted':
return 'Interrupted';
@ -510,7 +450,7 @@ const sessionStatusColor = computed(() => {
return 'warning';
case 'finished':
return 'success';
return sessionExitCode.value === 0 ? 'success' : 'error';
case 'error':
return 'error';
@ -527,7 +467,7 @@ const sessionStatusIcon = computed(() => {
return 'i-lucide-loader-circle';
case 'finished':
return 'i-lucide-circle-check';
return sessionExitCode.value === 0 ? 'i-lucide-circle-check' : 'i-lucide-triangle-alert';
case 'interrupted':
return 'i-lucide-circle-off';
@ -543,38 +483,6 @@ const sessionStatusIcon = computed(() => {
}
});
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) => {
storedCommand.value = value;
});

View file

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

View file

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

View file

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

View file

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

View file

@ -21,69 +21,63 @@
</div>
</div>
<div class="flex flex-col gap-3 xl:items-end">
<div class="flex flex-wrap gap-2 xl:justify-end">
<UButton
v-if="presetsNoDefault.length > 0"
color="neutral"
:variant="showFilter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilterPanel"
>
<span>Filter</span>
</UButton>
<div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<UButton
v-if="presetsNoDefault.length > 0"
color="neutral"
:variant="showFilter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilterPanel"
>
<span>Filter</span>
</UButton>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-plus"
@click="editor.openCreate()"
>
<span>New Preset</span>
</UButton>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-plus"
@click="editor.openCreate()"
>
<span>New Preset</span>
</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>
<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>
<UButton
v-if="presets.length > 0"
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent()"
>
<span>Reload</span>
</UButton>
</div>
<UButton
v-if="presets.length > 0"
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent()"
>
<span>Reload</span>
</UButton>
<div v-if="showFilter && presetsNoDefault.length > 0" 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="filter"
ref="filterInput"
v-model="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>
<UInput
v-if="showFilter && presetsNoDefault.length > 0"
id="filter"
ref="filterInput"
v-model="query"
type="search"
placeholder="Filter displayed content"
icon="i-lucide-filter"
size="sm"
class="order-last w-full sm:order-first sm:w-80"
/>
</div>
</div>
@ -413,10 +407,6 @@
title="No Results"
: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>
<UAlert
@ -490,7 +480,7 @@ const isMobile = useMediaQuery({ maxWidth: 639 });
const query = ref('');
const showFilter = ref(false);
const filterInput = ref<HTMLInputElement | null>(null);
const filterInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const selectedIds = ref<number[]>([]);
const massDelete = ref(false);
@ -562,7 +552,7 @@ const toggleFilterPanel = async (): Promise<void> => {
}
await nextTick();
filterInput.value?.focus();
filterInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
};
const reloadContent = async (): Promise<void> => {

View file

@ -21,78 +21,72 @@
</div>
</div>
<div class="flex flex-col gap-3 xl:items-end">
<div class="flex flex-wrap gap-2 xl:justify-end">
<UButton
v-if="definitions.length > 0"
color="neutral"
:variant="showFilter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilterPanel"
>
<span>Filter</span>
</UButton>
<div class="flex min-w-0 flex-wrap items-center gap-2 xl:justify-end">
<UButton
v-if="definitions.length > 0"
color="neutral"
:variant="showFilter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilterPanel"
>
<span>Filter</span>
</UButton>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-search"
@click="inspect = true"
>
<span>Inspect</span>
</UButton>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-search"
@click="inspect = true"
>
<span>Inspect</span>
</UButton>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-plus"
@click="openCreate"
>
<span>New Definition</span>
</UButton>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-plus"
@click="openCreate"
>
<span>New Definition</span>
</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>
<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>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent()"
>
<span>Reload</span>
</UButton>
</div>
<UButton
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent()"
>
<span>Reload</span>
</UButton>
<div v-if="showFilter && definitions.length > 0" 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="filter"
ref="filterInput"
v-model="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>
<UInput
v-if="showFilter && definitions.length > 0"
id="filter"
ref="filterInput"
v-model="query"
type="search"
placeholder="Filter displayed content"
icon="i-lucide-filter"
size="sm"
class="order-last w-full sm:order-first sm:w-80"
/>
</div>
</div>
@ -404,10 +398,6 @@
title="No Results"
: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>
<UAlert
@ -422,8 +412,16 @@
<UModal
v-if="isEditorOpen"
:open="isEditorOpen"
:title="editorTitle"
:description="editorDescription"
:title="
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"
:ui="{ content: 'w-full sm:max-w-7xl', body: 'max-h-[85vh] overflow-y-auto p-4 sm:p-6' }"
@update:open="handleEditorOpenChange"
@ -528,7 +526,7 @@ const isMobile = useMediaQuery({ maxWidth: 639 });
const query = ref('');
const showFilter = ref(false);
const filterInput = ref<HTMLInputElement | null>(null);
const filterInput = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const hideImportByDefault = ref(false);
const selectedIds = ref<number[]>([]);
const massDelete = ref(false);
@ -590,20 +588,6 @@ const currentSummary = computed<TaskDefinitionSummary | undefined>(() => {
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(
() => editorMode.value === 'create' && !hideImportByDefault.value,
);
@ -655,7 +639,7 @@ const toggleFilterPanel = async (): Promise<void> => {
}
await nextTick();
filterInput.value?.focus();
filterInput.value?.inputRef?.value?.focus?.({ preventScroll: true });
};
const reloadContent = async (): Promise<void> => {

View file

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