Merge remote-tracking branch 'origin/develop'

This commit is contained in:
WolverinDEV 2019-03-17 15:20:39 +01:00
commit cb3d6fbf5d
141 changed files with 108552 additions and 895 deletions

View file

@ -1,4 +1,25 @@
# Changelog: # Changelog:
* **17.03.19**
- Using VAD by default instead of PPT
- Improved mobile experience:
- Double touch join channel
- Removed the info bar for devices smaller than 500px
- Added country flags and names
- Added favicon, which change when you're recording
- Fixed double cache loading
- Fixed modal sizing scroll bug
- Added a channel subscribe all button
- Added individual channel subscribe settings
- Improved chat switch performance
- Added a chat message URL finder
- Escape URL detection with `!<url>`
- Improved chat experience
- Displaying offline chats as offline
- Notify when user closes the chat
- Notify when user disconnect/reconnects
- Preloading hostbanners to prevent flickering
- Fixed empty channel and server kick messages
* **17.02.19** * **17.02.19**
- Removed WebAssembly as dependency (Now working with MS Edge as well (but without audio)) - Removed WebAssembly as dependency (Now working with MS Edge as well (but without audio))
- Improved channel tree performance - Improved channel tree performance

BIN
TeaWeb-release-ad43894.zip Normal file

Binary file not shown.

196
files.php
View file

@ -258,9 +258,97 @@
], ],
]; ];
function list_dir($base_dir, $match = null, $depth = -1, &$results = array(), $dir = "") { function systemify_path($path) {
return str_replace("/", DIRECTORY_SEPARATOR, $path);
}
function join_path(...$paths) {
$result_path = "";
foreach ($paths as $path) {
if(strlen($result_path) > 0)
$result_path .= DIRECTORY_SEPARATOR . $path;
else
$result_path = $path;
}
return $result_path;
}
function create_directories(&$error, $path, $dry_run = false) {
if(strpos(PHP_OS, "Linux") !== false) {
$command = "mkdir -p " . $path;
} else if(strpos(PHP_OS, "WINNT") !== false) {
$command = "mkdir " . $path; /* default path tree */
} else {
$error = "unsupported system";
return false;
}
echo $command . PHP_EOL;
if(!$dry_run) {
exec($command, $error, $state);
if($state) {
$error = "Command execution results in " . $state . ": " . implode(' ', $error);
return false;
}
}
return true;
}
function delete_directories(&$error, $path, $dry_run = false) {
if(strpos(PHP_OS, "Linux") !== false) {
$command = "rm -r " . $path;
} else if(strpos(PHP_OS, "WINNT") !== false) {
$command = "rm -r " . $path;
} else {
$error = "unsupported system";
return false;
}
echo $command . PHP_EOL;
if(!$dry_run) {
$state = 0;
exec($command, $output, $state);
if($state !== 0) {
$error = "Command execution results in " . $state . ": " . implode(' ', $output);
return false;
}
}
return true;
}
function create_link(&$error, $source, $target, $dry_run = false) {
if(strpos(PHP_OS, "Linux") !== false) {
$command = "ln -s " . $source . " " . $target;
} else if(strpos(PHP_OS, "WINNT") !== false) {
$command = "mklink " . (is_dir($target) ? "/D " : "") . " " . $target . " " . $source;
} else {
$error = "unsupported system";
return false;
}
echo $command . PHP_EOL;
if(!$dry_run) {
$state = 0;
exec($command, $output, $state);
if($state !== 0) {
$error = "Command execution results in " . $state . ": " . implode(' ', $output);
return false;
}
}
return true;
}
function list_dir($base_dir, $match = null, $depth = -1, &$results = array(), $dir = "") {
if($depth == 0) return $results; if($depth == 0) return $results;
if(!is_dir($base_dir . $dir)) {
echo "Skipping directory " . $base_dir . $dir . PHP_EOL;
return $results;
}
$files = scandir($base_dir . $dir); $files = scandir($base_dir . $dir);
foreach($files as $key => $value){ foreach($files as $key => $value){
@ -279,12 +367,14 @@
class AppFile { class AppFile {
public $type; public $type;
public $name; public $name;
public $path;
public $local_path; public $target_path; /* relative path to the target file viewed from the file root */
public $local_path; /* absolute path to local file */
public $hash; public $hash;
} }
function find_files($flag = 0b11, $local_path_prefix = "./", $type = "dev", $args = []) { //TODO Use cache here! function find_files($flag = 0b11, $local_path_prefix = "." . DIRECTORY_SEPARATOR, $type = "dev", $args = []) { //TODO Use cache here!
global $APP_FILE_LIST; global $APP_FILE_LIST;
$result = []; $result = [];
@ -303,23 +393,22 @@
if(!$valid) if(!$valid)
continue; continue;
} }
$entries = list_dir($local_path_prefix . $entry["local-path"], $entry["search-pattern"], isset($entry["search-depth"]) ? $entry["search-depth"] : -1); $entries = list_dir(
systemify_path($local_path_prefix . $entry["local-path"]),
$entry["search-pattern"],
isset($entry["search-depth"]) ? $entry["search-depth"] : -1
);
foreach ($entries as $f_entry) { foreach ($entries as $f_entry) {
if(isset($entry["search-exclude"]) && preg_match($entry["search-exclude"], $f_entry)) continue; if(isset($entry["search-exclude"]) && preg_match($entry["search-exclude"], $f_entry)) continue;
$file = new AppFile; $file = new AppFile;
$idx_sep = strrpos($f_entry, DIRECTORY_SEPARATOR); $f_info = pathinfo($f_entry);
$file->path = "./" . $entry["path"] . "/"; $file->target_path = systemify_path($entry["path"]) . DIRECTORY_SEPARATOR . $f_info["dirname"] . DIRECTORY_SEPARATOR;
if($idx_sep > 0) { $file->local_path = getcwd() . DIRECTORY_SEPARATOR . systemify_path($entry["local-path"]) . DIRECTORY_SEPARATOR . $f_info["dirname"] . DIRECTORY_SEPARATOR;
$file->name = substr($f_entry, strrpos($f_entry, DIRECTORY_SEPARATOR) + 1);
$file->path = $file->path . substr($f_entry, 0, strrpos($f_entry, DIRECTORY_SEPARATOR));
} else {
$file->name = $f_entry;
}
$file->local_path = $local_path_prefix . $entry["local-path"] . DIRECTORY_SEPARATOR . $f_entry; $file->name = $f_info["basename"];
$file->type = $entry["type"]; $file->type = $entry["type"];
$file->hash = sha1_file($file->local_path); $file->hash = sha1_file($file->local_path . DIRECTORY_SEPARATOR . $file->name);
if(strlen($file->hash) > 0) { if(strlen($file->hash) > 0) {
foreach ($result as $e) foreach ($result as $e)
@ -334,10 +423,17 @@
} }
if(isset($_SERVER["argv"])) { //Executed by command line if(isset($_SERVER["argv"])) { //Executed by command line
if(strpos(PHP_OS, "Linux") == -1) { $supported = false;
error_log("Invalid operating system! Help tool only available under linux!"); if(strpos(PHP_OS, "Linux") !== false) {
exit(1); $supported = true;
} } else if(strpos(PHP_OS, "WIN") !== false) {
$supported = true;
}
if(!$supported) {
error_log("Invalid operating system (" . PHP_OS . ")! Help tool only available under linux!");
exit(1);
}
if(count($_SERVER["argv"]) < 2) { if(count($_SERVER["argv"]) < 2) {
error_log("Invalid parameters!"); error_log("Invalid parameters!");
goto help; goto help;
@ -358,10 +454,10 @@
if($_SERVER["argv"][3] == "dev" || $_SERVER["argv"][3] == "development") { if($_SERVER["argv"][3] == "dev" || $_SERVER["argv"][3] == "development") {
if ($_SERVER["argv"][2] == "web") { if ($_SERVER["argv"][2] == "web") {
$flagset = 0b01; $flagset = 0b01;
$environment = "web/environment/development"; $environment = join_path("web", "environment", "development");
} else if ($_SERVER["argv"][2] == "client") { } else if ($_SERVER["argv"][2] == "client") {
$flagset = 0b10; $flagset = 0b10;
$environment = "client-api/environment/ui-files/raw"; $environment = join_path("client-api", "environment", "ui-files", "raw");
} else { } else {
error_log("Invalid type!"); error_log("Invalid type!");
goto help; goto help;
@ -370,10 +466,10 @@
$type = "rel"; $type = "rel";
if ($_SERVER["argv"][2] == "web") { if ($_SERVER["argv"][2] == "web") {
$flagset = 0b01; $flagset = 0b01;
$environment = "web/environment/release"; $environment = join_path("web", "environment", "release");
} else if ($_SERVER["argv"][2] == "client") { } else if ($_SERVER["argv"][2] == "client") {
$flagset = 0b10; $flagset = 0b10;
$environment = "client-api/environment/ui-files/raw"; $environment = join_path("client-api", "environment", "ui-files", "raw");
} else { } else {
error_log("Invalid type!"); error_log("Invalid type!");
goto help; goto help;
@ -385,37 +481,29 @@
{ {
if(!$dry_run) { if(!$dry_run) {
exec($command = "rm -r " . $environment, $output, $state); if(delete_directories($error, $environment) === false)
exec($command = "mkdir -p " . $environment, $output, $state); if($state) goto handle_error; goto handle_error;
if(create_directories($error, $environment) === false)
goto handle_error;
} }
$files = find_files($flagset, "./", $type, array_slice($_SERVER["argv"], 4)); $files = find_files($flagset, "." . DIRECTORY_SEPARATOR, $type, array_slice($_SERVER["argv"], 4));
$original_path = realpath("."); $original_path = realpath(".");
if(!chdir($environment)) { if(!chdir($environment)) {
error_log("Failed to enter directory " . $environment . "!"); error_log("Failed to enter directory " . $environment . "!");
exit(1); exit(1);
} }
foreach($files as $file) { /** @var AppFile $file */
if(!$dry_run && !is_dir($file->path)) { foreach($files as $file) {
exec($command = "mkdir -p " . $file->path, $output, $state); if(!$dry_run && !is_dir($file->target_path) && strlen($file->target_path) > 0) {
if($state) goto handle_error; if(create_directories($error, $file->target_path, $dry_run) === false)
goto handle_error;
} }
$parent_base = substr_count(realpath($file->path), DIRECTORY_SEPARATOR) - substr_count(realpath('.'), DIRECTORY_SEPARATOR); if(create_link($output, $file->local_path . $file->name, $file->target_path . $file->name, $dry_run) === false)
$parent_file = substr_count(realpath("."), DIRECTORY_SEPARATOR) - substr_count($original_path, DIRECTORY_SEPARATOR); //Current to parent goto handle_error;
$parent = $parent_base + $parent_file;
$path = "";
for($index = 0; $index < $parent; $index++)
$path = $path . "../";
$command = "ln -s " . $path . $file->local_path . " " . $file->path;
if(!$dry_run) {
exec($command, $output, $state);
if($state) goto handle_error;
}
echo $command . PHP_EOL;
} }
if(!chdir($original_path)) { if(!chdir($original_path)) {
error_log("Failed to reset directory!"); error_log("Failed to reset directory!");
@ -425,18 +513,20 @@
} }
if(!$dry_run) { if(!$dry_run) {
exec("./scripts/git_index.sh sort-tag", $output, $state); exec("." . DIRECTORY_SEPARATOR . "scripts" . DIRECTORY_SEPARATOR . "git_index.sh sort-tag", $output, $state);
file_put_contents($environment . DIRECTORY_SEPARATOR . "version", $output); file_put_contents($environment . DIRECTORY_SEPARATOR . "version", $output);
if ($_SERVER["argv"][2] == "client") { if ($_SERVER["argv"][2] == "client") {
if(!chdir("client-api/environment")) { if(!chdir("client-api" . DIRECTORY_SEPARATOR . "environment")) {
error_log("Failed to enter directory client-api/environment!"); error_log("Failed to enter directory client-api/environment!");
exit(1); exit(1);
} }
if(!is_dir("versions/beta")) if(!is_dir("versions" . DIRECTORY_SEPARATOR . "beta")) {
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error; exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error;
if(!is_dir("versions/stable")) }
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error; if(!is_dir("versions/stable")) {
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error;
}
exec($command = "ln -s ../api.php ./", $output, $state); $state = 0; //Dont handle an error here! exec($command = "ln -s ../api.php ./", $output, $state); $state = 0; //Dont handle an error here!
if($state) goto handle_error; if($state) goto handle_error;
@ -445,10 +535,8 @@
exit(0); exit(0);
handle_error: handle_error:
error_log("Failed to execute command '" . $command . "'!"); error_log("Command execution failed!");
error_log("Command returned code " . $state . ". Output: " . PHP_EOL); error_log("Error message: " . $error);
foreach ($output as $line)
error_log($line);
exit(1); exit(1);
} }
} }

View file

@ -6,7 +6,9 @@
"directories": {}, "directories": {},
"scripts": { "scripts": {
"compile-sass": "sass --update .:.", "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",
"build-worker": "npm run build-worker-codec; npm run build-worker-pow;",
"dtsgen": "node tools/dtsgen/index.js", "dtsgen": "node tools/dtsgen/index.js",
"trgen": "node tools/trgen/index.js", "trgen": "node tools/trgen/index.js",
"ttsc": "ttsc", "ttsc": "ttsc",

View file

@ -28,9 +28,14 @@ if [[ $? -ne 0 ]]; then
fi fi
echo "Generating web workers" echo "Generating web workers"
npm run build-worker npm run build-worker-codec
if [[ $? -ne 0 ]]; then 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 exit 1
fi 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; flex-direction: row;
justify-content: stretch; justify-content: stretch;
cursor: pointer;
margin-left: 0; margin-left: 0;
.server_type { .server_type {
@ -134,6 +135,10 @@
display: block; display: block;
} }
} }
.icon_no_sound {
display: flex;
}
} }
.container-clients { .container-clients {
@ -193,7 +198,15 @@
} }
/* all icons related to basic_icons */ /* 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} .host {background-position: 0 -448px}

View file

@ -47,13 +47,11 @@ $background:lightgray;
.button-dropdown { .button-dropdown {
.buttons { .buttons {
display: grid; display: flex;
grid-template-columns: auto auto; flex-direction: row;
grid-template-rows: 100%;
grid-gap: 2px;
.button { .button {
margin-right: 0px; margin-right: 0;
} }
.button-dropdown { .button-dropdown {
@ -83,6 +81,7 @@ $background:lightgray;
background-color: rgba(0,0,0,0.4); background-color: rgba(0,0,0,0.4);
border-color: rgba(255, 255, 255, .75); border-color: rgba(255, 255, 255, .75);
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/ /*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
border-left: 2px solid rgba(255, 255, 255, .75);
} }
} }
} }
@ -103,6 +102,11 @@ $background:lightgray;
z-index: 1000; 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);*/ /*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
&.right {
}
.icon { .icon {
vertical-align: middle; vertical-align: middle;
margin-right: 5px; margin-right: 5px;
@ -131,8 +135,8 @@ $background:lightgray;
} }
} }
&:hover { &:hover.displayed {
.dropdown.displayed { .dropdown {
display: block; display: block;
} }
} }

View file

@ -1,11 +1,18 @@
.select_info_table { } .select_info_table {
.select_info_table tr { } tr {
.select_info_table tr td { } td {
&:nth-child(1) {
font-weight: bold;
padding-right: 5px;
//min-width: max(35%, 20px);
}
.select_info_table tr td:nth-child(1) { &:nth-child(2) {
font-weight: bold; //min-width: max(75%, 40px);
padding-right: 5px; word-break: break-word;
min-width: 20%; }
}
}
} }
.select_server { .select_server {
@ -17,21 +24,18 @@
.button-update { .button-update {
width: 100%; width: 100%;
height: 23px;
&:disabled { &:disabled {
color: red;
pointer-events: none; pointer-events: none;
} }
&:not(:disabled) {
color: green;
}
} }
.container { .container {
max-height: 100%; max-height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-right: 0;
padding-left: 0;
.hostbanner { .hostbanner {
overflow: hidden; overflow: hidden;
@ -39,23 +43,27 @@
} }
} }
/*
<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 { .select_info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: stretch; justify-content: stretch;
width: 100%; width: 100%;
> .close {
z-index: 500;
display: none;
position: absolute;
right: 5px;
top: 5px;
}
> div { > div {
width: 100%; width: 100%;
} }
.container-banner { .container-banner {
position: relative;
flex-grow: 1; flex-grow: 1;
flex-shrink: 2; flex-shrink: 2;
max-height: 25%; max-height: 25%;
@ -74,9 +82,29 @@
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
img { .image-container {
position: absolute; display: flex;
} flex-direction: row;
justify-content: center;
height: 100%;
div {
background-position: center;
&.hostbanner-mode-0 { }
&.hostbanner-mode-1 {
width: 100%;
height: auto;
}
&.hostbanner-mode-2 {
background-size: contain!important;
width:100%;
height:100%
}
}
}
} }
} }
@ -105,4 +133,13 @@
} }
} }
} }
.button-browser-info {
vertical-align: bottom;
cursor: pointer;
&:hover {
background-color: gray;
}
}
} }

View file

@ -228,8 +228,14 @@ footer .container {
} }
$separator_thickness: 4px; $separator_thickness: 4px;
$small_device: 650px;
$animation_length: .5s;
.app { .app {
min-width: 350px;
.container-app-main { .container-app-main {
position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: stretch; justify-content: stretch;
@ -301,8 +307,78 @@ $separator_thickness: 4px;
flex-direction: row; flex-direction: row;
justify-content: stretch; justify-content: stretch;
} }
.hide-small {
opacity: 1;
transition: opacity $animation_length linear;
}
.show-small {
display: none;
opacity: 0;
transition: opacity $animation_length linear;
}
} }
@media only screen and (max-width: $small_device) {
.app-container {
right: 0;
left: 0;
bottom: 25px;
top: 0;
transition: all $animation_length linear;
overflow: auto;
}
.app {
.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;
animation: fadeout $animation_length linear;
}
.container-channel-chat {
width: 100%!important; /* override the seperator property */
}
}
}
.hide-small {
display: none;
opacity: 0;
transition: opacity $animation_length linear;
}
.show-small {
display: block!important;
opacity: 1!important;
transition: opacity $animation_length linear;
}
}
.container-seperator { .container-seperator {
background: lightgray; background: lightgray;
flex-grow: 0; flex-grow: 0;
@ -328,6 +404,7 @@ $separator_thickness: 4px;
} }
.icon-container { .icon-container {
position: relative;
display: inline-block; display: inline-block;
height: 16px; height: 16px;
width: 16px; width: 16px;
@ -354,8 +431,6 @@ $separator_thickness: 4px;
} }
html, body { html, body {
min-height: 500px;
min-width: 500px;
overflow: hidden; overflow: hidden;
} }
@ -365,13 +440,24 @@ body {
} }
.icon-playlist-manage { .icon-playlist-manage {
display: inline-block; &.icon {
width: 32px; width: 16px;
height: 32px; height: 16px;
background-position: -5px -5px;
background-size: 25px;
}
&.icon_x32 {
width: 32px;
height: 32px;
background-position: -11px -9px;
background-size: 50px;
}
display: inline-block;
background: url('../../img/music/playlist.svg') no-repeat; background: url('../../img/music/playlist.svg') no-repeat;
background-position: -11px -9px;
background-size: 50px;
} }
x-content { x-content {

View file

@ -35,6 +35,20 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
} }
.event-message { /* special formated messages */
&.event-partner-disconnect {
color: red;
}
&.event-partner-connect {
color: green;
}
&.event-partner-closed {
color: orange;
}
}
} }
} }
} }
@ -62,11 +76,9 @@
cursor: pointer; cursor: pointer;
height: 18px; height: 18px;
&.active {
background: #11111111;
}
.btn_close { .btn_close {
display: none;
float: none; float: none;
margin-right: -5px; margin-right: -5px;
margin-left: 8px; margin-left: 8px;
@ -78,9 +90,34 @@
} }
} }
.name, .chatIcon { .name, .chat-type {
display: inline-block; display: inline-block;
} }
.name {
color: black;
}
&.closeable {
.btn_close {
display: inline-block;
}
}
&.active {
background: #11111111;
}
&.offline {
.name {
color: gray;
}
}
&.unread {
.name {
color: blue;
}
}
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,9 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
flex-shrink: 0;
flex-grow: 0;
background: url('../../../img/client_icon_sprite.svg'), url('../../img/client_icon_sprite.svg') no-repeat; background: url('../../../img/client_icon_sprite.svg'), url('../../img/client_icon_sprite.svg') no-repeat;
} }
@ -1028,7 +1031,7 @@
} }
.icon_x32.client-refresh { .icon_x32.client-refresh {
background-position: calc(-224px * 2) calc(-256px * 2); background-position: calc(-224px * 2) calc(-256px * 2);
}pe the key you wish }
.icon_x32.client-register { .icon_x32.client-register {
background-position: calc(-256px * 2) calc(-256px * 2); background-position: calc(-256px * 2) calc(-256px * 2);
} }

