Starting with react

This commit is contained in:
WolverinDEV 2020-04-06 16:29:40 +02:00
parent e5c7342182
commit 25901ff72c
26 changed files with 943 additions and 263 deletions

View file

@ -15307,8 +15307,10 @@ class ConnectionHandler {
}
}
}
if (control_bar.current_connection_handler() === this)
if (control_bar.current_connection_handler() === this) {
control_bar.apply_server_voice_state();
top_menu.update_state(); //TODO: Only run "small" update?
}
}
sync_status_with_server() {
if (this.serverConnection.connected())

View file

@ -190,7 +190,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
}
}
&:hover.displayed, &.force-show {
&:hover.dropdownDisplayed, &.force-show {
.dropdown {
display: block;
}

View file

@ -930,6 +930,9 @@
font-size: .85em;
color: #3c3c3c;
width: fit-content;
align-self: end;
}
}
}

View file

@ -12,119 +12,6 @@
<!-- navigation bar -->
<div class="container-control-bar">
<div id="control_bar" class="control_bar">
{{if multi_session}}
<div class="button-dropdown container-connect" title="{{tr 'Connect to a server' /}}">
<div class="buttons">
<div class="button btn_connect">
<div class="icon_em client-connect"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown" style="width: 350px">
<div class="btn_connect">
<div class="icon client-connect"></div>
<a>{{tr "Connect to a server" /}}</a></div>
<div class="btn_connect_new_tab">
<div class="icon client-connect"></div>
<a>{{tr "Connect to a server in another tab" /}}</a></div>
</div>
</div>
<div class="button-dropdown container-disconnect" title="{{tr 'Disconnect from server' /}}"
style="display: none">
<div class="buttons">
<div class="button btn_disconnect">
<div class="icon_em client-disconnect"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown" style="width: 350px">
<div class="btn_disconnect">
<div class="icon client-disconnect"></div>
<a>{{tr "Disconnect from current server" /}}</a></div>
<div class="btn_connect">
<div class="icon client-connect"></div>
<a>{{tr "Connect to a server" /}}</a></div>
</div>
</div>
{{else}}
<div class="button container-connect btn_connect">
<div class="icon_em client-connect" title="{{tr 'Connect to a server' /}}"></div>
</div>
<div class="button container-disconnect btn_disconnect">
<div class="icon_em client-disconnect" title="{{tr 'Disconnect from server' /}}"></div>
</div>
{{/if}}
<div class="button-dropdown btn_bookmark" title="{{tr 'Bookmarks' /}}">
<div class="buttons">
<div class="button btn_bookmark_list">
<div class="icon_em client-bookmark_manager"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown bookmark-dropdown" style="width: 350px;">
<div class="btn_bookmark_list">
<div class="icon client-bookmark_manager"></div>
<a>{{tr "Manage bookmarks" /}}</a></div>
<div class="btn_bookmark_add">
<div class="icon client-bookmark_add"></div>
<a>{{tr "Add current server to bookmarks" /}}</a></div>
<div class="btn_bookmark_remove">
<div class="icon client-bookmark_remove"></div>
<a>{{tr "Remove current server to bookmarks" /}}</a></div>
<hr>
</div>
</div>
<div class="divider"></div>
<div class="button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
<div class="buttons">
<div class="button btn_away_toggle">
<div class="icon_em client-away"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown" style="width: 350px">
<div class="btn_away_disable">
<div class="icon client-present"></div>
<a>{{tr "Go online" /}}</a></div>
<div class="btn_away_enable">
<div class="icon client-away"></div>
<a>{{tr "Set away on this server" /}}</a></div>
<div class="btn_away_message">
<div class="icon client-away"></div>
<a>{{tr "Set away message on this server" /}}</a></div>
<hr class="btn_away_message_global">
<!-- applied to this HR this class because it needs to get hidden as well if we dont have global settings -->
<div class="btn_away_enable_global">
<div class="icon client-away"></div>
<a>{{tr "Set away for all servers" /}}</a></div>
<div class="btn_away_message_global">
<div class="icon client-away"></div>
<a>{{tr "Set away message for all servers" /}}</a></div>
<div class="btn_away_disable_global">
<div class="icon client-present"></div>
<a>{{tr "Go online for all servers" /}}</a></div>
</div>
</div>
<div class="button button-red btn_mute_input">
<div class="icon_em client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div>
</div>
<div class="button button-red btn_mute_output">
<div class="icon_em client-output_muted"
title="{{tr 'Mute/unmute headphones' /}}"></div>
</div>
<!--
<div class="show-small button-dropdown dropdown-audio" title="{{tr 'Audio settings' /}}">
<div class="buttons">
@ -148,38 +35,6 @@
</div>
<div class="divider"></div>
-->
<div class="divider"></div>
<div class="button button-subscribe-mode">
<div class="icon_em" title="{{tr 'Toggle channel subscribe mode' /}}"></div>
</div>
<!-- the query button -->
<div class="button-dropdown btn_query" title="{{tr 'Show/hide server queries' /}}">
<div class="buttons">
<div class="button btn_query_toggle">
<div class="icon_em client-server_query"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown">
<div class="btn_query_toggle">
<div class="icon client-toggle_server_query_clients"></div>
<a class="query-text"></a></div>
<div class="btn_query_manage">
<div class="icon client-server_query"></div>
<a>{{tr "Manage server queries" /}}</a></div>
<!-- <div class="btn_query_create"><div class="icon client-away"></div><a>{{tr "Create server query login" /}}</a></div> -->
</div>
</div>
<div style="width: 100%"></div>
<!-- -->
<div class="button button-hostbutton" title="{{tr 'Hostbutton' /}}">
<img alt="{{tr 'hostbutton' /}}">
</div>
</div>
</div>
<div class="container-connection-handlers scrollbar" id="connection-handlers">

View file

