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 {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 {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 {
|
||||
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() {
|
||||
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() {
|
||||
|
||||
Object.keys(this.propertyProvider).forEach(key => delete this.propertyProvider[key]);
|
||||
this.uiEvents.destroy();
|
||||
}
|
||||
|
||||
|
||||
private sendProperty(property: keyof ConnectProperties) {
|
||||
switch (property) {
|
||||
case "address":
|
||||
generateConnectParameters() : ConnectParameters | undefined {
|
||||
if(Object.keys(this.validStates).findIndex(key => this.validStates[key] === false) !== -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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", () => {
|
||||
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;
|
|
@ -1,4 +1,5 @@
|
|||
import {kUnknownHistoryServerUniqueId} from "tc-shared/connectionlog/History";
|
||||
import { RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
|
||||
export type ConnectProfileEntry = {
|
||||
id: string,
|
||||
|
@ -13,36 +14,36 @@ export type ConnectHistoryEntry = {
|
|||
}
|
||||
|
||||
export type ConnectHistoryServerInfo = {
|
||||
iconId: number,
|
||||
icon: RemoteIconInfo,
|
||||
name: string,
|
||||
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 {
|
||||
address: ConnectServerAddress,
|
||||
nickname: ConnectServerNickname,
|
||||
password: string,
|
||||
profiles: ConnectProfiles,
|
||||
address: {
|
||||
currentAddress: string,
|
||||
defaultAddress: string,
|
||||
},
|
||||
nickname: {
|
||||
currentNickname: string | undefined,
|
||||
defaultNickname: string | undefined,
|
||||
},
|
||||
password: {
|
||||
password: string,
|
||||
hashed: boolean
|
||||
} | undefined,
|
||||
profiles: {
|
||||
profiles: ConnectProfileEntry[],
|
||||
selected: string
|
||||
},
|
||||
historyShown: boolean,
|
||||
history: {
|
||||
history: ConnectHistoryEntry[],
|
||||
selected: number | -1,
|
||||
state: "shown" | "hidden"
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -50,11 +51,12 @@ export interface PropertyValidState {
|
|||
address: boolean,
|
||||
nickname: boolean,
|
||||
password: boolean,
|
||||
profile: boolean
|
||||
}
|
||||
|
||||
type ConnectProperty<T extends keyof ConnectProperties> = {
|
||||
type IAccess<I, T extends keyof I> = {
|
||||
property: T,
|
||||
value: ConnectProperties[T]
|
||||
value: I[T]
|
||||
};
|
||||
|
||||
export interface ConnectUiEvents {
|
||||
|
@ -66,13 +68,20 @@ export interface ConnectUiEvents {
|
|||
action_delete_history: {
|
||||
target: string,
|
||||
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: {
|
||||
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: {
|
||||
serverUniqueId: string
|
||||
|
|
|
@ -5,330 +5,36 @@
|
|||
@include user-select(none);
|
||||
|
||||
font-size: 1rem;
|
||||
padding: 1em;
|
||||
|
||||
width: 50em;
|
||||
width: 60em;
|
||||
min-width: 25em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex!important;
|
||||
flex-direction: column!important;
|
||||
justify-content: stretch!important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.container-last-servers {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
> * {
|
||||
padding-left: 1.5em;
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
padding-top: .75em;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
border-left: 2px solid #0073d4;
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
|
||||
|
@ -337,8 +43,9 @@
|
|||
justify-content: stretch;
|
||||
|
||||
.inputAddress, .inputNickname {
|
||||
width: 75%;
|
||||
width: 35em;
|
||||
min-width: 10em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
@ -347,12 +54,11 @@
|
|||
}
|
||||
|
||||
.inputPassword, .inputProfile {
|
||||
width: 25%;
|
||||
|
||||
width: 25em;
|
||||
min-width: 15em;
|
||||
max-width: 21em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
|
@ -362,10 +68,15 @@
|
|||
justify-content: stretch;
|
||||
|
||||
.input {
|
||||
overflow: visible;
|
||||
min-width: 0;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
.invalidFeedback {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.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) {
|
||||
.container {
|
||||
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 {
|
||||
.inputAddress, .inputNickname {
|
||||
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 {Registry} from "tc-shared/events";
|
||||
import * as React from "react";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {ControlledSelect, FlatInputField, Select} from "tc-shared/ui/react-elements/InputField";
|
||||
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {ControlledFlatInputField, ControlledSelect, FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
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 ConnectDefaultNewTabContext = React.createContext<boolean>(false);
|
||||
|
||||
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 {
|
||||
const events = useContext(EventContext);
|
||||
const [ value, setValue ] = useState<ConnectProperties[T] | V>(() => {
|
||||
|
@ -110,55 +35,93 @@ function useProperty<T extends keyof ConnectProperties, V>(key: T, defaultValue:
|
|||
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 events = useContext(EventContext);
|
||||
const address = useProperty("address", undefined);
|
||||
const valid = usePropertyValid("address", true);
|
||||
const newTab = useContext(ConnectDefaultNewTabContext);
|
||||
|
||||
return (
|
||||
<FlatInputField
|
||||
<ControlledFlatInputField
|
||||
value={address?.currentAddress || ""}
|
||||
placeholder={address?.defaultAddress || tr("Please enter a address")}
|
||||
|
||||
className={cssStyle.inputAddress}
|
||||
value={"ts.teaspeak.de"}
|
||||
placeholder={"ts.teaspeak.de"}
|
||||
|
||||
label={<Translatable>Server address</Translatable>}
|
||||
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 events = useContext(EventContext);
|
||||
const password = useProperty("password", undefined);
|
||||
|
||||
return (
|
||||
<FlatInputField
|
||||
className={cssStyle.inputPassword}
|
||||
value={"ts.teaspeak.de"}
|
||||
placeholder={"ts.teaspeak.de"}
|
||||
value={!password?.hashed ? password?.password || "" : ""}
|
||||
placeholder={password?.hashed ? tr("Password Hidden") : null}
|
||||
type={"password"}
|
||||
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 events = useContext(EventContext);
|
||||
const nickname = useProperty("nickname", undefined);
|
||||
const valid = usePropertyValid("nickname", true);
|
||||
|
||||
return (
|
||||
<FlatInputField
|
||||
<ControlledFlatInputField
|
||||
className={cssStyle.inputNickname}
|
||||
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>}
|
||||
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 events = useContext(EventContext);
|
||||
const profiles = useProperty("profiles", undefined);
|
||||
const selectedProfile = profiles?.profiles.find(profile => profile.id === profiles?.selected);
|
||||
|
||||
let invalidMarker;
|
||||
if(profiles) {
|
||||
if(!selectedProfile) {
|
||||
invalidMarker = <Translatable key={"no-profile"}>Select a profile</Translatable>;
|
||||
if(!profiles.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>;
|
||||
} else if(!selectedProfile) {
|
||||
invalidMarker = <Translatable key={"no-profile"}>Unknown select profile</Translatable>;
|
||||
} 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}>
|
||||
<ControlledSelect
|
||||
className={cssStyle.input}
|
||||
value={profiles?.selected || "loading"}
|
||||
value={selectedProfile ? selectedProfile.id : profiles?.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 })}
|
||||
>
|
||||
<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>
|
||||
{profiles?.profiles.forEach(profile => {
|
||||
return (
|
||||
<option key={"profile-" + profile.id}>{profile.name}</option>
|
||||
);
|
||||
})}
|
||||
{profiles?.profiles.map(profile => (
|
||||
<option key={"profile-" + profile.id} value={profile.id}>{profile.name}</option>
|
||||
))}
|
||||
</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>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -192,12 +156,248 @@ const ConnectContainer = () => (
|
|||
<InputServerAddress />
|
||||
<InputServerPassword />
|
||||
</div>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.row + " " + cssStyle.smallColumn}>
|
||||
<InputNickname />
|
||||
<InputProfile />
|
||||
</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 {
|
||||
private readonly events: Registry<ConnectUiEvents>;
|
||||
|
@ -216,6 +416,8 @@ export class ConnectModal extends InternalModal {
|
|||
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
|
||||
<div className={cssStyle.container}>
|
||||
<ConnectContainer />
|
||||
<ButtonContainer />
|
||||
<HistoryContainer />
|
||||
</div>
|
||||
</ConnectDefaultNewTabContext.Provider>
|
||||
</EventContext.Provider>
|
||||
|
@ -229,4 +431,8 @@ export class ConnectModal extends InternalModal {
|
|||
color(): "none" | "blue" {
|
||||
return "blue";
|
||||
}
|
||||
|
||||
verticalAlignment(): "top" | "center" | "bottom" {
|
||||
return "top";
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue