Added the possibility to popout modals and finished the work on the bookmark modals
parent
95f4a7bfb7
commit
1b2de3ed63
|
@ -88,6 +88,31 @@ function handleIpcMessage(type: string, payload: any) {
|
||||||
channel?.showContextMenu(pageX, pageY);
|
channel?.showContextMenu(pageX, pageY);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "contextmenu-server": {
|
||||||
|
const {
|
||||||
|
handlerId,
|
||||||
|
serverUniqueId,
|
||||||
|
|
||||||
|
pageX,
|
||||||
|
pageY
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
if(typeof pageX !== "number" || typeof pageY !== "number") {
|
||||||
|
logWarn(LogCategory.IPC, tr("Received server context menu action with an invalid page coordinated: %ox%o."), pageX, pageY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof handlerId !== "string") {
|
||||||
|
logWarn(LogCategory.IPC, tr("Received server context menu action with an invalid handler id: %o."), handlerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = server_connections.findConnection(handlerId);
|
||||||
|
if(!handler?.connected || (serverUniqueId && handler.getCurrentServerUniqueId() !== serverUniqueId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler.channelTree.server.showContextMenu(pageX, pageY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,18 @@ export function parseServerAddress(address: string) : ServerAddress | undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stringifyServerAddress(address: ServerAddress) : string {
|
||||||
|
let result = address.host;
|
||||||
|
if(address.port !== 9987) {
|
||||||
|
if(address.host.indexOf(":") === -1) {
|
||||||
|
result += ":" + address.port;
|
||||||
|
} else {
|
||||||
|
result = "[" + result + "]:" + address.port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerEvents extends ChannelTreeEntryEvents {
|
export interface ServerEvents extends ChannelTreeEntryEvents {
|
||||||
notify_properties_updated: {
|
notify_properties_updated: {
|
||||||
updated_properties: Partial<ServerProperties>;
|
updated_properties: Partial<ServerProperties>;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
||||||
import {ClientEntry, MusicClientEntry} from "tc-shared/tree/Client";
|
import {ClientEntry, MusicClientEntry} from "tc-shared/tree/Client";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
const cssStyle = require("./ModalChangeVolume.scss");
|
const cssStyle = require("./ModalChangeVolume.scss");
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,9 @@ import PermissionType from "tc-shared/permission/PermissionType";
|
||||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {tra} from "tc-shared/i18n/localize";
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
|
||||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||||
import {LogCategory, logError} from "tc-shared/log";
|
import {LogCategory, logError} from "tc-shared/log";
|
||||||
|
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
const cssStyle = require("./ModalGroupCreate.scss");
|
const cssStyle = require("./ModalGroupCreate.scss");
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,9 @@ import PermissionType from "tc-shared/permission/PermissionType";
|
||||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {tra} from "tc-shared/i18n/localize";
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
|
||||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||||
import {LogCategory, logWarn} from "tc-shared/log";
|
import {LogCategory, logWarn} from "tc-shared/log";
|
||||||
|
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
const cssStyle = require("./ModalGroupPermissionCopy.scss");
|
const cssStyle = require("./ModalGroupPermissionCopy.scss");
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,134 @@
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
|
import {
|
||||||
|
ModalBookmarksAddServerEvents,
|
||||||
|
ModalBookmarksAddServerVariables, TargetBookmarkInfo
|
||||||
|
} from "tc-shared/ui/modal/bookmarks-add-server/Definitions";
|
||||||
|
import {Registry} from "tc-events";
|
||||||
|
import {createIpcUiVariableProvider, IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||||
|
import {stringifyServerAddress} from "tc-shared/tree/Server";
|
||||||
|
import {bookmarks} from "tc-shared/Bookmarks";
|
||||||
|
|
||||||
|
class Controller {
|
||||||
|
readonly handler: ConnectionHandler;
|
||||||
|
readonly variables: IpcUiVariableProvider<ModalBookmarksAddServerVariables>;
|
||||||
|
readonly events: Registry<ModalBookmarksAddServerEvents>;
|
||||||
|
|
||||||
|
private readonly serverInfo: TargetBookmarkInfo;
|
||||||
|
|
||||||
|
private readonly targetChannelId: number;
|
||||||
|
private readonly targetChannelPassword: string;
|
||||||
|
|
||||||
|
private readonly targetServerAddress: string;
|
||||||
|
private readonly targetServerPassword: string;
|
||||||
|
|
||||||
|
private readonly connectProfile: string;
|
||||||
|
|
||||||
|
private bookmarkName: string;
|
||||||
|
private useCurrentChannel: boolean;
|
||||||
|
|
||||||
|
constructor(handler: ConnectionHandler) {
|
||||||
|
this.handler = handler;
|
||||||
|
|
||||||
|
this.variables = createIpcUiVariableProvider();
|
||||||
|
this.events = new Registry<ModalBookmarksAddServerEvents>();
|
||||||
|
|
||||||
|
if(handler.connected && handler.getClient().currentChannel()) {
|
||||||
|
const currentChannel = handler.getClient().currentChannel();
|
||||||
|
const handshakeHandler = handler.serverConnection.handshake_handler();
|
||||||
|
|
||||||
|
this.targetServerAddress = stringifyServerAddress(handler.channelTree.server.remote_address);
|
||||||
|
/* Password will be hashed since we're already connected to the server */
|
||||||
|
this.targetServerPassword = handshakeHandler.parameters.serverPassword;
|
||||||
|
this.connectProfile = handshakeHandler.parameters.profile.id;
|
||||||
|
|
||||||
|
this.targetChannelId = currentChannel.channelId;
|
||||||
|
this.targetChannelPassword = currentChannel.getCachedPasswordHash();
|
||||||
|
|
||||||
|
this.serverInfo = {
|
||||||
|
type: "success",
|
||||||
|
|
||||||
|
handlerId: handler.handlerId,
|
||||||
|
|
||||||
|
serverName: handler.channelTree.server.properties.virtualserver_name,
|
||||||
|
serverUniqueId: handler.getCurrentServerUniqueId(),
|
||||||
|
|
||||||
|
currentChannelName: currentChannel.channelName(),
|
||||||
|
currentChannelId: currentChannel.channelId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.bookmarkName = this.serverInfo.serverName;
|
||||||
|
} else {
|
||||||
|
this.serverInfo = { type: "not-connected" };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.useCurrentChannel = false;
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("serverInfo", () => this.serverInfo);
|
||||||
|
this.variables.setVariableProvider("bookmarkNameValid", () => {
|
||||||
|
if(!this.bookmarkName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.bookmarkName.length > 0 && this.bookmarkName.length < 40;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("bookmarkName", () => this.bookmarkName);
|
||||||
|
this.variables.setVariableEditor("bookmarkName", newValue => {
|
||||||
|
this.bookmarkName = newValue;
|
||||||
|
this.variables.sendVariable("bookmarkNameValid");
|
||||||
|
});
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("saveCurrentChannel", () => this.useCurrentChannel);
|
||||||
|
this.variables.setVariableEditor("saveCurrentChannel", newValue => {
|
||||||
|
this.useCurrentChannel = newValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.events.on("action_add_bookmark", () => {
|
||||||
|
if(this.serverInfo.type !== "success") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.variables.getVariableSync("bookmarkNameValid")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarks.createBookmark({
|
||||||
|
displayName: this.bookmarkName || this.serverInfo.serverName,
|
||||||
|
connectProfile: this.connectProfile,
|
||||||
|
connectOnStartup: false,
|
||||||
|
|
||||||
|
defaultChannelPasswordHash: this.targetChannelPassword,
|
||||||
|
defaultChannel: this.useCurrentChannel ? ("/" + this.targetChannelId) : undefined,
|
||||||
|
|
||||||
|
serverPasswordHash: this.targetServerPassword,
|
||||||
|
serverAddress: this.targetServerAddress,
|
||||||
|
|
||||||
|
previousEntry: undefined,
|
||||||
|
parentEntry: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
this.events.fire("notify_bookmark_added");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.events.destroy();
|
||||||
|
this.variables.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function spawnModalAddCurrentServerToBookmarks(handler: ConnectionHandler) {
|
export function spawnModalAddCurrentServerToBookmarks(handler: ConnectionHandler) {
|
||||||
/* TODO! */
|
const controller = new Controller(handler);
|
||||||
|
const modal = spawnModal("modal-bookmark-add-server", [
|
||||||
|
controller.events.generateIpcDescription(),
|
||||||
|
controller.variables.generateConsumerDescription()
|
||||||
|
], {
|
||||||
|
popoutable: true,
|
||||||
|
popedOut: false
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.events.on(["action_cancel", "notify_bookmark_added"], () => modal.destroy());
|
||||||
|
modal.getEvents().on("destroy", () => controller.destroy());
|
||||||
|
modal.show().then(undefined);
|
||||||
}
|
}
|
|
@ -1,7 +1,28 @@
|
||||||
export interface ModalBookmarksAddServerVariables {
|
export type TargetBookmarkInfo = {
|
||||||
|
type: "success",
|
||||||
|
|
||||||
|
handlerId: string,
|
||||||
|
serverName: string,
|
||||||
|
serverUniqueId: string,
|
||||||
|
|
||||||
|
currentChannelId: number,
|
||||||
|
currentChannelName: string,
|
||||||
|
} | {
|
||||||
|
type: "not-connected" | "loading"
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ModalBookmarksAddServerVariables {
|
||||||
|
readonly serverInfo: TargetBookmarkInfo,
|
||||||
|
|
||||||
|
bookmarkName: string,
|
||||||
|
bookmarkNameValid: boolean,
|
||||||
|
|
||||||
|
saveCurrentChannel: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModalBookmarksAddServerEvents {
|
export interface ModalBookmarksAddServerEvents {
|
||||||
|
action_add_bookmark: {},
|
||||||
|
action_cancel: {},
|
||||||
|
|
||||||
|
notify_bookmark_added: {}
|
||||||
}
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
padding: .5em;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarkInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
&.addServer {}
|
||||||
|
|
||||||
|
&.serverName {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bookmarkName {
|
||||||
|
margin-top: .5em;
|
||||||
|
color: #557edc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editableBookmarkName {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelIcon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelTooltipOuter {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-left: .5em;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelLabel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
|
@ -1,13 +1,164 @@
|
||||||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
import React from "react";
|
import React, {useContext, useEffect, useRef} from "react";
|
||||||
|
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||||
|
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||||
|
import {
|
||||||
|
ModalBookmarksAddServerEvents,
|
||||||
|
ModalBookmarksAddServerVariables, TargetBookmarkInfo
|
||||||
|
} from "tc-shared/ui/modal/bookmarks-add-server/Definitions";
|
||||||
|
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
|
import {ControlledBoxedInputField} from "tc-shared/ui/react-elements/InputField";
|
||||||
|
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||||
|
import {ChannelTag, ServerTag} from "tc-shared/ui/tree/EntryTags";
|
||||||
|
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||||
|
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
|
const EventContext = React.createContext<Registry<ModalBookmarksAddServerEvents>>(undefined);
|
||||||
|
const VariablesContext = React.createContext<UiVariableConsumer<ModalBookmarksAddServerVariables>>(undefined);
|
||||||
|
const BookmarkInfoContext = React.createContext<TargetBookmarkInfo>(undefined);
|
||||||
|
|
||||||
|
const BookmarkInfoProvider = React.memo((props: { children }) => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const info = variables.useReadOnly("serverInfo", undefined, { type: "loading" });
|
||||||
|
return (
|
||||||
|
<BookmarkInfoContext.Provider value={info}>
|
||||||
|
{props.children}
|
||||||
|
</BookmarkInfoContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const BookmarkName = React.memo(() => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const name = variables.useVariable("bookmarkName", undefined);
|
||||||
|
const nameValid = variables.useReadOnly("bookmarkNameValid", undefined, true);
|
||||||
|
const info = useContext(BookmarkInfoContext);
|
||||||
|
|
||||||
|
const refField = useRef<HTMLInputElement>();
|
||||||
|
useEffect(() => {
|
||||||
|
if(info.type === "success") {
|
||||||
|
refField.current?.focus();
|
||||||
|
}
|
||||||
|
}, [ info.type === "success" ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ControlledBoxedInputField
|
||||||
|
refInput={refField}
|
||||||
|
|
||||||
|
value={name.localValue}
|
||||||
|
className={cssStyle.editableBookmarkName}
|
||||||
|
isInvalid={!nameValid}
|
||||||
|
|
||||||
|
onChange={newValue => name.setValue(newValue, true)}
|
||||||
|
onBlur={() => name.setValue(name.localValue)}
|
||||||
|
onEnter={() => events.fire("action_add_bookmark")}
|
||||||
|
|
||||||
|
disabled={info.type !== "success"}
|
||||||
|
finishOnEnter={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const BookmarkChannel = React.memo(() => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const info = useContext(BookmarkInfoContext);
|
||||||
|
const saveCurrentChannel = variables.useVariable("saveCurrentChannel", undefined, false);
|
||||||
|
|
||||||
|
let helpIcon;
|
||||||
|
if(info.type === "success") {
|
||||||
|
helpIcon = (
|
||||||
|
<IconTooltip className={cssStyle.channelIcon} outerClassName={cssStyle.channelTooltipOuter}>
|
||||||
|
<Translatable>Current channel:</Translatable><br />
|
||||||
|
<ChannelTag channelName={info.currentChannelName} channelId={info.currentChannelId} handlerId={info.handlerId} />
|
||||||
|
</IconTooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
disabled={info.type !== "success"}
|
||||||
|
value={saveCurrentChannel.localValue}
|
||||||
|
onChange={value => saveCurrentChannel.setValue(value)}
|
||||||
|
label={<div className={cssStyle.channelLabel}><Translatable key={"unknown-channel"}>Save current channel as default channel</Translatable> {helpIcon}</div>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ServerName = React.memo(() => {
|
||||||
|
const info = useContext(BookmarkInfoContext);
|
||||||
|
if(info.type !== "success") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ServerTag serverName={info.serverName} handlerId={info.handlerId} serverUniqueId={info.serverUniqueId} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const RendererBookmarkInfo = React.memo(() => {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.bookmarkInfo}>
|
||||||
|
<div className={cssStyle.text + " " + cssStyle.addServer}>Add server to bookmarks:</div>
|
||||||
|
<div className={cssStyle.text + " " + cssStyle.serverName}><ServerName /></div>
|
||||||
|
<div className={cssStyle.text + " " + cssStyle.bookmarkName}>Bookmark name:</div>
|
||||||
|
<BookmarkName />
|
||||||
|
<BookmarkChannel />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const RendererButtons = React.memo(() => {
|
||||||
|
const info = useContext(BookmarkInfoContext);
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const nameValid = variables.useReadOnly("bookmarkNameValid", undefined, true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.buttons}>
|
||||||
|
<Button color={"red"} onClick={() => events.fire("action_cancel")}>
|
||||||
|
<Translatable>Cancel</Translatable>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={"green"}
|
||||||
|
onClick={() => events.fire("action_add_bookmark")}
|
||||||
|
disabled={info.type !== "success" || !nameValid}
|
||||||
|
>
|
||||||
|
<Translatable>Create bookmark</Translatable>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
class ModalBookmarksAddServer extends AbstractModal {
|
class ModalBookmarksAddServer extends AbstractModal {
|
||||||
|
private readonly variables: UiVariableConsumer<ModalBookmarksAddServerVariables>;
|
||||||
|
private readonly events: Registry<ModalBookmarksAddServerEvents>;
|
||||||
|
|
||||||
|
constructor(events: IpcRegistryDescription<ModalBookmarksAddServerEvents>, variables: IpcVariableDescriptor<ModalBookmarksAddServerVariables>) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.variables = createIpcUiVariableConsumer(variables);
|
||||||
|
this.events = Registry.fromIpcDescription(events);
|
||||||
|
}
|
||||||
|
|
||||||
renderBody(): React.ReactElement {
|
renderBody(): React.ReactElement {
|
||||||
return undefined;
|
return (
|
||||||
|
<EventContext.Provider value={this.events}>
|
||||||
|
<VariablesContext.Provider value={this.variables}>
|
||||||
|
<div className={cssStyle.container}>
|
||||||
|
<BookmarkInfoProvider>
|
||||||
|
<RendererBookmarkInfo />
|
||||||
|
<RendererButtons />
|
||||||
|
</BookmarkInfoProvider>
|
||||||
|
</div>
|
||||||
|
</VariablesContext.Provider>
|
||||||
|
</EventContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTitle(): string | React.ReactElement {
|
renderTitle(): string | React.ReactElement {
|
||||||
return undefined;
|
return <Translatable>Add server to bookmarks</Translatable>;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
export = ModalBookmarksAddServer;
|
|
@ -579,6 +579,7 @@ const BookmarkSettingChannelPassword = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const BookmarkInfoRenderer = React.memo(() => {
|
const BookmarkInfoRenderer = React.memo(() => {
|
||||||
|
const selectedBookmark = useContext(SelectedBookmarkIdContext);
|
||||||
const bookmarkInfo = useContext(SelectedBookmarkInfoContext);
|
const bookmarkInfo = useContext(SelectedBookmarkInfoContext);
|
||||||
let connectCount = bookmarkInfo ? Math.max(bookmarkInfo.connectCountUniqueId, bookmarkInfo.connectCountAddress) : -1;
|
let connectCount = bookmarkInfo ? Math.max(bookmarkInfo.connectCountUniqueId, bookmarkInfo.connectCountAddress) : -1;
|
||||||
|
|
||||||
|
@ -635,6 +636,11 @@ const BookmarkInfoRenderer = React.memo(() => {
|
||||||
<Translatable>You never connected to that server.</Translatable>
|
<Translatable>You never connected to that server.</Translatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={cssStyle.overlay + " " + (selectedBookmark.type !== "bookmark" ? cssStyle.shown : "")}>
|
||||||
|
<div className={cssStyle.text}>
|
||||||
|
{/* bookmark is a directory */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -774,11 +780,11 @@ class ModalBookmarks extends AbstractModal {
|
||||||
this.variables.destroy();
|
this.variables.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBody(renderBody): React.ReactElement {
|
renderBody(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<EventContext.Provider value={this.events}>
|
<EventContext.Provider value={this.events}>
|
||||||
<VariableContext.Provider value={this.variables}>
|
<VariableContext.Provider value={this.variables}>
|
||||||
<div className={cssStyle.container + " " + (renderBody.windowed ? cssStyle.windowed : "")}>
|
<div className={cssStyle.container + " " + (this.properties.windowed ? cssStyle.windowed : "")}>
|
||||||
<BookmarkListContainer />
|
<BookmarkListContainer />
|
||||||
<ContextDivider id={"separator-bookmarks"} direction={"horizontal"} defaultValue={25} />
|
<ContextDivider id={"separator-bookmarks"} direction={"horizontal"} defaultValue={25} />
|
||||||
<BookmarkInfoContainer />
|
<BookmarkInfoContainer />
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
import {getIconManager} from "tc-shared/file/Icons";
|
import {getIconManager} from "tc-shared/file/Icons";
|
||||||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
import {ChannelNameAlignment, ChannelNameParser} from "tc-shared/tree/Channel";
|
import {ChannelNameAlignment} from "tc-shared/tree/Channel";
|
||||||
|
|
||||||
const cssStyle = require("./Renderer.scss");
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
import {ControlledFlatInputField, ControlledSelect, FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
import {ControlledFlatInputField, ControlledSelect, FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
||||||
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
import * as i18n from "../../../i18n/country";
|
|
||||||
import {getIconManager} from "tc-shared/file/Icons";
|
import {getIconManager} from "tc-shared/file/Icons";
|
||||||
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||||
|
@ -414,10 +413,6 @@ class ConnectModal extends AbstractModal {
|
||||||
return <Translatable>Connect to a server</Translatable>;
|
return <Translatable>Connect to a server</Translatable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
color(): "none" | "blue" {
|
|
||||||
return "blue";
|
|
||||||
}
|
|
||||||
|
|
||||||
verticalAlignment(): "top" | "center" | "bottom" {
|
verticalAlignment(): "top" | "center" | "bottom" {
|
||||||
return "top";
|
return "top";
|
||||||
}
|
}
|
||||||
|
|
|
@ -404,13 +404,4 @@ class ModalInvite extends AbstractModal {
|
||||||
return <><Translatable>Invite People to</Translatable> {this.serverName}</>;
|
return <><Translatable>Invite People to</Translatable> {this.serverName}</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export = ModalInvite;
|
export = ModalInvite;
|
||||||
|
|
||||||
/*
|
|
||||||
const modal = spawnModal("global-settings-editor", [ events.generateIpcDescription() ], { popoutable: true, popedOut: false });
|
|
||||||
modal.show();
|
|
||||||
modal.getEvents().on("destroy", () => {
|
|
||||||
events.fire("notify_destroy");
|
|
||||||
events.destroy();
|
|
||||||
});
|
|
||||||
*/
|
|
|
@ -30,11 +30,11 @@ import {
|
||||||
} from "tc-shared/ui/modal/permission/SenselessPermissions";
|
} from "tc-shared/ui/modal/permission/SenselessPermissions";
|
||||||
import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroupCreate";
|
import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroupCreate";
|
||||||
import {spawnModalGroupPermissionCopy} from "tc-shared/ui/modal/ModalGroupPermissionCopy";
|
import {spawnModalGroupPermissionCopy} from "tc-shared/ui/modal/ModalGroupPermissionCopy";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
|
||||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||||
import {PermissionEditorTab} from "tc-shared/events/GlobalEvents";
|
import {PermissionEditorTab} from "tc-shared/events/GlobalEvents";
|
||||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||||
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
const cssStyle = require("./ModalPermissionEditor.scss");
|
const cssStyle = require("./ModalPermissionEditor.scss");
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||||
import {Registry} from "../../../events";
|
import {Registry} from "tc-events";
|
||||||
import {FileType} from "../../../file/FileManager";
|
import {FileType} from "../../../file/FileManager";
|
||||||
import {CommandResult} from "../../../connection/ServerConnectionDeclaration";
|
import {CommandResult} from "../../../connection/ServerConnectionDeclaration";
|
||||||
import PermissionType from "../../../permission/PermissionType";
|
import PermissionType from "../../../permission/PermissionType";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||||
import {Registry} from "../../../events";
|
import {Registry} from "tc-events";
|
||||||
import {
|
import {
|
||||||
FileTransfer,
|
FileTransfer,
|
||||||
FileTransferDirection,
|
FileTransferDirection,
|
||||||
|
|
|
@ -6,10 +6,10 @@ import {FileTransferInfo} from "./FileTransferInfo";
|
||||||
import {initializeRemoteFileBrowserController} from "./FileBrowserControllerRemote";
|
import {initializeRemoteFileBrowserController} from "./FileBrowserControllerRemote";
|
||||||
import {initializeTransferInfoController} from "./FileTransferInfoController";
|
import {initializeTransferInfoController} from "./FileTransferInfoController";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions";
|
import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions";
|
||||||
import {TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
|
import {TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
|
||||||
|
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
const cssStyle = require("./FileBrowserRenderer.scss");
|
const cssStyle = require("./FileBrowserRenderer.scss");
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
VideoPreviewStatus,
|
VideoPreviewStatus,
|
||||||
VideoSourceState
|
VideoSourceState
|
||||||
} from "tc-shared/ui/modal/video-source/Definitions";
|
} from "tc-shared/ui/modal/video-source/Definitions";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
|
||||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {BoxedInputField, Select} from "tc-shared/ui/react-elements/InputField";
|
import {BoxedInputField, Select} from "tc-shared/ui/react-elements/InputField";
|
||||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
|
@ -20,6 +19,7 @@ import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||||
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
|
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
|
||||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
import {ScreenCaptureDevice} from "tc-shared/video/VideoSource";
|
import {ScreenCaptureDevice} from "tc-shared/video/VideoSource";
|
||||||
|
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
const cssStyle = require("./Renderer.scss");
|
const cssStyle = require("./Renderer.scss");
|
||||||
const ModalEvents = React.createContext<Registry<ModalVideoSourceEvents>>(undefined);
|
const ModalEvents = React.createContext<Registry<ModalVideoSourceEvents>>(undefined);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
|
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {WhatsNew} from "tc-shared/ui/modal/whats-new/Renderer";
|
import {WhatsNew} from "tc-shared/ui/modal/whats-new/Renderer";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {ChangeLog} from "tc-shared/update/ChangeLog";
|
import {ChangeLog} from "tc-shared/update/ChangeLog";
|
||||||
|
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
export function spawnUpdatedModal(changes: { changesUI?: ChangeLog, changesClient?: ChangeLog }) {
|
export function spawnUpdatedModal(changes: { changesUI?: ChangeLog, changesClient?: ChangeLog }) {
|
||||||
const modal = spawnReactModal(class extends InternalModal {
|
const modal = spawnReactModal(class extends InternalModal {
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const ControlledBoxedInputField = (props: {
|
||||||
onBlur?: () => void,
|
onBlur?: () => void,
|
||||||
|
|
||||||
finishOnEnter?: boolean,
|
finishOnEnter?: boolean,
|
||||||
|
refInput?: React.RefObject<HTMLInputElement>
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -61,6 +62,8 @@ export const ControlledBoxedInputField = (props: {
|
||||||
|
|
||||||
<input key={"input"}
|
<input key={"input"}
|
||||||
|
|
||||||
|
ref={props.refInput}
|
||||||
|
|
||||||
value={props.value || ""}
|
value={props.value || ""}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ReactElement} from "react";
|
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
|
import {ReactElement} from "react";
|
||||||
import {guid} from "tc-shared/crypto/uid";
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
|
||||||
const cssStyle = require("./Tooltip.scss");
|
const cssStyle = require("./Tooltip.scss");
|
||||||
|
@ -73,6 +73,7 @@ export interface TooltipState {
|
||||||
|
|
||||||
export interface TooltipProperties {
|
export interface TooltipProperties {
|
||||||
tooltip: () => ReactElement | ReactElement[] | string;
|
tooltip: () => ReactElement | ReactElement[] | string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
||||||
|
@ -103,6 +104,7 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
||||||
onMouseLeave={() => this.setState({ hovered: false })}
|
onMouseLeave={() => this.setState({ hovered: false })}
|
||||||
onClick={() => this.setState({ hovered: !this.state.hovered })}
|
onClick={() => this.setState({ hovered: !this.state.hovered })}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
|
className={this.props.className}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</span>
|
</span>
|
||||||
|
@ -124,8 +126,9 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
||||||
|
|
||||||
private onMouseEnter(event: React.MouseEvent) {
|
private onMouseEnter(event: React.MouseEvent) {
|
||||||
/* check if may only the span has been hovered, should not be the case! */
|
/* check if may only the span has been hovered, should not be the case! */
|
||||||
if(event.target === this.refContainer.current)
|
if(event.target === this.refContainer.current) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ hovered: true });
|
this.setState({ hovered: true });
|
||||||
|
|
||||||
|
@ -149,8 +152,8 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string }) => (
|
export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string, outerClassName?: string }) => (
|
||||||
<Tooltip tooltip={() => props.children}>
|
<Tooltip tooltip={() => props.children} className={props.outerClassName}>
|
||||||
<div className={cssStyle.iconTooltip + " " + props.className}>
|
<div className={cssStyle.iconTooltip + " " + props.className}>
|
||||||
<img src="img/icon_tooltip.svg" alt={""} />
|
<img src="img/icon_tooltip.svg" alt={""} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,14 +8,15 @@ import {
|
||||||
Popout2ControllerMessages,
|
Popout2ControllerMessages,
|
||||||
PopoutIPCMessage
|
PopoutIPCMessage
|
||||||
} from "../../../ui/react-elements/external-modal/IPCMessage";
|
} from "../../../ui/react-elements/external-modal/IPCMessage";
|
||||||
import {ModalController, ModalEvents, ModalOptions, ModalState} from "../../../ui/react-elements/ModalDefinitions";
|
import {ModalEvents, ModalOptions, ModalState} from "../../../ui/react-elements/ModalDefinitions";
|
||||||
import {guid} from "tc-shared/crypto/uid";
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
import {ModalInstanceController, ModalInstanceEvents} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalController {
|
export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalInstanceController {
|
||||||
public readonly modalType: string;
|
public readonly modalType: string;
|
||||||
public readonly constructorArguments: any[];
|
public readonly constructorArguments: any[];
|
||||||
|
|
||||||
private readonly modalEvents: Registry<ModalEvents>;
|
private readonly modalEvents: Registry<ModalInstanceEvents>;
|
||||||
private modalState: ModalState = ModalState.DESTROYED;
|
private modalState: ModalState = ModalState.DESTROYED;
|
||||||
|
|
||||||
private readonly documentUnloadListener: () => void;
|
private readonly documentUnloadListener: () => void;
|
||||||
|
@ -26,7 +27,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
||||||
this.modalType = modalType;
|
this.modalType = modalType;
|
||||||
this.constructorArguments = constructorArguments;
|
this.constructorArguments = constructorArguments;
|
||||||
|
|
||||||
this.modalEvents = new Registry<ModalEvents>();
|
this.modalEvents = new Registry<ModalInstanceEvents>();
|
||||||
|
|
||||||
this.ipcChannel = ipc.getIpcInstance().createChannel(kPopoutIPCChannelId);
|
this.ipcChannel = ipc.getIpcInstance().createChannel(kPopoutIPCChannelId);
|
||||||
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
||||||
|
@ -38,7 +39,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
||||||
return {}; /* FIXME! */
|
return {}; /* FIXME! */
|
||||||
}
|
}
|
||||||
|
|
||||||
getEvents(): Registry<ModalEvents> {
|
getEvents(): Registry<ModalInstanceEvents> {
|
||||||
return this.modalEvents;
|
return this.modalEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +86,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("unload", this.documentUnloadListener);
|
window.addEventListener("unload", this.documentUnloadListener);
|
||||||
this.modalEvents.fire("open");
|
this.modalEvents.fire("notify_open");
|
||||||
}
|
}
|
||||||
|
|
||||||
private doDestroyWindow() {
|
private doDestroyWindow() {
|
||||||
|
@ -99,12 +100,13 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
||||||
|
|
||||||
this.doDestroyWindow();
|
this.doDestroyWindow();
|
||||||
this.modalState = ModalState.HIDDEN;
|
this.modalState = ModalState.HIDDEN;
|
||||||
this.modalEvents.fire("close");
|
this.modalEvents.fire("notify_close");
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if(this.modalState === ModalState.DESTROYED)
|
if(this.modalState === ModalState.DESTROYED) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.doDestroyWindow();
|
this.doDestroyWindow();
|
||||||
if(this.ipcChannel) {
|
if(this.ipcChannel) {
|
||||||
|
@ -113,7 +115,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
||||||
|
|
||||||
this.destroyIPC();
|
this.destroyIPC();
|
||||||
this.modalState = ModalState.DESTROYED;
|
this.modalState = ModalState.DESTROYED;
|
||||||
this.modalEvents.fire("destroy");
|
this.modalEvents.fire("notify_destroy");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleWindowClosed() {
|
protected handleWindowClosed() {
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
|
||||||
|
/* FIXME: Remove this wired import */
|
||||||
|
:global {
|
||||||
|
@import "../../../../css/static/general";
|
||||||
|
@import "../../../../css/static/modal";
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
background: #19191b;
|
||||||
|
color: #999;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
ModalBodyRenderer, ModalFrameRenderer,
|
||||||
|
ModalFrameTopRenderer,
|
||||||
|
WindowModalRenderer
|
||||||
|
} from "tc-shared/ui/react-elements/modal/Renderer";
|
||||||
|
|
||||||
|
import "./ModalRenderer.scss";
|
||||||
|
|
||||||
|
export interface ModalControlFunctions {
|
||||||
|
close();
|
||||||
|
minimize();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModalRenderer {
|
||||||
|
private readonly functionController: ModalControlFunctions;
|
||||||
|
private readonly container: HTMLDivElement;
|
||||||
|
|
||||||
|
constructor(functionController: ModalControlFunctions) {
|
||||||
|
this.functionController = functionController;
|
||||||
|
|
||||||
|
this.container = document.createElement("div");
|
||||||
|
document.body.appendChild(this.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderModal(modal: AbstractModal | undefined) {
|
||||||
|
if(typeof modal === "undefined") {
|
||||||
|
ReactDOM.unmountComponentAtNode(this.container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(__build.target === "client") {
|
||||||
|
ReactDOM.render(
|
||||||
|
<ModalFrameRenderer>
|
||||||
|
<ModalFrameTopRenderer
|
||||||
|
replacePageTitle={true}
|
||||||
|
modalInstance={modal}
|
||||||
|
|
||||||
|
onClose={() => this.functionController.close()}
|
||||||
|
onMinimize={() => this.functionController.minimize()}
|
||||||
|
/>
|
||||||
|
<ModalBodyRenderer modalInstance={modal} />
|
||||||
|
</ModalFrameRenderer>,
|
||||||
|
this.container
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ReactDOM.render(
|
||||||
|
<WindowModalRenderer>
|
||||||
|
<ModalFrameTopRenderer
|
||||||
|
replacePageTitle={true}
|
||||||
|
modalInstance={modal}
|
||||||
|
|
||||||
|
onClose={() => this.functionController.close()}
|
||||||
|
onMinimize={() => this.functionController.minimize()}
|
||||||
|
/>
|
||||||
|
<ModalBodyRenderer modalInstance={modal} />
|
||||||
|
</WindowModalRenderer>,
|
||||||
|
this.container
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,16 +2,16 @@ import * as loader from "tc-loader";
|
||||||
import {Stage} from "tc-loader";
|
import {Stage} from "tc-loader";
|
||||||
import * as ipc from "../../../ipc/BrowserIPC";
|
import * as ipc from "../../../ipc/BrowserIPC";
|
||||||
import * as i18n from "../../../i18n/localize";
|
import * as i18n from "../../../i18n/localize";
|
||||||
import {AbstractModal, ModalRenderer} from "../../../ui/react-elements/ModalDefinitions";
|
import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions";
|
||||||
import {AppParameters} from "../../../settings";
|
import {AppParameters} from "../../../settings";
|
||||||
import {getPopoutController} from "./PopoutController";
|
import {getPopoutController} from "./PopoutController";
|
||||||
import {WebModalRenderer} from "../../../ui/react-elements/external-modal/PopoutRendererWeb";
|
|
||||||
import {ClientModalRenderer} from "../../../ui/react-elements/external-modal/PopoutRendererClient";
|
|
||||||
import {setupJSRender} from "../../../ui/jsrender";
|
import {setupJSRender} from "../../../ui/jsrender";
|
||||||
|
|
||||||
import "../../../file/RemoteAvatars";
|
import "../../../file/RemoteAvatars";
|
||||||
import "../../../file/RemoteIcons";
|
import "../../../file/RemoteIcons";
|
||||||
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||||
|
import {ModalRenderer} from "tc-shared/ui/react-elements/external-modal/ModalRenderer";
|
||||||
|
import {constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
|
||||||
if("__native_client_init_shared" in window) {
|
if("__native_client_init_shared" in window) {
|
||||||
(window as any).__native_client_init_shared(__webpack_require__);
|
(window as any).__native_client_init_shared(__webpack_require__);
|
||||||
|
@ -47,18 +47,14 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
name: "modal renderer loader",
|
name: "modal renderer loader",
|
||||||
priority: 10,
|
priority: 10,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
if(__build.target === "web") {
|
modalRenderer = new ModalRenderer({
|
||||||
modalRenderer = new WebModalRenderer();
|
close() {
|
||||||
} else {
|
getPopoutController().doClose()
|
||||||
modalRenderer = new ClientModalRenderer({
|
},
|
||||||
close() {
|
minimize() {
|
||||||
getPopoutController().doClose()
|
getPopoutController().doMinimize()
|
||||||
},
|
}
|
||||||
minimize() {
|
});
|
||||||
getPopoutController().doMinimize()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,7 +84,7 @@ loader.register_task(Stage.LOADED, {
|
||||||
priority: 100,
|
priority: 100,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
try {
|
try {
|
||||||
modalInstance = new modalClass(...getPopoutController().getConstructorArguments());
|
modalInstance = constructAbstractModalClass(modalClass, { windowed: true }, getPopoutController().getConstructorArguments());
|
||||||
modalRenderer.renderModal(modalInstance);
|
modalRenderer.renderModal(modalInstance);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
|
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
@import "../../../../css/static/mixin";
|
|
||||||
|
|
||||||
/* FIXME: Remove this wired import */
|
|
||||||
:global {
|
|
||||||
@import "../../../../css/static/general";
|
|
||||||
@import "../../../../css/static/modal";
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
|
|
||||||
background: #19191b;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
max-height: 100vh;
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
import {InternalModalContentRenderer} from "tc-shared/ui/react-elements/internal-modal/Renderer";
|
|
||||||
import {AbstractModal, ModalRenderer} from "tc-shared/ui/react-elements/ModalDefinitions";
|
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
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}
|
|
||||||
|
|
||||||
windowed={true}
|
|
||||||
/>,
|
|
||||||
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,76 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import {AbstractModal, ModalRenderer} from "tc-shared/ui/react-elements/ModalDefinitions";
|
|
||||||
|
|
||||||
const cssStyle = require("./PopoutRenderer.scss");
|
|
||||||
|
|
||||||
class TitleRenderer {
|
|
||||||
private readonly htmlContainer: HTMLElement;
|
|
||||||
private modalInstance: AbstractModal;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const titleElements = document.getElementsByTagName("title");
|
|
||||||
if(titleElements.length === 0) {
|
|
||||||
this.htmlContainer = document.createElement("title");
|
|
||||||
document.head.appendChild(this.htmlContainer);
|
|
||||||
} else {
|
|
||||||
this.htmlContainer = titleElements[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInstance(instance: AbstractModal) {
|
|
||||||
if(this.modalInstance) {
|
|
||||||
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.modalInstance = instance;
|
|
||||||
if(this.modalInstance) {
|
|
||||||
ReactDOM.render(<>{this.modalInstance.renderTitle()}</>, this.htmlContainer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BodyRenderer {
|
|
||||||
private readonly htmlContainer: HTMLElement;
|
|
||||||
private modalInstance: AbstractModal;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.htmlContainer = document.createElement("div");
|
|
||||||
this.htmlContainer.classList.add(cssStyle.container);
|
|
||||||
|
|
||||||
document.body.appendChild(this.htmlContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
setInstance(instance: AbstractModal) {
|
|
||||||
if(this.modalInstance) {
|
|
||||||
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.modalInstance = instance;
|
|
||||||
if(this.modalInstance) {
|
|
||||||
ReactDOM.render(<>{this.modalInstance.renderBody({ windowed: true })}</>, this.htmlContainer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,14 +1,14 @@
|
||||||
|
import {ModalInstanceController, ModalOptions} from "../modal/Definitions";
|
||||||
import "./Controller";
|
import "./Controller";
|
||||||
import {ModalController, ModalOptions} from "../ModalDefinitions";
|
|
||||||
|
|
||||||
export type ControllerFactory = (modalType: string, constructorArguments?: any[], options?: ModalOptions) => ModalController;
|
export type ControllerFactory = (modalType: string, constructorArguments?: any[], options?: ModalOptions) => ModalInstanceController;
|
||||||
|
|
||||||
let modalControllerFactory: ControllerFactory;
|
let modalControllerFactory: ControllerFactory;
|
||||||
|
|
||||||
export function setExternalModalControllerFactory(factory: ControllerFactory) {
|
export function setExternalModalControllerFactory(factory: ControllerFactory) {
|
||||||
modalControllerFactory = factory;
|
modalControllerFactory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnExternalModal<EventClass extends { [key: string]: any }>(modalType: string, constructorArguments?: any[], options?: ModalOptions) : ModalController {
|
export function spawnExternalModal<EventClass extends { [key: string]: any }>(modalType: string, constructorArguments: any[], options: ModalOptions) : ModalInstanceController {
|
||||||
if(typeof modalControllerFactory === "undefined") {
|
if(typeof modalControllerFactory === "undefined") {
|
||||||
throw tr("No external modal factory has been set");
|
throw tr("No external modal factory has been set");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
import {Registry} from "../../../events";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import {
|
|
||||||
AbstractModal,
|
|
||||||
ModalController,
|
|
||||||
ModalEvents,
|
|
||||||
ModalOptions,
|
|
||||||
ModalState
|
|
||||||
} from "../../../ui/react-elements/ModalDefinitions";
|
|
||||||
import {InternalModalRenderer} from "../../../ui/react-elements/internal-modal/Renderer";
|
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
|
||||||
import {RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
|
||||||
|
|
||||||
export class InternalModalController implements ModalController {
|
|
||||||
readonly events: Registry<ModalEvents>;
|
|
||||||
|
|
||||||
private readonly modalType: RegisteredModal<any>;
|
|
||||||
private readonly constructorArguments: any[];
|
|
||||||
private modalInstance: AbstractModal;
|
|
||||||
|
|
||||||
private initializedPromise: Promise<void>;
|
|
||||||
|
|
||||||
private domElement: Element;
|
|
||||||
private refModal: React.RefObject<InternalModalRenderer>;
|
|
||||||
private modalState_: ModalState = ModalState.HIDDEN;
|
|
||||||
|
|
||||||
constructor(modalType: RegisteredModal<any>, constructorArguments: any[]) {
|
|
||||||
this.modalType = modalType;
|
|
||||||
this.constructorArguments = constructorArguments;
|
|
||||||
|
|
||||||
this.events = new Registry<ModalEvents>();
|
|
||||||
this.initializedPromise = this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
getOptions(): Readonly<ModalOptions> {
|
|
||||||
/* FIXME! */
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
getEvents(): Registry<ModalEvents> {
|
|
||||||
return this.events;
|
|
||||||
}
|
|
||||||
|
|
||||||
getState() {
|
|
||||||
return this.modalState_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initialize() {
|
|
||||||
this.refModal = React.createRef();
|
|
||||||
this.domElement = document.createElement("div");
|
|
||||||
|
|
||||||
this.modalInstance = new (await this.modalType.classLoader()).default(...this.constructorArguments);
|
|
||||||
const element = React.createElement(InternalModalRenderer, {
|
|
||||||
ref: this.refModal,
|
|
||||||
modal: this.modalInstance,
|
|
||||||
onClose: () => this.destroy()
|
|
||||||
});
|
|
||||||
document.body.appendChild(this.domElement);
|
|
||||||
await 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 {}
|
|
|
@ -1,249 +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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.align-top {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.align-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.align-bottom {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
@include user-select(none);
|
|
||||||
|
|
||||||
.icon, .button {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
height: 1.4em;
|
|
||||||
width: 1.4em;
|
|
||||||
|
|
||||||
padding: .2em;
|
|
||||||
border-radius: .2em;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
-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;
|
|
||||||
|
|
||||||
/* explicitly set the background color so the next element could use background-color: inherited; */
|
|
||||||
background: var(--modal-content-background);
|
|
||||||
|
|
||||||
@include chat-scrollbar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 */
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
|
||||||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
|
||||||
import {useMemo} from "react";
|
|
||||||
|
|
||||||
const cssStyle = require("./Modal.scss");
|
|
||||||
|
|
||||||
export const InternalModalContentRenderer = React.memo((props: {
|
|
||||||
modal: AbstractModal,
|
|
||||||
|
|
||||||
onClose?: () => void,
|
|
||||||
onMinimize?: () => void,
|
|
||||||
|
|
||||||
containerClass?: string,
|
|
||||||
headerClass?: string,
|
|
||||||
headerTitleClass?: string,
|
|
||||||
bodyClass?: string,
|
|
||||||
|
|
||||||
refContent?: React.Ref<HTMLDivElement>,
|
|
||||||
|
|
||||||
windowed: boolean,
|
|
||||||
}) => {
|
|
||||||
const body = useMemo(() => props.modal.renderBody({ windowed: props.windowed }), [props.modal]);
|
|
||||||
const title = useMemo(() => props.modal.renderTitle(), [props.modal]);
|
|
||||||
|
|
||||||
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")} draggable={false} />
|
|
||||||
</div>
|
|
||||||
<div className={cssStyle.title + " " + props.headerTitleClass}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
{title}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</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}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
{body}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</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 + " " + cssStyle["align-" + this.props.modal.verticalAlignment()]}
|
|
||||||
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 + " " + cssStyle["modal-" + this.props.modal.color()]}
|
|
||||||
|
|
||||||
windowed={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onBackdropClick(event: React.MouseEvent) {
|
|
||||||
if(event.target !== this.refModal.current || event.isDefaultPrevented())
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.props.onClose();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
import {
|
||||||
|
AbstractModal,
|
||||||
|
InternalModal,
|
||||||
|
ModalConstructorArguments,
|
||||||
|
ModalController,
|
||||||
|
ModalEvents,
|
||||||
|
ModalInstanceController,
|
||||||
|
ModalOptions,
|
||||||
|
ModalState
|
||||||
|
} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
import {Registry} from "tc-events";
|
||||||
|
import {findRegisteredModal, RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||||
|
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
||||||
|
import {InternalModalInstance} from "tc-shared/ui/react-elements/modal/internal";
|
||||||
|
|
||||||
|
export class GenericModalController<T extends keyof ModalConstructorArguments> implements ModalController {
|
||||||
|
private readonly events: Registry<ModalEvents>;
|
||||||
|
|
||||||
|
private readonly modalType: T;
|
||||||
|
private readonly modalConstructorArguments: ModalConstructorArguments[T];
|
||||||
|
private readonly modalOptions: ModalOptions;
|
||||||
|
|
||||||
|
private modalKlass: RegisteredModal<T> | undefined;
|
||||||
|
private instance: ModalInstanceController;
|
||||||
|
private popedOut: boolean;
|
||||||
|
|
||||||
|
public static fromInternalModal<ModalClass extends InternalModal>(klass: new (...args: any[]) => ModalClass, constructorArguments: any[]) : GenericModalController<"__internal__modal__"> {
|
||||||
|
const result = new GenericModalController("__internal__modal__", constructorArguments, {
|
||||||
|
popoutable: false,
|
||||||
|
popedOut: false
|
||||||
|
});
|
||||||
|
|
||||||
|
result.modalKlass = new class implements RegisteredModal<"__internal__modal__"> {
|
||||||
|
async classLoader(): Promise<{ default: { new(...args: ModalConstructorArguments["__internal__modal__"]): AbstractModal } }> {
|
||||||
|
return { default: klass };
|
||||||
|
}
|
||||||
|
|
||||||
|
modalId: "__internal__modal__";
|
||||||
|
popoutSupported: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(modalType: T, constructorArguments: ModalConstructorArguments[T], options: ModalOptions) {
|
||||||
|
this.events = new Registry<ModalEvents>();
|
||||||
|
|
||||||
|
this.modalType = modalType;
|
||||||
|
this.modalConstructorArguments = constructorArguments;
|
||||||
|
this.modalOptions = options || {};
|
||||||
|
|
||||||
|
this.popedOut = this.modalOptions.popedOut;
|
||||||
|
if(typeof this.popedOut !== "boolean") {
|
||||||
|
this.popedOut = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getModalClass() {
|
||||||
|
const modalKlass = this.modalKlass || findRegisteredModal(this.modalType);
|
||||||
|
if(!modalKlass) {
|
||||||
|
throw tr("missing modal registration for") + this.modalType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modalKlass;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createModalInstance() {
|
||||||
|
if(this.popedOut) {
|
||||||
|
this.instance = spawnExternalModal(this.modalType, this.modalConstructorArguments, this.modalOptions);
|
||||||
|
} else {
|
||||||
|
this.instance = new InternalModalInstance(this.getModalClass(), this.modalConstructorArguments, this.modalOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = this.instance.getEvents();
|
||||||
|
events.on("notify_destroy", events.on("notify_open", () => this.events.fire("open")));
|
||||||
|
events.on("notify_destroy", events.on("notify_close", () => this.events.fire("close")));
|
||||||
|
|
||||||
|
events.on("action_close", () => this.destroy());
|
||||||
|
events.on("action_popout", () => {
|
||||||
|
if(!this.popedOut) {
|
||||||
|
if(!this.getModalClass().popoutSupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.destroyModalInstance();
|
||||||
|
this.popedOut = true;
|
||||||
|
this.show().then(undefined);
|
||||||
|
} else {
|
||||||
|
if(this.modalOptions.popedOut) {
|
||||||
|
/* fixed poped out */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.destroyModalInstance();
|
||||||
|
this.popedOut = false;
|
||||||
|
this.show().then(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroyModalInstance() {
|
||||||
|
this.instance?.destroy();
|
||||||
|
this.instance = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.destroyModalInstance();
|
||||||
|
this.events.fire("destroy");
|
||||||
|
}
|
||||||
|
|
||||||
|
getEvents(): Registry<ModalEvents> {
|
||||||
|
return this.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions(): Readonly<ModalOptions> {
|
||||||
|
return this.modalOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): ModalState {
|
||||||
|
return this.instance ? this.instance.getState() : ModalState.DESTROYED;
|
||||||
|
}
|
||||||
|
|
||||||
|
async hide(): Promise<void> {
|
||||||
|
await this.instance?.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
async show(): Promise<void> {
|
||||||
|
if(!this.instance) {
|
||||||
|
this.createModalInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.instance.show();
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,11 +4,13 @@ import {ChannelEditEvents} from "tc-shared/ui/modal/channel-edit/Definitions";
|
||||||
import {EchoTestEvents} from "tc-shared/ui/modal/echo-test/Definitions";
|
import {EchoTestEvents} from "tc-shared/ui/modal/echo-test/Definitions";
|
||||||
import {ModalGlobalSettingsEditorEvents} from "tc-shared/ui/modal/global-settings-editor/Definitions";
|
import {ModalGlobalSettingsEditorEvents} from "tc-shared/ui/modal/global-settings-editor/Definitions";
|
||||||
import {InviteUiEvents, InviteUiVariables} from "tc-shared/ui/modal/invite/Definitions";
|
import {InviteUiEvents, InviteUiVariables} from "tc-shared/ui/modal/invite/Definitions";
|
||||||
|
import React, {ReactElement} from "react";
|
||||||
import {ReactElement} from "react";
|
|
||||||
import * as React from "react";
|
|
||||||
import {IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
import {IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||||
import {ModalBookmarkEvents, ModalBookmarkVariables} from "tc-shared/ui/modal/bookmarks/Definitions";
|
import {ModalBookmarkEvents, ModalBookmarkVariables} from "tc-shared/ui/modal/bookmarks/Definitions";
|
||||||
|
import {
|
||||||
|
ModalBookmarksAddServerEvents,
|
||||||
|
ModalBookmarksAddServerVariables
|
||||||
|
} from "tc-shared/ui/modal/bookmarks-add-server/Definitions";
|
||||||
|
|
||||||
export type ModalType = "error" | "warning" | "info" | "none";
|
export type ModalType = "error" | "warning" | "info" | "none";
|
||||||
export type ModalRenderType = "page" | "dialog";
|
export type ModalRenderType = "page" | "dialog";
|
||||||
|
@ -52,21 +54,10 @@ export interface ModalOptions {
|
||||||
popedOut?: boolean
|
popedOut?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModalFunctionController {
|
|
||||||
minimize();
|
|
||||||
supportMinimize() : boolean;
|
|
||||||
|
|
||||||
maximize();
|
|
||||||
supportMaximize() : boolean;
|
|
||||||
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModalEvents {
|
export interface ModalEvents {
|
||||||
"open": {},
|
"open": {},
|
||||||
"close": {},
|
"close": {},
|
||||||
|
|
||||||
/* create is implicitly at object creation */
|
|
||||||
"destroy": {}
|
"destroy": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +67,27 @@ export enum ModalState {
|
||||||
DESTROYED
|
DESTROYED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModalInstanceEvents {
|
||||||
|
action_close: {},
|
||||||
|
action_minimize: {},
|
||||||
|
action_popout: {},
|
||||||
|
|
||||||
|
notify_open: {}
|
||||||
|
notify_minimize: {},
|
||||||
|
notify_close: {},
|
||||||
|
notify_destroy: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalInstanceController {
|
||||||
|
getState() : ModalState;
|
||||||
|
getEvents() : Registry<ModalInstanceEvents>;
|
||||||
|
|
||||||
|
show() : Promise<void>;
|
||||||
|
hide() : Promise<void>;
|
||||||
|
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
|
||||||
export interface ModalController {
|
export interface ModalController {
|
||||||
getOptions() : Readonly<ModalOptions>;
|
getOptions() : Readonly<ModalOptions>;
|
||||||
getEvents() : Registry<ModalEvents>;
|
getEvents() : Registry<ModalEvents>;
|
||||||
|
@ -87,10 +99,23 @@ export interface ModalController {
|
||||||
destroy();
|
destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AbstractModal {
|
export interface ModalInstanceProperties {
|
||||||
protected constructor() {}
|
windowed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
abstract renderBody(properties: { windowed: boolean }) : ReactElement;
|
let currentModalProperties: ModalInstanceProperties
|
||||||
|
export abstract class AbstractModal {
|
||||||
|
protected readonly properties: ModalInstanceProperties;
|
||||||
|
|
||||||
|
protected constructor() {
|
||||||
|
if(typeof currentModalProperties === "undefined") {
|
||||||
|
throw "missing modal properties";
|
||||||
|
}
|
||||||
|
this.properties = currentModalProperties;
|
||||||
|
currentModalProperties = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract renderBody() : ReactElement;
|
||||||
abstract renderTitle() : string | React.ReactElement;
|
abstract renderTitle() : string | React.ReactElement;
|
||||||
|
|
||||||
/* only valid for the "inline" modals */
|
/* only valid for the "inline" modals */
|
||||||
|
@ -104,13 +129,24 @@ export abstract class AbstractModal {
|
||||||
protected onClose() {}
|
protected onClose() {}
|
||||||
protected onOpen() {}
|
protected onOpen() {}
|
||||||
}
|
}
|
||||||
|
export abstract class InternalModal extends AbstractModal {}
|
||||||
|
|
||||||
|
export function constructAbstractModalClass<T extends keyof ModalConstructorArguments>(
|
||||||
export interface ModalRenderer {
|
klass: new (...args: ModalConstructorArguments[T]) => AbstractModal,
|
||||||
renderModal(modal: AbstractModal | undefined);
|
properties: ModalInstanceProperties,
|
||||||
|
args: ModalConstructorArguments[T]) : AbstractModal {
|
||||||
|
currentModalProperties = properties;
|
||||||
|
try {
|
||||||
|
return new klass(...args);
|
||||||
|
} finally {
|
||||||
|
currentModalProperties = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ModalConstructorArguments {
|
export interface ModalConstructorArguments {
|
||||||
|
"__internal__modal__": any[],
|
||||||
|
|
||||||
"video-viewer": [
|
"video-viewer": [
|
||||||
/* events */ IpcRegistryDescription<VideoViewerEvents>,
|
/* events */ IpcRegistryDescription<VideoViewerEvents>,
|
||||||
/* handlerId */ string,
|
/* handlerId */ string,
|
||||||
|
@ -137,5 +173,9 @@ export interface ModalConstructorArguments {
|
||||||
"modal-bookmarks": [
|
"modal-bookmarks": [
|
||||||
/* events */ IpcRegistryDescription<ModalBookmarkEvents>,
|
/* events */ IpcRegistryDescription<ModalBookmarkEvents>,
|
||||||
/* variables */ IpcVariableDescriptor<ModalBookmarkVariables>,
|
/* variables */ IpcVariableDescriptor<ModalBookmarkVariables>,
|
||||||
|
],
|
||||||
|
"modal-bookmark-add-server": [
|
||||||
|
/* events */ IpcRegistryDescription<ModalBookmarksAddServerEvents>,
|
||||||
|
/* variables */ IpcVariableDescriptor<ModalBookmarksAddServerVariables>,
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -79,3 +79,9 @@ registerModal({
|
||||||
popoutSupported: true
|
popoutSupported: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerModal({
|
||||||
|
modalId: "modal-bookmark-add-server",
|
||||||
|
classLoader: async () => await import("tc-shared/ui/modal/bookmarks-add-server/Renderer"),
|
||||||
|
popoutSupported: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
html:root {
|
||||||
|
--modal-content-background: #19191b;
|
||||||
|
|
||||||
|
--modal-color-blue: #0a73d2;
|
||||||
|
--modal-color-green: #00d400;
|
||||||
|
--modal-color-red: #d50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalTitle {
|
||||||
|
background-color: #222224;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
padding: .25em;
|
||||||
|
@include user-select(none);
|
||||||
|
|
||||||
|
.icon, .button {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
height: 1.4em;
|
||||||
|
width: 1.4em;
|
||||||
|
|
||||||
|
padding: .2em;
|
||||||
|
border-radius: .2em;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #1b1b1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBody {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
min-width: 10em;
|
||||||
|
min-height: 5em;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
/* explicitly set the background color so the next element could use background-color: inherited; */
|
||||||
|
background: var(--modal-content-background);
|
||||||
|
|
||||||
|
@include chat-scrollbar();
|
||||||
|
|
||||||
|
&.color-blue {
|
||||||
|
border-left: 2px solid var(--modal-color-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-green {
|
||||||
|
border-left: 2px solid var(--modal-color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-red {
|
||||||
|
border-left: 2px solid var(--modal-color-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFrame {
|
||||||
|
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 */
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalWindowContainer {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
max-height: 100vh;
|
||||||
|
max-width: 100vw;
|
||||||
|
|
||||||
|
.modalTitle {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBody {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalPageContainer {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.align-top {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.align-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.align-bottom {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
margin: 1.75rem 0;
|
||||||
|
|
||||||
|
/* width calculations */
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
/* height stuff */
|
||||||
|
max-height: calc(100% - 3.5em);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,43 +1,189 @@
|
||||||
import {
|
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
AbstractModal,
|
import React from "react";
|
||||||
ModalFunctionController,
|
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
|
||||||
ModalOptions,
|
import TeaCupImage from "./TeaCup.png";
|
||||||
ModalRenderType
|
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||||
} from "tc-shared/ui/react-elements/modal/Definitions";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
import {useContext} from "react";
|
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||||
|
|
||||||
const ControllerContext = useContext<ModalRendererController>(undefined);
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
interface RendererControllerEvents {
|
export class ModalFrameTopRenderer extends React.PureComponent<{
|
||||||
|
modalInstance: AbstractModal,
|
||||||
|
|
||||||
}
|
className?: string,
|
||||||
|
|
||||||
export class ModalRendererController {
|
onClose?: () => void,
|
||||||
readonly renderType: ModalRenderType;
|
onPopout?: () => void,
|
||||||
readonly modal: AbstractModal;
|
onMinimize?: () => void,
|
||||||
|
|
||||||
constructor(renderType: ModalRenderType, modal: AbstractModal,) {
|
replacePageTitle: boolean,
|
||||||
this.renderType = renderType;
|
}> {
|
||||||
this.modal = modal;
|
private readonly refTitle = React.createRef<HTMLDivElement>();
|
||||||
|
private titleElement: HTMLTitleElement;
|
||||||
|
private observer: MutationObserver;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if(this.props.replacePageTitle) {
|
||||||
|
const titleElements = document.getElementsByTagName("title");
|
||||||
|
if(titleElements.length === 0) {
|
||||||
|
this.titleElement = document.createElement("title");
|
||||||
|
document.head.appendChild(this.titleElement);
|
||||||
|
} else {
|
||||||
|
this.titleElement = titleElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.observer = new MutationObserver(() => this.updatePageTitle());
|
||||||
|
this.observer.observe(this.refTitle.current, {
|
||||||
|
attributes: true,
|
||||||
|
subtree: true,
|
||||||
|
childList: true,
|
||||||
|
characterData: true
|
||||||
|
});
|
||||||
|
this.updatePageTitle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setShown(shown: boolean) {
|
componentWillUnmount() {
|
||||||
|
this.observer?.disconnect();
|
||||||
|
this.observer = undefined;
|
||||||
|
this.titleElement = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const buttons = [];
|
||||||
|
if(this.props.onMinimize) {
|
||||||
|
buttons.push(
|
||||||
|
<div className={cssStyle.button} onClick={this.props.onMinimize} key={"button-minimize"}>
|
||||||
|
<ClientIconRenderer icon={ClientIcon.MinimizeButton} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.props.onPopout) {
|
||||||
|
buttons.push(
|
||||||
|
<div className={cssStyle.button} onClick={this.props.onPopout} key={"button-popout"}>
|
||||||
|
<ClientIconRenderer icon={ClientIcon.ChannelPopout} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.props.onClose) {
|
||||||
|
buttons.push(
|
||||||
|
<div className={cssStyle.button} onClick={this.props.onClose} key={"button-close"}>
|
||||||
|
<ClientIconRenderer icon={ClientIcon.CloseButton} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={joinClassList(cssStyle.modalTitle, this.props.className)}>
|
||||||
|
<div className={cssStyle.icon}>
|
||||||
|
<img src={TeaCupImage} alt={""} draggable={false} />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.title} ref={this.refTitle}>
|
||||||
|
<ErrorBoundary>
|
||||||
|
{this.props.modalInstance.renderTitle()}
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePageTitle() {
|
||||||
|
if(!this.refTitle.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.titleElement.innerText = this.refTitle.current.textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModalRenderer = (props: {
|
|
||||||
mode: "page" | "dialog",
|
|
||||||
modal: AbstractModal,
|
|
||||||
modalOptions: ModalOptions,
|
|
||||||
modalActions: ModalFunctionController
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
|
export class ModalBodyRenderer extends React.PureComponent<{
|
||||||
|
modalInstance: AbstractModal,
|
||||||
|
className?: string
|
||||||
|
}> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={joinClassList(
|
||||||
|
cssStyle.modalBody,
|
||||||
|
this.props.className,
|
||||||
|
cssStyle["color-" + this.props.modalInstance.color()]
|
||||||
|
)}>
|
||||||
|
{this.props.modalInstance.renderBody()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalRendererDialog = (props: {
|
export class ModalFrameRenderer extends React.PureComponent<{
|
||||||
modal: AbstractModal,
|
children: [React.ReactElement<ModalFrameTopRenderer>, React.ReactElement<ModalBodyRenderer>]
|
||||||
modalOptions: ModalOptions,
|
}> {
|
||||||
modalActions: ModalFunctionController
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.modalFrame}>
|
||||||
|
{this.props.children[0]}
|
||||||
|
{this.props.children[1]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PageModalRenderer extends React.PureComponent<{
|
||||||
|
modalInstance: AbstractModal,
|
||||||
|
onBackdropClicked: () => void,
|
||||||
|
children: React.ReactElement<ModalFrameRenderer>
|
||||||
|
}, {
|
||||||
|
shown: boolean
|
||||||
|
}> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
shown: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={joinClassList(
|
||||||
|
cssStyle.modalPageContainer,
|
||||||
|
cssStyle["align-" + this.props.modalInstance.verticalAlignment()],
|
||||||
|
this.state.shown ? cssStyle.shown : undefined
|
||||||
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
|
role={"dialog"}
|
||||||
|
aria-hidden={true}
|
||||||
|
onClick={event => {
|
||||||
|
if(event.target !== event.currentTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onBackdropClicked();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={cssStyle.dialog}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WindowModalRenderer = (props: {
|
||||||
|
children: [React.ReactElement<ModalFrameTopRenderer>, React.ReactElement<ModalBodyRenderer>]
|
||||||
}) => {
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.modalWindowContainer}>
|
||||||
|
{props.children[0]}
|
||||||
|
{props.children[1]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
|
@ -1,31 +1,17 @@
|
||||||
import {ModalConstructorArguments} from "tc-shared/ui/react-elements/modal/Definitions";
|
import {InternalModal, ModalConstructorArguments} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
import {ModalController, ModalOptions} from "tc-shared/ui/react-elements/ModalDefinitions";
|
import {ModalController, ModalOptions} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||||
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
import {GenericModalController} from "tc-shared/ui/react-elements/modal/Controller";
|
||||||
import {InternalModal, InternalModalController} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
|
||||||
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
|
||||||
|
|
||||||
export function spawnModal<T extends keyof ModalConstructorArguments>(modal: T, constructorArguments: ModalConstructorArguments[T], options?: ModalOptions) : ModalController {
|
export function spawnModal<T extends keyof ModalConstructorArguments>(modal: T, constructorArguments: ModalConstructorArguments[T], options?: ModalOptions) : ModalController {
|
||||||
if(options?.popedOut) {
|
return new GenericModalController(modal, constructorArguments, options);
|
||||||
return spawnExternalModal(modal, constructorArguments, options);
|
|
||||||
} else {
|
|
||||||
return spawnInternalModal(modal, constructorArguments, options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new () => ModalClass) : InternalModalController;
|
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new () => ModalClass) : ModalController;
|
||||||
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : InternalModalController;
|
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : ModalController;
|
||||||
export function spawnReactModal<ModalClass extends InternalModal, A1, A2>(modalClass: new (..._: [A1, A2]) => ModalClass, arg1: A1, arg2: A2) : InternalModalController;
|
export function spawnReactModal<ModalClass extends InternalModal, A1, A2>(modalClass: new (..._: [A1, A2]) => ModalClass, arg1: A1, arg2: A2) : ModalController;
|
||||||
export function spawnReactModal<ModalClass extends InternalModal, A1, A2, A3>(modalClass: new (..._: [A1, A2, A3]) => ModalClass, arg1: A1, arg2: A2, arg3: A3) : InternalModalController;
|
export function spawnReactModal<ModalClass extends InternalModal, A1, A2, A3>(modalClass: new (..._: [A1, A2, A3]) => ModalClass, arg1: A1, arg2: A2, arg3: A3) : ModalController;
|
||||||
export function spawnReactModal<ModalClass extends InternalModal, A1, A2, A3, A4>(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4) : InternalModalController;
|
export function spawnReactModal<ModalClass extends InternalModal, A1, A2, A3, A4>(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4) : ModalController;
|
||||||
export function spawnReactModal<ModalClass extends InternalModal, A1, A2, A3, A4, A5>(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5) : InternalModalController;
|
export function spawnReactModal<ModalClass extends InternalModal, A1, A2, A3, A4, A5>(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5) : ModalController;
|
||||||
export function spawnReactModal<ModalClass extends InternalModal>(modalClass: new (..._: any[]) => ModalClass, ...args: any[]) : InternalModalController {
|
export function spawnReactModal<ModalClass extends InternalModal>(modalClass: new (..._: any[]) => ModalClass, ...args: any[]) : ModalController {
|
||||||
return new InternalModalController({
|
return GenericModalController.fromInternalModal(modalClass, args);
|
||||||
popoutSupported: false,
|
|
||||||
modalId: "__internal__unregistered",
|
|
||||||
classLoader: async () => ({ default: modalClass })
|
|
||||||
}, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function spawnInternalModal<T extends keyof ModalConstructorArguments>(modal: T, constructorArguments: ModalConstructorArguments[T], options?: ModalOptions) : InternalModalController {
|
|
||||||
return new InternalModalController(findRegisteredModal(modal), constructorArguments);
|
|
||||||
}
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
import {
|
||||||
|
AbstractModal,
|
||||||
|
constructAbstractModalClass, ModalInstanceController, ModalInstanceEvents,
|
||||||
|
ModalOptions,
|
||||||
|
ModalState
|
||||||
|
} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import {
|
||||||
|
ModalBodyRenderer,
|
||||||
|
ModalFrameRenderer,
|
||||||
|
ModalFrameTopRenderer,
|
||||||
|
PageModalRenderer
|
||||||
|
} from "tc-shared/ui/react-elements/modal/Renderer";
|
||||||
|
import {RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||||
|
import {LogCategory, logError} from "tc-shared/log";
|
||||||
|
import {Registry} from "tc-events";
|
||||||
|
|
||||||
|
export class InternalModalInstance implements ModalInstanceController {
|
||||||
|
readonly events: Registry<ModalInstanceEvents>;
|
||||||
|
|
||||||
|
private readonly modalKlass: RegisteredModal<any>;
|
||||||
|
private readonly constructorArguments: any[];
|
||||||
|
private readonly rendererInstance: React.RefObject<PageModalRenderer>;
|
||||||
|
|
||||||
|
private readonly modalOptions: ModalOptions;
|
||||||
|
|
||||||
|
private state: ModalState;
|
||||||
|
|
||||||
|
private modalInstance: AbstractModal;
|
||||||
|
private htmlContainer: HTMLDivElement;
|
||||||
|
|
||||||
|
private modalInitializePromise: Promise<void>;
|
||||||
|
|
||||||
|
constructor(modalType: RegisteredModal<any>, constructorArguments: any[], modalOptions: ModalOptions) {
|
||||||
|
this.events = new Registry<ModalInstanceEvents>();
|
||||||
|
|
||||||
|
this.modalKlass = modalType;
|
||||||
|
this.modalOptions = modalOptions;
|
||||||
|
this.constructorArguments = constructorArguments;
|
||||||
|
|
||||||
|
this.rendererInstance = React.createRef();
|
||||||
|
this.state = ModalState.DESTROYED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async constructModal() {
|
||||||
|
if(this.htmlContainer || this.modalInstance) {
|
||||||
|
throw tr("internal modal has already been constructed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalClass = await this.modalKlass.classLoader();
|
||||||
|
if(!modalClass) {
|
||||||
|
throw tr("invalid modal class");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.modalInstance = constructAbstractModalClass(modalClass.default, { windowed: false }, this.constructorArguments);
|
||||||
|
} catch (error) {
|
||||||
|
logError(LogCategory.GENERAL, tr("Failed to create new modal of instance type %s: %o"), this.modalKlass.modalId, error);
|
||||||
|
throw tr("failed to create new modal instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.htmlContainer = document.createElement("div");
|
||||||
|
document.body.appendChild(this.htmlContainer);
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
ReactDOM.render(
|
||||||
|
<PageModalRenderer modalInstance={this.modalInstance} onBackdropClicked={this.getCloseCallback()} ref={this.rendererInstance}>
|
||||||
|
<ModalFrameRenderer>
|
||||||
|
<ModalFrameTopRenderer
|
||||||
|
replacePageTitle={false}
|
||||||
|
modalInstance={this.modalInstance}
|
||||||
|
|
||||||
|
onClose={this.getCloseCallback()}
|
||||||
|
onPopout={this.getPopoutCallback()}
|
||||||
|
onMinimize={this.getMinimizeCallback()}
|
||||||
|
/>
|
||||||
|
<ModalBodyRenderer modalInstance={this.modalInstance} />
|
||||||
|
</ModalFrameRenderer>
|
||||||
|
</PageModalRenderer>,
|
||||||
|
this.htmlContainer,
|
||||||
|
resolve
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private destructModal() {
|
||||||
|
this.state = ModalState.DESTROYED;
|
||||||
|
if(this.htmlContainer) {
|
||||||
|
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
||||||
|
this.htmlContainer.remove();
|
||||||
|
this.htmlContainer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.modalInstance) {
|
||||||
|
this.modalInstance["onDestroy"]();
|
||||||
|
this.modalInstance = undefined;
|
||||||
|
}
|
||||||
|
this.events.fire("notify_destroy");
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): ModalState {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEvents(): Registry<ModalInstanceEvents> {
|
||||||
|
return this.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
async show() : Promise<void> {
|
||||||
|
if(!this.modalInstance) {
|
||||||
|
this.modalInitializePromise = this.constructModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.modalInitializePromise) {
|
||||||
|
await this.modalInitializePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.rendererInstance.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = ModalState.SHOWN;
|
||||||
|
this.modalInstance["onOpen"]();
|
||||||
|
await new Promise(resolve => this.rendererInstance.current.setState({ shown: true }, resolve));
|
||||||
|
this.events.fire("notify_open");
|
||||||
|
}
|
||||||
|
|
||||||
|
async hide() : Promise<void> {
|
||||||
|
if(this.modalInitializePromise) {
|
||||||
|
await this.modalInitializePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.rendererInstance.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = ModalState.HIDDEN;
|
||||||
|
this.modalInstance["onClose"]();
|
||||||
|
await new Promise(resolve => this.rendererInstance.current.setState({ shown: false }, resolve));
|
||||||
|
|
||||||
|
/* TODO: Somehow get the real animation finish signal? */
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
this.events.fire("notify_close");
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.destructModal();
|
||||||
|
this.events.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCloseCallback() {
|
||||||
|
return () => this.events.fire("action_close");
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPopoutCallback() {
|
||||||
|
if(!this.modalKlass.popoutSupported) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof this.modalOptions.popoutable !== "boolean" || !this.modalOptions.popoutable) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => this.events.fire("action_popout");
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMinimizeCallback() {
|
||||||
|
/* We can't minimize any windows */
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
.client {
|
.tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #D8D8D8;
|
color: #D8D8D8;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
|
@ -11,8 +11,42 @@ const cssStyle = require("./EntryTags.scss");
|
||||||
|
|
||||||
let ipcChannel: IPCChannel;
|
let ipcChannel: IPCChannel;
|
||||||
|
|
||||||
export const ClientTag = (props: { clientName: string, clientUniqueId: string, handlerId: string, clientId?: number, clientDatabaseId?: number, className?: string }) => (
|
export const ServerTag = React.memo((props: {
|
||||||
<div className={cssStyle.client + (props.className ? ` ${props.className}` : ``)}
|
serverName: string,
|
||||||
|
handlerId: string,
|
||||||
|
serverUniqueId?: string,
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
||||||
|
onContextMenu={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
ipcChannel.sendMessage("contextmenu-server", {
|
||||||
|
handlerId: props.handlerId,
|
||||||
|
serverUniqueId: props.serverUniqueId,
|
||||||
|
|
||||||
|
pageX: event.pageX,
|
||||||
|
pageY: event.pageY
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
draggable={false}
|
||||||
|
>
|
||||||
|
{props.serverName}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ClientTag = React.memo((props: {
|
||||||
|
clientName: string,
|
||||||
|
clientUniqueId: string,
|
||||||
|
handlerId: string,
|
||||||
|
clientId?: number,
|
||||||
|
clientDatabaseId?: number,
|
||||||
|
className?: string
|
||||||
|
}) => (
|
||||||
|
<div className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
||||||
onContextMenu={event => {
|
onContextMenu={event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
@ -45,11 +79,16 @@ export const ClientTag = (props: { clientName: string, clientUniqueId: string, h
|
||||||
>
|
>
|
||||||
{props.clientName}
|
{props.clientName}
|
||||||
</div>
|
</div>
|
||||||
);
|
));
|
||||||
|
|
||||||
export const ChannelTag = (props: { channelName: string, channelId: number, handlerId: string, className?: string }) => (
|
export const ChannelTag = React.memo((props: {
|
||||||
|
channelName: string,
|
||||||
|
channelId: number,
|
||||||
|
handlerId: string,
|
||||||
|
className?: string
|
||||||
|
}) => (
|
||||||
<div
|
<div
|
||||||
className={cssStyle.client + (props.className ? ` ${props.className}` : ``)}
|
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
||||||
onContextMenu={event => {
|
onContextMenu={event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
@ -77,8 +116,7 @@ export const ChannelTag = (props: { channelName: string, channelId: number, hand
|
||||||
>
|
>
|
||||||
{props.channelName}
|
{props.channelName}
|
||||||
</div>
|
</div>
|
||||||
);
|
));
|
||||||
|
|
||||||
|
|
||||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
name: "entry tags",
|
name: "entry tags",
|
||||||
|
|
|
@ -22,7 +22,7 @@ export class AudioLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static spawnNewWorker() : Worker {
|
private static spawnNewWorker() : Worker {
|
||||||
/*
|
/*
|
||||||
* Attention don't use () => new Worker(...).
|
* Attention don't use () => new Worker(...).
|
||||||
* This confuses the worker plugin and will not emit any modules
|
* This confuses the worker plugin and will not emit any modules
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue