Removed the old server info modal and using the new React based and popoutable modal
This commit is contained in:
parent
d6449760bb
commit
7ed13f5b6a
21 changed files with 1131 additions and 690 deletions
|
@ -21,7 +21,6 @@ import "./static/modal-query.scss"
|
||||||
import "./static/modal-server.scss"
|
import "./static/modal-server.scss"
|
||||||
import "./static/modal-musicmanage.scss"
|
import "./static/modal-musicmanage.scss"
|
||||||
import "./static/modal-serverinfobandwidth.scss"
|
import "./static/modal-serverinfobandwidth.scss"
|
||||||
import "./static/modal-serverinfo.scss"
|
|
||||||
import "./static/modal-settings.scss"
|
import "./static/modal-settings.scss"
|
||||||
import "./static/overlay-image-preview.scss"
|
import "./static/overlay-image-preview.scss"
|
||||||
import "./static/color-variables.scss"
|
import "./static/color-variables.scss"
|
||||||
|
|
|
@ -42,4 +42,14 @@ html:root {
|
||||||
|
|
||||||
/* The host banner */
|
/* The host banner */
|
||||||
--hostbanner-background: #2e2e2e;
|
--hostbanner-background: #2e2e2e;
|
||||||
|
|
||||||
|
/* Server Info */
|
||||||
|
--serverinfo-background: #2f2f35;
|
||||||
|
--serverinfo-hostbanner-background: #26222a;
|
||||||
|
|
||||||
|
--serverinfo-group-border: #1f2122;
|
||||||
|
--serverinfo-group-background: #28292b;
|
||||||
|
|
||||||
|
--serverinfo-key: #557edc;
|
||||||
|
--serverinfo-value: #d6d6d7;
|
||||||
}
|
}
|
|
@ -1,267 +0,0 @@
|
||||||
@import "mixin";
|
|
||||||
|
|
||||||
html:root {
|
|
||||||
--serverinfo-background: #2f2f35;
|
|
||||||
--serverinfo-hostbanner-background: #26222a;
|
|
||||||
|
|
||||||
--serverinfo-group-border: #1f2122;
|
|
||||||
--serverinfo-group-background: #28292b;
|
|
||||||
|
|
||||||
--serverinfo-key: #557edc;
|
|
||||||
--serverinfo-value: #d6d6d7;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global {
|
|
||||||
.modal-body.modal-server-info {
|
|
||||||
padding: 0!important;
|
|
||||||
width: 55em;
|
|
||||||
|
|
||||||
display: flex!important;
|
|
||||||
flex-direction: column!important;
|
|
||||||
justify-content: flex-start!important;
|
|
||||||
|
|
||||||
background-color: var(--serverinfo-background);
|
|
||||||
|
|
||||||
.container-tooltip {
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
width: 1.6em;
|
|
||||||
margin-left: .5em;
|
|
||||||
margin-right: .5em;
|
|
||||||
font-size: .9em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 1em;
|
|
||||||
width: 1em;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-top {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
max-height: 9em;
|
|
||||||
//width: 30em; /* set a default width where we have to grow/shrink */
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
.container-hostbanner {
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
background-color: var(--serverinfo-hostbanner-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-body {
|
|
||||||
flex-shrink: 1;
|
|
||||||
min-height: 12em; /* 10em + 2 * 1em margin */
|
|
||||||
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
@include chat-scrollbar-vertical();
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
margin: 1em;
|
|
||||||
padding: .5em;
|
|
||||||
|
|
||||||
border-radius: .2em;
|
|
||||||
border: 1px solid var(--serverinfo-group-border);
|
|
||||||
|
|
||||||
background-color: var(--serverinfo-group-background);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
height: 10em;
|
|
||||||
max-height: 10em;
|
|
||||||
|
|
||||||
.container-image {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
max-width: 15em;
|
|
||||||
max-height: 9em; /* minus one padding */
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
object-fit: contain;
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
margin-right: 2em;
|
|
||||||
@include transition(.25s ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-properties {
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
min-width: 20em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
height: inherit;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
height: 1.8em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
.key {
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
|
|
||||||
color: var(--serverinfo-key);
|
|
||||||
text-transform: uppercase;
|
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
width: 15em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
color: var(--serverinfo-value);
|
|
||||||
align-self: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
.country {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: .25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.server-version {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
a {
|
|
||||||
flex-shrink: 1;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-network {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.container-button {
|
|
||||||
margin-right: 1em;
|
|
||||||
|
|
||||||
flex-shrink: 1e8;
|
|
||||||
min-width: 5em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: 2.5em;
|
|
||||||
width: 12em;
|
|
||||||
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
min-width: 10em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.reverse {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
.container-image {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-left: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-properties {
|
|
||||||
.row {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-buttons {
|
|
||||||
margin: 1em;
|
|
||||||
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
button {
|
|
||||||
min-width: 8em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 50em) {
|
|
||||||
:global {
|
|
||||||
.modal-body.modal-server-info {
|
|
||||||
.container-image {
|
|
||||||
margin: 0!important;
|
|
||||||
max-width: 0!important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,118 +2,28 @@ import {ChannelTree} from "./ChannelTree";
|
||||||
import {Settings, settings} from "../settings";
|
import {Settings, settings} from "../settings";
|
||||||
import * as contextmenu from "../ui/elements/ContextMenu";
|
import * as contextmenu from "../ui/elements/ContextMenu";
|
||||||
import * as log from "../log";
|
import * as log from "../log";
|
||||||
import {LogCategory, logInfo, LogType} from "../log";
|
import {LogCategory, logError, logInfo, LogType} from "../log";
|
||||||
import {Sound} from "../audio/Sounds";
|
import {Sound} from "../audio/Sounds";
|
||||||
import {openServerInfo} from "../ui/modal/ModalServerInfo";
|
|
||||||
import {createServerModal} from "../ui/modal/ModalServerEdit";
|
import {createServerModal} from "../ui/modal/ModalServerEdit";
|
||||||
import {spawnIconSelect} from "../ui/modal/ModalIconSelect";
|
import {spawnIconSelect} from "../ui/modal/ModalIconSelect";
|
||||||
import {spawnAvatarList} from "../ui/modal/ModalAvatarList";
|
import {spawnAvatarList} from "../ui/modal/ModalAvatarList";
|
||||||
import {Registry} from "../events";
|
import {Registry} from "../events";
|
||||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
|
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
import {spawnInviteGenerator} from "tc-shared/ui/modal/invite/Controller";
|
import {spawnInviteGenerator} from "tc-shared/ui/modal/invite/Controller";
|
||||||
|
import {HostBannerInfo, HostBannerInfoMode} from "tc-shared/ui/frames/HostBannerDefinitions";
|
||||||
|
import {spawnServerInfoNew} from "tc-shared/ui/modal/server-info/Controller";
|
||||||
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
|
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||||
|
import {
|
||||||
|
kServerConnectionInfoFields,
|
||||||
|
ServerConnectionInfo,
|
||||||
|
ServerConnectionInfoResult,
|
||||||
|
ServerProperties
|
||||||
|
} from "tc-shared/tree/ServerDefinitions";
|
||||||
|
|
||||||
export class ServerProperties {
|
/* TODO: Rework all imports */
|
||||||
virtualserver_host: string = "";
|
export * from "./ServerDefinitions";
|
||||||
virtualserver_port: number = 0;
|
|
||||||
|
|
||||||
virtualserver_name: string = "";
|
|
||||||
virtualserver_name_phonetic: string = "";
|
|
||||||
virtualserver_icon_id: number = 0;
|
|
||||||
virtualserver_version: string = "unknown";
|
|
||||||
virtualserver_platform: string = "unknown";
|
|
||||||
virtualserver_unique_identifier: string = "";
|
|
||||||
|
|
||||||
virtualserver_clientsonline: number = 0;
|
|
||||||
virtualserver_queryclientsonline: number = 0;
|
|
||||||
virtualserver_channelsonline: number = 0;
|
|
||||||
virtualserver_uptime: number = 0;
|
|
||||||
virtualserver_created: number = 0;
|
|
||||||
virtualserver_maxclients: number = 0;
|
|
||||||
virtualserver_reserved_slots: number = 0;
|
|
||||||
|
|
||||||
virtualserver_password: string = "";
|
|
||||||
virtualserver_flag_password: boolean = false;
|
|
||||||
|
|
||||||
virtualserver_ask_for_privilegekey: boolean = false;
|
|
||||||
|
|
||||||
virtualserver_welcomemessage: string = "";
|
|
||||||
|
|
||||||
virtualserver_hostmessage: string = "";
|
|
||||||
virtualserver_hostmessage_mode: number = 0;
|
|
||||||
|
|
||||||
virtualserver_hostbanner_url: string = "";
|
|
||||||
virtualserver_hostbanner_gfx_url: string = "";
|
|
||||||
virtualserver_hostbanner_gfx_interval: number = 0;
|
|
||||||
virtualserver_hostbanner_mode: number = 0;
|
|
||||||
|
|
||||||
virtualserver_hostbutton_tooltip: string = "";
|
|
||||||
virtualserver_hostbutton_url: string = "";
|
|
||||||
virtualserver_hostbutton_gfx_url: string = "";
|
|
||||||
|
|
||||||
virtualserver_codec_encryption_mode: number = 0;
|
|
||||||
|
|
||||||
virtualserver_default_music_group: number = 0;
|
|
||||||
virtualserver_default_server_group: number = 0;
|
|
||||||
virtualserver_default_channel_group: number = 0;
|
|
||||||
virtualserver_default_channel_admin_group: number = 0;
|
|
||||||
|
|
||||||
//Special requested properties
|
|
||||||
virtualserver_default_client_description: string = "";
|
|
||||||
virtualserver_default_channel_description: string = "";
|
|
||||||
virtualserver_default_channel_topic: string = "";
|
|
||||||
|
|
||||||
virtualserver_antiflood_points_tick_reduce: number = 0;
|
|
||||||
virtualserver_antiflood_points_needed_command_block: number = 0;
|
|
||||||
virtualserver_antiflood_points_needed_ip_block: number = 0;
|
|
||||||
|
|
||||||
virtualserver_country_code: string = "XX";
|
|
||||||
|
|
||||||
virtualserver_complain_autoban_count: number = 0;
|
|
||||||
virtualserver_complain_autoban_time: number = 0;
|
|
||||||
virtualserver_complain_remove_time: number = 0;
|
|
||||||
|
|
||||||
virtualserver_needed_identity_security_level: number = 8;
|
|
||||||
virtualserver_weblist_enabled: boolean = false;
|
|
||||||
virtualserver_min_clients_in_channel_before_forced_silence: number = 0;
|
|
||||||
virtualserver_channel_temp_delete_delay_default: number = 60;
|
|
||||||
virtualserver_priority_speaker_dimm_modificator: number = -18;
|
|
||||||
|
|
||||||
virtualserver_max_upload_total_bandwidth: number = 0;
|
|
||||||
virtualserver_upload_quota: number = 0;
|
|
||||||
virtualserver_max_download_total_bandwidth: number = 0;
|
|
||||||
virtualserver_download_quota: number = 0;
|
|
||||||
|
|
||||||
virtualserver_month_bytes_downloaded: number = 0;
|
|
||||||
virtualserver_month_bytes_uploaded: number = 0;
|
|
||||||
virtualserver_total_bytes_downloaded: number = 0;
|
|
||||||
virtualserver_total_bytes_uploaded: number = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerConnectionInfo {
|
|
||||||
connection_filetransfer_bandwidth_sent: number;
|
|
||||||
connection_filetransfer_bandwidth_received: number;
|
|
||||||
|
|
||||||
connection_filetransfer_bytes_sent_total: number;
|
|
||||||
connection_filetransfer_bytes_received_total: number;
|
|
||||||
|
|
||||||
connection_filetransfer_bytes_sent_month: number;
|
|
||||||
connection_filetransfer_bytes_received_month: number;
|
|
||||||
|
|
||||||
connection_packets_sent_total: number;
|
|
||||||
connection_bytes_sent_total: number;
|
|
||||||
connection_packets_received_total: number;
|
|
||||||
connection_bytes_received_total: number;
|
|
||||||
|
|
||||||
connection_bandwidth_sent_last_second_total: number;
|
|
||||||
connection_bandwidth_sent_last_minute_total: number;
|
|
||||||
connection_bandwidth_received_last_second_total: number;
|
|
||||||
connection_bandwidth_received_last_minute_total: number;
|
|
||||||
|
|
||||||
connection_connected_time: number;
|
|
||||||
connection_packetloss_total: number;
|
|
||||||
connection_ping: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerAddress {
|
export interface ServerAddress {
|
||||||
host: string;
|
host: string;
|
||||||
|
@ -163,7 +73,8 @@ export interface ServerEvents extends ChannelTreeEntryEvents {
|
||||||
notify_properties_updated: {
|
notify_properties_updated: {
|
||||||
updated_properties: Partial<ServerProperties>;
|
updated_properties: Partial<ServerProperties>;
|
||||||
server_properties: ServerProperties
|
server_properties: ServerProperties
|
||||||
}
|
},
|
||||||
|
notify_host_banner_updated: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
|
@ -177,6 +88,10 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
private info_request_promise_resolve: any = undefined;
|
private info_request_promise_resolve: any = undefined;
|
||||||
private info_request_promise_reject: any = undefined;
|
private info_request_promise_reject: any = undefined;
|
||||||
|
|
||||||
|
private requestInfoPromise: Promise<ServerConnectionInfoResult>;
|
||||||
|
private requestInfoPromiseTimestamp: number;
|
||||||
|
|
||||||
|
/* TODO: Remove this? */
|
||||||
private _info_connection_promise: Promise<ServerConnectionInfo>;
|
private _info_connection_promise: Promise<ServerConnectionInfo>;
|
||||||
private _info_connection_promise_timestamp: number;
|
private _info_connection_promise_timestamp: number;
|
||||||
private _info_connection_promise_resolve: any;
|
private _info_connection_promise_resolve: any;
|
||||||
|
@ -195,6 +110,17 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
this.channelTree = tree;
|
this.channelTree = tree;
|
||||||
this.remote_address = Object.assign({}, address); /* copy the address because it might get changed due to the DNS resolve */
|
this.remote_address = Object.assign({}, address); /* copy the address because it might get changed due to the DNS resolve */
|
||||||
this.properties.virtualserver_name = name;
|
this.properties.virtualserver_name = name;
|
||||||
|
|
||||||
|
this.events.on("notify_properties_updated", event => {
|
||||||
|
if(
|
||||||
|
"virtualserver_hostbanner_url" in event.updated_properties ||
|
||||||
|
"virtualserver_hostbanner_mode" in event.updated_properties ||
|
||||||
|
"virtualserver_hostbanner_gfx_url" in event.updated_properties ||
|
||||||
|
"virtualserver_hostbanner_gfx_interval" in event.updated_properties
|
||||||
|
) {
|
||||||
|
this.events.fire("notify_host_banner_updated");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -212,9 +138,7 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
{
|
{
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
name: tr("Show server info"),
|
name: tr("Show server info"),
|
||||||
callback: () => {
|
callback: () => spawnServerInfoNew(this.channelTree.client),
|
||||||
openServerInfo(this);
|
|
||||||
},
|
|
||||||
icon_class: "client-about"
|
icon_class: "client-about"
|
||||||
}, {
|
}, {
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
@ -350,11 +274,13 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
|
|
||||||
/* max 1s ago, so we could update every second */
|
/* max 1s ago, so we could update every second */
|
||||||
request_connection_info() : Promise<ServerConnectionInfo> {
|
request_connection_info() : Promise<ServerConnectionInfo> {
|
||||||
if(Date.now() - 900 < this._info_connection_promise_timestamp && this._info_connection_promise)
|
if(Date.now() - 900 < this._info_connection_promise_timestamp && this._info_connection_promise) {
|
||||||
return this._info_connection_promise;
|
return this._info_connection_promise;
|
||||||
|
}
|
||||||
|
|
||||||
if(this._info_connection_promise_reject)
|
if(this._info_connection_promise_reject) {
|
||||||
this._info_connection_promise_resolve("timeout");
|
this._info_connection_promise_resolve("timeout");
|
||||||
|
}
|
||||||
|
|
||||||
let _local_reject; /* to ensure we're using the right resolve! */
|
let _local_reject; /* to ensure we're using the right resolve! */
|
||||||
this._info_connection_promise = new Promise<ServerConnectionInfo>((resolve, reject) => {
|
this._info_connection_promise = new Promise<ServerConnectionInfo>((resolve, reject) => {
|
||||||
|
@ -364,10 +290,61 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._info_connection_promise_timestamp = Date.now();
|
this._info_connection_promise_timestamp = Date.now();
|
||||||
this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo", {}, {process_result: false}).catch(error => _local_reject(error));
|
this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo", {}, { process_result: false }).catch(error => _local_reject(error));
|
||||||
return this._info_connection_promise;
|
return this._info_connection_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestConnectionInfo() : Promise<ServerConnectionInfoResult> {
|
||||||
|
if(this.requestInfoPromise && Date.now() - 1000 < this.requestInfoPromiseTimestamp) {
|
||||||
|
return this.requestInfoPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestInfoPromiseTimestamp = Date.now();
|
||||||
|
return this.requestInfoPromise = this.doRequestConnectionInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doRequestConnectionInfo() : Promise<ServerConnectionInfoResult> {
|
||||||
|
const connection = this.channelTree.client.serverConnection;
|
||||||
|
|
||||||
|
let result: ServerConnectionInfoResult = { status: "error", message: "missing notify" };
|
||||||
|
const handlerUnregister = connection.command_handler_boss().register_explicit_handler("notifyserverconnectioninfo", command => {
|
||||||
|
const payload = command.arguments[0];
|
||||||
|
|
||||||
|
const info = {} as any;
|
||||||
|
for(const key of Object.keys(kServerConnectionInfoFields)) {
|
||||||
|
if(!(key in payload)) {
|
||||||
|
result = { status: "error", message: "missing key " + key };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info[key] = parseFloat(payload[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = { status: "success", resultCached: false, result: info };
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.send_command("serverrequestconnectioninfo", {}, { process_result: false });
|
||||||
|
} catch (error) {
|
||||||
|
if(error instanceof CommandResult) {
|
||||||
|
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||||
|
result = { status: "no-permission", failedPermission: this.channelTree.client.permissions.getFailedPermission(error) };
|
||||||
|
} else {
|
||||||
|
result = { status: "error", message: error.formattedMessage() };
|
||||||
|
}
|
||||||
|
} else if(typeof error === "string") {
|
||||||
|
result = { status: "error", message: error };
|
||||||
|
} else {
|
||||||
|
logError(LogCategory.NETWORKING, tr("Failed to request the server connection info: %o"), error);
|
||||||
|
result = { status: "error", message: tr("lookup the console") };
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
handlerUnregister();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
set_connection_info(info: ServerConnectionInfo) {
|
set_connection_info(info: ServerConnectionInfo) {
|
||||||
if(!this._info_connection_promise_resolve)
|
if(!this._info_connection_promise_resolve)
|
||||||
return;
|
return;
|
||||||
|
@ -392,4 +369,36 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
this._info_connection_promise_resolve = undefined;
|
this._info_connection_promise_resolve = undefined;
|
||||||
this._info_connection_promise_timestamp = undefined;
|
this._info_connection_promise_timestamp = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateHostBannerInfo() : HostBannerInfo {
|
||||||
|
if(!this.properties.virtualserver_hostbanner_gfx_url) {
|
||||||
|
return { status: "none" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode: HostBannerInfoMode;
|
||||||
|
switch (this.properties.virtualserver_hostbanner_mode) {
|
||||||
|
case 0:
|
||||||
|
mode = "original";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
mode = "resize";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
default:
|
||||||
|
mode = "resize-ratio";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "set",
|
||||||
|
|
||||||
|
linkUrl: this.properties.virtualserver_hostbanner_url,
|
||||||
|
mode: mode,
|
||||||
|
|
||||||
|
imageUrl: this.properties.virtualserver_hostbanner_gfx_url,
|
||||||
|
updateInterval: this.properties.virtualserver_hostbanner_gfx_interval,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
138
shared/js/tree/ServerDefinitions.ts
Normal file
138
shared/js/tree/ServerDefinitions.ts
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
export class ServerProperties {
|
||||||
|
virtualserver_host: string = "";
|
||||||
|
virtualserver_port: number = 0;
|
||||||
|
|
||||||
|
virtualserver_name: string = "";
|
||||||
|
virtualserver_name_phonetic: string = "";
|
||||||
|
virtualserver_icon_id: number = 0;
|
||||||
|
virtualserver_version: string = "unknown";
|
||||||
|
virtualserver_platform: string = "unknown";
|
||||||
|
virtualserver_unique_identifier: string = "";
|
||||||
|
|
||||||
|
virtualserver_clientsonline: number = 0;
|
||||||
|
virtualserver_queryclientsonline: number = 0;
|
||||||
|
virtualserver_channelsonline: number = 0;
|
||||||
|
virtualserver_uptime: number = 0;
|
||||||
|
virtualserver_created: number = 0;
|
||||||
|
virtualserver_maxclients: number = 0;
|
||||||
|
virtualserver_reserved_slots: number = 0;
|
||||||
|
|
||||||
|
virtualserver_password: string = "";
|
||||||
|
virtualserver_flag_password: boolean = false;
|
||||||
|
|
||||||
|
virtualserver_ask_for_privilegekey: boolean = false;
|
||||||
|
|
||||||
|
virtualserver_welcomemessage: string = "";
|
||||||
|
|
||||||
|
virtualserver_hostmessage: string = "";
|
||||||
|
virtualserver_hostmessage_mode: number = 0;
|
||||||
|
|
||||||
|
virtualserver_hostbanner_url: string = "";
|
||||||
|
virtualserver_hostbanner_gfx_url: string = "";
|
||||||
|
virtualserver_hostbanner_gfx_interval: number = 0;
|
||||||
|
virtualserver_hostbanner_mode: number = 0;
|
||||||
|
|
||||||
|
virtualserver_hostbutton_tooltip: string = "";
|
||||||
|
virtualserver_hostbutton_url: string = "";
|
||||||
|
virtualserver_hostbutton_gfx_url: string = "";
|
||||||
|
|
||||||
|
virtualserver_codec_encryption_mode: number = 0;
|
||||||
|
|
||||||
|
virtualserver_default_music_group: number = 0;
|
||||||
|
virtualserver_default_server_group: number = 0;
|
||||||
|
virtualserver_default_channel_group: number = 0;
|
||||||
|
virtualserver_default_channel_admin_group: number = 0;
|
||||||
|
|
||||||
|
//Special requested properties
|
||||||
|
virtualserver_default_client_description: string = "";
|
||||||
|
virtualserver_default_channel_description: string = "";
|
||||||
|
virtualserver_default_channel_topic: string = "";
|
||||||
|
|
||||||
|
virtualserver_antiflood_points_tick_reduce: number = 0;
|
||||||
|
virtualserver_antiflood_points_needed_command_block: number = 0;
|
||||||
|
virtualserver_antiflood_points_needed_ip_block: number = 0;
|
||||||
|
|
||||||
|
virtualserver_country_code: string = "XX";
|
||||||
|
|
||||||
|
virtualserver_complain_autoban_count: number = 0;
|
||||||
|
virtualserver_complain_autoban_time: number = 0;
|
||||||
|
virtualserver_complain_remove_time: number = 0;
|
||||||
|
|
||||||
|
virtualserver_needed_identity_security_level: number = 8;
|
||||||
|
virtualserver_weblist_enabled: boolean = false;
|
||||||
|
virtualserver_min_clients_in_channel_before_forced_silence: number = 0;
|
||||||
|
virtualserver_channel_temp_delete_delay_default: number = 60;
|
||||||
|
virtualserver_priority_speaker_dimm_modificator: number = -18;
|
||||||
|
|
||||||
|
virtualserver_max_upload_total_bandwidth: number = 0;
|
||||||
|
virtualserver_upload_quota: number = 0;
|
||||||
|
virtualserver_max_download_total_bandwidth: number = 0;
|
||||||
|
virtualserver_download_quota: number = 0;
|
||||||
|
|
||||||
|
virtualserver_month_bytes_downloaded: number = 0;
|
||||||
|
virtualserver_month_bytes_uploaded: number = 0;
|
||||||
|
virtualserver_total_bytes_downloaded: number = 0;
|
||||||
|
virtualserver_total_bytes_uploaded: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const kServerConnectionInfoFields = {
|
||||||
|
"connection_filetransfer_bandwidth_sent": "number",
|
||||||
|
"connection_filetransfer_bandwidth_received": "number",
|
||||||
|
|
||||||
|
"connection_filetransfer_bytes_sent_total": "number",
|
||||||
|
"connection_filetransfer_bytes_received_total": "number",
|
||||||
|
|
||||||
|
"connection_filetransfer_bytes_sent_month": "number",
|
||||||
|
"connection_filetransfer_bytes_received_month": "number",
|
||||||
|
|
||||||
|
"connection_packets_sent_total": "number",
|
||||||
|
"connection_bytes_sent_total": "number",
|
||||||
|
"connection_packets_received_total": "number",
|
||||||
|
"connection_bytes_received_total": "number",
|
||||||
|
|
||||||
|
"connection_bandwidth_sent_last_second_total": "number",
|
||||||
|
"connection_bandwidth_sent_last_minute_total": "number",
|
||||||
|
"connection_bandwidth_received_last_second_total": "number",
|
||||||
|
"connection_bandwidth_received_last_minute_total": "number",
|
||||||
|
|
||||||
|
"connection_connected_time": "number",
|
||||||
|
"connection_packetloss_total": "number",
|
||||||
|
"connection_ping": "number",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ServerConnectionInfo {
|
||||||
|
connection_filetransfer_bandwidth_sent: number;
|
||||||
|
connection_filetransfer_bandwidth_received: number;
|
||||||
|
|
||||||
|
connection_filetransfer_bytes_sent_total: number;
|
||||||
|
connection_filetransfer_bytes_received_total: number;
|
||||||
|
|
||||||
|
connection_filetransfer_bytes_sent_month: number;
|
||||||
|
connection_filetransfer_bytes_received_month: number;
|
||||||
|
|
||||||
|
connection_packets_sent_total: number;
|
||||||
|
connection_bytes_sent_total: number;
|
||||||
|
connection_packets_received_total: number;
|
||||||
|
connection_bytes_received_total: number;
|
||||||
|
|
||||||
|
connection_bandwidth_sent_last_second_total: number;
|
||||||
|
connection_bandwidth_sent_last_minute_total: number;
|
||||||
|
connection_bandwidth_received_last_second_total: number;
|
||||||
|
connection_bandwidth_received_last_minute_total: number;
|
||||||
|
|
||||||
|
connection_connected_time: number;
|
||||||
|
connection_packetloss_total: number;
|
||||||
|
connection_ping: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerConnectionInfoResult = {
|
||||||
|
status: "success",
|
||||||
|
result: ServerConnectionInfo,
|
||||||
|
resultCached: boolean
|
||||||
|
} | {
|
||||||
|
status: "no-permission",
|
||||||
|
failedPermission: string
|
||||||
|
} | {
|
||||||
|
status: "error",
|
||||||
|
message: string
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||||
|
import {HostBannerUiEvents} from "tc-shared/ui/frames/HostBannerDefinitions";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {HostBannerInfoMode, HostBannerUiEvents} from "tc-shared/ui/frames/HostBannerDefinitions";
|
|
||||||
|
|
||||||
export class HostBannerController {
|
export class HostBannerController {
|
||||||
readonly uiEvents: Registry<HostBannerUiEvents>;
|
readonly uiEvents: Registry<HostBannerUiEvents>;
|
||||||
|
@ -38,15 +38,8 @@ export class HostBannerController {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected initializeConnectionHandler(handler: ConnectionHandler) {
|
protected initializeConnectionHandler(handler: ConnectionHandler) {
|
||||||
this.listenerConnection.push(handler.channelTree.server.events.on("notify_properties_updated", event => {
|
this.listenerConnection.push(handler.channelTree.server.events.on("notify_host_banner_updated", () => {
|
||||||
if(
|
this.notifyHostBanner();
|
||||||
"virtualserver_hostbanner_url" in event.updated_properties ||
|
|
||||||
"virtualserver_hostbanner_mode" in event.updated_properties ||
|
|
||||||
"virtualserver_hostbanner_gfx_url" in event.updated_properties ||
|
|
||||||
"virtualserver_hostbanner_gfx_interval" in event.updated_properties
|
|
||||||
) {
|
|
||||||
this.notifyHostBanner();
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.listenerConnection.push(handler.events().on("notify_connection_state_changed", event => {
|
this.listenerConnection.push(handler.events().on("notify_connection_state_changed", event => {
|
||||||
|
@ -58,39 +51,11 @@ export class HostBannerController {
|
||||||
|
|
||||||
private notifyHostBanner() {
|
private notifyHostBanner() {
|
||||||
if(this.currentConnection?.connected) {
|
if(this.currentConnection?.connected) {
|
||||||
const properties = this.currentConnection.channelTree.server.properties;
|
this.uiEvents.fire_react("notify_host_banner", {
|
||||||
if(properties.virtualserver_hostbanner_gfx_url) {
|
banner: this.currentConnection.channelTree.server.generateHostBannerInfo()
|
||||||
let mode: HostBannerInfoMode;
|
});
|
||||||
switch (properties.virtualserver_hostbanner_mode) {
|
} else {
|
||||||
case 0:
|
this.uiEvents.fire_react("notify_host_banner", { banner: { status: "none" }});
|
||||||
mode = "original";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
mode = "resize";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
default:
|
|
||||||
mode = "resize-ratio";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uiEvents.fire_react("notify_host_banner", {
|
|
||||||
banner: {
|
|
||||||
status: "set",
|
|
||||||
|
|
||||||
linkUrl: properties.virtualserver_hostbanner_url,
|
|
||||||
mode: mode,
|
|
||||||
|
|
||||||
imageUrl: properties.virtualserver_hostbanner_gfx_url,
|
|
||||||
updateInterval: properties.virtualserver_hostbanner_gfx_interval,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.uiEvents.fire_react("notify_host_banner", { banner: { status: "none" }});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -32,7 +32,10 @@
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* We're disabling the transition since it only works on appearing not on disappearing */
|
/* We're disabling the transition since it only works on appearing not on disappearing */
|
||||||
/* @include transition(height 0.5s ease-in-out); */
|
/* @include transition(height 0.5s ease-in-out); */
|
||||||
|
@ -60,8 +63,13 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
> img {
|
> img {
|
||||||
width: 100%;
|
object-fit: contain;
|
||||||
height: 100%;
|
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {Settings} from "tc-shared/settings";
|
||||||
|
|
||||||
const cssStyle = require("./HostBannerRenderer.scss");
|
const cssStyle = require("./HostBannerRenderer.scss");
|
||||||
|
|
||||||
export const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet, className?: string }) => {
|
export const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet, clickable: boolean, className?: string }) => {
|
||||||
const [ revision, setRevision ] = useState(Date.now());
|
const [ revision, setRevision ] = useState(Date.now());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!props.banner.updateInterval) {
|
if(!props.banner.updateInterval) {
|
||||||
|
@ -35,10 +35,11 @@ export const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
cssStyle.containerImage + " " + cssStyle["mode-" + props.banner.mode] + " " + cssStyle["state-" + loadingState] + " " +
|
cssStyle.containerImage + " " + cssStyle["mode-" + props.banner.mode] + " " + cssStyle["state-" + loadingState] + " " +
|
||||||
(withBackground ? cssStyle.withBackground : "") + " " + props.className
|
(withBackground ? cssStyle.withBackground : "") + " " + props.className + " "
|
||||||
|
+ (props.clickable ? cssStyle.clickable : "")
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if(props.banner.linkUrl) {
|
if(props.banner.linkUrl && props.clickable) {
|
||||||
window.open(props.banner.linkUrl, "_blank");
|
window.open(props.banner.linkUrl, "_blank");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -67,7 +68,7 @@ export const HostBanner = React.memo((props: { events: Registry<HostBannerUiEven
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.container + " " + (hostBanner.status !== "set" ? cssStyle.disabled : "")}>
|
<div className={cssStyle.container + " " + (hostBanner.status !== "set" ? cssStyle.disabled : "")}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{hostBanner.status === "set" ? <HostBannerRenderer key={"banner"} banner={hostBanner} /> : undefined}
|
{hostBanner.status === "set" ? <HostBannerRenderer key={"banner"} banner={hostBanner} clickable={true} /> : undefined}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,250 +0,0 @@
|
||||||
import {
|
|
||||||
openServerInfoBandwidth,
|
|
||||||
RequestInfoStatus,
|
|
||||||
ServerBandwidthInfoUpdateCallback
|
|
||||||
} from "../../ui/modal/ModalServerInfoBandwidth";
|
|
||||||
import {ServerEntry} from "../../tree/Server";
|
|
||||||
import {CommandResult} from "../../connection/ServerConnectionDeclaration";
|
|
||||||
import {createErrorModal, createModal, Modal} from "../../ui/elements/Modal";
|
|
||||||
import {LogCategory, logWarn} from "../../log";
|
|
||||||
import * as tooltip from "../../ui/elements/Tooltip";
|
|
||||||
import * as i18nc from "../../i18n/country";
|
|
||||||
import {format_time, formatMessage} from "../../ui/frames/chat";
|
|
||||||
import moment from "moment";
|
|
||||||
import {ErrorCode} from "../../connection/ErrorCode";
|
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
|
||||||
|
|
||||||
export function openServerInfo(server: ServerEntry) {
|
|
||||||
let modal: Modal;
|
|
||||||
let update_callbacks: ServerBandwidthInfoUpdateCallback[] = [];
|
|
||||||
|
|
||||||
modal = createModal({
|
|
||||||
header: tr("Server Information: ") + server.properties.virtualserver_name,
|
|
||||||
body: () => {
|
|
||||||
const template = $("#tmpl_server_info").renderTag();
|
|
||||||
|
|
||||||
const children = template.children();
|
|
||||||
const top = template.find(".container-top");
|
|
||||||
const update_values = () => {
|
|
||||||
apply_hostbanner(server, top);
|
|
||||||
apply_category_1(server, children, update_callbacks);
|
|
||||||
apply_category_2(server, children, update_callbacks);
|
|
||||||
apply_category_3(server, children, update_callbacks);
|
|
||||||
};
|
|
||||||
|
|
||||||
const button_update = template.find(".button-update");
|
|
||||||
button_update.on('click', event => {
|
|
||||||
button_update.prop("disabled", true);
|
|
||||||
server.updateProperties().then(() => {
|
|
||||||
update_callbacks = [];
|
|
||||||
update_values();
|
|
||||||
}).catch(error => {
|
|
||||||
logWarn(LogCategory.CLIENT, tr("Failed to refresh server properties: %o"), error);
|
|
||||||
if (error instanceof CommandResult)
|
|
||||||
error = error.extra_message || error.message;
|
|
||||||
createErrorModal(tr("Refresh failed"), formatMessage(tr("Failed to refresh server properties.{:br:}Error: {}"), error)).open();
|
|
||||||
}).then(() => {
|
|
||||||
button_update.prop("disabled", false);
|
|
||||||
});
|
|
||||||
}).trigger('click');
|
|
||||||
|
|
||||||
update_values();
|
|
||||||
tooltip.initialize(template);
|
|
||||||
return template.children();
|
|
||||||
},
|
|
||||||
footer: null,
|
|
||||||
min_width: "25em"
|
|
||||||
});
|
|
||||||
|
|
||||||
const updater = setInterval(() => {
|
|
||||||
server.request_connection_info().then(info => update_callbacks.forEach(e => e(RequestInfoStatus.SUCCESS, info))).catch(error => {
|
|
||||||
if (error instanceof CommandResult && error.id == ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
|
||||||
update_callbacks.forEach(e => e(RequestInfoStatus.NO_PERMISSION));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
update_callbacks.forEach(e => e(RequestInfoStatus.UNKNOWN));
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
|
|
||||||
modal.htmlTag.find(".button-close").on('click', event => modal.close());
|
|
||||||
modal.htmlTag.find(".button-show-bandwidth").on('click', event => {
|
|
||||||
const custom_callbacks = [];
|
|
||||||
const custom_callback_caller = (status, info) => {
|
|
||||||
custom_callbacks.forEach(e => e(status, info));
|
|
||||||
};
|
|
||||||
|
|
||||||
update_callbacks.push(custom_callback_caller);
|
|
||||||
openServerInfoBandwidth(server, custom_callbacks).close_listener.push(() => {
|
|
||||||
update_callbacks.remove(custom_callback_caller);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modal.htmlTag.find(".modal-body").addClass("modal-server-info");
|
|
||||||
modal.open();
|
|
||||||
modal.close_listener.push(() => clearInterval(updater));
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_hostbanner(server: ServerEntry, tag: JQuery) {
|
|
||||||
let container: JQuery;
|
|
||||||
tag.empty().append(
|
|
||||||
container = $.spawn("div").addClass("container-hostbanner")
|
|
||||||
).addClass("hidden");
|
|
||||||
|
|
||||||
/* FIXME: .... */
|
|
||||||
/*
|
|
||||||
const htag = Hostbanner.generate_tag(server.properties.virtualserver_hostbanner_gfx_url, server.properties.virtualserver_hostbanner_gfx_interval, server.properties.virtualserver_hostbanner_mode);
|
|
||||||
htag.then(t => {
|
|
||||||
if (!t) return;
|
|
||||||
|
|
||||||
tag.removeClass("hidden");
|
|
||||||
container.append(t);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_category_1(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) {
|
|
||||||
/* server name */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-name");
|
|
||||||
container.text(server.properties.virtualserver_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* server region */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-region").empty();
|
|
||||||
container.append(
|
|
||||||
$.spawn("div").addClass("country flag-" + server.properties.virtualserver_country_code.toLowerCase()),
|
|
||||||
$.spawn("a").text(i18nc.getCountryName(server.properties.virtualserver_country_code, tr("Global")))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* slots */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-slots");
|
|
||||||
|
|
||||||
let text = server.properties.virtualserver_clientsonline + "/" + server.properties.virtualserver_maxclients;
|
|
||||||
if (server.properties.virtualserver_queryclientsonline)
|
|
||||||
text += " +" + (server.properties.virtualserver_queryclientsonline > 1 ?
|
|
||||||
server.properties.virtualserver_queryclientsonline + " " + tr("Queries") :
|
|
||||||
server.properties.virtualserver_queryclientsonline + " " + tr("Query"));
|
|
||||||
if (server.properties.virtualserver_reserved_slots)
|
|
||||||
text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")";
|
|
||||||
|
|
||||||
container.text(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* first run */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-first-run");
|
|
||||||
|
|
||||||
container.text(
|
|
||||||
server.properties.virtualserver_created > 0 ?
|
|
||||||
moment(server.properties.virtualserver_created * 1000).format('MMMM Do YYYY, h:mm:ss a') :
|
|
||||||
tr("Unknown")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* uptime */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-uptime");
|
|
||||||
const update = () => container.text(format_time(server.calculateUptime() * 1000, tr("just started")));
|
|
||||||
update_callbacks.push(update);
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_category_2(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) {
|
|
||||||
/* ip */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-ip");
|
|
||||||
container.text(server.remote_address.host + (server.remote_address.port == 9987 ? "" : (":" + server.remote_address.port)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/* version */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-version");
|
|
||||||
|
|
||||||
let timestamp = -1;
|
|
||||||
const version = (server.properties.virtualserver_version || "unknwon").replace(/ ?\[build: ?([0-9]+)]/gmi, (group, ts) => {
|
|
||||||
timestamp = parseInt(ts);
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
|
|
||||||
container.find("a").text(version);
|
|
||||||
container.find(".container-tooltip").toggle(timestamp > 0).find(".tooltip a").text(
|
|
||||||
moment(timestamp * 1000).format('[Build timestamp:] YYYY-MM-DD HH:mm Z')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* platform */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-platform");
|
|
||||||
container.text(server.properties.virtualserver_platform);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ping */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-ping");
|
|
||||||
container.text(tr("calculating..."));
|
|
||||||
update_callbacks.push((status, data) => {
|
|
||||||
if (status === RequestInfoStatus.SUCCESS)
|
|
||||||
container.text(data.connection_ping.toFixed(0) + " " + "ms");
|
|
||||||
else if (status === RequestInfoStatus.NO_PERMISSION)
|
|
||||||
container.text(tr("No Permissions"));
|
|
||||||
else
|
|
||||||
container.text(tr("receiving..."));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* packet loss */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-packet-loss");
|
|
||||||
container.text(tr("receiving..."));
|
|
||||||
update_callbacks.push((status, data) => {
|
|
||||||
if (status === RequestInfoStatus.SUCCESS)
|
|
||||||
container.text(data.connection_packetloss_total.toFixed(2) + "%");
|
|
||||||
else if (status === RequestInfoStatus.NO_PERMISSION)
|
|
||||||
container.text(tr("No Permissions"));
|
|
||||||
else
|
|
||||||
container.text(tr("receiving..."));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_category_3(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) {
|
|
||||||
/* unique id */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-unique-id");
|
|
||||||
container.text(server.properties.virtualserver_unique_identifier || tr("Unknown"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* voice encryption */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-voice-encryption");
|
|
||||||
if (server.properties.virtualserver_codec_encryption_mode == 0)
|
|
||||||
container.text(tr("Globally off"));
|
|
||||||
else if (server.properties.virtualserver_codec_encryption_mode == 1)
|
|
||||||
container.text(tr("Individually configured per channel"));
|
|
||||||
else
|
|
||||||
container.text(tr("Globally on"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* channel count */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-channel-count");
|
|
||||||
container.text(server.properties.virtualserver_channelsonline);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* minimal security level */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-min-security-level");
|
|
||||||
container.text(server.properties.virtualserver_needed_identity_security_level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* complains */
|
|
||||||
{
|
|
||||||
const container = tag.find(".server-complains");
|
|
||||||
container.text(server.properties.virtualserver_complain_autoban_count);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -276,6 +276,7 @@ const SelectedBookmarkBanner = React.memo(() => {
|
||||||
updateInterval: 0
|
updateInterval: 0
|
||||||
}}
|
}}
|
||||||
className={cssStyle.renderer}
|
className={cssStyle.renderer}
|
||||||
|
clickable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
190
shared/js/ui/modal/server-info/Controller.ts
Normal file
190
shared/js/ui/modal/server-info/Controller.ts
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||||
|
import {Registry} from "tc-events";
|
||||||
|
import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/modal/server-info/Definitions";
|
||||||
|
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {CallOnce, ignorePromise} from "tc-shared/proto";
|
||||||
|
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||||
|
import {ServerConnectionInfoResult, ServerProperties} from "tc-shared/tree/Server";
|
||||||
|
import {LogCategory, logWarn} from "tc-shared/log";
|
||||||
|
import {openServerInfoBandwidth} from "tc-shared/ui/modal/ModalServerInfoBandwidth";
|
||||||
|
|
||||||
|
const kPropertyUpdateMatrix: {[T in keyof ServerProperties]?: [keyof ModalServerInfoVariables]} = {
|
||||||
|
"virtualserver_name": [ "name" ],
|
||||||
|
"virtualserver_country_code": [ "region" ],
|
||||||
|
"virtualserver_reserved_slots": [ "slots" ],
|
||||||
|
"virtualserver_maxclients": [ "slots" ],
|
||||||
|
"virtualserver_clientsonline": [ "slots" ],
|
||||||
|
"virtualserver_queryclientsonline": [ "slots" ],
|
||||||
|
"virtualserver_created": [ "firstRun" ],
|
||||||
|
"virtualserver_uptime": [ "uptime" ],
|
||||||
|
|
||||||
|
"virtualserver_version": [ "version" ],
|
||||||
|
"virtualserver_platform": [ "platform" ],
|
||||||
|
"virtualserver_unique_identifier": [ "uniqueId" ],
|
||||||
|
"virtualserver_channelsonline": [ "channelCount" ],
|
||||||
|
"virtualserver_codec_encryption_mode": [ "voiceDataEncryption" ],
|
||||||
|
"virtualserver_needed_identity_security_level": [ "securityLevel" ],
|
||||||
|
"virtualserver_complain_autoban_count": [ "complainsUntilBan" ],
|
||||||
|
};
|
||||||
|
|
||||||
|
class Controller {
|
||||||
|
readonly handler: ConnectionHandler;
|
||||||
|
readonly events: Registry<ModalServerInfoEvents>;
|
||||||
|
readonly variables: IpcUiVariableProvider<ModalServerInfoVariables>;
|
||||||
|
|
||||||
|
private serverListeners: (() => void)[];
|
||||||
|
|
||||||
|
private connectionInfoInterval: number;
|
||||||
|
private connectionInfo: ServerConnectionInfoResult;
|
||||||
|
|
||||||
|
private propertyUpdateInterval: number;
|
||||||
|
private nextRefreshAllowed: number;
|
||||||
|
|
||||||
|
constructor(handler: ConnectionHandler) {
|
||||||
|
this.handler = handler;
|
||||||
|
|
||||||
|
this.events = new Registry<ModalServerInfoEvents>();
|
||||||
|
this.variables = new IpcUiVariableProvider<ModalServerInfoVariables>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getServerProperties() : ServerProperties {
|
||||||
|
return this.handler.channelTree.server.properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallOnce
|
||||||
|
initialize() {
|
||||||
|
this.variables.setVariableProvider("name", () => this.getServerProperties().virtualserver_name);
|
||||||
|
this.variables.setVariableProvider("region", () => this.getServerProperties().virtualserver_country_code);
|
||||||
|
this.variables.setVariableProvider("slots", () => ({
|
||||||
|
max: this.getServerProperties().virtualserver_maxclients,
|
||||||
|
reserved: this.getServerProperties().virtualserver_reserved_slots,
|
||||||
|
|
||||||
|
used: this.getServerProperties().virtualserver_clientsonline,
|
||||||
|
queries: this.getServerProperties().virtualserver_queryclientsonline
|
||||||
|
}));
|
||||||
|
this.variables.setVariableProvider("firstRun", () => this.getServerProperties().virtualserver_created);
|
||||||
|
this.variables.setVariableProvider("uptime", () => this.getServerProperties().virtualserver_uptime);
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("ipAddress", () => {
|
||||||
|
const address = this.handler.channelTree.server.remote_address;
|
||||||
|
return address.host + (address.port === 9987 ? "" : ":" + address.port);
|
||||||
|
});
|
||||||
|
this.variables.setVariableProvider("version", () => this.getServerProperties().virtualserver_version);
|
||||||
|
this.variables.setVariableProvider("platform", () => this.getServerProperties().virtualserver_platform);
|
||||||
|
/* TODO: Ping & Packet loss */
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("uniqueId", () => this.getServerProperties().virtualserver_unique_identifier);
|
||||||
|
this.variables.setVariableProvider("channelCount", () => this.getServerProperties().virtualserver_channelsonline);
|
||||||
|
this.variables.setVariableProvider("voiceDataEncryption", () => {
|
||||||
|
switch(this.getServerProperties().virtualserver_codec_encryption_mode) {
|
||||||
|
case 0:
|
||||||
|
return "global-off";
|
||||||
|
case 1:
|
||||||
|
return "channel-individual";
|
||||||
|
case 2:
|
||||||
|
return "global-on";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("securityLevel", () => this.getServerProperties().virtualserver_needed_identity_security_level);
|
||||||
|
this.variables.setVariableProvider("complainsUntilBan", () => this.getServerProperties().virtualserver_complain_autoban_count);
|
||||||
|
|
||||||
|
this.variables.setVariableProvider("hostBanner", () => this.handler.channelTree.server.generateHostBannerInfo());
|
||||||
|
this.variables.setVariableProvider("connectionInfo", () => {
|
||||||
|
if(this.connectionInfo) {
|
||||||
|
return this.connectionInfo;
|
||||||
|
} else {
|
||||||
|
return { status: "loading" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.variables.setVariableProvider("refreshAllowed", () => this.nextRefreshAllowed);
|
||||||
|
|
||||||
|
this.serverListeners = [];
|
||||||
|
this.serverListeners.push(this.handler.channelTree.server.events.on("notify_properties_updated", event => {
|
||||||
|
const updatedVariables = new Set<keyof ModalServerInfoVariables>();
|
||||||
|
for(const key of Object.keys(event.updated_properties)) {
|
||||||
|
kPropertyUpdateMatrix[key]?.forEach(update => updatedVariables.add(update));
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedVariables.forEach(entry => this.variables.sendVariable(entry));
|
||||||
|
}));
|
||||||
|
this.serverListeners.push(this.handler.channelTree.server.events.on("notify_host_banner_updated",
|
||||||
|
() => this.variables.sendVariable("hostBanner")
|
||||||
|
));
|
||||||
|
|
||||||
|
this.events.on("action_refresh", () => this.refreshProperties());
|
||||||
|
|
||||||
|
this.refreshConnectionInfo();
|
||||||
|
this.connectionInfoInterval = setInterval(() => this.refreshConnectionInfo(), 1000);
|
||||||
|
|
||||||
|
this.refreshProperties();
|
||||||
|
this.propertyUpdateInterval = setInterval(() => this.refreshProperties(), 30 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallOnce
|
||||||
|
destroy() {
|
||||||
|
clearInterval(this.connectionInfoInterval);
|
||||||
|
this.connectionInfoInterval = 0;
|
||||||
|
|
||||||
|
clearInterval(this.propertyUpdateInterval);
|
||||||
|
this.propertyUpdateInterval = 0;
|
||||||
|
|
||||||
|
this.serverListeners?.forEach(callback => callback());
|
||||||
|
this.serverListeners = undefined;
|
||||||
|
|
||||||
|
this.events.destroy();
|
||||||
|
this.variables.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshProperties() {
|
||||||
|
if(Date.now() < this.nextRefreshAllowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextRefreshAllowed = Date.now() + 10 * 1000;
|
||||||
|
this.variables.sendVariable("refreshAllowed");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Updates itself will be triggered via the notify_properties_updated event
|
||||||
|
*/
|
||||||
|
const server = this.handler.channelTree.server;
|
||||||
|
server.updateProperties().catch(error => {
|
||||||
|
logWarn(LogCategory.GENERAL, tr("Failed to update server properties: %o"), error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshConnectionInfo() {
|
||||||
|
const server = this.handler.channelTree.server;
|
||||||
|
server.requestConnectionInfo().then(info => {
|
||||||
|
this.connectionInfo = info;
|
||||||
|
this.variables.sendVariable("connectionInfo");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnServerInfoNew(handler: ConnectionHandler) {
|
||||||
|
const controller = new Controller(handler);
|
||||||
|
controller.initialize();
|
||||||
|
|
||||||
|
const modal = spawnModal("modal-server-info", [
|
||||||
|
controller.events.generateIpcDescription(),
|
||||||
|
controller.variables.generateConsumerDescription()
|
||||||
|
], {
|
||||||
|
popoutable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.events.on("action_close", () => modal.destroy());
|
||||||
|
controller.events.on("action_show_bandwidth", () => {
|
||||||
|
openServerInfoBandwidth(handler.channelTree.server);
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.getEvents().on("destroy", () => controller.destroy());
|
||||||
|
modal.getEvents().on("destroy", handler.events().on("notify_connection_state_changed", event => {
|
||||||
|
if(event.newState !== ConnectionState.CONNECTED) {
|
||||||
|
modal.destroy();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
ignorePromise(modal.show());
|
||||||
|
}
|
31
shared/js/ui/modal/server-info/Definitions.ts
Normal file
31
shared/js/ui/modal/server-info/Definitions.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import {HostBannerInfo} from "tc-shared/ui/frames/HostBannerDefinitions";
|
||||||
|
import {ServerConnectionInfoResult} from "tc-shared/tree/ServerDefinitions";
|
||||||
|
|
||||||
|
export interface ModalServerInfoVariables {
|
||||||
|
readonly name: string,
|
||||||
|
readonly region: string,
|
||||||
|
readonly slots: { max: number, used: number, reserved: number, queries: number },
|
||||||
|
readonly firstRun: number,
|
||||||
|
readonly uptime: number,
|
||||||
|
|
||||||
|
readonly ipAddress: string,
|
||||||
|
readonly version: string,
|
||||||
|
readonly platform: string,
|
||||||
|
readonly connectionInfo: ServerConnectionInfoResult | { status: "loading" },
|
||||||
|
|
||||||
|
readonly uniqueId: string,
|
||||||
|
readonly channelCount: number,
|
||||||
|
readonly voiceDataEncryption: "global-on" | "global-off" | "channel-individual" | "unknown",
|
||||||
|
readonly securityLevel: number,
|
||||||
|
readonly complainsUntilBan: number,
|
||||||
|
|
||||||
|
readonly hostBanner: HostBannerInfo,
|
||||||
|
|
||||||
|
readonly refreshAllowed: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalServerInfoEvents {
|
||||||
|
action_show_bandwidth: {},
|
||||||
|
action_refresh: {},
|
||||||
|
action_close: {},
|
||||||
|
}
|
250
shared/js/ui/modal/server-info/Renderer.scss
Normal file
250
shared/js/ui/modal/server-info/Renderer.scss
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0;
|
||||||
|
width: 55em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
background-color: var(--serverinfo-background);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.windowed {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerHostBanner {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
max-height: 10em;
|
||||||
|
|
||||||
|
background-color: var(--serverinfo-hostbanner-background);
|
||||||
|
|
||||||
|
.hostBanner {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerProperties {
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-height: 12em; /* 10em + 2 * 1em margin */
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin: 1em;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
margin: 1em;
|
||||||
|
padding: .5em;
|
||||||
|
|
||||||
|
border-radius: .2em;
|
||||||
|
border: 1px solid var(--serverinfo-group-border);
|
||||||
|
|
||||||
|
background-color: var(--serverinfo-group-background);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
height: 10em;
|
||||||
|
max-height: 10em;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
max-width: 15em;
|
||||||
|
max-height: 9em; /* minus one padding */
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-right: 2em;
|
||||||
|
@include transition(.25s ease-in-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties {
|
||||||
|
.row {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
min-width: 20em;
|
||||||
|
min-height: 10em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
height: inherit;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
@include chat-scrollbar();
|
||||||
|
|
||||||
|
.row {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
height: 1.8em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
color: var(--serverinfo-key);
|
||||||
|
text-transform: uppercase;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--serverinfo-value);
|
||||||
|
align-self: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
|
.country {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.server-version {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
a {
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.network {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-right: 1em;
|
||||||
|
|
||||||
|
flex-shrink: 1e8;
|
||||||
|
min-width: 5em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: 2.5em;
|
||||||
|
width: 12em;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 50em) {
|
||||||
|
.group .image {
|
||||||
|
margin: 0!important;
|
||||||
|
max-width: 0!important;
|
||||||
|
}
|
||||||
|
}
|
342
shared/js/ui/modal/server-info/Renderer.tsx
Normal file
342
shared/js/ui/modal/server-info/Renderer.tsx
Normal file
|
@ -0,0 +1,342 @@
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||||
|
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||||
|
import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/modal/server-info/Definitions";
|
||||||
|
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||||
|
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {HostBannerRenderer} from "tc-shared/ui/frames/HostBannerRenderer";
|
||||||
|
|
||||||
|
import ImageServerEdit1 from "./serveredit_1.png";
|
||||||
|
import ImageServerEdit2 from "./serveredit_2.png";
|
||||||
|
import ImageServerEdit3 from "./serveredit_3.png";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
import {CountryCode} from "tc-shared/ui/react-elements/CountryCode";
|
||||||
|
import moment from "moment";
|
||||||
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
|
import {format_online_time} from "tc-shared/utils/TimeUtils";
|
||||||
|
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||||
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
|
import {ServerConnectionInfo} from "tc-shared/tree/ServerDefinitions";
|
||||||
|
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
|
const EventContext = React.createContext<Registry<ModalServerInfoEvents>>(undefined);
|
||||||
|
const VariablesContext = React.createContext<UiVariableConsumer<ModalServerInfoVariables>>(undefined);
|
||||||
|
|
||||||
|
const Group = React.memo((props: {
|
||||||
|
children: React.ReactElement[],
|
||||||
|
reverse: boolean
|
||||||
|
}) => (
|
||||||
|
<div className={joinClassList(cssStyle.group, props.reverse && cssStyle.reverse)}>
|
||||||
|
<div className={cssStyle.image}>
|
||||||
|
{props.children[0]}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.properties}>
|
||||||
|
{...props.children.slice(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
const HostBanner = React.memo(() => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const hostBanner = variables.useReadOnly("hostBanner", undefined, { status: "none" });
|
||||||
|
|
||||||
|
if(hostBanner.status === "none") {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.containerHostBanner}>
|
||||||
|
<HostBannerRenderer banner={hostBanner} className={cssStyle.hostBanner} clickable={false} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const TitleRenderer = React.memo(() => {
|
||||||
|
return <>Server Info</>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const VariablePropertyName: {[T in keyof ModalServerInfoVariables]?: () => React.ReactElement } = {
|
||||||
|
name: () => <Translatable>Server name</Translatable>,
|
||||||
|
region: () => <Translatable>Server region</Translatable>,
|
||||||
|
slots: () => <Translatable>Slots</Translatable>,
|
||||||
|
firstRun: () => <Translatable>First run</Translatable>,
|
||||||
|
uptime: () => <Translatable>Uptime</Translatable>,
|
||||||
|
ipAddress: () => <Translatable>Ip Address</Translatable>,
|
||||||
|
version: () => <Translatable>Version</Translatable>,
|
||||||
|
platform: () => <Translatable>Platform</Translatable>,
|
||||||
|
uniqueId: () => <Translatable>Global unique id</Translatable>,
|
||||||
|
channelCount: () => <Translatable>Current channels</Translatable>,
|
||||||
|
voiceDataEncryption: () => <Translatable>Voice data encryption</Translatable>,
|
||||||
|
securityLevel: () => <Translatable>Minimal security level</Translatable>,
|
||||||
|
complainsUntilBan: () => <Translatable>Complains until ban</Translatable>
|
||||||
|
};
|
||||||
|
|
||||||
|
const VariableProperty = <T extends keyof ModalServerInfoVariables>(props: {
|
||||||
|
property: T,
|
||||||
|
children?: (value: ModalServerInfoVariables[T]) => React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const value = variables.useReadOnly(props.property, undefined, undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.row}>
|
||||||
|
<div className={cssStyle.key}>
|
||||||
|
{(VariablePropertyName[props.property] || (() => props.property))()}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value}>
|
||||||
|
{(props.children || ((value) => value))(value)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectionProperty = React.memo((props: { children: [
|
||||||
|
React.ReactNode,
|
||||||
|
(value: ServerConnectionInfo ) => React.ReactNode
|
||||||
|
] }) => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const value = variables.useReadOnly("connectionInfo", undefined, { status: "loading" });
|
||||||
|
|
||||||
|
let body;
|
||||||
|
switch (value.status) {
|
||||||
|
case "loading":
|
||||||
|
body = <Translatable key={"loading"}>loading</Translatable>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
body = <React.Fragment key={"error"}>error: {value.message}</React.Fragment>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "no-permission":
|
||||||
|
body = <Translatable key={"no-permission"}>No Permission</Translatable>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "success":
|
||||||
|
body = <React.Fragment key={"success"}>{props.children[1](value.result)}</React.Fragment>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.row}>
|
||||||
|
<div className={cssStyle.key}>
|
||||||
|
{props.children[0]}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value}>
|
||||||
|
{body}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const ServerFirstRun = React.memo(() => (
|
||||||
|
<VariableProperty property={"firstRun"}>
|
||||||
|
{value => (
|
||||||
|
value > 0 ? moment(value * 1000).format('MMMM Do YYYY, h:mm:ss a') :
|
||||||
|
tr("Unknown")
|
||||||
|
)}
|
||||||
|
</VariableProperty>
|
||||||
|
));
|
||||||
|
|
||||||
|
const ServerSlots = React.memo(() => (
|
||||||
|
<VariableProperty property={"slots"}>
|
||||||
|
{value => {
|
||||||
|
if(!value) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = value.used + "/" + value.max;
|
||||||
|
if(value.reserved > 0) {
|
||||||
|
text += " (" + value.reserved + " " + tr("Reserved") + ")";
|
||||||
|
}
|
||||||
|
if(value.queries > 1) {
|
||||||
|
text += " +" + value.queries + " " + tr("Queries");
|
||||||
|
} else if(value.queries === 1) {
|
||||||
|
text += " " + tr("+1 Query");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}}
|
||||||
|
</VariableProperty>
|
||||||
|
));
|
||||||
|
|
||||||
|
const OnlineTimestampRenderer = React.memo((props: { timestamp: number }) => {
|
||||||
|
const initialRenderTimestamp = useRef(Date.now());
|
||||||
|
const [ , setRenderedTimestamp ] = useState(0);
|
||||||
|
|
||||||
|
const difference = props.timestamp + Math.ceil((Date.now() - initialRenderTimestamp.current) / 1000);
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setRenderedTimestamp(Date.now());
|
||||||
|
}, 900);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>{format_online_time(difference)}</>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const kVersionsRegex = /(.*)\[Build: ([0-9]+)]/;
|
||||||
|
const VersionsTimestamp = (props: { timestamp: string }) => {
|
||||||
|
if(!props.timestamp) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = props.timestamp.match(kVersionsRegex);
|
||||||
|
if(!match || !match[2]) {
|
||||||
|
return <React.Fragment key={"unknown"}>{props.timestamp}</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.version} key={"parsed"}>
|
||||||
|
<IconTooltip className={cssStyle.tooltip}>
|
||||||
|
{"Build timestamp: " + moment(parseInt(match[2]) * 1000).format("YYYY-MM-DD HH:mm Z")}
|
||||||
|
</IconTooltip>
|
||||||
|
{match[1].trim()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ButtonRefresh = React.memo(() => {
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
|
||||||
|
const nextRefresh = variables.useReadOnly("refreshAllowed");
|
||||||
|
const [ renderTimestamp, setRenderTimestamp ] = useState(Date.now());
|
||||||
|
|
||||||
|
const allowed = nextRefresh.status === "loaded" && renderTimestamp >= nextRefresh.value;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(nextRefresh.status !== "loaded" || allowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = nextRefresh.value - Date.now();
|
||||||
|
const timeout = setTimeout(() => setRenderTimestamp(Date.now()), time);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [ nextRefresh.value ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button color={"green"} onClick={() => events.fire("action_refresh")} disabled={!allowed}>
|
||||||
|
<Translatable>Refresh</Translatable>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Buttons = React.memo(() => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.buttons}>
|
||||||
|
<ButtonRefresh />
|
||||||
|
|
||||||
|
<Button color={"red"} onClick={() => events.fire("action_close")}>
|
||||||
|
<Translatable>Close</Translatable>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
class Modal extends AbstractModal {
|
||||||
|
private readonly events: Registry<ModalServerInfoEvents>;
|
||||||
|
private readonly variables: UiVariableConsumer<ModalServerInfoVariables>;
|
||||||
|
|
||||||
|
constructor(events: IpcRegistryDescription<ModalServerInfoEvents>, variables: IpcVariableDescriptor<ModalServerInfoVariables>) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.events = Registry.fromIpcDescription(events);
|
||||||
|
this.variables = createIpcUiVariableConsumer(variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
this.events.destroy();
|
||||||
|
this.variables.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<EventContext.Provider value={this.events}>
|
||||||
|
<VariablesContext.Provider value={this.variables}>
|
||||||
|
<div className={joinClassList(cssStyle.container, this.properties.windowed && cssStyle.windowed)}>
|
||||||
|
<HostBanner />
|
||||||
|
<div className={cssStyle.properties}>
|
||||||
|
<Group reverse={false}>
|
||||||
|
<img draggable={false} src={ImageServerEdit1} alt={""} />
|
||||||
|
<VariableProperty property={"name"} />
|
||||||
|
<VariableProperty property={"region"}>
|
||||||
|
{value => <CountryCode alphaCode={value} />}
|
||||||
|
</VariableProperty>
|
||||||
|
<ServerSlots />
|
||||||
|
<ServerFirstRun />
|
||||||
|
|
||||||
|
<VariableProperty property={"uptime"}>
|
||||||
|
{value => <OnlineTimestampRenderer timestamp={value} />}
|
||||||
|
</VariableProperty>
|
||||||
|
</Group>
|
||||||
|
<Group reverse={true}>
|
||||||
|
<img draggable={false} src={ImageServerEdit2} alt={""} />
|
||||||
|
<VariableProperty property={"ipAddress"} />
|
||||||
|
<VariableProperty property={"version"}>
|
||||||
|
{value => <VersionsTimestamp timestamp={value} />}
|
||||||
|
</VariableProperty>
|
||||||
|
<VariableProperty property={"platform"} />
|
||||||
|
<div className={cssStyle.network}>
|
||||||
|
<div className={cssStyle.button}>
|
||||||
|
<Button color={"purple"} onClick={() => this.events.fire("action_show_bandwidth")}>
|
||||||
|
<Translatable>Show Bandwidth</Translatable>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.right}>
|
||||||
|
<ConnectionProperty>
|
||||||
|
<Translatable>Average ping</Translatable>
|
||||||
|
{value => value.connection_ping.toFixed(2) + " ms"}
|
||||||
|
</ConnectionProperty>
|
||||||
|
<ConnectionProperty>
|
||||||
|
<Translatable>Average packet loss</Translatable>
|
||||||
|
{value => value.connection_packetloss_total.toFixed(2) + " %"}
|
||||||
|
</ConnectionProperty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
<Group reverse={false}>
|
||||||
|
<img draggable={false} src={ImageServerEdit3} alt={""} />
|
||||||
|
<VariableProperty property={"uniqueId"} />
|
||||||
|
<VariableProperty property={"channelCount"} />
|
||||||
|
<VariableProperty property={"voiceDataEncryption"}>
|
||||||
|
{value => {
|
||||||
|
switch(value) {
|
||||||
|
case "global-off":
|
||||||
|
return <Translatable key={value}>Globally off</Translatable>;
|
||||||
|
|
||||||
|
case "global-on":
|
||||||
|
return <Translatable key={value}>Globally on</Translatable>;
|
||||||
|
|
||||||
|
case "channel-individual":
|
||||||
|
return <Translatable key={value}>Individually configured per channel</Translatable>;
|
||||||
|
|
||||||
|
case "unknown":
|
||||||
|
default:
|
||||||
|
return <Translatable key={value}>Unknown</Translatable>;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</VariableProperty>
|
||||||
|
<VariableProperty property={"securityLevel"} />
|
||||||
|
<VariableProperty property={"complainsUntilBan"} />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
<Buttons />
|
||||||
|
</div>
|
||||||
|
</VariablesContext.Provider>
|
||||||
|
</EventContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTitle(): string | React.ReactElement {
|
||||||
|
return <TitleRenderer />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 294 KiB |
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import {ReactElement} from "react";
|
import {ReactElement, ReactNode} from "react";
|
||||||
import {guid} from "tc-shared/crypto/uid";
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
|
||||||
const cssStyle = require("./Tooltip.scss");
|
const cssStyle = require("./Tooltip.scss");
|
||||||
|
@ -73,7 +73,7 @@ export interface TooltipState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TooltipProperties {
|
export interface TooltipProperties {
|
||||||
tooltip: () => ReactElement | ReactElement[] | string;
|
tooltip: () => ReactNode | ReactNode[] | string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string, outerClassName?: string }) => (
|
export const IconTooltip = (props: { children?: React.ReactNode | React.ReactNode[], className?: string, outerClassName?: string }) => (
|
||||||
<Tooltip tooltip={() => props.children} className={props.outerClassName}>
|
<Tooltip tooltip={() => props.children} className={props.outerClassName}>
|
||||||
<div className={cssStyle.iconTooltip + " " + props.className}>
|
<div className={cssStyle.iconTooltip + " " + props.className}>
|
||||||
<img src="img/icon_tooltip.svg" alt={""} />
|
<img src="img/icon_tooltip.svg" alt={""} />
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefini
|
||||||
import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
|
import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
|
||||||
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
||||||
import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios";
|
import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios";
|
||||||
|
import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/modal/server-info/Definitions";
|
||||||
import {ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
import {ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
||||||
|
|
||||||
export type ModalType = "error" | "warning" | "info" | "none";
|
export type ModalType = "error" | "warning" | "info" | "none";
|
||||||
|
@ -220,5 +221,9 @@ export interface ModalConstructorArguments {
|
||||||
"modal-about": [
|
"modal-about": [
|
||||||
/* events */ IpcRegistryDescription,
|
/* events */ IpcRegistryDescription,
|
||||||
/* variables */ IpcVariableDescriptor<ModalAboutVariables>
|
/* variables */ IpcVariableDescriptor<ModalAboutVariables>
|
||||||
|
],
|
||||||
|
"modal-server-info": [
|
||||||
|
/* events */ IpcRegistryDescription<ModalServerInfoEvents>,
|
||||||
|
/* variables */ IpcVariableDescriptor<ModalServerInfoVariables>
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -116,7 +116,7 @@ registerModal({
|
||||||
});
|
});
|
||||||
|
|
||||||
registerModal({
|
registerModal({
|
||||||
modalId: "modal-about",
|
modalId: "modal-server-info",
|
||||||
classLoader: async () => await import("tc-shared/ui/modal/about/Renderer"),
|
classLoader: async () => await import("tc-shared/ui/modal/server-info/Renderer"),
|
||||||
popoutSupported: true
|
popoutSupported: true
|
||||||
});
|
});
|
|
@ -8,18 +8,27 @@ export function format_online_time(secs: number) : string {
|
||||||
let seconds = Math.floor(secs % 60);
|
let seconds = Math.floor(secs % 60);
|
||||||
|
|
||||||
let result = "";
|
let result = "";
|
||||||
if(years > 0)
|
if(years > 0) {
|
||||||
result += years + " " + tr("years") + " ";
|
result += years + " " + tr("years") + " ";
|
||||||
if(years > 0 || days > 0)
|
}
|
||||||
|
|
||||||
|
if(years > 0 || days > 0) {
|
||||||
result += days + " " + tr("days") + " ";
|
result += days + " " + tr("days") + " ";
|
||||||
if(years > 0 || days > 0 || hours > 0)
|
}
|
||||||
|
|
||||||
|
if(years > 0 || days > 0 || hours > 0) {
|
||||||
result += hours + " " + tr("hours") + " ";
|
result += hours + " " + tr("hours") + " ";
|
||||||
if(years > 0 || days > 0 || hours > 0 || minutes > 0)
|
}
|
||||||
|
|
||||||
|
if(years > 0 || days > 0 || hours > 0 || minutes > 0) {
|
||||||
result += minutes + " " + tr("minutes") + " ";
|
result += minutes + " " + tr("minutes") + " ";
|
||||||
if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0)
|
}
|
||||||
|
|
||||||
|
if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0) {
|
||||||
result += seconds + " " + tr("seconds") + " ";
|
result += seconds + " " + tr("seconds") + " ";
|
||||||
else
|
} else {
|
||||||
result = tr("now") + " ";
|
result = tr("now") + " ";
|
||||||
|
}
|
||||||
|
|
||||||
return result.substr(0, result.length - 1);
|
return result.substr(0, result.length - 1);
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue