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
|
||||
}
|
||||
|
||||
function loadMustaches () {
|
||||
// Loading mustache templates, dans filling constants.
|
||||
const r = []
|
||||
r['MUSTACHE_CONFIGURATION_HOME'] = loadMustache('client/common/configuration/templates/home.mustache')
|
||||
r['MUSTACHE_CONFIGURATION_CHANNEL'] = loadMustache('client/common/configuration/templates/channel.mustache')
|
||||
return r
|
||||
}
|
||||
|
||||
function loadMustache (file) {
|
||||
const filePath = path.resolve(__dirname, file)
|
||||
return JSON.stringify(fs.readFileSync(filePath).toString())
|
||||
}
|
||||
|
||||
const define = Object.assign({
|
||||
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
||||
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
||||
}, loadLocs(), loadMustaches())
|
||||
}, loadLocs())
|
||||
|
||||
const configs = clientFiles.map(f => ({
|
||||
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
||||
|
@ -29,7 +29,8 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
|
||||
onMount: async ({ rootEl }) => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const channelId = urlParams.get('channelId') ?? ''
|
||||
render(html`<channel-configuration .registerClientOptions=${clientOptions}></channel-configuration>`, rootEl)
|
||||
render(html`<channel-configuration .registerClientOptions=${clientOptions}
|
||||
.channelId=${channelId}></channel-configuration>`, rootEl)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,33 +1,53 @@
|
||||
import { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||
import { html, LitElement } from 'lit'
|
||||
import { css, html, LitElement } from 'lit'
|
||||
import { repeat } from 'lit-html/directives/repeat.js'
|
||||
import { customElement, property } from 'lit/decorators.js'
|
||||
import { customElement, property, state } from 'lit/decorators.js'
|
||||
import { ptTr } from './TranslationDirective'
|
||||
import { localizedHelpUrl } from '../../../utils/help'
|
||||
import './DynamicTableFormElement'
|
||||
import './PluginConfigurationRow'
|
||||
import './HelpButtonElement'
|
||||
import { until } from 'async'
|
||||
import { Task } from '@lit/task';
|
||||
import { ChannelConfiguration } from 'shared/lib/types'
|
||||
import { ChannelConfigurationService } from './ChannelConfigurationService'
|
||||
import { createContext, provide } from '@lit/context'
|
||||
import { getGlobalStyleSheets } from '../../global-styles'
|
||||
|
||||
export const registerClientOptionsContext = createContext<RegisterClientOptions | undefined>(Symbol('register-client-options'));
|
||||
export const channelConfigurationContext = createContext<ChannelConfiguration | undefined>(Symbol('channel-configuration'));
|
||||
export const channelConfigurationServiceContext = createContext<ChannelConfigurationService | undefined>(Symbol('channel-configuration-service'));
|
||||
|
||||
@customElement('channel-configuration')
|
||||
export class ChannelConfigurationElement extends LitElement {
|
||||
|
||||
@provide({ context: registerClientOptionsContext })
|
||||
@property({ attribute: false })
|
||||
public registerClientOptions: RegisterClientOptions | undefined
|
||||
|
||||
createRenderRoot = () => {
|
||||
return this
|
||||
}
|
||||
@property({ attribute: false })
|
||||
public channelId: number | undefined
|
||||
|
||||
@provide({ context: channelConfigurationContext })
|
||||
@state()
|
||||
public _channelConfiguration: ChannelConfiguration | undefined
|
||||
|
||||
@provide({ context: channelConfigurationServiceContext })
|
||||
private _configurationService: ChannelConfigurationService | undefined
|
||||
|
||||
static styles = [
|
||||
...getGlobalStyleSheets()
|
||||
];
|
||||
|
||||
@state()
|
||||
public _formStatus: boolean | any = undefined
|
||||
|
||||
private _asyncTaskRender = new Task(this, {
|
||||
|
||||
task: async ([registerClientOptions], {signal}) => {
|
||||
let link = registerClientOptions ? await localizedHelpUrl(registerClientOptions, { page: 'documentation/user/streamers/bot/forbidden_words' }) : '';
|
||||
|
||||
return {
|
||||
url : new URL(link),
|
||||
title: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)
|
||||
if (this.registerClientOptions) {
|
||||
this._configurationService = new ChannelConfigurationService(this.registerClientOptions)
|
||||
this._channelConfiguration = await this._configurationService.fetchConfiguration(this.channelId ?? 0)
|
||||
}
|
||||
},
|
||||
|
||||
@ -35,9 +55,26 @@ export class ChannelConfigurationElement extends LitElement {
|
||||
|
||||
});
|
||||
|
||||
private _saveConfig = () => {
|
||||
if(this._configurationService && this._channelConfiguration) {
|
||||
this._configurationService.saveOptions(this._channelConfiguration.channel.id, this._channelConfiguration.configuration)
|
||||
.then((value) => {
|
||||
this._formStatus = { success: true }
|
||||
console.log(`Configuration has been updated`)
|
||||
this.requestUpdate('_formStatus')
|
||||
})
|
||||
.catch((error) => {
|
||||
this._formStatus = error
|
||||
console.log(`An error occurred : ${JSON.stringify(this._formStatus)}`)
|
||||
this.requestUpdate('_formStatus')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render = () => {
|
||||
let tableHeader = {
|
||||
words: {
|
||||
let tableHeaderList = {
|
||||
forbiddenWords: {
|
||||
entries: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
|
||||
},
|
||||
@ -61,15 +98,38 @@ export class ChannelConfigurationElement extends LitElement {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC)
|
||||
}
|
||||
},
|
||||
quotes: {
|
||||
messages: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL2),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC2)
|
||||
},
|
||||
delay: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC)
|
||||
}
|
||||
},
|
||||
commands: {
|
||||
command: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_DESC)
|
||||
},
|
||||
message: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_DESC)
|
||||
}
|
||||
}
|
||||
}
|
||||
let tableSchema = {
|
||||
words: {
|
||||
inputType: 'text',
|
||||
default: 'helloqwesad'
|
||||
forbiddenWords: {
|
||||
entries: {
|
||||
inputType: 'textarea',
|
||||
default: ['helloqwesad'],
|
||||
separator: '\n',
|
||||
},
|
||||
regex: {
|
||||
inputType: 'text',
|
||||
default: 'helloaxzca'
|
||||
default: 'helloaxzca',
|
||||
},
|
||||
applyToModerators: {
|
||||
inputType: 'checkbox',
|
||||
@ -80,10 +140,9 @@ export class ChannelConfigurationElement extends LitElement {
|
||||
default: 'helloasx'
|
||||
},
|
||||
reason: {
|
||||
inputType: 'select',
|
||||
inputType: 'text',
|
||||
default: 'transphobia',
|
||||
label: 'choose your poison',
|
||||
options: {'racism': 'Racism', 'sexism': 'Sexism', 'transphobia': 'Transphobia', 'bigotry': 'Bigotry'}
|
||||
datalist: ['Racism', 'Sexism', 'Transphobia', 'Bigotry']
|
||||
},
|
||||
comments: {
|
||||
inputType: 'textarea',
|
||||
@ -95,52 +154,179 @@ export class ChannelConfigurationElement extends LitElement {
|
||||
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
|
||||
culpa qui officia deserunt mollit anim id est laborum.`
|
||||
},
|
||||
},
|
||||
quotes: {
|
||||
messages: {
|
||||
inputType: 'textarea',
|
||||
default: ['default message'],
|
||||
separator: '\n',
|
||||
},
|
||||
delay: {
|
||||
inputType: 'number',
|
||||
default: 100,
|
||||
}
|
||||
let tableRows = [
|
||||
{
|
||||
words: 'teweqwst',
|
||||
regex: 'tesdgst',
|
||||
applyToModerators: false,
|
||||
label: 'teswet',
|
||||
reason: 'sexism',
|
||||
comments: 'tsdaswest',
|
||||
},
|
||||
{
|
||||
words: 'tedsadst',
|
||||
regex: 'tezxccst',
|
||||
applyToModerators: true,
|
||||
label: 'tewest',
|
||||
reason: 'racism',
|
||||
comments: 'tesxzct',
|
||||
commands: {
|
||||
command: {
|
||||
inputType: 'text',
|
||||
default: 'default command',
|
||||
},
|
||||
{
|
||||
words: 'tesadsdxst',
|
||||
regex: 'dsfsdf',
|
||||
applyToModerators: false,
|
||||
label: 'tesdadst',
|
||||
reason: 'bigotry',
|
||||
comments: 'tsadest',
|
||||
},
|
||||
]
|
||||
message: {
|
||||
inputType: 'text',
|
||||
default: 'default message',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._asyncTaskRender.render({
|
||||
complete: (helpLink) => html`
|
||||
<div class="container">
|
||||
<channel-configuration></channel-configuration>
|
||||
complete: () => html`
|
||||
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel">
|
||||
<h1>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE)}:
|
||||
<span class="peertube-plugin-livechat-configuration-channel-info">
|
||||
<span>${this._channelConfiguration?.channel.displayName}</span>
|
||||
<span>${this._channelConfiguration?.channel.name}</span>
|
||||
</span>
|
||||
<help-button .page="documentation/user/streamers/channel">
|
||||
</help-button>
|
||||
</h1>
|
||||
<p>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}</p>
|
||||
<form livechat-configuration-channel-options role="form">
|
||||
<div class="row mt-3">
|
||||
<plugin-configuration-row
|
||||
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC, true)}
|
||||
.helpPage=${"documentation/user/streamers/slow_mode"}>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="number"
|
||||
name="slow_mode_duration"
|
||||
class="form-control"
|
||||
min="0"
|
||||
max="1000"
|
||||
id="peertube-livechat-slow-mode-duration"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && this._channelConfiguration)
|
||||
this._channelConfiguration.configuration.slowMode.duration = Number((event.target as HTMLInputElement).value)
|
||||
this.requestUpdate('_channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="${this._channelConfiguration?.configuration.slowMode.duration}"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</plugin-configuration-row>
|
||||
<plugin-configuration-row
|
||||
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
||||
.description=${''}
|
||||
.helpPage=${"documentation/user/streamers/channel"}>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bot"
|
||||
id="peertube-livechat-bot"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && this._channelConfiguration)
|
||||
this._channelConfiguration.configuration.bot.enabled = (event.target as HTMLInputElement).checked
|
||||
this.requestUpdate('_channelConfiguration')
|
||||
}
|
||||
}
|
||||
.value=${this._channelConfiguration?.configuration.bot.enabled}
|
||||
?checked=${this._channelConfiguration?.configuration.bot.enabled}
|
||||
/>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL)}
|
||||
</label>
|
||||
</div>
|
||||
${this._channelConfiguration?.configuration.bot.enabled ?
|
||||
html`<div class="form-group">
|
||||
<label for="peertube-livechat-bot-nickname">${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME)}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="bot_nickname"
|
||||
class="form-control"
|
||||
id="peertube-livechat-bot-nickname"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && this._channelConfiguration)
|
||||
this._channelConfiguration.configuration.bot.nickname = (event.target as HTMLInputElement).value
|
||||
this.requestUpdate('_channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="${this._channelConfiguration?.configuration.bot.nickname}"
|
||||
/>
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
</plugin-configuration-row>
|
||||
${this._channelConfiguration?.configuration.bot.enabled ?
|
||||
html`<plugin-configuration-row
|
||||
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
||||
.helpLink=${helpLink}
|
||||
>
|
||||
.helpPage=${"documentation/user/streamers/bot/forbidden_words"}>
|
||||
<dynamic-table-form
|
||||
.header=${tableHeader}
|
||||
.schema=${tableSchema}
|
||||
.rows=${tableRows}
|
||||
.formName=${'forbidden-words'}
|
||||
>
|
||||
.header=${tableHeaderList.forbiddenWords}
|
||||
.schema=${tableSchema.forbiddenWords}
|
||||
.rows=${this._channelConfiguration?.configuration.bot.forbiddenWords}
|
||||
@update=${(e: CustomEvent) => {
|
||||
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.forbiddenWords = e.detail
|
||||
this.requestUpdate('_channelConfiguration')
|
||||
}
|
||||
}
|
||||
.formName=${'forbidden-words'}>
|
||||
</dynamic-table-form>
|
||||
</plugin-configuration-row>
|
||||
<plugin-configuration-row
|
||||
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)}
|
||||
.helpPage=${"documentation/user/streamers/bot/quotes"}>
|
||||
<dynamic-table-form
|
||||
.header=${tableHeaderList.quotes}
|
||||
.schema=${tableSchema.quotes}
|
||||
.rows=${this._channelConfiguration?.configuration.bot.quotes}
|
||||
@update=${(e: CustomEvent) => {
|
||||
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.quotes = e.detail
|
||||
this.requestUpdate('_channelConfiguration')
|
||||
}
|
||||
}
|
||||
.formName=${'quote'}>
|
||||
</dynamic-table-form>
|
||||
</plugin-configuration-row>
|
||||
<plugin-configuration-row
|
||||
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)}
|
||||
.helpPage=${"documentation/user/streamers/bot/commands"}>
|
||||
<dynamic-table-form
|
||||
.header=${tableHeaderList.commands}
|
||||
.schema=${tableSchema.commands}
|
||||
.rows=${this._channelConfiguration?.configuration.bot.commands}
|
||||
@update=${(e: CustomEvent) => {
|
||||
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.commands = e.detail
|
||||
this.requestUpdate('_channelConfiguration')
|
||||
}
|
||||
}
|
||||
.formName=${'command'}>
|
||||
</dynamic-table-form>
|
||||
</plugin-configuration-row>`
|
||||
: ''
|
||||
}
|
||||
<div class="form-group mt-5">
|
||||
<button type="button" class="orange-button" @click=${this._saveConfig}>${ptTr(LOC_SAVE)}</button>
|
||||
</div>
|
||||
${(this._formStatus && this._formStatus.success === undefined) ?
|
||||
html`<div class="alert alert-warning" role="alert">
|
||||
An error occurred : ${JSON.stringify(this._formStatus)}
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
${(this._formStatus && this._formStatus.success === true) ?
|
||||
html`<div class="alert alert-success" role="alert">
|
||||
Configuration has been updated
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
</form>
|
||||
</div>${JSON.stringify(this._channelConfiguration)}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,11 +1,26 @@
|
||||
import { html, LitElement, TemplateResult } from 'lit'
|
||||
import { css, html, LitElement, nothing, TemplateResult } from 'lit'
|
||||
import { repeat } from 'lit/directives/repeat.js'
|
||||
import { customElement, property, state } from 'lit/decorators.js'
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
||||
import { ifDefined } from 'lit/directives/if-defined.js'
|
||||
import { StaticValue, unsafeStatic } from 'lit/static-html.js'
|
||||
import { getGlobalStyleSheets } from '../../global-styles'
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
||||
|
||||
type DynamicTableAcceptedTypes = number | string | boolean | Date
|
||||
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
||||
const AddSVG: string =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>
|
||||
</svg>`
|
||||
|
||||
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
||||
const RemoveSVG: string =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-square">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
</svg>`
|
||||
|
||||
|
||||
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
|
||||
|
||||
type DynamicTableAcceptedInputTypes = 'textarea'
|
||||
| 'select'
|
||||
@ -36,6 +51,8 @@ interface CellDataSchema {
|
||||
size?: number
|
||||
label?: string
|
||||
options?: { [key: string]: string }
|
||||
datalist?: DynamicTableAcceptedTypes[]
|
||||
separator?: string
|
||||
inputType?: DynamicTableAcceptedInputTypes
|
||||
default?: DynamicTableAcceptedTypes
|
||||
}
|
||||
@ -44,55 +61,100 @@ interface CellDataSchema {
|
||||
export class DynamicTableFormElement extends LitElement {
|
||||
|
||||
@property({ attribute: false })
|
||||
public header: { [key : string]: { colName: TemplateResult, description: TemplateResult } } = {}
|
||||
public header: { [key: string]: { colName: TemplateResult, description: TemplateResult } } = {}
|
||||
|
||||
|
||||
@property({ attribute: false })
|
||||
public schema: { [key : string]: CellDataSchema } = {}
|
||||
public schema: { [key: string]: CellDataSchema } = {}
|
||||
|
||||
@property({ attribute: false })
|
||||
public rows: { [key: string]: DynamicTableAcceptedTypes }[] = []
|
||||
|
||||
@property({ reflect: true })
|
||||
public rows: { _id: number; [key : string]: DynamicTableAcceptedTypes }[] = []
|
||||
|
||||
@state()
|
||||
public _rowsById: { _id: number; row: { [key: string]: DynamicTableAcceptedTypes } }[] = []
|
||||
|
||||
@property({ attribute: false })
|
||||
public formName: string = ''
|
||||
|
||||
|
||||
@state()
|
||||
private _lastRowId = 1
|
||||
|
||||
createRenderRoot = () => {
|
||||
return this
|
||||
@property({ attribute: false })
|
||||
private _colOrder: string[] = []
|
||||
|
||||
static styles = [
|
||||
...getGlobalStyleSheets(),
|
||||
css`
|
||||
:host table {
|
||||
table-layout: fixed;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
private _getDefaultRow = () => {
|
||||
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? '']), ['_id', this._lastRowId++]])
|
||||
:host table td, table th {
|
||||
word-wrap:break-word;
|
||||
vertical-align: top;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
|
||||
:host table tbody > :nth-child(odd) {
|
||||
background-color: var(--greySecondaryBackgroundColor);
|
||||
}
|
||||
|
||||
:host button {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
:host .dynamic-table-add-row {
|
||||
background-color: var(--bs-green);
|
||||
}
|
||||
|
||||
:host .dynamic-table-remove-row {
|
||||
background-color: var(--bs-orange);
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
// fixes situations when list has been reinitialized or changed outside of CustomElement
|
||||
private _updateLastRowId = () => {
|
||||
for (let rowById of this._rowsById) {
|
||||
this._lastRowId = Math.max(this._lastRowId, rowById._id + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private _getDefaultRow = () : { [key: string]: DynamicTableAcceptedTypes } => {
|
||||
this._updateLastRowId()
|
||||
|
||||
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
||||
|
||||
}
|
||||
|
||||
private _addRow = () => {
|
||||
this.rows.push(this._getDefaultRow())
|
||||
|
||||
let newRow = this._getDefaultRow()
|
||||
this._rowsById.push({_id:this._lastRowId++, row: newRow})
|
||||
this.rows.push(newRow)
|
||||
this.requestUpdate('rows')
|
||||
|
||||
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
||||
}
|
||||
|
||||
|
||||
private _removeRow = (rowId: number) => {
|
||||
this.rows = this.rows.filter((x) => x._id != rowId)
|
||||
|
||||
let rowToRemove = this._rowsById.filter(rowById => rowById._id == rowId).map(rowById => rowById.row)[0]
|
||||
this._rowsById = this._rowsById.filter((rowById) => rowById._id !== rowId)
|
||||
this.rows = this.rows.filter((row) => row !== rowToRemove)
|
||||
this.requestUpdate('rows')
|
||||
|
||||
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
||||
}
|
||||
|
||||
|
||||
render = () => {
|
||||
const inputId = `peertube-livechat-${this.formName.replaceAll('_','-')}-table`
|
||||
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-table`
|
||||
|
||||
for(let row of this.rows) {
|
||||
if (!row._id) {
|
||||
row._id = this._lastRowId++
|
||||
this._updateLastRowId()
|
||||
|
||||
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}>
|
||||
${this._renderHeader()}
|
||||
<tbody>
|
||||
${repeat(this.rows,(row) => row._id, this._renderDataRow)}
|
||||
${repeat(this._rowsById, (rowById) => rowById._id, this._renderDataRow)}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><td><button @click=${this._addRow}>Add Row</button></td></tr>
|
||||
</tfoot>
|
||||
${this._renderFooter()}
|
||||
</table>
|
||||
`
|
||||
|
||||
}
|
||||
|
||||
private _renderHeader = () => {
|
||||
if (this._colOrder.length !== Object.keys(this.header).length) {
|
||||
this._colOrder = this._colOrder.filter(key => Object.keys(this.header).includes(key))
|
||||
this._colOrder.push(...Object.keys(this.header).filter(key => !this._colOrder.includes(key)))
|
||||
}
|
||||
|
||||
return html`<thead>
|
||||
<tr>
|
||||
<!-- <th scope="col">#</th> -->
|
||||
${Object.values(this.header).map(this._renderHeaderCell)}
|
||||
<th scope="col">Remove Row</th>
|
||||
${Object.entries(this.header).sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2))
|
||||
.map(([k,v]) => this._renderHeaderCell(v))}
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>`
|
||||
|
||||
}
|
||||
|
||||
private _renderHeaderCell = (headerCellData: { colName: TemplateResult, description: TemplateResult }) => {
|
||||
return html`<th scope="col">
|
||||
<div data-toggle="tooltip" data-placement="bottom" data-html="true" title="${headerCellData.description}">${headerCellData.colName}</div>
|
||||
<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${headerCellData.description}>${headerCellData.colName}</div>
|
||||
</th>`
|
||||
}
|
||||
|
||||
private _renderDataRow = (rowData: { _id: number; [key : string]: DynamicTableAcceptedTypes }) => {
|
||||
const inputId = `peertube-livechat-${this.formName.replaceAll('_','-')}-row-${rowData._id}`
|
||||
private _renderDataRow = (rowData: { _id: number; row: {[key: string]: DynamicTableAcceptedTypes} }) => {
|
||||
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-row-${rowData._id}`
|
||||
|
||||
return html`<tr id=${inputId}>
|
||||
<!-- <td class="form-group">${rowData._id}</td> -->
|
||||
${Object.entries(rowData).filter(([k,v]) => k != '_id').map((data) => this.renderDataCell(data, rowData._id))}
|
||||
<td class="form-group"><button @click=${() => this._removeRow(rowData._id)}>Remove</button></td>
|
||||
${Object.entries(rowData.row).filter(([k, v]) => k != '_id')
|
||||
.sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2))
|
||||
.map((data) => this.renderDataCell(data, rowData._id))}
|
||||
<td class="form-group"><button class="btn dynamic-table-remove-row" @click=${() => this._removeRow(rowData._id)}>${unsafeHTML(RemoveSVG)}</button></td>
|
||||
</tr>`
|
||||
|
||||
}
|
||||
|
||||
private _renderFooter = () => {
|
||||
return html`<tfoot>
|
||||
<tr>
|
||||
${Object.values(this.header).map(() => html`<td></td>`)}
|
||||
<td><button class="btn dynamic-table-add-row" @click=${this._addRow}>${unsafeHTML(AddSVG)}</button></td>
|
||||
</tr>
|
||||
</tfoot>`
|
||||
}
|
||||
|
||||
renderDataCell = (property: [string, DynamicTableAcceptedTypes], rowId: number) => {
|
||||
const [propertyName, propertyValue] = property
|
||||
const propertySchema = this.schema[propertyName] ?? {}
|
||||
|
||||
let formElement
|
||||
|
||||
const inputName = `${this.formName.replaceAll('-','_')}_${propertyName.toString().replaceAll('-','_')}_${rowId}`
|
||||
const inputId = `peertube-livechat-${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}`
|
||||
|
||||
switch (propertyValue.constructor) {
|
||||
case String:
|
||||
@ -169,48 +242,16 @@ export class DynamicTableFormElement extends LitElement {
|
||||
case 'time':
|
||||
case 'url':
|
||||
case 'week':
|
||||
formElement = html`<input
|
||||
type=${propertySchema.inputType}
|
||||
name=${inputName}
|
||||
class="form-control"
|
||||
id=${inputId}
|
||||
min=${ifDefined(propertySchema?.min)}
|
||||
max=${ifDefined(propertySchema?.max)}
|
||||
minlength=${ifDefined(propertySchema?.minlength)}
|
||||
maxlength=${ifDefined(propertySchema?.maxlength)}
|
||||
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
|
||||
.value=${propertyValue as string}
|
||||
/>`
|
||||
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
|
||||
break
|
||||
|
||||
case 'textarea':
|
||||
formElement = html`<textarea
|
||||
name=${inputName}
|
||||
class="form-control"
|
||||
id=${inputId}
|
||||
min=${ifDefined(propertySchema?.min)}
|
||||
max=${ifDefined(propertySchema?.max)}
|
||||
minlength=${ifDefined(propertySchema?.minlength)}
|
||||
maxlength=${ifDefined(propertySchema?.maxlength)}
|
||||
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
|
||||
.value=${propertyValue as string}
|
||||
></textarea>`
|
||||
formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
|
||||
break
|
||||
|
||||
case 'select':
|
||||
formElement = html`<select
|
||||
class="form-select"
|
||||
aria-label="Default select example"
|
||||
@change=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
|
||||
>
|
||||
<option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option>
|
||||
${Object.entries(propertySchema?.options ?? {})
|
||||
?.map(([value,name]) =>
|
||||
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
||||
)}
|
||||
</select>`
|
||||
formElement = this._renderSelect(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
|
||||
break
|
||||
|
||||
}
|
||||
break
|
||||
|
||||
@ -223,20 +264,8 @@ export class DynamicTableFormElement extends LitElement {
|
||||
case 'datetime':
|
||||
case 'datetime-local':
|
||||
case 'time':
|
||||
formElement = html`<input
|
||||
type=${propertySchema.inputType}
|
||||
name=${inputName}
|
||||
class="form-control"
|
||||
id=${inputId}
|
||||
min=${ifDefined(propertySchema?.min)}
|
||||
max=${ifDefined(propertySchema?.max)}
|
||||
minlength=${ifDefined(propertySchema?.minlength)}
|
||||
maxlength=${ifDefined(propertySchema?.maxlength)}
|
||||
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
|
||||
.value=${(propertyValue as Date).toISOString()}
|
||||
/>`
|
||||
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, (propertyValue as Date).toISOString())
|
||||
break
|
||||
|
||||
}
|
||||
break
|
||||
|
||||
@ -247,20 +276,8 @@ export class DynamicTableFormElement extends LitElement {
|
||||
|
||||
case 'number':
|
||||
case 'range':
|
||||
formElement = html`<input
|
||||
type=${propertySchema.inputType}
|
||||
name=${inputName}
|
||||
class="form-control"
|
||||
id=${inputId}
|
||||
min=${ifDefined(propertySchema?.min)}
|
||||
max=${ifDefined(propertySchema?.max)}
|
||||
minlength=${ifDefined(propertySchema?.minlength)}
|
||||
maxlength=${ifDefined(propertySchema?.maxlength)}
|
||||
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
|
||||
.value=${propertyValue as String}
|
||||
/>`
|
||||
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
|
||||
break
|
||||
|
||||
}
|
||||
break
|
||||
|
||||
@ -270,44 +287,130 @@ export class DynamicTableFormElement extends LitElement {
|
||||
propertySchema.inputType = 'checkbox'
|
||||
|
||||
case 'checkbox':
|
||||
formElement = html`<input
|
||||
type="checkbox"
|
||||
name=${inputName}
|
||||
class="form-check-input"
|
||||
id=${inputId}
|
||||
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
|
||||
.value=${propertyValue as String}
|
||||
?checked=${propertyValue as Boolean}
|
||||
/>`
|
||||
formElement = this._renderCheckbox(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as boolean)
|
||||
break
|
||||
|
||||
}
|
||||
break
|
||||
|
||||
case Array:
|
||||
switch (propertySchema.inputType) {
|
||||
case undefined:
|
||||
propertySchema.inputType = 'text'
|
||||
|
||||
case 'text':
|
||||
case 'color':
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
case 'datetime-local':
|
||||
case 'email':
|
||||
case 'file':
|
||||
case 'image':
|
||||
case 'month':
|
||||
case 'number':
|
||||
case 'password':
|
||||
case 'range':
|
||||
case 'tel':
|
||||
case 'time':
|
||||
case 'url':
|
||||
case 'week':
|
||||
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema,
|
||||
(propertyValue as Array<number | string>).join(propertySchema.separator ?? ','))
|
||||
break
|
||||
case 'textarea':
|
||||
formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema,
|
||||
(propertyValue as Array<number | string>).join(propertySchema.separator ?? ','))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!formElement) {
|
||||
console.warn(`value type '${propertyValue.constructor}' is incompatible`
|
||||
+ `with field type '${propertySchema.inputType}' for form entry '${propertyName.toString()}'.`)
|
||||
|
||||
}
|
||||
|
||||
return html`<td class="form-group">${formElement}</td>`
|
||||
}
|
||||
|
||||
_renderInput = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
|
||||
return html`<input
|
||||
type=${propertySchema.inputType}
|
||||
name=${inputName}
|
||||
class="form-control"
|
||||
id=${inputId}
|
||||
list=${(propertySchema?.datalist) ? inputId + '-datalist' : nothing}
|
||||
min=${ifDefined(propertySchema?.min)}
|
||||
max=${ifDefined(propertySchema?.max)}
|
||||
minlength=${ifDefined(propertySchema?.minlength)}
|
||||
maxlength=${ifDefined(propertySchema?.maxlength)}
|
||||
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||
.value=${propertyValue}
|
||||
/>
|
||||
${(propertySchema?.datalist) ? html`<datalist id=${inputId + '-datalist'}>
|
||||
${(propertySchema?.datalist ?? []).map((value) => html`<option value=${value} />`)}
|
||||
</datalist>` : nothing}
|
||||
`
|
||||
}
|
||||
|
||||
_renderTextarea = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
|
||||
return html`<textarea
|
||||
name=${inputName}
|
||||
class="form-control"
|
||||
id=${inputId}
|
||||
min=${ifDefined(propertySchema?.min)}
|
||||
max=${ifDefined(propertySchema?.max)}
|
||||
minlength=${ifDefined(propertySchema?.minlength)}
|
||||
maxlength=${ifDefined(propertySchema?.maxlength)}
|
||||
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||
.value=${propertyValue}
|
||||
></textarea>`
|
||||
}
|
||||
|
||||
_renderCheckbox = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: boolean) => {
|
||||
return html`<input
|
||||
type="checkbox"
|
||||
name=${inputName}
|
||||
class="form-check-input"
|
||||
id=${inputId}
|
||||
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||
.value=${propertyValue}
|
||||
?checked=${propertyValue}
|
||||
/>`
|
||||
}
|
||||
|
||||
_renderSelect = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
|
||||
return html`<select
|
||||
class="form-select"
|
||||
aria-label="Default select example"
|
||||
@change=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||
>
|
||||
<option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option>
|
||||
${Object.entries(propertySchema?.options ?? {})
|
||||
?.map(([value, name]) =>
|
||||
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
||||
)}
|
||||
</select>`
|
||||
}
|
||||
|
||||
|
||||
_updatePropertyFromValue(event: Event, propertyName: string, rowId : number) {
|
||||
_updatePropertyFromValue = (event: Event, propertyName: string, propertySchema: CellDataSchema, rowId: number) => {
|
||||
let target = event?.target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)
|
||||
let value = (target && target instanceof HTMLInputElement && target.type == "checkbox") ? !!(target?.checked) : target?.value
|
||||
|
||||
if(value !== undefined) {
|
||||
for(let row of this.rows) {
|
||||
if(row._id === rowId) {
|
||||
row[propertyName] = value
|
||||
if (value !== undefined) {
|
||||
for (let rowById of this._rowsById) {
|
||||
if (rowById._id === rowId) {
|
||||
switch (rowById.row[propertyName].constructor) {
|
||||
case Array:
|
||||
rowById.row[propertyName] = (value as string).split(propertySchema.separator ?? ',')
|
||||
default:
|
||||
rowById.row[propertyName] = value
|
||||
}
|
||||
|
||||
this.rows = this._rowsById.map(rowById => rowById.row)
|
||||
|
||||
this.requestUpdate('rows')
|
||||
|
||||
this.requestUpdate('rowsById')
|
||||
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
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 { unsafeSVG } from 'lit/directives/unsafe-svg.js'
|
||||
import { StaticValue } from 'lit/static-html.js'
|
||||
import { helpButtonSVG } from '../../../videowatch/buttons'
|
||||
import './HelpButtonElement'
|
||||
import { getGlobalStyleSheets } from '../../global-styles'
|
||||
|
||||
@customElement('plugin-configuration-row')
|
||||
export class PLuginConfigurationRow extends LitElement {
|
||||
export class PluginConfigurationRow extends LitElement {
|
||||
|
||||
@property({ attribute: false })
|
||||
public title: string = `title`
|
||||
@ -14,11 +13,11 @@ export class PLuginConfigurationRow extends LitElement {
|
||||
public description: string = `Here's a description`
|
||||
|
||||
@property({ attribute: false })
|
||||
public helpLink: { url: URL, title: string } = { url : new URL('https://lmddgtfy.net/'), title: 'Online Help'}
|
||||
public helpPage: string = 'documentation'
|
||||
|
||||
createRenderRoot = () => {
|
||||
return this
|
||||
}
|
||||
static styles = [
|
||||
...getGlobalStyleSheets()
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
@ -26,12 +25,8 @@ export class PLuginConfigurationRow extends LitElement {
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2>${this.title}</h2>
|
||||
<p>${this.description}</p>
|
||||
<a
|
||||
href="${this.helpLink.url.href}"
|
||||
target=_blank
|
||||
title="${this.helpLink.title}"
|
||||
class="orange-button peertube-button-link"
|
||||
>${unsafeSVG(helpButtonSVG())}</a>
|
||||
<help-button .page=${this.helpPage}>
|
||||
</help-button>
|
||||
</div>
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
<slot><p>Nothing in this row.</p></slot>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { PartInfo, directive } from 'lit/directive.js'
|
||||
import { PartInfo, PartType, directive } from 'lit/directive.js'
|
||||
import { AsyncDirective } from 'lit/async-directive.js'
|
||||
import { RegisterClientHelpers } from '@peertube/peertube-types/client';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { html } from 'lit';
|
||||
import { unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
export class TranslationDirective extends AsyncDirective {
|
||||
|
||||
@ -9,22 +12,35 @@ export class TranslationDirective extends AsyncDirective {
|
||||
private _translatedValue : string = ''
|
||||
private _localizationId : string = ''
|
||||
|
||||
private _allowUnsafeHTML = false
|
||||
|
||||
constructor(partInfo: PartInfo) {
|
||||
super(partInfo);
|
||||
|
||||
//_peertubeOptionsPromise.then((options) => this._peertubeHelpers = options.peertubeHelpers)
|
||||
}
|
||||
|
||||
override render = (locId: string) => {
|
||||
// update = (part: ElementPart) => {
|
||||
// if (part) console.log(`Element : ${part?.element?.getAttributeNames?.().join(' ')}`);
|
||||
// return this.render(this._localizationId);
|
||||
// }
|
||||
|
||||
override render = (locId: string, allowHTML: boolean = false) => {
|
||||
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
||||
|
||||
this._allowUnsafeHTML = allowHTML
|
||||
|
||||
if (this._translatedValue === '') {
|
||||
this._translatedValue = locId
|
||||
}
|
||||
|
||||
this._asyncUpdateTranslation()
|
||||
|
||||
return this._translatedValue
|
||||
return this._internalRender()
|
||||
}
|
||||
|
||||
_internalRender = () => {
|
||||
return this._allowUnsafeHTML ? html`${unsafeHTML(this._translatedValue)}` : this._translatedValue
|
||||
}
|
||||
|
||||
_asyncUpdateTranslation = async () => {
|
||||
@ -32,7 +48,7 @@ export class TranslationDirective extends AsyncDirective {
|
||||
|
||||
if (newValue !== '' && newValue !== this._translatedValue) {
|
||||
this._translatedValue = newValue
|
||||
this.setValue(newValue)
|
||||
this.setValue(this._internalRender())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 { helpButtonSVG } from '../../../videowatch/buttons'
|
||||
import { TemplateResult, html } from 'lit'
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
||||
import { ptTr } from './TranslationDirective'
|
||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
|
||||
interface HomeViewData {
|
||||
title: string
|
||||
@ -89,7 +87,7 @@ async function _fillViewHelpButtons ( // TODO: refactor with the similar functio
|
||||
return html`<a
|
||||
href="${helpUrl}"
|
||||
target=_blank
|
||||
title="${ptTr(LOC_ONLINE_HELP)}"
|
||||
title="${title}"
|
||||
class="orange-button peertube-button-link"
|
||||
>${unsafeHTML(helpIcon)}</a>`
|
||||
}
|
||||
|
@ -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