commit
f75a6e29fe
15
ChangeLog.md
15
ChangeLog.md
|
@ -1,4 +1,19 @@
|
|||
# Changelog:
|
||||
* **22.01.21**
|
||||
- Allowing the user to easily change the channel name mode
|
||||
- Fixed channel name mode parsing
|
||||
- Improved the modal algorithms as preparation for easier popoutable modals
|
||||
|
||||
* **16.01.21**
|
||||
- Various bugfixes (Thanks to Vafin)
|
||||
|
||||
* **15.01.21**
|
||||
- Fixed the history toggle (Thanks to Vafin)
|
||||
|
||||
* **12.01.21**
|
||||
- Fixed bug where the quick video select popup did not start the video broadcasting
|
||||
- Fixed a bug where an invalid H264 codec may caused video connection setup failure
|
||||
|
||||
* **09.01.21**
|
||||
- The connect modal now connects when pressing `Enter` on the address line
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ function load_script_url(url: string) : Promise<void> {
|
|||
const timeout_handle = setTimeout(() => {
|
||||
cleanup();
|
||||
reject("timeout");
|
||||
}, 15 * 1000);
|
||||
}, 120 * 1000);
|
||||
script_tag.type = "application/javascript";
|
||||
script_tag.async = true;
|
||||
script_tag.defer = true;
|
||||
|
|
|
@ -41,6 +41,9 @@ import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog";
|
|||
import {PlaylistManager} from "tc-shared/music/PlaylistManager";
|
||||
import {connectionHistory} from "tc-shared/connectionlog/History";
|
||||
import {ConnectParameters} from "tc-shared/ui/modal/connect/Controller";
|
||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
export enum InputHardwareState {
|
||||
MISSING,
|
||||
|
@ -240,7 +243,7 @@ export class ConnectionHandler {
|
|||
this.localClient = new LocalClientEntry(this);
|
||||
this.localClient.channelTree = this.channelTree;
|
||||
|
||||
this.events_.register_handler(this);
|
||||
this.events_.registerHandler(this);
|
||||
this.pluginCmdRegistry.registerHandler(new W2GPluginCmdHandler());
|
||||
|
||||
this.events_.fire("notify_handler_initialized");
|
||||
|
@ -1073,7 +1076,7 @@ export class ConnectionHandler {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this.events_.unregister_handler(this);
|
||||
this.events_.unregisterHandler(this);
|
||||
this.cancelAutoReconnect(true);
|
||||
|
||||
this.pluginCmdRegistry?.destroy();
|
||||
|
|
|
@ -2,6 +2,9 @@ import {ConnectionHandler, DisconnectReason} from "./ConnectionHandler";
|
|||
import {Registry} from "./events";
|
||||
import {Stage} from "tc-loader";
|
||||
import * as loader from "tc-loader";
|
||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
export interface ConnectionManagerEvents {
|
||||
notify_handler_created: {
|
||||
|
|
|
@ -74,6 +74,7 @@ class ClientServiceConnection {
|
|||
let address;
|
||||
address = "client-services.teaspeak.de:27791";
|
||||
//address = "localhost:1244";
|
||||
//address = "192.168.40.135:1244";
|
||||
|
||||
this.connection = new WebSocket(`wss://${address}/ws-api/v${kApiVersion}`);
|
||||
this.connection.onclose = event => {
|
||||
|
@ -375,7 +376,7 @@ export class ClientServices {
|
|||
} else {
|
||||
const os = window.detectedBrowser.os;
|
||||
const osParts = os.split(" ");
|
||||
if(osParts.last().match(/^[0-9]+$/)) {
|
||||
if(osParts.last().match(/^[0-9\.]+$/)) {
|
||||
payload.platform_version = osParts.last();
|
||||
osParts.splice(osParts.length - 1, 1);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class ServerFeatures {
|
|||
readonly events: Registry<ServerFeatureEvents>;
|
||||
private readonly connection: ConnectionHandler;
|
||||
private readonly explicitCommandHandler: ExplicitCommandHandler;
|
||||
private readonly stateChangeListener: (event: ConnectionEvents["notify_connection_state_changed"]) => void;
|
||||
private readonly stateChangeListener: () => void;
|
||||
|
||||
private featureAwait: Promise<boolean>;
|
||||
private featureAwaitCallback: (success: boolean) => void;
|
||||
|
@ -68,7 +68,7 @@ export class ServerFeatures {
|
|||
}
|
||||
});
|
||||
|
||||
this.connection.events().on("notify_connection_state_changed", this.stateChangeListener = event => {
|
||||
this.stateChangeListener = this.connection.events().on("notify_connection_state_changed", event => {
|
||||
if(event.newState === ConnectionState.CONNECTED) {
|
||||
this.connection.getServerConnection().send_command("listfeaturesupport").catch(error => {
|
||||
this.disableAllFeatures();
|
||||
|
@ -95,7 +95,7 @@ export class ServerFeatures {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this.connection.events().off(this.stateChangeListener);
|
||||
this.stateChangeListener();
|
||||
this.connection.getServerConnection()?.command_handler_boss()?.unregister_explicit_handler("notifyfeaturesupport", this.explicitCommandHandler);
|
||||
|
||||
if(this.featureAwaitCallback) {
|
||||
|
|
|
@ -63,7 +63,6 @@ class RetryTimeCalculator {
|
|||
}
|
||||
|
||||
calculateRetryTime() {
|
||||
return 0;
|
||||
if(this.retryCount >= 5) {
|
||||
/* no more retries */
|
||||
return 0;
|
||||
|
@ -542,6 +541,8 @@ export class RTCConnection {
|
|||
this.reset(true);
|
||||
|
||||
this.connection.events.on("notify_connection_state_changed", event => this.handleConnectionStateChanged(event));
|
||||
|
||||
(window as any).rtp = this;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
|
|
@ -48,7 +48,7 @@ export class SdpProcessor {
|
|||
rate: 90000,
|
||||
rtcpFb: [ "nack", "nack pli", "ccm fir", "transport-cc" ],
|
||||
},
|
||||
{
|
||||
window.detectedBrowser.name.indexOf("ios") === -1 && window.detectedBrowser.name !== "safari" ? {
|
||||
payload: H264_PAYLOAD_TYPE,
|
||||
codec: "H264",
|
||||
rate: 90000,
|
||||
|
@ -57,10 +57,10 @@ export class SdpProcessor {
|
|||
fmtp: {
|
||||
"level-asymmetry-allowed": 1,
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "4d0028",
|
||||
"profile-level-id": "42001f",
|
||||
"max-fr": 30,
|
||||
}
|
||||
},
|
||||
} : undefined,
|
||||
];
|
||||
|
||||
private rtpRemoteChannelMapping: {[key: string]: number};
|
||||
|
@ -150,7 +150,11 @@ export class SdpProcessor {
|
|||
media.rtcpFb = [];
|
||||
media.rtcpFbTrrInt = [];
|
||||
|
||||
for(let codec of (media.type === "audio" ? this.kAudioCodecs : this.kVideoCodecs)) {
|
||||
for(const codec of (media.type === "audio" ? this.kAudioCodecs : this.kVideoCodecs)) {
|
||||
if(!codec) {
|
||||
continue;
|
||||
}
|
||||
|
||||
media.rtp.push({
|
||||
payload: codec.payload,
|
||||
codec: codec.codec,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -29,6 +29,7 @@ import {IPCChannel} from "../ipc/BrowserIPC";
|
|||
import {ConnectionHandler} from "../ConnectionHandler";
|
||||
import {ErrorCode} from "../connection/ErrorCode";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {EventDispatchType} from "tc-shared/events";
|
||||
|
||||
/* FIXME: Retry avatar download after some time! */
|
||||
|
||||
|
@ -424,8 +425,10 @@ class LocalAvatarManagerFactory extends AbstractAvatarManagerFactory {
|
|||
subscribedAvatars.push({
|
||||
avatar: avatar,
|
||||
remoteAvatarId: avatarId,
|
||||
unregisterCallback: avatar.events.onAll(event => {
|
||||
this.ipcChannel.sendMessage("avatar-event", { handlerId: handlerId, avatarId: avatarId, event: event }, remoteId);
|
||||
unregisterCallback: avatar.events.registerConsumer({
|
||||
handleEvent(mode: EventDispatchType, type: string, payload: any) {
|
||||
this.ipcChannel.sendMessage("avatar-event", { handlerId: handlerId, avatarId: avatarId, type, payload }, remoteId);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -142,11 +142,11 @@ class RemoteAvatarManager extends AbstractAvatarManager {
|
|||
avatar.updateStateFromRemote(data.state, data.stateData);
|
||||
}
|
||||
|
||||
handleAvatarEvent(data: any) {
|
||||
const avatar = this.knownAvatars.find(e => e.avatarId === data.avatarId);
|
||||
handleAvatarEvent(type: string, payload: any) {
|
||||
const avatar = this.knownAvatars.find(e => e.avatarId === payload.avatarId);
|
||||
if(!avatar) return;
|
||||
|
||||
avatar.events.fire(data.event.type, data.event, true);
|
||||
avatar.events.fire(type as any, payload, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,7 +211,7 @@ class RemoteAvatarManagerFactory extends AbstractAvatarManagerFactory {
|
|||
manager?.handleAvatarLoadCallback(message.data);
|
||||
} else if(message.type === "avatar-event") {
|
||||
const manager = this.manager[message.data.handlerId];
|
||||
manager?.handleAvatarEvent(message.data);
|
||||
manager?.handleAvatarEvent(message.data.type, message.data.payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {LogCategory, logError, logInfo, logWarn} from "../log";
|
||||
import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "../log";
|
||||
import {guid} from "../crypto/uid";
|
||||
import {Settings, StaticSettings} from "../settings";
|
||||
import * as loader from "tc-loader";
|
||||
|
@ -48,22 +48,29 @@ export interface TranslationRepository {
|
|||
}
|
||||
|
||||
let translations: Translation[] = [];
|
||||
let fast_translate: { [key:string]:string; } = {};
|
||||
export function tr(message: string, key?: string) {
|
||||
const sloppy = fast_translate[message];
|
||||
if(sloppy) return sloppy;
|
||||
let translateCache: { [key:string]: string; } = {};
|
||||
export function tr(message: string, key?: string) : string {
|
||||
const sloppy = translateCache[message];
|
||||
if(sloppy) {
|
||||
return sloppy;
|
||||
}
|
||||
|
||||
logInfo(LogCategory.I18N, "Translating \"%s\". Default: \"%s\"", key, message);
|
||||
logTrace(LogCategory.I18N, "Translating \"%s\". Default: \"%s\"", key, message);
|
||||
|
||||
let translated = message;
|
||||
let translated;
|
||||
for(const translation of translations) {
|
||||
if(translation.key.message == message) {
|
||||
if(translation.key.message === message) {
|
||||
translated = translation.translated;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fast_translate[message] = translated;
|
||||
if(typeof translated === "string") {
|
||||
translateCache[message] = translated;
|
||||
} else {
|
||||
logDebug(LogCategory.I18N, "Missing translation for \"%s\".", message);
|
||||
translateCache[message] = translated = message;
|
||||
}
|
||||
return translated;
|
||||
}
|
||||
|
||||
|
@ -316,6 +323,7 @@ export async function initialize() {
|
|||
if(cfg.current_translation_url) {
|
||||
try {
|
||||
await load_file(cfg.current_translation_url, cfg.current_translation_path);
|
||||
translateCache = {};
|
||||
} catch (error) {
|
||||
logError(LogCategory.I18N, tr("Failed to initialize selected translation: %o"), error);
|
||||
const show_error = () => {
|
||||
|
|
|
@ -47,6 +47,9 @@ import "./ui/elements/ContextDivider";
|
|||
import "./ui/elements/Tab";
|
||||
import "./clientservice";
|
||||
import {initializeKeyControl} from "./KeyControl";
|
||||
import {assertMainApplication} from "tc-shared/ui/utils";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
let preventWelcomeUI = false;
|
||||
async function initialize() {
|
||||
|
|
|
@ -248,9 +248,9 @@ export class WebVideoSource implements VideoSource {
|
|||
private readonly deviceId: string;
|
||||
private readonly displayName: string;
|
||||
private readonly stream: MediaStream;
|
||||
private readonly initialSettings: VideoSourceInitialSettings;
|
||||
private referenceCount = 1;
|
||||
|
||||
private initialSettings: VideoSourceInitialSettings;
|
||||
|
||||
constructor(deviceId: string, displayName: string, stream: MediaStream) {
|
||||
this.deviceId = deviceId;
|
||||
|
@ -291,13 +291,13 @@ export class WebVideoSource implements VideoSource {
|
|||
|
||||
return {
|
||||
minWidth: capabilities?.width?.min || 1,
|
||||
maxWidth: capabilities?.width?.max || this.initialSettings.width,
|
||||
maxWidth: capabilities?.width?.max || this.initialSettings.width || undefined,
|
||||
|
||||
minHeight: capabilities?.height?.min || 1,
|
||||
maxHeight: capabilities?.height?.max || this.initialSettings.height,
|
||||
maxHeight: capabilities?.height?.max || this.initialSettings.height || undefined,
|
||||
|
||||
minFrameRate: capabilities?.frameRate?.min || 1,
|
||||
maxFrameRate: capabilities?.frameRate?.max || this.initialSettings.frameRate
|
||||
maxFrameRate: capabilities?.frameRate?.max || this.initialSettings.frameRate || undefined
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -143,6 +143,7 @@ export namespace AppParameters {
|
|||
|
||||
parseParameters();
|
||||
}
|
||||
(window as any).AppParameters = AppParameters;
|
||||
|
||||
export namespace AppParameters {
|
||||
export const KEY_CONNECT_ADDRESS: RegistryKey<string> = {
|
||||
|
@ -216,6 +217,12 @@ export namespace AppParameters {
|
|||
description: "Address of the owner for IPC communication."
|
||||
};
|
||||
|
||||
export const KEY_IPC_REMOTE_POPOUT_CHANNEL: RegistryKey<string> = {
|
||||
key: "ipc-channel",
|
||||
valueType: "string",
|
||||
description: "The channel name of the popout channel communication id"
|
||||
};
|
||||
|
||||
export const KEY_MODAL_TARGET: RegistryKey<string> = {
|
||||
key: "modal-target",
|
||||
valueType: "string",
|
||||
|
@ -644,6 +651,20 @@ export class Settings {
|
|||
valueType: "number",
|
||||
};
|
||||
|
||||
static readonly KEY_VIDEO_DEFAULT_MAX_BANDWIDTH: ValuedRegistryKey<number> = {
|
||||
key: "video_default_max_bandwidth",
|
||||
defaultValue: 1_600_000,
|
||||
description: "The default video bandwidth to use in bits/seconds.\nA too high value might not be allowed by all server permissions.",
|
||||
valueType: "number",
|
||||
};
|
||||
|
||||
static readonly KEY_VIDEO_DEFAULT_KEYFRAME_INTERVAL: ValuedRegistryKey<number> = {
|
||||
key: "video_default_keyframe_interval",
|
||||
defaultValue: 0,
|
||||
description: "The default interval to forcibly request a keyframe from ourself in seconds. A value of zero means no such interval.",
|
||||
valueType: "number",
|
||||
};
|
||||
|
||||
static readonly KEY_VIDEO_DYNAMIC_QUALITY: ValuedRegistryKey<boolean> = {
|
||||
key: "video_dynamic_quality",
|
||||
defaultValue: true,
|
||||
|
|
|
@ -114,55 +114,73 @@ export interface ChannelEvents extends ChannelTreeEntryEvents {
|
|||
notify_description_changed: {}
|
||||
}
|
||||
|
||||
export class ParsedChannelName {
|
||||
export type ChannelNameAlignment = "center" | "right" | "left" | "normal" | "repetitive";
|
||||
export class ChannelNameParser {
|
||||
readonly originalName: string;
|
||||
alignment: "center" | "right" | "left" | "normal" | "repetitive";
|
||||
alignment: ChannelNameAlignment;
|
||||
text: string; /* does not contain any alignment codes */
|
||||
uniqueId: string;
|
||||
|
||||
constructor(name: string, hasParentChannel: boolean) {
|
||||
this.originalName = name;
|
||||
this.parse(hasParentChannel);
|
||||
}
|
||||
|
||||
private parse(has_parent_channel: boolean) {
|
||||
private parse(hasParentChannel: boolean) {
|
||||
this.alignment = "normal";
|
||||
if(this.originalName.length < 3) {
|
||||
this.text = this.originalName;
|
||||
return;
|
||||
}
|
||||
|
||||
parse_type:
|
||||
if(!has_parent_channel && this.originalName.charAt(0) == '[') {
|
||||
|
||||
parseType:
|
||||
if(!hasParentChannel && this.originalName.charAt(0) == '[') {
|
||||
let end = this.originalName.indexOf(']');
|
||||
if(end === -1) break parse_type;
|
||||
if(end === -1) {
|
||||
break parseType;
|
||||
}
|
||||
|
||||
let options = this.originalName.substr(1, end - 1);
|
||||
if(options.indexOf("spacer") === -1) break parse_type;
|
||||
options = options.substr(0, options.indexOf("spacer"));
|
||||
const spacerIndex = options.indexOf("spacer");
|
||||
if(spacerIndex === -1) break parseType;
|
||||
this.uniqueId = options.substring(spacerIndex + 6);
|
||||
options = options.substr(0, spacerIndex);
|
||||
|
||||
if(options.length == 0)
|
||||
if(options.length == 0) {
|
||||
options = "l";
|
||||
else if(options.length > 1)
|
||||
} else if(options.length > 1) {
|
||||
options = options[0];
|
||||
}
|
||||
|
||||
switch (options) {
|
||||
case "r":
|
||||
this.alignment = "right";
|
||||
break;
|
||||
|
||||
case "l":
|
||||
this.alignment = "center";
|
||||
this.alignment = "left";
|
||||
break;
|
||||
|
||||
case "c":
|
||||
this.alignment = "center";
|
||||
break;
|
||||
|
||||
case "*":
|
||||
this.alignment = "repetitive";
|
||||
break;
|
||||
|
||||
default:
|
||||
break parse_type;
|
||||
break parseType;
|
||||
}
|
||||
|
||||
this.text = this.originalName.substr(end + 1);
|
||||
}
|
||||
if(!this.text && this.alignment === "normal")
|
||||
|
||||
if(!this.text && this.alignment === "normal") {
|
||||
this.text = this.originalName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||
|
@ -177,7 +195,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
|
||||
readonly events: Registry<ChannelEvents>;
|
||||
|
||||
parsed_channel_name: ParsedChannelName;
|
||||
parsed_channel_name: ChannelNameParser;
|
||||
|
||||
private _family_index: number = 0;
|
||||
|
||||
|
@ -206,7 +224,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
this.properties = new ChannelProperties();
|
||||
this.channelId = channelId;
|
||||
this.properties.channel_name = channelName;
|
||||
this.parsed_channel_name = new ParsedChannelName(channelName, false);
|
||||
this.parsed_channel_name = new ChannelNameParser(channelName, false);
|
||||
|
||||
this.clientPropertyChangedListener = (event: ClientEvents["notify_properties_updated"]) => {
|
||||
if("client_nickname" in event.updated_properties || "client_talk_power" in event.updated_properties) {
|
||||
|
@ -627,7 +645,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
|
||||
if(hasUpdate) {
|
||||
if(key == "channel_name") {
|
||||
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
|
||||
this.parsed_channel_name = new ChannelNameParser(value, this.hasParent());
|
||||
} else if(key == "channel_order") {
|
||||
let order = this.channelTree.findChannel(this.properties.channel_order);
|
||||
this.channelTree.moveChannel(this, order, this.parent, false);
|
||||
|
|
|
@ -36,14 +36,14 @@ export class ChannelConversationController extends AbstractConversationControlle
|
|||
});
|
||||
*/
|
||||
|
||||
this.uiEvents.register_handler(this, true);
|
||||
this.uiEvents.registerHandler(this, true);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.connectionListener.forEach(callback => callback());
|
||||
this.connectionListener = [];
|
||||
|
||||
this.uiEvents.unregister_handler(this);
|
||||
this.uiEvents.unregisterHandler(this);
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class PopoutConversationRenderer extends AbstractModal {
|
|||
noFirstMessageOverlay={this.userData.noFirstMessageOverlay} />;
|
||||
}
|
||||
|
||||
title() {
|
||||
renderTitle() {
|
||||
return "Conversations";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,14 +52,14 @@ export class PrivateConversationController extends AbstractConversationControlle
|
|||
this.connectionListener = [];
|
||||
this.listenerConversation = {};
|
||||
|
||||
this.uiEvents.register_handler(this, true);
|
||||
this.uiEvents.registerHandler(this, true);
|
||||
this.uiEvents.enableDebug("private-conversations");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
/* listenerConversation will be cleaned up via the listenerManager callbacks */
|
||||
|
||||
this.uiEvents.unregister_handler(this);
|
||||
this.uiEvents.unregisterHandler(this);
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
|
||||
import * as React from "react";
|
||||
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
|
@ -243,7 +243,7 @@ class VolumeChange extends InternalModal {
|
|||
return <VolumeChangeModal remote={false} clientName={this.client.clientNickName()} events={this.events} />;
|
||||
}
|
||||
|
||||
title() {
|
||||
renderTitle() {
|
||||
return <Translatable>Change local volume</Translatable>;
|
||||
}
|
||||
}
|
||||
|
@ -284,7 +284,7 @@ class VolumeChangeBot extends InternalModal {
|
|||
return <VolumeChangeModal remote={true} clientName={this.client.clientNickName()} maxVolume={this.maxValue} events={this.events} />;
|
||||
}
|
||||
|
||||
title() {
|
||||
renderTitle() {
|
||||
return <Translatable>Change remote volume</Translatable>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {FlatInputField, Select} from "tc-shared/ui/react-elements/InputField";
|
||||
|
@ -272,7 +272,7 @@ class ModalGroupCreate extends InternalModal {
|
|||
</div>;
|
||||
}
|
||||
|
||||
title() {
|
||||
renderTitle() {
|
||||
return this.target === "server" ? <Translatable>Create a new server group</Translatable> : <Translatable>Create a new channel group</Translatable>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import * as React from "react";
|
||||
|
@ -161,7 +161,7 @@ class ModalGroupPermissionCopy extends InternalModal {
|
|||
</div>;
|
||||
}
|
||||
|
||||
title() {
|
||||
renderTitle() {
|
||||
return <Translatable>Copy group permissions</Translatable>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {createErrorModal, createModal} from "../../ui/elements/Modal";
|
||||
import {ConnectionHandler} from "../../ConnectionHandler";
|
||||
import {MusicClientEntry} from "../../tree/Client";
|
||||
import {modal, Registry} from "../../events";
|
||||
import {Registry} from "../../events";
|
||||
import {CommandResult} from "../../connection/ServerConnectionDeclaration";
|
||||
import {LogCategory, logError, logWarn} from "../../log";
|
||||
import {tr, tra} from "../../i18n/localize";
|
||||
|
@ -12,8 +12,160 @@ import * as htmltags from "../../ui/htmltags";
|
|||
import {ErrorCode} from "../../connection/ErrorCode";
|
||||
import ServerGroup = find.ServerGroup;
|
||||
|
||||
type BotStatusType = "name" | "description" | "volume" | "country_code" | "channel_commander" | "priority_speaker";
|
||||
type PlaylistStatusType = "replay_mode" | "finished" | "delete_played" | "max_size" | "notify_song_change";
|
||||
interface music_manage {
|
||||
show_container: { container: "settings" | "permissions"; };
|
||||
|
||||
/* setting relevant */
|
||||
query_bot_status: {},
|
||||
bot_status: {
|
||||
status: "success" | "error";
|
||||
error_msg?: string;
|
||||
data?: {
|
||||
name: string,
|
||||
description: string,
|
||||
volume: number,
|
||||
|
||||
country_code: string,
|
||||
default_country_code: string,
|
||||
|
||||
channel_commander: boolean,
|
||||
priority_speaker: boolean,
|
||||
|
||||
client_version: string,
|
||||
client_platform: string,
|
||||
|
||||
uptime_mode: number,
|
||||
bot_type: number
|
||||
}
|
||||
},
|
||||
set_bot_status: {
|
||||
key: BotStatusType,
|
||||
value: any
|
||||
},
|
||||
set_bot_status_result: {
|
||||
key: BotStatusType,
|
||||
status: "success" | "error" | "timeout",
|
||||
error_msg?: string,
|
||||
value?: any
|
||||
}
|
||||
|
||||
query_playlist_status: {},
|
||||
playlist_status: {
|
||||
status: "success" | "error",
|
||||
error_msg?: string,
|
||||
data?: {
|
||||
replay_mode: number,
|
||||
finished: boolean,
|
||||
delete_played: boolean,
|
||||
max_size: number,
|
||||
notify_song_change: boolean
|
||||
}
|
||||
},
|
||||
set_playlist_status: {
|
||||
key: PlaylistStatusType,
|
||||
value: any
|
||||
},
|
||||
set_playlist_status_result: {
|
||||
key: PlaylistStatusType,
|
||||
status: "success" | "error" | "timeout",
|
||||
error_msg?: string,
|
||||
value?: any
|
||||
}
|
||||
|
||||
/* permission relevant */
|
||||
show_client_list: {},
|
||||
hide_client_list: {},
|
||||
|
||||
filter_client_list: { filter: string | undefined },
|
||||
|
||||
"refresh_permissions": {},
|
||||
|
||||
query_special_clients: {},
|
||||
special_client_list: {
|
||||
status: "success" | "error" | "error-permission",
|
||||
error_msg?: string,
|
||||
clients?: {
|
||||
name: string,
|
||||
unique_id: string,
|
||||
database_id: number
|
||||
}[]
|
||||
},
|
||||
|
||||
search_client: { text: string },
|
||||
search_client_result: {
|
||||
status: "error" | "timeout" | "empty" | "success",
|
||||
error_msg?: string,
|
||||
client?: {
|
||||
name: string,
|
||||
unique_id: string,
|
||||
database_id: number
|
||||
}
|
||||
},
|
||||
|
||||
/* sets a client to set the permission for */
|
||||
special_client_set: {
|
||||
client?: {
|
||||
name: string,
|
||||
unique_id: string,
|
||||
database_id: number
|
||||
}
|
||||
},
|
||||
|
||||
"query_general_permissions": {},
|
||||
"general_permissions": {
|
||||
status: "error" | "timeout" | "success",
|
||||
error_msg?: string,
|
||||
permissions?: {[key: string]:number}
|
||||
},
|
||||
"set_general_permission_result": {
|
||||
status: "error" | "success",
|
||||
key: string,
|
||||
value?: number,
|
||||
error_msg?: string
|
||||
},
|
||||
"set_general_permission": { /* try to change a permission for the server */
|
||||
key: string,
|
||||
value: number
|
||||
},
|
||||
|
||||
|
||||
"query_client_permissions": { client_database_id: number },
|
||||
"client_permissions": {
|
||||
status: "error" | "timeout" | "success",
|
||||
client_database_id: number,
|
||||
error_msg?: string,
|
||||
permissions?: {[key: string]:number}
|
||||
},
|
||||
"set_client_permission_result": {
|
||||
status: "error" | "success",
|
||||
client_database_id: number,
|
||||
key: string,
|
||||
value?: number,
|
||||
error_msg?: string
|
||||
},
|
||||
"set_client_permission": { /* try to change a permission for the server */
|
||||
client_database_id: number,
|
||||
key: string,
|
||||
value: number
|
||||
},
|
||||
|
||||
"query_group_permissions": { permission_name: string },
|
||||
"group_permissions": {
|
||||
permission_name: string;
|
||||
status: "error" | "timeout" | "success"
|
||||
groups?: {
|
||||
name: string,
|
||||
value: number,
|
||||
id: number
|
||||
}[],
|
||||
error_msg?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function openMusicManage(client: ConnectionHandler, bot: MusicClientEntry) {
|
||||
const ev_registry = new Registry<modal.music_manage>();
|
||||
const ev_registry = new Registry<music_manage>();
|
||||
ev_registry.enableDebug("music-manage");
|
||||
//dummy_controller(ev_registry);
|
||||
permission_controller(ev_registry, bot, client);
|
||||
|
@ -36,7 +188,7 @@ export function openMusicManage(client: ConnectionHandler, bot: MusicClientEntry
|
|||
modal.open();
|
||||
}
|
||||
|
||||
function permission_controller(event_registry: Registry<modal.music_manage>, bot: MusicClientEntry, client: ConnectionHandler) {
|
||||
function permission_controller(event_registry: Registry<music_manage>, bot: MusicClientEntry, client: ConnectionHandler) {
|
||||
const error_msg = error => {
|
||||
if (error instanceof CommandResult) {
|
||||
if (error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
|
@ -380,7 +532,7 @@ function permission_controller(event_registry: Registry<modal.music_manage>, bot
|
|||
}
|
||||
}
|
||||
|
||||
function dummy_controller(event_registry: Registry<modal.music_manage>) {
|
||||
function dummy_controller(event_registry: Registry<music_manage>) {
|
||||
/* settings */
|
||||
{
|
||||
event_registry.on("query_bot_status", event => {
|
||||
|
@ -510,7 +662,7 @@ function dummy_controller(event_registry: Registry<modal.music_manage>) {
|
|||
}
|
||||
|
||||
|
||||
function build_modal(event_registry: Registry<modal.music_manage>): JQuery<HTMLElement> {
|
||||
function build_modal(event_registry: Registry<music_manage>): JQuery<HTMLElement> {
|
||||
const tag = $("#tmpl_music_manage").renderTag();
|
||||
|
||||
const container_settings = tag.find(".body > .category-settings");
|
||||
|
@ -563,7 +715,7 @@ function build_modal(event_registry: Registry<modal.music_manage>): JQuery<HTMLE
|
|||
return tag.children();
|
||||
}
|
||||
|
||||
function build_settings_container(event_registry: Registry<modal.music_manage>, tag: JQuery<HTMLElement>) {
|
||||
function build_settings_container(event_registry: Registry<music_manage>, tag: JQuery<HTMLElement>) {
|
||||
const show_change_error = (header, message) => {
|
||||
createErrorModal(tr("Failed to change value"), header + "<br>" + message).open();
|
||||
};
|
||||
|
@ -1169,7 +1321,7 @@ function build_settings_container(event_registry: Registry<modal.music_manage>,
|
|||
}
|
||||
}
|
||||
|
||||
function build_permission_container(event_registry: Registry<modal.music_manage>, tag: JQuery<HTMLElement>) {
|
||||
function build_permission_container(event_registry: Registry<music_manage>, tag: JQuery<HTMLElement>) {
|
||||
/* client search mechanism */
|
||||
{
|
||||
const container = tag.find(".table-head .column-client-specific .client-select");
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
|
||||
import {tra} from "tc-shared/i18n/localize";
|
||||
import {modal as emodal, Registry} from "tc-shared/events";
|
||||
import {modal_settings} from "tc-shared/ui/modal/ModalSettings";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {modal_settings, SettingProfileEvents} from "tc-shared/ui/modal/ModalSettings";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import {initialize_audio_microphone_controller, MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/Microphone";
|
||||
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
||||
|
@ -151,7 +151,7 @@ function initializeStepFinish(tag: JQuery, event_registry: Registry<EventModalNe
|
|||
}
|
||||
|
||||
function initializeStepIdentity(tag: JQuery, event_registry: Registry<EventModalNewcomer>) {
|
||||
const profile_events = new Registry<emodal.settings.profiles>();
|
||||
const profile_events = new Registry<SettingProfileEvents>();
|
||||
profile_events.enableDebug("settings-identity");
|
||||
modal_settings.initialize_identity_profiles_controller(profile_events);
|
||||
modal_settings.initialize_identity_profiles_view(tag, profile_events, {forum_setuppable: false});
|
||||
|
|
|
@ -28,6 +28,164 @@ import {NotificationSettings} from "tc-shared/ui/modal/settings/Notifications";
|
|||
import {initialize_audio_microphone_controller, MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/Microphone";
|
||||
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
||||
|
||||
type ProfileInfoEvent = {
|
||||
id: string,
|
||||
name: string,
|
||||
nickname: string,
|
||||
identity_type: "teaforo" | "teamspeak" | "nickname",
|
||||
|
||||
identity_forum?: {
|
||||
valid: boolean,
|
||||
fallback_name: string
|
||||
},
|
||||
identity_nickname?: {
|
||||
name: string,
|
||||
fallback_name: string
|
||||
},
|
||||
identity_teamspeak?: {
|
||||
unique_id: string,
|
||||
fallback_name: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SettingProfileEvents {
|
||||
"reload-profile": { profile_id?: string },
|
||||
"select-profile": { profile_id: string },
|
||||
|
||||
"query-profile-list": { },
|
||||
"query-profile-list-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
|
||||
error?: string;
|
||||
profiles?: ProfileInfoEvent[]
|
||||
}
|
||||
|
||||
"query-profile": { profile_id: string },
|
||||
"query-profile-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
profile_id: string,
|
||||
|
||||
error?: string;
|
||||
info?: ProfileInfoEvent
|
||||
},
|
||||
|
||||
"select-identity-type": {
|
||||
profile_id: string,
|
||||
identity_type: "teamspeak" | "teaforo" | "nickname" | "unset"
|
||||
},
|
||||
|
||||
"query-profile-validity": { profile_id: string },
|
||||
"query-profile-validity-result": {
|
||||
profile_id: string,
|
||||
status: "error" | "success" | "timeout",
|
||||
|
||||
error?: string,
|
||||
valid?: boolean
|
||||
}
|
||||
|
||||
"create-profile": { name: string },
|
||||
"create-profile-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
name: string;
|
||||
|
||||
profile_id?: string;
|
||||
error?: string;
|
||||
},
|
||||
|
||||
"delete-profile": { profile_id: string },
|
||||
"delete-profile-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
profile_id: string,
|
||||
error?: string
|
||||
}
|
||||
|
||||
"set-default-profile": { profile_id: string },
|
||||
"set-default-profile-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
|
||||
/* the profile which now has the id "default" */
|
||||
old_profile_id: string,
|
||||
|
||||
/* the "default" profile which now has a new id */
|
||||
new_profile_id?: string
|
||||
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/* profile name events */
|
||||
"set-profile-name": {
|
||||
profile_id: string,
|
||||
name: string
|
||||
},
|
||||
"set-profile-name-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
profile_id: string,
|
||||
name?: string
|
||||
},
|
||||
|
||||
/* profile nickname events */
|
||||
"set-default-name": {
|
||||
profile_id: string,
|
||||
name: string | null
|
||||
},
|
||||
"set-default-name-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
profile_id: string,
|
||||
name?: string | null
|
||||
},
|
||||
|
||||
"query-identity-teamspeak": { profile_id: string },
|
||||
"query-identity-teamspeak-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
profile_id: string,
|
||||
|
||||
error?: string,
|
||||
level?: number
|
||||
}
|
||||
|
||||
"set-identity-name-name": { profile_id: string, name: string },
|
||||
"set-identity-name-name-result": {
|
||||
status: "error" | "success" | "timeout",
|
||||
profile_id: string,
|
||||
|
||||
error?: string,
|
||||
name?: string
|
||||
},
|
||||
|
||||
"generate-identity-teamspeak": { profile_id: string },
|
||||
"generate-identity-teamspeak-result": {
|
||||
profile_id: string,
|
||||
status: "error" | "success" | "timeout",
|
||||
|
||||
error?: string,
|
||||
|
||||
level?: number
|
||||
unique_id?: string
|
||||
},
|
||||
|
||||
"improve-identity-teamspeak-level": { profile_id: string },
|
||||
"improve-identity-teamspeak-level-update": {
|
||||
profile_id: string,
|
||||
new_level: number
|
||||
},
|
||||
|
||||
"import-identity-teamspeak": { profile_id: string },
|
||||
"import-identity-teamspeak-result": {
|
||||
profile_id: string,
|
||||
|
||||
level?: number
|
||||
unique_id?: string
|
||||
}
|
||||
|
||||
"export-identity-teamspeak": {
|
||||
profile_id: string,
|
||||
filename: string
|
||||
},
|
||||
|
||||
|
||||
"setup-forum-connection": {}
|
||||
}
|
||||
|
||||
export function spawnSettingsModal(default_page?: string): Modal {
|
||||
let modal: Modal;
|
||||
modal = createModal({
|
||||
|
@ -449,7 +607,7 @@ function settings_audio_microphone(container: JQuery, modal: Modal) {
|
|||
}
|
||||
|
||||
function settings_identity_profiles(container: JQuery, modal: Modal) {
|
||||
const registry = new Registry<events.modal.settings.profiles>();
|
||||
const registry = new Registry<SettingProfileEvents>();
|
||||
//registry.enable_debug("settings-identity");
|
||||
modal_settings.initialize_identity_profiles_controller(registry);
|
||||
modal_settings.initialize_identity_profiles_view(container, registry, {
|
||||
|
@ -689,7 +847,7 @@ export namespace modal_settings {
|
|||
forum_setuppable: boolean
|
||||
}
|
||||
|
||||
export function initialize_identity_profiles_controller(event_registry: Registry<events.modal.settings.profiles>) {
|
||||
export function initialize_identity_profiles_controller(event_registry: Registry<SettingProfileEvents>) {
|
||||
const send_error = (event, profile, text) => event_registry.fire_react(event, {
|
||||
status: "error",
|
||||
profile_id: profile,
|
||||
|
@ -997,7 +1155,7 @@ export namespace modal_settings {
|
|||
});
|
||||
}
|
||||
|
||||
export function initialize_identity_profiles_view(container: JQuery, event_registry: Registry<events.modal.settings.profiles>, settings: ProfileViewSettings) {
|
||||
export function initialize_identity_profiles_view(container: JQuery, event_registry: Registry<SettingProfileEvents>, settings: ProfileViewSettings) {
|
||||
/* profile list */
|
||||
{
|
||||
const container_profiles = container.find(".container-profiles");
|
||||
|
@ -1007,7 +1165,7 @@ export namespace modal_settings {
|
|||
const overlay_timeout = container_profiles.find(".overlay-timeout");
|
||||
const overlay_empty = container_profiles.find(".overlay-empty");
|
||||
|
||||
const build_profile = (profile: events.modal.settings.ProfileInfo, selected: boolean) => {
|
||||
const build_profile = (profile: ProfileInfoEvent, selected: boolean) => {
|
||||
let tag_avatar: JQuery, tag_default: JQuery;
|
||||
let tag = $.spawn("div").addClass("profile").attr("profile-id", profile.id).append(
|
||||
tag_avatar = $.spawn("div").addClass("container-avatar"),
|
||||
|
@ -1693,7 +1851,7 @@ export namespace modal_settings {
|
|||
});
|
||||
}
|
||||
|
||||
const create_standard_timeout = (event: keyof events.modal.settings.profiles, response_event: keyof events.modal.settings.profiles, key: string) => {
|
||||
const create_standard_timeout = (event: keyof SettingProfileEvents, response_event: keyof SettingProfileEvents, key: string) => {
|
||||
const timeouts = {};
|
||||
event_registry.on(event, event => {
|
||||
clearTimeout(timeouts[event[key]]);
|
||||
|
|
|
@ -10,8 +10,7 @@ import {Registry} from "tc-shared/events";
|
|||
import {ChannelPropertyProviders} from "tc-shared/ui/modal/channel-edit/ControllerProperties";
|
||||
import {LogCategory, logDebug, logError} from "tc-shared/log";
|
||||
import {ChannelPropertyPermissionsProviders} from "tc-shared/ui/modal/channel-edit/ControllerPermissions";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {ChannelEditModal} from "tc-shared/ui/modal/channel-edit/Renderer";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {PermissionValue} from "tc-shared/permission/PermissionManager";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import PermissionType from "tc-shared/permission/PermissionType";
|
||||
|
@ -25,10 +24,13 @@ export type ChannelEditChangedPermission = { permission: PermissionType, value:
|
|||
|
||||
export const spawnChannelEditNew = (connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, callback: ChannelEditCallback) => {
|
||||
const controller = new ChannelEditController(connection, channel, parent);
|
||||
const modal = spawnReactModal(ChannelEditModal, controller.uiEvents, typeof channel !== "object");
|
||||
const modal = spawnModal("channel-edit", [controller.uiEvents.generateIpcDescription(), typeof channel !== "object"], {
|
||||
popedOut: false,
|
||||
popoutable: true
|
||||
});
|
||||
modal.show().then(undefined);
|
||||
|
||||
modal.events.on("destroy", () => {
|
||||
modal.getEvents().on("destroy", () => {
|
||||
controller.destroy();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {ChannelEntry, ChannelProperties, ChannelSidebarMode} from "tc-shared/tree/Channel";
|
||||
import {ChannelEntry, ChannelNameParser, ChannelProperties, ChannelSidebarMode} from "tc-shared/tree/Channel";
|
||||
import {ChannelEditableProperty} from "tc-shared/ui/modal/channel-edit/Definitions";
|
||||
import {ChannelTree} from "tc-shared/tree/ChannelTree";
|
||||
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
|
||||
|
@ -17,7 +17,41 @@ const SimplePropertyProvider = <P extends keyof ChannelProperties>(channelProper
|
|||
};
|
||||
}
|
||||
|
||||
ChannelPropertyProviders["name"] = SimplePropertyProvider("channel_name", "");
|
||||
ChannelPropertyProviders["name"] = {
|
||||
provider: async (properties, channel, parentChannel, channelTree) => {
|
||||
let spacerUniqueId = 0;
|
||||
const hasParent = !!(channel?.hasParent() || parentChannel);
|
||||
if(!hasParent) {
|
||||
const channels = channelTree.rootChannel();
|
||||
while(true) {
|
||||
let matchFound = false;
|
||||
for(const channel of channels) {
|
||||
if(channel.parsed_channel_name.uniqueId === spacerUniqueId.toString()) {
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!matchFound) {
|
||||
break;
|
||||
}
|
||||
|
||||
spacerUniqueId++;
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = new ChannelNameParser(properties.channel_name, hasParent);
|
||||
return {
|
||||
rawName: properties.channel_name,
|
||||
spacerUniqueId: parsed.uniqueId || spacerUniqueId.toString(),
|
||||
hasParent,
|
||||
maxNameLength: 30 - (parsed.originalName.length - parsed.text.length),
|
||||
parsedAlignment: parsed.alignment,
|
||||
parsedName: parsed.text
|
||||
}
|
||||
},
|
||||
applier: (value, properties) => properties.channel_name = value.rawName
|
||||
}
|
||||
ChannelPropertyProviders["phoneticName"] = SimplePropertyProvider("channel_name_phonetic", "");
|
||||
ChannelPropertyProviders["icon"] = {
|
||||
provider: async (properties, _channel, _parentChannel, channelTree) => {
|
||||
|
|
|
@ -32,7 +32,14 @@ export type ChannelEditPermissionsState = {
|
|||
};
|
||||
|
||||
export interface ChannelEditableProperty {
|
||||
"name": string,
|
||||
"name": {
|
||||
rawName: string,
|
||||
parsedName?: string,
|
||||
parsedAlignment?: "center" | "right" | "left" | "normal" | "repetitive",
|
||||
maxNameLength?: number,
|
||||
hasParent?: boolean,
|
||||
spacerUniqueId?: string
|
||||
},
|
||||
"phoneticName": string,
|
||||
|
||||
"icon": {
|
||||
|
|
|
@ -40,6 +40,26 @@
|
|||
}
|
||||
}
|
||||
|
||||
.channelName {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 100%;
|
||||
min-width: 10em;
|
||||
|
||||
.select {
|
||||
margin-right: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
&.hasParent {
|
||||
.select {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 1em;
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import * as React from "react";
|
||||
import {useContext, useEffect, useRef, useState} from "react";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {IpcRegistryDescription, Registry} from "tc-shared/events";
|
||||
import {
|
||||
ChannelEditablePermissions,
|
||||
ChannelEditablePermissionValue,
|
||||
|
@ -16,13 +15,15 @@ import {Switch} from "tc-shared/ui/react-elements/Switch";
|
|||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||
import {RadioButton} from "tc-shared/ui/react-elements/RadioButton";
|
||||
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
||||
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";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
|
@ -121,6 +122,10 @@ function useValidationState<T extends keyof ChannelEditableProperty>(property: T
|
|||
return valid;
|
||||
}
|
||||
|
||||
const ChannelNameType = (props: { selected: ChannelNameAlignment }) => {
|
||||
|
||||
}
|
||||
|
||||
const ChannelName = React.memo(() => {
|
||||
const modalType = useContext(ModalTypeContext);
|
||||
|
||||
|
@ -128,16 +133,73 @@ const ChannelName = React.memo(() => {
|
|||
const editable = usePropertyPermission("name", modalType === "channel-create");
|
||||
const valid = useValidationState("name");
|
||||
|
||||
const refSelect = useRef<HTMLSelectElement>();
|
||||
|
||||
const setValue = (text: string | undefined, localOnly: boolean) => {
|
||||
let rawName;
|
||||
switch(propertyValue.hasParent ? "normal" : refSelect.current.value) {
|
||||
case "center":
|
||||
rawName = "[cspacer" + propertyValue.spacerUniqueId + "]" + text;
|
||||
break;
|
||||
|
||||
case "left":
|
||||
rawName = "[lspacer" + propertyValue.spacerUniqueId + "]" + text;
|
||||
break;
|
||||
|
||||
case "right":
|
||||
rawName = "[rspacer" + propertyValue.spacerUniqueId + "]" + text;
|
||||
break;
|
||||
|
||||
case "repetitive":
|
||||
rawName ="[*spacer" + propertyValue.spacerUniqueId + "]" + text;
|
||||
break;
|
||||
|
||||
default:
|
||||
case "normal":
|
||||
rawName = text;
|
||||
break;
|
||||
}
|
||||
|
||||
setPropertyValue({
|
||||
rawName,
|
||||
parsedName: text,
|
||||
|
||||
hasParent: propertyValue.hasParent,
|
||||
spacerUniqueId: propertyValue.spacerUniqueId,
|
||||
maxNameLength: propertyValue.maxNameLength,
|
||||
parsedAlignment: propertyValue.parsedAlignment
|
||||
}, localOnly);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={joinClassList(cssStyle.channelName, propertyValue?.hasParent && cssStyle.hasParent)}>
|
||||
<Select
|
||||
value={propertyValue?.parsedAlignment || "loading"}
|
||||
className={cssStyle.select}
|
||||
onChange={() => setValue(propertyValue.parsedName, false)}
|
||||
type={"boxed"}
|
||||
title={useTr("Channel name mode")}
|
||||
refSelect={refSelect}
|
||||
disabled={!editable}
|
||||
>
|
||||
<option value={"loading"} style={{ display: "none" }}>{useTr("loading")}</option>
|
||||
<option value={"normal"}>{useTr("Normal Name")}</option>
|
||||
<option value={"center"}>{useTr("Centered")}</option>
|
||||
<option value={"left"}>{useTr("Left Aligned")}</option>
|
||||
<option value={"right"}>{useTr("Right Aligned")}</option>
|
||||
<option value={"repetitive"}>{useTr("Repetitive")}</option>
|
||||
</Select>
|
||||
<BoxedInputField
|
||||
className={cssStyle.input}
|
||||
disabled={!editable || propertyState !== "normal"}
|
||||
value={propertyValue || ""}
|
||||
value={(propertyValue?.hasParent ? propertyValue?.rawName : propertyValue?.parsedName) || ""}
|
||||
placeholder={propertyState === "normal" ? tr("Channel name") : tr("loading")}
|
||||
onInput={value => setPropertyValue(value, true)}
|
||||
onChange={value => setPropertyValue(value)}
|
||||
onInput={value => setValue(value, true)}
|
||||
onChange={value => setValue(value, false)}
|
||||
isInvalid={!valid}
|
||||
maxLength={propertyValue?.maxNameLength}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -159,7 +221,7 @@ const ChannelIcon = () => {
|
|||
<div className="arrow down" />
|
||||
</div>
|
||||
<div className={cssStyle.dropdown}>
|
||||
<div className={cssStyle.entry} onClick={() => enabled && events.fire("action_icon_select")}>Edit icon</div>
|
||||
<div className={cssStyle.entry} onClick={() => enabled && events.fire("action_icon_select")}><Translatable>Edit icon</Translatable></div>
|
||||
<div className={cssStyle.entry} onClick={() => {
|
||||
if(!enabled) {
|
||||
return;
|
||||
|
@ -173,7 +235,7 @@ const ChannelIcon = () => {
|
|||
serverUniqueId: propertyValue.remoteIcon.serverUniqueId
|
||||
}
|
||||
});
|
||||
}}>Remove icon</div>
|
||||
}}><Translatable>Remove icon</Translatable></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -351,31 +413,31 @@ const SidebarType = React.memo(() => {
|
|||
);
|
||||
});
|
||||
|
||||
type SimpleCodecQualityTemplate = { id: string, codec: number, quality: number, name: string };
|
||||
type SimpleCodecQualityTemplate = { id: string, codec: number, quality: number, name: () => string };
|
||||
const kCodecTemplates: SimpleCodecQualityTemplate[] = [
|
||||
{
|
||||
id: "mobile",
|
||||
codec: 4,
|
||||
quality: 4,
|
||||
name: useTr("Mobile")
|
||||
name: () => useTr("Mobile")
|
||||
},
|
||||
{
|
||||
id: "voice",
|
||||
codec: 4,
|
||||
quality: 6,
|
||||
name: useTr("Voice")
|
||||
name: () => useTr("Voice")
|
||||
},
|
||||
{
|
||||
id: "music",
|
||||
codec: 5,
|
||||
quality: 6,
|
||||
name: useTr("Music")
|
||||
name: () => useTr("Music")
|
||||
},
|
||||
{
|
||||
id: "loading",
|
||||
codec: undefined,
|
||||
quality: undefined,
|
||||
name: useTr("loading")
|
||||
name: () => useTr("loading")
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -404,7 +466,7 @@ const SimpleCodecQuality = React.memo(() => {
|
|||
>
|
||||
{
|
||||
kCodecTemplates.map(template => (
|
||||
<option style={{ display: template.id === "loading" ? "none" : undefined }} key={template.id} value={template.id} disabled={!hasPermission(template)}>{template.name}</option>
|
||||
<option style={{ display: template.id === "loading" ? "none" : undefined }} key={template.id} value={template.id} disabled={!hasPermission(template)}>{template.name()}</option>
|
||||
))
|
||||
}
|
||||
<option style={{ display: "none" }} key={"advanced"} value={"advanced"}>{useTr("Custom (Advanced settings)")}</option>
|
||||
|
@ -431,7 +493,7 @@ const AdvancedCodecPresets = React.memo(() => {
|
|||
disabled={!hasPermission(template) || propertyState !== "normal"}
|
||||
onChange={() => setPropertyValue({ quality: template.quality, type: template.codec})}
|
||||
>
|
||||
<div>{template.name}</div>
|
||||
<div>{template.name()}</div>
|
||||
</RadioButton>
|
||||
))
|
||||
}
|
||||
|
@ -1138,13 +1200,13 @@ const Buttons = React.memo(() => {
|
|||
)
|
||||
});
|
||||
|
||||
export class ChannelEditModal extends InternalModal {
|
||||
class ChannelEditModal extends AbstractModal {
|
||||
private readonly events: Registry<ChannelEditEvents>;
|
||||
private readonly isChannelCreate: boolean;
|
||||
|
||||
constructor(events: Registry<ChannelEditEvents>, isChannelCreate: boolean) {
|
||||
constructor(events: IpcRegistryDescription<ChannelEditEvents>, isChannelCreate: boolean) {
|
||||
super();
|
||||
this.events = events;
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
this.isChannelCreate = isChannelCreate;
|
||||
}
|
||||
|
||||
|
@ -1162,7 +1224,7 @@ export class ChannelEditModal extends InternalModal {
|
|||
);
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement {
|
||||
renderTitle(): string | React.ReactElement {
|
||||
if(this.isChannelCreate) {
|
||||
return <Translatable key={"create"}>Create channel</Translatable>;
|
||||
} else {
|
||||
|
@ -1175,3 +1237,5 @@ export class ChannelEditModal extends InternalModal {
|
|||
return "blue";
|
||||
}
|
||||
}
|
||||
|
||||
export = ChannelEditModal;
|
|
@ -1,7 +1,8 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {ConnectProperties, ConnectUiEvents, PropertyValidState} from "tc-shared/ui/modal/connect/Definitions";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {ConnectModal} from "tc-shared/ui/modal/connect/Renderer";
|
||||
import {
|
||||
ConnectUiEvents,
|
||||
ConnectUiVariables,
|
||||
} from "tc-shared/ui/modal/connect/Definitions";
|
||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import {
|
||||
availableConnectProfiles,
|
||||
|
@ -17,7 +18,9 @@ import {server_connections} from "tc-shared/ConnectionManager";
|
|||
import {parseServerAddress} from "tc-shared/tree/Server";
|
||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||
import * as ipRegex from "ip-regex";
|
||||
import _ = require("lodash");
|
||||
import {UiVariableProvider} from "tc-shared/ui/utils/Variable";
|
||||
import {createIpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
|
||||
const kRegexDomain = /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))$/i;
|
||||
|
||||
|
@ -37,19 +40,11 @@ export type ConnectParameters = {
|
|||
defaultChannelPassword?: string,
|
||||
}
|
||||
|
||||
type ValidityStates = {[T in keyof PropertyValidState]: boolean};
|
||||
const kDefaultValidityStates: ValidityStates = {
|
||||
address: false,
|
||||
nickname: false,
|
||||
password: false,
|
||||
profile: false
|
||||
}
|
||||
|
||||
class ConnectController {
|
||||
readonly uiEvents: Registry<ConnectUiEvents>;
|
||||
readonly uiVariables: UiVariableProvider<ConnectUiVariables>;
|
||||
|
||||
private readonly defaultAddress: string;
|
||||
private readonly propertyProvider: {[K in keyof ConnectProperties]?: () => Promise<ConnectProperties[K]>} = {};
|
||||
|
||||
private historyShown: boolean;
|
||||
|
||||
|
@ -59,32 +54,22 @@ class ConnectController {
|
|||
private currentPasswordHashed: boolean;
|
||||
private currentProfile: ConnectionProfile | undefined;
|
||||
|
||||
private addressChanged: boolean;
|
||||
private nicknameChanged: boolean;
|
||||
|
||||
private selectedHistoryId: number;
|
||||
private history: ConnectionHistoryEntry[];
|
||||
|
||||
private validStates: {[T in keyof PropertyValidState]: boolean} = {
|
||||
address: false,
|
||||
nickname: false,
|
||||
password: false,
|
||||
profile: false
|
||||
};
|
||||
private validateNickname: boolean;
|
||||
private validateAddress: boolean;
|
||||
|
||||
private validateStates: {[T in keyof PropertyValidState]: boolean} = {
|
||||
profile: false,
|
||||
password: false,
|
||||
nickname: false,
|
||||
address: false
|
||||
};
|
||||
|
||||
constructor() {
|
||||
constructor(uiVariables: UiVariableProvider<ConnectUiVariables>) {7
|
||||
this.uiEvents = new Registry<ConnectUiEvents>();
|
||||
this.uiEvents.enableDebug("modal-connect");
|
||||
|
||||
this.uiVariables = uiVariables;
|
||||
this.history = undefined;
|
||||
|
||||
this.validateNickname = false;
|
||||
this.validateAddress = false;
|
||||
|
||||
this.defaultAddress = "ts.teaspeak.de";
|
||||
this.historyShown = settings.getValue(Settings.KEY_CONNECT_SHOW_HISTORY);
|
||||
|
||||
|
@ -92,35 +77,117 @@ class ConnectController {
|
|||
this.currentProfile = findConnectProfile(settings.getValue(Settings.KEY_CONNECT_PROFILE)) || defaultConnectProfile();
|
||||
this.currentNickname = settings.getValue(Settings.KEY_CONNECT_USERNAME);
|
||||
|
||||
this.addressChanged = false;
|
||||
this.nicknameChanged = false;
|
||||
this.uiEvents.on("action_delete_history", event => {
|
||||
connectionHistory.deleteConnectionAttempts(event.target, event.targetType).then(() => {
|
||||
this.history = undefined;
|
||||
this.uiVariables.sendVariable("history");
|
||||
}).catch(error => {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to delete connection attempts: %o"), error);
|
||||
})
|
||||
});
|
||||
|
||||
this.propertyProvider["nickname"] = async () => ({
|
||||
this.uiEvents.on("action_manage_profiles", () => {
|
||||
/* TODO: This is more a hack. Proper solution is that the connection profiles fire events if they've been changed... */
|
||||
const modal = spawnSettingsModal("identity-profiles");
|
||||
modal.close_listener.push(() => {
|
||||
this.uiVariables.sendVariable("profiles", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_select_history", event => this.setSelectedHistoryId(event.id));
|
||||
|
||||
this.uiEvents.on("action_connect", () => {
|
||||
this.validateNickname = true;
|
||||
this.validateAddress = true;
|
||||
this.updateValidityStates();
|
||||
});
|
||||
|
||||
this.uiVariables.setVariableProvider("server_address", () => ({
|
||||
currentAddress: this.currentAddress,
|
||||
defaultAddress: this.defaultAddress
|
||||
}));
|
||||
|
||||
this.uiVariables.setVariableProvider("server_address_valid", () => {
|
||||
if(this.validateAddress) {
|
||||
const address = this.currentAddress || this.defaultAddress || "";
|
||||
const parsedAddress = parseServerAddress(address);
|
||||
|
||||
if(parsedAddress) {
|
||||
kRegexDomain.lastIndex = 0;
|
||||
return kRegexDomain.test(parsedAddress.host) || ipRegex({ exact: true }).test(parsedAddress.host);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this.uiVariables.setVariableEditor("server_address", newValue => {
|
||||
if(this.currentAddress === newValue.currentAddress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setSelectedAddress(newValue.currentAddress, true, false);
|
||||
return true;
|
||||
});
|
||||
|
||||
this.uiVariables.setVariableProvider("nickname", () => ({
|
||||
defaultNickname: this.currentProfile?.connectUsername(),
|
||||
currentNickname: this.currentNickname,
|
||||
}));
|
||||
|
||||
this.uiVariables.setVariableProvider("nickname_valid", () => {
|
||||
if(this.validateNickname) {
|
||||
const nickname = this.currentNickname || this.currentProfile?.connectUsername() || "";
|
||||
return nickname.length >= 3 && nickname.length <= 30;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this.propertyProvider["address"] = async () => ({
|
||||
currentAddress: this.currentAddress,
|
||||
defaultAddress: this.defaultAddress,
|
||||
this.uiVariables.setVariableEditor("nickname", newValue => {
|
||||
if(this.currentNickname === newValue.currentNickname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentNickname = newValue.currentNickname;
|
||||
settings.setValue(Settings.KEY_CONNECT_USERNAME, this.currentNickname);
|
||||
|
||||
this.validateNickname = true;
|
||||
this.uiVariables.sendVariable("nickname_valid");
|
||||
return true;
|
||||
});
|
||||
|
||||
this.propertyProvider["password"] = async () => this.currentPassword ? ({
|
||||
hashed: this.currentPasswordHashed,
|
||||
password: this.currentPassword
|
||||
}) : undefined;
|
||||
this.uiVariables.setVariableProvider("password", () => ({
|
||||
password: this.currentPassword,
|
||||
hashed: this.currentPasswordHashed
|
||||
}));
|
||||
|
||||
this.propertyProvider["profiles"] = async () => ({
|
||||
selected: this.currentProfile?.id,
|
||||
profiles: availableConnectProfiles().map(profile => ({
|
||||
id: profile.id,
|
||||
valid: profile.valid(),
|
||||
name: profile.profileName
|
||||
}))
|
||||
this.uiVariables.setVariableEditor("password", newValue => {
|
||||
if(this.currentPassword === newValue.password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentPassword = newValue.password;
|
||||
this.currentPasswordHashed = newValue.hashed;
|
||||
return true;
|
||||
});
|
||||
|
||||
this.propertyProvider["historyShown"] = async () => this.historyShown;
|
||||
this.propertyProvider["history"] = async () => {
|
||||
this.uiVariables.setVariableProvider("profile_valid", () => !!this.currentProfile?.valid());
|
||||
|
||||
this.uiVariables.setVariableProvider("historyShown", () => this.historyShown);
|
||||
this.uiVariables.setVariableEditor("historyShown", newValue => {
|
||||
if(this.historyShown === newValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.historyShown = newValue;
|
||||
settings.setValue(Settings.KEY_CONNECT_SHOW_HISTORY, newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
this.uiVariables.setVariableProvider("history",async () => {
|
||||
if(!this.history) {
|
||||
this.history = await connectionHistory.lastConnectedServers(10);
|
||||
}
|
||||
|
@ -133,30 +200,14 @@ class ConnectController {
|
|||
uniqueServerId: entry.serverUniqueId
|
||||
}))
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
this.uiEvents.on("query_property", event => this.sendProperty(event.property));
|
||||
this.uiEvents.on("query_property_valid", event => this.uiEvents.fire_react("notify_property_valid", { property: event.property, value: this.validStates[event.property] }));
|
||||
this.uiEvents.on("query_history_connections", event => {
|
||||
connectionHistory.countConnectCount(event.target, event.targetType).catch(async error => {
|
||||
logError(LogCategory.GENERAL, tr("Failed to query the connect count for %s (%s): %o"), event.target, event.targetType, error);
|
||||
return -1;
|
||||
}).then(count => {
|
||||
this.uiEvents.fire_react("notify_history_connections", {
|
||||
target: event.target,
|
||||
targetType: event.targetType,
|
||||
value: count
|
||||
});
|
||||
});
|
||||
});
|
||||
this.uiEvents.on("query_history_entry", event => {
|
||||
connectionHistory.queryServerInfo(event.serverUniqueId).then(info => {
|
||||
this.uiEvents.fire_react("notify_history_entry", {
|
||||
serverUniqueId: event.serverUniqueId,
|
||||
info: {
|
||||
this.uiVariables.setVariableProvider("history_entry", async customData => {
|
||||
const info = await connectionHistory.queryServerInfo(customData.serverUniqueId);
|
||||
return {
|
||||
icon: {
|
||||
iconId: info.iconId,
|
||||
serverUniqueId: event.serverUniqueId,
|
||||
serverUniqueId: customData.serverUniqueId,
|
||||
handlerId: undefined
|
||||
},
|
||||
name: info.name,
|
||||
|
@ -164,99 +215,52 @@ class ConnectController {
|
|||
country: info.country,
|
||||
clients: info.clientsOnline,
|
||||
maxClients: info.clientsMax
|
||||
}
|
||||
});
|
||||
}).catch(async error => {
|
||||
logError(LogCategory.GENERAL, tr("Failed to query the history server info for %s: %o"), event.serverUniqueId, error);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_toggle_history", event => {
|
||||
if(this.historyShown === event.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.historyShown = event.enabled;
|
||||
this.sendProperty("historyShown").then(undefined);
|
||||
settings.setValue(Settings.KEY_CONNECT_SHOW_HISTORY, event.enabled);
|
||||
this.uiVariables.setVariableProvider("history_connections", async customData => {
|
||||
return await connectionHistory.countConnectCount(customData.target, customData.targetType).catch(async error => {
|
||||
logError(LogCategory.GENERAL, tr("Failed to query the connect count for %s (%s): %o"), customData.target, customData.targetType, error);
|
||||
return -1;
|
||||
});
|
||||
|
||||
|
||||
this.uiEvents.on("action_delete_history", event => {
|
||||
connectionHistory.deleteConnectionAttempts(event.target, event.targetType).then(() => {
|
||||
this.history = undefined;
|
||||
this.sendProperty("history").then(undefined);
|
||||
}).catch(error => {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to delete connection attempts: %o"), error);
|
||||
})
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_manage_profiles", () => {
|
||||
/* TODO: This is more a hack. Proper solution is that the connection profiles fire events if they've been changed... */
|
||||
const modal = spawnSettingsModal("identity-profiles");
|
||||
modal.close_listener.push(() => {
|
||||
this.sendProperty("profiles").then(undefined);
|
||||
});
|
||||
});
|
||||
this.uiVariables.setVariableProvider("profiles", () => ({
|
||||
selected: this.currentProfile?.id,
|
||||
profiles: availableConnectProfiles().map(profile => ({
|
||||
id: profile.id,
|
||||
valid: profile.valid(),
|
||||
name: profile.profileName
|
||||
}))
|
||||
}));
|
||||
|
||||
this.uiEvents.on("action_select_profile", event => {
|
||||
const profile = findConnectProfile(event.id);
|
||||
this.uiVariables.setVariableEditor("profiles", newValue => {
|
||||
const profile = findConnectProfile(newValue.selected);
|
||||
if(!profile) {
|
||||
createErrorModal(tr("Invalid profile"), tr("Target connect profile is missing.")).open();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setSelectedProfile(profile);
|
||||
return; /* No need to update anything. The ui should received the values needed already */
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_set_address", event => this.setSelectedAddress(event.address, event.validate, event.updateUi));
|
||||
|
||||
this.uiEvents.on("action_set_nickname", event => {
|
||||
if(this.currentNickname !== event.nickname) {
|
||||
this.currentNickname = event.nickname;
|
||||
settings.setValue(Settings.KEY_CONNECT_USERNAME, event.nickname);
|
||||
|
||||
if(event.updateUi) {
|
||||
this.sendProperty("nickname").then(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
this.validateStates["nickname"] = event.validate;
|
||||
this.updateValidityStates();
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_set_password", event => {
|
||||
if(this.currentPassword === event.password) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPassword = event.password;
|
||||
this.currentPasswordHashed = event.hashed;
|
||||
if(event.updateUi) {
|
||||
this.sendProperty("password").then(undefined);
|
||||
}
|
||||
|
||||
this.validateStates["password"] = true;
|
||||
this.updateValidityStates();
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_select_history", event => this.setSelectedHistoryId(event.id));
|
||||
|
||||
this.uiEvents.on("action_connect", () => {
|
||||
Object.keys(this.validateStates).forEach(key => this.validateStates[key] = true);
|
||||
this.updateValidityStates();
|
||||
});
|
||||
|
||||
this.updateValidityStates();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
Object.keys(this.propertyProvider).forEach(key => delete this.propertyProvider[key]);
|
||||
this.uiEvents.destroy();
|
||||
this.uiVariables.destroy();
|
||||
}
|
||||
|
||||
generateConnectParameters() : ConnectParameters | undefined {
|
||||
if(Object.keys(this.validStates).findIndex(key => this.validStates[key] === false) !== -1) {
|
||||
if(!this.uiVariables.getVariableSync("nickname_valid", undefined, true)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(!this.uiVariables.getVariableSync("server_address_valid", undefined, true)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(!this.uiVariables.getVariableSync("profile_valid", undefined, true)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -279,7 +283,7 @@ class ConnectController {
|
|||
}
|
||||
|
||||
this.selectedHistoryId = id;
|
||||
this.sendProperty("history").then(undefined);
|
||||
this.uiVariables.sendVariable("history");
|
||||
|
||||
const historyEntry = this.history?.find(entry => entry.id === id);
|
||||
if(!historyEntry) { return; }
|
||||
|
@ -289,9 +293,9 @@ class ConnectController {
|
|||
this.currentPassword = historyEntry.hashedPassword;
|
||||
this.currentPasswordHashed = true;
|
||||
|
||||
this.sendProperty("address").then(undefined);
|
||||
this.sendProperty("password").then(undefined);
|
||||
this.sendProperty("nickname").then(undefined);
|
||||
this.uiVariables.sendVariable("server_address");
|
||||
this.uiVariables.sendVariable("password");
|
||||
this.uiVariables.sendVariable("nickname");
|
||||
}
|
||||
|
||||
setSelectedAddress(address: string | undefined, validate: boolean, updateUi: boolean) {
|
||||
|
@ -301,12 +305,12 @@ class ConnectController {
|
|||
this.setSelectedHistoryId(-1);
|
||||
|
||||
if(updateUi) {
|
||||
this.sendProperty("address").then(undefined);
|
||||
this.uiVariables.sendVariable("server_address");
|
||||
}
|
||||
}
|
||||
|
||||
this.validateStates["address"] = validate;
|
||||
this.updateValidityStates();
|
||||
this.validateAddress = true;
|
||||
this.uiVariables.sendVariable("server_address_valid");
|
||||
}
|
||||
|
||||
setSelectedProfile(profile: ConnectionProfile | undefined) {
|
||||
|
@ -315,62 +319,19 @@ class ConnectController {
|
|||
}
|
||||
|
||||
this.currentProfile = profile;
|
||||
this.sendProperty("profiles").then(undefined);
|
||||
this.uiVariables.sendVariable("profile_valid");
|
||||
this.uiVariables.sendVariable("profiles");
|
||||
settings.setValue(Settings.KEY_CONNECT_PROFILE, profile.id);
|
||||
|
||||
/* Clear out the nickname on profile switch and use the default nickname */
|
||||
this.uiEvents.fire("action_set_nickname", { nickname: undefined, validate: true, updateUi: true });
|
||||
|
||||
this.validateStates["profile"] = true;
|
||||
this.updateValidityStates();
|
||||
this.currentNickname = undefined;
|
||||
this.uiVariables.sendVariable("nickname");
|
||||
}
|
||||
|
||||
private updateValidityStates() {
|
||||
const newStates = Object.assign({}, kDefaultValidityStates);
|
||||
if(this.validateStates["nickname"]) {
|
||||
const nickname = this.currentNickname || this.currentProfile?.connectUsername() || "";
|
||||
newStates["nickname"] = nickname.length >= 3 && nickname.length <= 30;
|
||||
} else {
|
||||
newStates["nickname"] = true;
|
||||
}
|
||||
|
||||
if(this.validateStates["address"]) {
|
||||
const address = this.currentAddress || this.defaultAddress || "";
|
||||
const parsedAddress = parseServerAddress(address);
|
||||
|
||||
if(parsedAddress) {
|
||||
kRegexDomain.lastIndex = 0;
|
||||
newStates["address"] = kRegexDomain.test(parsedAddress.host) || ipRegex({ exact: true }).test(parsedAddress.host);
|
||||
} else {
|
||||
newStates["address"] = false;
|
||||
}
|
||||
} else {
|
||||
newStates["address"] = true;
|
||||
}
|
||||
|
||||
newStates["profile"] = !!this.currentProfile?.valid();
|
||||
newStates["password"] = true;
|
||||
|
||||
for(const key of Object.keys(newStates)) {
|
||||
if(_.isEqual(this.validStates[key], newStates[key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.validStates[key] = newStates[key];
|
||||
this.uiEvents.fire_react("notify_property_valid", { property: key as any, value: this.validStates[key] });
|
||||
}
|
||||
}
|
||||
|
||||
private async sendProperty(property: keyof ConnectProperties) {
|
||||
if(!this.propertyProvider[property]) {
|
||||
logWarn(LogCategory.GENERAL, tr("Tried to send a property where we don't have a provider for"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_property", {
|
||||
property: property,
|
||||
value: await this.propertyProvider[property]()
|
||||
});
|
||||
this.uiVariables.sendVariable("server_address_valid");
|
||||
this.uiVariables.sendVariable("nickname_valid");
|
||||
this.uiVariables.sendVariable("profile_valid");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,7 +343,8 @@ export type ConnectModalOptions = {
|
|||
}
|
||||
|
||||
export function spawnConnectModalNew(options: ConnectModalOptions) {
|
||||
const controller = new ConnectController();
|
||||
const variableProvider = createIpcUiVariableProvider();
|
||||
const controller = new ConnectController(variableProvider);
|
||||
|
||||
if(typeof options.selectedAddress === "string") {
|
||||
controller.setSelectedAddress(options.selectedAddress, false, true);
|
||||
|
@ -392,10 +354,9 @@ export function spawnConnectModalNew(options: ConnectModalOptions) {
|
|||
controller.setSelectedProfile(options.selectedProfile);
|
||||
}
|
||||
|
||||
const modal = spawnReactModal(ConnectModal, controller.uiEvents, options.connectInANewTab || false);
|
||||
const modal = spawnModal("modal-connect", [controller.uiEvents.generateIpcDescription(), variableProvider.generateConsumerDescription(), options.connectInANewTab || false]);
|
||||
modal.show();
|
||||
|
||||
modal.events.one("destroy", () => {
|
||||
modal.getEvents().one("destroy", () => {
|
||||
controller.destroy();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {kUnknownHistoryServerUniqueId} from "tc-shared/connectionlog/History";
|
||||
import {RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
|
||||
export const kUnknownHistoryServerUniqueId = "unknown";
|
||||
export type ConnectProfileEntry = {
|
||||
id: string,
|
||||
name: string,
|
||||
|
@ -23,81 +23,46 @@ export type ConnectHistoryServerInfo = {
|
|||
maxClients: number | -1
|
||||
}
|
||||
|
||||
export interface ConnectProperties {
|
||||
address: {
|
||||
export interface ConnectUiVariables {
|
||||
"server_address": {
|
||||
currentAddress: string,
|
||||
defaultAddress: string,
|
||||
defaultAddress?: string,
|
||||
},
|
||||
nickname: {
|
||||
readonly "server_address_valid": boolean,
|
||||
|
||||
"nickname": {
|
||||
currentNickname: string | undefined,
|
||||
defaultNickname: string | undefined,
|
||||
defaultNickname?: string,
|
||||
},
|
||||
password: {
|
||||
readonly "nickname_valid": boolean,
|
||||
|
||||
"password": {
|
||||
password: string,
|
||||
hashed: boolean
|
||||
} | undefined,
|
||||
profiles: {
|
||||
profiles: ConnectProfileEntry[],
|
||||
|
||||
"profiles": {
|
||||
profiles?: ConnectProfileEntry[],
|
||||
selected: string
|
||||
},
|
||||
historyShown: boolean,
|
||||
history: {
|
||||
readonly "profile_valid": boolean,
|
||||
|
||||
"historyShown": boolean,
|
||||
readonly "history": {
|
||||
history: ConnectHistoryEntry[],
|
||||
selected: number | -1,
|
||||
},
|
||||
}
|
||||
|
||||
export interface PropertyValidState {
|
||||
address: boolean,
|
||||
nickname: boolean,
|
||||
password: boolean,
|
||||
profile: boolean
|
||||
readonly "history_entry": ConnectHistoryServerInfo,
|
||||
readonly "history_connections": number
|
||||
}
|
||||
|
||||
type IAccess<I, T extends keyof I> = {
|
||||
property: T,
|
||||
value: I[T]
|
||||
};
|
||||
|
||||
export interface ConnectUiEvents {
|
||||
action_manage_profiles: {},
|
||||
action_select_profile: { id: string },
|
||||
action_select_history: { id: number },
|
||||
action_connect: { newTab: boolean },
|
||||
action_toggle_history: { enabled: boolean }
|
||||
action_delete_history: {
|
||||
target: string,
|
||||
targetType: "address" | "server-unique-id"
|
||||
},
|
||||
action_set_nickname: { nickname: string, validate: boolean, updateUi: boolean },
|
||||
action_set_address: { address: string, validate: boolean, updateUi: boolean },
|
||||
action_set_password: { password: string, hashed: boolean, updateUi: boolean },
|
||||
|
||||
query_property: {
|
||||
property: keyof ConnectProperties
|
||||
},
|
||||
query_property_valid: {
|
||||
property: keyof PropertyValidState
|
||||
},
|
||||
|
||||
notify_property: IAccess<ConnectProperties, keyof ConnectProperties>,
|
||||
notify_property_valid: IAccess<PropertyValidState, keyof PropertyValidState>,
|
||||
|
||||
query_history_entry: {
|
||||
serverUniqueId: string
|
||||
},
|
||||
query_history_connections: {
|
||||
target: string,
|
||||
targetType: "address" | "server-unique-id"
|
||||
}
|
||||
|
||||
notify_history_entry: {
|
||||
serverUniqueId: string,
|
||||
info: ConnectHistoryServerInfo
|
||||
},
|
||||
notify_history_connections: {
|
||||
target: string,
|
||||
targetType: "address" | "server-unique-id",
|
||||
value: number
|
||||
}
|
||||
}
|
|
@ -106,7 +106,8 @@
|
|||
.buttonShowHistory {
|
||||
.containerText {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
min-width: 10em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.containerArrow {
|
||||
|
@ -239,6 +240,8 @@
|
|||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,141 +1,109 @@
|
|||
import {
|
||||
ConnectHistoryEntry,
|
||||
ConnectHistoryServerInfo,
|
||||
ConnectProperties,
|
||||
ConnectUiEvents,
|
||||
PropertyValidState
|
||||
ConnectUiEvents, ConnectUiVariables, kUnknownHistoryServerUniqueId,
|
||||
} from "tc-shared/ui/modal/connect/Definitions";
|
||||
import * as React from "react";
|
||||
import {useContext, useState} from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {ControlledFlatInputField, ControlledSelect, FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
import {useContext} from "react";
|
||||
import {IpcRegistryDescription, Registry} from "tc-shared/events";
|
||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {kUnknownHistoryServerUniqueId} from "tc-shared/connectionlog/History";
|
||||
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";
|
||||
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||
|
||||
const EventContext = React.createContext<Registry<ConnectUiEvents>>(undefined);
|
||||
const VariablesContext = React.createContext<UiVariableConsumer<ConnectUiVariables>>(undefined);
|
||||
|
||||
const ConnectDefaultNewTabContext = React.createContext<boolean>(false);
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
function useProperty<T extends keyof ConnectProperties, V>(key: T, defaultValue: V) : [ConnectProperties[T] | V, (value: ConnectProperties[T]) => void] {
|
||||
const InputServerAddress = React.memo(() => {
|
||||
const events = useContext(EventContext);
|
||||
const [ value, setValue ] = useState<ConnectProperties[T] | V>(() => {
|
||||
events.fire("query_property", { property: key });
|
||||
return defaultValue;
|
||||
});
|
||||
events.reactUse("notify_property", event => event.property === key && setValue(event.value as any));
|
||||
|
||||
return [value, setValue];
|
||||
}
|
||||
|
||||
function usePropertyValid<T extends keyof PropertyValidState>(key: T, defaultValue: PropertyValidState[T]) : PropertyValidState[T] {
|
||||
const events = useContext(EventContext);
|
||||
const [ value, setValue ] = useState<PropertyValidState[T]>(() => {
|
||||
events.fire("query_property_valid", { property: key });
|
||||
return defaultValue;
|
||||
});
|
||||
events.reactUse("notify_property_valid", event => event.property === key && setValue(event.value as any));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const InputServerAddress = () => {
|
||||
const events = useContext(EventContext);
|
||||
const [address, setAddress] = useProperty("address", undefined);
|
||||
const valid = usePropertyValid("address", true);
|
||||
const newTab = useContext(ConnectDefaultNewTabContext);
|
||||
|
||||
const variables = useContext(VariablesContext);
|
||||
const address = variables.useVariable("server_address");
|
||||
const addressValid = variables.useReadOnly("server_address_valid", undefined, true) || address.localValue !== address.remoteValue;
|
||||
|
||||
return (
|
||||
<ControlledFlatInputField
|
||||
value={address?.currentAddress || ""}
|
||||
placeholder={address?.defaultAddress || tr("Please enter a address")}
|
||||
value={address.localValue?.currentAddress || ""}
|
||||
placeholder={address.remoteValue?.defaultAddress || tr("Please enter a address")}
|
||||
|
||||
className={cssStyle.inputAddress}
|
||||
|
||||
label={<Translatable>Server address</Translatable>}
|
||||
labelType={"static"}
|
||||
|
||||
invalid={valid ? undefined : <Translatable>Please enter a valid server address</Translatable>}
|
||||
invalid={addressValid ? undefined : <Translatable>Please enter a valid server address</Translatable>}
|
||||
editable={address.status === "loaded"}
|
||||
|
||||
onInput={value => {
|
||||
setAddress({ currentAddress: value, defaultAddress: address.defaultAddress });
|
||||
events.fire("action_set_address", { address: value, validate: true, updateUi: false });
|
||||
}}
|
||||
onBlur={() => {
|
||||
events.fire("action_set_address", { address: address?.currentAddress, validate: true, updateUi: true });
|
||||
}}
|
||||
onInput={value => address.setValue({ currentAddress: value }, true)}
|
||||
onBlur={() => address.setValue({ currentAddress: address.localValue?.currentAddress })}
|
||||
onEnter={() => {
|
||||
/* Setting the address just to ensure */
|
||||
events.fire("action_set_address", { address: address?.currentAddress, validate: true, updateUi: true });
|
||||
address.setValue({ currentAddress: address.localValue?.currentAddress });
|
||||
events.fire("action_connect", { newTab });
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
const InputServerPassword = () => {
|
||||
const events = useContext(EventContext);
|
||||
const [password, setPassword] = useProperty("password", undefined);
|
||||
const variables = useContext(VariablesContext);
|
||||
const password = variables.useVariable("password");
|
||||
|
||||
return (
|
||||
<FlatInputField
|
||||
className={cssStyle.inputPassword}
|
||||
value={!password?.hashed ? password?.password || "" : ""}
|
||||
placeholder={password?.hashed ? tr("Password Hidden") : null}
|
||||
value={!password.localValue?.hashed ? password.localValue?.password || "" : ""}
|
||||
placeholder={password.localValue?.hashed ? tr("Password Hidden") : null}
|
||||
type={"password"}
|
||||
label={<Translatable>Server password</Translatable>}
|
||||
labelType={password?.hashed ? "static" : "floating"}
|
||||
onInput={value => {
|
||||
setPassword({ password: value, hashed: false });
|
||||
events.fire("action_set_password", { password: value, hashed: false, updateUi: false });
|
||||
}}
|
||||
onBlur={() => {
|
||||
if(password) {
|
||||
events.fire("action_set_password", { password: password.password, hashed: password.hashed, updateUi: true });
|
||||
}
|
||||
}}
|
||||
labelType={password.localValue?.hashed ? "static" : "floating"}
|
||||
onInput={value => password.setValue({ password: value, hashed: false }, true)}
|
||||
onBlur={() => password.setValue(password.localValue)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const InputNickname = () => {
|
||||
const events = useContext(EventContext);
|
||||
const [nickname, setNickname] = useProperty("nickname", undefined);
|
||||
const valid = usePropertyValid("nickname", true);
|
||||
const variables = useContext(VariablesContext);
|
||||
|
||||
const nickname = variables.useVariable("nickname");
|
||||
const validState = variables.useReadOnly("nickname_valid", undefined, true) || nickname.localValue !== nickname.remoteValue;
|
||||
|
||||
return (
|
||||
<ControlledFlatInputField
|
||||
className={cssStyle.inputNickname}
|
||||
value={nickname?.currentNickname || ""}
|
||||
placeholder={nickname ? nickname.defaultNickname ? nickname.defaultNickname : tr("Please enter a nickname") : tr("loading...")}
|
||||
value={nickname.localValue?.currentNickname || ""}
|
||||
placeholder={nickname.remoteValue ? nickname.remoteValue.defaultNickname ? nickname.remoteValue.defaultNickname : tr("Please enter a nickname") : tr("loading...")}
|
||||
label={<Translatable>Nickname</Translatable>}
|
||||
labelType={"static"}
|
||||
invalid={valid ? undefined : <Translatable>Nickname too short or too long</Translatable>}
|
||||
onInput={value => {
|
||||
setNickname({ currentNickname: value, defaultNickname: nickname.defaultNickname });
|
||||
events.fire("action_set_nickname", { nickname: value, validate: true, updateUi: false });
|
||||
}}
|
||||
onBlur={() => events.fire("action_set_nickname", { nickname: nickname?.currentNickname, validate: true, updateUi: true })}
|
||||
invalid={validState ? undefined : <Translatable>Nickname too short or too long</Translatable>}
|
||||
onInput={value => nickname.setValue({ currentNickname: value }, true)}
|
||||
onBlur={() => nickname.setValue({ currentNickname: nickname.localValue?.currentNickname })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const InputProfile = () => {
|
||||
const events = useContext(EventContext);
|
||||
const [profiles] = useProperty("profiles", undefined);
|
||||
const selectedProfile = profiles?.profiles.find(profile => profile.id === profiles?.selected);
|
||||
const variables = useContext(VariablesContext);
|
||||
const profiles = variables.useVariable("profiles");
|
||||
const selectedProfile = profiles.remoteValue?.profiles.find(profile => profile.id === profiles.remoteValue?.selected);
|
||||
|
||||
let invalidMarker;
|
||||
if(profiles) {
|
||||
if(!profiles.selected) {
|
||||
if(!profiles.remoteValue?.selected) {
|
||||
/* We have to select a profile. */
|
||||
/* TODO: Only show if we've tried to press connect */
|
||||
//invalidMarker = <Translatable key={"no-profile"}>Please select a profile</Translatable>;
|
||||
|
@ -150,19 +118,23 @@ const InputProfile = () => {
|
|||
<div className={cssStyle.inputProfile}>
|
||||
<ControlledSelect
|
||||
className={cssStyle.input}
|
||||
value={selectedProfile ? selectedProfile.id : profiles?.selected ? "invalid" : profiles ? "no-selected" : "loading"}
|
||||
value={selectedProfile ? selectedProfile.id : profiles.remoteValue?.selected ? "invalid" : profiles ? "no-selected" : "loading"}
|
||||
type={"flat"}
|
||||
label={<Translatable>Connect profile</Translatable>}
|
||||
invalid={invalidMarker}
|
||||
invalidClassName={cssStyle.invalidFeedback}
|
||||
onChange={event => events.fire("action_select_profile", { id: event.target.value })}
|
||||
onChange={event => profiles.setValue({ selected: event.target.value })}
|
||||
>
|
||||
<option key={"no-selected"} value={"no-selected"} style={{ display: "none" }}>{useTr("please select")}</option>
|
||||
<option key={"invalid"} value={"invalid"} style={{ display: "none" }}>{useTr("unknown profile")}</option>
|
||||
<option key={"loading"} value={"loading"} style={{ display: "none" }}>{useTr("loading") + "..."}</option>
|
||||
{profiles?.profiles.map(profile => (
|
||||
<option value={"no-selected"} style={{ display: "none" }}>{useTr("please select")}</option>
|
||||
<option value={"invalid"} style={{ display: "none" }}>{useTr("unknown profile")}</option>
|
||||
<option value={"loading"} style={{ display: "none" }}>{useTr("loading") + "..."}</option>
|
||||
<React.Fragment>
|
||||
{
|
||||
profiles.remoteValue?.profiles.map(profile => (
|
||||
<option key={"profile-" + profile.id} value={profile.id}>{profile.name}</option>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</React.Fragment>
|
||||
</ControlledSelect>
|
||||
<Button className={cssStyle.button} type={"small"} color={"none"} onClick={() => events.fire("action_manage_profiles")}>
|
||||
<Translatable>Profiles</Translatable>
|
||||
|
@ -174,6 +146,7 @@ const InputProfile = () => {
|
|||
const ConnectContainer = () => (
|
||||
<div className={cssStyle.connectContainer}>
|
||||
<div className={cssStyle.row}>
|
||||
{/* <InputServerAddress /> */}
|
||||
<InputServerAddress />
|
||||
<InputServerPassword />
|
||||
</div>
|
||||
|
@ -185,11 +158,11 @@ const ConnectContainer = () => (
|
|||
);
|
||||
|
||||
const ButtonToggleHistory = () => {
|
||||
const state = useProperty("historyShown", false);
|
||||
const events = useContext(EventContext);
|
||||
const variables = useContext(VariablesContext);
|
||||
const historyShown = variables.useVariable("historyShown");
|
||||
|
||||
let body;
|
||||
if(state) {
|
||||
if(historyShown.localValue) {
|
||||
body = (
|
||||
<React.Fragment key={"hide"}>
|
||||
<div className={cssStyle.containerText}><Translatable>Hide connect history</Translatable></div>
|
||||
|
@ -209,7 +182,7 @@ const ButtonToggleHistory = () => {
|
|||
className={cssStyle.buttonShowHistory + " " + cssStyle.button}
|
||||
type={"small"}
|
||||
color={"none"}
|
||||
onClick={() => events.fire("action_toggle_history", { enabled: !state })}
|
||||
onClick={() => historyShown.setValue(!historyShown.localValue)}
|
||||
>
|
||||
{body}
|
||||
</Button>
|
||||
|
@ -281,35 +254,24 @@ const HistoryTableEntryConnectCount = React.memo((props: { entry: ConnectHistory
|
|||
const targetType = props.entry.uniqueServerId === kUnknownHistoryServerUniqueId ? "address" : "server-unique-id";
|
||||
const target = targetType === "address" ? props.entry.targetAddress : props.entry.uniqueServerId;
|
||||
|
||||
const events = useContext(EventContext);
|
||||
const [ amount, setAmount ] = useState(() => {
|
||||
events.fire("query_history_connections", {
|
||||
const value = useContext(VariablesContext).useReadOnly("history_connections", {
|
||||
target,
|
||||
targetType
|
||||
});
|
||||
return -1;
|
||||
});
|
||||
}, -1);
|
||||
|
||||
events.reactUse("notify_history_connections", event => event.targetType === targetType && event.target === target && setAmount(event.value));
|
||||
|
||||
if(amount >= 0) {
|
||||
return <React.Fragment key={"set"}>{amount}</React.Fragment>;
|
||||
if(value >= 0) {
|
||||
return <React.Fragment key={"set"}>{value}</React.Fragment>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const HistoryTableEntry = React.memo((props: { entry: ConnectHistoryEntry, selected: boolean }) => {
|
||||
const connectNewTab = useContext(ConnectDefaultNewTabContext);
|
||||
const events = useContext(EventContext);
|
||||
const [ info, setInfo ] = useState<ConnectHistoryServerInfo>(() => {
|
||||
if(props.entry.uniqueServerId !== kUnknownHistoryServerUniqueId) {
|
||||
events.fire("query_history_entry", { serverUniqueId: props.entry.uniqueServerId });
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
events.reactUse("notify_history_entry", event => event.serverUniqueId === props.entry.uniqueServerId && setInfo(event.info));
|
||||
const connectNewTab = useContext(ConnectDefaultNewTabContext);
|
||||
const variables = useContext(VariablesContext);
|
||||
|
||||
const info = variables.useReadOnly("history_entry", { serverUniqueId: props.entry.uniqueServerId }, undefined);
|
||||
const icon = getIconManager().resolveIcon(info ? info.icon.iconId : 0, info?.icon.serverUniqueId, info?.icon.handlerId);
|
||||
|
||||
return (
|
||||
|
@ -364,9 +326,9 @@ const HistoryTableEntry = React.memo((props: { entry: ConnectHistoryEntry, selec
|
|||
});
|
||||
|
||||
const HistoryTable = () => {
|
||||
const [history] = useProperty("history", undefined);
|
||||
let body;
|
||||
const history = useContext(VariablesContext).useReadOnly("history", undefined, undefined);
|
||||
|
||||
let body;
|
||||
if(history) {
|
||||
if(history.history.length > 0) {
|
||||
body = history.history.map(entry => <HistoryTableEntry entry={entry} key={"entry-" + entry.id} selected={entry.id === history.selected} />);
|
||||
|
@ -385,22 +347,22 @@ const HistoryTable = () => {
|
|||
<div className={cssStyle.head}>
|
||||
<div className={cssStyle.column + " " + cssStyle.delete} />
|
||||
<div className={cssStyle.column + " " + cssStyle.name}>
|
||||
<Translatable>Name</Translatable>
|
||||
<a title={useTr("Name")}><Translatable>Name</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.column + " " + cssStyle.address}>
|
||||
<Translatable>Address</Translatable>
|
||||
<a title={useTr("Address")}><Translatable>Address</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.column + " " + cssStyle.password}>
|
||||
<Translatable>Password</Translatable>
|
||||
<a title={useTr("Password")}><Translatable>Password</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.column + " " + cssStyle.country}>
|
||||
<Translatable>Country</Translatable>
|
||||
<a title={useTr("Country")}><Translatable>Country</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.column + " " + cssStyle.clients}>
|
||||
<Translatable>Clients</Translatable>
|
||||
<a title={useTr("Clients")}><Translatable>Clients</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.column + " " + cssStyle.connections}>
|
||||
<Translatable>Connections</Translatable>
|
||||
<a title={useTr("Connections")}><Translatable>Connections</Translatable></a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
|
@ -411,7 +373,8 @@ const HistoryTable = () => {
|
|||
}
|
||||
|
||||
const HistoryContainer = () => {
|
||||
const historyShown = useProperty("historyShown", false);
|
||||
const variables = useContext(VariablesContext);
|
||||
const historyShown = variables.useReadOnly("historyShown", undefined, false);
|
||||
|
||||
return (
|
||||
<div className={joinClassList(cssStyle.historyContainer, historyShown && cssStyle.shown)}>
|
||||
|
@ -420,20 +383,29 @@ const HistoryContainer = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export class ConnectModal extends InternalModal {
|
||||
class ConnectModal extends AbstractModal {
|
||||
private readonly events: Registry<ConnectUiEvents>;
|
||||
private readonly variables: UiVariableConsumer<ConnectUiVariables>;
|
||||
private readonly connectNewTabByDefault: boolean;
|
||||
|
||||
constructor(events: Registry<ConnectUiEvents>, connectNewTabByDefault: boolean) {
|
||||
constructor(events: IpcRegistryDescription<ConnectUiEvents>, variables: IpcVariableDescriptor<ConnectUiVariables>, connectNewTabByDefault: boolean) {
|
||||
super();
|
||||
|
||||
this.events = events;
|
||||
this.variables = createIpcUiVariableConsumer(variables);
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
this.connectNewTabByDefault = connectNewTabByDefault;
|
||||
}
|
||||
|
||||
protected onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
this.variables.destroy();
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EventContext.Provider value={this.events}>
|
||||
<VariablesContext.Provider value={this.variables}>
|
||||
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
|
||||
<div className={cssStyle.container}>
|
||||
<ConnectContainer />
|
||||
|
@ -441,11 +413,12 @@ export class ConnectModal extends InternalModal {
|
|||
<HistoryContainer />
|
||||
</div>
|
||||
</ConnectDefaultNewTabContext.Provider>
|
||||
</VariablesContext.Provider>
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement {
|
||||
renderTitle(): string | React.ReactElement {
|
||||
return <Translatable>Connect to a server</Translatable>;
|
||||
}
|
||||
|
||||
|
@ -457,3 +430,4 @@ export class ConnectModal extends InternalModal {
|
|||
return "top";
|
||||
}
|
||||
}
|
||||
export = ConnectModal;
|
|
@ -1,10 +1,10 @@
|
|||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {CssEditorEvents, CssVariable} from "../../../ui/modal/css-editor/Definitions";
|
||||
import {spawnExternalModal} from "../../../ui/react-elements/external-modal";
|
||||
import {Registry} from "../../../events";
|
||||
import {LogCategory, logWarn} from "../../../log";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
|
||||
interface CustomVariable {
|
||||
name: string;
|
||||
|
@ -172,7 +172,7 @@ export function spawnModalCssVariableEditor() {
|
|||
const events = new Registry<CssEditorEvents>();
|
||||
cssVariableEditorController(events);
|
||||
|
||||
const modal = spawnExternalModal("css-editor", { default: events }, {});
|
||||
const modal = spawnModal("css-editor", [ events.generateIpcDescription() ], { popedOut: true });
|
||||
modal.show();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,6 @@ export interface CssVariable {
|
|||
customValue?: string;
|
||||
}
|
||||
|
||||
export interface CssEditorUserData {
|
||||
|
||||
}
|
||||
|
||||
export interface CssEditorEvents {
|
||||
action_set_filter: { filter: string | undefined },
|
||||
action_select_entry: { variable: CssVariable },
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
import {CssEditorEvents, CssEditorUserData, CssVariable} from "tc-shared/ui/modal/css-editor/Definitions";
|
||||
import {Registry, RegistryMap} from "tc-shared/events";
|
||||
import {CssEditorEvents, CssVariable} from "tc-shared/ui/modal/css-editor/Definitions";
|
||||
import {IpcRegistryDescription, Registry} from "tc-shared/events";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {BoxedInputField, FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
|
@ -391,14 +391,11 @@ const requestFileAsText = async (): Promise<string> => {
|
|||
|
||||
class PopoutConversationUI extends AbstractModal {
|
||||
private readonly events: Registry<CssEditorEvents>;
|
||||
private readonly userData: CssEditorUserData;
|
||||
|
||||
constructor(registryMap: RegistryMap, userData: CssEditorUserData) {
|
||||
constructor(events: IpcRegistryDescription<CssEditorEvents>) {
|
||||
super();
|
||||
|
||||
this.userData = userData;
|
||||
this.events = registryMap["default"] as any;
|
||||
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
this.events.on("notify_export_result", event => {
|
||||
createInfoModal(tr("Config exported successfully"), tr("The config has been exported successfully.")).open();
|
||||
downloadTextAsFile(event.config, "teaweb-style.json");
|
||||
|
@ -420,8 +417,8 @@ class PopoutConversationUI extends AbstractModal {
|
|||
);
|
||||
}
|
||||
|
||||
title() {
|
||||
return "CSS Variable editor";
|
||||
renderTitle() {
|
||||
return <Translatable>"CSS Variable editor"</Translatable>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
import * as React from "react";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {EchoTestEventRegistry, EchoTestModal} from "tc-shared/ui/modal/echo-test/Renderer";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {EchoTestEvents, TestState} from "tc-shared/ui/modal/echo-test/Definitions";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
|
@ -18,30 +15,13 @@ export function spawnEchoTestModal(connection: ConnectionHandler) {
|
|||
|
||||
initializeController(connection, events);
|
||||
|
||||
const modal = spawnReactModal(class extends InternalModal {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EchoTestEventRegistry.Provider value={events}>
|
||||
<EchoTestModal/>
|
||||
</EchoTestEventRegistry.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement<Translatable> {
|
||||
return <Translatable>Voice echo test</Translatable>;
|
||||
}
|
||||
});
|
||||
|
||||
const modal = spawnModal("echo-test", [ events.generateIpcDescription() ], { popedOut: false });
|
||||
events.on("action_close", () => {
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
modal.events.on("close", () => events.fire_react("notify_close"));
|
||||
modal.events.on("destroy", () => {
|
||||
modal.getEvents().on("close", () => events.fire_react("notify_close"));
|
||||
modal.getEvents().on("destroy", () => {
|
||||
events.fire("notify_destroy");
|
||||
events.destroy();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import {useContext, useState} from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {IpcRegistryDescription, Registry} from "tc-shared/events";
|
||||
import {EchoTestEvents, TestState, VoiceConnectionState} from "./Definitions";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
|
@ -8,10 +8,11 @@ import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
|||
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
export const EchoTestEventRegistry = React.createContext<Registry<EchoTestEvents>>(undefined);
|
||||
const EchoTestEventRegistry = React.createContext<Registry<EchoTestEvents>>(undefined);
|
||||
|
||||
const VoiceStateOverlay = () => {
|
||||
const events = useContext(EchoTestEventRegistry);
|
||||
|
@ -235,7 +236,7 @@ const TroubleshootingSoundOverlay = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export const TestToggle = () => {
|
||||
const TestToggle = () => {
|
||||
const events = useContext(EchoTestEventRegistry);
|
||||
|
||||
const [state, setState] = useState<"loading" | boolean>(() => {
|
||||
|
@ -255,7 +256,7 @@ export const TestToggle = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export const EchoTestModal = () => {
|
||||
const EchoTestModalRenderer = () => {
|
||||
const events = useContext(EchoTestEventRegistry);
|
||||
|
||||
return (
|
||||
|
@ -291,3 +292,27 @@ export const EchoTestModal = () => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
class ModalEchoTest extends AbstractModal {
|
||||
private readonly events: Registry<EchoTestEvents>;
|
||||
|
||||
constructor(events: IpcRegistryDescription<EchoTestEvents>) {
|
||||
super();
|
||||
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EchoTestEventRegistry.Provider value={this.events}>
|
||||
<EchoTestModalRenderer />
|
||||
</EchoTestEventRegistry.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle(): string | React.ReactElement<Translatable> {
|
||||
return <Translatable>Voice echo test</Translatable>;
|
||||
}
|
||||
}
|
||||
|
||||
export = ModalEchoTest;
|
|
@ -1,5 +1,4 @@
|
|||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {ModalGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Renderer";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ModalGlobalSettingsEditorEvents, Setting} from "tc-shared/ui/modal/global-settings-editor/Definitions";
|
||||
import {RegistryKey, RegistryValueType, Settings, settings} from "tc-shared/settings";
|
||||
|
@ -8,9 +7,9 @@ export function spawnGlobalSettingsEditor() {
|
|||
const events = new Registry<ModalGlobalSettingsEditorEvents>();
|
||||
initializeController(events);
|
||||
|
||||
const modal = spawnReactModal(ModalGlobalSettingsEditor, events);
|
||||
const modal = spawnModal("global-settings-editor", [ events.generateIpcDescription() ], { popoutable: true, popedOut: false });
|
||||
modal.show();
|
||||
modal.events.on("destroy", () => {
|
||||
modal.getEvents().on("destroy", () => {
|
||||
events.fire("notify_destroy");
|
||||
events.destroy();
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import * as React from "react";
|
||||
import {createContext, useContext, useRef, useState} from "react";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {IpcRegistryDescription, Registry} from "tc-shared/events";
|
||||
import {ModalGlobalSettingsEditorEvents, Setting} from "tc-shared/ui/modal/global-settings-editor/Definitions";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
const ModalEvents = createContext<Registry<ModalGlobalSettingsEditorEvents>>(undefined);
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
@ -159,13 +159,13 @@ const SettingList = () => {
|
|||
);
|
||||
}
|
||||
|
||||
export class ModalGlobalSettingsEditor extends InternalModal {
|
||||
class ModalGlobalSettingsEditor extends AbstractModal {
|
||||
protected readonly events: Registry<ModalGlobalSettingsEditorEvents>;
|
||||
|
||||
constructor(events: Registry<ModalGlobalSettingsEditorEvents>) {
|
||||
constructor(events: IpcRegistryDescription<ModalGlobalSettingsEditorEvents>) {
|
||||
super();
|
||||
|
||||
this.events = events;
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
|
@ -189,8 +189,9 @@ export class ModalGlobalSettingsEditor extends InternalModal {
|
|||
);
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement<Translatable> {
|
||||
renderTitle(): string | React.ReactElement<Translatable> {
|
||||
return <Translatable>Global settings registry</Translatable>;
|
||||
}
|
||||
}
|
||||
|
||||
export = ModalGlobalSettingsEditor;
|
|
@ -54,10 +54,11 @@ html:root {
|
|||
min-width: 20em;
|
||||
max-width: 100%;
|
||||
|
||||
min-height: 20em;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
|
||||
.contextContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
|
@ -34,6 +34,7 @@ import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controll
|
|||
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";
|
||||
|
||||
const cssStyle = require("./ModalPermissionEditor.scss");
|
||||
|
||||
|
@ -44,12 +45,12 @@ export type PermissionEditorSubject =
|
|||
| "client"
|
||||
| "client-channel"
|
||||
| "none";
|
||||
export const PermissionTabName: { [T in PermissionEditorTab]: { name: string, translated: string } } = {
|
||||
"groups-server": {name: "Server Groups", translated: tr("Server Groups")},
|
||||
"groups-channel": {name: "Channel Groups", translated: tr("Channel Groups")},
|
||||
"channel": {name: "Channel Permissions", translated: tr("Channel Permissions")},
|
||||
"client": {name: "Client Permissions", translated: tr("Client Permissions")},
|
||||
"client-channel": {name: "Client Channel Permissions", translated: tr("Client Channel Permissions")},
|
||||
export const PermissionTabName: { [T in PermissionEditorTab]: { name: string, useTranslate: () => string, renderTranslate: () => React.ReactNode } } = {
|
||||
"groups-server": {name: "Server Groups", useTranslate: () => useTr("Server Groups"), renderTranslate: () => <Translatable>Server Groups</Translatable>},
|
||||
"groups-channel": {name: "Channel Groups", useTranslate: () => useTr("Channel Groups"), renderTranslate: () => <Translatable>Channel Groups</Translatable>},
|
||||
"channel": {name: "Channel Permissions", useTranslate: () => useTr("Channel Permissions"), renderTranslate: () => <Translatable>Channel Permissions</Translatable>},
|
||||
"client": {name: "Client Permissions", useTranslate: () => useTr("Client Permissions"), renderTranslate: () => <Translatable>Client Permissions</Translatable>},
|
||||
"client-channel": {name: "Client Channel Permissions", useTranslate: () => useTr("Client Channel Permissions"), renderTranslate: () => <Translatable>Client Channel Permissions</Translatable>},
|
||||
};
|
||||
|
||||
export type GroupProperties = {
|
||||
|
@ -244,13 +245,15 @@ const ActiveTabInfo = (props: { events: Registry<PermissionModalEvents> }) => {
|
|||
const [activeTab, setActiveTab] = useState<PermissionEditorTab>("groups-server");
|
||||
props.events.reactUse("action_activate_tab", event => setActiveTab(event.tab));
|
||||
|
||||
return <div className={cssStyle.header + " " + cssStyle.activeTabInfo}>
|
||||
return (
|
||||
<div className={cssStyle.header + " " + cssStyle.activeTabInfo}>
|
||||
<div className={cssStyle.entry}>
|
||||
<a title={PermissionTabName[activeTab].translated}>
|
||||
<Translatable trIgnore={true}>{PermissionTabName[activeTab].name}</Translatable>
|
||||
<a title={PermissionTabName[activeTab].useTranslate()} key={"tab-" + activeTab}>
|
||||
{PermissionTabName[activeTab].renderTranslate()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TabSelectorEntry = (props: { events: Registry<PermissionModalEvents>, entry: PermissionEditorTab }) => {
|
||||
|
@ -258,22 +261,26 @@ const TabSelectorEntry = (props: { events: Registry<PermissionModalEvents>, entr
|
|||
|
||||
props.events.reactUse("action_activate_tab", event => setActive(event.tab === props.entry));
|
||||
|
||||
return <div className={cssStyle.entry + " " + (active ? cssStyle.selected : "")}
|
||||
return (
|
||||
<div className={cssStyle.entry + " " + (active ? cssStyle.selected : "")}
|
||||
onClick={() => !active && props.events.fire("action_activate_tab", {tab: props.entry})}>
|
||||
<a title={PermissionTabName[props.entry].translated}>
|
||||
<Translatable trIgnore={true}>{PermissionTabName[props.entry].translated}</Translatable>
|
||||
<a title={PermissionTabName[props.entry].useTranslate()}>
|
||||
{PermissionTabName[props.entry].renderTranslate()}
|
||||
</a>
|
||||
</div>;
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TabSelector = (props: { events: Registry<PermissionModalEvents> }) => {
|
||||
return <div className={cssStyle.header + " " + cssStyle.tabSelector}>
|
||||
return (
|
||||
<div className={cssStyle.header + " " + cssStyle.tabSelector}>
|
||||
<TabSelectorEntry events={props.events} entry={"groups-server"}/>
|
||||
<TabSelectorEntry events={props.events} entry={"groups-channel"}/>
|
||||
<TabSelectorEntry events={props.events} entry={"channel"}/>
|
||||
<TabSelectorEntry events={props.events} entry={"client"}/>
|
||||
<TabSelectorEntry events={props.events} entry={"client-channel"}/>
|
||||
</div>;
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type DefaultTabValues = { groupId?: number, channelId?: number, clientDatabaseId?: number };
|
||||
|
@ -336,7 +343,7 @@ class PermissionEditorModal extends InternalModal {
|
|||
);
|
||||
}
|
||||
|
||||
title(): React.ReactElement<Translatable> {
|
||||
renderTitle(): React.ReactElement<Translatable> {
|
||||
return <Translatable>Server permissions</Translatable>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1354,7 +1354,7 @@ export class PermissionEditor extends React.Component<PermissionEditorProperties
|
|||
<div key={"footer"} className={cssStyle.containerFooter}>
|
||||
<RefreshButton events={this.props.events}/>
|
||||
</div>
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
|
|
|
@ -409,8 +409,9 @@ function initializeController(events: Registry<NotificationSettingsEvents>) {
|
|||
let filter = undefined;
|
||||
|
||||
events.on(["query_events", "action_set_filter"], event => {
|
||||
if (event.type === "action_set_filter")
|
||||
filter = event.as<"action_set_filter">().filter;
|
||||
if (event.type === "action_set_filter") {
|
||||
filter = event.asUnchecked("action_set_filter").filter;
|
||||
}
|
||||
|
||||
const groupMapper = (group: EventGroup) => {
|
||||
const result = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
|
||||
import * as React from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {FileBrowserRenderer, NavigationBar} from "./FileBrowserRenderer";
|
||||
|
@ -41,7 +41,7 @@ class FileTransferModal extends InternalModal {
|
|||
this.transferInfoEvents.fire("notify_destroy");
|
||||
}
|
||||
|
||||
title() {
|
||||
renderTitle() {
|
||||
return <Translatable>File Browser</Translatable>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {ModalVideoSourceEvents} from "tc-shared/ui/modal/video-source/Definitions";
|
||||
import {ModalVideoSource} from "tc-shared/ui/modal/video-source/Renderer";
|
||||
import {getVideoDriver, VideoPermissionStatus, VideoSource} from "tc-shared/video/VideoSource";
|
||||
|
@ -94,7 +94,10 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, mode
|
|||
}
|
||||
|
||||
if(event.status.status === "preview") {
|
||||
/* we've successfully selected something */
|
||||
/* We've successfully selected something. Use that device instead. */
|
||||
result.source?.deref();
|
||||
result.source = controller.getCurrentSource()?.ref();
|
||||
result.config = controller.getBroadcastConstraints();
|
||||
modal.destroy();
|
||||
}
|
||||
});
|
||||
|
@ -125,13 +128,20 @@ async function generateAndApplyDefaultConfig(source: VideoSource) : Promise<Vide
|
|||
const trackSettings = videoTrack.getSettings();
|
||||
const capabilities = source.getCapabilities();
|
||||
|
||||
maxHeight = Math.min(maxHeight, capabilities.maxHeight);
|
||||
maxWidth = Math.min(maxWidth, capabilities.maxWidth);
|
||||
/* Safari */
|
||||
if(trackSettings.height === 0) {
|
||||
trackSettings.height = capabilities.maxHeight;
|
||||
}
|
||||
if(trackSettings.width === 0) {
|
||||
trackSettings.width = capabilities.maxWidth;
|
||||
}
|
||||
|
||||
maxHeight = maxHeight ? Math.min(maxHeight, capabilities.maxHeight) : capabilities.maxHeight;
|
||||
maxWidth = maxWidth ? Math.min(maxWidth, capabilities.maxWidth) : capabilities.maxWidth;
|
||||
|
||||
/* FIXME: Get these values somewhere else! */
|
||||
const broadcastConstraints: VideoBroadcastConfig = {
|
||||
maxBandwidth: 1_600_000,
|
||||
keyframeInterval: 0
|
||||
maxBandwidth: settings.getValue(Settings.KEY_VIDEO_DEFAULT_MAX_BANDWIDTH),
|
||||
keyframeInterval: settings.getValue(Settings.KEY_VIDEO_DEFAULT_KEYFRAME_INTERVAL)
|
||||
} as VideoBroadcastConfig;
|
||||
|
||||
{
|
||||
|
@ -160,7 +170,7 @@ async function generateAndApplyDefaultConfig(source: VideoSource) : Promise<Vide
|
|||
try {
|
||||
await applyBroadcastConfig(source, broadcastConstraints);
|
||||
} catch (error) {
|
||||
logWarn(LogCategory.VIDEO, tr("Failed to apply initial default broadcast config: %o"), error);
|
||||
logWarn(LogCategory.VIDEO, tr("Failed to apply initial default broadcast config (%o): %o"), broadcastConstraints, error);
|
||||
}
|
||||
|
||||
updateBroadcastConfigFromSource(source, broadcastConstraints);
|
||||
|
|
|
@ -855,7 +855,7 @@ export class ModalVideoSource extends InternalModal {
|
|||
);
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement<Translatable> {
|
||||
renderTitle(): string | React.ReactElement<Translatable> {
|
||||
return <Translatable>Start video Broadcasting</Translatable>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
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 {WhatsNew} from "tc-shared/ui/modal/whats-new/Renderer";
|
||||
|
@ -15,7 +15,7 @@ export function spawnUpdatedModal(changes: { changesUI?: ChangeLog, changesClien
|
|||
return <WhatsNew changesUI={changes.changesUI} changesClient={changes.changesClient}/>;
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement<Translatable> {
|
||||
renderTitle(): string | React.ReactElement<Translatable> {
|
||||
return <Translatable>We've updated the client for you</Translatable>;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ export class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> {
|
|||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
/* TODO: Some kind of logging? */
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() : Partial<ErrorBoundaryState> {
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface BoxedInputFieldProperties {
|
|||
isInvalid?: boolean;
|
||||
|
||||
className?: string;
|
||||
maxLength?: number,
|
||||
|
||||
size?: "normal" | "large" | "small";
|
||||
type?: "text" | "password" | "number";
|
||||
|
@ -86,6 +87,7 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
|
|||
disabled={this.state.disabled || this.props.disabled}
|
||||
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
|
||||
onKeyDown={e => this.onKeyDown(e)}
|
||||
maxLength={this.props.maxLength}
|
||||
/>
|
||||
}
|
||||
{this.props.suffix ? <a key={"suffix"} className={cssStyle.suffix}>{this.props.suffix}</a> : undefined}
|
||||
|
@ -399,6 +401,7 @@ export const ControlledSelect = (props: {
|
|||
|
||||
export interface SelectProperties {
|
||||
type?: "flat" | "boxed";
|
||||
refSelect?: React.RefObject<HTMLSelectElement>,
|
||||
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
|
@ -416,6 +419,8 @@ export interface SelectProperties {
|
|||
disabled?: boolean;
|
||||
editable?: boolean;
|
||||
|
||||
title?: string,
|
||||
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
|
||||
|
@ -430,11 +435,13 @@ export interface SelectFieldState {
|
|||
}
|
||||
|
||||
export class Select extends React.Component<SelectProperties, SelectFieldState> {
|
||||
private refSelect = React.createRef<HTMLSelectElement>();
|
||||
private refSelect;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.refSelect = this.props.refSelect || React.createRef<HTMLSelectElement>();
|
||||
|
||||
this.state = {
|
||||
isInvalid: false,
|
||||
invalidMessage: ""
|
||||
|
@ -444,7 +451,12 @@ export class Select extends React.Component<SelectProperties, SelectFieldState>
|
|||
render() {
|
||||
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : typeof this.props.disabled === "boolean" ? this.props.disabled : false;
|
||||
return (
|
||||
<div className={(this.props.type === "boxed" ? cssStyle.containerBoxed : cssStyle.containerFlat) + " " + cssStyle["size-normal"] + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (this.props.className || "") + " " + cssStyle.noLeftIcon + " " + cssStyle.noRightIcon}>
|
||||
<div className={
|
||||
(this.props.type === "boxed" ? cssStyle.containerBoxed : cssStyle.containerFlat) + " " +
|
||||
cssStyle["size-normal"] + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " +
|
||||
(this.props.className || "") + " " + cssStyle.noLeftIcon + " " + cssStyle.noRightIcon + " " +
|
||||
(this.props.disabled ? cssStyle.disabled : "")
|
||||
}>
|
||||
{this.props.label ?
|
||||
<label className={cssStyle["type-static"] + " " + (this.props.labelClassName || "")}>{this.props.label}</label> : undefined}
|
||||
<select
|
||||
|
@ -453,6 +465,7 @@ export class Select extends React.Component<SelectProperties, SelectFieldState>
|
|||
value={this.props.value}
|
||||
defaultValue={this.props.defaultValue}
|
||||
disabled={disabled}
|
||||
title={this.props.title}
|
||||
|
||||
onFocus={this.props.onFocus}
|
||||
onBlur={this.props.onBlur}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import {InternalModal, InternalModalController} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
|
||||
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new () => ModalClass) : InternalModalController<ModalClass>;
|
||||
export function spawnReactModal<ModalClass extends InternalModal, A1>(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : InternalModalController<ModalClass>;
|
||||
export function spawnReactModal<ModalClass extends InternalModal, A1, A2>(modalClass: new (..._: [A1, A2]) => ModalClass, arg1: A1, arg2: A2) : InternalModalController<ModalClass>;
|
||||
export function spawnReactModal<ModalClass extends InternalModal, A1, A2, A3>(modalClass: new (..._: [A1, A2, A3]) => ModalClass, arg1: A1, arg2: A2, arg3: A3) : InternalModalController<ModalClass>;
|
||||
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<ModalClass>;
|
||||
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<ModalClass>;
|
||||
export function spawnReactModal<ModalClass extends InternalModal>(modalClass: new (..._: any[]) => ModalClass, ...args: any[]) : InternalModalController<ModalClass> {
|
||||
return new InternalModalController(new modalClass(...args));
|
||||
}
|
|
@ -1,59 +1,3 @@
|
|||
import * as React from "react";
|
||||
import {ReactElement} from "react";
|
||||
import {Registry} from "../../events";
|
||||
|
||||
export type ModalType = "error" | "warning" | "info" | "none";
|
||||
|
||||
export interface ModalOptions {
|
||||
destroyOnClose?: boolean;
|
||||
|
||||
defaultSize?: { width: number, height: number };
|
||||
}
|
||||
|
||||
export interface ModalEvents {
|
||||
"open": {},
|
||||
"close": {},
|
||||
|
||||
/* create is implicitly at object creation */
|
||||
"destroy": {}
|
||||
}
|
||||
|
||||
export enum ModalState {
|
||||
SHOWN,
|
||||
HIDDEN,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
export interface ModalController {
|
||||
getOptions() : Readonly<ModalOptions>;
|
||||
getEvents() : Registry<ModalEvents>;
|
||||
getState() : ModalState;
|
||||
|
||||
show() : Promise<void>;
|
||||
hide() : Promise<void>;
|
||||
|
||||
destroy();
|
||||
}
|
||||
|
||||
export abstract class AbstractModal {
|
||||
protected constructor() {}
|
||||
|
||||
abstract renderBody() : ReactElement;
|
||||
abstract title() : string | React.ReactElement;
|
||||
|
||||
/* only valid for the "inline" modals */
|
||||
type() : ModalType { return "none"; }
|
||||
color() : "none" | "blue" { return "none"; }
|
||||
verticalAlignment() : "top" | "center" | "bottom" { return "center"; }
|
||||
|
||||
protected onInitialize() {}
|
||||
protected onDestroy() {}
|
||||
|
||||
protected onClose() {}
|
||||
protected onOpen() {}
|
||||
}
|
||||
|
||||
|
||||
export interface ModalRenderer {
|
||||
renderModal(modal: AbstractModal | undefined);
|
||||
}
|
||||
/* TODO: Remove this! */
|
||||
import * as definitions from "./modal/Definitions";
|
||||
export = definitions;
|
|
@ -1,7 +1,7 @@
|
|||
import {LogCategory, logDebug, logTrace, logWarn} from "../../../log";
|
||||
import * as ipc from "../../../ipc/BrowserIPC";
|
||||
import {ChannelMessage} from "../../../ipc/BrowserIPC";
|
||||
import {Registry, RegistryMap} from "../../../events";
|
||||
import {Registry} from "../../../events";
|
||||
import {
|
||||
EventControllerBase,
|
||||
Popout2ControllerMessages,
|
||||
|
@ -11,7 +11,7 @@ import {ModalController, ModalEvents, ModalOptions, ModalState} from "../../../u
|
|||
|
||||
export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalController {
|
||||
public readonly modalType: string;
|
||||
public readonly userData: any;
|
||||
public readonly constructorArguments: any[];
|
||||
|
||||
private readonly modalEvents: Registry<ModalEvents>;
|
||||
private modalState: ModalState = ModalState.DESTROYED;
|
||||
|
@ -19,15 +19,13 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
|||
private readonly documentUnloadListener: () => void;
|
||||
private callbackWindowInitialized: (error?: string) => void;
|
||||
|
||||
protected constructor(modal: string, registries: RegistryMap, userData: any) {
|
||||
protected constructor(modalType: string, constructorArguments: any[]) {
|
||||
super();
|
||||
this.initializeRegistries(registries);
|
||||
this.modalType = modalType;
|
||||
this.constructorArguments = constructorArguments;
|
||||
|
||||
this.modalEvents = new Registry<ModalEvents>();
|
||||
|
||||
this.modalType = modal;
|
||||
this.userData = userData;
|
||||
|
||||
this.ipcChannel = ipc.getIpcInstance().createChannel();
|
||||
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
||||
|
||||
|
@ -156,15 +154,10 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
|||
this.callbackWindowInitialized = undefined;
|
||||
}
|
||||
|
||||
this.sendIPCMessage("hello-controller", { accepted: true, userData: this.userData, registries: Object.keys(this.localRegistries) });
|
||||
this.sendIPCMessage("hello-controller", { accepted: true, constructorArguments: this.constructorArguments });
|
||||
break;
|
||||
}
|
||||
|
||||
case "fire-event":
|
||||
case "fire-event-callback":
|
||||
/* already handled by out base class */
|
||||
break;
|
||||
|
||||
case "invoke-modal-action":
|
||||
/* must be handled by the underlying handler */
|
||||
break;
|
||||
|
|
|
@ -1,29 +1,15 @@
|
|||
import {ChannelMessage, IPCChannel} from "../../../ipc/BrowserIPC";
|
||||
import {EventReceiver, RegistryMap} from "../../../events";
|
||||
|
||||
export interface PopoutIPCMessage {
|
||||
"hello-popout": { version: string },
|
||||
"hello-controller": { accepted: boolean, message?: string, userData?: any, registries?: string[] },
|
||||
|
||||
"fire-event": {
|
||||
type: "sync" | "react" | "later";
|
||||
eventType: string;
|
||||
payload: any;
|
||||
callbackId: string;
|
||||
registry: string;
|
||||
},
|
||||
|
||||
"fire-event-callback": {
|
||||
callbackId: string
|
||||
},
|
||||
|
||||
"hello-controller": { accepted: boolean, message?: string, constructorArguments?: any[] },
|
||||
"invoke-modal-action": {
|
||||
action: "close" | "minimize"
|
||||
}
|
||||
}
|
||||
|
||||
export type Controller2PopoutMessages = "hello-controller" | "fire-event" | "fire-event-callback";
|
||||
export type Popout2ControllerMessages = "hello-popout" | "fire-event" | "fire-event-callback" | "invoke-modal-action";
|
||||
export type Controller2PopoutMessages = "hello-controller";
|
||||
export type Popout2ControllerMessages = "hello-popout" | "invoke-modal-action";
|
||||
|
||||
export interface SendIPCMessage {
|
||||
"controller": Controller2PopoutMessages;
|
||||
|
@ -35,71 +21,12 @@ export interface ReceivedIPCMessage {
|
|||
"popout": Controller2PopoutMessages;
|
||||
}
|
||||
|
||||
let callbackIdIndex = 0;
|
||||
export abstract class EventControllerBase<Type extends "controller" | "popout"> {
|
||||
protected ipcChannel: IPCChannel;
|
||||
protected ipcRemoteId: string;
|
||||
|
||||
protected localRegistries: RegistryMap;
|
||||
private localEventReceiver: {[key: string]: EventReceiver};
|
||||
|
||||
private omitEventType: string = undefined;
|
||||
private omitEventData: any;
|
||||
private eventFiredListeners: {[key: string]:{ callback: () => void, timeout: number }} = {};
|
||||
|
||||
protected constructor() { }
|
||||
|
||||
protected initializeRegistries(registries: RegistryMap) {
|
||||
if(typeof this.localRegistries !== "undefined") { throw "event registries have already been initialized" };
|
||||
|
||||
this.localEventReceiver = {};
|
||||
this.localRegistries = registries;
|
||||
|
||||
for(const key of Object.keys(this.localRegistries)) {
|
||||
this.localEventReceiver[key] = this.createEventReceiver(key);
|
||||
this.localRegistries[key].connectAll(this.localEventReceiver[key]);
|
||||
}
|
||||
}
|
||||
|
||||
private createEventReceiver(key: string) : EventReceiver {
|
||||
let refThis = this;
|
||||
|
||||
const fireEvent = (type: "react" | "later", eventType: any, data?: any[], callback?: () => void) => {
|
||||
const callbackId = callback ? (++callbackIdIndex) + "-ev-cb" : undefined;
|
||||
refThis.sendIPCMessage("fire-event", { type: type, eventType: eventType, payload: data, callbackId: callbackId, registry: key });
|
||||
if(callbackId) {
|
||||
const timeout = setTimeout(() => {
|
||||
delete refThis.eventFiredListeners[callbackId];
|
||||
callback();
|
||||
}, 2500);
|
||||
|
||||
refThis.eventFiredListeners[callbackId] = {
|
||||
callback: callback,
|
||||
timeout: timeout
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return new class implements EventReceiver {
|
||||
fire<T extends keyof {}>(eventType: T, data?: any[T], overrideTypeKey?: boolean) {
|
||||
if(refThis.omitEventType === eventType && refThis.omitEventData === data) {
|
||||
refThis.omitEventType = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
refThis.sendIPCMessage("fire-event", { type: "sync", eventType: eventType, payload: data, callbackId: undefined, registry: key });
|
||||
}
|
||||
|
||||
fire_later<T extends keyof { [p: string]: any }>(eventType: T, data?: { [p: string]: any }[T], callback?: () => void) {
|
||||
fireEvent("later", eventType, data, callback);
|
||||
}
|
||||
|
||||
fire_react<T extends keyof {}>(eventType: T, data?: any[T], callback?: () => void) {
|
||||
fireEvent("react", eventType, data, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||
if(this.ipcRemoteId !== remoteId) {
|
||||
console.warn("Received message from unknown end: %s. Expected: %s", remoteId, this.ipcRemoteId);
|
||||
|
@ -113,37 +40,10 @@ export abstract class EventControllerBase<Type extends "controller" | "popout">
|
|||
this.ipcChannel.sendMessage(type, payload, this.ipcRemoteId);
|
||||
}
|
||||
|
||||
protected handleTypedIPCMessage<T extends ReceivedIPCMessage[Type]>(type: T, payload: PopoutIPCMessage[T]) {
|
||||
switch (type) {
|
||||
case "fire-event": {
|
||||
const tpayload = payload as PopoutIPCMessage["fire-event"];
|
||||
|
||||
/* FIXME: Pay respect to the different event types and may bundle react updates! */
|
||||
this.omitEventData = tpayload.payload;
|
||||
this.omitEventType = tpayload.eventType;
|
||||
this.localRegistries[tpayload.registry].fire(tpayload.eventType, tpayload.payload);
|
||||
if(tpayload.callbackId)
|
||||
this.sendIPCMessage("fire-event-callback", { callbackId: tpayload.callbackId });
|
||||
break;
|
||||
}
|
||||
|
||||
case "fire-event-callback": {
|
||||
const tpayload = payload as PopoutIPCMessage["fire-event-callback"];
|
||||
const callback = this.eventFiredListeners[tpayload.callbackId];
|
||||
delete this.eventFiredListeners[tpayload.callbackId];
|
||||
if(callback) {
|
||||
clearTimeout(callback.timeout);
|
||||
callback.callback();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
protected handleTypedIPCMessage<T extends ReceivedIPCMessage[Type]>(type: T, payload: PopoutIPCMessage[T]) {}
|
||||
|
||||
protected destroyIPC() {
|
||||
Object.keys(this.localRegistries).forEach(key => this.localRegistries[key].disconnectAll(this.localEventReceiver[key]));
|
||||
this.ipcChannel = undefined;
|
||||
this.ipcRemoteId = undefined;
|
||||
this.eventFiredListeners = {};
|
||||
}
|
||||
}
|
|
@ -5,29 +5,32 @@ import {
|
|||
EventControllerBase,
|
||||
PopoutIPCMessage
|
||||
} from "../../../ui/react-elements/external-modal/IPCMessage";
|
||||
import {Registry, RegistryMap} from "../../../events";
|
||||
|
||||
let controller: PopoutController;
|
||||
export function getPopoutController() {
|
||||
if(!controller)
|
||||
if(!controller) {
|
||||
controller = new PopoutController();
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
||||
class PopoutController extends EventControllerBase<"popout"> {
|
||||
private userData: any;
|
||||
private constructorArguments: any[];
|
||||
private callbackControllerHello: (accepted: boolean | string) => void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ipcRemoteId = AppParameters.getValue(AppParameters.KEY_IPC_REMOTE_ADDRESS, "invalid");
|
||||
|
||||
this.ipcChannel = getIPCInstance().createChannel(this.ipcRemoteId, AppParameters.getValue(AppParameters.KEY_IPC_REMOTE_ADDRESS, "invalid"));
|
||||
this.ipcChannel = getIPCInstance().createChannel(this.ipcRemoteId, AppParameters.getValue(AppParameters.KEY_IPC_REMOTE_POPOUT_CHANNEL, "invalid"));
|
||||
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
||||
}
|
||||
|
||||
getEventRegistries() : RegistryMap { return this.localRegistries; }
|
||||
getConstructorArguments() : any[] {
|
||||
return this.constructorArguments;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.sendIPCMessage("hello-popout", { version: __build.version });
|
||||
|
@ -63,39 +66,17 @@ class PopoutController extends EventControllerBase<"popout"> {
|
|||
return;
|
||||
}
|
||||
|
||||
if(this.getEventRegistries()) {
|
||||
const registries = this.getEventRegistries();
|
||||
const invalidIndex = tpayload.registries.findIndex(reg => !registries[reg]);
|
||||
if(invalidIndex !== -1) {
|
||||
console.error("Received miss matching event registry keys (missing %s)", tpayload.registries[invalidIndex]);
|
||||
this.callbackControllerHello("miss matching registry keys (locally)");
|
||||
}
|
||||
} else {
|
||||
let map = {};
|
||||
tpayload.registries.forEach(reg => map[reg] = new Registry());
|
||||
this.initializeRegistries(map);
|
||||
}
|
||||
|
||||
this.userData = tpayload.userData;
|
||||
this.constructorArguments = tpayload.constructorArguments;
|
||||
this.callbackControllerHello(tpayload.accepted ? true : tpayload.message || false);
|
||||
break;
|
||||
}
|
||||
|
||||
case "fire-event-callback":
|
||||
case "fire-event":
|
||||
/* handled by out base class */
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("Received unknown message type from controller: %s", type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getUserData() {
|
||||
return this.userData;
|
||||
}
|
||||
|
||||
doClose() {
|
||||
this.sendIPCMessage("invoke-modal-action", { action: "close" });
|
||||
}
|
||||
|
|
|
@ -5,14 +5,13 @@ import * as i18n from "../../../i18n/localize";
|
|||
import {AbstractModal, ModalRenderer} from "../../../ui/react-elements/ModalDefinitions";
|
||||
import {AppParameters} from "../../../settings";
|
||||
import {getPopoutController} from "./PopoutController";
|
||||
import {findPopoutHandler} from "../../../ui/react-elements/external-modal/PopoutRegistry";
|
||||
import {RegistryMap} from "../../../events";
|
||||
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";
|
||||
|
||||
if("__native_client_init_shared" in window) {
|
||||
(window as any).__native_client_init_shared(__webpack_require__);
|
||||
|
@ -20,7 +19,7 @@ if("__native_client_init_shared" in window) {
|
|||
|
||||
let modalRenderer: ModalRenderer;
|
||||
let modalInstance: AbstractModal;
|
||||
let modalClass: new (events: RegistryMap, userData: any) => AbstractModal;
|
||||
let modalClass: new (...args: any[]) => AbstractModal;
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "setup",
|
||||
|
@ -70,13 +69,13 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||
const modalTarget = AppParameters.getValue(AppParameters.KEY_MODAL_TARGET, "unknown");
|
||||
console.error("Loading modal class %s", modalTarget);
|
||||
try {
|
||||
const handler = findPopoutHandler(modalTarget);
|
||||
if(!handler) {
|
||||
const registeredModal = findRegisteredModal(modalTarget as any);
|
||||
if(!registeredModal) {
|
||||
loader.critical_error("Missing popout handler", "Handler " + modalTarget + " is missing.");
|
||||
throw "missing handler";
|
||||
}
|
||||
|
||||
modalClass = await handler.loadClass();
|
||||
modalClass = await registeredModal.classLoader();
|
||||
} catch(error) {
|
||||
loader.critical_error("Failed to load modal", "Lookup the console for more detail");
|
||||
console.error("Failed to load modal %s: %o", modalTarget, error);
|
||||
|
@ -89,7 +88,7 @@ loader.register_task(Stage.LOADED, {
|
|||
priority: 100,
|
||||
function: async () => {
|
||||
try {
|
||||
modalInstance = new modalClass(getPopoutController().getEventRegistries(), getPopoutController().getUserData());
|
||||
modalInstance = new modalClass(...getPopoutController().getConstructorArguments());
|
||||
modalRenderer.renderModal(modalInstance);
|
||||
} catch(error) {
|
||||
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions";
|
||||
|
||||
export interface PopoutHandler {
|
||||
name: string;
|
||||
loadClass: <T extends AbstractModal>() => Promise<any>;
|
||||
}
|
||||
|
||||
const registeredHandler: {[key: string]: PopoutHandler} = {};
|
||||
|
||||
export function findPopoutHandler(name: string) {
|
||||
return registeredHandler[name];
|
||||
}
|
||||
|
||||
function registerHandler(handler: PopoutHandler) {
|
||||
registeredHandler[handler.name] = handler;
|
||||
}
|
||||
|
||||
registerHandler({
|
||||
name: "video-viewer",
|
||||
loadClass: async () => await import("tc-shared/video-viewer/Renderer")
|
||||
});
|
||||
|
||||
|
||||
registerHandler({
|
||||
name: "conversation",
|
||||
loadClass: async () => await import("../../frames/side/PopoutConversationRenderer")
|
||||
});
|
||||
|
||||
|
||||
registerHandler({
|
||||
name: "css-editor",
|
||||
loadClass: async () => await import("tc-shared/ui/modal/css-editor/Renderer")
|
||||
});
|
||||
|
||||
registerHandler({
|
||||
name: "channel-tree",
|
||||
loadClass: async () => await import("tc-shared/ui/tree/popout/RendererModal")
|
||||
});
|
|
@ -13,7 +13,8 @@ html, body {
|
|||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
background: #212529;
|
||||
background: #19191b;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.container {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 {InternalModalContentRenderer} from "tc-shared/ui/react-elements/internal-modal/Renderer";
|
||||
import * as React from "react";
|
||||
|
||||
export interface ModalControlFunctions {
|
||||
|
@ -39,11 +39,13 @@ export class ClientModalRenderer implements ModalRenderer {
|
|||
}
|
||||
|
||||
renderModal(modal: AbstractModal | undefined) {
|
||||
if(this.currentModal === modal)
|
||||
if(this.currentModal === modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.titleChangeObserver.disconnect();
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
|
||||
this.currentModal = modal;
|
||||
ReactDOM.render(
|
||||
<InternalModalContentRenderer
|
||||
|
@ -71,8 +73,9 @@ export class ClientModalRenderer implements ModalRenderer {
|
|||
}
|
||||
|
||||
private updateTitle() {
|
||||
if(!this.titleContainer)
|
||||
if(!this.titleContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.titleElement.innerText = this.titleContainer.textContent;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class TitleRenderer {
|
|||
|
||||
this.modalInstance = instance;
|
||||
if(this.modalInstance) {
|
||||
ReactDOM.render(<>{this.modalInstance.title()}</>, this.htmlContainer);
|
||||
ReactDOM.render(<>{this.modalInstance.renderTitle()}</>, this.htmlContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,8 +65,9 @@ export class WebModalRenderer implements ModalRenderer {
|
|||
}
|
||||
|
||||
renderModal(modal: AbstractModal | undefined) {
|
||||
if(this.currentModal === modal)
|
||||
if(this.currentModal === modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentModal = modal;
|
||||
this.titleRenderer.setInstance(modal);
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {RegistryMap} from "../../../events";
|
||||
import "./Controller";
|
||||
import {ModalController} from "../../../ui/react-elements/ModalDefinitions"; /* we've to reference him here, else the client would not */
|
||||
import {ModalController, ModalOptions} from "../ModalDefinitions";
|
||||
|
||||
export type ControllerFactory = (modal: string, registryMap: RegistryMap, userData: any, uniqueModalId: string) => ModalController;
|
||||
export type ControllerFactory = (modalType: string, constructorArguments?: any[], options?: ModalOptions) => ModalController;
|
||||
let modalControllerFactory: ControllerFactory;
|
||||
|
||||
export function setExternalModalControllerFactory(factory: ControllerFactory) {
|
||||
modalControllerFactory = factory;
|
||||
}
|
||||
|
||||
export function spawnExternalModal<EventClass extends { [key: string]: any }>(modal: string, registryMap: RegistryMap, userData: any, uniqueModalId?: string) : ModalController {
|
||||
if(typeof modalControllerFactory === "undefined")
|
||||
export function spawnExternalModal<EventClass extends { [key: string]: any }>(modalType: string, constructorArguments?: any[], options?: ModalOptions) : ModalController {
|
||||
if(typeof modalControllerFactory === "undefined") {
|
||||
throw tr("No external modal factory has been set");
|
||||
}
|
||||
|
||||
return modalControllerFactory(modal, registryMap, userData, uniqueModalId);
|
||||
return modalControllerFactory(modalType, constructorArguments, options);
|
||||
}
|
|
@ -10,10 +10,14 @@ import {
|
|||
} 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<InstanceType extends InternalModal = InternalModal> implements ModalController {
|
||||
export class InternalModalController implements ModalController {
|
||||
readonly events: Registry<ModalEvents>;
|
||||
readonly modalInstance: InstanceType;
|
||||
|
||||
private readonly modalType: RegisteredModal<any>;
|
||||
private readonly constructorArguments: any[];
|
||||
private modalInstance: AbstractModal;
|
||||
|
||||
private initializedPromise: Promise<void>;
|
||||
|
||||
|
@ -21,10 +25,12 @@ export class InternalModalController<InstanceType extends InternalModal = Intern
|
|||
private refModal: React.RefObject<InternalModalRenderer>;
|
||||
private modalState_: ModalState = ModalState.HIDDEN;
|
||||
|
||||
constructor(instance: InstanceType) {
|
||||
this.modalInstance = instance;
|
||||
constructor(modalType: RegisteredModal<any>, constructorArguments: any[]) {
|
||||
this.modalType = modalType;
|
||||
this.constructorArguments = constructorArguments;
|
||||
|
||||
this.events = new Registry<ModalEvents>();
|
||||
this.initialize();
|
||||
this.initializedPromise = this.initialize();
|
||||
}
|
||||
|
||||
getOptions(): Readonly<ModalOptions> {
|
||||
|
@ -40,17 +46,19 @@ export class InternalModalController<InstanceType extends InternalModal = Intern
|
|||
return this.modalState_;
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
private async initialize() {
|
||||
this.refModal = React.createRef();
|
||||
this.domElement = document.createElement("div");
|
||||
|
||||
this.modalInstance = new (await this.modalType.classLoader())(...this.constructorArguments);
|
||||
console.error(this.modalInstance);
|
||||
const element = React.createElement(InternalModalRenderer, {
|
||||
ref: this.refModal,
|
||||
modal: this.modalInstance,
|
||||
onClose: () => this.destroy()
|
||||
});
|
||||
document.body.appendChild(this.domElement);
|
||||
this.initializedPromise = new Promise<void>(resolve => {
|
||||
await new Promise<void>(resolve => {
|
||||
ReactDOM.render(element, this.domElement, () => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
|
@ -59,10 +67,11 @@ export class InternalModalController<InstanceType extends InternalModal = Intern
|
|||
|
||||
async show() : Promise<void> {
|
||||
await this.initializedPromise;
|
||||
if(this.modalState_ === ModalState.DESTROYED)
|
||||
if(this.modalState_ === ModalState.DESTROYED) {
|
||||
throw tr("modal has been destroyed");
|
||||
else if(this.modalState_ === ModalState.SHOWN)
|
||||
} else if(this.modalState_ === ModalState.SHOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refModal.current?.setState({ show: true });
|
||||
this.modalState_ = ModalState.SHOWN;
|
||||
|
|
|
@ -211,6 +211,8 @@ html:root {
|
|||
|
||||
/* explicitly set the background color so the next element could use background-color: inherited; */
|
||||
background: var(--modal-content-background);
|
||||
|
||||
@include chat-scrollbar();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@ 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 = (props: {
|
||||
export const InternalModalContentRenderer = React.memo((props: {
|
||||
modal: AbstractModal,
|
||||
|
||||
onClose?: () => void,
|
||||
|
@ -18,6 +19,9 @@ export const InternalModalContentRenderer = (props: {
|
|||
|
||||
refContent?: React.Ref<HTMLDivElement>
|
||||
}) => {
|
||||
const body = useMemo(() => props.modal.renderBody(), [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}>
|
||||
|
@ -26,7 +30,7 @@ export const InternalModalContentRenderer = (props: {
|
|||
</div>
|
||||
<div className={cssStyle.title + " " + props.headerTitleClass}>
|
||||
<ErrorBoundary>
|
||||
{props.modal.title()}
|
||||
{title}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
{!props.onMinimize ? undefined : (
|
||||
|
@ -42,12 +46,12 @@ export const InternalModalContentRenderer = (props: {
|
|||
</div>
|
||||
<div className={cssStyle.body + " " + props.bodyClass}>
|
||||
<ErrorBoundary>
|
||||
{props.modal.renderBody()}
|
||||
{body}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export class InternalModalRenderer extends React.PureComponent<{ modal: AbstractModal, onClose: () => void }, { show: boolean }> {
|
||||
private readonly refModal = React.createRef<HTMLDivElement>();
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
import {IpcRegistryDescription, Registry} from "tc-shared/events";
|
||||
import {VideoViewerEvents} from "tc-shared/video-viewer/Definitions";
|
||||
import {ReactElement} from "react";
|
||||
import * as React from "react";
|
||||
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";
|
||||
|
||||
export type ModalType = "error" | "warning" | "info" | "none";
|
||||
export type ModalRenderType = "page" | "dialog";
|
||||
|
||||
export interface ModalOptions {
|
||||
/**
|
||||
* Unique modal id.
|
||||
*/
|
||||
uniqueId?: string,
|
||||
|
||||
/**
|
||||
* Destroy the modal if it has been closed.
|
||||
* If the value is `false` it *might* destroy the modal anyways.
|
||||
* Default: `true`.
|
||||
*/
|
||||
destroyOnClose?: boolean,
|
||||
|
||||
/**
|
||||
* Default size of the modal in pixel.
|
||||
* This value might or might not be respected.
|
||||
*/
|
||||
defaultSize?: { width: number, height: number },
|
||||
|
||||
/**
|
||||
* Determines if the modal is resizeable or now.
|
||||
* Some browsers might not support non resizeable modals.
|
||||
* Default: `both`
|
||||
*/
|
||||
resizeable?: "none" | "vertical" | "horizontal" | "both",
|
||||
|
||||
/**
|
||||
* If the modal should be popoutable.
|
||||
* Default: `false`
|
||||
*/
|
||||
popoutable?: boolean,
|
||||
|
||||
/**
|
||||
* The default popout state.
|
||||
* Default: `false`
|
||||
*/
|
||||
popedOut?: boolean
|
||||
}
|
||||
|
||||
export interface ModalFunctionController {
|
||||
minimize();
|
||||
supportMinimize() : boolean;
|
||||
|
||||
maximize();
|
||||
supportMaximize() : boolean;
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
export interface ModalEvents {
|
||||
"open": {},
|
||||
"close": {},
|
||||
|
||||
/* create is implicitly at object creation */
|
||||
"destroy": {}
|
||||
}
|
||||
|
||||
export enum ModalState {
|
||||
SHOWN,
|
||||
HIDDEN,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
export interface ModalController {
|
||||
getOptions() : Readonly<ModalOptions>;
|
||||
getEvents() : Registry<ModalEvents>;
|
||||
getState() : ModalState;
|
||||
|
||||
show() : Promise<void>;
|
||||
hide() : Promise<void>;
|
||||
|
||||
destroy();
|
||||
}
|
||||
|
||||
export abstract class AbstractModal {
|
||||
protected constructor() {}
|
||||
|
||||
abstract renderBody() : ReactElement;
|
||||
abstract renderTitle() : string | React.ReactElement;
|
||||
|
||||
/* only valid for the "inline" modals */
|
||||
type() : ModalType { return "none"; }
|
||||
color() : "none" | "blue" { return "none"; }
|
||||
verticalAlignment() : "top" | "center" | "bottom" { return "center"; }
|
||||
|
||||
protected onInitialize() {}
|
||||
protected onDestroy() {}
|
||||
|
||||
protected onClose() {}
|
||||
protected onOpen() {}
|
||||
}
|
||||
|
||||
|
||||
export interface ModalRenderer {
|
||||
renderModal(modal: AbstractModal | undefined);
|
||||
}
|
||||
|
||||
export interface ModalConstructorArguments {
|
||||
"video-viewer": [
|
||||
/* events */ IpcRegistryDescription<VideoViewerEvents>,
|
||||
/* handlerId */ string,
|
||||
],
|
||||
"channel-edit": [
|
||||
/* events */ IpcRegistryDescription<ChannelEditEvents>,
|
||||
/* isChannelCreate */ boolean
|
||||
],
|
||||
"echo-test": [
|
||||
/* events */ IpcRegistryDescription<EchoTestEvents>
|
||||
],
|
||||
"global-settings-editor": [
|
||||
/* events */ IpcRegistryDescription<ModalGlobalSettingsEditorEvents>
|
||||
],
|
||||
"conversation": any,
|
||||
"css-editor": any,
|
||||
"channel-tree": any,
|
||||
"modal-connect": any
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions";
|
||||
import {ModalConstructorArguments} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
export interface RegisteredModal<T extends keyof ModalConstructorArguments> {
|
||||
modalId: T,
|
||||
classLoader: () => Promise<new (...args: ModalConstructorArguments[T]) => AbstractModal>,
|
||||
popoutSupported: boolean
|
||||
}
|
||||
|
||||
const registeredModals: {
|
||||
[T in keyof ModalConstructorArguments]?: RegisteredModal<T>
|
||||
} = {};
|
||||
|
||||
export function findRegisteredModal<T extends keyof ModalConstructorArguments>(name: T) : RegisteredModal<T> | undefined {
|
||||
return registeredModals[name] as any;
|
||||
}
|
||||
|
||||
function registerModal<T extends keyof ModalConstructorArguments>(modal: RegisteredModal<T>) {
|
||||
registeredModals[modal.modalId] = modal as any;
|
||||
}
|
||||
|
||||
registerModal({
|
||||
modalId: "video-viewer",
|
||||
classLoader: async () => await import("tc-shared/video-viewer/Renderer"),
|
||||
popoutSupported: true
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "channel-edit",
|
||||
classLoader: async () => await import("tc-shared/ui/modal/channel-edit/Renderer"),
|
||||
popoutSupported: false /* TODO: Needs style fixing */
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "echo-test",
|
||||
classLoader: async () => await import("tc-shared/ui/modal/echo-test/Renderer"),
|
||||
popoutSupported: false /* TODO: Needs style fixing */
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "global-settings-editor",
|
||||
classLoader: async () => await import("tc-shared/ui/modal/global-settings-editor/Renderer"),
|
||||
popoutSupported: true
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "conversation",
|
||||
classLoader: async () => await import("../../frames/side/PopoutConversationRenderer"),
|
||||
popoutSupported: true
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "css-editor",
|
||||
classLoader: async () => await import("tc-shared/ui/modal/css-editor/Renderer"),
|
||||
popoutSupported: true
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "channel-tree",
|
||||
classLoader: async () => await import("tc-shared/ui/tree/popout/RendererModal"),
|
||||
popoutSupported: true
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "modal-connect",
|
||||
classLoader: async () => await import("tc-shared/ui/modal/connect/Renderer"),
|
||||
popoutSupported: true
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import {
|
||||
AbstractModal,
|
||||
ModalFunctionController,
|
||||
ModalOptions,
|
||||
ModalRenderType
|
||||
} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {useContext} from "react";
|
||||
|
||||
const ControllerContext = useContext<ModalRendererController>(undefined);
|
||||
|
||||
interface RendererControllerEvents {
|
||||
|
||||
}
|
||||
|
||||
export class ModalRendererController {
|
||||
readonly renderType: ModalRenderType;
|
||||
readonly modal: AbstractModal;
|
||||
|
||||
constructor(renderType: ModalRenderType, modal: AbstractModal,) {
|
||||
this.renderType = renderType;
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
setShown(shown: boolean) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const ModalRenderer = (props: {
|
||||
mode: "page" | "dialog",
|
||||
modal: AbstractModal,
|
||||
modalOptions: ModalOptions,
|
||||
modalActions: ModalFunctionController
|
||||
}) => {
|
||||
|
||||
}
|
||||
|
||||
const ModalRendererDialog = (props: {
|
||||
modal: AbstractModal,
|
||||
modalOptions: ModalOptions,
|
||||
modalActions: ModalFunctionController
|
||||
}) => {
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import {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";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 () => modalClass
|
||||
}, args);
|
||||
}
|
||||
|
||||
export function spawnInternalModal<T extends keyof ModalConstructorArguments>(modal: T, constructorArguments: ModalConstructorArguments[T], options?: ModalOptions) : InternalModalController {
|
||||
return new InternalModalController(findRegisteredModal(modal), constructorArguments);
|
||||
}
|
|
@ -50,8 +50,13 @@ html:root {
|
|||
max-width: 100%; /* important for the repetitive channel name! */
|
||||
overflow-x: hidden;
|
||||
|
||||
&.align-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&.align-right {
|
||||
justify-content: right;
|
||||
justify-content: flex-end;
|
||||
padding-right: .25em;
|
||||
}
|
||||
|
||||
&.align-center, &.align-repetitive {
|
||||
|
|
|
@ -247,7 +247,7 @@ class ChannelTreeController {
|
|||
this.channelTree.client.groups.events.on("notify_groups_received", this.groupsReceivedListener);
|
||||
this.initializeServerEvents(this.channelTree.server);
|
||||
|
||||
this.channelTree.events.register_handler(this);
|
||||
this.channelTree.events.registerHandler(this);
|
||||
|
||||
if(this.channelTree.channelsInitialized) {
|
||||
this.handleChannelListReceived();
|
||||
|
@ -261,7 +261,7 @@ class ChannelTreeController {
|
|||
this.channelTree.client.groups.events.off("notify_groups_received", this.groupsReceivedListener);
|
||||
this.finalizeEvents(this.channelTree.server);
|
||||
|
||||
this.channelTree.events.unregister_handler(this);
|
||||
this.channelTree.events.unregisterHandler(this);
|
||||
Object.values(this.eventListeners).forEach(callbacks => callbacks.forEach(callback => callback()));
|
||||
this.eventListeners = {};
|
||||
}
|
||||
|
|
|
@ -338,7 +338,7 @@ export class RDPChannelTree {
|
|||
}
|
||||
|
||||
initialize() {
|
||||
this.events.register_handler(this);
|
||||
this.events.registerHandler(this);
|
||||
|
||||
const events = this.registeredEventHandlers;
|
||||
|
||||
|
@ -466,7 +466,7 @@ export class RDPChannelTree {
|
|||
document.removeEventListener("focusout", this.documentDragStopListener);
|
||||
document.removeEventListener("mouseout", this.documentDragStopListener);
|
||||
|
||||
this.events.unregister_handler(this);
|
||||
this.events.unregisterHandler(this);
|
||||
this.registeredEventHandlers.forEach(callback => callback());
|
||||
this.registeredEventHandlers = [];
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
||||
import {initializeChannelTreeController} from "tc-shared/ui/tree/Controller";
|
||||
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions";
|
||||
import {initializePopoutControlBarController} from "tc-shared/ui/frames/control-bar/Controller";
|
||||
import {ChannelTree} from "tc-shared/tree/ChannelTree";
|
||||
import {ModalController} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||
import {ChannelTreePopoutEvents} from "tc-shared/ui/tree/popout/Definitions";
|
||||
import {ChannelTreePopoutConstructorArguments, ChannelTreePopoutEvents} from "tc-shared/ui/tree/popout/Definitions";
|
||||
import {ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {tr, tra} from "tc-shared/i18n/localize";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
|
||||
export class ChannelTreePopoutController {
|
||||
readonly channelTree: ChannelTree;
|
||||
|
@ -58,11 +58,15 @@ export class ChannelTreePopoutController {
|
|||
this.controlBarEvents = new Registry<ControlBarEvents>();
|
||||
initializePopoutControlBarController(this.controlBarEvents, this.channelTree.client);
|
||||
|
||||
this.popoutInstance = spawnExternalModal("channel-tree", {
|
||||
tree: this.treeEvents,
|
||||
controlBar: this.controlBarEvents,
|
||||
base: this.uiEvents
|
||||
}, { handlerId: this.channelTree.client.handlerId }, "channel-tree-" + this.channelTree.client.handlerId);
|
||||
this.popoutInstance = spawnModal("channel-tree", [{
|
||||
events: this.uiEvents.generateIpcDescription(),
|
||||
eventsTree: this.treeEvents.generateIpcDescription(),
|
||||
eventsControlBar: this.controlBarEvents.generateIpcDescription(),
|
||||
handlerId: this.channelTree.client.handlerId
|
||||
} as ChannelTreePopoutConstructorArguments], {
|
||||
uniqueId: "channel-tree-" + this.channelTree.client.handlerId,
|
||||
popedOut: true
|
||||
});
|
||||
|
||||
this.popoutInstance.getEvents().one("destroy", () => {
|
||||
this.treeEvents.fire("notify_destroy");
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
import {IpcRegistryDescription} from "tc-shared/events";
|
||||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions";
|
||||
|
||||
export interface ChannelTreePopoutEvents {
|
||||
query_title: {},
|
||||
notify_title: { title: string }
|
||||
}
|
||||
|
||||
export type ChannelTreePopoutConstructorArguments = {
|
||||
events: IpcRegistryDescription<ChannelTreePopoutEvents>,
|
||||
eventsTree: IpcRegistryDescription<ChannelTreeUIEvents>,
|
||||
eventsControlBar: IpcRegistryDescription<ControlBarEvents>,
|
||||
handlerId: string
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||
import {Registry, RegistryMap} from "tc-shared/events";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer";
|
||||
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions";
|
||||
import {ControlBar2} from "tc-shared/ui/frames/control-bar/Renderer";
|
||||
import {ChannelTreePopoutEvents} from "tc-shared/ui/tree/popout/Definitions";
|
||||
import {ChannelTreePopoutConstructorArguments, ChannelTreePopoutEvents} from "tc-shared/ui/tree/popout/Definitions";
|
||||
|
||||
const TitleRenderer = (props: { events: Registry<ChannelTreePopoutEvents> }) => {
|
||||
const [ title, setTitle ] = useState<string>(() => {
|
||||
|
@ -26,13 +26,13 @@ class ChannelTreeModal extends AbstractModal {
|
|||
|
||||
readonly handlerId: string;
|
||||
|
||||
constructor(registryMap: RegistryMap, userData: any) {
|
||||
constructor(info: ChannelTreePopoutConstructorArguments) {
|
||||
super();
|
||||
|
||||
this.handlerId = userData.handlerId;
|
||||
this.eventsUI = registryMap["base"] as any;
|
||||
this.eventsTree = registryMap["tree"] as any;
|
||||
this.eventsControlBar = registryMap["controlBar"] as any;
|
||||
this.handlerId = info.handlerId;
|
||||
this.eventsUI = Registry.fromIpcDescription(info.events);
|
||||
this.eventsTree = Registry.fromIpcDescription(info.eventsTree);
|
||||
this.eventsControlBar = Registry.fromIpcDescription(info.eventsControlBar);
|
||||
|
||||
this.eventsUI.fire("query_title");
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ class ChannelTreeModal extends AbstractModal {
|
|||
)
|
||||
}
|
||||
|
||||
title(): React.ReactElement {
|
||||
renderTitle(): React.ReactElement {
|
||||
return <TitleRenderer events={this.eventsUI} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import * as loader from "tc-loader";
|
||||
|
||||
const getUrlParameter = key => {
|
||||
const match = location.search.match(new RegExp("(.*[?&]|^)" + key + "=([^&]+)($|&.*)"));
|
||||
if(!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return match[2];
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure that the module has been loaded within the main application and not
|
||||
* within a popout.
|
||||
*/
|
||||
export function assertMainApplication() {
|
||||
/* TODO: get this directly from the loader itself */
|
||||
if((getUrlParameter("loader-target") || "app") !== "app") {
|
||||
debugger;
|
||||
loader.critical_error("Invalid module context", "Module only available in the main app context");
|
||||
throw "invalid module context";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
import {UiVariableConsumer, UiVariableMap, UiVariableProvider} from "tc-shared/ui/utils/Variable";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
|
||||
class IpcUiVariableProvider<Variables extends UiVariableMap> extends UiVariableProvider<Variables> {
|
||||
readonly ipcChannelId: string;
|
||||
private broadcastChannel: BroadcastChannel;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.ipcChannelId = "teaspeak-ipc-vars-" + guid();
|
||||
this.broadcastChannel = new BroadcastChannel(this.ipcChannelId);
|
||||
this.broadcastChannel.onmessage = event => this.handleIpcMessage(event.data, event.source, event.origin);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
|
||||
if(this.broadcastChannel) {
|
||||
this.broadcastChannel.onmessage = undefined;
|
||||
this.broadcastChannel.onmessageerror = undefined;
|
||||
this.broadcastChannel.close();
|
||||
}
|
||||
|
||||
this.broadcastChannel = undefined;
|
||||
}
|
||||
|
||||
protected doSendVariable(variable: string, customData: any, value: any) {
|
||||
this.broadcastChannel.postMessage({
|
||||
type: "notify",
|
||||
|
||||
variable,
|
||||
customData,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
private handleIpcMessage(message: any, source: MessageEventSource | null, origin: string) {
|
||||
if(message.type === "edit") {
|
||||
const token = message.token;
|
||||
const sendResult = (error?: any) => {
|
||||
if(source) {
|
||||
// @ts-ignore
|
||||
source.postMessage({
|
||||
type: "edit-result",
|
||||
token,
|
||||
error
|
||||
});
|
||||
} else {
|
||||
this.broadcastChannel.postMessage({
|
||||
type: "edit-result",
|
||||
token,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = this.doEditVariable(message.variable, message.customData, message.newValue);
|
||||
if(result instanceof Promise) {
|
||||
result.then(sendResult)
|
||||
.catch(error => {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to edit variable %s: %o"), message.variable, error);
|
||||
sendResult(tr("invoke error"));
|
||||
});
|
||||
} else {
|
||||
sendResult();
|
||||
}
|
||||
} catch (error) {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to edit variable %s: %o"), message.variable, error);
|
||||
sendResult(tr("invoke error"));
|
||||
}
|
||||
} else if(message.type === "query") {
|
||||
this.sendVariable(message.variable, message.customData, true);
|
||||
}
|
||||
}
|
||||
|
||||
generateConsumerDescription() : IpcVariableDescriptor<Variables> {
|
||||
return {
|
||||
ipcChannelId: this.ipcChannelId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type IpcVariableDescriptor<Variables extends UiVariableMap> = {
|
||||
readonly ipcChannelId: string
|
||||
}
|
||||
|
||||
let editTokenIndex = 0;
|
||||
class IpcUiVariableConsumer<Variables extends UiVariableMap> extends UiVariableConsumer<Variables> {
|
||||
readonly description: IpcVariableDescriptor<Variables>;
|
||||
private broadcastChannel: BroadcastChannel;
|
||||
private editListener: {[key: string]: { resolve: () => void, reject: (error) => void }};
|
||||
|
||||
constructor(description: IpcVariableDescriptor<Variables>) {
|
||||
super();
|
||||
this.description = description;
|
||||
this.editListener = {};
|
||||
|
||||
this.broadcastChannel = new BroadcastChannel(this.description.ipcChannelId);
|
||||
this.broadcastChannel.onmessage = event => this.handleIpcMessage(event.data, event.source);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
|
||||
if(this.broadcastChannel) {
|
||||
this.broadcastChannel.onmessage = undefined;
|
||||
this.broadcastChannel.onmessageerror = undefined;
|
||||
this.broadcastChannel.close();
|
||||
}
|
||||
|
||||
this.broadcastChannel = undefined;
|
||||
|
||||
Object.values(this.editListener).forEach(listener => listener.reject(tr("consumer destroyed")));
|
||||
this.editListener = {};
|
||||
}
|
||||
|
||||
protected doEditVariable(variable: string, customData: any, newValue: any): Promise<void> | void {
|
||||
const token = "t" + ++editTokenIndex;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.broadcastChannel.postMessage({
|
||||
type: "edit",
|
||||
token,
|
||||
variable,
|
||||
customData,
|
||||
newValue
|
||||
});
|
||||
|
||||
this.editListener[token] = {
|
||||
reject,
|
||||
resolve
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected doRequestVariable(variable: string, customData: any) {
|
||||
this.broadcastChannel.postMessage({
|
||||
type: "query",
|
||||
variable,
|
||||
customData,
|
||||
});
|
||||
}
|
||||
|
||||
private handleIpcMessage(message: any, _source: MessageEventSource | null) {
|
||||
if(message.type === "notify") {
|
||||
console.error("Received notify %s", message.variable);
|
||||
this.notifyRemoteVariable(message.variable, message.customData, message.value);
|
||||
} else if(message.type === "edit-result") {
|
||||
const payload = this.editListener[message.token];
|
||||
if(!payload) {
|
||||
return;
|
||||
}
|
||||
delete this.editListener[message.token];
|
||||
|
||||
if(typeof message.error !== "undefined") {
|
||||
payload.reject(message.error);
|
||||
} else {
|
||||
payload.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createIpcUiVariableProvider<Variables extends UiVariableMap>() : IpcUiVariableProvider<Variables> {
|
||||
return new IpcUiVariableProvider();
|
||||
}
|
||||
|
||||
export function createIpcUiVariableConsumer<Variables extends UiVariableMap>(description: IpcVariableDescriptor<Variables>) : IpcUiVariableConsumer<Variables> {
|
||||
return new IpcUiVariableConsumer<Variables>(description);
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import {UiVariableConsumer, UiVariableMap, UiVariableProvider} from "tc-shared/ui/utils/Variable";
|
||||
|
||||
class LocalUiVariableProvider<Variables extends UiVariableMap> extends UiVariableProvider<Variables> {
|
||||
private consumer: LocalUiVariableConsumer<Variables>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.consumer = undefined;
|
||||
}
|
||||
|
||||
setConsumer(consumer: LocalUiVariableConsumer<Variables>) {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
protected doSendVariable(variable: string, customData: any, value: any) {
|
||||
this.consumer.notifyRemoteVariable(variable, customData, value);
|
||||
}
|
||||
|
||||
public doEditVariable(variable: string, customData: any, newValue: any): Promise<void> | void {
|
||||
return super.doEditVariable(variable, customData, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
class LocalUiVariableConsumer<Variables extends UiVariableMap> extends UiVariableConsumer<Variables> {
|
||||
private provider: LocalUiVariableProvider<Variables>;
|
||||
|
||||
constructor(provider: LocalUiVariableProvider<Variables>) {
|
||||
super();
|
||||
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.provider = undefined;
|
||||
}
|
||||
|
||||
protected doEditVariable(variable: string, customData: any, value: any): Promise<void> | void {
|
||||
return this.provider.doEditVariable(variable, customData, value);
|
||||
}
|
||||
|
||||
protected doRequestVariable(variable: string, customData: any) {
|
||||
return this.provider.sendVariable(variable, customData);
|
||||
}
|
||||
|
||||
public notifyRemoteVariable(variable: string, customData: any, value: any) {
|
||||
super.notifyRemoteVariable(variable, customData, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function createLocalUiVariables<Variables extends UiVariableMap>() : [UiVariableProvider<Variables>, UiVariableConsumer<Variables>] {
|
||||
const provider = new LocalUiVariableProvider();
|
||||
const consumer = new LocalUiVariableConsumer(provider);
|
||||
provider.setConsumer(consumer);
|
||||
return [provider as any, consumer as any];
|
||||
}
|
|
@ -0,0 +1,403 @@
|
|||
import {useEffect, useState} from "react";
|
||||
import * as _ from "lodash";
|
||||
|
||||
/*
|
||||
* To deliver optimized performance, we only promisify the values we need.
|
||||
* If done so, we have the change to instantaneously load local values without needing
|
||||
* to rerender the ui.
|
||||
*/
|
||||
|
||||
export type UiVariable = Transferable | undefined | null | number | string | object;
|
||||
export type UiVariableMap = { [key: string]: any }; //UiVariable | Readonly<UiVariable>
|
||||
|
||||
type IfEquals<X, Y, A=X, B=never> =
|
||||
(<T>() => T extends X ? 1 : 2) extends
|
||||
(<T>() => T extends Y ? 1 : 2) ? A : B;
|
||||
|
||||
type WritableKeys<T> = {
|
||||
[P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>
|
||||
}[keyof T];
|
||||
|
||||
type ReadonlyKeys<T> = {
|
||||
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>
|
||||
}[keyof T];
|
||||
|
||||
export type ReadonlyVariables<Variables extends UiVariableMap> = Pick<Variables, ReadonlyKeys<Variables>>
|
||||
export type WriteableVariables<Variables extends UiVariableMap> = Pick<Variables, WritableKeys<Variables>>
|
||||
|
||||
type UiVariableEditor<Variables extends UiVariableMap, T extends keyof Variables> = Variables[T] extends { __readonly } ?
|
||||
never :
|
||||
(newValue: Variables[T], customData: any) => Variables[T] | void | boolean;
|
||||
|
||||
type UiVariableEditorPromise<Variables extends UiVariableMap, T extends keyof Variables> = Variables[T] extends { __readonly } ?
|
||||
never :
|
||||
(newValue: Variables[T], customData: any) => Promise<Variables[T] | void | boolean>;
|
||||
|
||||
export abstract class UiVariableProvider<Variables extends UiVariableMap> {
|
||||
private variableProvider: {[key: string]: (customData: any) => any | Promise<any>} = {};
|
||||
private variableEditor: {[key: string]: (newValue, customData: any) => any | Promise<any>} = {};
|
||||
|
||||
protected constructor() { }
|
||||
|
||||
destroy() { }
|
||||
|
||||
setVariableProvider<T extends keyof Variables>(variable: T, provider: (customData: any) => Variables[T] | Promise<Variables[T]>) {
|
||||
this.variableProvider[variable as any] = provider;
|
||||
}
|
||||
|
||||
setVariableEditor<T extends keyof Variables>(variable: T, editor: UiVariableEditor<Variables, T>) {
|
||||
this.variableEditor[variable as any] = editor;
|
||||
}
|
||||
|
||||
setVariableEditorAsync<T extends keyof Variables>(variable: T, editor: UiVariableEditorPromise<Variables, T>) {
|
||||
this.variableEditor[variable as any] = editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send/update a variable
|
||||
* @param variable The target variable to send.
|
||||
* @param customData
|
||||
* @param forceSend If `true` the variable will be send event though it hasn't changed.
|
||||
*/
|
||||
sendVariable<T extends keyof Variables>(variable: T, customData?: any, forceSend?: boolean) : void | Promise<void> {
|
||||
const providers = this.variableProvider[variable as any];
|
||||
if(!providers) {
|
||||
throw tra("missing provider for {}", variable);
|
||||
}
|
||||
|
||||
const result = providers(customData);
|
||||
if(result instanceof Promise) {
|
||||
return result
|
||||
.then(result => this.doSendVariable(variable as any, customData, result))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
} else {
|
||||
this.doSendVariable(variable as any, customData, result);
|
||||
}
|
||||
}
|
||||
|
||||
async getVariable<T extends keyof Variables>(variable: T, customData?: any, ignoreCache?: boolean) : Promise<Variables[T]> {
|
||||
const providers = this.variableProvider[variable as any];
|
||||
if(!providers) {
|
||||
throw tra("missing provider for {}", variable);
|
||||
}
|
||||
|
||||
const result = providers(customData);
|
||||
if(result instanceof Promise) {
|
||||
return await result;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
getVariableSync<T extends keyof Variables>(variable: T, customData?: any, ignoreCache?: boolean) : Variables[T] {
|
||||
const providers = this.variableProvider[variable as any];
|
||||
if(!providers) {
|
||||
throw tr("missing provider");
|
||||
}
|
||||
|
||||
const result = providers(customData);
|
||||
if(result instanceof Promise) {
|
||||
throw tr("tried to get an async variable synchronous");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected resolveVariable(variable: string, customData: any): Promise<any> | any {
|
||||
const providers = this.variableProvider[variable];
|
||||
if(!providers) {
|
||||
throw tr("missing provider");
|
||||
}
|
||||
|
||||
return providers(customData);
|
||||
}
|
||||
|
||||
protected doEditVariable(variable: string, customData: any, newValue: any) : Promise<void> | void {
|
||||
const editor = this.variableEditor[variable];
|
||||
if(!editor) {
|
||||
throw tr("variable is read only");
|
||||
}
|
||||
|
||||
const handleEditResult = result => {
|
||||
if(typeof result === "undefined") {
|
||||
/* change succeeded, no need to notify any variable since the consumer already has the newest value */
|
||||
} else if(result === true || result === false) {
|
||||
/* The new variable has been accepted/rejected and the variable should be updated on the remote side. */
|
||||
/* TODO: Use cached value if the result is `false` */
|
||||
this.sendVariable(variable, customData, true);
|
||||
} else {
|
||||
/* The new value hasn't been accepted. Instead a new value has been returned. */
|
||||
this.doSendVariable(variable, customData, result);
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditError = error => {
|
||||
console.error("Failed to change variable %s: %o", variable, error);
|
||||
this.sendVariable(variable, customData, true);
|
||||
}
|
||||
|
||||
try {
|
||||
let result = editor(newValue, customData);
|
||||
if(result instanceof Promise) {
|
||||
return result.then(handleEditResult).catch(handleEditError);
|
||||
} else {
|
||||
handleEditResult(result);
|
||||
}
|
||||
} catch (error) {
|
||||
handleEditError(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract doSendVariable(variable: string, customData: any, value: any);
|
||||
}
|
||||
|
||||
export type UiVariableStatus<Variables extends UiVariableMap, T extends keyof Variables> = {
|
||||
status: "loading",
|
||||
|
||||
localValue: Variables[T] | undefined,
|
||||
remoteValue: undefined,
|
||||
|
||||
/* Will do nothing */
|
||||
setValue: (newValue: Variables[T], localOnly?: boolean) => void
|
||||
} | {
|
||||
status: "loaded" | "applying",
|
||||
|
||||
localValue: Variables[T],
|
||||
remoteValue: Variables[T],
|
||||
|
||||
setValue: (newValue: Variables[T], localOnly?: boolean) => void
|
||||
};
|
||||
|
||||
export type UiReadOnlyVariableStatus<Variables extends UiVariableMap, T extends keyof Variables> = {
|
||||
status: "loading" | "loaded",
|
||||
value: Variables[T],
|
||||
};
|
||||
|
||||
type UiVariableCacheEntry = {
|
||||
key: string,
|
||||
useCount: number,
|
||||
customData: any | undefined,
|
||||
currentValue: any | undefined,
|
||||
status: "loading" | "loaded" | "applying",
|
||||
updateListener: ((clearLocalValue: boolean) => void)[]
|
||||
}
|
||||
|
||||
type LocalVariableValue = {
|
||||
status: "set" | "default",
|
||||
value: any
|
||||
} | {
|
||||
status: "unset"
|
||||
}
|
||||
|
||||
let staticRevisionId = 0;
|
||||
export abstract class UiVariableConsumer<Variables extends UiVariableMap> {
|
||||
private variableCache: {[key: string]: UiVariableCacheEntry[]} = {};
|
||||
|
||||
destroy() {
|
||||
this.variableCache = {};
|
||||
}
|
||||
|
||||
private getOrCreateVariable<T extends keyof Variables>(
|
||||
variable: string,
|
||||
customData?: any
|
||||
) : UiVariableCacheEntry {
|
||||
let cacheEntry = this.variableCache[variable]?.find(variable => _.isEqual(variable.customData, customData));
|
||||
if(!cacheEntry) {
|
||||
this.variableCache[variable] = this.variableCache[variable] || [];
|
||||
this.variableCache[variable].push(cacheEntry = {
|
||||
key: variable,
|
||||
customData,
|
||||
currentValue: undefined,
|
||||
status: "loading",
|
||||
useCount: 0,
|
||||
updateListener: []
|
||||
});
|
||||
|
||||
/* Might already call notifyRemoteVariable */
|
||||
this.doRequestVariable(variable, customData);
|
||||
}
|
||||
return cacheEntry;
|
||||
}
|
||||
|
||||
private derefVariable(variable: UiVariableCacheEntry) {
|
||||
if(--variable.useCount === 0) {
|
||||
const cache = this.variableCache[variable.key];
|
||||
if(!cache) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.remove(variable);
|
||||
if(cache.length === 0) {
|
||||
delete this.variableCache[variable.key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useVariable<T extends keyof WriteableVariables<Variables>>(
|
||||
variable: T,
|
||||
customData?: any,
|
||||
defaultValue?: Variables[T]
|
||||
) : UiVariableStatus<Variables, T> {
|
||||
const haveDefaultValue = arguments.length >= 3;
|
||||
const cacheEntry = this.getOrCreateVariable(variable as string, customData);
|
||||
|
||||
const [ localValue, setLocalValue ] = useState<LocalVariableValue>(() => {
|
||||
/* Variable constructor */
|
||||
cacheEntry.useCount++;
|
||||
|
||||
if(cacheEntry.status === "loading") {
|
||||
return {
|
||||
status: "set",
|
||||
value: cacheEntry.currentValue
|
||||
};
|
||||
} else if(haveDefaultValue) {
|
||||
return {
|
||||
status: "default",
|
||||
value: defaultValue
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: "unset"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const [, setRemoteVersion ] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
/* Initial rendered */
|
||||
if(cacheEntry.status === "loaded" && localValue.status !== "set") {
|
||||
/* Update the local value to the current state */
|
||||
setLocalValue(cacheEntry.currentValue);
|
||||
}
|
||||
|
||||
let listener;
|
||||
cacheEntry.updateListener.push(listener = clearLocalValue => {
|
||||
if(clearLocalValue) {
|
||||
setLocalValue({ status: "unset" });
|
||||
}
|
||||
|
||||
/* We can't just increment the old one by one since this update listener may fires twice before rendering */
|
||||
setRemoteVersion(++staticRevisionId);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cacheEntry.updateListener.remove(listener);
|
||||
this.derefVariable(cacheEntry);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if(cacheEntry.status === "loading") {
|
||||
return {
|
||||
status: "loading",
|
||||
localValue: localValue.status === "unset" ? undefined : localValue.value,
|
||||
remoteValue: undefined,
|
||||
setValue: () => {}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: cacheEntry.status,
|
||||
|
||||
localValue: localValue.status === "set" ? localValue.value : cacheEntry.currentValue,
|
||||
remoteValue: cacheEntry.currentValue,
|
||||
|
||||
setValue: (newValue, localOnly) => {
|
||||
if(!localOnly && !_.isEqual(cacheEntry.currentValue, newValue)) {
|
||||
const editingFinished = (succeeded: boolean) => {
|
||||
if(cacheEntry.status !== "applying") {
|
||||
/* A new value has already been emitted */
|
||||
return;
|
||||
}
|
||||
|
||||
cacheEntry.status = "loaded";
|
||||
cacheEntry.currentValue = succeeded ? newValue : cacheEntry.currentValue;
|
||||
cacheEntry.updateListener.forEach(callback => callback(true));
|
||||
};
|
||||
|
||||
cacheEntry.status = "applying";
|
||||
const result = this.doEditVariable(variable as string, customData, newValue);
|
||||
if(result instanceof Promise) {
|
||||
result
|
||||
.then(() => editingFinished(true))
|
||||
.catch(async error => {
|
||||
console.error("Failed to change variable %s: %o", variable, error);
|
||||
editingFinished(false);
|
||||
});
|
||||
|
||||
/* State has changed, enforce a rerender */
|
||||
cacheEntry.updateListener.forEach(callback => callback(false));
|
||||
} else {
|
||||
editingFinished(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(localValue.status !== "set" || !_.isEqual(newValue, localValue.value)) {
|
||||
setLocalValue({
|
||||
status: "set",
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
useReadOnly<T extends keyof Variables>(
|
||||
variable: T,
|
||||
customData?: any,
|
||||
defaultValue?: never
|
||||
) : UiReadOnlyVariableStatus<Variables, T>;
|
||||
|
||||
useReadOnly<T extends keyof Variables>(
|
||||
variable: T,
|
||||
customData: any | undefined,
|
||||
defaultValue: Variables[T]
|
||||
) : Variables[T];
|
||||
|
||||
useReadOnly(variable, customData?, defaultValue?) {
|
||||
const cacheEntry = this.getOrCreateVariable(variable as string, customData);
|
||||
const [, setRemoteVersion ] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
/* Initial rendered */
|
||||
cacheEntry.useCount++;
|
||||
|
||||
let listener;
|
||||
cacheEntry.updateListener.push(listener = () => {
|
||||
/* We can't just increment the old one by one since this update listener may fires twice before rendering */
|
||||
setRemoteVersion(++staticRevisionId);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cacheEntry.updateListener.remove(listener);
|
||||
this.derefVariable(cacheEntry);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if(arguments.length >= 3) {
|
||||
return cacheEntry.status === "loaded" ? cacheEntry.currentValue : defaultValue;
|
||||
} else {
|
||||
return {
|
||||
status: cacheEntry.status,
|
||||
value: cacheEntry.currentValue
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected notifyRemoteVariable(variable: string, customData: any | undefined, value: any) {
|
||||
let cacheEntry = this.variableCache[variable]?.find(variable => _.isEqual(variable.customData, customData));
|
||||
if(!cacheEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
cacheEntry.status = "loaded";
|
||||
cacheEntry.currentValue = value;
|
||||
cacheEntry.updateListener.forEach(callback => callback(true));
|
||||
}
|
||||
|
||||
protected abstract doRequestVariable(variable: string, customData: any | undefined);
|
||||
protected abstract doEditVariable(variable: string, customData: any | undefined, value: any) : Promise<void> | void;
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import * as log from "../log";
|
||||
import {LogCategory, logError, logWarn} from "../log";
|
||||
import {spawnExternalModal} from "../ui/react-elements/external-modal";
|
||||
import {EventHandler, Registry} from "../events";
|
||||
import {VideoViewerEvents} from "./Definitions";
|
||||
import {ConnectionHandler} from "../ConnectionHandler";
|
||||
|
@ -11,6 +9,7 @@ import {createErrorModal} from "../ui/elements/Modal";
|
|||
import {ModalController} from "../ui/react-elements/ModalDefinitions";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import { tr, tra } from "tc-shared/i18n/localize";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
|
||||
const parseWatcherId = (id: string): { clientId: number, clientUniqueId: string} => {
|
||||
const [ clientIdString, clientUniqueId ] = id.split(" - ");
|
||||
|
@ -34,14 +33,16 @@ class VideoViewer {
|
|||
this.connection = connection;
|
||||
|
||||
this.events = new Registry<VideoViewerEvents>();
|
||||
this.events.register_handler(this);
|
||||
this.events.registerHandler(this);
|
||||
|
||||
this.plugin = connection.getPluginCmdRegistry().getPluginHandler<W2GPluginCmdHandler>(W2GPluginCmdHandler.kPluginChannel);
|
||||
if(!this.plugin) {
|
||||
throw tr("Missing video viewer plugin");
|
||||
}
|
||||
|
||||
this.modal = spawnExternalModal("video-viewer", { default: this.events }, { handlerId: connection.handlerId });
|
||||
this.modal = spawnModal("video-viewer", [ this.events.generateIpcDescription(), connection.handlerId ], {
|
||||
popedOut: true,
|
||||
});
|
||||
|
||||
this.registerPluginListeners();
|
||||
this.plugin.getCurrentWatchers().forEach(watcher => this.registerWatcherEvents(watcher));
|
||||
|
@ -57,7 +58,7 @@ class VideoViewer {
|
|||
this.plugin.setLocalPlayerClosed();
|
||||
|
||||
this.events.fire("notify_destroy");
|
||||
this.events.unregister_handler(this);
|
||||
this.events.unregisterHandler(this);
|
||||
|
||||
this.modal.destroy();
|
||||
this.events.destroy();
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
@import "../../css/static/properties";
|
||||
|
||||
$sidebar-width: 20em;
|
||||
.container {
|
||||
background: #19191b;
|
||||
|
||||
.outerContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
@ -15,13 +13,29 @@ $sidebar-width: 20em;
|
|||
min-height: 10em;
|
||||
min-width: 20em;
|
||||
|
||||
position: absolute;
|
||||
/* We're using the full with by default */
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #19191b;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,12 @@ import {LogCategory, logDebug, logTrace} from "tc-shared/log";
|
|||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import * as React from "react";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {Registry, RegistryMap} from "tc-shared/events";
|
||||
import {IpcRegistryDescription, Registry} from "tc-shared/events";
|
||||
import {PlayerStatus, VideoViewerEvents} from "./Definitions";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import ReactPlayer from 'react-player'
|
||||
import {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
|
||||
import "tc-shared/file/RemoteAvatars";
|
||||
import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar";
|
||||
import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
|
@ -491,25 +489,29 @@ class ModalVideoPopout extends AbstractModal {
|
|||
readonly events: Registry<VideoViewerEvents>;
|
||||
readonly handlerId: string;
|
||||
|
||||
constructor(registryMap: RegistryMap, userData: any) {
|
||||
constructor(events: IpcRegistryDescription<VideoViewerEvents>, handlerId: any) {
|
||||
super();
|
||||
|
||||
this.handlerId = userData.handlerId;
|
||||
this.events = registryMap["default"] as any;
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
this.handlerId = handlerId;
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement<Translatable> {
|
||||
renderTitle(): string | React.ReactElement<Translatable> {
|
||||
return <TitleRenderer events={this.events} />;
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return <div className={cssStyle.container} >
|
||||
return (
|
||||
<div className={cssStyle.outerContainer}>
|
||||
<div className={cssStyle.container} >
|
||||
<Sidebar events={this.events} handlerId={this.handlerId} />
|
||||
<ToggleSidebarButton events={this.events} />
|
||||
<div className={cssStyle.containerPlayer}>
|
||||
<PlayerController events={this.events} />
|
||||
</div>
|
||||
</div>;
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -423,11 +423,11 @@ export class W2GPluginCmdHandler extends PluginCmdHandler {
|
|||
private handleLocalWatcherEvent(event: Event<W2GWatcherEvents, "notify_watcher_url_changed" | "notify_watcher_status_changed" | "notify_destroyed">) {
|
||||
switch (event.type) {
|
||||
case "notify_watcher_url_changed":
|
||||
this.events.fire("notify_following_url", { newUrl: event.as<"notify_watcher_url_changed">().newVideo });
|
||||
this.events.fire("notify_following_url", { newUrl: event.asUnchecked("notify_watcher_url_changed").newVideo });
|
||||
break;
|
||||
|
||||
case "notify_watcher_status_changed":
|
||||
this.events.fire("notify_following_watcher_status", { newStatus: event.as<"notify_watcher_status_changed">().newStatus });
|
||||
this.events.fire("notify_following_watcher_status", { newStatus: event.asUnchecked("notify_watcher_status_changed").newStatus });
|
||||
break;
|
||||
|
||||
case "notify_destroyed":
|
||||
|
|
|
@ -13,8 +13,8 @@ export interface VideoSourceCapabilities {
|
|||
}
|
||||
|
||||
export interface VideoSourceInitialSettings {
|
||||
width: number,
|
||||
height: number,
|
||||
width: number | 0,
|
||||
height: number | 0,
|
||||
frameRate: number
|
||||
}
|
||||
|
||||
|
|
|
@ -4,23 +4,24 @@ import * as ipc from "tc-shared/ipc/BrowserIPC";
|
|||
import {ChannelMessage} from "tc-shared/ipc/BrowserIPC";
|
||||
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
|
||||
import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage";
|
||||
import {RegistryMap} from "tc-shared/events";
|
||||
import {tr, tra} from "tc-shared/i18n/localize";
|
||||
import {ModalOptions} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
export class ExternalModalController extends AbstractExternalModalController {
|
||||
private readonly uniqueModalId: string;
|
||||
private readonly options: ModalOptions;
|
||||
private currentWindow: Window;
|
||||
private windowClosedTestInterval: number = 0;
|
||||
private windowClosedTimeout: number;
|
||||
|
||||
constructor(modal: string, registries: RegistryMap, userData: any, uniqueModalId: string) {
|
||||
super(modal, registries, userData);
|
||||
this.uniqueModalId = uniqueModalId || modal;
|
||||
constructor(modalType: string, constructorArguments: any[] | undefined, options: ModalOptions | undefined) {
|
||||
super(modalType, constructorArguments);
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
protected async spawnWindow() : Promise<boolean> {
|
||||
if(this.currentWindow)
|
||||
if(this.currentWindow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.currentWindow = this.trySpawnWindow0();
|
||||
if(!this.currentWindow) {
|
||||
|
@ -106,7 +107,7 @@ export class ExternalModalController extends AbstractExternalModalController {
|
|||
let baseUrl = location.origin + location.pathname + "?";
|
||||
return window.open(
|
||||
baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&"),
|
||||
this.uniqueModalId,
|
||||
this.options?.uniqueId || this.modalType,
|
||||
Object.keys(features).map(e => e + "=" + features[e]).join(",")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
|||
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
||||
import {WhisperSession, WhisperTarget} from "tc-shared/voice/VoiceWhisper";
|
||||
import {AbstractServerConnection, ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {EventDispatchType, Registry} from "tc-shared/events";
|
||||
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {RtpVoiceConnection} from "tc-backend/web/voice/Connection";
|
||||
|
@ -21,6 +21,7 @@ class ProxiedVoiceClient implements VoiceClient {
|
|||
|
||||
private volume: number;
|
||||
private latencySettings: VoicePlayerLatencySettings | undefined;
|
||||
private eventDisconnect: () => void;
|
||||
|
||||
constructor(clientId: number) {
|
||||
this.clientId = clientId;
|
||||
|
@ -30,14 +31,36 @@ class ProxiedVoiceClient implements VoiceClient {
|
|||
}
|
||||
|
||||
setHandle(handle: VoiceClient | undefined) {
|
||||
this.handle?.events.disconnectAll(this.events);
|
||||
if(this.eventDisconnect) {
|
||||
this.eventDisconnect();
|
||||
this.eventDisconnect = undefined;
|
||||
}
|
||||
this.handle = handle;
|
||||
|
||||
if(this.latencySettings) {
|
||||
this.handle?.setLatencySettings(this.latencySettings);
|
||||
}
|
||||
this.handle?.setVolume(this.volume);
|
||||
this.handle?.events.connectAll(this.events);
|
||||
if(this.handle) {
|
||||
const targetEvents = this.events;
|
||||
this.eventDisconnect = this.handle.events.registerConsumer({
|
||||
handleEvent(mode: EventDispatchType, type: string, data: any) {
|
||||
switch (mode) {
|
||||
case "later":
|
||||
targetEvents.fire_later(type as any, data);
|
||||
break;
|
||||
|
||||
case "react":
|
||||
targetEvents.fire_react(type as any, data);
|
||||
break;
|
||||
|
||||
case "sync":
|
||||
targetEvents.fire(type as any, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
abortReplay() {
|
||||
|
@ -86,6 +109,7 @@ export class LegacySupportVoiceBridge extends AbstractVoiceConnection {
|
|||
private readonly oldVoiceBridge: VoiceConnection;
|
||||
|
||||
private activeBridge: AbstractVoiceConnection;
|
||||
private disconnectEvents: () => void;
|
||||
|
||||
private encoderCodec: number;
|
||||
private currentRecorder: RecorderProfile;
|
||||
|
@ -108,11 +132,31 @@ export class LegacySupportVoiceBridge extends AbstractVoiceConnection {
|
|||
e.setHandle(undefined);
|
||||
}
|
||||
});
|
||||
this.activeBridge?.events.disconnectAll(this.events);
|
||||
if(this.disconnectEvents) {
|
||||
this.disconnectEvents();
|
||||
this.disconnectEvents = undefined;
|
||||
}
|
||||
this.activeBridge = type === "old" ? this.oldVoiceBridge : type === "new" ? this.newVoiceBride : undefined;
|
||||
|
||||
if(this.activeBridge) {
|
||||
this.activeBridge.events.connectAll(this.events);
|
||||
const targetEvents = this.events;
|
||||
this.disconnectEvents = this.activeBridge.events.registerConsumer({
|
||||
handleEvent(mode: EventDispatchType, type: string, data: any) {
|
||||
switch (mode) {
|
||||
case "later":
|
||||
targetEvents.fire_later(type as any, data);
|
||||
break;
|
||||
|
||||
case "react":
|
||||
targetEvents.fire_react(type as any, data);
|
||||
break;
|
||||
|
||||
case "sync":
|
||||
targetEvents.fire(type as any, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.registeredClients.forEach(e => {
|
||||
if(!e.handle) {
|
||||
|
|
|
@ -7,6 +7,6 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||
priority: 50,
|
||||
name: "external modal controller factory setup",
|
||||
function: async () => {
|
||||
setExternalModalControllerFactory((modal, events, userData, uniqueModalId) => new ExternalModalController(modal, events, userData, uniqueModalId));
|
||||
setExternalModalControllerFactory((modalType, constructorArguments, options) => new ExternalModalController(modalType, constructorArguments, options));
|
||||
}
|
||||
});
|
|
@ -23,6 +23,7 @@ export class VoicePlayer implements VoicePlayer {
|
|||
private currentRtpTrack: RemoteRTPAudioTrack;
|
||||
|
||||
constructor() {
|
||||
this.volume = 1;
|
||||
this.listenerTrackStateChanged = event => this.handleTrackStateChanged(event.newState);
|
||||
this.events = new Registry<VoicePlayerEvents>();
|
||||
this.currentState = VoicePlayerState.STOPPED;
|
||||
|
|
Loading…
Reference in New Issue