Added a lot of new stuff

canary
WolverinDEV 2019-04-04 21:47:52 +02:00
parent efb4bad182
commit 6d537bac15
66 changed files with 3564 additions and 2103 deletions

View File

@ -1,4 +1,12 @@
# Changelog:
* **04.04.19**
- Fixed issue with client icons not updating correctly (Showing invalid microphone state)
- Added multi server mode
- Connect URL not disconnecting from all other servers
- Open certificate accept URL in another tab
- Improved the host banner experience
- Hiding server group types in permission editor which are not editable
* **28.03.19**
- Improved icon and avatar cache handling
- Added an icon manager

View File

@ -13,7 +13,7 @@ html, body {
height: 100%;
position: absolute;
display: flex;
justify-content: center;
justify-content: stretch;
.app {
width: 100%;

View File

@ -1,3 +1,5 @@
@import "properties";
/* the channel tree */
.channel-tree {
-webkit-touch-callout: none;
@ -60,7 +62,11 @@
}
&.selected {
background-color: blue;
background-color: $channel_tree_entry_selected;
.name {
color: whitesmoke;
}
}
}
@ -79,10 +85,6 @@
align-items: center;
cursor: pointer;
&.selected {
background-color: blue;
}
.channel-type {
flex-grow: 0;
flex-shrink: 0;
@ -128,6 +130,13 @@
border-bottom: 1px solid black;
}
&.selected {
background-color: $channel_tree_entry_selected;
.channel-name {
color: whitesmoke;
}
}
.show-channel-normal-only {
display: none;
@ -191,7 +200,11 @@
}
&.selected {
background-color: blue;
background-color: $channel_tree_entry_selected;
.client-name {
color: whitesmoke;
}
}
}
}

View File

@ -0,0 +1,119 @@
.container-connection-handlers {
height: 35px;
background-color: lightgray;
border: 4px solid lightgray;
border-top: 1px dotted gray;
border-bottom-width: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
.connection-handlers {
height: 100%;
width: fit-content;
display: flex;
flex-direction: row;
justify-content: left;
.connection-container {
flex-grow: 0;
flex-shrink: 0;
cursor: pointer;
display: inline-flex;
margin-top: 5px;
padding-left: 5px;
padding-right: 5px;
border: 1px #2222223b solid;
border-radius: 2px 2px 0 0;
.server-icon {
align-self: center;
margin-right: 5px;
}
.server-name {
align-self: center;
margin-right: 5px;
}
.button-close {
align-self: center;
&:hover {
background-color: #e7e7e7;
}
}
&.active {
background-color: #FFFFFF33;
}
}
&::-webkit-scrollbar {
display: none;
}
overflow-x: auto;
overflow-y: visible;
}
.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 #2222223b solid;
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);
}
}
}

View File

@ -6,6 +6,11 @@ $background:lightgray;
display: flex;
flex-direction: row;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* tmp fix for ultra small devices */
overflow-y: visible;
@ -99,8 +104,6 @@ $background:lightgray;
border: 2px solid $border_color_activated;
width: 230px;
user-select: none;
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);*/
@ -136,7 +139,7 @@ $background:lightgray;
}
}
&:hover.displayed {
&:hover.displayed, &.force-show {
.dropdown {
display: block;
}

View File

@ -66,10 +66,12 @@
flex-grow: 1;
flex-shrink: 2;
max-height: 25%;
min-height: 0;
display: flex;
justify-content: stretch;
overflow: hidden;
@ -81,6 +83,12 @@
.hostbanner {
position: relative;
flex-grow: 1;
flex-shrink: 1;
.meta-image {
display: none;
}
.image-container {
display: flex;

View File

@ -238,6 +238,7 @@ $animation_length: .5s;
.app {
min-width: 350px;
min-height: 330px;
.container-app-main {
position: relative;
@ -245,6 +246,7 @@ $animation_length: .5s;
flex-direction: row;
justify-content: stretch;
min-height: 0;
height: 100%;
width: 100%;
@ -254,9 +256,11 @@ $animation_length: .5s;
}
.container-control-bar {
flex-shrink: 0;
height: 45px;
width: 100%;
border-radius: 2px 0 0 0;
border-radius: 2px 2px 0 0;
border-bottom-width: 0;
background-color: lightgrey;
@ -327,6 +331,12 @@ $animation_length: .5s;
}
}
@media only screen and (max-width: 400px), only screen and (max-height: 400px) {
.app-container {
overflow: auto;
}
}
@media only screen and (max-width: $small_device) {
.app-container {
right: 0;
@ -334,11 +344,12 @@ $animation_length: .5s;
top: 0;
transition: all $animation_length linear;
overflow: auto;
overflow: auto;
}
.app {
.container-app-main {
.container-info {
display: none;
position: absolute;

View File

@ -0,0 +1 @@
$channel_tree_entry_selected: blue;

View File

@ -1,4 +1,4 @@
#chat {
.container-frame-chat {
font-family: Arial, serif;
font-size: 12px;
/*white-space: pre;*/

View File

@ -12,12 +12,27 @@
<!-- navigation bar -->
<div class="container-control-bar">
<div id="control_bar" class="control_bar">
<div class="button btn_connect" title="{{tr 'Connect to a server' /}}">
<div class="button btn_connect container-connect" title="{{tr 'Connect to a server' /}}">
<div class="icon_x32 client-connect"></div>
</div>
<div class="button-dropdown container-disconnect" title="{{tr 'Disconnect from server' /}}" style="display: none">
<div class="buttons">
<div class="button icon_x32 client-disconnect btn_disconnect"></div>
<div class="button-dropdown">
<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>
<!--
<div class="button btn_disconnect" title="{{tr 'Disconnect from server' /}}" style="display: none">
<div class="icon_x32 client-disconnect"></div>
</div>
-->
<div class="button-dropdown btn_bookmark" title="{{tr 'Bookmarks' /}}">
<div class="buttons">
@ -44,9 +59,14 @@
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown">
<div class="btn_away_toggle"><div class="icon client-away"></div><a>{{tr "Toggle away status" /}}</a></div>
<div class="btn_away_message"><div class="icon client-away"></div><a>{{tr "Set away message" /}}</a></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="hide-small button btn_mute_input">
@ -154,6 +174,17 @@
</div>
</div>
</div>
<div class="container-connection-handlers scrollbar" id="connection-handlers">
<div class="connection-handlers"> </div>
<div class="container-scroll">
<div class="button-scroll button-scroll-left">
<div class="icon client-arrow_left"></div>
</div>
<div class="button-scroll button-scroll-right disabled">
<div class="icon client-arrow_right"></div>
</div>
</div>
</div>
<div class="container-app-main">
<div class="container-channel-chat">
@ -164,43 +195,47 @@
<div class="container-seperator horizontal" seperator-id="seperator-channel-chat"></div>
<!-- Chat window -->
<div class="main_container container-chat">
<div id="chat">
<div class="messages">
<div class="message_box"></div>
</div>
<div class="chats"></div>
<div class="input">
<!--<div contentEditable="true" class="input_box"></div>-->
<div class="form-group">
<textarea id="input-chat-input"
type="text"
class="form-control input-message"
placeholder="{{tr 'enter a chat message...' /}}"
autocomplete="off"></textarea>
</div>
<button class="btn btn-primary button-send">{{tr "Send" /}}</button>
</div>
</div>
</div>
<div class="main_container container-chat" id="chat"> </div>
</div>
<div class="container-seperator vertical" seperator-id="seperator-main-info"></div>
<div id="select_info" class="main_container container-info">
<div class="select_info" style="width: 100%; max-width: 100%">
<button type="button" class="close" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<div class="container-banner"></div>
<!-- <div class="container-seperator horizontal" seperator-id="seperator-hostbanner-info"></div> -->
<div class="container-select-info"></div>
</div>
</div> <!-- Selection info -->
<div id="select_info" class="main_container container-info"> </div> <!-- Selection info -->
</div>
</div>
</div>
<div id="contextMenu" class="context-menu"></div>
</script>
<script class="jsrender-template" id="tmpl_select_info" type="text/html">
<div class="select_info" style="width: 100%; max-width: 100%">
<button type="button" class="close" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<div class="container-banner"></div>
<!-- <div class="container-seperator horizontal" seperator-id="seperator-hostbanner-info"></div> -->
<div class="container-select-info"></div>
</div>
</script>
<script class="jsrender-template" id="tmpl_frame_chat" type="text/html">
<div class="container-frame-chat">
<div class="messages">
<div class="message_box"></div>
</div>
<div class="chats"></div>
<div class="input">
<!--<div contentEditable="true" class="input_box"></div>-->
<div class="form-group">
<textarea id="input-chat-input"
type="text"
class="form-control input-message"
placeholder="{{tr 'enter a chat message...' /}}"
autocomplete="off"></textarea>
</div>
<button class="btn btn-primary button-send">{{tr "Send" /}}</button>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_modal" type="text/html">
<div class="modal fade" tabindex="-1" role="dialog" style="padding-right: 8%; padding-left: 8%;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document" style="max-width: unset;">
@ -330,7 +365,10 @@
</div>
</modal-body>
<modal-footer>
<button type="button" class="btn btn-raised btn-success button-connect">Connect</button>
<button type="button" class="btn btn-raised btn-success button-connect">{{tr "Connect" /}}</button>
{{if multi_tab}}
<button type="button" class="btn btn-raised btn-success button-connect-new-tab">{{tr "Connect in a new tab" /}}</button>
{{/if}}
</modal-footer>
</modal>
</script>
@ -2313,8 +2351,13 @@
</div>
</script>
<script class="jsrender-template" id="tmpl_selected_hostbanner" type="text/html">
<div class="hostbanner">
<a class="image-container" href="{{:property_virtualserver_hostbanner_url}}" target="_blank">
<div class="hostbanner" x-divider-require-resize="1">
<img class="meta-image hostbanner-image" src="{{:hostbanner_gfx_url}}"/>
<a class="image-container"
{{if property_virtualserver_hostbanner_url}}
href="{{:property_virtualserver_hostbanner_url}}"
{{/if}}
target="_blank">
<div
style="background: center no-repeat url({{:hostbanner_gfx_url}})"
alt="{{tr 'Host banner'/}}"

215
shared/js/BrowserIPC.ts Normal file
View File

@ -0,0 +1,215 @@
interface Window {
BroadcastChannel: BroadcastChannel;
}
namespace bipc {
export interface BroadcastMessage {
timestamp: number;
receiver: string;
sender: string;
type: string;
data: any;
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
interface ProcessQuery {
timestamp: number
query_id: string;
}
export interface ProcessQueryResponse {
request_timestamp: number
request_query_id: string;
device_id: string;
protocol: number;
}
export interface CertificateAcceptCallback {
request_id: string;
}
export interface CertificateAcceptSucceeded {
}
export abstract class BasicIPCHandler {
protected static readonly BROADCAST_UNIQUE_ID = "00000000-0000-4000-0000-000000000000";
protected static readonly PROTOCOL_VERSION = 1;
protected unique_id;
protected constructor() { }
setup() {
this.unique_id = uuidv4(); /* lets get an unique identifier */
}
abstract send_message(type: string, data: any, target?: string);
protected handle_message(message: BroadcastMessage) {
console.log("Received: %o", message);
if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) {
if(message.type == "process-query") {
log.debug(LogCategory.IPC, tr("Received a device query from %s."), message.sender);
this.send_message("process-query-response", {
request_query_id: (<ProcessQuery>message.data).query_id,
request_timestamp: (<ProcessQuery>message.data).timestamp,
device_id: this.unique_id,
protocol: BasicIPCHandler.PROTOCOL_VERSION
} as ProcessQueryResponse, message.sender);
return;
}
} else if(message.receiver === this.unique_id) {
if(message.type == "process-query-response") {
const response: ProcessQueryResponse = message.data;
if(this._query_results[response.request_query_id])
this._query_results[response.request_query_id].push(response);
else {
log.warn(LogCategory.IPC, tr("Received a query response for an unknown request."));
}
return;
} else if(message.type == "certificate-accept-callback") {
const data: CertificateAcceptCallback = message.data;
if(!this._cert_accept_callbacks[data.request_id]) {
log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID."));
return;
}
this._cert_accept_callbacks[data.request_id]();
delete this._cert_accept_callbacks[data.request_id];
this.send_message("certificate-accept-succeeded", {
} as CertificateAcceptSucceeded, message.sender);
return;
} else if(message.type == "certificate-accept-succeeded") {
if(!this._cert_accept_succeeded[message.sender]) {
log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback."));
return;
}
this._cert_accept_succeeded[message.sender]();
return;
}
}
}
private _query_results: {[key: string]:ProcessQueryResponse[]} = {};
async query_processes(timeout?: number) : Promise<ProcessQueryResponse[]> {
const query_id = uuidv4();
this._query_results[query_id] = [];
this.send_message("process-query", {
query_id: query_id,
timestamp: Date.now()
} as ProcessQuery);
await new Promise(resolve => setTimeout(resolve, timeout || 250));
const result = this._query_results[query_id];
delete this._query_results[query_id];
return result;
}
private _cert_accept_callbacks: {[key: string]:(() => any)} = {};
register_certificate_accept_callback(callback: () => any) : string {
const id = uuidv4();
this._cert_accept_callbacks[id] = callback;
return this.unique_id + ":" + id;
}
private _cert_accept_succeeded: {[sender: string]:(() => any)} = {};
post_certificate_accpected(id: string, timeout?: number) : Promise<void> {
return new Promise((resolve, reject) => {
const data = id.split(":");
const timeout_id = setTimeout(() => {
delete this._cert_accept_succeeded[data[0]];
clearTimeout(timeout_id);
reject("timeout");
}, timeout || 250);
this._cert_accept_succeeded[data[0]] = () => {
delete this._cert_accept_succeeded[data[0]];
clearTimeout(timeout_id);
resolve();
};
this.send_message("certificate-accept-callback", {
request_id: data[1]
} as CertificateAcceptCallback, data[0]);
})
}
}
class BroadcastChannelIPC extends BasicIPCHandler {
private static readonly CHANNEL_NAME = "TeaSpeak-Web";
private channel: BroadcastChannel;
constructor() {
super();
}
setup() {
super.setup();
this.channel = new BroadcastChannel(BroadcastChannelIPC.CHANNEL_NAME);
this.channel.onmessage = this.on_message.bind(this);
this.channel.onmessageerror = this.on_error.bind(this);
}
private on_message(event: MessageEvent) {
if(typeof(event.data) !== "string") {
log.warn(LogCategory.IPC, tr("Received message with an invalid type (%s): %o"), typeof(event.data), event.data);
return;
}
let message: BroadcastMessage;
try {
message = JSON.parse(event.data);
} catch(error) {
log.error(LogCategory.IPC, tr("Received an invalid encoded message: %o"), event.data);
return;
}
super.handle_message(message);
}
private on_error(event: MessageEvent) {
log.warn(LogCategory.IPC, tr("Received error: %o"), event);
}
send_message(type: string, data: any, target?: string) {
const message: BroadcastMessage = {} as any;
message.sender = this.unique_id;
message.receiver = target ? target : BasicIPCHandler.BROADCAST_UNIQUE_ID;
message.timestamp = Date.now();
message.type = type;
message.data = data;
this.channel.postMessage(JSON.stringify(message));
}
}
let handler: BasicIPCHandler;
export function setup() {
if(!supported())
return;
/* TODO: test for support */
handler = new BroadcastChannelIPC();
handler.setup();
}
export function get_handler() {
return handler;
}
export function supported() {
/* ios does not support this */
return typeof(window.BroadcastChannel) !== "undefined";
}
}

View File

@ -0,0 +1,596 @@
/// <reference path="log.ts" />
/// <reference path="voice/VoiceClient.ts" />
/// <reference path="proto.ts" />
/// <reference path="ui/view.ts" />
/// <reference path="settings.ts" />
/// <reference path="ui/frames/SelectedItemInfo.ts" />
/// <reference path="FileManager.ts" />
/// <reference path="permission/PermissionManager.ts" />
/// <reference path="permission/GroupManager.ts" />
/// <reference path="ui/frames/ControlBar.ts" />
/// <reference path="connection/ConnectionBase.ts" />
enum DisconnectReason {
HANDLER_DESTROYED,
REQUESTED,
CONNECT_FAILURE,
CONNECTION_CLOSED,
CONNECTION_FATAL_ERROR,
CONNECTION_PING_TIMEOUT,
CLIENT_KICKED,
CLIENT_BANNED,
HANDSHAKE_FAILED,
SERVER_CLOSED,
SERVER_REQUIRES_PASSWORD,
UNKNOWN
}
enum ConnectionState {
UNCONNECTED,
CONNECTING,
INITIALISING,
CONNECTED,
DISCONNECTING
}
enum ViewReasonId {
VREASON_USER_ACTION = 0,
VREASON_MOVED = 1,
VREASON_SYSTEM = 2,
VREASON_TIMEOUT = 3,
VREASON_CHANNEL_KICK = 4,
VREASON_SERVER_KICK = 5,
VREASON_BAN = 6,
VREASON_SERVER_STOPPED = 7,
VREASON_SERVER_LEFT = 8,
VREASON_CHANNEL_UPDATED = 9,
VREASON_EDITED = 10,
VREASON_SERVER_SHUTDOWN = 11
}
interface VoiceStatus {
input_hardware: boolean;
input_muted: boolean;
output_muted: boolean;
channel_codec_encoding_supported: boolean;
channel_codec_decoding_supported: boolean;
sound_playback_supported: boolean;
sound_record_supported;
away: boolean | string;
channel_subscribe_all: boolean;
queries_visible: boolean;
}
class ConnectionHandler {
channelTree: ChannelTree;
serverConnection: connection.ServerConnection;
fileManager: FileManager;
permissions: PermissionManager;
groups: GroupManager;
select_info: InfoBar;
chat: ChatBox;
settings: ServerSettings;
sound: sound.SoundManager;
readonly tag_connection_handler: JQuery;
private _clientId: number = 0;
private _local_client: LocalClientEntry;
private _reconnect_timer: NodeJS.Timer;
private _reconnect_attempt: boolean = false;
client_status: VoiceStatus = {
input_hardware: false,
input_muted: false,
output_muted: false,
away: false,
channel_subscribe_all: true,
queries_visible: false,
sound_playback_supported: undefined,
sound_record_supported: undefined,
channel_codec_encoding_supported: undefined,
channel_codec_decoding_supported: undefined
};
invoke_resized_on_activate: boolean = false;
constructor() {
this.settings = new ServerSettings();
this.select_info = new InfoBar(this);
this.channelTree = new ChannelTree(this);
this.chat = new ChatBox(this);
this.sound = new sound.SoundManager(this);
this.serverConnection = new connection.ServerConnection(this);
this.serverConnection.onconnectionstatechanged = this.on_connection_state_changed.bind(this);
this.fileManager = new FileManager(this);
this.permissions = new PermissionManager(this);
this.groups = new GroupManager(this);
this._local_client = new LocalClientEntry(this);
this.channelTree.registerClient(this._local_client);
//settings.static_global(Settings.KEY_DISABLE_VOICE, false)
this.chat.initialize();
this.tag_connection_handler = $.spawn("div").addClass("connection-container");
$.spawn("div").addClass("server-icon icon client-server_green").appendTo(this.tag_connection_handler);
$.spawn("div").addClass("server-name").text(tr("Not connected")).appendTo(this.tag_connection_handler);
$.spawn("div").addClass("button-close icon client-tab_close_button").appendTo(this.tag_connection_handler);
this.tag_connection_handler.on('click', event => {
if(event.isDefaultPrevented())
return;
server_connections.set_active_connection_handler(this);
});
this.tag_connection_handler.find(".button-close").on('click', event => {
server_connections.destroy_server_connection_handler(this);
event.preventDefault();
});
}
setup() { }
startConnection(addr: string, profile: profiles.ConnectionProfile, name?: string, password?: {password: string, hashed: boolean}) {
this.tag_connection_handler.find(".server-name").text(tr("Connecting"));
this.cancel_reconnect();
this._reconnect_attempt = false;
if(this.serverConnection)
this.handleDisconnect(DisconnectReason.REQUESTED);
let idx = addr.lastIndexOf(':');
let port: number;
let host: string;
if(idx != -1) {
port = parseInt(addr.substr(idx + 1));
host = addr.substr(0, idx);
} else {
host = addr;
port = 9987;
}
console.log(tr("Start connection to %s:%d"), host, port);
this.channelTree.initialiseHead(addr, {host, port});
if(password && !password.hashed) {
helpers.hashPassword(password.password).then(password => {
/* errors will be already handled via the handle disconnect thing */
this.serverConnection.connect({host, port}, new connection.HandshakeHandler(profile, name, password));
}).catch(error => {
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open();
})
} else {
/* errors will be already handled via the handle disconnect thing */
this.serverConnection.connect({host, port}, new connection.HandshakeHandler(profile, name, password ? password.password : undefined));
}
}
getClient() : LocalClientEntry { return this._local_client; }
getClientId() { return this._clientId; }
set clientId(id: number) {
this._clientId = id;
this._local_client["_clientId"] = id;
}
get clientId() {
return this._clientId;
}
getServerConnection() : connection.ServerConnection { return this.serverConnection; }
/**
* LISTENER
*/
onConnected() {
console.log("Client connected!");
this.channelTree.registerClient(this._local_client);
this.permissions.requestPermissionList();
if(this.groups.serverGroups.length == 0)
this.groups.requestGroups();
this.initialize_server_settings();
/* apply the server settings */
if(this.client_status.channel_subscribe_all)
this.channelTree.subscribe_all_channels();
else
this.channelTree.unsubscribe_all_channels();
this.channelTree.toggle_server_queries(this.client_status.queries_visible);
this.sync_status_with_server();
/*
No need to update the voice stuff because as soon we see ourself we're doing it
this.update_voice_status();
if(control_bar.current_connection_handler() === this)
control_bar.apply_server_voice_state();
*/
}
private initialize_server_settings() {
let update_control = false;
this.settings.setServer(this.channelTree.server);
{
const flag_subscribe = this.settings.server(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, true);
if(this.client_status.channel_subscribe_all != flag_subscribe) {
this.client_status.channel_subscribe_all = flag_subscribe;
update_control = true;
}
}
{
const flag_query = this.settings.server(Settings.KEY_CONTROL_SHOW_QUERIES, false);
if(this.client_status.queries_visible != flag_query) {
this.client_status.queries_visible = flag_query;
update_control = true;
}
}
if(update_control && server_connections.active_connection_handler() === this) {
control_bar.apply_server_state();
}
}
get connected() : boolean {
return this.serverConnection && this.serverConnection.connected();
}
private generate_ssl_certificate_accept() : JQuery {
const properties = {
connect_default: true,
connect_profile: this.serverConnection._handshakeHandler.profile.id,
connect_address: this.serverConnection._remote_address.host + (this.serverConnection._remote_address.port !== 9987 ? ":" + this.serverConnection._remote_address.port : "")
};
const build_url = props => {
const parameters: string[] = [];
for(const key of Object.keys(props))
parameters.push(key + "=" + encodeURIComponent(props[key]));
let callback = document.location.origin + document.location.pathname + document.location.search; /* don't use document.URL because it may contains a #! */
if(document.location.search.length == 0)
callback += "?" + parameters.join("&");
else
callback += "&" + parameters.join("&");
return "https://" + this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port + "/?forward_url=" + encodeURIComponent(callback);
};
/* generate the tag */
const tag = $.spawn("a").text(tr("here"));
if(bipc.supported()) {
tag.attr('href', "#");
let popup: Window;
tag.on('click', event => {
const features = {
status: "no",
location: "no",
toolbar: "no",
menubar: "no",
width: 600,
height: 400
};
if(popup)
popup.close();
properties["certificate_callback"] = bipc.get_handler().register_certificate_accept_callback(() => {
log.info(LogCategory.GENERAL, tr("Received notification that the certificate has been accepted! Attempting reconnect!"));
if(this._certificate_modal)
this._certificate_modal.close();
popup.close(); /* no need, but nicer */
this.startConnection(properties.connect_address, profiles.find_profile(properties.connect_profile) || profiles.default_profile());
});
const url = build_url(properties);
const features_string = [...Object.keys(features)].map(e => e + "=" + features[e]).reduce((a, b) => a + "," + b);
popup = window.open(url, "TeaWeb certificate accept", features_string);
try {
popup.focus();
} catch(e) {
log.warn(LogCategory.GENERAL, tr("Certificate accept popup has been blocked. Trying a blank page and replacing href"));
window.open(url, "TeaWeb certificate accept"); /* trying without features */
tag.attr("target", "_blank");
tag.attr("href", url);
tag.unbind('click');
}
});
} else {
tag.attr('href', build_url(properties));
}
return tag;
}
private _certificate_modal: Modal;
handleDisconnect(type: DisconnectReason, data: any = {}) {
this.tag_connection_handler.find(".server-name").text(tr("Not connected"));
let auto_reconnect = false;
switch (type) {
case DisconnectReason.REQUESTED:
break;
case DisconnectReason.HANDLER_DESTROYED:
if(data)
this.sound.play(Sound.CONNECTION_DISCONNECTED);
break;
case DisconnectReason.CONNECT_FAILURE:
if(this._reconnect_attempt) {
auto_reconnect = true;
this.chat.serverChat().appendError(tr("Connect failed"));
break;
}
console.error(tr("Could not connect to remote host! Exception: %o"), data);
if(native_client) {
createErrorModal(
tr("Could not connect"),
tr("Could not connect to remote host (Connection refused)")
).open();
} else {
const error_message_format =
"Could not connect to remote host (Connection refused)\n" +
"If you're sure that the remote host is up, than you may not allow unsigned certificates.\n" +
"Click {0} to accept the remote certificate";
this._certificate_modal = createErrorModal(
tr("Could not connect"),
MessageHelper.formatMessage(tr(error_message_format), this.generate_ssl_certificate_accept())
);
this._certificate_modal.close_listener.push(() => this._certificate_modal = undefined);
this._certificate_modal.open();
}
this.sound.play(Sound.CONNECTION_REFUSED);
break;
case DisconnectReason.HANDSHAKE_FAILED:
//TODO sound
console.error(tr("Failed to process handshake: %o"), data);
createErrorModal(
tr("Could not connect"),
tr("Failed to process handshake: ") + data as string
).open();
break;
case DisconnectReason.CONNECTION_CLOSED:
console.error(tr("Lost connection to remote server!"));
createErrorModal(
tr("Connection closed"),
tr("The connection was closed by remote host")
).open();
this.sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = true;
break;
case DisconnectReason.CONNECTION_PING_TIMEOUT:
console.error(tr("Connection ping timeout"));
this.sound.play(Sound.CONNECTION_DISCONNECTED_TIMEOUT);
createErrorModal(
tr("Connection lost"),
tr("Lost connection to remote host (Ping timeout)<br>Even possible?")
).open();
break;
case DisconnectReason.SERVER_CLOSED:
this.chat.serverChat().appendError(tr("Server closed ({0})"), data.reasonmsg);
createErrorModal(
tr("Server closed"),
"The server is closed.<br>" + //TODO tr
"Reason: " + data.reasonmsg
).open();
this.sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = true;
break;
case DisconnectReason.SERVER_REQUIRES_PASSWORD:
this.chat.serverChat().appendError(tr("Server requires password"));
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
if(!(typeof password === "string")) return;
this.startConnection(this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port,
this.serverConnection._handshakeHandler.profile,
this.serverConnection._handshakeHandler.name,
{password: password as string, hashed: false});
}).open();
break;
case DisconnectReason.CLIENT_KICKED:
this.chat.serverChat().appendError(tr("You got kicked from the server by {0}{1}"),
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]),
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : "");
this.sound.play(Sound.SERVER_KICKED);
auto_reconnect = true;
break;
case DisconnectReason.CLIENT_BANNED:
this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"),
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]),
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : "");
this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
break;
default:
console.error(tr("Got uncaught disconnect!"));
console.error(tr("Type: %o Data:"), type);
console.error(data);
break;
}
this.channelTree.reset();
if(this.serverConnection)
this.serverConnection.disconnect();
if(control_bar.current_connection_handler() == this)
control_bar.update_connection_state();
this.select_info.setCurrentSelected(null);
this.select_info.update_banner();
if(auto_reconnect) {
if(!this.serverConnection) {
console.log(tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
return;
}
this.chat.serverChat().appendMessage(tr("Reconnecting in 5 seconds"));
console.log(tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
const server_address = this.serverConnection._remote_address;
const profile = this.serverConnection._handshakeHandler.profile;
const name = this.serverConnection._handshakeHandler.name;
const password = this.serverConnection._handshakeHandler.server_password;
this._reconnect_timer = setTimeout(() => {
this._reconnect_timer = undefined;
this.chat.serverChat().appendMessage(tr("Reconnecting..."));
log.info(LogCategory.NETWORKING, tr("Reconnecting..."))
this.startConnection(server_address.host + ":" + server_address.port, profile, name, password ? { password: password, hashed: true} : undefined);
this._reconnect_attempt = true;
}, 5000);
}
}
cancel_reconnect() {
if(this._reconnect_timer) {
this.chat.serverChat().appendMessage(tr("Reconnect canceled"));
clearTimeout(this._reconnect_timer);
this._reconnect_timer = undefined;
}
}
private on_connection_state_changed() {
if(control_bar.current_connection_handler() == this)
control_bar.update_connection_state();
}
update_voice_status(targetChannel?: ChannelEntry) {
targetChannel = targetChannel || this.getClient().currentChannel();
const vconnection = this.serverConnection.voice_connection();
const basic_voice_support = this.serverConnection.support_voice() && vconnection.connected();
const support_record = basic_voice_support && (!targetChannel || vconnection.encoding_supported(targetChannel.properties.channel_codec));
const support_playback = basic_voice_support && (!targetChannel || vconnection.decoding_supported(targetChannel.properties.channel_codec));
const property_update = {
client_input_muted: this.client_status.input_muted,
client_output_muted: this.client_status.output_muted
};
if(!this.serverConnection.support_voice() || !vconnection.connected()) {
property_update["client_input_hardware"] = false;
property_update["client_output_hardware"] = false;
this.client_status.input_hardware = true; /* IDK if we have input hardware or not, but it dosn't matter at all so */
} else {
const audio_source = vconnection.voice_recorder();
const recording_supported = typeof(audio_source) !== "undefined" && audio_source.is_recording_supported() && (!targetChannel || vconnection.encoding_supported(targetChannel.properties.channel_codec));
const playback_supported = !targetChannel || vconnection.decoding_supported(targetChannel.properties.channel_codec);
property_update["client_input_hardware"] = recording_supported;
property_update["client_output_hardware"] = playback_supported;
this.client_status.input_hardware = recording_supported;
}
if(this.serverConnection && this.serverConnection.connected()) {
const client_properties = this.getClient().properties;
for(const key of Object.keys(property_update)) {
if(client_properties[key] === property_update[key])
delete property_update[key];
}
if(Object.keys(property_update).length > 0) {
this.serverConnection.send_command("clientupdate", property_update).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error);
this.chat.serverChat().appendError(tr("Failed to update audio hardware properties."));
/* Update these properties anyways (for case the server fails to handle the command) */
const updates = [];
for(const key of Object.keys(property_update))
updates.push({key: key, value: (property_update[key]) + ""});
this.getClient().updateVariables(...updates);
});
}
} else { /* no icons are shown so no update at all */ }
if(targetChannel && (!vconnection || vconnection.connected())) {
const encoding_supported = vconnection.encoding_supported(targetChannel.properties.channel_codec);
const decoding_supported = vconnection.decoding_supported(targetChannel.properties.channel_codec);
if(this.client_status.channel_codec_decoding_supported !== decoding_supported || this.client_status.channel_codec_encoding_supported !== encoding_supported) {
this.client_status.channel_codec_decoding_supported = decoding_supported;
this.client_status.channel_codec_encoding_supported = encoding_supported;
let message;
if(!encoding_supported && !decoding_supported)
message = tr("This channel has an unsupported codec.<br>You cant speak or listen to anybody within this channel!");
else if(!encoding_supported)
message = tr("This channel has an unsupported codec.<br>You cant speak within this channel!");
else if(!decoding_supported)
message = tr("This channel has an unsupported codec.<br>You listen to anybody within this channel!"); /* implies speaking does not work as well */
if(message)
createErrorModal(tr("Channel codec unsupported"), message).open();
}
}
this.client_status = this.client_status || {} as any;
this.client_status.sound_record_supported = support_record;
this.client_status.sound_playback_supported = support_playback;
if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().is_recording_supported())
vconnection.voice_recorder().set_recording(!this.client_status.input_muted && !this.client_status.output_muted);
if(control_bar.current_connection_handler() === this)
control_bar.apply_server_voice_state();
}
sync_status_with_server() {
if(this.serverConnection.connected())
this.serverConnection.send_command("clientupdate", {
client_input_muted: this.client_status.input_muted,
client_output_muted: this.client_status.output_muted,
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
client_input_hardware: this.client_status.sound_record_supported && this.client_status.input_hardware,
client_output_hardware: this.client_status.sound_playback_supported
}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to sync handler state with server. Error: %o"), error);
this.chat.serverChat().appendError(tr("Failed to sync handler state with server."));
});
}
set_away_status(state: boolean | string) {
if(this.client_status.away === state)
return;
this.client_status.away = state;
this.serverConnection.send_command("clientupdate", {
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
this.chat.serverChat().appendError(tr("Failed to update away status."));
});
control_bar.update_button_away();
}
resize_elements() {
this.channelTree.handle_resized();
this.select_info.handle_resize();
this.invoke_resized_on_activate = false;
}
acquire_recorder(voice_recoder: VoiceRecorder, update_control_bar: boolean) {
const vconnection = this.serverConnection.voice_connection();
if(vconnection)
vconnection.acquire_voice_recorder(voice_recoder);
if(voice_recoder)
voice_recoder.clean_recording_supported();
this.update_voice_status(undefined);
}
}

