added export to tasks.

This commit is contained in:
ArabCoders 2025-03-14 21:30:29 +03:00
parent ada7b5db97
commit b5803f94b1
4 changed files with 213 additions and 28 deletions

13
.vscode/launch.json vendored
View file

@ -43,6 +43,19 @@
"YTP_ACCESS_LOG": "true",
}
},
{
"name": "Node: Generate UI",
"request": "launch",
"runtimeArgs": [
"run",
"generate"
],
"runtimeExecutable": "yarn",
"type": "node",
"cwd": "${workspaceFolder}/ui",
"console": "internalConsole",
"outputCapture": "std",
},
{
"name": "Python: main.py (Deep)",
"type": "debugpy",

View file

@ -1,7 +1,41 @@
<template>
<main class="columns mt-2">
<div class="column">
<form id="taskForm" @submit.prevent="checkInfo()" class="box">
<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 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;">
@ -40,23 +74,6 @@
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<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">
<div class="field">
<label class="label is-inline" for="folder">
@ -104,6 +121,23 @@
</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">
@ -179,7 +213,7 @@
<p class="control">
<button class="button is-info" type="button" @click="showAdvanced = !showAdvanced">
<span class="icon"><i class="fa-solid fa-cog" /></span>
<span>Opts</span>
<span>Advanced</span>
</button>
</p>
</div>
@ -196,13 +230,14 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { parseExpression } from 'cron-parser'
import { request } from '~/utils/index'
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 import_string = ref('');
const props = defineProps({
reference: {
@ -311,4 +346,66 @@ const hasFormatInConfig = computed(() => {
return false
}
})
const importTask = async () => {
let val = import_string.value.trim()
if (!val) {
toast.error('The preset import string is required.')
return
}
if (false === val.startsWith('{')) {
try {
val = base64UrlDecode(val)
} catch (e) {
console.error(e)
toast.error('Invalid base64 string.')
return
}
}
try {
const task = JSON.parse(val)
if (form.config || form.url || form.timer) {
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
return
}
}
if (task.name) {
form.name = task.name
}
if (task.url) {
form.url = task.url
}
if (task.preset) {
form.preset = task.preset
}
if (task.timer) {
form.timer = task.timer
}
if (task.folder) {
form.folder = task.folder
}
if (task.template) {
form.template = task.template
}
if (task.config) {
form.config = JSON.stringify(task.config, null, 2)
}
import_string.value = ''
importExpanded.value = false
} catch (e) {
console.error(e)
toast.error(`Failed to import task. ${e.message}`)
}
}
</script>

View file

@ -59,7 +59,7 @@ div.is-centered {
<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)">
<span class="icon"><i class="fa-solid fa-copy" /></span>
<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" :class="{
@ -120,8 +120,7 @@ div.is-centered {
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
When you export the preset, it doesn't include <code>Cookies</code> field for security reasons. The exported
string is URL safe base64 encoded JSON string.
When you export preset, it doesn't include <code>Cookies</code> field for security reasons.
</li>
</ul>
</Message>

View file

@ -59,9 +59,9 @@ 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="'Copy url.'"
@click.prevent="copyText(item.url)">
<span class="icon"><i class="fa-solid fa-copy" /></span>
<a :href="item.url" class="has-text-primary" v-tooltip="'Export task.'"
@click.prevent="exportTask(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"
@ -121,6 +121,17 @@ div.is-centered {
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
When you export task, the preset settings is automatically merged into the task and the preset set to
<code>default</code> to be more portable. The exporter doesn't include <code>Cookies</code> field for
security reasons.
</li>
</ul>
</Message>
</div>
</div>
</template>
@ -347,4 +358,69 @@ const statusHandler = async stream => {
return
}
}
const exportTask = async task => {
let preset = config.presets.find(p => p.name === task.preset)
if (!preset) {
toast.error('Preset not found.')
return
}
const info = JSON.parse(JSON.stringify(task))
preset = JSON.parse(JSON.stringify(preset))
let data = {
name: info.name,
url: info.url,
preset: 'default',
timer: info.timer,
folder: info.folder,
template: info.template,
config: info.config,
}
// -- merge preset options with task args.
let args = {}
if (preset.args && Object.keys(preset.args).length > 0) {
for (const key of Object.keys(preset.args)) {
args[key] = preset.args[key]
}
}
if (preset.format !== 'not_set' && preset.format) {
args.format = preset.format
}
if (preset.postprocessors && preset.postprocessors.length > 0) {
args.postprocessors = preset.postprocessors
}
if (preset.folder && !info.folder) {
data.folder = preset.folder
}
if (preset.template && !info.template) {
data.template = preset.template
}
if (!data.config || Object.keys(data.config).length < 1) {
data.config = {}
}
for (const key of Object.keys(args)) {
if (key in data.config && Array.isArray(args[key]) && Array.isArray(data.config[key])) {
data.config[key] = data.config[key].concat(args[key])
continue
}
if (data?.config[key]) {
continue
}
data.config[key] = args[key]
}
return copyText(base64UrlEncode(JSON.stringify(data)));
}
</script>