Added a proper popup for the native client.

canary
WolverinDEV 2020-08-09 18:58:19 +02:00
parent 79308b1dc6
commit 3218682c92
36 changed files with 732 additions and 526 deletions

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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() {

View File

@ -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();
}
})

View File

@ -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");

View File

@ -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 */

View File

@ -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<ConversationUIEvents>;

View File

@ -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) {

View File

@ -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 {

View File

@ -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");

View File

@ -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");

View File

@ -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");

View File

@ -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");

View File

@ -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") + " ";

View File

@ -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;
}
}

View File

@ -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<ModalOptions>;
getEvents() : Registry<ModalEvents>;
getState() : ModalState;
show() : Promise<void>;
hide() : Promise<void>;
destroy();
}
export abstract class AbstractModal {
protected constructor() {}
abstract renderBody() : ReactElement;
abstract title() : string | React.ReactElement<Translatable>;
/* only valid for the "inline" modals */
type() : ModalType { return "none"; }
protected onInitialize() {}
protected onDestroy() {}
protected onClose() {}
protected onOpen() {}
}
export class InternalModalController<InstanceType extends InternalModal = InternalModal> implements ModalController {
readonly events: Registry<ModalEvents>;
readonly modalInstance: InstanceType;
private initializedPromise: Promise<void>;
private domElement: Element;
private refModal: React.RefObject<InternalModalRenderer>;
private modalState_: ModalState = ModalState.HIDDEN;
constructor(instance: InstanceType) {
this.modalInstance = instance;
this.events = new Registry<ModalEvents>();
this.initialize();
}
getOptions(): Readonly<ModalOptions> {
/* FIXME! */
return {};
}
getEvents(): Registry<ModalEvents> {
return this.events;
}
getState() {
return this.modalState_;
}
private initialize() {
this.refModal = React.createRef();
this.domElement = document.createElement("div");
const element = <InternalModalRenderer controller={this} ref={this.refModal} />;
document.body.appendChild(this.domElement);
this.initializedPromise = new Promise<void>(resolve => {
ReactDOM.render(element, this.domElement, () => setTimeout(resolve, 0));
});
this.modalInstance["onInitialize"]();
}
async show() : Promise<void> {
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<void> {
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<void>(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<HTMLDivElement>();
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 (
<div className={cssStyle.modal + " " + modalExtraClass + " " + showClass} tabIndex={-1} role={"dialog"} aria-hidden={true} onClick={event => this.onBackdropClick(event)} ref={this.refModal}>
<div className={cssStyle.dialog}>
<div className={cssStyle.content}>
<div className={cssStyle.header}>
<div className={cssStyle.icon}>
<img src="img/favicon/teacup.png" alt={tr("Modal - Icon")} />
</div>
<div className={cssStyle.title}>{modal.title()}</div>
<div className={cssStyle.buttonClose} onClick={() => this.props.controller.destroy() }>
<div className="icon_em client-close_button" />
</div>
</div>
<div className={cssStyle.body}>
{modal.renderBody()}
</div>
</div>
</div>
</div>
);
}
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 extends InternalModal, A1>(modalClass: new () => ModalClass) : InternalModalController<ModalClass>;
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : InternalModalController<ModalClass>;

View File

@ -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<ModalOptions>;
getEvents() : Registry<ModalEvents>;
getState() : ModalState;
show() : Promise<void>;
hide() : Promise<void>;
destroy();
}
export abstract class AbstractModal {
protected constructor() {}
abstract renderBody() : ReactElement;
abstract title() : string | React.ReactElement<Translatable>;
/* 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);
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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" });
}
}

View File

@ -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 <T>(events: Registry<T>, 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);

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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(
<InternalModalContentRenderer
modal={this.currentModal}
onClose={() => 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;
}
}

View File

@ -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();
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);
}
}

View File

@ -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;

View File

@ -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<InstanceType extends InternalModal = InternalModal> implements ModalController {
readonly events: Registry<ModalEvents>;
readonly modalInstance: InstanceType;
private initializedPromise: Promise<void>;
private domElement: Element;
private refModal: React.RefObject<InternalModalRenderer>;
private modalState_: ModalState = ModalState.HIDDEN;
constructor(instance: InstanceType) {
this.modalInstance = instance;
this.events = new Registry<ModalEvents>();
this.initialize();
}
getOptions(): Readonly<ModalOptions> {
/* FIXME! */
return {};
}
getEvents(): Registry<ModalEvents> {
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<void>(resolve => {
ReactDOM.render(element, this.domElement, () => setTimeout(resolve, 0));
});
this.modalInstance["onInitialize"]();
}
async show() : Promise<void> {
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<void> {
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<void>(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 { }

View File

@ -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 */
}
}

View File

@ -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<HTMLDivElement>
}) => {
return (
<div className={cssStyle.content + " " + props.containerClass} ref={props.refContent}>
<div className={cssStyle.header + " " + props.headerClass}>
<div className={cssStyle.icon}>
<img src="img/favicon/teacup.png" alt={tr("Modal - Icon")} />
</div>
<div className={cssStyle.title + " " + props.headerTitleClass}>{props.modal.title()}</div>
{!props.onMinimize ? undefined : (
<div className={cssStyle.button} onClick={props.onMinimize}>
<div className={"icon_em " + ClientIcon.MinimizeButton} />
</div>
)}
{!props.onClose ? undefined : (
<div className={cssStyle.button} onClick={props.onClose}>
<div className={"icon_em " + ClientIcon.CloseButton} />
</div>
)}
</div>
<div className={cssStyle.body + " " + props.bodyClass}>
{props.modal.renderBody()}
</div>
</div>
);
};
export class InternalModalRenderer extends React.PureComponent<{ modal: AbstractModal, onClose: () => void }, { show: boolean }> {
private readonly refModal = React.createRef<HTMLDivElement>();
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 (
<div className={cssStyle.modal + " " + modalExtraClass + " " + showClass} tabIndex={-1} role={"dialog"} aria-hidden={true} onClick={event => this.onBackdropClick(event)} ref={this.refModal}>
<div className={cssStyle.dialog}>
<InternalModalContentRenderer
modal={this.props.modal}
onClose={this.props.onClose}
containerClass={cssStyle.contentInternal}
bodyClass={cssStyle.body}
/>
</div>
</div>
);
}
private onBackdropClick(event: React.MouseEvent) {
if(event.target !== this.refModal.current || event.isDefaultPrevented())
return;
this.props.onClose();
}
}

View File

@ -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(" - ");

View File

@ -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 {

View File

@ -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");

View File

@ -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",

View File

@ -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<T extends Popout2ControllerMessages>(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, {