diff --git a/shared/js/events/ClientGlobalControlHandler.ts b/shared/js/events/ClientGlobalControlHandler.ts index c63548c4..f8c339d0 100644 --- a/shared/js/events/ClientGlobalControlHandler.ts +++ b/shared/js/events/ClientGlobalControlHandler.ts @@ -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) { @@ -137,10 +138,18 @@ export function initialize(event_registry: Registry) }).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); } diff --git a/shared/js/events/GlobalEvents.ts b/shared/js/events/GlobalEvents.ts index 55fca5cb..d9ddf465 100644 --- a/shared/js/events/GlobalEvents.ts +++ b/shared/js/events/GlobalEvents.ts @@ -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" | diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 828dd67c..d4046b94 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -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"); } diff --git a/shared/js/settings.ts b/shared/js/settings.ts index 0448e44d..8734db2f 100644 --- a/shared/js/settings.ts +++ b/shared/js/settings.ts @@ -439,7 +439,7 @@ export class Settings extends StaticSettings { static readonly KEY_FONT_SIZE: ValuedSettingsKey = { key: "font_size", valueType: "number", - defaultValue: 14 + defaultValue: 14 //parseInt(getComputedStyle(document.body).fontSize) }; static readonly KEY_ICON_SIZE: ValuedSettingsKey = { diff --git a/shared/js/ui/frames/MenuBar.ts b/shared/js/ui/frames/MenuBar.ts index 1405e8d5..17b0f7b0 100644 --- a/shared/js/ui/frames/MenuBar.ts +++ b/shared/js/ui/frames/MenuBar.ts @@ -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")); } { diff --git a/shared/js/ui/frames/log/Renderer.tsx b/shared/js/ui/frames/log/Renderer.tsx index 60abc552..bd2b68e8 100644 --- a/shared/js/ui/frames/log/Renderer.tsx +++ b/shared/js/ui/frames/log/Renderer.tsx @@ -54,8 +54,16 @@ export const ServerLogRenderer = (props: { events: Registry, }); 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)); diff --git a/shared/js/ui/modal/ModalSettings.tsx b/shared/js/ui/modal/ModalSettings.tsx index d856296a..8aa08999 100644 --- a/shared/js/ui/modal/ModalSettings.tsx +++ b/shared/js/ui/modal/ModalSettings.tsx @@ -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"); }); } diff --git a/shared/js/ui/modal/global-settings-editor/Controller.tsx b/shared/js/ui/modal/global-settings-editor/Controller.tsx new file mode 100644 index 00000000..843779c1 --- /dev/null +++ b/shared/js/ui/modal/global-settings-editor/Controller.tsx @@ -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(); + initializeController(events); + + const modal = spawnReactModal(ModalGlobalSettingsEditor, events); + modal.show(); + modal.events.on("destroy", () => { + events.fire("notify_destroy"); + events.destroy(); + }); +} + +function initializeController(events: Registry) { + events.on("query_settings", () => { + const settingsList: Setting[] = []; + + for(const key of Settings.KEYS) { + const setting = Settings[key] as SettingsKey; + 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).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).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 }); + } + })); +} \ No newline at end of file diff --git a/shared/js/ui/modal/global-settings-editor/Definitions.ts b/shared/js/ui/modal/global-settings-editor/Definitions.ts new file mode 100644 index 00000000..bde5d3f2 --- /dev/null +++ b/shared/js/ui/modal/global-settings-editor/Definitions.ts @@ -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: {} +} \ No newline at end of file diff --git a/shared/js/ui/modal/global-settings-editor/Renderer.scss b/shared/js/ui/modal/global-settings-editor/Renderer.scss new file mode 100644 index 00000000..96c869dd --- /dev/null +++ b/shared/js/ui/modal/global-settings-editor/Renderer.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/global-settings-editor/Renderer.tsx b/shared/js/ui/modal/global-settings-editor/Renderer.tsx new file mode 100644 index 00000000..51d85d40 --- /dev/null +++ b/shared/js/ui/modal/global-settings-editor/Renderer.tsx @@ -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>(undefined); +const cssStyle = require("./Renderer.scss"); + +const SettingInfoRenderer = (props: { children: [React.ReactNode, React.ReactNode ], className?: string }) => ( +
+
{props.children[0]}
+
{props.children[1]}
+
+); + +const SettingEditor = () => { + const events = useContext(ModalEvents); + + const [ isApplying, setApplying ] = useState(false); + const [ currentValue, setCurrentValue ] = useState(); + const [ currentSetting, setCurrentSetting ] = useState(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 ( +
+ + Setting key + {currentSetting.key} + + + Description + {currentSetting.description} + + + Default Value + {typeof currentSetting.defaultValue !== "undefined" ? (currentSetting.defaultValue + "") : tr("unset")} + + + Value + { + setCurrentValue(text); + }} + finishOnEnter={true} + onBlur={() => { + events.fire("action_set_value", { setting: currentSettingKey.current, value: currentValue }); + }} + /> + +
+ ); +} + +const SettingEntryRenderer = (props: { setting: Setting, selected: boolean }) => { + const events = useContext(ModalEvents); + + return ( +
events.fire("action_select_setting", { setting: props.setting.key })}> + {props.setting.key} +
+ ); +} + +const SettingList = () => { + const events = useContext(ModalEvents); + const [ settings, setSettings ] = useState<"loading" | Setting[]>(() => { + events.fire("query_settings"); + return "loading"; + }); + const [ selectedSetting, setSelectedSetting ] = useState(undefined); + const [ filter, setFilter ] = useState(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 ( +
+
+ {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 ; + }) + } +
+
+ events.fire("action_set_filter", { filter: text })} placeholder={tr("Filter settings")} /> +
+ +
+ ); +} + +export class ModalGlobalSettingsEditor extends InternalModal { + protected readonly events: Registry; + + constructor(events: Registry) { + super(); + + this.events = events; + } + + renderBody(): React.ReactElement { + return ( + +
+
+ + +
+
+ + +
+
+
+ ); + } + + title(): string | React.ReactElement { + return Global settings registry; + } +} + diff --git a/shared/js/ui/react-elements/InputField.scss b/shared/js/ui/react-elements/InputField.scss index e076cfbb..3bcb7774 100644 --- a/shared/js/ui/react-elements/InputField.scss +++ b/shared/js/ui/react-elements/InputField.scss @@ -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; diff --git a/shared/js/ui/react-elements/InputField.tsx b/shared/js/ui/react-elements/InputField.tsx index 74db72b3..56835a35 100644 --- a/shared/js/ui/react-elements/InputField.tsx +++ b/shared/js/ui/react-elements/InputField.tsx @@ -111,6 +111,8 @@ export class BoxedInputField extends React.Component {this.props.label ? @@ -174,6 +177,8 @@ export class FlatInputField extends React.Component{this.props.label} : undefined}