TeaWeb/shared/js/ui/modal/css-editor/Renderer.tsx

396 lines
15 KiB
TypeScript

import * as React from "react";
import {useState} from "react";
import {CssEditorEvents, CssVariable} from "tc-shared/ui/modal/css-editor/Definitions";
import {IpcRegistryDescription, Registry} from "tc-shared/events";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {BoxedInputField, FlatInputField} from "tc-shared/ui/react-elements/InputField";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
import {Button} from "tc-shared/ui/react-elements/Button";
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
import {downloadTextAsFile, requestFileAsText} from "tc-shared/file/Utils";
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_react("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>&nbsp;
<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}>
<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);
}
}, true, [selectedVariable]);
props.events.reactUse("action_change_override_value", event => {
if (event.variableName !== selectedVariable?.name)
return;
setOverwriteValue(event.value);
}, true, [selectedVariable]);
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={event => props.events.fire("action_export", {allValues: event.shiftKey})}
title={tr("Click to export the changed values, Shift click to export all values")}
><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>
)
};
class PopoutConversationUI extends AbstractModal {
private readonly events: Registry<CssEditorEvents>;
constructor(events: IpcRegistryDescription<CssEditorEvents>) {
super();
this.events = Registry.fromIpcDescription(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>
);
}
renderTitle() {
return <Translatable>"CSS Variable editor"</Translatable>;
}
}
export = PopoutConversationUI;