diff --git a/ChangeLog.md b/ChangeLog.md index cb354f61..0b3ae160 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,9 @@ - Added a "watch to gather" context menu entry for clients - Disassembled the current client icon sprite into his icons - Added an icon spread generator. This now allows dynamically adding new icons to the spread sheet - + - Fixed a bug that prevented the microphone settings from saving + - Enabled the CSS editor for the client as well + * **08.08.20** - Added a watch to gather mode - Added API support for the popout able browsers for the native client diff --git a/loader/app/animation.ts b/loader/app/animation.ts index 304865b6..fff1a0c2 100644 --- a/loader/app/animation.ts +++ b/loader/app/animation.ts @@ -81,7 +81,12 @@ export async function initialize() { StageNames[Stage.LOADED] = "starting app"; overlay.classList.add("initialized"); - setupContainer.classList.add("visible"); + + if(parseInt(getUrlParameter("animation-short")) === 1) { + setupAnimationFinished(); + } else { + setupContainer.classList.add("visible"); + } initializeTimestamp = Date.now(); return true; diff --git a/loader/css/loader.scss b/loader/css/loader.scss index 8d6d9e6e..0f997a87 100644 --- a/loader/css/loader.scss +++ b/loader/css/loader.scss @@ -19,6 +19,8 @@ $setup-time: 80s / 24; /* 24 frames / sec; the initial sequence is 80 seconds */ flex-direction: column; justify-content: center; + -webkit-app-region: drag; + .container { flex-shrink: 0; diff --git a/loader/css/overlay.scss b/loader/css/overlay.scss index e32aab12..3e76ba17 100644 --- a/loader/css/overlay.scss +++ b/loader/css/overlay.scss @@ -11,6 +11,12 @@ background: #1e1e1e; text-align: center; + -webkit-app-region: drag; + + h1, h3, a { + -webkit-app-region: no-drag; + } + .container { position: relative; display: inline-block; diff --git a/shared/css/static/mixin.scss b/shared/css/static/mixin.scss index 0362c58c..0199d98b 100644 --- a/shared/css/static/mixin.scss +++ b/shared/css/static/mixin.scss @@ -74,50 +74,32 @@ } &::-webkit-scrollbar-track { - //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); border-radius: .25em; background-color: transparent; + cursor: pointer; } &::-webkit-scrollbar { width: .5em; + height: .5em; + background-color: transparent; + cursor: pointer; } &::-webkit-scrollbar-thumb { border-radius: .25em; - //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); background-color: #555; } + + &::-webkit-scrollbar-corner { + //background: #19191b; + background-color: transparent; + } } @mixin chat-scrollbar-vertical() { - - & { - // for moz - scrollbar-color: #353535 #555; - scrollbarWidth: .5em; - } - - &::-webkit-scrollbar-track { - //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); - border-radius: .25em; - background-color: transparent; - cursor: pointer; - } - - &::-webkit-scrollbar { - width: .5em; - background-color: transparent; - cursor: pointer; - } - - &::-webkit-scrollbar-thumb { - border-radius: .25em; - //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); - background-color: #555; - } - + @include chat-scrollbar(); & > .simplebar-track { .simplebar-scrollbar { @@ -136,32 +118,7 @@ } @mixin chat-scrollbar-horizontal() { - - & { - // MOZ - scrollbar-color: #353535 #555; - scrollbarWidth: .5em; - } - - &::-webkit-scrollbar-track { - //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); - border-radius: .25em; - background-color: transparent; - cursor: pointer; - } - - &::-webkit-scrollbar { - height: .5em; - background-color: transparent; - cursor: pointer; - } - - &::-webkit-scrollbar-thumb { - border-radius: .25em; - //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); - background-color: #555; - } - + @include chat-scrollbar(); } @mixin text-dotdotdot() { diff --git a/shared/js/ui/elements/Modal.ts b/shared/js/ui/elements/Modal.ts index 9d3e1e1a..c4cd13ed 100644 --- a/shared/js/ui/elements/Modal.ts +++ b/shared/js/ui/elements/Modal.ts @@ -1,4 +1,5 @@ -import * as $ from "jquery"; +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; import {KeyCode} from "tc-shared/PPTListener"; export enum ElementType { @@ -179,7 +180,6 @@ namespace modal { }); } } -modal.initialize_modals(); let _global_modal_count = 0; let _global_modal_last: HTMLElement; @@ -395,26 +395,10 @@ export function createInfoModal(header: BodyCreator, message: BodyCreator, props return modal; } - - - - - - - - - - - - - - - - - - - - - - - +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 80, + name: "modal init", + function: async () => { + modal.initialize_modals(); + } +}) \ No newline at end of file diff --git a/shared/js/ui/frames/MenuBar.ts b/shared/js/ui/frames/MenuBar.ts index 6798b4ff..059552a3 100644 --- a/shared/js/ui/frames/MenuBar.ts +++ b/shared/js/ui/frames/MenuBar.ts @@ -522,10 +522,8 @@ export function initialize() { _state_updater["tools.qc"] = { item: item, conditions: [condition_connected]}; menu.append_hr(); - if(__build.target === "web") { - item = menu.append_item(tr("Modify CSS variables")); - item.click(() => spawnModalCssVariableEditor()); - } + item = menu.append_item(tr("Modify CSS variables")); + item.click(() => spawnModalCssVariableEditor()); item = menu.append_item(tr("Settings")); item.icon("client-settings"); diff --git a/shared/js/ui/frames/side/ConversationManager.ts b/shared/js/ui/frames/side/ConversationManager.ts index d3e4216a..a48bd951 100644 --- a/shared/js/ui/frames/side/ConversationManager.ts +++ b/shared/js/ui/frames/side/ConversationManager.ts @@ -8,7 +8,6 @@ import {ServerCommand} from "tc-shared/connection/ConnectionBase"; import {Settings} from "tc-shared/settings"; import {tra, traj} from "tc-shared/i18n/localize"; import {createErrorModal} from "tc-shared/ui/elements/Modal"; -import {helpers} from "tc-shared/ui/frames/side/chat_helper"; import ReactDOM = require("react-dom"); import { ChatEvent, @@ -19,7 +18,6 @@ import { } from "tc-shared/ui/frames/side/ConversationDefinitions"; import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI"; import {preprocessChatMessageForSend} from "tc-shared/text/chat"; -import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal"; const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */ diff --git a/shared/js/ui/frames/side/PopoutConversationUI.tsx b/shared/js/ui/frames/side/PopoutConversationUI.tsx index 3c242167..ad15a3cc 100644 --- a/shared/js/ui/frames/side/PopoutConversationUI.tsx +++ b/shared/js/ui/frames/side/PopoutConversationUI.tsx @@ -1,8 +1,8 @@ -import {AbstractModal} from "tc-shared/ui/react-elements/Modal"; import {Registry} from "tc-shared/events"; import {ConversationUIEvents} from "tc-shared/ui/frames/side/ConversationDefinitions"; import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI"; import * as React from "react"; +import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; class PopoutConversationUI extends AbstractModal { private readonly events: Registry; diff --git a/shared/js/ui/modal/ModalChangeVolume.ts b/shared/js/ui/modal/ModalChangeVolume.ts index 08012743..8271705e 100644 --- a/shared/js/ui/modal/ModalChangeVolume.ts +++ b/shared/js/ui/modal/ModalChangeVolume.ts @@ -4,7 +4,6 @@ import {sliderfy} from "tc-shared/ui/elements/Slider"; import {createModal, Modal} from "tc-shared/ui/elements/Modal"; import {ClientEntry} from "tc-shared/ui/client"; import * as htmltags from "tc-shared/ui/htmltags"; -import {spawnReactModal} from "tc-shared/ui/react-elements/Modal"; let modal: Modal; export function spawnChangeVolume(client: ClientEntry, local: boolean, current: number, max: number | undefined, callback: (number) => void) { diff --git a/shared/js/ui/modal/ModalChangeVolumeNew.tsx b/shared/js/ui/modal/ModalChangeVolumeNew.tsx index c2f9dd3e..2568662e 100644 --- a/shared/js/ui/modal/ModalChangeVolumeNew.tsx +++ b/shared/js/ui/modal/ModalChangeVolumeNew.tsx @@ -1,10 +1,11 @@ -import {InternalModal, spawnReactModal} from "tc-shared/ui/react-elements/Modal"; +import {spawnReactModal} from "tc-shared/ui/react-elements/Modal"; import * as React from "react"; import {Slider} from "tc-shared/ui/react-elements/Slider"; import {Button} from "tc-shared/ui/react-elements/Button"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {ClientEntry, MusicClientEntry} from "tc-shared/ui/client"; +import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; const cssStyle = require("./ModalChangeVolume.scss"); export interface VolumeChangeEvents { diff --git a/shared/js/ui/modal/ModalGroupCreate.tsx b/shared/js/ui/modal/ModalGroupCreate.tsx index 133b3ccc..2740903b 100644 --- a/shared/js/ui/modal/ModalGroupCreate.tsx +++ b/shared/js/ui/modal/ModalGroupCreate.tsx @@ -1,4 +1,4 @@ -import {InternalModal, spawnReactModal} from "tc-shared/ui/react-elements/Modal"; +import {spawnReactModal} from "tc-shared/ui/react-elements/Modal"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {Registry} from "tc-shared/events"; import {FlatInputField, FlatSelect} from "tc-shared/ui/react-elements/InputField"; @@ -11,6 +11,7 @@ import PermissionType from "tc-shared/permission/PermissionType"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal"; import {tra} from "tc-shared/i18n/localize"; +import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; const cssStyle = require("./ModalGroupCreate.scss"); diff --git a/shared/js/ui/modal/ModalGroupPermissionCopy.tsx b/shared/js/ui/modal/ModalGroupPermissionCopy.tsx index dd5bdd08..6e3ab9b2 100644 --- a/shared/js/ui/modal/ModalGroupPermissionCopy.tsx +++ b/shared/js/ui/modal/ModalGroupPermissionCopy.tsx @@ -1,4 +1,4 @@ -import {InternalModal, spawnReactModal} from "tc-shared/ui/react-elements/Modal"; +import {spawnReactModal} from "tc-shared/ui/react-elements/Modal"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {Registry} from "tc-shared/events"; import {useRef, useState} from "react"; @@ -11,7 +11,7 @@ import PermissionType from "tc-shared/permission/PermissionType"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal"; import {tra} from "tc-shared/i18n/localize"; -import {md5} from "tc-shared/crypto/md5"; +import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; const cssStyle = require("./ModalGroupPermissionCopy.scss"); diff --git a/shared/js/ui/modal/css-editor/Renderer.tsx b/shared/js/ui/modal/css-editor/Renderer.tsx index 057fde77..892621df 100644 --- a/shared/js/ui/modal/css-editor/Renderer.tsx +++ b/shared/js/ui/modal/css-editor/Renderer.tsx @@ -1,7 +1,6 @@ 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"; @@ -9,6 +8,7 @@ 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"; const cssStyle = require("./Renderer.scss"); diff --git a/shared/js/ui/modal/permission/ModalPermissionEditor.tsx b/shared/js/ui/modal/permission/ModalPermissionEditor.tsx index 88abba9d..c88e2e0a 100644 --- a/shared/js/ui/modal/permission/ModalPermissionEditor.tsx +++ b/shared/js/ui/modal/permission/ModalPermissionEditor.tsx @@ -1,4 +1,4 @@ -import {InternalModal, spawnReactModal} from "tc-shared/ui/react-elements/Modal"; +import {spawnReactModal} from "tc-shared/ui/react-elements/Modal"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import * as React from "react"; import {useState} from "react"; @@ -31,6 +31,7 @@ import { } from "tc-shared/ui/modal/permission/SenselessPermissions"; import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroupCreate"; import {spawnModalGroupPermissionCopy} from "tc-shared/ui/modal/ModalGroupPermissionCopy"; +import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; const cssStyle = require("./ModalPermissionEditor.scss"); diff --git a/shared/js/ui/modal/transfer/ModalFileTransfer.tsx b/shared/js/ui/modal/transfer/ModalFileTransfer.tsx index f4e0f4ec..68c1799c 100644 --- a/shared/js/ui/modal/transfer/ModalFileTransfer.tsx +++ b/shared/js/ui/modal/transfer/ModalFileTransfer.tsx @@ -1,4 +1,4 @@ -import {InternalModal, spawnReactModal} from "tc-shared/ui/react-elements/Modal"; +import {spawnReactModal} from "tc-shared/ui/react-elements/Modal"; import * as React from "react"; import {FileType} from "tc-shared/file/FileManager"; import {Registry} from "tc-shared/events"; @@ -12,6 +12,7 @@ import {initializeRemoteFileBrowserController} from "tc-shared/ui/modal/transfer import {ChannelEntry} from "tc-shared/ui/channel"; import {initializeTransferInfoController} from "tc-shared/ui/modal/transfer/TransferInfoController"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; +import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; const cssStyle = require("./ModalFileTransfer.scss"); export const channelPathPrefix = tr("Channel") + " "; diff --git a/shared/js/ui/react-elements/Modal.scss b/shared/js/ui/react-elements/Modal.scss deleted file mode 100644 index 7ada5f7f..00000000 --- a/shared/js/ui/react-elements/Modal.scss +++ /dev/null @@ -1,214 +0,0 @@ -@import "../../../css/static/mixin"; -@import "../../../css/static/properties"; - -html:root { - --modal-content-background: #19191b; -} - -.modal { - color: var(--text); /* base color */ - - overflow: auto; /* allow scrolling if a modal is too big */ - - background-color: rgba(0, 0, 0, 0.8); - - padding-right: 5%; - padding-left: 5%; - - z-index: 100000; - position: fixed; - - top: 0; - left: 0; - right: 0; - bottom: 0; - - display: flex; - flex-direction: column; - justify-content: center; - - opacity: 0; - margin-top: -1000vh; - - $animation_length: .3s; - @include transition(opacity $animation_length ease-in, margin-top $animation_length ease-in); - &.shown { - margin-top: 0; - opacity: 1; - } - - .dialog { - display: block; - - margin: 1.75rem 0; - - /* width calculations */ - align-items: center; - - /* height stuff */ - max-height: calc(100% - 3.5em); - - .content { - background: var(--modal-content-background); - - border: 1px solid black; - border-radius: $border_radius_middle; - - width: max-content; - max-width: 100%; - min-width: 20em; - - min-height: min-content; - - /* align us in the center */ - margin-right: auto; - margin-left: auto; - - flex-shrink: 1; - flex-grow: 0; /* we dont want a grow over the limit set within the content, but we want to shrink the content if necessary */ - align-self: center; - - overflow: hidden; - - display: flex; - flex-direction: column; - justify-content: stretch; - - .header { - background-color: #222224; - - flex-grow: 0; - flex-shrink: 0; - - display: flex; - flex-direction: row; - justify-content: stretch; - - padding: .25em; - - .icon, .buttonClose { - flex-grow: 0; - flex-shrink: 0; - } - - .buttonClose { - height: 1.4em; - width: 1.4em; - - padding: .2em; - border-radius: .2em; - - cursor: pointer; - - &:hover { - background-color: #1b1b1c; - } - } - - .icon { - margin-right: .25em; - - img { - height: 1em; - width: 1em; - } - } - - .title, { - flex-grow: 1; - flex-shrink: 1; - - color: #9d9d9e; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - h5 { - margin: 0; - padding: 0; - } - } - - .body { - max-width: 100%; - min-width: 20em; /* may adjust if needed */ - - max-height: calc(100vh - 8em); - min-height: 5em; - - overflow-y: auto; - overflow-x: auto; - - display: flex; - flex-direction: column; - } - } - } -} - -/* special modal types */ -.modal { - &.header-error .header { - background-color: #800000; - } - - &.header-info .header { - background-color: #014565; - } - - &.header-warning .header, - &.header-info .header, - &.header-error .header { - border-top-left-radius: .125rem; - border-top-right-radius: .125rem; - } -} - -.modal { - .dialog { - display: flex; - flex-direction: column; - justify-content: stretch; - - &.modal-dialog-centered { - justify-content: stretch; - } - } - - .content { - /* max-height: 500px; */ - min-height: 0; /* required for moz */ - flex-direction: column; - justify-content: stretch; - - .header { - flex-shrink: 0; - flex-grow: 0; - } - - .body { - flex-grow: 1; - flex-shrink: 1; - display: flex; - flex-direction: column; - min-height: 0; - } - } -} - -/* special general modals */ -.modal { - /* TODO! */ - .modal-body.modal-blue { - border-left: 2px solid #0a73d2; - } - .modal-body.modal-green { - border-left: 2px solid #00d400; - } - .modal-body.modal-red { - border: none; - border-left: 2px solid #d50000; - } -} \ No newline at end of file diff --git a/shared/js/ui/react-elements/Modal.tsx b/shared/js/ui/react-elements/Modal.tsx index 73c6d0ad..0b28f303 100644 --- a/shared/js/ui/react-elements/Modal.tsx +++ b/shared/js/ui/react-elements/Modal.tsx @@ -1,195 +1,4 @@ -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import {ReactElement} from "react"; -import {Registry} from "tc-shared/events"; -import {Translatable} from "tc-shared/ui/react-elements/i18n"; - -const cssStyle = require("./Modal.scss"); - -export type ModalType = "error" | "warning" | "info" | "none"; - -export interface ModalOptions { - destroyOnClose?: boolean; - - defaultSize?: { width: number, height: number }; -} - -export interface ModalEvents { - "open": {}, - "close": {}, - - /* create is implicitly at object creation */ - "destroy": {} -} - -export enum ModalState { - SHOWN, - HIDDEN, - DESTROYED -} - -export interface ModalController { - getOptions() : Readonly; - getEvents() : Registry; - getState() : ModalState; - - show() : Promise; - hide() : Promise; - - destroy(); -} - -export abstract class AbstractModal { - protected constructor() {} - - abstract renderBody() : ReactElement; - abstract title() : string | React.ReactElement; - - /* only valid for the "inline" modals */ - type() : ModalType { return "none"; } - - protected onInitialize() {} - protected onDestroy() {} - - protected onClose() {} - protected onOpen() {} -} - -export class InternalModalController implements ModalController { - readonly events: Registry; - readonly modalInstance: InstanceType; - - private initializedPromise: Promise; - - private domElement: Element; - private refModal: React.RefObject; - private modalState_: ModalState = ModalState.HIDDEN; - - constructor(instance: InstanceType) { - this.modalInstance = instance; - this.events = new Registry(); - this.initialize(); - } - - getOptions(): Readonly { - /* FIXME! */ - return {}; - } - - getEvents(): Registry { - return this.events; - } - - getState() { - return this.modalState_; - } - - private initialize() { - this.refModal = React.createRef(); - this.domElement = document.createElement("div"); - - const element = ; - document.body.appendChild(this.domElement); - this.initializedPromise = new Promise(resolve => { - ReactDOM.render(element, this.domElement, () => setTimeout(resolve, 0)); - }); - - this.modalInstance["onInitialize"](); - } - - async show() : Promise { - await this.initializedPromise; - if(this.modalState_ === ModalState.DESTROYED) - throw tr("modal has been destroyed"); - else if(this.modalState_ === ModalState.SHOWN) - return; - - this.refModal.current?.setState({ show: true }); - this.modalState_ = ModalState.SHOWN; - this.modalInstance["onOpen"](); - this.events.fire("open"); - } - - async hide() : Promise { - await this.initializedPromise; - if(this.modalState_ === ModalState.DESTROYED) - throw tr("modal has been destroyed"); - else if(this.modalState_ === ModalState.HIDDEN) - return; - - this.refModal.current?.setState({ show: false }); - this.modalState_ = ModalState.HIDDEN; - this.modalInstance["onClose"](); - this.events.fire("close"); - - return new Promise(resolve => setTimeout(resolve, 500)); - } - - destroy() { - if(this.modalState_ === ModalState.SHOWN) { - this.hide().then(() => this.destroy()); - return; - } - - ReactDOM.unmountComponentAtNode(this.domElement); - this.domElement.remove(); - - this.domElement = undefined; - this.modalState_ = ModalState.DESTROYED; - this.modalInstance["onDestroy"](); - this.events.fire("destroy"); - } -} - -export abstract class InternalModal extends AbstractModal { } - -class InternalModalRenderer extends React.PureComponent<{ controller: InternalModalController }, { show: boolean }> { - private readonly refModal = React.createRef(); - - constructor(props) { - super(props); - - this.state = { show: false }; - } - - render() { - const modal = this.props.controller.modalInstance; - let modalExtraClass = ""; - - const type = modal.type(); - if(typeof type === "string" && type !== "none") - modalExtraClass = cssStyle["modal-type-" + type]; - - const showClass = this.state.show ? cssStyle.shown : ""; - return ( -
this.onBackdropClick(event)} ref={this.refModal}> -
-
-
-
- {tr("Modal -
-
{modal.title()}
-
this.props.controller.destroy() }> -
-
-
-
- {modal.renderBody()} -
-
-
-
- ); - } - - private onBackdropClick(event: React.MouseEvent) { - if(event.target !== this.refModal.current || event.isDefaultPrevented()) - return; - - this.props.controller.destroy(); - } -} +import {InternalModal, InternalModalController} from "tc-shared/ui/react-elements/internal-modal/Controller"; export function spawnReactModal(modalClass: new () => ModalClass) : InternalModalController; export function spawnReactModal(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : InternalModalController; diff --git a/shared/js/ui/react-elements/ModalDefinitions.ts b/shared/js/ui/react-elements/ModalDefinitions.ts new file mode 100644 index 00000000..b13ce834 --- /dev/null +++ b/shared/js/ui/react-elements/ModalDefinitions.ts @@ -0,0 +1,58 @@ +import * as React from "react"; +import {ReactElement} from "react"; +import {Registry} from "tc-shared/events"; +import {Translatable} from "tc-shared/ui/react-elements/i18n"; + +export type ModalType = "error" | "warning" | "info" | "none"; + +export interface ModalOptions { + destroyOnClose?: boolean; + + defaultSize?: { width: number, height: number }; +} + +export interface ModalEvents { + "open": {}, + "close": {}, + + /* create is implicitly at object creation */ + "destroy": {} +} + +export enum ModalState { + SHOWN, + HIDDEN, + DESTROYED +} + +export interface ModalController { + getOptions() : Readonly; + getEvents() : Registry; + getState() : ModalState; + + show() : Promise; + hide() : Promise; + + destroy(); +} + +export abstract class AbstractModal { + protected constructor() {} + + abstract renderBody() : ReactElement; + abstract title() : string | React.ReactElement; + + /* only valid for the "inline" modals */ + type() : ModalType { return "none"; } + + protected onInitialize() {} + protected onDestroy() {} + + protected onClose() {} + protected onOpen() {} +} + + +export interface ModalRenderer { + renderModal(modal: AbstractModal | undefined); +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/Controller.ts b/shared/js/ui/react-elements/external-modal/Controller.ts index 6b0b6e1a..18a0a18b 100644 --- a/shared/js/ui/react-elements/external-modal/Controller.ts +++ b/shared/js/ui/react-elements/external-modal/Controller.ts @@ -8,7 +8,7 @@ import { Popout2ControllerMessages, PopoutIPCMessage } from "tc-shared/ui/react-elements/external-modal/IPCMessage"; -import {ModalController, ModalEvents, ModalOptions, ModalState} from "tc-shared/ui/react-elements/Modal"; +import {ModalController, ModalEvents, ModalOptions, ModalState} from "tc-shared/ui/react-elements/ModalDefinitions"; export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalController { public readonly modalType: string; @@ -160,6 +160,10 @@ export abstract class AbstractExternalModalController extends EventControllerBas /* already handled by out base class */ break; + case "invoke-modal-action": + /* must be handled by the underlying handler */ + break; + default: log.warn(LogCategory.IPC, "Received unknown message type from popup window: %s", type); return; diff --git a/shared/js/ui/react-elements/external-modal/IPCMessage.ts b/shared/js/ui/react-elements/external-modal/IPCMessage.ts index 8cb9df49..af348116 100644 --- a/shared/js/ui/react-elements/external-modal/IPCMessage.ts +++ b/shared/js/ui/react-elements/external-modal/IPCMessage.ts @@ -13,17 +13,22 @@ export interface PopoutIPCMessage { "fire-event-callback": { callbackId: string + }, + + "invoke-modal-action": { + action: "close" | "minimize" } } export type Controller2PopoutMessages = "hello-controller" | "fire-event" | "fire-event-callback"; -export type Popout2ControllerMessages = "hello-popout" | "fire-event" | "fire-event-callback"; +export type Popout2ControllerMessages = "hello-popout" | "fire-event" | "fire-event-callback" | "invoke-modal-action"; -interface SendIPCMessage { +export interface SendIPCMessage { "controller": Controller2PopoutMessages; "popout": Popout2ControllerMessages; } -interface ReceivedIPCMessage { + +export interface ReceivedIPCMessage { "controller": Popout2ControllerMessages; "popout": Controller2PopoutMessages; } diff --git a/shared/js/ui/react-elements/external-modal/PopoutController.ts b/shared/js/ui/react-elements/external-modal/PopoutController.ts index d2b230be..ce80e284 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutController.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutController.ts @@ -87,4 +87,12 @@ class PopoutController extends EventControllerBase<"popout"> { getUserData() { return this.userData; } + + doClose() { + this.sendIPCMessage("invoke-modal-action", { action: "close" }); + } + + doMinimize() { + this.sendIPCMessage("invoke-modal-action", { action: "minimize" }); + } } \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts index 5d301aeb..67879ecf 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts @@ -1,5 +1,3 @@ -import * as log from "tc-shared/log"; -import {LogCategory} from "tc-shared/log"; import * as loader from "tc-loader"; import * as ipc from "../../../ipc/BrowserIPC"; import * as i18n from "../../../i18n/localize"; @@ -7,16 +5,15 @@ import * as i18n from "../../../i18n/localize"; import "tc-shared/proto"; import {Stage} from "tc-loader"; -import {AbstractModal} from "tc-shared/ui/react-elements/Modal"; +import {AbstractModal, ModalRenderer} from "tc-shared/ui/react-elements/ModalDefinitions"; import {Settings, SettingsKey} from "tc-shared/settings"; import {getPopoutController} from "./PopoutController"; import {findPopoutHandler} from "tc-shared/ui/react-elements/external-modal/PopoutRegistry"; -import {bodyRenderer, titleRenderer} from "tc-shared/ui/react-elements/external-modal/PopoutRenderer"; import {Registry} from "tc-shared/events"; +import {WebModalRenderer} from "tc-shared/ui/react-elements/external-modal/PopoutRendererWeb"; +import {ClientModalRenderer} from "tc-shared/ui/react-elements/external-modal/PopoutRendererClient"; -log.info(LogCategory.GENERAL, "Hello World"); -console.error("External modal said hello!"); - +let modalRenderer: ModalRenderer; let modalInstance: AbstractModal; let modalClass: new (events: Registry, userData: any) => AbstractModal; @@ -43,6 +40,26 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { } }); + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "modal renderer loader", + priority: 10, + function: async () => { + if(__build.target === "web") { + modalRenderer = new WebModalRenderer(); + } else { + modalRenderer = new ClientModalRenderer({ + close() { + getPopoutController().doClose() + }, + minimize() { + getPopoutController().doMinimize() + } + }); + } + } +}); + loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { name: "modal class loader", priority: 10, @@ -70,8 +87,7 @@ loader.register_task(Stage.LOADED, { function: async () => { try { modalInstance = new modalClass(getPopoutController().getEventRegistry(), getPopoutController().getUserData()); - titleRenderer.setInstance(modalInstance); - bodyRenderer.setInstance(modalInstance); + modalRenderer.renderModal(modalInstance); } catch(error) { loader.critical_error("Failed to invoker modal", "Lookup the console for more detail"); console.error("Failed to load modal: %o", error); diff --git a/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts b/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts index 4f1a8736..2b15f0c9 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts @@ -1,4 +1,4 @@ -import {AbstractModal} from "tc-shared/ui/react-elements/Modal"; +import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; export interface PopoutHandler { name: string; diff --git a/shared/js/ui/react-elements/external-modal/PopoutRenderer.scss b/shared/js/ui/react-elements/external-modal/PopoutRenderer.scss index 5702744c..c9bc1107 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutRenderer.scss +++ b/shared/js/ui/react-elements/external-modal/PopoutRenderer.scss @@ -1,3 +1,5 @@ +@import "../../../../css/static/mixin"; + /* FIXME: Remove this wired import */ :global { @import "../../../../css/static/general"; @@ -10,6 +12,8 @@ html, body { height: 100vh; width: 100vw; + + background: #212529; } .container { @@ -28,4 +32,25 @@ html, body { max-width: 100vw; overflow: hidden; +} + +.container { + margin: 0!important; + + height: 100% !important; + width: 100% !important; + + .header { + -webkit-user-select: none; + -webkit-app-region: drag; + + .title { + --stay-alive: none; + } + } + + .body { + overflow: auto; + @include chat-scrollbar(); + } } \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/PopoutRendererClient.tsx b/shared/js/ui/react-elements/external-modal/PopoutRendererClient.tsx new file mode 100644 index 00000000..e9f909ea --- /dev/null +++ b/shared/js/ui/react-elements/external-modal/PopoutRendererClient.tsx @@ -0,0 +1,79 @@ +import {AbstractModal, ModalRenderer} from "tc-shared/ui/react-elements/ModalDefinitions"; +import * as ReactDOM from "react-dom"; +import {InternalModalContentRenderer} from "tc-shared/ui/react-elements/internal-modal/Renderer"; +import * as React from "react"; + +export interface ModalControlFunctions { + close(); + minimize(); +} + +const cssStyle = require("./PopoutRenderer.scss"); +export class ClientModalRenderer implements ModalRenderer { + private readonly functionController: ModalControlFunctions; + + private readonly titleElement: HTMLTitleElement; + private readonly container: HTMLDivElement; + private readonly titleChangeObserver: MutationObserver; + + private titleContainer: HTMLDivElement; + private currentModal: AbstractModal; + + constructor(functionController: ModalControlFunctions) { + this.functionController = functionController; + + this.container = document.createElement("div"); + this.container.classList.add(cssStyle.container); + + const titleElements = document.getElementsByTagName("title"); + if(titleElements.length === 0) { + this.titleElement = document.createElement("title"); + document.head.appendChild(this.titleElement); + } else { + this.titleElement = titleElements[0]; + } + + document.body.append(this.container); + + this.titleChangeObserver = new MutationObserver(() => this.updateTitle()); + } + + renderModal(modal: AbstractModal | undefined) { + if(this.currentModal === modal) + return; + + this.titleChangeObserver.disconnect(); + ReactDOM.unmountComponentAtNode(this.container); + this.currentModal = modal; + ReactDOM.render( + this.functionController.close()} + onMinimize={() => this.functionController.minimize()} + + containerClass={cssStyle.container} + headerClass={cssStyle.header} + headerTitleClass={cssStyle.title} + bodyClass={cssStyle.body} + />, + this.container, + () => { + this.titleContainer = this.container.querySelector("." + cssStyle.title) as HTMLDivElement; + this.titleChangeObserver.observe(this.titleContainer, { + attributes: true, + subtree: true, + childList: true, + characterData: true + }); + this.updateTitle(); + } + ); + } + + private updateTitle() { + if(!this.titleContainer) + return; + + this.titleElement.innerText = this.titleContainer.textContent; + } +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/PopoutRenderer.tsx b/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx similarity index 69% rename from shared/js/ui/react-elements/external-modal/PopoutRenderer.tsx rename to shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx index ff70d364..ef1b147c 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutRenderer.tsx +++ b/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; - -import {AbstractModal} from "tc-shared/ui/react-elements/Modal"; +import {AbstractModal, ModalRenderer} from "tc-shared/ui/react-elements/ModalDefinitions"; const cssStyle = require("./PopoutRenderer.scss"); @@ -27,7 +26,6 @@ class TitleRenderer { ReactDOM.render(<>{this.modalInstance.title()}, this.htmlContainer); } } -export const titleRenderer = new TitleRenderer(); class BodyRenderer { private readonly htmlContainer: HTMLElement; @@ -48,4 +46,24 @@ class BodyRenderer { ReactDOM.render(<>{this.modalInstance.renderBody()}, this.htmlContainer); } } -export const bodyRenderer = new BodyRenderer(); \ No newline at end of file + +export class WebModalRenderer implements ModalRenderer { + private readonly titleRenderer: TitleRenderer; + private readonly bodyRenderer: BodyRenderer; + + private currentModal: AbstractModal; + + constructor() { + this.titleRenderer = new TitleRenderer(); + this.bodyRenderer = new BodyRenderer(); + } + + renderModal(modal: AbstractModal | undefined) { + if(this.currentModal === modal) + return; + + this.currentModal = modal; + this.titleRenderer.setInstance(modal); + this.bodyRenderer.setInstance(modal); + } +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/index.ts b/shared/js/ui/react-elements/external-modal/index.ts index 2b174996..5b6a93e3 100644 --- a/shared/js/ui/react-elements/external-modal/index.ts +++ b/shared/js/ui/react-elements/external-modal/index.ts @@ -1,6 +1,6 @@ import {Registry} from "tc-shared/events"; -import {ModalController} from "tc-shared/ui/react-elements/Modal"; -import "./Controller"; /* we've to reference him here, else the client would not */ +import "./Controller"; +import {ModalController} from "tc-shared/ui/react-elements/ModalDefinitions"; /* we've to reference him here, else the client would not */ export type ControllerFactory = (modal: string, events: Registry, userData: any) => ModalController; let modalControllerFactory: ControllerFactory; diff --git a/shared/js/ui/react-elements/internal-modal/Controller.ts b/shared/js/ui/react-elements/internal-modal/Controller.ts new file mode 100644 index 00000000..ed87c654 --- /dev/null +++ b/shared/js/ui/react-elements/internal-modal/Controller.ts @@ -0,0 +1,97 @@ +import {Registry} from "tc-shared/events"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import {AbstractModal, ModalController, ModalEvents, ModalOptions, ModalState} from "tc-shared/ui/react-elements/ModalDefinitions"; +import {InternalModalRenderer} from "tc-shared/ui/react-elements/internal-modal/Renderer"; + +export class InternalModalController implements ModalController { + readonly events: Registry; + readonly modalInstance: InstanceType; + + private initializedPromise: Promise; + + private domElement: Element; + private refModal: React.RefObject; + private modalState_: ModalState = ModalState.HIDDEN; + + constructor(instance: InstanceType) { + this.modalInstance = instance; + this.events = new Registry(); + this.initialize(); + } + + getOptions(): Readonly { + /* FIXME! */ + return {}; + } + + getEvents(): Registry { + return this.events; + } + + getState() { + return this.modalState_; + } + + private initialize() { + this.refModal = React.createRef(); + this.domElement = document.createElement("div"); + + const element = React.createElement(InternalModalRenderer, { + ref: this.refModal, + modal: this.modalInstance, + onClose: () => this.destroy() + }); + document.body.appendChild(this.domElement); + this.initializedPromise = new Promise(resolve => { + ReactDOM.render(element, this.domElement, () => setTimeout(resolve, 0)); + }); + + this.modalInstance["onInitialize"](); + } + + async show() : Promise { + await this.initializedPromise; + if(this.modalState_ === ModalState.DESTROYED) + throw tr("modal has been destroyed"); + else if(this.modalState_ === ModalState.SHOWN) + return; + + this.refModal.current?.setState({ show: true }); + this.modalState_ = ModalState.SHOWN; + this.modalInstance["onOpen"](); + this.events.fire("open"); + } + + async hide() : Promise { + await this.initializedPromise; + if(this.modalState_ === ModalState.DESTROYED) + throw tr("modal has been destroyed"); + else if(this.modalState_ === ModalState.HIDDEN) + return; + + this.refModal.current?.setState({ show: false }); + this.modalState_ = ModalState.HIDDEN; + this.modalInstance["onClose"](); + this.events.fire("close"); + + return new Promise(resolve => setTimeout(resolve, 500)); + } + + destroy() { + if(this.modalState_ === ModalState.SHOWN) { + this.hide().then(() => this.destroy()); + return; + } + + ReactDOM.unmountComponentAtNode(this.domElement); + this.domElement.remove(); + + this.domElement = undefined; + this.modalState_ = ModalState.DESTROYED; + this.modalInstance["onDestroy"](); + this.events.fire("destroy"); + } +} + +export abstract class InternalModal extends AbstractModal { } \ No newline at end of file diff --git a/shared/js/ui/react-elements/internal-modal/Modal.scss b/shared/js/ui/react-elements/internal-modal/Modal.scss new file mode 100644 index 00000000..48a683ee --- /dev/null +++ b/shared/js/ui/react-elements/internal-modal/Modal.scss @@ -0,0 +1,231 @@ +@import "../../../../css/static/mixin"; +@import "../../../../css/static/properties"; + +html:root { + --modal-content-background: #19191b; +} + +.modal { + color: var(--text); /* base color */ + + overflow: auto; /* allow scrolling if a modal is too big */ + + background-color: rgba(0, 0, 0, 0.8); + + padding-right: 5%; + padding-left: 5%; + + z-index: 100000; + position: fixed; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: flex; + flex-direction: column; + justify-content: center; + + opacity: 0; + margin-top: -1000vh; + + $animation_length: .3s; + @include transition(opacity $animation_length ease-in, margin-top $animation_length ease-in); + &.shown { + margin-top: 0; + opacity: 1; + } + + .dialog { + display: block; + + margin: 1.75rem 0; + + /* width calculations */ + align-items: center; + + /* height stuff */ + max-height: calc(100% - 3.5em); + } +} + +/* special modal types */ +.modal { + &.header-error .header { + background-color: #800000; + } + + &.header-info .header { + background-color: #014565; + } + + &.header-warning .header, + &.header-info .header, + &.header-error .header { + border-top-left-radius: .125rem; + border-top-right-radius: .125rem; + } +} + +.modal { + .dialog { + display: flex; + flex-direction: column; + justify-content: stretch; + + &.modal-dialog-centered { + justify-content: stretch; + } + } +} + +/* special general modals */ +.modal { + /* TODO! */ + .modal-body.modal-blue { + border-left: 2px solid #0a73d2; + } + .modal-body.modal-green { + border-left: 2px solid #00d400; + } + .modal-body.modal-red { + border: none; + border-left: 2px solid #d50000; + } +} + + +.content { + background: var(--modal-content-background); + + border: 1px solid black; + border-radius: $border_radius_middle; + + width: max-content; + max-width: 100%; + min-width: 20em; + + min-height: min-content; + max-height: 100%; + + flex-shrink: 1; + flex-grow: 0; /* we dont want a grow over the limit set within the content, but we want to shrink the content if necessary */ + align-self: center; + + overflow: hidden; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .header { + background-color: #222224; + + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + padding: .25em; + + .icon, .button { + flex-grow: 0; + flex-shrink: 0; + } + + .button { + height: 1.4em; + width: 1.4em; + + padding: .2em; + border-radius: .2em; + + cursor: pointer; + + -webkit-app-region: no-drag; + pointer-events: all; + + &:hover { + background-color: #1b1b1c; + } + } + + .icon { + margin-left: .25em; + margin-right: .5em; + + display: flex; + flex-direction: column; + justify-content: center; + + img { + height: 1.2em; + width: 1.2em; + margin-bottom: .2em; + } + } + + .title, { + flex-grow: 1; + flex-shrink: 1; + + color: #9d9d9e; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + h5 { + margin: 0; + padding: 0; + } + } + + .body { + max-width: 100%; + min-width: 20em; /* may adjust if needed */ + + min-height: 5em; + + overflow-y: auto; + overflow-x: auto; + + display: flex; + flex-direction: column; + position: relative; + } +} + +.content { + /* max-height: 500px; */ + min-height: 0; /* required for moz */ + flex-direction: column; + justify-content: stretch; + + .header { + flex-shrink: 0; + flex-grow: 0; + } + + .body { + flex-grow: 1; + flex-shrink: 1; + display: flex; + flex-direction: column; + min-height: 0; + } +} + +.contentInternal { + /* align us in the center */ + margin-right: auto; + margin-left: auto; + + .body { + max-height: calc(100vh - 8em); /* leave some space at the bottom */ + } +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/internal-modal/Renderer.tsx b/shared/js/ui/react-elements/internal-modal/Renderer.tsx new file mode 100644 index 00000000..1c4fdfc6 --- /dev/null +++ b/shared/js/ui/react-elements/internal-modal/Renderer.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; +import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; +import {ClientIcon} from "svg-sprites/client-icons"; + +const cssStyle = require("./Modal.scss"); + +export const InternalModalContentRenderer = (props: { + modal: AbstractModal, + + onClose?: () => void, + onMinimize?: () => void, + + containerClass?: string, + headerClass?: string, + headerTitleClass?: string, + bodyClass?: string, + + refContent?: React.Ref +}) => { + return ( +
+
+
+ {tr("Modal +
+
{props.modal.title()}
+ {!props.onMinimize ? undefined : ( +
+
+
+ )} + {!props.onClose ? undefined : ( +
+
+
+ )} +
+
+ {props.modal.renderBody()} +
+
+ ); +}; + +export class InternalModalRenderer extends React.PureComponent<{ modal: AbstractModal, onClose: () => void }, { show: boolean }> { + private readonly refModal = React.createRef(); + + constructor(props) { + super(props); + + this.state = { show: false }; + } + + render() { + let modalExtraClass = ""; + + const type = this.props.modal.type(); + if(typeof type === "string" && type !== "none") + modalExtraClass = cssStyle["modal-type-" + type]; + + const showClass = this.state.show ? cssStyle.shown : ""; + return ( +
this.onBackdropClick(event)} ref={this.refModal}> +
+ +
+
+ ); + } + + private onBackdropClick(event: React.MouseEvent) { + if(event.target !== this.refModal.current || event.isDefaultPrevented()) + return; + + this.props.onClose(); + } +} \ No newline at end of file diff --git a/shared/js/video-viewer/Controller.ts b/shared/js/video-viewer/Controller.ts index 4bec0e44..1c11d8c2 100644 --- a/shared/js/video-viewer/Controller.ts +++ b/shared/js/video-viewer/Controller.ts @@ -5,11 +5,11 @@ import {EventHandler, Registry} from "tc-shared/events"; import {VideoViewerEvents} from "./Definitions"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {W2GPluginCmdHandler, W2GWatcher, W2GWatcherFollower} from "tc-shared/video-viewer/W2GPlugin"; -import {ModalController} from "tc-shared/ui/react-elements/Modal"; import {settings, Settings} from "tc-shared/settings"; import {global_client_actions} from "tc-shared/events/GlobalEvents"; import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {createErrorModal} from "tc-shared/ui/elements/Modal"; +import {ModalController} from "tc-shared/ui/react-elements/ModalDefinitions"; const parseWatcherId = (id: string): { clientId: number, clientUniqueId: string} => { const [ clientIdString, clientUniqueId ] = id.split(" - "); diff --git a/shared/js/video-viewer/Renderer.scss b/shared/js/video-viewer/Renderer.scss index ac061296..42db2d00 100644 --- a/shared/js/video-viewer/Renderer.scss +++ b/shared/js/video-viewer/Renderer.scss @@ -14,6 +14,15 @@ $sidebar-width: 20em; min-height: 10em; min-width: 20em; + + position: absolute; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + overflow: hidden; } .containerPlayer { @@ -26,6 +35,8 @@ $sidebar-width: 20em; display: flex; flex-direction: row; justify-content: stretch; + + position: relative; } .sidebarButton { diff --git a/shared/js/video-viewer/Renderer.tsx b/shared/js/video-viewer/Renderer.tsx index 2a9af775..5223c9aa 100644 --- a/shared/js/video-viewer/Renderer.tsx +++ b/shared/js/video-viewer/Renderer.tsx @@ -1,6 +1,5 @@ import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; -import {AbstractModal} from "tc-shared/ui/react-elements/Modal"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import * as React from "react"; import {useEffect, useRef, useState} from "react"; @@ -15,6 +14,7 @@ import "tc-shared/file/RemoteAvatars"; import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar"; import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars"; import {Settings, settings} from "tc-shared/settings"; +import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; const iconNavbar = require("./icon-navbar.svg"); const cssStyle = require("./Renderer.scss"); diff --git a/shared/svg-sprites/client-icons.d.ts b/shared/svg-sprites/client-icons.d.ts index 31075c9a..2d574769 100644 --- a/shared/svg-sprites/client-icons.d.ts +++ b/shared/svg-sprites/client-icons.d.ts @@ -3,9 +3,9 @@ * * This file has been auto generated by the svg-sprite generator. * Sprite source directory: D:\TeaSpeak\web\shared\img\client-icons - * Sprite count: 200 + * Sprite count: 201 */ -export type ClientIconClass = "client-about" | "client-activate_microphone" | "client-add" | "client-add_foe" | "client-add_folder" | "client-add_friend" | "client-addon-collection" | "client-addon" | "client-apply" | "client-arrow_down" | "client-arrow_left" | "client-arrow_right" | "client-arrow_up" | "client-away" | "client-ban_client" | "client-ban_list" | "client-bookmark_add" | "client-bookmark_add_folder" | "client-bookmark_duplicate" | "client-bookmark_manager" | "client-bookmark_remove" | "client-broken_image" | "client-browse-addon-online" | "client-capture" | "client-change_nickname" | "client-changelog" | "client-channel_chat" | "client-channel_collapse_all" | "client-channel_commander" | "client-channel_create" | "client-channel_create_sub" | "client-channel_default" | "client-channel_delete" | "client-channel_edit" | "client-channel_expand_all" | "client-channel_green" | "client-channel_green_subscribed" | "client-channel_green_subscribed2" | "client-channel_private" | "client-channel_red" | "client-channel_red_subscribed" | "client-channel_switch" | "client-channel_unsubscribed" | "client-channel_yellow" | "client-channel_yellow_subscribed" | "client-check_update" | "client-client_hide" | "client-client_show" | "client-close_button" | "client-complaint_list" | "client-conflict-icon" | "client-connect" | "client-contact" | "client-copy" | "client-copy_url" | "client-d_sound" | "client-d_sound_me" | "client-d_sound_user" | "client-default" | "client-default_for_all_bookmarks" | "client-delete" | "client-delete_avatar" | "client-disconnect" | "client-down" | "client-download" | "client-edit" | "client-edit_friend_foe_status" | "client-emoticon" | "client-error" | "client-file_home" | "client-file_refresh" | "client-filetransfer" | "client-find" | "client-folder" | "client-folder_up" | "client-group_100" | "client-group_200" | "client-group_300" | "client-group_500" | "client-group_600" | "client-guisetup" | "client-hardware_input_muted" | "client-hardware_output_muted" | "client-home" | "client-hoster_button" | "client-hotkeys" | "client-icon-pack" | "client-iconsview" | "client-iconviewer" | "client-identity_default" | "client-identity_export" | "client-identity_import" | "client-identity_manager" | "client-info" | "client-input_muted" | "client-input_muted_local" | "client-invite_buddy" | "client-is_talker" | "client-kick_channel" | "client-kick_server" | "client-listview" | "client-loading_image" | "client-message_incoming" | "client-message_info" | "client-message_outgoing" | "client-messages" | "client-moderated" | "client-move_client_to_own_channel" | "client-music" | "client-new_chat" | "client-notifications" | "client-offline_messages" | "client-on_whisperlist" | "client-output_muted" | "client-permission_channel" | "client-permission_client" | "client-permission_overview" | "client-permission_server_groups" | "client-phoneticsnickname" | "client-ping_1" | "client-ping_2" | "client-ping_3" | "client-ping_4" | "client-ping_calculating" | "client-ping_disconnected" | "client-play" | "client-player_chat" | "client-player_commander_off" | "client-player_commander_on" | "client-player_off" | "client-player_on" | "client-player_whisper" | "client-plugins" | "client-poke" | "client-present" | "client-recording_start" | "client-recording_stop" | "client-refresh" | "client-register" | "client-reload" | "client-remove_foe" | "client-remove_friend" | "client-security" | "client-selectfolder" | "client-send_complaint" | "client-server_green" | "client-server_log" | "client-server_query" | "client-settings" | "client-sort_by_name" | "client-sound-pack" | "client-soundpack" | "client-stop" | "client-subscribe_mode" | "client-subscribe_to_all_channels" | "client-subscribe_to_channel" | "client-subscribe_to_channel_family" | "client-switch_advanced" | "client-switch_standard" | "client-sync-disable" | "client-sync-enable" | "client-sync-icon" | "client-tab_close_button" | "client-talk_power_grant" | "client-talk_power_grant_next" | "client-talk_power_request" | "client-talk_power_request_cancel" | "client-talk_power_revoke" | "client-talk_power_revoke_all_grant_next" | "client-temp_server_password" | "client-temp_server_password_add" | "client-textformat" | "client-textformat_bold" | "client-textformat_foreground" | "client-textformat_italic" | "client-textformat_underline" | "client-theme" | "client-toggle_server_query_clients" | "client-toggle_whisper" | "client-token" | "client-token_use" | "client-translation" | "client-unsubscribe_from_all_channels" | "client-unsubscribe_from_channel_family" | "client-unsubscribe_mode" | "client-up" | "client-upload" | "client-upload_avatar" | "client-urlcatcher" | "client-user-account" | "client-virtualserver_edit" | "client-volume" | "client-w2g" | "client-warning" | "client-warning_external_link" | "client-warning_info" | "client-warning_question" | "client-weblist" | "client-whisper" | "client-whisperlists"; +export type ClientIconClass = "client-about" | "client-activate_microphone" | "client-add" | "client-add_foe" | "client-add_folder" | "client-add_friend" | "client-addon-collection" | "client-addon" | "client-apply" | "client-arrow_down" | "client-arrow_left" | "client-arrow_right" | "client-arrow_up" | "client-away" | "client-ban_client" | "client-ban_list" | "client-bookmark_add" | "client-bookmark_add_folder" | "client-bookmark_duplicate" | "client-bookmark_manager" | "client-bookmark_remove" | "client-broken_image" | "client-browse-addon-online" | "client-capture" | "client-change_nickname" | "client-changelog" | "client-channel_chat" | "client-channel_collapse_all" | "client-channel_commander" | "client-channel_create" | "client-channel_create_sub" | "client-channel_default" | "client-channel_delete" | "client-channel_edit" | "client-channel_expand_all" | "client-channel_green" | "client-channel_green_subscribed" | "client-channel_green_subscribed2" | "client-channel_private" | "client-channel_red" | "client-channel_red_subscribed" | "client-channel_switch" | "client-channel_unsubscribed" | "client-channel_yellow" | "client-channel_yellow_subscribed" | "client-check_update" | "client-client_hide" | "client-client_show" | "client-close_button" | "client-complaint_list" | "client-conflict-icon" | "client-connect" | "client-contact" | "client-copy" | "client-copy_url" | "client-d_sound" | "client-d_sound_me" | "client-d_sound_user" | "client-default" | "client-default_for_all_bookmarks" | "client-delete" | "client-delete_avatar" | "client-disconnect" | "client-down" | "client-download" | "client-edit" | "client-edit_friend_foe_status" | "client-emoticon" | "client-error" | "client-file_home" | "client-file_refresh" | "client-filetransfer" | "client-find" | "client-folder" | "client-folder_up" | "client-group_100" | "client-group_200" | "client-group_300" | "client-group_500" | "client-group_600" | "client-guisetup" | "client-hardware_input_muted" | "client-hardware_output_muted" | "client-home" | "client-hoster_button" | "client-hotkeys" | "client-icon-pack" | "client-iconsview" | "client-iconviewer" | "client-identity_default" | "client-identity_export" | "client-identity_import" | "client-identity_manager" | "client-info" | "client-input_muted" | "client-input_muted_local" | "client-invite_buddy" | "client-is_talker" | "client-kick_channel" | "client-kick_server" | "client-listview" | "client-loading_image" | "client-message_incoming" | "client-message_info" | "client-message_outgoing" | "client-messages" | "client-minimize_button" | "client-moderated" | "client-move_client_to_own_channel" | "client-music" | "client-new_chat" | "client-notifications" | "client-offline_messages" | "client-on_whisperlist" | "client-output_muted" | "client-permission_channel" | "client-permission_client" | "client-permission_overview" | "client-permission_server_groups" | "client-phoneticsnickname" | "client-ping_1" | "client-ping_2" | "client-ping_3" | "client-ping_4" | "client-ping_calculating" | "client-ping_disconnected" | "client-play" | "client-player_chat" | "client-player_commander_off" | "client-player_commander_on" | "client-player_off" | "client-player_on" | "client-player_whisper" | "client-plugins" | "client-poke" | "client-present" | "client-recording_start" | "client-recording_stop" | "client-refresh" | "client-register" | "client-reload" | "client-remove_foe" | "client-remove_friend" | "client-security" | "client-selectfolder" | "client-send_complaint" | "client-server_green" | "client-server_log" | "client-server_query" | "client-settings" | "client-sort_by_name" | "client-sound-pack" | "client-soundpack" | "client-stop" | "client-subscribe_mode" | "client-subscribe_to_all_channels" | "client-subscribe_to_channel" | "client-subscribe_to_channel_family" | "client-switch_advanced" | "client-switch_standard" | "client-sync-disable" | "client-sync-enable" | "client-sync-icon" | "client-tab_close_button" | "client-talk_power_grant" | "client-talk_power_grant_next" | "client-talk_power_request" | "client-talk_power_request_cancel" | "client-talk_power_revoke" | "client-talk_power_revoke_all_grant_next" | "client-temp_server_password" | "client-temp_server_password_add" | "client-textformat" | "client-textformat_bold" | "client-textformat_foreground" | "client-textformat_italic" | "client-textformat_underline" | "client-theme" | "client-toggle_server_query_clients" | "client-toggle_whisper" | "client-token" | "client-token_use" | "client-translation" | "client-unsubscribe_from_all_channels" | "client-unsubscribe_from_channel_family" | "client-unsubscribe_mode" | "client-up" | "client-upload" | "client-upload_avatar" | "client-urlcatcher" | "client-user-account" | "client-virtualserver_edit" | "client-volume" | "client-w2g" | "client-warning" | "client-warning_external_link" | "client-warning_info" | "client-warning_question" | "client-weblist" | "client-whisper" | "client-whisperlists"; export enum ClientIcon { About = "client-about", @@ -114,6 +114,7 @@ export enum ClientIcon { MessageInfo = "client-message_info", MessageOutgoing = "client-message_outgoing", Messages = "client-messages", + MinimizeButton = "client-minimize_button", Moderated = "client-moderated", MoveClientToOwnChannel = "client-move_client_to_own_channel", Music = "client-music", diff --git a/web/app/ExternalModalFactory.ts b/web/app/ExternalModalFactory.ts index 802ce064..51743006 100644 --- a/web/app/ExternalModalFactory.ts +++ b/web/app/ExternalModalFactory.ts @@ -6,6 +6,7 @@ import {Stage} from "tc-loader"; import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal"; import {ChannelMessage} from "tc-shared/ipc/BrowserIPC"; import {LogCategory, logDebug, logWarn} from "tc-shared/log"; +import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage"; class ExternalModalController extends AbstractExternalModalController { private currentWindow: Window; @@ -122,6 +123,25 @@ class ExternalModalController extends AbstractExternalModalController { super.handleIPCMessage(remoteId, broadcast, message); } + + protected handleTypedIPCMessage(type: T, payload: PopoutIPCMessage[T]) { + super.handleTypedIPCMessage(type, payload); + + switch (type) { + case "invoke-modal-action": + const data = payload as PopoutIPCMessage["invoke-modal-action"]; + switch (data.action) { + case "close": + this.destroy(); + break; + + case "minimize": + window.focus(); + break; + } + break; + } + } } loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {