Adding a setting editor

This commit is contained in:
WolverinDEV 2020-09-17 23:06:02 +02:00
parent 2a8027ec23
commit 3771f40625
13 changed files with 568 additions and 24 deletions

View file

@ -4,7 +4,6 @@ import {Sound} from "../sound/Sounds";
import {ConnectionHandler} from "../ConnectionHandler";
import {server_connections} from "../ui/frames/connection_handlers";
import {createErrorModal, createInfoModal, createInputModal} from "../ui/elements/Modal";
import {settings} from "../settings";
import {spawnConnectModal} from "../ui/modal/ModalConnect";
import PermissionType from "../permission/PermissionType";
import {spawnQueryCreate} from "../ui/modal/ModalQuery";
@ -14,6 +13,8 @@ import {CommandResult} from "../connection/ServerConnectionDeclaration";
import {spawnSettingsModal} from "../ui/modal/ModalSettings";
import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalPermissionEditor";
import {tr} from "../i18n/localize";
import {spawnGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Controller";
import {spawnModalCssVariableEditor} from "tc-shared/ui/modal/css-editor/Controller";
/*
function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>) {
@ -137,10 +138,18 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
}).open();
break;
case "css-variable-editor":
spawnModalCssVariableEditor();
break;
case "settings":
spawnSettingsModal();
break;
case "settings-registry":
spawnGlobalSettingsEditor();
break;
default:
console.warn(tr("Received open window event for an unknown window: %s"), event.window);
}

View file

@ -6,6 +6,8 @@ export interface ClientGlobalControlEvents {
action_open_window: {
window:
"settings" | /* use action_open_window_settings! */
"settings-registry" |
"css-variable-editor" |
"bookmark-manage" |
"query-manage" |
"query-create" |

View file

@ -46,6 +46,7 @@ import "./profiles/ConnectionProfile";
import "./update/UpdaterWeb";
import ContextMenuEvent = JQuery.ContextMenuEvent;
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
import {spawnGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Controller";
let preventWelcomeUI = false;
async function initialize() {
@ -151,8 +152,11 @@ export function handle_connect_request(properties: ConnectRequestData, connectio
function main() {
/* initialize font */
{
const font = settings.static_global(Settings.KEY_FONT_SIZE, 14); //parseInt(getComputedStyle(document.body).fontSize)
const font = settings.static_global(Settings.KEY_FONT_SIZE);
$(document.body).css("font-size", font + "px");
settings.globalChangeListener(Settings.KEY_FONT_SIZE, value => {
$(document.body).css("font-size", value + "px");
})
}
/* context menu prevent */
@ -308,6 +312,7 @@ function main() {
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
}
spawnGlobalSettingsEditor();
//spawnVideoPopout(server_connections.active_connection(), "https://www.youtube.com/watch?v=9683D18fyvs");
}

View file

@ -439,7 +439,7 @@ export class Settings extends StaticSettings {
static readonly KEY_FONT_SIZE: ValuedSettingsKey<number> = {
key: "font_size",
valueType: "number",
defaultValue: 14
defaultValue: 14 //parseInt(getComputedStyle(document.body).fontSize)
};
static readonly KEY_ICON_SIZE: ValuedSettingsKey<number> = {

View file

@ -25,6 +25,7 @@ import {control_bar_instance} from "../../ui/frames/control-bar";
import {icon_cache_loader, IconManager, LocalIcon} from "../../file/Icons";
import {spawnPermissionEditorModal} from "../../ui/modal/permission/ModalPermissionEditor";
import {spawnModalCssVariableEditor} from "../../ui/modal/css-editor/Controller";
import {global_client_actions} from "tc-shared/events/GlobalEvents";
export interface HRItem { }
@ -523,11 +524,16 @@ export function initialize() {
menu.append_hr();
item = menu.append_item(tr("Modify CSS variables"));
item.click(() => spawnModalCssVariableEditor());
item.click(() => global_client_actions.fire("action_open_window", { window: "css-variable-editor" }));
item = menu.append_item(tr("Open Registry"));
item.click(() => global_client_actions.fire("action_open_window", { window: "settings-registry" }));
menu.append_hr();
item = menu.append_item(tr("Settings"));
item.icon("client-settings");
item.click(() => spawnSettingsModal());
item.click(() => global_client_actions.fire("action_open_window_settings"));
}
{

View file

@ -54,8 +54,16 @@ export const ServerLogRenderer = (props: { events: Registry<ServerLogUIEvents>,
});
props.events.reactUse("notify_log_add", event => {
if(logs === "loading")
if(logs === "loading") {
return;
}
if(__build.mode === "debug") {
const index = logs.findIndex(e => e.uniqueId === event.event.uniqueId);
if(index !== -1) {
debugger;
}
}
logs.push(event.event);
logs.splice(0, Math.max(0, logs.length - 100));

View file

@ -103,17 +103,16 @@ function settings_general_application(container: JQuery, modal: Modal) {
const current_size = parseInt(getComputedStyle(document.body).fontSize); //settings.static_global(Settings.KEY_FONT_SIZE, 12);
const select = container.find(".option-font-size");
if (select.find("option[value='" + current_size + "']").length)
if (select.find("option[value='" + current_size + "']").length) {
select.find("option[value='" + current_size + "']").prop("selected", true);
else
} else {
select.find("option[value='-1']").prop("selected", true);
}
select.on('change', event => {
const value = parseInt(select.val() as string);
settings.changeGlobal(Settings.KEY_FONT_SIZE, value);
console.log("Changed font size to %dpx", value);
$(document.body).css("font-size", value + "px");
});
}

View file

@ -0,0 +1,80 @@
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
import {ModalGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Renderer";
import {Registry} from "tc-shared/events";
import {ModalGlobalSettingsEditorEvents, Setting} from "tc-shared/ui/modal/global-settings-editor/Definitions";
import {settings, Settings, SettingsKey} from "tc-shared/settings";
export function spawnGlobalSettingsEditor() {
const events = new Registry<ModalGlobalSettingsEditorEvents>();
initializeController(events);
const modal = spawnReactModal(ModalGlobalSettingsEditor, events);
modal.show();
modal.events.on("destroy", () => {
events.fire("notify_destroy");
events.destroy();
});
}
function initializeController(events: Registry<ModalGlobalSettingsEditorEvents>) {
events.on("query_settings", () => {
const settingsList: Setting[] = [];
for(const key of Settings.KEYS) {
const setting = Settings[key] as SettingsKey<ConfigValueTypes>;
settingsList.push({
key: setting.key,
description: setting.description,
type: setting.valueType,
defaultValue: setting.defaultValue
});
}
events.fire_async("notify_settings", { settings: settingsList });
});
events.on("action_select_setting", event => {
events.fire("notify_selected_setting", { setting: event.setting });
});
events.on("query_setting", event => {
const setting = Settings.KEYS.map(setting => Settings[setting] as SettingsKey<ConfigValueTypes>).find(e => e.key === event.setting);
if(typeof setting === "undefined") {
events.fire("notify_setting", {
key: event.setting,
status: "not-found"
});
return;
}
events.fire("notify_setting", {
setting: event.setting,
status: "success",
info: {
key: setting.key,
description: setting.description,
type: setting.valueType,
defaultValue: setting.defaultValue
},
value: settings.global(setting)
});
});
events.on("action_set_value", event => {
const setting = Settings.KEYS.map(setting => Settings[setting] as SettingsKey<ConfigValueTypes>).find(e => e.key === event.setting);
if(typeof setting === "undefined") {
return;
}
/* the change will may already trigger a notify_setting_value, but just to ensure we're fiering it later as well */
settings.changeGlobal(setting, event.value);
events.fire_async("notify_setting_value", { setting: event.setting, value: event.value });
});
events.on("notify_destroy", settings.events.on("notify_setting_changed", event => {
if(event.mode === "global") {
events.fire_async("notify_setting_value", { setting: event.setting, value: event.newValue });
}
}));
}

View file

@ -0,0 +1,36 @@
export interface Setting {
key: string;
type: ConfigValueTypeNames;
description: string | undefined;
defaultValue: string | undefined;
}
export interface ModalGlobalSettingsEditorEvents {
action_select_setting: { setting: string }
action_set_filter: { filter: string },
action_set_value: { setting: string, value: string }
query_settings: {},
query_setting: { setting: string }
notify_settings: {
settings: Setting[]
},
notify_setting: {
setting: string,
status: "success" | "not-found",
info?: Setting,
value?: string
},
notify_selected_setting: {
setting: string
},
notify_setting_value: {
setting: string,
value: string
},
notify_destroy: {}
}

View file

@ -0,0 +1,198 @@
@import "../../../../css/static/mixin";
@import "../../../../css/static/properties";
.container {
display: flex;
flex-direction: row;
justify-content: stretch;
flex-grow: 1;
flex-shrink: 1;
padding: .5em;
width: 50em;
max-width: 50em;
min-width: 20em;
height: 30em;
@include user-select(none);
.subContainer {
display: flex;
flex-direction: column;
justify-content: stretch;
/* allocate as much space as we can get */
width: 100vw;
flex-grow: 1;
flex-shrink: 1;
.header {
flex-grow: 0;
flex-shrink: 0;
font-weight: bold;
color: #e0e0e0;
@include text-dotdotdot();
}
.body {
flex-grow: 1;
flex-shrink: 1;
min-height: 5em;
}
&.containerList {
max-width: 20em;
min-width: 6em;
}
&.containerEdit {
min-width: 10em;
}
}
.list {
flex-grow: 1;
flex-shrink: 1;
margin-right: 1em;
min-height: 6.5em;
position: relative;
display: flex;
flex-direction: column;
justify-content: stretch;
border: 1px #161616 solid;
border-radius: 0.2em;
background-color: #28292b;
.entries {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
justify-content: stretch;
overflow-x: hidden;
overflow-y: auto;
min-height: 3em;
@include chat-scrollbar-vertical();
.entry {
flex-grow: 0;
flex-shrink: 0;
padding-left: .5em;
padding-right: .5em;
display: flex;
flex-direction: row;
justify-content: stretch;
height: 1.5em;
cursor: pointer;
&:hover {
background-color: #2c2d2f;
}
&.selected {
background-color: #1a1a1b;
}
}
}
.filter {
border-top: 1px #161616 solid;
display: flex;
flex-direction: row;
justify-content: stretch;
.input {
flex-grow: 1;
flex-shrink: 1;
margin: 0;
padding: .5em 1em;
min-width: 3em;
}
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
background-color: #28292b;
display: none;
flex-direction: column;
justify-content: center;
&.shown {
display: flex;
}
a {
text-align: center;
font-size: 1.2em;
}
}
}
.editor {
.info {
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
margin-bottom: 1em;
.title {
text-transform: uppercase;
color: var(--modal-query-key);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis
}
.value {
user-select: text;
@include text-dotdotdot();
}
}
.infoDescription {
.value {
white-space: pre-wrap!important;
height: 3.2em!important;
}
}
.infoValue {
.input {
padding: 0;
margin: 0;
}
}
}
}

View file

@ -0,0 +1,196 @@
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import * as React from "react";
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
import {createContext, useContext, useRef, useState} from "react";
import {Registry} from "tc-shared/events";
import {ModalGlobalSettingsEditorEvents, Setting} from "tc-shared/ui/modal/global-settings-editor/Definitions";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {FlatInputField} from "tc-shared/ui/react-elements/InputField";
const ModalEvents = createContext<Registry<ModalGlobalSettingsEditorEvents>>(undefined);
const cssStyle = require("./Renderer.scss");
const SettingInfoRenderer = (props: { children: [React.ReactNode, React.ReactNode ], className?: string }) => (
<div className={cssStyle.info + " " + (props.className || "")}>
<div className={cssStyle.title}>{props.children[0]}</div>
<div className={cssStyle.value}>{props.children[1]}</div>
</div>
);
const SettingEditor = () => {
const events = useContext(ModalEvents);
const [ isApplying, setApplying ] = useState(false);
const [ currentValue, setCurrentValue ] = useState<string>();
const [ currentSetting, setCurrentSetting ] = useState<Setting | "not-found">(false);
const currentSettingKey = useRef();
events.reactUse("notify_selected_setting", event => {
if(event.setting === currentSettingKey.current) {
return;
}
currentSettingKey.current = event.setting;
events.fire("query_setting", { setting: event.setting });
});
events.reactUse("notify_setting", event => {
if(event.setting !== currentSettingKey.current) {
return;
}
setApplying(false);
if(event.status === "not-found") {
setCurrentSetting("not-found");
} else {
setCurrentValue(event.value);
setCurrentSetting(event.info);
}
});
events.reactUse("action_set_value", event => {
if(event.setting !== currentSettingKey.current) {
return;
}
setApplying(true);
});
events.reactUse("notify_setting_value", event => {
if(event.setting !== currentSettingKey.current) {
return;
}
setApplying(false);
setCurrentValue(event.value);
});
if(currentSetting === "not-found") {
return null;
} else if(!currentSetting) {
return null;
}
return (
<div className={cssStyle.body + " " + cssStyle.editor}>
<SettingInfoRenderer>
<Translatable>Setting key</Translatable>
{currentSetting.key}
</SettingInfoRenderer>
<SettingInfoRenderer className={cssStyle.infoDescription}>
<Translatable>Description</Translatable>
{currentSetting.description}
</SettingInfoRenderer>
<SettingInfoRenderer className={cssStyle}>
<Translatable>Default Value</Translatable>
{typeof currentSetting.defaultValue !== "undefined" ? (currentSetting.defaultValue + "") : tr("unset")}
</SettingInfoRenderer>
<SettingInfoRenderer className={cssStyle.infoValue}>
<Translatable>Value</Translatable>
<FlatInputField
className={cssStyle.input}
value={isApplying ? "" : currentValue ? currentValue : " "}
editable={!isApplying}
placeholder={isApplying ? tr("applying...") : tr("setting unset")}
onChange={text => {
setCurrentValue(text);
}}
finishOnEnter={true}
onBlur={() => {
events.fire("action_set_value", { setting: currentSettingKey.current, value: currentValue });
}}
/>
</SettingInfoRenderer>
</div>
);
}
const SettingEntryRenderer = (props: { setting: Setting, selected: boolean }) => {
const events = useContext(ModalEvents);
return (
<div className={cssStyle.entry + " " + (props.selected ? cssStyle.selected : "")} onClick={() => events.fire("action_select_setting", { setting: props.setting.key })}>
{props.setting.key}
</div>
);
}
const SettingList = () => {
const events = useContext(ModalEvents);
const [ settings, setSettings ] = useState<"loading" | Setting[]>(() => {
events.fire("query_settings");
return "loading";
});
const [ selectedSetting, setSelectedSetting ] = useState<string>(undefined);
const [ filter, setFilter ] = useState<string>(undefined);
events.reactUse("notify_settings", event => setSettings(event.settings));
events.reactUse("notify_selected_setting", event => setSelectedSetting(event.setting));
events.reactUse("action_set_filter", event => setFilter((event.filter || "").toLowerCase()));
return (
<div className={cssStyle.body + " " + cssStyle.list}>
<div className={cssStyle.entries}>
{settings === "loading" ? undefined :
settings.map(setting => {
filterBlock:
if(filter) {
if(setting.key.toLowerCase().indexOf(filter) !== -1) {
break filterBlock;
}
if(setting.description && setting.description.toLowerCase().indexOf(filter) !== -1) {
break filterBlock;
}
return undefined;
}
return <SettingEntryRenderer setting={setting} selected={setting.key === selectedSetting} key={setting.key} />;
})
}
</div>
<div className={cssStyle.filter}>
<FlatInputField className={cssStyle.input} onInput={text => events.fire("action_set_filter", { filter: text })} placeholder={tr("Filter settings")} />
</div>
<div className={cssStyle.overlay + " " + (settings === "loading" ? cssStyle.shown : "")}>
<a><Translatable>loading</Translatable> <LoadingDots /></a>
</div>
</div>
);
}
export class ModalGlobalSettingsEditor extends InternalModal {
protected readonly events: Registry<ModalGlobalSettingsEditorEvents>;
constructor(events: Registry<ModalGlobalSettingsEditorEvents>) {
super();
this.events = events;
}
renderBody(): React.ReactElement {
return (
<ModalEvents.Provider value={this.events}>
<div className={cssStyle.container}>
<div className={cssStyle.subContainer + " " + cssStyle.containerList}>
<div className={cssStyle.header}>
<a><Translatable>Setting list</Translatable></a>
</div>
<SettingList />
</div>
<div className={cssStyle.subContainer + " " + cssStyle.containerEdit}>
<div className={cssStyle.header}>
<a><Translatable>Setting editor</Translatable></a>
</div>
<SettingEditor />
</div>
</div>
</ModalEvents.Provider>
);
}
title(): string | React.ReactElement<Translatable> {
return <Translatable>Global settings registry</Translatable>;
}
}

View file

@ -157,15 +157,15 @@ html:root {
.containerFlat {
position: relative;
padding-top: 1.75rem; /* the label above (might be floating) */
margin-bottom: 1rem; /* for invalid label/help label */
padding-top: 1.75em; /* the label above (might be floating) */
margin-bottom: 1em; /* for invalid label/help label */
label {
color: #999999;
top: 1rem;
top: 1em;
left: 0;
font-size: .75rem;
font-size: .75em;
position: absolute;
pointer-events: none;
@ -182,13 +182,13 @@ html:root {
&.type-floating {
will-change: left, top, contents;
color: #999999;
top: 2.42rem;
font-size: 1rem;
top: 2.42em;
font-size: 1em;
}
&.type-static {
top: 1rem;
font-size: .75rem;
top: 1em;
font-size: .75em;
}
@include transition(color $button_hover_animation_time ease-in-out, top $button_hover_animation_time ease-in-out, font-size $button_hover_animation_time ease-in-out);
@ -199,8 +199,8 @@ html:root {
color: #3c74a2;
&.type-floating {
font-size: .75rem;
top: 1rem;
font-size: .75em;
top: 1em;
}
}
}
@ -211,7 +211,7 @@ html:root {
height: 2.25em;
width: 100%;
font-size: 1rem;
font-size: 1em;
line-height: 1.5;
color: #cdd1d0;
@ -226,7 +226,7 @@ html:root {
box-shadow: none;
transition: background 0s ease-out;
padding: .4375rem 0;
padding: .4375em 0;
@include transition(all .15s ease-in-out);
@ -248,7 +248,7 @@ html:root {
position: absolute;
opacity: 0;
width: 100%;
margin-top: .25rem;
margin-top: .25em;
font-size: 80%;
color: #f44336;
@ -274,7 +274,7 @@ html:root {
position: absolute;
opacity: 0;
width: 100%;
margin-top: .25rem;
margin-top: .25em;
font-size: .75em;

View file

@ -111,6 +111,8 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
export interface FlatInputFieldProperties {
defaultValue?: string;
value?: string;
placeholder?: string;
className?: string;
@ -165,6 +167,7 @@ export class FlatInputField extends React.Component<FlatInputFieldProperties, Fl
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : typeof this.props.disabled === "boolean" ? this.props.disabled : false;
const readOnly = typeof this.state.editable === "boolean" ? !this.state.editable : typeof this.props.editable === "boolean" ? !this.props.editable : false;
const placeholder = typeof this.state.placeholder === "string" ? this.state.placeholder : typeof this.props.placeholder === "string" ? this.props.placeholder : undefined;
return (
<div className={cssStyle.containerFlat + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (this.state.filled ? cssStyle.isFilled : "") + " " + (this.props.className || "")}>
{this.props.label ?
@ -174,6 +177,8 @@ export class FlatInputField extends React.Component<FlatInputFieldProperties, Fl
(this.props.labelFloatingClassName && this.state.filled ? this.props.labelFloatingClassName : "")}>{this.props.label}</label> : undefined}
<input
defaultValue={this.props.defaultValue}
value={this.props.value}
type={"text"}
ref={this.refInput}
readOnly={readOnly}