import * as React from "react"; import {CssEditorEvents, CssEditorUserData, CssVariable} from "tc-shared/ui/modal/css-editor/Definitions"; import {Registry} from "tc-shared/events"; import {AbstractModal} from "tc-shared/ui/react-elements/Modal"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {BoxedInputField, FlatInputField} from "tc-shared/ui/react-elements/InputField"; import {useState} from "react"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {Checkbox} from "tc-shared/ui/react-elements/Checkbox"; import 'rc-color-picker/assets/index.css'; import {Button} from "tc-shared/ui/react-elements/Button"; import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal"; const cssStyle = require("./Renderer.scss"); const CssVariableRenderer = React.memo((props: { events: Registry<CssEditorEvents>, variable: CssVariable, selected: boolean }) => { const [ selected, setSelected ] = useState(props.selected); const [ override, setOverride ] = useState(props.variable.overwriteValue); const [ overrideColor, setOverrideColor ] = useState(props.variable.customValue); props.events.reactUse("action_select_entry", event => setSelected(event.variable === props.variable)); props.events.reactUse("action_override_toggle", event => { if(event.variableName !== props.variable.name) return; setOverride(event.enabled); if(event.enabled) setOverrideColor(event.value); }); props.events.reactUse("action_change_override_value", event => { if(event.variableName !== props.variable.name) return; setOverrideColor(event.value); }); return ( <div className={cssStyle.variable + " " + (selected ? cssStyle.selected : "")} onClick={() => { if(selected) return; props.events.fire("action_select_entry", { variable: props.variable }) }} > <div className={cssStyle.preview}> <div className={cssStyle.color} style={{ backgroundColor: props.variable.defaultValue }} /> </div> <div className={cssStyle.preview}> <div className={cssStyle.color} style={{ backgroundColor: override ? overrideColor : undefined }} /> </div> <a>{props.variable.name}</a> </div> ); }); const CssVariableListBodyRenderer = (props: { events: Registry<CssEditorEvents> }) => { const [ variables, setVariables ] = useState<"loading" | CssVariable[]>(() => { props.events.fire_async("query_css_variables"); return "loading"; }); const [ filter, setFilter ] = useState(undefined); const [ selectedVariable, setSelectedVariable ] = useState(undefined); props.events.reactUse("action_select_entry", event => setSelectedVariable(event.variable)); props.events.reactUse("query_css_variables", () => setVariables("loading")); let content; if(variables === "loading") { content = ( <div className={cssStyle.overlay} key={"loading"}> <a> <Translatable>Loading</Translatable> <LoadingDots /> </a> </div> ); } else { content = []; for(const variable of variables) { if(filter && variable.name.toLowerCase().indexOf(filter) === -1) continue; content.push(<CssVariableRenderer key={"variable-" + variable.name} events={props.events} variable={variable} selected={selectedVariable === variable.name} />); } if(content.length === 0) { content.push( <div className={cssStyle.overlay} key={"no-match"}> <a><Translatable>No variable matched your filter</Translatable></a> </div> ); } } props.events.reactUse("action_set_filter", event => setFilter(event.filter?.toLowerCase())); props.events.reactUse("notify_css_variables", event => setVariables(event.variables)); return ( <div className={cssStyle.body} onKeyPress={event => { if(variables === "loading") return; /* TODO: This isn't working since the div isn't focused properly yet */ let offset = 0; if(event.key === "ArrowDown") { offset = 1; } else if(event.key === "ArrowUp") { offset = -1; } if(offset !== 0) { const selectIndex = variables.findIndex(e => e === selectedVariable); if(selectIndex === -1) return; const variable = variables[selectIndex + offset]; if(!variable) return; props.events.fire("action_select_entry", { variable: variable }); } }} tabIndex={0}> {content} </div> ); }; const CssVariableListSearchRenderer = (props: { events: Registry<CssEditorEvents> }) => { const [ isLoading, setLoading ] = useState(true); props.events.reactUse("notify_css_variables", () => setLoading(false)); props.events.reactUse("query_css_variables", () => setLoading(true)); return ( <div className={cssStyle.search}> <FlatInputField label={<Translatable>Filter variables</Translatable>} labelType={"floating"} className={cssStyle.input} onInput={text => props.events.fire("action_set_filter", { filter: text })} disabled={isLoading} /> </div> ); }; const CssVariableListRenderer = (props: { events: Registry<CssEditorEvents> }) => ( <div className={cssStyle.containerList}> <div className={cssStyle.header}> <a><Translatable>CSS Variable list</Translatable></a> </div> <div className={cssStyle.list} onKeyPress={event => console.error(event.key)}> <CssVariableListBodyRenderer events={props.events} /> <CssVariableListSearchRenderer events={props.events} /> </div> </div> ); const SelectedVariableInfo = (props: { events: Registry<CssEditorEvents> }) => { const [ selectedVariable, setSelectedVariable ] = useState<CssVariable>(undefined); props.events.reactUse("action_select_entry", event => setSelectedVariable(event.variable)); return (<> <div className={cssStyle.detail}> <div className={cssStyle.title}> <Translatable>Name</Translatable> </div> <div className={cssStyle.value}> <BoxedInputField editable={false} value={selectedVariable ? selectedVariable.name : "-"} /> </div> </div> <div className={cssStyle.detail}> <div className={cssStyle.title}> <Translatable>Default Value</Translatable> </div> <div className={cssStyle.value}> <BoxedInputField editable={false} value={selectedVariable ? selectedVariable.defaultValue : "-"} /> </div> </div> </>); }; const OverrideVariableInfo = (props: { events: Registry<CssEditorEvents> }) => { const [ selectedVariable, setSelectedVariable ] = useState<CssVariable>(undefined); const [ overwriteValue, setOverwriteValue ] = useState<string>(undefined); const [ overwriteEnabled, setOverwriteEnabled ] = useState(false); props.events.reactUse("action_select_entry", event => { setSelectedVariable(event.variable); setOverwriteEnabled(event.variable?.overwriteValue); setOverwriteValue(event.variable?.customValue); }); props.events.reactUse("action_override_toggle", event => { if(event.variableName !== selectedVariable?.name) return; selectedVariable.overwriteValue = event.enabled; setOverwriteEnabled(event.enabled); if(event.enabled) setOverwriteValue(event.value); }); props.events.reactUse("action_change_override_value", event => { if(event.variableName !== selectedVariable?.name) return; setOverwriteValue(event.value); }); return (<> <div className={cssStyle.detail}> <div className={cssStyle.title}> <Translatable>Override Value</Translatable> <Checkbox value={overwriteEnabled} disabled={!selectedVariable} onChange={value => { props.events.fire("action_override_toggle", { variableName: selectedVariable.name, value: typeof selectedVariable.customValue === "string" ? selectedVariable.customValue : selectedVariable.defaultValue, enabled: value }); }} /> </div> <div className={cssStyle.value + " " + cssStyle.color}> <BoxedInputField className={cssStyle.input} disabled={!overwriteEnabled} value={overwriteValue || " "} onInput={text => { selectedVariable.customValue = text; props.events.fire("action_change_override_value", { value: text, variableName: selectedVariable.name }); }} /> <CssVariableColorPicker events={props.events} selectedVariable={selectedVariable} /> </div> </div> </>); }; const CssVariableColorPicker = (props: { events: Registry<CssEditorEvents>, selectedVariable: CssVariable }) => { const [ overwriteValue, setOverwriteValue ] = useState<string>(undefined); const [ overwriteEnabled, setOverwriteEnabled ] = useState(false); props.events.reactUse("action_override_toggle", event => { if(event.variableName !== props.selectedVariable?.name) return; props.selectedVariable.overwriteValue = event.enabled; setOverwriteEnabled(event.enabled); if(event.enabled) setOverwriteValue(event.value); }); props.events.reactUse("action_change_override_value", event => { if(event.variableName !== props.selectedVariable?.name || 'cpInvoker' in event) return; setOverwriteValue(event.value); }); let currentInput: string; let inputTimeout: number; return ( <label className={cssStyle.colorButton} > <input disabled={!overwriteEnabled} type={"color"} value={overwriteValue} onChange={event => { currentInput = event.target.value; if(inputTimeout) return; inputTimeout = setTimeout(() => { inputTimeout = undefined; props.events.fire("action_change_override_value", { value: currentInput, variableName: props.selectedVariable.name }); }, 150); }} /> <a className="rainbow-letter" style={{ borderBottomColor: overwriteValue }}>C</a> </label> ) }; const ControlButtons = (props: { events: Registry<CssEditorEvents> }) => { return ( <div className={cssStyle.buttons}> <Button color={"blue"} type={"normal"} className={cssStyle.button} onClick={() => props.events.fire("action_randomize")} ><Translatable>Randomize</Translatable></Button> <Button color={"red"} type={"normal"} className={cssStyle.button + " " + cssStyle.buttonReset} onClick={() => props.events.fire("action_reset")} ><Translatable>Reset</Translatable></Button> <Button color={"blue"} type={"normal"} className={cssStyle.button} onClick={() => props.events.fire("action_export")} ><Translatable>Export</Translatable></Button> <Button color={"green"} type={"normal"} className={cssStyle.button} onClick={() => requestFileAsText().then(content => { props.events.fire("action_import", { config: content }) })} ><Translatable>Import</Translatable></Button> </div> ) }; const CssVariableEditor = (props: { events: Registry<CssEditorEvents> }) => { return ( <div className={cssStyle.containerEdit}> <div className={cssStyle.header}> <a><Translatable>Variable details</Translatable></a> </div> <SelectedVariableInfo events={props.events} /> <OverrideVariableInfo events={props.events} /> <ControlButtons events={props.events} /> </div> ) }; const downloadTextAsFile = (text, name) => { const element = document.createElement("a"); element.text = "download"; element.href = "data:test/plain;charset=utf-8," + encodeURIComponent(text); element.download = name; element.style.display = "none"; document.body.appendChild(element); element.click(); element.remove(); }; const requestFileAsText = async (): Promise<string> => { const element = document.createElement("input"); element.style.display = "none"; element.type = "file"; document.body.appendChild(element); element.click(); await new Promise(resolve => { element.onchange = resolve; }); if(element.files.length !== 1) return undefined; const file = element.files[0]; element.remove(); return await file.text(); }; class PopoutConversationUI extends AbstractModal { private readonly events: Registry<CssEditorEvents>; private readonly userData: CssEditorUserData; constructor(events: Registry<CssEditorEvents>, userData: CssEditorUserData) { super(); this.userData = userData; this.events = events; this.events.on("notify_export_result", event => { createInfoModal(tr("Config exported successfully"), tr("The config has been exported successfully.")).open(); downloadTextAsFile(event.config, "teaweb-style.json"); }); this.events.on("notify_import_result", event => { if(event.success) createInfoModal(tr("Config imported successfully"), tr("The config has been imported successfully.")).open(); else createErrorModal(tr("Config imported failed"), tr("The config import has been failed.")).open(); }) } renderBody() { return ( <div className={cssStyle.container}> <CssVariableListRenderer events={this.events} /> <CssVariableEditor events={this.events} /> </div> ); } title() { return "CSS Variable editor"; } } export = PopoutConversationUI;