Merge pull request #289 from arabcoders/dev
Major WebUI improvements + minor bug fixes
This commit is contained in:
commit
cf2fb5fac8
33 changed files with 1085 additions and 379 deletions
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
|
@ -74,4 +74,13 @@
|
|||
"spellright.documentTypes": [
|
||||
"latex"
|
||||
],
|
||||
"search.exclude": {
|
||||
"ui/.nuxt": true,
|
||||
"ui/.output": true,
|
||||
"ui/dist": true,
|
||||
"ui/exported": true,
|
||||
"ui/node_modules": true,
|
||||
"ui/.out": true,
|
||||
"**/.venv": true,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -871,6 +871,83 @@ class HttpAPI(Common):
|
|||
await self._notify.emit(Events.CONDITIONS_UPDATE, data=items)
|
||||
return web.json_response(data=items, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
|
||||
|
||||
@route("POST", "api/conditions/test")
|
||||
async def conditions_test(self, request: Request) -> Response:
|
||||
"""
|
||||
Test condition against URL.
|
||||
|
||||
Args:
|
||||
request (Request): The request object.
|
||||
|
||||
Returns:
|
||||
Response: The response object
|
||||
|
||||
"""
|
||||
params = await request.json()
|
||||
|
||||
if not isinstance(params, dict):
|
||||
return web.json_response(
|
||||
{"error": "Invalid request body expecting dict."},
|
||||
status=web.HTTPBadRequest.status_code,
|
||||
)
|
||||
|
||||
url = params.get("url")
|
||||
if not url:
|
||||
return web.json_response({"error": "url is required."}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
cond = params.get("condition")
|
||||
if not cond:
|
||||
return web.json_response({"error": "condition is required."}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
try:
|
||||
preset = params.get("preset", self.config.default_preset)
|
||||
key = self.cache.hash(url + str(preset))
|
||||
if not self.cache.has(key):
|
||||
opts = {}
|
||||
if ytdlp_proxy := self.config.get_ytdlp_args().get("proxy", None):
|
||||
opts["proxy"] = ytdlp_proxy
|
||||
ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset, with_cookies=True).add(opts).get_all()
|
||||
|
||||
data = extract_info(
|
||||
config=ytdlp_opts,
|
||||
url=url,
|
||||
debug=False,
|
||||
no_archive=True,
|
||||
follow_redirect=True,
|
||||
sanitize_info=True,
|
||||
)
|
||||
if not data:
|
||||
return web.json_response(
|
||||
data={"error": f"Failed to extract info from '{url!s}'."},
|
||||
status=web.HTTPBadRequest.status_code,
|
||||
)
|
||||
self.cache.set(key=key, value=data, ttl=600)
|
||||
else:
|
||||
data = self.cache.get(key)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
return web.json_response(
|
||||
data={"error": f"Failed to extract video info. '{e!s}'"},
|
||||
status=web.HTTPInternalServerError.status_code,
|
||||
)
|
||||
|
||||
try:
|
||||
from yt_dlp.utils import match_str
|
||||
|
||||
status = match_str(cond, data)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
return web.json_response(
|
||||
data={"error": str(e)},
|
||||
status=web.HTTPBadRequest.status_code,
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
data={"status": status, "condition": cond, "data": data},
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=self.encoder.encode,
|
||||
)
|
||||
|
||||
@route("GET", "api/presets")
|
||||
async def presets(self, request: Request) -> Response:
|
||||
"""
|
||||
|
|
@ -1007,9 +1084,6 @@ class HttpAPI(Common):
|
|||
if not item.get("id", None) or not validate_uuid(item.get("id"), version=4):
|
||||
item["id"] = str(uuid.uuid4())
|
||||
|
||||
if not item.get("timer", None) or str(item.get("timer")).strip() == "":
|
||||
item["timer"] = f"{random.randint(1,59)} */1 * * *" # noqa: S311
|
||||
|
||||
if not item.get("template", None):
|
||||
item["template"] = ""
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class Tasks(metaclass=Singleton):
|
|||
self.load()
|
||||
self._notify.subscribe(
|
||||
Events.TASKS_ADD,
|
||||
lambda data, _, **kwargs: self.add(**data.data), # noqa: ARG005
|
||||
lambda data, _, **kwargs: self.save(**data.data), # noqa: ARG005
|
||||
f"{__class__.__name__}.add",
|
||||
)
|
||||
|
||||
|
|
@ -145,6 +145,7 @@ class Tasks(metaclass=Singleton):
|
|||
for i, task in enumerate(tasks):
|
||||
try:
|
||||
task, task_status = clean_item(task, keys=("cookies", "config"))
|
||||
self.validate(task)
|
||||
task = Task(**task)
|
||||
if task_status:
|
||||
need_save = True
|
||||
|
|
@ -152,9 +153,13 @@ class Tasks(metaclass=Singleton):
|
|||
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
|
||||
continue
|
||||
|
||||
self._tasks.append(task)
|
||||
|
||||
if not task.timer:
|
||||
continue
|
||||
|
||||
try:
|
||||
self._scheduler.add(timer=task.timer, func=self._runner, args=(task,), id=task.id)
|
||||
self._tasks.append(task)
|
||||
|
||||
try:
|
||||
from cronsim import CronSim
|
||||
|
|
@ -221,14 +226,19 @@ class Tasks(metaclass=Singleton):
|
|||
msg = "No name found."
|
||||
raise ValueError(msg)
|
||||
|
||||
if not task.get("timer"):
|
||||
msg = "No timer found."
|
||||
raise ValueError(msg)
|
||||
|
||||
if not task.get("url"):
|
||||
msg = "No URL found."
|
||||
raise ValueError(msg)
|
||||
|
||||
if task.get("timer"):
|
||||
try:
|
||||
from cronsim import CronSim
|
||||
|
||||
CronSim(task.get("timer"), datetime.now(UTC))
|
||||
except Exception as e:
|
||||
msg = f"Invalid timer format. '{e!s}'."
|
||||
raise ValueError(msg) from e
|
||||
|
||||
if task.get("cli"):
|
||||
try:
|
||||
from .Utils import arg_converter
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ def arg_converter(
|
|||
if isinstance(matchFilter, set):
|
||||
diff["match_filter"] = {"filters": list(matchFilter)}
|
||||
|
||||
return json.loads(json.dumps(diff, cls=Encoder))
|
||||
return json.loads(json.dumps(diff, cls=Encoder, default=str))
|
||||
|
||||
return diff
|
||||
|
||||
|
|
@ -760,15 +760,40 @@ def get_files(base_path: str, dir: str | None = None):
|
|||
else:
|
||||
dir_path = base_path
|
||||
|
||||
if not os.path.isdir(dir_path):
|
||||
dir_path = pathlib.Path(dir_path)
|
||||
if dir_path.is_symlink():
|
||||
try:
|
||||
dir_path = dir_path.resolve()
|
||||
except OSError as e:
|
||||
LOG.warning(f"Skipping broken symlink: {dir_path} - {e}")
|
||||
return []
|
||||
|
||||
if not str(dir_path).startswith(base_path):
|
||||
msg = f"Invalid path: '{dir_path}' - must be inside '{base_path}'."
|
||||
LOG.warning(msg)
|
||||
return []
|
||||
|
||||
if not dir_path.is_dir():
|
||||
msg = f"Invalid path: '{dir_path}' - must be a directory."
|
||||
raise OSError(msg)
|
||||
LOG.warning(msg)
|
||||
return []
|
||||
|
||||
contents: list = []
|
||||
for file in pathlib.Path(dir_path).iterdir():
|
||||
for file in dir_path.iterdir():
|
||||
if file.name.startswith(".") or file.name.startswith("_"):
|
||||
continue
|
||||
|
||||
if file.is_symlink():
|
||||
try:
|
||||
test: pathlib.Path = file.resolve()
|
||||
if not str(test).startswith(base_path):
|
||||
msg = f"Invalid symlink: '{file}' - must resolve inside '{base_path}'."
|
||||
LOG.warning(msg)
|
||||
continue
|
||||
except OSError:
|
||||
LOG.warning(f"Skipping broken symlink: {file}")
|
||||
continue
|
||||
|
||||
content_type = None
|
||||
|
||||
for pattern in FILES_TYPE:
|
||||
|
|
|
|||
|
|
@ -317,3 +317,26 @@ class Conditions(metaclass=Singleton):
|
|||
continue
|
||||
|
||||
return None
|
||||
|
||||
def single_match(self, name: str, info: dict) -> Condition | None:
|
||||
"""
|
||||
Check if condition matches the info dict.
|
||||
|
||||
Args:
|
||||
name (str): The condition name to check.
|
||||
info (dict): The info dict to check.
|
||||
|
||||
Returns:
|
||||
Condition|None: The condition if found, None otherwise.
|
||||
|
||||
"""
|
||||
if len(self._items) < 1 or not info or not isinstance(info, dict) or len(info) < 1:
|
||||
return None
|
||||
|
||||
item = self.get(name)
|
||||
if not item or not item.filter:
|
||||
return None
|
||||
|
||||
from yt_dlp.utils import match_str
|
||||
|
||||
return item if match_str(item.filter, info) else None
|
||||
|
|
|
|||
|
|
@ -325,7 +325,10 @@ hr {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.is-max-width {
|
||||
max-width: calc(100% - 10px);
|
||||
}
|
||||
|
||||
.is-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
</label>
|
||||
<div class="control">
|
||||
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress"
|
||||
placeholder="For the problematic channel or video name.">
|
||||
placeholder="For the problematic channel or video name.">
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
|
|
@ -70,15 +70,20 @@
|
|||
<div class="field">
|
||||
<label class="label is-inline" for="filter">
|
||||
<span class="icon"><i class="fa-solid fa-filter" /></span>
|
||||
Filter
|
||||
Condition Filter
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="text" class="input" id="filter" v-model="form.filter" :disabled="addInProgress"
|
||||
placeholder="availability = 'needs_auth' & channel_id = 'channel_id'">
|
||||
placeholder="availability = 'needs_auth' & channel_id = 'channel_id'">
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The yt-dlp <code>[--match-filters]</code> filter logic.</span>
|
||||
<span>
|
||||
The yt-dlp <code>[--match-filters]</code> filter logic.
|
||||
<NuxtLink @click="test_data.show = true" :disabled="addInProgress || !form.filter" class="is-bold">
|
||||
Test filter logic
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -108,29 +113,102 @@
|
|||
|
||||
<div class="card-footer mt-auto">
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
|
||||
:class="{ 'is-loading': addInProgress }" form="addForm">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</div>
|
||||
<div 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>
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
|
||||
:class="{ 'is-loading': addInProgress }" form="addForm">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</div>
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="test_data.show" class="column is-12">
|
||||
<Modal @close="test_data.show = false" title="Test condition">
|
||||
<form autocomplete="off" id="testCondition" @submit.prevent="run_test()">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="url">
|
||||
<span class="icon"><i class="fa-solid fa-link" /></span>
|
||||
URL
|
||||
</label>
|
||||
<div class="field-body">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<input type="url" class="input " id="url" v-model="test_data.url"
|
||||
:disabled="test_data.in_progress" placeholder="https://..." required>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit" :disabled="test_data.in_progress"
|
||||
:class="{ 'is-loading': test_data.in_progress }">
|
||||
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||
<span>Test</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The url to test the filter against.</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="filter">
|
||||
<span class="icon"><i class="fa-solid fa-filter" /></span>
|
||||
Condition Filter
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="text" class="input" id="filter" v-model="form.filter" :disabled="test_data.in_progress"
|
||||
placeholder="availability = 'needs_auth' & channel_id = 'channel_id'" required>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The yt-dlp <code>[--match-filters]</code> filter logic.</span><br>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="is-bold" :class="{
|
||||
'has-text-success': true === test_data?.data?.status,
|
||||
'has-text-danger': false === test_data?.data?.status,
|
||||
}">
|
||||
<span class="icon"><i class="fa-solid" :class="{
|
||||
'fa-check': true === test_data.data.status,
|
||||
'fa-xmark': false === test_data.data.status,
|
||||
'fa-question': null === (test_data.data.status || null),
|
||||
}" /></span>
|
||||
Filter Status: {{ test_data?.data?.status === null ? 'Not tested' : test_data?.data?.status ?
|
||||
'Matched' : 'Not matched' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<pre style="height:60vh;"><code>{{ show_data() }}</code></pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
const emitter = defineEmits(['cancel', 'submit']);
|
||||
const emitter = defineEmits(['cancel', 'submit'])
|
||||
|
||||
const props = defineProps({
|
||||
reference: {
|
||||
|
|
@ -149,30 +227,72 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const test_data = ref({ show: false, url: '', data: { status: null, data: {} }, in_progress: false })
|
||||
|
||||
const toast = useNotification()
|
||||
const box = useConfirm()
|
||||
const form = reactive(JSON.parse(JSON.stringify(props.item)))
|
||||
const import_string = ref('')
|
||||
const showImport = useStorage('showImport', false);
|
||||
const showImport = useStorage('showImport', false)
|
||||
|
||||
const run_test = async () => {
|
||||
if (!test_data.value.url) {
|
||||
toast.error('The URL is required for testing.', { force: true })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(test_data.value.url)
|
||||
} catch (e) {
|
||||
toast.error('The URL is invalid.', { force: true })
|
||||
return
|
||||
}
|
||||
|
||||
test_data.value.in_progress = true
|
||||
test_data.value.data.status = null
|
||||
|
||||
try {
|
||||
const response = await request('/api/conditions/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
url: test_data.value.url,
|
||||
condition: form.filter,
|
||||
}),
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
if (!response.ok) {
|
||||
toast.error(json.message || json.error || 'Unknown error', { force: true })
|
||||
return
|
||||
}
|
||||
|
||||
test_data.value.data = json
|
||||
} catch (e) {
|
||||
toast.error(`Failed to test condition. ${error.message}`)
|
||||
} finally {
|
||||
test_data.value.in_progress = false
|
||||
}
|
||||
}
|
||||
|
||||
const checkInfo = async () => {
|
||||
const required = ['name', 'filter', 'cli'];
|
||||
const required = ['name', 'filter', 'cli']
|
||||
|
||||
for (const key of required) {
|
||||
if (!form[key]) {
|
||||
toast.error(`The ${key} field is required.`);
|
||||
toast.error(`The ${key} field is required.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (form?.cli && '' !== form.cli) {
|
||||
const options = await convertOptions(form.cli);
|
||||
const options = await convertOptions(form.cli)
|
||||
if (null === options) {
|
||||
return
|
||||
}
|
||||
form.cli = form.cli.trim()
|
||||
}
|
||||
|
||||
let copy = JSON.parse(JSON.stringify(form));
|
||||
let copy = JSON.parse(JSON.stringify(form))
|
||||
|
||||
for (const key in copy) {
|
||||
if (typeof copy[key] !== 'string') {
|
||||
|
|
@ -181,7 +301,7 @@ const checkInfo = async () => {
|
|||
copy[key] = copy[key].trim()
|
||||
}
|
||||
|
||||
emitter('submit', { reference: toRaw(props.reference), item: toRaw(copy) });
|
||||
emitter('submit', { reference: toRaw(props.reference), item: toRaw(copy) })
|
||||
}
|
||||
|
||||
const convertOptions = async args => {
|
||||
|
|
@ -191,7 +311,7 @@ const convertOptions = async args => {
|
|||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const importItem = async () => {
|
||||
|
|
@ -219,7 +339,7 @@ const importItem = async () => {
|
|||
return
|
||||
}
|
||||
|
||||
if ((form.filter || form.cli) && false === confirm('This will overwrite the current data. Are you sure?')) {
|
||||
if ((form.filter || form.cli) && false === box.confirm('This will overwrite the current data. Are you sure?', true)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -242,4 +362,12 @@ const importItem = async () => {
|
|||
toast.error(`Failed to parse import string. ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const show_data = () => {
|
||||
if (!test_data.value.data?.data || !Object.keys(test_data.value.data?.data).length) {
|
||||
return 'No data to show.'
|
||||
}
|
||||
|
||||
return JSON.stringify(test_data.value.data.data, null, 2)
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
const cache = useSessionCache()
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const url = ref()
|
||||
const error = ref(false)
|
||||
const isPreloading = ref(false)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import { request } from '~/utils/index'
|
|||
const emitter = defineEmits(['closeModel'])
|
||||
const isLoading = ref(false)
|
||||
const data = ref({})
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
|
||||
const props = defineProps({
|
||||
link: {
|
||||
|
|
|
|||
|
|
@ -385,7 +385,8 @@ const emitter = defineEmits(['getInfo', 'add_new'])
|
|||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const socket = useSocketStore()
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const box = useConfirm()
|
||||
|
||||
const selectedElms = ref([])
|
||||
const masterSelectAll = ref(false)
|
||||
|
|
@ -488,7 +489,7 @@ const deleteSelectedItems = () => {
|
|||
msg += '\nThis will delete the files from the server if they exist.'
|
||||
}
|
||||
|
||||
if (false === confirm(msg)) {
|
||||
if (false === box.confirm(msg, config.app.remove_files)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -506,7 +507,7 @@ const deleteSelectedItems = () => {
|
|||
|
||||
const clearCompleted = () => {
|
||||
let msg = 'Are you sure you want to clear all completed downloads?'
|
||||
if (false === confirm(msg)) {
|
||||
if (false === box.confirm(msg)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -518,7 +519,7 @@ const clearCompleted = () => {
|
|||
}
|
||||
|
||||
const clearIncomplete = () => {
|
||||
if (false === confirm('Are you sure you want to clear all in-complete downloads?')) {
|
||||
if (false === box.confirm('Are you sure you want to clear all in-complete downloads?')) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -598,7 +599,7 @@ const setStatus = item => {
|
|||
}
|
||||
|
||||
const requeueIncomplete = () => {
|
||||
if (false === confirm('Are you sure you want to re-queue all incomplete downloads?')) {
|
||||
if (false === box.confirm('Are you sure you want to re-queue all incomplete downloads?')) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -612,7 +613,7 @@ const requeueIncomplete = () => {
|
|||
}
|
||||
|
||||
const archiveItem = item => {
|
||||
if (!confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)) {
|
||||
if (!box.confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)) {
|
||||
return
|
||||
}
|
||||
socket.emit('archive_item', item)
|
||||
|
|
@ -621,7 +622,7 @@ const archiveItem = item => {
|
|||
|
||||
const removeItem = item => {
|
||||
const msg = `Remove '${item.title ?? item.id ?? item.url ?? '??'}'?\n this will delete the file from the server.`
|
||||
if (config.app.remove_files && !confirm(msg)) {
|
||||
if (false === box.confirm(msg, config.app.remove_files)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { request } from '~/utils/index'
|
|||
const emitter = defineEmits(['closeModel'])
|
||||
const isLoading = ref(false)
|
||||
const data = ref({})
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const image = ref('')
|
||||
|
||||
const props = defineProps({
|
||||
|
|
|
|||
35
ui/components/Modal.vue
Normal file
35
ui/components/Modal.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="modal is-active">
|
||||
<div class="model-title" v-if="title" />
|
||||
<div class="modal-background" @click="closeModal"></div>
|
||||
<div class="modal-content" style="width:70vw;">
|
||||
<slot />
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="closeModal"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emitter = defineEmits(['close'])
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const closeModal = () => emitter('close')
|
||||
|
||||
const eventFunc = e => {
|
||||
if (e.key === 'Escape') {
|
||||
emitter('close')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', eventFunc))
|
||||
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
|
||||
</script>
|
||||
|
|
@ -189,7 +189,8 @@ const props = defineProps({
|
|||
const emitter = defineEmits(['getInfo', 'clear_form'])
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const box = useConfirm()
|
||||
|
||||
const showAdvanced = useStorage('show_advanced', false)
|
||||
const addInProgress = ref(false)
|
||||
|
|
@ -261,7 +262,7 @@ const addDownload = async () => {
|
|||
}
|
||||
|
||||
const resetConfig = () => {
|
||||
if (true !== confirm('Reset your local configuration?')) {
|
||||
if (true !== box.confirm('Reset your local configuration?')) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@
|
|||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
const emitter = defineEmits(['cancel', 'submit']);
|
||||
const toast = useToast();
|
||||
const toast = useNotification();
|
||||
const props = defineProps({
|
||||
reference: {
|
||||
type: String,
|
||||
|
|
@ -295,6 +295,7 @@ const requestMethods = ['POST', 'PUT'];
|
|||
const requestType = ['json', 'form'];
|
||||
const showImport = useStorage('showImport', false);
|
||||
const import_string = ref('');
|
||||
const box = useConfirm()
|
||||
|
||||
const checkInfo = async () => {
|
||||
const required = ['name', 'request.url', 'request.method', 'request.type', 'request.data_key'];
|
||||
|
|
@ -331,7 +332,6 @@ const checkInfo = async () => {
|
|||
emitter('submit', { reference: toRaw(props.reference), item: toRaw(form) });
|
||||
}
|
||||
|
||||
|
||||
const importItem = async () => {
|
||||
let val = import_string.value.trim()
|
||||
if (!val) {
|
||||
|
|
@ -359,7 +359,7 @@ const importItem = async () => {
|
|||
}
|
||||
|
||||
if (form.target) {
|
||||
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||
if (false === box.confirm('This will overwrite the current form fields. Are you sure?', true)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,10 +218,11 @@ const props = defineProps({
|
|||
})
|
||||
|
||||
const config = useConfigStore()
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const form = reactive(JSON.parse(JSON.stringify(props.preset)))
|
||||
const import_string = ref('')
|
||||
const showImport = useStorage('showImport', false);
|
||||
const showImport = useStorage('showImport', false)
|
||||
const box = useConfirm()
|
||||
|
||||
onMounted(() => {
|
||||
if (props.preset?.cli && '' !== props.preset?.cli) {
|
||||
|
|
@ -328,7 +329,7 @@ const importItem = async () => {
|
|||
return
|
||||
}
|
||||
|
||||
if (form.cli && false === confirm('This will overwrite the current data. Are you sure?')) {
|
||||
if (form.cli && false === box.confirm('This will overwrite the current data. Are you sure?', true)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -235,12 +235,13 @@ import { ucFirst } from '~/utils/index'
|
|||
import { isEmbedable, getEmbedable } from '~/utils/embedable'
|
||||
|
||||
const emitter = defineEmits(['getInfo'])
|
||||
const config = useConfigStore();
|
||||
const stateStore = useStateStore();
|
||||
const socket = useSocketStore();
|
||||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const socket = useSocketStore()
|
||||
const box = useConfirm()
|
||||
|
||||
const selectedElms = ref([]);
|
||||
const masterSelectAll = ref(false);
|
||||
const selectedElms = ref([])
|
||||
const masterSelectAll = ref(false)
|
||||
const showQueue = useStorage('showQueue', true)
|
||||
const hideThumbnail = useStorage('hideThumbnailQueue', false)
|
||||
const display_style = useStorage('display_style', 'cards')
|
||||
|
|
@ -252,11 +253,11 @@ const bg_opacity = useStorage('random_bg_opacity', 0.85)
|
|||
|
||||
watch(masterSelectAll, (value) => {
|
||||
for (const key in stateStore.queue) {
|
||||
const element = stateStore.queue[key];
|
||||
const element = stateStore.queue[key]
|
||||
if (value) {
|
||||
selectedElms.value.push(element._id);
|
||||
selectedElms.value.push(element._id)
|
||||
} else {
|
||||
selectedElms.value = [];
|
||||
selectedElms.value = []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -266,35 +267,35 @@ const hasQueuedItems = computed(() => stateStore.count('queue') > 0)
|
|||
|
||||
const setIcon = item => {
|
||||
if ('downloading' === item.status && item.is_live) {
|
||||
return 'fa-globe fa-spin';
|
||||
return 'fa-globe fa-spin'
|
||||
}
|
||||
|
||||
if ('downloading' === item.status) {
|
||||
return 'fa-download';
|
||||
return 'fa-download'
|
||||
}
|
||||
|
||||
if ('postprocessing' === item.status) {
|
||||
return 'fa-cog fa-spin';
|
||||
return 'fa-cog fa-spin'
|
||||
}
|
||||
|
||||
if (null === item.status && true === config.paused) {
|
||||
return 'fa-pause-circle';
|
||||
return 'fa-pause-circle'
|
||||
}
|
||||
|
||||
if (!item.status) {
|
||||
return 'fa-question';
|
||||
return 'fa-question'
|
||||
}
|
||||
|
||||
return 'fa-spinner fa-spin';
|
||||
return 'fa-spinner fa-spin'
|
||||
}
|
||||
|
||||
const setStatus = item => {
|
||||
if (null === item.status && true === config.paused) {
|
||||
return 'Paused';
|
||||
return 'Paused'
|
||||
}
|
||||
|
||||
if ('downloading' === item.status && item.is_live) {
|
||||
return 'Live Streaming';
|
||||
return 'Live Streaming'
|
||||
}
|
||||
|
||||
if ('preparing' === item.status) {
|
||||
|
|
@ -302,7 +303,7 @@ const setStatus = item => {
|
|||
}
|
||||
|
||||
if (!item.status) {
|
||||
return 'Unknown..';
|
||||
return 'Unknown..'
|
||||
}
|
||||
|
||||
return ucFirst(item.status)
|
||||
|
|
@ -324,84 +325,82 @@ const setIconColor = item => {
|
|||
return ''
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ETAPipe = value => {
|
||||
if (value === null || 0 === value) {
|
||||
return 'Live';
|
||||
return 'Live'
|
||||
}
|
||||
if (value < 60) {
|
||||
return `${Math.round(value)}s`;
|
||||
return `${Math.round(value)}s`
|
||||
}
|
||||
if (value < 3600) {
|
||||
return `${Math.floor(value / 60)}m ${Math.round(value % 60)}s`;
|
||||
return `${Math.floor(value / 60)}m ${Math.round(value % 60)}s`
|
||||
}
|
||||
const hours = Math.floor(value / 3600)
|
||||
const minutes = value % 3600
|
||||
return `${hours}h ${Math.floor(minutes / 60)}m ${Math.round(minutes % 60)}s`;
|
||||
return `${hours}h ${Math.floor(minutes / 60)}m ${Math.round(minutes % 60)}s`
|
||||
}
|
||||
|
||||
const speedPipe = value => {
|
||||
if (null === value || 0 === value) {
|
||||
return '0KB/s';
|
||||
return '0KB/s'
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const dm = 2;
|
||||
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s', 'EB/s', 'ZB/s', 'YB/s'];
|
||||
const i = Math.floor(Math.log(value) / Math.log(k));
|
||||
return parseFloat((value / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
const k = 1024
|
||||
const dm = 2
|
||||
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s', 'EB/s', 'ZB/s', 'YB/s']
|
||||
const i = Math.floor(Math.log(value) / Math.log(k))
|
||||
return parseFloat((value / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const percentPipe = value => {
|
||||
if (value === null || 0 === value) {
|
||||
return '00.00';
|
||||
return '00.00'
|
||||
}
|
||||
return parseFloat(value).toFixed(2);
|
||||
return parseFloat(value).toFixed(2)
|
||||
}
|
||||
|
||||
const updateProgress = (item) => {
|
||||
let string = '';
|
||||
let string = ''
|
||||
|
||||
if (null === item.status && true === config.paused) {
|
||||
return 'Paused';
|
||||
return 'Paused'
|
||||
}
|
||||
|
||||
if ('postprocessing' === item.status) {
|
||||
return 'Post-processors are running.';
|
||||
return 'Post-processors are running.'
|
||||
}
|
||||
|
||||
if ('preparing' === item.status) {
|
||||
return ag(item, 'extras.external_downloader') ? 'External downloader.' : 'Preparing';
|
||||
return ag(item, 'extras.external_downloader') ? 'External downloader.' : 'Preparing'
|
||||
}
|
||||
|
||||
if (null != item.status) {
|
||||
string += item.percent && !item.is_live ? percentPipe(item.percent) + '%' : 'Live';
|
||||
string += item.percent && !item.is_live ? percentPipe(item.percent) + '%' : 'Live'
|
||||
}
|
||||
|
||||
string += item.speed ? ' - ' + speedPipe(item.speed) : ' - Waiting..';
|
||||
string += item.speed ? ' - ' + speedPipe(item.speed) : ' - Waiting..'
|
||||
|
||||
if (null != item.status && item.eta) {
|
||||
string += ' - ' + ETAPipe(item.eta);
|
||||
string += ' - ' + ETAPipe(item.eta)
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
const confirmCancel = item => {
|
||||
if (true !== confirm(`Are you sure you want to cancel (${item.title})?`)) {
|
||||
return false;
|
||||
if (true !== box.confirm(`Are you sure you want to cancel (${item.title})?`)) {
|
||||
return false
|
||||
}
|
||||
cancelItems(item._id);
|
||||
return true;
|
||||
cancelItems(item._id)
|
||||
return true
|
||||
}
|
||||
|
||||
const cancelSelected = () => {
|
||||
if (true !== confirm('Are you sure you want to cancel selected items?')) {
|
||||
if (true !== box.confirm('Are you sure you want to cancel selected items?')) {
|
||||
return false;
|
||||
}
|
||||
cancelItems(selectedElms.value);
|
||||
selectedElms.value = [];
|
||||
cancelItems(selectedElms.value)
|
||||
selectedElms.value = []
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -410,20 +409,21 @@ const cancelItems = item => {
|
|||
|
||||
if (typeof item === 'object') {
|
||||
for (const key in item) {
|
||||
items.push(item[key]);
|
||||
items.push(item[key])
|
||||
}
|
||||
} else {
|
||||
items.push(item);
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
if (items.length < 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
items.forEach(id => socket.emit('item_cancel', id));
|
||||
items.forEach(id => socket.emit('item_cancel', id))
|
||||
}
|
||||
|
||||
const pImg = e => e.target.naturalHeight > e.target.naturalWidth ? e.target.classList.add('image-portrait') : null
|
||||
|
||||
watch(embed_url, v => {
|
||||
if (!bg_enable.value) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -9,71 +9,121 @@
|
|||
</span>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<label class="label" for="random_bg">Color scheme</label>
|
||||
<div class="control">
|
||||
<label for="auto" class="radio">
|
||||
<input id="auto" type="radio" v-model="selectedTheme" value="auto">
|
||||
System Default
|
||||
</label>
|
||||
<label for="light" class="radio">
|
||||
<input id="light" type="radio" v-model="selectedTheme" value="light">
|
||||
Light
|
||||
</label>
|
||||
<label for="dark" class="radio">
|
||||
<input id="dark" type="radio" v-model="selectedTheme" value="dark">
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Select the color scheme for the WebUI.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Color scheme</label>
|
||||
<div class="control">
|
||||
<label for="auto" class="radio">
|
||||
<input id="auto" type="radio" v-model="selectedTheme" value="auto">
|
||||
System Default
|
||||
</label>
|
||||
<label for="light" class="radio">
|
||||
<input id="light" type="radio" v-model="selectedTheme" value="light">
|
||||
Light
|
||||
</label>
|
||||
<label for="dark" class="radio">
|
||||
<input id="dark" type="radio" v-model="selectedTheme" value="dark">
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Select the color scheme for the WebUI.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="random_bg">Backgrounds</label>
|
||||
<div class="control">
|
||||
<input id="random_bg" type="checkbox" class="switch is-success" v-model="bg_enable">
|
||||
<label for="random_bg" class="is-unselectable"> Enable</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use random background image.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="random_bg">Backgrounds</label>
|
||||
<div class="control">
|
||||
<input id="random_bg" type="checkbox" class="switch is-success" v-model="bg_enable">
|
||||
<label for="random_bg" class="is-unselectable"> Enable</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
Use random background image.
|
||||
<NuxtLink @click="$emit('reload_bg')" class="is-bold" v-if="bg_enable">
|
||||
Reload
|
||||
</NuxtLink>
|
||||
<span class="icon" v-if="isLoading"><i class="fa fa-spin fa-spinner" /></span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="random_bg_opacity">
|
||||
Background Visibility: (<code>{{ bg_opacity }}</code>)
|
||||
</label>
|
||||
<div class="control">
|
||||
<input id="random_bg_opacity" style="width: 100%" type="range" v-model="bg_opacity" min="0.50" max="1.00"
|
||||
step="0.05">
|
||||
<div class="field">
|
||||
<label class="label" for="random_bg_opacity">
|
||||
Background Visibility: (<code>{{ bg_opacity }}</code>)
|
||||
</label>
|
||||
<div class="control">
|
||||
<input id="random_bg_opacity" style="width: 100%" type="range" v-model="bg_opacity" min="0.50"
|
||||
max="1.00" step="0.05">
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>How visible the background image should be.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>How visible the background image should be.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label" for="reduce_confirm">Reduce confirm box usage</label>
|
||||
<div class="control">
|
||||
<input id="reduce_confirm" type="checkbox" class="switch is-success" v-model="reduce_confirm">
|
||||
<label for="reduce_confirm" class="is-unselectable">
|
||||
{{ reduce_confirm ? 'Enabled' : 'Disabled' }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Reduce the usage of confirm boxes in the WebUI.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="allow_toasts">Show toasts</label>
|
||||
<div class="control">
|
||||
<input id="allow_toasts" type="checkbox" class="switch is-success" v-model="allow_toasts">
|
||||
<label for="allow_toasts" class="is-unselectable">
|
||||
{{ allow_toasts ? 'Enabled' : 'Disabled' }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span class="has-text-danger is-bold">
|
||||
Show notification toasts. If disabled, you will not see errors reported or anything else.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Toasts position</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="toast_position">
|
||||
<option :value="POSITION.TOP_RIGHT">{{ POSITION.TOP_RIGHT }}</option>
|
||||
<option :value="POSITION.TOP_CENTER">{{ POSITION.TOP_CENTER }}</option>
|
||||
<option :value="POSITION.TOP_LEFT">{{ POSITION.TOP_LEFT }}</option>
|
||||
<option :value="POSITION.BOTTOM_RIGHT">{{ POSITION.BOTTOM_RIGHT }}</option>
|
||||
<option :value="POSITION.BOTTOM_CENTER">{{ POSITION.BOTTOM_CENTER }}</option>
|
||||
<option :value="POSITION.BOTTOM_LEFT">{{ POSITION.BOTTOM_LEFT }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="dismiss_on_click">Dismiss toasts on click</label>
|
||||
<div class="control">
|
||||
<input id="dismiss_on_click" type="checkbox" class="switch is-success"
|
||||
v-model="toast_dismiss_on_click">
|
||||
<label for="dismiss_on_click" class="is-unselectable">
|
||||
{{ toast_dismiss_on_click ? 'Enabled' : 'Disabled' }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="bg_enable">
|
||||
<label class="label" for="random_bg_opacity">
|
||||
Reload the currently displayed background image.
|
||||
</label>
|
||||
<div class="control">
|
||||
<button class="button is-info" @click="$emit('reload_bg')" :class="{ 'is-loading': isLoading }"
|
||||
:disabled="isLoading">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-sync-alt" /></span>
|
||||
<span>Reload</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Change the displayed picture.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -81,10 +131,9 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { POSITION } from 'vue-toastification'
|
||||
|
||||
defineProps({
|
||||
isLoading: {
|
||||
|
|
@ -96,5 +145,8 @@ defineProps({
|
|||
const bg_enable = useStorage('random_bg', true)
|
||||
const bg_opacity = useStorage('random_bg_opacity', 0.85)
|
||||
const selectedTheme = useStorage('theme', 'auto')
|
||||
|
||||
const allow_toasts = useStorage('allow_toasts', true)
|
||||
const reduce_confirm = useStorage('reduce_confirm', false)
|
||||
const toast_position = useStorage('toast_position', POSITION.TOP_RIGHT)
|
||||
const toast_dismiss_on_click = useStorage('toast_dismiss_on_click', true)
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -111,8 +111,9 @@
|
|||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Select the preset to use for this URL. <span class="text-has-danger">If the
|
||||
<code>-f, --format</code> argument is present in the command line options, the preset and all
|
||||
it's options will be ignored.</span>
|
||||
<code>-f, --format</code> <span class="has-text-danger">
|
||||
argument is present in the command line options, the preset and all
|
||||
it's options will be ignored.</span></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -125,16 +126,15 @@
|
|||
CRON expression timer.
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="text" class="input" id="timer" placeholder="leave empty to run once every hour."
|
||||
v-model="form.timer" :disabled="addInProgress">
|
||||
<input type="text" class="input" id="timer" v-model="form.timer" :disabled="addInProgress"
|
||||
placeholder="0 12 * * 5">
|
||||
</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>.
|
||||
The CRON timer expression to use for this task. If not set, the task will be disabled. For more
|
||||
information on CRON expressions, see <NuxtLink to="https://crontab.guru/" target="_blank">
|
||||
crontab.guru</NuxtLink>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -152,8 +152,8 @@
|
|||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Paths are relative to global download path, defaults to preset download path if set otherwise,
|
||||
fallback root path if not set.</span>
|
||||
<span>Current download folder: <code>{{ get_download_folder() }}</code>. All folders are
|
||||
sub-folders of <code>{{ config.app.download_path }}</code>.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -170,11 +170,7 @@
|
|||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use this output template if non are given with URL. 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>Current output format: <code>{{ get_output_template() }}</code>.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -232,11 +228,12 @@
|
|||
import { useStorage } from '@vueuse/core'
|
||||
import { CronExpressionParser } from 'cron-parser'
|
||||
|
||||
const emitter = defineEmits(['cancel', 'submit']);
|
||||
const toast = useToast();
|
||||
const config = useConfigStore();
|
||||
const showImport = useStorage('showImport', false);
|
||||
const import_string = ref('');
|
||||
const emitter = defineEmits(['cancel', 'submit'])
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
const showImport = useStorage('showImport', false)
|
||||
const import_string = ref('')
|
||||
const box = useConfirm()
|
||||
|
||||
const props = defineProps({
|
||||
reference: {
|
||||
|
|
@ -255,49 +252,49 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const form = reactive(props.task);
|
||||
const form = reactive(props.task)
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.task?.preset || '' === props.task.preset) {
|
||||
form.preset = toRaw(config.app.default_preset);
|
||||
form.preset = toRaw(config.app.default_preset)
|
||||
}
|
||||
})
|
||||
|
||||
const checkInfo = async () => {
|
||||
const required = ['name', 'url'];
|
||||
const required = ['name', 'url']
|
||||
for (const key of required) {
|
||||
if (!form[key]) {
|
||||
toast.error(`The ${key} field is required.`);
|
||||
return;
|
||||
toast.error(`The ${key} field is required.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (form.timer) {
|
||||
try {
|
||||
CronExpressionParser.parse(form.timer);
|
||||
CronExpressionParser.parse(form.timer)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`Invalid CRON expression. ${e.message}`);
|
||||
return;
|
||||
toast.error(`Invalid CRON expression. ${e.message}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(form.url);
|
||||
new URL(form.url)
|
||||
} catch (e) {
|
||||
toast.error('Invalid URL');
|
||||
return;
|
||||
toast.error('Invalid URL')
|
||||
return
|
||||
}
|
||||
|
||||
if (form?.cli && '' !== form.cli) {
|
||||
const options = await convertOptions(form.cli);
|
||||
const options = await convertOptions(form.cli)
|
||||
if (null === options) {
|
||||
return
|
||||
}
|
||||
form.cli = form.cli.trim(" ")
|
||||
}
|
||||
|
||||
emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) });
|
||||
emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) })
|
||||
}
|
||||
|
||||
const importItem = async () => {
|
||||
|
|
@ -327,7 +324,7 @@ const importItem = async () => {
|
|||
}
|
||||
|
||||
if (form.url || form.timer) {
|
||||
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||
if (false === box.confirm('This will overwrite the current form fields. Are you sure?', true)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -391,7 +388,7 @@ const convertOptions = async args => {
|
|||
toast.error(e.message)
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const hasFormatInConfig = computed(() => {
|
||||
|
|
@ -404,4 +401,23 @@ const hasFormatInConfig = computed(() => {
|
|||
|
||||
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
|
||||
|
||||
const get_download_folder = () => {
|
||||
if (form.preset && !hasFormatInConfig.value) {
|
||||
const preset = config.presets.find(p => p.name === form.preset)
|
||||
if (preset && preset.folder) {
|
||||
return preset.folder.replace(config.app.download_path, '')
|
||||
}
|
||||
}
|
||||
return '/'
|
||||
}
|
||||
|
||||
const get_output_template = () => {
|
||||
if (form.preset && !hasFormatInConfig.value) {
|
||||
const preset = config.presets.find(p => p.name === form.preset)
|
||||
if (preset && preset.template) {
|
||||
return preset.template
|
||||
}
|
||||
}
|
||||
return config.app.output_template || '%(title)s.%(ext)s'
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { onMounted, onUpdated, ref, onUnmounted } from 'vue'
|
|||
import Hls from 'hls.js'
|
||||
import { makeDownload } from '~/utils/index'
|
||||
const config = useConfigStore()
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
|
|
|
|||
23
ui/composables/useConfirm.ts
Normal file
23
ui/composables/useConfirm.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const reduceConfirm = useStorage<boolean>('reduce_confirm', false)
|
||||
|
||||
function confirm(msg: string, force: boolean = false): boolean {
|
||||
if (false === force && true === reduceConfirm.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
return window.confirm(msg)
|
||||
}
|
||||
|
||||
function alert(msg: string): boolean {
|
||||
return window.confirm(msg)
|
||||
}
|
||||
|
||||
function prompt(msg: string, defaultValue: string = ''): string | null {
|
||||
return window.prompt(msg, defaultValue)
|
||||
}
|
||||
|
||||
export default function useConfirm() {
|
||||
return { confirm, alert, prompt, reduceConfirm }
|
||||
}
|
||||
60
ui/composables/useNotification.ts
Normal file
60
ui/composables/useNotification.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { useStorage } from '@vueuse/core'
|
||||
import { POSITION, useToast } from "vue-toastification"
|
||||
|
||||
type notificationType = 'info' | 'success' | 'warning' | 'error'
|
||||
type notificationOptions = {
|
||||
timeout?: number,
|
||||
force?: boolean,
|
||||
closeOnClick?: boolean,
|
||||
position?: POSITION
|
||||
}
|
||||
|
||||
const allowToast = useStorage<boolean>('allow_toasts', true)
|
||||
const toastPosition = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
|
||||
const toastDismissOnClick = useStorage<boolean>('toast_dismiss_on_click', true)
|
||||
const toast = useToast()
|
||||
|
||||
function notify(type: notificationType, message: string, opts?: notificationOptions): void {
|
||||
let force = opts?.force || false;
|
||||
if (false === allowToast.value && false === force) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts) {
|
||||
opts = {}
|
||||
}
|
||||
|
||||
if (opts?.force) {
|
||||
delete opts.force
|
||||
}
|
||||
|
||||
opts.closeOnClick = toastDismissOnClick.value
|
||||
opts.position = toastPosition.value ?? POSITION.TOP_RIGHT
|
||||
|
||||
switch (type) {
|
||||
case 'info':
|
||||
toast.info(message, opts)
|
||||
break;
|
||||
case 'success':
|
||||
toast.success(message, opts)
|
||||
break;
|
||||
case 'warning':
|
||||
toast.warning(message, opts)
|
||||
break;
|
||||
case 'error':
|
||||
toast.error(message, opts)
|
||||
break;
|
||||
default:
|
||||
toast.error(`Unknown notification type: ${type}. ${message}`, opts)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default function useNotification() {
|
||||
return {
|
||||
info: (message: string, opts?: notificationOptions) => notify('info', message, opts),
|
||||
success: (message: string, opts?: notificationOptions) => notify('success', message, opts),
|
||||
warning: (message: string, opts?: notificationOptions) => notify('warning', message, opts),
|
||||
error: (message: string, opts?: notificationOptions) => notify('error', message, opts),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
import { useToast } from "vue-toastification";
|
||||
export default () => useToast()
|
||||
|
|
@ -87,7 +87,7 @@
|
|||
</div>
|
||||
|
||||
<div class="navbar-item is-hidden-mobile">
|
||||
<button class="button is-dark has-tooltip-bottom" v-tooltip.bottom="'WebUI Settings'"
|
||||
<button class="button is-dark has-tooltip-bottom mr-4" v-tooltip.bottom="'WebUI Settings'"
|
||||
@click="show_settings = !show_settings">
|
||||
<span class="icon"><i class="fas fa-cog" /></span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ import moment from 'moment'
|
|||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +1,88 @@
|
|||
<template>
|
||||
<div>
|
||||
<main>
|
||||
<div class="mt-1 columns is-multiline">
|
||||
<div class="column is-12 is-clearfix is-unselectable">
|
||||
<span class="title is-4">
|
||||
<span class="icon"><i class="fas fa-cogs" /></span>
|
||||
CHANGELOG
|
||||
</span>
|
||||
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">This page display the latest changes and updates from the project.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12" v-for="(log, index) in logs" :key="log.tag">
|
||||
<div class="content">
|
||||
<h1 class="is-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-code-branch"></i></span>
|
||||
<span>
|
||||
{{ log.tag }} <span class="tag has-text-success" v-if="isInstalled(log.tag)">Installed</span>
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
<hr>
|
||||
<ul>
|
||||
<li v-for="commit in log.commits" :key="commit.sha">
|
||||
<strong>{{ ucFirst(commit.message).replace(/\.$/, "") }}.</strong> -
|
||||
<small>
|
||||
<NuxtLink :to="`https://github.com/arabcoders/ytptube/commit/${commit.sha}`" target="_blank">
|
||||
<span class="has-tooltip" v-tooltip="`SHA: ${commit.sha} - Date: ${commit.date}`">
|
||||
{{ moment(commit.date).fromNow() }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
<hr v-if="index < logs.length - 1">
|
||||
|
||||
<div class="columns is-multiline" v-if="isLoading">
|
||||
<div class="column is-12">
|
||||
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
|
||||
message="Loading data. Please wait..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!isLoading">
|
||||
<div class="logs-container">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12" v-for="(log, index) in logs" :key="log.tag">
|
||||
<div class="content">
|
||||
<h1 class="is-4">
|
||||
<span class="icon"><i class="fas fa-code-branch" /></span>
|
||||
{{ formatTag(log.tag) }} <span class="tag has-text-success" v-if="isInstalled(log.tag)">Installed</span>
|
||||
</h1>
|
||||
<hr>
|
||||
<ul>
|
||||
<li v-for="commit in log.commits" :key="commit.sha">
|
||||
<strong>{{ ucFirst(commit.message).replace(/\.$/, "") }}.</strong> -
|
||||
<small>
|
||||
<NuxtLink :to="`https://github.com/arabcoders/ytptube/commit/${commit.sha}`" target="_blank">
|
||||
<span class="has-tooltip" v-tooltip="`SHA: ${commit.sha} - Date: ${commit.date}`">
|
||||
{{ moment(commit.date).fromNow() }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
<hr v-if="index < logs.length - 1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from 'moment'
|
||||
|
||||
useHead({ title: 'CHANGELOG' })
|
||||
|
||||
const REPO_URL = "https://arabcoders.github.io/ytptube/CHANGELOG-{branch}.json?version={version}";
|
||||
const logs = ref([]);
|
||||
const toast = useNotification();
|
||||
const config = useConfigStore()
|
||||
|
||||
const logs = ref([]);
|
||||
const api_version = ref('')
|
||||
const isLoading = ref(false);
|
||||
|
||||
const branch = computed(() => {
|
||||
const branch = String(api_version.value).split('-')[0] ?? 'master';
|
||||
return ['master', 'dev'].includes(branch) ? branch : 'master';
|
||||
});
|
||||
|
||||
|
||||
const formatTag = tag => {
|
||||
const parts = tag.split('-');
|
||||
if (parts.length < 3) {
|
||||
return tag;
|
||||
}
|
||||
const branch = parts[0];
|
||||
const date = parts[1];
|
||||
const shortSha = parts[2].substring(0, 7);
|
||||
return `${ucFirst(branch)}: ${moment(date, 'YYYYMMDD').format('YYYY-MM-DD')} - ${shortSha}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
watch(() => config.app.version, async value => {
|
||||
if (!value) {
|
||||
return
|
||||
|
|
@ -70,19 +99,37 @@ const loadContent = async () => {
|
|||
return
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const changes = await fetch(r(REPO_URL, { branch: branch.value, version: api_version.value }));
|
||||
logs.value = await changes.json();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notification('error', 'Error', `Failed to fetch changelog. ${e.message}`);
|
||||
toast.error('error', 'Error', `Failed to fetch changelog. ${e.message}`);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const isInstalled = tag => {
|
||||
const installed = String(api_version.value).split('-').pop();
|
||||
return tag.endsWith(installed);
|
||||
const tagHash = tag.split('-').pop();
|
||||
return tagHash.startsWith(installed);
|
||||
}
|
||||
|
||||
onMounted(() => loadContent());
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs-container {
|
||||
padding: 1rem;
|
||||
min-width: 100%;
|
||||
max-height: 73vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: 1px solid #b2d1ff;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
</div>
|
||||
<div class="card-content" v-if="cond?.raw">
|
||||
<div class="content">
|
||||
<pre><code>{{ filterItem(cond) }}</code></pre>
|
||||
<pre><code>{{ JSON.stringify(cleanObject(cond, remove_keys), null, 2) }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer mt-auto">
|
||||
|
|
@ -104,6 +104,10 @@
|
|||
filter i am able to bypass it <code>availability = 'needs_auth' & channel_id = 'channel_id'</code>.
|
||||
and set proxy for that specific video, while leaving the rest of the videos to be downloaded normally.
|
||||
</li>
|
||||
<li>
|
||||
The data which the filter is applied on is the same data that yt-dlp returns, simply, click on the
|
||||
information button, and check the data to craft your filter.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
|
|
@ -113,9 +117,10 @@
|
|||
<script setup>
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const box = useConfirm()
|
||||
|
||||
const items = ref([])
|
||||
const item = ref({})
|
||||
|
|
@ -124,6 +129,7 @@ const toggleForm = ref(false)
|
|||
const isLoading = ref(false)
|
||||
const initialLoad = ref(true)
|
||||
const addInProgress = ref(false)
|
||||
const remove_keys = ['in_progress', 'raw']
|
||||
|
||||
watch(() => config.app.basic_mode, async () => {
|
||||
if (!config.app.basic_mode) {
|
||||
|
|
@ -213,7 +219,7 @@ const updateItems = async items => {
|
|||
}
|
||||
|
||||
const deleteItem = async (item) => {
|
||||
if (true !== confirm(`Delete '${item.name}'?`)) {
|
||||
if (true !== box.confirm(`Delete '${item.name}'?`, true)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -235,6 +241,7 @@ const deleteItem = async (item) => {
|
|||
}
|
||||
|
||||
const updateItem = async ({ reference, item }) => {
|
||||
item = cleanObject(item, remove_keys)
|
||||
if (reference) {
|
||||
const index = items.value.findIndex(t => t?.id === reference)
|
||||
if (index > -1) {
|
||||
|
|
@ -253,11 +260,6 @@ const updateItem = async ({ reference, item }) => {
|
|||
resetForm(true)
|
||||
}
|
||||
|
||||
const filterItem = item => {
|
||||
const { raw, ...rest } = item
|
||||
return JSON.stringify(rest, null, 2)
|
||||
}
|
||||
|
||||
const editItem = _item => {
|
||||
item.value = _item
|
||||
itemRef.value = _item.id
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ const emitter = defineEmits(['getInfo'])
|
|||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const socket = useSocketStore()
|
||||
const box = useConfirm()
|
||||
|
||||
const get_info = ref('')
|
||||
const bg_enable = useStorage('random_bg', true)
|
||||
const bg_opacity = useStorage('random_bg_opacity', 0.85)
|
||||
|
|
@ -91,7 +93,7 @@ watch(() => stateStore.queue, () => {
|
|||
}, { deep: true })
|
||||
|
||||
const pauseDownload = () => {
|
||||
if (false === confirm('Are you sure you want to pause all non-active downloads?')) {
|
||||
if (false === box.confirm('Are you sure you want to pause all non-active downloads?')) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ import { useStorage } from '@vueuse/core'
|
|||
|
||||
let scrollTimeout = null
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const socket = useSocketStore()
|
||||
const config = useConfigStore()
|
||||
|
||||
|
|
|
|||
|
|
@ -140,9 +140,10 @@ div.is-centered {
|
|||
<script setup>
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const box = useConfirm()
|
||||
|
||||
const allowedEvents = ref([])
|
||||
const notifications = ref([])
|
||||
|
|
@ -234,7 +235,7 @@ const updateData = async notifications => {
|
|||
}
|
||||
|
||||
const deleteItem = async item => {
|
||||
if (true !== confirm(`Are you sure you want to delete notification target (${item.name})?`)) {
|
||||
if (true !== box.confirm(`Are you sure you want to delete notification target (${item.name})?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -294,7 +295,7 @@ const editItem = item => {
|
|||
const join_events = events => (!events || events.length < 1) ? 'ALL' : events.map(e => ucFirst(e)).join(', ')
|
||||
|
||||
const sendTest = async () => {
|
||||
if (true !== confirm('Are you sure you want to send a test notification?')) {
|
||||
if (true !== box.confirm('Are you sure you want to send a test notification?')) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,13 @@ table.is-fixed {
|
|||
table-layout: fixed;
|
||||
}
|
||||
|
||||
div.table-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.is-centered {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<main>
|
||||
<div class="mt-1 columns is-multiline">
|
||||
<div class="column is-12 is-clearfix is-unselectable">
|
||||
<span class="title is-4">
|
||||
|
|
@ -25,17 +21,23 @@ div.is-centered {
|
|||
<div class="is-pulled-right">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-primary" @click="
|
||||
resetForm(false);
|
||||
toggleForm = !toggleForm;
|
||||
">
|
||||
<span class="icon"><i class="fas fa-add"></i></span>
|
||||
<button class="button is-primary" @click="resetForm(false); toggleForm = !toggleForm;"
|
||||
v-tooltip.bottom="'Toggle add form'">
|
||||
<span class="icon"><i class="fas fa-add" /></span>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p class="control">
|
||||
<button v-tooltip.bottom="'Change display style'" class="button has-tooltip-bottom"
|
||||
@click="() => display_style = display_style === 'cards' ? 'list' : 'cards'">
|
||||
<span class="icon"><i class="fa-solid"
|
||||
:class="{ 'fa-table': display_style === 'cards', 'fa-table-list': display_style === 'list' }" /></span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-info" @click="reloadContent" :class="{ 'is-loading': isLoading }"
|
||||
:disabled="!socket.isConnected || isLoading" v-if="presets && presets.length > 0">
|
||||
<span class="icon"><i class="fas fa-refresh"></i></span>
|
||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -45,20 +47,71 @@ div.is-centered {
|
|||
that you want to apply to given download.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="toggleForm">
|
||||
<div class="columns" v-if="toggleForm">
|
||||
<div class="column is-12">
|
||||
<PresetForm :addInProgress="addInProgress" :reference="presetRef" :preset="preset" @cancel="resetForm(true)"
|
||||
@submit="updateItem" :presets="presets" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="!toggleForm">
|
||||
<div class="columns is-multiline" v-if="presetsNoDefault && presetsNoDefault.length > 0">
|
||||
<template v-if="!toggleForm">
|
||||
<div class="columns is-multiline" v-if="presetsNoDefault && presetsNoDefault.length > 0">
|
||||
<template v-if="'list' === display_style">
|
||||
<div class="column is-12">
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
|
||||
style="min-width: 650px; table-layout: fixed;">
|
||||
<thead>
|
||||
<tr class="has-text-centered is-unselectable">
|
||||
<th width="80%">Preset</th>
|
||||
<th width="20%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in presetsNoDefault" :key="item.id">
|
||||
<td class="is-vcentered">
|
||||
<div class="is-text-overflow">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="is-vcentered is-items-center">
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control">
|
||||
<button class="button is-primary is-small is-fullwidth" v-tooltip="'Export'"
|
||||
@click="exportItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-warning is-small is-fullwidth" v-tooltip="'Edit'"
|
||||
@click="editItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-danger is-small is-fullwidth" v-tooltip="'Delete'"
|
||||
@click="deleteItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-trash" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="column is-6" v-for="item in presetsNoDefault" :key="item.id">
|
||||
<div class="card is-flex is-full-height is-flex-direction-column">
|
||||
<header class="card-header">
|
||||
<div class="card-header-title is-text-overflow is-block" v-text="item.name" />
|
||||
<div class="card-header-icon">
|
||||
<button class="has-text-primary" v-tooltip="'Export preset.'" @click="exportItem(item)">
|
||||
<button class="has-text-primary" v-tooltip="'Export'" @click="exportItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</button>
|
||||
<button @click="item.raw = !item.raw">
|
||||
|
|
@ -123,47 +176,61 @@ div.is-centered {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Message title="No presets" message="There are no custom defined presets."
|
||||
class="is-background-warning-80 has-text-dark" icon="fas fa-exclamation-circle"
|
||||
v-if="!presets || presets.length < 1" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
When you export preset, it doesn't include <code>Cookies</code> field for security reasons.
|
||||
</li>
|
||||
<li>
|
||||
If you have created a global <code>config/ytdlp.cli</code> file, it will be appended to your exported preset
|
||||
<code><i class="fa-solid fa-terminal" /> Command arguments for yt-dlp</code> field for better compatibility
|
||||
and completeness.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline" v-if="!presets || presets.length < 1">
|
||||
<div class="column is-12">
|
||||
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
|
||||
message="Loading data. Please wait..." v-if="isLoading" />
|
||||
<Message title="No presets" message="There are no custom defined presets."
|
||||
class="is-background-warning-80 has-text-dark" icon="fas fa-exclamation-circle" v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline" v-if="presets && presets.length > 0">
|
||||
<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 preset, it doesn't include <code>Cookies</code> field for security reasons.
|
||||
</li>
|
||||
<li>
|
||||
If you have created a global <code>config/ytdlp.cli</code> file, it will be appended to your exported
|
||||
preset
|
||||
<code><i class="fa-solid fa-terminal" /> Command options for yt-dlp</code> field for better
|
||||
compatibility
|
||||
and completeness.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const box = useConfirm()
|
||||
|
||||
const display_style = useStorage("preset_display_style", "cards")
|
||||
|
||||
const presets = ref([])
|
||||
const preset = ref({})
|
||||
const presetRef = ref("")
|
||||
const toggleForm = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const initialLoad = ref(true)
|
||||
const addInProgress = ref(false)
|
||||
const remove_keys = ["raw", "toggle_description"]
|
||||
|
||||
const presetsNoDefault = computed(() =>
|
||||
presets.value.filter((t) => !t.default),
|
||||
)
|
||||
const presetsNoDefault = computed(() => presets.value.filter((t) => !t.default))
|
||||
|
||||
watch(
|
||||
() => config.app.basic_mode,
|
||||
|
|
@ -175,15 +242,12 @@ watch(
|
|||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => socket.isConnected,
|
||||
async () => {
|
||||
if (socket.isConnected && initialLoad.value) {
|
||||
await reloadContent(true)
|
||||
initialLoad.value = false
|
||||
}
|
||||
},
|
||||
)
|
||||
watch(() => socket.isConnected, async () => {
|
||||
if (socket.isConnected && initialLoad.value) {
|
||||
await reloadContent(true)
|
||||
initialLoad.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const reloadContent = async (fromMounted = false) => {
|
||||
try {
|
||||
|
|
@ -249,7 +313,7 @@ const updatePresets = async (items) => {
|
|||
}
|
||||
|
||||
const deleteItem = async (item) => {
|
||||
if (true !== confirm(`Delete preset '${item.name}'?`)) {
|
||||
if (true !== box.confirm(`Delete preset '${item.name}'?`, true)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -271,6 +335,7 @@ const deleteItem = async (item) => {
|
|||
}
|
||||
|
||||
const updateItem = async ({ reference, preset }) => {
|
||||
preset = cleanObject(preset, remove_keys)
|
||||
if (reference) {
|
||||
const index = presets.value.findIndex((t) => t?.id === reference)
|
||||
if (index > -1) {
|
||||
|
|
@ -290,7 +355,7 @@ const updateItem = async ({ reference, preset }) => {
|
|||
}
|
||||
|
||||
const filterItem = item => {
|
||||
const { raw, toggle_description, ...rest } = item
|
||||
const rest = cleanObject(item, remove_keys)
|
||||
if ("default" in rest) {
|
||||
delete rest.default
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,13 @@ table.is-fixed {
|
|||
table-layout: fixed;
|
||||
}
|
||||
|
||||
div.table-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.is-centered {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<main>
|
||||
<div class="mt-1 columns is-multiline">
|
||||
<div class="column is-12 is-clearfix is-unselectable">
|
||||
<span class="title is-4">
|
||||
|
|
@ -25,14 +21,24 @@ div.is-centered {
|
|||
<div class="is-pulled-right">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-primary" @click="resetForm(false); toggleForm = !toggleForm">
|
||||
<span class="icon"><i class="fas fa-add"></i></span>
|
||||
<button class="button is-primary" @click="resetForm(false); toggleForm = !toggleForm"
|
||||
v-tooltip.bottom="'Toggle Add form'">
|
||||
<span class="icon"><i class="fas fa-add" /></span>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p class="control">
|
||||
<button v-tooltip.bottom="'Change display style'" class="button has-tooltip-bottom"
|
||||
@click="() => display_style = display_style === 'cards' ? 'list' : 'cards'">
|
||||
<span class="icon"><i class="fa-solid"
|
||||
:class="{ 'fa-table': display_style === 'cards', 'fa-table-list': display_style === 'list' }" /></span>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p class="control">
|
||||
<button class="button is-info" @click="reloadContent" :class="{ 'is-loading': isLoading }"
|
||||
:disabled="!socket.isConnected || isLoading" v-if="tasks && tasks.length > 0">
|
||||
<span class="icon"><i class="fas fa-refresh"></i></span>
|
||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -43,96 +49,190 @@ div.is-centered {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="toggleForm">
|
||||
<div class="columns is-multiline" v-if="toggleForm">
|
||||
<div class="column is-12">
|
||||
<TaskForm :addInProgress="addInProgress" :reference="taskRef" :task="task" @cancel="resetForm(true);"
|
||||
@submit="updateItem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="!toggleForm">
|
||||
<div class="columns is-multiline" v-if="tasks && tasks.length > 0">
|
||||
<div class="column is-6" v-for="item in tasks" :key="item.id">
|
||||
<div class="card is-flex is-full-height is-flex-direction-column">
|
||||
<header class="card-header">
|
||||
<div class="card-header-title is-text-overflow is-block">
|
||||
<NuxtLink target="_blank" :href="item.url">{{ item.name }}</NuxtLink>
|
||||
</div>
|
||||
<div class="card-header-icon">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-content is-flex-grow-1">
|
||||
<div class="content">
|
||||
<p class="is-text-overflow">
|
||||
<div class="columns is-multiline" v-if="!isLoading && !toggleForm && tasks && tasks.length > 0">
|
||||
<template v-if="'list' === display_style">
|
||||
<div class="column is-12">
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
|
||||
style="min-width: 850px; table-layout: fixed;">
|
||||
<thead>
|
||||
<tr class="has-text-centered is-unselectable">
|
||||
<th width="50%">
|
||||
<span class="icon"><i class="fa-solid fa-tasks" /></span>
|
||||
<span>Task</span>
|
||||
</th>
|
||||
<th width="30%">
|
||||
<span class="icon"><i class="fa-solid fa-clock" /></span>
|
||||
<span>{{ tryParse(item.timer) }} - {{ item.timer }}</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.folder">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
<span>{{ calcPath(item.folder) }}</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.template">
|
||||
<span class="icon"><i class="fa-solid fa-file" /></span>
|
||||
<span>{{ item.template }}</span>
|
||||
</p>
|
||||
<p class="is-text-overflow">
|
||||
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
||||
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.cli">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
<span>{{ item.cli }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<span>Timer</span>
|
||||
</th>
|
||||
<th width="20%">
|
||||
<span class="icon"><i class="fa-solid fa-gear" /></span>
|
||||
<span>Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in tasks" :key="item.id">
|
||||
<td class="is-vcentered">
|
||||
<div class="is-text-overflow">
|
||||
<NuxtLink target="_blank" :href="item.url" class="is-bold">
|
||||
{{ item.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div v-if="item.preset">
|
||||
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
||||
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="is-vcentered has-text-centered">
|
||||
<span v-if="item.timer" class="has-tooltip" v-tooltip="item.timer">
|
||||
{{ tryParse(item.timer) }}
|
||||
</span>
|
||||
<span v-else class="has-text-danger">
|
||||
<span class="icon"><i class="fa-solid fa-exclamation-triangle" /></span>
|
||||
<span>No timer is set</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="is-vcentered is-items-center">
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
|
||||
<div class="control">
|
||||
<button class="button is-purple is-small is-fullwidth" v-tooltip="'Run now'"
|
||||
@click="runNow(item)" :class="{ 'is-loading': item?.in_progress }">
|
||||
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button class="button is-primary is-small is-fullwidth" v-tooltip="'Export'"
|
||||
@click="exportItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-warning is-small is-fullwidth" v-tooltip="'Edit'"
|
||||
@click="editItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-danger is-small is-fullwidth" v-tooltip="'Delete'"
|
||||
@click="deleteItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-trash" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="column is-6" v-for="item in tasks" :key="item.id">
|
||||
<div class="card is-flex is-full-height is-flex-direction-column">
|
||||
<header class="card-header">
|
||||
<div class="card-header-title is-text-overflow is-block">
|
||||
<NuxtLink target="_blank" :href="item.url">{{ item.name }}</NuxtLink>
|
||||
</div>
|
||||
<div class="card-footer mt-auto">
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-warning is-fullwidth" @click="editItem(item);">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-danger is-fullwidth" @click="deleteItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-trash" /></span>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-purple is-fullwidth" @click="runNow(item)">
|
||||
<span class="icon"><i class="fa-solid fa-microchip" /></span>
|
||||
<span>Run now</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-header-icon">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-content is-flex-grow-1">
|
||||
<div class="content">
|
||||
<p class="is-text-overflow">
|
||||
<span class="icon"><i class="fa-solid fa-clock" /></span>
|
||||
<span v-if="item.timer">{{ tryParse(item.timer) }} - {{ item.timer }}</span>
|
||||
<span v-else>No timer is set</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.folder">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
<span>{{ calcPath(item.folder) }}</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.template">
|
||||
<span class="icon"><i class="fa-solid fa-file" /></span>
|
||||
<span>{{ item.template }}</span>
|
||||
</p>
|
||||
<p class="is-text-overflow">
|
||||
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
||||
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.cli">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
<span>{{ item.cli }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer mt-auto">
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-warning is-fullwidth" @click="editItem(item);">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-danger is-fullwidth" @click="deleteItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-trash" /></span>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-purple is-fullwidth" @click="runNow(item)"
|
||||
:class="{ 'is-loading': item?.in_progress }">
|
||||
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
|
||||
<span>Run now</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline" v-if="!tasks || tasks.length < 1">
|
||||
<div class="column is-12">
|
||||
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
|
||||
message="Loading data. Please wait..." v-if="isLoading" />
|
||||
<Message title="No tasks" message="No tasks are defined." class="is-background-warning-80 has-text-dark"
|
||||
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
|
||||
icon="fas fa-exclamation-circle" v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from 'moment'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { CronExpressionParser } from 'cron-parser'
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const box = useConfirm()
|
||||
|
||||
const tasks = ref([])
|
||||
const task = ref({})
|
||||
const taskRef = ref('')
|
||||
const toggleForm = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const initialLoad = ref(true)
|
||||
const addInProgress = ref(false)
|
||||
const display_style = useStorage("tasks_display_style", "cards")
|
||||
|
||||
watch(() => config.app.basic_mode, async () => {
|
||||
if (!config.app.basic_mode) {
|
||||
|
|
@ -216,7 +316,7 @@ const updateTasks = async items => {
|
|||
}
|
||||
|
||||
const deleteItem = async item => {
|
||||
if (true !== confirm(`Are you sure you want to delete (${item.name})?`)) {
|
||||
if (true !== box.confirm(`Delete '${item.name}' task?`, true)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -238,6 +338,7 @@ const deleteItem = async item => {
|
|||
}
|
||||
|
||||
const updateItem = async ({ reference, task }) => {
|
||||
task = cleanObject(task, ['in_progress'])
|
||||
if (reference) {
|
||||
// -- find the task index.
|
||||
const index = tasks.value.findIndex((t) => t?.id === reference)
|
||||
|
|
@ -289,11 +390,13 @@ const tryParse = expression => {
|
|||
}
|
||||
}
|
||||
|
||||
const runNow = item => {
|
||||
if (true !== confirm(`Run '${item.name}' now? it will also run at the scheduled time.`)) {
|
||||
const runNow = async item => {
|
||||
if (true !== box.confirm(`Run '${item.name}' now? it will also run at the scheduled time.`)) {
|
||||
return
|
||||
}
|
||||
|
||||
item.in_progress = true
|
||||
|
||||
let data = {
|
||||
url: item.url,
|
||||
preset: item.preset,
|
||||
|
|
@ -312,6 +415,11 @@ const runNow = item => {
|
|||
}
|
||||
|
||||
socket.emit('add_url', data)
|
||||
|
||||
setTimeout(async () => {
|
||||
await nextTick()
|
||||
item.in_progress = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onUnmounted(() => socket.off('status', statusHandler))
|
||||
|
|
@ -349,4 +457,6 @@ const exportItem = async item => {
|
|||
|
||||
return copyText(base64UrlEncode(JSON.stringify(data)));
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
const runtimeConfig = useRuntimeConfig()
|
||||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
|
||||
const socket = ref(null);
|
||||
const isConnected = ref(false);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useNotification()
|
||||
const AG_SEPARATOR = '.'
|
||||
|
||||
/**
|
||||
|
|
@ -221,7 +219,12 @@ const encodePath = item => {
|
|||
}
|
||||
|
||||
item = item.replace(/#/g, '%23')
|
||||
return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/')
|
||||
try {
|
||||
return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/')
|
||||
} catch (e) {
|
||||
console.error('Error encoding path:', e, item)
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -473,6 +476,22 @@ const toggleClass = (target, className) => {
|
|||
target.classList.add(className)
|
||||
}
|
||||
|
||||
const cleanObject = (item, fields = []) => {
|
||||
if (!item || typeof item !== 'object' || fields.length < 1) {
|
||||
return item
|
||||
}
|
||||
|
||||
const cleaned = {}
|
||||
|
||||
for (const key of Object.keys(item)) {
|
||||
if (false === fields.includes(key)) {
|
||||
cleaned[key] = item[key]
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
export {
|
||||
ag_set,
|
||||
ag,
|
||||
|
|
@ -499,4 +518,5 @@ export {
|
|||
base64UrlDecode,
|
||||
has_data,
|
||||
toggleClass,
|
||||
cleanObject,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue