Technically working options. Need CSS fix (plugin CSS not loaded) and data validation

This commit is contained in:
Mehdi Benadel 2024-05-23 02:26:38 +02:00
parent 9ea26dfd26
commit 687c4742f7
16 changed files with 717 additions and 1237 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@ -39,23 +39,10 @@ function loadLocs() {
return r
}
function loadMustaches () {
// Loading mustache templates, dans filling constants.
const r = []
r['MUSTACHE_CONFIGURATION_HOME'] = loadMustache('client/common/configuration/templates/home.mustache')
r['MUSTACHE_CONFIGURATION_CHANNEL'] = loadMustache('client/common/configuration/templates/channel.mustache')
return r
}
function loadMustache (file) {
const filePath = path.resolve(__dirname, file)
return JSON.stringify(fs.readFileSync(filePath).toString())
}
const define = Object.assign({
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
}, loadLocs(), loadMustaches())
}, loadLocs())
const configs = clientFiles.map(f => ({
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],

View File

@ -29,7 +29,8 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
onMount: async ({ rootEl }) => {
const urlParams = new URLSearchParams(window.location.search)
const channelId = urlParams.get('channelId') ?? ''
render(html`<channel-configuration .registerClientOptions=${clientOptions}></channel-configuration>`, rootEl)
render(html`<channel-configuration .registerClientOptions=${clientOptions}
.channelId=${channelId}></channel-configuration>`, rootEl)
}
})

View File

@ -1,33 +1,53 @@
import { RegisterClientOptions } from '@peertube/peertube-types/client'
import { html, LitElement } from 'lit'
import { css, html, LitElement } from 'lit'
import { repeat } from 'lit-html/directives/repeat.js'
import { customElement, property } from 'lit/decorators.js'
import { customElement, property, state } from 'lit/decorators.js'
import { ptTr } from './TranslationDirective'
import { localizedHelpUrl } from '../../../utils/help'
import './DynamicTableFormElement'
import './PluginConfigurationRow'
import './HelpButtonElement'
import { until } from 'async'
import { Task } from '@lit/task';
import { ChannelConfiguration } from 'shared/lib/types'
import { ChannelConfigurationService } from './ChannelConfigurationService'
import { createContext, provide } from '@lit/context'
import { getGlobalStyleSheets } from '../../global-styles'
export const registerClientOptionsContext = createContext<RegisterClientOptions | undefined>(Symbol('register-client-options'));
export const channelConfigurationContext = createContext<ChannelConfiguration | undefined>(Symbol('channel-configuration'));
export const channelConfigurationServiceContext = createContext<ChannelConfigurationService | undefined>(Symbol('channel-configuration-service'));
@customElement('channel-configuration')
export class ChannelConfigurationElement extends LitElement {
@provide({ context: registerClientOptionsContext })
@property({ attribute: false })
public registerClientOptions: RegisterClientOptions | undefined
createRenderRoot = () => {
return this
}
@property({ attribute: false })
public channelId: number | undefined
@provide({ context: channelConfigurationContext })
@state()
public _channelConfiguration: ChannelConfiguration | undefined
@provide({ context: channelConfigurationServiceContext })
private _configurationService: ChannelConfigurationService | undefined
static styles = [
...getGlobalStyleSheets()
];
@state()
public _formStatus: boolean | any = undefined
private _asyncTaskRender = new Task(this, {
task: async ([registerClientOptions], {signal}) => {
let link = registerClientOptions ? await localizedHelpUrl(registerClientOptions, { page: 'documentation/user/streamers/bot/forbidden_words' }) : '';
return {
url : new URL(link),
title: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)
if (this.registerClientOptions) {
this._configurationService = new ChannelConfigurationService(this.registerClientOptions)
this._channelConfiguration = await this._configurationService.fetchConfiguration(this.channelId ?? 0)
}
},
@ -35,9 +55,26 @@ export class ChannelConfigurationElement extends LitElement {
});
private _saveConfig = () => {
if(this._configurationService && this._channelConfiguration) {
this._configurationService.saveOptions(this._channelConfiguration.channel.id, this._channelConfiguration.configuration)
.then((value) => {
this._formStatus = { success: true }
console.log(`Configuration has been updated`)
this.requestUpdate('_formStatus')
})
.catch((error) => {
this._formStatus = error
console.log(`An error occurred : ${JSON.stringify(this._formStatus)}`)
this.requestUpdate('_formStatus')
});
}
}
render = () => {
let tableHeader = {
words: {
let tableHeaderList = {
forbiddenWords: {
entries: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
},
@ -61,15 +98,38 @@ export class ChannelConfigurationElement extends LitElement {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC)
}
},
quotes: {
messages: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL2),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC2)
},
delay: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC)
}
},
commands: {
command: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_DESC)
},
message: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_DESC)
}
}
}
let tableSchema = {
words: {
inputType: 'text',
default: 'helloqwesad'
forbiddenWords: {
entries: {
inputType: 'textarea',
default: ['helloqwesad'],
separator: '\n',
},
regex: {
inputType: 'text',
default: 'helloaxzca'
default: 'helloaxzca',
},
applyToModerators: {
inputType: 'checkbox',
@ -80,10 +140,9 @@ export class ChannelConfigurationElement extends LitElement {
default: 'helloasx'
},
reason: {
inputType: 'select',
inputType: 'text',
default: 'transphobia',
label: 'choose your poison',
options: {'racism': 'Racism', 'sexism': 'Sexism', 'transphobia': 'Transphobia', 'bigotry': 'Bigotry'}
datalist: ['Racism', 'Sexism', 'Transphobia', 'Bigotry']
},
comments: {
inputType: 'textarea',
@ -95,52 +154,179 @@ export class ChannelConfigurationElement extends LitElement {
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.`
},
},
quotes: {
messages: {
inputType: 'textarea',
default: ['default message'],
separator: '\n',
},
delay: {
inputType: 'number',
default: 100,
}
let tableRows = [
{
words: 'teweqwst',
regex: 'tesdgst',
applyToModerators: false,
label: 'teswet',
reason: 'sexism',
comments: 'tsdaswest',
},
{
words: 'tedsadst',
regex: 'tezxccst',
applyToModerators: true,
label: 'tewest',
reason: 'racism',
comments: 'tesxzct',
commands: {
command: {
inputType: 'text',
default: 'default command',
},
{
words: 'tesadsdxst',
regex: 'dsfsdf',
applyToModerators: false,
label: 'tesdadst',
reason: 'bigotry',
comments: 'tsadest',
},
]
message: {
inputType: 'text',
default: 'default message',
}
}
}
return this._asyncTaskRender.render({
complete: (helpLink) => html`
<div class="container">
<channel-configuration></channel-configuration>
complete: () => html`
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel">
<h1>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE)}:
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${this._channelConfiguration?.channel.displayName}</span>
<span>${this._channelConfiguration?.channel.name}</span>
</span>
<help-button .page="documentation/user/streamers/channel">
</help-button>
</h1>
<p>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}</p>
<form livechat-configuration-channel-options role="form">
<div class="row mt-3">
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC, true)}
.helpPage=${"documentation/user/streamers/slow_mode"}>
<div class="form-group">
<label>
<input
type="number"
name="slow_mode_duration"
class="form-control"
min="0"
max="1000"
id="peertube-livechat-slow-mode-duration"
@input=${(event: InputEvent) => {
if (event?.target && this._channelConfiguration)
this._channelConfiguration.configuration.slowMode.duration = Number((event.target as HTMLInputElement).value)
this.requestUpdate('_channelConfiguration')
}
}
value="${this._channelConfiguration?.configuration.slowMode.duration}"
/>
</label>
</div>
</plugin-configuration-row>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
.description=${''}
.helpPage=${"documentation/user/streamers/channel"}>
<div class="form-group">
<label>
<input
type="checkbox"
name="bot"
id="peertube-livechat-bot"
@input=${(event: InputEvent) => {
if (event?.target && this._channelConfiguration)
this._channelConfiguration.configuration.bot.enabled = (event.target as HTMLInputElement).checked
this.requestUpdate('_channelConfiguration')
}
}
.value=${this._channelConfiguration?.configuration.bot.enabled}
?checked=${this._channelConfiguration?.configuration.bot.enabled}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL)}
</label>
</div>
${this._channelConfiguration?.configuration.bot.enabled ?
html`<div class="form-group">
<label for="peertube-livechat-bot-nickname">${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME)}</label>
<input
type="text"
name="bot_nickname"
class="form-control"
id="peertube-livechat-bot-nickname"
@input=${(event: InputEvent) => {
if (event?.target && this._channelConfiguration)
this._channelConfiguration.configuration.bot.nickname = (event.target as HTMLInputElement).value
this.requestUpdate('_channelConfiguration')
}
}
value="${this._channelConfiguration?.configuration.bot.nickname}"
/>
</div>`
: ''
}
</plugin-configuration-row>
${this._channelConfiguration?.configuration.bot.enabled ?
html`<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
.helpLink=${helpLink}
>
.helpPage=${"documentation/user/streamers/bot/forbidden_words"}>
<dynamic-table-form
.header=${tableHeader}
.schema=${tableSchema}
.rows=${tableRows}
.formName=${'forbidden-words'}
>
.header=${tableHeaderList.forbiddenWords}
.schema=${tableSchema.forbiddenWords}
.rows=${this._channelConfiguration?.configuration.bot.forbiddenWords}
@update=${(e: CustomEvent) => {
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.forbiddenWords = e.detail
this.requestUpdate('_channelConfiguration')
}
}
.formName=${'forbidden-words'}>
</dynamic-table-form>
</plugin-configuration-row>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)}
.helpPage=${"documentation/user/streamers/bot/quotes"}>
<dynamic-table-form
.header=${tableHeaderList.quotes}
.schema=${tableSchema.quotes}
.rows=${this._channelConfiguration?.configuration.bot.quotes}
@update=${(e: CustomEvent) => {
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.quotes = e.detail
this.requestUpdate('_channelConfiguration')
}
}
.formName=${'quote'}>
</dynamic-table-form>
</plugin-configuration-row>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)}
.helpPage=${"documentation/user/streamers/bot/commands"}>
<dynamic-table-form
.header=${tableHeaderList.commands}
.schema=${tableSchema.commands}
.rows=${this._channelConfiguration?.configuration.bot.commands}
@update=${(e: CustomEvent) => {
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.commands = e.detail
this.requestUpdate('_channelConfiguration')
}
}
.formName=${'command'}>
</dynamic-table-form>
</plugin-configuration-row>`
: ''
}
<div class="form-group mt-5">
<button type="button" class="orange-button" @click=${this._saveConfig}>${ptTr(LOC_SAVE)}</button>
</div>
${(this._formStatus && this._formStatus.success === undefined) ?
html`<div class="alert alert-warning" role="alert">
An error occurred : ${JSON.stringify(this._formStatus)}
</div>`
: ''
}
${(this._formStatus && this._formStatus.success === true) ?
html`<div class="alert alert-success" role="alert">
Configuration has been updated
</div>`
: ''
}
</form>
</div>${JSON.stringify(this._channelConfiguration)}`
})
}
}

