Finishing my work on the new connect modal
This commit is contained in:
parent
c3b64447db
commit
6748a0c978
4 changed files with 965 additions and 473 deletions
|
@ -1,24 +1,343 @@
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {ConnectProperties, ConnectUiEvents} from "tc-shared/ui/modal/connect/Definitions";
|
import {ConnectProperties, ConnectUiEvents, PropertyValidState} from "tc-shared/ui/modal/connect/Definitions";
|
||||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
import {ConnectModal} from "tc-shared/ui/modal/connect/Renderer";
|
import {ConnectModal} from "tc-shared/ui/modal/connect/Renderer";
|
||||||
|
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||||
|
import {
|
||||||
|
availableConnectProfiles,
|
||||||
|
ConnectionProfile,
|
||||||
|
defaultConnectProfile,
|
||||||
|
findConnectProfile
|
||||||
|
} from "tc-shared/profiles/ConnectionProfile";
|
||||||
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
|
import {connectionHistory, ConnectionHistoryEntry} from "tc-shared/connectionlog/History";
|
||||||
|
import {global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||||
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
|
import _ = require("lodash");
|
||||||
|
import {parseServerAddress} from "tc-shared/tree/Server";
|
||||||
|
import * as ipRegex from "ip-regex";
|
||||||
|
|
||||||
|
const kRegexDomain = /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))$/i;
|
||||||
|
|
||||||
|
export type ConnectParameters = {
|
||||||
|
targetAddress: string,
|
||||||
|
targetPassword?: string,
|
||||||
|
targetPasswordHashed?: boolean,
|
||||||
|
|
||||||
|
nickname: string,
|
||||||
|
nicknameSpecified: boolean,
|
||||||
|
|
||||||
|
profile: ConnectionProfile,
|
||||||
|
|
||||||
|
token?: string,
|
||||||
|
|
||||||
|
defaultChannel?: string | number,
|
||||||
|
defaultChannelPassword?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidityStates = {[T in keyof PropertyValidState]: boolean};
|
||||||
|
const kDefaultValidityStates: ValidityStates = {
|
||||||
|
address: false,
|
||||||
|
nickname: false,
|
||||||
|
password: false,
|
||||||
|
profile: false
|
||||||
|
}
|
||||||
|
|
||||||
class ConnectController {
|
class ConnectController {
|
||||||
readonly uiEvents: Registry<ConnectUiEvents>;
|
readonly uiEvents: Registry<ConnectUiEvents>;
|
||||||
|
|
||||||
|
private readonly defaultAddress: string;
|
||||||
|
private readonly propertyProvider: {[K in keyof ConnectProperties]?: () => Promise<ConnectProperties[K]>} = {};
|
||||||
|
|
||||||
|
private historyShown: boolean;
|
||||||
|
|
||||||
|
private currentAddress: string;
|
||||||
|
private currentNickname: string;
|
||||||
|
private currentPassword: string;
|
||||||
|
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 validateStates: {[T in keyof PropertyValidState]: boolean} = {
|
||||||
|
profile: false,
|
||||||
|
password: false,
|
||||||
|
nickname: false,
|
||||||
|
address: false
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.uiEvents = new Registry<ConnectUiEvents>();
|
this.uiEvents = new Registry<ConnectUiEvents>();
|
||||||
|
this.uiEvents.enableDebug("modal-connect");
|
||||||
|
|
||||||
|
this.defaultAddress = "ts.teaspeak.de";
|
||||||
|
this.historyShown = settings.static_global(Settings.KEY_CONNECT_SHOW_HISTORY);
|
||||||
|
|
||||||
|
this.currentAddress = settings.static_global(Settings.KEY_CONNECT_ADDRESS);
|
||||||
|
this.currentProfile = findConnectProfile(settings.static_global(Settings.KEY_CONNECT_PROFILE)) || defaultConnectProfile();
|
||||||
|
this.currentNickname = settings.static_global(Settings.KEY_CONNECT_USERNAME);
|
||||||
|
|
||||||
|
this.addressChanged = false;
|
||||||
|
this.nicknameChanged = false;
|
||||||
|
|
||||||
|
this.propertyProvider["nickname"] = async () => {
|
||||||
|
return {
|
||||||
|
defaultNickname: this.currentProfile?.connectUsername(),
|
||||||
|
currentNickname: this.currentNickname,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
this.propertyProvider["address"] = async () => {
|
||||||
|
return {
|
||||||
|
currentAddress: this.currentAddress,
|
||||||
|
defaultAddress: this.defaultAddress,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
this.propertyProvider["password"] = async () => this.currentPassword ? ({
|
||||||
|
hashed: this.currentPasswordHashed,
|
||||||
|
password: this.currentPassword
|
||||||
|
}) : undefined;
|
||||||
|
this.propertyProvider["profiles"] = async () => ({
|
||||||
|
selected: this.currentProfile?.id,
|
||||||
|
profiles: availableConnectProfiles().map(profile => ({
|
||||||
|
id: profile.id,
|
||||||
|
valid: profile.valid(),
|
||||||
|
name: profile.profileName
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
this.propertyProvider["historyShown"] = async () => this.historyShown;
|
||||||
|
this.propertyProvider["history"] = async () => {
|
||||||
|
if(!this.history) {
|
||||||
|
this.history = await connectionHistory.lastConnectedServers(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selected: this.selectedHistoryId,
|
||||||
|
history: this.history.map(entry => ({
|
||||||
|
id: entry.id,
|
||||||
|
targetAddress: entry.targetAddress,
|
||||||
|
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: {
|
||||||
|
icon: {
|
||||||
|
iconId: info.iconId,
|
||||||
|
serverUniqueId: event.serverUniqueId,
|
||||||
|
handlerId: undefined
|
||||||
|
},
|
||||||
|
name: info.name,
|
||||||
|
password: info.passwordProtected,
|
||||||
|
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.changeGlobal(Settings.KEY_CONNECT_SHOW_HISTORY, event.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiEvents.on("action_manage_profiles", () => {
|
||||||
|
/* FIXME: Reload profiles if their status have changed... */
|
||||||
|
global_client_actions.fire("action_open_window_settings", { defaultCategory: "identity-profiles" });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiEvents.on("action_select_profile", event => {
|
||||||
|
const profile = findConnectProfile(event.id);
|
||||||
|
if(!profile) {
|
||||||
|
createErrorModal(tr("Invalid profile"), tr("Target connect profile is missing.")).open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentProfile = profile;
|
||||||
|
this.sendProperty("profiles").then(undefined);
|
||||||
|
settings.changeGlobal(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 });
|
||||||
|
|
||||||
|
this.validateStates["profile"] = true;
|
||||||
|
this.updateValidityStates();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiEvents.on("action_set_address", event => {
|
||||||
|
if(this.currentAddress !== event.address) {
|
||||||
|
this.currentAddress = event.address;
|
||||||
|
this.sendProperty("address").then(undefined);
|
||||||
|
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, event.address);
|
||||||
|
this.setSelectedHistoryId(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validateStates["address"] = event.validate;
|
||||||
|
this.updateValidityStates();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiEvents.on("action_set_nickname", event => {
|
||||||
|
if(this.currentNickname !== event.nickname) {
|
||||||
|
this.currentNickname = event.nickname;
|
||||||
|
this.sendProperty("nickname").then(undefined);
|
||||||
|
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, event.nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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() {
|
destroy() {
|
||||||
|
Object.keys(this.propertyProvider).forEach(key => delete this.propertyProvider[key]);
|
||||||
|
this.uiEvents.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateConnectParameters() : ConnectParameters | undefined {
|
||||||
private sendProperty(property: keyof ConnectProperties) {
|
if(Object.keys(this.validStates).findIndex(key => this.validStates[key] === false) !== -1) {
|
||||||
switch (property) {
|
return undefined;
|
||||||
case "address":
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nickname: this.currentNickname || this.currentProfile?.connectUsername(),
|
||||||
|
nicknameSpecified: !!this.currentNickname,
|
||||||
|
|
||||||
|
targetAddress: this.currentAddress || this.defaultAddress,
|
||||||
|
|
||||||
|
profile: this.currentProfile,
|
||||||
|
|
||||||
|
targetPassword: this.currentPassword,
|
||||||
|
targetPasswordHashed: this.currentPasswordHashed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedHistoryId(id: number | -1) {
|
||||||
|
if(this.selectedHistoryId === id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedHistoryId = id;
|
||||||
|
this.sendProperty("history").then(undefined);
|
||||||
|
|
||||||
|
const historyEntry = this.history?.find(entry => entry.id === id);
|
||||||
|
if(!historyEntry) { return; }
|
||||||
|
|
||||||
|
this.currentAddress = historyEntry.targetAddress;
|
||||||
|
this.currentNickname = historyEntry.nickname;
|
||||||
|
this.currentPassword = historyEntry.hashedPassword;
|
||||||
|
this.currentPasswordHashed = true;
|
||||||
|
|
||||||
|
this.sendProperty("address").then(undefined);
|
||||||
|
this.sendProperty("password").then(undefined);
|
||||||
|
this.sendProperty("nickname").then(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
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]()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +355,29 @@ export function spawnConnectModalNew(options: ConnectModalOptions) {
|
||||||
modal.events.one("destroy", () => {
|
modal.events.one("destroy", () => {
|
||||||
controller.destroy();
|
controller.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
controller.uiEvents.on("action_connect", event => {
|
||||||
|
const parameters = controller.generateConnectParameters();
|
||||||
|
if(!parameters) {
|
||||||
|
/* invalid parameters detected */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.destroy();
|
||||||
|
|
||||||
|
let connection: ConnectionHandler;
|
||||||
|
if(event.newTab) {
|
||||||
|
connection = server_connections.spawn_server_connection();
|
||||||
|
} else {
|
||||||
|
connection = server_connections.active_connection();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!connection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.startConnectionNew(parameters, false).then(undefined);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
(window as any).spawnConnectModalNew = spawnConnectModalNew;
|
(window as any).spawnConnectModalNew = spawnConnectModalNew;
|
|
@ -1,4 +1,5 @@
|
||||||
import {kUnknownHistoryServerUniqueId} from "tc-shared/connectionlog/History";
|
import {kUnknownHistoryServerUniqueId} from "tc-shared/connectionlog/History";
|
||||||
|
import { RemoteIconInfo} from "tc-shared/file/Icons";
|
||||||
|
|
||||||
export type ConnectProfileEntry = {
|
export type ConnectProfileEntry = {
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -13,36 +14,36 @@ export type ConnectHistoryEntry = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectHistoryServerInfo = {
|
export type ConnectHistoryServerInfo = {
|
||||||
iconId: number,
|
icon: RemoteIconInfo,
|
||||||
name: string,
|
name: string,
|
||||||
password: boolean,
|
password: boolean,
|
||||||
|
country: string,
|
||||||
|
|
||||||
|
clients: number | -1,
|
||||||
|
maxClients: number | -1
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectServerAddress = {
|
|
||||||
currentAddress: string,
|
|
||||||
defaultAddress: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConnectServerNickname = {
|
|
||||||
currentNickname: string,
|
|
||||||
defaultNickname: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConnectProfiles = {
|
|
||||||
profiles: ConnectProfileEntry[],
|
|
||||||
selected: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ConnectProperties {
|
export interface ConnectProperties {
|
||||||
address: ConnectServerAddress,
|
address: {
|
||||||
nickname: ConnectServerNickname,
|
currentAddress: string,
|
||||||
password: string,
|
defaultAddress: string,
|
||||||
profiles: ConnectProfiles,
|
},
|
||||||
|
nickname: {
|
||||||
|
currentNickname: string | undefined,
|
||||||
|
defaultNickname: string | undefined,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
password: string,
|
||||||
|
hashed: boolean
|
||||||
|
} | undefined,
|
||||||
|
profiles: {
|
||||||
|
profiles: ConnectProfileEntry[],
|
||||||
|
selected: string
|
||||||
|
},
|
||||||
|
historyShown: boolean,
|
||||||
history: {
|
history: {
|
||||||
history: ConnectHistoryEntry[],
|
history: ConnectHistoryEntry[],
|
||||||
selected: number | -1,
|
selected: number | -1,
|
||||||
state: "shown" | "hidden"
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,11 +51,12 @@ export interface PropertyValidState {
|
||||||
address: boolean,
|
address: boolean,
|
||||||
nickname: boolean,
|
nickname: boolean,
|
||||||
password: boolean,
|
password: boolean,
|
||||||
|
profile: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectProperty<T extends keyof ConnectProperties> = {
|
type IAccess<I, T extends keyof I> = {
|
||||||
property: T,
|
property: T,
|
||||||
value: ConnectProperties[T]
|
value: I[T]
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ConnectUiEvents {
|
export interface ConnectUiEvents {
|
||||||
|
@ -66,13 +68,20 @@ export interface ConnectUiEvents {
|
||||||
action_delete_history: {
|
action_delete_history: {
|
||||||
target: string,
|
target: string,
|
||||||
targetType: "address" | "server-unique-id"
|
targetType: "address" | "server-unique-id"
|
||||||
}
|
},
|
||||||
|
action_set_nickname: { nickname: string, validate: boolean },
|
||||||
|
action_set_address: { address: string, validate: boolean },
|
||||||
|
action_set_password: { password: string, hashed: boolean },
|
||||||
|
|
||||||
query_property: {
|
query_property: {
|
||||||
property: keyof ConnectProperties
|
property: keyof ConnectProperties
|
||||||
},
|
},
|
||||||
|
query_property_valid: {
|
||||||
|
property: keyof PropertyValidState
|
||||||
|
},
|
||||||
|
|
||||||
notify_property: ConnectProperty<keyof ConnectProperties>
|
notify_property: IAccess<ConnectProperties, keyof ConnectProperties>,
|
||||||
|
notify_property_valid: IAccess<PropertyValidState, keyof PropertyValidState>,
|
||||||
|
|
||||||
query_history_entry: {
|
query_history_entry: {
|
||||||
serverUniqueId: string
|
serverUniqueId: string
|
||||||
|
|
|
@ -5,330 +5,36 @@
|
||||||
@include user-select(none);
|
@include user-select(none);
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 1em;
|
|
||||||
|
|
||||||
width: 50em;
|
width: 60em;
|
||||||
min-width: 25em;
|
min-width: 25em;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
|
||||||
display: flex!important;
|
display: flex;
|
||||||
flex-direction: column!important;
|
flex-direction: column;
|
||||||
justify-content: stretch!important;
|
justify-content: stretch;
|
||||||
|
|
||||||
.container-last-servers {
|
> * {
|
||||||
flex-grow: 0;
|
padding-left: 1.5em;
|
||||||
flex-shrink: 1;
|
padding-right: 1.5em;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
max-height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
border-left: 2px solid #7a7a7a;
|
|
||||||
|
|
||||||
@include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out);
|
|
||||||
&.shown {
|
|
||||||
/* apply the default padding */
|
|
||||||
padding: 0 24px 24px;
|
|
||||||
|
|
||||||
max-height: 100%;
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
@include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out)
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
height: 0;
|
|
||||||
width: calc(100% + 46px);
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
margin: 0 0 0 -23px;
|
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid #090909;
|
|
||||||
|
|
||||||
margin-bottom: .75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
color: #7a7a7a;
|
|
||||||
|
|
||||||
/* general table class */
|
|
||||||
.table {
|
|
||||||
width: 100em;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
.head {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid #161618;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.body {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #202022;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color: #131315;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-empty {
|
|
||||||
height: 3em;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
font-size: 1.25em;
|
|
||||||
color: rgba(121, 121, 121, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
padding-right: .25em;
|
|
||||||
padding-left: .25em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
|
||||||
border-right: 1px solid #161618;
|
|
||||||
}
|
|
||||||
|
|
||||||
> a {
|
|
||||||
max-width: 100%;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* connect table */
|
|
||||||
.table {
|
|
||||||
margin-left: -1.5em; /* the delete row */
|
|
||||||
|
|
||||||
.head {
|
|
||||||
margin-left: 1.5em; /* the delete row */
|
|
||||||
.column.delete {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
align-self: center;
|
|
||||||
.country, .icon-container {
|
|
||||||
align-self: center;
|
|
||||||
margin-right: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@mixin fixed-column($name, $width) {
|
|
||||||
&.#{$name} {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
width: $width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include fixed-column(delete, 1.5em);
|
|
||||||
@include fixed-column(password, 5em);
|
|
||||||
@include fixed-column(country-name, 7em);
|
|
||||||
@include fixed-column(clients, 4em);
|
|
||||||
@include fixed-column(connections, 6.5em);
|
|
||||||
|
|
||||||
&.delete {
|
|
||||||
opacity: 0;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: none;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
@include transition(opacity .25 ease-in-out);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
@include transition(opacity .25 ease-in-out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.address {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.name {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.connectContainer_ {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
/* apply the default padding */
|
|
||||||
padding: .75em 24px;
|
|
||||||
|
|
||||||
border-left: 2px solid #0073d4;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> .row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
> *:not(:last-of-type) {
|
|
||||||
margin-right: 3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-address-password {
|
|
||||||
.container-address {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-password {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 4;
|
|
||||||
|
|
||||||
min-width: 21.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-profile-manage {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 4;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
.container-select-profile {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
min-width: 14em;
|
|
||||||
|
|
||||||
> .invalid-feedback {
|
|
||||||
width: max-content; /* allow overflow here */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-manage {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 4;
|
|
||||||
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-manage-profiles {
|
|
||||||
min-width: 7em;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-nickname {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-buttons {
|
|
||||||
padding-top: 1em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.container-buttons-connect {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
flex-shrink: 1;
|
|
||||||
min-width: 6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-right {
|
|
||||||
min-width: 7em;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-left {
|
|
||||||
min-width: 14em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
border-color: #7a7a7a;
|
|
||||||
margin-left: .5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.connectContainer {
|
.connectContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
padding-top: .75em;
|
||||||
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|
||||||
|
border-left: 2px solid #0073d4;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -337,8 +43,9 @@
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
|
||||||
.inputAddress, .inputNickname {
|
.inputAddress, .inputNickname {
|
||||||
width: 75%;
|
width: 35em;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
@ -347,12 +54,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputPassword, .inputProfile {
|
.inputPassword, .inputProfile {
|
||||||
width: 25%;
|
width: 25em;
|
||||||
|
|
||||||
min-width: 15em;
|
min-width: 15em;
|
||||||
max-width: 21em;
|
max-width: 100%;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 0;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,10 +68,15 @@
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
overflow: visible;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.invalidFeedback {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
@ -382,37 +93,261 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-bottom: 1.5em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
border-left: 2px solid #0073d4;
|
||||||
|
|
||||||
|
.buttonShowHistory {
|
||||||
|
.containerText {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerArrow {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: .5em;
|
||||||
|
|
||||||
|
:global(.arrow) {
|
||||||
|
border-color: #7a7a7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.arrow.up) {
|
||||||
|
margin-bottom: -.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsConnect {
|
||||||
|
padding-left: .5em;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.button:not(:first-of-type) {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyContainer {
|
||||||
|
border-left: 2px solid #7a7a7a;
|
||||||
|
border-top: 1px solid #090909;
|
||||||
|
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@include transition(all .3s);
|
||||||
|
|
||||||
|
&.shown {
|
||||||
|
max-height: 30em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyTable {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
color: #7a7a7a;
|
||||||
|
|
||||||
|
width: 100em;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #161618;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #202022;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: #131315;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyEmpty {
|
||||||
|
height: 3em;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-size: 1.25em;
|
||||||
|
color: rgba(121, 121, 121, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
padding-right: .25em;
|
||||||
|
padding-left: .25em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
border-right: 1px solid #161618;
|
||||||
|
}
|
||||||
|
|
||||||
|
> a {
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-left: -1.5em; /* the delete row */
|
||||||
|
|
||||||
|
.head {
|
||||||
|
margin-left: 1.5em; /* the delete row */
|
||||||
|
|
||||||
|
.column.delete {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
align-self: center;
|
||||||
|
.country, .iconContainer {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mixin fixed-column($name, $width) {
|
||||||
|
&.#{$name} {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
width: $width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include fixed-column(delete, 1.5em);
|
||||||
|
@include fixed-column(password, 5em);
|
||||||
|
@include fixed-column(country, 7em);
|
||||||
|
@include fixed-column(clients, 4em);
|
||||||
|
@include fixed-column(connections, 6.5em);
|
||||||
|
|
||||||
|
&.delete {
|
||||||
|
opacity: 0;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
@include transition(opacity .25s ease-in-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.address {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
&:hover {
|
||||||
|
.delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.countryContainer {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
:global(.country) {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 55rem) {
|
@media all and (max-width: 55rem) {
|
||||||
.container {
|
.container {
|
||||||
padding: .5em!important;
|
padding: .5em!important;
|
||||||
|
padding-top: 0!important;
|
||||||
|
|
||||||
.container-address-password {
|
|
||||||
.container-password {
|
|
||||||
min-width: unset!important;
|
|
||||||
margin-left: 1em!important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-buttons {
|
|
||||||
justify-content: flex-end!important;
|
|
||||||
|
|
||||||
.button-toggle-last-servers {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-profile-name {
|
|
||||||
flex-direction: column!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-last-servers {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectContainer {
|
.connectContainer {
|
||||||
.inputAddress, .inputNickname {
|
.inputAddress, .inputNickname {
|
||||||
margin-right: 1em!important;
|
margin-right: 1em!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smallColumn {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
.buttonShowHistory {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyContainer {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,104 +1,29 @@
|
||||||
import {ConnectProperties, ConnectUiEvents} from "tc-shared/ui/modal/connect/Definitions";
|
import {
|
||||||
|
ConnectHistoryEntry,
|
||||||
|
ConnectHistoryServerInfo,
|
||||||
|
ConnectProperties,
|
||||||
|
ConnectUiEvents, PropertyValidState
|
||||||
|
} from "tc-shared/ui/modal/connect/Definitions";
|
||||||
|
import * as React from "react";
|
||||||
import {useContext, useState} from "react";
|
import {useContext, useState} from "react";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import * as React from "react";
|
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {ControlledSelect, FlatInputField, Select} from "tc-shared/ui/react-elements/InputField";
|
import {ControlledFlatInputField, ControlledSelect, FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
||||||
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
|
import {kUnknownHistoryServerUniqueId} from "tc-shared/connectionlog/History";
|
||||||
|
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";
|
||||||
|
|
||||||
const EventContext = React.createContext<Registry<ConnectUiEvents>>(undefined);
|
const EventContext = React.createContext<Registry<ConnectUiEvents>>(undefined);
|
||||||
const ConnectDefaultNewTabContext = React.createContext<boolean>(false);
|
const ConnectDefaultNewTabContext = React.createContext<boolean>(false);
|
||||||
|
|
||||||
const cssStyle = require("./Renderer.scss");
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
/*
|
|
||||||
<div class="container-connect-input">
|
|
||||||
<div class="row container-address-password">
|
|
||||||
<div class="form-group container-address">
|
|
||||||
<label>{{tr "Server Address" /}}</label>
|
|
||||||
<input type="text" class="form-control" aria-describedby="input-connect-address-help"
|
|
||||||
placeholder="ts.teaspeak.de" autocomplete="off">
|
|
||||||
<div class="invalid-feedback">{{tr "Please enter a valid server address" /}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group container-password">
|
|
||||||
<label class="bmd-label-floating">{{tr "Server password" /}}</label>
|
|
||||||
<form autocomplete="off" onsubmit="return false;">
|
|
||||||
<input id="input-connect-password-{{>password_id}}" type="password" class="form-control"
|
|
||||||
autocomplete="off">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row container-profile-name">
|
|
||||||
<div class="form-group container-nickname">
|
|
||||||
<label>{{tr "Nickname" /}}</label>
|
|
||||||
<input type="text" class="form-control" aria-describedby="input-connect-nickname-help"
|
|
||||||
placeholder="Another TeaSpeak user">
|
|
||||||
<div class="invalid-feedback">{{tr "Please enter a valid server nickname" /}}</div>
|
|
||||||
<!-- <small id="input-connect-nickname-help" class="form-text text-muted">We'll never share your email with anyone else.</small> -->
|
|
||||||
</div>
|
|
||||||
<div class="container-profile-manage">
|
|
||||||
<div class="form-group container-select-profile">
|
|
||||||
<label for="select-connect-profile">{{tr "Connect Profile" /}}</label>
|
|
||||||
<select class="form-control" id="select-connect-profile"> </select>
|
|
||||||
<div class="invalid-feedback">{{tr "Selected profile is invalid. Select another one or fix the profile." /}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<button type="button" class="btn btn-raised button-manage-profiles">{{tr "Profiles" /}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container-buttons">
|
|
||||||
<button type="button" class="btn btn-raised button-toggle-last-servers"><a>{{tr "Show last servers"
|
|
||||||
/}}</a>
|
|
||||||
<div class="arrow down"></div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="container-buttons-connect">
|
|
||||||
{{if default_connect_new_tab}}
|
|
||||||
<button type="button" class="btn btn-raised btn-success button-connect button-left">{{tr
|
|
||||||
"Connect in same tab" /}}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-raised btn-success button-connect-new-tab button-right">
|
|
||||||
{{tr "Connect" /}}
|
|
||||||
</button>
|
|
||||||
{{else}}
|
|
||||||
{{if multi_tab}}
|
|
||||||
<button type="button" class="btn btn-raised btn-success button-connect-new-tab button-left">{{tr
|
|
||||||
"Connect in a new tab" /}}
|
|
||||||
</button>
|
|
||||||
{{/if}}
|
|
||||||
<button type="button" class="btn btn-raised btn-success button-connect button-right">{{tr
|
|
||||||
"Connect" /}}
|
|
||||||
</button>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container-last-servers">
|
|
||||||
<hr>
|
|
||||||
<div class="table">
|
|
||||||
<div class="head">
|
|
||||||
<div class="column delete">Nr</div>
|
|
||||||
<div class="column name">{{tr "Name" /}}</div>
|
|
||||||
<div class="column address">{{tr "Address" /}}</div>
|
|
||||||
<div class="column password">{{tr "Password" /}}</div>
|
|
||||||
<div class="column country-name">{{tr "Country" /}}</div>
|
|
||||||
<div class="column clients">{{tr "Clients" /}}</div>
|
|
||||||
<div class="column connections">{{tr "Connections" /}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="body">
|
|
||||||
<div class="body-empty">
|
|
||||||
<a>{{tr "No connections yet made" /}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
*/
|
|
||||||
|
|
||||||
function useProperty<T extends keyof ConnectProperties, V>(key: T, defaultValue: V) : ConnectProperties[T] | V {
|
function useProperty<T extends keyof ConnectProperties, V>(key: T, defaultValue: V) : ConnectProperties[T] | V {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
const [ value, setValue ] = useState<ConnectProperties[T] | V>(() => {
|
const [ value, setValue ] = useState<ConnectProperties[T] | V>(() => {
|
||||||
|
@ -110,55 +35,93 @@ function useProperty<T extends keyof ConnectProperties, V>(key: T, defaultValue:
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 InputServerAddress = () => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
const address = useProperty("address", undefined);
|
||||||
|
const valid = usePropertyValid("address", true);
|
||||||
|
const newTab = useContext(ConnectDefaultNewTabContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatInputField
|
<ControlledFlatInputField
|
||||||
|
value={address?.currentAddress || ""}
|
||||||
|
placeholder={address?.defaultAddress || tr("Please enter a address")}
|
||||||
|
|
||||||
className={cssStyle.inputAddress}
|
className={cssStyle.inputAddress}
|
||||||
value={"ts.teaspeak.de"}
|
|
||||||
placeholder={"ts.teaspeak.de"}
|
|
||||||
label={<Translatable>Server address</Translatable>}
|
label={<Translatable>Server address</Translatable>}
|
||||||
labelType={"static"}
|
labelType={"static"}
|
||||||
|
|
||||||
|
invalid={valid ? undefined : <Translatable>Please enter a valid server address</Translatable>}
|
||||||
|
|
||||||
|
onInput={value => events.fire("action_set_address", { address: value, validate: false })}
|
||||||
|
onBlur={() => events.fire("action_set_address", { address: address?.currentAddress, validate: true })}
|
||||||
|
onEnter={() => events.fire("action_connect", { newTab })}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputServerPassword = () => {
|
const InputServerPassword = () => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
const password = useProperty("password", undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatInputField
|
<FlatInputField
|
||||||
className={cssStyle.inputPassword}
|
className={cssStyle.inputPassword}
|
||||||
value={"ts.teaspeak.de"}
|
value={!password?.hashed ? password?.password || "" : ""}
|
||||||
placeholder={"ts.teaspeak.de"}
|
placeholder={password?.hashed ? tr("Password Hidden") : null}
|
||||||
type={"password"}
|
type={"password"}
|
||||||
label={<Translatable>Server password</Translatable>}
|
label={<Translatable>Server password</Translatable>}
|
||||||
labelType={"floating"}
|
labelType={password?.hashed ? "static" : "floating"}
|
||||||
|
onInput={value => events.fire("action_set_password", { password: value, hashed: false })}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputNickname = () => {
|
const InputNickname = () => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
const nickname = useProperty("nickname", undefined);
|
const nickname = useProperty("nickname", undefined);
|
||||||
|
const valid = usePropertyValid("nickname", true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatInputField
|
<ControlledFlatInputField
|
||||||
className={cssStyle.inputNickname}
|
className={cssStyle.inputNickname}
|
||||||
value={nickname?.currentNickname || ""}
|
value={nickname?.currentNickname || ""}
|
||||||
placeholder={nickname ? nickname.defaultNickname : tr("loading...")}
|
placeholder={nickname ? nickname.defaultNickname ? nickname.defaultNickname : tr("Please enter a nickname") : tr("loading...")}
|
||||||
label={<Translatable>Nickname</Translatable>}
|
label={<Translatable>Nickname</Translatable>}
|
||||||
labelType={"static"}
|
labelType={"static"}
|
||||||
|
invalid={valid ? undefined : <Translatable>Nickname too short or too long</Translatable>}
|
||||||
|
onInput={value => events.fire("action_set_nickname", { nickname: value, validate: false })}
|
||||||
|
onBlur={() => events.fire("action_set_nickname", { nickname: nickname?.currentNickname, validate: true })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputProfile = () => {
|
const InputProfile = () => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
const profiles = useProperty("profiles", undefined);
|
const profiles = useProperty("profiles", undefined);
|
||||||
const selectedProfile = profiles?.profiles.find(profile => profile.id === profiles?.selected);
|
const selectedProfile = profiles?.profiles.find(profile => profile.id === profiles?.selected);
|
||||||
|
|
||||||
let invalidMarker;
|
let invalidMarker;
|
||||||
if(profiles) {
|
if(profiles) {
|
||||||
if(!selectedProfile) {
|
if(!profiles.selected) {
|
||||||
invalidMarker = <Translatable key={"no-profile"}>Select a profile</Translatable>;
|
/* 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>;
|
||||||
|
} else if(!selectedProfile) {
|
||||||
|
invalidMarker = <Translatable key={"no-profile"}>Unknown select profile</Translatable>;
|
||||||
} else if(!selectedProfile.valid) {
|
} else if(!selectedProfile.valid) {
|
||||||
invalidMarker = <Translatable key={"invalid"}>Selected profile is invalid</Translatable>
|
invalidMarker = <Translatable key={"invalid"}>Selected profile has an invalid config</Translatable>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,20 +129,21 @@ const InputProfile = () => {
|
||||||
<div className={cssStyle.inputProfile}>
|
<div className={cssStyle.inputProfile}>
|
||||||
<ControlledSelect
|
<ControlledSelect
|
||||||
className={cssStyle.input}
|
className={cssStyle.input}
|
||||||
value={profiles?.selected || "loading"}
|
value={selectedProfile ? selectedProfile.id : profiles?.selected ? "invalid" : profiles ? "no-selected" : "loading"}
|
||||||
type={"flat"}
|
type={"flat"}
|
||||||
label={<Translatable>Connect profile</Translatable>}
|
label={<Translatable>Connect profile</Translatable>}
|
||||||
invalid={invalidMarker}
|
invalid={invalidMarker}
|
||||||
|
invalidClassName={cssStyle.invalidFeedback}
|
||||||
|
onChange={event => events.fire("action_select_profile", { id: event.target.value })}
|
||||||
>
|
>
|
||||||
<option key={"loading"} value={"invalid"} style={{ display: "none" }}>{useTr("unknown profile")}</option>
|
<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>
|
<option key={"loading"} value={"loading"} style={{ display: "none" }}>{useTr("loading") + "..."}</option>
|
||||||
{profiles?.profiles.forEach(profile => {
|
{profiles?.profiles.map(profile => (
|
||||||
return (
|
<option key={"profile-" + profile.id} value={profile.id}>{profile.name}</option>
|
||||||
<option key={"profile-" + profile.id}>{profile.name}</option>
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ControlledSelect>
|
</ControlledSelect>
|
||||||
<Button className={cssStyle.button} type={"small"} color={"none"}>
|
<Button className={cssStyle.button} type={"small"} color={"none"} onClick={() => events.fire("action_manage_profiles")}>
|
||||||
<Translatable>Profiles</Translatable>
|
<Translatable>Profiles</Translatable>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -192,12 +156,248 @@ const ConnectContainer = () => (
|
||||||
<InputServerAddress />
|
<InputServerAddress />
|
||||||
<InputServerPassword />
|
<InputServerPassword />
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.row}>
|
<div className={cssStyle.row + " " + cssStyle.smallColumn}>
|
||||||
<InputNickname />
|
<InputNickname />
|
||||||
<InputProfile />
|
<InputProfile />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
|
const ButtonToggleHistory = () => {
|
||||||
|
const state = useProperty("historyShown", false);
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if(state) {
|
||||||
|
body = (
|
||||||
|
<React.Fragment key={"hide"}>
|
||||||
|
<div className={cssStyle.containerText}><Translatable>Hide connect history</Translatable></div>
|
||||||
|
<div className={cssStyle.containerArrow}><div className={"arrow down"} /></div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body = (
|
||||||
|
<React.Fragment key={"show"}>
|
||||||
|
<div className={cssStyle.containerText}><Translatable>Show connect history</Translatable></div>
|
||||||
|
<div className={cssStyle.containerArrow}><div className={"arrow up"} /></div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cssStyle.buttonShowHistory + " " + cssStyle.button}
|
||||||
|
type={"small"}
|
||||||
|
color={"none"}
|
||||||
|
onClick={() => events.fire("action_toggle_history", { enabled: !state })}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonsConnect = () => {
|
||||||
|
const connectNewTab = useContext(ConnectDefaultNewTabContext);
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
|
||||||
|
let left;
|
||||||
|
if(connectNewTab) {
|
||||||
|
left = (
|
||||||
|
<Button
|
||||||
|
color={"green"}
|
||||||
|
type={"small"}
|
||||||
|
key={"same-tab"}
|
||||||
|
onClick={() => events.fire("action_connect", { newTab: false })}
|
||||||
|
className={cssStyle.button}
|
||||||
|
>
|
||||||
|
<Translatable>Connect in the same tab</Translatable>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
left = (
|
||||||
|
<Button
|
||||||
|
color={"green"}
|
||||||
|
type={"small"}
|
||||||
|
key={"new-tab"}
|
||||||
|
onClick={() => events.fire("action_connect", { newTab: true })}
|
||||||
|
className={cssStyle.button}
|
||||||
|
>
|
||||||
|
<Translatable>Connect in a new tab</Translatable>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.buttonsConnect}>
|
||||||
|
{left}
|
||||||
|
<Button
|
||||||
|
color={"green"}
|
||||||
|
type={"small"}
|
||||||
|
onClick={() => events.fire("action_connect", { newTab: connectNewTab })}
|
||||||
|
className={cssStyle.button}
|
||||||
|
>
|
||||||
|
<Translatable>Connect</Translatable>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ButtonContainer = () => (
|
||||||
|
<div className={cssStyle.buttonContainer}>
|
||||||
|
<ButtonToggleHistory />
|
||||||
|
<ButtonsConnect />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CountryIcon = (props: { country: string }) => {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.countryContainer}>
|
||||||
|
<div className={"country flag-" + props.country} />
|
||||||
|
{i18n.country_name(props.country, useTr("Global"))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryTableEntryConnectCount = React.memo((props: { entry: ConnectHistoryEntry }) => {
|
||||||
|
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", {
|
||||||
|
target,
|
||||||
|
targetType
|
||||||
|
});
|
||||||
|
return -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>;
|
||||||
|
} 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 icon = getIconManager().resolveIcon(info ? info.icon.iconId : 0, info?.icon.serverUniqueId, info?.icon.handlerId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.row + " " + (props.selected ? cssStyle.selected : "")}
|
||||||
|
onClick={event => {
|
||||||
|
if(event.isDefaultPrevented()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.fire("action_select_history", { id: props.entry.id });
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => events.fire("action_connect", { newTab: connectNewTab })}
|
||||||
|
>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.delete} onClick={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if(props.entry.uniqueServerId === kUnknownHistoryServerUniqueId) {
|
||||||
|
events.fire("action_delete_history", {
|
||||||
|
targetType: "address",
|
||||||
|
target: props.entry.targetAddress
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
events.fire("action_delete_history", {
|
||||||
|
targetType: "server-unique-id",
|
||||||
|
target: props.entry.uniqueServerId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<ClientIconRenderer icon={ClientIcon.Delete} />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.name}>
|
||||||
|
<RemoteIconRenderer icon={icon} className={cssStyle.iconContainer} />
|
||||||
|
{info?.name}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.address}>
|
||||||
|
{props.entry.targetAddress}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.password}>
|
||||||
|
{info ? info.password ? tr("Yes") : tr("No") : ""}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.country}>
|
||||||
|
{info ? <CountryIcon country={info.country || "xx"} key={"country"} /> : null}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.clients}>
|
||||||
|
{info && info.maxClients !== -1 ? `${info.clients}/${info.maxClients}` : ""}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.connections}>
|
||||||
|
<HistoryTableEntryConnectCount entry={props.entry} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const HistoryTable = () => {
|
||||||
|
const history = useProperty("history", 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} />);
|
||||||
|
} else {
|
||||||
|
body = (
|
||||||
|
<div className={cssStyle.bodyEmpty} key={"no-history"}>
|
||||||
|
<a><Translatable>No connections yet made</Translatable></a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.historyTable}>
|
||||||
|
<div className={cssStyle.head}>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.delete} />
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.name}>
|
||||||
|
<Translatable>Name</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.address}>
|
||||||
|
<Translatable>Address</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.password}>
|
||||||
|
<Translatable>Password</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.country}>
|
||||||
|
<Translatable>Country</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.clients}>
|
||||||
|
<Translatable>Clients</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.column + " " + cssStyle.connections}>
|
||||||
|
<Translatable>Connections</Translatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.body}>
|
||||||
|
{body}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryContainer = () => {
|
||||||
|
const historyShown = useProperty("historyShown", false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={joinClassList(cssStyle.historyContainer, historyShown && cssStyle.shown)}>
|
||||||
|
<HistoryTable />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export class ConnectModal extends InternalModal {
|
export class ConnectModal extends InternalModal {
|
||||||
private readonly events: Registry<ConnectUiEvents>;
|
private readonly events: Registry<ConnectUiEvents>;
|
||||||
|
@ -216,6 +416,8 @@ export class ConnectModal extends InternalModal {
|
||||||
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
|
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
|
||||||
<div className={cssStyle.container}>
|
<div className={cssStyle.container}>
|
||||||
<ConnectContainer />
|
<ConnectContainer />
|
||||||
|
<ButtonContainer />
|
||||||
|
<HistoryContainer />
|
||||||
</div>
|
</div>
|
||||||
</ConnectDefaultNewTabContext.Provider>
|
</ConnectDefaultNewTabContext.Provider>
|
||||||
</EventContext.Provider>
|
</EventContext.Provider>
|
||||||
|
@ -229,4 +431,8 @@ export class ConnectModal extends InternalModal {
|
||||||
color(): "none" | "blue" {
|
color(): "none" | "blue" {
|
||||||
return "blue";
|
return "blue";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verticalAlignment(): "top" | "center" | "bottom" {
|
||||||
|
return "top";
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue