Added the possibility to popout modals and finished the work on the bookmark modals

master
WolverinDEV 2021-03-16 15:14:03 +01:00
parent 95f4a7bfb7
commit 1b2de3ed63
43 changed files with 1371 additions and 831 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>,
] ]
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
.client { .tag {
display: inline-block; display: inline-block;
color: #D8D8D8; color: #D8D8D8;
font-weight: 700; font-weight: 700;

View File

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

View File

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