Added a proper popup for the native client.
parent
79308b1dc6
commit
3218682c92
|
@ -3,6 +3,8 @@
|
|||
- 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
|
||||
|
|
|
@ -81,7 +81,12 @@ export async function initialize() {
|
|||
StageNames[Stage.LOADED] = "starting app";
|
||||
|
||||
overlay.classList.add("initialized");
|
||||
|
||||
if(parseInt(getUrlParameter("animation-short")) === 1) {
|
||||
setupAnimationFinished();
|
||||
} else {
|
||||
setupContainer.classList.add("visible");
|
||||
}
|
||||
|
||||
initializeTimestamp = Date.now();
|
||||
return true;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
})
|
|
@ -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("Settings"));
|
||||
item.icon("client-settings");
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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") + " ";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" });
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
@ -29,3 +33,24 @@ html, body {
|
|||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 { }
|
|
@ -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 */
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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(" - ");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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, {
|
||||
|
|
Loading…
Reference in New Issue