View File

@ -0,0 +1,60 @@
import { RegisterClientOptions } from "@peertube/peertube-types/client"
import { ChannelConfiguration, ChannelConfigurationOptions } from "shared/lib/types"
import { getBaseRoute } from "../../../utils/uri"
export class ChannelConfigurationService {
public _registerClientOptions: RegisterClientOptions
private _headers : any = {}
constructor(registerClientOptions: RegisterClientOptions) {
this._registerClientOptions = registerClientOptions
this._headers = this._registerClientOptions.peertubeHelpers.getAuthHeader() ?? {}
this._headers['content-type'] = 'application/json;charset=UTF-8'
}
validateOptions = (channelConfigurationOptions: ChannelConfigurationOptions) => {
return true
}
saveOptions = async (channelId: number, channelConfigurationOptions: ChannelConfigurationOptions) => {
if (!await this.validateOptions(channelConfigurationOptions)) {
throw new Error('Invalid form data')
}
const response = await fetch(
getBaseRoute(this._registerClientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId),
{
method: 'POST',
headers: this._headers,
body: JSON.stringify(channelConfigurationOptions)
}
)
if (!response.ok) {
throw new Error('Failed to save configuration options.')
}
return await response.json()
}
fetchConfiguration = async (channelId: number): Promise<ChannelConfiguration> => {
const response = await fetch(
getBaseRoute(this._registerClientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId),
{
method: 'GET',
headers: this._headers
}
)
if (!response.ok) {
throw new Error('Can\'t get channel configuration options.')
}
return await response.json()
}
}

View File

