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

View file

@ -360,7 +360,10 @@ class Notification(metaclass=Singleton):
method=target.get("request", {}).get("method", "POST"), method=target.get("request", {}).get("method", "POST"),
url=target.get("request", {}).get("url"), url=target.get("request", {}).get("url"),
headers=[ 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", []) for h in target.get("request", {}).get("headers", [])
], ],
), ),

View file

@ -1,16 +1,57 @@
<template> <template>
<main class="columns mt-2"> <main class="columns mt-2 is-multiline">
<div class="column">
<form id="taskForm" @submit.prevent="checkInfo()">
<div class="box">
<div class="columns is-multiline is-mobile">
<div class="column is-12"> <div class="column is-12">
<h1 class="title is-6" style="border-bottom: 1px solid #dbdbdb;"> <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-text">
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span> <span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
<span>{{ reference ? 'Edit' : 'Add' }}</span> <span>{{ reference ? 'Edit' : 'Add' }}</span>
</span> </span>
</h1> </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>
<span v-if="showImport">Hide</span>
<span v-else>Show</span>
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>
<div class="column is-6-tablet is-12-mobile"> <div class="column is-6-tablet is-12-mobile">
@ -119,7 +160,8 @@
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span> <span>
Subscribe to the events you want to listen for. When the event is triggered, the notification will 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. be sent to the target URL. If no events are selected, the notification will be sent for all
events.
</span> </span>
</span> </span>
</div> </div>
@ -178,26 +220,25 @@
</span> </span>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="card-footer">
<div class="field is-grouped is-grouped-right"> <p class="card-footer-item">
<p class="control"> <button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
<button class="button is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="taskForm"> :class="{ 'is-loading': addInProgress }" form="taskForm">
<span class="icon"><i class="fa-solid fa-save" /></span> <span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span> <span>Save</span>
</button> </button>
</p> </p>
<p class="control"> <p class="card-footer-item">
<button class="button is-danger" @click="emitter('cancel')" :disabled="addInProgress" type="button"> <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 class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span> <span>Cancel</span>
</button> </button>
</p> </p>
</div> </div>
</div> </div>
</div>
</div> </div>
</form> </form>
</div> </div>
@ -205,6 +246,7 @@
</template> </template>
<script setup> <script setup>
import { useStorage } from '@vueuse/core'
const emitter = defineEmits(['cancel', 'submit']); const emitter = defineEmits(['cancel', 'submit']);
const toast = useToast(); const toast = useToast();
const props = defineProps({ const props = defineProps({
@ -231,6 +273,8 @@ const props = defineProps({
const form = reactive(props.item); const form = reactive(props.item);
const requestMethods = ['POST', 'PUT']; const requestMethods = ['POST', 'PUT'];
const requestType = ['json', 'form']; const requestType = ['json', 'form'];
const showImport = useStorage('showImport', false);
const import_string = ref('');
const checkInfo = async () => { const checkInfo = async () => {
const required = ['name', 'request.url', 'request.method', 'request.type']; const required = ['name', 'request.url', 'request.method', 'request.type'];
@ -254,13 +298,73 @@ const checkInfo = async () => {
return; return;
} }
let headers = []
for (const header of form.request.headers) { for (const header of form.request.headers) {
if (!header.key || !header.value) { 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) }); 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> </script>

View file

@ -1,38 +1,5 @@
<template> <template>
<main class="columns mt-2 is-multiline"> <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"> <div class="column is-12">
<h1 class="is-unselectable is-pointer title is-5" @click="convertExpanded = !convertExpanded"> <h1 class="is-unselectable is-pointer title is-5" @click="convertExpanded = !convertExpanded">
<span class="icon-text"> <span class="icon-text">
@ -40,10 +7,8 @@
<span>Convert yt-dlp cli options.</span> <span>Convert yt-dlp cli options.</span>
</span> </span>
</h1> </h1>
<form autocomplete="off" id="convertOpts" @submit.prevent="convertOptions()" v-if="convertExpanded"> <form autocomplete="off" id="convertOpts" @submit.prevent="convertOptions()" v-if="convertExpanded">
<div class="box"> <div class="box">
<label class="label" for="opts"> <label class="label" for="opts">
yt-dlp CLI options yt-dlp CLI options
</label> </label>
@ -74,18 +39,54 @@
</div> </div>
<div class="column is-12"> <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">
<div class="card-header"> <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-text">
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span> <span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
<span>{{ reference ? 'Edit' : 'Add' }}</span> <span>{{ reference ? 'Edit' : 'Add' }}</span>
</span> </span>
</div> </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>
<div class="card-content"> <div class="card-content">
<div class="columns is-multiline is-mobile"> <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="column is-6-tablet is-12-mobile">
<div class="field"> <div class="field">
<label class="label is-inline" for="name" v-text="'Name'" /> <label class="label is-inline" for="name" v-text="'Name'" />
@ -218,6 +219,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="card-footer"> <div class="card-footer">
<div class="card-footer-item"> <div class="card-footer-item">
@ -235,8 +237,6 @@
</button> </button>
</div> </div>
</div> </div>
</div>
</div> </div>
</form> </form>
</div> </div>
@ -247,6 +247,7 @@
</template> </template>
<script setup> <script setup>
import { useStorage } from '@vueuse/core'
const emitter = defineEmits(['cancel', 'submit']); const emitter = defineEmits(['cancel', 'submit']);
const props = defineProps({ const props = defineProps({
@ -276,8 +277,8 @@ const toast = useToast()
const convertInProgress = ref(false) const convertInProgress = ref(false)
const form = reactive(JSON.parse(JSON.stringify(props.preset))) const form = reactive(JSON.parse(JSON.stringify(props.preset)))
const opts = ref('') const opts = ref('')
const json_preset = ref('') const import_string = ref('')
const importExpanded = ref(false) const showImport = useStorage('showImport', false);
const convertExpanded = ref(false) const convertExpanded = ref(false)
onMounted(() => { onMounted(() => {
@ -395,10 +396,10 @@ const convertOptions = async () => {
} }
} }
const importPreset = async () => { const importItem = async () => {
let val = json_preset.value.trim() let val = import_string.value.trim()
if (!val) { if (!val) {
toast.error('The preset import string is required.') toast.error('The import string is required.')
return return
} }
@ -406,13 +407,20 @@ const importPreset = async () => {
try { try {
val = base64UrlDecode(val) val = base64UrlDecode(val)
} catch (e) { } catch (e) {
toast.error('Invalid base64 string.', e) console.error(e)
toast.error(`Failed to decode string. ${e.message}`)
return return
} }
} }
try { 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 (form.format || form.args || form.postprocessors) {
if (false === confirm('This will overwrite the current form fields. Are you sure?')) { if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
@ -420,35 +428,35 @@ const importPreset = async () => {
} }
} }
if (preset.name) { if (item.name) {
form.name = preset.name form.name = item.name
} }
if (preset.format) { if (item.format) {
form.format = preset.format form.format = item.format
} }
if (preset.args) { if (item.args) {
form.args = JSON.stringify(preset.args, null, 2) form.args = JSON.stringify(item.args, null, 2)
} }
if (preset.postprocessors) { if (item.postprocessors) {
form.postprocessors = JSON.stringify(preset.postprocessors, null, 2) form.postprocessors = JSON.stringify(item.postprocessors, null, 2)
} }
if (preset.output_template) { if (item.output_template) {
form.template = preset.output_template form.template = item.output_template
} }
if (preset.folder) { if (item.folder) {
form.folder = preset.folder form.folder = item.folder
} }
json_preset.value = '' import_string.value = ''
importExpanded.value = false showImport.value = false
} catch (e) { } catch (e) {
console.error(e) console.error(e)
toast.error(`Failed to import preset. ${e.message}`) toast.error(`Failed to string. ${e.message}`)
} }
} }
</script> </script>

View file

@ -1,51 +1,56 @@
<template> <template>
<main class="columns mt-2 is-multiline"> <main class="columns mt-2 is-multiline">
<div class="column is-12"> <div class="column is-12">
<h1 class="is-pointer is-unselectable title is-5" @click="importExpanded = !importExpanded"> <form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()">
<span class="icon-text"> <div class="card">
<span class="icon"><i class="fa-solid" :class="importExpanded ? 'fa-arrow-up' : 'fa-arrow-down'" /></span>
<span>Import task.</span>
</span>
</h1>
<form id="importForm" @submit.prevent="importTask()" v-if="importExpanded"> <div class="card-header">
<div class="box"> <div class="card-header-title is-text-overflow is-block">
<label class="label" for="import_string"> <span class="icon-text">
Base64 encoded JSON string. <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> </label>
<div class="field has-addons"> <div class="field has-addons">
<div class="control has-icons-left is-expanded"> <div class="control has-icons-left is-expanded">
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off"> <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> <span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
</div> </div>
<div class="control"> <div class="control">
<button form="importForm" class="button is-primary" :disabled="!import_string" type="submit"> <button class="button is-primary" :disabled="!import_string" type="button" @click="importItem">
<span class="icon"><i class="fa-solid fa-add" /></span> <span class="icon"><i class="fa-solid fa-add" /></span>
<span>Import</span> <span>Import</span>
</button> </button>
</div> </div>
</div> </div>
</div> <span class="help">
</form> <span class="icon"><i class="fa-solid fa-info" /></span>
</div> <span>You can use this field to populate the data, using shared string.</span>
<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;">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
<span>{{ reference ? 'Edit' : 'Add' }}</span>
</span> </span>
</h1>
</div> </div>
<div class="column is-6-tablet is-12-mobile"> <div class="column is-6-tablet is-12-mobile">
<div class="field"> <div class="field">
<label class="label is-inline" for="name" v-text="'Name'" /> <label class="label is-inline" for="name" v-text="'Name'" />
@ -161,7 +166,8 @@
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced"> <div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field"> <div class="field">
<label class="label is-inline" for="config" v-tooltip="'Extends current global yt-dlp config. (JSON)'"> <label class="label is-inline" for="config"
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
JSON yt-dlp config or CLI options. <NuxtLink JSON yt-dlp config or CLI options. <NuxtLink
v-if="form.config && (typeof form.config === 'string') && !form.config.trim().startsWith('{')" v-if="form.config && (typeof form.config === 'string') && !form.config.trim().startsWith('{')"
@click="convertOptions()">Convert to JSON</NuxtLink> @click="convertOptions()">Convert to JSON</NuxtLink>
@ -173,7 +179,8 @@
<span class="help"> <span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span> <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 <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 <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> some of those options can break yt-dlp or the frontend.</span>
</span> </span>
</div> </div>
@ -181,7 +188,8 @@
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced"> <div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
<div class="field"> <div class="field">
<label class="label is-inline" for="cookies" v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label> <label class="label is-inline" for="cookies"
v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
<div class="control"> <div class="control">
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" /> <textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" />
</div> </div>
@ -195,30 +203,32 @@
</div> </div>
</div> </div>
<div class="column is-12"> </div>
<div class="field is-grouped is-grouped-right"> </div>
<p class="control">
<button class="button is-primary" :disabled="addInProgress" type="submit" <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"> :class="{ 'is-loading': addInProgress }" form="taskForm">
<span class="icon"><i class="fa-solid fa-save" /></span> <span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span> <span>Save</span>
</button> </button>
</p> </p>
<p class="control"> <p class="card-footer-item">
<button class="button is-danger" @click="emitter('cancel')" :disabled="addInProgress" type="button"> <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 class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span> <span>Cancel</span>
</button> </button>
</p> </p>
<p class="control"> <p class="card-footer-item">
<button class="button is-info" type="button" @click="showAdvanced = !showAdvanced"> <button class="button is-fullwidth is-info" type="button" @click="showAdvanced = !showAdvanced">
<span class="icon"><i class="fa-solid fa-cog" /></span> <span class="icon"><i class="fa-solid fa-cog" /></span>
<span>Advanced</span> <span>Advanced</span>
</button> </button>
</p> </p>
</div> </div>
</div> </div>
</div>
</form> </form>
</div> </div>
<datalist id="folders" v-if="config?.folders"> <datalist id="folders" v-if="config?.folders">
@ -229,14 +239,14 @@
<script setup> <script setup>
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { parseExpression } from 'cron-parser' import { CronExpressionParser } from 'cron-parser'
const emitter = defineEmits(['cancel', 'submit']); const emitter = defineEmits(['cancel', 'submit']);
const toast = useToast(); const toast = useToast();
const config = useConfigStore(); const config = useConfigStore();
const convertInProgress = ref(false); const convertInProgress = ref(false);
const showAdvanced = useStorage('task.showAdvanced', false); const showAdvanced = useStorage('task.showAdvanced', false);
const importExpanded = ref(false); const showImport = useStorage('showImport', false);
const import_string = ref(''); const import_string = ref('');
const props = defineProps({ const props = defineProps({
@ -279,9 +289,10 @@ const checkInfo = async () => {
if (form.timer) { if (form.timer) {
try { try {
parseExpression(form.timer); CronExpressionParser.parse(form.timer);
} catch (e) { } catch (e) {
toast.error('Invalid CRON expression.'); console.log(e)
toast.error(`Invalid CRON expression. ${e.message}`);
return; return;
} }
} }
@ -347,10 +358,10 @@ const hasFormatInConfig = computed(() => {
} }
}) })
const importTask = async () => { const importItem = async () => {
let val = import_string.value.trim() let val = import_string.value.trim()
if (!val) { if (!val) {
toast.error('The preset import string is required.') toast.error('The import string is required.')
return return
} }
@ -359,13 +370,19 @@ const importTask = async () => {
val = base64UrlDecode(val) val = base64UrlDecode(val)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
toast.error('Invalid base64 string.') toast.error(`Failed to decode string. ${e.message}`)
return return
} }
} }
try { 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 (form.config || form.url || form.timer) {
if (false === confirm('This will overwrite the current form fields. Are you sure?')) { if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
@ -373,39 +390,38 @@ const importTask = async () => {
} }
} }
if (task.name) { if (item.name) {
form.name = task.name form.name = item.name
} }
if (task.url) { if (item.url) {
form.url = task.url form.url = item.url
} }
if (task.preset) { if (item.preset) {
form.preset = task.preset form.preset = item.preset
} }
if (task.timer) { if (item.timer) {
form.timer = task.timer form.timer = item.timer
} }
if (task.folder) { if (item.folder) {
form.folder = task.folder form.folder = item.folder
} }
if (task.template) { if (item.template) {
form.template = task.template form.template = item.template
} }
if (task.config) { if (item.config) {
form.config = JSON.stringify(task.config, null, 2) form.config = JSON.stringify(item.config, null, 2)
} }
import_string.value = '' import_string.value = ''
importExpanded.value = false
} catch (e) { } catch (e) {
console.error(e) console.error(e)
toast.error(`Failed to import task. ${e.message}`) toast.error(`Failed to import string. ${e.message}`)
} }
} }
</script> </script>

View file

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

View file

@ -66,9 +66,8 @@ div.is-centered {
<NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink> <NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink>
</div> </div>
<div class="card-header-icon"> <div class="card-header-icon">
<a :href="item.url" class="has-text-primary" v-tooltip="'Copy url.'" <a class="has-text-primary" v-tooltip="'Export target.'" @click.prevent="exportItem(item)">
@click.prevent="copyText(item.request.url)"> <span class="icon"><i class="fa-solid fa-file-export" /></span>
<span class="icon"><i class="fa-solid fa-copy" /></span>
</a> </a>
<button @click="item.raw = !item.raw"> <button @click="item.raw = !item.raw">
<span class="icon"><i class="fa-solid" <span class="icon"><i class="fa-solid"
@ -116,6 +115,19 @@ div.is-centered {
</Message> </Message>
</div> </div>
</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> </div>
</template> </template>
@ -301,4 +313,23 @@ const sendTest = async () => {
onMounted(async () => socket.isConnected ? await reloadContent(true) : '') 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> </script>

View file

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

View file

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

File diff suppressed because it is too large Load diff