View File

@ -1,11 +1,6 @@
/// <reference path="client.ts" />
/// <reference path="connection/CommandHandler.ts" />
/// <reference path="connection/ConnectionBase.ts" />
/*
FIXME: Dont use item storage with base64! Use the larger cache API and drop IE support!
https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage#Browser_compatibility
*/
class FileEntry {
name: string;
datetime: number;
@ -218,7 +213,7 @@ class RequestFileUpload {
}
class FileManager extends connection.AbstractCommandHandler {
handle: TSClient;
handle: ConnectionHandler;
icons: IconManager;
avatars: AvatarManager;
@ -228,7 +223,7 @@ class FileManager extends connection.AbstractCommandHandler {
private transfer_counter : number = 0;
constructor(client: TSClient) {
constructor(client: ConnectionHandler) {
super(client.serverConnection);
this.handle = client;
@ -533,14 +528,17 @@ class CacheManager {
}
class IconManager {
private static cache: CacheManager;
handle: FileManager;
private cache: CacheManager;
private _id_urls: {[id:number]:string} = {};
private _loading_promises: {[id:number]:Promise<Icon>} = {};
constructor(handle: FileManager) {
this.handle = handle;
this.cache = new CacheManager("icons");
if(!IconManager.cache)
IconManager.cache = new CacheManager("icons");
}
iconList() : Promise<FileEntry[]> {
@ -572,10 +570,10 @@ class IconManager {
url: this._id_urls[id]
};
if(!this.cache.setupped())
await this.cache.setup();
if(!IconManager.cache.setupped())
await IconManager.cache.setup();
const response = await this.cache.resolve_cached('icon_' + id); //TODO age!
const response = await IconManager.cache.resolve_cached('icon_' + id); //TODO age!
if(response)
return {
id: id,
@ -606,7 +604,7 @@ class IconManager {
const type = image_type(response.headers.get('X-media-bytes'));
const media = media_image_type(type);
await this.cache.put_cache('icon_' + id, response.clone(), "image/" + media);
await IconManager.cache.put_cache('icon_' + id, response.clone(), "image/" + media);
const url = (this._id_urls[id] = await this._response_url(response.clone()));
this._loading_promises[id] = undefined;
@ -694,14 +692,15 @@ class Avatar {
class AvatarManager {
handle: FileManager;
private cache: CacheManager;
private static cache: CacheManager;
private _cached_avatars: {[response_avatar_id:number]:Avatar} = {};
private _loading_promises: {[response_avatar_id:number]:Promise<Icon>} = {};
constructor(handle: FileManager) {
this.handle = handle;
this.cache = new CacheManager("avatars");
if(!AvatarManager.cache)
AvatarManager.cache = new CacheManager("avatars");
}
private async _response_url(response: Response, type: ImageType) : Promise<string> {
@ -724,10 +723,10 @@ class AvatarManager {
this._cached_avatars[avatar_id] = (avatar = undefined);
}
if(!this.cache.setupped())
await this.cache.setup();
if(!AvatarManager.cache.setupped())
await AvatarManager.cache.setup();
const response = await this.cache.resolve_cached('avatar_' + client_avatar_id); //TODO age!
const response = await AvatarManager.cache.resolve_cached('avatar_' + client_avatar_id); //TODO age!
if(!response)
return undefined;
@ -770,7 +769,7 @@ class AvatarManager {
const type = image_type(response.headers.get('X-media-bytes'));
const media = media_image_type(type);
await this.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, {
await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, {
"X-avatar-id": avatar_id
});
const url = await this._response_url(response.clone(), type);

View File

@ -1,315 +0,0 @@
/// <reference path="log.ts" />
/// <reference path="voice/AudioController.ts" />
/// <reference path="proto.ts" />
/// <reference path="ui/view.ts" />
/// <reference path="settings.ts" />
/// <reference path="ui/frames/SelectedItemInfo.ts" />
/// <reference path="FileManager.ts" />
/// <reference path="permission/PermissionManager.ts" />
/// <reference path="permission/GroupManager.ts" />
/// <reference path="ui/frames/ControlBar.ts" />
/// <reference path="connection/ConnectionBase.ts" />
enum DisconnectReason {
REQUESTED,
CONNECT_FAILURE,
CONNECTION_CLOSED,
CONNECTION_FATAL_ERROR,
CONNECTION_PING_TIMEOUT,
CLIENT_KICKED,
CLIENT_BANNED,
HANDSHAKE_FAILED,
SERVER_CLOSED,
SERVER_REQUIRES_PASSWORD,
UNKNOWN
}
enum ConnectionState {
UNCONNECTED,
CONNECTING,
INITIALISING,
CONNECTED,
DISCONNECTING
}
enum ViewReasonId {
VREASON_USER_ACTION = 0,
VREASON_MOVED = 1,
VREASON_SYSTEM = 2,
VREASON_TIMEOUT = 3,
VREASON_CHANNEL_KICK = 4,
VREASON_SERVER_KICK = 5,
VREASON_BAN = 6,
VREASON_SERVER_STOPPED = 7,
VREASON_SERVER_LEFT = 8,
VREASON_CHANNEL_UPDATED = 9,
VREASON_EDITED = 10,
VREASON_SERVER_SHUTDOWN = 11
}
class TSClient {
channelTree: ChannelTree;
serverConnection: connection.ServerConnection;
voiceConnection: VoiceConnection | undefined;
fileManager: FileManager;
selectInfo: InfoBar;
permissions: PermissionManager;
groups: GroupManager;
controlBar: ControlBar;
private _clientId: number = 0;
private _ownEntry: LocalClientEntry;
private _reconnect_timer: NodeJS.Timer;
private _reconnect_attempt: boolean = false;
constructor() {
this.selectInfo = new InfoBar(this, $("#select_info"));
this.channelTree = new ChannelTree(this, $("#channelTree"));
this.serverConnection = new connection.ServerConnection(this);
this.fileManager = new FileManager(this);
this.permissions = new PermissionManager(this);
this.groups = new GroupManager(this);
this._ownEntry = new LocalClientEntry(this);
this.controlBar = new ControlBar(this, $("#control_bar"));
this.channelTree.registerClient(this._ownEntry);
if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false))
this.voiceConnection = new VoiceConnection(this);
}
setup() {
this.controlBar.initialise();
}
startConnection(addr: string, profile: profiles.ConnectionProfile, name?: string, password?: {password: string, hashed: boolean}) {
this.cancel_reconnect();
this._reconnect_attempt = false;
if(this.serverConnection)
this.handleDisconnect(DisconnectReason.REQUESTED);
let idx = addr.lastIndexOf(':');
let port: number;
let host: string;
if(idx != -1) {
port = parseInt(addr.substr(idx + 1));
host = addr.substr(0, idx);
} else {
host = addr;
port = 9987;
}
console.log(tr("Start connection to %s:%d"), host, port);
this.channelTree.initialiseHead(addr, {host, port});
if(password && !password.hashed) {
helpers.hashPassword(password.password).then(password => {
/* errors will be already handled via the handle disconnect thing */
this.serverConnection.connect({host, port}, new connection.HandshakeHandler(profile, name, password));
}).catch(error => {
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open();
})
} else {
/* errors will be already handled via the handle disconnect thing */
this.serverConnection.connect({host, port}, new connection.HandshakeHandler(profile, name, password ? password.password : undefined));
}
}
getClient() : LocalClientEntry { return this._ownEntry; }
getClientId() { return this._clientId; }
set clientId(id: number) {
this._clientId = id;
this._ownEntry["_clientId"] = id;
}
get clientId() {
return this._clientId;
}
getServerConnection() : connection.ServerConnection { return this.serverConnection; }
/**
* LISTENER
*/
onConnected() {
console.log("Client connected!");
this.channelTree.registerClient(this._ownEntry);
settings.setServer(this.channelTree.server);
this.permissions.requestPermissionList();
if(this.groups.serverGroups.length == 0)
this.groups.requestGroups();
this.controlBar.updateProperties();
if(this.controlBar.channel_subscribe_all)
this.channelTree.subscribe_all_channels();
else
this.channelTree.unsubscribe_all_channels();
if(this.voiceConnection && !this.voiceConnection.current_encoding_supported())
createErrorModal(tr("Codec encode type not supported!"), tr("Codec encode type " + VoiceConnectionType[this.voiceConnection.type] + " not supported by this browser!<br>Choose another one!")).open(); //TODO tr
}
get connected() : boolean {
return this.serverConnection && this.serverConnection.connected();
}
private certAcceptUrl() {
const properties = {
connect_default: true,
connect_profile: this.serverConnection._handshakeHandler.profile.id,
connect_address: this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port
};
const parameters: string[] = [];
for(const key in properties)
parameters.push(key + "=" + encodeURIComponent(properties[key]));
// document.URL
let callback = document.URL;
if(document.location.search.length == 0)
callback += "?" + parameters.join("&");
else
callback += "&" + parameters.join("&");
return "https://" + this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port + "/?forward_url=" + encodeURIComponent(callback);
}
handleDisconnect(type: DisconnectReason, data: any = {}) {
let auto_reconnect = false;
switch (type) {
case DisconnectReason.REQUESTED:
break;
case DisconnectReason.CONNECT_FAILURE:
if(this._reconnect_attempt) {
auto_reconnect = true;
chat.serverChat().appendError(tr("Connect failed"));
break;
}
console.error(tr("Could not connect to remote host! Exception: %o"), data);
if(native_client) {
createErrorModal(
tr("Could not connect"),
tr("Could not connect to remote host (Connection refused)")
).open();
} else {
//TODO tr
createErrorModal(
tr("Could not connect"),
"Could not connect to remote host (Connection refused)<br>" +
"If you're sure that the remote host is up, than you may not allow unsigned certificates.<br>" +
"Click <a href='" + this.certAcceptUrl() + "'>here</a> to accept the remote certificate"
).open();
}
sound.play(Sound.CONNECTION_REFUSED);
break;
case DisconnectReason.HANDSHAKE_FAILED:
//TODO sound
console.error(tr("Failed to process handshake: %o"), data);
createErrorModal(
tr("Could not connect"),
tr("Failed to process handshake: ") + data as string
).open();
break;
case DisconnectReason.CONNECTION_CLOSED:
console.error(tr("Lost connection to remote server!"));
createErrorModal(
tr("Connection closed"),
tr("The connection was closed by remote host")
).open();
sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = true;
break;
case DisconnectReason.CONNECTION_PING_TIMEOUT:
console.error(tr("Connection ping timeout"));
sound.play(Sound.CONNECTION_DISCONNECTED_TIMEOUT);
createErrorModal(
tr("Connection lost"),
tr("Lost connection to remote host (Ping timeout)<br>Even possible?")
).open();
break;
case DisconnectReason.SERVER_CLOSED:
chat.serverChat().appendError(tr("Server closed ({0})"), data.reasonmsg);
createErrorModal(
tr("Server closed"),
"The server is closed.<br>" + //TODO tr
"Reason: " + data.reasonmsg
).open();
sound.play(Sound.CONNECTION_DISCONNECTED);
auto_reconnect = true;
break;
case DisconnectReason.SERVER_REQUIRES_PASSWORD:
chat.serverChat().appendError(tr("Server requires password"));
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
if(!(typeof password === "string")) return;
this.startConnection(this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port,
this.serverConnection._handshakeHandler.profile,
this.serverConnection._handshakeHandler.name,
{password: password as string, hashed: false});
}).open();
break;
case DisconnectReason.CLIENT_KICKED:
chat.serverChat().appendError(tr("You got kicked from the server by {0}{1}"),
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]),
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : "");
sound.play(Sound.SERVER_KICKED);
auto_reconnect = true;
break;
case DisconnectReason.CLIENT_BANNED:
chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"),
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]),
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : "");
sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
break;
default:
console.error(tr("Got uncaught disconnect!"));
console.error(tr("Type: %o Data:"), type);
console.error(data);
break;
}
this.channelTree.reset();
if(this.voiceConnection)
this.voiceConnection.dropSession();
if(this.serverConnection) this.serverConnection.disconnect();
this.controlBar.update_connection_state();
this.selectInfo.setCurrentSelected(null);
this.selectInfo.update_banner();
if(auto_reconnect) {
if(!this.serverConnection) {
console.log(tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
return;
}
chat.serverChat().appendMessage(tr("Reconnecting in 5 seconds"));
console.log(tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
const server_address = this.serverConnection._remote_address;
const profile = this.serverConnection._handshakeHandler.profile;
const name = this.serverConnection._handshakeHandler.name;
const password = this.serverConnection._handshakeHandler.server_password;
this._reconnect_timer = setTimeout(() => {
this._reconnect_timer = undefined;
chat.serverChat().appendMessage(tr("Reconnecting..."));
log.info(LogCategory.NETWORKING, tr("Reconnecting..."))
this.startConnection(server_address.host + ":" + server_address.port, profile, name, password ? { password: password, hashed: true} : undefined);
this._reconnect_attempt = true;
}, 5000);
}
}
cancel_reconnect() {
if(this._reconnect_timer) {
chat.serverChat().appendMessage(tr("Reconnect canceled"));
clearTimeout(this._reconnect_timer);
this._reconnect_timer = undefined;
}
}
}

View File

@ -35,6 +35,7 @@ class BufferChunk {
}
class CodecClientCache {
_last_access: number;
_chunks: BufferChunk[] = [];
bufferedSamples(max: number = 0) : number {

View File

@ -173,7 +173,7 @@ class CodecWrapperWorker extends BasicCodec {
this._workerCallbackResolve = undefined;
return;
} else if(message["type"] == "chatmessage_server") {
chat.serverChat().appendMessage(message["message"]);
//FIXME?
return;
}
console.log(tr("Costume callback! (%o)"), message);

View File

@ -8,9 +8,11 @@ namespace connection {
export class ConnectionCommandHandler extends AbstractCommandHandler {
readonly connection: ServerConnection;
readonly connection_handler: ConnectionHandler;
constructor(connection: ServerConnection) {
super(connection);
this.connection_handler = connection.client;
this["error"] = this.handleCommandResult;
this["channellist"] = this.handleCommandChannelList;
@ -86,9 +88,14 @@ namespace connection {
handleCommandServerInit(json){
//We could setup the voice channel
if( this.connection.client.voiceConnection) {
if(this.connection.support_voice()) {
console.log(tr("Setting up voice"));
this.connection.client.voiceConnection.createSession();
const connection = this.connection.voice_connection();
if(connection instanceof audio.js.VoiceConnection)
connection.createSession();
else
throw "unknown voice connection";
} else {
console.log(tr("Skipping voice setup (No voice bridge available)"));
}
@ -112,10 +119,10 @@ namespace connection {
this.connection.client.channelTree.server.updateVariables(false, ...updates);
chat.serverChat().name = this.connection.client.channelTree.server.properties["virtualserver_name"];
chat.serverChat().appendMessage(tr("Connected as {0}"), true, this.connection.client.getClient().createChatTag(true));
sound.play(Sound.CONNECTION_CONNECTED);
globalClient.onConnected();
this.connection_handler.chat.serverChat().name = this.connection.client.channelTree.server.properties["virtualserver_name"];
this.connection_handler.chat.serverChat().appendMessage(tr("Connected as {0}"), true, this.connection.client.getClient().createChatTag(true));
this.connection_handler.sound.play(Sound.CONNECTION_CONNECTED);
this.connection.client.onConnected();
}
private createChannelFromJson(json, ignoreOrder: boolean = false) {
@ -233,28 +240,28 @@ namespace connection {
client = tree.insertClient(client, channel);
} else {
if(client == this.connection.client.getClient())
chat.channelChat().name = channel.channelName();
this.connection_handler.chat.channelChat().name = channel.channelName();
tree.moveClient(client, channel);
}
if(this.connection.client.controlBar.query_visible || client.properties.client_type != ClientType.CLIENT_QUERY) {
if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) {
const own_channel = this.connection.client.getClient().currentChannel();
if(json["reasonid"] == ViewReasonId.VREASON_USER_ACTION) {
if(own_channel == channel)
if(old_channel)
sound.play(Sound.USER_ENTERED);
this.connection_handler.sound.play(Sound.USER_ENTERED);
else
sound.play(Sound.USER_ENTERED_CONNECT);
this.connection_handler.sound.play(Sound.USER_ENTERED_CONNECT);
if(old_channel) {
chat.serverChat().appendMessage(tr("{0} appeared from {1} to {2}"), true, client.createChatTag(true), old_channel.generate_tag(true), channel.generate_tag(true));
this.connection_handler.chat.serverChat().appendMessage(tr("{0} appeared from {1} to {2}"), true, client.createChatTag(true), old_channel.generate_tag(true), channel.generate_tag(true));
} else {
chat.serverChat().appendMessage(tr("{0} connected to channel {1}"), true, client.createChatTag(true), channel.generate_tag(true));
this.connection_handler.chat.serverChat().appendMessage(tr("{0} connected to channel {1}"), true, client.createChatTag(true), channel.generate_tag(true));
}
} else if(json["reasonid"] == ViewReasonId.VREASON_MOVED) {
if(own_channel == channel)
sound.play(Sound.USER_ENTERED_MOVED);
this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED);
chat.serverChat().appendMessage(tr("{0} appeared from {1} to {2}, moved by {3}"), true,
this.connection_handler.chat.serverChat().appendMessage(tr("{0} appeared from {1} to {2}, moved by {3}"), true,
client.createChatTag(true),
old_channel ? old_channel.generate_tag(true) : undefined,
channel.generate_tag(true),
@ -262,9 +269,9 @@ namespace connection {
);
} else if(json["reasonid"] == ViewReasonId.VREASON_CHANNEL_KICK) {
if(own_channel == channel)
sound.play(Sound.USER_ENTERED_KICKED);
this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED);
chat.serverChat().appendMessage(tr("{0} appeared from {1} to {2}, kicked by {3}{4}"), true,
this.connection_handler.chat.serverChat().appendMessage(tr("{0} appeared from {1} to {2}, kicked by {3}{4}"), true,
client.createChatTag(true),
old_channel ? old_channel.generate_tag(true) : undefined,
channel.generate_tag(true),
@ -297,7 +304,7 @@ namespace connection {
{
let client_chat = client.chat(false);
if(!client_chat) {
for(const c of chat.open_chats()) {
for(const c of this.connection_handler.chat.open_chats()) {
if(c.owner_unique_id == client.properties.client_unique_identifier && c.flag_offline) {
client_chat = c;
break;
@ -314,8 +321,10 @@ namespace connection {
client_chat.flag_offline = false;
}
}
if(client instanceof LocalClientEntry)
this.connection.client.controlBar.updateVoice();
if(client instanceof LocalClientEntry) {
this.connection_handler.update_voice_status();
}
}
handleCommandClientLeftView(json) {
@ -340,48 +349,49 @@ namespace connection {
return;
}
if(this.connection.client.controlBar.query_visible || client.properties.client_type != ClientType.CLIENT_QUERY) {
if(this.connection_handler.client_status.queries_visible || client.properties.client_type != ClientType.CLIENT_QUERY) {
const own_channel = this.connection.client.getClient().currentChannel();
let channel_from = tree.findChannel(json["cfid"]);
let channel_to = tree.findChannel(json["ctid"]);
if(json["reasonid"] == ViewReasonId.VREASON_USER_ACTION) {
chat.serverChat().appendMessage(tr("{0} disappeared from {1} to {2}"), true, client.createChatTag(true), channel_from.generate_tag(true), channel_to.generate_tag(true));
this.connection_handler.chat.serverChat().appendMessage(tr("{0} disappeared from {1} to {2}"), true, client.createChatTag(true), channel_from.generate_tag(true), channel_to.generate_tag(true));
if(channel_from == own_channel)
sound.play(Sound.USER_LEFT);
this.connection_handler.sound.play(Sound.USER_LEFT);
} else if(json["reasonid"] == ViewReasonId.VREASON_SERVER_LEFT) {
chat.serverChat().appendMessage(tr("{0} left the server{1}"), true,
this.connection_handler.chat.serverChat().appendMessage(tr("{0} left the server{1}"), true,
client.createChatTag(true),
json["reasonmsg"] ? " (" + json["reasonmsg"] + ")" : ""
);
if(channel_from == own_channel)
sound.play(Sound.USER_LEFT_DISCONNECT);
this.connection_handler.sound.play(Sound.USER_LEFT_DISCONNECT);
} else if(json["reasonid"] == ViewReasonId.VREASON_SERVER_KICK) {
chat.serverChat().appendError(tr("{0} was kicked from the server by {1}.{2}"),
this.connection_handler.chat.serverChat().appendError(tr("{0} was kicked from the server by {1}.{2}"),
client.createChatTag(true),
ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"]),
json["reasonmsg"] ? " (" + json["reasonmsg"] + ")" : ""
);
if(channel_from == own_channel)
sound.play(Sound.USER_LEFT_KICKED_SERVER);
this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_SERVER);
} else if(json["reasonid"] == ViewReasonId.VREASON_CHANNEL_KICK) {
chat.serverChat().appendError(tr("{0} was kicked from your channel by {1}.{2}"),
this.connection_handler.chat.serverChat().appendError(tr("{0} was kicked from your channel by {1}.{2}"),
client.createChatTag(true),
ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"]),
json["reasonmsg"] ? " (" + json["reasonmsg"] + ")" : ""
);
if(channel_from == own_channel)
sound.play(Sound.USER_LEFT_KICKED_CHANNEL);
this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL);
} else if(json["reasonid"] == ViewReasonId.VREASON_BAN) {
//"Mulus" was banned for 1 second from the server by "WolverinDEV" (Sry brauchte kurz ein opfer :P <3 (Nohomo))
let duration = "permanently";
if(json["bantime"])
duration = "for " + formatDate(Number.parseInt(json["bantime"]));
chat.serverChat().appendError(tr("{0} was banned {1} by {2}.{3}"),
this.connection_handler.chat.serverChat().appendError(tr("{0} was banned {1} by {2}.{3}"),
client.createChatTag(true),
duration,
ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"]),
@ -389,7 +399,7 @@ namespace connection {
);
if(channel_from == own_channel)
sound.play(Sound.USER_LEFT_BANNED);
this.connection_handler.sound.play(Sound.USER_LEFT_BANNED);
} else {
console.error(tr("Unknown client left reason!"));
}
@ -431,44 +441,45 @@ namespace connection {
console.error(tr("Unknown client move (Channel from)!"));
let self = client instanceof LocalClientEntry;
let current_clients;
let current_clients: ClientEntry[];
if(self) {
chat.channelChat().name = channel_to.channelName();
current_clients = client.channelTree.clientsByChannel(client.currentChannel())
this.connection.client.controlBar.updateVoice(channel_to);
this.connection_handler.chat.channelChat().name = channel_to.channelName();
current_clients = client.channelTree.clientsByChannel(client.currentChannel());
this.connection_handler.update_voice_status(channel_to);
}
tree.moveClient(client, channel_to);
for(const entry of current_clients || [])
if(entry !== client) entry.getAudioController().stopAudio(true);
if(entry !== client)
entry.get_audio_handle().abort_replay();
const own_channel = this.connection.client.getClient().currentChannel();
if(json["reasonid"] == ViewReasonId.VREASON_MOVED) {
chat.serverChat().appendMessage(self ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"), true,
this.connection_handler.chat.serverChat().appendMessage(self ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"), true,
client.createChatTag(true),
channel_from ? channel_from.generate_tag(true) : undefined,
channel_to.generate_tag(true),
ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"])
);
if(self)
sound.play(Sound.USER_MOVED_SELF);
this.connection_handler.sound.play(Sound.USER_MOVED_SELF);
else if(own_channel == channel_to)
sound.play(Sound.USER_ENTERED_MOVED);
this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED);
else if(own_channel == channel_from)
sound.play(Sound.USER_LEFT_MOVED);
this.connection_handler.sound.play(Sound.USER_LEFT_MOVED);
} else if(json["reasonid"] == ViewReasonId.VREASON_USER_ACTION) {
chat.serverChat().appendMessage(self ? tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}"), true,
this.connection_handler.chat.serverChat().appendMessage(self ? tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}"), true,
client.createChatTag(true),
channel_from ? channel_from.generate_tag(true) : undefined,
channel_to.generate_tag(true)
);
if(self) {} //If we do an action we wait for the error response
else if(own_channel == channel_to)
sound.play(Sound.USER_ENTERED);
this.connection_handler.sound.play(Sound.USER_ENTERED);
else if(own_channel == channel_from)
sound.play(Sound.USER_LEFT);
this.connection_handler.sound.play(Sound.USER_LEFT);
} else if(json["reasonid"] == ViewReasonId.VREASON_CHANNEL_KICK) {
chat.serverChat().appendMessage(self ? tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}"), true,
this.connection_handler.chat.serverChat().appendMessage(self ? tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}"), true,
client.createChatTag(true),
channel_from ? channel_from.generate_tag(true) : undefined,
channel_to.generate_tag(true),
@ -476,11 +487,11 @@ namespace connection {
json["reasonmsg"] ? " (" + json["reasonmsg"] + ")" : ""
);
if(self) {
sound.play(Sound.CHANNEL_KICKED);
this.connection_handler.sound.play(Sound.CHANNEL_KICKED);
} else if(own_channel == channel_to)
sound.play(Sound.USER_ENTERED_KICKED);
this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED);
else if(own_channel == channel_from)
sound.play(Sound.USER_LEFT_KICKED_CHANNEL);
this.connection_handler.sound.play(Sound.USER_LEFT_KICKED_CHANNEL);
} else {
console.warn(tr("Unknown reason id %o"), json["reasonid"]);
}
@ -554,20 +565,20 @@ namespace connection {
return;
}
if(invoker == this.connection.client.getClient()) {
sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
target.chat(true).appendMessage("{0}: {1}", true, this.connection.client.getClient().createChatTag(true), MessageHelper.bbcode_chat(json["msg"]));
} else {
sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
invoker.chat(true).appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]));
}
} else if(mode == 2) {
if(json["invokerid"] == this.connection.client.clientId)
sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
else
sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
chat.channelChat().appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]))
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
this.connection_handler.chat.channelChat().appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]))
} else if(mode == 3) {
chat.serverChat().appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]));
this.connection_handler.chat.serverChat().appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]));
}
}
@ -621,8 +632,8 @@ namespace connection {
updates.push({key: key, value: json[key]});
}
client.updateVariables(...updates);
if(this.connection.client.selectInfo.currentSelected == client)
this.connection.client.selectInfo.update();
if(this.connection.client.select_info.currentSelected == client)
this.connection.client.select_info.update();
}
handleNotifyServerEdited(json) {
@ -641,8 +652,8 @@ namespace connection {
updates.push({key: key, value: json[key]});
}
this.connection.client.channelTree.server.updateVariables(false, ...updates);
if(this.connection.client.selectInfo.currentSelected == this.connection.client.channelTree.server)
this.connection.client.selectInfo.update();
if(this.connection.client.select_info.currentSelected == this.connection.client.channelTree.server)
this.connection.client.select_info.update();
}
handleNotifyServerUpdated(json) {
@ -661,7 +672,7 @@ namespace connection {
updates.push({key: key, value: json[key]});
}
this.connection.client.channelTree.server.updateVariables(true, ...updates);
let info = this.connection.client.selectInfo;
let info = this.connection.client.select_info;
if(info.currentSelected instanceof ServerEntry)
info.update();
}
@ -686,7 +697,7 @@ namespace connection {
unique_id: json["invokeruid"]
}, json["msg"]);
sound.play(Sound.USER_POKED_SELF);
this.connection_handler.sound.play(Sound.USER_POKED_SELF);
}
//TODO server chat message
@ -695,7 +706,7 @@ namespace connection {
const self = this.connection.client.getClient();
if(json["clid"] == self.clientId())
sound.play(Sound.GROUP_SERVER_ASSIGNED_SELF);
this.connection_handler.sound.play(Sound.GROUP_SERVER_ASSIGNED_SELF);
}
//TODO server chat message
@ -704,7 +715,7 @@ namespace connection {
const self = this.connection.client.getClient();
if(json["clid"] == self.clientId()) {
sound.play(Sound.GROUP_SERVER_REVOKED_SELF);
this.connection_handler.sound.play(Sound.GROUP_SERVER_REVOKED_SELF);
} else {
}
}
@ -715,7 +726,7 @@ namespace connection {
const self = this.connection.client.getClient();
if(json["clid"] == self.clientId()) {
sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF);
this.connection_handler.sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF);
}
}

View File

@ -11,11 +11,12 @@ namespace connection {
timeout: 1000
};
export type ConnectionStateListener = (old_state: ConnectionState, new_state: ConnectionState) => any;
export abstract class AbstractServerConnection {
readonly client: TSClient;
readonly client: ConnectionHandler;
readonly command_helper: CommandHelper;
protected constructor(client: TSClient) {
protected constructor(client: ConnectionHandler) {
this.client = client;
this.command_helper = new CommandHelper(this);
@ -32,18 +33,34 @@ namespace connection {
abstract command_handler_boss() : AbstractCommandHandlerBoss;
abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>;
abstract get onconnectionstatechanged() : ConnectionStateListener;
abstract set onconnectionstatechanged(listener: ConnectionStateListener);
}
export namespace voice {
export enum PlayerState {
PREBUFFERING,
PLAYING,
BUFFERING,
STOPPING,
STOPPED
}
export interface VoiceClient {
client_id: number;
callback_playback: () => any;
callback_timeout: () => any;
callback_stopped: () => any;
callback_state_changed: (new_state: PlayerState) => any;
get_state() : PlayerState;
get_volume() : number;
set_volume(volume: number) : Promise<void>;
set_volume(volume: number) : void;
abort_replay();
}
export abstract class AbstractVoiceConnection {
@ -54,10 +71,15 @@ namespace connection {
}
abstract connected() : boolean;
abstract encoding_supported(codec: number) : boolean;
abstract decoding_supported(codec: number) : boolean;
abstract register_client(client_id: number) : VoiceClient;
abstract availible_clients() : VoiceClient[];
abstract available_clients() : VoiceClient[];
abstract unregister_client(client: VoiceClient) : Promise<void>;
abstract voice_recorder() : VoiceRecorder;
abstract acquire_voice_recorder(recorder: VoiceRecorder | undefined);
}
}

View File

@ -71,7 +71,12 @@ namespace connection {
client_version: "TeaWeb " + git_version + " (" + navigator.userAgent + ")",
client_server_password: this.server_password,
client_browser_engine: navigator.product
client_browser_engine: navigator.product,
client_input_hardware: this.connection.client.client_status.input_hardware,
client_output_hardware: false,
client_input_muted: this.connection.client.client_status.input_muted,
client_output_muted: this.connection.client.client_status.output_muted,
};
if(version) {

View File

@ -48,7 +48,10 @@ namespace connection {
private _retCodeIdx: number;
private _retListener: ReturnListener<CommandResult>[];
constructor(client : TSClient) {
private _connection_state_listener: connection.ConnectionStateListener;
private _voice_connection: audio.js.VoiceConnection;
constructor(client : ConnectionHandler) {
super(client);
this._socket = null;
@ -60,11 +63,14 @@ namespace connection {
this._command_boss.register_handler(this._command_handler_default);
this.command_helper.initialize();
if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false))
this._voice_connection = new audio.js.VoiceConnection(this);
}
on_connect: () => void = () => {
console.log(tr("Socket connected"));
chat.serverChat().appendMessage(tr("Logging in..."));
this.client.chat.serverChat().appendMessage(tr("Logging in..."));
this._handshakeHandler.startHandshake();
};
@ -90,12 +96,12 @@ namespace connection {
this._handshakeHandler = handshake;
this._handshakeHandler.setConnection(this);
this._connected = false;
chat.serverChat().appendMessage(tr("Connecting to {0}:{1}"), true, address.host, address.port);
this.client.chat.serverChat().appendMessage(tr("Connecting to {0}:{1}"), true, address.host, address.port);
const self = this;
let local_socket: WebSocket;
let local_timeout_timer: NodeJS.Timer;
try {
let local_socket: WebSocket;
let local_timeout_timer: NodeJS.Timer;
local_timeout_timer = setTimeout(async () => {
console.log(tr("Connect timeout triggered!"));
@ -144,6 +150,7 @@ namespace connection {
};
this.updateConnectionState(ConnectionState.INITIALISING);
} catch (e) {
clearTimeout(local_timeout_timer);
try {
await this.disconnect();
} catch(error) {
@ -154,8 +161,10 @@ namespace connection {
}
updateConnectionState(state: ConnectionState) {
const old_state = this._connectionState;
this._connectionState = state;
this.client.controlBar.update_connection_state();
if(this._connection_state_listener)
this._connection_state_listener(old_state, state);
}
async disconnect(reason?: string) : Promise<void> {
@ -176,6 +185,9 @@ namespace connection {
this._retListener = [];
this._retCodeIdx = 0;
this._connected = false;
if(this._voice_connection)
this._voice_connection.dropSession();
}
private handle_socket_message(data) {
@ -203,10 +215,10 @@ namespace connection {
});
group.end();
} else if(json["type"] === "WebRTC") {
if(this.client.voiceConnection)
this.client.voiceConnection.handleControlPacket(json);
if(this._voice_connection)
this._voice_connection.handleControlPacket(json);
else
console.log(tr("Dropping WebRTC command packet, because we havent a bridge."))
console.log(tr("Dropping WebRTC command packet, because we haven't a bridge."))
}
else {
console.log(tr("Unknown command type %o"), json["type"]);
@ -280,14 +292,14 @@ namespace connection {
if(!res.success) {
if(res.id == 2568) { //Permission error
res.message = tr("Insufficient client permissions. Failed on permission ") + this.client.permissions.resolveInfo(res.json["failed_permid"] as number).name;
chat.serverChat().appendError(tr("Insufficient client permissions. Failed on permission {}"), this.client.permissions.resolveInfo(res.json["failed_permid"] as number).name);
sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
this.client.chat.serverChat().appendError(tr("Insufficient client permissions. Failed on permission {}"), this.client.permissions.resolveInfo(res.json["failed_permid"] as number).name);
this.client.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
} else {
chat.serverChat().appendError(res.extra_message.length == 0 ? res.message : res.extra_message);
this.client.chat.serverChat().appendError(res.extra_message.length == 0 ? res.message : res.extra_message);
}
}
} else if(typeof(ex) === "string") {
chat.serverChat().appendError(tr("Command execution results in ") + ex);
this.client.chat.serverChat().appendError(tr("Command execution results in ") + ex);
} else {
console.error(tr("Invalid promise result type: %o. Result:"), typeof (ex));
console.error(ex);
@ -303,16 +315,24 @@ namespace connection {
}
support_voice(): boolean {
return false;
return this._voice_connection !== undefined;
}
voice_connection(): connection.voice.AbstractVoiceConnection | undefined {
return undefined;
return this._voice_connection;
}
command_handler_boss(): connection.AbstractCommandHandlerBoss {
return this._command_boss;
}
get onconnectionstatechanged() : connection.ConnectionStateListener {
return this._connection_state_listener;
}
set onconnectionstatechanged(listener: connection.ConnectionStateListener) {
this._connection_state_listener = listener;
}
}
}

View File

@ -295,4 +295,5 @@ namespace i18n {
}
}
// @ts-ignore
const tr: typeof i18n.tr = i18n.tr;

View File

@ -7,10 +7,14 @@ namespace app {
WEB_RELEASE
}
export let type: Type = Type.UNKNOWN;
export function is_web() {
return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG;
}
}
namespace loader {
type Task = {
export type Task = {
name: string,
priority: number, /* tasks with the same priority will be executed in sync */
function: () => Promise<void>
@ -518,8 +522,6 @@ const loader_javascript = {
"js/sound/Sounds.js",
"js/utils/modal.js",
"js/utils/tab.js",
"js/utils/helpers.js",
"js/crypto/sha.js",
@ -530,6 +532,12 @@ const loader_javascript = {
"js/profiles/ConnectionProfile.js",
"js/profiles/Identity.js",
//Basic UI elements
"js/ui/elements/context_divider.js",
"js/ui/elements/context_menu.js",
"js/ui/elements/modal.js",
"js/ui/elements/tab.js",
//Load UI
"js/ui/modal/ModalAvatarList.js",
"js/ui/modal/ModalQuery.js",
@ -556,11 +564,12 @@ const loader_javascript = {
"js/ui/server.js",
"js/ui/view.js",
"js/ui/client_move.js",
"js/ui/context_divider.js",
"js/ui/htmltags.js",
"js/ui/frames/SelectedItemInfo.js",
"js/ui/frames/ControlBar.js",
"js/ui/frames/chat.js",
"js/ui/frames/connection_handlers.js",
//Load permissions
"js/permission/PermissionManager.js",
@ -570,7 +579,7 @@ const loader_javascript = {
"js/voice/VoiceHandler.js",
"js/voice/VoiceRecorder.js",
"js/voice/AudioResampler.js",
"js/voice/AudioController.js",
"js/voice/VoiceClient.js",
//Load codec
"js/codec/Codec.js",
@ -579,10 +588,9 @@ const loader_javascript = {
//Load general stuff
"js/settings.js",
"js/bookmarks.js",
"js/contextMenu.js",
"js/FileManager.js",
"js/client.js",
"js/chat.js",
"js/ConnectionHandler.js",
"js/BrowserIPC.js",
//Connection
"js/connection/CommandHandler.js",
@ -591,7 +599,6 @@ const loader_javascript = {
"js/connection/ServerConnection.js",
"js/stats.js",
"js/PPTListener.js",
@ -687,6 +694,7 @@ const loader_style = {
"css/static/frame/SelectInfo.css",
"css/static/control_bar.css",
"css/static/context_menu.css",
"css/static/connection_handlers.css",
"css/static/htmltags.css"
]);
},

View File

@ -8,6 +8,7 @@ enum LogCategory {
NETWORKING,
VOICE,
I18N,
IPC,
IDENTITIES
}
@ -30,7 +31,8 @@ namespace log {
[LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "],
[LogCategory.I18N, "I18N "],
[LogCategory.IDENTITIES, "IDENTITIES "]
[LogCategory.IDENTITIES, "IDENTITIES "],
[LogCategory.IPC, "IPC "]
]);
export let enabled_mapping = new Map<number, boolean>([
@ -43,7 +45,8 @@ namespace log {
[LogCategory.NETWORKING, true],
[LogCategory.VOICE, true],
[LogCategory.I18N, false],
[LogCategory.IDENTITIES, true]
[LogCategory.IDENTITIES, true],
[LogCategory.IPC, true]
]);
enum GroupMode {

View File

@ -1,6 +1,4 @@
/// <reference path="chat.ts" />
/// <reference path="client.ts" />
/// <reference path="utils/modal.ts" />
/// <reference path="ui/frames/chat.ts" />
/// <reference path="ui/modal/ModalConnect.ts" />
/// <reference path="ui/modal/ModalCreateChannel.ts" />
/// <reference path="ui/modal/ModalBanCreate.ts" />
@ -11,8 +9,6 @@
/// <reference path="log.ts" />
let settings: Settings;
let globalClient: TSClient;
let chat: ChatBox;
const js_render = window.jsrender || $;
const native_client = window.require !== undefined;
@ -34,7 +30,8 @@ function setup_close() {
profiles.save();
if(!settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG, false)) {
if(!globalClient.serverConnection || !globalClient.serverConnection.connected) return;
const active_connections = server_connections.server_connection_handlers().filter(e => e.connected);
if(active_connections.length == 0) return;
if(!native_client) {
event.returnValue = "Are you really sure?<br>You're still connected!";
@ -92,13 +89,6 @@ function setup_jsrender() : boolean {
}
async function initialize() {
const display_load_error = message => {
if(typeof(display_critical_load) !== "undefined")
display_critical_load(message);
else
displayCriticalError(message);
};
settings = new Settings();
try {
@ -109,6 +99,18 @@ async function initialize() {
return;
}
bipc.setup();
}
async function initialize_app() {
const display_load_error = message => {
if(typeof(display_critical_load) !== "undefined")
display_critical_load(message);
else
displayCriticalError(message);
};
try {
if(!setup_jsrender())
throw "invalid load";
@ -128,7 +130,15 @@ async function initialize() {
return;
}
AudioController.initializeAudioController();
control_bar = new ControlBar($("#control_bar")); /* setup the control bar */
if(!audio.player.initialize())
console.warn(tr("Failed to initialize audio controller!"));
sound.initialize().then(() => {
console.log(tr("Sounds initialitzed"));
});
await profiles.load();
try {
@ -230,47 +240,31 @@ function Base64DecodeUrl(str: string, pad?: boolean){
function main() {
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
voice_recoder = new VoiceRecorder();
voice_recoder.reinitialiseVAD();
globalClient = new TSClient();
server_connections = new ServerConnectionManager($("#connection-handlers"));
control_bar.initialise(); /* before connection handler to allow property apply */
const initial_handler = server_connections.spawn_server_connection_handler();
initial_handler.acquire_recorder(voice_recoder, false);
control_bar.set_connection_handler(initial_handler);
/** Setup the XF forum identity **/
profiles.identities.setup_forum();
chat = new ChatBox($("#chat"));
globalClient.setup();
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
console.log("UUID: %s", profile_uuid);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
if(profile && profile.valid()) {
globalClient.startConnection(address, profile, username, password.length > 0 ? {
password: password,
hashed: password_hashed
} : undefined);
} else {
Modals.spawnConnectModal({
url: address,
enforce: true
}, {
profile: profile,
enforce: true
});
}
}
let _resize_timeout: NodeJS.Timer;
$(window).on('resize', () => {
$(window).on('resize', event => {
if(event.target !== window)
return;
if(_resize_timeout)
clearTimeout(_resize_timeout);
_resize_timeout = setTimeout(() => {
globalClient.channelTree.handle_resized();
globalClient.selectInfo.handle_resize();
for(const connection of server_connections.server_connection_handlers())
connection.invoke_resized_on_activate = true;
const active_connection = server_connections.active_connection_handler();
if(active_connection)
active_connection.resize_elements();
}, 1000);
});
@ -288,22 +282,24 @@ function main() {
Modals.spawnAvatarList(globalClient);
}, 1000);
*/
(<any>window).test_upload = () => {
const data = "Hello World";
globalClient.fileManager.upload_file({
size: data.length,
(<any>window).test_upload = (message?: string) => {
message = message || "Hello World";
const connection = server_connections.active_connection_handler();
connection.fileManager.upload_file({
size: message.length,
overwrite: true,
channel: globalClient.getClient().currentChannel(),
channel: connection.getClient().currentChannel(),
name: '/HelloWorld.txt',
path: ''
}).then(key => {
console.log("Got key: %o", key);
const upload = new RequestFileUpload(key);
const buffer = new Uint8Array(data.length);
const buffer = new Uint8Array(message.length);
{
for(let index = 0; index < data.length; index++)
buffer[index] = data.charCodeAt(index);
for(let index = 0; index < message.length; index++)
buffer[index] = message.charCodeAt(index);
}
upload.put_data(buffer).catch(error => {
@ -311,13 +307,42 @@ function main() {
});
})
};
server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]);
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
console.log("UUID: %s", profile_uuid);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
if(profile && profile.valid()) {
const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler();
connection.startConnection(address, profile, username, password.length > 0 ? {
password: password,
hashed: password_hashed
} : undefined);
} else {
Modals.spawnConnectModal({
url: address,
enforce: true
}, {
profile: profile,
enforce: true
});
}
}
}
loader.register_task(loader.Stage.LOADED, {
name: "async main invoke",
const task_teaweb_starter: loader.Task = {
name: "voice app starter",
function: async () => {
try {
await initialize();
await initialize_app();
main();
if(!audio.player.initialized()) {
log.info(LogCategory.VOICE, tr("Initialize audio controller later!"));
@ -334,5 +359,77 @@ loader.register_task(loader.Stage.LOADED, {
}
},
priority: 10
};
const task_certificate_callback: loader.Task = {
name: "certificate accept tester",
function: async () => {
const certificate_accept = settings.static_global(Settings.KEY_CERTIFICATE_CALLBACK, undefined);
if(certificate_accept) {
log.info(LogCategory.IPC, tr("Using this instance as certificate callback. ID: %s"), certificate_accept);
try {
try {
await bipc.get_handler().post_certificate_accpected(certificate_accept);
} catch(e) {} //FIXME remove!
log.info(LogCategory.IPC, tr("Other instance has acknowledged out work. Closing this window."));
const seconds_tag = $.spawn("a");
let seconds = 5;
let interval_id;
interval_id = setInterval(() => {
seconds--;
seconds_tag.text(seconds.toString());
if(seconds <= 0) {
clearTimeout(interval_id);
log.info(LogCategory.GENERAL, tr("Closing window"));
window.close();
return;
}
}, 1000);
const message =
"You've successfully accepted the certificate.{:br:}" +
"This page will close in {0} seconds.";
createInfoModal(
tr("Certificate acccepted successfully"),
MessageHelper.formatMessage(tr(message), seconds_tag),
{
closeable: false,
footer: undefined
}
).open();
return;
} catch(error) {
log.warn(LogCategory.IPC, tr("Failed to successfully post certificate accept status: %o"), error);
}
} else {
log.info(LogCategory.IPC, tr("We're not used to accept certificated. Booting app."));
}
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
},
priority: 10
};
loader.register_task(loader.Stage.LOADED, {
name: "app starter",
function: async () => {
try {
await initialize();
if(app.is_web()) {
loader.register_task(loader.Stage.LOADED, task_certificate_callback);
} else
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
} catch (ex) {
console.error(ex.stack);
if(ex instanceof ReferenceError || ex instanceof TypeError)
ex = ex.name + ": " + ex.message;
displayCriticalError("Failed to boot app function:<br>" + ex);
}
},
priority: 10
});

View File

@ -60,13 +60,13 @@ class Group {
}
class GroupManager extends connection.AbstractCommandHandler {
readonly handle: TSClient;
readonly handle: ConnectionHandler;
serverGroups: Group[] = [];
channelGroups: Group[] = [];
private requests_group_permissions: GroupPermissionRequest[] = [];
constructor(client: TSClient) {
constructor(client: ConnectionHandler) {
super(client.serverConnection);
client.serverConnection.command_handler_boss().register_handler(this);

View File

@ -1,4 +1,4 @@
/// <reference path="../client.ts" />
/// <reference path="../ConnectionHandler.ts" />
/// <reference path="../connection/ConnectionBase.ts" />
enum PermissionType {
@ -418,7 +418,7 @@ class TeaPermissionRequest {
}
class PermissionManager extends connection.AbstractCommandHandler {
readonly handle: TSClient;
readonly handle: ConnectionHandler;
permissionList: PermissionInfo[] = [];
permissionGroups: PermissionGroup[] = [];
@ -505,7 +505,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
return permissions;
}
constructor(client: TSClient) {
constructor(client: ConnectionHandler) {
super(client.serverConnection);
//FIXME? Dont register the handler like this?

View File

@ -10,10 +10,11 @@ interface JSON {
map_field_to<T>(object: T, value: any, field: string) : T;
}
type JQueryScrollType = "height" | "width";
interface JQuery<TElement = HTMLElement> {
render(values?: any) : string;
renderTag(values?: any) : JQuery<TElement>;
hasScrollBar() : boolean;
hasScrollBar(direction?: JQueryScrollType) : boolean;
visible_height() : number;
@ -137,10 +138,21 @@ if(typeof ($) !== "undefined") {
}
}
if(!$.fn.hasScrollBar)
$.fn.hasScrollBar = function() {
if(this.length <= 0) return false;
return this.get(0).scrollHeight > this.height();
}
$.fn.hasScrollBar = function(direction?: "height" | "width") {
if(this.length <= 0)
return false;
const scroll_height = this.get(0).scrollHeight > this.height();
const scroll_width = this.get(0).scrollWidth > this.width();
if(typeof(direction) === "string") {
if(direction === "height")
return scroll_height;
if(direction === "width")
return scroll_width;
}
return scroll_width || scroll_height;
};
if(!$.fn.visible_height)
$.fn.visible_height = function (this: JQuery<HTMLElement>) {

View File

@ -1,5 +1,3 @@
/// <reference path="client.ts" />
if(typeof(customElements) !== "undefined") {
try {
class X_Properties extends HTMLElement {}
@ -19,15 +17,11 @@ interface SettingsKey<T> {
fallback_keys?: string | string[];
fallback_imports?: {[key: string]:(value: string) => T};
description?: string;
default_value?: T;
}
class StaticSettings {
private static _instance: StaticSettings;
static get instance() : StaticSettings {
if(!this._instance)
this._instance = new StaticSettings(true);
return this._instance;
}
class SettingsBase {
protected static readonly UPDATE_DIRECT: boolean = true;
protected static transformStO?<T>(input?: string, _default?: T, default_type?: string) : T {
default_type = default_type || typeof _default;
@ -66,7 +60,7 @@ class StaticSettings {
if(typeof(value) !== 'string')
return _default;
return StaticSettings.transformStO(value as string, _default, default_type);
return SettingsBase.transformStO(value as string, _default, default_type);
}
protected static keyify<T>(key: string | SettingsKey<T>) : SettingsKey<T> {
@ -76,11 +70,21 @@ class StaticSettings {
return key;
throw "key is not a key";
}
}
class StaticSettings extends SettingsBase {
private static _instance: StaticSettings;
static get instance() : StaticSettings {
if(!this._instance)
this._instance = new StaticSettings(true);
return this._instance;
}
protected _handle: StaticSettings;
protected _staticPropsTag: JQuery;
protected constructor(_reserved = undefined) {
super();
if(_reserved && !StaticSettings._instance) {
this._staticPropsTag = $("#properties");
this.initializeStatic();
@ -90,7 +94,14 @@ class StaticSettings {
}
private initializeStatic() {
location.search.substr(1).split("&").forEach(part => {
let search;
if(window.opener && window.opener !== window) {
search = new URL(window.location.href).search;
} else {
search = location.search;
}
search.substr(1).split("&").forEach(part => {
let item = part.split("=");
$("<x-property></x-property>")
.attr("key", item[0])
@ -136,6 +147,9 @@ class Settings extends StaticSettings {
key: 'disableVoice',
description: 'Disables the voice bridge. If disabled, the audio and codec workers aren\'t required anymore'
};
static readonly KEY_DISABLE_MULTI_SESSION: SettingsKey<boolean> = {
key: 'disableMultiSession',
};
static readonly KEY_LOAD_DUMMY_ERROR: SettingsKey<boolean> = {
key: 'dummy_load_error',
@ -176,6 +190,10 @@ class Settings extends StaticSettings {
key: 'connect_password_hashed'
};
static readonly KEY_CERTIFICATE_CALLBACK: SettingsKey<string> = {
key: 'certificate_callback'
};
static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel: ChannelEntry) => SettingsKey<ChannelSubscribeMode> = channel => {
return {
key: 'channel_subscribe_mode_' + channel.getChannelId()
@ -197,10 +215,7 @@ class Settings extends StaticSettings {
return result;
})();
private static readonly UPDATE_DIRECT: boolean = true;
private cacheGlobal = {};
private cacheServer = {};
private currentServer: ServerEntry;
private saveWorker: NodeJS.Timer;
private updated: boolean = false;
@ -225,10 +240,6 @@ class Settings extends StaticSettings {
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheGlobal[key]);
}
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
}
changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
key = Settings.keyify(key);
@ -242,12 +253,37 @@ class Settings extends StaticSettings {
this.save();
}
save() {
this.updated = false;
let global = JSON.stringify(this.cacheGlobal);
localStorage.setItem("settings.global", global);
}
}
class ServerSettings extends SettingsBase {
private cacheServer = {};
private currentServer: ServerEntry;
private _server_save_worker: NodeJS.Timer;
private _server_settings_updated: boolean = false;
constructor() {
super();
this._server_save_worker = setInterval(() => {
if(this._server_settings_updated)
this.save();
}, 5 * 1000);
}
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
}
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
key = Settings.keyify(key);
if(this.cacheServer[key.key] == value) return;
this.updated = true;
this._server_settings_updated = true;
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
if(Settings.UPDATE_DIRECT)
@ -271,15 +307,12 @@ class Settings extends StaticSettings {
}
save() {
this.updated = false;
this._server_settings_updated = false;
if(this.currentServer) {
let serverId = this.currentServer.properties.virtualserver_unique_identifier;
let server = JSON.stringify(this.cacheServer);
localStorage.setItem("settings.server_" + serverId, server);
}
let global = JSON.stringify(this.cacheGlobal);
localStorage.setItem("settings.global", global);
}
}

View File

@ -55,7 +55,7 @@ enum Sound {
}
namespace sound {
interface SpeechFile {
export interface SoundHandle {
key: string;
filename: string;
@ -68,7 +68,7 @@ namespace sound {
}
let warned = false;
let speech_mapping: {[key: string]:SpeechFile} = {};
let speech_mapping: {[key: string]:SoundHandle} = {};
let volume_require_save = false;
let speech_volume: {[key: string]:number} = {};
@ -80,7 +80,7 @@ namespace sound {
let master_mixed: GainNode;
function register_sound(key: string, file: string) {
speech_mapping[key] = {key: key, filename: file} as SpeechFile;
speech_mapping[key] = {key: key, filename: file} as SoundHandle;
}
export function get_sound_volume(sound: Sound, default_volume?: number) : number {
@ -192,6 +192,7 @@ namespace sound {
register_sound("message.received", "effects/message_received.wav");
register_sound("message.send", "effects/message_send.wav");
manager = new SoundManager(undefined);
audio.player.on_ready(reinitialisize_audio);
return new Promise<void>(resolve => {
$.ajax({
@ -211,7 +212,7 @@ namespace sound {
async: true,
type: 'GET'
});
})
});
}
export interface PlaybackOptions {
@ -221,84 +222,44 @@ namespace sound {
default_volume?: number;
}
export function play(sound: Sound, options?: PlaybackOptions) {
if(!options) {
options = {};
}
export async function resolve_sound(sound: Sound) : Promise<SoundHandle> {
const file: SoundHandle = speech_mapping[sound];
if(!file)
throw tr("Missing sound handle");
const file: SpeechFile = speech_mapping[sound];
if(!file) {
console.warn(tr("Missing sound %o"), sound);
return;
}
if(file.not_supported) {
if(!file.not_supported_timeout || Date.now() < file.not_supported_timeout) //Test if the not supported isn't may timeouted
return;
if(!file.not_supported_timeout || Date.now() < file.not_supported_timeout) //Test if the not supported flag has been expired
return file;
file.not_supported = false;
file.not_supported_timeout = undefined;
}
const path = "audio/" + file.filename;
const context = audio.player.context();
if(!context) {
console.warn(tr("Tried to replay a sound without an audio context (Sound: %o). Dropping playback"), sound);
return;
}
const volume = get_sound_volume(sound, options.default_volume);
console.log(tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), sound, volume, master_volume);
if(volume == 0) return;
if(master_volume == 0) return;
if(!options.ignore_muted && !ignore_muted && globalClient.controlBar.muteOutput) return;
if(!context)
return file;
const path = "audio/" + file.filename;
if(context.decodeAudioData) {
if(file.cached) {
if(!options.ignore_overlap && file.replaying && !overlap_sounds) {
console.log(tr("Dropping requested playback for sound %s because it would overlap."), sound);
return;
}
console.log(tr("Using cached buffer: %o"), file.cached);
const player = context.createBufferSource();
player.buffer = file.cached;
player.start(0);
file.replaying = true;
player.onended = event => {
file.replaying = false;
};
if(volume != 1 && context.createGain) {
const gain = context.createGain();
if(gain.gain.setValueAtTime)
gain.gain.setValueAtTime(volume, 0);
else
gain.gain.value = volume;
player.connect(gain);
gain.connect(master_mixed);
} else {
player.connect(master_mixed);
}
} else {
if(!file.cached) {
const decode_data = buffer => {
console.log(buffer);
try {
console.log(tr("Decoding data"));
context.decodeAudioData(buffer, result => {
log.info(LogCategory.VOICE, tr("Got decoded data"));
file.cached = result;
play(sound, options);
}, error => {
console.error(tr("Failed to decode audio data for %o"), sound);
console.error(error);
file.not_supported = true;
file.not_supported_timeout = Date.now() + 1000 * 60 * 60; //Try in 2min again!
file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again!
})
} catch (error) {
console.error(error);
file.not_supported = true;
file.not_supported_timeout = Date.now() + 1000 * 60 * 60; //Try in 2min again!
file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again!
}
};
@ -306,31 +267,33 @@ namespace sound {
xhr.open('GET', path, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
if (this.status == 200) {
decode_data(this.response);
} else {
console.error(tr("Failed to load audio file. (Response code %o)"), this.status);
file.not_supported = true;
file.not_supported_timeout = Date.now() + 1000 * 60 * 60; //Try in 2min again!
try {
const result = new Promise((resolve, reject) => {
xhr.onload = resolve;
xhr.onerror = reject;
});
xhr.send();
await result;
if (xhr.status != 200)
throw "invalid response code (" + xhr.status + ")";
console.log(tr("Decoding data"));
try {
file.cached = await context.decodeAudioData(xhr.response);
} catch(error) {
console.error(error);
throw "failed to decode audio data";
}
};
xhr.onerror = error => {
console.error(tr("Failed to load audio file "), sound);
console.error(error);
} catch(error) {
console.error(tr("Failed to load audio file %s. Error: %o"), sound, error);
file.not_supported = true;
file.not_supported_timeout = Date.now() + 1000 * 60 * 60; //Try in 2min again!
};
xhr.send();
file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again!
}
}
} else {
console.log(tr("Replaying %s"), path);
if(file.node) {
file.node.currentTime = 0;
file.node.play();
} else {
if(!file.node) {
if(!warned) {
warned = true;
console.warn(tr("Your browser does not support decodeAudioData! Using a node to playback! This bypasses the audio output and volume regulation!"));
@ -340,8 +303,81 @@ namespace sound {
node.appendTo(container);
file.node = node[0];
file.node.play();
}
}
return file;
}
export let manager: SoundManager;
export class SoundManager {
private _handle: ConnectionHandler;
private _playing_sounds: {[key: string]:number} = {};
constructor(handle: ConnectionHandler) {
this._handle = handle;
}
play(_sound: Sound, options?: PlaybackOptions) {
options = options || {};
const volume = get_sound_volume(_sound, options.default_volume);
console.log(tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume);
if(volume == 0 || master_volume == 0)
return;
if(this._handle && !options.ignore_muted && !sound.ignore_output_muted() && this._handle.client_status.output_muted)
return;
const context = audio.player.context();
if(!context) {
console.warn(tr("Tried to replay a sound without an audio context (Sound: %o). Dropping playback"), _sound);
return;
}
sound.resolve_sound(_sound).then(handle => {
if(!handle)
return;
if(!options.ignore_overlap && (this._playing_sounds[_sound] > 0) && !sound.overlap_activated()) {
console.log(tr("Dropping requested playback for sound %s because it would overlap."), _sound);
return;
}
if(handle.cached) {
this._playing_sounds[_sound] = Date.now();
const player = context.createBufferSource();
player.buffer = handle.cached;
player.start(0);
handle.replaying = true;
player.onended = event => {
delete this._playing_sounds[_sound];
};
if(volume != 1 && context.createGain) {
const gain = context.createGain();
if(gain.gain.setValueAtTime)
gain.gain.setValueAtTime(volume, 0);
else
gain.gain.value = volume;
player.connect(gain);
gain.connect(master_mixed);
} else {
player.connect(master_mixed);
}
} else if(handle.node) {
handle.node.currentTime = 0;
handle.node.play();
} else {
console.warn(tr("Failed to replay sound because of missing handles."), sound);
return;
}
});
}
}
}

View File

@ -457,13 +457,13 @@ class ChannelEntry {
name: tr("Show channel info"),
callback: () => {
trigger_close = false;
this.channelTree.client.selectInfo.open_popover()
this.channelTree.client.select_info.open_popover()
},
icon: "client-about",
visible: this.channelTree.client.selectInfo.is_popover()
visible: this.channelTree.client.select_info.is_popover()
}, {
type: MenuEntryType.HR,
visible: this.channelTree.client.selectInfo.is_popover(),
visible: this.channelTree.client.select_info.is_popover(),
name: ''
}, {
type: MenuEntryType.ENTRY,
@ -500,7 +500,7 @@ class ChannelEntry {
name: tr("Edit channel"),
invalidPermission: !channelModify,
callback: () => {
Modals.createChannelModal(this, undefined, this.channelTree.client.permissions, (changes?, permissions?) => {
Modals.createChannelModal(this.channelTree.client, this, undefined, this.channelTree.client.permissions, (changes?, permissions?) => {
if(changes) {
changes["cid"] = this.channelId;
this.channelTree.client.serverConnection.send_command("channeledit", changes);
@ -522,7 +522,7 @@ class ChannelEntry {
this.channelTree.client.serverConnection.send_command("channeladdperm", perms, {
flagset: ["continueonerror"]
}).then(() => {
sound.play(Sound.CHANNEL_EDITED_SELF);
this.channelTree.client.sound.play(Sound.CHANNEL_EDITED_SELF);
});
}
});
@ -535,7 +535,7 @@ class ChannelEntry {
invalidPermission: !flagDelete,
callback: () => {
this.channelTree.client.serverConnection.send_command("channeldelete", {cid: this.channelId}).then(() => {
sound.play(Sound.CHANNEL_DELETED);
this.channelTree.client.sound.play(Sound.CHANNEL_DELETED);
})
}
},
@ -694,8 +694,8 @@ class ChannelEntry {
} else if(key == "channel_codec") {
(this.properties.channel_codec == 5 || this.properties.channel_codec == 3 ? $.fn.show : $.fn.hide).apply(this.channelTag().find(".icons .icon_music"));
this.channelTag().find(".icons .icon_no_sound").toggle(!(
this.channelTree.client.voiceConnection &&
this.channelTree.client.voiceConnection.codecSupported(this.properties.channel_codec)
this.channelTree.client.serverConnection.support_voice() &&
this.channelTree.client.serverConnection.voice_connection().decoding_supported(this.properties.channel_codec)
));
} else if(key == "channel_flag_default") {
(this.properties.channel_flag_default ? $.fn.show : $.fn.hide).apply(this.channelTag().find(".icons .icon_default"));
@ -771,7 +771,7 @@ class ChannelEntry {
}).open();
} else if(this.channelTree.client.getClient().currentChannel() != this)
this.channelTree.client.getServerConnection().command_helper.joinChannel(this, this._cachedPassword).then(() => {
sound.play(Sound.CHANNEL_JOINED);
this.channelTree.client.sound.play(Sound.CHANNEL_JOINED);
}).catch(error => {
if(error instanceof CommandResult) {
if(error.id == 781) { //Invalid password
@ -803,7 +803,7 @@ class ChannelEntry {
if(inherited_subscription_mode) {
this.subscribe_mode = ChannelSubscribeMode.INHERITED;
unsubscribe = this.flag_subscribed && !this.channelTree.client.controlBar.channel_subscribe_all;
unsubscribe = this.flag_subscribed && !this.channelTree.client.client_status.channel_subscribe_all;
} else {
this.subscribe_mode = ChannelSubscribeMode.UNSUBSCRIBED;
unsubscribe = this.flag_subscribed;
@ -831,7 +831,7 @@ class ChannelEntry {
}
get subscribe_mode() : ChannelSubscribeMode {
return typeof(this._subscribe_mode) !== 'undefined' ? this._subscribe_mode : (this._subscribe_mode = settings.server(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), ChannelSubscribeMode.INHERITED));
return typeof(this._subscribe_mode) !== 'undefined' ? this._subscribe_mode : (this._subscribe_mode = this.channelTree.client.settings.server(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), ChannelSubscribeMode.INHERITED));
}
set subscribe_mode(mode: ChannelSubscribeMode) {
@ -839,21 +839,6 @@ class ChannelEntry {
return;
this._subscribe_mode = mode;
settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode);
this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode);
}
}
//Global functions
function chat_channel_contextmenu(_element: any, event: any) {
event.preventDefault();
let element = $(_element);
let chid : number = Number.parseInt(element.attr("channelId"));
let channel = globalClient.channelTree.findChannel(chid);
if(!channel) {
//TODO
return;
}
channel.showContextMenu(event.pageX, event.pageY);
}

View File

@ -54,28 +54,36 @@ class ClientEntry {
protected _properties: ClientProperties;
protected lastVariableUpdate: number = 0;
protected _speaking: boolean = false;
private _listener_initialized: boolean;
protected _listener_initialized: boolean;
protected _audio_handle: connection.voice.VoiceClient;
channelTree: ChannelTree;
audioController: AudioController;
constructor(clientId, clientName, properties: ClientProperties = new ClientProperties()) {
constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) {
this._properties = properties;
this._properties.client_nickname = clientName;
this._clientId = clientId;
this.channelTree = null;
this._channel = null;
this.audioController = new AudioController();
}
const _this = this;
this.audioController.onSpeaking = function () {
_this.speaking = true;
};
set_audio_handle(handle: connection.voice.VoiceClient) {
if(this._audio_handle === handle)
return;
this.audioController.onSilence = function () {
_this.speaking = false;
};
this.audioController.initialize();
//TODO may ensure that the id is the same?
this._audio_handle = handle;
if(!handle) {
this.speaking = false;
return;
}
handle.callback_playback = () => this.speaking = true;
handle.callback_stopped = () => this.speaking = false;
}
get_audio_handle() : connection.voice.VoiceClient {
return this._audio_handle;
}
get properties() : ClientProperties {
@ -87,10 +95,6 @@ class ClientEntry {
clientUid(){ return this.properties.client_unique_identifier; }
clientId(){ return this._clientId; }
getAudioController() : AudioController {
return this.audioController;
}
protected initializeListener(){
if(this._listener_initialized) return;
this._listener_initialized = true;
@ -100,8 +104,9 @@ class ClientEntry {
this.channelTree.onSelect(this);
}
});
this.tag.click(event => {
console.log("Clicked!");
this.tag.on('click', event => {
console.log("I've been clicked!");
});
if(!(this instanceof LocalClientEntry) && !(this instanceof MusicClientEntry))
@ -126,7 +131,7 @@ class ClientEntry {
});
}
this.tag.mousedown(event => {
this.tag.on('mousedown', event => {
if(event.which != 1) return; //Only the left button
let clients = this.channelTree.currently_selected as (ClientEntry | ClientEntry[]);
@ -151,9 +156,9 @@ class ClientEntry {
cid: target.getChannelId()
}).then(event => {
if(client.clientId() == this.channelTree.client.clientId)
sound.play(Sound.CHANNEL_JOINED);
this.channelTree.client.sound.play(Sound.CHANNEL_JOINED);
else if(target !== source && target != self.currentChannel())
sound.play(Sound.USER_MOVED);
this.channelTree.client.sound.play(Sound.USER_MOVED);
});
}
@ -273,21 +278,21 @@ class ClientEntry {
name: tr("Show client info"),
callback: () => {
trigger_close = false;
this.channelTree.client.selectInfo.open_popover()
this.channelTree.client.select_info.open_popover()
},
icon: "client-about",
visible: this.channelTree.client.selectInfo.is_popover()
visible: this.channelTree.client.select_info.is_popover()
}, {
type: MenuEntryType.HR,
visible: this.channelTree.client.selectInfo.is_popover(),
visible: this.channelTree.client.select_info.is_popover(),
name: ''
}, {
type: MenuEntryType.ENTRY,
icon: "client-change_nickname",
name: tr("<b>Open text chat</b>"),
callback: () => {
chat.activeChat = this.chat(true);
chat.focus();
this.channelTree.client.chat.activeChat = this.chat(true);
this.channelTree.client.chat.focus();
}
}, {
type: MenuEntryType.ENTRY,
@ -386,7 +391,7 @@ class ClientEntry {
}, {
flagset: [data.no_ip ? "no-ip" : "", data.no_hwid ? "no-hardware-id" : "", data.no_name ? "no-nickname" : ""]
}).then(() => {
sound.play(Sound.USER_BANNED);
this.channelTree.client.sound.play(Sound.USER_BANNED);
});
});
}
@ -415,11 +420,11 @@ class ClientEntry {
icon: "client-volume",
name: tr("Change Volume"),
callback: () => {
Modals.spawnChangeVolume(this.audioController.volume, volume => {
settings.changeServer("volume_client_" + this.clientUid(), volume);
this.audioController.volume = volume;
if(globalClient.selectInfo.currentSelected == this)
globalClient.selectInfo.update();
Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => {
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume);
this._audio_handle.set_volume(volume);
if(this.channelTree.client.select_info.currentSelected == this)
this.channelTree.client.select_info.update();
});
}
},
@ -592,7 +597,7 @@ class ClientEntry {
log.table("Client update properties", entries);
}
for(let variable of variables) {
for(const variable of variables) {
JSON.map_field_to(this._properties, variable.value, variable.key);
if(variable.key == "client_nickname") {
@ -602,17 +607,26 @@ class ClientEntry {
reorder_channel = true;
}
if(variable.key == "client_away" || variable.key == "client_output_muted" || variable.key == "client_input_hardware" || variable.key == "client_input_muted" || variable.key == "client_is_channel_commander"){
if(
variable.key == "client_away" ||
variable.key == "client_input_hardware" ||
variable.key == "client_output_hardware" ||
variable.key == "client_output_muted" ||
variable.key == "client_input_muted" ||
variable.key == "client_is_channel_commander"){
update_icon_speech = true;
}
if(variable.key == "client_away_message" || variable.key == "client_away") {
update_away = true;
}
if(variable.key == "client_unique_identifier") {
this.audioController.volume = parseFloat(settings.server("volume_client_" + this.clientUid(), "1"));
//TODO tr
console.error("Updated volume from config " + this.audioController.volume + " - " + "volume_client_" + this.clientUid() + " - " + settings.server("volume_client_" + this.clientUid(), "1"));
console.log(this.avatarId());
if(this._audio_handle) {
const volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1"));
this._audio_handle.set_volume(volume);
log.debug(LogCategory.CLIENT, tr("Loaded client volume %d for client %s from config."), volume, this.clientUid());
} else {
log.warn(LogCategory.CLIENT, tr("Visible client got unique id assigned, but hasn't yet an audio handle. Ignoring volume assignment."));
}
}
if(variable.key == "client_talk_power") {
reorder_channel = true;
@ -681,26 +695,26 @@ class ClientEntry {
chat(create: boolean = false) : ChatEntry {
let chatName = "client_" + this.clientUid() + ":" + this.clientId();
let c = chat.findChat(chatName);
if(!c && create) {
c = chat.createChat(chatName);
c.flag_closeable = true;
c.name = this.clientNickName();
c.owner_unique_id = this.properties.client_unique_identifier;
let chat = this.channelTree.client.chat.findChat(chatName);
if(!chat && create) {
chat = this.channelTree.client.chat.createChat(chatName);
chat.flag_closeable = true;
chat.name = this.clientNickName();
chat.owner_unique_id = this.properties.client_unique_identifier;
c.onMessageSend = text => {
chat.onMessageSend = text => {
this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, this);
};
c.onClose = () => {
if(!c.flag_offline)
chat.onClose = () => {
if(!chat.flag_offline)
this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": this.clientId()}, {process_result: false}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to notify chat participant (%o) that the chat has been closed. Error: %o"), this, error);
});
return true;
}
}
return c;
return chat;
}
updateClientIcon() {
@ -746,10 +760,7 @@ class ClientEntry {
} else return group.id == this.assignedChannelGroup();
}
onDelete() {
this.audioController.close();
this.audioController = undefined;
}
onDelete() { }
calculateOnlineTime() : number {
return Date.now() / 1000 - this.properties.client_lastconnected;
@ -796,11 +807,11 @@ class ClientEntry {
}
class LocalClientEntry extends ClientEntry {
handle: TSClient;
handle: ConnectionHandler;
private renaming: boolean;
constructor(handle: TSClient) {
constructor(handle: ConnectionHandler) {
super(0, "local client");
this.handle = handle;
}
@ -866,7 +877,7 @@ class LocalClientEntry extends ClientEntry {
}
});
elm.focusout(function (e) {
elm.focusout(e => {
if(!_self.renaming) return;
_self.renaming = false;
@ -878,9 +889,9 @@ class LocalClientEntry extends ClientEntry {
elm.text(_self.clientNickName());
_self.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => {
chat.serverChat().appendMessage(tr("Nickname successfully changed"));
this.channelTree.client.chat.serverChat().appendMessage(tr("Nickname successfully changed"));
}).catch((e: CommandResult) => {
chat.serverChat().appendError(tr("Could not change nickname ({})"), e.extra_message);
this.channelTree.client.chat.serverChat().appendError(tr("Could not change nickname ({})"), e.extra_message);
_self.openRename();
});
});
@ -938,13 +949,13 @@ class MusicClientEntry extends ClientEntry {
name: tr("Show bot info"),
callback: () => {
trigger_close = false;
this.channelTree.client.selectInfo.open_popover()
this.channelTree.client.select_info.open_popover()
},
icon: "client-about",
visible: this.channelTree.client.selectInfo.is_popover()
visible: this.channelTree.client.select_info.is_popover()
}, {
type: MenuEntryType.HR,
visible: this.channelTree.client.selectInfo.is_popover(),
visible: this.channelTree.client.select_info.is_popover(),
name: ''
}, {
name: tr("<b>Change bot name</b>"),
@ -1065,11 +1076,11 @@ class MusicClientEntry extends ClientEntry {
icon: "client-volume",
name: tr("Change local volume"),
callback: () => {
Modals.spawnChangeVolume(this.audioController.volume, volume => {
settings.changeServer("volume_client_" + this.clientUid(), volume);
this.audioController.volume = volume;
if(globalClient.selectInfo.currentSelected == this)
(<MusicInfoManager>globalClient.selectInfo.current_manager()).update_local_volume(volume);
Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => {
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume);
this._audio_handle.set_volume(volume);
if(this.channelTree.client.select_info.currentSelected == this)
(<MusicInfoManager>this.channelTree.client.select_info.current_manager()).update_local_volume(volume);
});
}
},
@ -1087,8 +1098,8 @@ class MusicClientEntry extends ClientEntry {
clid: this.clientId(),
player_volume: value,
}).then(() => {
if(globalClient.selectInfo.currentSelected == this)
(<MusicInfoManager>globalClient.selectInfo.current_manager()).update_remote_volume(value);
if(this.channelTree.client.select_info.currentSelected == this)
(<MusicInfoManager>this.channelTree.client.select_info.current_manager()).update_remote_volume(value);
});
});
}

View File

@ -79,6 +79,9 @@ if(!$.fn.dividerfy) {
$(document.documentElement).css("user-select", "");
element.removeClass("seperator-selected");
next_element.find("[x-divider-require-resize]").trigger('resize');
previous_element.find("[x-divider-require-resize]").trigger('resize');
};
element.on('mousedown', () => {

View File

@ -1,12 +1,13 @@
let context_menu: JQuery;
$(document).bind("mousedown", function (e) {
$(document).bind("click", function (e) {
let menu = context_menu || (context_menu = $(".context-menu"));
if(!menu.is(":visible")) return;
if ($(e.target).parents(".context-menu").length == 0) {
despawn_context_menu();
e.preventDefault();
}
});

View File

@ -1,4 +1,4 @@
/// <reference path="../i18n/localize.ts" />
/// <reference path="../../i18n/localize.ts" />
interface JQuery<TElement = HTMLElement> {
asTabWidget(copy?: boolean) : JQuery<TElement>;
@ -110,7 +110,7 @@ var TabFunctions = {
tag.on('tab.resize', update_height);
return tag;
}
}
};
if(!$.fn.asTabWidget) {
$.fn.asTabWidget = function (copy?: boolean) : JQuery {

View File

@ -1,4 +1,4 @@
/// <reference path="../../client.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../modal/ModalSettings.ts" />
/// <reference path="../modal/ModalBanList.ts" />
/*
@ -13,37 +13,72 @@
client_away_message Value: ''
*/
let control_bar: ControlBar; /* global variable to access the control bar */
type MicrophoneState = "disabled" | "muted" | "enabled";
type HeadphoneState = "muted" | "enabled";
type AwayState = "away-global" | "away" | "online";
class ControlBar {
private _muteInput: boolean;
private _muteOutput: boolean;
private _away: boolean;
private _query_visible: boolean;
private _awayMessage: string;
private _channel_subscribe_all: boolean;
private _button_away_active: AwayState;
private _button_microphone: MicrophoneState;
private _button_speakers: HeadphoneState;
private _button_subscribe_all: boolean;
private _button_query_visible: boolean;
private codec_supported: boolean = false;
private support_playback: boolean = false;
private support_record: boolean = false;
private connection_handler: ConnectionHandler | undefined;
readonly handle: TSClient;
htmlTag: JQuery;
constructor(handle: TSClient, htmlTag: JQuery) {
this.handle = handle;
constructor(htmlTag: JQuery) {
this.htmlTag = htmlTag;
}
initialise() {
this.htmlTag.find(".btn_connect").on('click', this.onConnect.bind(this));
this.htmlTag.find(".btn_disconnect").on('click', this.onDisconnect.bind(this));
this.htmlTag.find(".btn_mute_input").on('click', this.onInputMute.bind(this));
this.htmlTag.find(".btn_mute_output").on('click', this.onOutputMute.bind(this));
this.htmlTag.find(".btn_open_settings").on('click', this.onOpenSettings.bind(this));
this.htmlTag.find(".btn_permissions").on('click', this.onPermission.bind(this));
this.htmlTag.find(".btn_banlist").on('click', this.onBanlist.bind(this));
this.htmlTag.find(".button-subscribe-mode").on('click', this.on_toggle_channel_subscribe_all.bind(this));
this.htmlTag.find(".button-playlist-manage").on('click', this.on_playlist_manage.bind(this));
initialize_connection_handler_state(handler?: ConnectionHandler) {
/* setup the state like the last displayed one */
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;
}
set_connection_handler(handler?: ConnectionHandler) {
if(this.connection_handler == handler)
return;
this.connection_handler = handler;
this.apply_server_state();
}
apply_server_state() {
if(!this.connection_handler)
return;
const flag_away = typeof(this.connection_handler.client_status.away) === "string" || this.connection_handler.client_status.away;
if(!flag_away)
this.button_away_active = "online";
else if(flag_away && this._button_away_active === "online")
this.button_away_active = "away";
this.button_query_visible = this.connection_handler.client_status.queries_visible;
this.button_subscribe_all = this.connection_handler.client_status.channel_subscribe_all;
this.apply_server_voice_state();
}
apply_server_voice_state() {
if(!this.connection_handler)
return;
this.button_microphone = !this.connection_handler.client_status.input_hardware ? "disabled" : this.connection_handler.client_status.input_muted ? "muted" : "enabled";
this.button_speaker = this.connection_handler.client_status.output_muted ? "muted" : "enabled";
}
current_connection_handler() {
return this.connection_handler;
}
initialise() {
let dropdownify = (tag: JQuery) => {
tag.find(".button-dropdown").on('click', () => {
tag.addClass("displayed");
@ -58,201 +93,293 @@ class ControlBar {
tag.removeClass("displayed");
});
};
{
let tokens = this.htmlTag.find(".btn_token");
dropdownify(tokens);
tokens.find(".btn_token_use").on('click', this.on_token_use.bind(this));
tokens.find(".btn_token_list").on('click', this.on_token_list.bind(this));
this.htmlTag.find(".btn_connect").on('click', this.on_open_connect.bind(this));
this.htmlTag.find(".btn_disconnect").on('click', this.on_execute_disconnect.bind(this));
this.htmlTag.find(".btn_mute_input").on('click', this.on_toggle_microphone.bind(this));
this.htmlTag.find(".btn_mute_output").on('click', this.on_toggle_sound.bind(this));
this.htmlTag.find(".button-subscribe-mode").on('click', this.on_toggle_channel_subscribe.bind(this));
this.htmlTag.find(".btn_query_toggle").on('click', this.on_toggle_query_view.bind(this));
this.htmlTag.find(".btn_open_settings").on('click', this.on_open_settings.bind(this));
this.htmlTag.find(".btn_permissions").on('click', this.on_open_permissions.bind(this));
this.htmlTag.find(".btn_banlist").on('click', this.on_open_banslist.bind(this));
this.htmlTag.find(".button-playlist-manage").on('click', this.on_open_playlist_manage.bind(this));
this.htmlTag.find(".btn_token_use").on('click', this.on_token_use.bind(this));
this.htmlTag.find(".btn_token_list").on('click', this.on_token_list.bind(this));
{
this.htmlTag.find(".btn_away_disable").on('click', this.on_away_disable.bind(this));
this.htmlTag.find(".btn_away_disable_global").on('click', this.on_away_disable_global.bind(this));
this.htmlTag.find(".btn_away_enable").on('click', this.on_away_enable.bind(this));
this.htmlTag.find(".btn_away_enable_global").on('click', this.on_away_enable_global.bind(this));
this.htmlTag.find(".btn_away_message").on('click', this.on_away_set_message.bind(this));
this.htmlTag.find(".btn_away_message_global").on('click', this.on_away_set_message_global.bind(this));
this.htmlTag.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
}
dropdownify(this.htmlTag.find(".container-disconnect"));
dropdownify(this.htmlTag.find(".btn_token"));
dropdownify(this.htmlTag.find(".btn_away"));
dropdownify(this.htmlTag.find(".btn_bookmark"));
dropdownify(this.htmlTag.find(".btn_query"));
dropdownify(this.htmlTag.find(".dropdown-audio"));
dropdownify(this.htmlTag.find(".dropdown-servertools"));
{
}
{
let away = this.htmlTag.find(".btn_away");
dropdownify(away);
away.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
away.find(".btn_away_message").on('click', this.on_away_set_message.bind(this));
this.htmlTag.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this));
this.htmlTag.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
}
{
let bookmark = this.htmlTag.find(".btn_bookmark");
dropdownify(bookmark);
bookmark.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this));
bookmark.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
this.update_bookmarks();
this.update_bookmark_status();
}
{
let query = this.htmlTag.find(".btn_query");
dropdownify(query);
/* search for query buttons not only on the large device button */
this.htmlTag.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
this.htmlTag.find(".btn_query_create").on('click', this.on_query_create.bind(this));
this.htmlTag.find(".btn_query_manage").on('click', this.on_query_manage.bind(this));
this.htmlTag.find(".btn_query_create").on('click', this.on_open_query_create.bind(this));
this.htmlTag.find(".btn_query_manage").on('click', this.on_open_query_manage.bind(this));
}
/* Mobile dropdowns */
{
const dropdown = this.htmlTag.find(".dropdown-audio");
dropdownify(dropdown);
dropdown.find(".button-display").on('click', () => dropdown.addClass("displayed"));
}
{
const dropdown = this.htmlTag.find(".dropdown-servertools");
dropdownify(dropdown);
dropdown.find(".button-display").on('click', () => dropdown.addClass("displayed"));
}
this.update_bookmarks();
this.update_bookmark_status();
//Need an initialise
this.muteInput = settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false);
this.muteOutput = settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false);
this.query_visible = settings.static_global(Settings.KEY_CONTROL_SHOW_QUERIES, false);
this.channel_subscribe_all = settings.static_global(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, true);
this.button_speaker = settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false) ? "muted" : "enabled";
this.button_microphone = settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false) ? "muted" : "enabled";
this.button_subscribe_all = true;
this.button_query_visible = false;
}
on_away_toggle() {
this._awayMessage = "";
this.away = !this._away;
/* Update the UI */
set button_away_active(flag: AwayState) {
if(this._button_away_active === flag)
return;
this._button_away_active = flag;
this.update_button_away();
}
update_button_away() {
const button_away_enable = this.htmlTag.find(".btn_away_enable");
const button_away_disable = this.htmlTag.find(".btn_away_disable");
const button_away_toggle = this.htmlTag.find(".btn_away_toggle");
const button_away_disable_global = this.htmlTag.find(".btn_away_disable_global");
const button_away_enable_global = this.htmlTag.find(".btn_away_enable_global");
const button_away_message_global = this.htmlTag.find(".btn_away_message_global");
button_away_toggle.toggleClass("activated", this._button_away_active !== "online");
button_away_enable.toggle(this._button_away_active === "online");
button_away_disable.toggle(this._button_away_active !== "online");
const connections = server_connections.server_connection_handlers();
if(connections.length <= 1) {
button_away_disable_global.hide();
button_away_enable_global.hide();
button_away_message_global.hide();
} else {
button_away_message_global.show();
button_away_enable_global.toggle(server_connections.server_connection_handlers().filter(e => !e.client_status.away).length > 0);
button_away_disable_global.toggle(
this._button_away_active === "away-global" ||
server_connections.server_connection_handlers().filter(e => typeof(e.client_status.away) === "string" || e.client_status.away).length > 0
)
}
}
on_away_set_message() {
createInputModal(tr("Set away message"), tr("Please enter the away message"), message => true, message => {
if(message)
this.away = message;
}).open();
}
onInputMute() {
this.muteInput = !this._muteInput;
}
onOutputMute() {
this.muteOutput = !this._muteOutput;
}
set muteInput(flag: boolean) {
if(this._muteInput == flag) return;
this._muteInput = flag;
set button_microphone(state: MicrophoneState) {
if(this._button_microphone === state)
return;
this._button_microphone = state;
let tag = this.htmlTag.find(".btn_mute_input");
const tag_icon = tag.find(".icon_x32, .icon");
tag.toggleClass('activated', flag)
tag.toggleClass('activated', state === "muted");
tag_icon
.toggleClass('client-input_muted', flag)
.toggleClass('client-capture', !flag);
.toggleClass('client-input_muted', state === "muted")
.toggleClass('client-capture', state === "enabled")
.toggleClass('client-activate_microphone', state === "disabled");
if(this.handle.serverConnection.connected())
this.handle.serverConnection.send_command("clientupdate", {
client_input_muted: this._muteInput
});
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, this._muteInput);
this.updateMicrophoneRecordState();
if(state === "disabled")
tag_icon.attr('title', tr("Enable your microphone on this server"));
else if(state === "enabled")
tag_icon.attr('title', tr("Mute microphone"));
else
tag_icon.attr('title', tr("Unmute microphone"));
}
get muteOutput() : boolean { return this._muteOutput; }
set muteOutput(flag: boolean) {
if(this._muteOutput == flag) return;
this._muteOutput = flag;
set button_speaker(state: HeadphoneState) {
if(this._button_speakers === state)
return;
this._button_speakers = state;
let tag = this.htmlTag.find(".btn_mute_output");
const tag_icon = tag.find(".icon_x32, .icon");
tag.toggleClass('activated', flag)
tag.toggleClass('activated', state === "muted");
tag_icon
.toggleClass('client-output_muted', flag)
.toggleClass('client-volume', !flag);
.toggleClass('client-output_muted', state !== "enabled")
.toggleClass('client-volume', state === "enabled");
if(this.handle.serverConnection.connected())
this.handle.serverConnection.send_command("clientupdate", {
client_output_muted: this._muteOutput
});
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, this._muteOutput);
this.updateMicrophoneRecordState();
if(state === "enabled")
tag_icon.attr('title', tr("Mute sound"));
else
tag_icon.attr('title', tr("Unmute sound"));
}
set away(value: boolean | string) {
if(typeof(value) == "boolean") {
if(this._away == value) return;
this._away = value;
this._awayMessage = "";
} else {
this._awayMessage = value;
this._away = true;
set button_subscribe_all(state: boolean) {
if(this._button_subscribe_all === state)
return;
this._button_subscribe_all = state;
this.htmlTag
.find(".button-subscribe-mode")
.toggleClass('activated', this._button_subscribe_all)
.find('.icon_x32')
.toggleClass('client-unsubscribe_from_all_channels', !this._button_subscribe_all)
.toggleClass('client-subscribe_to_all_channels', this._button_subscribe_all);
}
set button_query_visible(state: boolean) {
if(this._button_query_visible === state)
return;
this._button_query_visible = state;
const button = this.htmlTag.find(".btn_query_toggle");
button.toggleClass('activated', this._button_query_visible);
if(this._button_query_visible)
button.find(".query-text").text(tr("Hide server queries"));
else
button.find(".query-text").text(tr("Show server queries"));
}
/* UI listener */
private on_away_toggle() {
if(this._button_away_active === "away" || this._button_away_active === "away-global")
this.button_away_active = "online";
else
this.button_away_active = "away";
if(this.connection_handler)
this.connection_handler.set_away_status(this._button_away_active !== "online");
}
private on_away_enable() {
this.button_away_active = "away";
if(this.connection_handler)
this.connection_handler.set_away_status(true);
}
private on_away_disable() {
this.button_away_active = "online";
if(this.connection_handler)
this.connection_handler.set_away_status(false);
}
private on_away_set_message() {
createInputModal(tr("Set away message"), tr("Please enter your away message"), message => true, message => {
if(typeof(message) === "string") {
this.button_away_active = "away";
if(this.connection_handler)
this.connection_handler.set_away_status(message);
}
}).open();
}
private on_away_enable_global() {
this.button_away_active = "away-global";
for(const connection of server_connections.server_connection_handlers())
connection.set_away_status(true);
}
private on_away_disable_global() {
this.button_away_active = "online";
for(const connection of server_connections.server_connection_handlers())
connection.set_away_status(false);
}
private on_away_set_message_global() {
createInputModal(tr("Set global away message"), tr("Please enter your global away message"), message => true, message => {
if(typeof(message) === "string") {
this.button_away_active = "away";
for(const connection of server_connections.server_connection_handlers())
connection.set_away_status(message);
}
}).open();
}
private on_toggle_microphone() {
if(this._button_microphone === "disabled" || this._button_microphone === "muted")
this.button_microphone = "enabled";
else
this.button_microphone = "muted";
if(this.connection_handler) {
this.connection_handler.client_status.input_muted = this._button_microphone !== "enabled";
if(!this.connection_handler.client_status.input_hardware)
this.connection_handler.acquire_recorder(voice_recoder, true); /* acquire_recorder already updates the voice status */
else
this.connection_handler.update_voice_status(undefined);
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, this.connection_handler.client_status.input_muted)
}
}
let tag = this.htmlTag.find(".btn_away_toggle");
if( this._away) {
tag.addClass("activated");
} else {
tag.removeClass("activated");
private on_toggle_sound() {
if(this._button_speakers === "muted")
this.button_speaker = "enabled";
else
this.button_speaker = "muted";
if(this.connection_handler) {
this.connection_handler.client_status.output_muted = this._button_speakers !== "enabled";
this.connection_handler.update_voice_status(undefined);
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, this.connection_handler.client_status.output_muted)
}
if(this.handle.serverConnection.connected)
this.handle.serverConnection.send_command("clientupdate", {
client_away: this._away,
client_away_message: this._awayMessage
});
this.updateMicrophoneRecordState();
}
private updateMicrophoneRecordState() {
let enabled = !this._muteInput && !this._muteOutput && !this._away;
if(this.handle.voiceConnection)
this.handle.voiceConnection.voiceRecorder.update(enabled);
private on_toggle_channel_subscribe() {
this.button_subscribe_all = !this._button_subscribe_all;
if(this.connection_handler) {
this.connection_handler.client_status.channel_subscribe_all = this._button_subscribe_all;
if(this._button_subscribe_all)
this.connection_handler.channelTree.subscribe_all_channels();
else
this.connection_handler.channelTree.unsubscribe_all_channels(true);
this.connection_handler.settings.changeServer(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, this._button_subscribe_all);
}
}
updateProperties() {
if(this.handle.serverConnection.connected)
this.handle.serverConnection.send_command("clientupdate", {
client_input_muted: this._muteInput,
client_output_muted: this._muteOutput,
client_away: this._away,
client_away_message: this._awayMessage,
client_input_hardware: this.codec_supported && this.support_record,
client_output_hardware: this.codec_supported && this.support_playback
});
private on_toggle_query_view() {
this.button_query_visible = !this._button_query_visible;
if(this.connection_handler) {
this.connection_handler.client_status.queries_visible = this._button_query_visible;
this.connection_handler.channelTree.toggle_server_queries(this._button_query_visible);
this.connection_handler.settings.changeServer(Settings.KEY_CONTROL_SHOW_QUERIES, this._button_subscribe_all);
}
}
updateVoice(targetChannel?: ChannelEntry) {
if(!targetChannel)
targetChannel = this.handle.getClient().currentChannel();
let client = this.handle.getClient();
this.codec_supported = targetChannel ? this.handle.voiceConnection && this.handle.voiceConnection.codecSupported(targetChannel.properties.channel_codec) : true;
this.support_record = this.handle.voiceConnection && this.handle.voiceConnection.voice_send_support();
this.support_playback = this.handle.voiceConnection && this.handle.voiceConnection.voice_playback_support();
this.htmlTag.find(".btn_mute_input").prop("disabled", !this.codec_supported|| !this.support_playback || !this.support_record);
this.htmlTag.find(".btn_mute_output").prop("disabled", !this.codec_supported || !this.support_playback);
this.handle.serverConnection.send_command("clientupdate", {
client_input_hardware: this.codec_supported && this.support_record,
client_output_hardware: this.codec_supported && this.support_playback
});
if(!this.codec_supported)
createErrorModal(tr("Channel codec unsupported"), tr("This channel has an unsupported codec.<br>You cant speak or listen to anybody within this channel!")).open();
/* Update these properties anyways (for case the server fails to handle the command) */
client.updateVariables(
{key: "client_input_hardware", value: (this.codec_supported && this.support_record) + ""},
{key: "client_output_hardware", value: (this.codec_supported && this.support_playback) + ""}
);
}
private onOpenSettings() {
private on_open_settings() {
Modals.spawnSettingsModal();
}
private onConnect() {
this.handle.cancel_reconnect();
private on_open_connect() {
if(this.connection_handler)
this.connection_handler.cancel_reconnect();
Modals.spawnConnectModal({
url: "ts.TeaSpeak.de",
enforce: false
@ -260,31 +387,31 @@ class ControlBar {
}
update_connection_state() {
switch (this.handle.serverConnection ? this.handle.serverConnection._connectionState : ConnectionState.UNCONNECTED) {
switch (this.connection_handler.serverConnection ? this.connection_handler.serverConnection._connectionState : ConnectionState.UNCONNECTED) {
case ConnectionState.CONNECTED:
case ConnectionState.CONNECTING:
case ConnectionState.INITIALISING:
this.htmlTag.find(".btn_disconnect").show();
this.htmlTag.find(".btn_connect").hide();
this.htmlTag.find(".container-disconnect").show();
this.htmlTag.find(".container-connect").hide();
break;
default:
this.htmlTag.find(".btn_disconnect").hide();
this.htmlTag.find(".btn_connect").show();
this.htmlTag.find(".container-disconnect").hide();
this.htmlTag.find(".container-connect").show();
}
}
private onDisconnect() {
this.handle.cancel_reconnect();
this.handle.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
private on_execute_disconnect() {
this.connection_handler.cancel_reconnect();
this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
this.update_connection_state();
sound.play(Sound.CONNECTION_DISCONNECTED);
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
}
private on_token_use() {
createInputModal(tr("Use token"), tr("Please enter your token/priviledge key"), message => message.length > 0, result => {
if(!result) return;
if(this.handle.serverConnection.connected)
this.handle.serverConnection.send_command("tokenuse", {
if(this.connection_handler.serverConnection.connected)
this.connection_handler.serverConnection.send_command("tokenuse", {
token: result
}).then(() => {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
@ -299,37 +426,40 @@ class ControlBar {
createErrorModal(tr("Not implemented"), tr("Token list is not implemented yet!")).open();
}
private onPermission() {
private on_open_permissions() {
let button = this.htmlTag.find(".btn_permissions");
button.addClass("activated");
setTimeout(() => {
Modals.spawnPermissionEdit().open();
if(this.connection_handler)
Modals.spawnPermissionEdit(this.connection_handler).open();
else
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
button.removeClass("activated");
}, 0);
}
private onBanlist() {
if(!this.handle.serverConnection) return;
private on_open_banslist() {
if(!this.connection_handler.serverConnection) return;
if(this.handle.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
Modals.openBanList(this.handle);
if(this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
Modals.openBanList(this.connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open();
sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
}
private on_bookmark_server_add() {
if(globalClient && globalClient.connected) {
if(this.connection_handler && this.connection_handler.connected) {
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => true, result => {
if(result) {
const bookmark = bookmarks.create_bookmark(result as string, bookmarks.bookmarks(), {
server_port: globalClient.serverConnection._remote_address.port,
server_address: globalClient.serverConnection._remote_address.host,
server_port: this.connection_handler.serverConnection._remote_address.port,
server_address: this.connection_handler.serverConnection._remote_address.host,
server_password: "",
server_password_hash: ""
}, globalClient.getClient().clientNickName());
}, this.connection_handler.getClient().clientNickName());
bookmarks.save_bookmark(bookmark);
this.update_bookmarks()
}
@ -353,6 +483,34 @@ class ControlBar {
const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => {
if(bookmark.type == bookmarks.BookmarkType.ENTRY) {
const mark = <bookmarks.Bookmark>bookmark;
const bookmark_connect = (new_tab: boolean) => {
this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working
const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile();
if(profile.valid()) {
const connection = this.connection_handler && !new_tab ? this.connection_handler : server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
connection.startConnection(
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
profile,
mark.nickname,
{
password: mark.server_properties.server_password_hash,
hashed: true
}
);
} else {
Modals.spawnConnectModal({
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
enforce: true
}, {
profile: profile,
enforce: true
})
}
};
return $.spawn("div")
.addClass("bookmark")
.append(
@ -363,27 +521,32 @@ class ControlBar {
.addClass("name")
.text(bookmark.display_name)
.on('click', event => {
this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed");
const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile();
if(profile.valid()) {
this.handle.startConnection(
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
profile,
mark.nickname,
{
password: mark.server_properties.server_password_hash,
hashed: true
}
);
} else {
Modals.spawnConnectModal({
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
enforce: true
}, {
profile: profile,
enforce: true
})
}
if(event.isDefaultPrevented())
return;
bookmark_connect(false);
})
.on('contextmenu', event => {
if(event.isDefaultPrevented())
return;
event.preventDefault();
spawn_context_menu(event.pageX, event.pageY, {
type: MenuEntryType.ENTRY,
name: tr("Connect"),
icon: 'client-connect',
callback: () => bookmark_connect(false)
}, {
type: MenuEntryType.ENTRY,
name: tr("Connect in a new tab"),
icon: 'client-connect',
callback: () => bookmark_connect(true)
}, MenuEntry.CLOSE(() => {
setTimeout(() => {
this.htmlTag.find(".btn_bookmark.button-dropdown").removeClass("force-show")
}, 250);
}));
this.htmlTag.find(".btn_bookmark.button-dropdown").addClass("force-show");
})
)
} else {
@ -423,84 +586,28 @@ class ControlBar {
Modals.spawnBookmarkModal();
}
get query_visible() {
return this._query_visible;
}
set query_visible(flag: boolean) {
if(this._query_visible == flag) return;
this._query_visible = flag;
settings.changeGlobal(Settings.KEY_CONTROL_SHOW_QUERIES, flag);
this.update_query_visibility_button();
this.handle.channelTree.toggle_server_queries(flag);
}
private on_query_visibility_toggle() {
this.query_visible = !this._query_visible;
this.update_query_visibility_button();
}
private update_query_visibility_button() {
const button = this.htmlTag.find(".btn_query_toggle");
button.toggleClass('activated', this._query_visible);
if(this._query_visible)
button.find(".query-text").text(tr("Hide server queries"));
else
button.find(".query-text").text(tr("Show server queries"));
}
private on_query_create() {
if(this.handle.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) {
Modals.spawnQueryCreate();
private on_open_query_create() {
if(this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) {
Modals.spawnQueryCreate(this.connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open();
sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
}
private on_query_manage() {
if(globalClient && globalClient.connected) {
Modals.spawnQueryManage(globalClient);
private on_open_query_manage() {
if(this.connection_handler && this.connection_handler.connected) {
Modals.spawnQueryManage(this.connection_handler);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
}
}
private on_playlist_manage() {
if(this.handle && this.handle.connected) {
Modals.spawnPlaylistManage(this.handle);
private on_open_playlist_manage() {
if(this.connection_handler && this.connection_handler.connected) {
Modals.spawnPlaylistManage(this.connection_handler);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
}
get channel_subscribe_all() : boolean {
return this._channel_subscribe_all;
}
set channel_subscribe_all(flag: boolean) {
if(this._channel_subscribe_all == flag)
return;
this._channel_subscribe_all = flag;
this.htmlTag
.find(".button-subscribe-mode")
.toggleClass('activated', this._channel_subscribe_all)
.find('.icon_x32')
.toggleClass('client-unsubscribe_from_all_channels', !this._channel_subscribe_all)
.toggleClass('client-subscribe_to_all_channels', this._channel_subscribe_all);
settings.changeGlobal(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, flag);
if(flag)
this.handle.channelTree.subscribe_all_channels();
else
this.handle.channelTree.unsubscribe_all_channels();
}
private on_toggle_channel_subscribe_all() {
this.channel_subscribe_all = !this.channel_subscribe_all;
}
}

View File

@ -1,4 +1,4 @@
/// <reference path="../../client.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../../../vendor/bbcode/xbbcode.ts" />
abstract class InfoManagerBase {
@ -50,24 +50,24 @@ abstract class InfoManager<T> extends InfoManagerBase {
}
class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefined> {
readonly handle: TSClient;
readonly handle: ConnectionHandler;
private current_selected?: AvailableTypes;
private _tag: JQuery<HTMLElement>;
private _tag_content: JQuery<HTMLElement>;
private _tag_info: JQuery<HTMLElement>;
private _tag_banner: JQuery<HTMLElement>;
private readonly _tag_info: JQuery<HTMLElement>;
private readonly _tag_banner: JQuery<HTMLElement>;
private _current_manager: InfoManagerBase = undefined;
private managers: InfoManagerBase[] = [];
private banner_manager: Hostbanner;
constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) {
constructor(client: ConnectionHandler) {
this.handle = client;
this._tag = htmlTag;
this._tag_content = htmlTag.find("> .select_info");
this._tag_info = this._tag_content.find(".container-select-info");
this._tag_banner = this._tag_content.find(".container-banner");
this._tag = $("#tmpl_select_info").renderTag();
this._tag_info = this._tag.find(".container-select-info");
this._tag_banner = this._tag.find(".container-banner");
this.managers.push(new MusicInfoManager());
this.managers.push(new ClientInfoManager());
@ -76,18 +76,22 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
this.banner_manager = new Hostbanner(client, this._tag_banner);
this._tag.find("button.close").on('click', () => {
this._tag.toggleClass('shown', false);
});
this._tag.find("button.close").on('click', () => this.close_popover());
}
get_tag() : JQuery {
return this._tag;
}
handle_resize() {
/* test if the popover isn't a popover anymore */
if(this._tag.hasClass('shown')) {
this._tag.removeClass('shown');
if(this._tag.parent().hasClass('shown')) {
this._tag.parent().removeClass('shown');
if(this.is_popover())
this._tag.addClass('shown');
this._tag.parent().addClass('shown');
}
this.banner_manager.handle_resize();
}
setCurrentSelected(entry: AvailableTypes) {
@ -128,14 +132,21 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
current_manager() { return this._current_manager; }
html_tag() { return this._tag_content; }
is_popover() : boolean {
return !this._tag.is(':visible') || this._tag.hasClass('shown');
return !this._tag.parent().is(':visible') || this._tag.parent().hasClass('shown');
}
open_popover() {
this._tag.toggleClass('shown', true);
this._tag.parent().toggleClass('shown', true);
this.banner_manager.handle_resize();
}
close_popover() {
this._tag.parent().toggleClass('shown', false);
}
rendered_tag() {
return this._tag_info;
}
}
@ -146,12 +157,12 @@ interface Window {
class Hostbanner {
readonly html_tag: JQuery<HTMLElement>;
readonly client: TSClient;
readonly client: ConnectionHandler;
private updater: NodeJS.Timer;
private _hostbanner_url: string;
constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) {
constructor(client: ConnectionHandler, htmlTag: JQuery<HTMLElement>) {
this.client = client;
this.html_tag = htmlTag;
}
@ -189,6 +200,10 @@ class Hostbanner {
}
}
handle_resize() {
this.html_tag.find("[x-divider-require-resize]").trigger('resize');
}
private generate_tag?() : Promise<JQuery<HTMLElement>> {
if(!this.client.connected) return undefined;
@ -220,6 +235,43 @@ class Hostbanner {
const rendered = $("#tmpl_selected_hostbanner").renderTag(properties);
/* ration watcher */
if(server.properties.virtualserver_hostbanner_mode == 2) {
const jimage = rendered.find(".meta-image");
if(jimage.length == 0) {
log.warn(LogCategory.SERVER, tr("Missing hostbanner meta image tag"));
} else {
const image = jimage[0];
image.onload = event => {
const image: HTMLImageElement = jimage[0] as any;
rendered.on('resize', event => {
const container = rendered.parent();
container.css('height', null);
container.css('flex-grow', '1');
const max_height = rendered.visible_height();
const max_width = rendered.visible_width();
container.css('flex-grow', '0');
const original_height = image.naturalHeight;
const original_width = image.naturalWidth;
const ratio_height = max_height / original_height;
const ratio_width = max_width / original_width;
const ratio = Math.min(ratio_height, ratio_width);
if(ratio == 0)
return;
const hostbanner_height = ratio * original_height;
container.css('height', Math.ceil(hostbanner_height) + "px");
/* the width is ignorable*/
});
setTimeout(() => rendered.trigger('resize'), 100);
};
}
}
if(window.fetch) {
return (async () => {
@ -244,6 +296,7 @@ class Hostbanner {
}
const url = (this._hostbanner_url = URL.createObjectURL(await result.blob()));
tag_image.css('background-image', 'url(' + url + ')');
tag_image.attr('src', url);
log.debug(LogCategory.SERVER, tr("Fetsched hostbanner successfully (%o, type: %o, url: %o)"), Date.now() - start, result.type, url);
} catch(error) {
log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), error);
@ -288,7 +341,7 @@ class ClientInfoManager extends InfoManager<ClientEntry> {
properties["client_name"] = client.createChatTag()[0];
properties["client_onlinetime"] = formatDate(client.calculateOnlineTime());
properties["sound_volume"] = client.audioController.volume * 100;
properties["sound_volume"] = client.get_audio_handle() ? client.get_audio_handle().get_volume() * 100 : -1;
properties["client_is_query"] = client.properties.client_type == ClientType.CLIENT_QUERY;
properties["client_is_web"] = client.properties.client_type_exact == ClientType.CLIENT_WEB;
@ -777,11 +830,11 @@ class MusicInfoManager extends ClientInfoManager {
}
update_local_volume(volume: number) {
this.handle.html_tag().find(".property-volume-local").text(Math.floor(volume * 100) + "%");
this.handle.rendered_tag().find(".property-volume-local").text(Math.floor(volume * 100) + "%");
}
update_remote_volume(volume: number) {
this.handle.html_tag().find(".property-volume-remote").text(Math.floor(volume * 100) + "%")
this.handle.rendered_tag().find(".property-volume-remote").text(Math.floor(volume * 100) + "%")
}
available<V>(object: V): boolean {

View File

@ -1,5 +1,3 @@
import LogType = log.LogType;
enum ChatType {
GENERAL,
SERVER,
@ -56,20 +54,34 @@ namespace MessageHelper {
result.push(...formatElement(pattern.substr(begin, found - begin))); //Append the text
let number;
let offset = 0;
while ("0123456789".includes(pattern[found + 1 + offset])) offset++;
number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0");
if(pattern[found + 1] == ':') {
offset++; /* the beginning : */
while (pattern[found + 1 + offset] != ':') offset++;
const tag = pattern.substr(found + 2, offset - 1);
if(pattern[found + offset + 1] != '}') {
found++;
continue;
offset++; /* the ending : */
if(pattern[found + offset + 1] != '}') {
found++;
continue;
}
result.push($.spawn(tag as any));
} else {
let number;
while ("0123456789".includes(pattern[found + 1 + offset])) offset++;
number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0");
if(pattern[found + offset + 1] != '}') {
found++;
continue;
}
if(objects.length < number)
log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
result.push(...formatElement(objects[number]));
}
if(objects.length < number)
log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
result.push(...formatElement(objects[number]));
found = found + 1 + offset;
begin = found + 1;
} while(found++);
@ -251,9 +263,7 @@ class ChatEntry {
type: MenuEntryType.ENTRY,
icon: "client-tab_close_button",
name: tr("Close"),
callback: () => {
chat.deleteChat(this);
}
callback: () => this.handle.deleteChat(this)
});
}
@ -341,6 +351,7 @@ class ChatBox {
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
static readonly URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
readonly connection_handler: ConnectionHandler;
htmlTag: JQuery;
chats: ChatEntry[];
private _activeChat: ChatEntry;
@ -348,9 +359,12 @@ class ChatBox {
private _button_send: JQuery;
private _input_message: JQuery;
constructor(htmlTag: JQuery) {
this.htmlTag = htmlTag;
constructor(connection_handler: ConnectionHandler) {
this.connection_handler = connection_handler;
}
initialize() {
this.htmlTag = $("#tmpl_frame_chat").renderTag();
this._button_send = this.htmlTag.find(".button-send");
this._input_message = this.htmlTag.find(".input-message");
@ -372,15 +386,15 @@ class ChatBox {
this._activeChat = undefined;
this.createChat("chat_server", ChatType.SERVER).onMessageSend = (text: string) => {
if(!globalClient.serverConnection) {
chat.serverChat().appendError(tr("Could not send chant message (Not connected)"));
if(!this.connection_handler.serverConnection) {
this.serverChat().appendError(tr("Could not send chant message (Not connected)"));
return;
}
globalClient.serverConnection.command_helper.sendMessage(text, ChatType.SERVER).catch(error => {
this.connection_handler.serverConnection.command_helper.sendMessage(text, ChatType.SERVER).catch(error => {
if(error instanceof CommandResult)
return;
chat.serverChat().appendMessage(tr("Failed to send text message."));
this.serverChat().appendMessage(tr("Failed to send text message."));
log.error(LogCategory.GENERAL, tr("Failed to send server text message: %o"), error);
});
};
@ -388,20 +402,20 @@ class ChatBox {
this.serverChat().flag_closeable = false;
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
if(!globalClient.serverConnection) {
chat.channelChat().appendError(tr("Could not send chant message (Not connected)"));
if(!this.connection_handler.serverConnection) {
this.channelChat().appendError(tr("Could not send chant message (Not connected)"));
return;
}
globalClient.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel()).catch(error => {
chat.channelChat().appendMessage(tr("Failed to send text message."));
this.connection_handler.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, this.connection_handler.getClient().currentChannel()).catch(error => {
this.channelChat().appendMessage(tr("Failed to send text message."));
log.error(LogCategory.GENERAL, tr("Failed to send channel text message: %o"), error);
});
};
this.channelChat().name = tr("Channel chat");
this.channelChat().flag_closeable = false;
globalClient.permissions.initializedListener.push(flag => {
this.connection_handler.permissions.initializedListener.push(flag => {
if(flag) this.activeChat0(this._activeChat);
});
}
@ -492,16 +506,16 @@ class ChatBox {
this._activeChat.html_tag.addClass("active");
this._activeChat.displayHistory();
if(!disable_input && globalClient && globalClient.permissions && globalClient.permissions.initialized())
if(!disable_input && this.connection_handler && this.connection_handler.permissions && this.connection_handler.permissions.initialized())
switch (this._activeChat.type) {
case ChatType.CLIENT:
disable_input = false;
break;
case ChatType.SERVER:
disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
disable_input = !this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
break;
case ChatType.CHANNEL:
disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
disable_input = !this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
break;
}
}

View File

@ -0,0 +1,129 @@
let server_connections: ServerConnectionManager;
class ServerConnectionManager {
private connection_handlers: ConnectionHandler[] = [];
private active_handler: ConnectionHandler | undefined;
private _container_channel_tree: JQuery;
private _container_select_info: JQuery;
private _container_chat_box: JQuery;
private _tag: JQuery;
private _tag_connection_entries: JQuery;
private _tag_buttons_scoll: JQuery;
private _tag_button_scoll_right: JQuery;
private _tag_button_scoll_left: JQuery;
constructor(tag: JQuery) {
this._tag = tag;
if(settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false))
this._tag.hide();
this._tag_connection_entries = this._tag.find(".connection-handlers");
this._tag_buttons_scoll = this._tag.find(".container-scroll");
this._tag_button_scoll_left = this._tag_buttons_scoll.find(".button-scroll-left");
this._tag_button_scoll_right = this._tag_buttons_scoll.find(".button-scroll-right");
this._tag_button_scoll_left.on('click', this._button_scroll_left_clicked.bind(this));
this._tag_button_scoll_right.on('click', this._button_scroll_right_clicked.bind(this));
this._container_channel_tree = $("#channelTree");
this._container_select_info = $("#select_info");
this._container_chat_box = $("#chat");
this.set_active_connection_handler(undefined);
}
spawn_server_connection_handler() : ConnectionHandler {
const handler = new ConnectionHandler();
this.connection_handlers.push(handler);
control_bar.update_button_away();
control_bar.initialize_connection_handler_state(handler);
handler.tag_connection_handler.appendTo(this._tag_connection_entries);
this._update_scroll();
return handler;
}
destroy_server_connection_handler(handler: ConnectionHandler) {
this.connection_handlers.remove(handler);
handler.tag_connection_handler.detach();
this._update_scroll();
if(handler.serverConnection) {
const connected = handler.connected;
handler.serverConnection.disconnect("handler destroyed");
handler.handleDisconnect(DisconnectReason.HANDLER_DESTROYED, connected);
}
if(handler === this.active_handler)
this.set_active_connection_handler(this.connection_handlers[0]);
}
set_active_connection_handler(handler: ConnectionHandler) {
if(handler && this.connection_handlers.indexOf(handler) == -1)
throw "Handler hasn't been registrated or is already obsolete!";
if(this.active_handler)
this.active_handler.select_info.close_popover();
this._tag_connection_entries.find(".active").removeClass("active");
this._container_channel_tree.children().detach();
this._container_select_info.children().detach();
this._container_chat_box.children().detach();
control_bar.set_connection_handler(handler);
if(handler) {
handler.tag_connection_handler.addClass("active");
this._container_channel_tree.append(handler.channelTree.tag_tree());
this._container_select_info.append(handler.select_info.get_tag());
this._container_chat_box.append(handler.chat.htmlTag);
if(handler.invoke_resized_on_activate)
handler.resize_elements();
}
this.active_handler = handler;
}
active_connection_handler() : ConnectionHandler | undefined {
return this.active_handler;
}
server_connection_handlers() : ConnectionHandler[] {
return this.connection_handlers;
}
update_ui() {
this._update_scroll();
}
private _update_scroll() {
const has_scroll = this._tag_connection_entries.hasScrollBar("width")
&& this._tag_connection_entries.width() + 10 >= this._tag_connection_entries.parent().width();
this._tag_buttons_scoll.toggleClass("enabled", has_scroll);
this._tag.toggleClass("scrollbar", has_scroll);
if(has_scroll)
this._update_scroll_buttons();
}
private _button_scroll_right_clicked() {
this._tag_connection_entries.scrollLeft((this._tag_connection_entries.scrollLeft() || 0) + 50);
this._update_scroll_buttons();
}
private _button_scroll_left_clicked() {
this._tag_connection_entries.scrollLeft((this._tag_connection_entries.scrollLeft() || 0) - 50);
this._update_scroll_buttons();
}
private _update_scroll_buttons() {
const scroll = this._tag_connection_entries.scrollLeft() || 0;
this._tag_button_scoll_left.toggleClass("disabled", scroll <= 0);
this._tag_button_scoll_right.toggleClass("disabled", scroll + this._tag_connection_entries.width() + 2 >= this._tag_connection_entries[0].scrollWidth);
}
}

View File

@ -111,15 +111,16 @@ namespace htmltags {
let client: ClientEntry;
if(globalClient && globalClient.channelTree) {
const current_connection = server_connections.active_connection_handler();
if(current_connection && current_connection.channelTree) {
if(!client && client_id) {
client = globalClient.channelTree.findClient(client_id);
client = current_connection.channelTree.findClient(client_id);
if(client && (client_unique_id && client.properties.client_unique_identifier != client_unique_id)) {
client = undefined; /* client id dosn't match anymore, lets search for the unique id */
}
}
if(!client && client_unique_id)
client = globalClient.channelTree.find_client_by_unique_id(client_unique_id);
client = current_connection.channelTree.find_client_by_unique_id(client_unique_id);
}
if(!client) {
/* we may should open a "offline" menu? */
@ -138,9 +139,10 @@ namespace htmltags {
export function callback_context_channel(element: JQuery) {
const channel_id = parseInt(element.attr("channel-id") || "0");
const current_connection = server_connections.active_connection_handler();
let channel: ChannelEntry;
if(globalClient && globalClient.channelTree) {
channel = globalClient.channelTree.findChannel(channel_id);
if(current_connection && current_connection.channelTree) {
channel = current_connection.channelTree.findChannel(channel_id);
}
if(!channel)

View File

@ -1,6 +1,6 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
const avatar_to_uid = (id: string) => {
@ -20,7 +20,7 @@ namespace Modals {
return (size / Math.pow(1024, exp)).toFixed(2) + 'KMGTPE'.charAt(exp - 1) + "iB";
};
export function spawnAvatarList(client: TSClient) {
export function spawnAvatarList(client: ConnectionHandler) {
const modal = createModal({
header: tr("Avatars"),
footer: undefined,

View File

@ -1,6 +1,6 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
export function spawnBanClient(name: string | string[], callback: (data: {

View File

@ -1,5 +1,5 @@
namespace Modals {
export function spawnBanCreate(base?: BanEntry, callback?: (entry?: BanEntry) => any) {
export function spawnBanCreate(connection: ConnectionHandler, base?: BanEntry, callback?: (entry?: BanEntry) => any) {
let result: BanEntry = {} as any;
result.banid = base ? base.banid : 0;
@ -98,8 +98,8 @@ namespace Modals {
input_global.prop("checked", base.server_id == 0);
}
if(globalClient && globalClient.permissions)
input_global.prop("disabled", !globalClient.permissions.neededPermission(base ? PermissionType.B_CLIENT_BAN_EDIT_GLOBAL : PermissionType.B_CLIENT_BAN_CREATE_GLOBAL));
if(connection && connection.permissions)
input_global.prop("disabled", !connection.permissions.neededPermission(base ? PermissionType.B_CLIENT_BAN_EDIT_GLOBAL : PermissionType.B_CLIENT_BAN_CREATE_GLOBAL));
return template;
},

View File

@ -28,10 +28,10 @@ namespace Modals {
modal: Modal;
}
export function openBanList(client: TSClient) {
export function openBanList(client: ConnectionHandler) {
let update;
const modal = spawnBanListModal(() => update(), () => {
spawnBanCreate(undefined, result => {
spawnBanCreate(client, undefined, result => {
if(result.server_id < 0) result.server_id = undefined;
console.log(tr("Adding ban %o"), result);
@ -52,7 +52,7 @@ namespace Modals {
});
}, ban => {
console.log(tr("Editing ban %o"), ban);
spawnBanCreate(ban, result => {
spawnBanCreate(client, ban, result => {
console.log(tr("Apply edit changes %o"), result);
if(result.server_id < 0) result.server_id = undefined;

View File

@ -1,4 +1,5 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
namespace Modals {
@ -252,7 +253,7 @@ namespace Modals {
width: 750
});
modal.close_listener.push(() => globalClient.controlBar.update_bookmarks());
modal.close_listener.push(() => control_bar.update_bookmarks());
modal.open();
}
}

View File

@ -1,4 +1,5 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
namespace Modals {

View File

@ -1,4 +1,4 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
namespace Modals {
export function spawnConnectModal(defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) {
@ -13,9 +13,11 @@ namespace Modals {
const connect_modal = $("#tmpl_connect").renderTag({
client: native_client,
forum_path: settings.static("forum_path"),
password_id: random_id
password_id: random_id,
multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false)
}).modalize((header, body, footer) => {
const button_connect = footer.find(".button-connect");
const button_connect_tab = footer.find(".button-connect-new-tab");
const button_manage = body.find(".button-manage-profiles");
const input_profile = body.find(".container-select-profile select");
@ -41,11 +43,9 @@ namespace Modals {
input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address);
input_nickname.attr('pattern', flag_nickname ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_nickname);
if(!flag_nickname || !flag_address || !selected_profile || !selected_profile.valid()) {
button_connect.prop("disabled", true);
} else {
button_connect.prop("disabled", false);
}
const flag_disabled = !flag_nickname || !flag_address || !selected_profile || !selected_profile.valid();
button_connect.prop("disabled", flag_disabled);
button_connect_tab.prop("disabled", flag_disabled);
};
input_nickname.val(settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined));
@ -89,7 +89,24 @@ namespace Modals {
button_connect.on('click', event => {
connect_modal.close();
globalClient.startConnection(
const connection = server_connections.active_connection_handler();
if(connection) {
connection.startConnection(
input_address.val().toString(),
selected_profile,
input_nickname.val().toString() || selected_profile.default_username,
{password: input_password.val().toString(), hashed: false}
);
} else {
button_connect_tab.trigger('click');
}
});
button_connect_tab.on('click', event => {
connect_modal.close();
const connection = server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
connection.startConnection(
input_address.val().toString(),
selected_profile,
input_nickname.val().toString() || selected_profile.default_username,
@ -102,57 +119,6 @@ namespace Modals {
connect_modal.open();
return;
const connectModal = createModal({
header: (tr("Create a new connection")),
body: function () {
const random_id = (() => {
const array = new Uint32Array(10);
window.crypto.getRandomValues(array);
return array.join("");
})();
let tag = $("#tmpl_connect").renderTag({
client: native_client,
forum_path: settings.static("forum_path"),
password_id: random_id
});
//connect_address
return tag;
},
footer: function () {
let tag = $.spawn("div");
tag.css("text-align", "right");
tag.css("margin-top", "3px");
tag.css("margin-bottom", "6px");
tag.addClass("modal-button-group");
let button = $.spawn("button");
button.addClass("connect_connect_button");
button.text(tr("Connect"));
button.on("click", function () {
connectModal.close();
let field_address = tag.parents(".modal-content").find(".connect_address");
let address = field_address.val().toString();
globalClient.startConnection(
address,
selected_profile,
tag.parents(".modal-content").find(".connect_nickname").val().toString() || selected_profile.default_username,
{password: tag.parents(".modal-content").find(".connect_password").val().toString(), hashed: false}
);
});
tag.append(button);
return tag;
},
width: '70%',
//flag_closeable: false
});
connectModal.open();
}
let Regex = {

View File

@ -1,7 +1,7 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
namespace Modals {
export function createChannelModal(channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, permissions: PermissionManager, callback: (properties?: ChannelProperties, permissions?: PermissionValue[]) => any) {
export function createChannelModal(connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, permissions: PermissionManager, callback: (properties?: ChannelProperties, permissions?: PermissionValue[]) => any) {
let properties: ChannelProperties = { } as ChannelProperties; //The changes properties
const modal = createModal({
header: channel ? tr("Edit channel") : tr("Create channel"),
@ -11,7 +11,7 @@ namespace Modals {
channel_flag_maxfamilyclients_unlimited: true,
channel_flag_maxclients_unlimited: true,
});
render_properties["channel_icon"] = globalClient.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0);
render_properties["channel_icon"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0);
let template = $("#tmpl_channel_edit").renderTag(render_properties);
return template.tabify();
@ -36,11 +36,11 @@ namespace Modals {
});
applyGeneralListener(properties, modal.htmlTag.find(".general_properties"), modal.htmlTag.find(".button_ok"), channel);
applyStandardListener(properties, modal.htmlTag.find(".settings_standard"), modal.htmlTag.find(".button_ok"), parent, !channel);
applyPermissionListener(properties, modal.htmlTag.find(".settings_permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
applyAudioListener(properties, modal.htmlTag.find(".container-channel-settings-audio"), modal.htmlTag.find(".button_ok"), channel);
applyAdvancedListener(properties, modal.htmlTag.find(".settings_advanced"), modal.htmlTag.find(".button_ok"), channel);
applyGeneralListener(connection, properties, modal.htmlTag.find(".general_properties"), modal.htmlTag.find(".button_ok"), channel);
applyStandardListener(connection, properties, modal.htmlTag.find(".settings_standard"), modal.htmlTag.find(".button_ok"), parent, !channel);
applyPermissionListener(connection, properties, modal.htmlTag.find(".settings_permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
applyAudioListener(connection, properties, modal.htmlTag.find(".container-channel-settings-audio"), modal.htmlTag.find(".button_ok"), channel);
applyAdvancedListener(connection, properties, modal.htmlTag.find(".settings_advanced"), modal.htmlTag.find(".button_ok"), channel);
let updated: PermissionValue[] = [];
modal.htmlTag.find(".button_ok").click(() => {
@ -72,7 +72,7 @@ namespace Modals {
modal.htmlTag.find(".channel_name").focus();
}
function applyGeneralListener(properties: ChannelProperties, tag: JQuery, button: JQuery, channel: ChannelEntry | undefined) {
function applyGeneralListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel: ChannelEntry | undefined) {
let updateButton = () => {
if(tag.find(".input_error").length == 0)
button.removeAttr("disabled");
@ -86,13 +86,13 @@ namespace Modals {
if(this.value.length < 1 || this.value.length > 40)
$(this).addClass("input_error");
updateButton();
}).prop("disabled", channel && !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NAME).granted(1));
}).prop("disabled", channel && !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NAME).granted(1));
tag.find(".button-select-icon").on('click', event => {
Modals.spawnIconSelect(globalClient, id => {
Modals.spawnIconSelect(connection, id => {
const icon_node = tag.find(".button-select-icon").find(".icon-node");
icon_node.empty();
icon_node.append(globalClient.fileManager.icons.generateTag(id));
icon_node.append(connection.fileManager.icons.generateTag(id));
console.log("Selected icon ID: %d", id);
properties.channel_icon_id = id;
@ -106,18 +106,18 @@ namespace Modals {
$(this).removeClass("input_error");
if(!properties.channel_flag_password)
if(globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1))
if(connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1))
$(this).addClass("input_error");
updateButton();
}).prop("disabled", !globalClient.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_PASSWORD : PermissionType.B_CHANNEL_MODIFY_PASSWORD).granted(1));
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_PASSWORD : PermissionType.B_CHANNEL_MODIFY_PASSWORD).granted(1));
tag.find(".channel_topic").change(function (this: HTMLInputElement) {
properties.channel_topic = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1));
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1));
tag.find(".channel_description").change(function (this: HTMLInputElement) {
properties.channel_description = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1));
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1));
if(!channel) {
setTimeout(() => {
@ -127,7 +127,7 @@ namespace Modals {
}
}
function applyStandardListener(properties: ChannelProperties, tag: JQuery, button: JQuery, parent: ChannelEntry, create: boolean) {
function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, parent: ChannelEntry, create: boolean) {
tag.find("input[name=\"channel_type\"]").change(function (this: HTMLInputElement) {
switch(this.value) {
case "semi":
@ -145,11 +145,11 @@ namespace Modals {
}
});
tag.find("input[name=\"channel_type\"][value=\"temp\"]")
.prop("disabled", !globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1));
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1));
tag.find("input[name=\"channel_type\"][value=\"semi\"]")
.prop("disabled", !globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1));
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1));
tag.find("input[name=\"channel_type\"][value=\"perm\"]")
.prop("disabled", !globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1));
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1));
if(create)
tag.find("input[name=\"channel_type\"]:not(:disabled)").last().prop("checked", true).trigger('change');
@ -164,26 +164,26 @@ namespace Modals {
tag.find("input[name=\"channel_type\"][value=\"perm\"]").prop("checked", true).trigger("change");
}
}).prop("disabled",
!globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) ||
!globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1));
!connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) ||
!connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1));
tag.find("input[name=\"talk_power\"]").change(function (this: HTMLInputElement) {
properties.channel_needed_talk_power = parseInt(this.value);
}).prop("disabled", !globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1));
}).prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1));
let orderTag = tag.find(".order_id");
for(let channel of (parent ? parent.children() : globalClient.channelTree.rootChannel()))
for(let channel of (parent ? parent.children() : connection.channelTree.rootChannel()))
$.spawn("option").attr("channelId", channel.channelId.toString()).text(channel.channelName()).appendTo(orderTag);
orderTag.change(function (this: HTMLSelectElement) {
let selected = $(this.options.item(this.selectedIndex));
properties.channel_order = parseInt(selected.attr("channelId"));
}).prop("disabled", !globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1));
}).prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1));
orderTag.find("option").last().prop("selected", true);
}
function applyPermissionListener(properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) {
function applyPermissionListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) {
let apply_permissions = (channel_permissions: PermissionValue[]) => {
console.log(tr("Got permissions: %o"), channel_permissions);
let required_power = -2;
@ -229,7 +229,7 @@ namespace Modals {
} else apply_permissions([]);
}
function applyAudioListener(properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
let update_template = () => {
let codec = properties.channel_codec;
if(!codec && channel)
@ -291,19 +291,19 @@ namespace Modals {
}
});
tag.find("input[name=\"voice_template\"][value=\"voice_mobile\"]")
.prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
tag.find("input[name=\"voice_template\"][value=\"voice_desktop\"]")
.prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
tag.find("input[name=\"voice_template\"][value=\"music\"]")
.prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
let codecs = tag.find(".voice_codec option");
codecs.eq(0).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8).granted(1));
codecs.eq(1).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX16).granted(1));
codecs.eq(2).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX32).granted(1));
codecs.eq(3).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_CELTMONO48).granted(1));
codecs.eq(4).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
codecs.eq(5).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
codecs.eq(0).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8).granted(1));
codecs.eq(1).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX16).granted(1));
codecs.eq(2).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX32).granted(1));
codecs.eq(3).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_CELTMONO48).granted(1));
codecs.eq(4).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
codecs.eq(5).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
tag.find(".voice_codec").change(function (this: HTMLSelectElement) {
if($(this.item(this.selectedIndex)).prop("disabled")) return false;
@ -322,25 +322,25 @@ namespace Modals {
quality_slider.on('input', event => change_quality(parseInt(quality_slider.val() as string)));
}
function applyAdvancedListener(properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
function applyAdvancedListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
tag.find(".channel_name_phonetic").change(function (this: HTMLInputElement) {
properties.channel_topic = this.value;
});
tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) {
properties.channel_delete_delay = parseInt(this.value);
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1));
}).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1));
tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) {
properties.channel_codec_is_unencrypted = parseInt(this.value) == 0;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1));
}).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1));
{
let tag_infinity = tag.find("input[name=\"max_users\"][value=\"infinity\"]");
let tag_limited = tag.find("input[name=\"max_users\"][value=\"limited\"]");
let tag_limited_value = tag.find(".channel_maxclients");
if(!globalClient.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) {
if(!connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) {
tag_infinity.prop("disabled", true);
tag_limited.prop("disabled", true);
tag_limited_value.prop("disabled", true);
@ -363,7 +363,7 @@ namespace Modals {
let tag_limited = tag.find("input[name=\"max_users_family\"][value=\"limited\"]");
let tag_limited_value = tag.find(".channel_maxfamilyclients");
if(!globalClient.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) {
if(!connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) {
tag_inherited.prop("disabled", true);
tag_infinity.prop("disabled", true);
tag_limited.prop("disabled", true);

View File

@ -1,10 +1,10 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
//TODO Upload/delete button
export function spawnIconSelect(client: TSClient, callback_icon?: (id: number) => any, selected_icon?: number) {
export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id: number) => any, selected_icon?: number) {
callback_icon = callback_icon || (() => {});
selected_icon = selected_icon || 0;

View File

@ -1,6 +1,6 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
/*
TODO: Check needed permissions and may not even try to request, because we dont have the permission. Permissions:
@ -542,7 +542,7 @@ namespace Modals {
}
}
export function spawnPermissionEdit() : Modal {
export function spawnPermissionEdit(connection: ConnectionHandler) : Modal {
const connectModal = createModal({
header: function() {
return tr("Server Permissions");
@ -551,7 +551,7 @@ namespace Modals {
let properties: any = {};
let tag = $("#tmpl_server_permissions").renderTag(properties);
const pe = new PermissionEditor(globalClient.permissions.groupedPermissions());
const pe = new PermissionEditor(connection.permissions.groupedPermissions());
pe.build_tag();
/* initialisation */
{
@ -560,11 +560,11 @@ namespace Modals {
}
apply_server_groups(pe, tag.find(".tab-group-server"));
apply_channel_groups(pe, tag.find(".tab-group-channel"));
apply_channel_permission(pe, tag.find(".tab-channel"));
apply_client_permission(pe, tag.find(".tab-client"));
apply_client_channel_permission(pe, tag.find(".tab-client-channel"));
apply_server_groups(connection, pe, tag.find(".tab-group-server"));
apply_channel_groups(connection, pe, tag.find(".tab-group-channel"));
apply_channel_permission(connection, pe, tag.find(".tab-channel"));
apply_client_permission(connection, pe, tag.find(".tab-client"));
apply_client_channel_permission(connection, pe, tag.find(".tab-client-channel"));
return tag.tabify(false);
},
footer: undefined,
@ -583,16 +583,16 @@ namespace Modals {
return connectModal;
}
function build_channel_tree(channel_list: JQuery, select_callback: (channel: ChannelEntry) => any) {
const root = globalClient.channelTree.get_first_channel();
function build_channel_tree(connection: ConnectionHandler, channel_list: JQuery, select_callback: (channel: ChannelEntry) => any) {
const root = connection.channelTree.get_first_channel();
if(!root) return;
const build_channel = (channel: ChannelEntry) => {
let tag = $.spawn("div").addClass("channel").attr("channel-id", channel.channelId);
globalClient.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(tag);
connection.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(tag);
{
let name = $.spawn("a").text(channel.channelName() + " (" + channel.channelId + ")").addClass("name");
//if(globalClient.channelTree.server.properties. == group.id)
//if(connection.channelTree.server.properties. == group.id)
// name.addClass("default");
name.appendTo(tag);
}
@ -619,7 +619,7 @@ namespace Modals {
setTimeout(() => channel_list.find('.channel').first().trigger('click'), 0);
}
function apply_client_channel_permission(editor: PermissionEditor, tab_tag: JQuery) {
function apply_client_channel_permission(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) {
let current_cldbid: number = 0;
let current_channel: ChannelEntry;
@ -629,7 +629,7 @@ namespace Modals {
tab_tag.on('show', event => {
console.error("Channel tab show");
pe_client.append(editor.container);
if(globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) {
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) {
if(current_cldbid && current_channel)
editor.set_mode(PermissionEditorMode.VISIBLE);
else
@ -643,7 +643,7 @@ namespace Modals {
editor.set_listener_update(() => {
if(!current_cldbid || !current_channel) return;
globalClient.permissions.requestClientChannelPermissions(current_cldbid, current_channel.channelId).then(result => {
connection.permissions.requestClientChannelPermissions(current_cldbid, current_channel.channelId).then(result => {
editor.set_permissions(result);
editor.set_mode(PermissionEditorMode.VISIBLE);
}).catch(error => {
@ -665,7 +665,7 @@ namespace Modals {
permission.id,
);
return globalClient.serverConnection.send_command("channelclientdelperm", {
return connection.serverConnection.send_command("channelclientdelperm", {
cldbid: current_cldbid,
cid: current_channel.channelId,
permid: permission.id,
@ -677,7 +677,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("channelclientdelperm", {
return connection.serverConnection.send_command("channelclientdelperm", {
cldbid: current_cldbid,
cid: current_channel.channelId,
permid: permission.id_grant(),
@ -694,7 +694,7 @@ namespace Modals {
value.flag_negate
);
return globalClient.serverConnection.send_command("channelclientaddperm", {
return connection.serverConnection.send_command("channelclientaddperm", {
cldbid: current_cldbid,
cid: current_channel.channelId,
permid: permission.id,
@ -709,7 +709,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("channelclientaddperm", {
return connection.serverConnection.send_command("channelclientaddperm", {
cldbid: current_cldbid,
cid: current_channel.channelId,
permid: permission.id_grant(),
@ -726,7 +726,7 @@ namespace Modals {
});
}
build_channel_tree(tab_tag.find(".list-channel .entries"), channel => {
build_channel_tree(connection, tab_tag.find(".list-channel .entries"), channel => {
if(current_channel == channel) return;
current_channel = channel;
@ -745,7 +745,7 @@ namespace Modals {
const resolve_client = () => {
let client_uid = tag_select_uid.val() as string;
globalClient.serverConnection.command_helper.info_from_uid(client_uid).then(result => {
connection.serverConnection.command_helper.info_from_uid(client_uid).then(result => {
if(!result || result.length == 0) return Promise.reject("invalid data");
tag_select_uid.attr('pattern', null).removeClass('is-invalid');
@ -778,7 +778,7 @@ namespace Modals {
}
}
function apply_client_permission(editor: PermissionEditor, tab_tag: JQuery) {
function apply_client_permission(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) {
let current_cldbid: number = 0;
/* the editor */
@ -788,7 +788,7 @@ namespace Modals {
console.error("Channel tab show");
pe_client.append(editor.container);
if(globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) {
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) {
if(current_cldbid)
editor.set_mode(PermissionEditorMode.VISIBLE);
else
@ -801,7 +801,7 @@ namespace Modals {
editor.set_listener_update(() => {
if(!current_cldbid) return;
globalClient.permissions.requestClientPermissions(current_cldbid).then(result => {
connection.permissions.requestClientPermissions(current_cldbid).then(result => {
editor.set_permissions(result);
editor.set_mode(PermissionEditorMode.VISIBLE);
}).catch(error => {
@ -821,7 +821,7 @@ namespace Modals {
permission.id,
);
return globalClient.serverConnection.send_command("clientaddperm", {
return connection.serverConnection.send_command("clientaddperm", {
cldbid: current_cldbid,
permid: permission.id,
});
@ -832,7 +832,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("clientaddperm", {
return connection.serverConnection.send_command("clientaddperm", {
cldbid: current_cldbid,
permid: permission.id_grant(),
});
@ -848,7 +848,7 @@ namespace Modals {
value.flag_negate
);
return globalClient.serverConnection.send_command("clientaddperm", {
return connection.serverConnection.send_command("clientaddperm", {
cldbid: current_cldbid,
permid: permission.id,
permvalue: value.value,
@ -862,7 +862,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("clientaddperm", {
return connection.serverConnection.send_command("clientaddperm", {
cldbid: current_cldbid,
permid: permission.id_grant(),
permvalue: value.granted,
@ -888,7 +888,7 @@ namespace Modals {
const resolve_client = () => {
let client_uid = tag_select_uid.val() as string;
globalClient.serverConnection.command_helper.info_from_uid(client_uid).then(result => {
connection.serverConnection.command_helper.info_from_uid(client_uid).then(result => {
if(!result || result.length == 0) return Promise.reject("invalid data");
tag_select_uid.attr('pattern', null).removeClass('is-invalid');
@ -920,7 +920,7 @@ namespace Modals {
tab_tag.find(".client-select-uid").on('change', event => resolve_client());
}
function apply_channel_permission(editor: PermissionEditor, tab_tag: JQuery) {
function apply_channel_permission(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) {
let current_channel: ChannelEntry | undefined;
/* the editor */
@ -929,7 +929,7 @@ namespace Modals {
tab_tag.on('show', event => {
console.error("Channel tab show");
pe_channel.append(editor.container);
if(globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST).granted(1))
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST).granted(1))
editor.set_mode(PermissionEditorMode.VISIBLE);
else {
editor.set_mode(PermissionEditorMode.NO_PERMISSION);
@ -939,7 +939,7 @@ namespace Modals {
editor.set_listener_update(() => {
if(!current_channel) return;
globalClient.permissions.requestChannelPermissions(current_channel.channelId).then(result => editor.set_permissions(result)).catch(error => {
connection.permissions.requestChannelPermissions(current_channel.channelId).then(result => editor.set_permissions(result)).catch(error => {
console.log(error); //TODO handling?
});
});
@ -956,7 +956,7 @@ namespace Modals {
permission.id,
);
return globalClient.serverConnection.send_command("channeldelperm", {
return connection.serverConnection.send_command("channeldelperm", {
cid: current_channel.channelId,
permid: permission.id,
});
@ -968,7 +968,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("channeldelperm", {
return connection.serverConnection.send_command("channeldelperm", {
cid: current_channel.channelId,
permid: permission.id_grant(),
});
@ -984,7 +984,7 @@ namespace Modals {
value.flag_negate
);
return globalClient.serverConnection.send_command("channeladdperm", {
return connection.serverConnection.send_command("channeladdperm", {
cid: current_channel.channelId,
permid: permission.id,
permvalue: value.value,
@ -999,7 +999,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("channeladdperm", {
return connection.serverConnection.send_command("channeladdperm", {
cid: current_channel.channelId,
permid: permission.id_grant(),
permvalue: value.granted,
@ -1016,13 +1016,13 @@ namespace Modals {
}
let channel_list = tab_tag.find(".list-channel .entries");
build_channel_tree(channel_list, channel => {
build_channel_tree(connection, channel_list, channel => {
current_channel = channel;
editor.trigger_update();
});
}
function apply_channel_groups(editor: PermissionEditor, tab_tag: JQuery) {
function apply_channel_groups(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) {
let current_group;
/* the editor */
@ -1031,7 +1031,7 @@ namespace Modals {
tab_tag.on('show', event => {
console.error("Channel group tab show");
pe_server.append(editor.container);
if(globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST).granted(1))
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST).granted(1))
editor.set_mode(PermissionEditorMode.VISIBLE);
else {
editor.set_mode(PermissionEditorMode.NO_PERMISSION);
@ -1041,7 +1041,7 @@ namespace Modals {
editor.set_listener_update(() => {
if(!current_group) return;
globalClient.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => {
connection.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => {
console.log(error); //TODO handling?
});
});
@ -1058,7 +1058,7 @@ namespace Modals {
permission.id,
);
return globalClient.serverConnection.send_command("channelgroupdelperm", {
return connection.serverConnection.send_command("channelgroupdelperm", {
cgid: current_group.id,
permid: permission.id,
});
@ -1069,7 +1069,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("channelgroupdelperm", {
return connection.serverConnection.send_command("channelgroupdelperm", {
cgid: current_group.id,
permid: permission.id_grant(),
});
@ -1085,7 +1085,7 @@ namespace Modals {
value.flag_negate
);
return globalClient.serverConnection.send_command("channelgroupaddperm", {
return connection.serverConnection.send_command("channelgroupaddperm", {
cgid: current_group.id,
permid: permission.id,
permvalue: value.value,
@ -1099,7 +1099,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("channelgroupaddperm", {
return connection.serverConnection.send_command("channelgroupaddperm", {
cgid: current_group.id,
permid: permission.id_grant(),
permvalue: value.granted,
@ -1120,14 +1120,24 @@ namespace Modals {
{
let group_list = tab_tag.find(".list-group-channel .entries");
for(let group of globalClient.groups.channelGroups.sort(GroupManager.sorter())) {
const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1);
const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1);
for(let group of connection.groups.channelGroups.sort(GroupManager.sorter())) {
if(group.type == GroupType.QUERY) {
if(!allow_query_groups)
continue;
} else if(group.type == GroupType.TEMPLATE) {
if(!allow_template_groups)
continue;
}
let tag = $.spawn("div").addClass("group").attr("group-id", group.id);
globalClient.fileManager.icons.generateTag(group.properties.iconid).appendTo(tag);
connection.fileManager.icons.generateTag(group.properties.iconid).appendTo(tag);
{
let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name");
if(group.properties.savedb)
name.addClass("savedb");
if(globalClient.channelTree.server.properties.virtualserver_default_channel_group == group.id)
if(connection.channelTree.server.properties.virtualserver_default_channel_group == group.id)
name.addClass("default");
name.appendTo(tag);
}
@ -1156,21 +1166,30 @@ namespace Modals {
b_virtualserver_channelclient_permission_list
b_virtualserver_playlist_permission_list
*/
function apply_server_groups(editor: PermissionEditor, tab_tag: JQuery) {
function apply_server_groups(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) {
let current_group;
/* list all groups */
{
let group_list = tab_tag.find(".list-group-server .entries");
for(let group of globalClient.groups.serverGroups.sort(GroupManager.sorter())) {
const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1);
const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1);
for(const group of connection.groups.serverGroups.sort(GroupManager.sorter())) {
if(group.type == GroupType.QUERY) {
if(!allow_query_groups)
continue;
} else if(group.type == GroupType.TEMPLATE) {
if(!allow_template_groups)
continue;
}
let tag = $.spawn("div").addClass("group").attr("group-id", group.id);
globalClient.fileManager.icons.generateTag(group.properties.iconid).appendTo(tag);
connection.fileManager.icons.generateTag(group.properties.iconid).appendTo(tag);
{
let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name");
if(group.properties.savedb)
name.addClass("savedb");
if(globalClient.channelTree.server.properties.virtualserver_default_server_group == group.id)
if(connection.channelTree.server.properties.virtualserver_default_server_group == group.id)
name.addClass("default");
name.appendTo(tag);
}
@ -1194,14 +1213,14 @@ namespace Modals {
tab_tag.on('show', event => {
console.error("Server tab show");
pe_server.append(editor.container);
if(globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST).granted(1))
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST).granted(1))
editor.set_mode(PermissionEditorMode.VISIBLE);
else {
editor.set_mode(PermissionEditorMode.NO_PERMISSION);
return;
}
editor.set_listener_update(() => {
globalClient.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => {
connection.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => {
console.log(error); //TODO handling?
});
});
@ -1218,7 +1237,7 @@ namespace Modals {
permission.id,
);
return globalClient.serverConnection.send_command("servergroupdelperm", {
return connection.serverConnection.send_command("servergroupdelperm", {
sgid: current_group.id,
permid: permission.id,
});
@ -1229,7 +1248,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("servergroupdelperm", {
return connection.serverConnection.send_command("servergroupdelperm", {
sgid: current_group.id,
permid: permission.id_grant(),
});
@ -1245,7 +1264,7 @@ namespace Modals {
value.flag_negate
);
return globalClient.serverConnection.send_command("servergroupaddperm", {
return connection.serverConnection.send_command("servergroupaddperm", {
sgid: current_group.id,
permid: permission.id,
permvalue: value.value,
@ -1259,7 +1278,7 @@ namespace Modals {
value.granted,
);
return globalClient.serverConnection.send_command("servergroupaddperm", {
return connection.serverConnection.send_command("servergroupaddperm", {
sgid: current_group.id,
permid: permission.id_grant(),
permvalue: value.granted,

View File

@ -1,6 +1,6 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
export function spawnPlaylistSongInfo(song: PlaylistSong) {
@ -68,7 +68,7 @@ namespace Modals {
modal.open();
}
export function spawnPlaylistEdit(client: TSClient, playlist: Playlist) {
export function spawnPlaylistEdit(client: ConnectionHandler, playlist: Playlist) {
let modal: Modal;
let changed_properties = {};
let changed_permissions = {};
@ -153,7 +153,7 @@ namespace Modals {
return modal;
}
function apply_songs(tag: JQuery, client: TSClient, playlist: Playlist) {
function apply_songs(tag: JQuery, client: ConnectionHandler, playlist: Playlist) {
const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id;
const song_tag = tag.find(".container-songs");
@ -254,7 +254,7 @@ namespace Modals {
return set_current_song;
}
function apply_permissions(tag: JQuery, client: TSClient, playlist: Playlist, change_permission: (key: string, value: number) => any) {
function apply_permissions(tag: JQuery, client: ConnectionHandler, playlist: Playlist, change_permission: (key: string, value: number) => any) {
const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id;
const permission_tag = tag.find(".container-permissions");
const nopermission_tag = tag.find(".container-no-permissions");
@ -294,7 +294,7 @@ namespace Modals {
return update_permissions;
}
function apply_properties(tag: JQuery, client: TSClient, playlist: Playlist, change_property: (key: string, value: string) => any, callback_current_song: (id: number) => any) {
function apply_properties(tag: JQuery, client: ConnectionHandler, playlist: Playlist, change_property: (key: string, value: string) => any, callback_current_song: (id: number) => any) {
const owns_playlist = playlist.playlist_owner_dbid == client.getClient().properties.client_database_id;
client.serverConnection.command_helper.request_playlist_info(playlist.playlist_id).then(info => {

View File

@ -1,9 +1,9 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
export function spawnPlaylistManage(client: TSClient) {
export function spawnPlaylistManage(client: ConnectionHandler) {
let modal: Modal;
let selected_playlist: Playlist;
let available_playlists: Playlist[];

View File

@ -1,6 +1,6 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
export function spawnPoke(invoker: {

View File

@ -1,9 +1,9 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
export function spawnQueryCreate(callback_created?: (user, pass) => any) {
export function spawnQueryCreate(connection: ConnectionHandler, callback_created?: (user, pass) => any) {
let modal;
modal = createModal({
header: tr("Create a server query login"),
@ -34,11 +34,11 @@ namespace Modals {
return true;
}
};
globalClient.serverConnection.command_handler_boss().register_single_handler(single_handler);
globalClient.serverConnection.send_command("querycreate", {
connection.serverConnection.command_handler_boss().register_single_handler(single_handler);
connection.serverConnection.send_command("querycreate", {
client_login_name: name
}).catch(error => {
globalClient.serverConnection.command_handler_boss().remove_single_handler(single_handler);
connection.serverConnection.command_handler_boss().remove_single_handler(single_handler);
if(error instanceof CommandResult)
error = error.extra_message || error.message;

View File

@ -1,9 +1,9 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
export function spawnQueryManage(client: TSClient) {
export function spawnQueryManage(client: ConnectionHandler) {
let modal: Modal;
let selected_query: QueryListEntry;
@ -66,7 +66,7 @@ namespace Modals {
template.find(".footer .buttons .button-refresh").on('click', update_list);
template.find(".button-query-create").on('click', () => {
Modals.spawnQueryCreate((user, pass) => update_list());
Modals.spawnQueryCreate(client, (user, pass) => update_list());
});
template.find(".button-query-rename").on('click', () => {
if(!selected_query) return;

View File

@ -1,4 +1,4 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
namespace Modals {
export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => any) {
@ -37,6 +37,8 @@ namespace Modals {
}
function server_applyGeneralListener(properties: ServerProperties, server: ServerEntry, tag: JQuery, button: JQuery) {
const connection_handler = server.channelTree.client;
let updateButton = () => {
if(tag.find(".input_error").length == 0)
button.removeAttr("disabled");
@ -50,11 +52,11 @@ namespace Modals {
if(this.value.length < 1 || this.value.length > 70)
$(this).addClass("input_error");
updateButton();
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1));
tag.find(".virtualserver_name_phonetic").change(function (this: HTMLInputElement) {
properties.virtualserver_name_phonetic = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1));
tag.find(".virtualserver_password").change(function (this: HTMLInputElement) {
properties.virtualserver_flag_password = this.value.length != 0;
@ -63,16 +65,16 @@ namespace Modals {
$(this).removeClass("input_error");
if(!properties.virtualserver_flag_password)
if(globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1))
if(connection_handler.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1))
$(this).addClass("input_error");
updateButton();
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PASSWORD).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PASSWORD).granted(1));
tag.find(".virtualserver_maxclients").change(function (this: HTMLInputElement) {
properties.virtualserver_maxclients = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_MAXCLIENTS).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_MAXCLIENTS).granted(1));
tag.find(".virtualserver_reserved_slots").change(function (this: HTMLInputElement) {
properties.virtualserver_reserved_slots = this.valueAsNumber;
@ -80,17 +82,17 @@ namespace Modals {
if(this.valueAsNumber > properties.virtualserver_maxclients)
$(this).addClass("input_error");
updateButton();
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS).granted(1));
tag.find(".virtualserver_welcomemessage").change(function (this: HTMLInputElement) {
properties.virtualserver_welcomemessage = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE).granted(1));
tag.find(".button-select-icon").on('click', event => {
Modals.spawnIconSelect(globalClient, id => {
Modals.spawnIconSelect(connection_handler, id => {
const icon_node = tag.find(".button-select-icon").find(".icon-node");
icon_node.empty();
icon_node.append(globalClient.fileManager.icons.generateTag(id));
icon_node.append(connection_handler.fileManager.icons.generateTag(id));
console.log("Selected icon ID: %d", id);
properties.virtualserver_icon_id = id;
@ -100,54 +102,56 @@ namespace Modals {
function server_applyHostListener(server: ServerEntry, properties: ServerProperties, original_properties: ServerProperties, tag: JQuery, button: JQuery) {
const connection_handler = server.channelTree.client;
tag.find(".virtualserver_host").change(function (this: HTMLInputElement) {
properties.virtualserver_host = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOST).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOST).granted(1));
tag.find(".virtualserver_port").change(function (this: HTMLInputElement) {
properties.virtualserver_port = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PORT).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PORT).granted(1));
tag.find(".virtualserver_hostmessage").change(function (this: HTMLInputElement) {
properties.virtualserver_hostmessage = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1));
tag.find(".virtualserver_hostmessage_mode").change(function (this: HTMLSelectElement) {
properties.virtualserver_hostmessage_mode = this.selectedIndex;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1))
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1))
.find("option").eq(original_properties.virtualserver_hostmessage_mode).prop('selected', true);
tag.find(".virtualserver_hostbanner_url").change(function (this: HTMLInputElement) {
properties.virtualserver_hostbanner_url = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1));
tag.find(".virtualserver_hostbanner_gfx_url").change(function (this: HTMLInputElement) {
properties.virtualserver_hostbanner_gfx_url = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1));
tag.find(".virtualserver_hostbanner_gfx_interval").change(function (this: HTMLInputElement) {
properties.virtualserver_hostbanner_gfx_interval = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1));
tag.find(".virtualserver_hostbanner_mode").change(function (this: HTMLSelectElement) {
properties.virtualserver_hostbanner_mode = this.selectedIndex;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1))
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1))
.find("option").eq(original_properties.virtualserver_hostbanner_mode).prop('selected', true);
tag.find(".virtualserver_hostbutton_tooltip").change(function (this: HTMLInputElement) {
properties.virtualserver_hostbutton_tooltip = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1));
tag.find(".virtualserver_hostbutton_url").change(function (this: HTMLInputElement) {
properties.virtualserver_hostbutton_url = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1));
tag.find(".virtualserver_hostbutton_gfx_url").change(function (this: HTMLInputElement) {
properties.virtualserver_hostbutton_gfx_url = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1));
server.updateProperties().then(() => {
tag.find(".virtualserver_host").val(server.properties.virtualserver_host);
@ -156,6 +160,8 @@ namespace Modals {
}
function server_applyMessages(properties: ServerProperties, server: ServerEntry, tag: JQuery) {
const connection_handler = server.channelTree.client;
server.updateProperties().then(() => {
tag.find(".virtualserver_default_client_description").val(server.properties.virtualserver_default_client_description);
tag.find(".virtualserver_default_channel_description").val(server.properties.virtualserver_default_channel_description);
@ -164,18 +170,20 @@ namespace Modals {
tag.find(".virtualserver_default_client_description").change(function (this: HTMLInputElement) {
properties.virtualserver_default_client_description = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1));
tag.find(".virtualserver_default_channel_description").change(function (this: HTMLInputElement) {
properties.virtualserver_default_channel_description = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1));
tag.find(".virtualserver_default_channel_topic").change(function (this: HTMLInputElement) {
properties.virtualserver_default_channel_topic = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1));
}
function server_applyFlood(properties: ServerProperties, server: ServerEntry, tag: JQuery) {
const connection_handler = server.channelTree.client;
server.updateProperties().then(() => {
tag.find(".virtualserver_antiflood_points_tick_reduce").val(server.properties.virtualserver_antiflood_points_tick_reduce);
tag.find(".virtualserver_antiflood_points_needed_command_block").val(server.properties.virtualserver_antiflood_points_needed_command_block);
@ -184,34 +192,38 @@ namespace Modals {
tag.find(".virtualserver_antiflood_points_tick_reduce").change(function (this: HTMLInputElement) {
properties.virtualserver_antiflood_points_tick_reduce = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
tag.find(".virtualserver_antiflood_points_needed_command_block").change(function (this: HTMLInputElement) {
properties.virtualserver_antiflood_points_needed_command_block = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
tag.find(".virtualserver_antiflood_points_needed_ip_block").change(function (this: HTMLInputElement) {
properties.virtualserver_antiflood_points_needed_ip_block = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
}
function server_applySecurity(properties: ServerProperties, server: ServerEntry, tag: JQuery) {
const connection_handler = server.channelTree.client;
server.updateProperties().then(() => {
tag.find(".virtualserver_needed_identity_security_level").val(server.properties.virtualserver_needed_identity_security_level);
});
tag.find(".virtualserver_needed_identity_security_level").change(function (this: HTMLInputElement) {
properties.virtualserver_needed_identity_security_level = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL).granted(1));
tag.find(".virtualserver_codec_encryption_mode").change(function (this: HTMLSelectElement) {
properties.virtualserver_codec_encryption_mode = this.selectedIndex;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1))
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1))
.find("option").eq(server.properties.virtualserver_codec_encryption_mode).prop('selected', true);
}
function server_applyMisc(properties: ServerProperties, server: ServerEntry, tag: JQuery) {
const connection_handler = server.channelTree.client;
{ //TODO notify on tmp channeladmin group and vice versa
{
let groups_tag = tag.find(".default_server_group");
@ -293,37 +305,38 @@ namespace Modals {
tag.find(".virtualserver_antiflood_points_needed_ip_block").change(function (this: HTMLInputElement) {
properties.virtualserver_antiflood_points_needed_ip_block = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
tag.find(".virtualserver_antiflood_points_needed_command_block").change(function (this: HTMLInputElement) {
properties.virtualserver_antiflood_points_needed_command_block = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
tag.find(".virtualserver_antiflood_points_tick_reduce").change(function (this: HTMLInputElement) {
properties.virtualserver_antiflood_points_tick_reduce = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1));
tag.find(".virtualserver_complain_autoban_count").change(function (this: HTMLInputElement) {
properties.virtualserver_complain_autoban_count = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1));
tag.find(".virtualserver_complain_autoban_time").change(function (this: HTMLInputElement) {
properties.virtualserver_complain_autoban_time = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1));
tag.find(".virtualserver_complain_remove_time").change(function (this: HTMLInputElement) {
properties.virtualserver_complain_remove_time = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1));
tag.find(".virtualserver_weblist_enabled").change(function (this: HTMLInputElement) {
properties.virtualserver_weblist_enabled = $(this).prop("checked");
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WEBLIST).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WEBLIST).granted(1));
}
function server_applyTransferListener(properties: ServerProperties, server: ServerEntry, tag: JQuery) {
const connection_handler = server.channelTree.client;
server.updateProperties().then(() => {
//virtualserver_max_upload_total_bandwidth
//virtualserver_upload_quota
@ -339,16 +352,16 @@ namespace Modals {
tag.find(".virtualserver_max_upload_total_bandwidth").change(function (this: HTMLInputElement) {
properties.virtualserver_max_upload_total_bandwidth = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1));
tag.find(".virtualserver_max_download_total_bandwidth").change(function (this: HTMLInputElement) {
properties.virtualserver_max_download_total_bandwidth = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1));
tag.find(".virtualserver_upload_quota").change(function (this: HTMLInputElement) {
properties.virtualserver_upload_quota = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1));
tag.find(".virtualserver_download_quota").change(function (this: HTMLInputElement) {
properties.virtualserver_download_quota = this.valueAsNumber;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1));
}).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1));
}
}

View File

@ -1,8 +1,6 @@
/// <reference path="../../../declarations/imports_client.d.ts" />
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../utils/tab.ts" />
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../voice/AudioController.ts" />
/// <reference path="../../voice/VoiceClient.ts" />
/// <reference path="../../profiles/Identity.ts" />
namespace Modals {
@ -207,7 +205,7 @@ namespace Modals {
client: native_client,
valid_forum_identity: profiles.identities.valid_static_forum_identity(),
forum_path: settings.static("forum_path"),
voice_available: !!globalClient.voiceConnection
voice_available: !settings.static_global(Settings.KEY_DISABLE_VOICE, false)
});
initialiseVoiceListeners(modal, (template = template.tabify()).find(".settings_audio"));
@ -262,7 +260,7 @@ namespace Modals {
.text(message);
};
if (globalClient.voiceConnection) {
if (!settings.static_global(Settings.KEY_DISABLE_VOICE, false)) {
{ //Initialized voice activation detection
const vad_tag = tag.find(".settings-vad-container");
@ -274,7 +272,7 @@ namespace Modals {
}
{
settings.changeGlobal("vad_type", select.value);
globalClient.voiceConnection.voiceRecorder.reinitialiseVAD();
voice_recoder.reinitialiseVAD();
}
switch (select.value) {
@ -288,10 +286,10 @@ namespace Modals {
break;
case "vad":
let slider = vad_tag.find(".vad_vad_slider");
let vad: VoiceActivityDetectorVAD = globalClient.voiceConnection.voiceRecorder.getVADHandler() as VoiceActivityDetectorVAD;
let vad: VoiceActivityDetectorVAD = voice_recoder.getVADHandler() as VoiceActivityDetectorVAD;
slider.val(vad.percentageThreshold);
slider.trigger("change");
globalClient.voiceConnection.voiceRecorder.update(true);
voice_recoder.set_recording(true);
vad.percentage_listener = per => {
vad_tag.find(".vad_vad_bar_filler")
.css("width", (100 - per) + "%");
@ -323,7 +321,7 @@ namespace Modals {
Object.assign(ppt_settings, event);
settings.changeGlobal('vad_ppt_settings', ppt_settings);
globalClient.voiceConnection.voiceRecorder.reinitialiseVAD();
voice_recoder.reinitialiseVAD();
ppt.unregister_key_listener(listener);
modal.close();
@ -340,7 +338,7 @@ namespace Modals {
ppt_settings.delay = (<HTMLInputElement>event.target).valueAsNumber;
settings.changeGlobal('vad_ppt_settings', ppt_settings);
globalClient.voiceConnection.voiceRecorder.reinitialiseVAD();
voice_recoder.reinitialiseVAD();
});
}
@ -348,13 +346,13 @@ namespace Modals {
let slider = vad_tag.find(".vad_vad_slider");
slider.on("input change", () => {
settings.changeGlobal("vad_threshold", slider.val().toString());
let vad = globalClient.voiceConnection.voiceRecorder.getVADHandler();
let vad = voice_recoder.getVADHandler();
if (vad instanceof VoiceActivityDetectorVAD)
vad.percentageThreshold = slider.val() as number;
vad_tag.find(".vad_vad_slider_value").text(slider.val().toString());
});
modal.properties.registerCloseListener(() => {
let vad = globalClient.voiceConnection.voiceRecorder.getVADHandler();
let vad = voice_recoder.getVADHandler();
if (vad instanceof VoiceActivityDetectorVAD)
vad.percentage_listener = undefined;
@ -386,7 +384,7 @@ namespace Modals {
.appendTo(tag_select);
navigator.mediaDevices.enumerateDevices().then(devices => {
const active_device = globalClient.voiceConnection.voiceRecorder.device_id();
const active_device = voice_recoder.device_id();
for (const device of devices) {
console.debug(tr("Got device %s (%s): %s (%o)"), device.deviceId, device.kind, device.label);
@ -416,7 +414,7 @@ namespace Modals {
let deviceId = selected_tag.attr("device-id");
let groupId = selected_tag.attr("device-group");
console.log(tr("Selected microphone device: id: %o group: %o"), deviceId, groupId);
globalClient.voiceConnection.voiceRecorder.change_device(deviceId, groupId);
voice_recoder.change_device(deviceId, groupId);
});
}
@ -519,7 +517,7 @@ namespace Modals {
});
entry.find(".button-playback").on('click', event => {
sound.play(sound_name as Sound);
sound.manager.play(sound_name as Sound);
});
entry_tag.append(entry);

View File

@ -1,4 +1,4 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../ui/elements/modal.ts" />
namespace Modals {
export function spawnYesNo(header: BodyCreator, body: BodyCreator, callback: (_: boolean) => any, properties?: {

View File

@ -141,13 +141,13 @@ class ServerEntry {
name: tr("Show server info"),
callback: () => {
trigger_close = false;
this.channelTree.client.selectInfo.open_popover()
this.channelTree.client.select_info.open_popover()
},
icon: "client-about",
visible: this.channelTree.client.selectInfo.is_popover()
visible: this.channelTree.client.select_info.is_popover()
}, {
type: MenuEntryType.HR,
visible: this.channelTree.client.selectInfo.is_popover(),
visible: this.channelTree.client.select_info.is_popover(),
name: ''
}, {
type: MenuEntryType.ENTRY,
@ -159,7 +159,7 @@ class ServerEntry {
console.log(tr("Changed properties: %o"), properties);
if (properties)
this.channelTree.client.serverConnection.send_command("serveredit", properties).then(() => {
sound.play(Sound.SERVER_EDITED_SELF);
this.channelTree.client.sound.play(Sound.SERVER_EDITED_SELF);
});
});
}
@ -210,6 +210,8 @@ class ServerEntry {
if(variable.key == "virtualserver_name") {
this.htmlTag.find(".name").text(variable.value);
this.channelTree.client.tag_connection_handler.find(".server-name").text(variable.value);
server_connections.update_ui();
} else if(variable.key == "virtualserver_icon_id") {
if(this.channelTree.client.fileManager && this.channelTree.client.fileManager.icons)
this.htmlTag.find(".icon_property").replaceWith(this.channelTree.client.fileManager.icons.generateTag(this.properties.virtualserver_icon_id).addClass("icon_property"));
@ -218,7 +220,7 @@ class ServerEntry {
}
}
if(update_bannner)
this.channelTree.client.selectInfo.update_banner();
this.channelTree.client.select_info.update_banner();
group.end();
if(is_self_notify && this.info_request_promise_resolve) {

View File

@ -1,6 +1,5 @@
/// <reference path="../voice/VoiceHandler.ts" />
/// <reference path="../client.ts" />
/// <reference path="../contextMenu.ts" />
/// <reference path="../ConnectionHandler.ts" />
/// <reference path="../proto.ts" />
/// <reference path="channel.ts" />
/// <reference path="client.ts" />
@ -8,10 +7,9 @@
class ChannelTree {
client: TSClient;
htmlTree: JQuery;
htmlTree_parent: JQuery;
client: ConnectionHandler;
server: ServerEntry;
channels: ChannelEntry[];
clients: ClientEntry[];
@ -19,6 +17,9 @@ class ChannelTree {
currently_selected_context_callback: (event) => any = undefined;
readonly client_mover: ClientMover;
private _tag_container: JQuery;
private _tag_entries: JQuery;
private _tree_detached: boolean = false;
private _show_queries: boolean;
private channel_last?: ChannelEntry;
@ -26,18 +27,19 @@ class ChannelTree {
private selected_event?: Event;
constructor(client, htmlTree) {
constructor(client) {
document.addEventListener("touchstart", function(){}, true);
this.client = client;
this.htmlTree = htmlTree;
this.htmlTree_parent = this.htmlTree.parent();
this._tag_container = $.spawn("div").addClass("channel-tree-container");
this._tag_entries = $.spawn("div").addClass("channel-tree");
this.client_mover = new ClientMover(this);
this.reset();
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
this.htmlTree.parent().on("contextmenu", (event) => {
this._tag_container.on("contextmenu", (event) => {
if(event.isDefaultPrevented()) return;
for(const element of document.elementsFromPoint(event.pageX, event.pageY))
@ -54,7 +56,7 @@ class ChannelTree {
});
}
this.htmlTree.on('resize', this.handle_resized.bind(this));
this._tag_container.on('resize', this.handle_resized.bind(this));
/* TODO release these events again when ChannelTree get deinitialized */
$(document).on('click', event => {
@ -62,19 +64,23 @@ class ChannelTree {
this.selected_event = undefined;
});
$(document).on('keydown', this.handle_key_press.bind(this));
this.htmlTree.on('click', event => {{
this._tag_container.on('click', event => {{
this.selected_event = event.originalEvent;
}});
}
tag_tree() : JQuery {
return this._tag_container;
}
hide_channel_tree() {
this.htmlTree.detach();
this._tag_entries.detach();
this._tree_detached = true;
}
show_channel_tree() {
this._tree_detached = false;
this.htmlTree.appendTo(this.htmlTree_parent);
this._tag_entries.appendTo(this._tag_container);
this.channels.forEach(e => e.recalculate_repetitive_name());
}
@ -99,14 +105,14 @@ class ChannelTree {
initialiseHead(serverName: string, address: ServerAddress) {
this.server = new ServerEntry(this, serverName, address);
this.server.htmlTag.appendTo(this.htmlTree);
this.server.htmlTag.appendTo(this._tag_entries);
this.server.initializeListener();
}
private __deleteAnimation(element: ChannelEntry | ClientEntry) {
let tag = element instanceof ChannelEntry ? element.rootTag() : element.tag;
this.htmlTree.find(tag).fadeOut("slow", () => {
tag.remove();
tag.fadeOut("slow", () => {
tag.detach();
});
}
@ -152,7 +158,7 @@ class ChannelTree {
this.channels.push(channel);
let elm = undefined;
let tag = this.htmlTree;
let tag = this._tag_entries;
let previous_channel = null;
if(channel.hasParent()) {
@ -266,7 +272,7 @@ class ChannelTree {
channel.channel_next.channel_previous = channel;
}
} else {
this.htmlTree.find(".server").after(channel.rootTag());
this._tag_entries.find(".server").after(channel.rootTag());
channel.channel_next = this.channel_first;
if(this.channel_first)
@ -290,24 +296,45 @@ class ChannelTree {
}
}
deleteClient(client: ClientEntry) {
deleteClient(client: ClientEntry, animate_tag?: boolean) {
this.clients.remove(client);
this.__deleteAnimation(client);
if(typeof(animate_tag) !== "boolean" || animate_tag)
this.__deleteAnimation(client);
else
client.tag.detach();
client.onDelete();
const voice_connection = this.client.serverConnection.voice_connection();
if(client.get_audio_handle()) {
if(!voice_connection) {
log.warn(LogCategory.VOICE, tr("Deleting client with a voice handle, but we haven't a voice connection!"));
} else {
voice_connection.unregister_client(client.get_audio_handle());
}
}
client.set_audio_handle(undefined); /* just to be sure */
}
registerClient(client: ClientEntry) {
this.clients.push(client);
client.channelTree = this;
const voice_connection = this.client.serverConnection.voice_connection();
if(voice_connection)
client.set_audio_handle(voice_connection.register_client(client.clientId()));
}
insertClient(client: ClientEntry, channel: ChannelEntry) : ClientEntry {
let newClient = this.findClient(client.clientId());
if(newClient) client = newClient; //Got new client :)
else
this.clients.push(client);
if(newClient)
client = newClient; //Got new client :)
else {
this.registerClient(client);
}
client.channelTree = this;
client["_channel"] = channel;
let tag = client.tag;
if(!this._show_queries && client.properties.client_type == ClientType.CLIENT_QUERY)
client.tag.hide();
else if(!this._tree_detached)
@ -321,11 +348,6 @@ class ChannelTree {
return client;
}
registerClient(client: ClientEntry) {
this.clients.push(client);
client.channelTree = this;
}
moveClient(client: ClientEntry, channel: ChannelEntry) {
let oldChannel = client.currentChannel();
client["_channel"] = channel;
@ -397,7 +419,7 @@ class ChannelTree {
if(!$.isArray(this.currently_selected) || enforce_single) {
this.currently_selected = entry;
this.htmlTree.find(".selected").each(function (idx, e) {
this._tag_entries.find(".selected").each(function (idx, e) {
$(e).removeClass("selected");
});
} else {
@ -427,7 +449,7 @@ class ChannelTree {
else if(entry instanceof ServerEntry)
(entry as ServerEntry).htmlTag.addClass("selected");
this.client.selectInfo.setCurrentSelected($.isArray(this.currently_selected) ? undefined : entry);
this.client.select_info.setCurrentSelected($.isArray(this.currently_selected) ? undefined : entry);
}
private callback_multiselect_channel(event) {
@ -527,7 +549,7 @@ class ChannelTree {
}, {
flagset: [data.no_ip ? "no-ip" : "", data.no_hwid ? "no-hardware-id" : "", data.no_name ? "no-nickname" : ""]
}).then(() => {
sound.play(Sound.USER_BANNED);
this.client.sound.play(Sound.USER_BANNED);
});
});
}
@ -586,14 +608,14 @@ class ChannelTree {
this.server = null;
this.clients = [];
this.channels = [];
this.htmlTree.children().detach(); //Do not remove the listener!
this._tag_entries.children().detach(); //Do not remove the listener!
this.channel_first = undefined;
this.channel_last = undefined;
}
spawnCreateChannel(parent?: ChannelEntry) {
Modals.createChannelModal(undefined, parent, this.client.permissions, (properties?, permissions?) => {
Modals.createChannelModal(this.client, undefined, parent, this.client.permissions, (properties?, permissions?) => {
if(!properties) return;
properties["cpid"] = parent ? parent.channelId : 0;
log.debug(LogCategory.CHANNEL, tr("Creating a new channel.\nProperties: %o\nPermissions: %o"), properties);
@ -622,8 +644,8 @@ class ChannelTree {
return new Promise<ChannelEntry>(resolve => { resolve(channel); })
}).then(channel => {
chat.serverChat().appendMessage(tr("Channel {} successfully created!"), true, channel.generate_tag(true));
sound.play(Sound.CHANNEL_CREATED);
this.client.chat.serverChat().appendMessage(tr("Channel {} successfully created!"), true, channel.generate_tag(true));
this.client.sound.play(Sound.CHANNEL_CREATED);
});
});
}

View File

@ -1,248 +0,0 @@
enum PlayerState {
PREBUFFERING,
PLAYING,
BUFFERING,
STOPPING,
STOPPED
}
class AudioController {
private static _audioInstances: AudioController[] = [];
private static _globalReplayScheduler: NodeJS.Timer;
private static _timeIndex: number = 0;
private static _audioDestinationStream: MediaStream;
static initializeAudioController() {
if(!audio.player.initialize())
console.warn(tr("Failed to initialize audio controller!"));
sound.initialize().then(() => {
console.log(tr("Sounds initialitzed"));
});
//this._globalReplayScheduler = setInterval(() => { AudioController.invokeNextReplay(); }, 20); //Fix me
}
/*
private static joinTracks(tracks: AudioBuffer[]) : Promise<AudioBuffer> {
let length = Math.max.apply(Math, tracks.map(e => e.length));
if(length == 0 || tracks.length == 0) return new Promise<AudioBuffer>((resolve, reject) => {}); //Do nothink
let context = new OfflineAudioContext(2, length, 44100);
//let context = new OfflineAudioContext(tracks[0].numberOfChannels, tracks[0].length, tracks[0].sampleRate);
tracks.forEach(track => {
let player = context.createBufferSource();
player.buffer = track;
player.connect(context.destination);
player.start(0);
});
return context.startRendering();
}
static invokeNextReplay() {
let replay: {controller: AudioController,buffer: AudioBuffer}[] = [];
for(let instance of this._audioInstances)
if(instance.playerState == PlayerState.PLAYING || instance.playerState == PlayerState.STOPPING) {
let entry = {controller: instance, buffer: instance.audioCache.pop_front() };
instance.flagPlaying = !!entry.buffer;
instance.testBufferQueue();
if(!!entry.buffer) replay.push(entry);
} else if(instance.flagPlaying) {
instance.flagPlaying = false;
instance.testBufferQueue();
}
this.joinTracks(replay.map(e => e.buffer)).then(buffer => {
if(this._timeIndex < this._globalContext.currentTime) {
this._timeIndex = this._globalContext.currentTime;
console.log("Resetting time index!");
}
//console.log(buffer.length + "|" + buffer.duration);
//console.log(buffer);
let player = this._globalContext.createBufferSource();
player.buffer = buffer;
player.connect(this._globalContext.destination);
player.start(this._timeIndex);
this._timeIndex += buffer.duration;
});
}
*/
speakerContext: AudioContext;
private playerState: PlayerState = PlayerState.STOPPED;
private audioCache: AudioBuffer[] = [];
private playingAudioCache: AudioBufferSourceNode[] = [];
private _volume: number = 1;
private _codecCache: CodecClientCache[] = [];
private _timeIndex: number = 0;
private _latencyBufferLength: number = 3;
private _buffer_timeout: NodeJS.Timer;
allowBuffering: boolean = true;
//Events
onSpeaking: () => void;
onSilence: () => void;
constructor() {
audio.player.on_ready(() => this.speakerContext = audio.player.context());
this.onSpeaking = function () { };
this.onSilence = function () { };
}
public initialize() {
AudioController._audioInstances.push(this);
}
public close(){
AudioController._audioInstances.remove(this);
}
playBuffer(buffer: AudioBuffer) {
if(!buffer) {
console.warn(tr("[AudioController] Got empty or undefined buffer! Dropping it"));
return;
}
if(!this.speakerContext) {
console.warn(tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!"));
return;
}
if (buffer.sampleRate != this.speakerContext.sampleRate)
console.warn(tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate);
this.applyVolume(buffer);
this.audioCache.push(buffer);
if(this.playerState == PlayerState.STOPPED || this.playerState == PlayerState.STOPPING) {
console.log(tr("[Audio] Starting new playback"));
this.playerState = PlayerState.PREBUFFERING;
//New audio
}
switch (this.playerState) {
case PlayerState.PREBUFFERING:
case PlayerState.BUFFERING:
this.reset_buffer_timeout(true); //Reset timeout, we got a new buffer
if(this.audioCache.length <= this._latencyBufferLength) {
if(this.playerState == PlayerState.BUFFERING) {
if(this.allowBuffering) break;
} else break;
}
if(this.playerState == PlayerState.PREBUFFERING) {
console.log(tr("[Audio] Prebuffering succeeded (Replaying now)"));
this.onSpeaking();
} else {
if(this.allowBuffering)
console.log(tr("[Audio] Buffering succeeded (Replaying now)"));
}
this.playerState = PlayerState.PLAYING;
case PlayerState.PLAYING:
this.playQueue();
break;
default:
break;
}
}
private playQueue() {
let buffer: AudioBuffer;
while((buffer = this.audioCache.pop_front())) {
if(this.playingAudioCache.length >= this._latencyBufferLength * 1.5 + 3) {
console.log(tr("Dropping buffer because playing queue grows to much"));
continue; /* drop the data (we're behind) */
}
if(this._timeIndex < this.speakerContext.currentTime) this._timeIndex = this.speakerContext.currentTime;
let player = this.speakerContext.createBufferSource();
player.buffer = buffer;
player.onended = () => this.removeNode(player);
this.playingAudioCache.push(player);
player.connect(audio.player.destination());
player.start(this._timeIndex);
this._timeIndex += buffer.duration;
}
}
private removeNode(node: AudioBufferSourceNode) {
this.playingAudioCache.remove(node);
this.testBufferQueue();
}
stopAudio(now: boolean = false) {
this.playerState = PlayerState.STOPPING;
if(now) {
this.playerState = PlayerState.STOPPED;
this.audioCache = [];
for(let entry of this.playingAudioCache)
entry.stop(0);
this.playingAudioCache = [];
}
this.testBufferQueue();
this.playQueue(); //Flush queue
}
private testBufferQueue() {
if(this.audioCache.length == 0 && this.playingAudioCache.length == 0) {
if(this.playerState != PlayerState.STOPPING && this.playerState != PlayerState.STOPPED) {
if(this.playerState == PlayerState.BUFFERING) return; //We're already buffering
this.playerState = PlayerState.BUFFERING;
if(!this.allowBuffering)
console.warn(tr("[Audio] Detected a buffer underflow!"));
this.reset_buffer_timeout(true);
} else {
this.playerState = PlayerState.STOPPED;
this.onSilence();
}
}
}
private reset_buffer_timeout(restart: boolean) {
if(this._buffer_timeout)
clearTimeout(this._buffer_timeout);
if(restart)
this._buffer_timeout = setTimeout(() => {
if(this.playerState == PlayerState.PREBUFFERING || this.playerState == PlayerState.BUFFERING) {
console.warn(tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay"));
this.stopAudio();
}
this._buffer_timeout = undefined;
}, 1000);
}
get volume() : number { return this._volume; }
set volume(val: number) {
if(this._volume == val) return;
this._volume = val;
for(let buffer of this.audioCache)
this.applyVolume(buffer);
}
private applyVolume(buffer: AudioBuffer) {
if(this._volume == 1) return;
for(let channel = 0; channel < buffer.numberOfChannels; channel++) {
let data = buffer.getChannelData(channel);
for(let sample = 0; sample < data.length; sample++) {
let lane = data[sample];
lane *= this._volume;
data[sample] = lane;
}
}
}
codecCache(codec: number) : CodecClientCache {
while(this._codecCache.length <= codec)
this._codecCache.push(new CodecClientCache());
return this._codecCache[codec];
}
}

View File

@ -0,0 +1,216 @@
/// <reference path="../connection/ConnectionBase.ts" />
namespace audio {
export namespace js {
export class VoiceClientController implements connection.voice.VoiceClient {
callback_playback: () => any;
callback_state_changed: (new_state: connection.voice.PlayerState) => any;
callback_stopped: () => any;
client_id: number;
speakerContext: AudioContext;
private _player_state: connection.voice.PlayerState = connection.voice.PlayerState.STOPPED;
private _codecCache: CodecClientCache[] = [];
private _time_index: number = 0;
private _latency_buffer_length: number = 3;
private _buffer_timeout: NodeJS.Timer;
private _buffered_samples: AudioBuffer[] = [];
private _playing_nodes: AudioBufferSourceNode[] = [];
private _volume: number = 1;
allowBuffering: boolean = true;
constructor(client_id: number) {
this.client_id = client_id;
audio.player.on_ready(() => this.speakerContext = audio.player.context());
}
public initialize() { }
public close(){ }
playback_buffer(buffer: AudioBuffer) {
if(!buffer) {
console.warn(tr("[AudioController] Got empty or undefined buffer! Dropping it"));
return;
}
if(!this.speakerContext) {
console.warn(tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!"));
return;
}
if (buffer.sampleRate != this.speakerContext.sampleRate)
console.warn(tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate);
this.apply_volume_to_buffer(buffer);
this._buffered_samples.push(buffer);
if(this._player_state == connection.voice.PlayerState.STOPPED || this._player_state == connection.voice.PlayerState.STOPPING) {
console.log(tr("[Audio] Starting new playback"));
this.set_state(connection.voice.PlayerState.PREBUFFERING);
}
switch (this._player_state) {
case connection.voice.PlayerState.PREBUFFERING:
case connection.voice.PlayerState.BUFFERING:
this.reset_buffer_timeout(true); //Reset timeout, we got a new buffer
if(this._buffered_samples.length <= this._latency_buffer_length) {
if(this._player_state == connection.voice.PlayerState.BUFFERING) {
if(this.allowBuffering)
break;
} else
break;
}
if(this._player_state == connection.voice.PlayerState.PREBUFFERING) {
console.log(tr("[Audio] Prebuffering succeeded (Replaying now)"));
if(this.callback_playback)
this.callback_playback();
} else if(this.allowBuffering) {
console.log(tr("[Audio] Buffering succeeded (Replaying now)"));
}
this._player_state = connection.voice.PlayerState.PLAYING;
case connection.voice.PlayerState.PLAYING:
this.replay_queue();
break;
default:
break;
}
}
private replay_queue() {
let buffer: AudioBuffer;
while((buffer = this._buffered_samples.pop_front())) {
if(this._playing_nodes.length >= this._latency_buffer_length * 1.5 + 3) {
console.log(tr("Dropping buffer because playing queue grows to much"));
continue; /* drop the data (we're behind) */
}
if(this._time_index < this.speakerContext.currentTime)
this._time_index = this.speakerContext.currentTime;
const player = this.speakerContext.createBufferSource();
player.buffer = buffer;
player.onended = () => this.on_buffer_replay_finished(player);
this._playing_nodes.push(player);
player.connect(audio.player.destination());
player.start(this._time_index);
this._time_index += buffer.duration;
}
}
private on_buffer_replay_finished(node: AudioBufferSourceNode) {
this._playing_nodes.remove(node);
this.test_buffer_queue();
}
stopAudio(now: boolean = false) {
this._player_state = connection.voice.PlayerState.STOPPING;
if(now) {
this._player_state = connection.voice.PlayerState.STOPPED;
this._buffered_samples = [];
for(const entry of this._playing_nodes)
entry.stop(0);
this._playing_nodes = [];
if(this.callback_stopped)
this.callback_stopped();
} else {
this.test_buffer_queue(); /* test if we're not already done */
this.replay_queue(); /* flush the queue */
}
}
private test_buffer_queue() {
if(this._buffered_samples.length == 0 && this._playing_nodes.length == 0) {
if(this._player_state != connection.voice.PlayerState.STOPPING && this._player_state != connection.voice.PlayerState.STOPPED) {
if(this._player_state == connection.voice.PlayerState.BUFFERING)
return; //We're already buffering
this._player_state = connection.voice.PlayerState.BUFFERING;
if(!this.allowBuffering)
console.warn(tr("[Audio] Detected a buffer underflow!"));
this.reset_buffer_timeout(true);
} else {
this._player_state = connection.voice.PlayerState.STOPPED;
if(this.callback_stopped)
this.callback_stopped();
}
}
}
private reset_buffer_timeout(restart: boolean) {
if(this._buffer_timeout)
clearTimeout(this._buffer_timeout);
if(restart)
this._buffer_timeout = setTimeout(() => {
if(this._player_state == connection.voice.PlayerState.PREBUFFERING || this._player_state == connection.voice.PlayerState.BUFFERING) {
console.warn(tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay"));
this.stopAudio();
}
this._buffer_timeout = undefined;
}, 1000);
}
private apply_volume_to_buffer(buffer: AudioBuffer) {
if(this._volume == 1)
return;
for(let channel = 0; channel < buffer.numberOfChannels; channel++) {
let data = buffer.getChannelData(channel);
for(let sample = 0; sample < data.length; sample++) {
let lane = data[sample];
lane *= this._volume;
data[sample] = lane;
}
}
}
private set_state(state: connection.voice.PlayerState) {
if(this._player_state == state)
return;
this._player_state = state;
if(this.callback_state_changed)
this.callback_state_changed(this._player_state);
}
get_codec_cache(codec: number) : CodecClientCache {
while(this._codecCache.length <= codec)
this._codecCache.push(new CodecClientCache());
return this._codecCache[codec];
}
get_state(): connection.voice.PlayerState {
return this._player_state;
}
get_volume(): number {
return this._volume;
}
set_volume(volume: number): void {
if(this._volume == volume)
return;
this._volume = volume;
/* apply the volume to all other buffers */
for(const buffer of this._buffered_samples)
this.apply_volume_to_buffer(buffer);
}
abort_replay() {
this.stopAudio(true);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/// <reference path="VoiceHandler.ts" />
/// <reference path="../utils/modal.ts" />
/// <reference path="../ui/elements/modal.ts" />
abstract class VoiceActivityDetector {
protected handle: VoiceRecorder;
@ -34,17 +34,25 @@ if(!AudioBuffer.prototype.copyToChannel) { //Webkit does not implement this func
}
}
let voice_recoder: VoiceRecorder;
class VoiceRecorder {
private static readonly CHANNEL = 0;
private static readonly CHANNELS = 2;
private static readonly BUFFER_SIZE = 1024 * 4;
handle: VoiceConnection;
on_support_state_change: () => any;
on_data: (data: AudioBuffer, head: boolean) => void = undefined;
on_end: () => any;
on_start: () => any;
on_yield: () => any; /* called when owner looses ownership */
owner: connection.voice.AbstractVoiceConnection | undefined;
private on_ready_callbacks: (() => any)[] = [];
private _recording: boolean = false;
private _recording_supported: boolean = true; /* recording is supported until anything else had been set */
private _tag_favicon: JQuery;
private microphoneStream: MediaStreamAudioSourceNode = undefined;
private mediaStream: MediaStream = undefined;
@ -59,9 +67,9 @@ class VoiceRecorder {
private _deviceId: string;
private _deviceGroup: string;
constructor(handle: VoiceConnection) {
this.handle = handle;
private current_handler: ConnectionHandler;
constructor() {
this._deviceId = settings.global("microphone_device_id", "default");
this._deviceGroup = settings.global("microphone_device_group", "default");
@ -72,8 +80,8 @@ class VoiceRecorder {
const empty_buffer = this.audioContext.createBuffer(VoiceRecorder.CHANNELS, VoiceRecorder.BUFFER_SIZE, 48000);
this.processor.addEventListener('audioprocess', ev => {
if(this.microphoneStream && this.vadHandler.shouldRecord(ev.inputBuffer)) {
if(this._chunkCount == 0 && this.on_start)
this.on_start();
if(this._chunkCount == 0)
this.on_voice_start();
if(this.on_data)
this.on_data(ev.inputBuffer, this._chunkCount == 0);
@ -83,8 +91,8 @@ class VoiceRecorder {
}
this._chunkCount++;
} else {
if(this._chunkCount != 0 && this.on_end)
this.on_end();
if(this._chunkCount != 0 )
this.on_voice_end();
this._chunkCount = 0;
for(let channel = 0; channel < ev.inputBuffer.numberOfChannels; channel++)
@ -96,17 +104,39 @@ class VoiceRecorder {
if(this.vadHandler)
this.vadHandler.initialise();
this.on_microphone(this.mediaStream);
for(const callback of this.on_ready_callbacks)
callback();
this.on_ready_callbacks = [];
});
this.setVADHandler(new PassThroughVAD());
this._tag_favicon = $("head link[rel='icon']");
}
available() : boolean {
return !!getUserMediaFunction() && !!getUserMediaFunction();
own_recoder(connection: connection.voice.AbstractVoiceConnection | undefined) {
if(connection === this.owner)
return;
if(this.on_yield)
this.on_yield();
this.owner = connection;
this.on_end = undefined;
this.on_start = undefined;
this.on_data = undefined;
this.on_yield = undefined;
this.on_support_state_change = undefined;
this.on_ready_callbacks = [];
this._chunkCount = 0;
if(this.processor) /* processor stream might be null because of the late audio initialisation */
this.processor.connect(this.audioContext.destination);
}
recording() : boolean {
return this._recording;
input_available() : boolean {
return !!getUserMediaFunction();
}
getMediaStream() : MediaStream {
@ -182,12 +212,22 @@ class VoiceRecorder {
return this.vadHandler;
}
update(flag: boolean) {
if(this._recording == flag) return;
if(flag) this.start(this._deviceId, this._deviceGroup);
else this.stop();
set_recording(flag_enabled: boolean) {
if(this._recording == flag_enabled)
return;
if(flag_enabled)
this.start_recording(this._deviceId, this._deviceGroup);
else
this.stop_recording();
}
clean_recording_supported() { this._recording_supported = true; }
is_recording_supported() { return this._recording_supported; }
is_recording() { return this._recording; }
device_group_id() : string { return this._deviceGroup; }
device_id() : string { return this._deviceId; }
@ -199,12 +239,12 @@ class VoiceRecorder {
settings.changeGlobal("microphone_device_id", device);
settings.changeGlobal("microphone_device_group", group);
if(this._recording) {
this.stop();
this.start(device, group);
this.stop_recording();
this.start_recording(device, group);
}
}
start(device: string, groupId: string){
start_recording(device: string, groupId: string){
this._deviceId = device;
this._deviceGroup = groupId;
@ -220,13 +260,20 @@ class VoiceRecorder {
echoCancellationType: 'browser'
}
}, this.on_microphone.bind(this), error => {
this._recording = false;
if(this._recording_supported) {
this._recording_supported = false;
if(this.on_support_state_change)
this.on_support_state_change();
}
createErrorModal(tr("Could not resolve microphone!"), tr("Could not resolve microphone!<br>Message: ") + error).open();
console.error(tr("Could not get microphone!"));
console.error(error);
});
}
stop(stop_media_stream: boolean = true){
stop_recording(stop_media_stream: boolean = true){
console.log(tr("Stop recording!"));
this._recording = false;
@ -244,13 +291,21 @@ class VoiceRecorder {
}
}
on_initialized(callback: () => any) {
if(this.processor)
callback();
else
this.on_ready_callbacks.push(callback);
}
private on_microphone(stream: MediaStream) {
const old_microphone_stream = this.microphoneStream;
if(old_microphone_stream)
this.stop(this.mediaStream != stream); //Disconnect old stream
this.stop_recording(this.mediaStream != stream); //Disconnect old stream
this.mediaStream = stream;
if(!this.mediaStream) return;
if(!this.mediaStream)
return;
if(!this.audioContext) {
console.log(tr("[VoiceRecorder] Got microphone stream, but havn't a audio context. Waiting until its initialized"));
@ -261,6 +316,24 @@ class VoiceRecorder {
this.microphoneStream.connect(this.processor);
if(this.vadHandler)
this.vadHandler.initialiseNewStream(old_microphone_stream, this.microphoneStream);
if(!this._recording_supported) {
this._recording_supported = true;
if(this.on_support_state_change)
this.on_support_state_change();
}
}
private on_voice_start() {
this._tag_favicon.attr('href', "img/favicon/speaking.png");
if(this.on_start)
this.on_start();
}
private on_voice_end() {
this._tag_favicon.attr('href', "img/favicon/teacup.png");
if(this.on_end)
this.on_end();
}
}
class MuteVAD extends VoiceActivityDetector {

View File

@ -322,6 +322,9 @@ generators[SyntaxKind.ClassDeclaration] = (settings, stack, node: ts.ClassDeclar
};
generators[SyntaxKind.PropertySignature] = (settings, stack, node: ts.PropertySignature) => {
if(!node.type)
return node;
console.log(SyntaxKind[node.type.kind]);
let type: ts.TypeNode = node.type;
switch (node.type.kind) {

View File

@ -5,13 +5,13 @@ html, body {
width: 100%;
position: fixed;
min-height: 250px;
min-width: 250px;
//min-height: 250px;
//min-width: 250px;
}
.app-container {
display: flex;
justify-content: center;
justify-content: stretch;
position: absolute;
right: 10px;