Refactor: facelift the WebUI
This commit is contained in:
parent
48e653b70a
commit
e0a47e2fe3
19 changed files with 475 additions and 454 deletions
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 item’s <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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 item’s <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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<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">
|
||||
|
||||
<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">
|
||||
|
||||
<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-dlp’s <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-dlp’s <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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue