Temp changed for connect modal
parent
38d655eee4
commit
c3b64447db
|
@ -337,7 +337,7 @@ export class ConnectionHandler {
|
|||
}
|
||||
|
||||
if(user_action) {
|
||||
this.currentConnectId = await connectionHistory.logConnectionAttempt(originalAddress.host, originalAddress.port);
|
||||
this.currentConnectId = await connectionHistory.logConnectionAttempt(originalAddress.host + (originalAddress.port === 9987 ? "" : (":" + originalAddress.port)));
|
||||
} else {
|
||||
this.currentConnectId = -1;
|
||||
}
|
||||
|
@ -921,7 +921,7 @@ export class ConnectionHandler {
|
|||
errorMessage = tr("lookup the console");
|
||||
}
|
||||
|
||||
logWarn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error);
|
||||
logWarn(LogCategory.VOICE, tr("Failed to start microphone input (%o)."), error);
|
||||
if(notifyError) {
|
||||
this.lastRecordErrorPopup = Date.now();
|
||||
createErrorModal(tr("Failed to start recording"), tra("Microphone start failed.\nError: {}", errorMessage)).open();
|
||||
|
|
|
@ -6,16 +6,15 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
|||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {ServerProperties} from "tc-shared/tree/Server";
|
||||
|
||||
const kUnknownServerUniqueId = "unknown";
|
||||
export const kUnknownHistoryServerUniqueId = "unknown";
|
||||
|
||||
export type ConnectionHistoryEntry = {
|
||||
id: number,
|
||||
timestamp: number,
|
||||
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
|
||||
serverUniqueId: string | typeof kUnknownServerUniqueId
|
||||
/* Target address how it has been given by the user */
|
||||
targetAddress: string;
|
||||
serverUniqueId: string | typeof kUnknownHistoryServerUniqueId
|
||||
};
|
||||
|
||||
export type ConnectionHistoryServerEntry = {
|
||||
|
@ -30,6 +29,8 @@ export type ConnectionHistoryServerInfo = {
|
|||
name: string,
|
||||
iconId: number,
|
||||
|
||||
country: string,
|
||||
|
||||
/* These properties are only available upon server variable retrieval */
|
||||
clientsOnline: number | -1,
|
||||
clientsMax: number | -1,
|
||||
|
@ -49,28 +50,42 @@ export class ConnectionHistory {
|
|||
switch (event.oldVersion) {
|
||||
case 0:
|
||||
if(!database.objectStoreNames.contains("attempt-history")) {
|
||||
/*
|
||||
Schema:
|
||||
{
|
||||
timestamp: number,
|
||||
targetAddress: string,
|
||||
serverUniqueId: string | typeof kUnknownHistoryServerUniqueId
|
||||
}
|
||||
*/
|
||||
const store = database.createObjectStore("attempt-history", { keyPath: "id", autoIncrement: true });
|
||||
store.createIndex("timestamp", "timestamp", { unique: false });
|
||||
store.createIndex("targetHost", "targetHost", { unique: false });
|
||||
store.createIndex("targetPort", "targetPort", { unique: false });
|
||||
store.createIndex("targetAddress", "targetAddress", { unique: false });
|
||||
store.createIndex("serverUniqueId", "serverUniqueId", { unique: false });
|
||||
}
|
||||
|
||||
if(!database.objectStoreNames.contains("server-info")) {
|
||||
const store = database.createObjectStore("server-info", { keyPath: "uniqueId" });
|
||||
store.createIndex("firstConnectTimestamp", "firstConnectTimestamp", { unique: false });
|
||||
store.createIndex("firstConnectId", "firstConnectId", { unique: false });
|
||||
database.createObjectStore("server-info", { keyPath: "uniqueId" });
|
||||
/*
|
||||
Schema:
|
||||
{
|
||||
firstConnectTimestamp: number,
|
||||
firstConnectId: number,
|
||||
|
||||
store.createIndex("lastConnectTimestamp", "lastConnectTimestamp", { unique: false });
|
||||
store.createIndex("lastConnectId", "lastConnectId", { unique: false });
|
||||
lastConnectTimestamp: number,
|
||||
lastConnectId: number,
|
||||
|
||||
store.createIndex("name", "name", { unique: false });
|
||||
store.createIndex("iconId", "iconId", { unique: false });
|
||||
name: string,
|
||||
iconId: number,
|
||||
|
||||
store.createIndex("clientsOnline", "clientsOnline", { unique: false });
|
||||
store.createIndex("clientsMax", "clientsMax", { unique: false });
|
||||
country: string,
|
||||
|
||||
store.createIndex("passwordProtected", "passwordProtected", { unique: false });
|
||||
clientsOnline: number | -1,
|
||||
clientsMax: number | -1,
|
||||
|
||||
passwordProtected: boolean
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/* fall through wanted */
|
||||
|
@ -96,11 +111,10 @@ export class ConnectionHistory {
|
|||
|
||||
/**
|
||||
* Register a new connection attempt.
|
||||
* @param targetHost
|
||||
* @param targetPort
|
||||
* @param targetAddress
|
||||
* @return Returns a unique connect attempt identifier id which could be later used to set the unique server id.
|
||||
*/
|
||||
async logConnectionAttempt(targetHost: string, targetPort: number) : Promise<number> {
|
||||
async logConnectionAttempt(targetAddress: string) : Promise<number> {
|
||||
if(!this.database) {
|
||||
return;
|
||||
}
|
||||
|
@ -111,9 +125,8 @@ export class ConnectionHistory {
|
|||
const id = await new Promise<IDBValidKey>((resolve, reject) => {
|
||||
const insert = store.put({
|
||||
timestamp: Date.now(),
|
||||
targetHost: targetHost,
|
||||
targetPort: targetPort,
|
||||
serverUniqueId: kUnknownServerUniqueId
|
||||
targetAddress: targetAddress,
|
||||
serverUniqueId: kUnknownHistoryServerUniqueId
|
||||
});
|
||||
|
||||
insert.onsuccess = () => resolve(insert.result);
|
||||
|
@ -128,8 +141,8 @@ export class ConnectionHistory {
|
|||
return id;
|
||||
}
|
||||
|
||||
private async resolveDatabaseServerInfo(serverUniqueId: string) : Promise<IDBCursorWithValue | null> {
|
||||
const transaction = this.database.transaction(["server-info"], "readwrite");
|
||||
private async resolveDatabaseServerInfo(serverUniqueId: string, mode: IDBTransactionMode) : Promise<IDBCursorWithValue | null> {
|
||||
const transaction = this.database.transaction(["server-info"], mode);
|
||||
const store = transaction.objectStore("server-info");
|
||||
|
||||
return await new Promise<IDBCursorWithValue | null>((resolve, reject) => {
|
||||
|
@ -140,7 +153,7 @@ export class ConnectionHistory {
|
|||
}
|
||||
|
||||
private async updateDatabaseServerInfo(serverUniqueId: string, updateCallback: (databaseValue) => void) {
|
||||
let entry = await this.resolveDatabaseServerInfo(serverUniqueId);
|
||||
let entry = await this.resolveDatabaseServerInfo(serverUniqueId, "readwrite");
|
||||
|
||||
if(entry) {
|
||||
const newValue = Object.assign({}, entry.value);
|
||||
|
@ -209,7 +222,7 @@ export class ConnectionHistory {
|
|||
if(entry.value.serverUniqueId === serverUniqueId) {
|
||||
logWarn(LogCategory.GENERAL, tr("updateConnectionServerUniqueId(...) has been called twice"));
|
||||
return;
|
||||
} else if(entry.value.serverUniqueId !== kUnknownServerUniqueId) {
|
||||
} else if(entry.value.serverUniqueId !== kUnknownHistoryServerUniqueId) {
|
||||
throw tr("connection attempt has already a server unique id set");
|
||||
}
|
||||
|
||||
|
@ -263,7 +276,7 @@ export class ConnectionHistory {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
let entry = await this.resolveDatabaseServerInfo(serverUniqueId);
|
||||
let entry = await this.resolveDatabaseServerInfo(serverUniqueId, "readonly");
|
||||
if(!entry) {
|
||||
return;
|
||||
}
|
||||
|
@ -279,6 +292,8 @@ export class ConnectionHistory {
|
|||
name: value.name,
|
||||
iconId: value.iconId,
|
||||
|
||||
country: value.country,
|
||||
|
||||
clientsOnline: value.clientsOnline,
|
||||
clientsMax: value.clientsMax,
|
||||
|
||||
|
@ -297,7 +312,7 @@ export class ConnectionHistory {
|
|||
|
||||
const result: ConnectionHistoryEntry[] = [];
|
||||
|
||||
const transaction = this.database.transaction(["attempt-history"], "readwrite");
|
||||
const transaction = this.database.transaction(["attempt-history"], "readonly");
|
||||
const store = transaction.objectStore("attempt-history");
|
||||
|
||||
const cursor = store.index("timestamp").openCursor(undefined, "prev");
|
||||
|
@ -315,21 +330,17 @@ export class ConnectionHistory {
|
|||
id: entry.value.id,
|
||||
timestamp: entry.value.timestamp,
|
||||
|
||||
targetHost: entry.value.targetHost,
|
||||
targetPort: entry.value.targetPort,
|
||||
|
||||
targetAddress: entry.value.targetAddress,
|
||||
serverUniqueId: entry.value.serverUniqueId,
|
||||
} as ConnectionHistoryEntry;
|
||||
entry.continue();
|
||||
|
||||
if(parsedEntry.serverUniqueId !== kUnknownServerUniqueId) {
|
||||
if(parsedEntry.serverUniqueId !== kUnknownHistoryServerUniqueId) {
|
||||
if(result.findIndex(entry => entry.serverUniqueId === parsedEntry.serverUniqueId) !== -1) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if(result.findIndex(entry => {
|
||||
return entry.targetHost === parsedEntry.targetHost && entry.targetPort === parsedEntry.targetPort;
|
||||
}) !== -1) {
|
||||
if(result.findIndex(entry => entry.targetAddress === parsedEntry.targetAddress) !== -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -339,6 +350,22 @@ export class ConnectionHistory {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
async countConnectCount(target: string, targetType: "address" | "server-unique-id") : Promise<number> {
|
||||
if(!this.database) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const transaction = this.database.transaction(["attempt-history"], "readonly");
|
||||
const store = transaction.objectStore("attempt-history");
|
||||
|
||||
|
||||
const count = store.index(targetType === "server-unique-id" ? "serverUniqueId" : "targetAddress").count(target);
|
||||
return await new Promise<number>((resolve, reject) => {
|
||||
count.onsuccess = () => resolve(count.result);
|
||||
count.onerror = () => reject(count.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const kConnectServerInfoUpdatePropertyKeys: (keyof ServerProperties)[] = [
|
||||
|
@ -347,7 +374,8 @@ const kConnectServerInfoUpdatePropertyKeys: (keyof ServerProperties)[] = [
|
|||
"virtualserver_flag_password",
|
||||
"virtualserver_maxclients",
|
||||
"virtualserver_clientsonline",
|
||||
"virtualserver_flag_password"
|
||||
"virtualserver_flag_password",
|
||||
"virtualserver_country_code"
|
||||
];
|
||||
|
||||
class ConnectionHistoryUpdateListener {
|
||||
|
@ -394,6 +422,8 @@ class ConnectionHistoryUpdateListener {
|
|||
name: event.server_properties.virtualserver_name,
|
||||
iconId: event.server_properties.virtualserver_icon_id,
|
||||
|
||||
country: event.server_properties.virtualserver_country_code,
|
||||
|
||||
clientsMax: event.server_properties.virtualserver_maxclients,
|
||||
clientsOnline: event.server_properties.virtualserver_clientsonline,
|
||||
|
||||
|
|
|
@ -52,6 +52,8 @@ import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/Conn
|
|||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import ContextMenuEvent = JQuery.ContextMenuEvent;
|
||||
|
||||
import "./ui/modal/connect/Controller";
|
||||
|
||||
let preventWelcomeUI = false;
|
||||
async function initialize() {
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {ConnectProperties, ConnectUiEvents} 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";
|
||||
|
||||
class ConnectController {
|
||||
readonly uiEvents: Registry<ConnectUiEvents>;
|
||||
|
||||
constructor() {
|
||||
this.uiEvents = new Registry<ConnectUiEvents>();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private sendProperty(property: keyof ConnectProperties) {
|
||||
switch (property) {
|
||||
case "address":
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ConnectModalOptions = {
|
||||
connectInANewTab?: boolean,
|
||||
defaultAddress?: string,
|
||||
defaultProfile?: string
|
||||
}
|
||||
|
||||
export function spawnConnectModalNew(options: ConnectModalOptions) {
|
||||
const controller = new ConnectController();
|
||||
const modal = spawnReactModal(ConnectModal, controller.uiEvents, options.connectInANewTab || false);
|
||||
modal.show();
|
||||
|
||||
modal.events.one("destroy", () => {
|
||||
controller.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).spawnConnectModalNew = spawnConnectModalNew;
|
|
@ -0,0 +1,94 @@
|
|||
import {kUnknownHistoryServerUniqueId} from "tc-shared/connectionlog/History";
|
||||
|
||||
export type ConnectProfileEntry = {
|
||||
id: string,
|
||||
name: string,
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
export type ConnectHistoryEntry = {
|
||||
id: number,
|
||||
targetAddress: string,
|
||||
uniqueServerId: string | typeof kUnknownHistoryServerUniqueId,
|
||||
}
|
||||
|
||||
export type ConnectHistoryServerInfo = {
|
||||
iconId: number,
|
||||
name: string,
|
||||
password: boolean,
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
history: {
|
||||
history: ConnectHistoryEntry[],
|
||||
selected: number | -1,
|
||||
state: "shown" | "hidden"
|
||||
},
|
||||
}
|
||||
|
||||
export interface PropertyValidState {
|
||||
address: boolean,
|
||||
nickname: boolean,
|
||||
password: boolean,
|
||||
}
|
||||
|
||||
type ConnectProperty<T extends keyof ConnectProperties> = {
|
||||
property: T,
|
||||
value: ConnectProperties[T]
|
||||
};
|
||||
|
||||
export interface ConnectUiEvents {
|
||||
action_manage_profiles: {},
|
||||
action_select_profile: { id: string },
|
||||
action_select_history: { id: number },
|
||||
action_connect: { newTab: boolean },
|
||||
action_toggle_history: { enabled: boolean }
|
||||
action_delete_history: {
|
||||
target: string,
|
||||
targetType: "address" | "server-unique-id"
|
||||
}
|
||||
|
||||
query_property: {
|
||||
property: keyof ConnectProperties
|
||||
},
|
||||
|
||||
notify_property: ConnectProperty<keyof ConnectProperties>
|
||||
|
||||
query_history_entry: {
|
||||
serverUniqueId: string
|
||||
},
|
||||
query_history_connections: {
|
||||
target: string,
|
||||
targetType: "address" | "server-unique-id"
|
||||
}
|
||||
|
||||
notify_history_entry: {
|
||||
serverUniqueId: string,
|
||||
info: ConnectHistoryServerInfo
|
||||
},
|
||||
notify_history_connections: {
|
||||
target: string,
|
||||
targetType: "address" | "server-unique-id",
|
||||
value: number
|
||||
}
|
||||
}
|
|
@ -0,0 +1,418 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
.container {
|
||||
@include user-select(none);
|
||||
|
||||
font-size: 1rem;
|
||||
padding: 1em;
|
||||
|
||||
width: 50em;
|
||||
min-width: 25em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex!important;
|
||||
flex-direction: column!important;
|
||||
justify-content: stretch!important;
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.inputAddress, .inputNickname {
|
||||
width: 75%;
|
||||
min-width: 10em;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
.inputPassword, .inputProfile {
|
||||
width: 25%;
|
||||
|
||||
min-width: 15em;
|
||||
max-width: 21em;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.inputProfile {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.input {
|
||||
min-width: 0;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 2em;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
align-self: flex-end;
|
||||
margin-bottom: 1em;
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 55rem) {
|
||||
.container {
|
||||
padding: .5em!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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
import {ConnectProperties, ConnectUiEvents} from "tc-shared/ui/modal/connect/Definitions";
|
||||
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 {Button} from "tc-shared/ui/react-elements/Button";
|
||||
|
||||
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>(() => {
|
||||
events.fire("query_property", { property: key });
|
||||
return defaultValue;
|
||||
});
|
||||
events.reactUse("notify_property", event => event.property === key && setValue(event.value as any));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const InputServerAddress = () => {
|
||||
return (
|
||||
<FlatInputField
|
||||
className={cssStyle.inputAddress}
|
||||
value={"ts.teaspeak.de"}
|
||||
placeholder={"ts.teaspeak.de"}
|
||||
label={<Translatable>Server address</Translatable>}
|
||||
labelType={"static"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const InputServerPassword = () => {
|
||||
return (
|
||||
<FlatInputField
|
||||
className={cssStyle.inputPassword}
|
||||
value={"ts.teaspeak.de"}
|
||||
placeholder={"ts.teaspeak.de"}
|
||||
type={"password"}
|
||||
label={<Translatable>Server password</Translatable>}
|
||||
labelType={"floating"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const InputNickname = () => {
|
||||
const nickname = useProperty("nickname", undefined);
|
||||
|
||||
return (
|
||||
<FlatInputField
|
||||
className={cssStyle.inputNickname}
|
||||
value={nickname?.currentNickname || ""}
|
||||
placeholder={nickname ? nickname.defaultNickname : tr("loading...")}
|
||||
label={<Translatable>Nickname</Translatable>}
|
||||
labelType={"static"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const InputProfile = () => {
|
||||
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>;
|
||||
} else if(!selectedProfile.valid) {
|
||||
invalidMarker = <Translatable key={"invalid"}>Selected profile is invalid</Translatable>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.inputProfile}>
|
||||
<ControlledSelect
|
||||
className={cssStyle.input}
|
||||
value={profiles?.selected || "loading"}
|
||||
type={"flat"}
|
||||
label={<Translatable>Connect profile</Translatable>}
|
||||
invalid={invalidMarker}
|
||||
>
|
||||
<option key={"loading"} 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>
|
||||
);
|
||||
})}
|
||||
</ControlledSelect>
|
||||
<Button className={cssStyle.button} type={"small"} color={"none"}>
|
||||
<Translatable>Profiles</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ConnectContainer = () => (
|
||||
<div className={cssStyle.connectContainer}>
|
||||
<div className={cssStyle.row}>
|
||||
<InputServerAddress />
|
||||
<InputServerPassword />
|
||||
</div>
|
||||
<div className={cssStyle.row}>
|
||||
<InputNickname />
|
||||
<InputProfile />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export class ConnectModal extends InternalModal {
|
||||
private readonly events: Registry<ConnectUiEvents>;
|
||||
private readonly connectNewTabByDefault: boolean;
|
||||
|
||||
constructor(events: Registry<ConnectUiEvents>, connectNewTabByDefault: boolean) {
|
||||
super();
|
||||
|
||||
this.events = events;
|
||||
this.connectNewTabByDefault = connectNewTabByDefault;
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EventContext.Provider value={this.events}>
|
||||
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
|
||||
<div className={cssStyle.container}>
|
||||
<ConnectContainer />
|
||||
</div>
|
||||
</ConnectDefaultNewTabContext.Provider>
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement {
|
||||
return <Translatable>Connect to a server</Translatable>;
|
||||
}
|
||||
|
||||
color(): "none" | "blue" {
|
||||
return "blue";
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from "react";
|
||||
import {ReactElement} from "react";
|
||||
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
|
||||
|
||||
const cssStyle = require("./InputField.scss");
|
||||
|
||||
|
@ -128,6 +129,8 @@ export interface FlatInputFieldProperties {
|
|||
labelClassName?: string;
|
||||
labelFloatingClassName?: string;
|
||||
|
||||
type?: "text" | "password" | "number";
|
||||
|
||||
help?: string | React.ReactElement;
|
||||
helpClassName?: string;
|
||||
|
||||
|
@ -173,19 +176,20 @@ export class FlatInputField extends React.Component<FlatInputFieldProperties, Fl
|
|||
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : typeof this.props.disabled === "boolean" ? this.props.disabled : false;
|
||||
const readOnly = typeof this.state.editable === "boolean" ? !this.state.editable : typeof this.props.editable === "boolean" ? !this.props.editable : false;
|
||||
const placeholder = typeof this.state.placeholder === "string" ? this.state.placeholder : typeof this.props.placeholder === "string" ? this.props.placeholder : undefined;
|
||||
const filled = this.state.filled || this.props.value?.length > 0;
|
||||
|
||||
return (
|
||||
<div className={cssStyle.containerFlat + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (this.state.filled ? cssStyle.isFilled : "") + " " + (this.props.className || "")}>
|
||||
<div className={cssStyle.containerFlat + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (filled ? cssStyle.isFilled : "") + " " + (this.props.className || "")}>
|
||||
{this.props.label ?
|
||||
<label className={
|
||||
cssStyle["type-" + (this.props.labelType || "static")] + " " +
|
||||
(this.props.labelClassName || "") + " " +
|
||||
(this.props.labelFloatingClassName && this.state.filled ? this.props.labelFloatingClassName : "")}>{this.props.label}</label> : undefined}
|
||||
(this.props.labelFloatingClassName && filled ? this.props.labelFloatingClassName : "")}>{this.props.label}</label> : undefined}
|
||||
<input
|
||||
defaultValue={this.props.defaultValue}
|
||||
value={this.props.value}
|
||||
|
||||
type={"text"}
|
||||
type={this.props.type || "text"}
|
||||
ref={this.refInput}
|
||||
readOnly={readOnly}
|
||||
disabled={disabled}
|
||||
|
@ -228,6 +232,61 @@ export class FlatInputField extends React.Component<FlatInputFieldProperties, Fl
|
|||
}
|
||||
}
|
||||
|
||||
export const ControlledSelect = (props: {
|
||||
type?: "flat" | "boxed",
|
||||
className?: string,
|
||||
|
||||
value: string,
|
||||
placeHolder?: string,
|
||||
|
||||
label?: React.ReactNode,
|
||||
labelClassName?: string,
|
||||
|
||||
help?: React.ReactNode,
|
||||
helpClassName?: string,
|
||||
|
||||
invalid?: React.ReactNode,
|
||||
invalidClassName?: string,
|
||||
|
||||
disabled?: boolean,
|
||||
|
||||
onFocus?: () => void,
|
||||
onBlur?: () => void,
|
||||
|
||||
onChange?: (event?: React.ChangeEvent<HTMLSelectElement>) => void,
|
||||
|
||||
children: React.ReactElement<HTMLOptionElement | HTMLOptGroupElement> | React.ReactElement<HTMLOptionElement | HTMLOptGroupElement>[]
|
||||
}) => {
|
||||
const disabled = typeof props.disabled === "boolean" ? props.disabled : false;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={joinClassList(
|
||||
props.type === "boxed" ? cssStyle.containerBoxed : cssStyle.containerFlat,
|
||||
cssStyle["size-normal"],
|
||||
props.invalid ? cssStyle.isInvalid : undefined,
|
||||
props.className,
|
||||
cssStyle.noLeftIcon, cssStyle.noRightIcon
|
||||
)}
|
||||
>
|
||||
{!props.label ? undefined :
|
||||
<label className={joinClassList(cssStyle["type-static"], props.labelClassName)} key={"label"}>{props.label}</label>
|
||||
}
|
||||
<select
|
||||
value={props.value}
|
||||
disabled={disabled}
|
||||
|
||||
onFocus={props.onFocus}
|
||||
onBlur={props.onBlur}
|
||||
onChange={props.onChange}
|
||||
>
|
||||
{props.children}
|
||||
</select>
|
||||
{props.invalid ? <small className={joinClassList(cssStyle.invalidFeedback, props.invalidClassName)} key={"invalid"}>{props.invalid}</small> : undefined}
|
||||
{props.help ? <small className={joinClassList(cssStyle.invalidFeedback, props.help)} key={"help"}>{props.help}</small> : undefined}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectProperties {
|
||||
type?: "flat" | "boxed";
|
||||
|
|
|
@ -42,11 +42,14 @@ class BodyRenderer {
|
|||
}
|
||||
|
||||
setInstance(instance: AbstractModal) {
|
||||
if(this.modalInstance)
|
||||
if(this.modalInstance) {
|
||||
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
||||
}
|
||||
|
||||
this.modalInstance = instance;
|
||||
if(this.modalInstance)
|
||||
if(this.modalInstance) {
|
||||
ReactDOM.render(<>{this.modalInstance.renderBody()}</>, this.htmlContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||
|
||||
const cssStyle = require("./Modal.scss");
|
||||
|
||||
|
@ -23,7 +24,11 @@ export const InternalModalContentRenderer = (props: {
|
|||
<div className={cssStyle.icon}>
|
||||
<img src="img/favicon/teacup.png" alt={tr("Modal - Icon")} />
|
||||
</div>
|
||||
<div className={cssStyle.title + " " + props.headerTitleClass}>{props.modal.title()}</div>
|
||||
<div className={cssStyle.title + " " + props.headerTitleClass}>
|
||||
<ErrorBoundary>
|
||||
{props.modal.title()}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
{!props.onMinimize ? undefined : (
|
||||
<div className={cssStyle.button} onClick={props.onMinimize}>
|
||||
<div className={"icon_em " + ClientIcon.MinimizeButton} />
|
||||
|
@ -36,7 +41,9 @@ export const InternalModalContentRenderer = (props: {
|
|||
)}
|
||||
</div>
|
||||
<div className={cssStyle.body + " " + props.bodyClass}>
|
||||
{props.modal.renderBody()}
|
||||
<ErrorBoundary>
|
||||
{props.modal.renderBody()}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue