Refactor: standardize the look and feel of the pages after recent nuxt ui migration
This commit is contained in:
parent
bb246cc6f5
commit
70648c2253
14 changed files with 1222 additions and 942 deletions
|
|
@ -486,22 +486,19 @@ import { ref, onMounted, readonly } from 'vue';
|
|||
import { useStorage } from '@vueuse/core';
|
||||
import moment from 'moment';
|
||||
import type { toastPosition } from '~/composables/useNotification';
|
||||
import { getDocsNavigationEntries } from '~/composables/useDocs';
|
||||
import type { YTDLPOption } from '~/types/ytdlp';
|
||||
import { useDialog } from '~/composables/useDialog';
|
||||
import Dialog from '~/components/Dialog.vue';
|
||||
import Simple from '~/components/Simple.vue';
|
||||
import Shutdown from '~/components/shutdown.vue';
|
||||
import type { version_check } from '~/types';
|
||||
|
||||
type NavEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon: string;
|
||||
to?: string;
|
||||
children?: NavEntry[];
|
||||
};
|
||||
import {
|
||||
getActiveNavItem,
|
||||
getNavItems,
|
||||
getNavSections,
|
||||
isNavItemActive,
|
||||
type NavItem,
|
||||
} from '~/utils/topLevelNavigation';
|
||||
|
||||
type SidebarSection = {
|
||||
id: string;
|
||||
|
|
@ -552,166 +549,51 @@ const navigationUi = (collapsed: boolean) => ({
|
|||
linkLabel: collapsed ? 'hidden' : 'truncate',
|
||||
});
|
||||
|
||||
const navEntryPath = (item: NavEntry): string => (item.to ? item.to.split(/[?#]/)[0] || '/' : '');
|
||||
|
||||
const isNavEntryActive = (item: NavEntry): boolean => {
|
||||
if (item.id === 'downloads') {
|
||||
return route.path === '/' && route.hash !== '#history';
|
||||
}
|
||||
|
||||
if (item.id === 'history') {
|
||||
return route.path === '/' && route.hash === '#history';
|
||||
}
|
||||
|
||||
const path = navEntryPath(item);
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return path === '/'
|
||||
? route.path === '/'
|
||||
: route.path === path || route.path.startsWith(`${path}/`);
|
||||
};
|
||||
|
||||
const makeNavigationItem = (item: NavEntry): NavigationMenuItem => ({
|
||||
const makeNavigationItem = (item: NavItem): NavigationMenuItem => ({
|
||||
label: item.label,
|
||||
icon: item.icon,
|
||||
to: item.to,
|
||||
value: item.id,
|
||||
active: isNavEntryActive(item),
|
||||
active: isNavItemActive(item, route),
|
||||
});
|
||||
|
||||
const docsNavigationEntries = getDocsNavigationEntries();
|
||||
const navigationAvailability = computed(() => ({
|
||||
fileLogging: Boolean(config.app?.file_logging),
|
||||
consoleEnabled: Boolean(config.app?.console_enabled),
|
||||
}));
|
||||
|
||||
const allNavItems = computed<NavEntry[]>(() => [
|
||||
{
|
||||
id: 'downloads',
|
||||
label: 'Downloads',
|
||||
description: 'Manage queued, active, and completed downloads in one workspace.',
|
||||
icon: 'i-lucide-download',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
id: 'history',
|
||||
label: 'History',
|
||||
description: 'Jump to the history section on the downloads page.',
|
||||
icon: 'i-lucide-history',
|
||||
to: '/#history',
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
label: 'Files',
|
||||
description: 'Browse downloaded files.',
|
||||
icon: 'i-lucide-folder-tree',
|
||||
to: '/browser',
|
||||
},
|
||||
{
|
||||
id: 'presets',
|
||||
label: 'Presets',
|
||||
description:
|
||||
'Presets are pre-defined command options for yt-dlp that you want to apply to given download.',
|
||||
icon: 'i-lucide-sliders-horizontal',
|
||||
to: '/presets',
|
||||
},
|
||||
{
|
||||
id: 'custom-fields',
|
||||
label: 'Custom Fields',
|
||||
description: 'Custom fields allow you to add new fields to the download form.',
|
||||
icon: 'i-lucide-braces',
|
||||
to: '/dl_fields',
|
||||
},
|
||||
{
|
||||
id: 'conditions',
|
||||
label: 'Conditions',
|
||||
description: 'Run yt-dlp custom match filter on returned info and apply options.',
|
||||
icon: 'i-lucide-filter',
|
||||
to: '/conditions',
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
label: 'Notifications',
|
||||
description: 'Send notifications to your webhooks based on specified events or presets.',
|
||||
icon: 'i-lucide-bell',
|
||||
to: '/notifications',
|
||||
},
|
||||
{
|
||||
id: 'tasks',
|
||||
label: 'Tasks',
|
||||
icon: 'i-lucide-list-todo',
|
||||
children: [
|
||||
{
|
||||
id: 'tasks-list',
|
||||
label: 'Tasks',
|
||||
description: 'Queue playlist/channels for automatic download at specified intervals.',
|
||||
icon: 'i-lucide-list-todo',
|
||||
to: '/tasks',
|
||||
},
|
||||
{
|
||||
id: 'task-definitions',
|
||||
label: 'Task Definitions',
|
||||
description: 'Create definitions to turn any website into a downloadable feed of links.',
|
||||
icon: 'i-lucide-workflow',
|
||||
to: '/task_definitions',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
label: 'Tools',
|
||||
icon: 'i-lucide-wrench',
|
||||
children: [
|
||||
...(config.app?.file_logging
|
||||
? [
|
||||
{
|
||||
id: 'logs',
|
||||
label: 'Logs',
|
||||
description: 'Scroll near the top to load older logs.',
|
||||
icon: 'i-lucide-file-text',
|
||||
to: '/logs',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(config.app.console_enabled
|
||||
? [
|
||||
{
|
||||
id: 'console',
|
||||
label: 'Console',
|
||||
description: 'Run yt-dlp commands directly in a non-interactive session.',
|
||||
icon: 'i-lucide-terminal',
|
||||
to: '/console',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
label: 'Docs',
|
||||
icon: 'i-lucide-book-open',
|
||||
children: [
|
||||
...docsNavigationEntries,
|
||||
{
|
||||
id: 'changelog',
|
||||
label: 'Changelog',
|
||||
description:
|
||||
'Latest project changes, loaded remotely when available and falling back to the bundled changelog file.',
|
||||
icon: 'i-lucide-list',
|
||||
to: '/changelog',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const navItems = computed(() => getNavItems(navigationAvailability.value));
|
||||
|
||||
const groupSectionEntries = (entries: Array<NavItem>): Array<Array<NavItem>> => {
|
||||
const order = [...new Set(entries.map((entry) => entry.group))];
|
||||
return order.map((group) => entries.filter((entry) => entry.group === group));
|
||||
};
|
||||
|
||||
const sidebarItems = computed<
|
||||
Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
items: Array<Array<NavItem>>;
|
||||
}>
|
||||
>(() => {
|
||||
return getNavSections()
|
||||
.map((section) => {
|
||||
const sectionEntries = navItems.value.filter(
|
||||
(entry) => entry.section === section.id && entry.sidebarVisible !== false,
|
||||
);
|
||||
|
||||
return {
|
||||
id: section.id,
|
||||
label: section.label,
|
||||
items: groupSectionEntries(sectionEntries),
|
||||
};
|
||||
})
|
||||
.filter((section) => section.items.some((group) => group.length > 0));
|
||||
});
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (route.path === '/') {
|
||||
return 'Downloads';
|
||||
}
|
||||
|
||||
const flat = allNavItems.value.flatMap((item) => [item, ...(item.children || [])]);
|
||||
const match = flat
|
||||
.filter((item) => isNavEntryActive(item))
|
||||
.sort((left, right) => navEntryPath(right).length - navEntryPath(left).length)[0];
|
||||
return match?.label || 'YTPTube';
|
||||
const match = getActiveNavItem(route, navigationAvailability.value);
|
||||
return match?.navbarTitle || match?.label || 'YTPTube';
|
||||
});
|
||||
|
||||
const buildTooltip = computed(
|
||||
|
|
@ -735,81 +617,30 @@ const connectionBannerIcon = computed(() =>
|
|||
socket.connectionStatus === 'connecting' ? 'i-lucide-loader-circle' : 'i-lucide-info',
|
||||
);
|
||||
|
||||
const sidebarSections = computed<Array<SidebarSection>>(() => {
|
||||
const topLevelItems = (ids: string[]) =>
|
||||
ids
|
||||
.map((id) => allNavItems.value.find((item) => item.id === id))
|
||||
.filter((item): item is NavEntry => Boolean(item))
|
||||
.map((item) => makeNavigationItem(item));
|
||||
|
||||
const childItems = (id: string) =>
|
||||
(allNavItems.value.find((item) => item.id === id)?.children || []).map((item) =>
|
||||
makeNavigationItem(item),
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'downloads',
|
||||
label: 'Downloads',
|
||||
items:
|
||||
topLevelItems(['downloads', 'history', 'files']).length > 0
|
||||
? [topLevelItems(['downloads', 'history', 'files'])]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
id: 'automation',
|
||||
label: 'Automation',
|
||||
items: childItems('tasks').length > 0 ? [childItems('tasks')] : [],
|
||||
},
|
||||
{
|
||||
id: 'configuration',
|
||||
label: 'Configuration',
|
||||
items:
|
||||
topLevelItems(['presets', 'custom-fields', 'conditions', 'notifications']).length > 0
|
||||
? [topLevelItems(['presets', 'custom-fields', 'conditions', 'notifications'])]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
label: 'Tools',
|
||||
items: childItems('tools').length > 0 ? [childItems('tools')] : [],
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
label: 'Docs',
|
||||
items: childItems('docs').length > 0 ? [childItems('docs')] : [],
|
||||
},
|
||||
].filter((section) => section.items.some((group) => group.length > 0));
|
||||
});
|
||||
const sidebarSections = computed<Array<SidebarSection>>(() =>
|
||||
sidebarItems.value.map((section) => ({
|
||||
...section,
|
||||
items: section.items.map((group) => group.map((entry) => makeNavigationItem(entry))),
|
||||
})),
|
||||
);
|
||||
|
||||
const routeSearchGroups = computed(() => [
|
||||
{
|
||||
id: 'routes',
|
||||
label: 'Routes',
|
||||
items: allNavItems.value.flatMap((item) => {
|
||||
const self = item.to
|
||||
? [
|
||||
{
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
icon: item.icon,
|
||||
suffix: item.to,
|
||||
onSelect: () => handleRouteSelect(item),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const children = (item.children || []).map((child) => ({
|
||||
label: child.label,
|
||||
description: child.description,
|
||||
icon: child.icon,
|
||||
suffix: child.to || '',
|
||||
onSelect: () => handleRouteSelect(child),
|
||||
}));
|
||||
|
||||
return [...self, ...children];
|
||||
}),
|
||||
},
|
||||
...sidebarItems.value
|
||||
.map((section) => ({
|
||||
id: section.id,
|
||||
label: section.label,
|
||||
items: section.items
|
||||
.flat()
|
||||
.filter((entry) => entry.searchable !== false)
|
||||
.map((entry) => ({
|
||||
label: entry.label,
|
||||
description: entry.description,
|
||||
icon: entry.icon,
|
||||
suffix: entry.to,
|
||||
onSelect: () => handleRouteSelect(entry),
|
||||
})),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0),
|
||||
{
|
||||
id: 'downloads',
|
||||
label: 'Downloads',
|
||||
|
|
@ -872,7 +703,7 @@ const syncShellModeClass = () => {
|
|||
html.classList.toggle('simple-mode', simpleMode.value);
|
||||
};
|
||||
|
||||
const handleRouteSelect = async (item: NavEntry) => {
|
||||
const handleRouteSelect = async (item: NavItem) => {
|
||||
await closeRouteSearch();
|
||||
|
||||
if (item.to) {
|
||||
|
|
|
|||
|
|
@ -1,43 +1,109 @@
|
|||
<template>
|
||||
<div class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm text-toned">
|
||||
<nav aria-label="Breadcrumb" class="min-w-0">
|
||||
<ol class="flex flex-wrap items-center gap-1.5">
|
||||
<template v-for="(item, index) in breadcrumbItems" :key="item.path">
|
||||
<li v-if="index > 0" aria-hidden="true" class="text-muted">/</li>
|
||||
<li>
|
||||
<a
|
||||
v-if="index !== breadcrumbItems.length - 1"
|
||||
:href="buildStateUrl(item.path)"
|
||||
class="rounded px-1 py-0.5 text-left text-default hover:text-highlighted"
|
||||
@click="handleBreadcrumbClick($event, item.path)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
<span v-else class="rounded px-1 py-0.5 font-medium text-highlighted">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ol>
|
||||
<div class="w-full min-w-0 max-w-full space-y-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
|
||||
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
|
||||
>
|
||||
<UIcon :name="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-3">
|
||||
<nav
|
||||
aria-label="Breadcrumb"
|
||||
class="flex min-w-0 flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
|
||||
>
|
||||
<span>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
|
||||
<template v-for="item in breadcrumbTrailItems" :key="item.path">
|
||||
<span>/</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="max-w-full truncate normal-case tracking-normal transition hover:text-highlighted"
|
||||
@click="() => void reloadContent(item.path)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<UIcon
|
||||
v-if="isLoading"
|
||||
name="i-lucide-loader-circle"
|
||||
class="size-4 animate-spin text-info"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<UIcon
|
||||
v-if="isLoading"
|
||||
name="i-lucide-loader-circle"
|
||||
class="size-4 animate-spin text-info"
|
||||
/>
|
||||
<div>
|
||||
<h1 class="truncate text-2xl font-semibold text-highlighted">
|
||||
{{ currentDirectoryName }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
{{ browserPath }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div v-if="show_filter" class="relative w-full sm:w-72">
|
||||
<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>
|
||||
|
||||
<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="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>
|
||||
</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"
|
||||
>
|
||||
|
|
@ -52,62 +118,6 @@
|
|||
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>
|
||||
|
||||
<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
|
||||
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="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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -532,6 +542,7 @@ import moment from 'moment';
|
|||
import { useStorage } from '@vueuse/core';
|
||||
import type { DropdownMenuItem } from '@nuxt/ui';
|
||||
import type { FileItem } from '~/types/filebrowser';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
const route = useRoute();
|
||||
const toast = useNotification();
|
||||
|
|
@ -559,9 +570,14 @@ const controlEnabled = computed(() => Boolean(config.app.browser_control_enabled
|
|||
const contentStyle = computed<'list' | 'grid'>(() =>
|
||||
isMobile.value ? 'grid' : 'list' === display_style.value ? 'list' : 'grid',
|
||||
);
|
||||
const pageShell = requirePageShell('files');
|
||||
const hasItems = computed(() => filteredItems.value.length > 0);
|
||||
const hasSelected = computed(() => selectedElms.value.length > 0);
|
||||
const displayedItemPaths = computed(() => filteredItems.value.map((item) => item.path));
|
||||
const currentDirectoryName = computed(
|
||||
() => breadcrumbItems.value[breadcrumbItems.value.length - 1]?.name || 'Home',
|
||||
);
|
||||
const breadcrumbTrailItems = computed(() => breadcrumbItems.value.slice(0, -1));
|
||||
const sortDirectionIcon = computed(() =>
|
||||
sort_order.value === 'asc' ? 'i-lucide-arrow-down' : 'i-lucide-arrow-up',
|
||||
);
|
||||
|
|
@ -735,19 +751,6 @@ const clearFilter = (): void => {
|
|||
show_filter.value = false;
|
||||
};
|
||||
|
||||
const isPlainLeftClick = (event: MouseEvent): boolean => {
|
||||
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
||||
};
|
||||
|
||||
const handleBreadcrumbClick = (event: MouseEvent, path: string): void => {
|
||||
if (!isPlainLeftClick(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
void reloadContent(path);
|
||||
};
|
||||
|
||||
const itemHref = (item: FileItem): string => {
|
||||
return item.content_type === 'dir' ? uri(`/browser/${item.path}`) : downloadHref(item);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,49 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-git-commit-horizontal" class="size-5 text-toned" />
|
||||
<span>CHANGELOG</span>
|
||||
</div>
|
||||
<main class="w-full min-w-0 max-w-full space-y-6">
|
||||
<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="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
Latest project changes, loaded remotely when available and falling back to the bundled
|
||||
changelog file.
|
||||
</p>
|
||||
<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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div v-if="toggleFilter && logs.length > 0" class="relative w-full sm:w-80">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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"
|
||||
>
|
||||
|
|
@ -29,25 +58,6 @@
|
|||
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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -188,9 +198,11 @@ import moment from 'moment';
|
|||
import { useStorage } from '@vueuse/core';
|
||||
import type { changelogs, changeset } from '~/types/changelogs';
|
||||
import { request, ucFirst, uri } from '~/utils';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
const toast = useNotification();
|
||||
const config = useConfigStore();
|
||||
const pageShell = requirePageShell('changelog');
|
||||
|
||||
useHead({ title: 'CHANGELOG' });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,76 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-filter" class="size-5 text-toned" />
|
||||
<span>Conditions</span>
|
||||
</div>
|
||||
<main class="w-full min-w-0 max-w-full space-y-6">
|
||||
<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="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
Run yt-dlp custom match filter on returned info. and apply options.
|
||||
</p>
|
||||
<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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div v-if="showFilter && items.length > 0" class="relative w-full sm:w-80">
|
||||
<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>
|
||||
|
||||
<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="i-lucide-refresh-cw"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
|
|
@ -28,52 +85,6 @@
|
|||
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>
|
||||
|
||||
<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
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
|
@ -499,10 +510,12 @@ import { useConditions } from '~/composables/useConditions';
|
|||
import type { Condition } from '~/types/conditions';
|
||||
import type { APIResponse } from '~/types/responses';
|
||||
import { cleanObject, copyText, encode } from '~/utils';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
type ConditionItemWithUI = Condition & { raw?: boolean };
|
||||
|
||||
const box = useConfirm();
|
||||
const pageShell = requirePageShell('conditions');
|
||||
const displayStyle = useStorage<'list' | 'grid'>('conditions_display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
|
|
|||
|
|
@ -1,44 +1,57 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-terminal" class="size-5 text-toned" />
|
||||
<span>Console</span>
|
||||
<main class="w-full min-w-0 max-w-full space-y-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
|
||||
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
|
||||
>
|
||||
<UIcon :name="pageShell.icon" class="size-5" />
|
||||
</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>
|
||||
|
||||
<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"
|
||||
<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"
|
||||
>
|
||||
Exit {{ sessionExitCode }}
|
||||
</UBadge>
|
||||
<span>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<UBadge v-if="commandHistory.length > 0" color="neutral" variant="outline" size="sm">
|
||||
{{ commandHistory.length }} saved
|
||||
</UBadge>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-toned">{{ sessionStatusDescription }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div class="flex flex-wrap gap-2 xl:justify-end">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
|
|
@ -354,6 +367,7 @@ import { useConsoleSession } from '~/composables/useConsoleSession';
|
|||
import { useDialog } from '~/composables/useDialog';
|
||||
import type { AutoCompleteOptions } from '~/types/autocomplete';
|
||||
import { disableOpacity, enableOpacity } from '~/utils';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
useHead({ title: 'Console' });
|
||||
|
||||
|
|
@ -370,6 +384,7 @@ const config = useConfigStore();
|
|||
const toast = useNotification();
|
||||
const dialog = useDialog();
|
||||
const consoleSession = useConsoleSession();
|
||||
const pageShell = requirePageShell('console');
|
||||
|
||||
const terminal = ref<Terminal | null>(null);
|
||||
const terminalFit = ref<FitAddon | null>(null);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,76 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-braces" class="size-5 text-toned" />
|
||||
<span>Custom Fields</span>
|
||||
</div>
|
||||
<main class="w-full min-w-0 max-w-full space-y-6">
|
||||
<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="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
Custom fields allow you to add new fields to the download form.
|
||||
</p>
|
||||
<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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div v-if="showFilter && items.length > 0" class="relative w-full sm:w-80">
|
||||
<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>
|
||||
|
||||
<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="i-lucide-refresh-cw"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
|
|
@ -28,52 +85,6 @@
|
|||
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>
|
||||
|
||||
<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
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
|
@ -422,9 +433,11 @@ import { useDlFields } from '~/composables/useDlFields';
|
|||
import type { DLField } from '~/types/dl_fields';
|
||||
import type { APIResponse } from '~/types/responses';
|
||||
import { copyText, encode } from '~/utils';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
const box = useConfirm();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const pageShell = requirePageShell('custom-fields');
|
||||
const displayStyle = useStorage<'list' | 'grid'>('dl_fields_display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 639 });
|
||||
const dlFields = useDlFields();
|
||||
|
|
|
|||
|
|
@ -1,22 +1,40 @@
|
|||
<template>
|
||||
<main class="space-y-6">
|
||||
<UPageHeader :title="docEntry.title" :description="docEntry.description" :ui="pageHeaderUi">
|
||||
<template #links>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
v-for="entry in docsEntries"
|
||||
:key="entry.id"
|
||||
:to="entry.route"
|
||||
:color="entry.file === docEntry.file ? 'primary' : 'neutral'"
|
||||
:variant="entry.file === docEntry.file ? 'solid' : 'outline'"
|
||||
size="sm"
|
||||
:icon="entry.icon"
|
||||
<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="pageShell.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"
|
||||
>
|
||||
{{ entry.navLabel }}
|
||||
</UButton>
|
||||
<span>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</UPageHeader>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 xl:justify-end">
|
||||
<UButton
|
||||
v-for="entry in docsEntries"
|
||||
:key="entry.id"
|
||||
:to="entry.route"
|
||||
:color="entry.file === docEntry.file ? 'primary' : 'neutral'"
|
||||
:variant="entry.file === docEntry.file ? 'solid' : 'outline'"
|
||||
size="sm"
|
||||
:icon="entry.icon"
|
||||
>
|
||||
{{ entry.navLabel }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UPageCard variant="outline" :ui="pageCardUi">
|
||||
<template #body>
|
||||
|
|
@ -31,6 +49,7 @@
|
|||
<script setup lang="ts">
|
||||
import Markdown from '~/components/Markdown.vue';
|
||||
import { DOCS_ENTRIES, getDocsEntryBySlug } from '~/composables/useDocs';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
|
|
@ -49,19 +68,12 @@ const docEntry = computed(() => {
|
|||
return entry;
|
||||
});
|
||||
|
||||
const pageShell = computed(() => requirePageShell(docEntry.value.id));
|
||||
|
||||
useHead(() => ({
|
||||
title: docEntry.value.title,
|
||||
}));
|
||||
|
||||
const pageHeaderUi = {
|
||||
root: 'border-b border-default py-4',
|
||||
headline: 'hidden',
|
||||
title: 'text-2xl font-semibold text-highlighted',
|
||||
description: 'text-sm text-toned',
|
||||
wrapper: 'flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between',
|
||||
links: 'flex flex-wrap items-center gap-2',
|
||||
};
|
||||
|
||||
const pageCardUi = {
|
||||
root: 'w-full bg-default',
|
||||
container: 'w-full p-0',
|
||||
|
|
|
|||
|
|
@ -1,95 +1,94 @@
|
|||
<template>
|
||||
<div class="space-y-6">
|
||||
<UPageHeader
|
||||
title="Downloads"
|
||||
description="Manage queued, active, and completed downloads in one workspace."
|
||||
:ui="{
|
||||
root: 'border-b border-default py-4',
|
||||
headline: 'hidden',
|
||||
title: 'text-2xl font-semibold text-highlighted',
|
||||
description: 'text-sm text-toned',
|
||||
wrapper: 'flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between',
|
||||
links: 'flex flex-wrap items-center gap-2',
|
||||
}"
|
||||
>
|
||||
<template #links>
|
||||
<UDashboardToolbar
|
||||
:ui="{
|
||||
root: 'w-full border-0 p-0',
|
||||
left: 'flex flex-wrap items-center gap-2',
|
||||
right: 'flex flex-wrap items-center gap-2',
|
||||
}"
|
||||
<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"
|
||||
>
|
||||
<template #left>
|
||||
<UInput
|
||||
v-if="toggleFilter"
|
||||
id="filter"
|
||||
v-model.lazy="query"
|
||||
type="search"
|
||||
placeholder="Filter displayed content"
|
||||
icon="i-lucide-filter"
|
||||
size="sm"
|
||||
class="w-full sm:w-72"
|
||||
/>
|
||||
<UIcon :name="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-filter"
|
||||
@click="toggleFilter = !toggleFilter"
|
||||
>
|
||||
<span>Filter</span>
|
||||
</UButton>
|
||||
<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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-if="false === config.paused"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-pause"
|
||||
@click="() => void pauseDownload()"
|
||||
>
|
||||
<span>Pause</span>
|
||||
</UButton>
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-else
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-play"
|
||||
@click="() => void resumeDownload()"
|
||||
>
|
||||
<span>Resume</span>
|
||||
</UButton>
|
||||
<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>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-plus"
|
||||
@click="config.showForm = !config.showForm"
|
||||
>
|
||||
<span>Add</span>
|
||||
</UButton>
|
||||
</template>
|
||||
<UButton
|
||||
v-if="false === config.paused"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-pause"
|
||||
@click="() => void pauseDownload()"
|
||||
>
|
||||
<span>Pause</span>
|
||||
</UButton>
|
||||
|
||||
<template #right>
|
||||
<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>
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
</UPageHeader>
|
||||
<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="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>
|
||||
|
||||
<UInput
|
||||
v-if="toggleFilter"
|
||||
id="filter"
|
||||
v-model.lazy="query"
|
||||
type="search"
|
||||
placeholder="Filter displayed content"
|
||||
icon="i-lucide-filter"
|
||||
size="sm"
|
||||
class="w-full xl:w-80"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="config.showForm" ref="formSection" class="page-form-wrap scroll-mt-24">
|
||||
<NewDownload
|
||||
|
|
@ -111,19 +110,6 @@
|
|||
|
||||
<div v-else class="space-y-8">
|
||||
<section id="queue" class="scroll-mt-24 space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-default pb-3">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-download" class="size-4 text-toned" />
|
||||
<h2>Queue</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-toned">Active and queued downloads.</p>
|
||||
</div>
|
||||
|
||||
<UBadge color="info" variant="soft" size="sm">{{ queueCount }}</UBadge>
|
||||
</div>
|
||||
|
||||
<Queue
|
||||
:thumbnails="show_thumbnail"
|
||||
:query="query"
|
||||
|
|
@ -137,14 +123,14 @@
|
|||
</section>
|
||||
|
||||
<section id="history" class="scroll-mt-24 space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-default pb-3">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-history" class="size-4 text-toned" />
|
||||
<h2>History</h2>
|
||||
<div 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>
|
||||
|
||||
<p class="text-sm text-toned">Completed, skipped, and failed downloads.</p>
|
||||
<p class="text-sm text-toned">{{ historyShell.description }}</p>
|
||||
</div>
|
||||
|
||||
<UBadge color="neutral" variant="soft" size="sm">{{ historyCount }}</UBadge>
|
||||
|
|
@ -180,6 +166,7 @@ import { useStorage } from '@vueuse/core';
|
|||
import { useDialog } from '~/composables/useDialog';
|
||||
import type { item_request } from '~/types/item';
|
||||
import type { StoreItem } from '~/types/store';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
const config = useConfigStore();
|
||||
const stateStore = useStateStore();
|
||||
|
|
@ -190,6 +177,8 @@ const bg_enable = useStorage<boolean>('random_bg', true);
|
|||
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95);
|
||||
const display_style = useStorage<string>('display_style', 'grid');
|
||||
const show_thumbnail = useStorage<boolean>('show_thumbnail', true);
|
||||
const pageShell = requirePageShell('downloads');
|
||||
const historyShell = requirePageShell('history');
|
||||
|
||||
const formSection = ref<HTMLElement | null>(null);
|
||||
const info_view = ref({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<main class="w-full min-w-0 max-w-full space-y-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
|
||||
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-file-text"
|
||||
:class="[
|
||||
|
|
@ -12,7 +14,16 @@
|
|||
:title="loading ? 'Loading history' : 'Live stream active'"
|
||||
:aria-label="loading ? 'Loading history' : 'Live stream active'"
|
||||
/>
|
||||
<span>Logs</span>
|
||||
</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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<UBadge v-if="loading" color="info" variant="soft" size="sm">Loading history</UBadge>
|
||||
|
||||
|
|
@ -36,24 +47,52 @@
|
|||
<UBadge v-if="reachedEnd && !hasActiveFilter" color="neutral" variant="soft" size="sm">
|
||||
Start of file loaded
|
||||
</UBadge>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-toned">Scroll near the top to load older logs.</p>
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<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 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 v-if="toggleFilter || query" class="relative w-full sm:w-72">
|
||||
<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>
|
||||
|
||||
<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"
|
||||
>
|
||||
|
|
@ -68,29 +107,6 @@
|
|||
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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -185,6 +201,7 @@ import moment from 'moment';
|
|||
import { useStorage } from '@vueuse/core';
|
||||
import type { log_line } from '~/types/logs';
|
||||
import { disableOpacity, enableOpacity, parse_api_error, request, uri } from '~/utils';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
type FilteredLogEntry = {
|
||||
log: log_line;
|
||||
|
|
@ -201,6 +218,7 @@ let scrollTimeout: NodeJS.Timeout | null = null;
|
|||
const toast = useNotification();
|
||||
const config = useConfigStore();
|
||||
const route = useRoute();
|
||||
const pageShell = requirePageShell('logs');
|
||||
|
||||
const logContainer = useTemplateRef<HTMLDivElement>('logContainer');
|
||||
const bottomMarker = useTemplateRef<HTMLDivElement>('bottomMarker');
|
||||
|
|
|
|||
|
|
@ -1,19 +1,89 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-bell" class="size-5 text-toned" />
|
||||
<span>Notifications</span>
|
||||
</div>
|
||||
<main class="w-full min-w-0 max-w-full space-y-6">
|
||||
<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="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
Send notifications to your webhooks based on specified events or presets.
|
||||
</p>
|
||||
<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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div v-if="showFilter && notifications.length > 0" class="relative w-full sm:w-80">
|
||||
<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>
|
||||
|
||||
<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="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>
|
||||
|
||||
<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"
|
||||
>
|
||||
|
|
@ -28,65 +98,6 @@
|
|||
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>
|
||||
|
||||
<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
|
||||
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="i-lucide-refresh-cw"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -553,11 +564,13 @@ import { useNotifications } from '~/composables/useNotifications';
|
|||
import { copyText, encode, parse_api_error, request, ucFirst } from '~/utils';
|
||||
import type { ImportedItem } from '~/types';
|
||||
import type { notification } from '~/types/notification';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
const toast = useNotification();
|
||||
const box = useConfirm();
|
||||
const { confirmDialog } = useDialog();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const pageShell = requirePageShell('notifications');
|
||||
const displayStyleState = useStorage<'list' | 'grid' | 'cards'>(
|
||||
'notification_display_style',
|
||||
'cards',
|
||||
|
|
|
|||
|
|
@ -1,20 +1,75 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-5 text-toned" />
|
||||
<span>Presets</span>
|
||||
</div>
|
||||
<main class="w-full min-w-0 max-w-full space-y-6">
|
||||
<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="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
Presets are pre-defined command options for yt-dlp that you want to apply to given
|
||||
download.
|
||||
</p>
|
||||
<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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div v-if="showFilter && presetsNoDefault.length > 0" class="relative w-full sm:w-80">
|
||||
<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>
|
||||
|
||||
<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
|
||||
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>
|
||||
|
||||
<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"
|
||||
>
|
||||
|
|
@ -29,51 +84,6 @@
|
|||
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>
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
|
||||
|
|
@ -463,6 +473,7 @@ import { useDialog } from '~/composables/useDialog';
|
|||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
import { prettyName } from '~/utils';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
type PresetWithUI = Preset & { raw?: boolean; toggle_description?: boolean };
|
||||
|
||||
|
|
@ -470,6 +481,7 @@ const presetsStore = usePresets();
|
|||
const config = useConfigStore();
|
||||
const box = useConfirm();
|
||||
const editor = usePresetEditor();
|
||||
const pageShell = requirePageShell('presets');
|
||||
const { confirmDialog } = useDialog();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,84 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-workflow" class="size-5 text-toned" />
|
||||
<span>Task Definitions</span>
|
||||
</div>
|
||||
<main class="w-full min-w-0 max-w-full space-y-6">
|
||||
<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="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
Create definitions to turn any website into a downloadable feed of links.
|
||||
</p>
|
||||
<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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div v-if="showFilter && definitions.length > 0" class="relative w-full sm:w-80">
|
||||
<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>
|
||||
|
||||
<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="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>
|
||||
|
||||
<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"
|
||||
>
|
||||
|
|
@ -28,60 +93,6 @@
|
|||
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>
|
||||
|
||||
<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-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="i-lucide-refresh-cw"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="() => void reloadContent()"
|
||||
>
|
||||
<span>Reload</span>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -458,6 +469,7 @@ import useTaskDefinitionsComposable from '~/composables/useTaskDefinitions';
|
|||
import { useDialog } from '~/composables/useDialog';
|
||||
import { useMediaQuery } from '~/composables/useMediaQuery';
|
||||
import { copyText, encode } from '~/utils';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
import type {
|
||||
TaskDefinitionDetailed,
|
||||
|
|
@ -484,6 +496,8 @@ const DEFAULT_DEFINITION: TaskDefinitionDocument = {
|
|||
},
|
||||
};
|
||||
|
||||
const pageShell = requirePageShell('task-definitions');
|
||||
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
||||
const taskDefs = useTaskDefinitionsComposable();
|
||||
|
|
|
|||
|
|
@ -1,20 +1,75 @@
|
|||
<template>
|
||||
<main class="w-full min-w-0 max-w-full space-y-4">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-highlighted">
|
||||
<UIcon name="i-lucide-list-todo" class="size-5 text-toned" />
|
||||
<span>Tasks</span>
|
||||
</div>
|
||||
<main class="w-full min-w-0 max-w-full space-y-6">
|
||||
<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="pageShell.icon" class="size-5" />
|
||||
</span>
|
||||
|
||||
<p class="text-sm text-toned">
|
||||
The task runner is simple queue system that allows you to poll channels or playlists for
|
||||
new content at specified intervals.
|
||||
</p>
|
||||
<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>{{ pageShell.sectionLabel }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ pageShell.pageLabel }}</span>
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div v-if="showFilter && tasks.length > 0" class="relative w-full sm:w-80">
|
||||
<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>
|
||||
|
||||
<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
|
||||
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>
|
||||
|
||||
<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"
|
||||
>
|
||||
|
|
@ -29,51 +84,6 @@
|
|||
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>
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
|
||||
|
|
@ -710,12 +720,14 @@ import type { WSEP } from '~/types/sockets';
|
|||
import { sleep } from '~/utils';
|
||||
import { useSessionCache } from '~/utils/cache';
|
||||
import type { item_request } from '~/types/item';
|
||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||
|
||||
const box = useConfirm();
|
||||
const toast = useNotification();
|
||||
const config = useConfigStore();
|
||||
const socket = useSocketStore();
|
||||
const stateStore = useStateStore();
|
||||
const pageShell = requirePageShell('tasks');
|
||||
const { confirmDialog } = useDialog();
|
||||
const sessionCache = useSessionCache();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
|
|
|||
323
ui/app/utils/topLevelNavigation.ts
Normal file
323
ui/app/utils/topLevelNavigation.ts
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
import { DOCS_ENTRIES } from '~/composables/useDocs';
|
||||
|
||||
export type SectionId = 'downloads' | 'automation' | 'configuration' | 'tools' | 'docs';
|
||||
|
||||
type NavSection = {
|
||||
id: SectionId;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type NavDefinition = {
|
||||
id: string;
|
||||
section: SectionId;
|
||||
group: string;
|
||||
label: string;
|
||||
pageLabel?: string;
|
||||
breadcrumbSectionLabel?: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
to: string;
|
||||
matchPath?: string;
|
||||
sidebarVisible?: boolean;
|
||||
searchable?: boolean;
|
||||
activeMode?: 'path' | 'downloads' | 'history';
|
||||
navbarTitle?: string;
|
||||
requires?: 'file_logging' | 'console_enabled';
|
||||
};
|
||||
|
||||
export type NavItem = NavDefinition & {
|
||||
sectionLabel: string;
|
||||
pageLabel: string;
|
||||
matchPath: string;
|
||||
sidebarVisible: boolean;
|
||||
searchable: boolean;
|
||||
};
|
||||
|
||||
export type PageShell = {
|
||||
icon: string;
|
||||
sectionLabel: string;
|
||||
pageLabel: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type LocationPath = {
|
||||
path: string;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
type NavAvailability = {
|
||||
fileLogging?: boolean;
|
||||
consoleEnabled?: boolean;
|
||||
};
|
||||
|
||||
const SECTIONS: Array<NavSection> = [
|
||||
{ id: 'downloads', label: 'Downloads' },
|
||||
{ id: 'automation', label: 'Automation' },
|
||||
{ id: 'configuration', label: 'Configuration' },
|
||||
{ id: 'tools', label: 'Tools' },
|
||||
{ id: 'docs', label: 'Docs' },
|
||||
];
|
||||
|
||||
const NavItems: Array<NavDefinition> = [
|
||||
{
|
||||
id: 'downloads',
|
||||
section: 'downloads',
|
||||
group: 'workspace',
|
||||
label: 'Queue',
|
||||
pageLabel: 'Queue',
|
||||
breadcrumbSectionLabel: 'Workspace',
|
||||
description: 'Active and queued downloads.',
|
||||
icon: 'i-lucide-download',
|
||||
to: '/',
|
||||
matchPath: '/',
|
||||
activeMode: 'downloads',
|
||||
},
|
||||
{
|
||||
id: 'history',
|
||||
section: 'downloads',
|
||||
group: 'workspace',
|
||||
label: 'History',
|
||||
pageLabel: 'History',
|
||||
breadcrumbSectionLabel: 'Workspace',
|
||||
description: 'Completed, skipped, and failed downloads.',
|
||||
icon: 'i-lucide-history',
|
||||
to: '/#history',
|
||||
matchPath: '/',
|
||||
activeMode: 'history',
|
||||
navbarTitle: 'Downloads',
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
section: 'downloads',
|
||||
group: 'workspace',
|
||||
label: 'Files',
|
||||
pageLabel: 'Files',
|
||||
breadcrumbSectionLabel: 'Workspace',
|
||||
description: 'Browse downloaded files.',
|
||||
icon: 'i-lucide-folder-tree',
|
||||
to: '/browser',
|
||||
matchPath: '/browser',
|
||||
},
|
||||
{
|
||||
id: 'tasks',
|
||||
section: 'automation',
|
||||
group: 'automation',
|
||||
label: 'Tasks',
|
||||
pageLabel: 'Tasks',
|
||||
description: 'Queue playlist/channels for automatic download at specified intervals.',
|
||||
icon: 'i-lucide-list-todo',
|
||||
to: '/tasks',
|
||||
matchPath: '/tasks',
|
||||
},
|
||||
{
|
||||
id: 'task-definitions',
|
||||
section: 'automation',
|
||||
group: 'automation',
|
||||
label: 'Task Definitions',
|
||||
pageLabel: 'Task Definitions',
|
||||
description: 'Create definitions to turn any website into a downloadable feed of links.',
|
||||
icon: 'i-lucide-workflow',
|
||||
to: '/task_definitions',
|
||||
matchPath: '/task_definitions',
|
||||
},
|
||||
{
|
||||
id: 'presets',
|
||||
section: 'configuration',
|
||||
group: 'configuration',
|
||||
label: 'Presets',
|
||||
pageLabel: 'Presets',
|
||||
description:
|
||||
'Presets are pre-defined command options for yt-dlp that you want to apply to given download.',
|
||||
icon: 'i-lucide-sliders-horizontal',
|
||||
to: '/presets',
|
||||
matchPath: '/presets',
|
||||
},
|
||||
{
|
||||
id: 'custom-fields',
|
||||
section: 'configuration',
|
||||
group: 'configuration',
|
||||
label: 'Custom Fields',
|
||||
pageLabel: 'Custom Fields',
|
||||
description: 'Custom fields allow you to add new fields to the download form.',
|
||||
icon: 'i-lucide-braces',
|
||||
to: '/dl_fields',
|
||||
matchPath: '/dl_fields',
|
||||
},
|
||||
{
|
||||
id: 'conditions',
|
||||
section: 'configuration',
|
||||
group: 'configuration',
|
||||
label: 'Conditions',
|
||||
pageLabel: 'Conditions',
|
||||
description: 'Run yt-dlp custom match filter on returned info and apply options.',
|
||||
icon: 'i-lucide-filter',
|
||||
to: '/conditions',
|
||||
matchPath: '/conditions',
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
section: 'configuration',
|
||||
group: 'configuration',
|
||||
label: 'Notifications',
|
||||
pageLabel: 'Notifications',
|
||||
description: 'Send notifications to your webhooks based on specified events or presets.',
|
||||
icon: 'i-lucide-bell',
|
||||
to: '/notifications',
|
||||
matchPath: '/notifications',
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
section: 'tools',
|
||||
group: 'tools',
|
||||
label: 'Logs',
|
||||
pageLabel: 'Logs',
|
||||
description: 'Scroll near the top to load older logs.',
|
||||
icon: 'i-lucide-file-text',
|
||||
to: '/logs',
|
||||
matchPath: '/logs',
|
||||
requires: 'file_logging',
|
||||
},
|
||||
{
|
||||
id: 'console',
|
||||
section: 'tools',
|
||||
group: 'tools',
|
||||
label: 'Console',
|
||||
pageLabel: 'Console',
|
||||
description: 'Run yt-dlp commands directly in a non-interactive session.',
|
||||
icon: 'i-lucide-terminal',
|
||||
to: '/console',
|
||||
matchPath: '/console',
|
||||
requires: 'console_enabled',
|
||||
},
|
||||
...DOCS_ENTRIES.map<NavDefinition>((entry) => ({
|
||||
id: entry.id,
|
||||
section: 'docs',
|
||||
group: 'docs',
|
||||
label: entry.navLabel,
|
||||
pageLabel: entry.title,
|
||||
description: entry.description,
|
||||
icon: entry.icon,
|
||||
to: entry.route,
|
||||
matchPath: entry.route,
|
||||
})),
|
||||
{
|
||||
id: 'changelog',
|
||||
section: 'docs',
|
||||
group: 'docs',
|
||||
label: 'Changelog',
|
||||
pageLabel: 'Changelog',
|
||||
description:
|
||||
'Latest project changes, loaded remotely when available and falling back to the bundled changelog file.',
|
||||
icon: 'i-lucide-git-commit-horizontal',
|
||||
to: '/changelog',
|
||||
matchPath: '/changelog',
|
||||
},
|
||||
];
|
||||
|
||||
const normalizePath = (value?: string | null): string => {
|
||||
if (!value || value === '/') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
const trimmed = value.replace(/\/+$/, '');
|
||||
return trimmed === '' ? '/' : trimmed;
|
||||
};
|
||||
|
||||
const getSectionLabel = (sectionId: SectionId): string => {
|
||||
const section = SECTIONS.find((item) => item.id === sectionId);
|
||||
return section?.label ?? sectionId;
|
||||
};
|
||||
|
||||
const resolveEntry = (entry: NavDefinition): NavItem => ({
|
||||
...entry,
|
||||
sectionLabel: getSectionLabel(entry.section),
|
||||
pageLabel: entry.pageLabel ?? entry.label,
|
||||
matchPath: normalizePath(entry.matchPath ?? (entry.to.split(/[?#]/)[0] || '/')),
|
||||
sidebarVisible: entry.sidebarVisible !== false,
|
||||
searchable: entry.searchable !== false,
|
||||
});
|
||||
|
||||
const resolvedNavigation = NavItems.map((entry) => resolveEntry(entry));
|
||||
|
||||
const matchesAvailability = (entry: NavItem, options: NavAvailability): boolean => {
|
||||
switch (entry.requires) {
|
||||
case 'file_logging':
|
||||
return options.fileLogging === true;
|
||||
|
||||
case 'console_enabled':
|
||||
return options.consoleEnabled === true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const getNavItems = (options?: NavAvailability): Array<NavItem> => {
|
||||
if (!options) {
|
||||
return resolvedNavigation;
|
||||
}
|
||||
|
||||
return resolvedNavigation.filter((entry) => matchesAvailability(entry, options));
|
||||
};
|
||||
|
||||
export const getNavSections = (): Array<NavSection> => {
|
||||
return SECTIONS;
|
||||
};
|
||||
|
||||
export const getNavItemById = (id: string): NavItem | undefined => {
|
||||
return resolvedNavigation.find((entry) => entry.id === id);
|
||||
};
|
||||
|
||||
export const isNavItemActive = (entry: NavItem, route: LocationPath): boolean => {
|
||||
switch (entry.activeMode) {
|
||||
case 'downloads':
|
||||
return route.path === '/' && route.hash !== '#history';
|
||||
|
||||
case 'history':
|
||||
return route.path === '/' && route.hash === '#history';
|
||||
|
||||
default: {
|
||||
const current = normalizePath(route.path);
|
||||
const target = normalizePath(entry.matchPath);
|
||||
|
||||
if (target === '/') {
|
||||
return current === '/';
|
||||
}
|
||||
|
||||
return current === target || current.startsWith(`${target}/`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getActiveNavItem = (
|
||||
route: LocationPath,
|
||||
options?: NavAvailability,
|
||||
): NavItem | undefined => {
|
||||
return getNavItems(options)
|
||||
.filter((entry) => isNavItemActive(entry, route))
|
||||
.sort((left, right) => right.matchPath.length - left.matchPath.length)[0];
|
||||
};
|
||||
|
||||
export const getPageShell = (id: string): PageShell | undefined => {
|
||||
const entry = getNavItemById(id);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
icon: entry.icon,
|
||||
sectionLabel: entry.breadcrumbSectionLabel ?? entry.sectionLabel,
|
||||
pageLabel: entry.pageLabel,
|
||||
description: entry.description,
|
||||
};
|
||||
};
|
||||
|
||||
export const requirePageShell = (id: string): PageShell => {
|
||||
const shell = getPageShell(id);
|
||||
|
||||
if (!shell) {
|
||||
throw new Error(`Missing top-level navigation shell for '${id}'`);
|
||||
}
|
||||
|
||||
return shell;
|
||||
};
|
||||
Loading…
Reference in a new issue