Refactor: facelift the WebUI

This commit is contained in:
arabcoders 2026-01-14 18:56:07 +03:00
parent 48e653b70a
commit e0a47e2fe3
19 changed files with 475 additions and 454 deletions

View file

@ -132,13 +132,9 @@
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-warning-90 has-text-dark" v-if="items.length === 0">
<span class="icon">
<i class="fas fa-exclamation-circle" />
</span>
<span>
No custom fields found, you can add new fields using the button above.
</span>
<Message class="is-warning" v-if="items.length === 0">
<span class="icon"><i class="fas fa-exclamation-circle" /></span>
<span>No custom fields found, you can add new fields using the button above.</span>
</Message>
</div>
</div>
@ -250,7 +246,6 @@ const addNewField = () => items.value.push({
const deleteField = (index: number) => items.value.splice(index, 1)
const validateItem = (item: DLField, index: number): boolean => {
const requiredFields = ['name', 'field', 'kind', 'description']
for (const field of requiredFields) {
@ -282,9 +277,10 @@ const validateItem = (item: DLField, index: number): boolean => {
const sortedDLFields = computed(() => items.value.sort((a, b) => (a.order || 0) - (b.order || 0)))
watch(() => config.ytdlp_options, newOptions => ytDlpOptions.value = newOptions
.filter(opt => !opt.ignored).flatMap(opt => opt.flags
.filter(flag => flag.startsWith('--'))
.map(flag => ({ value: flag, description: opt.description || '' }))),
.filter(opt => !opt.ignored)
.flatMap(
opt => opt.flags.filter(flag => flag.startsWith('--')
).map(flag => ({ value: flag, description: opt.description || '' }))),
{ immediate: true }
)
onMounted(async () => {

View file

@ -1,67 +0,0 @@
<template>
<Message :message_class="messageClass" :title="title" :icon="icon" :useClose="true" @close="() => dismissed = true"
v-if="!isDismissed">
<slot />
</Message>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useStorage } from '@vueuse/core'
import Message from "~/components/Message.vue";
const props = withDefaults(defineProps<{
version: string
storageKey?: string
title?: string
icon?: string
tone?: 'warning' | 'danger' | 'info' | 'success'
}>(), {
storageKey: 'deprecated-notice',
title: 'Deprecated Feature',
icon: 'fas fa-exclamation-triangle',
tone: 'warning',
})
const config = useConfigStore()
const isDev = computed(() => 'development' === config.app?.app_env)
const storageKeyComputed = computed<string>(() => `${props.storageKey}:${props.version}`)
const dismissed = useStorage<boolean>(storageKeyComputed, false)
const isDismissed = computed(() => dismissed.value)
const messageClass = computed(() => {
switch (props.tone) {
case 'danger':
return 'is-danger has-background-danger-90 has-text-dark'
case 'info':
return 'is-info has-background-info-90 has-text-dark'
case 'success':
return 'is-success has-background-success-90 has-text-dark'
case 'warning':
default:
return 'is-warning has-background-warning-90 has-text-dark'
}
})
onMounted(() => {
if (!isDev.value) {
return
}
document.addEventListener('keydown', handle_event)
})
onBeforeUnmount(() => {
if (!isDev.value) {
return
}
document.removeEventListener('keydown', handle_event)
})
const handle_event = (e: KeyboardEvent) => {
if (e.ctrlKey && e.altKey && 'd' === e.key.toLowerCase()) {
e.preventDefault()
dismissed.value = !dismissed.value
}
}
</script>

View file

@ -434,9 +434,9 @@
<div class="columns is-multiline" v-if="!hasItems && !paginationInfo.isLoading">
<div class="column is-12">
<Message message_class="is-warning" title="Filter results" icon="fas fa-search" :useClose="true"
@close="() => emitter('clear_search')" v-if="query" :newStyle="true">
<span class="is-block">No results found for '<span class="is-underlined is-bold">{{ query }}</span>'.</span>
<Message class="is-warning" title="Filter results" icon="fas fa-search" :useClose="true"
@close="() => emitter('clear_search')" v-if="query">
<span class="is-block">No results found for: <code>{{ query }}</code>.</span>
<hr>
<p>
You can search using any value shown in the items <code><span class="icon"><i
@ -452,9 +452,9 @@
<li><code>source_name:task_name</code> - items added by the specified task.</li>
</ul>
</Message>
<Message message_class="is-primary" title="No items" icon="fas fa-exclamation-triangle"
v-else-if="socket.isConnected" :new-style="true">
<p>Your download history is empty. Once queued downloads are completed, they will appear here.</p>
<Message v-else-if="socket.isConnected" class="is-primary" title="No items" icon="fas fa-exclamation-triangle"
:new-style="true">
<p>Download history is empty.</p>
</Message>
</div>
</div>

View file

@ -71,8 +71,9 @@ code {
</div>
<div style="position: relative" v-if="!isLoading">
<Message v-if="error" message_class="has-background-warning-90 has-text-dark" title="Error"
icon="fas fa-exclamation" :message="error" />
<Message v-if="error" class="is-warning" title="Error" icon="fas fa-exclamation">
{{ error }}
</Message>
<div class="card" v-else>
<div class="card-body p-4">
<div class="content" v-html="content" />

View file

@ -1,16 +1,13 @@
<template>
<div :class="[newStyle ? 'message' : 'notification', message_class]">
<button class="delete" @click="$emit('close')" v-if="!useToggle && useClose && !newStyle"></button>
<div class="message">
<div @click="$emit('toggle')" class="is-clickable is-pulled-right is-unselectable" v-if="useToggle">
<span class="icon">
<i class="fas" :class="{'fa-arrow-up':toggle,'fa-arrow-down':!toggle}"></i>
<i class="fas" :class="{ 'fa-arrow-up': toggle, 'fa-arrow-down': !toggle }"></i>
</span>
<span>{{ toggle ? 'Close' : 'Open' }}</span>
</div>
<div class="is-unselectable"
:class="{'is-clickable':useToggle, 'notification-title': !newStyle, 'message-header': newStyle}"
v-if="title || icon"
@click="true === useToggle ? $emit('toggle', toggle): null">
<div class="is-unselectable message-header" :class="{ 'is-clickable': useToggle, }" v-if="title || icon"
@click="true === useToggle ? $emit('toggle', toggle) : null">
<template v-if="icon">
<span class="icon-text">
<span class="icon"><i :class="icon"></i></span>
@ -18,12 +15,11 @@
</span>
</template>
<template v-else>{{ title }}</template>
<button class="delete" @click="$emit('close')" v-if="!useToggle && useClose && newStyle"/>
<button class="delete" @click="$emit('close')" v-if="!useToggle && useClose" />
</div>
<div class="content is-text-break" v-if="false === useToggle || toggle"
:class="{'notification-body': !newStyle, 'message-body': newStyle}">
<div class="content message-body is-text-break" v-if="false === useToggle || toggle" :class="body_class">
<template v-if="message">{{ message }}</template>
<slot/>
<slot />
</div>
</div>
</template>
@ -36,24 +32,21 @@ withDefaults(defineProps<{
icon?: string | null
/** Main message content */
message?: string | null
/** CSS class for the notification */
message_class?: string
/** If true, show toggle button */
useToggle?: boolean
/** Current toggle state */
toggle?: boolean
/** If true, show close button */
useClose?: boolean,
newStyle?: boolean
body_class?: string | null,
}>(), {
title: null,
icon: null,
message: null,
message_class: 'is-info',
useToggle: false,
toggle: false,
useClose: false,
newStyle: false
body_class: null,
})
defineEmits<{

View file

@ -1,5 +1,5 @@
<template>
<Message message_class="is-success has-background-success-90 has-text-dark">
<Message class="is-success">
<span class="icon"><i class="fas fa-info-circle" /></span>
<span>
A New WebUI Version installed, please

View file

@ -302,9 +302,9 @@
<div class="columns is-multiline" v-if="filteredItems.length < 1">
<div class="column is-12">
<Message message_class="is-warning" title="Filter results" :newStyle="true" icon="fas fa-search" :useClose="true"
<Message class="is-warning" title="Filter results" icon="fas fa-search" :useClose="true"
@close="() => emitter('clear_search')" v-if="query">
<span class="is-block">No results found for '<span class="is-underlined is-bold">{{ query }}</span>'.</span>
<span class="is-block">No results found for: <code>{{ query }}</code>.</span>
<hr>
<p>
You can search using any value shown in the items <code><span class="icon"><i
@ -320,9 +320,9 @@
<li><code>source_name:task_name</code> - items added by the specified task.</li>
</ul>
</Message>
<Message message_class="is-info" title="No items" icon="fas fa-exclamation-triangle" :useClose="false"
:newStyle="true" v-else>
<p>The download queue is empty.</p>
<Message v-else class="is-info" title="No items" icon="fas fa-exclamation-triangle" :useClose="false"
>
<p>Download queue is empty.</p>
</Message>
</div>
</div>

View file

@ -85,15 +85,10 @@
</div>
</div>
<Message v-if="!guiSupported" message_class="is-warning">
<p>
<span>
<span class="icon"><i class="fa-solid fa-triangle-exclamation" /></span>
<span>This task definition uses features that cannot be represented with the visual editor. You can still
update it
via the advanced view.</span>
</span>
</p>
<Message v-if="!guiSupported" class="is-warning">
<span class="icon"><i class="fa-solid fa-triangle-exclamation" /></span>
<span>This task definition uses features that cannot be represented with the visual editor. You can still
update it via the advanced view.</span>
</Message>
<div v-if="'gui' === mode">

View file

@ -1,16 +1,15 @@
<template>
<main class="columns mt-2 is-multiline">
<div class="column is-12" v-if="form?.url && is_yt_handle(form.url)">
<Message title="Information" class="is-info is-background-info-80 has-text-dark" icon="fas fa-info-circle">
<span>You are using a YouTube link with <b>@handle</b> instead of <b>channel_id</b>. To activate RSS feed
support for URL click on the <NuxtLink @click="async () => form.url = await convert_url(form.url)"><b>Convert
URL</b></NuxtLink> link.</span>
<Message title="Information" class="is-info" icon="fas fa-info-circle">
You are using a YouTube link with <code>@handle</code> instead of <code>channel_id</code>. To activate RSS
feed support for URL click on the <b>Convert URL</b> link.
</Message>
</div>
<div class="column is-12" v-if="form?.url && is_generic_rss(form.url)">
<Message title="Information" class="is-info is-background-info-80 has-text-dark" icon="fas fa-info-circle">
<span>You are using a generic RSS/Atom feed URL. The task handler will automatically download new items found
in this feed.</span>
<Message title="Information" class="is-warning" icon="fas fa-info-circle">
You are using a generic RSS/Atom feed URL. The task handler will automatically download new items found
in this feed.
</Message>
</div>
<div class="column is-12">
@ -330,7 +329,7 @@
</div>
<div class="column is-12">
<Message class="is-info" :newStyle="true">
<Message class="is-info">
<span>
<ul>
<li><strong>Tasks:</strong> requires <code>--download-archive</code> in

View file

@ -86,7 +86,7 @@ code {
</div>
</form>
<Message v-if="loading" class="has-background-info-90 has-text-dark mt-5">
<Message v-if="loading" class="is-info">
<p>
<span class="icon-text">
<span class="icon"><i class="fas fa-spinner fa-spin" /></span>
@ -96,7 +96,7 @@ code {
</Message>
<div v-if="response" class="mt-4">
<Message v-if="response.error" message_class="has-background-danger-90 has-text-dark" title="Error"
<Message v-if="response.error" class="is-danger" title="Error"
icon="fas fa-exclamation-triangle">
<p>{{ response.error }}</p>
<p v-if="response.message">{{ response.message }}</p>
@ -109,7 +109,6 @@ code {
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { request } from '~/utils'

View file

@ -132,7 +132,7 @@
<div>
<NuxtLoadingIndicator />
<NuxtPage v-if="config.is_loaded" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
<Message v-if="!config.is_loaded" class="mt-5" :newStyle="true" title="Loading Configuration"
<Message v-if="!config.is_loaded" class="is-info mt-5" title="Loading Configuration"
icon="fas fa-spinner fa-spin">
<p>This usually takes less than a second.
<span v-if="!socket.isConnected" class="mt-2">

View file

@ -105,7 +105,8 @@
</div>
</div>
<div class="columns is-mobile is-multiline is-justify-content-flex-end" v-if="config.app.browser_control_enabled && items && items.length > 0">
<div class="columns is-mobile is-multiline is-justify-content-flex-end"
v-if="config.app.browser_control_enabled && items && items.length > 0">
<div class="column is-narrow">
<button type="button" class="button" @click="masterSelectAll = !masterSelectAll"
:class="{ 'has-text-primary': !masterSelectAll, 'has-text-danger': masterSelectAll }"
@ -341,8 +342,8 @@
</template>
<div class="column is-12" v-if="search && filteredItems.length < 1">
<Message message_class="has-background-warning-90 has-text-dark" title="No results" icon="fas fa-filter"
:useClose="true" @close="() => search = ''" v-if="search">
<Message class="is-warning" title="No results" icon="fas fa-filter" :useClose="true"
@close="() => search = ''" v-if="search">
<p class="is-block">
No results found for '<span class="is-underlined is-bold">{{ search }}</span>'.
</p>
@ -350,13 +351,12 @@
</div>
<div class="column is-12" v-if="!items || items.length < 1">
<Message title="Loading content" class="has-background-info-90 has-text-dark" icon="fas fa-refresh fa-spin"
<Message title="Loading content" class="is-info" icon="fas fa-refresh fa-spin"
v-if="isLoading">
Loading file browser contents...
</Message>
<Message v-else title="No Content" class="is-background-warning-80 has-text-dark"
icon="fas fa-exclamation-circle">
No content found in this directory.
<Message v-else title="No Content" class="is-warning" icon="fas fa-exclamation-circle">
Directory is empty.
</Message>
</div>
<div class="column is-12" v-if="!config.app.browser_control_enabled">
@ -446,13 +446,10 @@ const filteredItems = computed<FileItem[]>(() => {
}
const searchLower = search.value.toLowerCase()
return sortedItems(items.value.filter((item: FileItem) => {
return item.name.toLowerCase().includes(searchLower)
}))
return sortedItems(items.value.filter((item: FileItem) => deepIncludes(item, searchLower, new WeakSet())))
})
const sortedItems = (items: FileItem[]): FileItem[] => {
if (!items || items.length < 1) {
return []
}

View file

@ -47,8 +47,9 @@ hr {
<div class="columns is-multiline" v-if="isLoading">
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
message="Loading data. Please wait..." />
<Message class="is-info" title="Loading" icon="fas fa-spinner fa-spin">
Loading data. Please wait...
</Message>
</div>
</div>
@ -92,8 +93,8 @@ hr {
<div class="columns is-multiline" v-if="!filteredLogs || filteredLogs.length < 1">
<div class="column is-12">
<Message title="No Results" class="is-background-warning-80 has-text-dark" icon="fas fa-search"
v-if="query" :useClose="true" @close="query = ''">
<Message title="No Results" class="is-warning" icon="fas fa-search" v-if="query"
:useClose="true" @close="query = ''">
<p>No changelog entries found for the query: <strong>{{ query }}</strong>.</p>
<p>Please try a different search term.</p>
</Message>
@ -135,29 +136,21 @@ const filteredLogs = computed<changelogs>(() => {
const q = query.value?.toLowerCase()
if (!q) return logs.value
return logs.value
.map(log => {
// Check if tag matches
const tagMatches = log.tag.toLowerCase().includes(q)
return logs.value.map(log => {
const tagMatches = log.tag.toLowerCase().includes(q)
// Filter commits that match the query
const filteredCommits = log.commits?.filter(commit =>
commit.message.toLowerCase().includes(q) ||
commit.author.toLowerCase().includes(q) ||
commit.full_sha.toLowerCase().includes(q)
) ?? []
const filteredCommits = log.commits?.filter(commit =>
commit.message.toLowerCase().includes(q) ||
commit.author.toLowerCase().includes(q) ||
commit.full_sha.toLowerCase().includes(q)
) ?? []
// Return log only if tag matches OR if there are matching commits
if (tagMatches || filteredCommits.length > 0) {
return {
...log,
commits: tagMatches ? log.commits : filteredCommits
}
}
if (tagMatches || filteredCommits.length > 0) {
return { ...log, commits: tagMatches ? log.commits : filteredCommits }
}
return null
})
.filter((log): log is changeset => log !== null)
return null
}).filter((log): log is changeset => log !== null)
})
const loadContent = async () => {

View file

@ -16,6 +16,19 @@
</span>
<div class="is-pulled-right" v-if="!toggleForm">
<div class="field is-grouped">
<p class="control has-icons-left" v-if="toggleFilter && items && items.length > 0">
<input type="search" v-model.lazy="query" class="input" id="filter"
placeholder="Filter displayed content">
<span class="icon is-left"><i class="fas fa-filter" /></span>
</p>
<p class="control" v-if="items && items.length > 0">
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter" /></span>
<span v-if="!isMobile">Filter</span>
</button>
</p>
<p class="control">
<button class="button is-primary" @click="resetForm(false); toggleForm = !toggleForm;">
<span class="icon"><i class="fas fa-add" /></span>
@ -44,7 +57,7 @@
</div>
<div class="is-hidden-mobile" v-if="!toggleForm">
<span class="subtitle">
Run yt-dlp custom match filter on returned info. and apply cli arguments if matched.
Run yt-dlp custom match filter on returned info. and apply options.
</span>
</div>
</div>
@ -53,192 +66,202 @@
<ConditionForm :addInProgress="addInProgress" :reference="itemRef" :item="item as ConditionItem"
@cancel="resetForm(true)" @submit="updateItem" />
</div>
</div>
<div class="column is-12" v-if="!toggleForm">
<div class="columns is-multiline" v-if="items.length > 0">
<template v-if="'list' === display_style">
<div class="column is-12">
<div class="table-container">
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 850px; table-layout: fixed;">
<thead>
<tr class="has-text-centered is-unselectable">
<th width="80%">
<div class="columns is-multiline" v-if="!isLoading && !toggleForm && (filteredItems && filteredItems.length > 0)">
<div class="column is-12" v-if="'list' === display_style">
<div class="table-container">
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 850px; table-layout: fixed;">
<thead>
<tr class="has-text-centered is-unselectable">
<th width="80%">
<span class="icon"><i class="fa-solid fa-filter" /></span>
<span>Condition</span>
</th>
<th width="20%">
<span class="icon"><i class="fa-solid fa-gear" /></span>
<span>Actions</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="cond in filteredItems" :key="cond.id">
<td class="is-vcentered">
<div class="is-text-overflow is-bold">
{{ cond.name }}
</div>
<div class="is-unselectable">
<span class="icon-text is-clickable" @click="toggleEnabled(cond)"
v-tooltip="'Click to ' + (cond.enabled !== false ? 'disable' : 'enable') + ' condition'">
<span class="icon">
<i class="fa-solid fa-power-off"
:class="{ 'has-text-success': cond.enabled !== false, 'has-text-danger': cond.enabled === false }" />
</span>
<span>{{ cond.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
</span>
&nbsp;
<Popover :maxWidth="450">
<template #trigger>
<span class="is-clickable">
<span class="icon"> <i class="fa-solid fa-info-circle" /></span>
<span>Show Details</span>
</span>
</template>
<template #title><strong>Condition Details</strong></template>
<div v-if="cond.filter">
<span class="icon"><i class="fa-solid fa-filter" /></span>
<span>Condition</span>
</th>
<th width="20%">
<span class="icon"><i class="fa-solid fa-gear" /></span>
<span>Actions</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="cond in items" :key="cond.id">
<td class="is-vcentered">
<div class="is-text-overflow is-bold">
{{ cond.name }}
</div>
<div class="is-unselectable">
<span class="icon-text is-clickable" @click="toggleEnabled(cond)"
v-tooltip="'Click to ' + (cond.enabled !== false ? 'disable' : 'enable') + ' condition'">
<span class="icon">
<i class="fa-solid fa-power-off"
:class="{ 'has-text-success': cond.enabled !== false, 'has-text-danger': cond.enabled === false }" />
</span>
<span>{{ cond.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
</span>
&nbsp;
<Popover :maxWidth="450">
<template #trigger>
<span class="is-clickable">
<span class="icon"> <i class="fa-solid fa-info-circle" /></span>
<span>Show Details</span>
</span>
</template>
<template #title><strong>Condition Details</strong></template>
<div v-if="cond.filter">
<span class="icon"><i class="fa-solid fa-filter" /></span>
<code>{{ cond.filter }}</code>
</div>
<div v-if="cond.cli">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<code>{{ cond.cli }}</code>
</div>
<span v-if="cond.extras && Object.keys(cond.extras).length > 0">
<template v-for="(value, key) in cond.extras" :key="key">
<div>
<span class="icon"><i class="fa-solid fa-list" /></span>
<code>{{ key }}: {{ value }}</code>
</div>
</template>
</span>
</Popover>
<template v-if="cond.priority > 0">
&nbsp;
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-sort-numeric-down" /></span>
<span>Priority: {{ cond.priority }}</span>
</span>
</template>
</div>
</td>
<td class="is-vcentered is-items-center">
<div class="field is-grouped is-grouped-centered">
<div class="control">
<button class="button is-info is-small is-fullwidth" @click="exportItem(cond)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
<span v-if="!isMobile">Export</span>
</button>
</div>
<div class="control">
<button class="button is-warning is-small is-fullwidth" @click="editItem(cond)">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span v-if="!isMobile">Edit</span>
</button>
</div>
<div class="control">
<button class="button is-danger is-small is-fullwidth" @click="deleteItem(cond)">
<span class="icon"><i class="fa-solid fa-trash" /></span>
<span v-if="!isMobile">Delete</span>
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<template v-else>
<div class="column is-6" v-for="cond in items" :key="cond.id">
<div class="card is-flex is-full-height is-flex-direction-column">
<header class="card-header">
<div class="card-header-title is-text-overflow is-block" v-text="cond.name" />
<div class="card-header-icon">
<div class="field is-grouped">
<div class="control" v-if="cond.priority > 0">
<span class="tag is-dark">
<span class="icon"><i class="fa-solid fa-sort-numeric-down" /></span>
<span v-text="cond.priority" />
</span>
<code>{{ cond.filter }}</code>
</div>
<div class="control" @click="toggleEnabled(cond)">
<span class="icon" :class="cond.enabled ? 'has-text-success' : 'has-text-danger'"
v-tooltip="`Condition is ${cond.enabled !== false ? 'enabled' : 'disabled'}. Click to toggle.`">
<i class="fa-solid fa-power-off" />
</span>
</div>
<div class="control">
<a class="has-text-info" v-tooltip="'Export item'" @click.prevent="exportItem(cond)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
</a>
<div v-if="cond.cli">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<code>{{ cond.cli }}</code>
</div>
<span v-if="cond.extras && Object.keys(cond.extras).length > 0">
<template v-for="(value, key) in cond.extras" :key="key">
<div>
<span class="icon"><i class="fa-solid fa-list" /></span>
<code>{{ key }}: {{ value }}</code>
</div>
</template>
</span>
</Popover>
<template v-if="cond.priority > 0">
&nbsp;
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-sort-numeric-down" /></span>
<span>Priority: {{ cond.priority }}</span>
</span>
</template>
</div>
</td>
<td class="is-vcentered is-items-center">
<div class="field is-grouped is-grouped-centered">
<div class="control">
<button class="button is-info is-small is-fullwidth" @click="exportItem(cond)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
<span v-if="!isMobile">Export</span>
</button>
</div>
<div class="control">
<button class="button is-warning is-small is-fullwidth" @click="editItem(cond)">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span v-if="!isMobile">Edit</span>
</button>
</div>
<div class="control">
<button class="button is-danger is-small is-fullwidth" @click="deleteItem(cond)">
<span class="icon"><i class="fa-solid fa-trash" /></span>
<span v-if="!isMobile">Delete</span>
</button>
</div>
</div>
</header>
<div class="card-content is-flex-grow-1">
<div class="content">
<p class="is-text-overflow">
<span class="icon"><i class="fa-solid fa-filter" /></span>
<span v-text="cond.filter" />
</p>
<p class="is-text-overflow" v-if="cond.cli">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>{{ cond.cli }}</span>
</p>
<p class="is-text-overflow" v-if="cond.extras && Object.keys(cond.extras).length > 0">
<span class="icon"><i class="fa-solid fa-list" /></span>
<span>Extras:
<span v-for="(value, key) in cond.extras" :key="key" class="tag is-info mr-2">
<b>{{ key }}</b>: {{ value }}
</span>
</span>
</p>
<p class="is-clickable" :class="{ 'is-text-overflow': !isExpanded(cond.id, 'description') }"
v-if="cond.description" @click="toggleExpand(cond.id, 'description')">
<span class="icon"><i class="fa-solid fa-comment" /></span>
<span>{{ cond.description }}</span>
</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<template v-else>
<div class="column is-6" v-for="cond in filteredItems" :key="cond.id">
<div class="card is-flex is-full-height is-flex-direction-column">
<header class="card-header">
<div class="card-header-title is-text-overflow is-block" v-text="cond.name" />
<div class="card-header-icon">
<div class="field is-grouped">
<div class="control" v-if="cond.priority > 0">
<span class="tag is-dark">
<span class="icon"><i class="fa-solid fa-sort-numeric-down" /></span>
<span v-text="cond.priority" />
</span>
</div>
</div>
<div class="card-footer mt-auto">
<div class="card-footer-item">
<button class="button is-warning is-fullwidth" @click="editItem(cond)">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span>Edit</span>
</button>
<div class="control" @click="toggleEnabled(cond)">
<span class="icon" :class="cond.enabled ? 'has-text-success' : 'has-text-danger'"
v-tooltip="`Condition is ${cond.enabled !== false ? 'enabled' : 'disabled'}. Click to toggle.`">
<i class="fa-solid fa-power-off" />
</span>
</div>
<div class="card-footer-item">
<button class="button is-danger is-fullwidth" @click="deleteItem(cond)">
<span class="icon"><i class="fa-solid fa-trash" /></span>
<span>Delete</span>
</button>
<div class="control">
<a class="has-text-info" v-tooltip="'Export item'" @click.prevent="exportItem(cond)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
</a>
</div>
</div>
</div>
</header>
<div class="card-content is-flex-grow-1">
<div class="content">
<p class="is-text-overflow">
<span class="icon"><i class="fa-solid fa-filter" /></span>
<span v-text="cond.filter" />
</p>
<p class="is-text-overflow" v-if="cond.cli">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>{{ cond.cli }}</span>
</p>
<p class="is-text-overflow" v-if="cond.extras && Object.keys(cond.extras).length > 0">
<span class="icon"><i class="fa-solid fa-list" /></span>
<span>Extras:
<span v-for="(value, key) in cond.extras" :key="key" class="tag is-info mr-2">
<b>{{ key }}</b>: {{ value }}
</span>
</span>
</p>
<p class="is-clickable" :class="{ 'is-text-overflow': !isExpanded(cond.id, 'description') }"
v-if="cond.description" @click="toggleExpand(cond.id, 'description')">
<span class="icon"><i class="fa-solid fa-comment" /></span>
<span>{{ cond.description }}</span>
</p>
</div>
</div>
</template>
<div class="card-footer mt-auto">
<div class="card-footer-item">
<button class="button is-warning is-fullwidth" @click="editItem(cond)">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span>Edit</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-danger is-fullwidth" @click="deleteItem(cond)">
<span class="icon"><i class="fa-solid fa-trash" /></span>
<span>Delete</span>
</button>
</div>
</div>
</div>
</div>
<Message title="No items" message="There are no custom conditions defined."
class="is-background-warning-80 has-text-dark" icon="fas fa-exclamation-circle"
v-if="!items || items.length < 1" />
</template>
</div>
<div class="columns is-multiline" v-if="!toggleForm && (isLoading || !filteredItems || filteredItems.length < 1)">
<div class="column is-12">
<Message v-if="isLoading" class="is-info" title="Loading" icon="fas fa-spinner fa-spin">
Loading data. Please wait...
</Message>
<Message title="No Results" class="is-warning" icon="fas fa-search" v-else-if="query" :useClose="true"
@close="query = ''">
<p>No results found for the query: <code>{{ query }}</code>.</p>
<p>Please try a different search term.</p>
</Message>
<Message v-else title="No items" class="is-warning" icon="fas fa-exclamation-circle">
There are no custom defined conditions yet. Click the <span class="icon"><i class="fas fa-add" /></span>
<strong>New Condition</strong> button to add your first condition.
</Message>
</div>
</div>
<div class="column is-12" v-if="items && items.length > 0 && !toggleForm">
<div class="message is-info">
<div class="message-body content pl-0">
<div class="columns is-multiline" v-if="filteredItems && filteredItems.length > 0 && !toggleForm">
<div class="column is-12">
<Message class="is-info" :body_class="'pl-0'">
<ul>
<li>Filtering is based on yt-dlps <code>--match-filter</code> logic. Any expression that works with yt-dlp
will also work here, including the same boolean operators. We added extended support for the
<code>OR</code> ( <code>||</code> ) operator, which yt-dlp does not natively support. This allows you to combine
multiple conditions more flexibly.
<li>Filtering is based on yt-dlps <code>--match-filter</code> logic. Any expression that works with
yt-dlp will also work here, including the same boolean operators. We added extended support for the
<code>OR</code> ( <code>||</code> ) operator, which yt-dlp does not natively support. This allows you to
combine multiple conditions more flexibly.
</li>
<li>
The primary use case for this feature is to apply custom cli arguments to specific returned info.
@ -254,7 +277,7 @@
filter matches or not.
</li>
</ul>
</div>
</Message>
</div>
</div>
</div>
@ -280,9 +303,18 @@ const toggleForm = ref(false)
const isLoading = ref(false)
const initialLoad = ref(true)
const addInProgress = ref(false)
const query = ref<string>('')
const toggleFilter = ref(false)
const remove_keys = ['raw', 'toggle_description']
const expandedItems = ref<Record<string, Set<string>>>({})
const filteredItems = computed<ConditionItemWithUI[]>(() => {
const q = query.value?.toLowerCase();
if (!q) return items.value;
return items.value.filter((item: ConditionItemWithUI) => deepIncludes(item, q, new WeakSet()));
});
const toggleExpand = (itemId: string | undefined, field: string) => {
if (!itemId) return
@ -309,6 +341,12 @@ watch(() => socket.isConnected, async () => {
}
})
watch(toggleFilter, (val) => {
if (!val) {
query.value = ''
}
})
const reloadContent = async (fromMounted = false): Promise<void> => {
try {

View file

@ -85,7 +85,7 @@
</p>
</header>
<div v-show="!isHistoryCollapsed" class="card-content p-2">
<Message :newStyle="true" class="is-info" v-if="commandHistory.length < 1">
<Message class="is-info" v-if="commandHistory.length < 1">
Commands history is empty.
</Message>
<div class="table-container" v-if="commandHistory.length > 0">

View file

@ -16,6 +16,19 @@
</span>
<div class="is-pulled-right" v-if="!toggleForm">
<div class="field is-grouped">
<p class="control has-icons-left" v-if="toggleFilter && notifications && notifications.length > 0">
<input type="search" v-model.lazy="query" class="input" id="filter"
placeholder="Filter displayed content">
<span class="icon is-left"><i class="fas fa-filter" /></span>
</p>
<p class="control" v-if="notifications && notifications.length > 0">
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter" /></span>
<span v-if="!isMobile">Filter</span>
</button>
</p>
<p class="control">
<button class="button is-primary" @click="resetForm(false); toggleForm = true"
v-tooltip="'Add new notification target.'">
@ -25,7 +38,7 @@
</p>
<p class="control" v-if="notifications.length > 0">
<button class="button is-warning" @click="sendTest" v-tooltip="'Send test notification.'"
:class="{ 'is-loading': isLoading }" :disabled="isLoading">
:class="{ 'is-loading': sendingTest }" :disabled="!notifications.length || sendingTest">
<span class="icon"><i class="fas fa-paper-plane" /></span>
<span v-if="!isMobile">Send Test</span>
</button>
@ -62,9 +75,11 @@
<NotificationForm :addInProgress="addInProgress" :reference="targetRef as string" :item="target"
@cancel="resetForm(true);" @submit="updateItem" :allowedEvents="allowedEvents" />
</div>
</div>
<div class="column is-12" v-if="!toggleForm && notifications && notifications.length > 0">
<template v-if="'list' === display_style">
<div class="columns is-multiline" v-if="!isLoading && !toggleForm && filteredTargets && filteredTargets.length > 0">
<template v-if="'list' === display_style">
<div class="column is-12">
<div class="table-container">
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 850px; table-layout: fixed;">
@ -81,7 +96,7 @@
</tr>
</thead>
<tbody>
<tr v-for="item in notifications" :key="item.id">
<tr v-for="item in filteredTargets" :key="item.id">
<td class="is-text-overflow is-vcentered">
<div class="is-bold">
{{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }}) @
@ -133,89 +148,99 @@
</tbody>
</table>
</div>
</template>
<template v-else>
<div class="columns is-multiline" v-if="notifications && notifications.length > 0">
<div class="column is-6" v-for="item in notifications" :key="item.id">
<div class="card is-flex is-full-height is-flex-direction-column">
<header class="card-header">
<div class="card-header-title is-text-overflow is-block">
{{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }}) @
<NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink>
</div>
</template>
<template v-else>
<div class="column is-6 is-12-mobile" v-for="item in filteredTargets" :key="item.id">
<div class="card is-flex is-full-height is-flex-direction-column">
<header class="card-header">
<div class="card-header-title is-text-overflow is-block">
{{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }}) @
<NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink>
</div>
<div class="card-header-icon">
<div class="field is-grouped">
<div class="control" @click="toggleEnabled(item)">
<span class="icon" :class="item.enabled ? 'has-text-success' : 'has-text-danger'"
v-tooltip="`Notification is ${item.enabled !== false ? 'enabled' : 'disabled'}. Click to toggle.`">
<i class="fa-solid fa-power-off" />
</span>
</div>
<div class="card-header-icon">
<div class="field is-grouped">
<div class="control" @click="toggleEnabled(item)">
<span class="icon" :class="item.enabled ? 'has-text-success' : 'has-text-danger'"
v-tooltip="`Notification is ${item.enabled !== false ? 'enabled' : 'disabled'}. Click to toggle.`">
<i class="fa-solid fa-power-off" />
</span>
</div>
<div class="control">
<a class="has-text-info" v-tooltip="'Export target.'" @click.prevent="exportItem(item)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
</a>
</div>
<div class="control">
<button @click="item.raw = !item.raw">
<span class="icon"><i class="fa-solid"
:class="{ 'fa-arrow-down': !item?.raw, 'fa-arrow-up': item?.raw }" /></span>
</button>
</div>
</div>
<div class="control">
<a class="has-text-info" v-tooltip="'Export target.'" @click.prevent="exportItem(item)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
</a>
</div>
</header>
<div class="card-content is-flex-grow-1">
<div class="content">
<p>
<span class="icon"><i class="fa-solid fa-list-ul" /></span>
<span>On: {{ join_events(item.on) }}</span>
</p>
<p>
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span>Presets: {{ join_presets(item.presets) }}</span>
</p>
<p v-if="item.request?.headers && item.request.headers.length > 0">
<span class="icon"><i class="fa-solid fa-heading" /></span>
<span>Headers: {{item.request.headers.map(h => h.key).join(', ')}}</span>
</p>
</div>
</div>
<div class="card-content" v-if="item?.raw">
<div class="content">
<pre><code>{{ JSON.stringify(cleanObject(item, ['raw']), null, 2) }}</code></pre>
</div>
</div>
<div class="card-footer mt-auto">
<div class="card-footer-item">
<button class="button is-warning is-fullwidth" @click="editItem(item);">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span>Edit</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-danger is-fullwidth" @click="deleteItem(item)">
<span class="icon"><i class="fa-solid fa-trash" /></span>
<span>Delete</span>
<div class="control">
<button @click="item.raw = !item.raw">
<span class="icon"><i class="fa-solid"
:class="{ 'fa-arrow-down': !item?.raw, 'fa-arrow-up': item?.raw }" /></span>
</button>
</div>
</div>
</div>
</header>
<div class="card-content is-flex-grow-1">
<div class="content">
<p>
<span class="icon"><i class="fa-solid fa-list-ul" /></span>
<span>On: {{ join_events(item.on) }}</span>
</p>
<p>
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span>Presets: {{ join_presets(item.presets) }}</span>
</p>
<p v-if="item.request?.headers && item.request.headers.length > 0">
<span class="icon"><i class="fa-solid fa-heading" /></span>
<span>Headers: {{item.request.headers.map(h => h.key).join(', ')}}</span>
</p>
</div>
</div>
<div class="card-content" v-if="item?.raw">
<div class="content">
<pre><code>{{ JSON.stringify(cleanObject(item, ['raw']), null, 2) }}</code></pre>
</div>
</div>
<div class="card-footer mt-auto">
<div class="card-footer-item">
<button class="button is-warning is-fullwidth" @click="editItem(item);">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span>Edit</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-danger is-fullwidth" @click="deleteItem(item)">
<span class="icon"><i class="fa-solid fa-trash" /></span>
<span>Delete</span>
</button>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
<div class="column is-12" v-if="!toggleForm && (!notifications || notifications.length < 1)">
<Message title="No Endpoints" class="is-background-warning-80 has-text-dark" icon="fas fa-exclamation-circle">
There are no notifications endpoints configured to receive web notifications.
<div class="columns is-multiline"
v-if="!toggleForm && (isLoading || !filteredTargets || filteredTargets.length < 1)">
<div class="column is-12">
<Message v-if="isLoading" class="is-info" title="Loading" icon="fas fa-spinner fa-spin">
Loading data. Please wait...
</Message>
<Message title="No Results" class="is-warning" icon="fas fa-search" v-else-if="query" :useClose="true"
@close="query = ''">
<p>No results found for the query: <code>{{ query }}</code>.</p>
<p>Please try a different search term.</p>
</Message>
<Message v-else title="No targets" class="is-warning" icon="fas fa-exclamation-circle">
No notification targets found. Click on the <span class="icon"><i class="fas fa-add" /></span> <strong>New
Notification</strong> button to add your first notification target.
</Message>
</div>
</div>
<div class="column is-12" v-if="notifications && notifications.length > 0 && !toggleForm">
<div class="message is-info">
<div class="message-body content pl-0">
<div class="columns is-multiline" v-if="!toggleForm && filteredTargets && filteredTargets.length > 0">
<div class="column is-12">
<Message class="is-info" :body_class="'pl-0'">
<ul>
<li>
When you export notification target, We remove <code>Authorization</code> header key by default,
@ -227,16 +252,19 @@
<code>...&data_key=json_string</code>, only the <code>data</code> field will be JSON encoded.
The other keys <code>id</code>, <code>event</code> and <code>created_at</code> will be sent as they are.
</li>
<li>We also send two special headers <code>X-Event-ID</code> and <code>X-Event</code> with the request.</li>
<li>We also send two special headers <code>X-Event-ID</code> and <code>X-Event</code> with the request.
</li>
<li>
If you have selected specific presets or events, this will take priority, For example, if you limited the
target to <code>default</code> preset and selected <code>ALL</code> events, only events that reference the
If you have selected specific presets or events, this will take priority, For example, if you limited
the
target to <code>default</code> preset and selected <code>ALL</code> events, only events that reference
the
<code>default</code> preset will be sent to that target. Like wise, if you have limited both events and
presets, then ONLY events that satisfy both conditions will be sent to that target. Only the
<code>test</code> events can bypass these conditions.
</li>
</ul>
</div>
</Message>
</div>
</div>
</div>
@ -272,6 +300,22 @@ const toggleForm = ref(false)
const isLoading = ref(false)
const initialLoad = ref(true)
const addInProgress = ref(false)
const sendingTest = ref(false)
const query = ref<string>('')
const toggleFilter = ref(false)
const filteredTargets = computed<notificationItemWithUI[]>(() => {
const q = query.value?.toLowerCase();
if (!q) return notifications.value;
return notifications.value.filter((item: notificationItemWithUI) => deepIncludes(item, q, new WeakSet()));
});
watch(toggleFilter, (val) => {
if (!val) {
query.value = ''
}
})
watch(() => socket.isConnected, async () => {
if (socket.isConnected && initialLoad.value) {
@ -413,7 +457,7 @@ const sendTest = async () => {
}
try {
isLoading.value = true
sendingTest.value = true
const response = await request('/api/notifications/test', { method: 'POST' })
if (200 !== response.status) {
@ -427,7 +471,7 @@ const sendTest = async () => {
console.error(e)
toast.error(`Failed to send test notification. ${e.message}`)
} finally {
isLoading.value = false
sendingTest.value = false
}
}

View file

@ -16,6 +16,19 @@
</span>
<div class="is-pulled-right" v-if="!toggleForm">
<div class="field is-grouped">
<p class="control has-icons-left" v-if="toggleFilter && presets && presets.length > 0">
<input type="search" v-model.lazy="query" class="input" id="filter"
placeholder="Filter displayed content">
<span class="icon is-left"><i class="fas fa-filter" /></span>
</p>
<p class="control" v-if="presets && presets.length > 0">
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter" /></span>
<span v-if="!isMobile">Filter</span>
</button>
</p>
<p class="control">
<button class="button is-primary" @click="resetForm(false); toggleForm = !toggleForm;"
v-tooltip.bottom="'Toggle add form'">
@ -35,6 +48,7 @@
</span>
</button>
</p>
<p class="control">
<button class="button is-info" @click="reloadContent()" :class="{ 'is-loading': isLoading }"
:disabled="isLoading" v-if="presets && presets.length > 0">
@ -72,7 +86,7 @@
</tr>
</thead>
<tbody>
<tr v-for="item in presetsNoDefault" :key="item.id">
<tr v-for="item in filteredPresets" :key="item.id">
<td class="is-text-overflow is-vcentered">
<div class="is-text-overflow is-bold">
{{ item.name }}
@ -122,7 +136,7 @@
</template>
<template v-else>
<div class="column is-6" v-for="item in presetsNoDefault" :key="item.id">
<div class="column is-6" v-for="item in filteredPresets" :key="item.id">
<div class="card is-flex is-full-height is-flex-direction-column">
<header class="card-header">
<div class="card-header-title is-block is-clickable"
@ -204,24 +218,29 @@
</template>
</div>
<div class="columns is-multiline" v-if="!presets || presets.length < 1">
<div class="columns is-multiline" v-if="!toggleForm && (!filteredPresets || filteredPresets.length < 1)">
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
message="Loading data. Please wait..." v-if="isLoading" />
<Message title="No presets" message="There are no custom defined presets."
class="is-background-warning-80 has-text-dark" icon="fas fa-exclamation-circle" v-else />
<Message v-if="isLoading" class="is-info" title="Loading" icon="fas fa-spinner fa-spin">
Loading data. Please wait...
</Message>
<Message title="No Results" class="is-warning" icon="fas fa-search" v-else-if="query"
:useClose="true" @close="query = ''">
<p>No results found for the query: <code>{{ query }}</code>.</p>
<p>Please try a different search term.</p>
</Message>
<Message v-else title="No presets" class="is-warning" icon="fas fa-exclamation-circle">
There are no custom defined presets.
</Message>
</div>
</div>
<div class="columns is-multiline" v-if="presets && presets.length > 0">
<div class="columns is-multiline" v-if="!query && presets && presets.length > 0">
<div class="column is-12">
<div class="message is-info">
<p class="message-body">
<span class="icon"> <i class="fas fa-info-circle" /></span>
When you export preset, it doesn't include the <strong>cookies</strong> field contents for security
reasons.
</p>
</div>
<Message class="is-info">
<span class="icon"><i class="fas fa-info-circle" /></span>
When you <b>export</b> preset, it doesn't include the <code>cookies</code> field contents for security
reasons.
</Message>
</div>
</div>
</template>
@ -243,6 +262,9 @@ const box = useConfirm()
const display_style = useStorage<string>('preset_display_style', 'cards')
const isMobile = useMediaQuery({ maxWidth: 1024 })
const query = ref<string>('')
const toggleFilter = ref(false)
const presets = ref<PresetWithUI[]>([])
const preset = ref<Partial<Preset>>({})
const presetRef = ref<string | null>('')
@ -255,6 +277,12 @@ const expandedItems = ref<Record<string, Set<string>>>({})
const presetsNoDefault = computed(() => presets.value.filter((t) => !t.default))
const filteredPresets = computed<PresetWithUI[]>(() => {
const q = query.value?.toLowerCase();
if (!q) return presetsNoDefault.value;
return presetsNoDefault.value.filter((item: PresetWithUI) => deepIncludes(item, q, new WeakSet()));
});
const toggleExpand = (itemId: string | undefined, field: string) => {
if (!itemId) return
@ -281,6 +309,12 @@ watch(() => socket.isConnected, async () => {
}
})
watch(toggleFilter, (val) => {
if (!val) {
query.value = ''
}
})
const reloadContent = async (fromMounted = false) => {
try {
isLoading.value = true

View file

@ -80,7 +80,8 @@
</td>
<td class="is-vcentered has-text-centered">{{ definition.priority }}</td>
<td class="is-vcentered has-text-centered">
<span class="has-tooltip" :date-datetime="moment.unix(definition.updated_at).format('YYYY-M-DD H:mm Z')"
<span class="has-tooltip"
:date-datetime="moment.unix(definition.updated_at).format('YYYY-M-DD H:mm Z')"
v-tooltip="moment.unix(definition.updated_at).format('YYYY-M-DD H:mm Z')"
v-rtime="definition.updated_at" />
</td>
@ -170,11 +171,14 @@
<div class="columns is-multiline" v-if="!definitions.length">
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
message="Loading data. Please wait..." v-if="isLoading" />
<Message title="No definitions" message="No task definitions are configured yet. Create one to get started."
class="is-background-warning-80 has-text-dark" icon="fas fa-exclamation-circle"
v-if="!isLoading && !isEditorOpen" />
<Message v-if="isLoading" class="is-info" title="Loading" icon="fas fa-spinner fa-spin">
Loading data. Please wait...
</Message>
<Message v-if="!isLoading && !isEditorOpen" title="No definitions" class="is-warning"
icon="fas fa-exclamation-circle">
There are no task definitions. Click the <span class="icon"><i class="fas fa-add" /></span> <strong>New
Definition</strong> button to create your first task definition.
</Message>
</div>
</div>

View file

@ -407,29 +407,26 @@
</div>
</div>
<div class="columns is-multiline" v-if="!filteredTasks || filteredTasks.length < 1">
<div class="columns is-multiline" v-if="!toggleForm && (!filteredTasks || filteredTasks.length < 1)">
<div class="column is-12">
<Message :newStyle="true" message_class="is-info" v-if="isLoading">
<p><span class="icon"><i class="fas fa-spinner fa-spin" /></span> Loading data. Please wait...</p>
<Message class="is-info" title="Loading" icon="fas fa-spinner fa-spin" v-if="isLoading">
Loading data. Please wait...
</Message>
<Message title="No Results" message_class="is-warning" icon="fas fa-search" v-else-if="query" :useClose="true"
@close="query = ''" :newStyle="true">
<p>No results found for the query: <strong>{{ query }}</strong>.</p>
<Message title="No Results" class="is-warning" icon="fas fa-search" v-else-if="query"
:useClose="true" @close="query = ''">
<p>No results found for the query: <code>{{ query }}</code>.</p>
<p>Please try a different search term.</p>
</Message>
<Message message_class="is-warning" :newStyle="true" v-else>
<p>
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>No tasks are defined.</span>
</p>
<Message class="is-warning" icon="fa-solid fa-info-circle" title="No tasks." v-else>
There are no tasks defined yet. Click the <span class="icon"><i class="fas fa-add" /></span> <strong>New
Task</strong> button to create your first automated download task.
</Message>
</div>
</div>
<div class="columns is-multiline" v-if="!toggleForm && tasks && tasks.length > 0">
<div class="column is-12">
<Message :newStyle="true" message_class="is-info">
<Message class="is-info">
<ul>
<li class="has-text-danger">
<span class="icon">
@ -611,9 +608,7 @@ const filteredTasks = computed<task_item[]>(() => {
const q = query.value?.toLowerCase();
if (!q) return tasks.value;
return tasks.value.filter(
task => Object.values(task).some(value => typeof value === 'string' && value.toLowerCase().includes(q))
);
return tasks.value.filter(task => deepIncludes(task, q, new WeakSet()));
});
const reloadContent = async (fromMounted: boolean = false) => {