@ -1,11 +1,26 @@
import { html, LitElement, TemplateResult } from 'lit'
import { css, html, LitElement, nothing, TemplateResult } from 'lit'
import { repeat } from 'lit/directives/repeat.js'
import { customElement, property, state } from 'lit/decorators.js'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { StaticValue, unsafeStatic } from 'lit/static-html.js'
import { getGlobalStyleSheets } from '../../global-styles'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
type DynamicTableAcceptedTypes = number | string | boolean | Date
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
const AddSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>
</svg>`
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
const RemoveSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line>
</svg>`
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
type DynamicTableAcceptedInputTypes = 'textarea'
| 'select'
@ -36,6 +51,8 @@ interface CellDataSchema {
size?: number
label?: string
options?: { [key: string]: string }
datalist?: DynamicTableAcceptedTypes[]
separator?: string
inputType?: DynamicTableAcceptedInputTypes
default?: DynamicTableAcceptedTypes
}
@ -50,49 +67,94 @@ export class DynamicTableFormElement extends LitElement {
@property({ attribute: false })
public schema: { [key: string]: CellDataSchema } = {}
@property({ attribute: false })
public rows: { [key: string]: DynamicTableAcceptedTypes }[] = []
@property({ reflect: true })
public rows: { _id: number; [key : string]: DynamicTableAcceptedTypes }[] = []
@state()
public _rowsById: { _id: number; row: { [key: string]: DynamicTableAcceptedTypes } }[] = []
@property({ attribute: false })
public formName: string = ''
@state()
private _lastRowId = 1
createRenderRoot = () => {
return this
@property({ attribute: false })
private _colOrder: string[] = []
static styles = [
...getGlobalStyleSheets(),
css`
:host table {
table-layout: fixed;
text-align: center;
}
private _getDefaultRow = () => {
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? '']), ['_id', this._lastRowId++]])
:host table td, table th {
word-wrap:break-word;
vertical-align: top;
padding: 5px 7px;
}
:host table tbody > :nth-child(odd) {
background-color: var(--greySecondaryBackgroundColor);
}
:host button {
padding: 2px;
}
:host .dynamic-table-add-row {
background-color: var(--bs-green);
}
:host .dynamic-table-remove-row {
background-color: var(--bs-orange);
}
`
];
// fixes situations when list has been reinitialized or changed outside of CustomElement
private _updateLastRowId = () => {
for (let rowById of this._rowsById) {
this._lastRowId = Math.max(this._lastRowId, rowById._id + 1);
}
}
private _getDefaultRow = () : { [key: string]: DynamicTableAcceptedTypes } => {
this._updateLastRowId()
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
}
private _addRow = () => {
this.rows.push(this._getDefaultRow())
let newRow = this._getDefaultRow()
this._rowsById.push({_id:this._lastRowId++, row: newRow})
this.rows.push(newRow)
this.requestUpdate('rows')
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
}
private _removeRow = (rowId: number) => {
this.rows = this.rows.filter((x) => x._id != rowId)
let rowToRemove = this._rowsById.filter(rowById => rowById._id == rowId).map(rowById => rowById.row)[0]
this._rowsById = this._rowsById.filter((rowById) => rowById._id !== rowId)
this.rows = this.rows.filter((row) => row !== rowToRemove)
this.requestUpdate('rows')
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
}
render = () => {
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-table`
this._updateLastRowId()
this._rowsById.filter(rowById => this.rows.includes(rowById.row))
for (let row of this.rows) {
if (!row._id) {
row._id = this._lastRowId++
if (this._rowsById.filter(rowById => rowById.row === row).length == 0) {
this._rowsById.push({_id: this._lastRowId++, row })
}
}
@ -100,44 +162,55 @@ export class DynamicTableFormElement extends LitElement {
<table class="table table-striped table-hover table-sm" id=${inputId}>
${this._renderHeader()}
<tbody>
${repeat(this.rows,(row) => row._id, this._renderDataRow)}
${repeat(this._rowsById, (rowById) => rowById._id, this._renderDataRow)}
</tbody>
<tfoot>
<tr><td><button @click=${this._addRow}>Add Row</button></td></tr>
</tfoot>
${this._renderFooter()}
</table>
`
}
private _renderHeader = () => {
if (this._colOrder.length !== Object.keys(this.header).length) {
this._colOrder = this._colOrder.filter(key => Object.keys(this.header).includes(key))
this._colOrder.push(...Object.keys(this.header).filter(key => !this._colOrder.includes(key)))
}
return html`<thead>
<tr>
<!-- <th scope="col">#</th> -->
${Object.values(this.header).map(this._renderHeaderCell)}
<th scope="col">Remove Row</th>
${Object.entries(this.header).sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2))
.map(([k,v]) => this._renderHeaderCell(v))}
<th scope="col"></th>
</tr>
</thead>`
}
private _renderHeaderCell = (headerCellData: { colName: TemplateResult, description: TemplateResult }) => {
return html`<th scope="col">
<div data-toggle="tooltip" data-placement="bottom" data-html="true" title="${headerCellData.description}">${headerCellData.colName}</div>
<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${headerCellData.description}>${headerCellData.colName}</div>
</th>`
}
private _renderDataRow = (rowData: { _id: number; [key : string]: DynamicTableAcceptedTypes }) => {
private _renderDataRow = (rowData: { _id: number; row: {[key: string]: DynamicTableAcceptedTypes} }) => {
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-row-${rowData._id}`
return html`<tr id=${inputId}>
<!-- <td class="form-group">${rowData._id}</td> -->
${Object.entries(rowData).filter(([k,v]) => k != '_id').map((data) => this.renderDataCell(data, rowData._id))}
<td class="form-group"><button @click=${() => this._removeRow(rowData._id)}>Remove</button></td>
${Object.entries(rowData.row).filter(([k, v]) => k != '_id')
.sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2))
.map((data) => this.renderDataCell(data, rowData._id))}
<td class="form-group"><button class="btn dynamic-table-remove-row" @click=${() => this._removeRow(rowData._id)}>${unsafeHTML(RemoveSVG)}</button></td>
</tr>`
}
private _renderFooter = () => {
return html`<tfoot>
<tr>
${Object.values(this.header).map(() => html`<td></td>`)}
<td><button class="btn dynamic-table-add-row" @click=${this._addRow}>${unsafeHTML(AddSVG)}</button></td>
</tr>
</tfoot>`
}
renderDataCell = (property: [string, DynamicTableAcceptedTypes], rowId: number) => {
const [propertyName, propertyValue] = property
const propertySchema = this.schema[propertyName] ?? {}
@ -169,48 +242,16 @@ export class DynamicTableFormElement extends LitElement {
case 'time':
case 'url':
case 'week':
formElement = html`<input
type=${propertySchema.inputType}
name=${inputName}
class="form-control"
id=${inputId}
min=${ifDefined(propertySchema?.min)}
max=${ifDefined(propertySchema?.max)}
minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${ifDefined(propertySchema?.maxlength)}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${propertyValue as string}
/>`
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
break
case 'textarea':
formElement = html`<textarea
name=${inputName}
class="form-control"
id=${inputId}
min=${ifDefined(propertySchema?.min)}
max=${ifDefined(propertySchema?.max)}
minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${ifDefined(propertySchema?.maxlength)}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${propertyValue as string}
></textarea>`
formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
break
case 'select':
formElement = html`<select
class="form-select"
aria-label="Default select example"
@change=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
>
<option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option>
${Object.entries(propertySchema?.options ?? {})
?.map(([value,name]) =>
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
)}
</select>`
formElement = this._renderSelect(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
break
}
break
@ -223,20 +264,8 @@ export class DynamicTableFormElement extends LitElement {
case 'datetime':
case 'datetime-local':
case 'time':
formElement = html`<input
type=${propertySchema.inputType}
name=${inputName}
class="form-control"
id=${inputId}
min=${ifDefined(propertySchema?.min)}
max=${ifDefined(propertySchema?.max)}
minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${ifDefined(propertySchema?.maxlength)}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${(propertyValue as Date).toISOString()}
/>`
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, (propertyValue as Date).toISOString())
break
}
break
@ -247,20 +276,8 @@ export class DynamicTableFormElement extends LitElement {
case 'number':
case 'range':
formElement = html`<input
type=${propertySchema.inputType}
name=${inputName}
class="form-control"
id=${inputId}
min=${ifDefined(propertySchema?.min)}
max=${ifDefined(propertySchema?.max)}
minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${ifDefined(propertySchema?.maxlength)}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${propertyValue as String}
/>`
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
break
}
break
@ -270,44 +287,130 @@ export class DynamicTableFormElement extends LitElement {
propertySchema.inputType = 'checkbox'
case 'checkbox':
formElement = html`<input
type="checkbox"
name=${inputName}
class="form-check-input"
id=${inputId}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${propertyValue as String}
?checked=${propertyValue as Boolean}
/>`
formElement = this._renderCheckbox(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as boolean)
break
}
break
case Array:
switch (propertySchema.inputType) {
case undefined:
propertySchema.inputType = 'text'
case 'text':
case 'color':
case 'date':
case 'datetime':
case 'datetime-local':
case 'email':
case 'file':
case 'image':
case 'month':
case 'number':
case 'password':
case 'range':
case 'tel':
case 'time':
case 'url':
case 'week':
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema,
(propertyValue as Array<number | string>).join(propertySchema.separator ?? ','))
break
case 'textarea':
formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema,
(propertyValue as Array<number | string>).join(propertySchema.separator ?? ','))
break
}
}
if (!formElement) {
console.warn(`value type '${propertyValue.constructor}' is incompatible`
+ `with field type '${propertySchema.inputType}' for form entry '${propertyName.toString()}'.`)
}
return html`<td class="form-group">${formElement}</td>`
}
_renderInput = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
return html`<input
type=${propertySchema.inputType}
name=${inputName}
class="form-control"
id=${inputId}
list=${(propertySchema?.datalist) ? inputId + '-datalist' : nothing}
min=${ifDefined(propertySchema?.min)}
max=${ifDefined(propertySchema?.max)}
minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${ifDefined(propertySchema?.maxlength)}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
.value=${propertyValue}
/>
${(propertySchema?.datalist) ? html`<datalist id=${inputId + '-datalist'}>
${(propertySchema?.datalist ?? []).map((value) => html`<option value=${value} />`)}
</datalist>` : nothing}
`
}
_renderTextarea = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
return html`<textarea
name=${inputName}
class="form-control"
id=${inputId}
min=${ifDefined(propertySchema?.min)}
max=${ifDefined(propertySchema?.max)}
minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${ifDefined(propertySchema?.maxlength)}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
.value=${propertyValue}
></textarea>`
}
_renderCheckbox = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: boolean) => {
return html`<input
type="checkbox"
name=${inputName}
class="form-check-input"
id=${inputId}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
.value=${propertyValue}
?checked=${propertyValue}
/>`
}
_renderSelect = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
return html`<select
class="form-select"
aria-label="Default select example"
@change=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
>
<option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option>
${Object.entries(propertySchema?.options ?? {})
?.map(([value, name]) =>
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
)}
</select>`
}
_updatePropertyFromValue(event: Event, propertyName: string, rowId : number) {
_updatePropertyFromValue = (event: Event, propertyName: string, propertySchema: CellDataSchema, rowId: number) => {
let target = event?.target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)
let value = (target && target instanceof HTMLInputElement && target.type == "checkbox") ? !!(target?.checked) : target?.value
if (value !== undefined) {
for(let row of this.rows) {
if(row._id === rowId) {
row[propertyName] = value
for (let rowById of this._rowsById) {
if (rowById._id === rowId) {
switch (rowById.row[propertyName].constructor) {
case Array:
rowById.row[propertyName] = (value as string).split(propertySchema.separator ?? ',')
default:
rowById.row[propertyName] = value
}
this.rows = this._rowsById.map(rowById => rowById.row)
this.requestUpdate('rows')
this.requestUpdate('rowsById')
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
return
}
}

View File

@ -0,0 +1,50 @@
import { css, html, LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { helpButtonSVG } from '../../../videowatch/buttons'
import { consume } from '@lit/context'
import { registerClientOptionsContext } from './ChannelConfigurationElement'
import { RegisterClientOptions } from '@peertube/peertube-types/client'
import { Task } from '@lit/task'
import { localizedHelpUrl } from '../../../utils/help'
import { ptTr } from './TranslationDirective'
import { DirectiveResult } from 'lit/directive'
import { getGlobalStyleSheets } from '../../global-styles'
@customElement('help-button')
export class HelpButtonElement extends LitElement {
@consume({context: registerClientOptionsContext})
public registerClientOptions: RegisterClientOptions | undefined
@property({ attribute: false })
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
@property({ attribute: false })
public page: string = ''
@state()
public url: URL = new URL('https://lmddgtfy.net/')
static styles = [
...getGlobalStyleSheets()
];
private _asyncTaskRender = new Task(this, {
task: async ([registerClientOptions], {signal}) => {
this.url = new URL(registerClientOptions ? await localizedHelpUrl(registerClientOptions, { page: this.page }) : '')
},
args: () => [this.registerClientOptions]
});
render() {
return this._asyncTaskRender.render({
complete: () => html`<a
href="${this.url.href}"
target=_blank
title="${this.buttonTitle}"
class="orange-button peertube-button-link"
>${unsafeHTML(helpButtonSVG())}</a>`
})
}
}

View File

@ -1,11 +1,10 @@
import { html, LitElement } from 'lit'
import { css, html, LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
import { StaticValue } from 'lit/static-html.js'
import { helpButtonSVG } from '../../../videowatch/buttons'
import './HelpButtonElement'
import { getGlobalStyleSheets } from '../../global-styles'
@customElement('plugin-configuration-row')
export class PLuginConfigurationRow extends LitElement {
export class PluginConfigurationRow extends LitElement {
@property({ attribute: false })
public title: string = `title`
@ -14,11 +13,11 @@ export class PLuginConfigurationRow extends LitElement {
public description: string = `Here's a description`
@property({ attribute: false })
public helpLink: { url: URL, title: string } = { url : new URL('https://lmddgtfy.net/'), title: 'Online Help'}
public helpPage: string = 'documentation'
createRenderRoot = () => {
return this
}
static styles = [
...getGlobalStyleSheets()
];
render() {
return html`
@ -26,12 +25,8 @@ export class PLuginConfigurationRow extends LitElement {
<div class="col-12 col-lg-4 col-xl-3">
<h2>${this.title}</h2>
<p>${this.description}</p>
<a
href="${this.helpLink.url.href}"
target=_blank
title="${this.helpLink.title}"
class="orange-button peertube-button-link"
>${unsafeSVG(helpButtonSVG())}</a>
<help-button .page=${this.helpPage}>
</help-button>
</div>
<div class="col-12 col-lg-8 col-xl-9">
<slot><p>Nothing in this row.</p></slot>

View File

@ -1,6 +1,9 @@
import { PartInfo, directive } from 'lit/directive.js'
import { PartInfo, PartType, directive } from 'lit/directive.js'
import { AsyncDirective } from 'lit/async-directive.js'
import { RegisterClientHelpers } from '@peertube/peertube-types/client';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { html } from 'lit';
import { unsafeStatic } from 'lit/static-html.js';
export class TranslationDirective extends AsyncDirective {
@ -9,22 +12,35 @@ export class TranslationDirective extends AsyncDirective {
private _translatedValue : string = ''
private _localizationId : string = ''
private _allowUnsafeHTML = false
constructor(partInfo: PartInfo) {
super(partInfo);
//_peertubeOptionsPromise.then((options) => this._peertubeHelpers = options.peertubeHelpers)
}
override render = (locId: string) => {
// update = (part: ElementPart) => {
// if (part) console.log(`Element : ${part?.element?.getAttributeNames?.().join(' ')}`);
// return this.render(this._localizationId);
// }
override render = (locId: string, allowHTML: boolean = false) => {
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
this._allowUnsafeHTML = allowHTML
if (this._translatedValue === '') {
this._translatedValue = locId
}
this._asyncUpdateTranslation()
return this._translatedValue
return this._internalRender()
}
_internalRender = () => {
return this._allowUnsafeHTML ? html`${unsafeHTML(this._translatedValue)}` : this._translatedValue
}
_asyncUpdateTranslation = async () => {
@ -32,7 +48,7 @@ export class TranslationDirective extends AsyncDirective {
if (newValue !== '' && newValue !== this._translatedValue) {
this._translatedValue = newValue
this.setValue(newValue)
this.setValue(this._internalRender())
}
}
}

View File

@ -1,233 +0,0 @@
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel">
<h1>
{{title}}:
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>{{channelConfiguration.channel.displayName}}</span>
<span>{{channelConfiguration.channel.name}}</span>
</span>
{{{helpButton}}}
</h1>
<p>{{description}}</p>
<form livechat-configuration-channel-options role="form">
<div class="row mt-3">
<div class="col-12 col-lg-4 col-xl-3">
<h2>{{slowModeLabel}}</h2>
<p>{{{slowModeDesc}}}</p>
{{{helpButtonSlowMode}}}
</div>
<div class="col-12 col-lg-8 col-xl-9">
<div class="form-group">
<label>
<input
type="number"
name="slow_mode_duration"
class="form-control"
min="0"
max="1000"
id="peertube-livechat-slow-mode-duration"
value="{{channelConfiguration.configuration.slowMode.duration}}"
/>
</label>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-4 col-xl-3">
<h2>{{botOptions}}</h2>
{{{helpButtonBot}}}
</div>
<div class="col-12 col-lg-8 col-xl-9">
<div class="form-group">
<label>
<input
type="checkbox"
name="bot"
id="peertube-livechat-bot"
value="1"
{{#channelConfiguration.configuration.bot.enabled}}
checked="checked"
{{/channelConfiguration.configuration.bot.enabled}}
/>
{{enableBot}}
</label>
</div>
<div class="form-group" livechat-configuration-channel-options-bot-enabled>
<label for="peertube-livechat-bot-nickname">{{botNickname}}</label>
<input
type="text"
name="bot_nickname"
class="form-control"
id="peertube-livechat-bot-nickname"
value="{{channelConfiguration.configuration.bot.nickname}}"
/>
</div>
</div>
</div>
<div class="row mt-5" livechat-configuration-channel-options-bot-enabled>
<div class="col-12 col-lg-4 col-xl-3">
<h2>{{forbiddenWords}} #{{displayNumber}}</h2>
{{#displayHelp}}
<p>{{forbiddenWordsDesc}} {{moreInfo}}</p>
{{{helpButtonForbiddenWords}}}
{{/displayHelp}}
</div>
<table class="col-12 col-lg-4 col-xl-3 forbidden_words_table">
<thead>
<tr>
<th scope="col">{{forbiddenWords}} <span class="form-group-description">{{forbiddenWordsDesc2}}</span></th>
<th scope="col">{{forbiddenWordsRegexp}} <span class="form-group-description">{{forbiddenWordsRegexpDesc}}</span></th>
<th scope="col">{{forbiddenWordsApplyToModerators}} <span class="form-group-description">{{forbiddenWordsApplyToModeratorsDesc}}</span></th>
<th scope="col">{{forbiddenWordsLabel}} <span class="form-group-description">{{forbiddenWordsLabelDesc}}</span></th>
<th scope="col">{{forbiddenWordsReason}} <span class="form-group-description">{{forbiddenWordsReasonDesc}}</span></th>
<th scope="col">{{forbiddenWordsComments}} <span class="form-group-description">{{forbiddenWordsCommentsDesc}}</span></th>
<th scope="col">Remove <span class="form-group-description">Remove Row</span></th>
</tr>
</thead>
{{#forbiddenWordsArray}}{{! iterating on forbiddenWordsArray to display N fields }}
<tbody>
<tr class="peertube-livechat-forbidden-words-row-{{fieldNumber}}">
<td>
{{! warning: don't add extra line break in textarea! }}
<textarea
name="forbidden_words_{{fieldNumber}}"
id="peertube-livechat-forbidden-words-{{fieldNumber}}"
class="form-control"
>{{joinedEntries}}</textarea>
</td>
<td>
<input
type="checkbox"
name="forbidden_words_regexp_{{fieldNumber}}"
value="1"
{{#regexp}}
checked="checked"
{{/regexp}}
/>
</td>
<td>
<input
type="checkbox"
name="forbidden_words_applytomoderators_{{fieldNumber}}"
value="1"
{{#applyToModerators}}
checked="checked"
{{/applyToModerators}}
/>
</td>
<td>
<input
type="text"
name="forbidden_words_label_{{fieldNumber}}"
class="form-control"
id="peertube-livechat-forbidden-words-label-{{fieldNumber}}"
value="{{label}}"
/>
</td>
<td>
<input
type="text"
name="forbidden_words_reason_{{fieldNumber}}"
class="form-control"
id="peertube-livechat-forbidden-words-reason-{{fieldNumber}}"
value="{{reason}}"
/>
</td>
<td>
{{! warning: don't add extra line break in textarea! }}
<textarea
name="forbidden_words_comments_{{fieldNumber}}"
id="peertube-livechat-forbidden-words-comments-{{fieldNumber}}"
class="form-control"
>{{comments}}</textarea>
</td>
<td>
<button type="button" class="btn btn-danger peertube-livechat-forbidden-words-{{fieldNumber}}-remove">x</button>
</td>
</tr>
{{/forbiddenWordsArray}}
<tr>
<button type="button" class="btn btn-success peertube-livechat-forbidden-words-add">+</button>
</tr>
</tbody>
</table>
</div>
{{#quotesArray}}{{! iterating on quotesArray to display N fields }}
<div class="row mt-5" livechat-configuration-channel-options-bot-enabled>
<div class="col-12 col-lg-4 col-xl-3">
<h2>{{quoteLabel}} #{{displayNumber}}</h2>
{{#displayHelp}}
<p>{{quoteDesc}} {{moreInfo}}</p>
{{{helpButtonQuotes}}}
{{/displayHelp}}
</div>
<div class="col-12 col-lg-8 col-xl-9">
<div class="form-group">
<label for="peertube-livechat-quote-{{fieldNumber}}">{{quoteLabel2}}</label>
{{! warning: don't add extra line break in textarea! }}
<textarea
name="quote_{{fieldNumber}}"
id="peertube-livechat-quote-{{fieldNumber}}"
class="form-control"
>{{joinedMessages}}</textarea>
<p class="form-group-description">{{quoteDesc2}}</p>
</div>
<div class="form-group">
<label for="peertube-livechat-quote-delay-{{fieldNumber}}">{{quoteDelayLabel}}</label>
<input
type="number"
min="1"
max="6000"
step="1"
name="quote_delay_{{fieldNumber}}"
class="form-control"
id="peertube-livechat-quote-delay-{{fieldNumber}}"
value="{{delay}}"
/>
<p class="form-group-description">{{quoteDelayDesc}}</p>
</div>
</div>
</div>
{{/quotesArray}}
{{#cmdsArray}}{{! iterating on cmdsArray to display N fields }}
<div class="row mt-5" livechat-configuration-channel-options-bot-enabled>
<div class="col-12 col-lg-4 col-xl-3">
<h2>{{commandLabel}} #{{displayNumber}}</h2>
{{#displayHelp}}
<p>{{commandDesc}} {{moreInfo}}</p>
{{{helpButtonCommands}}}
{{/displayHelp}}
</div>
<div class="col-12 col-lg-8 col-xl-9">
<div class="form-group">
<label for="peertube-livechat-command-{{fieldNumber}}">{{commandCmdLabel}}</label>
<input
type="text"
name="command_{{fieldNumber}}"
class="form-control"
id="peertube-livechat-command-{{fieldNumber}}"
value="{{command}}"
/>
<p class="form-group-description">{{commandCmdDesc}}</p>
</div>
<div class="form-group">
<label for="peertube-livechat-command-message-{{fieldNumber}}">{{commandMessageLabel}}</label>
<input
type="text"
name="command_message_{{fieldNumber}}"
class="form-control"
id="peertube-livechat-command-message-{{fieldNumber}}"
value="{{message}}"
/>
<p class="form-group-description">{{commandMessageDesc}}</p>
</div>
</div>
</div>
{{/cmdsArray}}
<div class="form-group mt-5">
<input type="submit" value="{{save}}" />
<input type="reset" value="{{cancel}}" />
</div>
</form>
</div>

View File

@ -1,246 +0,0 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterClientHelpers, RegisterClientOptions } from '@peertube/peertube-types/client'
import { localizedHelpUrl } from '../../../utils/help'
import { helpButtonSVG } from '../../../videowatch/buttons'
import { getConfigurationChannelViewData } from './logic/channel'
import { TemplateResult, html } from 'lit'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
// Must use require for mustache, import seems buggy.
const Mustache = require('mustache')
import './DynamicTableFormElement'
import './ChannelConfigurationElement'
import './PluginConfigurationRow'
import { ptTr } from './TranslationDirective'
/**
* Renders the configuration settings page for a given channel,
* and set it as innerHTML to rootEl.
* The page content can be empty. In such case, the notifier will be used to display a message.
* @param registerClientOptions Peertube client options
* @param channelId The channel id
* @param rootEl The HTMLElement in which insert the generated DOM.
*/
async function renderConfigurationChannel (
registerClientOptions: RegisterClientOptions,
channelId: string,
rootEl: HTMLElement
): Promise<TemplateResult> {
const peertubeHelpers = registerClientOptions.peertubeHelpers
try {
const view : {[key: string] : any} = await getConfigurationChannelViewData(registerClientOptions, channelId)
await fillViewHelpButtons(registerClientOptions, view)
await fillLabels(registerClientOptions, view)
//await vivifyConfigurationChannel(registerClientOptions, rootEl, channelId)
let tableHeader = {
words: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
},
regex: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
},
applyToModerators: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
},
label: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
},
reason: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
},
comments: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC)
}
}
let tableSchema = {
words: {
inputType: 'text',
default: 'helloqwesad'
},
regex: {
inputType: 'text',
default: 'helloaxzca'
},
applyToModerators: {
inputType: 'checkbox',
default: true
},
label: {
inputType: 'text',
default: 'helloasx'
},
reason: {
inputType: 'select',
default: 'transphobia',
label: 'choose your poison',
options: {'racism': 'Racism', 'sexism': 'Sexism', 'transphobia': 'Transphobia', 'bigotry': 'Bigotry'}
},
comments: {
inputType: 'textarea',
default: `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.`
},
}
let tableRows = [
{
words: 'teweqwst',
regex: 'tesdgst',
applyToModerators: false,
label: 'teswet',
reason: 'sexism',
comments: 'tsdaswest',
},
{
words: 'tedsadst',
regex: 'tezxccst',
applyToModerators: true,
label: 'tewest',
reason: 'racism',
comments: 'tesxzct',
},
{
words: 'tesadsdxst',
regex: 'dsfsdf',
applyToModerators: false,
label: 'tesdadst',
reason: 'bigotry',
comments: 'tsadest',
},
]
let helpLink = {
url : new URL(await localizedHelpUrl(registerClientOptions, { page: 'documentation/user/streamers/bot/forbidden_words' })),
title: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)
}
return html`
<div class="container">
<channel-configuration></channel-configuration>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
.helpLink=${helpLink}
>
<dynamic-table-form
.header=${tableHeader}
.schema=${tableSchema}
.rows=${tableRows}
.formName=${'forbidden-words'}
>
</dynamic-table-form>
</plugin-configuration-row>
</div>`
} catch (err: any) {
peertubeHelpers.notifier.error(err.toString())
return html``
}
}
async function fillViewHelpButtons (
registerClientOptions: RegisterClientOptions,
view: {[key: string]: string}
): Promise<void> {
const title = await registerClientOptions.peertubeHelpers.translate(LOC_ONLINE_HELP)
const button = async (page: string): Promise<string> => {
const helpUrl = await localizedHelpUrl(registerClientOptions, {
page
})
const helpIcon = helpButtonSVG()
return `<a
href="${helpUrl}"
target=_blank
title="${title}"
class="orange-button peertube-button-link"
>${helpIcon}</a>`
}
view.helpButton = await button('documentation/user/streamers/channel')
view.helpButtonBot = await button('documentation/user/streamers/bot')
view.helpButtonForbiddenWords = await button('documentation/user/streamers/bot/forbidden_words')
view.helpButtonQuotes = await button('documentation/user/streamers/bot/quotes')
view.helpButtonCommands = await button('documentation/user/streamers/bot/commands')
view.helpButtonSlowMode = await button('documentation/user/streamers/slow_mode')
}
async function fillLabels (
registerClientOptions: RegisterClientOptions,
view: {[key: string] : string}
): Promise<void> {
const peertubeHelpers = registerClientOptions.peertubeHelpers
view.title = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE)
view.description = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)
view.slowModeLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)
view.slowModeDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC)
view.enableBot = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL)
view.botOptions = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)
view.forbiddenWords = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)
view.forbiddenWordsDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)
view.forbiddenWordsDesc2 = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
view.forbiddenWordsReason = await peertubeHelpers.translate(
LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL
)
view.forbiddenWordsReasonDesc = await peertubeHelpers.translate(
LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC
)
view.forbiddenWordsRegexp = await peertubeHelpers.translate(
LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL
)
view.forbiddenWordsRegexpDesc = await peertubeHelpers.translate(
LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC
)
view.forbiddenWordsApplyToModerators = await peertubeHelpers.translate(
LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL
)
view.forbiddenWordsApplyToModeratorsDesc = await peertubeHelpers.translate(
LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC
)
view.forbiddenWordsComments = await peertubeHelpers.translate(
LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL
)
view.forbiddenWordsCommentsDesc = await peertubeHelpers.translate(
LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC
)
view.quoteLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)
view.quoteLabel2 = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL2)
view.quoteDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)
view.quoteDesc2 = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC2)
view.quoteDelayLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_LABEL)
view.quoteDelayDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC)
view.commandLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)
view.commandDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)
view.commandCmdLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_LABEL)
view.commandCmdDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_DESC)
view.commandMessageLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_LABEL)
view.commandMessageDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_DESC)
// view.bannedJIDs = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BANNED_JIDS_LABEL)
view.save = await peertubeHelpers.translate(LOC_SAVE)
view.cancel = await peertubeHelpers.translate(LOC_CANCEL)
view.botNickname = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME)
view.moreInfo = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO)
}
export {
renderConfigurationChannel
}

View File

@ -1,28 +0,0 @@
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-home">
<h1>
{{title}}
{{{helpButton}}}
</h1>
<p>{{description}}</p>
<p>{{please_select}}</p>
<ul class="peertube-plugin-livechat-configuration-home-channels">
{{#channels}}
<li>
<a href="{{livechatConfigurationUri}}">
{{#avatar}}
<img class="avatar channel" src="{{path}}">
{{/avatar}}
{{^avatar}}
<div class="avatar channel initial gray"></div>
{{/avatar}}
</a>
<div class="peertube-plugin-livechat-configuration-home-info">
<a href="{{livechatConfigurationUri}}">
<div>{{displayName}}</div>
<div>{{name}}</div>
</a>
</div>
</li>
{{/channels}}
</ul>
</div>

View File

@ -6,9 +6,7 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { localizedHelpUrl } from '../../../utils/help'
import { helpButtonSVG } from '../../../videowatch/buttons'
import { TemplateResult, html } from 'lit'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { ptTr } from './TranslationDirective'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
interface HomeViewData {
title: string
@ -89,7 +87,7 @@ async function _fillViewHelpButtons ( // TODO: refactor with the similar functio
return html`<a
href="${helpUrl}"
target=_blank
title="${ptTr(LOC_ONLINE_HELP)}"
title="${title}"
class="orange-button peertube-button-link"
>${unsafeHTML(helpIcon)}</a>`
}

View File

@ -1,432 +0,0 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import type { ChannelConfiguration, ChannelConfigurationOptions } from 'shared/lib/types'
import { getBaseRoute } from '../../../../utils/uri'
/**
* Returns the data that can be feed into the template view
* @param registerClientOptions
* @param channelId
*/
async function getConfigurationChannelViewData (
registerClientOptions: RegisterClientOptions,
channelId: string
): Promise<{[key: string] : any}> {
if (!channelId || !/^\d+$/.test(channelId)) {
throw new Error('Missing or invalid channel id.')
}
const { peertubeHelpers } = registerClientOptions
const response = await fetch(
getBaseRoute(registerClientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId),
{
method: 'GET',
headers: peertubeHelpers.getAuthHeader()
}
)
if (!response.ok) {
throw new Error('Can\'t get channel configuration options.')
}
const channelConfiguration: ChannelConfiguration = await (response).json()
// Basic testing that channelConfiguration has the correct format
if ((typeof channelConfiguration !== 'object') || !channelConfiguration.channel) {
throw new Error('Invalid channel configuration options.')
}
const forbiddenWordsArray: Object[] = []
for (let i = 0; i < channelConfiguration.configuration.bot.forbiddenWords.length; i++) {
const fw = channelConfiguration.configuration.bot.forbiddenWords[i]
forbiddenWordsArray.push({
displayNumber: i + 1,
fieldNumber: i,
displayHelp: i === 0,
joinedEntries: fw.entries.join('\n'),
regexp: !!fw.regexp,
applyToModerators: fw.applyToModerators,
label:fw.label,
reason: fw.reason,
comments: fw.comments
})
}
// Ensuring we have at least N blocks:
while (forbiddenWordsArray.length < 1) {
const i = forbiddenWordsArray.length
// default value
forbiddenWordsArray.push({
displayNumber: i + 1,
fieldNumber: i,
displayHelp: i === 0,
joinedEntries: '',
regexp: false,
applyToModerators: false,
label:'',
reason: '',
comments: ''
})
continue
}
const quotesArray: Object[] = []
for (let i = 0; i < channelConfiguration.configuration.bot.quotes.length; i++) {
const qs = channelConfiguration.configuration.bot.quotes[i]
quotesArray.push({
displayNumber: i + 1,
fieldNumber: i,
displayHelp: i === 0,
joinedMessages: qs.messages.join('\n'),
delay: Math.round(qs.delay / 60) // converting to minutes
})
}
// Ensuring we have at least N blocks:
while (quotesArray.length < 1) {
const i = quotesArray.length
// default value
quotesArray.push({
displayNumber: i + 1,
fieldNumber: i,
displayHelp: i === 0,
joinedMessages: '',
delay: 5
})
continue
}
const cmdsArray: Object[] = []
for (let i = 0; i < channelConfiguration.configuration.bot.commands.length; i++) {
const cs = channelConfiguration.configuration.bot.commands[i]
cmdsArray.push({
displayNumber: i + 1,
fieldNumber: i,
displayHelp: i === 0,
message: cs.message,
command: cs.command
})
}
// Ensuring we have at least N blocks:
while (cmdsArray.length < 1) {
const i = cmdsArray.length
// default value
cmdsArray.push({
displayNumber: i + 1,
fieldNumber: i,
displayHelp: i === 0,
message: '',
command: ''
})
continue
}
return {
channelConfiguration,
forbiddenWordsArray,
quotesArray,
cmdsArray
}
}
/**
* Adds the front-end logic on the generated html for the channel configuration options.
* @param clientOptions Peertube client options
* @param rootEl The root element in which the template was rendered
*/
async function vivifyConfigurationChannel (
clientOptions: RegisterClientOptions,
rootEl: HTMLElement,
channelId: string
): Promise<void> {
const form = rootEl.querySelector('form[livechat-configuration-channel-options]') as HTMLFormElement
if (!form) { return }
const translate = clientOptions.peertubeHelpers.translate
const labelSaved = await translate(LOC_SUCCESSFULLY_SAVED)
const labelError = await translate(LOC_ERROR)
const enableBotCB = form.querySelector('input[name=bot]') as HTMLInputElement
const botEnabledEl = form.querySelectorAll('[livechat-configuration-channel-options-bot-enabled]')
const dataClasses = ['forbidden-words', 'command', 'quote']
type ChannelConfigClass = (typeof dataClasses)[number]
type ChannelRowData = Record<ChannelConfigClass,{ rows: HTMLTableRowElement[], addButton: HTMLButtonElement, removeButtons: HTMLButtonElement[]}>
const populateRowData: Function = () => {
let modifiers : ChannelRowData = {};
for (let dataClass in dataClasses) {
let rows : HTMLTableRowElement[] = [];
let removeButtons : HTMLButtonElement[] = [];
for (let i = 0, row : HTMLTableRowElement; row = form.querySelector(`button.peertube-livechat-${dataClass}-${i}-row`) as HTMLTableRowElement; i++) {
rows.push(row)
}
for (let i = 0, button : HTMLButtonElement; button = form.querySelector(`button.peertube-livechat-${dataClass}-${i}-remove`) as HTMLButtonElement; i++) {
removeButtons.push(button)
}
modifiers[dataClass] = {
rows,
addButton: form.querySelector(`button.peertube-livechat-${dataClass}-add`) as HTMLButtonElement,
removeButtons
}
}
return modifiers
}
let rowDataRecords : ChannelRowData = populateRowData();
function removeRow(dataClass: ChannelConfigClass, index: number): any {
let {rows} = rowDataRecords[dataClass]
let rowToDelete = rows.splice(index,1)[0]
rowToDelete
for (let i = index, row : HTMLTableRowElement; row = form.querySelector(`button.peertube-livechat-${dataClass}-${i}-row`) as HTMLTableRowElement; i++) {
rows.push(row)
}
}
function addRow(dataClass: ChannelConfigClass): any {
throw new Error('Function not implemented.')
}
const refresh: Function = () => {
botEnabledEl.forEach(el => {
if (enableBotCB.checked) {
(el as HTMLElement).style.removeProperty('display')
} else {
(el as HTMLElement).style.display = 'none'
}
})
}
const removeDisplayedErrors = (): void => {
form.querySelectorAll('.form-error').forEach(el => el.remove())
}
const displayError = async (fieldSelector: string, message: string): Promise<void> => {
form.querySelectorAll(fieldSelector).forEach(el => {
const erEl = document.createElement('div')
erEl.classList.add('form-error')
erEl.textContent = message
el.after(erEl)
})
}
const validateData: Function = async (channelConfigurationOptions: ChannelConfigurationOptions): Promise<boolean> => {
const botConf = channelConfigurationOptions.bot
const slowModeDuration = channelConfigurationOptions.slowMode.duration
const errorFieldSelectors = []
if (
(typeof slowModeDuration !== 'number') ||
isNaN(slowModeDuration) ||
slowModeDuration < 0 ||
slowModeDuration > 1000
) {
const selector = '#peertube-livechat-slow-mode-duration'
errorFieldSelectors.push(selector)
await displayError(selector, await translate(LOC_INVALID_VALUE))
}
// If !bot.enabled, we don't have to validate these fields:
// The backend will ignore those values.
if (botConf.enabled) {
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
const selector = '#peertube-livechat-bot-nickname'
errorFieldSelectors.push(selector)
await displayError(selector, await translate(LOC_INVALID_VALUE))
}
for (let iFw = 0; iFw < botConf.forbiddenWords.length; iFw++) {
const fw = botConf.forbiddenWords[iFw]
if (fw.regexp) {
for (const v of fw.entries) {
if (v === '' || /^\s+$/.test(v)) { continue }
try {
// eslint-disable-next-line no-new
new RegExp(v)
} catch (err) {
const selector = '#peertube-livechat-forbidden-words-' + iFw.toString()
errorFieldSelectors.push(selector)
let message = await translate(LOC_INVALID_VALUE)
message += ` "${v}": ${err as string}`
await displayError(selector, message)
}
}
}
}
for (let iQt = 0; iQt < botConf.quotes.length; iQt++) {
const qt = botConf.quotes[iQt]
if (qt.messages.some(/\s+/.test)) {
const selector = '#peertube-livechat-quote-' + iQt.toString()
errorFieldSelectors.push(selector)
const message = await translate(LOC_INVALID_VALUE)
await displayError(selector, message)
}
}
for (let iCd = 0; iCd < botConf.commands.length; iCd++) {
const cd = botConf.commands[iCd]
if (/\s+/.test(cd.command)) {
const selector = '#peertube-livechat-command-' + iCd.toString()
errorFieldSelectors.push(selector)
const message = await translate(LOC_INVALID_VALUE)
await displayError(selector, message)
}
}
}
if (errorFieldSelectors.length) {
// Set the focus to the first in-error field:
const el: HTMLInputElement | HTMLTextAreaElement | null = document.querySelector(errorFieldSelectors[0])
el?.focus()
return false
}
return true
}
const submitForm: Function = async () => {
const data = new FormData(form)
removeDisplayedErrors()
const channelConfigurationOptions: ChannelConfigurationOptions = {
slowMode: {
duration: parseInt(data.get('slow_mode_duration')?.toString() ?? '0')
},
bot: {
enabled: data.get('bot') === '1',
nickname: data.get('bot_nickname')?.toString() ?? '',
// TODO bannedJIDs
forbiddenWords: [],
quotes: [],
commands: []
}
}
// Note: but data in order, because validateData assume index are okay to find associated fields.
for (let i = 0; data.has('forbidden_words_' + i.toString()); i++) {
const entries = (data.get('forbidden_words_' + i.toString())?.toString() ?? '')
.split(/\r?\n|\r|\n/g)
.filter(s => !/^\s*$/.test(s)) // filtering empty lines
const regexp = data.get('forbidden_words_regexp_' + i.toString())
const applyToModerators = data.get('forbidden_words_applytomoderators_' + i.toString())
const label = data.get('forbidden_words_label_' + i.toString())?.toString()
const reason = data.get('forbidden_words_reason_' + i.toString())?.toString()
const comments = data.get('forbidden_words_comments_' + i.toString())?.toString()
const fw: ChannelConfigurationOptions['bot']['forbiddenWords'][0] = {
entries,
applyToModerators: !!applyToModerators,
regexp: !!regexp
}
if (label) {
fw.label = label
}
if (reason) {
fw.reason = reason
}
if (comments) {
fw.comments = comments
}
channelConfigurationOptions.bot.forbiddenWords.push(fw)
}
// Note: but data in order, because validateData assume index are okay to find associated fields.
for (let i = 0; data.has('quote_' + i.toString()); i++) {
const messages = (data.get('quote_' + i.toString())?.toString() ?? '')
.split(/\r?\n|\r|\n/g)
.filter(s => !/^\s*$/.test(s)) // filtering empty lines
let delay = parseInt(data.get('quote_delay_' + i.toString())?.toString() ?? '')
if (!delay || isNaN(delay) || delay < 1) {
delay = 5
}
delay = delay * 60 // converting to seconds
const q: ChannelConfigurationOptions['bot']['quotes'][0] = {
messages,
delay
}
channelConfigurationOptions.bot.quotes.push(q)
}
// Note: but data in order, because validateData assume index are okay to find associated fields.
for (let i = 0; data.has('command_' + i.toString()); i++) {
const command = (data.get('command_' + i.toString())?.toString() ?? '')
const message = (data.get('command_message_' + i.toString())?.toString() ?? '')
const c: ChannelConfigurationOptions['bot']['commands'][0] = {
command,
message
}
channelConfigurationOptions.bot.commands.push(c)
}
if (!await validateData(channelConfigurationOptions)) {
throw new Error('Invalid form data')
}
const headers: any = clientOptions.peertubeHelpers.getAuthHeader() ?? {}
headers['content-type'] = 'application/json;charset=UTF-8'
const response = await fetch(
getBaseRoute(clientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId),
{
method: 'POST',
headers,
body: JSON.stringify(channelConfigurationOptions)
}
)
if (!response.ok) {
throw new Error('Failed to save configuration options.')
}
}
const toggleSubmit: Function = (disabled: boolean) => {
form.querySelectorAll('input[type=submit], input[type=reset]').forEach((el) => {
if (disabled) {
el.setAttribute('disabled', 'disabled')
} else {
el.removeAttribute('disabled')
}
})
}
enableBotCB.onclick = () => refresh()
for(let [dataClass, rowData] of Object.entries(rowDataRecords)) {
rowData.addButton.onclick = () => addRow(dataClass)
for (let i = 0; i < rowData.removeButtons.length; i++) {
rowData.removeButtons[i].onclick = () => removeRow(dataClass, i)
}
}
form.onsubmit = () => {
toggleSubmit(true)
if (!form.checkValidity()) {
return false
}
submitForm().then(
() => {
clientOptions.peertubeHelpers.notifier.success(labelSaved)
toggleSubmit(false)
},
() => {
clientOptions.peertubeHelpers.notifier.error(labelError)
toggleSubmit(false)
}
)
return false
}
form.onreset = () => {
// Must refresh in a setTimeout, otherwise the checkbox state is not up to date.
setTimeout(() => refresh(), 1)
}
refresh()
}
export {
getConfigurationChannelViewData,
vivifyConfigurationChannel
}

View File

@ -0,0 +1,21 @@
let globalSheets: CSSStyleSheet[] | undefined = undefined;
export function getGlobalStyleSheets() {
if (globalSheets === undefined) {
globalSheets = Array.from(document.styleSheets)
.map(x => {
const sheet = new CSSStyleSheet();
const css = Array.from(x.cssRules).map(rule => rule.cssText).join(' ');
sheet.replaceSync(css);
return sheet;
});
}
return globalSheets;
}
export function addGlobalStylesToShadowRoot(shadowRoot: ShadowRoot) {
shadowRoot.adoptedStyleSheets.push(
...getGlobalStyleSheets()
);
}