standardize the form design, and update packages to latest version.

This commit is contained in:
ArabCoders 2025-03-14 22:46:31 +03:00
parent b5803f94b1
commit fc696b6afa
10 changed files with 1072 additions and 1230 deletions

36
Pipfile.lock generated
View file

@ -27,11 +27,11 @@
},
"aiohappyeyeballs": {
"hashes": [
"sha256:0850b580748c7071db98bffff6d4c94028d0d3035acc20fd721a0ce7e8cac35d",
"sha256:18fde6204a76deeabc97c48bdd01d5801cfda5d6b9c8bbeb1aaaee9d648ca191"
"sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558",
"sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"
],
"markers": "python_version >= '3.9'",
"version": "==2.5.0"
"version": "==2.6.1"
},
"aiohttp": {
"hashes": [
@ -149,11 +149,11 @@
},
"attrs": {
"hashes": [
"sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e",
"sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"
"sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3",
"sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"
],
"markers": "python_version >= '3.8'",
"version": "==25.1.0"
"version": "==25.3.0"
},
"bidict": {
"hashes": [
@ -436,20 +436,20 @@
},
"curl-cffi": {
"hashes": [
"sha256:202145686c5ebc00635b9b15cc8afa82523f14197a640b618bb09d59e74889ba",
"sha256:26d61c5b5c59e9c4a006cba35feccd73c75ce981db0da89a3da88dd280ead7a0",
"sha256:429a14e724898b7001be2776caa87c294e8062f0ac652619df5854eeae6e156c",
"sha256:4818e074b61cb209bd8d4d0d03783313d4773e6b51f8b815e25aad9cc146a7b7",
"sha256:752a4686c062a8bd6e25961845ea812704ba089a0e6863957deb2a30590105bb",
"sha256:7968c3ea37bec96bbe4b623c0fa59c09d3cdaf173da2e0a516b19db52f05caa4",
"sha256:7b1177203e47d3248ccb250e63983eaddf39fce5332f91cc2993ea54cae5aa21",
"sha256:7f41bd0a5dafe3fe19319da1c31302a40634ec4779769ca3d954d8673eb2b4f2",
"sha256:81ba09622e68f8392236212dd0afd75dcd18cbe1be16467deb4d441d62cfc8a3",
"sha256:ea8f0511bc6882d30a985ddc53fa26d109f2977715d4851aef162bfa308cd2a8"
"sha256:04b1d23f0f54f94b8298ed417e6bece85a635d674723cde2b155da686efbf78f",
"sha256:15053d01c6a3e3c4c5331ce9e07e1dc31ca5aa063babca05d18b1b5aad369fac",
"sha256:1e60b8ecc80bfb0da4ff73ac9d194e80482b50ecbb8aefec1b0edaf45fafd80e",
"sha256:3969e4260ad4dab638fb6dbe349623f9f5f022435c7fd21daf760231380367fa",
"sha256:3e37b35268ca58492f54ed020ae4b50c33ee0debad4145db9f746f04ed466eb0",
"sha256:458f53c41bd76d90d8974d60c3a8a0dd902a1af1f9056215cf24f454bcedc6fd",
"sha256:59389773a1556e087120e91eac1e33f84f1599d853e1bc168b153e4cdf360002",
"sha256:cfc74f09e44d2d8d61b8e8fda3a7004b5bc0217a703fbbe9e16ef8caa1f3d4e4",
"sha256:f03f4b17dc679c82bd3c946feb1ad38749b2ad731d7c26daefaac857d1c72fd9",
"sha256:f1b0c7b7b81afca15a0e56c593d3c2bdcd4fd4c9ca49b9ded5b9d8076ba78ff9"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==0.9.0"
"markers": "python_version >= '3.9'",
"version": "==0.10.0"
},
"debugpy": {
"hashes": [

View file

@ -360,7 +360,10 @@ class Notification(metaclass=Singleton):
method=target.get("request", {}).get("method", "POST"),
url=target.get("request", {}).get("url"),
headers=[
TargetRequestHeader(key=h.get("key"), value=h.get("value"))
TargetRequestHeader(
key=str(h.get("key", "")).strip(),
value=str(h.get("value", "")).strip(),
)
for h in target.get("request", {}).get("headers", [])
],
),

View file

@ -1,202 +1,243 @@
<template>
<main class="columns mt-2">
<div class="column">
<form id="taskForm" @submit.prevent="checkInfo()">
<div class="box">
<div class="columns is-multiline is-mobile">
<div class="column is-12">
<h1 class="title is-6" style="border-bottom: 1px solid #dbdbdb;">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
<span>{{ reference ? 'Edit' : 'Add' }}</span>
</span>
</h1>
<main class="columns mt-2 is-multiline">
<div class="column is-12">
<form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()">
<div class="card">
<div class="card-header">
<div class="card-header-title is-text-overflow is-block">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
<span>{{ reference ? 'Edit' : 'Add' }}</span>
</span>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="name">
Target name
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The notification target name, this is used to identify the target in the logs and
notifications.</span>
<div class="card-header-icon" v-if="reference">
<button type="button" @click="showImport = !showImport">
<span class="icon"><i class="fa-solid" :class="{
'fa-arrow-down': !showImport,
'fa-arrow-up': showImport,
}" /></span>
<span>
<span v-if="showImport">Hide</span>
<span v-else>Show</span>
import
</span>
</div>
</button>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="url">
Target URL
</label>
<div class="control has-icons-left">
<input type="url" class="input" id="url" v-model="form.request.url" :disabled="addInProgress"
required>
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The URL to send the notification to.</span>
</span>
</div>
</div>
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="method">
Request method
<div class="column is-12" v-if="showImport || !reference">
<label class="label is-inline" for="import_string">
Import string
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="method" class="is-fullwidth" v-model="form.request.method" :disabled="addInProgress">
<option v-for="item, index in requestMethods" :key="`${index}-${item}`" :value="item">
{{ item }}
</option>
</select>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The request method to use when sending the notification. This can be any of the standard HTTP
methods.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="type">
Request Type
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="type" class="is-fullwidth" v-model="form.request.type" :disabled="addInProgress">
<option v-for="item, index in requestType" :key="`${index}-${item}`" :value="item">
{{ ucFirst(item) }}
</option>
</select>
<div class="control">
<button class="button is-primary" :disabled="!import_string" type="button" @click="importItem">
<span class="icon"><i class="fa-solid fa-add" /></span>
<span>Import</span>
</button>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The request type to use when sending the notification. This can be <code>JSON</code> or
<code>FORM</code> request.
</span>
<span>You can use this field to populate the data, using shared string.</span>
</span>
</div>
</div>
<div class="column is-12 is-clearfix">
<div class="field">
<label class="label is-inline" for="on">
Select Events
<template v-if="form.on.length > 0">
- <NuxtLink @click="form.on = []">Clear selection</NuxtLink>
</template>
</label>
<div class="control has-icons-left">
<div class="select is-multiple is-fullwidth">
<select id="on" class="is-fullwidth" v-model="form.on" :disabled="addInProgress" multiple>
<option v-for="item, index in allowedEvents" :key="`${index}-${item}`" :value="item">
{{ item }}
</option>
</select>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="name">
Target name
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-paper-plane" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
Subscribe to the events you want to listen for. When the event is triggered, the notification will
be sent to the target URL. If no events are selected, the notification will be sent for all events.
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The notification target name, this is used to identify the target in the logs and
notifications.</span>
</span>
</span>
</div>
</div>
</div>
<div class="column is-12">
<div class="field">
<label class="label is-inline is-unselectable">
Optional Headers - <button type="button" class="has-text-link"
@click="form.request.headers.push({ key: '', value: '' });">Add Header
</button>
</label>
<div class="columns is-multiline is-mobile">
<template v-for="_, key in form.request.headers" :key="key">
<div class="column is-5">
<div class="field">
<div class="control has-icons-left">
<input type="text" class="input" v-model="form.request.headers[key].key"
:disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="url">
Target URL
</label>
<div class="control has-icons-left">
<input type="url" class="input" id="url" v-model="form.request.url" :disabled="addInProgress"
required>
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The URL to send the notification to.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="method">
Request method
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="method" class="is-fullwidth" v-model="form.request.method" :disabled="addInProgress">
<option v-for="item, index in requestMethods" :key="`${index}-${item}`" :value="item">
{{ item }}
</option>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The request method to use when sending the notification. This can be any of the standard HTTP
methods.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="type">
Request Type
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="type" class="is-fullwidth" v-model="form.request.type" :disabled="addInProgress">
<option v-for="item, index in requestType" :key="`${index}-${item}`" :value="item">
{{ ucFirst(item) }}
</option>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The request type to use when sending the notification. This can be <code>JSON</code> or
<code>FORM</code> request.
</span>
</span>
</div>
</div>
<div class="column is-12 is-clearfix">
<div class="field">
<label class="label is-inline" for="on">
Select Events
<template v-if="form.on.length > 0">
- <NuxtLink @click="form.on = []">Clear selection</NuxtLink>
</template>
</label>
<div class="control has-icons-left">
<div class="select is-multiple is-fullwidth">
<select id="on" class="is-fullwidth" v-model="form.on" :disabled="addInProgress" multiple>
<option v-for="item, index in allowedEvents" :key="`${index}-${item}`" :value="item">
{{ item }}
</option>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-paper-plane" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
Subscribe to the events you want to listen for. When the event is triggered, the notification will
be sent to the target URL. If no events are selected, the notification will be sent for all
events.
</span>
</span>
</div>
</div>
<div class="column is-12">
<div class="field">
<label class="label is-inline is-unselectable">
Optional Headers - <button type="button" class="has-text-link"
@click="form.request.headers.push({ key: '', value: '' });">Add Header
</button>
</label>
<div class="columns is-multiline is-mobile">
<template v-for="_, key in form.request.headers" :key="key">
<div class="column is-5">
<div class="field">
<div class="control has-icons-left">
<input type="text" class="input" v-model="form.request.headers[key].key"
:disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header key to send with the notification.</span>
</span>
</div>
<div class="column is-6">
<div class="field">
<div class="control has-icons-left">
<input type="text" class="input" v-model="form.request.headers[key].value"
:disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-v" /></span>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header value to send with the notification.</span>
</span>
</div>
<div class="column is-1">
<div class="control">
<button type="button" class="button is-danger" @click="form.request.headers.splice(key, 1)"
:disabled="addInProgress">
<span class="icon"><i class="fa-solid fa-trash" /></span>
</button>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header key to send with the notification.</span>
</span>
</div>
<div class="column is-6">
<div class="field">
<div class="control has-icons-left">
<input type="text" class="input" v-model="form.request.headers[key].value"
:disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-v" /></span>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header value to send with the notification.</span>
</span>
</div>
<div class="column is-1">
<div class="control">
<button type="button" class="button is-danger" @click="form.request.headers.splice(key, 1)"
:disabled="addInProgress">
<span class="icon"><i class="fa-solid fa-trash" /></span>
</button>
</div>
</div>
</template>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-exclamation" /></span>
<span class="has-text-danger">
If header key or value is empty, the header will not be sent.
</template>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-exclamation" /></span>
<span class="has-text-danger">
If header key or value is empty, the header will not be sent.
</span>
</span>
</span>
</div>
</div>
</div>
<div class="column is-12">
<div class="field is-grouped is-grouped-right">
<p class="control">
<button class="button is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="taskForm">
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span>
</button>
</p>
<p class="control">
<button class="button is-danger" @click="emitter('cancel')" :disabled="addInProgress" type="button">
<span class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span>
</button>
</p>
</div>
<div class="card-footer">
<p class="card-footer-item">
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="taskForm">
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span>
</button>
</p>
<p class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="emitter('cancel')" :disabled="addInProgress"
type="button">
<span class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span>
</button>
</p>
</div>
</div>
</div>
</form>
@ -205,6 +246,7 @@
</template>
<script setup>
import { useStorage } from '@vueuse/core'
const emitter = defineEmits(['cancel', 'submit']);
const toast = useToast();
const props = defineProps({
@ -231,6 +273,8 @@ const props = defineProps({
const form = reactive(props.item);
const requestMethods = ['POST', 'PUT'];
const requestType = ['json', 'form'];
const showImport = useStorage('showImport', false);
const import_string = ref('');
const checkInfo = async () => {
const required = ['name', 'request.url', 'request.method', 'request.type'];
@ -254,13 +298,73 @@ const checkInfo = async () => {
return;
}
let headers = []
for (const header of form.request.headers) {
if (!header.key || !header.value) {
form.request.headers.splice(form.request.headers.indexOf(header), 1);
continue
}
headers.push({ key: String(header.key).trim(), value: String(header.value).trim() })
}
form.request.headers = headers;
emitter('submit', { reference: toRaw(props.reference), item: toRaw(form) });
}
const importItem = async () => {
let val = import_string.value.trim()
if (!val) {
toast.error('The import string is required.')
return
}
if (false === val.startsWith('{')) {
try {
val = base64UrlDecode(val)
} catch (e) {
console.error(e)
toast.error(`Failed to decode string. ${e.message}`)
return
}
}
try {
const item = JSON.parse(val)
if ('notification' !== item._type) {
toast.error(`Invalid import string. Expected type 'notification', got '${item._type}'.`)
import_string.value = ''
return
}
if (form.target) {
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
return
}
}
if (item.name) {
form.name = item.name
}
if (item.url) {
form.url = item.url
}
if (item.request) {
form.request = item.request
}
if (item.on) {
form.on = item.on
}
import_string.value = ''
} catch (e) {
console.error(e)
toast.error(`Failed to import task. ${e.message}`)
}
}
</script>

View file

@ -1,38 +1,5 @@
<template>
<main class="columns mt-2 is-multiline">
<div class="column is-12">
<h1 class="is-pointer is-unselectable title is-5" @click="importExpanded = !importExpanded">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="importExpanded ? 'fa-arrow-up' : 'fa-arrow-down'" /></span>
<span>Import preset.</span>
</span>
</h1>
<form id="importForm" @submit.prevent="importPreset()" v-if="importExpanded">
<div class="box">
<label class="label" for="json_preset">
Base64 encoded JSON string.
</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input type="text" class="input" id="json_preset" v-model="json_preset" autocomplete="off">
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
</div>
<div class="control">
<button form="importForm" class="button is-primary" :disabled="!json_preset" type="submit">
<span class="icon"><i class="fa-solid fa-add" /></span>
<span>Import</span>
</button>
</div>
</div>
</div>
</form>
</div>
<div class="column is-12">
<h1 class="is-unselectable is-pointer title is-5" @click="convertExpanded = !convertExpanded">
<span class="icon-text">
@ -40,10 +7,8 @@
<span>Convert yt-dlp cli options.</span>
</span>
</h1>
<form autocomplete="off" id="convertOpts" @submit.prevent="convertOptions()" v-if="convertExpanded">
<div class="box">
<label class="label" for="opts">
yt-dlp CLI options
</label>
@ -74,18 +39,54 @@
</div>
<div class="column is-12">
<form id="presetForm" @submit.prevent="checkInfo()">
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()">
<div class="card">
<div class="card-header">
<div class="card-header-title">
<div class="card-header-title is-text-overflow is-block">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
<span>{{ reference ? 'Edit' : 'Add' }}</span>
</span>
</div>
<div class="card-header-icon" v-if="reference">
<button type="button" @click="showImport = !showImport">
<span class="icon"><i class="fa-solid" :class="{
'fa-arrow-down': !showImport,
'fa-arrow-up': showImport,
}" /></span>
<span>{{ showImport ? 'Hide' : 'Show' }} import</span>
</button>
</div>
</div>
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-12" v-if="showImport || !reference">
<label class="label is-inline" for="import_string">
Import string
</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
</div>
<div class="control">
<button class="button is-primary" :disabled="!import_string" type="button" @click="importItem">
<span class="icon"><i class="fa-solid fa-add" /></span>
<span>Import</span>
</button>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>You can use this field to populate the data, using shared string.</span>
</span>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="name" v-text="'Name'" />
@ -218,9 +219,10 @@
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<div class="card-footer">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="presetForm">
<span class="icon"><i class="fa-solid fa-save" /></span>
@ -234,8 +236,6 @@
<span>Cancel</span>
</button>
</div>
</div>
</div>
</div>
</form>
@ -247,6 +247,7 @@
</template>
<script setup>
import { useStorage } from '@vueuse/core'
const emitter = defineEmits(['cancel', 'submit']);
const props = defineProps({
@ -276,8 +277,8 @@ const toast = useToast()
const convertInProgress = ref(false)
const form = reactive(JSON.parse(JSON.stringify(props.preset)))
const opts = ref('')
const json_preset = ref('')
const importExpanded = ref(false)
const import_string = ref('')
const showImport = useStorage('showImport', false);
const convertExpanded = ref(false)
onMounted(() => {
@ -395,10 +396,10 @@ const convertOptions = async () => {
}
}
const importPreset = async () => {
let val = json_preset.value.trim()
const importItem = async () => {
let val = import_string.value.trim()
if (!val) {
toast.error('The preset import string is required.')
toast.error('The import string is required.')
return
}
@ -406,13 +407,20 @@ const importPreset = async () => {
try {
val = base64UrlDecode(val)
} catch (e) {
toast.error('Invalid base64 string.', e)
console.error(e)
toast.error(`Failed to decode string. ${e.message}`)
return
}
}
try {
const preset = JSON.parse(val)
const item = JSON.parse(val)
if ('preset' !== item._type) {
toast.error(`Invalid import string. Expected type 'preset', got '${item._type}'.`)
import_string.value = ''
return
}
if (form.format || form.args || form.postprocessors) {
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
@ -420,35 +428,35 @@ const importPreset = async () => {
}
}
if (preset.name) {
form.name = preset.name
if (item.name) {
form.name = item.name
}
if (preset.format) {
form.format = preset.format
if (item.format) {
form.format = item.format
}
if (preset.args) {
form.args = JSON.stringify(preset.args, null, 2)
if (item.args) {
form.args = JSON.stringify(item.args, null, 2)
}
if (preset.postprocessors) {
form.postprocessors = JSON.stringify(preset.postprocessors, null, 2)
if (item.postprocessors) {
form.postprocessors = JSON.stringify(item.postprocessors, null, 2)
}
if (preset.output_template) {
form.template = preset.output_template
if (item.output_template) {
form.template = item.output_template
}
if (preset.folder) {
form.folder = preset.folder
if (item.folder) {
form.folder = item.folder
}
json_preset.value = ''
importExpanded.value = false
import_string.value = ''
showImport.value = false
} catch (e) {
console.error(e)
toast.error(`Failed to import preset. ${e.message}`)
toast.error(`Failed to string. ${e.message}`)
}
}
</script>

View file

@ -1,222 +1,232 @@
<template>
<main class="columns mt-2 is-multiline">
<div class="column is-12">
<h1 class="is-pointer is-unselectable title is-5" @click="importExpanded = !importExpanded">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="importExpanded ? 'fa-arrow-up' : 'fa-arrow-down'" /></span>
<span>Import task.</span>
</span>
</h1>
<form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()">
<div class="card">
<form id="importForm" @submit.prevent="importTask()" v-if="importExpanded">
<div class="box">
<label class="label" for="import_string">
Base64 encoded JSON string.
</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
</div>
<div class="control">
<button form="importForm" class="button is-primary" :disabled="!import_string" type="submit">
<span class="icon"><i class="fa-solid fa-add" /></span>
<span>Import</span>
</button>
</div>
</div>
</div>
</form>
</div>
<div class="column is-12">
<form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()" class="box">
<div class="columns is-multiline is-mobile">
<div class="column is-12">
<h1 class="title is-6" style="border-bottom: 1px solid #dbdbdb;">
<div class="card-header">
<div class="card-header-title is-text-overflow is-block">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
<span>{{ reference ? 'Edit' : 'Add' }}</span>
</span>
</h1>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="name" v-text="'Name'" />
<div class="control has-icons-left">
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Task name is used to identify the task in the task list and in logs.</span>
</span>
<div class="card-header-icon" v-if="reference">
<button type="button" @click="showImport = !showImport">
<span class="icon"><i class="fa-solid" :class="{
'fa-arrow-down': !showImport,
'fa-arrow-up': showImport,
}" /></span>
<span>{{ showImport ? 'Hide' : 'Show' }} import</span>
</button>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="url" v-text="'URL'" />
<div class="control has-icons-left">
<input type="url" class="input" id="url" v-model="form.url" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The channel or playlist URL.</span>
</span>
</div>
</div>
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="folder">
Preset
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="preset" class="is-fullwidth" v-model="form.preset"
:disabled="addInProgress || hasFormatInConfig"
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the config.' : ''">
<option v-for="item in config.presets" :key="item.name" :value="item.name">
{{ item.name }}
</option>
</select>
<div class="column is-12" v-if="showImport || !reference">
<label class="label is-inline" for="import_string">
Import string
</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
</div>
<div class="control">
<button class="button is-primary" :disabled="!import_string" type="button" @click="importItem">
<span class="icon"><i class="fa-solid fa-add" /></span>
<span>Import</span>
</button>
</div>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Select the preset to use for this URL. The preset will be ignored if format key is present in
config.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="timer">
CRON expression timer.
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="timer" placeholder="leave empty to run once every hour."
v-model="form.timer" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-clock" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The CRON timer expression to use for this task. If not set, the task will run once an hour in a
random
minute. For more information on CRON expressions, see <NuxtLink to="https://crontab.guru/"
target="_blank">crontab.guru</NuxtLink>.
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>You can use this field to populate the data, using shared string.</span>
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field">
<label class="label is-inline" for="folder">
Download path
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
v-model="form.folder" :disabled="addInProgress" list="folders">
<span class="icon is-small is-left"><i class="fa-solid fa-folder" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Downloads are relative to download path, defaults to root path if not set.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field">
<label class="label is-inline" for="output_template">
Output template
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="output_template" placeholder="The output template to use"
v-model="form.template" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="name" v-text="'Name'" />
<div class="control has-icons-left">
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Task name is used to identify the task in the task list and in logs.</span>
</span>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The output template to use, if not set, it will defaults to
<code>{{ config.app.output_template }}</code>.
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
target="_blank">visit this url</NuxtLink>.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field">
<label class="label is-inline" for="config" v-tooltip="'Extends current global yt-dlp config. (JSON)'">
JSON yt-dlp config or CLI options. <NuxtLink
v-if="form.config && (typeof form.config === 'string') && !form.config.trim().startsWith('{')"
@click="convertOptions()">Convert to JSON</NuxtLink>
</label>
<div class="control">
<textarea class="textarea" id="config" v-model="form.config" :disabled="addInProgress"
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="url" v-text="'URL'" />
<div class="control has-icons-left">
<input type="url" class="input" id="url" v-model="form.url" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The channel or playlist URL.</span>
</span>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span> Extends current global yt-dlp config with given options. Some fields are ignored like
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with caution
some of those options can break yt-dlp or the frontend.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field">
<label class="label is-inline" for="cookies" v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
<div class="control">
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" />
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="folder">
Preset
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="preset" class="is-fullwidth" v-model="form.preset"
:disabled="addInProgress || hasFormatInConfig"
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the config.' : ''">
<option v-for="item in config.presets" :key="item.name" :value="item.name">
{{ item.name }}
</option>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Select the preset to use for this URL. The preset will be ignored if format key is present in
config.</span>
</span>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Use the <NuxtLink target="_blank"
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
Recommended addon</NuxtLink> by yt-dlp to export cookies. The cookies MUST be in Netscape HTTP
Cookie format.</span>
</span>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="timer">
CRON expression timer.
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="timer" placeholder="leave empty to run once every hour."
v-model="form.timer" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-clock" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The CRON timer expression to use for this task. If not set, the task will run once an hour in a
random
minute. For more information on CRON expressions, see <NuxtLink to="https://crontab.guru/"
target="_blank">crontab.guru</NuxtLink>.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field">
<label class="label is-inline" for="folder">
Download path
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
v-model="form.folder" :disabled="addInProgress" list="folders">
<span class="icon is-small is-left"><i class="fa-solid fa-folder" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Downloads are relative to download path, defaults to root path if not set.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field">
<label class="label is-inline" for="output_template">
Output template
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="output_template" placeholder="The output template to use"
v-model="form.template" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The output template to use, if not set, it will defaults to
<code>{{ config.app.output_template }}</code>.
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
target="_blank">visit this url</NuxtLink>.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field">
<label class="label is-inline" for="config"
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
JSON yt-dlp config or CLI options. <NuxtLink
v-if="form.config && (typeof form.config === 'string') && !form.config.trim().startsWith('{')"
@click="convertOptions()">Convert to JSON</NuxtLink>
</label>
<div class="control">
<textarea class="textarea" id="config" v-model="form.config" :disabled="addInProgress"
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span> Extends current global yt-dlp config with given options. Some fields are ignored like
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with
caution
some of those options can break yt-dlp or the frontend.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field">
<label class="label is-inline" for="cookies"
v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
<div class="control">
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" />
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Use the <NuxtLink target="_blank"
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
Recommended addon</NuxtLink> by yt-dlp to export cookies. The cookies MUST be in Netscape HTTP
Cookie format.</span>
</span>
</div>
</div>
</div>
</div>
<div class="column is-12">
<div class="field is-grouped is-grouped-right">
<p class="control">
<button class="button is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="taskForm">
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span>
</button>
</p>
<p class="control">
<button class="button is-danger" @click="emitter('cancel')" :disabled="addInProgress" type="button">
<span class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span>
</button>
</p>
<p class="control">
<button class="button is-info" type="button" @click="showAdvanced = !showAdvanced">
<span class="icon"><i class="fa-solid fa-cog" /></span>
<span>Advanced</span>
</button>
</p>
</div>
<div class="card-footer">
<p class="card-footer-item">
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="taskForm">
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span>
</button>
</p>
<p class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="emitter('cancel')" :disabled="addInProgress"
type="button">
<span class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span>
</button>
</p>
<p class="card-footer-item">
<button class="button is-fullwidth is-info" type="button" @click="showAdvanced = !showAdvanced">
<span class="icon"><i class="fa-solid fa-cog" /></span>
<span>Advanced</span>
</button>
</p>
</div>
</div>
</form>
@ -229,14 +239,14 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { parseExpression } from 'cron-parser'
import { CronExpressionParser } from 'cron-parser'
const emitter = defineEmits(['cancel', 'submit']);
const toast = useToast();
const config = useConfigStore();
const convertInProgress = ref(false);
const showAdvanced = useStorage('task.showAdvanced', false);
const importExpanded = ref(false);
const showImport = useStorage('showImport', false);
const import_string = ref('');
const props = defineProps({
@ -279,9 +289,10 @@ const checkInfo = async () => {
if (form.timer) {
try {
parseExpression(form.timer);
CronExpressionParser.parse(form.timer);
} catch (e) {
toast.error('Invalid CRON expression.');
console.log(e)
toast.error(`Invalid CRON expression. ${e.message}`);
return;
}
}
@ -347,10 +358,10 @@ const hasFormatInConfig = computed(() => {
}
})
const importTask = async () => {
const importItem = async () => {
let val = import_string.value.trim()
if (!val) {
toast.error('The preset import string is required.')
toast.error('The import string is required.')
return
}
@ -359,13 +370,19 @@ const importTask = async () => {
val = base64UrlDecode(val)
} catch (e) {
console.error(e)
toast.error('Invalid base64 string.')
toast.error(`Failed to decode string. ${e.message}`)
return
}
}
try {
const task = JSON.parse(val)
const item = JSON.parse(val)
if ('task' !== item._type) {
toast.error(`Invalid import string. Expected type 'task', got '${item._type}'.`)
import_string.value = ''
return
}
if (form.config || form.url || form.timer) {
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
@ -373,39 +390,38 @@ const importTask = async () => {
}
}
if (task.name) {
form.name = task.name
if (item.name) {
form.name = item.name
}
if (task.url) {
form.url = task.url
if (item.url) {
form.url = item.url
}
if (task.preset) {
form.preset = task.preset
if (item.preset) {
form.preset = item.preset
}
if (task.timer) {
form.timer = task.timer
if (item.timer) {
form.timer = item.timer
}
if (task.folder) {
form.folder = task.folder
if (item.folder) {
form.folder = item.folder
}
if (task.template) {
form.template = task.template
if (item.template) {
form.template = item.template
}
if (task.config) {
form.config = JSON.stringify(task.config, null, 2)
if (item.config) {
form.config = JSON.stringify(item.config, null, 2)
}
import_string.value = ''
importExpanded.value = false
} catch (e) {
console.error(e)
toast.error(`Failed to import task. ${e.message}`)
toast.error(`Failed to import string. ${e.message}`)
}
}
</script>

View file

@ -11,19 +11,19 @@
},
"web-types": "./web-types.json",
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"@sentry/nuxt": "^9.2.0",
"@vueuse/core": "^10.9.0",
"@vueuse/nuxt": "^10.9.0",
"@pinia/nuxt": "^0.10.1",
"@sentry/nuxt": "^9.5.0",
"@vueuse/core": "^13.0.0",
"@vueuse/nuxt": "^13.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"cron-parser": "^4.9.0",
"cron-parser": "^5.0.4",
"cronstrue": "^2.49.0",
"floating-vue": "^5.2.2",
"hls.js": "^1.4.12",
"moment": "^2.30.1",
"nuxt": "^3.16",
"pinia": "^2.1.7",
"pinia": "^3.0.1",
"socket.io-client": "^4.7.2",
"vue": "^3.4",
"vue-router": "^4.3.0",

View file

@ -66,9 +66,8 @@ div.is-centered {
<NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink>
</div>
<div class="card-header-icon">
<a :href="item.url" class="has-text-primary" v-tooltip="'Copy url.'"
@click.prevent="copyText(item.request.url)">
<span class="icon"><i class="fa-solid fa-copy" /></span>
<a class="has-text-primary" v-tooltip="'Export target.'" @click.prevent="exportItem(item)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
</a>
<button @click="item.raw = !item.raw">
<span class="icon"><i class="fa-solid"
@ -84,7 +83,7 @@ div.is-centered {
</p>
<p v-if="item.request?.headers && item.request.headers.length > 0">
<span class="icon"><i class="fa-solid fa-heading" /></span>
<span>{{ item.request.headers.map(h => h.key).join(', ') }}</span>
<span>{{item.request.headers.map(h => h.key).join(', ')}}</span>
</p>
</div>
</div>
@ -116,6 +115,19 @@ div.is-centered {
</Message>
</div>
</div>
<div class="column is-12" v-if="notifications && notifications.length > 0 && !toggleForm">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
When you export notification target, We remove <code>Authorization</code> header key by default,
However this might not be enough to remove credentials from the exported data. it's your responsibility
to ensure that the exported data does not contain any sensitive information for sharing.
</li>
</ul>
</Message>
</div>
</div>
</template>
@ -301,4 +313,23 @@ const sendTest = async () => {
onMounted(async () => socket.isConnected ? await reloadContent(true) : '')
const exportItem = async item => {
let data = JSON.parse(JSON.stringify(item))
const keys = ['id', 'raw']
keys.forEach(k => {
if (data.hasOwnProperty(k)) {
delete data[k]
}
})
// -- Remove authorization headers
if (data.request?.headers && data.request.headers.length > 0) {
data.request.headers = data.request.headers.filter(h => h.key.toLowerCase() !== 'authorization')
}
data['_type'] = 'notification'
data['_version'] = '1.0'
return copyText(base64UrlEncode(JSON.stringify(data)));
}
</script>

View file

@ -58,7 +58,7 @@ div.is-centered {
<header class="card-header">
<div class="card-header-title is-text-overflow is-block" v-text="item.name" />
<div class="card-header-icon">
<a class="has-text-primary" v-tooltip="'Export preset.'" @click.prevent="copyItem(item)">
<a class="has-text-primary" v-tooltip="'Export preset.'" @click.prevent="exportItem(item)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
</a>
<button @click="item.raw = !item.raw">
@ -116,7 +116,7 @@ div.is-centered {
v-if="!presets || presets.length < 1" />
</div>
</div>
<div class="column is-12">
<div class="column is-12" v-if="presets && presets.length > 0 && !toggleForm">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
@ -284,10 +284,10 @@ const editItem = (item) => {
onMounted(async () => (socket.isConnected ? await reloadContent(true) : ""));
const copyItem = (item) => {
const exportItem = item => {
let data = JSON.parse(JSON.stringify(item));
const keys = ["id", "default", "raw", "cookies"];
keys.forEach((key) => {
keys.forEach(key => {
if (key in data) {
delete data[key];
}
@ -310,6 +310,9 @@ const copyItem = (item) => {
userData[key] = data[key];
}
userData['_type'] = 'preset';
userData['_version'] = '1.0'
return copyText(base64UrlEncode(JSON.stringify(userData)));
};

View file

@ -39,8 +39,8 @@ div.is-centered {
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
The task runner is simply queuing given urls at the specified time. It's basically doing what you are doing
when you click the add button on the WebGUI, this just fancy way to automate that.
The task runner is simple queue system that allows you to schedule downloads. The tasks are run at the
scheduled time.
</span>
</div>
</div>
@ -59,8 +59,7 @@ div.is-centered {
<NuxtLink target="_blank" :href="item.url">{{ item.name }}</NuxtLink>
</div>
<div class="card-header-icon">
<a :href="item.url" class="has-text-primary" v-tooltip="'Export task.'"
@click.prevent="exportTask(item)">
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
</a>
<button @click="item.raw = !item.raw">
@ -121,7 +120,7 @@ div.is-centered {
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
</div>
</div>
<div class="column is-12">
<div class="column is-12" v-if="tasks && tasks.length > 0 && !toggleForm">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
@ -137,7 +136,7 @@ div.is-centered {
<script setup>
import moment from 'moment'
import { parseExpression } from 'cron-parser'
import { CronExpressionParser } from 'cron-parser'
import { request } from '~/utils/index'
const toast = useToast()
@ -304,7 +303,7 @@ onMounted(async () => {
const tryParse = expression => {
try {
return moment(parseExpression(expression).next().toISOString()).fromNow()
return moment(CronExpressionParser.parse(expression).next().toISOString()).fromNow()
} catch (e) {
return "Invalid"
}
@ -359,14 +358,14 @@ const statusHandler = async stream => {
}
}
const exportTask = async task => {
let preset = config.presets.find(p => p.name === task.preset)
const exportItem = async item => {
let preset = config.presets.find(p => p.name === item.preset)
if (!preset) {
toast.error('Preset not found.')
return
}
const info = JSON.parse(JSON.stringify(task))
const info = JSON.parse(JSON.stringify(item))
preset = JSON.parse(JSON.stringify(preset))
let data = {
@ -421,6 +420,9 @@ const exportTask = async task => {
data.config[key] = args[key]
}
data['_type'] = 'task'
data['_version'] = '1.0'
return copyText(base64UrlEncode(JSON.stringify(data)));
}
</script>

File diff suppressed because it is too large Load diff