@ -26,11 +26,12 @@ import {Frame} from "tc-shared/ui/frames/chat_frame";
import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar";
import * as connection from "tc-backend/connection";
import * as dns from "tc-backend/dns";
import * as top_menu from "tc-shared/ui/frames/MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export enum DisconnectReason {
HANDLER_DESTROYED,
@ -344,10 +345,7 @@ export class ConnectionHandler {
}
}
if(update_control && server_connections.active_connection_handler() === this) {
control_bar.apply_server_state();
}
control_bar_instance()?.events().fire("server_updated", { category: "settings-initialized", handler: this });
}
get connected() : boolean {
@ -608,8 +606,7 @@ export class ConnectionHandler {
if(this.serverConnection)
this.serverConnection.disconnect();
if(control_bar.current_connection_handler() == this)
control_bar.update_connection_state();
this.on_connection_state_changed(); /* really required to call? */
this.side_bar.private_conversations().clear_client_ids();
this.hostbanner.update();
@ -643,8 +640,7 @@ export class ConnectionHandler {
}
private on_connection_state_changed() {
if(control_bar.current_connection_handler() == this)
control_bar.update_connection_state();
control_bar_instance()?.events().fire("server_updated", { category: "connection-state", handler: this });
}
private _last_record_error_popup: number;
@ -752,8 +748,9 @@ export class ConnectionHandler {
}
}
if(control_bar.current_connection_handler() === this)
control_bar.apply_server_voice_state();
control_bar_instance()?.events().fire("server_updated", { category: "audio", handler: this });
top_menu.update_state(); //TODO: Only run "small" update?
}
sync_status_with_server() {
@ -771,7 +768,7 @@ export class ConnectionHandler {
});
}
set_away_status(state: boolean | string) {
set_away_status(state: boolean | string, update_control_bar: boolean) {
if(this.client_status.away === state)
return;
@ -790,7 +787,8 @@ export class ConnectionHandler {
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
});
control_bar.update_button_away();
if(update_control_bar)
control_bar_instance()?.events().fire("server_updated", { category: "away-status", handler: this });
}
resize_elements() {

View file

@ -52,7 +52,7 @@ export enum BookmarkType {
}
export interface Bookmark {
type: /* BookmarkType.ENTRY */ BookmarkType;
type: BookmarkType.ENTRY;
/* readonly */ parent: DirectoryBookmark;
server_properties: ServerProperties;
@ -70,7 +70,7 @@ export interface Bookmark {
}
export interface DirectoryBookmark {
type: /* BookmarkType.DIRECTORY */ BookmarkType;
type: BookmarkType.DIRECTORY;
/* readonly */ parent: DirectoryBookmark;
readonly content: (Bookmark | DirectoryBookmark)[];

View file

@ -3,7 +3,7 @@ import {PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration";
import {guid} from "tc-shared/crypto/uid";
import * as React from "react";
export interface Event<Events, T> {
export interface Event<Events, T = keyof Events> {
readonly type: T;
as<T extends keyof Events>() : Events[T];
}
@ -31,6 +31,7 @@ export class Registry<Events> {
handlers: {[key: string]: ((event) => void)[]}
}[] = [];
private debug_prefix = undefined;
private warn_unhandled_events = true;
constructor() {
this.registry_uuid = "evreg_data_" + guid();
@ -40,6 +41,9 @@ export class Registry<Events> {
enable_debug(prefix: string) { this.debug_prefix = prefix || "---"; }
disable_debug() { this.debug_prefix = undefined; }
enable_warn_unhandled_events() { this.warn_unhandled_events = true; }
disable_warn_unhandled_events() { this.warn_unhandled_events = false; }
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void);
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
on(events, handler) {
@ -88,12 +92,14 @@ export class Registry<Events> {
}
}
connect<EOther, T extends keyof Events & keyof EOther>(event: T, target: Registry<EOther>) {
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
connect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
for(const event of Array.isArray(events) ? events : [events])
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
}
disconnect<EOther, T extends keyof Events & keyof EOther>(event: T, target: Registry<EOther>) {
(this.connections[event as string] || []).remove(target as any);
disconnect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
for(const event of Array.isArray(events) ? events : [events])
(this.connections[event as string] || []).remove(target as any);
}
disconnect_all<EOther>(target: Registry<EOther>) {
@ -109,24 +115,33 @@ export class Registry<Events> {
as: function () { return this; }
});
let invoke_count = 0;
for(const handler of (this.handler[event_type as string] || [])) {
handler(event);
invoke_count++;
const reg_data = handler[this.registry_uuid];
if(typeof reg_data === "object" && reg_data.singleshot)
this.handler[event_type as string].remove(handler);
}
for(const evhandler of (this.connections[event_type as string] || []))
for(const evhandler of (this.connections[event_type as string] || [])) {
evhandler.fire(event_type as any, event as any);
invoke_count++;
}
if(invoke_count === 0) {
console.warn("Event handler (%s) triggered event %s which has no consumers.", this.debug_prefix, event_type);
}
}
fire_async<T extends keyof Events>(event_type: T, data?: Events[T]) {
setTimeout(() => this.fire(event_type, data));
}
destory() {
destroy() {
this.handler = {};
this.connections = {};
this.event_handler_objects = [];
}
register_handler(handler: any) {
@ -142,8 +157,7 @@ export class Registry<Events> {
if(typeof proto[function_name][event_annotation_key] !== "object") continue;
const event_data = proto[function_name][event_annotation_key];
console.log("Registering method %s for %o", function_name, event_data.events);
const ev_handler = event => proto[function_name].call(handler, [event]);
const ev_handler = event => proto[function_name].call(handler, event);
for(const event of event_data.events) {
registered_events[event] = registered_events[event] || [];
registered_events[event].push(ev_handler);
@ -184,7 +198,7 @@ export function EventHandler<EventTypes>(events: (keyof EventTypes) | (keyof Eve
}
}
export function ReactEventHandler<ObjectClass, EventTypes = any>(registry_callback: (object: ObjectClass) => Registry<EventTypes>) {
export function ReactEventHandler<ObjectClass = React.Component<any, any>, EventTypes = any>(registry_callback: (object: ObjectClass) => Registry<EventTypes>) {
return function (constructor: Function) {
if(!React.Component.prototype.isPrototypeOf(constructor.prototype))
throw "Class/object isn't an instance of React.Component";

View file

@ -0,0 +1,237 @@
import {Registry} from "tc-shared/events";
import {ClientGlobalControlEvents} from "tc-shared/events/GlobalEvents";
import {control_bar_instance, ControlBarEvents} from "tc-shared/ui/frames/control-bar";
import {manager, Sound} from "tc-shared/sound/Sounds";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {default_recorder} from "tc-shared/voice/RecorderProfile";
import {Settings, settings} from "tc-shared/settings";
import {add_current_server} from "tc-shared/bookmarks";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import PermissionType from "tc-shared/permission/PermissionType";
import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
import {openBanList} from "tc-shared/ui/modal/ModalBanList";
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>) {
{
let microphone_muted = undefined;
event_registry.on("action_toggle_speaker", event => {
if(microphone_muted === event.state) return;
if(typeof microphone_muted !== "undefined")
manager.play(event.state ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
microphone_muted = event.state;
})
}
{
let speakers_muted = undefined;
event_registry.on("action_toggle_microphone", event => {
if(speakers_muted === event.state) return;
if(typeof speakers_muted !== "undefined")
manager.play(event.state ? Sound.SOUND_MUTED : Sound.SOUND_ACTIVATED);
speakers_muted = event.state;
})
}
}
function load_default_states() {
this.event_registry.fire("action_toggle_speaker", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false) });
this.event_registry.fire("action_toggle_microphone", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false) });
}
export function initialize(event_registry: Registry<ClientGlobalControlEvents>) {
let current_connection_handler: ConnectionHandler | undefined;
event_registry.on("action_set_active_connection_handler", event => { current_connection_handler = event.handler; });
initialize_sounds(event_registry);
/* away state handler */
event_registry.on("action_set_away", event => {
const set_away = message => {
for(const connection of event.globally ? server_connections.server_connection_handlers() : [server_connections.active_connection_handler()]) {
if(!connection) continue;
connection.set_away_status(typeof message === "string" && !!message ? message : true, false);
}
control_bar_instance()?.events()?.fire("update_state", { state: "away" });
};
if(event.prompt_reason) {
createInputModal(tr("Set away message"), tr("Please enter your away message"), () => true, message => {
if(typeof(message) === "string")
set_away(message);
}).open();
} else {
set_away(undefined);
}
});
event_registry.on("action_disable_away", event => {
for(const connection of event.globally ? server_connections.server_connection_handlers() : [server_connections.active_connection_handler()]) {
if(!connection) continue;
connection.set_away_status(false, false);
}
control_bar_instance()?.events()?.fire("update_state", { state: "away" });
});
event_registry.on("action_toggle_microphone", event => {
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, !event.state);
if(current_connection_handler) {
current_connection_handler.client_status.input_muted = !event.state;
if(!current_connection_handler.client_status.input_hardware)
current_connection_handler.acquire_recorder(default_recorder, true); /* acquire_recorder already updates the voice status */
else
current_connection_handler.update_voice_status(undefined);
}
});
event_registry.on("action_toggle_speaker", event => {
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, !event.state);
if(!current_connection_handler) return;
current_connection_handler.client_status.output_muted = !event.state;
current_connection_handler.update_voice_status(undefined);
});
event_registry.on("action_set_channel_subscribe_mode", event => {
if(!current_connection_handler) return;
current_connection_handler.client_status.channel_subscribe_all = event.subscribe;
if(event.subscribe)
current_connection_handler.channelTree.subscribe_all_channels();
else
current_connection_handler.channelTree.unsubscribe_all_channels(true);
current_connection_handler.settings.changeServer(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, event.subscribe);
});
event_registry.on("action_toggle_query", event => {
if(!current_connection_handler) return;
current_connection_handler.client_status.queries_visible = event.shown;
current_connection_handler.channelTree.toggle_server_queries(event.shown);
current_connection_handler.settings.changeServer(Settings.KEY_CONTROL_SHOW_QUERIES, event.shown);
});
event_registry.on("action_add_current_server_to_bookmarks", () => add_current_server());
event_registry.on("action_open_connect", event => {
current_connection_handler?.cancel_reconnect(true);
spawnConnectModal({
default_connect_new_tab: event.new_tab
}, {
url: "ts.TeaSpeak.de",
enforce: false
});
});
event_registry.on("action_open_window", event => {
const handle_import_error = error => {
console.error("Failed to import script: %o", error);
createErrorModal(tr("Failed to load window"), tr("Failed to load the bookmark window.\nSee the console for more details.")).open();
};
const connection_handler = event.connection || current_connection_handler;
switch (event.window) {
case "bookmark-manage":
import("../ui/modal/ModalBookmarks").catch(error => {
handle_import_error(error);
return undefined;
}).then(window => {
window?.spawnBookmarkModal();
});
break;
case "query-manage":
if(!connection_handler || !connection_handler.connected) {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
return;
}
import("../ui/modal/ModalQueryManage").catch(error => {
handle_import_error(error);
return undefined;
}).then(window => {
window?.spawnQueryManage(connection_handler);
});
break;
case "query-create":
if(!connection_handler || !connection_handler.connected) {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
return;
}
if(connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) {
spawnQueryCreate(connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open();
connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
break;
case "ban-list":
if(!connection_handler || !connection_handler.connected) {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
return;
}
if(connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
openBanList(connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open();
connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
break;
case "permissions":
if(!connection_handler || !connection_handler.connected) {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
return;
}
if(connection_handler)
spawnPermissionEdit(connection_handler).open();
else
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
break;
case "token-list":
createErrorModal(tr("Not implemented"), tr("Token list is not implemented yet!")).open();
break;
case "token-use":
//FIXME: Move this out to a dedicated method
createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => {
if(!result) return;
if(connection_handler.serverConnection.connected)
connection_handler.serverConnection.send_command("tokenuse", {
token: result
}).then(() => {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
}).catch(error => {
//TODO tr
createErrorModal(tr("Use token"), formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
});
}).open();
break;
case "settings":
spawnSettingsModal();
break;
default:
console.warn(tr("Received open window event for an unknown window: %s"), event.window);
}
});
load_default_states();
}

View file

@ -0,0 +1,49 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
export interface ClientGlobalControlEvents {
action_set_channel_subscribe_mode: {
subscribe: boolean
},
action_disconnect: {
globally: boolean
},
action_open_connect: {
new_tab: boolean
},
action_toggle_microphone: {
state: boolean
},
action_toggle_speaker: {
state: boolean
},
action_disable_away: {
globally: boolean
},
action_set_away: {
globally: boolean;
prompt_reason: boolean;
},
action_toggle_query: {
shown: boolean
},
action_open_window: {
window: "bookmark-manage" | "query-manage" | "query-create" | "ban-list" | "permissions" | "token-list" | "token-use" | "settings",
connection?: ConnectionHandler
},
action_add_current_server_to_bookmarks: {},
action_set_active_connection_handler: {
handler?: ConnectionHandler
},
//TODO
notify_microphone_state_changed: {
state: boolean
}
}

View file

@ -16,7 +16,6 @@ import * as fidentity from "./profiles/identities/TeaForumIdentity";
import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile";
import * as cmanager from "tc-shared/ui/frames/connection_handlers";
import {server_connections, ServerConnectionManager} from "tc-shared/ui/frames/connection_handlers";
import * as control_bar from "tc-shared/ui/frames/ControlBar";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import * as top_menu from "./ui/frames/MenuBar";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
@ -29,6 +28,8 @@ import * as ppt from "tc-backend/ppt";
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as cbar from "./ui/frames/control-bar";
import {Registry} from "tc-shared/events";
import {ClientGlobalControlEvents} from "tc-shared/events/GlobalEvents";
/* required import for init */
require("./proto").initialize();
@ -145,6 +146,8 @@ async function initialize() {
bipc.setup();
}
export let client_control_events: Registry<ClientGlobalControlEvents>;
async function initialize_app() {
try { //Initialize main template
const main = $("#tmpl_main").renderTag({
@ -159,13 +162,14 @@ async function initialize_app() {
return;
}
control_bar.set_control_bar(new control_bar.ControlBar($("#control_bar"))); /* setup the control bar */
client_control_events = new Registry<ClientGlobalControlEvents>();
{
const bar = (
<cbar.ControlBar multiSession={true} />
<cbar.ControlBar ref={cbar.react_reference()} multiSession={true} />
);
ReactDOM.render(bar, $(".container-control-bar")[0]);
cbar.control_bar_instance().load_default_states();
}
if(!aplayer.initialize())

View file

@ -21,10 +21,10 @@ import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
import {spawnAbout} from "tc-shared/ui/modal/ModalAbout";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as loader from "tc-loader";
import {formatMessage} from "tc-shared/ui/frames/chat";
import * as slog from "tc-shared/ui/frames/server_log";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export interface HRItem { }
@ -348,7 +348,8 @@ export function initialize() {
handler.sound.play(Sound.CONNECTION_DISCONNECTED);
handler.log.log(slog.Type.DISCONNECTED, {});
}
control_bar.update_connection_state();
control_bar_instance()?.events().fire("update_state", { state: "connect-state" });
update_state();
};
item = menu.append_item(tr("Disconnect from current server"));

View file

@ -1,7 +1,8 @@
import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler";
import {Settings, settings} from "tc-shared/settings";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as top_menu from "./MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
import {client_control_events} from "tc-shared/main";
export let server_connections: ServerConnectionManager;
export function initialize(manager: ServerConnectionManager) {
@ -97,7 +98,7 @@ export class ServerConnectionManager {
handler.resize_elements();
}
this.active_handler = handler;
control_bar.set_connection_handler(handler);
client_control_events.fire("action_set_active_connection_handler", { handler: handler }); //FIXME: This even should set the new handler, not vice versa!
top_menu.update_state();
}

View file

@ -0,0 +1,30 @@
import {Registry} from "tc-shared/events";
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/index";
import {manager, Sound} from "tc-shared/sound/Sounds";
function initialize_sounds(event_registry: Registry<ControlBarEvents>) {
{
let microphone_muted = undefined;
event_registry.on("update_microphone_state", event => {
if(microphone_muted === event.muted) return;
if(typeof microphone_muted !== "undefined")
manager.play(event.muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
microphone_muted = event.muted;
})
}
{
let speakers_muted = undefined;
event_registry.on("update_speaker_state", event => {
if(speakers_muted === event.muted) return;
if(typeof speakers_muted !== "undefined")
manager.play(event.muted ? Sound.SOUND_MUTED : Sound.SOUND_ACTIVATED);
speakers_muted = event.muted;
})
}
}
export = (event_registry: Registry<ControlBarEvents>) => {
initialize_sounds(event_registry);
};
//TODO: Left action handler!

View file

@ -0,0 +1,188 @@
/* Some general browser helpers */
/* border etc */
.button, .dropdownArrow {
text-align: center;
border: 0.05em solid rgba(0, 0, 0, 0);
border-radius: 0.1em;
background-color: #454545;
-moz-transition: background-color 0.25s ease-in-out, border-color 0.25s ease-in-out;
-o-transition: background-color 0.25s ease-in-out, border-color 0.25s ease-in-out;
-webkit-transition: background-color 0.25s ease-in-out, border-color 0.25s ease-in-out;
transition: background-color 0.25s ease-in-out, border-color 0.25s ease-in-out;
}
.button:hover, .dropdownArrow:hover {
background-color: #393c43;
border-color: #4a4c55;
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
}
.button.activated, .dropdownArrow.activated {
background-color: #2f3841;
border-color: #005fa1;
}
.button.activated:hover, .dropdownArrow.activated:hover {
background-color: #263340;
border-color: #005fa1;
}
.button.activated.theme-red, .dropdownArrow.activated.theme-red {
background-color: #412f2f;
border-color: #a10000;
}
.button.activated.theme-red:hover, .dropdownArrow.activated.theme-red:hover {
background-color: #402626;
border-color: #a10000;
}
.button :global(.icon_em), .dropdownArrow :global(.icon_em) {
font-size: 1.5em;
}
.button {
display: flex;
flex-direction: row;
justify-content: center;
height: 2em;
width: 2em;
cursor: pointer;
align-items: center;
margin-right: 5px;
margin-left: 5px;
}
.button.buttonHostbutton {
overflow: hidden;
padding: 0.25em;
}
.button.buttonHostbutton img {
min-width: 1.5em;
max-width: 1.5em;
height: 1.5em;
width: 1.5em;
}
.buttonDropdown {
height: 100%;
position: relative;
}
.buttonDropdown .buttons {
height: 2em;
align-items: center;
display: flex;
flex-direction: row;
}
.buttonDropdown .buttons .dropdownArrow {
height: 2em;
display: inline-flex;
justify-content: space-around;
width: 1.5em;
cursor: pointer;
border-radius: 0 0.1em 0.1em 0;
align-items: center;
border-left: 0;
}
.buttonDropdown .buttons .button {
margin-right: 0;
}
.buttonDropdown .buttons:hover .button, .buttonDropdown .buttons:hover .dropdownArrow {
background-color: #393c43;
border-color: #4a4c55;
}
.buttonDropdown .buttons:hover .button {
border-right-color: transparent;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.buttonDropdown .dropdown {
display: none;
position: absolute;
margin-left: 5px;
color: #c4c5c5;
background-color: #2d3032;
align-items: center;
border: 0.05em solid #2c2525;
border-radius: 0 0.15em 0.15em 0.15em;
width: 15em;
/* fallback */
width: max-content;
max-width: 25em;
z-index: 1000;
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
}
.buttonDropdown .dropdown:global(.right) {
right: 0;
}
.buttonDropdown .dropdown :global .icon, .buttonDropdown .dropdown :global .icon-container, .buttonDropdown .dropdown :global .icon_em {
vertical-align: middle;
margin-right: 5px;
}
.buttonDropdown .dropdown :global .icon-empty, .buttonDropdown .dropdown :global .icon_empty {
flex-shrink: 0;
flex-grow: 0;
height: 16px;
width: 16px;
}
.buttonDropdown .dropdown .dropdownEntry {
position: relative;
display: flex;
flex-direction: row;
cursor: pointer;
padding: 1px 2px 1px 4px;
align-items: center;
justify-content: stretch;
}
.buttonDropdown .dropdown .dropdownEntry .entryName {
flex-grow: 1;
flex-shrink: 1;
vertical-align: text-top;
margin-right: 0.5em;
}
.buttonDropdown .dropdown .dropdownEntry .icon, .buttonDropdown .dropdown .dropdownEntry .arrow {
flex-grow: 0;
flex-shrink: 0;
}
.buttonDropdown .dropdown .dropdownEntry .arrow {
margin-right: 0.5em;
}
.buttonDropdown .dropdown .dropdownEntry:first-of-type {
border-radius: 0.1em 0.1em 0 0;
}
.buttonDropdown .dropdown .dropdownEntry:last-of-type {
border-radius: 0 0 0.1em 0.1em;
}
.buttonDropdown .dropdown .dropdownEntry > .dropdown {
margin-left: 0;
}
.buttonDropdown .dropdown .dropdownEntry:hover {
background-color: #252729;
}
.buttonDropdown .dropdown .dropdownEntry:hover > .dropdown {
display: block;
margin-left: 0;
left: 100%;
top: 0;
}
.buttonDropdown .dropdown.displayLeft {
margin-left: -179px;
border-radius: 0.15em 0 0.15em 0.15em;
}
.buttonDropdown.dropdownDisplayed > .dropdown {
display: block;
}
.buttonDropdown.dropdownDisplayed .button, .buttonDropdown.dropdownDisplayed .dropdown-arrow {
background-color: #393c43;
border-color: #4a4c55;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.buttonDropdown.dropdownDisplayed .button {
border-right-color: transparent;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.buttonDropdown hr {
margin-top: 5px;
margin-bottom: 5px;
}
.buttonBookmarks .dropdown {
width: 300px;
}
/*# sourceMappingURL=button.css.map */

View file

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../../../css/static/mixin.scss","button.scss","../../../../css/static/properties.scss"],"names":[],"mappings":"AAAA;ACKA;AACA;EACI;EAEA;EACA,eCLkB;EDOlB;EDTH,iBCqCG;EDpCH,eCoCG;EDnCH,oBCmCG;EDlCH,YCkCG;;AA1BA;EACI;EACA;AACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAOZ;EACI;;;AAIR;EACI;EACA;EACA;EAEA;EACA;EAEA;EACA;EAEA;EACA;;AAEA;EASI;EACA;;AATA;EACI;EACA;EAEA;EACA;;;AAQZ;EACI;EACA;;AAEA;EACI;EAEA;EAEA;EACA;;AAEA;EACI;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAGJ;EACI;;AAIA;EACI;EACA;;AAGJ;EACI;EAEA;EACA;;AAKZ;EACI;EACA;EACA;EAEA;EAEA;EACA;EACA;EACA;EAEA;AAAa;EACb;EAEA;EAEA;AACA;;AAEA;EACI;;AAIA;EACI;EACA;;AAGJ;EACI;EACA;EAEA;EACA;;AAIR;EACI;EAEA;EACA;EACA;EACA;EAEA;EACA;;AAEA;EACI;EACA;EAEA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAEA;EACI;EACA;EAEA;EACA;;AAMZ;EACI;EACA;;AAKJ;EACI;;AAGJ;EACI;EACA;EAEA;EACA;;AAGJ;EACI;EAEA;EACA;;AAKR;EACI;EACA;;;AAKJ;EACI","file":"button.css"}

View file

@ -147,7 +147,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
margin-right: 5px;
}
.icon-empty {
.icon-empty, .icon_empty {
flex-shrink: 0;
flex-grow: 0;

View file

@ -6,6 +6,7 @@ const cssStyle = require("./button.scss");
export interface ButtonState {
switched: boolean;
dropdownShowed: boolean;
dropdownForceShow: boolean;
}
export interface ButtonProperties {
@ -29,7 +30,8 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
protected default_state(): ButtonState {
return {
switched: false,
dropdownShowed: false
dropdownShowed: false,
dropdownForceShow: false
}
}
@ -49,7 +51,7 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
return button;
return (
<div className={this.classList(cssStyle.buttonDropdown, this.state.dropdownShowed ? cssStyle.dropdownDisplayed : "", this.props.dropdownButtonExtraClass)} onMouseLeave={this.onMouseLeave.bind(this)}>
<div className={this.classList(cssStyle.buttonDropdown, this.state.dropdownShowed || this.state.dropdownForceShow ? cssStyle.dropdownDisplayed : "", this.props.dropdownButtonExtraClass)} onMouseLeave={this.onMouseLeave.bind(this)}>
<div className={cssStyle.buttons}>
{button}
<div className={cssStyle.dropdownArrow} onMouseEnter={this.onMouseEnter.bind(this)}>

View file

@ -3,21 +3,51 @@ import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
const cssStyle = require("./button.scss");
export interface DropdownEntryProperties {
icon?: string;
text: JSX.Element;
icon?: string | JQuery<HTMLDivElement>;
text: JSX.Element | string;
onClick?: () => void;
onClick?: (event) => void;
onContextMenu?: (event) => void;
}
class IconRenderer extends React.Component<{ icon: string | JQuery<HTMLDivElement> }, {}> {
private readonly icon_ref: React.RefObject<HTMLDivElement>;
constructor(props) {
super(props);
if(typeof this.props.icon === "object")
this.icon_ref = React.createRef();
}
render() {
if(!this.props.icon)
return <div className={"icon-container icon-empty"} />;
else if(typeof this.props.icon === "string")
return <div className={"icon " + this.props.icon} />;
return <div ref={this.icon_ref} />;
}
componentDidMount(): void {
if(this.icon_ref)
$(this.icon_ref.current).replaceWith(this.props.icon);
}
componentWillUnmount(): void {
if(this.icon_ref)
$(this.icon_ref.current).empty();
}
}
export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {}> {
protected default_state() { return {}; }
render() {
const icon = this.props.icon ? this.classList("icon", this.props.icon) : this.classList("icon-container", "icon-empty");
if(this.props.children) {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick}>
<div className={icon} />
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onContextMenu={this.props.onContextMenu}>
<IconRenderer icon={this.props.icon} />
<a className={cssStyle.entryName}>{this.props.text}</a>
<div className={this.classList("arrow", "right")} />
<DropdownContainer>
@ -27,8 +57,8 @@ export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {
);
} else {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick}>
<div className={icon} />
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onContextMenu={this.props.onContextMenu}>
<IconRenderer icon={this.props.icon} />
<a className={cssStyle.entryName}>{this.props.text}</a>
</div>
);

View file

@ -0,0 +1,28 @@
/* Some general browser helpers */
/* max height is 2em */
.controlBar {
display: flex;
flex-direction: row;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
height: 100%;
align-items: center;
/* tmp fix for ultra small devices */
overflow-y: visible;
}
.controlBar .divider {
flex-grow: 0;
flex-shrink: 0;
border-left: 2px solid #393838;
height: calc(100% - 3px);
margin: 3px;
}
.controlBar .spacer {
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
}
/*# sourceMappingURL=index.css.map */

View file

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../../../css/static/mixin.scss","index.scss"],"names":[],"mappings":"AAAA;ACGA;AACA;EACI;EACA;EDwDH,qBCtDwB;EDuDxB,kBCvDwB;EDwDxB,iBCxDwB;EDyDxB,aCzDwB;EAErB;EACA;AAEA;EACA;;AAEA;EACI;EACA;EAEA;EACA;EACA;;AAGJ;EACI;EACA;EAEA","file":"index.css"}

View file

@ -4,14 +4,25 @@ import {DropdownEntry} from "tc-shared/ui/frames/control-bar/dropdown";
import {Translatable} from "tc-shared/ui/elements/i18n";
import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
import {Event, EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {Settings, settings} from "tc-shared/settings";
import {
Bookmark,
bookmarks,
BookmarkType,
boorkmak_connect,
DirectoryBookmark,
find_bookmark
} from "tc-shared/bookmarks";
import {IconManager} from "tc-shared/FileManager";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {client_control_events} from "tc-shared/main";
const register_actions = require("./actions");
const cssStyle = require("./index.scss");
const cssButtonStyle = require("./button.scss");
export interface ControlBarProperties {
multiSession: boolean;
}
export interface ConnectionState {
connected: boolean;
connectedAnywhere: boolean;
@ -31,57 +42,128 @@ class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_re
if(this.props.multiSession) {
if(!this.state.connected) {
subentries.push(
<DropdownEntry key={"connect-server"} icon={"client-connect"} text={<Translatable message={"Connect to a server"} />} />
<DropdownEntry key={"connect-server"} icon={"client-connect"} text={<Translatable message={"Connect to a server"} />}
onClick={ () => client_control_events.fire("action_open_connect", { new_tab: false }) } />
);
} else {
subentries.push(
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from current server"} />} />
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from current server"} />}
onClick={ () => client_control_events.fire("action_disconnect", { globally: false }) }/>
);
}
if(this.state.connectedAnywhere) {
subentries.push(
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from all servers"} />}
onClick={ () => client_control_events.fire("action_disconnect", { globally: true }) }/>
);
}
subentries.push(
<DropdownEntry key={"connect-new-tab"} icon={"client-connect"} text={<Translatable message={"Connect to a server in another tab"} />} />
<DropdownEntry key={"connect-new-tab"} icon={"client-connect"} text={<Translatable message={"Connect to a server in another tab"} />}
onClick={ () => client_control_events.fire("action_open_connect", { new_tab: true }) } />
);
}
if(!this.state.connected) {
return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-connect"} tooltip={tr("Connect to a server")}>
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-connect"} tooltip={tr("Connect to a server")}
onToggle={ () => client_control_events.fire("action_open_connect", { new_tab: false }) }>
{subentries}
</Button>
);
} else {
return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-disconnect"} tooltip={tr("Disconnect from server")}>
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-disconnect"} tooltip={tr("Disconnect from server")}
onToggle={ () => client_control_events.fire("action_disconnect", { globally: false }) }>
{subentries}
</Button>
);
}
}
@EventHandler<ControlBarEvents>("set_connect_state")
@EventHandler<ControlBarEvents>("update_connect_state")
private handleStateUpdate(state: ConnectionState) {
this.updateState(state);
}
}
@ReactEventHandler(obj => obj.props.event_registry)
class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, {}> {
private button_ref: React.RefObject<Button>;
protected initialize() {
this.button_ref = React.createRef();
}
protected default_state() {
return {};
}
render() {
//TODO: <DropdownEntry icon={"client-bookmark_remove"} text={<Translatable message={"Remove current server to bookmarks"} />} />
const marks = bookmarks().content.map(e => e.type === BookmarkType.DIRECTORY ? this.renderDirectory(e) : this.renderBookmark(e));
if(marks.length)
marks.splice(0, 0, <hr key={"hr"} />);
return (
<Button dropdownButtonExtraClass={cssButtonStyle.buttonBookmarks} autoSwitch={false} iconNormal={"client-bookmark_manager"}>
<DropdownEntry icon={"client-bookmark_manager"} text={<Translatable message={"Manage bookmarks"} />} />
<Button ref={this.button_ref} dropdownButtonExtraClass={cssButtonStyle.buttonBookmarks} autoSwitch={false} iconNormal={"client-bookmark_manager"}>
<DropdownEntry icon={"client-bookmark_manager"} text={<Translatable message={"Manage bookmarks"} />}
onClick={() => client_control_events.fire("action_open_window", { window: "bookmark-manage" })} />
<DropdownEntry icon={"client-bookmark_add"} text={<Translatable message={"Add current server to bookmarks"} />} />
<hr />
<DropdownEntry text={<Translatable message={"Bookmark X"} />} />
<DropdownEntry text={<Translatable message={"Bookmark Y"} />} />
{marks}
</Button>
)
}
private renderBookmark(bookmark: Bookmark) {
return (
<DropdownEntry key={bookmark.unique_id}
icon={IconManager.generate_tag(IconManager.load_cached_icon(bookmark.last_icon_id || 0), {animate: false})}
text={bookmark.display_name}
onClick={BookmarkButton.onBookmarkClick.bind(undefined, bookmark.unique_id)}
onContextMenu={this.onBookmarkContextMenu.bind(this, bookmark.unique_id)}/>
);
}
private renderDirectory(directory: DirectoryBookmark) {
return (
<DropdownEntry key={directory.unique_id} text={directory.display_name} >
{directory.content.map(e => e.type === BookmarkType.DIRECTORY ? this.renderDirectory(e) : this.renderBookmark(e))}
</DropdownEntry>
)
}
private static onBookmarkClick(bookmark_id: string) {
const bookmark = find_bookmark(bookmark_id) as Bookmark;
if(!bookmark) return;
boorkmak_connect(bookmark, false);
}
private onBookmarkContextMenu(bookmark_id: string, event: MouseEvent) {
event.preventDefault();
const bookmark = find_bookmark(bookmark_id) as Bookmark;
if(!bookmark) return;
this.button_ref.current?.updateState({ dropdownForceShow: true });
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Connect"),
icon_class: 'client-connect',
callback: () => boorkmak_connect(bookmark, false)
}, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Connect in a new tab"),
icon_class: 'client-connect',
callback: () => boorkmak_connect(bookmark, true),
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
}, contextmenu.Entry.CLOSE(() => {
this.button_ref.current?.updateState({ dropdownForceShow: false });
}));
}
@EventHandler<ControlBarEvents>("update_bookmarks")
private handleStateUpdate() {
this.forceUpdate();
}
}
export interface AwayState {
@ -103,22 +185,26 @@ class AwayButton extends ReactComponentBase<{ event_registry: Registry<ControlBa
render() {
let dropdowns = [];
if(this.state.away) {
dropdowns.push(<DropdownEntry key={"cgo"} icon={"client-present"} text={<Translatable message={"Go online"} />} />);
dropdowns.push(<DropdownEntry key={"sam"} icon={"client-away"} text={<Translatable message={"Set away message on this server"} />} />);
dropdowns.push(<DropdownEntry key={"cgo"} icon={"client-present"} text={<Translatable message={"Go online"} />}
onClick={() => client_control_events.fire("action_disable_away", { globally: false })} />);
} else {
dropdowns.push(<DropdownEntry key={"sas"} icon={"client-away"} text={<Translatable message={"Set away on this server"} />} />);
dropdowns.push(<DropdownEntry key={"sas"} icon={"client-away"} text={<Translatable message={"Set away on this server"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: false, prompt_reason: false })} />);
}
dropdowns.push(<DropdownEntry key={"sam"} icon={"client-away"} text={<Translatable message={"Set away message on this server"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: false, prompt_reason: true })} />);
dropdowns.push(<hr key={"-hr"} />);
if(!this.state.awayAll) {
dropdowns.push(<DropdownEntry key={"saa"} icon={"client-away"} text={<Translatable message={"Set away on all servers"} />} />);
}
if(this.state.awayAnywhere) {
dropdowns.push(<DropdownEntry key={"goa"} icon={"client-present"} text={<Translatable message={"Go online for all servers"} />} />);
dropdowns.push(<DropdownEntry key={"sama"} icon={"client-present"} text={<Translatable message={"Set away message for all servers"} />} />);
} else {
dropdowns.push(<DropdownEntry key={"goas"} icon={"client-away"} text={<Translatable message={"Go away for all servers"} />} />);
dropdowns.push(<DropdownEntry key={"goa"} icon={"client-present"} text={<Translatable message={"Go online for all servers"} />}
onClick={() => client_control_events.fire("action_disable_away", { globally: true })} />);
}
if(!this.state.awayAll) {
dropdowns.push(<DropdownEntry key={"saa"} icon={"client-away"} text={<Translatable message={"Set away on all servers"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: true, prompt_reason: false })} />);
}
dropdowns.push(<DropdownEntry key={"sama"} icon={"client-away"} text={<Translatable message={"Set away message for all servers"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: true, prompt_reason: true })} />);
/* switchable because we're switching it manually */
return (
@ -128,7 +214,7 @@ class AwayButton extends ReactComponentBase<{ event_registry: Registry<ControlBa
);
}
@EventHandler<ControlBarEvents>("set_away_state")
@EventHandler<ControlBarEvents>("update_away_state")
private handleStateUpdate(state: AwayState) {
this.updateState(state);
}
@ -145,16 +231,11 @@ class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Regist
}
render() {
return <Button switched={this.state.subscribeEnabled} autoSwitch={false} iconNormal={"client-unsubscribe_from_all_channels"} iconSwitched={"client-subscribe_to_all_channels"} onToggle={this.onToggle.bind(this)} />;
return <Button switched={this.state.subscribeEnabled} autoSwitch={false} iconNormal={"client-unsubscribe_from_all_channels"} iconSwitched={"client-subscribe_to_all_channels"}
onToggle={flag => client_control_events.fire("action_set_channel_subscribe_mode", { subscribe: flag })}/>;
}
private onToggle() {
this.updateState({
subscribeEnabled: !this.state.subscribeEnabled
});
}
@EventHandler<ControlBarEvents>("set_subscribe_state")
@EventHandler<ControlBarEvents>("update_subscribe_state")
private handleStateUpdate(state: ChannelSubscribeState) {
this.updateState(state);
}
@ -176,13 +257,16 @@ class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<Con
render() {
if(!this.state.enabled)
return <Button autoSwitch={false} iconNormal={"client-activate_microphone"} tooltip={tr("Enable your microphone on this server")} />;
return <Button autoSwitch={false} iconNormal={"client-activate_microphone"} tooltip={tr("Enable your microphone on this server")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: true })} />;
if(this.state.muted)
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Unmute microphone")} />;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Mute microphone")} />;
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Unmute microphone")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: true })} />;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Mute microphone")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: false })} />;
}
@EventHandler<ControlBarEvents>("set_microphone_state")
@EventHandler<ControlBarEvents>("update_microphone_state")
private handleStateUpdate(state: MicrophoneState) {
this.updateState(state);
}
@ -202,11 +286,13 @@ class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<Contro
render() {
if(this.state.muted)
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Unmute headphones")} />;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Mute headphones")} />;
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Unmute headphones")}
onToggle={() => client_control_events.fire("action_toggle_speaker", { state: true })}/>;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Mute headphones")}
onToggle={() => client_control_events.fire("action_toggle_speaker", { state: false })}/>;
}
@EventHandler<ControlBarEvents>("set_speaker_state")
@EventHandler<ControlBarEvents>("update_speaker_state")
private handleStateUpdate(state: SpeakerState) {
this.updateState(state);
}
@ -227,18 +313,22 @@ class QueryButton extends ReactComponentBase<{ event_registry: Registry<ControlB
render() {
let toggle;
if(this.state.queryShown)
toggle = <DropdownEntry icon={""} text={<Translatable message={"Hide server queries"} />}/>;
toggle = <DropdownEntry icon={""} text={<Translatable message={"Hide server queries"} />}
onClick={() => client_control_events.fire("action_toggle_query", { shown: false })}/>;
else
toggle = <DropdownEntry icon={"client-toggle_server_query_clients"} text={<Translatable message={"Show server queries"} />}/>;
toggle = <DropdownEntry icon={"client-toggle_server_query_clients"} text={<Translatable message={"Show server queries"} />}
onClick={() => client_control_events.fire("action_toggle_query", { shown: true })}/>;
return (
<Button autoSwitch={false} iconNormal={"client-server_query"}>
<Button switched={this.state.queryShown} autoSwitch={false} iconNormal={"client-server_query"}
onToggle={flag => client_control_events.fire("action_toggle_query", { shown: flag })}>
{toggle}
<DropdownEntry icon={"client-server_query"} text={<Translatable message={"Manage server queries"} />}/>
<DropdownEntry icon={"client-server_query"} text={<Translatable message={"Manage server queries"} />}
onClick={() => client_control_events.fire("action_open_window", { window: "query-manage" })}/>
</Button>
)
}
@EventHandler<ControlBarEvents>("set_query_state")
@EventHandler<ControlBarEvents>("update_query_state")
private handleStateUpdate(state: QueryState) {
this.updateState(state);
}
@ -267,18 +357,29 @@ class HostButton extends ReactComponentBase<{ event_registry: Registry<ControlBa
<a
className={this.classList(cssButtonStyle.button, cssButtonStyle.buttonHostbutton)}
title={this.state.title || tr("Hostbutton")}
href={this.state.target_url || this.state.url}>
href={this.state.target_url || this.state.url}
target={"_blank"} /* just to ensure */
onClick={this.onClick.bind(this)}>
<img alt={tr("Hostbutton")} src={this.state.url} />
</a>
);
}
@EventHandler<ControlBarEvents>("set_host_button")
private onClick(event: MouseEvent) {
window.open(this.state.target_url || this.state.url, '_blank');
event.preventDefault();
}
@EventHandler<ControlBarEvents>("update_host_button")
private handleStateUpdate(state: HostButtonState) {
this.updateState(state);
}
}
export interface ControlBarProperties {
multiSession: boolean;
}
@ReactEventHandler<ControlBar>(obj => obj.event_registry)
export class ControlBar extends React.Component<ControlBarProperties, {}> {
private readonly event_registry: Registry<ControlBarEvents>;
@ -288,8 +389,27 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
super(props);
this.event_registry = new Registry<ControlBarEvents>();
this.event_registry.enable_debug("control-bar");
register_actions(this.event_registry);
}
componentDidMount(): void {
}
/*
initialize_connection_handler_state(handler?: ConnectionHandler) {
handler.client_status.output_muted = this._button_speakers === "muted";
handler.client_status.input_muted = this._button_microphone === "muted";
handler.client_status.channel_subscribe_all = this._button_subscribe_all;
handler.client_status.queries_visible = this._button_query_visible;
}
*/
events() : Registry<ControlBarEvents> { return this.event_registry; }
render() {
return (
<div className={cssStyle.controlBar}>
@ -313,30 +433,146 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
if(this.connection == event.handler) return;
this.connection = event.handler;
this.event_registry.fire("update_all_states");
this.event_registry.fire("update_state_all");
}
@EventHandler<ControlBarEvents>(["update_all_states", "update_state"])
private updateStateHostButton(event) {
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateHostButton(event: Event<ControlBarEvents>) {
if(event.type === "update_state")4
if(event.as<"update_state">().state !== "host-button")
return;
const sprops = this.connection?.channelTree.server?.properties;
if(!sprops || !sprops.virtualserver_hostbutton_gfx_url) {
this.event_registry.fire("update_host_button", {
url: undefined,
target_url: undefined,
title: undefined
});
return;
}
this.event_registry.fire("update_host_button", {
url: sprops.virtualserver_hostbutton_gfx_url,
target_url: sprops.virtualserver_hostbutton_url,
title: sprops.virtualserver_hostbutton_tooltip
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateSubscribe(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "subscribe-mode")
return;
this.event_registry.fire("update_subscribe_state", {
subscribeEnabled: !!this.connection?.client_status.channel_subscribe_all
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateConnect(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "connect-state")
return;
this.event_registry.fire("update_connect_state", {
connectedAnywhere: server_connections.server_connection_handlers().findIndex(e => e.connected) !== -1,
connected: !!this.connection?.connected
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateAway(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "away")
return;
const connections = server_connections.server_connection_handlers();
const away_connections = server_connections.server_connection_handlers().filter(e => e.client_status.away);
const away_status = this.connection?.client_status.away;
this.event_registry.fire("update_away_state", {
awayAnywhere: away_connections.length > 0,
away: typeof away_status === "string" ? true : !!away_status,
awayAll: connections.length === away_connections.length
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateMicrophone(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "microphone")
return;
this.event_registry.fire("update_microphone_state", {
enabled: !!this.connection?.client_status.input_hardware,
muted: this.connection?.client_status.input_muted
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateSpeaker(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "speaker")
return;
this.event_registry.fire("update_speaker_state", {
muted: this.connection?.client_status.output_muted
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateQuery(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "query")
return;
this.event_registry.fire("update_query_state", {
queryShown: !!this.connection?.client_status.queries_visible
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateBookmarks(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "bookmarks")
return;
this.event_registry.fire("update_bookmarks");
}
}
let react_reference_: React.RefObject<ControlBar>;
export function react_reference() { return react_reference_ || (react_reference_ = React.createRef()); }
export function control_bar_instance() : ControlBar | undefined {
return react_reference_?.current;
}
export interface ControlBarEvents {
set_host_button: HostButtonState;
set_subscribe_state: ChannelSubscribeState;
set_connect_state: ConnectionState;
set_away_state: AwayState;
set_microphone_state: MicrophoneState;
set_speaker_state: SpeakerState;
set_query_state: QueryState;
/* update the UI */
update_host_button: HostButtonState;
update_subscribe_state: ChannelSubscribeState;
update_connect_state: ConnectionState;
update_away_state: AwayState;
update_microphone_state: MicrophoneState;
update_speaker_state: SpeakerState;
update_query_state: QueryState;
update_bookmarks: {},
update_state: {
states: "host-button" | "subscribe-mode" | "connect-state" | "away" | "microphone" | "speaker" | "query"
}
update_all_states: {},
state: "host-button" | "bookmarks" | "subscribe-mode" | "connect-state" | "away" | "microphone" | "speaker" | "query"
},
update_state_all: { },
/* trigger actions */
set_connection_handler: {
handler?: ConnectionHandler
},
server_updated: {
handler: ConnectionHandler,
category: "audio" | "settings-initialized" | "connection-state" | "away-status" | "hostbanner"
}
//settings-initialized: Update query and channel flags
}

View file

@ -66,7 +66,7 @@ export class MusicInfo {
destroy() {
this.set_current_bot(undefined);
this.events.destory();
this.events.destroy();
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;

View file

@ -16,8 +16,8 @@ import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
import * as i18nc from "tc-shared/i18n/country";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as top_menu from "../frames/MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export function spawnBookmarkModal() {
let modal: Modal;
@ -375,7 +375,7 @@ export function spawnBookmarkModal() {
modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks");
modal.close_listener.push(() => {
control_bar.update_bookmarks();
control_bar_instance()?.events().fire("update_state", { state: "bookmarks" });
top_menu.rebuild_bookmarks();
});

View file

@ -1,7 +1,6 @@
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
import {tra} from "tc-shared/i18n/localize";
import {Registry} from "tc-shared/events";
import * as loader from "tc-loader";
import { modal as emodal } from "tc-shared/events";
import {modal_settings} from "tc-shared/ui/modal/ModalSettings";
import {profiles} from "tc-shared/profiles/ConnectionProfile";

View file

@ -11,9 +11,9 @@ import {createServerModal} from "tc-shared/ui/modal/ModalServerEdit";
import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect";
import {spawnAvatarList} from "tc-shared/ui/modal/ModalAvatarList";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import {connection_log} from "tc-shared/ui/modal/ModalConnect";
import * as top_menu from "./frames/MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export class ServerProperties {
virtualserver_host: string = "";
@ -318,7 +318,8 @@ export class ServerEntry {
});
bookmarks.save_bookmark();
top_menu.rebuild_bookmarks();
control_bar.update_bookmarks();
control_bar_instance()?.events().fire("update_state", { state: "bookmarks" });
}
if(this.channelTree.client.fileManager && this.channelTree.client.fileManager.icons)
@ -332,8 +333,7 @@ export class ServerEntry {
if(update_bannner)
this.channelTree.client.hostbanner.update();
if(update_button)
if(control_bar.current_connection_handler() === this.channelTree.client)
control_bar.apply_server_hostbutton();
control_bar_instance()?.events().fire("server_updated", { handler: this.channelTree.client, category: "hostbanner" });
group.end();
if(is_self_notify && this.info_request_promise_resolve) {

View file

@ -1,6 +1,7 @@
/* General file with least possible errors. This is just for your IDE (PHP-Storm for example) */
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es6",
"module": "commonjs",
"sourceMap": true,