Added initial webui code for custom GUID support. NYI

This commit is contained in:
Abdulmhsen B. A. A.
2024-10-05 16:52:46 +03:00
parent e2d8789c9e
commit b770d3c0d3
3 changed files with 468 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
<template>
<div>
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span id="env_page_title" class="title is-4">
<span class="icon"><i class="fas fa-map"></i></span>
Add Custom GUID
</span>
<div class="is-hidden-mobile">
<span class="subtitle"></span>
</div>
</div>
<div class="column is-12">
<form id="page_form" @submit.prevent="addIgnoreRule">
<div class="card">
<header class="card-header">
<p class="card-header-title is-unselectable is-justify-center">Add Custom GUID</p>
</header>
<div class="card-content">
<div class="field">
<label class="label is-unselectable" for="form_ignore_id">Name</label>
<div class="control has-icons-left">
<input class="input" id="form_guid_name" type="text" v-model="form.name" placeholder="guid_foobar">
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>All GUIDs names must start with <code>guid_</code>. For example,
<code>guid_foobar</code>. You cannot use the same name as an existing GUID.
</span>
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_select_type">Type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="form_select_type" v-model="form.type">
<option value="" disabled>Select Type</option>
<option value="string">String</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-cog"></i>
</div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>We currently only support <code>string</code> type.</span>
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_description">Description</label>
<div class="control has-icons-left">
<input class="input" id="form_description" type="text" v-model="form.description"
placeholder="This GUID is based on ... db reference">
<div class="icon is-small is-left"><i class="fas fa-envelope-open-text"></i></div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>GUID description, For information purposes only.</span>
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_validation_pattern">Regex validation pattern</label>
<div class="control has-icons-left">
<input class="input" id="form_validation_pattern" type="text" v-model="form.validator.pattern"
placeholder="/^[0-9\\/]+$/i">
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>
A Valid regular expression to check the value GUID value. To test your patterns, you can use this
website
<NuxtLink target="_blank" to="https://regex101.com/#php73" v-text="'regex101.com'"/>
.
</span>
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable">
Correct values.
<NuxtLink class="has-text-primary" @click="form.validator.tests.valid.push('')" v-text="'Add'"/>
</label>
<div class="columns is-multiline">
<template v-for="(_, index) in form.validator.tests.valid" :key="`valid-${index}`">
<div class="column is-11">
<div class="control has-icons-left">
<input class="input" type="text" v-model="form.validator.tests.valid[index]">
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
</div>
</div>
<div class="column">
<button class="button is-danger" type="button"
@click="form.validator.tests.valid.splice(index, 1)"
:disabled="index < 1 || form.validator.tests.valid < 1">
<span class="icon"><i class="fas fa-trash"></i></span>
</button>
</div>
</template>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>
The values added here must match the pattern defined above. Example: <code>123</code>.
Additionally, the pattern also must support <code>/</code> being part of the value. as we used it
for relative GUIDs. There must be a minimum of 1 correct value.
</span>
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable">
Incorrect values.
<NuxtLink class="has-text-danger" @click="form.validator.tests.invalid.push('')" v-text="'Add'"/>
</label>
<div class="columns is-multiline">
<template v-for="(_, index) in form.validator.tests.invalid" :key="`valid-${index}`">
<div class="column is-11">
<div class="control has-icons-left">
<input class="input" type="text" v-model="form.validator.tests.invalid[index]">
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
</div>
</div>
<div class="column">
<button class="button is-danger" type="button"
@click="form.validator.tests.invalid.splice(index, 1)"
:disabled="index < 1 || form.validator.tests.invalid < 1">
<span class="icon"><i class="fas fa-trash"></i></span>
</button>
</div>
</template>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>GUID values with should not match the pattern defined above. Example: <code>abc</code>. There
must be a minimum of 1 incorrect value.</span>
</span>
</p>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === checkForm">
<span class="icon-text">
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save</span>
</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" type="button" @click="cancelForm">
<span class="icon-text">
<span class="icon"><i class="fas fa-cancel"></i></span>
<span>Cancel</span>
</span>
</button>
</div>
</div>
</div>
</form>
</div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
</ul>
</Message>
</div>
</div>
</div>
</template>
<script setup>
import 'assets/css/bulma-switch.css'
import request from '~/utils/request'
import {notification, stringToRegex} from '~/utils/index'
import {useStorage} from '@vueuse/core'
import Message from '~/components/Message'
useHead({title: 'Add Custom GUID'})
const empty_form = {
name: '',
type: '',
description: '',
validator: {pattern: '', example: '', tests: {valid: [''], invalid: ['']}}
}
const show_page_tips = useStorage('show_page_tips', true)
const items = ref([])
const form = ref(JSON.parse(JSON.stringify(empty_form)))
const guids = ref([])
onMounted(async () => {
try {
const response = await request('/system/guids')
guids.value = await response.json()
} catch (e) {
notification('error', 'Error', `Request error. ${e.message}`, 5000)
}
})
const addIgnoreRule = async () => {
const val = guids.value.find(g => g.guid === form.value.db)
if (val && val?.validator && val.validator.pattern) {
if (!stringToRegex(val.validator.pattern).test(form.value.id)) {
notification('error', 'Error', `Invalid GUID value, must match the pattern: '${val.validator.pattern}'. Example ${val.validator.example}`, 5000)
return
}
}
try {
const response = await request(`/ignore`, {
method: 'POST',
body: JSON.stringify(form.value)
})
const json = await response.json()
if (!response.ok) {
notification('error', 'Error', `${json.error.code}: ${json.error.message}`, 5000)
return
}
items.value.push(json)
notification('success', 'Success', 'Successfully added new ignore rule.', 5000)
} catch (e) {
notification('error', 'Error', `Request error. ${e.message}`, 5000)
}
}
const checkForm = computed(() => {
const {id, type, backend, db} = form.value
return '' !== id && '' !== type && '' !== backend && '' !== db
})
</script>