View file

@ -1,7 +1,4 @@
x-tab { display:none } x-tab { display:none }
x-content {
width: 100%;
}
.tab { .tab {
padding: 2px; padding: 2px;
@ -18,15 +15,19 @@ x-content {
.tab .tab-content { .tab .tab-content {
min-height: 200px; min-height: 200px;
border-color: #6f6f6f; border-radius: 0 2px 2px 2px;
border-radius: 0px 2px 2px 2px; border: solid #6f6f6f;
border-style: solid; overflow-y: hidden;
overflow-y: auto;
height: 100%; height: 100%;
padding: 2px; padding: 2px;
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
x-content {
overflow-y: auto;
width: 100%;
}
} }
/* /*
@ -39,7 +40,7 @@ x-content {
*/ */
.tab .tab-header { .tab .tab-header {
font-family: Arial; font-family: Arial, serif;
font-size: 12px; font-size: 12px;
/*white-space: pre;*/ /*white-space: pre;*/
line-height: 1; line-height: 1;
@ -64,14 +65,10 @@ x-content {
.tab .tab-header .entry { .tab .tab-header .entry {
background: #5f5f5f5f; background: #5f5f5f5f;
display: inline-block; display: inline-block;
border: #6f6f6f; border: 1px solid #6f6f6f;
border-width: 1px;
border-style: solid;
border-radius: 2px 2px 0px 0px; border-radius: 2px 2px 0px 0px;
vertical-align: middle; vertical-align: middle;
padding: 2px; padding: 2px 5px;
padding-left: 5px;
padding-right: 5px;
cursor: pointer; cursor: pointer;
flex-grow: 1; flex-grow: 1;
} }

View file

@ -37,6 +37,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <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." /> <meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." />
<link rel="icon" href="img/favicon/teacup.png">
<?php <?php
if(!$WEB_CLIENT) { if(!$WEB_CLIENT) {
@ -189,9 +190,9 @@
<footer style="<?php echo $footer_style; ?>"> <footer style="<?php echo $footer_style; ?>">
<div class="container" style="display: flex; flex-direction: row; align-content: space-between;"> <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 class="hide-small" 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 class="hide-small" style="align-self: center; position: fixed; right: 5px;"><?php echo $footer_forum; ?></div>
</div> </div>
</footer> </footer>
</html> </html>

View file

@ -6,7 +6,6 @@
<title>TeaSpeak-Web client templates</title> <title>TeaSpeak-Web client templates</title>
</head> </head>
<body> <body>
<!-- main frame TODO tr -->
<script class="jsrender-template" id="tmpl_main" type="text/html"> <script class="jsrender-template" id="tmpl_main" type="text/html">
<div class="app-container"> <div class="app-container">
<div class="app"> <div class="app">
@ -38,7 +37,7 @@
<div class="divider"></div> <div class="divider"></div>
<div class="button-dropdown btn_away" title="{{tr 'Toggle away status' /}}"> <div class="hide-small button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
<div class="buttons"> <div class="buttons">
<div class="button icon_x32 client-away btn_away_toggle"></div> <div class="button icon_x32 client-away btn_away_toggle"></div>
<div class="button-dropdown"> <div class="button-dropdown">
@ -50,15 +49,36 @@
<div class="btn_away_message"><div class="icon client-away"></div><a>{{tr "Set away message" /}}</a></div> <div class="btn_away_message"><div class="icon client-away"></div><a>{{tr "Set away message" /}}</a></div>
</div> </div>
</div> </div>
<div class="button btn_mute_input"> <div class="hide-small button btn_mute_input">
<div class="icon_x32 client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div> <div class="icon_x32 client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div>
</div> </div>
<div class="button btn_mute_output"> <div class="hide-small button btn_mute_output">
<div class="icon_x32 client-output_muted" title="{{tr 'Mute/unmute headphones' /}}"></div> <div class="icon_x32 client-output_muted" title="{{tr 'Mute/unmute headphones' /}}"></div>
</div> </div>
<div class="divider"></div>
<div class="button-dropdown btn_token" title="{{tr 'Use token' /}}"> <div class="show-small button-dropdown dropdown-audio" title="{{tr 'Audio settings' /}}">
<div class="buttons">
<div class="button button-display icon_x32 client-music"></div>
<div class="button-dropdown">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown">
<div class="btn_mute_input" title="{{tr 'Mute/unmute microphone' /}}">
<div class="icon client-input_muted"></div>
<a>{{tr "Mute/unmute microphone" /}}</a>
</div>
<div class="btn_mute_output" title="{{tr 'Mute/unmute headphones' /}}">
<div class="icon client-output_muted"></div>
<a>{{tr "Mute/unmute headphones" /}}</a>
</div>
</div>
</div>
<div class="divider"></div>
<div class="button button-subscribe-mode">
<div class="icon_x32" title="{{tr 'Toggle channel subscribe mode' /}}"></div>
</div>
<div class="hide-small button-dropdown btn_token" title="{{tr 'Use token' /}}">
<div class="buttons"> <div class="buttons">
<div class="button icon_x32 client-token btn_token_use"></div> <div class="button icon_x32 client-token btn_token_use"></div>
<div class="button-dropdown"> <div class="button-dropdown">
@ -72,13 +92,37 @@
</div> </div>
<div style="width: 100%"></div> <div style="width: 100%"></div>
<div class="button button-playlist-manage" title="{{tr 'Playlists' /}}">
<div class="icon-playlist-manage"></div> <div class="show-small button-dropdown dropdown-servertools" title="{{tr 'Server tools' /}}">
<div class="buttons">
<div class="button button-display icon_x32 client-virtualserver_edit"></div>
<div class="button-dropdown">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown right">
<div class="button-playlist-manage" title="{{tr 'Playlists' /}}">
<div class="icon icon-playlist-manage"></div>
<a>{{tr "Playlists" /}}</a>
</div>
<div class="btn_banlist" title="{{tr 'Banlist' /}}">
<div class="icon client-ban_list"></div>
<a>{{tr "Banlist" /}}</a>
</div>
<div class="btn_permissions" title="{{tr 'View/edit permissions' /}}">
<div class="icon client-permission_overview"></div>
<a>{{tr "View/edit permissions" /}}</a>
</div>
</div>
</div> </div>
<div class="button btn_banlist" title="{{tr 'Banlist' /}}">
<div class="hide-small button button-playlist-manage" title="{{tr 'Playlists' /}}">
<div class="icon_x32 icon-playlist-manage"></div>
</div>
<div class="hide-small button btn_banlist" title="{{tr 'Banlist' /}}">
<div class="icon_x32 client-ban_list"></div> <div class="icon_x32 client-ban_list"></div>
</div> </div>
<div class="button btn_permissions" title="{{tr 'View/edit permissions' /}}"> <div class="hide-small button btn_permissions" title="{{tr 'View/edit permissions' /}}">
<div class="icon_x32 client-permission_overview"></div> <div class="icon_x32 client-permission_overview"></div>
</div> </div>
@ -133,8 +177,11 @@
</div> </div>
</div> </div>
<div class="container-seperator vertical" seperator-id="seperator-main-info"></div> <div class="container-seperator vertical" seperator-id="seperator-main-info"></div>
<div class="main_container container-info"> <div id="select_info" class="main_container container-info">
<div id="select_info" class="select_info" style="width: 100%; max-width: 100%"> <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-banner"></div>
<!-- <div class="container-seperator horizontal" seperator-id="seperator-hostbanner-info"></div> --> <!-- <div class="container-seperator horizontal" seperator-id="seperator-hostbanner-info"></div> -->
<div class="container-select-info"></div> <div class="container-select-info"></div>
@ -1029,7 +1076,8 @@
<div class="group_box"> <div class="group_box">
<div class="header">{{tr "Microphone" /}}</div> <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-row settings-device settings-device-microphone">
<div class="form-group settings-device-select"> <div class="form-group settings-device-select">
<label for="select-settings-microphone-device" class="bmd-label-static">{{tr "Device:" /}}</label> <label for="select-settings-microphone-device" class="bmd-label-static">{{tr "Device:" /}}</label>
@ -1088,6 +1136,9 @@
</div> </div>
</div> </div>
</div> </div>
{{else}}
<div>{{tr "Voice had been disabled" /}}</div>
{{/if}}
</div> </div>
</div> </div>
<div class="group_box"> <div class="group_box">
@ -2057,7 +2108,10 @@
<table class="select_info_table"> <table class="select_info_table">
<tr> <tr>
<td>{{tr "Name:" /}}</td> <td>{{tr "Name:" /}}</td>
<td><node key="client_name"/></td> <td style="display: flex; flex-direction: row">
<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> </tr>
{{if property_client_description.length > 0}} {{if property_client_description.length > 0}}
<tr> <tr>
@ -2068,7 +2122,14 @@
{{if !client_is_query}} {{if !client_is_query}}
<tr> <tr>
<td>{{tr "Version:"/}}</td> <td>{{tr "Version:"/}}</td>
<td><a title="{{>property_client_version}}">{{*: data.property_client_version.split(" ")[0]; }}</a> on {{>property_client_platform}}</td> <td>
<a title="{{>property_client_version}}">{{*: data.property_client_version.split(" ")[0]; }}</a>
{{if client_is_web && false}} <!-- we cant show any browser info because every browser claims to be any browser as well -->
<div class="icon client-message_info button-browser-info" title="{{tr 'Browser info' /}}"></div>
{{/if}}
on
<a>{{>property_client_platform}}</a>
</td>
</tr> </tr>
{{/if}} {{/if}}
<tr> <tr>
@ -2242,26 +2303,11 @@
</script> </script>
<script class="jsrender-template" id="tmpl_selected_hostbanner" type="text/html"> <script class="jsrender-template" id="tmpl_selected_hostbanner" type="text/html">
<div class="hostbanner"> <div class="hostbanner">
<a href="{{:property_virtualserver_hostbanner_url}}" target="_blank" style="display: flex; flex-direction: row; justify-content: center; height: 100%"> <a class="image-container" href="{{:property_virtualserver_hostbanner_url}}" target="_blank">
<div
<div style=" style="background: center no-repeat url({{:hostbanner_gfx_url}})"
background:center no-repeat url( alt="{{tr 'Host banner'/}}"
{{:property_virtualserver_hostbanner_gfx_url}}{{:cache_tag}} class="hostbanner-image hostbanner-mode-{{:property_virtualserver_hostbanner_mode}}"
);
background-position: center;
{{if property_virtualserver_hostbanner_mode == 0}}
{{else property_virtualserver_hostbanner_mode == 1}}
width: 100%; height: auto;
{{else property_virtualserver_hostbanner_mode == 2}}
background-size:contain;
width:100%;
height:100%
{{/if}}
"
alt="{{tr "Host banner"/}}"
></div> ></div>
</a> </a>
</div> </div>
@ -2312,7 +2358,7 @@
</table> </table>
</div> </div>
<button class="button-update btn_update">{{tr "Update info"/}}</button> <button class="button-update btn btn-success">{{tr "Update info"/}}</button>
</div> </div>
</script> </script>
<script class="jsrender-template" id="tmpl_selected_channel" type="text/html"> <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

View file

@ -1,6 +1,10 @@
/// <reference path="client.ts" /> /// <reference path="client.ts" />
/// <reference path="connection/ConnectionBase.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 { class FileEntry {
name: string; name: string;
datetime: number; datetime: number;

View file

@ -1,3 +1,5 @@
import LogType = log.LogType;
enum ChatType { enum ChatType {
GENERAL, GENERAL,
SERVER, SERVER,
@ -65,12 +67,11 @@ namespace MessageHelper {
} }
if(objects.length < number) if(objects.length < number)
console.warn(tr("Message to format contains invalid index (%o)"), number); log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
result.push(...formatElement(objects[number])); result.push(...formatElement(objects[number]));
found = found + 1 + offset; found = found + 1 + offset;
begin = found + 1; begin = found + 1;
console.log(tr("Offset: %d Number: %d"), offset, number);
} while(found++); } while(found++);
return result; return result;
@ -93,7 +94,7 @@ namespace MessageHelper {
}); });
if(result.error) { if(result.error) {
console.log("BBCode parse error: %o", result.errorQueue); log.error(LogCategory.GENERAL, tr("BBCode parse error: %o"), result.errorQueue);
return formatElement(message); return formatElement(message);
} }
@ -104,7 +105,7 @@ namespace MessageHelper {
class ChatMessage { class ChatMessage {
date: Date; date: Date;
message: JQuery[]; message: JQuery[];
private _htmlTag: JQuery<HTMLElement>; private _html_tag: JQuery<HTMLElement>;
constructor(message: JQuery[]) { constructor(message: JQuery[]) {
this.date = new Date(); this.date = new Date();
@ -117,8 +118,8 @@ class ChatMessage {
return str; return str;
} }
get htmlTag() { get html_tag() {
if(this._htmlTag) return this._htmlTag; if(this._html_tag) return this._html_tag;
let tag = $.spawn("div"); let tag = $.spawn("div");
tag.addClass("message"); tag.addClass("message");
@ -128,26 +129,30 @@ class ChatMessage {
dateTag.css("margin-right", "4px"); dateTag.css("margin-right", "4px");
dateTag.css("color", "dodgerblue"); dateTag.css("color", "dodgerblue");
this._htmlTag = tag; this._html_tag = tag;
tag.append(dateTag); tag.append(dateTag);
this.message.forEach(e => e.appendTo(tag)); this.message.forEach(e => e.appendTo(tag));
tag.hide();
return tag; return tag;
} }
} }
class ChatEntry { class ChatEntry {
handle: ChatBox; readonly handle: ChatBox;
type: ChatType; type: ChatType;
key: string; key: string;
history: ChatMessage[]; history: ChatMessage[];
owner_unique_id?: string;
private _name: string; private _name: string;
private _htmlTag: any; private _html_tag: any;
private _closeable: boolean;
private _unread : boolean; private _flag_closeable: boolean = true;
private _flag_unread : boolean = false;
private _flag_offline: boolean = false;
onMessageSend: (text: string) => void; onMessageSend: (text: string) => void;
onClose: () => boolean; onClose: () => boolean = () => true;
constructor(handle, type : ChatType, key) { constructor(handle, type : ChatType, key) {
this.handle = handle; this.handle = handle;
@ -155,8 +160,6 @@ class ChatEntry {
this.key = key; this.key = key;
this._name = key; this._name = key;
this.history = []; this.history = [];
this.onClose = function () { return true; }
} }
appendError(message: string, ...args) { appendError(message: string, ...args) {
@ -173,7 +176,7 @@ class ChatEntry {
this.history.push(entry); this.history.push(entry);
while(this.history.length > 100) { while(this.history.length > 100) {
let elm = this.history.pop_front(); let elm = this.history.pop_front();
elm.htmlTag.animate({opacity: 0}, 200, function () { elm.html_tag.animate({opacity: 0}, 200, function () {
$(this).detach(); $(this).detach();
}); });
} }
@ -181,66 +184,75 @@ class ChatEntry {
let box = $(this.handle.htmlTag).find(".messages"); let box = $(this.handle.htmlTag).find(".messages");
let mbox = $(this.handle.htmlTag).find(".message_box"); let mbox = $(this.handle.htmlTag).find(".message_box");
let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height(); let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height();
mbox.append(entry.htmlTag); mbox.append(entry.html_tag);
entry.htmlTag.show().css("opacity", "0").animate({opacity: 1}, 100); entry.html_tag.css("opacity", "0").animate({opacity: 1}, 100);
if(bottom) box.scrollTop(mbox.height()); if(bottom) box.scrollTop(mbox.height());
} else { } else {
this.unread = true; this.flag_unread = true;
} }
} }
displayHistory() { displayHistory() {
this.unread = false; this.flag_unread = false;
let box = $(this.handle.htmlTag).find(".messages"); let box = this.handle.htmlTag.find(".messages");
let mbox = $(this.handle.htmlTag).find(".message_box"); let mbox = box.find(".message_box").detach(); /* detach the message box to improve performance */
mbox.empty(); mbox.empty();
for(let e of this.history) { for(let e of this.history) {
mbox.append(e.htmlTag); mbox.append(e.html_tag);
if(e.htmlTag.is(":hidden")) e.htmlTag.show(); /* TODO Is this really totally useless?
Because its at least a performance bottleneck because is(...) recalculates the page style
if(e.htmlTag.is(":hidden"))
e.htmlTag.show();
*/
} }
mbox.appendTo(box);
box.scrollTop(mbox.height()); box.scrollTop(mbox.height());
} }
get htmlTag() { get html_tag() {
if(this._htmlTag) return this._htmlTag; if(this._html_tag)
return this._html_tag;
let tag = $.spawn("div"); let tag = $.spawn("div");
tag.addClass("chat"); tag.addClass("chat");
if(this._flag_unread)
tag.addClass('unread');
if(this._flag_offline)
tag.addClass('offline');
if(this._flag_closeable)
tag.addClass('closeable');
tag.append("<div class=\"chatIcon icon " + this.chatIcon() + "\"></div>"); tag.append($.spawn("div").addClass("chat-type icon " + this.chat_icon()));
tag.append("<a class='name'>" + this._name + "</a>"); tag.append($.spawn("a").addClass("name").text(this._name));
let closeTag = $.spawn("div"); let tag_close = $.spawn("div");
closeTag.addClass("btn_close icon client-tab_close_button"); tag_close.addClass("btn_close icon client-tab_close_button");
if(!this._closeable) closeTag.hide(); if(!this._flag_closeable) tag_close.hide();
tag.append(closeTag); tag.append(tag_close);
const _this = this; tag.click(() => { this.handle.activeChat = this; });
tag.click(function () { tag.on("contextmenu", (e) => {
_this.handle.activeChat = _this;
});
tag.on("contextmenu", function (e) {
e.preventDefault(); e.preventDefault();
let actions = []; let actions: ContextMenuEntry[] = [];
actions.push({ actions.push({
type: MenuEntryType.ENTRY, type: MenuEntryType.ENTRY,
icon: "", icon: "",
name: tr("Clear"), name: tr("Clear"),
callback: () => { callback: () => {
_this.history = []; this.history = [];
_this.displayHistory(); this.displayHistory();
} }
}); });
if(_this.closeable) { if(this.flag_closeable) {
actions.push({ actions.push({
type: MenuEntryType.ENTRY, type: MenuEntryType.ENTRY,
icon: "client-tab_close_button", icon: "client-tab_close_button",
name: tr("Close"), name: tr("Close"),
callback: () => { callback: () => {
chat.deleteChat(_this); chat.deleteChat(this);
} }
}); });
} }
@ -251,18 +263,20 @@ class ChatEntry {
name: tr("Close all private tabs"), name: tr("Close all private tabs"),
callback: () => { callback: () => {
//TODO Implement this? //TODO Implement this?
} },
visible: false
}); });
spawn_context_menu(e.pageX, e.pageY, ...actions); spawn_context_menu(e.pageX, e.pageY, ...actions);
}); });
closeTag.click(function () { tag_close.click(() => {
if($.isFunction(_this.onClose) && !_this.onClose()) return; if($.isFunction(this.onClose) && !this.onClose())
_this.handle.deleteChat(_this); return;
this.handle.deleteChat(this);
}); });
this._htmlTag = tag; return this._html_tag = tag;
return tag;
} }
focus() { focus() {
@ -271,33 +285,37 @@ class ChatEntry {
} }
set name(newName : string) { set name(newName : string) {
console.log(tr("Change name!"));
this._name = newName; this._name = newName;
this.htmlTag.find(".name").text(this._name); this.html_tag.find(".name").text(this._name);
} }
set closeable(flag : boolean) { set flag_closeable(flag : boolean) {
if(this._closeable == flag) return; if(this._flag_closeable == flag) return;
this._closeable = flag; this._flag_closeable = flag;
console.log(tr("Set closeable: ") + this._closeable);
if(flag) this.htmlTag.find(".btn_close").show(); this.html_tag.toggleClass('closeable', flag);
else this.htmlTag.find(".btn_close").hide();
} }
set unread(flag : boolean) { set flag_unread(flag : boolean) {
if(this._unread == flag) return; if(this._flag_unread == flag) return;
this._unread = flag; this._flag_unread = flag;
this.htmlTag.find(".chatIcon").attr("class", "chatIcon icon " + this.chatIcon()); this.html_tag.find(".chat-type").attr("class", "chat-type icon " + this.chat_icon());
if(flag) { this.html_tag.toggleClass('unread', flag);
this.htmlTag.find(".name").css("color", "blue");
} else {
this.htmlTag.find(".name").css("color", "black");
}
} }
private chatIcon() : string { get flag_offline() { return this._flag_offline; }
if(this._unread) {
set flag_offline(flag: boolean) {
if(flag == this._flag_offline)
return;
this._flag_offline = flag;
this.html_tag.toggleClass('offline', flag);
}
private chat_icon() : string {
if(this._flag_unread) {
switch (this.type) { switch (this.type) {
case ChatType.CLIENT: case ChatType.CLIENT:
return "client-new_chat"; return "client-new_chat";
@ -319,6 +337,10 @@ class ChatEntry {
class ChatBox { class ChatBox {
//https://regex101.com/r/YQbfcX/2
//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;
htmlTag: JQuery; htmlTag: JQuery;
chats: ChatEntry[]; chats: ChatEntry[];
private _activeChat: ChatEntry; private _activeChat: ChatEntry;
@ -359,10 +381,11 @@ class ChatBox {
return; return;
chat.serverChat().appendMessage(tr("Failed to send text message.")); chat.serverChat().appendMessage(tr("Failed to send text message."));
console.error(tr("Failed to send server text message: %o"), error); log.error(LogCategory.GENERAL, tr("Failed to send server text message: %o"), error);
}); });
}; };
this.serverChat().name = tr("Server chat"); this.serverChat().name = tr("Server chat");
this.serverChat().flag_closeable = false;
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => { this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
if(!globalClient.serverConnection) { if(!globalClient.serverConnection) {
@ -372,10 +395,11 @@ class ChatBox {
globalClient.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel()).catch(error => { globalClient.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel()).catch(error => {
chat.channelChat().appendMessage(tr("Failed to send text message.")); chat.channelChat().appendMessage(tr("Failed to send text message."));
console.error(tr("Failed to send channel text message: %o"), error); log.error(LogCategory.GENERAL, tr("Failed to send channel text message: %o"), error);
}); });
}; };
this.channelChat().name = tr("Channel chat"); this.channelChat().name = tr("Channel chat");
this.channelChat().flag_closeable = false;
globalClient.permissions.initializedListener.push(flag => { globalClient.permissions.initializedListener.push(flag => {
if(flag) this.activeChat0(this._activeChat); if(flag) this.activeChat0(this._activeChat);
@ -385,11 +409,15 @@ class ChatBox {
createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry { createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry {
let chat = new ChatEntry(this, type, key); let chat = new ChatEntry(this, type, key);
this.chats.push(chat); this.chats.push(chat);
this.htmlTag.find(".chats").append(chat.htmlTag); this.htmlTag.find(".chats").append(chat.html_tag);
if(!this._activeChat) this.activeChat = chat; if(!this._activeChat) this.activeChat = chat;
return chat; return chat;
} }
open_chats() : ChatEntry[] {
return this.chats;
}
findChat(key : string) : ChatEntry { findChat(key : string) : ChatEntry {
for(let e of this.chats) for(let e of this.chats)
if(e.key == key) return e; if(e.key == key) return e;
@ -398,7 +426,7 @@ class ChatBox {
deleteChat(chat : ChatEntry) { deleteChat(chat : ChatEntry) {
this.chats.remove(chat); this.chats.remove(chat);
chat.htmlTag.detach(); chat.html_tag.detach();
if(this._activeChat === chat) { if(this._activeChat === chat) {
if(this.chats.length > 0) if(this.chats.length > 0)
this.activeChat = this.chats.last(); this.activeChat = this.chats.last();
@ -414,8 +442,38 @@ class ChatBox {
this._input_message.val(""); this._input_message.val("");
this._input_message.trigger("input"); this._input_message.trigger("input");
/* preprocessing text */
const words = text.split(/[ \n]/);
for(let index = 0; index < words.length; index++) {
const flag_escaped = words[index].startsWith('!');
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
_try:
try {
const url = new URL(unescaped);
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
if(url.protocol !== 'http:' && url.protocol !== 'https:')
break _try;
if(flag_escaped)
words[index] = unescaped;
else {
text = undefined;
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
}
} catch(e) { /* word isn't an url */ }
if(unescaped.match(ChatBox.URL_REGEX)) {
if(flag_escaped)
words[index] = unescaped;
else {
text = undefined;
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
}
}
}
if(this._activeChat && $.isFunction(this._activeChat.onMessageSend)) if(this._activeChat && $.isFunction(this._activeChat.onMessageSend))
this._activeChat.onMessageSend(text); this._activeChat.onMessageSend(text || words.join(" "));
} }
set activeChat(chat : ChatEntry) { set activeChat(chat : ChatEntry) {
@ -427,27 +485,27 @@ class ChatBox {
private activeChat0(chat: ChatEntry) { private activeChat0(chat: ChatEntry) {
this._activeChat = chat; this._activeChat = chat;
for(let e of this.chats) for(let e of this.chats)
e.htmlTag.removeClass("active"); e.html_tag.removeClass("active");
let flagAllowSend = false; let disable_input = !chat;
if(this._activeChat) { if(this._activeChat) {
this._activeChat.htmlTag.addClass("active"); this._activeChat.html_tag.addClass("active");
this._activeChat.displayHistory(); this._activeChat.displayHistory();
if(globalClient && globalClient.permissions && globalClient.permissions.initialized()) if(!disable_input && globalClient && globalClient.permissions && globalClient.permissions.initialized())
switch (this._activeChat.type) { switch (this._activeChat.type) {
case ChatType.CLIENT: case ChatType.CLIENT:
flagAllowSend = true; disable_input = false;
break; break;
case ChatType.SERVER: case ChatType.SERVER:
flagAllowSend = globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1); disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
break; break;
case ChatType.CHANNEL: case ChatType.CHANNEL:
flagAllowSend = globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1); disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
break; break;
} }
} }
this._input_message.prop("disabled", !flagAllowSend); this._input_message.prop("disabled", disable_input);
} }
get activeChat(){ return this._activeChat; } get activeChat(){ return this._activeChat; }

View file

@ -50,7 +50,7 @@ enum ViewReasonId {
class TSClient { class TSClient {
channelTree: ChannelTree; channelTree: ChannelTree;
serverConnection: connection.ServerConnection; serverConnection: connection.ServerConnection;
voiceConnection: VoiceConnection; voiceConnection: VoiceConnection | undefined;
fileManager: FileManager; fileManager: FileManager;
selectInfo: InfoBar; selectInfo: InfoBar;
permissions: PermissionManager; permissions: PermissionManager;
@ -69,10 +69,12 @@ class TSClient {
this.fileManager = new FileManager(this); this.fileManager = new FileManager(this);
this.permissions = new PermissionManager(this); this.permissions = new PermissionManager(this);
this.groups = new GroupManager(this); this.groups = new GroupManager(this);
this.voiceConnection = new VoiceConnection(this);
this._ownEntry = new LocalClientEntry(this); this._ownEntry = new LocalClientEntry(this);
this.controlBar = new ControlBar(this, $("#control_bar")); this.controlBar = new ControlBar(this, $("#control_bar"));
this.channelTree.registerClient(this._ownEntry); this.channelTree.registerClient(this._ownEntry);
if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false))
this.voiceConnection = new VoiceConnection(this);
} }
setup() { setup() {
@ -114,7 +116,7 @@ class TSClient {
getClient() : LocalClientEntry { return this._ownEntry; } getClient() : LocalClientEntry { return this._ownEntry; }
getClientId() { return this._clientId; } //TODO here getClientId() { return this._clientId; }
set clientId(id: number) { set clientId(id: number) {
this._clientId = id; this._clientId = id;
@ -136,11 +138,14 @@ class TSClient {
this.channelTree.registerClient(this._ownEntry); this.channelTree.registerClient(this._ownEntry);
settings.setServer(this.channelTree.server); settings.setServer(this.channelTree.server);
this.permissions.requestPermissionList(); this.permissions.requestPermissionList();
this.serverConnection.send_command("channelsubscribeall");
if(this.groups.serverGroups.length == 0) if(this.groups.serverGroups.length == 0)
this.groups.requestGroups(); this.groups.requestGroups();
this.controlBar.updateProperties(); this.controlBar.updateProperties();
if(!this.voiceConnection.current_encoding_supported()) 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 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,7 +275,8 @@ class TSClient {
} }
this.channelTree.reset(); this.channelTree.reset();
this.voiceConnection.dropSession(); if(this.voiceConnection)
this.voiceConnection.dropSession();
if(this.serverConnection) this.serverConnection.disconnect(); if(this.serverConnection) this.serverConnection.disconnect();
this.controlBar.update_connection_state(); this.controlBar.update_connection_state();
this.selectInfo.setCurrentSelected(null); this.selectInfo.setCurrentSelected(null);
@ -292,7 +298,7 @@ class TSClient {
this._reconnect_timer = setTimeout(() => { this._reconnect_timer = setTimeout(() => {
this._reconnect_timer = undefined; this._reconnect_timer = undefined;
chat.serverChat().appendMessage(tr("Reconnecting...")); chat.serverChat().appendMessage(tr("Reconnecting..."));
console.log(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.startConnection(server_address.host + ":" + server_address.port, profile, name, password ? { password: password, hashed: true} : undefined);
this._reconnect_attempt = true; this._reconnect_attempt = true;
}, 5000); }, 5000);

View file

@ -1,3 +1,4 @@
namespace connection { namespace connection {
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
constructor(connection: AbstractServerConnection) { constructor(connection: AbstractServerConnection) {
@ -26,6 +27,7 @@ namespace connection {
this["notifychannelmoved"] = this.handleNotifyChannelMoved; this["notifychannelmoved"] = this.handleNotifyChannelMoved;
this["notifychanneledited"] = this.handleNotifyChannelEdited; this["notifychanneledited"] = this.handleNotifyChannelEdited;
this["notifytextmessage"] = this.handleNotifyTextMessage; this["notifytextmessage"] = this.handleNotifyTextMessage;
this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed;
this["notifyclientupdated"] = this.handleNotifyClientUpdated; this["notifyclientupdated"] = this.handleNotifyClientUpdated;
this["notifyserveredited"] = this.handleNotifyServerEdited; this["notifyserveredited"] = this.handleNotifyServerEdited;
this["notifyserverupdated"] = this.handleNotifyServerUpdated; this["notifyserverupdated"] = this.handleNotifyServerUpdated;
@ -37,6 +39,9 @@ namespace connection {
this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd; this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd;
this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove; this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove;
this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged; this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged;
this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed;
this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed;
} }
handle_command(command: ServerCommand) : boolean { handle_command(command: ServerCommand) : boolean {
@ -81,8 +86,12 @@ namespace connection {
handleCommandServerInit(json){ handleCommandServerInit(json){
//We could setup the voice channel //We could setup the voice channel
console.log(tr("Setting up voice")); if( this.connection.client.voiceConnection) {
this.connection.client.voiceConnection.createSession(); 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 json = json[0]; //Only one bulk
@ -285,6 +294,26 @@ namespace connection {
client.updateVariables(...updates); client.updateVariables(...updates);
{
let client_chat = client.chat(false);
if(!client_chat) {
for(const c of chat.open_chats()) {
if(c.owner_unique_id == client.properties.client_unique_identifier && c.flag_offline) {
client_chat = c;
break;
}
}
}
if(client_chat) {
client_chat.appendMessage(
"{0}", true,
$.spawn("div")
.addClass("event-message event-partner-connect")
.text(tr("Your chat partner has reconnected"))
);
client_chat.flag_offline = false;
}
}
if(client instanceof LocalClientEntry) if(client instanceof LocalClientEntry)
this.connection.client.controlBar.updateVoice(); this.connection.client.controlBar.updateVoice();
} }
@ -364,6 +393,19 @@ namespace connection {
} else { } else {
console.error(tr("Unknown client left reason!")); console.error(tr("Unknown client left reason!"));
} }
{
const chat = client.chat(false);
if(chat) {
chat.flag_offline = true;
chat.appendMessage(
"{0}", true,
$.spawn("div")
.addClass("event-message event-partner-disconnect")
.text(tr("Your chat partner has disconnected"))
);
}
}
} }
tree.deleteClient(client); tree.deleteClient(client);
@ -499,7 +541,6 @@ namespace connection {
handleNotifyTextMessage(json) { handleNotifyTextMessage(json) {
json = json[0]; //Only one bulk json = json[0]; //Only one bulk
//TODO chat format?
let mode = json["targetmode"]; let mode = json["targetmode"];
if(mode == 1){ if(mode == 1){
let invoker = this.connection.client.channelTree.findClient(json["invokerid"]); let invoker = this.connection.client.channelTree.findClient(json["invokerid"]);
@ -530,6 +571,38 @@ namespace connection {
} }
} }
handleNotifyClientChatClosed(json) {
json = json[0]; //Only one bulk
//Chat partner has closed the conversation
//clid: "6"
//cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY="
const client = this.connection.client.channelTree.findClient(json["clid"]);
if(!client) {
log.warn(LogCategory.GENERAL, tr("Received chat close for unknown client"));
return;
}
if(client.properties.client_unique_identifier !== json["cluid"]) {
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but unique ids dosn't match. (expected %o, received %o)"), client.properties.client_unique_identifier, json["cluid"]);
return;
}
const chat = client.chat(false);
if(!chat) {
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open."));
return;
}
chat.flag_offline = true;
chat.appendMessage(
"{0}", true,
$.spawn("div")
.addClass("event-message event-partner-closed")
.text(tr("Your chat partner has close the conversation"))
);
}
handleNotifyClientUpdated(json) { handleNotifyClientUpdated(json) {
json = json[0]; //Only one bulk json = json[0]; //Only one bulk
@ -645,5 +718,31 @@ namespace connection {
sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF); sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF);
} }
} }
handleNotifyChannelSubscribed(json) {
for(const entry of json) {
const channel = this.connection.client.channelTree.findChannel(entry["cid"]);
if(!channel) {
console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']);
continue;
}
channel.flag_subscribed = true;
}
}
handleNotifyChannelUnsubscribed(json) {
for(const entry of json) {
const channel = this.connection.client.channelTree.findChannel(entry["cid"]);
if(!channel) {
console.warn(tr("Received channel unsubscribed for not visible channel (cid: %d)"), entry['cid']);
continue;
}
channel.flag_subscribed = false;
for(const client of channel.clients(false))
this.connection.client.channelTree.deleteClient(client);
}
}
} }
} }

View file

@ -28,20 +28,37 @@ namespace connection {
abstract disconnect(reason?: string) : Promise<void>; abstract disconnect(reason?: string) : Promise<void>;
abstract support_voice() : boolean; abstract support_voice() : boolean;
abstract voice_connection() : AbstractVoiceConnection | undefined; abstract voice_connection() : voice.AbstractVoiceConnection | undefined;
abstract command_handler_boss() : AbstractCommandHandlerBoss; abstract command_handler_boss() : AbstractCommandHandlerBoss;
abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>; abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>;
} }
export abstract class AbstractVoiceConnection { export namespace voice {
readonly connection: AbstractServerConnection; export interface VoiceClient {
client_id: number;
protected constructor(connection: AbstractServerConnection) { callback_playback: () => any;
this.connection = connection; callback_timeout: () => any;
callback_stopped: () => any;
get_volume() : number;
set_volume(volume: number) : Promise<void>;
} }
abstract connected() : boolean; export abstract class AbstractVoiceConnection {
readonly connection: AbstractServerConnection;
protected constructor(connection: AbstractServerConnection) {
this.connection = 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 { export class ServerCommand {

View file

@ -202,7 +202,12 @@ namespace connection {
arguments: json["data"] arguments: json["data"]
}); });
group.end(); 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 { else {
console.log(tr("Unknown command type %o"), json["type"]); console.log(tr("Unknown command type %o"), json["type"]);
} }
@ -233,7 +238,7 @@ namespace connection {
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> { send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
if(!this._socket || !this.connected()) { if(!this._socket || !this.connected()) {
console.warn(tr("Tried to send a command without a valid connection.")); console.warn(tr("Tried to send a command without a valid connection."));
return; return Promise.reject(tr("not connected"));
} }
const options: CommandOptions = {}; const options: CommandOptions = {};
@ -241,6 +246,8 @@ namespace connection {
Object.assign(options, _options); Object.assign(options, _options);
data = $.isArray(data) ? data : [data || {}]; data = $.isArray(data) ? data : [data || {}];
if(data.length == 0) /* we require min one arg to append return_code */
data.push({});
const _this = this; const _this = this;
let result = new Promise<CommandResult>((resolve, failed) => { let result = new Promise<CommandResult>((resolve, failed) => {
@ -299,7 +306,7 @@ namespace connection {
return false; return false;
} }
voice_connection(): connection.AbstractVoiceConnection | undefined { voice_connection(): connection.voice.AbstractVoiceConnection | undefined {
return undefined; return undefined;
} }

View file

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

1504
shared/js/i18n/country.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -53,7 +53,7 @@ namespace loader {
DONE DONE
} }
export let allow_cached_files: boolean = false; export let cache_tag: string | undefined;
let current_stage: Stage = Stage.INITIALIZING; let current_stage: Stage = Stage.INITIALIZING;
const tasks: {[key:number]:Task[]} = {}; const tasks: {[key:number]:Task[]} = {};
@ -206,7 +206,7 @@ namespace loader {
document.getElementById("scripts").appendChild(tag); 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); document.getElementById("style").appendChild(tag);
tag.href = path + (allow_cached_files ? "" : "?_ts=" + Date.now()); tag.href = path + (cache_tag || "");
}); });
} }
} }
@ -480,6 +480,7 @@ const loader_javascript = {
//Load general API's //Load general API's
"js/proto.js", "js/proto.js",
"js/i18n/localize.js", "js/i18n/localize.js",
"js/i18n/country.js",
"js/log.js", "js/log.js",
"js/sound/Sounds.js", "js/sound/Sounds.js",
@ -508,6 +509,7 @@ const loader_javascript = {
"js/ui/modal/ModalServerEdit.js", "js/ui/modal/ModalServerEdit.js",
"js/ui/modal/ModalChangeVolume.js", "js/ui/modal/ModalChangeVolume.js",
"js/ui/modal/ModalBanClient.js", "js/ui/modal/ModalBanClient.js",
"js/ui/modal/ModalBanCreate.js", "js/ui/modal/ModalBanCreate.js",
"js/ui/modal/ModalBanList.js", "js/ui/modal/ModalBanList.js",
"js/ui/modal/ModalYesNo.js", "js/ui/modal/ModalYesNo.js",
@ -631,6 +633,7 @@ const loader_style = {
"css/static/ts/tab.css", "css/static/ts/tab.css",
"css/static/ts/chat.css", "css/static/ts/chat.css",
"css/static/ts/icons.css", "css/static/ts/icons.css",
"css/static/ts/country.css",
"css/static/general.css", "css/static/general.css",
"css/static/modals.css", "css/static/modals.css",
"css/static/modal-bookmarks.css", "css/static/modal-bookmarks.css",
@ -662,7 +665,7 @@ const loader_style = {
async function load_templates() { async function load_templates() {
try { try {
const response = await $.ajax("templates.html" + (loader.allow_cached_files ? "" : "?_ts" + Date.now())); const response = await $.ajax("templates.html" + (loader.cache_tag || ""));
let node = document.createElement("html"); let node = document.createElement("html");
node.innerHTML = response; node.innerHTML = response;
@ -706,12 +709,11 @@ async function check_updates() {
if(!app_version) { if(!app_version) {
/* TODO add warning */ /* TODO add warning */
loader.allow_cached_files = false; loader.cache_tag = "?_ts=" + Date.now();
return; return;
} }
const cached_version = localStorage.getItem("cached_version"); const cached_version = localStorage.getItem("cached_version");
if(!cached_version || cached_version != app_version) { if(!cached_version || cached_version != app_version) {
loader.allow_cached_files = false;
loader.register_task(loader.Stage.LOADED, { loader.register_task(loader.Stage.LOADED, {
priority: 0, priority: 0,
name: "cached version updater", name: "cached version updater",
@ -719,11 +721,8 @@ async function check_updates() {
localStorage.setItem("cached_version", app_version); localStorage.setItem("cached_version", app_version);
} }
}); });
/* loading screen */
return;
} }
loader.cache_tag = "?_version=" + app_version;
loader.allow_cached_files = true;
} }
interface Window { interface Window {

View file

@ -7,7 +7,8 @@ enum LogCategory {
GENERAL, GENERAL,
NETWORKING, NETWORKING,
VOICE, VOICE,
I18N I18N,
IDENTITIES
} }
namespace log { namespace log {
@ -21,14 +22,15 @@ namespace log {
let category_mapping = new Map<number, string>([ let category_mapping = new Map<number, string>([
[LogCategory.CHANNEL, "Channel "], [LogCategory.CHANNEL, "Channel "],
[LogCategory.CLIENT, "Channel "], [LogCategory.CHANNEL_PROPERTIES, "Channel "],
[LogCategory.CHANNEL_PROPERTIES, "Client "], [LogCategory.CLIENT, "Client "],
[LogCategory.SERVER, "Server "], [LogCategory.SERVER, "Server "],
[LogCategory.PERMISSIONS, "Permission "], [LogCategory.PERMISSIONS, "Permission "],
[LogCategory.GENERAL, "General "], [LogCategory.GENERAL, "General "],
[LogCategory.NETWORKING, "Network "], [LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "], [LogCategory.VOICE, "Voice "],
[LogCategory.I18N, "I18N "] [LogCategory.I18N, "I18N "],
[LogCategory.IDENTITIES, "IDENTITIES "]
]); ]);
export let enabled_mapping = new Map<number, boolean>([ export let enabled_mapping = new Map<number, boolean>([
@ -40,7 +42,8 @@ namespace log {
[LogCategory.GENERAL, true], [LogCategory.GENERAL, true],
[LogCategory.NETWORKING, true], [LogCategory.NETWORKING, true],
[LogCategory.VOICE, true], [LogCategory.VOICE, true],
[LogCategory.I18N, false] [LogCategory.I18N, false],
[LogCategory.IDENTITIES, true]
]); ]);
loader.register_task(loader.Stage.LOADED, { loader.register_task(loader.Stage.LOADED, {
@ -109,10 +112,16 @@ namespace log {
name = "[%s] " + name; name = "[%s] " + name;
optionalParams.unshift(category_mapping.get(category)); optionalParams.unshift(category_mapping.get(category));
return new Group(level, category, name, optionalParams); return new Group(GroupMode.PREFIX, level, category, name, optionalParams);
}
enum GroupMode {
NATIVE,
PREFIX
} }
export class Group { export class Group {
readonly mode: GroupMode;
readonly level: LogType; readonly level: LogType;
readonly category: LogCategory; readonly category: LogCategory;
readonly enabled: boolean; readonly enabled: boolean;
@ -123,9 +132,11 @@ namespace log {
private readonly optionalParams: any[][]; private readonly optionalParams: any[][];
private _collapsed: boolean = true; private _collapsed: boolean = true;
private initialized = false; private initialized = false;
private _log_prefix: string;
constructor(level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) { constructor(mode: GroupMode, level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
this.level = level; this.level = level;
this.mode = mode;
this.category = category; this.category = category;
this.name = name; this.name = name;
this.optionalParams = optionalParams; this.optionalParams = optionalParams;
@ -133,7 +144,7 @@ namespace log {
} }
group(level: LogType, name: string, ...optionalParams: any[]) : Group { group(level: LogType, name: string, ...optionalParams: any[]) : Group {
return new Group(level, this.category, name, optionalParams, this); return new Group(this.mode, level, this.category, name, optionalParams, this);
} }
collapsed(flag: boolean = true) : this { collapsed(flag: boolean = true) : this {
@ -146,19 +157,43 @@ namespace log {
return this; return this;
if(!this.initialized) { if(!this.initialized) {
if(this._collapsed && console.groupCollapsed) if(this.mode == GroupMode.NATIVE) {
console.groupCollapsed(this.name, ...this.optionalParams); if(this._collapsed && console.groupCollapsed)
else console.groupCollapsed(this.name, ...this.optionalParams);
console.group(this.name, ...this.optionalParams); else
console.group(this.name, ...this.optionalParams);
} else {
this._log_prefix = " ";
let parent = this.owner;
while(parent) {
if(parent.mode == GroupMode.PREFIX)
this._log_prefix = this._log_prefix + parent._log_prefix;
else
break;
}
}
this.initialized = true; this.initialized = true;
} }
logDirect(this.level, message, ...optionalParams); if(this.mode == GroupMode.NATIVE)
logDirect(this.level, message, ...optionalParams);
else
logDirect(this.level, this._log_prefix + message, ...optionalParams);
return this; return this;
} }
end() { end() {
if(this.initialized) if(this.initialized) {
console.groupEnd(); if(this.mode == GroupMode.NATIVE)
console.groupEnd();
}
}
get prefix() : string {
return this._log_prefix;
}
set prefix(prefix: string) {
this._log_prefix = prefix;
} }
} }
} }

View file

@ -238,15 +238,15 @@ function main() {
chat = new ChatBox($("#chat")); chat = new ChatBox($("#chat"));
globalClient.setup(); globalClient.setup();
if(settings.static("connect_default", false) && settings.static("connect_address", "")) { if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static("connect_profile") as string; const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
console.log("UUID: %s", profile_uuid); console.log("UUID: %s", profile_uuid);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile(); const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const address = settings.static("connect_address", ""); const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const username = settings.static("connect_username", "Another TeaSpeak user"); const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
const password = settings.static("connect_password", ""); const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
const password_hashed = settings.static("connect_password_hashed", false); const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
if(profile && profile.valid()) { if(profile && profile.valid()) {
globalClient.startConnection(address, profile, username, password.length > 0 ? { globalClient.startConnection(address, profile, username, password.length > 0 ? {
@ -270,6 +270,7 @@ function main() {
clearTimeout(_resize_timeout); clearTimeout(_resize_timeout);
_resize_timeout = setTimeout(() => { _resize_timeout = setTimeout(() => {
globalClient.channelTree.handle_resized(); globalClient.channelTree.handle_resized();
globalClient.selectInfo.handle_resize();
}, 1000); }, 1000);
}); });

View file

@ -20,8 +20,7 @@ namespace profiles.identities {
authentication_method: this.identity.type(), authentication_method: this.identity.type(),
client_nickname: this.identity.name() client_nickname: this.identity.name()
}).catch(error => { }).catch(error => {
console.error(tr("Failed to initialize name based handshake. Error: %o"), error); log.error(LogCategory.IDENTITIES, tr("Failed to initialize name based handshake. Error: %o"), error);
if(error instanceof CommandResult) if(error instanceof CommandResult)
error = error.extra_message || error.message; error = error.extra_message || error.message;
this.trigger_fail("failed to execute begin (" + error + ")"); this.trigger_fail("failed to execute begin (" + error + ")");

View file

@ -19,7 +19,7 @@ namespace profiles.identities {
authentication_method: this.identity.type(), authentication_method: this.identity.type(),
data: this.identity.data_json() data: this.identity.data_json()
}).catch(error => { }).catch(error => {
console.error(tr("Failed to initialize TeaForum based handshake. Error: %o"), error); log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
if(error instanceof CommandResult) if(error instanceof CommandResult)
error = error.extra_message || error.message; error = error.extra_message || error.message;
@ -32,7 +32,7 @@ namespace profiles.identities {
this.connection.send_command("handshakeindentityproof", { this.connection.send_command("handshakeindentityproof", {
proof: this.identity.data_sign() proof: this.identity.data_sign()
}).catch(error => { }).catch(error => {
console.error(tr("Failed to proof the identity. Error: %o"), error); log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
if(error instanceof CommandResult) if(error instanceof CommandResult)
error = error.extra_message || error.message; error = error.extra_message || error.message;

View file

@ -214,7 +214,7 @@ namespace profiles.identities {
authentication_method: this.identity.type(), authentication_method: this.identity.type(),
publicKey: this.identity.public_key publicKey: this.identity.public_key
}).catch(error => { }).catch(error => {
console.error(tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error); log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
if(error instanceof CommandResult) if(error instanceof CommandResult)
error = error.extra_message || error.message; error = error.extra_message || error.message;
@ -230,7 +230,7 @@ namespace profiles.identities {
this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => { this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => {
this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => { this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => {
console.error(tr("Failed to proof the identity. Error: %o"), error); log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
if(error instanceof CommandResult) if(error instanceof CommandResult)
error = error.extra_message || error.message; error = error.extra_message || error.message;
@ -281,7 +281,7 @@ namespace profiles.identities {
resolve(); resolve();
}; };
this._worker.onerror = event => { this._worker.onerror = event => {
console.error("POW Worker error %o", event); log.error(LogCategory.IDENTITIES, tr("POW Worker error %o"), event);
clearTimeout(timeout_id); clearTimeout(timeout_id);
reject("Failed to load worker (" + event.message + ")"); reject("Failed to load worker (" + event.message + ")");
}; };
@ -394,7 +394,7 @@ namespace profiles.identities {
}; };
}); });
} catch(error) { } catch(error) {
console.warn("Failed to finalize POW worker! (%o)", error); log.error(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error);
} }
this._worker.terminate(); this._worker.terminate();
@ -402,7 +402,7 @@ namespace profiles.identities {
} }
private handle_message(message: any) { private handle_message(message: any) {
console.log("Received message: %o", message); log.info(LogCategory.IDENTITIES, tr("Received message: %o"), message);
} }
} }
@ -412,7 +412,7 @@ namespace profiles.identities {
try { try {
key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
} catch(e) { } catch(e) {
console.error(tr("Could not generate a new key: %o"), e); log.error(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e);
throw "Failed to generate keypair"; throw "Failed to generate keypair";
} }
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false); const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
@ -483,7 +483,7 @@ namespace profiles.identities {
if(this.private_key && (typeof(initialize) === "undefined" || initialize)) { if(this.private_key && (typeof(initialize) === "undefined" || initialize)) {
this.initialize().catch(error => { this.initialize().catch(error => {
console.error("Failed to initialize TeaSpeakIdentity (%s)", error); log.error(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error);
this._initialized = false; this._initialized = false;
}); });
} }
@ -633,7 +633,7 @@ namespace profiles.identities {
try { try {
await Promise.all(initialize_promise); await Promise.all(initialize_promise);
} catch(error) { } catch(error) {
console.error(error); log.error(LogCategory.IDENTITIES, error);
throw "failed to initialize"; throw "failed to initialize";
} }
} }
@ -688,7 +688,7 @@ namespace profiles.identities {
if(worker.current_level() > best_level) { if(worker.current_level() > best_level) {
this.hash_number = worker.current_hash(); this.hash_number = worker.current_hash();
console.log("Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level); log.info(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
best_level = worker.current_level(); best_level = worker.current_level();
if(callback_level) if(callback_level)
callback_level(best_level); callback_level(best_level);
@ -712,7 +712,7 @@ namespace profiles.identities {
}).catch(error => { }).catch(error => {
worker_promise.remove(p); worker_promise.remove(p);
console.warn("POW worker error %o", error); log.warn(LogCategory.IDENTITIES, "POW worker error %o", error);
reject(error); reject(error);
return Promise.resolve(); return Promise.resolve();
@ -736,7 +736,7 @@ namespace profiles.identities {
try { try {
await Promise.all(finalize_promise); await Promise.all(finalize_promise);
} catch(error) { } catch(error) {
console.error(error); log.error(LogCategory.IDENTITIES, error);
throw "failed to finalize"; throw "failed to finalize";
} }
} }
@ -761,14 +761,14 @@ namespace profiles.identities {
try { try {
this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]); this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]);
} catch(error) { } catch(error) {
console.error(error); log.error(LogCategory.IDENTITIES, error);
throw "failed to create crypto sign key"; throw "failed to create crypto sign key";
} }
try { try {
this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
} catch(error) { } catch(error) {
console.error(error); log.error(LogCategory.IDENTITIES, error);
throw "failed to create crypto key"; throw "failed to create crypto key";
} }
@ -776,7 +776,7 @@ namespace profiles.identities {
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key)); this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key));
} catch(error) { } catch(error) {
console.error(error); log.error(LogCategory.IDENTITIES, error);
throw "failed to calculate unique id"; throw "failed to calculate unique id";
} }

View file

@ -12,6 +12,15 @@ if(typeof(customElements) !== "undefined") {
} }
} }
/* T = value type */
interface SettingsKey<T> {
key: string;
fallback_keys?: string | string[];
fallback_imports?: {[key: string]:(value: string) => T};
description?: string;
}
class StaticSettings { class StaticSettings {
private static _instance: StaticSettings; private static _instance: StaticSettings;
static get instance() : StaticSettings { static get instance() : StaticSettings {
@ -20,12 +29,14 @@ class StaticSettings {
return this._instance; return this._instance;
} }
protected static transformStO?<T>(input?: string, _default?: T) : T { protected static transformStO?<T>(input?: string, _default?: T, default_type?: string) : T {
default_type = default_type || typeof _default;
if (typeof input === "undefined") return _default; if (typeof input === "undefined") return _default;
if (typeof _default === "string") return input as any; if (default_type === "string") return input as any;
else if (typeof _default === "number") return parseInt(input) as any; else if (default_type === "number") return parseInt(input) as any;
else if (typeof _default === "boolean") return (input == "1" || input == "true") as any; else if (default_type === "boolean") return (input == "1" || input == "true") as any;
else if (typeof _default === "undefined") return input as any; else if (default_type === "undefined") return input as any;
return JSON.parse(input) as any; return JSON.parse(input) as any;
} }
@ -37,6 +48,35 @@ class StaticSettings {
return JSON.stringify(input); return JSON.stringify(input);
} }
protected static resolveKey<T>(key: SettingsKey<T>, _default: T, resolver: (key: string) => string | boolean, default_type?: string) : T {
let value = resolver(key.key);
if(!value) {
/* trying fallbacks */
for(const fallback of key.fallback_keys || []) {
value = resolver(fallback);
if(typeof(value) === "string") {
/* fallback key succeeded */
const importer = (key.fallback_imports || {})[fallback];
if(importer)
return importer(value);
break;
}
}
}
if(typeof(value) !== 'string')
return _default;
return StaticSettings.transformStO(value as string, _default, default_type);
}
protected static keyify<T>(key: string | SettingsKey<T>) : SettingsKey<T> {
if(typeof(key) === "string")
return {key: key};
if(typeof(key) === "object" && key.key)
return key;
throw "key is not a key";
}
protected _handle: StaticSettings; protected _handle: StaticSettings;
protected _staticPropsTag: JQuery; protected _staticPropsTag: JQuery;
@ -59,25 +99,98 @@ class StaticSettings {
}); });
} }
static?<T>(key: string, _default?: T) : T { static?<T>(key: string | SettingsKey<T>, _default?: T, default_type?: string) : T {
if(this._handle) return this._handle.static<T>(key, _default); if(this._handle) return this._handle.static<T>(key, _default, default_type);
let result = this._staticPropsTag.find("[key='" + key + "']");
return StaticSettings.transformStO(result.length > 0 ? decodeURIComponent(result.last().attr("value")) : undefined, _default); key = StaticSettings.keyify(key);
return StaticSettings.resolveKey(key, _default, key => {
let result = this._staticPropsTag.find("[key='" + key + "']");
if(result.length > 0)
return decodeURIComponent(result.last().attr('value'));
return false;
}, default_type);
} }
deleteStatic(key: string) { deleteStatic<T>(key: string | SettingsKey<T>) {
if(this._handle) { if(this._handle) {
this._handle.deleteStatic(key); this._handle.deleteStatic<T>(key);
return; return;
} }
let result = this._staticPropsTag.find("[key='" + key + "']");
key = StaticSettings.keyify(key);
let result = this._staticPropsTag.find("[key='" + key.key + "']");
if(result.length != 0) result.detach(); if(result.length != 0) result.detach();
} }
} }
class Settings extends StaticSettings { class Settings extends StaticSettings {
static readonly KEY_DISABLE_CONTEXT_MENU = "disableContextMenu"; static readonly KEY_DISABLE_CONTEXT_MENU: SettingsKey<boolean> = {
static readonly KEY_DISABLE_UNLOAD_DIALOG = "disableUnloadDialog"; key: 'disableContextMenu',
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier'
};
static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey<boolean> = {
key: 'disableUnloadDialog',
description: 'Disables the unload popup on side closing'
};
static readonly KEY_DISABLE_VOICE: SettingsKey<boolean> = {
key: 'disableVoice',
description: 'Disables the voice bridge. If disabled, the audio and codec workers aren\'t required anymore'
};
/* Control bar */
static readonly KEY_CONTROL_MUTE_INPUT: SettingsKey<boolean> = {
key: 'mute_input'
};
static readonly KEY_CONTROL_MUTE_OUTPUT: SettingsKey<boolean> = {
key: 'mute_output'
};
static readonly KEY_CONTROL_SHOW_QUERIES: SettingsKey<boolean> = {
key: 'show_server_queries'
};
static readonly KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL: SettingsKey<boolean> = {
key: 'channel_subscribe_all'
};
/* Connect parameters */
static readonly KEY_FLAG_CONNECT_DEFAULT: SettingsKey<boolean> = {
key: 'connect_default'
};
static readonly KEY_CONNECT_ADDRESS: SettingsKey<string> = {
key: 'connect_address'
};
static readonly KEY_CONNECT_PROFILE: SettingsKey<string> = {
key: 'connect_profile'
};
static readonly KEY_CONNECT_USERNAME: SettingsKey<string> = {
key: 'connect_username'
};
static readonly KEY_CONNECT_PASSWORD: SettingsKey<string> = {
key: 'connect_password'
};
static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey<boolean> = {
key: 'connect_password_hashed'
};
static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel: ChannelEntry) => SettingsKey<ChannelSubscribeMode> = channel => {
return {
key: 'channel_subscribe_mode_' + channel.getChannelId()
}
};
static readonly KEYS = (() => {
const result = [];
for(const key in Settings) {
if(!key.toUpperCase().startsWith("KEY_"))
continue;
if(key.toUpperCase() == "KEYS")
continue;
result.push(key);
}
return result;
})();
private static readonly UPDATE_DIRECT: boolean = true; private static readonly UPDATE_DIRECT: boolean = true;
private cacheGlobal = {}; private cacheGlobal = {};
@ -96,37 +209,41 @@ class Settings extends StaticSettings {
}, 5 * 1000); }, 5 * 1000);
} }
static_global?<T>(key: string, _default?: T) : T { static_global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
let _static = this.static<string>(key); const default_object = { seed: Math.random() } as any;
if(_static) return StaticSettings.transformStO(_static, _default); let _static = this.static(key, default_object, typeof _default);
if(_static !== default_object) return StaticSettings.transformStO(_static, _default);
return this.global<T>(key, _default); return this.global<T>(key, _default);
} }
global?<T>(key: string, _default?: T) : T { global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
let result = this.cacheGlobal[key]; return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheGlobal[key]);
return StaticSettings.transformStO(result, _default);
} }
server?<T>(key: string, _default?: T) : T { server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
let result = this.cacheServer[key]; return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
return StaticSettings.transformStO(result, _default);
} }
changeGlobal<T>(key: string, value?: T){ changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
if(this.cacheGlobal[key] == value) return; key = Settings.keyify(key);
if(this.cacheGlobal[key.key] == value) return;
this.updated = true; this.updated = true;
this.cacheGlobal[key] = StaticSettings.transformOtS(value); this.cacheGlobal[key.key] = StaticSettings.transformOtS(value);
if(Settings.UPDATE_DIRECT) if(Settings.UPDATE_DIRECT)
this.save(); this.save();
} }
changeServer<T>(key: string, value?: T) { changeServer<T>(key: string | SettingsKey<T>, value?: T) {
if(this.cacheServer[key] == value) return; key = Settings.keyify(key);
if(this.cacheServer[key.key] == value) return;
this.updated = true; this.updated = true;
this.cacheServer[key] = StaticSettings.transformOtS(value); this.cacheServer[key.key] = StaticSettings.transformOtS(value);
if(Settings.UPDATE_DIRECT) if(Settings.UPDATE_DIRECT)
this.save(); this.save();

View file

@ -286,7 +286,7 @@ namespace sound {
try { try {
console.log(tr("Decoding data")); console.log(tr("Decoding data"));
context.decodeAudioData(buffer, result => { context.decodeAudioData(buffer, result => {
console.log(tr("Got decoded data")); log.info(LogCategory.VOICE, tr("Got decoded data"));
file.cached = result; file.cached = result;
play(sound, options); play(sound, options);
}, error => { }, error => {

View file

@ -14,6 +14,12 @@ namespace ChannelType {
} }
} }
enum ChannelSubscribeMode {
SUBSCRIBED,
UNSUBSCRIBED,
INHERITED
}
class ChannelProperties { class ChannelProperties {
channel_order: number = 0; channel_order: number = 0;
channel_name: string = ""; channel_name: string = "";
@ -71,6 +77,9 @@ class ChannelEntry {
private _cached_channel_description_promise_resolve: any = undefined; private _cached_channel_description_promise_resolve: any = undefined;
private _cached_channel_description_promise_reject: any = undefined; private _cached_channel_description_promise_reject: any = undefined;
private _flag_subscribed: boolean;
private _subscribe_mode: ChannelSubscribeMode;
constructor(channelId, channelName, parent = null) { constructor(channelId, channelName, parent = null) {
this.properties = new ChannelProperties(); this.properties = new ChannelProperties();
this.channelId = channelId; this.channelId = channelId;
@ -359,17 +368,35 @@ class ChannelEntry {
} }
initializeListener() { initializeListener() {
const _this = this; const tag_channel = this.channelTag();
this.channelTag().click(function () { tag_channel.on('click', () => this.channelTree.onSelect(this));
_this.channelTree.onSelect(_this); tag_channel.on('dblclick', () => {
});
this.channelTag().dblclick(() => {
if($.isArray(this.channelTree.currently_selected)) { //Multiselect if($.isArray(this.channelTree.currently_selected)) { //Multiselect
return; return;
} }
this.joinChannel() this.joinChannel()
}); });
let last_touch: number = 0;
let touch_start: number = 0;
tag_channel.on('touchend', event => {
/* if over 250ms then its not a click its more a drag */
if(Date.now() - touch_start > 250) {
touch_start = 0;
return;
}
if(Date.now() - last_touch > 750) {
last_touch = Date.now();
return;
}
last_touch = Date.now();
/* double touch */
tag_channel.trigger('dblclick');
});
tag_channel.on('touchstart', event => {
touch_start = Date.now();
});
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
this.channelTag().on("contextmenu", (event) => { this.channelTag().on("contextmenu", (event) => {
event.preventDefault(); event.preventDefault();
@ -378,9 +405,9 @@ class ChannelEntry {
return; return;
} }
_this.channelTree.onSelect(_this, true); this.channelTree.onSelect(this, true);
_this.showContextMenu(event.pageX, event.pageY, () => { this.showContextMenu(event.pageX, event.pageY, () => {
_this.channelTree.onSelect(undefined, true); this.channelTree.onSelect(undefined, true);
}); });
}); });
} }
@ -424,13 +451,49 @@ class ChannelEntry {
flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_TEMPORARY).granted(1); flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_TEMPORARY).granted(1);
} }
let trigger_close = true;
spawn_context_menu(x, y, { 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, type: MenuEntryType.ENTRY,
icon: "client-channel_switch", icon: "client-channel_switch",
name: tr("<b>Switch to channel</b>"), name: tr("<b>Switch to channel</b>"),
callback: () => this.joinChannel() callback: () => this.joinChannel()
}, },
MenuEntry.HR(), MenuEntry.HR(),
{
type: MenuEntryType.ENTRY,
icon: "client-subscribe_to_channel",
name: tr("<b>Subscribe to channel</b>"),
callback: () => this.subscribe(),
visible: !this.flag_subscribed
},
{
type: MenuEntryType.ENTRY,
icon: "client-channel_unsubscribed",
name: tr("<b>Unsubscribe from channel</b>"),
callback: () => this.unsubscribe(),
visible: this.flag_subscribed
},
{
type: MenuEntryType.ENTRY,
icon: "client-subscribe_mode",
name: tr("<b>Use inherited subscribe mode</b>"),
callback: () => this.unsubscribe(true),
visible: this.subscribe_mode != ChannelSubscribeMode.INHERITED
},
MenuEntry.HR(),
{ {
type: MenuEntryType.ENTRY, type: MenuEntryType.ENTRY,
icon: "client-channel_edit", icon: "client-channel_edit",
@ -507,7 +570,7 @@ class ChannelEntry {
invalidPermission: !channelCreate, invalidPermission: !channelCreate,
callback: () => this.channelTree.spawnCreateChannel() callback: () => this.channelTree.spawnCreateChannel()
}, },
MenuEntry.CLOSE(on_close) MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
); );
} }
@ -621,7 +684,10 @@ class ChannelEntry {
} }
} else if(key == "channel_codec") { } 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.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") { } else if(key == "channel_flag_default") {
(this.properties.channel_flag_default ? $.fn.show : $.fn.hide).apply(this.channelTag().find(".icons .icon_default")); (this.properties.channel_flag_default ? $.fn.show : $.fn.hide).apply(this.channelTag().find(".icons .icon_default"));
} else if(key == "channel_flag_password") } else if(key == "channel_flag_password")
@ -646,6 +712,7 @@ class ChannelEntry {
let tag = this.channelTag().find(".channel-type"); let tag = this.channelTag().find(".channel-type");
tag.removeAttr('class'); tag.removeAttr('class');
tag.addClass("show-channel-normal-only channel-type icon"); tag.addClass("show-channel-normal-only channel-type icon");
if(this._channel_name_formatted === undefined) if(this._channel_name_formatted === undefined)
tag.addClass("channel-normal"); tag.addClass("channel-normal");
@ -660,7 +727,7 @@ class ChannelEntry {
else else
type = "green"; type = "green";
tag.addClass("client-channel_" + type + "_subscribed"); tag.addClass("client-channel_" + type + (this._flag_subscribed ? "_subscribed" : ""));
} }
generate_bbcode() { generate_bbcode() {
@ -705,6 +772,66 @@ class ChannelEntry {
} }
}); });
} }
async subscribe() : Promise<void> {
if(this.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
return;
this.subscribe_mode = ChannelSubscribeMode.SUBSCRIBED;
const connection = this.channelTree.client.getServerConnection();
if(!this.flag_subscribed && connection)
await connection.send_command('channelsubscribe', {
'cid': this.getChannelId()
});
else
this.flag_subscribed = false;
}
async unsubscribe(inherited_subscription_mode?: boolean) : Promise<void> {
const connection = this.channelTree.client.getServerConnection();
let unsubscribe: boolean;
if(inherited_subscription_mode) {
this.subscribe_mode = ChannelSubscribeMode.INHERITED;
unsubscribe = this.flag_subscribed && !this.channelTree.client.controlBar.channel_subscribe_all;
} else {
this.subscribe_mode = ChannelSubscribeMode.UNSUBSCRIBED;
unsubscribe = this.flag_subscribed;
}
if(unsubscribe) {
if(connection)
await connection.send_command('channelunsubscribe', {
'cid': this.getChannelId()
});
else
this.flag_subscribed = false;
}
}
get flag_subscribed() : boolean {
return this._flag_subscribed;
}
set flag_subscribed(flag: boolean) {
if(this._flag_subscribed == flag)
return;
this._flag_subscribed = flag;
this.updateChannelTypeIcon();
}
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));
}
set subscribe_mode(mode: ChannelSubscribeMode) {
if(this.subscribe_mode == mode)
return;
this._subscribe_mode = mode;
settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode);
}
} }
//Global functions //Global functions

View file

@ -266,28 +266,40 @@ class ClientEntry {
} }
showContextMenu(x: number, y: number, on_close: () => void = undefined) { showContextMenu(x: number, y: number, on_close: () => void = undefined) {
const _this = this; let trigger_close = true;
spawn_context_menu(x, y, 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, type: MenuEntryType.ENTRY,
icon: "client-change_nickname", icon: "client-change_nickname",
name: tr("<b>Open text chat</b>"), name: tr("<b>Open text chat</b>"),
callback: function () { callback: () => {
chat.activeChat = _this.chat(true); chat.activeChat = this.chat(true);
chat.focus(); chat.focus();
} }
}, { }, {
type: MenuEntryType.ENTRY, type: MenuEntryType.ENTRY,
icon: "client-poke", icon: "client-poke",
name: tr("Poke client"), name: tr("Poke client"),
callback: function () { callback: () => {
createInputModal(tr("Poke client"), tr("Poke message:<br>"), text => true, result => { createInputModal(tr("Poke client"), tr("Poke message:<br>"), text => true, result => {
if(typeof(result) === "string") { if(typeof(result) === "string") {
//TODO tr //TODO tr
console.log("Poking client " + _this.clientNickName() + " with message " + result); console.log("Poking client " + this.clientNickName() + " with message " + result);
_this.channelTree.client.serverConnection.send_command("clientpoke", { this.channelTree.client.serverConnection.send_command("clientpoke", {
clid: _this.clientId(), clid: this.clientId(),
msg: result msg: result
}); });
@ -298,13 +310,13 @@ class ClientEntry {
type: MenuEntryType.ENTRY, type: MenuEntryType.ENTRY,
icon: "client-edit", icon: "client-edit",
name: tr("Change description"), name: tr("Change description"),
callback: function () { callback: () => {
createInputModal(tr("Change client description"), tr("New description:<br>"), text => true, result => { createInputModal(tr("Change client description"), tr("New description:<br>"), text => true, result => {
if(typeof(result) === "string") { if(typeof(result) === "string") {
//TODO tr //TODO tr
console.log("Changing " + _this.clientNickName() + "'s description to " + result); console.log("Changing " + this.clientNickName() + "'s description to " + result);
_this.channelTree.client.serverConnection.send_command("clientedit", { this.channelTree.client.serverConnection.send_command("clientedit", {
clid: _this.clientId(), clid: this.clientId(),
client_description: result client_description: result
}); });
@ -330,11 +342,11 @@ class ClientEntry {
name: tr("Kick client from channel"), name: tr("Kick client from channel"),
callback: () => { callback: () => {
createInputModal(tr("Kick client from channel"), tr("Kick reason:<br>"), text => true, result => { createInputModal(tr("Kick client from channel"), tr("Kick reason:<br>"), text => true, result => {
if(result) { if(typeof(result) !== 'boolean' || result) {
//TODO tr //TODO tr
console.log("Kicking client " + _this.clientNickName() + " from channel with reason " + result); console.log("Kicking client " + this.clientNickName() + " from channel with reason " + result);
_this.channelTree.client.serverConnection.send_command("clientkick", { this.channelTree.client.serverConnection.send_command("clientkick", {
clid: _this.clientId(), clid: this.clientId(),
reasonid: ViewReasonId.VREASON_CHANNEL_KICK, reasonid: ViewReasonId.VREASON_CHANNEL_KICK,
reasonmsg: result reasonmsg: result
}); });
@ -348,11 +360,11 @@ class ClientEntry {
name: tr("Kick client fom server"), name: tr("Kick client fom server"),
callback: () => { callback: () => {
createInputModal(tr("Kick client from server"), tr("Kick reason:<br>"), text => true, result => { createInputModal(tr("Kick client from server"), tr("Kick reason:<br>"), text => true, result => {
if(result) { if(typeof(result) !== 'boolean' || result) {
//TODO tr //TODO tr
console.log("Kicking client " + _this.clientNickName() + " from server with reason " + result); console.log("Kicking client " + this.clientNickName() + " from server with reason " + result);
_this.channelTree.client.serverConnection.send_command("clientkick", { this.channelTree.client.serverConnection.send_command("clientkick", {
clid: _this.clientId(), clid: this.clientId(),
reasonid: ViewReasonId.VREASON_SERVER_KICK, reasonid: ViewReasonId.VREASON_SERVER_KICK,
reasonmsg: result reasonmsg: result
}); });
@ -411,7 +423,7 @@ class ClientEntry {
}); });
} }
}, },
MenuEntry.CLOSE(on_close) MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
); );
} }
@ -661,19 +673,21 @@ class ClientEntry {
chat(create: boolean = false) : ChatEntry { chat(create: boolean = false) : ChatEntry {
let chatName = "client_" + this.clientUid() + ":" + this.clientId(); let chatName = "client_" + this.clientUid() + ":" + this.clientId();
let c = chat.findChat(chatName); let c = chat.findChat(chatName);
if((!c) && create) { if(!c && create) {
c = chat.createChat(chatName); c = chat.createChat(chatName);
c.closeable = true; c.flag_closeable = true;
c.name = this.clientNickName(); c.name = this.clientNickName();
c.owner_unique_id = this.properties.client_unique_identifier;
const _this = this; c.onMessageSend = text => {
c.onMessageSend = function (text: string) { this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, this);
_this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, _this);
}; };
c.onClose = function () : boolean { c.onClose = () => {
//TODO check online? if(!c.flag_offline)
_this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": _this.clientId()}); 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 true;
} }
} }
@ -908,8 +922,22 @@ class MusicClientEntry extends ClientEntry {
} }
showContextMenu(x: number, y: number, on_close: () => void = undefined): void { showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
let trigger_close = true;
spawn_context_menu(x, y, 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>"), name: tr("<b>Change bot name</b>"),
icon: "client-change_nickname", icon: "client-change_nickname",
disabled: false, disabled: false,
@ -1011,7 +1039,7 @@ class MusicClientEntry extends ClientEntry {
name: tr("Kick client from channel"), name: tr("Kick client from channel"),
callback: () => { callback: () => {
createInputModal(tr("Kick client from channel"), tr("Kick reason:<br>"), text => true, result => { createInputModal(tr("Kick client from channel"), tr("Kick reason:<br>"), text => true, result => {
if(result) { if(typeof(result) !== 'boolean' || result) {
console.log(tr("Kicking client %o from channel with reason %o"), this.clientNickName(), result); console.log(tr("Kicking client %o from channel with reason %o"), this.clientNickName(), result);
this.channelTree.client.serverConnection.send_command("clientkick", { this.channelTree.client.serverConnection.send_command("clientkick", {
clid: this.clientId(), clid: this.clientId(),
@ -1073,7 +1101,7 @@ class MusicClientEntry extends ClientEntry {
}, },
type: MenuEntryType.ENTRY type: MenuEntryType.ENTRY
}, },
MenuEntry.CLOSE(on_close) MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
); );
} }

View file

@ -19,6 +19,7 @@ class ControlBar {
private _away: boolean; private _away: boolean;
private _query_visible: boolean; private _query_visible: boolean;
private _awayMessage: string; private _awayMessage: string;
private _channel_subscribe_all: boolean;
private codec_supported: boolean = false; private codec_supported: boolean = false;
private support_playback: boolean = false; private support_playback: boolean = false;
@ -40,39 +41,43 @@ class ControlBar {
this.htmlTag.find(".btn_open_settings").on('click', this.onOpenSettings.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_permissions").on('click', this.onPermission.bind(this));
this.htmlTag.find(".btn_banlist").on('click', this.onBanlist.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)); this.htmlTag.find(".button-playlist-manage").on('click', this.on_playlist_manage.bind(this));
let dropdownify = (tag: JQuery) => {
tag.find(".button-dropdown").on('click', () => {
tag.addClass("displayed");
}).hover(() => {
console.log("Add");
tag.addClass("displayed");
}, () => {
if(tag.find(".dropdown:hover").length > 0)
return;
console.log("Removed");
tag.removeClass("displayed");
});
tag.on('mouseleave', () => {
tag.removeClass("displayed");
});
};
{ {
let tokens = this.htmlTag.find(".btn_token"); let tokens = this.htmlTag.find(".btn_token");
tokens.find(".button-dropdown").on('click', () => { dropdownify(tokens);
tokens.find(".dropdown").addClass("displayed");
});
tokens.on('mouseleave', () => {
tokens.find(".dropdown").removeClass("displayed");
});
tokens.find(".btn_token_use").on('click', this.on_token_use.bind(this)); 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)); tokens.find(".btn_token_list").on('click', this.on_token_list.bind(this));
} }
{ {
let away = this.htmlTag.find(".btn_away"); let away = this.htmlTag.find(".btn_away");
away.find(".button-dropdown").on('click', () => { dropdownify(away);
away.find(".dropdown").addClass("displayed");
});
away.on('mouseleave', () => {
away.find(".dropdown").removeClass("displayed");
});
away.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this)); 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)); away.find(".btn_away_message").on('click', this.on_away_set_message.bind(this));
} }
{ {
let bookmark = this.htmlTag.find(".btn_bookmark"); let bookmark = this.htmlTag.find(".btn_bookmark");
bookmark.find(".button-dropdown").on('click', () => { dropdownify(bookmark);
bookmark.find("> .dropdown").addClass("displayed");
});
bookmark.on('mouseleave', () => {
bookmark.find("> .dropdown").removeClass("displayed");
});
bookmark.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this)); 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)); bookmark.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
@ -81,22 +86,30 @@ class ControlBar {
} }
{ {
let query = this.htmlTag.find(".btn_query"); let query = this.htmlTag.find(".btn_query");
query.find(".button-dropdown").on('click', () => { dropdownify(query);
query.find(".dropdown").addClass("displayed");
});
query.on('mouseleave', () => {
query.find(".dropdown").removeClass("displayed");
});
query.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this)); query.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
query.find(".btn_query_create").on('click', this.on_query_create.bind(this)); query.find(".btn_query_create").on('click', this.on_query_create.bind(this));
query.find(".btn_query_manage").on('click', this.on_query_manage.bind(this)); query.find(".btn_query_manage").on('click', this.on_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"));
}
//Need an initialise //Need an initialise
this.muteInput = settings.static_global("mute_input", false); this.muteInput = settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false);
this.muteOutput = settings.static_global("mute_output", false); this.muteOutput = settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false);
this.query_visible = settings.static_global("show_server_queries", 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);
} }
@ -125,22 +138,20 @@ class ControlBar {
this._muteInput = flag; this._muteInput = flag;
let tag = this.htmlTag.find(".btn_mute_input"); let tag = this.htmlTag.find(".btn_mute_input");
if(flag) { const tag_icon = tag.find(".icon_x32, .icon");
if(!tag.hasClass("activated"))
tag.addClass("activated"); tag.toggleClass('activated', flag)
tag.find(".icon_x32").attr("class", "icon_x32 client-input_muted");
} else { tag_icon
if(tag.hasClass("activated")) .toggleClass('client-input_muted', flag)
tag.removeClass("activated"); .toggleClass('client-capture', !flag);
tag.find(".icon_x32").attr("class", "icon_x32 client-capture");
}
if(this.handle.serverConnection.connected) if(this.handle.serverConnection.connected())
this.handle.serverConnection.send_command("clientupdate", { this.handle.serverConnection.send_command("clientupdate", {
client_input_muted: this._muteInput client_input_muted: this._muteInput
}); });
settings.changeGlobal("mute_input", this._muteInput); settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, this._muteInput);
this.updateMicrophoneRecordState(); this.updateMicrophoneRecordState();
} }
@ -150,22 +161,21 @@ class ControlBar {
if(this._muteOutput == flag) return; if(this._muteOutput == flag) return;
this._muteOutput = flag; this._muteOutput = flag;
let tag = this.htmlTag.find(".btn_mute_output");
if(flag) {
if(!tag.hasClass("activated"))
tag.addClass("activated");
tag.find(".icon_x32").attr("class", "icon_x32 client-output_muted");
} else {
if(tag.hasClass("activated"))
tag.removeClass("activated");
tag.find(".icon_x32").attr("class", "icon_x32 client-volume");
}
if(this.handle.serverConnection.connected) let tag = this.htmlTag.find(".btn_mute_output");
const tag_icon = tag.find(".icon_x32, .icon");
tag.toggleClass('activated', flag)
tag_icon
.toggleClass('client-output_muted', flag)
.toggleClass('client-volume', !flag);
if(this.handle.serverConnection.connected())
this.handle.serverConnection.send_command("clientupdate", { this.handle.serverConnection.send_command("clientupdate", {
client_output_muted: this._muteOutput client_output_muted: this._muteOutput
}); });
settings.changeGlobal("mute_output", this._muteOutput); settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, this._muteOutput);
this.updateMicrophoneRecordState(); this.updateMicrophoneRecordState();
} }
@ -196,7 +206,8 @@ class ControlBar {
private updateMicrophoneRecordState() { private updateMicrophoneRecordState() {
let enabled = !this._muteInput && !this._muteOutput && !this._away; let enabled = !this._muteInput && !this._muteOutput && !this._away;
this.handle.voiceConnection.voiceRecorder.update(enabled); if(this.handle.voiceConnection)
this.handle.voiceConnection.voiceRecorder.update(enabled);
} }
updateProperties() { updateProperties() {
@ -212,12 +223,13 @@ class ControlBar {
} }
updateVoice(targetChannel?: ChannelEntry) { updateVoice(targetChannel?: ChannelEntry) {
if(!targetChannel) targetChannel = this.handle.getClient().currentChannel(); if(!targetChannel)
targetChannel = this.handle.getClient().currentChannel();
let client = this.handle.getClient(); let client = this.handle.getClient();
this.codec_supported = targetChannel ? this.handle.voiceConnection.codecSupported(targetChannel.properties.channel_codec) : true; this.codec_supported = targetChannel ? this.handle.voiceConnection && this.handle.voiceConnection.codecSupported(targetChannel.properties.channel_codec) : true;
this.support_record = this.handle.voiceConnection.voice_send_support(); this.support_record = this.handle.voiceConnection && this.handle.voiceConnection.voice_send_support();
this.support_playback = this.handle.voiceConnection.voice_playback_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_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.htmlTag.find(".btn_mute_output").prop("disabled", !this.codec_supported || !this.support_playback);
@ -421,7 +433,7 @@ class ControlBar {
if(this._query_visible == flag) return; if(this._query_visible == flag) return;
this._query_visible = flag; this._query_visible = flag;
settings.changeGlobal("show_server_queries", flag); settings.changeGlobal(Settings.KEY_CONTROL_SHOW_QUERIES, flag);
this.update_query_visibility_button(); this.update_query_visibility_button();
this.handle.channelTree.toggle_server_queries(flag); this.handle.channelTree.toggle_server_queries(flag);
} }
@ -432,12 +444,7 @@ class ControlBar {
} }
private update_query_visibility_button() { private update_query_visibility_button() {
let tag = this.htmlTag.find(".btn_query_toggle"); this.htmlTag.find(".btn_query_toggle").toggleClass('activated', this._query_visible);
if(this._query_visible) {
tag.addClass("activated");
} else {
tag.removeClass("activated");
}
} }
private on_query_create() { private on_query_create() {
@ -464,4 +471,33 @@ class ControlBar {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); 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

@ -53,7 +53,8 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
readonly handle: TSClient; readonly handle: TSClient;
private current_selected?: AvailableTypes; private current_selected?: AvailableTypes;
private _htmlTag: JQuery<HTMLElement>; private _tag: JQuery<HTMLElement>;
private _tag_content: JQuery<HTMLElement>;
private _tag_info: JQuery<HTMLElement>; private _tag_info: JQuery<HTMLElement>;
private _tag_banner: JQuery<HTMLElement>; private _tag_banner: JQuery<HTMLElement>;
@ -63,9 +64,10 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) { constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) {
this.handle = client; this.handle = client;
this._htmlTag = htmlTag; this._tag = htmlTag;
this._tag_info = htmlTag.find(".container-select-info"); this._tag_content = htmlTag.find("> .select_info");
this._tag_banner = htmlTag.find(".container-banner"); 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 MusicInfoManager());
this.managers.push(new ClientInfoManager()); this.managers.push(new ClientInfoManager());
@ -73,10 +75,24 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
this.managers.push(new ServerInfoManager()); this.managers.push(new ServerInfoManager());
this.banner_manager = new Hostbanner(client, this._tag_banner); 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) { setCurrentSelected(entry: AvailableTypes) {
if(this.current_selected == entry) return; if(this.current_selected == entry) return;
if(this._current_manager) { if(this._current_manager) {
(this._current_manager as InfoManager<AvailableTypes>).finalizeFrame(this.current_selected, this._tag_info); (this._current_manager as InfoManager<AvailableTypes>).finalizeFrame(this.current_selected, this._tag_info);
this._current_manager = null; this._current_manager = null;
@ -112,7 +128,20 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
current_manager() { return this._current_manager; } 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);
}
}
interface Window {
Image: typeof HTMLImageElement;
HTMLImageElement: typeof HTMLImageElement;
} }
class Hostbanner { class Hostbanner {
@ -136,9 +165,20 @@ class Hostbanner {
if(tag) { if(tag) {
tag.then(element => { tag.then(element => {
this.html_tag.empty(); const children = this.html_tag.children();
this.html_tag.append(element).removeClass("disabled"); this.html_tag.append(element).removeClass("disabled");
/* allow the new image be loaded from cache URL */
{
children
.css('z-index', '2')
.css('position', 'absolute')
.css('height', '100%')
.css('width', '100%');
setTimeout(() => {
children.detach();
}, 250);
}
}).catch(error => { }).catch(error => {
console.warn(tr("Failed to load hostbanner: %o"), error); console.warn(tr("Failed to load hostbanner: %o"), error);
this.html_tag.empty().addClass("disabled"); this.html_tag.empty().addClass("disabled");
@ -159,44 +199,63 @@ class Hostbanner {
for(let key in server.properties) for(let key in server.properties)
properties["property_" + key] = server.properties[key]; properties["property_" + key] = server.properties[key];
properties["hostbanner_gfx_url"] = server.properties.virtualserver_hostbanner_gfx_url;
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) { if(server.properties.virtualserver_hostbanner_gfx_interval > 0) {
const update_interval = Math.min(server.properties.virtualserver_hostbanner_gfx_interval, 60); const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60);
const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString(); const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString();
try { try {
const url = new URL(server.properties.virtualserver_hostbanner_gfx_url); const url = new URL(server.properties.virtualserver_hostbanner_gfx_url);
if(url.search.length == 0) if(url.search.length == 0)
properties["cache_tag"] = "?_ts=" + update_timestamp; properties["hostbanner_gfx_url"] += "?_ts=" + update_timestamp;
else else
properties["cache_tag"] = "&_ts=" + update_timestamp; properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
} catch(error) { } catch(error) {
console.warn(tr("Failed to parse banner URL: %o"), error); console.warn(tr("Failed to parse banner URL: %o"), error);
properties["cache_tag"] = "&_ts=" + update_timestamp; properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
} }
this.updater = setTimeout(() => this.update(), update_interval * 1000); this.updater = setTimeout(() => this.update(), update_interval * 1000);
} else {
properties["cache_tag"] = "";
} }
const rendered = $("#tmpl_selected_hostbanner").renderTag(properties); const rendered = $("#tmpl_selected_hostbanner").renderTag(properties);
console.debug(tr("Hostbanner has been loaded"));
return Promise.resolve(rendered);
/* if(window.fetch) {
const image = rendered.find("img"); return (async () => {
return new Promise<JQuery<HTMLElement>>((resolve, reject) => { const start = Date.now();
const node_image = image[0] as HTMLImageElement;
node_image.onload = () => { const tag_image = rendered.find(".hostbanner-image");
console.debug(tr("Hostbanner has been loaded"));
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) _fetch:
this.updater = setTimeout(() => this.update(), Math.min(server.properties.virtualserver_hostbanner_gfx_interval, 60) * 1000); try {
resolve(rendered); const result = await fetch(properties["hostbanner_gfx_url"]);
};
node_image.onerror = event => { if(!result.ok) {
reject(event); if(result.type === 'opaque' || result.type === 'opaqueredirect') {
} log.warn(LogCategory.SERVER, tr("Could not load hostbanner because 'Access-Control-Allow-Origin' isnt valid!"));
}); break _fetch;
*/ }
}
const url = URL.createObjectURL(await result.blob());
tag_image.css('background-image', 'url(' + url + ')');
log.debug(LogCategory.SERVER, tr("Fetsched hostbanner successfully (%o, type: %o, url: %o)"), Date.now() - start, result.type, url);
if(URL.revokeObjectURL) {
setTimeout(() => {
log.debug(LogCategory.SERVER, tr("Revoked hostbanner url %s"), url);
URL.revokeObjectURL(url);
}, 10000);
}
} catch(error) {
log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), error);
}
return rendered;
})();
} else {
console.debug(tr("Hostbanner has been loaded"));
return Promise.resolve(rendered);
}
} }
} }
@ -233,6 +292,7 @@ class ClientInfoManager extends InfoManager<ClientEntry> {
properties["client_onlinetime"] = formatDate(client.calculateOnlineTime()); properties["client_onlinetime"] = formatDate(client.calculateOnlineTime());
properties["sound_volume"] = client.audioController.volume * 100; properties["sound_volume"] = client.audioController.volume * 100;
properties["client_is_query"] = client.properties.client_type == ClientType.CLIENT_QUERY; properties["client_is_query"] = client.properties.client_type == ClientType.CLIENT_QUERY;
properties["client_is_web"] = client.properties.client_type_exact == ClientType.CLIENT_WEB;
properties["group_server"] = []; properties["group_server"] = [];
for(let groupId of client.assignedServerGroupIds()) { for(let groupId of client.assignedServerGroupIds()) {
@ -299,8 +359,12 @@ class ServerInfoManager extends InfoManager<ServerEntry> {
{ {
let requestUpdate = rendered.find(".btn_update"); const disabled = !server.shouldUpdateProperties();
requestUpdate.prop("disabled", !server.shouldUpdateProperties()); let requestUpdate = rendered.find(".button-update");
requestUpdate
.prop("disabled", disabled)
.toggleClass('btn-success', !disabled)
.toggleClass('btn-danger', disabled);
requestUpdate.click(() => { requestUpdate.click(() => {
server.updateProperties(); server.updateProperties();
@ -308,7 +372,10 @@ class ServerInfoManager extends InfoManager<ServerEntry> {
}); });
this.registerTimer(setTimeout(function () { this.registerTimer(setTimeout(function () {
requestUpdate.prop("disabled", false); requestUpdate
.prop("disabled", false)
.toggleClass('btn-success', true)
.toggleClass('btn-danger', false);
}, server.nextInfoRequest - Date.now())); }, server.nextInfoRequest - Date.now()));
} }

View file

@ -31,11 +31,11 @@ namespace Modals {
input_nickname.attr("placeholder", ""); input_nickname.attr("placeholder", "");
let address = input_address.val().toString(); let address = input_address.val().toString();
settings.changeGlobal("connect_address", address); settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address);
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN); let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN);
let nickname = input_nickname.val().toString(); let nickname = input_nickname.val().toString();
settings.changeGlobal("connect_name", nickname); settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname);
let flag_nickname = (nickname.length == 0 && selected_profile && selected_profile.default_username.length > 0) || nickname.length >= 3 && nickname.length <= 32; let flag_nickname = (nickname.length == 0 && selected_profile && selected_profile.default_username.length > 0) || nickname.length >= 3 && nickname.length <= 32;
input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address); input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address);
@ -48,8 +48,8 @@ namespace Modals {
} }
}; };
input_nickname.val(settings.static_global("connect_name", undefined)); input_nickname.val(settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined));
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global("connect_address", defaultHost.url)); input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
input_address input_address
.on("keyup", () => updateFields()) .on("keyup", () => updateFields())
.on('keydown', event => { .on('keydown', event => {
@ -150,7 +150,7 @@ namespace Modals {
}, },
width: '70%', width: '70%',
//closeable: false //flag_closeable: false
}); });
connectModal.open(); connectModal.open();
} }

File diff suppressed because it is too large Load diff

View file

@ -135,7 +135,21 @@ class ServerEntry {
} }
spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) { spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) {
let trigger_close = true;
spawn_context_menu(x, y, { 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, type: MenuEntryType.ENTRY,
icon: "client-virtualserver_edit", icon: "client-virtualserver_edit",
name: tr("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(); 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

@ -755,4 +755,46 @@ class ChannelTree {
get_first_channel?() : ChannelEntry { get_first_channel?() : ChannelEntry {
return this.channel_first; return this.channel_first;
} }
unsubscribe_all_channels(subscribe_specified?: boolean) {
if(!this.client.serverConnection || !this.client.serverConnection.connected())
return;
this.client.serverConnection.send_command('channelunsubscribeall').then(() => {
const channels: number[] = [];
for(const channel of this.channels) {
if(channel.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
channels.push(channel.getChannelId());
}
if(channels.length > 0) {
this.client.serverConnection.send_command('channelsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
console.warn(tr("Failed to subscribe to specific channels (%o)"), channels);
});
}
}).catch(error => {
console.warn(tr("Failed to unsubscribe to all channels! (%o)"), error);
});
}
subscribe_all_channels() {
if(!this.client.serverConnection || !this.client.serverConnection.connected())
return;
this.client.serverConnection.send_command('channelsubscribeall').then(() => {
const channels: number[] = [];
for(const channel of this.channels) {
if(channel.subscribe_mode == ChannelSubscribeMode.UNSUBSCRIBED)
channels.push(channel.getChannelId());
}
if(channels.length > 0) {
this.client.serverConnection.send_command('channelunsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
console.warn(tr("Failed to unsubscribe to specific channels (%o)"), channels);
});
}
}).catch(error => {
console.warn(tr("Failed to subscribe to all channels! (%o)"), error);
});
}
} }

View file

@ -40,11 +40,14 @@ var TabFunctions = {
let content = $.spawn("div"); let content = $.spawn("div");
content.addClass("tab-content"); content.addClass("tab-content");
content.append($.spawn("div").addClass("height-watcher"));
let silentContent = $.spawn("div"); let silentContent = $.spawn("div");
silentContent.addClass("tab-content-invisible"); silentContent.addClass("tab-content-invisible");
/* add some kind of min height */ /* add some kind of min height */
const update_height = () => { const update_height = () => {
const height_watcher = tag.find("> .tab-content .height-watcher");
const entries: JQuery = tag.find("> .tab-content-invisible x-content, > .tab-content x-content"); const entries: JQuery = tag.find("> .tab-content-invisible x-content, > .tab-content x-content");
console.error(entries); console.error(entries);
let max_height = 0; let max_height = 0;
@ -56,13 +59,7 @@ var TabFunctions = {
max_height = height; max_height = height;
}); });
console.error("HIGHT: " + max_height); height_watcher.css('min-height', max_height + "px");
entries.each((_, _e) => {
const entry = $(_e);
entry.animate({
'min-height': max_height + "px"
}, 250);
})
}; };
template.find("x-entry").each( (_, _entry) => { template.find("x-entry").each( (_, _entry) => {

View file

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

View file

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

BIN
test/TeaWeb-release.zip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
[{"file": "sound.test.wav", "key": "sound.test"}, {"file": "sound.egg.wav", "key": "sound.egg"}, {"file": "away_activated.wav", "key": "away_activated"}, {"file": "away_deactivated.wav", "key": "away_deactivated"}, {"file": "connection.connected.wav", "key": "connection.connected"}, {"file": "connection.disconnected.wav", "key": "connection.disconnected"}, {"file": "connection.disconnected.timeout.wav", "key": "connection.disconnected.timeout"}, {"file": "connection.refused.wav", "key": "connection.refused"}, {"file": "connection.banned.wav", "key": "connection.banned"}, {"file": "server.edited.wav", "key": "server.edited"}, {"file": "server.edited.self.wav", "key": "server.edited.self"}, {"file": "server.kicked.wav", "key": "server.kicked"}, {"file": "channel.kicked.wav", "key": "channel.kicked"}, {"file": "channel.moved.wav", "key": "channel.moved"}, {"file": "channel.joined.wav", "key": "channel.joined"}, {"file": "channel.created.wav", "key": "channel.created"}, {"file": "channel.edited.wav", "key": "channel.edited"}, {"file": "channel.edited.self.wav", "key": "channel.edited.self"}, {"file": "channel.deleted.wav", "key": "channel.deleted"}, {"file": "user.moved.wav", "key": "user.moved"}, {"file": "user.moved.self.wav", "key": "user.moved.self"}, {"file": "user.poked.self.wav", "key": "user.poked.self"}, {"file": "user.banned.wav", "key": "user.banned"}, {"file": "user.joined.wav", "key": "user.joined"}, {"file": "user.joined.moved.wav", "key": "user.joined.moved"}, {"file": "user.joined.kicked.wav", "key": "user.joined.kicked"}, {"file": "user.joined.connect.wav", "key": "user.joined.connect"}, {"file": "user.left.wav", "key": "user.left"}, {"file": "user.left.kicked.channel.wav", "key": "user.left.kicked.channel"}, {"file": "user.left.kicked.server.wav", "key": "user.left.kicked.server"}, {"file": "user.left.moved.wav", "key": "user.left.moved"}, {"file": "user.left.disconnect.wav", "key": "user.left.disconnect"}, {"file": "user.left.banned.wav", "key": "user.left.banned"}, {"file": "error.insufficient_permissions.wav", "key": "error.insufficient_permissions"}, {"file": "group.server.assigned.wav", "key": "group.server.assigned"}, {"file": "group.server.revoked.wav", "key": "group.server.revoked"}, {"file": "group.channel.changed.wav", "key": "group.channel.changed"}, {"file": "group.server.assigned.self.wav", "key": "group.server.assigned.self"}, {"file": "group.server.revoked.self.wav", "key": "group.server.revoked.self"}, {"file": "group.channel.changed.self.wav", "key": "group.channel.changed.self"}]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

198
test/css/loader/loader.css Normal file
View file

@ -0,0 +1,198 @@
.loader {
margin: 0;
display: block;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 900;
text-align: center; }
.loader .half {
position: fixed;
background: #222222;
top: 0;
bottom: 0;
width: 50%;
height: 100%; }
.loader .half.right {
right: 0; }
.loader .half.left {
left: 0; }
.bookshelf_wrapper {
position: relative;
top: 40%;
left: 50%;
transform: translate(-50%, -50%); }
.books_list {
margin: 0 auto;
width: 300px;
padding: 0; }
.book_item {
position: absolute;
top: -120px;
box-sizing: border-box;
list-style: none;
width: 40px;
height: 120px;
opacity: 0;
background-color: #1e6cc7;
border: 5px solid white;
transform-origin: bottom left;
transform: translateX(300px);
animation: travel 2500ms linear infinite; }
.book_item.first {
top: -140px;
height: 140px; }
.book_item.first:before, .book_item.first:after {
content: '';
position: absolute;
top: 10px;
left: 0;
width: 100%;
height: 5px;
background-color: white; }
.book_item.first:after {
top: initial;
bottom: 10px; }
.book_item.second:before, .book_item.second:after, .book_item.fifth:before, .book_item.fifth:after {
box-sizing: border-box;
content: '';
position: absolute;
top: 10px;
left: 0;
width: 100%;
height: 17.5px;
border-top: 5px solid white;
border-bottom: 5px solid white; }
.book_item.second:after, .book_item.fifth:after {
top: initial;
bottom: 10px; }
.book_item.third:before, .book_item.third:after {
box-sizing: border-box;
content: '';
position: absolute;
top: 10px;
left: 9px;
width: 12px;
height: 12px;
border-radius: 50%;
border: 5px solid white; }
.book_item.third:after {
top: initial;
bottom: 10px; }
.book_item.fourth {
top: -130px;
height: 130px; }
.book_item.fourth:before {
box-sizing: border-box;
content: '';
position: absolute;
top: 46px;
left: 0;
width: 100%;
height: 17.5px;
border-top: 5px solid white;
border-bottom: 5px solid white; }
.book_item.fifth {
top: -100px;
height: 100px; }
.book_item.sixth {
top: -140px;
height: 140px; }
.book_item.sixth:before {
box-sizing: border-box;
content: '';
position: absolute;
bottom: 31px;
left: 0px;
width: 100%;
height: 5px;
background-color: white; }
.book_item.sixth:after {
box-sizing: border-box;
content: '';
position: absolute;
bottom: 10px;
left: 9px;
width: 12px;
height: 12px;
border-radius: 50%;
border: 5px solid white; }
.book_item:nth-child(2) {
animation-delay: 416.66667ms; }
.book_item:nth-child(3) {
animation-delay: 833.33333ms; }
.book_item:nth-child(4) {
animation-delay: 1250ms; }
.book_item:nth-child(5) {
animation-delay: 1666.66667ms; }
.book_item:nth-child(6) {
animation-delay: 2083.33333ms; }
.shelf {
width: 300px;
height: 5px;
margin: 0 auto;
background-color: white;
position: relative; }
.shelf:before, .shelf:after {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: #222222;
background-image: radial-gradient(rgba(255, 255, 255, 0.5) 30%, transparent 0);
background-size: 10px 10px;
background-position: 0 -2.5px;
top: 200%;
left: 5%;
animation: move 250ms linear infinite; }
.shelf:after {
top: 400%;
left: 7.5%; }
@keyframes move {
from {
background-position-x: 0; }
to {
background-position-x: 10px; } }
@keyframes travel {
0% {
opacity: 0;
transform: translateX(300px) rotateZ(0deg) scaleY(1); }
6.5% {
transform: translateX(279.5px) rotateZ(0deg) scaleY(1.1); }
8.8% {
transform: translateX(273.6px) rotateZ(0deg) scaleY(1); }
10% {
opacity: 1;
transform: translateX(270px) rotateZ(0deg); }
17.6% {
transform: translateX(247.2px) rotateZ(-30deg); }
45% {
transform: translateX(165px) rotateZ(-30deg); }
49.5% {
transform: translateX(151.5px) rotateZ(-45deg); }
61.5% {
transform: translateX(115.5px) rotateZ(-45deg); }
67% {
transform: translateX(99px) rotateZ(-60deg); }
76% {
transform: translateX(72px) rotateZ(-60deg); }
83.5% {
opacity: 1;
transform: translateX(49.5px) rotateZ(-90deg); }
90% {
opacity: 0; }
100% {
opacity: 0;
transform: translateX(0px) rotateZ(-90deg); } }
/*# sourceMappingURL=loader.css.map */

1
test/css/static/base.css Normal file

File diff suppressed because one or more lines are too long

18
test/css/static/main.css Normal file
View file

@ -0,0 +1,18 @@
html, body {
height: 100%;
overflow-y: hidden; }
.app-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center; }
.app-container .app {
width: 100%;
height: calc(100% - 50px);
margin: 0;
display: flex;
flex-direction: column;
resize: both; }
/*# sourceMappingURL=main.css.map */

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,32 @@
{
"info": {
"contributors": [
{
"name": "Markus Hadenfeldt",
"email": "i18n.client@teaspeak.de"
}
],
"name": "German translations"
},
"translations": [
{
"key": {
"message": "Show permission description",
"line": 374,
"character": 30,
"filename": "/home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/shared/js/ui/modal/ModalPermissionEdit.ts"
},
"translated": "Berechtigungsbeschreibung anzeigen",
"flags": [
"google-translate"
]
},
{
"key": {
"message": "Create a new connection"
},
"translated": "Verbinden",
"flags": [ ]
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

22
test/i18n/info.json Normal file
View file

@ -0,0 +1,22 @@
{
"translations": [
{
"key": "de_gt",
"path": "de_google_translate.translation"
},
{
"key": "pl_gt",
"path": "pl_google_translate.translation"
},
{
"key": "tr_gt",
"path": "tr_google_translate.translation"
},
{
"key": "fr_gt",
"path": "fr_google_translate.translation"
}
],
"name": "Default TeaSpeak repository",
"contact": "i18n@teaspeak.de"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
{
"info": {
"contributors": [
/* add yourself if you have done anything :) */
{
"name": "Markus Hadenfeldt", /* this field is required */
"email": "i18n.client@teaspeak.de" /* this field is required */
}
],
"name": "A template translation file" /* this field is required */
},
"translations": [ /* Array with all translation objects */
{ /* translation object */
"key": { /* the key */
"message": "Show permission description", /* necessary to identify the message */
"line": 374, /* optional, only for specify the translation for a specific case (Not supported yet!) */
"character": 30, /* optional, only for specify the translation for a specific case (Not supported yet!) */
"filename": "/home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/shared/js/ui/modal/ModalPermissionEdit.ts" /* optional, only for specify the translation for a specific case (Not supported yet!) */
},
"translated": "Berechtigungsbeschreibung anzeigen", /* The actual translation */
"flags": [ /* some flags for this translation */
"google-translate", /* this translation has been made with google translator */
"verified" /* this translation has been verified by a native speaker */
]
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 334 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Some files were not shown because too many files have changed in this diff Show more