Added simple file browser
This commit is contained in:
parent
fa73e817a6
commit
21e5dea5bf
9 changed files with 439 additions and 100 deletions
|
|
@ -15,6 +15,7 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
|
|||
* Send notification to targets based on selected events.
|
||||
* Support per link `yt-dlp JSON config or cli options`, `cookies` & `output format`.
|
||||
* Queue multiple URLs separated by comma.
|
||||
* Simple file browser. `Disabled by default`
|
||||
* A built in video player that can play any video file regardless of the format. **With support for sidecar external subtitles**.
|
||||
* New `POST /api/history` endpoint that allow one or multiple links to be sent at the same time.
|
||||
* New `GET /api/history/add?url=http://..` endpoint that allow to add single item via GET request.
|
||||
|
|
@ -112,6 +113,7 @@ Certain configuration values can be set via environment variables, using the `-e
|
|||
| YTP_PIP_IGNORE_UPDATES | Do not update the custom pip packages | `false` |
|
||||
| YTP_BASIC_MODE | Whether to run WebUI in basic mode | `false` |
|
||||
| YTP_PICTURES_BACKENDS | A comma separated list of pictures urls to use. | `empty string` |
|
||||
| YTP_BROWSER_ENABLED | Whether to enable the file browser | `false` |
|
||||
|
||||
# Browser extensions & bookmarklets
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import uuid
|
|||
from collections.abc import Awaitable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, urlparse
|
||||
from urllib.parse import quote, unquote_plus, urlparse
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
|
|
@ -42,6 +42,7 @@ from .Utils import (
|
|||
encrypt_data,
|
||||
extract_info,
|
||||
get_file,
|
||||
get_files,
|
||||
get_mime_type,
|
||||
get_sidecar_subtitles,
|
||||
validate_url,
|
||||
|
|
@ -1704,3 +1705,40 @@ class HttpAPI(Common):
|
|||
await self._notify.emit(Events.TEST, data=data)
|
||||
|
||||
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
|
||||
|
||||
@route("GET", "api/file/browser/{path:.*}")
|
||||
async def file_browser(self, request: Request) -> Response:
|
||||
"""
|
||||
Get the file browser.
|
||||
|
||||
Args:
|
||||
request (Request): The request object.
|
||||
|
||||
Returns:
|
||||
Response: The response object.
|
||||
|
||||
"""
|
||||
if not self.config.browser_enabled:
|
||||
return web.json_response(data={"error": "File browser is disabled."}, status=web.HTTPForbidden.status_code)
|
||||
|
||||
path = request.match_info.get("path")
|
||||
path = "/" if not path else unquote_plus(path)
|
||||
|
||||
test = os.path.realpath(os.path.join(self.config.download_path, path))
|
||||
if not os.path.exists(test):
|
||||
return web.json_response(
|
||||
data={"error": f"path '{path}' does not exist."}, status=web.HTTPNotFound.status_code
|
||||
)
|
||||
|
||||
try:
|
||||
return web.json_response(
|
||||
data={
|
||||
"path": path,
|
||||
"contents": get_files(base_path=self.config.download_path, dir=path),
|
||||
},
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=self.encoder.encode,
|
||||
)
|
||||
except OSError as e:
|
||||
LOG.exception(e)
|
||||
return web.json_response(data={"error": str(e)}, status=web.HTTPInternalServerError.status_code)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import base64
|
||||
import copy
|
||||
import datetime
|
||||
import glob
|
||||
import ipaddress
|
||||
import json
|
||||
|
|
@ -702,3 +703,53 @@ def get(
|
|||
else:
|
||||
return default() if callable(default) else default
|
||||
return data
|
||||
|
||||
|
||||
def get_files(base_path: str, dir: str | None = None):
|
||||
"""
|
||||
Get directory contents.
|
||||
|
||||
Args:
|
||||
base_path (str): Base download path.
|
||||
dir (str): Directory to check.
|
||||
|
||||
Returns:
|
||||
list: List of files and directories.
|
||||
|
||||
Raises:
|
||||
OSError: If the directory is invalid or not a directory.
|
||||
|
||||
"""
|
||||
if dir and dir != "/":
|
||||
path = os.path.normpath(os.path.join(base_path, str(dir)))
|
||||
if not path.startswith(base_path):
|
||||
msg = f"Invalid path: '{dir}' - '{path}' - must be inside '{base_path}'."
|
||||
raise OSError(msg)
|
||||
dir_path = os.path.realpath(path)
|
||||
else:
|
||||
dir_path = base_path
|
||||
|
||||
if not os.path.isdir(dir_path):
|
||||
msg = f"Invalid path: '{dir_path}' - must be a directory."
|
||||
raise OSError(msg)
|
||||
|
||||
contents: list = []
|
||||
for file in pathlib.Path(dir_path).iterdir():
|
||||
if file.name.startswith(".") or file.name.startswith("_"):
|
||||
continue
|
||||
|
||||
stat = file.stat()
|
||||
contents.append(
|
||||
{
|
||||
"type": "file" if file.is_file() else "dir",
|
||||
"name": file.name,
|
||||
"path": str(file).replace(base_path, "").strip("/"),
|
||||
"size": stat.st_size,
|
||||
"mtime": datetime.datetime.fromtimestamp(stat.st_mtime, tz=datetime.UTC).isoformat(),
|
||||
"ctime": datetime.datetime.fromtimestamp(stat.st_ctime, tz=datetime.UTC).isoformat(),
|
||||
"is_dir": file.is_dir(),
|
||||
"is_file": file.is_file(),
|
||||
}
|
||||
)
|
||||
|
||||
return contents
|
||||
|
|
|
|||
|
|
@ -146,6 +146,9 @@ class Config:
|
|||
console_enabled: bool = False
|
||||
"Enable direct access to yt-dlp console."
|
||||
|
||||
browser_enabled: bool = False
|
||||
"Enable file browser access."
|
||||
|
||||
pictures_backends: list[str] = [
|
||||
"https://unsplash.it/1920/1080?random",
|
||||
"https://picsum.photos/1920/1080",
|
||||
|
|
@ -196,6 +199,7 @@ class Config:
|
|||
"basic_mode",
|
||||
"file_logging",
|
||||
"console_enabled",
|
||||
"browser_enabled",
|
||||
)
|
||||
"The variables that are booleans."
|
||||
|
||||
|
|
@ -214,6 +218,7 @@ class Config:
|
|||
"instance_title",
|
||||
"sentry_dsn",
|
||||
"console_enabled",
|
||||
"browser_enabled"
|
||||
)
|
||||
"The variables that are relevant to the frontend."
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@
|
|||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" v-if="!config.app.basic_mode && config.app.browser_enabled">
|
||||
<NuxtLink class="button is-dark has-tooltip-bottom" to="/browser">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa-solid fa-folder-tree" /></span>
|
||||
<span class="is-hidden-mobile">Browser</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" v-if="!config.app.basic_mode">
|
||||
<NuxtLink class="button is-dark has-tooltip-bottom" to="/presets">
|
||||
<span class="icon-text">
|
||||
|
|
@ -108,7 +117,7 @@ import 'assets/css/style.css'
|
|||
import 'assets/css/all.css'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import moment from 'moment'
|
||||
import * as Sentry from "@sentry/nuxt";
|
||||
import * as Sentry from '@sentry/nuxt'
|
||||
|
||||
const Year = new Date().getFullYear()
|
||||
const selectedTheme = useStorage('theme', 'auto')
|
||||
|
|
@ -206,8 +215,8 @@ watch(loadedImage, v => {
|
|||
return
|
||||
}
|
||||
|
||||
const html = document.documentElement;
|
||||
const body = document.querySelector('body');
|
||||
const html = document.documentElement
|
||||
const body = document.querySelector('body')
|
||||
|
||||
const style = {
|
||||
"background-color": "unset",
|
||||
|
|
@ -219,7 +228,7 @@ watch(loadedImage, v => {
|
|||
|
||||
html.setAttribute("style", Object.keys(style).map(k => `${k}: ${style[k]}`).join('; ').trim())
|
||||
html.classList.add('bg-fanart')
|
||||
body.setAttribute("style", `opacity: ${bg_opacity.value}`);
|
||||
body.setAttribute("style", `opacity: ${bg_opacity.value}`)
|
||||
})
|
||||
|
||||
const handleImage = async enabled => {
|
||||
|
|
@ -228,14 +237,14 @@ const handleImage = async enabled => {
|
|||
return
|
||||
}
|
||||
|
||||
const html = document.documentElement;
|
||||
const body = document.querySelector('body');
|
||||
const html = document.documentElement
|
||||
const body = document.querySelector('body')
|
||||
|
||||
if (html.getAttribute("style")) {
|
||||
html.removeAttribute("style");
|
||||
html.removeAttribute("style")
|
||||
}
|
||||
if (body.getAttribute("style")) {
|
||||
body.removeAttribute("style");
|
||||
body.removeAttribute("style")
|
||||
}
|
||||
loadedImage.value = ''
|
||||
return
|
||||
|
|
|
|||
233
ui/pages/browser/[...slug].vue
Normal file
233
ui/pages/browser/[...slug].vue
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mt-1 columns is-multiline">
|
||||
<div class="column is-12 is-clearfix">
|
||||
<span class="title is-4">
|
||||
<nav class="breadcrumb is-inline-block">
|
||||
<ul>
|
||||
<li v-for="(item, index) in makeBreadCrumb(path)" :key="item.link"
|
||||
:class="{ 'is-active': index === makeBreadCrumb(path).length - 1 }">
|
||||
<a :href="item.link" @click.prevent="reloadContent(item.path)" v-text="item.name" />
|
||||
</li>
|
||||
<li class="is-active" v-if="isLoading">
|
||||
<NuxtLink>
|
||||
<span class="icon"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</span>
|
||||
<div class="is-pulled-right">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-info" @click="reloadContent(path, true)" :class="{ 'is-loading': isLoading }"
|
||||
:disabled="!socket.isConnected || isLoading">
|
||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">Files Browser</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12" v-if="items && items.length > 0">
|
||||
<div class="table-container is-responsive">
|
||||
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
|
||||
style="min-width: 1300px; table-layout: fixed;">
|
||||
<thead>
|
||||
<tr class="has-text-centered is-unselectable">
|
||||
<th width="5%">#</th>
|
||||
<th width="55%">Name</th>
|
||||
<th width="10%">Size</th>
|
||||
<th width="15%">Created</th>
|
||||
<th width="15%">Modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in items" :key="item.path">
|
||||
<td class="has-text-centered is-vcentered">
|
||||
<span class="icon">
|
||||
<i class="fas fa-solid"
|
||||
:class="{ 'fa-file': item.type === 'file', 'fa-folder': item.type === 'dir' }" />
|
||||
</span>
|
||||
</td>
|
||||
<td class="is-text-overflow is-vcentered" v-tooltip="item.name">
|
||||
<a :href="`/browser/${item.path}`" v-if="item.type === 'dir'"
|
||||
@click.prevent="reloadContent(item.path)">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
<a :href="makeDownload({}, { filename: item.path, folder: '' })" v-else>{{ item.name }}</a>
|
||||
</td>
|
||||
<td class="has-text-centered is-text-overflow is-unselectable">
|
||||
{{ 'file' === item.type ? formatBytes(item.size) : 'Dir' }}
|
||||
</td>
|
||||
<td class="has-text-centered is-text-overflow is-unselectable">
|
||||
<span :data-datetime="item.ctime" v-tooltip="moment(item.ctime).format('MMMM Do YYYY, h:mm:ss a')">
|
||||
{{ moment(item.ctime).fromNow() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="has-text-centered is-text-overflow is-unselectable">
|
||||
<span :data-datetime="item.mtime" v-tooltip="moment(item.mtime).format('MMMM Do YYYY, h:mm:ss a')">
|
||||
{{ moment(item.mtime).fromNow() }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12" v-else>
|
||||
<Message title="Loading content" class="is-background-info-80" icon="fas fa-refresh fa-spin" v-if="isLoading">
|
||||
Loading file browser contents...
|
||||
</Message>
|
||||
<Message v-else title="No Content" class="is-background-warning-80 has-text-dark"
|
||||
icon="fas fa-exclamation-circle">
|
||||
No content found in this directory.
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { request } from '~/utils/index'
|
||||
import moment from 'moment'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const initialLoad = ref(true)
|
||||
const items = ref([])
|
||||
const path = ref(`/${route.params.slug?.length > 0 ? route.params.slug?.join('/') : ''}`)
|
||||
|
||||
watch(() => config.app.basic_mode, async () => {
|
||||
if (!config.app.basic_mode) {
|
||||
return
|
||||
}
|
||||
await navigateTo('/')
|
||||
})
|
||||
|
||||
watch(() => config.app.browser_enabled, async () => {
|
||||
if (config.app.browser_enabled) {
|
||||
return
|
||||
}
|
||||
await navigateTo('/')
|
||||
})
|
||||
|
||||
watch(() => socket.isConnected, async () => {
|
||||
if (socket.isConnected && initialLoad.value) {
|
||||
await reloadContent(path.value, true)
|
||||
initialLoad.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const reloadContent = async (dir = '/', fromMounted = false) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (typeof dir !== 'string') {
|
||||
dir = '/'
|
||||
}
|
||||
|
||||
dir = sTrim(dir, '/')
|
||||
|
||||
const response = await request(`/api/file/browser/${sTrim(dir, '/')}`)
|
||||
|
||||
if (fromMounted && !response.ok) {
|
||||
return
|
||||
}
|
||||
|
||||
items.value = []
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!data?.contents || data.contents.length > 0) {
|
||||
items.value = data.contents
|
||||
}
|
||||
|
||||
if (data?.path) {
|
||||
path.value = sTrim(data.path, '/')
|
||||
}
|
||||
|
||||
const title = `File Browser: /${dir}`
|
||||
const stateUrl = `/browser/${dir}`
|
||||
|
||||
if (false === fromMounted) {
|
||||
history.pushState({ path: dir, title: title }, title, stateUrl)
|
||||
}
|
||||
|
||||
useHead({ title: title })
|
||||
} catch (e) {
|
||||
if (fromMounted) {
|
||||
return
|
||||
}
|
||||
console.error(e)
|
||||
toast.error('Failed to load file browser contents.')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const popstateHandler = e => {
|
||||
|
||||
if (!e.state) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.state.path === path.value || e.state.path === window.location.pathname) {
|
||||
return
|
||||
}
|
||||
|
||||
reloadContent(e.state.path, true)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (socket.isConnected && initialLoad.value) {
|
||||
await reloadContent(path.value, true)
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', popstateHandler)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('popstate', popstateHandler)
|
||||
})
|
||||
|
||||
const makeBreadCrumb = path => {
|
||||
const baseLink = '/'
|
||||
|
||||
path = path.replace(/^\/+/, '').replace(/\/+$/, '')
|
||||
|
||||
let links = []
|
||||
links.push({
|
||||
name: 'Home',
|
||||
link: baseLink,
|
||||
path: baseLink
|
||||
})
|
||||
|
||||
// -- explode path and create links
|
||||
let parts = path.split('/')
|
||||
parts.forEach((part, index) => {
|
||||
let path = baseLink + parts.slice(0, index + 1).join('/')
|
||||
links.push({
|
||||
name: part,
|
||||
link: path,
|
||||
path: path,
|
||||
})
|
||||
})
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
@ -225,7 +225,7 @@ const updateData = async notifications => {
|
|||
const data = await response.json()
|
||||
|
||||
if (200 !== response.status) {
|
||||
toast.error(`Failed to update notifications. ${data.error}`);
|
||||
toast.error(`Failed to update notifications. ${data.error}`)
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -304,14 +304,14 @@ const sendTest = async () => {
|
|||
|
||||
if (200 !== response.status) {
|
||||
const data = await response.json()
|
||||
toast.error(`Failed to send test notification. ${data.error}`);
|
||||
toast.error(`Failed to send test notification. ${data.error}`)
|
||||
return
|
||||
}
|
||||
|
||||
toast.success('Test notification sent.')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`Failed to send test notification. ${e.message}`);
|
||||
toast.error(`Failed to send test notification. ${e.message}`)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
|
@ -336,6 +336,6 @@ const exportItem = async item => {
|
|||
data['_type'] = 'notification'
|
||||
data['_version'] = '1.0'
|
||||
|
||||
return copyText(base64UrlEncode(JSON.stringify(data)));
|
||||
return copyText(base64UrlEncode(JSON.stringify(data)))
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -129,200 +129,200 @@ div.is-centered {
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { request } from "~/utils/index";
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const toast = useToast();
|
||||
const config = useConfigStore();
|
||||
const socket = useSocketStore();
|
||||
const toast = useToast()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
|
||||
const presets = ref([]);
|
||||
const preset = ref({});
|
||||
const presetRef = ref("");
|
||||
const toggleForm = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const initialLoad = ref(true);
|
||||
const addInProgress = ref(false);
|
||||
const presets = ref([])
|
||||
const preset = ref({})
|
||||
const presetRef = ref("")
|
||||
const toggleForm = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const initialLoad = ref(true)
|
||||
const addInProgress = ref(false)
|
||||
|
||||
const presetsNoDefault = computed(() =>
|
||||
presets.value.filter((t) => !t.default),
|
||||
);
|
||||
)
|
||||
|
||||
watch(
|
||||
() => config.app.basic_mode,
|
||||
async () => {
|
||||
if (!config.app.basic_mode) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
await navigateTo("/");
|
||||
await navigateTo("/")
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
watch(
|
||||
() => socket.isConnected,
|
||||
async () => {
|
||||
if (socket.isConnected && initialLoad.value) {
|
||||
await reloadContent(true);
|
||||
initialLoad.value = false;
|
||||
await reloadContent(true)
|
||||
initialLoad.value = false
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const reloadContent = async (fromMounted = false) => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await request("/api/presets");
|
||||
isLoading.value = true
|
||||
const response = await request("/api/presets")
|
||||
|
||||
if (fromMounted && !response.ok) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json()
|
||||
if (data.length < 1) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
presets.value = data;
|
||||
presets.value = data
|
||||
} catch (e) {
|
||||
if (fromMounted) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
console.error(e);
|
||||
toast.error("Failed to fetch tasks.");
|
||||
console.error(e)
|
||||
toast.error("Failed to fetch tasks.")
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isLoading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const resetForm = (closeForm = false) => {
|
||||
preset.value = {};
|
||||
presetRef.value = null;
|
||||
addInProgress.value = false;
|
||||
preset.value = {}
|
||||
presetRef.value = null
|
||||
addInProgress.value = false
|
||||
if (closeForm) {
|
||||
toggleForm.value = false;
|
||||
toggleForm.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const updatePresets = async (items) => {
|
||||
let data;
|
||||
let data
|
||||
try {
|
||||
addInProgress.value = true;
|
||||
addInProgress.value = true
|
||||
|
||||
const response = await request("/api/presets", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(items.filter((t) => !t.default)),
|
||||
});
|
||||
})
|
||||
|
||||
data = await response.json();
|
||||
data = await response.json()
|
||||
|
||||
if (200 !== response.status) {
|
||||
toast.error(`Failed to update presets. ${data.error}`);
|
||||
return false;
|
||||
toast.error(`Failed to update presets. ${data.error}`)
|
||||
return false
|
||||
}
|
||||
|
||||
presets.value = data;
|
||||
resetForm(true);
|
||||
return true;
|
||||
presets.value = data
|
||||
resetForm(true)
|
||||
return true
|
||||
} catch (e) {
|
||||
toast.error(`Failed to update presets. ${data?.error}. ${e.message}`);
|
||||
toast.error(`Failed to update presets. ${data?.error}. ${e.message}`)
|
||||
} finally {
|
||||
addInProgress.value = false;
|
||||
addInProgress.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const deleteItem = async (item) => {
|
||||
if (true !== confirm(`Delete preset '${item.name}'?`)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const index = presets.value.findIndex((t) => t?.id === item.id);
|
||||
const index = presets.value.findIndex((t) => t?.id === item.id)
|
||||
if (index > -1) {
|
||||
presets.value.splice(index, 1);
|
||||
presets.value.splice(index, 1)
|
||||
} else {
|
||||
toast.error("Preset not found.");
|
||||
return;
|
||||
toast.error("Preset not found.")
|
||||
return
|
||||
}
|
||||
|
||||
const status = await updatePresets(presets.value);
|
||||
const status = await updatePresets(presets.value)
|
||||
|
||||
if (!status) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
toast.success("Preset deleted.");
|
||||
};
|
||||
toast.success("Preset deleted.")
|
||||
}
|
||||
|
||||
const updateItem = async ({ reference, preset }) => {
|
||||
if (reference) {
|
||||
const index = presets.value.findIndex((t) => t?.id === reference);
|
||||
const index = presets.value.findIndex((t) => t?.id === reference)
|
||||
if (index > -1) {
|
||||
presets.value[index] = preset;
|
||||
presets.value[index] = preset
|
||||
}
|
||||
} else {
|
||||
presets.value.push(preset);
|
||||
presets.value.push(preset)
|
||||
}
|
||||
|
||||
const status = await updatePresets(presets.value);
|
||||
const status = await updatePresets(presets.value)
|
||||
if (!status) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
toast.success(`Preset ${reference ? "updated" : "added"}.`);
|
||||
resetForm(true);
|
||||
};
|
||||
toast.success(`Preset ${reference ? "updated" : "added"}.`)
|
||||
resetForm(true)
|
||||
}
|
||||
|
||||
const filterItem = (item) => {
|
||||
const { raw, ...rest } = item;
|
||||
return JSON.stringify(rest, null, 2);
|
||||
};
|
||||
const { raw, ...rest } = item
|
||||
return JSON.stringify(rest, null, 2)
|
||||
}
|
||||
|
||||
const editItem = (item) => {
|
||||
preset.value = item;
|
||||
presetRef.value = item.id;
|
||||
toggleForm.value = true;
|
||||
};
|
||||
preset.value = item
|
||||
presetRef.value = item.id
|
||||
toggleForm.value = true
|
||||
}
|
||||
|
||||
onMounted(async () => (socket.isConnected ? await reloadContent(true) : ""));
|
||||
onMounted(async () => (socket.isConnected ? await reloadContent(true) : ""))
|
||||
|
||||
const exportItem = item => {
|
||||
let data = JSON.parse(JSON.stringify(item));
|
||||
const keys = ["id", "default", "raw", "cookies"];
|
||||
let data = JSON.parse(JSON.stringify(item))
|
||||
const keys = ["id", "default", "raw", "cookies"]
|
||||
keys.forEach(key => {
|
||||
if (key in data) {
|
||||
delete data[key];
|
||||
delete data[key]
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (data.args && typeof data.args === "string") {
|
||||
data.args = JSON.parse(data.args);
|
||||
data.args = JSON.parse(data.args)
|
||||
}
|
||||
|
||||
if (data.postprocessors && typeof data.postprocessors === "string") {
|
||||
data.postprocessors = JSON.parse(data.postprocessors);
|
||||
data.postprocessors = JSON.parse(data.postprocessors)
|
||||
}
|
||||
|
||||
let userData = {};
|
||||
let userData = {}
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
if (!data[key]) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
userData[key] = data[key];
|
||||
userData[key] = data[key]
|
||||
}
|
||||
|
||||
userData['_type'] = 'preset';
|
||||
userData['_type'] = 'preset'
|
||||
userData['_version'] = '1.0'
|
||||
|
||||
return copyText(base64UrlEncode(JSON.stringify(userData)));
|
||||
};
|
||||
return copyText(base64UrlEncode(JSON.stringify(userData)))
|
||||
}
|
||||
|
||||
const calcPath = (path) => {
|
||||
const loc = config.app.download_path || "/downloads";
|
||||
const loc = config.app.download_path || "/downloads"
|
||||
|
||||
if (path) {
|
||||
return loc + "/" + sTrim(path, "/");
|
||||
return loc + "/" + sTrim(path, "/")
|
||||
}
|
||||
|
||||
return loc;
|
||||
};
|
||||
return loc
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const CONFIG_KEYS = {
|
|||
instance_title: null,
|
||||
sentry_dsn: null,
|
||||
console_enabled: false,
|
||||
browser_enabled: false,
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue