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);
|
||||
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 {
|
||||
notify_properties_updated: {
|
||||
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 {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
||||
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");
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ import PermissionType from "tc-shared/permission/PermissionType";
|
|||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||
import {tra} from "tc-shared/i18n/localize";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
import {LogCategory, logError} from "tc-shared/log";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
const cssStyle = require("./ModalGroupCreate.scss");
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ import PermissionType from "tc-shared/permission/PermissionType";
|
|||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||
import {tra} from "tc-shared/i18n/localize";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
const cssStyle = require("./ModalGroupPermissionCopy.scss");
|
||||
|
||||
|
|
|
@ -1,5 +1,134 @@
|
|||
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) {
|
||||
/* 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 {
|
||||
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 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 {
|
||||
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 {
|
||||
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 {
|
||||
return undefined;
|
||||
return <Translatable>Add server to bookmarks</Translatable>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export = ModalBookmarksAddServer;
|
|
@ -579,6 +579,7 @@ const BookmarkSettingChannelPassword = () => {
|
|||
}
|
||||
|
||||
const BookmarkInfoRenderer = React.memo(() => {
|
||||
const selectedBookmark = useContext(SelectedBookmarkIdContext);
|
||||
const bookmarkInfo = useContext(SelectedBookmarkInfoContext);
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.overlay + " " + (selectedBookmark.type !== "bookmark" ? cssStyle.shown : "")}>
|
||||
<div className={cssStyle.text}>
|
||||
{/* bookmark is a directory */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -774,11 +780,11 @@ class ModalBookmarks extends AbstractModal {
|
|||
this.variables.destroy();
|
||||
}
|
||||
|
||||
renderBody(renderBody): React.ReactElement {
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EventContext.Provider value={this.events}>
|
||||
<VariableContext.Provider value={this.variables}>
|
||||
<div className={cssStyle.container + " " + (renderBody.windowed ? cssStyle.windowed : "")}>
|
||||
<div className={cssStyle.container + " " + (this.properties.windowed ? cssStyle.windowed : "")}>
|
||||
<BookmarkListContainer />
|
||||
<ContextDivider id={"separator-bookmarks"} direction={"horizontal"} defaultValue={25} />
|
||||
<BookmarkInfoContainer />
|
||||
|
|
|
@ -23,7 +23,7 @@ import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
|||
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {getIconManager} from "tc-shared/file/Icons";
|
||||
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");
|
||||
|
||||
|
|
|
@ -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 {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
import * as i18n from "../../../i18n/country";
|
||||
import {getIconManager} from "tc-shared/file/Icons";
|
||||
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||
|
@ -414,10 +413,6 @@ class ConnectModal extends AbstractModal {
|
|||
return <Translatable>Connect to a server</Translatable>;
|
||||
}
|
||||
|
||||
color(): "none" | "blue" {
|
||||
return "blue";
|
||||
}
|
||||
|
||||
verticalAlignment(): "top" | "center" | "bottom" {
|
||||
return "top";
|
||||
}
|
||||
|
|
|
@ -405,12 +405,3 @@ class ModalInvite extends AbstractModal {
|
|||
}
|
||||
}
|
||||
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";
|
||||
import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroupCreate";
|
||||
import {spawnModalGroupPermissionCopy} from "tc-shared/ui/modal/ModalGroupPermissionCopy";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
import {PermissionEditorTab} from "tc-shared/events/GlobalEvents";
|
||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
const cssStyle = require("./ModalPermissionEditor.scss");
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||
import {Registry} from "../../../events";
|
||||
import {Registry} from "tc-events";
|
||||
import {FileType} from "../../../file/FileManager";
|
||||
import {CommandResult} from "../../../connection/ServerConnectionDeclaration";
|
||||
import PermissionType from "../../../permission/PermissionType";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||
import {Registry} from "../../../events";
|
||||
import {Registry} from "tc-events";
|
||||
import {
|
||||
FileTransfer,
|
||||
FileTransferDirection,
|
||||
|
|
|
@ -6,10 +6,10 @@ import {FileTransferInfo} from "./FileTransferInfo";
|
|||
import {initializeRemoteFileBrowserController} from "./FileBrowserControllerRemote";
|
||||
import {initializeTransferInfoController} from "./FileTransferInfoController";
|
||||
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 {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions";
|
||||
import {TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
const cssStyle = require("./FileBrowserRenderer.scss");
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
VideoPreviewStatus,
|
||||
VideoSourceState
|
||||
} 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 {BoxedInputField, Select} from "tc-shared/ui/react-elements/InputField";
|
||||
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 {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {ScreenCaptureDevice} from "tc-shared/video/VideoSource";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
const ModalEvents = React.createContext<Registry<ModalVideoSourceEvents>>(undefined);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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 {WhatsNew} from "tc-shared/ui/modal/whats-new/Renderer";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
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 }) {
|
||||
const modal = spawnReactModal(class extends InternalModal {
|
||||
|
|
|
@ -34,6 +34,7 @@ export const ControlledBoxedInputField = (props: {
|
|||
onBlur?: () => void,
|
||||
|
||||
finishOnEnter?: boolean,
|
||||
refInput?: React.RefObject<HTMLInputElement>
|
||||
}) => {
|
||||
|
||||
return (
|
||||
|
@ -61,6 +62,8 @@ export const ControlledBoxedInputField = (props: {
|
|||
|
||||
<input key={"input"}
|
||||
|
||||
ref={props.refInput}
|
||||
|
||||
value={props.value || ""}
|
||||
placeholder={props.placeholder}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import {ReactElement} from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import {ReactElement} from "react";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
|
||||
const cssStyle = require("./Tooltip.scss");
|
||||
|
@ -73,6 +73,7 @@ export interface TooltipState {
|
|||
|
||||
export interface TooltipProperties {
|
||||
tooltip: () => ReactElement | ReactElement[] | string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
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 })}
|
||||
onClick={() => this.setState({ hovered: !this.state.hovered })}
|
||||
style={{ cursor: "pointer" }}
|
||||
className={this.props.className}
|
||||
>
|
||||
{this.props.children}
|
||||
</span>
|
||||
|
@ -124,8 +126,9 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
|||
|
||||
private onMouseEnter(event: React.MouseEvent) {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
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 }) => (
|
||||
<Tooltip tooltip={() => props.children}>
|
||||
export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string, outerClassName?: string }) => (
|
||||
<Tooltip tooltip={() => props.children} className={props.outerClassName}>
|
||||
<div className={cssStyle.iconTooltip + " " + props.className}>
|
||||
<img src="img/icon_tooltip.svg" alt={""} />
|
||||
</div>
|
||||
|
|
|
@ -8,14 +8,15 @@ import {
|
|||
Popout2ControllerMessages,
|
||||
PopoutIPCMessage
|
||||
} 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 {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 constructorArguments: any[];
|
||||
|
||||
private readonly modalEvents: Registry<ModalEvents>;
|
||||
private readonly modalEvents: Registry<ModalInstanceEvents>;
|
||||
private modalState: ModalState = ModalState.DESTROYED;
|
||||
|
||||
private readonly documentUnloadListener: () => void;
|
||||
|
@ -26,7 +27,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
|||
this.modalType = modalType;
|
||||
this.constructorArguments = constructorArguments;
|
||||
|
||||
this.modalEvents = new Registry<ModalEvents>();
|
||||
this.modalEvents = new Registry<ModalInstanceEvents>();
|
||||
|
||||
this.ipcChannel = ipc.getIpcInstance().createChannel(kPopoutIPCChannelId);
|
||||
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
||||
|
@ -38,7 +39,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
|||
return {}; /* FIXME! */
|
||||
}
|
||||
|
||||
getEvents(): Registry<ModalEvents> {
|
||||
getEvents(): Registry<ModalInstanceEvents> {
|
||||
return this.modalEvents;
|
||||
}
|
||||
|
||||
|
@ -85,7 +86,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
|||
}
|
||||
|
||||
window.addEventListener("unload", this.documentUnloadListener);
|
||||
this.modalEvents.fire("open");
|
||||
this.modalEvents.fire("notify_open");
|
||||
}
|
||||
|
||||
private doDestroyWindow() {
|
||||
|
@ -99,12 +100,13 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
|||
|
||||
this.doDestroyWindow();
|
||||
this.modalState = ModalState.HIDDEN;
|
||||
this.modalEvents.fire("close");
|
||||
this.modalEvents.fire("notify_close");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(this.modalState === ModalState.DESTROYED)
|
||||
if(this.modalState === ModalState.DESTROYED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.doDestroyWindow();
|
||||
if(this.ipcChannel) {
|
||||
|
@ -113,7 +115,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
|||
|
||||
this.destroyIPC();
|
||||
this.modalState = ModalState.DESTROYED;
|
||||
this.modalEvents.fire("destroy");
|
||||
this.modalEvents.fire("notify_destroy");
|
||||
}
|
||||
|
||||
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 * as ipc from "../../../ipc/BrowserIPC";
|
||||
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 {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 "../../../file/RemoteAvatars";
|
||||
import "../../../file/RemoteIcons";
|
||||
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) {
|
||||
(window as any).__native_client_init_shared(__webpack_require__);
|
||||
|
@ -47,18 +47,14 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||
name: "modal renderer loader",
|
||||
priority: 10,
|
||||
function: async () => {
|
||||
if(__build.target === "web") {
|
||||
modalRenderer = new WebModalRenderer();
|
||||
} else {
|
||||
modalRenderer = new ClientModalRenderer({
|
||||
close() {
|
||||
getPopoutController().doClose()
|
||||
},
|
||||
minimize() {
|
||||
getPopoutController().doMinimize()
|
||||
}
|
||||
});
|
||||
}
|
||||
modalRenderer = new ModalRenderer({
|
||||
close() {
|
||||
getPopoutController().doClose()
|
||||
},
|
||||
minimize() {
|
||||
getPopoutController().doMinimize()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -88,7 +84,7 @@ loader.register_task(Stage.LOADED, {
|
|||
priority: 100,
|
||||
function: async () => {
|
||||
try {
|
||||
modalInstance = new modalClass(...getPopoutController().getConstructorArguments());
|
||||
modalInstance = constructAbstractModalClass(modalClass, { windowed: true }, getPopoutController().getConstructorArguments());
|
||||
modalRenderer.renderModal(modalInstance);
|
||||
} catch(error) {
|
||||
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 {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;
|
||||
|
||||
export function setExternalModalControllerFactory(factory: ControllerFactory) {
|
||||
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") {
|
||||
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 {ModalGlobalSettingsEditorEvents} from "tc-shared/ui/modal/global-settings-editor/Definitions";
|
||||
import {InviteUiEvents, InviteUiVariables} from "tc-shared/ui/modal/invite/Definitions";
|
||||
|
||||
import {ReactElement} from "react";
|
||||
import * as React from "react";
|
||||
import React, {ReactElement} from "react";
|
||||
import {IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||
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 ModalRenderType = "page" | "dialog";
|
||||
|
@ -52,21 +54,10 @@ export interface ModalOptions {
|
|||
popedOut?: boolean
|
||||
}
|
||||
|
||||
export interface ModalFunctionController {
|
||||
minimize();
|
||||
supportMinimize() : boolean;
|
||||
|
||||
maximize();
|
||||
supportMaximize() : boolean;
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
export interface ModalEvents {
|
||||
"open": {},
|
||||
"close": {},
|
||||
|
||||
/* create is implicitly at object creation */
|
||||
"destroy": {}
|
||||
}
|
||||
|
||||
|
@ -76,6 +67,27 @@ export enum ModalState {
|
|||
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 {
|
||||
getOptions() : Readonly<ModalOptions>;
|
||||
getEvents() : Registry<ModalEvents>;
|
||||
|
@ -87,10 +99,23 @@ export interface ModalController {
|
|||
destroy();
|
||||
}
|
||||
|
||||
export abstract class AbstractModal {
|
||||
protected constructor() {}
|
||||
export interface ModalInstanceProperties {
|
||||
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;
|
||||
|
||||
/* only valid for the "inline" modals */
|
||||
|
@ -104,13 +129,24 @@ export abstract class AbstractModal {
|
|||
protected onClose() {}
|
||||
protected onOpen() {}
|
||||
}
|
||||
export abstract class InternalModal extends AbstractModal {}
|
||||
|
||||
|
||||
export interface ModalRenderer {
|
||||
renderModal(modal: AbstractModal | undefined);
|
||||
export function constructAbstractModalClass<T extends keyof ModalConstructorArguments>(
|
||||
klass: new (...args: ModalConstructorArguments[T]) => AbstractModal,
|
||||
properties: ModalInstanceProperties,
|
||||
args: ModalConstructorArguments[T]) : AbstractModal {
|
||||
currentModalProperties = properties;
|
||||
try {
|
||||
return new klass(...args);
|
||||
} finally {
|
||||
currentModalProperties = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface ModalConstructorArguments {
|
||||
"__internal__modal__": any[],
|
||||
|
||||
"video-viewer": [
|
||||
/* events */ IpcRegistryDescription<VideoViewerEvents>,
|
||||
/* handlerId */ string,
|
||||
|
@ -137,5 +173,9 @@ export interface ModalConstructorArguments {
|
|||
"modal-bookmarks": [
|
||||
/* events */ IpcRegistryDescription<ModalBookmarkEvents>,
|
||||
/* variables */ IpcVariableDescriptor<ModalBookmarkVariables>,
|
||||
],
|
||||
"modal-bookmark-add-server": [
|
||||
/* events */ IpcRegistryDescription<ModalBookmarksAddServerEvents>,
|
||||
/* variables */ IpcVariableDescriptor<ModalBookmarksAddServerVariables>,
|
||||
]
|
||||
}
|
|
@ -79,3 +79,9 @@ registerModal({
|
|||
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 {
|
||||
AbstractModal,
|
||||
ModalFunctionController,
|
||||
ModalOptions,
|
||||
ModalRenderType
|
||||
} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {useContext} from "react";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import React from "react";
|
||||
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
|
||||
import TeaCupImage from "./TeaCup.png";
|
||||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
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 {
|
||||
readonly renderType: ModalRenderType;
|
||||
readonly modal: AbstractModal;
|
||||
onClose?: () => void,
|
||||
onPopout?: () => void,
|
||||
onMinimize?: () => void,
|
||||
|
||||
constructor(renderType: ModalRenderType, modal: AbstractModal,) {
|
||||
this.renderType = renderType;
|
||||
this.modal = modal;
|
||||
replacePageTitle: boolean,
|
||||
}> {
|
||||
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: {
|
||||
modal: AbstractModal,
|
||||
modalOptions: ModalOptions,
|
||||
modalActions: ModalFunctionController
|
||||
}) => {
|
||||
export class ModalFrameRenderer extends React.PureComponent<{
|
||||
children: [React.ReactElement<ModalFrameTopRenderer>, React.ReactElement<ModalBodyRenderer>]
|
||||
}> {
|
||||
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 {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
||||
import {InternalModal, InternalModalController} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
|
||||
import {GenericModalController} from "tc-shared/ui/react-elements/modal/Controller";
|
||||
|
||||
export function spawnModal<T extends keyof ModalConstructorArguments>(modal: T, constructorArguments: ModalConstructorArguments[T], options?: ModalOptions) : ModalController {
|
||||
if(options?.popedOut) {
|
||||
return spawnExternalModal(modal, constructorArguments, options);
|
||||
} else {
|
||||
return spawnInternalModal(modal, constructorArguments, options);
|
||||
}
|
||||
return new GenericModalController(modal, constructorArguments, options);
|
||||
}
|
||||
|
||||
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new () => ModalClass) : InternalModalController;
|
||||
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : InternalModalController;
|
||||
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, A3>(modalClass: new (..._: [A1, A2, A3]) => ModalClass, arg1: A1, arg2: A2, arg3: A3) : 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) : 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) : InternalModalController;
|
||||
export function spawnReactModal<ModalClass extends InternalModal>(modalClass: new (..._: any[]) => ModalClass, ...args: any[]) : InternalModalController {
|
||||
return new InternalModalController({
|
||||
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);
|
||||
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new () => ModalClass) : ModalController;
|
||||
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) : ModalController;
|
||||
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) : 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) : ModalController;
|
||||
export function spawnReactModal<ModalClass extends InternalModal>(modalClass: new (..._: any[]) => ModalClass, ...args: any[]) : ModalController {
|
||||
return GenericModalController.fromInternalModal(modalClass, args);
|
||||
}
|
|
@ -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;
|
||||
color: #D8D8D8;
|
||||
font-weight: 700;
|
||||
|
|
|
@ -11,8 +11,42 @@ const cssStyle = require("./EntryTags.scss");
|
|||
|
||||
let ipcChannel: IPCChannel;
|
||||
|
||||
export const ClientTag = (props: { clientName: string, clientUniqueId: string, handlerId: string, clientId?: number, clientDatabaseId?: number, className?: string }) => (
|
||||
<div className={cssStyle.client + (props.className ? ` ${props.className}` : ``)}
|
||||
export const ServerTag = React.memo((props: {
|
||||
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 => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -45,11 +79,16 @@ export const ClientTag = (props: { clientName: string, clientUniqueId: string, h
|
|||
>
|
||||
{props.clientName}
|
||||
</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
|
||||
className={cssStyle.client + (props.className ? ` ${props.className}` : ``)}
|
||||
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -77,8 +116,7 @@ export const ChannelTag = (props: { channelName: string, channelId: number, hand
|
|||
>
|
||||
{props.channelName}
|
||||
</div>
|
||||
);
|
||||
|
||||
));
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "entry tags",
|
||||
|
|
Loading…
Reference in New Issue