Added a lot of new things

canary
WolverinDEV 2019-03-07 15:30:53 +01:00
parent bfb7304606
commit d9c0fa37f7
31 changed files with 963 additions and 754 deletions

View File

@ -6,7 +6,8 @@
"directories": {},
"scripts": {
"compile-sass": "sass --update .:.",
"build-worker": "tsc -p shared/js/workers/tsconfig_worker_codec.json",
"build-worker-codec": "tsc -p shared/js/workers/tsconfig_worker_codec.json",
"build-worker-pow": "tsc -p shared/js/workers/tsconfig_worker_pow.json",
"dtsgen": "node tools/dtsgen/index.js",
"trgen": "node tools/trgen/index.js",
"ttsc": "ttsc",

View File

@ -28,9 +28,14 @@ if [[ $? -ne 0 ]]; then
fi
echo "Generating web workers"
npm run build-worker
npm run build-worker-codec
if [[ $? -ne 0 ]]; then
echo "Failed to build web workers"
echo "Failed to build web worker codec"
exit 1
fi
npm run build-worker-pow
if [[ $? -ne 0 ]]; then
echo "Failed to build web worker pow"
exit 1
fi

35
setup_windows.md Normal file
View File

@ -0,0 +1,35 @@
# Setup the develop environment on windows
## 1.0 Requirements
The following tools or applications are required to develop the web client:
- [1.1 IDE](#11-ide)
- [1.2 XAMPP (apache & php)](#12-xampp-with-a-web-server-and-php)
- [1.3 NPM](#13-npm)
- [1.4 Git bash](#14-git-bash)
### 1.1 IDE
For developing TeaWeb you require and IDE.
Preferable is PHPStorm from Jetbrains because the've already build in compiling on changes.
Else you've to run the compiler to compile the TS or SCSS files to js e.g. css files.
### 1.2 XAMPP with a web server and PHP
You require PHP (grater than 5) to setup and manage the project files.
PHP is required for the index page as well.
The web server is required for browsing your final environment and watch your result.
The final environment will be found at `web/environemnt/development/`. More information about
the file structure could be found [here (TODO: link me!)]().
### 1.3 NPM
NPM min 6.X is required to develop this project.
With NPM you could easily download all required dependencies by just typing `npm install`.
IMPORTANT: NPM must be available within the PATH environment variable!
### 1.4 Git bash
For using the `.sh` build scripts you require Git Bash.
A minimum of 4.2 is recommend, but in general every version should work.
## 2.0 Development environment setup
### 2.1 Native code (codecs) (Not required)
If you dont want to develop the codec part or something related to the native
webassembly part of TeaWeb you could skip this step and follow the steps in [2.1-b](#21-b-skip-native-code-setup)
### 2.1-b Skip native code setup

View File

@ -37,6 +37,7 @@
flex-direction: row;
justify-content: stretch;
cursor: pointer;
margin-left: 0;
.server_type {
@ -193,7 +194,15 @@
}
/* all icons related to basic_icons */
.clicon {width:16px;height:16px;background:url('../../img/ts/basic_icons.png') no-repeat;background-size: 16px 608px;}
.clicon {
width:16px;
height:16px;
background:url('../../img/ts/basic_icons.png') no-repeat;
background-size: 16px 608px;
flex-grow: 0;
flex-shrink: 0;
}
.host {background-position: 0 -448px}

View File

@ -1,11 +1,16 @@
.select_info_table { }
.select_info_table tr { }
.select_info_table tr td { }
.select_info_table tr td:nth-child(1) {
.select_info_table tr td {
&:nth-child(1) {
font-weight: bold;
padding-right: 5px;
min-width: 20%;
min-width: max(35%, 20px);
}
&:nth-child(2) {
min-width: max(75%, 40px);
word-break: break-word;
}
}
.select_server {
@ -17,21 +22,18 @@
.button-update {
width: 100%;
height: 23px;
&:disabled {
color: red;
pointer-events: none;
}
&:not(:disabled) {
color: green;
}
}
.container {
max-height: 100%;
display: flex;
flex-direction: column;
padding-right: 0;
padding-left: 0;
.hostbanner {
overflow: hidden;
@ -39,18 +41,20 @@
}
}
/*
<div id="select_info" class="select_info" style="width: 100%; max-width: 100%">
<div class="container-banner"></div>
<div class="container-info"></div>
</div>
*/
.select_info {
display: flex;
flex-direction: column;
justify-content: stretch;
width: 100%;
> .close {
z-index: 500;
display: none;
position: absolute;
right: 5px;
top: 5px;
}
> div {
width: 100%;
}

View File

@ -228,8 +228,11 @@ footer .container {
}
$separator_thickness: 4px;
$small_device: 500px;
.app {
.container-app-main {
position: relative;
display: flex;
flex-direction: row;
justify-content: stretch;
@ -303,6 +306,38 @@ $separator_thickness: 4px;
}
}
@media only screen and (max-width: $small_device) {
.container-app-main {
.container-info {
display: none;
position: absolute;
width: 100%!important; /* override the seperator property */
height: 100%;
z-index: 1000;
&.shown {
display: block;
}
.select_info {
> .close {
display: block;
}
}
}
.container-channel-chat + .container-seperator {
display: none;
}
.container-channel-chat {
width: 100%!important; /* override the seperator property */
}
}
}
.container-seperator {
background: lightgray;
flex-grow: 0;
@ -328,6 +363,7 @@ $separator_thickness: 4px;
}
.icon-container {
position: relative;
display: inline-block;
height: 16px;
width: 16px;
@ -354,8 +390,6 @@ $separator_thickness: 4px;
}
html, body {
min-height: 500px;
min-width: 500px;
overflow: hidden;
}

View File

@ -2,6 +2,9 @@
width: 16px;
height: 11px;
background: url('../../../img/ts/country_icons.png'), url('../../img/ts/country_icons.png') no-repeat;
flex-shrink: 0;
flex-grow: 0;
}
.country.flag-ad {

View File

@ -8,6 +8,9 @@
width: 16px;
height: 16px;
flex-shrink: 0;
flex-grow: 0;
background: url('../../../img/client_icon_sprite.svg'), url('../../img/client_icon_sprite.svg') no-repeat;
}

View File

@ -37,6 +37,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." />
<link rel="icon" href="img/favicon/teacup.png">
<?php
if(!$WEB_CLIENT) {
@ -190,7 +191,7 @@
<footer style="<?php echo $footer_style; ?>">
<div class="container" style="display: flex; flex-direction: row; align-content: space-between;">
<div style="align-self: center; position: fixed; left: 5px;">Open source on <a href="https://github.com/TeaSpeak/TeaSpeak-Web" style="display: inline-block; position: relative">github.com</a></div>
<div style="align-self: center;">TeaSpeak Web client (<?php echo $version; ?>) by WolverinDEV</div>
<div style="align-self: center;">TeaSpeak Web (<?php echo $version; ?>) by WolverinDEV</div>
<div style="align-self: center; position: fixed; right: 5px;"><?php echo $footer_forum; ?></div>
</div>
</footer>

View File

@ -133,8 +133,11 @@
</div>
</div>
<div class="container-seperator vertical" seperator-id="seperator-main-info"></div>
<div class="main_container container-info">
<div id="select_info" class="select_info" style="width: 100%; max-width: 100%">
<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>
@ -1029,7 +1032,8 @@
<div class="group_box">
<div class="header">{{tr "Microphone" /}}</div>
<div class="content settings-microphone">
<div class="content settings-microphone {{if !voice_available}}disabled{{/if}}">
{{if voice_available}}
<div class="form-row settings-device settings-device-microphone">
<div class="form-group settings-device-select">
<label for="select-settings-microphone-device" class="bmd-label-static">{{tr "Device:" /}}</label>
@ -1088,6 +1092,9 @@
</div>
</div>
</div>
{{else}}
<div>{{tr "Voice had been disabled" /}}</div>
{{/if}}
</div>
</div>
<div class="group_box">
@ -2058,7 +2065,7 @@
<tr>
<td>{{tr "Name:" /}}</td>
<td style="display: flex; flex-direction: row">
<div style="margin-right: 3px" class="country flag-{{*:(data.property_client_country || 'xx').toLowerCase()}}"></div>
<div style="margin-right: 3px" class="country flag-{{*:(data.property_client_country || 'xx').toLowerCase()}}" title="{{*:i18n.country_name(data.property_client_country || 'XX')}}"></div>
<node key="client_name"/>
</td>
</tr>
@ -2315,7 +2322,7 @@
</table>
</div>
<button class="button-update btn_update">{{tr "Update info"/}}</button>
<button class="button-update btn btn-success">{{tr "Update info"/}}</button>
</div>
</script>
<script class="jsrender-template" id="tmpl_selected_channel" type="text/html">

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

View File

@ -50,7 +50,7 @@ enum ViewReasonId {
class TSClient {
channelTree: ChannelTree;
serverConnection: connection.ServerConnection;
voiceConnection: VoiceConnection;
voiceConnection: VoiceConnection | undefined;
fileManager: FileManager;
selectInfo: InfoBar;
permissions: PermissionManager;
@ -69,10 +69,12 @@ class TSClient {
this.fileManager = new FileManager(this);
this.permissions = new PermissionManager(this);
this.groups = new GroupManager(this);
this.voiceConnection = new VoiceConnection(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() {
@ -140,7 +142,8 @@ class TSClient {
if(this.groups.serverGroups.length == 0)
this.groups.requestGroups();
this.controlBar.updateProperties();
if(!this.voiceConnection.current_encoding_supported())
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
}
@ -270,6 +273,7 @@ class TSClient {
}
this.channelTree.reset();
if(this.voiceConnection)
this.voiceConnection.dropSession();
if(this.serverConnection) this.serverConnection.disconnect();
this.controlBar.update_connection_state();

View File

@ -81,8 +81,12 @@ namespace connection {
handleCommandServerInit(json){
//We could setup the voice channel
if( this.connection.client.voiceConnection) {
console.log(tr("Setting up voice"));
this.connection.client.voiceConnection.createSession();
} else {
console.log(tr("Skipping voice setup (No voice bridge available)"));
}
json = json[0]; //Only one bulk

View File

@ -28,12 +28,24 @@ namespace connection {
abstract disconnect(reason?: string) : Promise<void>;
abstract support_voice() : boolean;
abstract voice_connection() : AbstractVoiceConnection | undefined;
abstract voice_connection() : voice.AbstractVoiceConnection | undefined;
abstract command_handler_boss() : AbstractCommandHandlerBoss;
abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>;
}
export namespace voice {
export interface VoiceClient {
client_id: number;
callback_playback: () => any;
callback_timeout: () => any;
callback_stopped: () => any;
get_volume() : number;
set_volume(volume: number) : Promise<void>;
}
export abstract class AbstractVoiceConnection {
readonly connection: AbstractServerConnection;
@ -42,6 +54,11 @@ namespace connection {
}
abstract connected() : boolean;
abstract register_client(client_id: number) : VoiceClient;
abstract availible_clients() : VoiceClient[];
abstract unregister_client(client: VoiceClient) : Promise<void>;
}
}
export class ServerCommand {

View File

@ -202,7 +202,12 @@ namespace connection {
arguments: json["data"]
});
group.end();
} else if(json["type"] === "WebRTC") this.client.voiceConnection.handleControlPacket(json);
} else if(json["type"] === "WebRTC") {
if(this.client.voiceConnection)
this.client.voiceConnection.handleControlPacket(json);
else
console.log(tr("Dropping WebRTC command packet, because we havent a bridge."))
}
else {
console.log(tr("Unknown command type %o"), json["type"]);
}
@ -299,7 +304,7 @@ namespace connection {
return false;
}
voice_connection(): connection.AbstractVoiceConnection | undefined {
voice_connection(): connection.voice.AbstractVoiceConnection | undefined {
return undefined;
}

View File

@ -52,8 +52,9 @@ interface ContextMenuEntry {
name: (() => string) | string;
icon?: (() => string) | string | JQuery;
disabled?: boolean;
invalidPermission?: boolean;
visible?: boolean;
invalidPermission?: boolean;
sub_menu?: ContextMenuEntry[];
}
@ -96,8 +97,11 @@ function generate_tag(entry: ContextMenuEntry) : JQuery {
if(entry.disabled || entry.invalidPermission) tag.addClass("disabled");
else {
let menu = $.spawn("div").addClass("sub-menu").addClass("context-menu");
for(let e of entry.sub_menu)
for(const e of entry.sub_menu) {
if(typeof(entry.visible) === 'boolean' && !entry.visible)
continue;
menu.append(generate_tag(e));
}
menu.appendTo(tag);
}
return tag;
@ -111,7 +115,10 @@ function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) {
contextMenuCloseFn = undefined;
for(let entry of entries){
for(const entry of entries){
if(typeof(entry.visible) === 'boolean' && !entry.visible)
continue;
if(entry.type == MenuEntryType.CLOSE) {
contextMenuCloseFn = entry.callback;
} else

View File

@ -53,7 +53,7 @@ namespace loader {
DONE
}
export let allow_cached_files: boolean = false;
export let cache_tag: string | undefined;
let current_stage: Stage = Stage.INITIALIZING;
const tasks: {[key:number]:Task[]} = {};
@ -206,7 +206,7 @@ namespace loader {
document.getElementById("scripts").appendChild(tag);
tag.src = path + (allow_cached_files ? "" : "?_ts=" + Date.now());
tag.src = path + (cache_tag || "");
});
}
}
@ -315,7 +315,7 @@ namespace loader {
};
document.getElementById("style").appendChild(tag);
tag.href = path + (allow_cached_files ? "" : "?_ts=" + Date.now());
tag.href = path + (cache_tag || "");
});
}
}
@ -708,12 +708,11 @@ async function check_updates() {
if(!app_version) {
/* TODO add warning */
loader.allow_cached_files = false;
loader.cache_tag = "?_ts=" + Date.now();
return;
}
const cached_version = localStorage.getItem("cached_version");
if(!cached_version || cached_version != app_version) {
loader.allow_cached_files = false;
loader.register_task(loader.Stage.LOADED, {
priority: 0,
name: "cached version updater",
@ -721,11 +720,8 @@ async function check_updates() {
localStorage.setItem("cached_version", app_version);
}
});
/* loading screen */
return;
}
loader.allow_cached_files = true;
loader.cache_tag = "?_version=" + app_version;
}
interface Window {

View File

@ -270,6 +270,7 @@ function main() {
clearTimeout(_resize_timeout);
_resize_timeout = setTimeout(() => {
globalClient.channelTree.handle_resized();
globalClient.selectInfo.handle_resize();
}, 1000);
});

View File

@ -78,6 +78,7 @@ class StaticSettings {
class Settings extends StaticSettings {
static readonly KEY_DISABLE_CONTEXT_MENU = "disableContextMenu";
static readonly KEY_DISABLE_UNLOAD_DIALOG = "disableUnloadDialog";
static readonly KEY_DISABLE_VOICE = "disableVoice";
private static readonly UPDATE_DIRECT: boolean = true;
private cacheGlobal = {};

View File

@ -442,7 +442,21 @@ class ChannelEntry {
flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_TEMPORARY).granted(1);
}
let trigger_close = true;
spawn_context_menu(x, y, {
type: MenuEntryType.ENTRY,
name: tr("Show channel info"),
callback: () => {
trigger_close = false;
this.channelTree.client.selectInfo.open_popover()
},
icon: "client-about",
visible: this.channelTree.client.selectInfo.is_popover()
}, {
type: MenuEntryType.HR,
visible: this.channelTree.client.selectInfo.is_popover(),
name: ''
}, {
type: MenuEntryType.ENTRY,
icon: "client-channel_switch",
name: tr("<b>Switch to channel</b>"),
@ -525,7 +539,7 @@ class ChannelEntry {
invalidPermission: !channelCreate,
callback: () => this.channelTree.spawnCreateChannel()
},
MenuEntry.CLOSE(on_close)
MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
);
}
@ -639,7 +653,10 @@ 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.channelTree.client.voiceConnection.codecSupported(this.properties.channel_codec) ? $.fn.hide : $.fn.show).apply(this.channelTag().find(".icons .icon_no_sound"));
this.channelTag().find(".icons .icon_no_sound").toggle(!(
this.channelTree.client.voiceConnection &&
this.channelTree.client.voiceConnection.codecSupported(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"));
} else if(key == "channel_flag_password")

View File

@ -266,28 +266,40 @@ class ClientEntry {
}
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
const _this = this;
let trigger_close = true;
spawn_context_menu(x, y,
{
type: MenuEntryType.ENTRY,
name: tr("Show client info"),
callback: () => {
trigger_close = false;
this.channelTree.client.selectInfo.open_popover()
},
icon: "client-about",
visible: this.channelTree.client.selectInfo.is_popover()
}, {
type: MenuEntryType.HR,
visible: this.channelTree.client.selectInfo.is_popover(),
name: ''
}, {
type: MenuEntryType.ENTRY,
icon: "client-change_nickname",
name: tr("<b>Open text chat</b>"),
callback: function () {
chat.activeChat = _this.chat(true);
callback: () => {
chat.activeChat = this.chat(true);
chat.focus();
}
}, {
type: MenuEntryType.ENTRY,
icon: "client-poke",
name: tr("Poke client"),
callback: function () {
callback: () => {
createInputModal(tr("Poke client"), tr("Poke message:<br>"), text => true, result => {
if(typeof(result) === "string") {
//TODO tr
console.log("Poking client " + _this.clientNickName() + " with message " + result);
_this.channelTree.client.serverConnection.send_command("clientpoke", {
clid: _this.clientId(),
console.log("Poking client " + this.clientNickName() + " with message " + result);
this.channelTree.client.serverConnection.send_command("clientpoke", {
clid: this.clientId(),
msg: result
});
@ -298,13 +310,13 @@ class ClientEntry {
type: MenuEntryType.ENTRY,
icon: "client-edit",
name: tr("Change description"),
callback: function () {
callback: () => {
createInputModal(tr("Change client description"), tr("New description:<br>"), text => true, result => {
if(typeof(result) === "string") {
//TODO tr
console.log("Changing " + _this.clientNickName() + "'s description to " + result);
_this.channelTree.client.serverConnection.send_command("clientedit", {
clid: _this.clientId(),
console.log("Changing " + this.clientNickName() + "'s description to " + result);
this.channelTree.client.serverConnection.send_command("clientedit", {
clid: this.clientId(),
client_description: result
});
@ -332,9 +344,9 @@ class ClientEntry {
createInputModal(tr("Kick client from channel"), tr("Kick reason:<br>"), text => true, result => {
if(result) {
//TODO tr
console.log("Kicking client " + _this.clientNickName() + " from channel with reason " + result);
_this.channelTree.client.serverConnection.send_command("clientkick", {
clid: _this.clientId(),
console.log("Kicking client " + this.clientNickName() + " from channel with reason " + result);
this.channelTree.client.serverConnection.send_command("clientkick", {
clid: this.clientId(),
reasonid: ViewReasonId.VREASON_CHANNEL_KICK,
reasonmsg: result
});
@ -350,9 +362,9 @@ class ClientEntry {
createInputModal(tr("Kick client from server"), tr("Kick reason:<br>"), text => true, result => {
if(result) {
//TODO tr
console.log("Kicking client " + _this.clientNickName() + " from server with reason " + result);
_this.channelTree.client.serverConnection.send_command("clientkick", {
clid: _this.clientId(),
console.log("Kicking client " + this.clientNickName() + " from server with reason " + result);
this.channelTree.client.serverConnection.send_command("clientkick", {
clid: this.clientId(),
reasonid: ViewReasonId.VREASON_SERVER_KICK,
reasonmsg: result
});
@ -411,7 +423,7 @@ class ClientEntry {
});
}
},
MenuEntry.CLOSE(on_close)
MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
);
}
@ -908,8 +920,22 @@ class MusicClientEntry extends ClientEntry {
}
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
let trigger_close = true;
spawn_context_menu(x, y,
{
type: MenuEntryType.ENTRY,
name: tr("Show bot info"),
callback: () => {
trigger_close = false;
this.channelTree.client.selectInfo.open_popover()
},
icon: "client-about",
visible: this.channelTree.client.selectInfo.is_popover()
}, {
type: MenuEntryType.HR,
visible: this.channelTree.client.selectInfo.is_popover(),
name: ''
}, {
name: tr("<b>Change bot name</b>"),
icon: "client-change_nickname",
disabled: false,
@ -1073,7 +1099,7 @@ class MusicClientEntry extends ClientEntry {
},
type: MenuEntryType.ENTRY
},
MenuEntry.CLOSE(on_close)
MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
);
}

View File

@ -196,6 +196,7 @@ class ControlBar {
private updateMicrophoneRecordState() {
let enabled = !this._muteInput && !this._muteOutput && !this._away;
if(this.handle.voiceConnection)
this.handle.voiceConnection.voiceRecorder.update(enabled);
}
@ -212,12 +213,13 @@ class ControlBar {
}
updateVoice(targetChannel?: ChannelEntry) {
if(!targetChannel) targetChannel = this.handle.getClient().currentChannel();
if(!targetChannel)
targetChannel = this.handle.getClient().currentChannel();
let client = this.handle.getClient();
this.codec_supported = targetChannel ? this.handle.voiceConnection.codecSupported(targetChannel.properties.channel_codec) : true;
this.support_record = this.handle.voiceConnection.voice_send_support();
this.support_playback = this.handle.voiceConnection.voice_playback_support();
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);

View File

@ -53,7 +53,8 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
readonly handle: TSClient;
private current_selected?: AvailableTypes;
private _htmlTag: JQuery<HTMLElement>;
private _tag: JQuery<HTMLElement>;
private _tag_content: JQuery<HTMLElement>;
private _tag_info: JQuery<HTMLElement>;
private _tag_banner: JQuery<HTMLElement>;
@ -63,9 +64,10 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) {
this.handle = client;
this._htmlTag = htmlTag;
this._tag_info = htmlTag.find(".container-select-info");
this._tag_banner = htmlTag.find(".container-banner");
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.managers.push(new MusicInfoManager());
this.managers.push(new ClientInfoManager());
@ -73,10 +75,24 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
this.managers.push(new ServerInfoManager());
this.banner_manager = new Hostbanner(client, this._tag_banner);
this._tag.find("button.close").on('click', () => {
this._tag.toggleClass('shown', false);
});
}
handle_resize() {
/* test if the popover isn't a popover anymore */
if(this._tag.hasClass('shown')) {
this._tag.removeClass('shown');
if(this.is_popover())
this._tag.addClass('shown');
}
}
setCurrentSelected(entry: AvailableTypes) {
if(this.current_selected == entry) return;
if(this._current_manager) {
(this._current_manager as InfoManager<AvailableTypes>).finalizeFrame(this.current_selected, this._tag_info);
this._current_manager = null;
@ -112,7 +128,15 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
current_manager() { return this._current_manager; }
html_tag() { return this._htmlTag; }
html_tag() { return this._tag_content; }
is_popover() : boolean {
return !this._tag.is(':visible') || this._tag.hasClass('shown');
}
open_popover() {
this._tag.toggleClass('shown', true);
}
}
class Hostbanner {
@ -299,8 +323,12 @@ class ServerInfoManager extends InfoManager<ServerEntry> {
{
let requestUpdate = rendered.find(".btn_update");
requestUpdate.prop("disabled", !server.shouldUpdateProperties());
const disabled = !server.shouldUpdateProperties();
let requestUpdate = rendered.find(".button-update");
requestUpdate
.prop("disabled", disabled)
.toggleClass('btn-success', !disabled)
.toggleClass('btn-danger', disabled);
requestUpdate.click(() => {
server.updateProperties();
@ -308,7 +336,10 @@ class ServerInfoManager extends InfoManager<ServerEntry> {
});
this.registerTimer(setTimeout(function () {
requestUpdate.prop("disabled", false);
requestUpdate
.prop("disabled", false)
.toggleClass('btn-success', true)
.toggleClass('btn-danger', false);
}, server.nextInfoRequest - Date.now()));
}

View File

@ -5,7 +5,7 @@
/// <reference path="../../profiles/Identity.ts" />
namespace Modals {
function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity) : Modal {
function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity): Modal {
let modal: Modal;
let elapsed_timer: NodeJS.Timer;
@ -25,10 +25,10 @@ namespace Modals {
const input_elapsed = template.find(".time-elapsed input");
button_close.on('click', event => {
if(active)
if (active)
button_start_stop.trigger('click');
if(modal.shown)
if (modal.shown)
modal.close();
});
@ -40,7 +40,7 @@ namespace Modals {
input_threads.prop("disabled", !active);
input_target_level.prop("disabled", !active);
if(active) {
if (active) {
input_hash_rate.val(0);
clearInterval(elapsed_timer);
active = false;
@ -51,7 +51,7 @@ namespace Modals {
const threads = parseInt(input_threads.val() as string);
const target_level = parseInt(input_target_level.val() as string);
if(target_level == 0) {
if (target_level == 0) {
identity.improve_level(-1, threads, () => active, current_level => {
input_current_level.val(current_level);
}, hash_rate => {
@ -59,7 +59,7 @@ namespace Modals {
}).catch(error => {
console.error(error);
createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.<br>Error:") + error).open();
if(active)
if (active)
button_start_stop.trigger('click');
});
} else {
@ -68,7 +68,7 @@ namespace Modals {
}, hash_rate => {
input_hash_rate.val(hash_rate);
}).then(success => {
if(success) {
if (success) {
identity.level().then(level => {
input_current_level.val(level);
createInfoModal(tr("Identity successfully improved"), MessageHelper.formatMessage(tr("Identity successfully improved to level {}"), level)).open();
@ -76,12 +76,12 @@ namespace Modals {
input_current_level.val("error: " + error);
});
}
if(active)
if (active)
button_start_stop.trigger('click');
}).catch(error => {
console.error(error);
createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.<br>Error:") + error).open();
if(active)
if (active)
button_start_stop.trigger('click');
});
}
@ -92,10 +92,10 @@ namespace Modals {
let seconds = Math.floor(time % 60).toString();
let minutes = Math.floor(time / 60).toString();
if(seconds.length < 2)
if (seconds.length < 2)
seconds = "0" + seconds;
if(minutes.length < 2)
if (minutes.length < 2)
minutes = "0" + minutes;
input_elapsed.val(minutes + ":" + seconds);
@ -119,7 +119,7 @@ namespace Modals {
return modal;
}
function spawnTeamSpeakIdentityImport(callback: (identity: profiles.identities.TeaSpeakIdentity) => any) : Modal {
function spawnTeamSpeakIdentityImport(callback: (identity: profiles.identities.TeaSpeakIdentity) => any): Modal {
let modal: Modal;
let loaded_identity: profiles.identities.TeaSpeakIdentity;
@ -134,7 +134,7 @@ namespace Modals {
const button_import = template.find(".button-import");
const set_error = message => {
template.find(".success").hide();
if(message) {
if (message) {
template.find(".error").text(message).show();
button_import.prop("disabled", true);
} else
@ -157,7 +157,7 @@ namespace Modals {
const element = event.target as HTMLInputElement;
const file_reader = new FileReader();
file_reader.onload = function() {
file_reader.onload = function () {
import_identity(file_reader.result as string, true);
};
@ -167,7 +167,7 @@ namespace Modals {
return;
};
if(element.files && element.files.length > 0)
if (element.files && element.files.length > 0)
file_reader.readAsText(element.files[0]);
});
}
@ -175,7 +175,7 @@ namespace Modals {
{ /* text input */
template.find(".button-load-text").on('click', event => {
createInputModal("Import identity from text", "Please paste your idenity bellow<br>", text => text.length > 0 && text.indexOf('V') != -1, result => {
if(result)
if (result)
import_identity(result as string, false);
}).open();
});
@ -197,7 +197,7 @@ namespace Modals {
return modal;
}
export function spawnSettingsModal() : Modal {
export function spawnSettingsModal(): Modal {
let modal: Modal;
modal = createModal({
header: tr("Settings"),
@ -206,6 +206,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
});
initialiseVoiceListeners(modal, (template = template.tabify()).find(".settings_audio"));
@ -226,12 +227,12 @@ namespace Modals {
console.log(tag);
{/* setup the forum */
const identity = profiles.identities.static_forum_identity();
if(identity && identity.valid()) {
if (identity && identity.valid()) {
tag.find(".not-connected").hide();
tag.find(".property.username .value").text(identity.name());
const premium_tag = tag.find(".property.premium .value").text("");
if(identity.is_stuff() || identity.is_premium())
if (identity.is_stuff() || identity.is_premium())
premium_tag.append($.spawn("div").addClass("premium").text(tr("yes")));
else
premium_tag.append($.spawn("div").addClass("non-premium").text(tr("no")));
@ -250,6 +251,17 @@ namespace Modals {
function initialiseVoiceListeners(modal: Modal, tag: JQuery) {
let currentVAD = settings.global("vad_type", "vad");
const display_error = (message: string) => {
const alert = tag.find(".settings-device-error").first();
alert.clone()
.alert()
.css("display", "block")
.insertAfter(alert)
.find(".message")
.text(message);
};
if (globalClient.voiceConnection) {
{ //Initialized voice activation detection
const vad_tag = tag.find(".settings-vad-container");
@ -301,7 +313,7 @@ namespace Modals {
});
let listener = (event: ppt.KeyEvent) => {
if(event.type == ppt.EventType.KEY_TYPED) {
if (event.type == ppt.EventType.KEY_TYPED) {
settings.changeGlobal('vad_ppt_key', undefined); //TODO remove that because its legacy shit
console.log(tr("Got key %o"), event);
@ -336,20 +348,20 @@ namespace Modals {
slider.on("input change", () => {
settings.changeGlobal("vad_threshold", slider.val().toString());
let vad = globalClient.voiceConnection.voiceRecorder.getVADHandler();
if(vad instanceof VoiceActivityDetectorVAD)
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();
if(vad instanceof VoiceActivityDetectorVAD)
if (vad instanceof VoiceActivityDetectorVAD)
vad.percentage_listener = undefined;
});
}
let target_tag = vad_tag.find('input[type=radio][name="vad_type"][value="' + currentVAD + '"]');
if(target_tag.length == 0) {
if (target_tag.length == 0) {
//TODO tr
console.warn("Failed to find tag for " + currentVAD + ". Using latest tag!");
target_tag = vad_tag.find('input[type=radio][name="vad_type"]').last();
@ -358,16 +370,6 @@ namespace Modals {
setTimeout(() => target_tag.trigger('change'), 0);
}
const display_error = (message: string) => {
const alert = tag.find(".settings-device-error").first();
alert.clone()
.alert()
.css("display", "block")
.insertAfter(alert)
.find(".message")
.text(message);
};
{ //Initialize microphone
const setting_tag = tag.find(".settings-microphone");
@ -385,9 +387,9 @@ namespace Modals {
navigator.mediaDevices.enumerateDevices().then(devices => {
const active_device = globalClient.voiceConnection.voiceRecorder.device_id();
for(const device of devices) {
for (const device of devices) {
console.debug(tr("Got device %s (%s): %s (%o)"), device.deviceId, device.kind, device.label);
if(device.kind !== 'audioinput') continue;
if (device.kind !== 'audioinput') continue;
$.spawn("option")
.attr("device-id", device.deviceId)
@ -402,7 +404,7 @@ namespace Modals {
display_error(tr("Could not get microphone device list!"));
});
if(tag_select.find("option:selected").length == 0)
if (tag_select.find("option:selected").length == 0)
tag_select.find("option").prop("selected", true);
};
@ -420,6 +422,7 @@ namespace Modals {
update_devices();
setting_tag.find(".button-device-update").on('click', event => update_devices());
}
}
{ //Initialize speaker
const setting_tag = tag.find(".settings-speaker");
@ -431,7 +434,7 @@ namespace Modals {
const active_device = audio.player.current_device();
audio.player.available_devices().then(devices => {
for(const device of devices) {
for (const device of devices) {
$.spawn("option")
.attr("device-id", device.device_id)
.text(device.name)
@ -445,7 +448,7 @@ namespace Modals {
});
if(tag_select.find("option:selected").length == 0)
if (tag_select.find("option:selected").length == 0)
tag_select.find("option").prop("selected", true);
}
@ -498,7 +501,7 @@ namespace Modals {
const template_tag = $("#tmpl_settings-sound_entry");
const entry_tag = sound_tag.find(".sound-list-entries");
for(const _sound in Sound) {
for (const _sound in Sound) {
const sound_name = Sound[_sound];
console.log(sound.get_sound_volume(sound_name as Sound));
@ -523,7 +526,7 @@ namespace Modals {
setTimeout(() => {
const entry_container = sound_tag.find(".sound-list-entries-container");
if(entry_container.hasScrollBar())
if (entry_container.hasScrollBar())
entry_container.addClass("scrollbar");
}, 100);
@ -531,12 +534,12 @@ namespace Modals {
const filter_tag = sound_tag.find(".sound-list-filter input");
filter_tag.on('change keyup', event => {
const filter = ((<HTMLInputElement>event.target).value || "").toLowerCase();
if(!filter)
if (!filter)
entry_tag.find(".entry").show();
else {
entry_tag.find(".entry").each((_, _entry) => {
const entry = $(_entry);
if(entry.text().toLowerCase().indexOf(filter) == -1)
if (entry.text().toLowerCase().indexOf(filter) == -1)
entry.hide();
else
entry.show();
@ -547,44 +550,6 @@ namespace Modals {
modal.close_listener.push(sound.save);
}
//Initialise microphones
/*
let select_microphone = tag.find(".voice_microphone_select");
let select_error = tag.find(".voice_microphone_select_error");
navigator.mediaDevices.enumerateDevices().then(devices => {
let recoder = globalClient.voiceConnection.voiceRecorder;
console.log("Got " + devices.length + " devices:");
for(let device of devices) {
console.log(" - Type: %s Name %s ID: %s Group: %s", device.kind, device.label, device.deviceId, device.groupId);
if(device.kind == "audioinput") {
let dtag = $.spawn("option");
dtag.attr("device-id", device.deviceId);
dtag.attr("device-group", device.groupId);
dtag.text(device.label);
select_microphone.append(dtag);
if(recoder) dtag.prop("selected", device.deviceId == recoder.device_id());
}
}
}).catch(error => {
console.error("Could not enumerate over devices!");
console.error(error);
select_error.text("Could not get device list!").show();
});
select_microphone.change(event => {
let deviceSelected = select_microphone.find("option:selected");
let deviceId = deviceSelected.attr("device-id");
let groupId = deviceSelected.attr("device-group");
console.log("Selected microphone device: id: %o group: %o", deviceId, groupId);
globalClient.voiceConnection.voiceRecorder.change_device(deviceId, groupId);
});
*/
//Initialise speakers
}
function initialise_translations(tag: JQuery) {
@ -648,7 +613,7 @@ namespace Modals {
tag_loading.show();
i18n.iterate_translations((repo, entry) => {
let repo_tag = tag_list.find("[repository=\"" + repo.unique_id + "\"]");
if(repo_tag.length == 0) {
if (repo_tag.length == 0) {
repo_tag = template.renderTag({
type: "repository",
name: repo.name || repo.url,
@ -659,7 +624,7 @@ namespace Modals {
e.preventDefault();
Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this repository?"), answer => {
if(answer) {
if (answer) {
i18n.delete_repository(repo);
update_list();
}
@ -716,7 +681,7 @@ namespace Modals {
info_modal.open()
});
tag.on('click', e => {
if(e.isDefaultPrevented()) return;
if (e.isDefaultPrevented()) return;
i18n.select_translation(repo, entry);
tag_list.find(".selected").removeClass("selected");
tag.addClass("selected");
@ -734,7 +699,7 @@ namespace Modals {
{
tag.find(".button-add-repository").on('click', () => {
createInputModal("Enter URL", tr("Enter repository URL:<br>"), text => true, url => { //FIXME test valid url
if(!url) return;
if (!url) return;
tag_loading.show();
i18n.load_repository(url as string).then(repository => {
@ -786,30 +751,30 @@ namespace Modals {
profiles.mark_need_save();
let tag: JQuery;
if(selected_type == profiles.identities.IdentitifyType.TEAFORO) {
if (selected_type == profiles.identities.IdentitifyType.TEAFORO) {
const forum_tag = tag = settings_tag.find(".identity-settings-teaforo");
forum_tag.find(".connected, .disconnected").hide();
if(identity && identity.valid()) {
if (identity && identity.valid()) {
forum_tag.find(".connected").show();
} else {
forum_tag.find(".disconnected").show();
}
} else if(selected_type == profiles.identities.IdentitifyType.TEAMSPEAK) {
} else if (selected_type == profiles.identities.IdentitifyType.TEAMSPEAK) {
console.log("Set: " + identity);
const teamspeak_tag = tag = settings_tag.find(".identity-settings-teamspeak");
teamspeak_tag.find(".identity_string").val("");
if(identity)
if (identity)
(identity as profiles.identities.TeaSpeakIdentity).export_ts().then(e => teamspeak_tag.find(".identity_string").val(e));
} else if(selected_type == profiles.identities.IdentitifyType.NICKNAME) {
} else if (selected_type == profiles.identities.IdentitifyType.NICKNAME) {
const name_tag = tag = settings_tag.find(".identity-settings-nickname");
if(identity)
if (identity)
name_tag.find("input").val(identity.name());
else
name_tag.find("input").val("");
}
if(tag)
if (tag)
tag.trigger('show');
};
@ -821,7 +786,7 @@ namespace Modals {
const update_profile_list = () => {
const profile_list = tag.find(".profile-list .list").empty();
const profile_template = $("#settings-profile-list-entry");
for(const profile of profiles.profiles()) {
for (const profile of profiles.profiles()) {
const list_tag = profile_template.renderTag({
profile_name: profile.profile_name,
id: profile.id
@ -829,7 +794,7 @@ namespace Modals {
const profile_status_update = () => {
list_tag.find(".status").hide();
if(profile.valid())
if (profile.valid())
list_tag.find(".status-valid").show();
else
list_tag.find(".status-invalid").show();
@ -839,7 +804,7 @@ namespace Modals {
profile_list.find(".selected").removeClass("selected");
list_tag.addClass("selected");
if(profile == selected_profile) return;
if (profile == selected_profile) return;
nickname_listener = () => list_tag.find(".name").text(profile.profile_name);
status_listener = profile_status_update;
@ -848,14 +813,14 @@ namespace Modals {
profile_list.append(list_tag);
if((!selected_profile && profile.id == "default") || selected_profile == profile)
if ((!selected_profile && profile.id == "default") || selected_profile == profile)
setTimeout(() => list_tag.trigger('click'), 1);
profile_status_update();
}
};
const display_error = (error?: string) => {
if(error){
if (error) {
settings_tag.find(".settings-profile-error").show().find(".message").html(error);
} else
settings_tag.find(".settings-profile-error").hide();
@ -881,9 +846,9 @@ namespace Modals {
createInfoModal(tr("Identity imported"), tr("Your identity has been successfully imported!")).open();
};
if(profile && profile.valid()) {
if (profile && profile.valid()) {
spawnYesNo(tr("Are you sure"), tr("Do you really want to import a new identity and override the old identity?"), result => {
if(result)
if (result)
spawnTeamSpeakIdentityImport(set_identity);
});
} else
@ -891,10 +856,10 @@ namespace Modals {
});
button_export.on('click', event => {
const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity;
if(!profile) return;
if (!profile) return;
createInputModal(tr("File name"), tr("Please enter the file name"), text => !!text, name => {
if(name) {
if (name) {
profile.export_ts(true).then(data => {
const element = $.spawn("a")
.text("donwload")
@ -925,9 +890,9 @@ namespace Modals {
});
};
if(profile && profile.valid()) {
if (profile && profile.valid()) {
spawnYesNo(tr("Are you sure"), tr("Do you really want to generate a new identity and override the old identity?"), result => {
if(result)
if (result)
generate_identity();
});
} else
@ -936,7 +901,7 @@ namespace Modals {
button_improve.on('click', event => {
const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity;
if(!profile) return;
if (!profile) return;
spawnTeamSpeakIdentityImprove(profile).close_listener.push(() => teamspeak_tag.trigger('show'));
});
@ -945,7 +910,7 @@ namespace Modals {
teamspeak_tag.on('show', event => {
const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity;
if(!profile || !profile.valid()) {
if (!profile || !profile.valid()) {
identity_info_tag.hide();
teamspeak_tag.find(".identity-undefined").show();
button_export.prop("disabled", true);
@ -964,12 +929,12 @@ namespace Modals {
{ //The forum
const teaforo_tag = settings_tag.find(".identity-settings-teaforo");
if(native_client) {
if (native_client) {
teaforo_tag.find(".native-teaforo-login").on('click', event => {
setTimeout(() => {
const forum = require("teaforo.js");
const call = () => {
if(modal.shown) {
if (modal.shown) {
display_settings(selected_profile);
status_listener();
}
@ -981,7 +946,8 @@ namespace Modals {
}
teaforo_tag.on('show', event => {
display_error(); /* clear error */
display_error();
/* clear error */
});
}
@ -992,7 +958,7 @@ namespace Modals {
selected_profile.set_identity(profiles.identities.IdentitifyType.NICKNAME, new profiles.identities.NameIdentity(name));
profiles.mark_need_save();
if(name.length < 3) {
if (name.length < 3) {
display_error("Name must be at least 3 characters long!");
return;
}
@ -1001,9 +967,9 @@ namespace Modals {
name_tag.on('show', event => {
const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.NICKNAME);
if(!profile)
if (!profile)
display_error("invalid profile");
else if(!profile.valid())
else if (!profile.valid())
display_error("Name must be at least 3 characters long!");
else
display_error();
@ -1015,9 +981,9 @@ namespace Modals {
{
settings_tag.find(".setting-name").on('change', event => {
const value = settings_tag.find(".setting-name").val() as string;
if(value && selected_profile) {
if (value && selected_profile) {
selected_profile.profile_name = value;
if(nickname_listener)
if (nickname_listener)
nickname_listener();
profiles.mark_need_save();
status_listener();
@ -1025,7 +991,7 @@ namespace Modals {
});
settings_tag.find(".setting-default-nickname").on('change', event => {
const value = settings_tag.find(".setting-default-nickname").val() as string;
if(value && selected_profile) {
if (value && selected_profile) {
selected_profile.default_username = value;
profiles.mark_need_save();
status_listener();
@ -1033,7 +999,7 @@ namespace Modals {
});
settings_tag.find(".setting-default-password").on('change', event => {
const value = settings_tag.find(".setting-default-password").val() as string;
if(value && selected_profile) {
if (value && selected_profile) {
selected_profile.default_username = value;
profiles.mark_need_save();
status_listener();
@ -1045,7 +1011,7 @@ namespace Modals {
{
tag.find(".button-add-profile").on('click', event => {
createInputModal(tr("Please enter a name"), tr("Please enter a name for the new profile:<br>"), text => text.length > 0 && !profiles.find_profile_by_name(text), value => {
if(value) {
if (value) {
display_settings(profiles.create_new_profile(value as string));
update_profile_list();
profiles.mark_need_save();
@ -1054,7 +1020,7 @@ namespace Modals {
});
tag.find(".button-set-default").on('click', event => {
if(selected_profile && selected_profile.id != 'default') {
if (selected_profile && selected_profile.id != 'default') {
profiles.set_default_profile(selected_profile);
update_profile_list();
profiles.mark_need_save();
@ -1062,10 +1028,10 @@ namespace Modals {
});
tag.find(".button-delete").on('click', event => {
if(selected_profile && selected_profile.id != 'default') {
if (selected_profile && selected_profile.id != 'default') {
event.preventDefault();
spawnYesNo(tr("Are you sure?"), tr ("Do you really want to delete this profile?"), result => {
if(result) {
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this profile?"), result => {
if (result) {
profiles.delete_profile(selected_profile);
update_profile_list();
}
@ -1075,7 +1041,7 @@ namespace Modals {
}
modal.close_listener.push(() => {
if(profiles.requires_save())
if (profiles.requires_save())
profiles.save();
});
update_profile_list();

View File

@ -135,7 +135,21 @@ class ServerEntry {
}
spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) {
let trigger_close = true;
spawn_context_menu(x, y, {
type: MenuEntryType.ENTRY,
name: tr("Show server info"),
callback: () => {
trigger_close = false;
this.channelTree.client.selectInfo.open_popover()
},
icon: "client-about",
visible: this.channelTree.client.selectInfo.is_popover()
}, {
type: MenuEntryType.HR,
visible: this.channelTree.client.selectInfo.is_popover(),
name: ''
}, {
type: MenuEntryType.ENTRY,
icon: "client-virtualserver_edit",
name: tr("Edit"),
@ -162,7 +176,7 @@ class ServerEntry {
createInfoModal(tr("Buddy invite URL"), tr("Your buddy invite URL:<br>") + url + tr("<bt>This has been copied to your clipboard.")).open();
}
},
MenuEntry.CLOSE(on_close)
MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
);
}

View File

@ -144,6 +144,7 @@ class VoiceConnection {
private vpacketId: number = 0;
private chunkVPacketId: number = 0;
private send_task: NodeJS.Timer;
private _tag_favicon: JQuery;
constructor(client) {
this.client = client;
@ -171,6 +172,7 @@ class VoiceConnection {
});
this.send_task = setInterval(this.sendNextVoicePacket.bind(this), 20);
this._tag_favicon = $("head link[rel='icon']");
}
native_encoding_supported() : boolean {
@ -463,6 +465,8 @@ class VoiceConnection {
if(this.dataChannel)
this.sendVoicePacket(new Uint8Array(0), this.current_channel_codec()); //TODO Use channel codec!
this._tag_favicon.attr('href', "img/favicon/teacup.png");
}
private handleVoiceStarted() {
@ -470,5 +474,6 @@ class VoiceConnection {
if(this.client && this.client.getClient())
this.client.getClient().speaking = true;
this._tag_favicon.attr('href', "img/favicon/speaking.png");
}
}

View File

@ -1,6 +1,7 @@
declare namespace WebAssembly {
export function instantiateStreaming(stream: Promise<Response>, imports?: any) : Promise<ResultObject>;
}
declare function postMessage(message: any): void;
const prefix = "[POWWorker] ";

2
vendor/bbcode vendored

@ -1 +1 @@
Subproject commit 9a1c31f27fcac129fa3503c2c1d2096c126d3fd2
Subproject commit 23f9aca6b6dc1ffccd20d6da04953776a1882f2b

View File

@ -1,17 +1,27 @@
html, body {
height: 100%;
overflow-y: hidden;
height: 100%;
width: 100%;
position: fixed;
min-height: 250px;
min-width: 250px;
}
.app-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
position: absolute;
right: 10px;
left: 10px;
bottom: 40px;
top: 10px;
.app {
width: 100%;
height: calc(100% - 50px);
height: 100%;
margin: 0;
display: flex; flex-direction: column; resize: both;