Technically working options. Need CSS fix (plugin CSS not loaded) and data validation
This commit is contained in:
parent
9ea26dfd26
commit
687c4742f7
1
assets/images/plus-square.svg
Normal file
1
assets/images/plus-square.svg
Normal 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 |
1
assets/images/x-square.svg
Normal file
1
assets/images/x-square.svg
Normal 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 |
@ -39,23 +39,10 @@ function loadLocs() {
|
|||||||
return r
|
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({
|
const define = Object.assign({
|
||||||
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
||||||
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
||||||
}, loadLocs(), loadMustaches())
|
}, loadLocs())
|
||||||
|
|
||||||
const configs = clientFiles.map(f => ({
|
const configs = clientFiles.map(f => ({
|
||||||
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
||||||
|
@ -29,7 +29,8 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
|
|||||||
onMount: async ({ rootEl }) => {
|
onMount: async ({ rootEl }) => {
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const channelId = urlParams.get('channelId') ?? ''
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,33 +1,53 @@
|
|||||||
import { RegisterClientOptions } from '@peertube/peertube-types/client'
|
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 { 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 { ptTr } from './TranslationDirective'
|
||||||
import { localizedHelpUrl } from '../../../utils/help'
|
import { localizedHelpUrl } from '../../../utils/help'
|
||||||
import './DynamicTableFormElement'
|
import './DynamicTableFormElement'
|
||||||
import './PluginConfigurationRow'
|
import './PluginConfigurationRow'
|
||||||
|
import './HelpButtonElement'
|
||||||
import { until } from 'async'
|
import { until } from 'async'
|
||||||
import { Task } from '@lit/task';
|
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')
|
@customElement('channel-configuration')
|
||||||
export class ChannelConfigurationElement extends LitElement {
|
export class ChannelConfigurationElement extends LitElement {
|
||||||
|
|
||||||
|
@provide({ context: registerClientOptionsContext })
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public registerClientOptions: RegisterClientOptions | undefined
|
public registerClientOptions: RegisterClientOptions | undefined
|
||||||
|
|
||||||
createRenderRoot = () => {
|
@property({ attribute: false })
|
||||||
return this
|
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, {
|
private _asyncTaskRender = new Task(this, {
|
||||||
|
|
||||||
task: async ([registerClientOptions], {signal}) => {
|
task: async ([registerClientOptions], {signal}) => {
|
||||||
let link = registerClientOptions ? await localizedHelpUrl(registerClientOptions, { page: 'documentation/user/streamers/bot/forbidden_words' }) : '';
|
if (this.registerClientOptions) {
|
||||||
|
this._configurationService = new ChannelConfigurationService(this.registerClientOptions)
|
||||||
return {
|
this._channelConfiguration = await this._configurationService.fetchConfiguration(this.channelId ?? 0)
|
||||||
url : new URL(link),
|
|
||||||
title: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -35,112 +55,278 @@ 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 = () => {
|
render = () => {
|
||||||
let tableHeader = {
|
let tableHeaderList = {
|
||||||
words: {
|
forbiddenWords: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
|
entries: {
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
|
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)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
regex: {
|
quotes: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
|
messages: {
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
|
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)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
applyToModerators: {
|
commands: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
|
command: {
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_LABEL),
|
||||||
},
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_DESC)
|
||||||
label: {
|
},
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
|
message: {
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_LABEL),
|
||||||
},
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_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 = {
|
let tableSchema = {
|
||||||
words: {
|
forbiddenWords: {
|
||||||
inputType: 'text',
|
entries: {
|
||||||
default: 'helloqwesad'
|
inputType: 'textarea',
|
||||||
|
default: ['helloqwesad'],
|
||||||
|
separator: '\n',
|
||||||
|
},
|
||||||
|
regex: {
|
||||||
|
inputType: 'text',
|
||||||
|
default: 'helloaxzca',
|
||||||
|
},
|
||||||
|
applyToModerators: {
|
||||||
|
inputType: 'checkbox',
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
inputType: 'text',
|
||||||
|
default: 'helloasx'
|
||||||
|
},
|
||||||
|
reason: {
|
||||||
|
inputType: 'text',
|
||||||
|
default: 'transphobia',
|
||||||
|
datalist: ['Racism', 'Sexism', 'Transphobia', '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.`
|
||||||
|
},
|
||||||
},
|
},
|
||||||
regex: {
|
quotes: {
|
||||||
inputType: 'text',
|
messages: {
|
||||||
default: 'helloaxzca'
|
inputType: 'textarea',
|
||||||
},
|
default: ['default message'],
|
||||||
applyToModerators: {
|
separator: '\n',
|
||||||
inputType: 'checkbox',
|
},
|
||||||
default: true
|
delay: {
|
||||||
},
|
inputType: 'number',
|
||||||
label: {
|
default: 100,
|
||||||
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.`
|
|
||||||
},
|
},
|
||||||
|
commands: {
|
||||||
|
command: {
|
||||||
|
inputType: 'text',
|
||||||
|
default: 'default command',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
inputType: 'text',
|
||||||
|
default: 'default message',
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return this._asyncTaskRender.render({
|
return this._asyncTaskRender.render({
|
||||||
complete: (helpLink) => html`
|
complete: () => html`
|
||||||
<div class="container">
|
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel">
|
||||||
<channel-configuration></channel-configuration>
|
<h1>
|
||||||
<plugin-configuration-row
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE)}:
|
||||||
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
<span class="peertube-plugin-livechat-configuration-channel-info">
|
||||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
<span>${this._channelConfiguration?.channel.displayName}</span>
|
||||||
.helpLink=${helpLink}
|
<span>${this._channelConfiguration?.channel.name}</span>
|
||||||
>
|
</span>
|
||||||
<dynamic-table-form
|
<help-button .page="documentation/user/streamers/channel">
|
||||||
.header=${tableHeader}
|
</help-button>
|
||||||
.schema=${tableSchema}
|
</h1>
|
||||||
.rows=${tableRows}
|
<p>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}</p>
|
||||||
.formName=${'forbidden-words'}
|
<form livechat-configuration-channel-options role="form">
|
||||||
>
|
<div class="row mt-3">
|
||||||
</dynamic-table-form>
|
<plugin-configuration-row
|
||||||
</plugin-configuration-row>
|
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
|
||||||
</div>`
|
.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)}
|
||||||
|
.helpPage=${"documentation/user/streamers/bot/forbidden_words"}>
|
||||||
|
<dynamic-table-form
|
||||||
|
.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)}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +1,46 @@
|
|||||||
import { html, LitElement, TemplateResult } from 'lit'
|
import { css, html, LitElement, nothing, TemplateResult } from 'lit'
|
||||||
import { repeat } from 'lit/directives/repeat.js'
|
import { repeat } from 'lit/directives/repeat.js'
|
||||||
import { customElement, property, state } from 'lit/decorators.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 { 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'
|
type DynamicTableAcceptedInputTypes = 'textarea'
|
||||||
| 'select'
|
| 'select'
|
||||||
| 'checkbox'
|
| 'checkbox'
|
||||||
| 'range'
|
| 'range'
|
||||||
| 'color'
|
| 'color'
|
||||||
| 'date'
|
| 'date'
|
||||||
| 'datetime'
|
| 'datetime'
|
||||||
| 'datetime-local'
|
| 'datetime-local'
|
||||||
| 'email'
|
| 'email'
|
||||||
| 'file'
|
| 'file'
|
||||||
| 'image'
|
| 'image'
|
||||||
| 'month'
|
| 'month'
|
||||||
| 'number'
|
| 'number'
|
||||||
| 'password'
|
| 'password'
|
||||||
| 'tel'
|
| 'tel'
|
||||||
| 'text'
|
| 'text'
|
||||||
| 'time'
|
| 'time'
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'week'
|
| 'week'
|
||||||
|
|
||||||
|
|
||||||
interface CellDataSchema {
|
interface CellDataSchema {
|
||||||
@ -36,6 +51,8 @@ interface CellDataSchema {
|
|||||||
size?: number
|
size?: number
|
||||||
label?: string
|
label?: string
|
||||||
options?: { [key: string]: string }
|
options?: { [key: string]: string }
|
||||||
|
datalist?: DynamicTableAcceptedTypes[]
|
||||||
|
separator?: string
|
||||||
inputType?: DynamicTableAcceptedInputTypes
|
inputType?: DynamicTableAcceptedInputTypes
|
||||||
default?: DynamicTableAcceptedTypes
|
default?: DynamicTableAcceptedTypes
|
||||||
}
|
}
|
||||||
@ -44,55 +61,100 @@ interface CellDataSchema {
|
|||||||
export class DynamicTableFormElement extends LitElement {
|
export class DynamicTableFormElement extends LitElement {
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public header: { [key : string]: { colName: TemplateResult, description: TemplateResult } } = {}
|
public header: { [key: string]: { colName: TemplateResult, description: TemplateResult } } = {}
|
||||||
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public schema: { [key : string]: CellDataSchema } = {}
|
public schema: { [key: string]: CellDataSchema } = {}
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public rows: { [key: string]: DynamicTableAcceptedTypes }[] = []
|
||||||
|
|
||||||
@property({ reflect: true })
|
@state()
|
||||||
public rows: { _id: number; [key : string]: DynamicTableAcceptedTypes }[] = []
|
public _rowsById: { _id: number; row: { [key: string]: DynamicTableAcceptedTypes } }[] = []
|
||||||
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public formName: string = ''
|
public formName: string = ''
|
||||||
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _lastRowId = 1
|
private _lastRowId = 1
|
||||||
|
|
||||||
createRenderRoot = () => {
|
@property({ attribute: false })
|
||||||
return this
|
private _colOrder: string[] = []
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
...getGlobalStyleSheets(),
|
||||||
|
css`
|
||||||
|
:host table {
|
||||||
|
table-layout: fixed;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 = () => {
|
private _getDefaultRow = () : { [key: string]: DynamicTableAcceptedTypes } => {
|
||||||
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? '']), ['_id', this._lastRowId++]])
|
this._updateLastRowId()
|
||||||
|
|
||||||
|
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addRow = () => {
|
private _addRow = () => {
|
||||||
this.rows.push(this._getDefaultRow())
|
let newRow = this._getDefaultRow()
|
||||||
|
this._rowsById.push({_id:this._lastRowId++, row: newRow})
|
||||||
this.requestUpdate('rows')
|
this.rows.push(newRow)
|
||||||
|
this.requestUpdate('rows')
|
||||||
|
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private _removeRow = (rowId: number) => {
|
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.requestUpdate('rows')
|
this.rows = this.rows.filter((row) => row !== rowToRemove)
|
||||||
|
this.requestUpdate('rows')
|
||||||
|
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
const inputId = `peertube-livechat-${this.formName.replaceAll('_','-')}-table`
|
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-table`
|
||||||
|
|
||||||
for(let row of this.rows) {
|
this._updateLastRowId()
|
||||||
if (!row._id) {
|
|
||||||
row._id = this._lastRowId++
|
this._rowsById.filter(rowById => this.rows.includes(rowById.row))
|
||||||
|
|
||||||
|
for (let row of this.rows) {
|
||||||
|
if (this._rowsById.filter(rowById => rowById.row === row).length == 0) {
|
||||||
|
this._rowsById.push({_id: this._lastRowId++, row })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,52 +162,63 @@ export class DynamicTableFormElement extends LitElement {
|
|||||||
<table class="table table-striped table-hover table-sm" id=${inputId}>
|
<table class="table table-striped table-hover table-sm" id=${inputId}>
|
||||||
${this._renderHeader()}
|
${this._renderHeader()}
|
||||||
<tbody>
|
<tbody>
|
||||||
${repeat(this.rows,(row) => row._id, this._renderDataRow)}
|
${repeat(this._rowsById, (rowById) => rowById._id, this._renderDataRow)}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
${this._renderFooter()}
|
||||||
<tr><td><button @click=${this._addRow}>Add Row</button></td></tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
`
|
`
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderHeader = () => {
|
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>
|
return html`<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<!-- <th scope="col">#</th> -->
|
${Object.entries(this.header).sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2))
|
||||||
${Object.values(this.header).map(this._renderHeaderCell)}
|
.map(([k,v]) => this._renderHeaderCell(v))}
|
||||||
<th scope="col">Remove Row</th>
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>`
|
</thead>`
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderHeaderCell = (headerCellData: { colName: TemplateResult, description: TemplateResult }) => {
|
private _renderHeaderCell = (headerCellData: { colName: TemplateResult, description: TemplateResult }) => {
|
||||||
return html`<th scope="col">
|
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>`
|
</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}`
|
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-row-${rowData._id}`
|
||||||
|
|
||||||
return html`<tr id=${inputId}>
|
return html`<tr id=${inputId}>
|
||||||
<!-- <td class="form-group">${rowData._id}</td> -->
|
${Object.entries(rowData.row).filter(([k, v]) => k != '_id')
|
||||||
${Object.entries(rowData).filter(([k,v]) => k != '_id').map((data) => this.renderDataCell(data, rowData._id))}
|
.sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2))
|
||||||
<td class="form-group"><button @click=${() => this._removeRow(rowData._id)}>Remove</button></td>
|
.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>`
|
</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) => {
|
renderDataCell = (property: [string, DynamicTableAcceptedTypes], rowId: number) => {
|
||||||
const [propertyName, propertyValue] = property
|
const [propertyName, propertyValue] = property
|
||||||
const propertySchema = this.schema[propertyName] ?? {}
|
const propertySchema = this.schema[propertyName] ?? {}
|
||||||
|
|
||||||
let formElement
|
let formElement
|
||||||
|
|
||||||
const inputName = `${this.formName.replaceAll('-','_')}_${propertyName.toString().replaceAll('-','_')}_${rowId}`
|
const inputName = `${this.formName.replaceAll('-', '_')}_${propertyName.toString().replaceAll('-', '_')}_${rowId}`
|
||||||
const inputId = `peertube-livechat-${this.formName.replaceAll('_','-')}-${propertyName.toString().replaceAll('_','-')}-${rowId}`
|
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-${propertyName.toString().replaceAll('_', '-')}-${rowId}`
|
||||||
|
|
||||||
switch (propertyValue.constructor) {
|
switch (propertyValue.constructor) {
|
||||||
case String:
|
case String:
|
||||||
@ -169,76 +242,32 @@ export class DynamicTableFormElement extends LitElement {
|
|||||||
case 'time':
|
case 'time':
|
||||||
case 'url':
|
case 'url':
|
||||||
case 'week':
|
case 'week':
|
||||||
formElement = html`<input
|
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
|
||||||
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}
|
|
||||||
/>`
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
formElement = html`<textarea
|
formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
|
||||||
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>`
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'select':
|
case 'select':
|
||||||
formElement = html`<select
|
formElement = this._renderSelect(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
|
||||||
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>`
|
|
||||||
break
|
break
|
||||||
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case Date:
|
case Date:
|
||||||
switch (propertySchema.inputType) {
|
switch (propertySchema.inputType) {
|
||||||
case undefined:
|
case undefined:
|
||||||
propertySchema.inputType = 'datetime'
|
propertySchema.inputType = 'datetime'
|
||||||
|
|
||||||
case 'date':
|
case 'date':
|
||||||
case 'datetime':
|
case 'datetime':
|
||||||
case 'datetime-local':
|
case 'datetime-local':
|
||||||
case 'time':
|
case 'time':
|
||||||
formElement = html`<input
|
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, (propertyValue as Date).toISOString())
|
||||||
type=${propertySchema.inputType}
|
break
|
||||||
name=${inputName}
|
}
|
||||||
class="form-control"
|
break
|
||||||
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()}
|
|
||||||
/>`
|
|
||||||
break
|
|
||||||
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case Number:
|
case Number:
|
||||||
switch (propertySchema.inputType) {
|
switch (propertySchema.inputType) {
|
||||||
@ -247,20 +276,8 @@ export class DynamicTableFormElement extends LitElement {
|
|||||||
|
|
||||||
case 'number':
|
case 'number':
|
||||||
case 'range':
|
case 'range':
|
||||||
formElement = html`<input
|
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
|
||||||
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}
|
|
||||||
/>`
|
|
||||||
break
|
break
|
||||||
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -270,44 +287,130 @@ export class DynamicTableFormElement extends LitElement {
|
|||||||
propertySchema.inputType = 'checkbox'
|
propertySchema.inputType = 'checkbox'
|
||||||
|
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
formElement = html`<input
|
formElement = this._renderCheckbox(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as boolean)
|
||||||
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}
|
|
||||||
/>`
|
|
||||||
break
|
break
|
||||||
|
|
||||||
}
|
}
|
||||||
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) {
|
if (!formElement) {
|
||||||
console.warn(`value type '${propertyValue.constructor}' is incompatible`
|
console.warn(`value type '${propertyValue.constructor}' is incompatible`
|
||||||
+ `with field type '${propertySchema.inputType}' for form entry '${propertyName.toString()}'.`)
|
+ `with field type '${propertySchema.inputType}' for form entry '${propertyName.toString()}'.`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<td class="form-group">${formElement}</td>`
|
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 target = event?.target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)
|
||||||
let value = (target && target instanceof HTMLInputElement && target.type == "checkbox") ? !!(target?.checked) : target?.value
|
let value = (target && target instanceof HTMLInputElement && target.type == "checkbox") ? !!(target?.checked) : target?.value
|
||||||
|
|
||||||
if(value !== undefined) {
|
if (value !== undefined) {
|
||||||
for(let row of this.rows) {
|
for (let rowById of this._rowsById) {
|
||||||
if(row._id === rowId) {
|
if (rowById._id === rowId) {
|
||||||
row[propertyName] = value
|
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('rows')
|
||||||
|
this.requestUpdate('rowsById')
|
||||||
|
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
50
client/common/configuration/templates/HelpButtonElement.ts
Normal file
50
client/common/configuration/templates/HelpButtonElement.ts
Normal 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>`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,10 @@
|
|||||||
import { html, LitElement } from 'lit'
|
import { css, html, LitElement } from 'lit'
|
||||||
import { customElement, property } from 'lit/decorators.js'
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
|
import './HelpButtonElement'
|
||||||
import { StaticValue } from 'lit/static-html.js'
|
import { getGlobalStyleSheets } from '../../global-styles'
|
||||||
import { helpButtonSVG } from '../../../videowatch/buttons'
|
|
||||||
|
|
||||||
@customElement('plugin-configuration-row')
|
@customElement('plugin-configuration-row')
|
||||||
export class PLuginConfigurationRow extends LitElement {
|
export class PluginConfigurationRow extends LitElement {
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public title: string = `title`
|
public title: string = `title`
|
||||||
@ -14,24 +13,20 @@ export class PLuginConfigurationRow extends LitElement {
|
|||||||
public description: string = `Here's a description`
|
public description: string = `Here's a description`
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public helpLink: { url: URL, title: string } = { url : new URL('https://lmddgtfy.net/'), title: 'Online Help'}
|
public helpPage: string = 'documentation'
|
||||||
|
|
||||||
createRenderRoot = () => {
|
static styles = [
|
||||||
return this
|
...getGlobalStyleSheets()
|
||||||
}
|
];
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col-12 col-lg-4 col-xl-3">
|
<div class="col-12 col-lg-4 col-xl-3">
|
||||||
<h2>${this.title}</h2>
|
<h2>${this.title}</h2>
|
||||||
<p>${this.description}</p>
|
<p>${this.description}</p>
|
||||||
<a
|
<help-button .page=${this.helpPage}>
|
||||||
href="${this.helpLink.url.href}"
|
</help-button>
|
||||||
target=_blank
|
|
||||||
title="${this.helpLink.title}"
|
|
||||||
class="orange-button peertube-button-link"
|
|
||||||
>${unsafeSVG(helpButtonSVG())}</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-8 col-xl-9">
|
<div class="col-12 col-lg-8 col-xl-9">
|
||||||
<slot><p>Nothing in this row.</p></slot>
|
<slot><p>Nothing in this row.</p></slot>
|
||||||
|
@ -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 { AsyncDirective } from 'lit/async-directive.js'
|
||||||
import { RegisterClientHelpers } from '@peertube/peertube-types/client';
|
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 {
|
export class TranslationDirective extends AsyncDirective {
|
||||||
|
|
||||||
@ -9,22 +12,35 @@ export class TranslationDirective extends AsyncDirective {
|
|||||||
private _translatedValue : string = ''
|
private _translatedValue : string = ''
|
||||||
private _localizationId : string = ''
|
private _localizationId : string = ''
|
||||||
|
|
||||||
|
private _allowUnsafeHTML = false
|
||||||
|
|
||||||
constructor(partInfo: PartInfo) {
|
constructor(partInfo: PartInfo) {
|
||||||
super(partInfo);
|
super(partInfo);
|
||||||
|
|
||||||
//_peertubeOptionsPromise.then((options) => this._peertubeHelpers = options.peertubeHelpers)
|
//_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._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
||||||
|
|
||||||
|
this._allowUnsafeHTML = allowHTML
|
||||||
|
|
||||||
if (this._translatedValue === '') {
|
if (this._translatedValue === '') {
|
||||||
this._translatedValue = locId
|
this._translatedValue = locId
|
||||||
}
|
}
|
||||||
|
|
||||||
this._asyncUpdateTranslation()
|
this._asyncUpdateTranslation()
|
||||||
|
|
||||||
return this._translatedValue
|
return this._internalRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
_internalRender = () => {
|
||||||
|
return this._allowUnsafeHTML ? html`${unsafeHTML(this._translatedValue)}` : this._translatedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
_asyncUpdateTranslation = async () => {
|
_asyncUpdateTranslation = async () => {
|
||||||
@ -32,7 +48,7 @@ export class TranslationDirective extends AsyncDirective {
|
|||||||
|
|
||||||
if (newValue !== '' && newValue !== this._translatedValue) {
|
if (newValue !== '' && newValue !== this._translatedValue) {
|
||||||
this._translatedValue = newValue
|
this._translatedValue = newValue
|
||||||
this.setValue(newValue)
|
this.setValue(this._internalRender())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
@ -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
|
|
||||||
}
|
|
@ -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>
|
|
@ -6,9 +6,7 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
|||||||
import { localizedHelpUrl } from '../../../utils/help'
|
import { localizedHelpUrl } from '../../../utils/help'
|
||||||
import { helpButtonSVG } from '../../../videowatch/buttons'
|
import { helpButtonSVG } from '../../../videowatch/buttons'
|
||||||
import { TemplateResult, html } from 'lit'
|
import { TemplateResult, html } from 'lit'
|
||||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||||
import { ptTr } from './TranslationDirective'
|
|
||||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
||||||
|
|
||||||
interface HomeViewData {
|
interface HomeViewData {
|
||||||
title: string
|
title: string
|
||||||
@ -89,7 +87,7 @@ async function _fillViewHelpButtons ( // TODO: refactor with the similar functio
|
|||||||
return html`<a
|
return html`<a
|
||||||
href="${helpUrl}"
|
href="${helpUrl}"
|
||||||
target=_blank
|
target=_blank
|
||||||
title="${ptTr(LOC_ONLINE_HELP)}"
|
title="${title}"
|
||||||
class="orange-button peertube-button-link"
|
class="orange-button peertube-button-link"
|
||||||
>${unsafeHTML(helpIcon)}</a>`
|
>${unsafeHTML(helpIcon)}</a>`
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
21
client/common/global-styles.ts
Normal file
21
client/common/global-styles.ts
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user