View File

@@ -0,0 +1,208 @@
<template>
<div>
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span id="env_page_title" class="title is-4">
<span class="icon"><i class="fas fa-map"></i></span>
Custom GUIDs Mapper
</span>
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<NuxtLink class="button is-primary" v-tooltip.bottom="'Add New GUID'" to="/custom/add">
<span class="icon">
<i class="fas fa-add"></i>
</span>
</NuxtLink>
</p>
<p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span class="icon">
<i class="fas fa-sync"></i>
</span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page allow you to add custom GUIDs to the system, this is useful when you want to map a GUID from a
backend to a different GUID in WatchState. Or add new GUID identifiers to the system.
</span>
</div>
</div>
<div class="column is-12" v-if="!data">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
<Message v-else message_class="has-background-success-90 has-text-dark" title="Information" icon="fas fa-check">
There are no custom GUIDs configured. You can add new GUIDs by clicking on the <i class="fa fa-add"></i>
button.
</Message>
</div>
<div class="column is-12" v-if="!isLoading &&data && data.guids">
<h1 class="is-unselectable title is-4">Custom GUIDs</h1>
<h2 class="is-unselectable subtitle">
This section contains the custom GUIDs that are currently configured in the system.
</h2>
<div class="columns is-multiline">
<div class="column is-3-tablet" v-for="(guid, index) in data.guids" :key="guid.name">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
{{ guid.name }}
</p>
<span class="card-header-icon">
<NuxtLink @click="deleteGUID(index, guid)" class="has-text-danger" v-tooltip="'Delete GUID.'">
<span class="icon"><i class="fas fa-trash"></i></span>
</NuxtLink>
</span>
</header>
<div class="card-content">
{{ guid.description }}
</div>
</div>
</div>
</div>
</div>
<div class="column is-12 is-clearfix is-unselectable" v-if="hasClientsData">
<span class="title is-4">
<span class="icon"><i class="fas fa-exchange-alt"></i></span>
Clients GUID Mapping
</span>
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-primary" v-tooltip.bottom="'Add new Link'">
<span class="icon"><i class="fas fa-add"></i></span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">This section contains the client to GUID mapping.</span>
</div>
</div>
<div class="column is-12" v-if="hasClientsData">
<div class="columns is-multiline">
<template v-if="data" v-for="client in supported" :key="client">
<div class="column is-6-tablet" v-if="client in data && data[client].length > 0"
v-for="(link, index) in data[client]" :key="`${client}-${index}`">
<div class="card">
<header class="card-header">
<template v-if="link.replace?.from">
<p class="card-header-title is-text-overflow pr-1 is-unselectable is-clickable"
@click="link.show = !link.show">
<span class="icon"><i
class="fas"
:class="{ 'fa-arrow-down': false === (link.show ?? false), 'fa-arrow-up': true === (link.show ?? false) }"
></i>&nbsp;</span>
{{ ucFirst(client) }} client link
</p>
</template>
<template v-else>
<p class="card-header-title is-text-overflow pr-1 is-unselectable">
{{ ucFirst(client) }} client link
</p>
</template>
<span class="card-header-icon">
<NuxtLink @click="deleteLink(index, link)" class="has-text-danger" v-tooltip="'Delete Link.'">
<span class="icon"><i class="fas fa-trash"></i></span>
</NuxtLink>
</span>
</header>
<div class="card-content">
<div class="columns is-mobile is-multiline">
<div class="column is-3 has-text-left">
<span class="icon"><i class="fas fa-arrow-right"></i>&nbsp;</span>
From
</div>
<div class="column is-9 has-text-right">
{{ link.map.from }}
</div>
<div class="column is-3 has-text-left">
<span class="icon"><i class="fas fa-arrow-left"></i>&nbsp;</span>
To
</div>
<div class="column is-9 has-text-right">
{{ link.map.to }}
</div>
<template v-if="link.replace?.from && ('show' in link && link.show)">
<div class="column is-3 has-text-left">
<span class="icon"><i class="fas fa-xmark"></i>&nbsp;</span>
Replace
</div>
<div class="column is-9 has-text-right">
{{ link.replace.from }}
</div>
<div class="column is-3 has-text-left">
<span class="icon"><i class="fas fa-check"></i>&nbsp;</span>
To
</div>
<div class="column is-9 has-text-right">
{{ link.replace.to }}
</div>
</template>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
Clients means the internal implementations of the backends in WatchState, for example, Plex, Emby,
Jellyfin, etc.
</li>
</ul>
</Message>
</div>
</div>
</div>
</template>
<script setup>
import 'assets/css/bulma-switch.css'
import request from '~/utils/request'
import {notification, parse_api_response} from '~/utils/index'
import {useStorage} from '@vueuse/core'
import Message from '~/components/Message'
useHead({title: 'Custom Guid Mapper'})
const data = ref({})
const show_page_tips = useStorage('show_page_tips', true)
const isLoading = ref(false)
const supported = ref([]);
const loadContent = async () => {
try {
isLoading.value = true
data.value = await parse_api_response(await request(`/system/guids/custom`))
} catch (e) {
isLoading.value = false
return notification('error', 'Error', e.message)
} finally {
isLoading.value = false
}
}
onMounted(async () => {
const supportedClients = await request('/system/supported')
supported.value = await supportedClients.json()
await loadContent()
})
const hasClientsData = computed(() => !isLoading.value && supported.value.length > 0 && Object.keys(data.value).length > 0 && supported.value.some(client => client in data.value))
</script>

View File

@@ -10,6 +10,7 @@ use App\Libs\Attributes\Route\Put;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\Container;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Guid;
use Psr\Http\Message\ResponseInterface as iResponse;
@@ -50,6 +51,8 @@ final class Guids
#[Put(self::URL . '/custom[/]', name: 'system.guids.custom.guid.add')]
public function custom_guid_add(iRequest $request): iResponse
{
$params = DataUtil::fromRequest($request);
return api_response(Status::OK, $request->getParsedBody());
}