Added initial webui code for custom GUID support. NYI
This commit is contained in:
257
frontend/pages/custom/add.vue
Normal file
257
frontend/pages/custom/add.vue
Normal 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>
|
||||
208
frontend/pages/custom/index.vue
Normal file
208
frontend/pages/custom/index.vue
Normal 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> </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> </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> </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> </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> </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>
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user