Improved the server tab navigation bar
parent
92dfd41e58
commit
f2a7f37c74
|
@ -1,4 +1,13 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **24.09.20**
|
||||||
|
- Improved the server tab menu
|
||||||
|
- Better scroll handling
|
||||||
|
- Automatic update on server state changes
|
||||||
|
- Added an audio playback indicator
|
||||||
|
- Rendering the server icon
|
||||||
|
- Changing the favicon according to the clients status
|
||||||
|
- Aborting all replaying audio streams when client mutes himself
|
||||||
|
|
||||||
* **17.09.20**
|
* **17.09.20**
|
||||||
- Added a settings registry and some minor bug fixing
|
- Added a settings registry and some minor bug fixing
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ var initial_css;
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<title>TeaSpeak-Web</title>
|
<title>TeaSpeak-Web</title>
|
||||||
<meta name='og:title' content='TeaSpeak-Web'>
|
<meta name='og:title' content='TeaSpeak-Web'>
|
||||||
<link rel='shortcut icon' href='img/favicon/teacup.png' type='image/x-icon'>
|
<link rel='shortcut icon' href='img/favicon/teacup.png' type='image/x-icon' id="favicon">
|
||||||
<%# <link rel="apple-touch-icon" sizes="194x194" href="/apple-touch-icon.png" type="image/png"> %>
|
<%# <link rel="apple-touch-icon" sizes="194x194" href="/apple-touch-icon.png" type="image/png"> %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
|
|
@ -5,163 +5,8 @@ html:root {
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-connection-handlers {
|
.container-connection-handlers {
|
||||||
$animation_length: .25s;
|
display: flex;
|
||||||
|
|
||||||
margin-top: 0;
|
|
||||||
height: 0;
|
|
||||||
|
|
||||||
transition: all $animation_length ease-in-out;
|
|
||||||
&.shown {
|
|
||||||
margin-top: -4px;
|
|
||||||
height: 24px;
|
|
||||||
|
|
||||||
transition: all $animation_length ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
@include user-select(none);
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.connection-handlers {
|
|
||||||
height: 100%;
|
|
||||||
width: fit-content;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: left;
|
|
||||||
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: visible;
|
|
||||||
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
.connection-container {
|
|
||||||
padding-top: 4px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
|
|
||||||
height: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.server-icon {
|
|
||||||
align-self: center;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-name {
|
|
||||||
color: #a8a8a8;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
margin-right: 20px;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
overflow: visible;
|
|
||||||
text-overflow: clip;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-close {
|
|
||||||
position: absolute;
|
|
||||||
right: 5px;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #212121;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cutoff-name {
|
|
||||||
.server-name {
|
|
||||||
max-width: 10em;
|
|
||||||
margin-right: -5px; /* 5px padding which have to be overcommed */
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: '';
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
background: linear-gradient(to right, transparent, #1e1e1e calc(100% - 20px));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #242425;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: #2d2f32;
|
|
||||||
border-bottom: 1px solid #0d9cfd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-scroll {
|
|
||||||
margin-top: 5px;
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
display: none;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
&.enabled {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-scroll {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
|
|
||||||
border: 1px solid;
|
|
||||||
@include hex-rgba(border-color, #2222223b);
|
|
||||||
|
|
||||||
border-radius: 2px;
|
|
||||||
background: #e7e7e7;
|
|
||||||
padding-left: 2px;
|
|
||||||
padding-right: 2px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #eeeeee;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background: #9e9e9e;
|
|
||||||
&:hover {
|
|
||||||
background: #9e9e9e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.scrollbar {
|
|
||||||
.connection-handlers {
|
|
||||||
width: calc(100% - 45px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
}
|
}
|
|
@ -12,31 +12,10 @@
|
||||||
<!-- navigation bar -->
|
<!-- navigation bar -->
|
||||||
<div class="container-control-bar">
|
<div class="container-control-bar">
|
||||||
<div id="control_bar" class="control_bar">
|
<div id="control_bar" class="control_bar">
|
||||||
<!--
|
|
||||||
<div class="show-small button-dropdown dropdown-audio" title="{{tr 'Audio settings' /}}">
|
|
||||||
<div class="buttons">
|
|
||||||
<div class="button button-display">
|
|
||||||
<div class="icon_em client-music"></div>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown-arrow">
|
|
||||||
<div class="arrow down"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown">
|
|
||||||
<div class="btn_mute_input" title="{{tr 'Mute/unmute microphone' /}}">
|
|
||||||
<div class="icon client-input_muted"></div>
|
|
||||||
<a>{{tr "Mute/unmute microphone" /}}</a>
|
|
||||||
</div>
|
|
||||||
<div class="btn_mute_output" title="{{tr 'Mute/unmute headphones' /}}">
|
|
||||||
<div class="icon client-output_muted"></div>
|
|
||||||
<a>{{tr "Mute/unmute headphones" /}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container-connection-handlers" id="connection-handler-list"></div>
|
||||||
|
<!--
|
||||||
<div class="container-connection-handlers scrollbar" id="connection-handlers">
|
<div class="container-connection-handlers scrollbar" id="connection-handlers">
|
||||||
<div class="connection-handlers"></div>
|
<div class="connection-handlers"></div>
|
||||||
<div class="container-scroll">
|
<div class="container-scroll">
|
||||||
|
@ -48,6 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<div class="container-app-main">
|
<div class="container-app-main">
|
||||||
<div class="container-channel-chat">
|
<div class="container-channel-chat">
|
||||||
|
|
|
@ -1074,6 +1074,7 @@ export class ConnectionHandler {
|
||||||
this.event_registry.fire("notify_state_updated", { state: "speaker" });
|
this.event_registry.fire("notify_state_updated", { state: "speaker" });
|
||||||
if(!muted) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */
|
if(!muted) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */
|
||||||
this.update_voice_status();
|
this.update_voice_status();
|
||||||
|
this.serverConnection.getVoiceConnection().stopAllVoiceReplays();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSpeakerMuted() { this.setSpeakerMuted(!this.isSpeakerMuted()); }
|
toggleSpeakerMuted() { this.setSpeakerMuted(!this.isSpeakerMuted()); }
|
||||||
|
|
|
@ -2,12 +2,11 @@ import {ConnectionHandler, DisconnectReason} from "./ConnectionHandler";
|
||||||
import {Settings, settings} from "./settings";
|
import {Settings, settings} from "./settings";
|
||||||
import {Registry} from "./events";
|
import {Registry} from "./events";
|
||||||
import * as top_menu from "./ui/frames/MenuBar";
|
import * as top_menu from "./ui/frames/MenuBar";
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
|
|
||||||
export let server_connections: ConnectionManager;
|
export let server_connections: ConnectionManager;
|
||||||
export function initializeServerConnections() {
|
|
||||||
if(server_connections) throw tr("Connection manager has already been initialized");
|
|
||||||
server_connections = new ConnectionManager($("#connection-handlers"));
|
|
||||||
}
|
|
||||||
export class ConnectionManager {
|
export class ConnectionManager {
|
||||||
private readonly event_registry: Registry<ConnectionManagerEvents>;
|
private readonly event_registry: Registry<ConnectionManagerEvents>;
|
||||||
private connection_handlers: ConnectionHandler[] = [];
|
private connection_handlers: ConnectionHandler[] = [];
|
||||||
|
@ -63,7 +62,7 @@ export class ConnectionManager {
|
||||||
this._tag.toggleClass("shown", this.connection_handlers.length > 1);
|
this._tag.toggleClass("shown", this.connection_handlers.length > 1);
|
||||||
this._update_scroll();
|
this._update_scroll();
|
||||||
|
|
||||||
this.event_registry.fire("notify_handler_created", { handler: handler });
|
this.event_registry.fire("notify_handler_created", { handler: handler, handlerId: handler.handlerId });
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +85,7 @@ export class ConnectionManager {
|
||||||
|
|
||||||
if(handler === this.active_handler)
|
if(handler === this.active_handler)
|
||||||
this.set_active_connection_(this.connection_handlers[0]);
|
this.set_active_connection_(this.connection_handlers[0]);
|
||||||
this.event_registry.fire("notify_handler_deleted", { handler: handler });
|
this.event_registry.fire("notify_handler_deleted", { handler: handler, handlerId: handler.handlerId });
|
||||||
|
|
||||||
/* destroy all elements */
|
/* destroy all elements */
|
||||||
handler.destroy();
|
handler.destroy();
|
||||||
|
@ -118,8 +117,11 @@ export class ConnectionManager {
|
||||||
const old_handler = this.active_handler;
|
const old_handler = this.active_handler;
|
||||||
this.active_handler = handler;
|
this.active_handler = handler;
|
||||||
this.event_registry.fire("notify_active_handler_changed", {
|
this.event_registry.fire("notify_active_handler_changed", {
|
||||||
old_handler: old_handler,
|
oldHandler: old_handler,
|
||||||
new_handler: handler
|
newHandler: handler,
|
||||||
|
|
||||||
|
oldHandlerId: old_handler?.handlerId,
|
||||||
|
newHandlerId: handler?.handlerId
|
||||||
});
|
});
|
||||||
old_handler?.events().fire("notify_visibility_changed", { visible: false });
|
old_handler?.events().fire("notify_visibility_changed", { visible: false });
|
||||||
handler?.events().fire("notify_visibility_changed", { visible: true });
|
handler?.events().fire("notify_visibility_changed", { visible: true });
|
||||||
|
@ -173,18 +175,31 @@ export class ConnectionManager {
|
||||||
|
|
||||||
export interface ConnectionManagerEvents {
|
export interface ConnectionManagerEvents {
|
||||||
notify_handler_created: {
|
notify_handler_created: {
|
||||||
|
handlerId: string,
|
||||||
handler: ConnectionHandler
|
handler: ConnectionHandler
|
||||||
},
|
},
|
||||||
|
|
||||||
/* This will also trigger when a connection gets deleted. So if you're just interested to connect event handler to the active connection,
|
/* This will also trigger when a connection gets deleted. So if you're just interested to connect event handler to the active connection,
|
||||||
unregister them from the old handler and register them for the new handler every time */
|
unregister them from the old handler and register them for the new handler every time */
|
||||||
notify_active_handler_changed: {
|
notify_active_handler_changed: {
|
||||||
old_handler: ConnectionHandler | undefined,
|
oldHandler: ConnectionHandler | undefined,
|
||||||
new_handler: ConnectionHandler | undefined
|
newHandler: ConnectionHandler | undefined,
|
||||||
|
|
||||||
|
oldHandlerId: string | undefined,
|
||||||
|
newHandlerId: string | undefined
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Will never fire on an active connection handler! */
|
/* Will never fire on an active connection handler! */
|
||||||
notify_handler_deleted: {
|
notify_handler_deleted: {
|
||||||
|
handlerId: string,
|
||||||
handler: ConnectionHandler
|
handler: ConnectionHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "server manager init",
|
||||||
|
function: async () => {
|
||||||
|
server_connections = new ConnectionManager($("#connection-handlers"));
|
||||||
|
},
|
||||||
|
priority: 80
|
||||||
|
});
|
||||||
|
|
|
@ -141,4 +141,10 @@ export class DummyVoiceConnection extends AbstractVoiceConnection {
|
||||||
getFailedMessage(): string {
|
getFailedMessage(): string {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isReplayingVoice(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAllVoiceReplays() { }
|
||||||
}
|
}
|
|
@ -32,6 +32,10 @@ export interface VoiceConnectionEvents {
|
||||||
},
|
},
|
||||||
"notify_whisper_destroyed": {
|
"notify_whisper_destroyed": {
|
||||||
session: WhisperSession
|
session: WhisperSession
|
||||||
|
},
|
||||||
|
|
||||||
|
"notify_voice_replay_state_change": {
|
||||||
|
replaying: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +76,9 @@ export abstract class AbstractVoiceConnection {
|
||||||
abstract getEncoderCodec() : number;
|
abstract getEncoderCodec() : number;
|
||||||
abstract setEncoderCodec(codec: number);
|
abstract setEncoderCodec(codec: number);
|
||||||
|
|
||||||
|
abstract stopAllVoiceReplays();
|
||||||
|
abstract isReplayingVoice() : boolean;
|
||||||
|
|
||||||
/* the whisper API */
|
/* the whisper API */
|
||||||
abstract getWhisperSessions() : WhisperSession[];
|
abstract getWhisperSessions() : WhisperSession[];
|
||||||
abstract dropWhisperSession(session: WhisperSession);
|
abstract dropWhisperSession(session: WhisperSession);
|
||||||
|
|
|
@ -114,7 +114,13 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
|
||||||
|
|
||||||
|
|
||||||
/* special helper methods for react components */
|
/* special helper methods for react components */
|
||||||
reactUse<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void, condition?: boolean) {
|
/**
|
||||||
|
* @param event
|
||||||
|
* @param handler
|
||||||
|
* @param condition
|
||||||
|
* @param reactEffectDependencies
|
||||||
|
*/
|
||||||
|
reactUse<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void, condition?: boolean, reactEffectDependencies?: any[]) {
|
||||||
if(typeof condition === "boolean" && !condition) {
|
if(typeof condition === "boolean" && !condition) {
|
||||||
useEffect(() => {});
|
useEffect(() => {});
|
||||||
return;
|
return;
|
||||||
|
@ -123,7 +129,7 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handlers.push(handler);
|
handlers.push(handler);
|
||||||
return () => handlers.remove(handler);
|
return () => handlers.remove(handler);
|
||||||
});
|
}, reactEffectDependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectAll<EOther, T extends keyof Events & keyof EOther>(target: EventReceiver<Events>) {
|
connectAll<EOther, T extends keyof Events & keyof EOther>(target: EventReceiver<Events>) {
|
||||||
|
|
|
@ -46,7 +46,7 @@ export function load_default_states(event_registry: Registry<ClientGlobalControl
|
||||||
|
|
||||||
export function initialize(event_registry: Registry<ClientGlobalControlEvents>) {
|
export function initialize(event_registry: Registry<ClientGlobalControlEvents>) {
|
||||||
let current_connection_handler: ConnectionHandler | undefined;
|
let current_connection_handler: ConnectionHandler | undefined;
|
||||||
server_connections.events().on("notify_active_handler_changed", event => current_connection_handler = event.new_handler);
|
server_connections.events().on("notify_active_handler_changed", event => current_connection_handler = event.newHandler);
|
||||||
//initialize_sounds(event_registry);
|
//initialize_sounds(event_registry);
|
||||||
|
|
||||||
event_registry.on("action_open_window", event => {
|
event_registry.on("action_open_window", event => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
import {settings, Settings} from "tc-shared/settings";
|
import {settings, Settings} from "tc-shared/settings";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
@ -6,7 +7,7 @@ import * as bipc from "./ipc/BrowserIPC";
|
||||||
import * as sound from "./sound/Sounds";
|
import * as sound from "./sound/Sounds";
|
||||||
import * as i18n from "./i18n/localize";
|
import * as i18n from "./i18n/localize";
|
||||||
import {tra} from "./i18n/localize";
|
import {tra} from "./i18n/localize";
|
||||||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import {createInfoModal} from "tc-shared/ui/elements/Modal";
|
import {createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||||
import * as stats from "./stats";
|
import * as stats from "./stats";
|
||||||
import * as fidentity from "./profiles/identities/TeaForumIdentity";
|
import * as fidentity from "./profiles/identities/TeaForumIdentity";
|
||||||
|
@ -42,10 +43,10 @@ import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
|
||||||
import "./video-viewer/Controller";
|
import "./video-viewer/Controller";
|
||||||
import "./profiles/ConnectionProfile";
|
import "./profiles/ConnectionProfile";
|
||||||
import "./update/UpdaterWeb";
|
import "./update/UpdaterWeb";
|
||||||
import ContextMenuEvent = JQuery.ContextMenuEvent;
|
|
||||||
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
|
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
|
||||||
import {spawnGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Controller";
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
import {initializeServerConnections, server_connections} from "tc-shared/ConnectionManager";
|
import {initializeConnectionUIList} from "tc-shared/ui/frames/connection-handler-list/Controller";
|
||||||
|
import ContextMenuEvent = JQuery.ContextMenuEvent;
|
||||||
|
|
||||||
let preventWelcomeUI = false;
|
let preventWelcomeUI = false;
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
|
@ -61,20 +62,8 @@ async function initialize() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initialize_app() {
|
async function initialize_app() {
|
||||||
try { //Initialize main template
|
initializeConnectionUIList();
|
||||||
const main = $("#tmpl_main").renderTag({
|
|
||||||
multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
|
|
||||||
app_version: __build.version
|
|
||||||
}).dividerfy();
|
|
||||||
|
|
||||||
$("body").append(main);
|
|
||||||
} catch(error) {
|
|
||||||
log.error(LogCategory.GENERAL, error);
|
|
||||||
loader.critical_error(tr("Failed to setup main page!"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeServerConnections();
|
|
||||||
global_ev_handler.initialize(global_client_actions);
|
global_ev_handler.initialize(global_client_actions);
|
||||||
{
|
{
|
||||||
const bar = (
|
const bar = (
|
||||||
|
@ -83,6 +72,7 @@ async function initialize_app() {
|
||||||
|
|
||||||
ReactDOM.render(bar, $(".container-control-bar")[0]);
|
ReactDOM.render(bar, $(".container-control-bar")[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
name: "settings init",
|
name: "settings init",
|
||||||
|
@ -201,6 +191,16 @@ function main() {
|
||||||
const initial_handler = server_connections.spawn_server_connection();
|
const initial_handler = server_connections.spawn_server_connection();
|
||||||
initial_handler.acquireInputHardware().then(() => {});
|
initial_handler.acquireInputHardware().then(() => {});
|
||||||
server_connections.set_active_connection(initial_handler);
|
server_connections.set_active_connection(initial_handler);
|
||||||
|
|
||||||
|
/* Just a test */
|
||||||
|
{
|
||||||
|
server_connections.spawn_server_connection();
|
||||||
|
server_connections.spawn_server_connection();
|
||||||
|
server_connections.spawn_server_connection();
|
||||||
|
server_connections.spawn_server_connection();
|
||||||
|
server_connections.spawn_server_connection();
|
||||||
|
}
|
||||||
|
|
||||||
/** Setup the XF forum identity **/
|
/** Setup the XF forum identity **/
|
||||||
fidentity.update_forum();
|
fidentity.update_forum();
|
||||||
keycontrol.initialize();
|
keycontrol.initialize();
|
||||||
|
@ -476,7 +476,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
priority: 100
|
priority: 110
|
||||||
});
|
});
|
||||||
|
|
||||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
@ -511,4 +511,23 @@ loader.register_task(loader.Stage.LOADED, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
priority: 20
|
priority: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING,{
|
||||||
|
name: "app init",
|
||||||
|
function: async () => {
|
||||||
|
try { //Initialize main template
|
||||||
|
const main = $("#tmpl_main").renderTag({
|
||||||
|
multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
|
||||||
|
app_version: __build.version
|
||||||
|
}).dividerfy();
|
||||||
|
|
||||||
|
$("body").append(main);
|
||||||
|
} catch(error) {
|
||||||
|
log.error(LogCategory.GENERAL, error);
|
||||||
|
loader.critical_error(tr("Failed to setup main page!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
priority: 100
|
||||||
});
|
});
|
|
@ -264,6 +264,38 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
||||||
return this._properties;
|
return this._properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStatusIcon() : ClientIcon {
|
||||||
|
if (this.properties.client_type_exact == ClientType.CLIENT_QUERY) {
|
||||||
|
return ClientIcon.ServerQuery;
|
||||||
|
} else if (this.properties.client_away) {
|
||||||
|
return ClientIcon.Away;
|
||||||
|
} else if (!this.getVoiceClient() && !(this instanceof LocalClientEntry)) {
|
||||||
|
return ClientIcon.InputMutedLocal;
|
||||||
|
} else if (!this.properties.client_output_hardware) {
|
||||||
|
return ClientIcon.HardwareOutputMuted;
|
||||||
|
} else if (this.properties.client_output_muted) {
|
||||||
|
return ClientIcon.OutputMuted;
|
||||||
|
} else if (!this.properties.client_input_hardware) {
|
||||||
|
return ClientIcon.HardwareInputMuted;
|
||||||
|
} else if (this.properties.client_input_muted) {
|
||||||
|
return ClientIcon.InputMuted;
|
||||||
|
} else {
|
||||||
|
if (this.isSpeaking()) {
|
||||||
|
if (this.properties.client_is_channel_commander) {
|
||||||
|
return ClientIcon.PlayerCommanderOn;
|
||||||
|
} else {
|
||||||
|
return ClientIcon.PlayerOn;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.properties.client_is_channel_commander) {
|
||||||
|
return ClientIcon.PlayerCommanderOff;
|
||||||
|
} else {
|
||||||
|
return ClientIcon.PlayerOff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentChannel() : ChannelEntry { return this._channel; }
|
currentChannel() : ChannelEntry { return this._channel; }
|
||||||
clientNickName(){ return this.properties.client_nickname; }
|
clientNickName(){ return this.properties.client_nickname; }
|
||||||
clientUid(){ return this.properties.client_unique_identifier; }
|
clientUid(){ return this.properties.client_unique_identifier; }
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {ConnectionListUIEvents, HandlerConnectionState} from "tc-shared/ui/frames/connection-handler-list/Definitions";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import {ConnectionHandlerList} from "tc-shared/ui/frames/connection-handler-list/Renderer";
|
||||||
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
|
import {LogCategory, logWarn} from "tc-shared/log";
|
||||||
|
import {ConnectionState} from "tc-shared/ConnectionHandler";
|
||||||
|
import {LocalIcon} from "tc-shared/file/Icons";
|
||||||
|
|
||||||
|
export function initializeConnectionUIList() {
|
||||||
|
const container = document.getElementById("connection-handler-list");
|
||||||
|
const events = new Registry<ConnectionListUIEvents>();
|
||||||
|
events.enableDebug("Handler-List");
|
||||||
|
initializeController(events);
|
||||||
|
|
||||||
|
ReactDOM.render(React.createElement(ConnectionHandlerList, { events: events }), container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeController(events: Registry<ConnectionListUIEvents>) {
|
||||||
|
let registeredHandlerEvents: {[key: string]:(() => void)[]} = {};
|
||||||
|
|
||||||
|
events.on("notify_destroy", () => {
|
||||||
|
Object.keys(registeredHandlerEvents).forEach(handlerId => registeredHandlerEvents[handlerId].forEach(callback => callback()));
|
||||||
|
registeredHandlerEvents = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("query_handler_list", () => {
|
||||||
|
events.fire_async("notify_handler_list", { handlerIds: server_connections.all_connections().map(e => e.handlerId), activeHandlerId: server_connections.active_connection()?.handlerId });
|
||||||
|
});
|
||||||
|
events.on("notify_destroy", server_connections.events().on("notify_handler_created", event => {
|
||||||
|
let listeners = [];
|
||||||
|
|
||||||
|
const handlerId = event.handlerId;
|
||||||
|
listeners.push(event.handler.events().on("notify_connection_state_changed", () => events.fire_async("query_handler_status", { handlerId: handlerId })));
|
||||||
|
|
||||||
|
/* register to icon and name change updates */
|
||||||
|
listeners.push(event.handler.channelTree.server.events.on("notify_properties_updated", event => {
|
||||||
|
if("virtualserver_name" in event.updated_properties || "virtualserver_icon_id" in event.updated_properties) {
|
||||||
|
events.fire_async("query_handler_status", { handlerId: handlerId });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* register to voice playback change events */
|
||||||
|
listeners.push(event.handler.getServerConnection().getVoiceConnection().events.on("notify_voice_replay_state_change", () => {
|
||||||
|
events.fire_async("query_handler_status", { handlerId: handlerId });
|
||||||
|
}));
|
||||||
|
|
||||||
|
registeredHandlerEvents[event.handlerId] = listeners;
|
||||||
|
|
||||||
|
events.fire_async("query_handler_list");
|
||||||
|
}));
|
||||||
|
events.on("notify_destroy", server_connections.events().on("notify_handler_deleted", event => {
|
||||||
|
(registeredHandlerEvents[event.handlerId] || []).forEach(callback => callback());
|
||||||
|
delete registeredHandlerEvents[event.handlerId];
|
||||||
|
|
||||||
|
events.fire_async("query_handler_list");
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
events.on("action_set_active_handler", event => {
|
||||||
|
const handler = server_connections.findConnection(event.handlerId);
|
||||||
|
if(!handler) {
|
||||||
|
logWarn(LogCategory.CLIENT, tr("Tried to activate an invalid server connection handler with id %s"), event.handlerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server_connections.set_active_connection(handler);
|
||||||
|
});
|
||||||
|
events.on("notify_destroy", server_connections.events().on("notify_active_handler_changed", event => {
|
||||||
|
events.fire_async("notify_active_handler", { handlerId: event.newHandlerId });
|
||||||
|
}));
|
||||||
|
|
||||||
|
events.on("action_destroy_handler", event => {
|
||||||
|
const handler = server_connections.findConnection(event.handlerId);
|
||||||
|
if(!handler) {
|
||||||
|
logWarn(LogCategory.CLIENT, tr("Tried to destroy an invalid server connection handler with id %s"), event.handlerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server_connections.destroy_server_connection(handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
events.on("query_handler_status", event => {
|
||||||
|
const handler = server_connections.findConnection(event.handlerId);
|
||||||
|
if(!handler) {
|
||||||
|
logWarn(LogCategory.CLIENT, tr("Tried to query a status for an invalid server connection handler with id %s"), event.handlerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state: HandlerConnectionState;
|
||||||
|
switch (handler.connection_state) {
|
||||||
|
case ConnectionState.CONNECTED:
|
||||||
|
state = "connected";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConnectionState.AUTHENTICATING:
|
||||||
|
case ConnectionState.CONNECTING:
|
||||||
|
case ConnectionState.INITIALISING:
|
||||||
|
state = "connecting";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
state = "disconnected";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon: LocalIcon | undefined;
|
||||||
|
let iconId = handler.channelTree.server.properties.virtualserver_icon_id;
|
||||||
|
if(iconId !== 0) {
|
||||||
|
icon = handler.fileManager.icons.load_icon(handler.channelTree.server.properties.virtualserver_icon_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
events.fire_async("notify_handler_status", {
|
||||||
|
handlerId: event.handlerId,
|
||||||
|
status: {
|
||||||
|
handlerName: handler.channelTree.server.properties.virtualserver_name,
|
||||||
|
connectionState: state,
|
||||||
|
voiceReplaying: handler.getServerConnection().getVoiceConnection().isReplayingVoice(),
|
||||||
|
serverIcon: icon
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import {LocalIcon} from "tc-shared/file/Icons";
|
||||||
|
|
||||||
|
export type HandlerConnectionState = "disconnected" | "connecting" | "connected";
|
||||||
|
|
||||||
|
export type HandlerStatus = {
|
||||||
|
connectionState: HandlerConnectionState,
|
||||||
|
handlerName: string,
|
||||||
|
voiceReplaying: boolean,
|
||||||
|
serverIcon: LocalIcon | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectionListUIEvents {
|
||||||
|
action_set_active_handler: { handlerId: string },
|
||||||
|
action_destroy_handler: { handlerId: string },
|
||||||
|
action_scroll: { direction: "left" | "right" }
|
||||||
|
|
||||||
|
query_handler_status: { handlerId: string },
|
||||||
|
query_handler_list: {},
|
||||||
|
|
||||||
|
notify_handler_list: {
|
||||||
|
handlerIds: string[],
|
||||||
|
activeHandlerId: string | undefined
|
||||||
|
},
|
||||||
|
notify_active_handler: {
|
||||||
|
handlerId: string
|
||||||
|
},
|
||||||
|
notify_handler_status: {
|
||||||
|
handlerId: string,
|
||||||
|
status: HandlerStatus
|
||||||
|
},
|
||||||
|
notify_scroll_status: {
|
||||||
|
left: boolean,
|
||||||
|
right: boolean
|
||||||
|
},
|
||||||
|
notify_destroy: {}
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
$animation_length: .25s;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
transition: all $animation_length ease-in-out;
|
||||||
|
&.shown {
|
||||||
|
margin-top: -4px;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
transition: all $animation_length ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
@include user-select(none);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.handlerList {
|
||||||
|
height: 100%;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: left;
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
|
.handler {
|
||||||
|
padding-top: 4px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
|
||||||
|
height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
@include transition(all ease-in-out $button_hover_animation_time);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: #a8a8a8;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 20px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonClose {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #212121;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cutoffName {
|
||||||
|
.name {
|
||||||
|
max-width: 15em;
|
||||||
|
margin-right: -5px; /* 5px padding which have to be overcommed */
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background: linear-gradient(to right, transparent calc(100% - 50px), #1e1e1e calc(100% - 25px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #242425;
|
||||||
|
|
||||||
|
&.cutoffName .name:before {
|
||||||
|
background: linear-gradient(to right, transparent calc(100% - 50px), #242425 calc(100% - 25px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-bottom-color: #0d9cfd;
|
||||||
|
background-color: #2d2f32;
|
||||||
|
|
||||||
|
&.cutoffName .name:before {
|
||||||
|
background: linear-gradient(to right, transparent calc(100% - 50px), #2d2f32 calc(100% - 25px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.audioPlayback {
|
||||||
|
border-bottom-color: #68c1fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollSpacer {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
width: calc(2em + 8px);
|
||||||
|
height: 1px;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerScroll {
|
||||||
|
margin-top: 5px;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
&.shown {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonScroll {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
|
||||||
|
border: 1px solid;
|
||||||
|
@include hex-rgba(border-color, #2222223b);
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #e7e7e7;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: #9e9e9e;
|
||||||
|
&:hover {
|
||||||
|
background: #9e9e9e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.scrollShown {
|
||||||
|
/*
|
||||||
|
.connection-handlers {
|
||||||
|
width: calc(100% - 45px);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.handlerList .scrollSpacer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,243 @@
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {ConnectionListUIEvents, HandlerStatus} from "tc-shared/ui/frames/connection-handler-list/Definitions";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useContext, useEffect, useRef, useState} from "react";
|
||||||
|
import {IconRenderer, LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
import {LogCategory, logWarn} from "tc-shared/log";
|
||||||
|
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||||
|
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
const Events = React.createContext<Registry<ConnectionListUIEvents>>(undefined);
|
||||||
|
|
||||||
|
|
||||||
|
const ConnectionHandler = (props: { handlerId: string, active: boolean }) => {
|
||||||
|
const events = useContext(Events);
|
||||||
|
|
||||||
|
const [ status, setStatus ] = useState<HandlerStatus | "loading">(() => {
|
||||||
|
events.fire_async("query_handler_status", { handlerId: props.handlerId });
|
||||||
|
return "loading";
|
||||||
|
});
|
||||||
|
|
||||||
|
events.reactUse("notify_handler_status", event => {
|
||||||
|
if(event.handlerId !== props.handlerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(event.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
let displayedName;
|
||||||
|
let cutoffName = false;
|
||||||
|
let voiceReplaying = false;
|
||||||
|
let icon = <IconRenderer icon={ClientIcon.ServerGreen} key={"default"} />;
|
||||||
|
if(status === "loading") {
|
||||||
|
displayedName = tr("loading status");
|
||||||
|
} else {
|
||||||
|
switch (status.connectionState) {
|
||||||
|
case "connected":
|
||||||
|
cutoffName = status.handlerName.length > 30;
|
||||||
|
voiceReplaying = status.voiceReplaying;
|
||||||
|
displayedName = <React.Fragment key={"connected"}>{status.handlerName}</React.Fragment>;
|
||||||
|
if(status.serverIcon) {
|
||||||
|
icon = <LocalIconRenderer icon={status.serverIcon} key={"server-icon"} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "connecting":
|
||||||
|
displayedName = <Translatable key={"connecting"}>Connecting to server <LoadingDots /></Translatable>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "disconnected":
|
||||||
|
displayedName = <Translatable key={"not connected"}>Not connected</Translatable>;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.handler + " " + (props.active ? cssStyle.active : "") + " " + (cutoffName ? cssStyle.cutoffName : "") + " " + (voiceReplaying ? cssStyle.audioPlayback : "")}
|
||||||
|
onClick={() => {
|
||||||
|
if(props.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.fire("action_set_active_handler", { handlerId: props.handlerId });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={cssStyle.icon}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.name} title={displayedName}>{displayedName}</div>
|
||||||
|
<div className={cssStyle.buttonClose} onClick={() => {
|
||||||
|
events.fire("action_destroy_handler", { handlerId: props.handlerId })
|
||||||
|
}}>
|
||||||
|
<IconRenderer icon={ClientIcon.TabCloseButton} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HandlerList = (props: { refContainer: React.Ref<HTMLDivElement>, refSpacer: React.Ref<HTMLDivElement> }) => {
|
||||||
|
const events = useContext(Events);
|
||||||
|
|
||||||
|
const [ handlers, setHandlers ] = useState<string[] | "loading">(() => {
|
||||||
|
events.fire("query_handler_list");
|
||||||
|
return "loading";
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ activeHandler, setActiveHandler ] = useState<string>();
|
||||||
|
|
||||||
|
events.reactUse("notify_handler_list", event => {
|
||||||
|
setHandlers(event.handlerIds.slice());
|
||||||
|
setActiveHandler(event.activeHandlerId);
|
||||||
|
});
|
||||||
|
events.reactUse("notify_active_handler", event => setActiveHandler(event.handlerId));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.handlerList} ref={props.refContainer}>
|
||||||
|
{handlers === "loading" ? undefined :
|
||||||
|
handlers.map(handlerId => <ConnectionHandler handlerId={handlerId} key={handlerId} active={handlerId === activeHandler} />)
|
||||||
|
}
|
||||||
|
<div className={cssStyle.scrollSpacer} ref={props.refSpacer} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScrollMenu = (props: { shown: boolean }) => {
|
||||||
|
const events = useContext(Events);
|
||||||
|
|
||||||
|
const [ scrollLeft, setScrollLeft ] = useState(false);
|
||||||
|
const [ scrollRight, setScrollRight ] = useState(false);
|
||||||
|
|
||||||
|
events.on("notify_scroll_status", event => {
|
||||||
|
setScrollLeft(event.left);
|
||||||
|
setScrollRight(event.right);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.containerScroll + " " + (props.shown ? cssStyle.shown : "")}>
|
||||||
|
<div className={cssStyle.buttonScroll + " " + (!scrollLeft ? cssStyle.disabled : "")} onClick={() => scrollLeft && events.fire_async("action_scroll", { direction: "left" })}>
|
||||||
|
<ClientIconRenderer icon={ClientIcon.ArrowLeft} />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.buttonScroll + " " + (!scrollRight ? cssStyle.disabled : "")} onClick={() => scrollRight && events.fire_async("action_scroll", { direction: "right" })}>
|
||||||
|
<ClientIconRenderer icon={ClientIcon.ArrowRight} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConnectionHandlerList = (props: { events: Registry<ConnectionListUIEvents> }) => {
|
||||||
|
const [ shown, setShown ] = useState(false);
|
||||||
|
const observer = useRef<ResizeObserver>();
|
||||||
|
const refHandlerContainer = useRef<HTMLDivElement>();
|
||||||
|
const refContainer = useRef<HTMLDivElement>();
|
||||||
|
const refScrollSpacer = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
const refScrollShown = useRef(false);
|
||||||
|
const [ scrollShown, setScrollShown ] = useState(false);
|
||||||
|
refScrollShown.current = scrollShown;
|
||||||
|
|
||||||
|
const updateScrollButtons = (scrollLeft: number) => {
|
||||||
|
const container = refHandlerContainer.current;
|
||||||
|
if(!container) { return; }
|
||||||
|
props.events.fire_async("notify_scroll_status", { right: Math.ceil(scrollLeft + container.clientWidth + 2) < container.scrollWidth, left: scrollLeft !== 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!refHandlerContainer.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(observer.current) {
|
||||||
|
throw "useEffect called without a detachment...";
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.current = new ResizeObserver(events => {
|
||||||
|
if(!refContainer.current || !refHandlerContainer.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(events.length !== 1) {
|
||||||
|
logWarn(LogCategory.CLIENT, tr("Handler list resize observer received events for more than one element (%d elements)."), events.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = events[0].target.scrollWidth;
|
||||||
|
const shouldScroll = width > refContainer.current.clientWidth;
|
||||||
|
|
||||||
|
let scrollShown = refScrollShown.current;
|
||||||
|
if(scrollShown && !shouldScroll) {
|
||||||
|
props.events.fire_async("notify_scroll_status", { left: false, right: false });
|
||||||
|
} else if(!scrollShown && shouldScroll) {
|
||||||
|
props.events.fire_async("notify_scroll_status", { left: false, right: true });
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setScrollShown(shouldScroll);
|
||||||
|
});
|
||||||
|
observer.current.observe(refHandlerContainer.current);
|
||||||
|
return () => {
|
||||||
|
observer.current?.disconnect();
|
||||||
|
observer.current = undefined;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
props.events.reactUse("notify_handler_list", event => setShown(event.handlerIds.length > 1));
|
||||||
|
props.events.reactUse("action_scroll", event => {
|
||||||
|
if(!scrollShown || !refHandlerContainer.current || !refScrollSpacer.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = refHandlerContainer.current;
|
||||||
|
const scrollContainer = refScrollSpacer.current;
|
||||||
|
const getChildAt = (width: number) => {
|
||||||
|
let currentChild: HTMLDivElement;
|
||||||
|
for(const childElement of container.children) {
|
||||||
|
const child = childElement as HTMLDivElement;
|
||||||
|
if((!currentChild || child.offsetLeft > currentChild.offsetLeft) && child.offsetLeft <= width) {
|
||||||
|
currentChild = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scrollLeft;
|
||||||
|
if(event.direction === "right") {
|
||||||
|
const currentLeft = container.scrollLeft;
|
||||||
|
const target = getChildAt(currentLeft + container.clientWidth + 5 - scrollContainer.clientWidth);
|
||||||
|
if(!target) {
|
||||||
|
/* well we're fucked up? :D */
|
||||||
|
scrollLeft = container.scrollLeft + 50;
|
||||||
|
} else {
|
||||||
|
scrollLeft = target.offsetLeft + target.clientWidth - container.clientWidth + scrollContainer.clientWidth;
|
||||||
|
}
|
||||||
|
} else if(event.direction === "left") {
|
||||||
|
const currentLeft = container.scrollLeft;
|
||||||
|
const target = getChildAt(currentLeft - 1);
|
||||||
|
if(!target) {
|
||||||
|
/* well we're fucked up? :D */
|
||||||
|
scrollLeft = container.scrollLeft - 50;
|
||||||
|
} else {
|
||||||
|
scrollLeft = target.offsetLeft;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.scrollLeft = scrollLeft;
|
||||||
|
updateScrollButtons(scrollLeft);
|
||||||
|
}, true, [scrollShown]);
|
||||||
|
return (
|
||||||
|
<Events.Provider value={props.events}>
|
||||||
|
<div className={cssStyle.container + " " + (shown ? cssStyle.shown : "") + " " + (scrollShown ? cssStyle.scrollShown : "")} ref={refContainer}>
|
||||||
|
<HandlerList refContainer={refHandlerContainer} refSpacer={refScrollSpacer} />
|
||||||
|
<ScrollMenu shown={scrollShown} />
|
||||||
|
</div>
|
||||||
|
</Events.Provider>
|
||||||
|
)
|
||||||
|
};
|
|
@ -434,12 +434,12 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleActiveConnectionHandlerChanged(event: ConnectionManagerEvents["notify_active_handler_changed"]) {
|
private handleActiveConnectionHandlerChanged(event: ConnectionManagerEvents["notify_active_handler_changed"]) {
|
||||||
if(event.old_handler)
|
if(event.oldHandler)
|
||||||
this.unregisterConnectionHandlerEvents(event.old_handler);
|
this.unregisterConnectionHandlerEvents(event.oldHandler);
|
||||||
|
|
||||||
this.connection = event.new_handler;
|
this.connection = event.newHandler;
|
||||||
if(event.new_handler)
|
if(event.newHandler)
|
||||||
this.registerConnectionHandlerEvents(event.new_handler);
|
this.registerConnectionHandlerEvents(event.newHandler);
|
||||||
|
|
||||||
this.event_registry.fire("set_connection_handler", { handler: this.connection });
|
this.event_registry.fire("set_connection_handler", { handler: this.connection });
|
||||||
this.event_registry.fire("update_state_all");
|
this.event_registry.fire("update_state_all");
|
||||||
|
|
|
@ -217,16 +217,17 @@ const OverrideVariableInfo = (props: { events: Registry<CssEditorEvents> }) => {
|
||||||
|
|
||||||
selectedVariable.overwriteValue = event.enabled;
|
selectedVariable.overwriteValue = event.enabled;
|
||||||
setOverwriteEnabled(event.enabled);
|
setOverwriteEnabled(event.enabled);
|
||||||
if (event.enabled)
|
if (event.enabled) {
|
||||||
setOverwriteValue(event.value);
|
setOverwriteValue(event.value);
|
||||||
});
|
}
|
||||||
|
}, true, [selectedVariable]);
|
||||||
|
|
||||||
props.events.reactUse("action_change_override_value", event => {
|
props.events.reactUse("action_change_override_value", event => {
|
||||||
if (event.variableName !== selectedVariable?.name)
|
if (event.variableName !== selectedVariable?.name)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
setOverwriteValue(event.value);
|
setOverwriteValue(event.value);
|
||||||
});
|
}, true, [selectedVariable]);
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<div className={cssStyle.detail}>
|
<div className={cssStyle.detail}>
|
||||||
|
|
|
@ -78,7 +78,7 @@ const ActivityBar = (props: { events: Registry<MicrophoneSettingsEvents>, device
|
||||||
setStatus({mode: "error", message: device.error + ""});
|
setStatus({mode: "error", message: device.error + ""});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, true, [status]);
|
||||||
|
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {LocalIcon} from "tc-shared/file/Icons";
|
import {LocalIcon} from "tc-shared/file/Icons";
|
||||||
|
|
||||||
export interface IconProperties {
|
export const IconRenderer = (props: {
|
||||||
icon: string | LocalIcon;
|
icon: string | LocalIcon;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
className?: string;
|
||||||
|
}) => {
|
||||||
export class IconRenderer extends React.Component<IconProperties, {}> {
|
if(!props.icon) {
|
||||||
render() {
|
return <div className={"icon-container icon-empty " + props.className} title={props.title} />;
|
||||||
if(!this.props.icon)
|
} else if(typeof props.icon === "string") {
|
||||||
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
return <div className={"icon " + props.icon + " " + props.className} title={props.title} />;
|
||||||
else if(typeof this.props.icon === "string")
|
} else if(props.icon instanceof LocalIcon) {
|
||||||
return <div className={"icon " + this.props.icon} title={this.props.title} />;
|
return <LocalIconRenderer icon={props.icon} title={props.title} className={props.className} />;
|
||||||
else if(this.props.icon instanceof LocalIcon)
|
} else {
|
||||||
return <LocalIconRenderer icon={this.props.icon} title={this.props.title} />;
|
throw "JQuery icons are not longer supported";
|
||||||
else throw "JQuery icons are not longer supported";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadedIconRenderer {
|
export interface LoadedIconRenderer {
|
||||||
icon: LocalIcon;
|
icon: LocalIcon;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LocalIconRenderer extends React.Component<LoadedIconRenderer, {}> {
|
export class LocalIconRenderer extends React.Component<LoadedIconRenderer, {}> {
|
||||||
|
@ -39,18 +39,18 @@ export class LocalIconRenderer extends React.Component<LoadedIconRenderer, {}> {
|
||||||
render() {
|
render() {
|
||||||
const icon = this.props.icon;
|
const icon = this.props.icon;
|
||||||
if(!icon || icon.status === "empty" || icon.status === "destroyed")
|
if(!icon || icon.status === "empty" || icon.status === "destroyed")
|
||||||
return <div key={"empty"} className={"icon-container icon-empty"} title={this.props.title} />;
|
return <div key={"empty"} className={"icon-container icon-empty " + this.props.className} title={this.props.title} />;
|
||||||
else if(icon.status === "loaded") {
|
else if(icon.status === "loaded") {
|
||||||
if(icon.icon_id >= 0 && icon.icon_id <= 1000) {
|
if(icon.icon_id >= 0 && icon.icon_id <= 1000) {
|
||||||
if(icon.icon_id === 0)
|
if(icon.icon_id === 0)
|
||||||
return <div key={"loaded-empty"} className={"icon-container icon-empty"} title={this.props.title} />;
|
return <div key={"loaded-empty"} className={"icon-container icon-empty"} title={this.props.title} />;
|
||||||
return <div key={"loaded"} className={"icon_em client-group_" + icon.icon_id} />;
|
return <div key={"loaded"} className={"icon_em client-group_" + icon.icon_id} />;
|
||||||
}
|
}
|
||||||
return <div key={"icon"} className={"icon-container"}><img style={{ maxWidth: "100%", maxHeight: "100%" }} src={icon.loaded_url} alt={this.props.title || ("icon " + icon.icon_id)} /></div>;
|
return <div key={"icon"} className={"icon-container " + this.props.className}><img style={{ maxWidth: "100%", maxHeight: "100%" }} src={icon.loaded_url} alt={this.props.title || ("icon " + icon.icon_id)} /></div>;
|
||||||
} else if(icon.status === "loading")
|
} else if(icon.status === "loading")
|
||||||
return <div key={"loading"} className={"icon-container"} title={this.props.title}><div className={"icon_loading"} /></div>;
|
return <div key={"loading"} className={"icon-container " + this.props.className} title={this.props.title}><div className={"icon_loading"} /></div>;
|
||||||
else if(icon.status === "error")
|
else if(icon.status === "error")
|
||||||
return <div key={"error"} className={"icon client-warning"} title={icon.error_message || tr("Failed to load icon")} />;
|
return <div key={"error"} className={"icon client-warning " + this.props.className} title={icon.error_message || tr("Failed to load icon")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
|
|
@ -19,11 +19,14 @@ class TitleRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
setInstance(instance: AbstractModal) {
|
setInstance(instance: AbstractModal) {
|
||||||
if(this.modalInstance)
|
if(this.modalInstance) {
|
||||||
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
||||||
|
}
|
||||||
|
|
||||||
this.modalInstance = instance;
|
this.modalInstance = instance;
|
||||||
if(this.modalInstance)
|
if(this.modalInstance) {
|
||||||
ReactDOM.render(<>{this.modalInstance.title()}</>, this.htmlContainer);
|
ReactDOM.render(<>{this.modalInstance.title()}</>, this.htmlContainer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,85 +20,37 @@ import {LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
import * as DOMPurify from "dompurify";
|
import * as DOMPurify from "dompurify";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
const clientStyle = require("./Client.scss");
|
const clientStyle = require("./Client.scss");
|
||||||
const viewStyle = require("./View.scss");
|
const viewStyle = require("./View.scss");
|
||||||
|
|
||||||
interface ClientIconProperties {
|
const IconUpdateKeys: (keyof ClientProperties)[] = [
|
||||||
client: ClientEntryController;
|
"client_away",
|
||||||
}
|
"client_input_hardware",
|
||||||
|
"client_output_hardware",
|
||||||
|
"client_output_muted",
|
||||||
|
"client_input_muted",
|
||||||
|
"client_is_channel_commander",
|
||||||
|
"client_talk_power"
|
||||||
|
];
|
||||||
|
|
||||||
@ReactEventHandler<ClientSpeakIcon>(e => e.props.client.events)
|
export const ClientStatusIndicator = (props: { client: ClientEntryController, renderer?: (icon: ClientIcon) => React.ReactElement }) => {
|
||||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
const [ revision, setRevision ] = useState(0);
|
||||||
class ClientSpeakIcon extends ReactComponentBase<ClientIconProperties, {}> {
|
|
||||||
private static readonly IconUpdateKeys: (keyof ClientProperties)[] = [
|
|
||||||
"client_away",
|
|
||||||
"client_input_hardware",
|
|
||||||
"client_output_hardware",
|
|
||||||
"client_output_muted",
|
|
||||||
"client_input_muted",
|
|
||||||
"client_is_channel_commander",
|
|
||||||
"client_talk_power"
|
|
||||||
];
|
|
||||||
|
|
||||||
render() {
|
props.client.events.reactUse("notify_properties_updated", event => {
|
||||||
const client = this.props.client;
|
for (const key of IconUpdateKeys) {
|
||||||
const properties = client.properties;
|
|
||||||
|
|
||||||
let icon: ClientIcon;
|
|
||||||
if (properties.client_type_exact == ClientType.CLIENT_QUERY) {
|
|
||||||
icon = ClientIcon.ServerQuery;
|
|
||||||
} else {
|
|
||||||
if (properties.client_away) {
|
|
||||||
icon = ClientIcon.Away;
|
|
||||||
} else if (!client.getVoiceClient() && !(this instanceof LocalClientEntry)) {
|
|
||||||
icon = ClientIcon.InputMutedLocal;
|
|
||||||
} else if (!properties.client_output_hardware) {
|
|
||||||
icon = ClientIcon.HardwareOutputMuted;
|
|
||||||
} else if (properties.client_output_muted) {
|
|
||||||
icon = ClientIcon.OutputMuted;
|
|
||||||
} else if (!properties.client_input_hardware) {
|
|
||||||
icon = ClientIcon.HardwareInputMuted;
|
|
||||||
} else if (properties.client_input_muted) {
|
|
||||||
icon = ClientIcon.InputMuted;
|
|
||||||
} else {
|
|
||||||
if (client.isSpeaking()) {
|
|
||||||
if (properties.client_is_channel_commander) {
|
|
||||||
icon = ClientIcon.PlayerCommanderOn;
|
|
||||||
} else {
|
|
||||||
icon = ClientIcon.PlayerOn;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (properties.client_is_channel_commander) {
|
|
||||||
icon = ClientIcon.PlayerCommanderOff;
|
|
||||||
} else {
|
|
||||||
icon = ClientIcon.PlayerOff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ClientIconRenderer icon={icon}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler<ClientEvents>("notify_properties_updated")
|
|
||||||
private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) {
|
|
||||||
for (const key of ClientSpeakIcon.IconUpdateKeys)
|
|
||||||
if (key in event.updated_properties) {
|
if (key in event.updated_properties) {
|
||||||
this.forceUpdate();
|
setRevision(revision + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@EventHandler<ClientEvents>("notify_mute_state_change")
|
props.client.events.reactUse("notify_mute_state_change", () => setRevision(revision + 1));
|
||||||
private handleMuteStateChange() {
|
props.client.events.reactUse("notify_speak_state_change", () => setRevision(revision + 1));
|
||||||
this.forceUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler<ClientEvents>("notify_speak_state_change")
|
return props.renderer ? props.renderer(props.client.getStatusIcon()) : <ClientIconRenderer icon={props.client.getStatusIcon()} />;
|
||||||
private handleSpeakStateChange() {
|
|
||||||
this.forceUpdate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClientServerGroupIconsProperties {
|
interface ClientServerGroupIconsProperties {
|
||||||
|
@ -390,7 +342,7 @@ export class ClientEntry extends TreeEntry<ClientEntryProperties, ClientEntrySta
|
||||||
onContextMenu={e => this.onContextMenu(e)}
|
onContextMenu={e => this.onContextMenu(e)}
|
||||||
>
|
>
|
||||||
<UnreadMarker entry={this.props.client}/>
|
<UnreadMarker entry={this.props.client}/>
|
||||||
<ClientSpeakIcon client={this.props.client}/>
|
<ClientStatusIndicator client={this.props.client}/>
|
||||||
{this.state.rename ?
|
{this.state.rename ?
|
||||||
<ClientNameEdit key={"rename"} editFinished={name => this.onEditFinished(name)}
|
<ClientNameEdit key={"rename"} editFinished={name => this.onEditFinished(name)}
|
||||||
initialName={this.state.renameInitialName || this.props.client.properties.client_nickname}/> :
|
initialName={this.state.renameInitialName || this.props.client.properties.client_nickname}/> :
|
||||||
|
|
|
@ -12,4 +12,7 @@ import "./hooks/AudioRecorder";
|
||||||
|
|
||||||
import "./UnloadHandler";
|
import "./UnloadHandler";
|
||||||
|
|
||||||
|
|
||||||
|
import "./ui/FaviconRenderer";
|
||||||
|
|
||||||
export = require("tc-shared/main");
|
export = require("tc-shared/main");
|
|
@ -0,0 +1,86 @@
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
|
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useState} from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import {ClientStatusIndicator} from "tc-shared/ui/tree/Client";
|
||||||
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ClientIcon,
|
||||||
|
spriteEntries as kClientSpriteEntries,
|
||||||
|
spriteUrl as kClientSpriteUrl
|
||||||
|
} from "svg-sprites/client-icons";
|
||||||
|
|
||||||
|
let iconImage: HTMLImageElement;
|
||||||
|
async function initializeFaviconRenderer() {
|
||||||
|
iconImage = new Image();
|
||||||
|
iconImage.src = kClientSpriteUrl;
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
iconImage.onload = resolve;
|
||||||
|
iconImage.onerror = () => reject("failed to load client icon sprite");
|
||||||
|
});
|
||||||
|
|
||||||
|
let container = document.createElement("span");
|
||||||
|
ReactDOM.render(ReactDOM.createPortal(<FaviconRenderer />, document.head), container, () => {
|
||||||
|
document.getElementById("favicon").remove();
|
||||||
|
});
|
||||||
|
//container.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientIconToDataUrl(icon: ClientIcon) : string | undefined {
|
||||||
|
const sprite = kClientSpriteEntries.find(e => e.className === icon);
|
||||||
|
if(!sprite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = sprite.width;
|
||||||
|
canvas.height = sprite.height;
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
context.drawImage(iconImage, sprite.xOffset, sprite.yOffset, sprite.width, sprite.height, 0, 0, sprite.width, sprite.height);
|
||||||
|
|
||||||
|
return canvas.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
const FaviconRenderer = () => {
|
||||||
|
const [ handler, setHandler ] = useState<ConnectionHandler>(server_connections.active_connection());
|
||||||
|
|
||||||
|
server_connections.events().reactUse("notify_active_handler_changed", event => setHandler(event.newHandler));
|
||||||
|
|
||||||
|
return handler ? <HandlerFaviconRenderer connection={handler} key={"handler-" + handler.handlerId} /> : <DefaultFaviconRenderer key={"default"} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultFaviconRenderer = () => <link key={"normal"} rel={"shortcut icon"} href={"img/favicon/teacup.png"} type={"image/x-icon"} />;
|
||||||
|
const ClientIconFaviconRenderer = (props: { icon: ClientIcon }) => {
|
||||||
|
const url = clientIconToDataUrl(props.icon);
|
||||||
|
if(!url) {
|
||||||
|
return <DefaultFaviconRenderer key={"broken"} />;
|
||||||
|
} else {
|
||||||
|
return <link key={"status"} rel={"shortcut icon"} href={url} type={"image/x-icon"}/>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const HandlerFaviconRenderer = (props: { connection: ConnectionHandler }) => {
|
||||||
|
const [ showClientStatus, setShowClientStatus ] = useState(props.connection.connection_state === ConnectionState.CONNECTED);
|
||||||
|
props.connection.events().reactUse("notify_connection_state_changed", event => setShowClientStatus(event.new_state === ConnectionState.CONNECTED));
|
||||||
|
|
||||||
|
if(showClientStatus) {
|
||||||
|
return <ClientStatusIndicator
|
||||||
|
client={props.connection.getClient()}
|
||||||
|
renderer={icon => <ClientIconFaviconRenderer icon={icon} key={icon} />}
|
||||||
|
key={"server"}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
return <DefaultFaviconRenderer key={"default"} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "favicon renderer",
|
||||||
|
function: initializeFaviconRenderer,
|
||||||
|
priority: 10
|
||||||
|
});
|
|
@ -25,6 +25,7 @@ import {
|
||||||
} from "tc-shared/voice/VoiceWhisper";
|
} from "tc-shared/voice/VoiceWhisper";
|
||||||
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
||||||
import {WebWhisperSession} from "tc-backend/web/voice/VoiceWhisper";
|
import {WebWhisperSession} from "tc-backend/web/voice/VoiceWhisper";
|
||||||
|
import {VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
||||||
|
|
||||||
export enum VoiceEncodeType {
|
export enum VoiceEncodeType {
|
||||||
JS_ENCODE,
|
JS_ENCODE,
|
||||||
|
@ -68,6 +69,10 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
|
|
||||||
private lastConnectAttempt: number = 0;
|
private lastConnectAttempt: number = 0;
|
||||||
|
|
||||||
|
private currentlyReplayingVoice: boolean = false;
|
||||||
|
private readonly voiceClientStateChangedEventListener;
|
||||||
|
private readonly whisperSessionStateChangedEventListener;
|
||||||
|
|
||||||
constructor(connection: ServerConnection) {
|
constructor(connection: ServerConnection) {
|
||||||
super(connection);
|
super(connection);
|
||||||
|
|
||||||
|
@ -80,6 +85,9 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
|
|
||||||
this.connection.events.on("notify_connection_state_changed",
|
this.connection.events.on("notify_connection_state_changed",
|
||||||
this.serverConnectionStateListener = this.handleServerConnectionStateChanged.bind(this));
|
this.serverConnectionStateListener = this.handleServerConnectionStateChanged.bind(this));
|
||||||
|
|
||||||
|
this.voiceClientStateChangedEventListener = this.handleVoiceClientStateChange.bind(this);
|
||||||
|
this.whisperSessionStateChangedEventListener = this.handleWhisperSessionStateChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConnectionState(): VoiceConnectionStatus {
|
getConnectionState(): VoiceConnectionStatus {
|
||||||
|
@ -341,6 +349,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
if(!(client instanceof VoiceClientController))
|
if(!(client instanceof VoiceClientController))
|
||||||
throw "Invalid client type";
|
throw "Invalid client type";
|
||||||
|
|
||||||
|
client.events.off("notify_state_changed", this.voiceClientStateChangedEventListener);
|
||||||
delete this.voiceClients[client.getClientId()];
|
delete this.voiceClients[client.getClientId()];
|
||||||
client.destroy();
|
client.destroy();
|
||||||
}
|
}
|
||||||
|
@ -352,6 +361,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
|
|
||||||
const client = new VoiceClientController(clientId);
|
const client = new VoiceClientController(clientId);
|
||||||
this.voiceClients[clientId] = client;
|
this.voiceClients[clientId] = client;
|
||||||
|
client.events.on("notify_state_changed", this.voiceClientStateChangedEventListener);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,6 +381,41 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
this.encoderCodec = codec;
|
this.encoderCodec = codec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopAllVoiceReplays() {
|
||||||
|
this.availableVoiceClients().forEach(e => e.abortReplay());
|
||||||
|
/* TODO: Whisper sessions as well */
|
||||||
|
}
|
||||||
|
|
||||||
|
isReplayingVoice(): boolean {
|
||||||
|
return this.currentlyReplayingVoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleVoiceClientStateChange(/* event: VoicePlayerEvents["notify_state_changed"] */) {
|
||||||
|
this.updateVoiceReplaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWhisperSessionStateChange() {
|
||||||
|
this.updateVoiceReplaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateVoiceReplaying() {
|
||||||
|
let replaying = false;
|
||||||
|
if(this.connectionState === VoiceConnectionStatus.Connected) {
|
||||||
|
let index = this.availableVoiceClients().findIndex(client => client.getState() === VoicePlayerState.PLAYING || client.getState() === VoicePlayerState.BUFFERING);
|
||||||
|
replaying = index !== -1;
|
||||||
|
|
||||||
|
if(!replaying) {
|
||||||
|
index = this.getWhisperSessions().findIndex(session => session.getSessionState() === WhisperSessionState.PLAYING);
|
||||||
|
replaying = index !== -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.currentlyReplayingVoice !== replaying) {
|
||||||
|
this.currentlyReplayingVoice = replaying;
|
||||||
|
this.events.fire_async("notify_voice_replay_state_change", { replaying: replaying });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected handleWhisperPacket(packet: VoiceWhisperPacket) {
|
protected handleWhisperPacket(packet: VoiceWhisperPacket) {
|
||||||
const clientId = packet.clientId;
|
const clientId = packet.clientId;
|
||||||
|
|
||||||
|
@ -378,6 +423,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
if(typeof session !== "object") {
|
if(typeof session !== "object") {
|
||||||
logDebug(LogCategory.VOICE, tr("Received new whisper from %d (%s)"), packet.clientId, packet.clientNickname);
|
logDebug(LogCategory.VOICE, tr("Received new whisper from %d (%s)"), packet.clientId, packet.clientNickname);
|
||||||
session = (this.whisperSessions[clientId] = new WebWhisperSession(packet));
|
session = (this.whisperSessions[clientId] = new WebWhisperSession(packet));
|
||||||
|
session.events.on("notify_state_changed", this.whisperSessionStateChangedEventListener);
|
||||||
this.whisperSessionInitializer(session).then(result => {
|
this.whisperSessionInitializer(session).then(result => {
|
||||||
session.initializeFromData(result).then(() => {
|
session.initializeFromData(result).then(() => {
|
||||||
if(this.whisperSessions[clientId] !== session) {
|
if(this.whisperSessions[clientId] !== session) {
|
||||||
|
@ -413,6 +459,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
throw tr("Session isn't an instance of the web whisper system");
|
throw tr("Session isn't an instance of the web whisper system");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.events.off("notify_state_changed", this.whisperSessionStateChangedEventListener);
|
||||||
delete this.whisperSessions[session.getClientId()];
|
delete this.whisperSessions[session.getClientId()];
|
||||||
session.destroy();
|
session.destroy();
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ export class WebVoicePlayer implements VoicePlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(): VoicePlayerState {
|
getState(): VoicePlayerState {
|
||||||
return undefined;
|
return this.playerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVolume(): number {
|
getVolume(): number {
|
||||||
|
@ -118,6 +118,7 @@ export class WebVoicePlayer implements VoicePlayer {
|
||||||
destroy() {
|
destroy() {
|
||||||
this.audioClient?.destroy();
|
this.audioClient?.destroy();
|
||||||
this.audioClient = undefined;
|
this.audioClient = undefined;
|
||||||
|
this.events.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeAudio() : Promise<void> {
|
private initializeAudio() : Promise<void> {
|
||||||
|
@ -252,6 +253,7 @@ export class WebVoicePlayer implements VoicePlayer {
|
||||||
} else if(this.playerState === VoicePlayerState.PLAYING) {
|
} else if(this.playerState === VoicePlayerState.PLAYING) {
|
||||||
logDebug(LogCategory.VOICE, tr("Voice player has a buffer underflow. Changing state to buffering."));
|
logDebug(LogCategory.VOICE, tr("Voice player has a buffer underflow. Changing state to buffering."));
|
||||||
this.setPlayerState(VoicePlayerState.BUFFERING);
|
this.setPlayerState(VoicePlayerState.BUFFERING);
|
||||||
|
this.resetBufferTimeout(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
|
||||||
devtool: isDevelopment ? "inline-source-map" : undefined,
|
devtool: isDevelopment ? "inline-source-map" : undefined,
|
||||||
mode: isDevelopment ? "development" : "production",
|
mode: isDevelopment ? "development" : "production",
|
||||||
plugins: [
|
plugins: [
|
||||||
new CleanWebpackPlugin(),
|
//new CleanWebpackPlugin(),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
|
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
|
||||||
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
|
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
|
||||||
|
|
Loading…
Reference in New Issue