Some updates

canary
WolverinDEV 2019-08-30 23:06:39 +02:00
parent 6df7b216ec
commit 3b3430db5e
74 changed files with 4964 additions and 2659 deletions

View File

@ -9,6 +9,24 @@
"path" => "./",
"local-path" => "./shared/html/"
],
[ /* javascript loader */
"type" => "js",
"search-pattern" => "/.*\.js$/",
"build-target" => "dev",
"path" => "loader/",
"local-path" => "./shared/loader/"
],
[ /* javascript loader for releases */
"type" => "js",
"search-pattern" => "/.*loader_[\S]+.min.js$/",
"build-target" => "rel",
"path" => "loader/",
"local-path" => "./shared/generated/"
],
[ /* shared javascript files (WebRTC adapter) */
"type" => "js",
"search-pattern" => "/.*\.js$/",
@ -17,6 +35,7 @@
"path" => "adapter/",
"local-path" => "./shared/adapter/"
],
[ /* shared javascript files (development mode only) */
"type" => "js",
"search-pattern" => "/.*\.js$/",
@ -36,6 +55,7 @@
"local-path" => "./shared/js/",
"req-parm" => ["--mappings"]
],
[ /* shared generated worker codec */
"type" => "js",
"search-pattern" => "/(WorkerPOW.js)$/",
@ -231,16 +251,6 @@
"path" => "js/",
"local-path" => "./web/generated/"
],
[ /* Add the shared generated files. Exclude the shared file because we're including it already */
"web-only" => true,
"type" => "js",
"search-pattern" => "/.*\.js$/",
"search-exclude" => "/shared\.js(.map)?$/",
"build-target" => "rel",
"path" => "js/",
"local-path" => "./shared/generated/"
],
[ /* web css files */
"web-only" => true,
"type" => "css",

View File

@ -14,7 +14,7 @@
"ttsc": "ttsc",
"csso": "csso",
"rebuild-structure-web-dev": "php files.php generate web dev",
"minify-web-rel-file": "minify web/generated/client.js --outFile web/generated/client.min.js --evaluate --removeDebugger --undefinedToVoid --mangle.keepClassName --deadcode.keepFnArgs"
"minify-web-rel-file": "terser --compress --mangle --ecma 6 --keep_classnames --keep_fnames --output"
},
"author": "TeaSpeak (WolverinDEV)",
"license": "ISC",
@ -25,15 +25,15 @@
"@types/node": "^12.7.2",
"@types/sha256": "^0.2.0",
"@types/websocket": "0.0.40",
"babel-minify": "^0.5.1",
"clean-css": "^4.2.1",
"csso-cli": "^2.0.2",
"gulp": "^4.0.2",
"sass": "^1.22.10",
"sha256": "^0.2.0",
"terser": "^4.2.1",
"ttypescript": "^1.5.7",
"typescript": "^3.5.3",
"wat2wasm": "^1.0.2",
"clean-css": "^4.2.1"
"wat2wasm": "^1.0.2"
},
"repository": {
"type": "git",

View File

@ -61,4 +61,7 @@ fi
#Last but not least the client imports
generate_link shared/declarations/exports.d.ts web/declarations/imports_shared.d.ts
generate_link shared/declarations/exports_loader_app.d.ts web/declarations/imports_shared_loader.d.ts
generate_link shared/declarations/exports.d.ts client/declarations/imports_shared.d.ts
generate_link shared/declarations/exports_loader_app.d.ts client/declarations/imports_shared_loader.d.ts

61
shared/css/generate_packed.sh Executable file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
cd $(dirname $0)
#find css/static/ -name '*.css' -exec cat {} \; | npm run csso -- --output `pwd`/generated/static/base.css
#File order
files=(
"css/static/main-layout.css"
"css/static/helptag.css"
"css/static/scroll.css"
"css/static/channel-tree.css"
"css/static/ts/tab.css"
"css/static/ts/chat.css"
"css/static/ts/icons.css"
"css/static/ts/icons_em.css"
"css/static/ts/country.css"
"css/static/general.css"
"css/static/modal.css"
"css/static/modals.css"
"css/static/modal-about.css"
"css/static/modal-avatar.css"
"css/static/modal-icons.css"
"css/static/modal-bookmarks.css"
"css/static/modal-connect.css"
"css/static/modal-channel.css"
"css/static/modal-query.css"
"css/static/modal-invite.css"
"css/static/modal-playlist.css"
"css/static/modal-banlist.css"
"css/static/modal-bancreate.css"
"css/static/modal-clientinfo.css"
"css/static/modal-serverinfo.css"
"css/static/modal-identity.css"
"css/static/modal-settings.css"
"css/static/modal-poke.css"
"css/static/modal-server.css"
"css/static/modal-keyselect.css"
"css/static/modal-permissions.css"
"css/static/modal-group-assignment.css"
"css/static/music/info_plate.css"
"css/static/frame/SelectInfo.css"
"css/static/control_bar.css"
"css/static/context_menu.css"
"css/static/frame-chat.css"
"css/static/connection_handlers.css"
"css/static/server-log.css"
"css/static/htmltags.css"
"css/static/hostbanner.css"
"css/static/menu-bar.css"
)
target_file=`pwd`/../generated/static/base.css
echo "/* Auto generated merged CSS file */" > ${target_file}
for file in "${files[@]}"; do
if [[ ${file} =~ css/* ]]; then
file="./${file:4}"
fi
cat ${file} >> ${target_file}
done
cat ${target_file} | npm run csso -- --output `pwd`/../generated/static/base.css

View File

@ -36,6 +36,7 @@ $background: #222222;
.loader .half.right {
right: 0;
}
.loader .half.left {
left: 0;
}
@ -298,3 +299,64 @@ $background: #222222;
}
}
/* Automated loader timeout */
$loader_timeout: 2.5s;
.loader:not(.started) {
&, & > .half, & > .bookshelf_wrapper {
-moz-animation: _loader_hide 0s ease-in $loader_timeout forwards;
-webkit-animation: _loader_hide 0s ease-in $loader_timeout forwards;
-o-animation: _loader_hide 0s ease-in $loader_timeout forwards;
animation: _loader_hide 0s ease-in $loader_timeout forwards;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
}
.loader:not(.started) + #critical-load {
display: block !important;
opacity: 0;
-moz-animation: _loader_show 0s ease-in $loader_timeout forwards;
-webkit-animation: _loader_show 0s ease-in $loader_timeout forwards;
-o-animation: _loader_show 0s ease-in $loader_timeout forwards;
animation: _loader_show 0s ease-in $loader_timeout forwards;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
.error::before {
content: 'Failed to startup app loader!';
}
.detail::before {
content: 'Lookup the console for more details';
}
}
@keyframes _loader_hide {
to {
width: 0;
height: 0;
overflow: hidden;
}
}
@-webkit-keyframes _loader_hide {
to {
width: 0;
height: 0;
visibility: hidden;
}
}
@keyframes _loader_show {
to {
opacity: 1;
}
}
@-webkit-keyframes _loader_show {
to {
opacity: 1;
}
}

View File

@ -51,11 +51,21 @@ $client_info_avatar_size: 10em;
&.right {
text-align: right;
&.mode-client_info {
max-width: calc(50% - #{$client_info_avatar_size / 2});
margin-left: calc(#{$client_info_avatar_size / 2});
}
}
&.left {
text-align: left;
padding-right: 10px;
&.mode-client_info {
max-width: calc(50% - #{$client_info_avatar_size / 2});
margin-right: calc(#{$client_info_avatar_size} / 2);
}
}
.title, .value, .small-value {
@ -167,11 +177,6 @@ $client_info_avatar_size: 10em;
}
@include transition(background-color $button_hover_animation_time ease-in-out);
}
&.mode-client_info {
min-width: 50%;
margin-right: calc(#{$client_info_avatar_size} / 2);
}
}
}
}
@ -507,6 +512,21 @@ $client_info_avatar_size: 10em;
flex-direction: column;
min-height: 2em;
.container-typing {
font-size: .85em;
padding-left: .6em;
line-height: 1;
color: hsla(0, 0%, 30%, 1);
opacity: 1;
&.hidden {
opacity: 0;
}
@include transition($button_hover_animation_time ease-in-out);
}
}
}
@ -1201,8 +1221,6 @@ $client_info_avatar_size: 10em;
}
}
.button-close {
font-size: 4em;
@ -1243,6 +1261,32 @@ $client_info_avatar_size: 10em;
transform: rotate(-45deg);
}
}
.button-more {
flex-grow: 0;
flex-shrink: 0;
height: 1.5em;
font-size: 1.25em;
text-align: center;
color: #999999;
cursor: pointer;
margin-left: -5px;
margin-right: -5px;
background-color: #2d2d2d;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
&:hover {
background-color: #393939;
}
@include transition($button_hover_animation_time ease-in-out);
}
}
.container-private-conversations, .container-channel-chat {

View File

@ -1,6 +1,6 @@
@import "./mixin.scss";
#hostbanner {
.hostbanner {
.container-hostbanner {
position: relative;

View File

@ -0,0 +1,618 @@
@import "mixin";
@import "properties";
.modal-body.modal-client-info {
padding: 0!important;
$avatar_size: 12em;
.head {
flex-shrink: 0;
flex-grow: 0;
z-index: 1;
height: 7em;
background-color: #212125;
.status-row {
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
.status-entry {
font-size: 1.5em;
margin: .25em;
width: 1em;
height: 1em;
}
}
.container-away-message {
$offset_left: (.25em) * 1.5 /* 1.5 is the font size of the icons */;
position: relative;
margin-left: $offset_left;
margin-top: .25em;
background-color: #1c1c1c;
border: 1px solid #161515;
border-radius: 3px;
max-width: calc(50% - #{$avatar_size / 2 + $offset_left + 1em}); /* do actual 1em space to the avatar */
max-height: 4em; /* else it will overflow the header */
display: flex;
flex-direction: column;
justify-content: start;
width: max-content;
padding: .15em;
overflow: hidden;
//A verry long away message, because I want to tell a story. There was a child....
a {
font-size: .85em;
}
&:hover {
max-height: 200em;
}
@include transition(.5s ease-in-out);
}
}
.body {
flex-shrink: 1;
flex-grow: 0;
//TODO: Min height here!
display: flex;
flex-direction: column;
justify-content: stretch;
background-color: #2f2f35;
.container-avatar {
z-index: 2; /* overlay the header */
flex-grow: 0;
flex-shrink: 0;
position: relative;
display: inline-block;
margin: calc(#{$avatar_size} / -2) 0.75em 0.5em 0.5em;
align-self: center;
.avatar {
height: $avatar_size;
width: $avatar_size;
border-radius: 50%;
overflow: hidden;
}
}
.container-name {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: center;
.htmltag-client {
text-align: center;
font-size: 1.5em;
color: #cccccc;
font-weight: bold;
}
}
.container-description {
flex-grow: 0;
flex-shrink: 0;
padding-right: calc(10em / 2);
padding-left: calc(10em / 2);
text-align: center;
display: flex;
flex-direction: column;
justify-content: stretch;
.client-description {
color: #6f6f6f;
max-width: 100%;
flex-shrink: 1;
flex-grow: 1;
overflow-wrap: break-word;
}
}
.container-categories {
margin-top: 1em;
display: flex;
flex-direction: column;
justify-content: stretch;
min-height: 14em;
.categories {
height: 2.5em;
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
padding-left: 2.5em;
padding-right: 2.5em;
border-bottom: 1px solid #1d1d1d;
.entry {
padding: .5em;
text-align: center;
flex-grow: 1;
flex-shrink: 1;
cursor: pointer;
&:hover {
color: #b6c4d6;
}
&.selected {
border-bottom: 3px solid #245184;
margin-bottom: -2px;
color: #245184;
}
@include transition(color $button_hover_animation_time, border-bottom-color $button_hover_animation_time);
}
}
.bodies {
position: relative;
flex-shrink: 1;
flex-grow: 1;
display: flex;
justify-content: stretch;
padding-left: .5em;
padding-right: .5em;
min-height: 10em;
height: 21em; /* body size 20 + .5 padding */
.container-tooltip {
flex-shrink: 0;
flex-grow: 0;
font-size: .8em; /* shrink the tip a bit */
position: relative;
width: 1.6em;
margin-left: .5em;
display: flex;
flex-direction: column;
justify-content: center;
img {
height: 1em;
width: 1em;
align-self: center;
font-size: 1.2em;
}
.tooltip {
display: none;
}
}
.body {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: .5em;
display: flex;
justify-content: stretch;
overflow: auto; /* else the tooltip will trigger the scrollbar */
@include chat-scrollbar-vertical();
&.hidden {
display: none;
}
&.container-basic {
flex-direction: row;
.spacer {
flex-grow: 0;
flex-shrink: 0;
width: 1em;
}
.left, .right {
height: 20em;
width: calc(50% - .5em); /* the spacer in the middle thats why -.5 em */
flex-grow: 1;
flex-shrink: 1;
border-radius: .2em;
border: 1px solid #1f2122;
background-color: #28292b;
padding: .5em;
.property {
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
.title, .value {
display: flex;
flex-direction: row;
justify-content: stretch;
white-space: nowrap;
overflow: hidden;
> * {
flex-shrink: 0;
flex-grow: 0;
align-self: center;
}
a {
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
}
}
.title {
color: #254d7b;
text-transform: uppercase;
}
.value {
color: #bdbdbd;
a, a:visited {
color: #bdbdbd!important;
}
.button {
width: 1.6em;
height: 1.6em;
display: flex;
flex-direction: column;
justify-content: space-around;
cursor: pointer;
opacity: .5;
> div {
align-self: center;
}
&:hover {
opacity: 1;
}
@include transition($button_hover_animation_time ease-in-out);
}
.country {
margin-right: .25em;
}
}
&:not(:first-of-type) {
margin-top: .5em;
}
&.property-unique-id, &.property-ip {
.value {
justify-content: space-between;
}
}
&.property-version {
.a-on {
flex-shrink: 0;
flex-grow: 0;
margin-left: .25em;
margin-right: .25em;
}
}
}
}
}
&.container-packets {
flex-direction: row;
.spacer {
flex-grow: 0;
flex-shrink: 0;
width: 1em;
}
.left, .right {
height: 20em;
width: calc(50% - .5em); /* the spacer in the middle thats why -.5 em */
flex-grow: 1;
flex-shrink: 1;
border-radius: .2em;
border: 1px solid #1f2122;
background-color: #28292b;
padding: .5em;
.statistic {
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
.title, .upstream, .downstream {
display: flex;
flex-direction: row;
justify-content: stretch;
white-space: nowrap;
overflow: hidden;
> * {
flex-shrink: 0;
flex-grow: 0;
align-self: center;
}
a {
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
}
}
.title {
color: #254d7b;
text-transform: uppercase;
}
.upstream, .downstream {
padding-top: .25em;
display: flex;
flex-direction: row;
justify-content: space-between;
> a {
align-self: center;
}
}
.upstream {
color: #fd3913;
}
.downstream {
color: #0e8afd;
}
&:not(:first-of-type) {
margin-top: .5em;
}
}
}
}
&.container-groups {
flex-direction: row;
.spacer {
flex-grow: 0;
flex-shrink: 0;
width: 1em;
}
.left, .right {
height: 20em;
width: calc(50% - .5em); /* the spacer in the middle thats why -.5 em */
flex-grow: 1;
flex-shrink: 1;
.title {
align-self: center;
color: #254d7b;
text-transform: uppercase;
}
.container {
margin-top: .5em;
}
}
.left {
display: flex;
flex-direction: column;
justify-content: stretch;
.container {
border-radius: .2em .2em 0 0;
border: 1px solid #1f2122;
border-bottom: 0;
padding: 0!important;
background-color: #28292b;
flex-grow: 1;
flex-shrink: 1;
overflow-y: auto;
min-height: 4em;
position: relative;
@include chat-scrollbar-vertical();
.entries {
flex-grow: 1;
flex-shrink: 1;
min-height: 4em;
.entry {
display: flex;
flex-direction: row;
justify-content: stretch;
height: 1.6em;
padding-left: .5em;
padding-right: .5em;
&:hover {
background-color: #232425;
}
> * {
align-self: center;
}
.icon-container {
margin-right: .25em;
}
.name {
flex-grow: 1;
flex-shrink: 1;
min-width: 1em;
line-height: normal;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.button-delete {
height: 1.3em;
width: 1.3em;
cursor: pointer;
border-radius: .2em;
&:hover {
background-color: #2c2d2e;
}
display: flex;
flex-direction: row;
justify-content: space-around;
> div {
align-self: center;
}
@include transition($button_hover_animation_time ease-in-out);
}
}
}
.container-default-groups {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
a {
align-self: center;
font-size: 1.25em;
color: hsla(0, 0%, 30%, 1);
}
}
}
.buttons {
flex-grow: 0;
flex-shrink: 0;
border-radius: 0 0 .2em .2em;
border: 1px solid #1f2122;
background-color: #28292b;
padding: .5em;
display: flex;
flex-direction: row;
justify-content: space-around;
.button {
align-self: center;
}
}
}
.right {
.container {
padding: 0!important;
select {
font-size: .8em;
width: 100%;
}
}
}
}
}
}
}
}
}

View File

@ -259,7 +259,7 @@
align-self: center;
.country, .icon-container {
align-self: center;
margin-right: 0.1em;
margin-right: 0.25em;
}

View File

@ -0,0 +1,188 @@
@import "mixin";
.modal-body.modal-identity-improve {
padding: 0!important;
display: flex;
flex-direction: column;
justify-content: flex-start;
.container-tooltip {
flex-grow: 0!important;
flex-shrink: 0!important;
min-width: unset!important;
position: relative;
width: .8em;
margin-right: .5em;
font-size: .9em;
display: inline-flex;
flex-direction: column;
justify-content: center;
vertical-align: middle;
margin-bottom: .1em;
img {
height: 1em;
width: 1em;
align-self: center;
font-size: 1.2em;
}
.tooltip {
display: none;
}
}
.options, .status {
flex-grow: 0;
flex-shrink: 0;
padding: 1em;
.title {
color: #387fb5;
}
.row {
display: flex;
flex-direction: row;
justify-content: stretch;
div {
flex-grow: 1;
flex-shrink: 1;
min-width: 4em;
&:not(:first-of-type) {
margin-left: 1em;
}
}
}
}
.status {
border-top: 3px solid #20262d;
}
.buttons {
flex-grow: 0;
flex-shrink: 0;
margin: 0 1em 1em;
display: flex;
flex-direction: row;
justify-content: flex-end;
button {
min-width: 8em;
&:not(:first-of-type) {
margin-left: 1em;
}
}
}
}
.modal-body.modal-identity-import {
padding: 0!important;
display: flex;
flex-direction: column;
justify-content: flex-start;
@include user-select(none);
.container-status {
display: flex;
flex-direction: row;
justify-content: center;
margin: 1em 1em -1em;
height: 1.6em;
overflow: hidden;
color: #389738;
&.error {
color: #973838;
}
&.loading {
color: #96903a;
}
&.hidden {
height: 0;
}
a {
align-self: center;
}
@include transition(.2s ease-in-out);
}
.container-text, .container-file {
display: flex;
flex-direction: row;
justify-content: stretch;
> label {
flex-shrink: 0;
flex-grow: 0;
width: 10em;
display: flex;
flex-direction: row;
justify-content: flex-start;
padding-left: .25em;
align-self: center;
margin-right: 1em;
.radio-button {
align-self: center;
margin-right: .5em;
}
}
> * {
align-self: center;
}
.form-group {
margin-bottom: 1.75em;
width: 100%;
}
button {
min-width: 8em;
margin-right: 1em;
}
}
.footer {
flex-shrink: 0;
flex-grow: 0;
padding: 1em;
display: flex;
flex-direction: row;
justify-content: flex-end;
button {
min-width: 8em;
}
}
.file-selector {
display: none;
}
}

View File

@ -0,0 +1,229 @@
@import "mixin";
.modal-body.modal-server-info {
padding: 0!important;
width: 55em;
display: flex;
flex-direction: column;
justify-content: flex-start;
background-color: #2f2f35;
.container-tooltip {
flex-shrink: 0;
flex-grow: 0;
position: relative;
width: 1.6em;
margin-left: .5em;
font-size: .9em;
display: flex;
flex-direction: column;
justify-content: center;
img {
height: 1em;
width: 1em;
align-self: center;
font-size: 1.2em;
}
.tooltip {
display: none;
}
}
.hostbanner {
flex-grow: 0;
flex-shrink: 0;
max-height: 9em;
//width: 30em; /* set a default width where we have to grow/shrink */
display: flex;
flex-direction: column;
justify-content: stretch;
.container-hostbanner {
border: none;
border-radius: 0;
//background-color: #261f30;
background-color: hsla(265, 10%, 15%, 1);
}
&.hidden {
display: none;
}
}
.group {
flex-grow: 0;
flex-shrink: 0;
margin: 1em;
padding: .5em;
border-radius: .2em;
border: 1px solid #1f2122;
background-color: #28292b;
display: flex;
flex-direction: row;
justify-content: stretch;
height: 10em;
max-height: 10em;
.container-image {
flex-grow: 0;
flex-shrink: 0;
max-width: 15em;
max-height: 9em; /* minus one padding */
display: flex;
flex-direction: column;
justify-content: center;
img {
object-fit: contain;
max-height: 100%;
max-width: 100%;
}
margin-right: 2em;
@include transition(.25s ease-in-out);
}
.container-properties {
flex-shrink: 1;
flex-grow: 1;
min-width: 25em;
display: flex;
flex-direction: column;
justify-content: flex-start;
height: inherit;
.row {
flex-grow: 0;
flex-shrink: 0;
height: 1.8em;
display: flex;
flex-direction: row;
justify-content: flex-start;
.key {
flex-shrink: 0;
flex-grow: 0;
color: #557edc;
text-transform: uppercase;
align-self: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 15em;
}
.value {
color: #d6d6d7;
align-self: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.country {
display: inline-block;
margin-right: .25em;
}
&.server-version {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
}
}
.container-network {
display: flex;
flex-direction: row;
justify-content: center;
.container-button {
margin-right: 1em;
flex-shrink: 1;
min-width: 5em;
display: flex;
flex-direction: column;
justify-content: flex-end;
button {
height: 2.5em;
width: 12em;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.right {
flex-grow: 1;
}
}
}
&.reverse {
flex-direction: row-reverse;
text-align: right;
.container-image {
margin-right: 0;
margin-left: 2em;
}
.container-properties {
.row {
flex-direction: row-reverse;
}
}
}
}
.container-buttons {
margin: 1em;
display: flex;
flex-direction: row;
justify-content: flex-end;
button {
min-width: 8em;
}
}
}
@media all and (max-width: 50em) {
.modal-body.modal-server-info {
.container-image {
margin: 0!important;
max-width: 0!important;
}
}
}

View File

@ -119,6 +119,10 @@
flex-shrink: 1;
color: #9d9d9e;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
h5 {
@ -461,7 +465,7 @@
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12);
&:hover {
background-color: #151515;
background-color: #0a0a0a;
}
&:disabled {
@ -473,21 +477,36 @@
}
}
&.btn-success {
&.btn-success, &.btn-green {
border-bottom-width: 2px;
border-bottom-color: #389738;
}
&.btn-info {
&.btn-info, &.btn-blue {
border-bottom-width: 2px;
border-bottom-color: #386896;
}
&.btn-warning, &.btn-danger {
&.btn-warning, &.btn-danger, &.btn-red {
border-bottom-width: 2px;
border-bottom-color: #973838;
}
&.btn-purple {
border-bottom-width: 2px;
border-bottom-color: #5f3586;
}
&.btn-brown {
border-bottom-width: 2px;
border-bottom-color: #965238;
}
&.btn-yellow {
border-bottom-width: 2px;
border-bottom-color: #96903a;
}
@include transition(background-color $button_hover_animation_time ease-in-out);
}
@ -595,8 +614,8 @@
}
}
/* general ratio button look */
.ratio-button {
/* general radio button look */
.ratio-button, .radio-button {
$button_size: 1.2em;
$mark_size: .6em;
@ -647,14 +666,18 @@
box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
}
label:hover > .ratio-button, .ratio-button:hover {
&.ratio-button, > .ratio-button {
label:hover > .ratio-button, .ratio-button:hover,
label:hover > .radio-button, .radio-button:hover{
&.ratio-button, > .ratio-button,
&.radio-button, > .radio-button{
background-color: #2c2b2b;
}
}
label.disabled > .ratio-button, .ratio-button.disabled, .ratio-button:disabled {
&.ratio-button, > .ratio-button {
label.disabled > .ratio-button, .ratio-button.disabled, .ratio-button:disabled,
label.disabled > .radio-button, .radio-button.disabled, .radio-button:disabled {
&.ratio-button, > .ratio-button,
&.radio-button, > .radio-button {
pointer-events: none!important;
background-color: #1a1919!important;
}

View File

@ -5,7 +5,7 @@ cd "$BASEDIR"
source ../scripts/resolve_commands.sh
#Generate the loader definitions first
LOADER_FILE="declarations/exports_loader.d.ts"
LOADER_FILE="declarations/exports_loader_app.d.ts"
if [[ -e ${LOADER_FILE} ]]; then
rm ${LOADER_FILE}
if [[ $? -ne 0 ]]; then
@ -13,10 +13,9 @@ if [[ -e ${LOADER_FILE} ]]; then
fi
fi
npm run dtsgen -- --config $(pwd)/tsconfig/dtsconfig_loader.json -v
npm run dtsgen -- --config $(pwd)/tsconfig/dtsconfig_loader_app.json -v
if [[ ! -e ${LOADER_FILE} ]]; then
echo "Failed to generate definitions"
echo "$result"
exit 1
fi
@ -26,12 +25,19 @@ if [[ $? -ne 0 ]]; then
exit 1
fi
execute_ttsc -p tsconfig/tsconfig_packed_loader.json
execute_ttsc -p tsconfig/tsconfig_packed_loader_app.json
if [[ $? -ne 0 ]]; then
echo "Failed to generate packed loader file!"
exit 1
fi
npm run minify-web-rel-file `pwd`/generated/loader_app.min.js `pwd`/generated/loader_app.js
if [[ $? -ne 0 ]]; then
echo "Failed to minimize packed loader file!"
exit 1
fi
execute_ttsc -p tsconfig/tsconfig_packed.json
if [[ $? -ne 0 ]]; then
echo "Failed to generate packed file!"
@ -49,7 +55,7 @@ if [[ ! -d generated/static/ ]]; then
fi
# Create packed CSS file
find css/static/ -name '*.css' -exec cat {} \; | npm run csso -- --output `pwd`/generated/static/base.css
./css/generate_packed.sh
echo "Packed file generated!"
exit 0

View File

@ -93,7 +93,9 @@
<link rel="stylesheet" href="css/loader/loader.css">
</div>
<div id="scripts">
<script type="application/javascript" src="js/load.js" defer></script>
<script type="application/javascript" src="loader/loader_app.min.js" defer></script>
<script type="application/javascript" src="loader/loader_app.js" defer></script>
<script type="application/javascript" src="loader/loader.js" defer></script>
</div>
</head>
<body>
@ -113,7 +115,7 @@
</script>
<!-- Loading screen -->
<div class="loader">
<div class="loader" id="loader-overlay">
<div class="half right"></div>
<div class="half left"></div>
<div class="bookshelf_wrapper">
@ -133,12 +135,12 @@
<div class="fulloverlay" id="critical-load">
<div class="container">
<img src="img/loading_error_right.svg" height="192px">
<h1 class="error" style="color: red">Ooops, we encountered some trouble while loading important files!</h1>
<h1 class="error" style="color: red"></h1>
<h3 class="detail"></h3>
</div>
</div>
<?php if($localhost) { ?>
<?php if($localhost && false) { ?>
<div id="spoiler-style" style="z-index: 1000000; position: absolute; display: block; background: white; right: 5px; left: 5px; top: 34px;">
<!-- <img src="https://www.chromatic-solutions.de/teaspeak/window/connect_opened.png"> -->
<!-- <img src="http://puu.sh/DZDgO/9149c0a1aa.png"> -->
@ -155,8 +157,29 @@
<!-- <img src="http://puu.sh/E4lHJ/1a4afcdf0b.png"> -->
<!-- <img src="http://puu.sh/E4HKK/5ee74d4cc7.png"> -->
<!-- <img src="http://puu.sh/E6LN1/8518c10898.png"> -->
<!--
http://puu.sh/E8IoF/242ed5ca3a.png
http://puu.sh/E8Ip9/9632d33591.png
http://puu.sh/E8Ips/6c314253e5.png
http://puu.sh/E8IpG/015a38b184.png
http://puu.sh/E8IpY/5be454a15e.png
<img src="http://puu.sh/E6NXv/eb2f19c7c3.png">
Identity imporve: http://puu.sh/E9jTp/380a734677.png
Identity select: http://puu.sh/E9jYi/3003c58a2f.png
Server Info Bandwidth: http://puu.sh/E9jTe/b41f6386de.png
Server Info: http://puu.sh/E9jT6/302912ae34.png
Bookmarks: http://puu.sh/Eb5w4/8d38fe5b8f.png
serveredit_1.png https://www.hypixel-koo.cf/tsapoijdsadpoijsadsapj.png
serveredit_2.png https://www.hypixel-koo.cf/tsandljsandljsamndoj3oiwejlkjmnlksandljsadmnlmsadnlsa.png
serveredit_3.png https://www.hypixel-koo.cf/toiuhsadouhgdsapoiugdsapouhdsapouhdsaouhwouhwwouhwwoiuhwoihwwoihwoijhwwoknw.png
-->
<!-- <img src="http://puu.sh/E6NXv/eb2f19c7c3.png"> -->
<!-- <img src="http://puu.sh/E9jT6/302912ae34.png"> -->
<img src="http://puu.sh/E9jYi/3003c58a2f.png">
</div>
<button class="toggle-spoiler-style" style="height: 30px; width: 100px; z-index: 100000000; position: absolute; bottom: 2px;">toggle style</button>
<script>

View File

@ -241,7 +241,7 @@
</div>
</div>
<div class="lane">
<div class="block left mode-based mode-channel_chat">
<div class="block left mode-based mode-channel_chat channel">
<div class="title"></div>
<div class="value value-text-channel"></div>
<div class="small-value value-text-limit"></div>
@ -253,7 +253,7 @@
<div class="spacer-client-info"></div>
</div>
<div class="block right">
<div class="block right mode-based mode-private_chat mode-channel_chat">
<div class="title">{{tr "Private chats" /}}
<div class="container-indicator">
<div class="chat-unread-counter">-1</div>
@ -261,6 +261,11 @@
</div>
<div class="value button chat-counter">Hmm... Something seems to be wrong!</div>
</div>
<div class="block right mode-based mode-client_info">
<div class="title">&nbsp;</div>
<div class="value button open-conversation">error: open conversation</div>
</div>
</div>
</script>
@ -275,7 +280,10 @@
<div class="conversation">
<div class="spacer"></div>
<div class="messages"></div>
<div class="chatbox"></div>
<div class="chatbox">
<div class="container-typing">{{tr "Partner is typing..." /}}</div>
<node key="chatbox"></node>
</div>
</div>
</div>
</script>
@ -469,6 +477,7 @@
</div>
</div>
<div class="button-close"></div>
<div class="button-more">{{tr "Full Info" /}}</div>
</div>
</script>
@ -2747,64 +2756,87 @@
</script>
<script class="jsrender-template" id="tmpl_settings-teamspeak_import" type="text/html">
<div class="container-teamspeak-import">
<div class="error">{{tr "Failed to import identity (my little error)" /}}</div>
<div class="load-data">
<div>{{tr "Import an identity from TeamSpeak or an exported identity" /}}</div>
<div class="buttons">
<button class="button-load-text">{{tr "Load from text" /}}</button>
<button class="button-load-file">{{tr "Load from file" /}}</button>
<input class="input-file" type="file">
<div>
<div class="container-status"><a></a></div>
<fieldset class="">
<div class="container-text">
<label>
<div class="radio-button">
<input type="radio" name="type" value="text"/>
<div class="mark"></div>
</div>
<a>{{tr "Import from text" /}}</a>
</label>
<div class="form-group">
<label>{{tr "Identity Data" /}}</label>
<input class="form-control input-identity-text" placeholder="{{tr 'Please paste your identity data here' /}}"/>
</div>
</div>
<div class="success">
{{tr "Identity successfully loaded. Ready to import" /}}
<div class="container-file">
<label>
<div class="radio-button">
<input type="radio" name="type" value="file"/>
<div class="mark"></div>
</div>
<a>{{tr "Import from file" /}}</a>
</label>
<button class="btn btn-brown button-load-file">{{tr "Select file" /}}</button>
<a class="current-file">{{tr "No file selected" /}}</a>
</div>
</fieldset>
<input type="file" class="file-selector"/>
<div class="footer">
<button class="button-import">{{tr "Import" /}}</button>
<button class="btn btn-success button-import">{{tr "Import" /}}</button>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_settings-teamspeak_improve" type="text/html">
<div class="container-teamspeak-improve">
<div>
<div class="options">
<div class="title">{{tr "Identity: "/}} {{>identity_name}}</div>
<div class="row">
<div class="form-group identity-unique-id">
<label>{{tr "Identity:" /}}</label>
<label>{{tr "Unique ID:" /}}</label>
<input class="form-control" value="" readonly>
</div>
<div class="form-group identity-level">
<label>{{tr "Current level: " /}}</label>
<input class="form-control" value="loading" readonly>
</div>
</div>
<div class="row">
<div class="form-group identity-target-level">
<div class="bmd-label-static">
<div class="help-tagged">{{tr "Target level: " /}}</div>
<div class="help-tip-container"> <!-- lets be absolute -->
<div class="help-tip tip-right tip-small">
<p>
{{tr "A value of zero means unlimited. Improve until you abort"/}}
</p>
<div class="container-tooltip">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>
{{tr "The number of threads used to improve your identity" /}}<br>
{{tr "The optimal value is equal to the amount of kernel threads." /}}
</a>
</div>
</div>
{{tr "Target level: " /}}
</div>
<input class="form-control" value="0" type="number" min="0" max="160">
</div>
<div class="form-group threads">
<div class="bmd-label-static">
<div class="help-tagged">{{tr "Threads: " /}}</div>
<div class="help-tip-container"> <!-- lets be absolute -->
<div class="help-tip tip-right tip-small">
<p>
{{tr "The number of threads used to improve your identity<br>The optimal value is
equal to the amount of kernal threads."/}}
</p>
<div class="container-tooltip">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "A value of zero means unlimited. Improve until you abort" /}}</a>
</div>
</div>
{{tr "Threads: " /}}
</div>
<input class="form-control" value="2" type="number" min="1" max="32">
</div>
<hr>
<div class="form-row">
</div>
</div>
<div class="status">
<div class="row">
<div class="form-group hash-rate">
<label>{{tr "Hashes/second:" /}}</label>
<input class="form-control" value="0" readonly>
@ -2814,8 +2846,9 @@
<input class="form-control" value="00:00" readonly>
</div>
</div>
</div>
<div class="buttons">
<button class="btn btn-secondary btn-raised button-close">{{tr "Close" /}}</button>
<button class="btn btn-danger btn-raised button-close">{{tr "Close" /}}</button>
<button class="btn btn-success btn-raised button-start-stop">{{tr "Start" /}}</button>
</div>
</div>
@ -4753,5 +4786,399 @@
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_client_info" type="text/html">
<div> <!-- Important for the renderer -->
<div class="head">
<div class="status-row">
<div class="status-entry status-away">
<div class="icon_em client-away"></div>
</div>
<div class="status-entry status-output-muted">
<div class="icon_em client-output_muted"></div>
</div>
<div class="status-entry status-output-disabled">
<div class="icon_em client-hardware_output_muted"></div>
</div>
<div class="status-entry status-input-muted">
<div class="icon_em client-input_muted"></div>
</div>
<div class="status-entry status-input-disabled">
<div class="icon_em client-hardware_input_muted"></div>
</div>
</div>
<div class="container-away-message" title="{{tr 'Away message' /}}">
<a>A verry long away message, because I want to tell a story. There was a child....A verry long away message, because I want to tell a story. There was a child....A verry long away message, because I want to tell a story. There was a child....A verry long away message, because I want to tell a story. There was a child....A verry long away message, because I want to tell a story. There was a child....A verry long away message, because I want to tell a story. There was a child....A verry long away message, because I want to tell a story. There was a child....A verry long away message, because I want to tell a story. There was a child....</a>
</div>
</div>
<div class="body">
<div class="container-avatar">
</div>
<div class="container-name"></div>
<div class="container-description">
<div class="client-description"></div>
</div>
<div class="container-categories">
<div class="categories">
<div class="entry" container="container-basic">{{tr "Basic Info" /}}</div>
<div class="entry" container="container-groups">{{tr "Groups" /}}</div>
<div class="entry" container="container-packets">{{tr "Packets" /}}</div>
</div>
<div class="bodies">
<div class="body container-basic">
<div class="left">
<div class="property property-unique-id">
<div class="title">
<a>{{tr "Unique ID" /}}</a>
<div class="container-tooltip">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Database ID:" /}} <span class="value-dbid">error: database id</span></a>
</div>
</div>
</div>
<div class="value">
<a>error: unique id</a>
<div class="button button-copy">
<div class="icon_em client-copy"></div>
</div>
</div>
</div>
<div class="property property-teaforo">
<div class="title">
<a>{{tr "TeaForo Connection" /}}</a>
</div>
<div class="value">
<a>This will be a really long string...</a>
</div>
</div>
<div class="property property-version">
<div class="title">
<a>{{tr "Version" /}}</a>
<div class="container-tooltip">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Build timestamp:" /}} <span class="value-timestamp">error: no timestamp</span></a>
</div>
</div>
</div>
<div class="value">
<a>This will be a really long string...</a>
</div>
</div>
<div class="property property-country">
<div class="title">
<a>{{tr "Country" /}}</a>
</div>
<div class="value">
<a>This will be a really long string...</a>
</div>
</div>
<div class="property property-ip">
<div class="title">
<a>{{tr "IP Address" /}}</a>
</div>
<div class="value">
<a>error: IP</a>
<div class="button button-copy">
<div class="icon_em client-copy"></div>
</div>
</div>
</div>
</div>
<div class="spacer"></div>
<div class="right">
<div class="property property-first-connected">
<div class="title">
<a>{{tr "First connected" /}}</a>
</div>
<div class="value">
<a>error: first connected</a>
</div>
</div>
<div class="property property-connect-count">
<div class="title">
<a>{{tr "Connect count" /}}</a>
</div>
<div class="value">
<a>error: connect count</a>
</div>
</div>
<div class="property property-online-since">
<div class="title">
<a>{{tr "Online since" /}}</a>
</div>
<div class="value">
<a>error: online since</a>
</div>
</div>
<div class="property property-idle-time">
<div class="title">
<a>{{tr "Idle time" /}}</a>
</div>
<div class="value">
<a>error: idle time</a>
</div>
</div>
<div class="property property-ping">
<div class="title">
<a>{{tr "Ping" /}}</a>
</div>
<div class="value">
<a>error: ping</a>
</div>
</div>
</div>
</div>
<div class="body container-groups">
<div class="left">
<!-- <div class="title">{{tr "Server Groups" /}}</div> -->
<div class="container">
<div class="entries">
</div>
<div class="container-default-groups">
<a>{{tr "Client has no own server group" /}}</a>
</div>
</div>
<div class="buttons">
<button class="btn btn-success button-group-add">{{tr "Manage client server groups" /}}</button>
</div>
</div>
<!--
<div class="spacer"></div>
<div class="right">
<div class="title">{{tr "Channel Groups" /}}</div>
<div class="container">
</div>
</div>
-->
</div>
<div class="body container-packets">
<div class="left">
<div class="statistic statistic-packet-loss">
<div class="title">
<a>{{tr "Packetloss" /}}</a>
</div>
<div class="downstream">
<a>{{tr "Downstream" /}}</a>
<a class="value"></a>
</div>
<div class="upstream">
<a>{{tr "Upstream" /}}</a>
<a class="value"></a>
</div>
</div>
<div class="statistic statistic-transmitted-packets">
<div class="title">
<a>{{tr "Transmitted Packets" /}}</a>
</div>
<div class="downstream">
<a>{{tr "Downstream" /}}</a>
<a class="value"></a>
</div>
<div class="upstream">
<a>{{tr "Upstream" /}}</a>
<a class="value"></a>
</div>
</div>
<div class="statistic statistic-transmitted-bytes">
<div class="title">
<a>{{tr "Transmitted Bytes" /}}</a>
</div>
<div class="downstream">
<a>{{tr "Downstream" /}}</a>
<a class="value"></a>
</div>
<div class="upstream">
<a>{{tr "Upstream" /}}</a>
<a class="value"></a>
</div>
</div>
</div>
<div class="spacer"></div>
<div class="right">
<div class="statistic statistic-quota">
<div class="title">
<a>{{tr "Used/Max file transfer quota" /}}</a>
</div>
<div class="downstream">
<a>{{tr "Downstream" /}}</a>
<a class="value"></a>
</div>
<div class="upstream">
<a>{{tr "Upstream" /}}</a>
<a class="value"></a>
</div>
</div>
<div class="statistic statistic-bandwidth-second">
<div class="title">
<a>{{tr "Bandwidth last second" /}}</a>
</div>
<div class="downstream">
<a>{{tr "Downstream" /}}</a>
<a class="value"></a>
</div>
<div class="upstream">
<a>{{tr "Upstream" /}}</a>
<a class="value"></a>
</div>
</div>
<div class="statistic statistic-bandwidth-minute">
<div class="title">
<a>{{tr "Bandwidth last minute" /}}</a>
</div>
<div class="downstream">
<a>{{tr "Downstream" /}}</a>
<a class="value"></a>
</div>
<div class="upstream">
<a>{{tr "Upstream" /}}</a>
<a class="value"></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_server_info" type="text/html">
<div> <!-- required for the renderer -->
<div class="container-top hostbanner">
</div>
<div class="container-body">
<div class="group">
<div class="container-image">
<img src="img/serveredit_1.png">
</div>
<div class="container-properties">
<div class="row">
<a class="key">{{tr "Server name" /}}</a>
<div class="value server-name">
error: name
</div>
</div>
<div class="row">
<a class="key">{{tr "Server region" /}}</a>
<div class="value server-region">
error: region
</div>
</div>
<div class="row">
<a class="key">{{tr "Slots" /}}</a>
<div class="value server-slots">
error: slots
</div>
</div>
<div class="row">
<a class="key">{{tr "First run" /}}</a>
<div class="value server-first-run">
error: first run
</div>
</div>
<div class="row">
<a class="key">{{tr "Uptime" /}}</a>
<div class="value server-uptime">
error: uptime
</div>
</div>
</div>
</div>
<div class="group reverse">
<div class="container-image">
<img src="img/serveredit_2.png">
</div>
<div class="container-properties">
<div class="row">
<a class="key">{{tr "IP Address" /}}</a>
<div class="value server-ip">
error: ip
</div>
</div>
<div class="row">
<a class="key">{{tr "Version" /}}</a>
<div class="value server-version">
<div class="container-tooltip">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a></a>
</div>
</div>
<a></a>
</div>
</div>
<div class="row">
<a class="key">{{tr "Platform" /}}</a>
<div class="value server-platform">
error: platform
</div>
</div>
<div class="container-network">
<div class="container-button">
<button class="btn btn-purple button-show-bandwidth">{{tr "Show Bandwidth" /}}</button>
</div>
<div class="right">
<div class="row">
<a class="key">{{tr "Average ping" /}}</a>
<div class="value server-ping">
error: average ping
</div>
</div>
<div class="row">
<a class="key">{{tr "Average packet loss" /}}</a>
<div class="value server-packet-loss">
error: average packet loss
</div>
</div>
</div>
</div>
</div>
</div>
<div class="group">
<div class="container-image">
<img src="img/serveredit_3.png">
</div>
<div class="container-properties">
<div class="row">
<a class="key">{{tr "Global unique ID" /}}</a>
<div class="value server-unique-id">
error: unique id
</div>
</div>
<div class="row">
<a class="key">{{tr "Current Channels" /}}</a>
<div class="value server-channel-count">
error: channel count
</div>
</div>
<div class="row">
<a class="key">{{tr "Voice data encoded" /}}</a>
<div class="value server-voice-encryption">
error: voice encryption
</div>
</div>
<div class="row">
<a class="key">{{tr "Minimal security level" /}}</a>
<div class="value server-min-security-level">
error: security level
</div>
</div>
<div class="row">
<a class="key">{{tr "Complains until ban" /}}</a>
<div class="value server-complains">
error: complains
</div>
</div>
</div>
</div>
<div class="container-buttons">
<button class="btn btn-danger button-close">{{tr "Close" /}}</button>
</div>
</div>
</div>
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
shared/img/serveredit_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
shared/img/serveredit_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
shared/img/serveredit_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

View File

@ -61,7 +61,7 @@ namespace bipc {
abstract send_message(type: string, data: any, target?: string);
protected handle_message(message: BroadcastMessage) {
console.log("Received: %o", message);
log.trace(LogCategory.IPC, tr("Received message %o"), message);
if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) {
if(message.type == "process-query") {
@ -327,10 +327,10 @@ namespace bipc {
protected register_method<R>(method: (...args: any[]) => Promise<R> | string) {
let method_name: string;
if(typeof method === "function") {
console.log("Proxy method: %o", method.name);
log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name);
method_name = method.name;
} else {
console.log("Proxy method: %o", method);
log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method);
method_name = method;
}
@ -415,11 +415,11 @@ namespace bipc {
}
try {
console.log(tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments);
log.info(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments);
const promise = this[data.method_name](...data.arguments);
promise.then(result => {
console.log(tr("Result: %o"), result);
log.info(LogCategory.IPC, tr("Result: %o"), result);
this._send_result(data.promise_id, true, result);
}).catch(error => {
this._send_result(data.promise_id, false, error);

View File

@ -2,7 +2,6 @@
/// <reference path="proto.ts" />
/// <reference path="ui/view.ts" />
/// <reference path="settings.ts" />
/// <reference path="ui/frames/SelectedItemInfo.ts" />
/// <reference path="FileManager.ts" />
/// <reference path="permission/PermissionManager.ts" />
/// <reference path="permission/GroupManager.ts" />
@ -90,7 +89,6 @@ class ConnectionHandler {
groups: GroupManager;
side_bar: chat.Frame;
select_info: InfoBar;
settings: ServerSettings;
sound: sound.SoundManager;
@ -126,7 +124,6 @@ class ConnectionHandler {
this.settings = new ServerSettings();
this.log = new log.ServerLog(this);
this.select_info = new InfoBar(this);
this.channelTree = new ChannelTree(this);
this.side_bar = new chat.Frame(this);
this.sound = new sound.SoundManager(this);
@ -192,7 +189,7 @@ class ConnectionHandler {
server_address.port = 9987;
}
}
console.log(tr("Start connection to %s:%d"), server_address.host, server_address.port);
log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port);
this.log.log(log.server.Type.CONNECTION_BEGIN, {
address: {
server_hostname: server_address.host,
@ -210,7 +207,7 @@ class ConnectionHandler {
password: password
}
} catch(error) {
console.error(tr("Failed to hash connect password: %o"), error);
log.error(LogCategory.CLIENT, tr("Failed to hash connect password: %o"), error);
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open();
}
}
@ -278,7 +275,7 @@ class ConnectionHandler {
* LISTENER
*/
onConnected() {
console.log("Client connected!");
log.info(LogCategory.CLIENT, tr("Client connected"));
this.permissions.requestPermissionList();
if(this.groups.serverGroups.length == 0)
this.groups.requestGroups();
@ -416,7 +413,7 @@ class ConnectionHandler {
this.sound.play(Sound.CONNECTION_DISCONNECTED);
break;
case DisconnectReason.DNS_FAILED:
console.error(tr("Failed to resolve hostname: %o"), data);
log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data);
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, {
message: data as any
});
@ -428,7 +425,7 @@ class ConnectionHandler {
this.log.log(log.server.Type.CONNECTION_FAILED, {});
break;
}
console.error(tr("Could not connect to remote host! Error: %o"), data);
log.error(LogCategory.CLIENT, tr("Could not connect to remote host! Error: %o"), data);
if(native_client) {
createErrorModal(
@ -452,7 +449,7 @@ class ConnectionHandler {
break;
case DisconnectReason.HANDSHAKE_FAILED:
//TODO sound
console.error(tr("Failed to process handshake: %o"), data);
log.error(LogCategory.CLIENT, tr("Failed to process handshake: %o"), data);
createErrorModal(
tr("Could not connect"),
tr("Failed to process handshake: ") + data as string
@ -476,7 +473,7 @@ class ConnectionHandler {
auto_reconnect = false;
break;
case DisconnectReason.CONNECTION_CLOSED:
console.error(tr("Lost connection to remote server!"));
log.error(LogCategory.CLIENT, tr("Lost connection to remote server!"));
createErrorModal(
tr("Connection closed"),
tr("The connection was closed by remote host")
@ -486,7 +483,7 @@ class ConnectionHandler {
auto_reconnect = true;
break;
case DisconnectReason.CONNECTION_PING_TIMEOUT:
console.error(tr("Connection ping timeout"));
log.error(LogCategory.CLIENT, tr("Connection ping timeout"));
this.sound.play(Sound.CONNECTION_DISCONNECTED_TIMEOUT);
createErrorModal(
tr("Connection lost"),
@ -561,9 +558,8 @@ class ConnectionHandler {
this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
break;
default:
console.error(tr("Got uncaught disconnect!"));
console.error(tr("Type: %o Data:"), type);
console.error(data);
log.error(LogCategory.CLIENT, tr("Got uncaught disconnect!"));
log.error(LogCategory.CLIENT, tr("Type: %o Data: %o"), type, data);
break;
}
@ -574,18 +570,17 @@ class ConnectionHandler {
if(control_bar.current_connection_handler() == this)
control_bar.update_connection_state();
this.select_info.setCurrentSelected(null);
this.side_bar.private_conversations().clear_client_ids();
this.hostbanner.update();
if(auto_reconnect) {
if(!this.serverConnection) {
console.log(tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
return;
}
this.log.log(log.server.Type.RECONNECT_SCHEDULED, {timeout: 50000});
console.log(tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
const server_address = this.serverConnection.remote_address();
const profile = this.serverConnection.handshake_handler().profile;
@ -695,11 +690,11 @@ class ConnectionHandler {
if(vconnection.voice_recorder().input.current_state() === audio.recorder.InputState.PAUSED) {
vconnection.voice_recorder().input.start().then(result => {
if(result != audio.recorder.InputStartResult.EOK) {
console.warn(tr("Failed to start microphone input (%s)."), result);
log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), result);
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), result)).open();
}
}).catch(error => {
console.warn(tr("Failed to start microphone input (%s)."), error);
log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error);
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open();
});
}
@ -751,14 +746,13 @@ class ConnectionHandler {
resize_elements() {
this.channelTree.handle_resized();
this.select_info.handle_resize();
this.invoke_resized_on_activate = false;
}
acquire_recorder(voice_recoder: RecorderProfile, update_control_bar: boolean) {
const vconnection = this.serverConnection.voice_connection();
(vconnection ? vconnection.acquire_voice_recorder(voice_recoder) : Promise.resolve()).catch(error => {
console.error(tr("Failed to acquire recorder (%o)"), error);
log.warn(LogCategory.VOICE, tr("Failed to acquire recorder (%o)"), error);
}).then(() => {
this.update_voice_status(undefined);
});
@ -785,7 +779,7 @@ class ConnectionHandler {
if(typeof(data) === "undefined")
return;
if(data === null) {
console.log(tr("Deleting existing avatar"));
log.info(LogCategory.CLIENT, tr("Deleting existing avatar"));
this.serverConnection.send_command('ftdeletefile', {
name: "/avatar_", /* delete own avatar */
path: "",
@ -793,7 +787,7 @@ class ConnectionHandler {
}).then(() => {
createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open();
}).catch(error => {
console.error(tr("Failed to reset avatar flag: %o"), error);
log.error(LogCategory.GENERAL, tr("Failed to reset avatar flag: %o"), error);
let message;
if(error instanceof CommandResult)
@ -804,7 +798,7 @@ class ConnectionHandler {
return;
});
} else {
console.log(tr("Uploading new avatar"));
log.info(LogCategory.CLIENT, tr("Uploading new avatar"));
(async () => {
let key: transfer.UploadKey;
try {
@ -817,7 +811,7 @@ class ConnectionHandler {
channel_password: undefined
});
} catch(error) {
console.error(tr("Failed to initialize avatar upload: %o"), error);
log.error(LogCategory.GENERAL, tr("Failed to initialize avatar upload: %o"), error);
let message;
if(error instanceof CommandResult) {
//TODO: Resolve permission name
@ -837,7 +831,7 @@ class ConnectionHandler {
try {
await transfer.spawn_upload_transfer(key).put_data(data);
} catch(error) {
console.error(tr("Failed to upload avatar: %o"), error);
log.error(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error);
let message;
if(typeof(error) === "string")
@ -853,7 +847,7 @@ class ConnectionHandler {
client_flag_avatar: guid()
});
} catch(error) {
console.error(tr("Failed to update avatar flag: %o"), error);
log.error(LogCategory.GENERAL, tr("Failed to update avatar flag: %o"), error);
let message;
if(error instanceof CommandResult)
@ -889,9 +883,6 @@ class ConnectionHandler {
this.side_bar && this.side_bar.destroy();
this.side_bar = undefined;
this.select_info && this.select_info.destroy();
this.select_info = undefined;
this.log && this.log.destroy();
this.log = undefined;

View File

@ -235,7 +235,7 @@ class FileManager extends connection.AbstractCommandHandler {
}
if(!entry) {
console.error(tr("Invalid file list entry. Path: %s"), json[0]["path"]);
log.error(LogCategory.CLIENT, tr("Invalid file list entry. Path: %s"), json[0]["path"]);
return;
}
for(let e of (json as Array<FileEntry>)) {
@ -258,7 +258,7 @@ class FileManager extends connection.AbstractCommandHandler {
}
if(!entry) {
console.error(tr("Invalid file list entry finish. Path: "), json[0]["path"]);
log.error(LogCategory.CLIENT, tr("Invalid file list entry finish. Path: "), json[0]["path"]);
return;
}
entry.callback(entry.entries);
@ -659,7 +659,7 @@ class IconManager {
try {
download_key = await this.create_icon_download(id);
} catch(error) {
console.error(tr("Could not request download for icon %d: %o"), id, error);
log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), id, error);
throw "Failed to request icon";
}
@ -668,7 +668,7 @@ class IconManager {
try {
response = await downloader.request_file();
} catch(error) {
console.error(tr("Could not download icon %d: %o"), id, error);
log.error(LogCategory.CLIENT, tr("Could not download icon %d: %o"), id, error);
throw "failed to download icon";
}
@ -713,7 +713,7 @@ class IconManager {
return result;
throw "load result is empty";
} catch(error) {
console.error(tr("Icon download failed of icon %d: %o"), id, error);
log.error(LogCategory.CLIENT, tr("Icon download failed of icon %d: %o"), id, error);
}
throw "icon not found";
@ -759,7 +759,7 @@ class IconManager {
};
if(icon instanceof Promise) {
icon.then(_apply).catch(error => {
console.error(tr("Could not load icon. Reason: %s"), error);
log.error(LogCategory.CLIENT, tr("Could not load icon. Reason: %s"), error);
icon_load_image.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon");
});
} else {
@ -858,7 +858,7 @@ class AvatarManager {
}
create_avatar_download(client_avatar_id: string) : Promise<transfer.DownloadKey> {
console.log(tr("Downloading avatar %s"), client_avatar_id);
log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id);
return this.handle.download_file("", "/avatar_" + client_avatar_id);
}
@ -868,7 +868,7 @@ class AvatarManager {
try {
download_key = await this.create_avatar_download(client_avatar_id);
} catch(error) {
console.error(tr("Could not request download for avatar %s: %o"), client_avatar_id, error);
log.error(LogCategory.GENERAL, tr("Could not request download for avatar %s: %o"), client_avatar_id, error);
throw "failed to request avatar download";
}
@ -877,7 +877,7 @@ class AvatarManager {
try {
response = await downloader.request_file();
} catch(error) {
console.error(tr("Could not download avatar %s: %o"), client_avatar_id, error);
log.error(LogCategory.GENERAL, tr("Could not download avatar %s: %o"), client_avatar_id, error);
throw "failed to download avatar";
}
@ -915,7 +915,7 @@ class AvatarManager {
if(_cached.avatar_id === avatar_id)
return; /* cache is up2date */
console.log(tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id);
log.info(LogCategory.GENERAL, tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id);
delete this._cached_avatars[client_avatar_id];
AvatarManager.cache.delete("avatar_" + client_avatar_id).catch(error => {
log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o: %o"), client_avatar_id, error);
@ -961,7 +961,7 @@ class AvatarManager {
try {
avatar = await this.resolved_cached(client_avatar_id, avatar_id);
} catch(error) {
console.error(error);
log.error(LogCategory.CLIENT, error);
}
if(!avatar)
@ -984,7 +984,7 @@ class AvatarManager {
});
});
})().catch(reason => {
console.error(tr("Could not load avatar for id %s. Reason: %s"), client_avatar_id, reason);
log.error(LogCategory.CLIENT, tr("Could not load avatar for id %s. Reason: %s"), client_avatar_id, reason);
//TODO Broken image
loader_image.addClass("icon client-warning").attr("tag", tr("Could not load avatar ") + client_avatar_id);
})
@ -1049,7 +1049,7 @@ class AvatarManager {
if(avatar_id) {
if(this._cached_avatars[avatar_id]) { /* Test if we're may able to load the client avatar sync without a loading screen */
const cache: Avatar = this._cached_avatars[avatar_id];
console.log("[AVATAR] Using cached avatar. ID: %o | Version: %o (Cached: %o)", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id);
log.debug(LogCategory.GENERAL, tr("Using cached avatar. ID: %o | Version: %o (Cached: %o)"), avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id);
if(!client_handle || client_handle.properties.client_flag_avatar == cache.avatar_id) {
const image = $.spawn("img").attr("src", cache.url).css({width: '100%', height: '100%'});
return container.append(image);
@ -1063,13 +1063,13 @@ class AvatarManager {
let avatar: Avatar;
let loaded_image = this.generate_default_image();
console.log("[AVATAR] Resolving avatar. ID: %o | Version: %o", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
log.debug(LogCategory.GENERAL, tr("Resolving avatar. ID: %o | Version: %o"), avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
try {
//TODO: Cache if avatar load failed and try again in some minutes/may just even consider using the default avatar 'till restart
try {
avatar = await this.resolved_cached(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
} catch(error) {
console.error(tr("Failed to use cached avatar: %o"), error);
log.error(LogCategory.GENERAL, tr("Failed to use cached avatar: %o"), error);
}
if(!avatar)

View File

@ -94,7 +94,7 @@ namespace bookmarks {
try {
bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig;
} catch(error) {
console.error(tr("Failed to load bookmarks: %o"), error);
log.error(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error);
bookmarks = {} as any;
}

View File

@ -26,6 +26,8 @@ namespace connection {
this["notifychannelshow"] = this.handleCommandChannelShow;
this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo;
this["notifyconnectioninfo"] = this.handleNotifyConnectionInfo;
this["notifycliententerview"] = this.handleCommandClientEnterView;
this["notifyclientleftview"] = this.handleCommandClientLeftView;
this["notifyclientmoved"] = this.handleNotifyClientMoved;
@ -33,6 +35,7 @@ namespace connection {
this["notifychannelmoved"] = this.handleNotifyChannelMoved;
this["notifychanneledited"] = this.handleNotifyChannelEdited;
this["notifytextmessage"] = this.handleNotifyTextMessage;
this["notifyclientchatcomposing"] = this.notifyClientChatComposing;
this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed;
this["notifyclientupdated"] = this.handleNotifyClientUpdated;
this["notifyserveredited"] = this.handleNotifyServerEdited;
@ -78,8 +81,7 @@ namespace connection {
} else if(typeof(ex) === "string") {
this.connection_handler.log.log(log.server.Type.CONNECTION_COMMAND_ERROR, {error: ex});
} else {
console.error(tr("Invalid promise result type: %o. Result:"), typeof (ex));
console.error(ex);
log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex);
}
}
@ -110,7 +112,7 @@ namespace connection {
let code : string = json["return_code"];
if(!code || code.length == 0) {
console.log(tr("Invalid return code! (%o)"), json);
log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json);
return;
}
let retListeners = this.connection["_retListener"];
@ -130,9 +132,9 @@ namespace connection {
handleCommandServerInit(json){
//We could setup the voice channel
if(this.connection.support_voice()) {
console.log(tr("Setting up voice"));
log.debug(LogCategory.NETWORKING, tr("Setting up voice"));
} else {
console.log(tr("Skipping voice setup (No voice bridge available)"));
log.debug(LogCategory.NETWORKING, tr("Skipping voice setup (No voice bridge available)"));
}
@ -222,11 +224,32 @@ namespace connection {
/* everything is a number, so lets parse it */
for(const key of Object.keys(json))
json[key] = parseInt(json[key]);
json[key] = parseFloat(json[key]);
this.connection_handler.channelTree.server.set_connection_info(json);
}
handleNotifyConnectionInfo(json) {
json = json[0];
const object = new ClientConnectionInfo();
/* everything is a number (except ip), so lets parse it */
for(const key of Object.keys(json)) {
if(key === "connection_client_ip")
object[key] = json[key];
else
object[key] = parseFloat(json[key]);
}
const client = this.connection_handler.channelTree.findClient(parseInt(json["clid"]));
if(!client) {
log.warn(LogCategory.NETWORKING, tr("Received client connection info for unknown client (%o)"), json["clid"]);
return;
}
client.set_connection_info(object);
}
private createChannelFromJson(json, ignoreOrder: boolean = false) {
let tree = this.connection.client.channelTree;
@ -236,14 +259,14 @@ namespace connection {
let prev = tree.findChannel(json["channel_order"]);
if(!prev && json["channel_order"] != 0) {
if(!ignoreOrder) {
console.error(tr("Invalid channel order id!"));
log.error(LogCategory.NETWORKING, tr("Invalid channel order id!"));
return;
}
}
let parent = tree.findChannel(json["cpid"]);
if(!parent && json["cpid"] != 0) {
console.error(tr("Invalid channel parent"));
log.error(LogCategory.NETWORKING, tr("Invalid channel parent"));
return;
}
tree.moveChannel(channel, prev, parent); //TODO test if channel exists!
@ -275,7 +298,7 @@ namespace connection {
handleCommandChannelList(json) {
this.connection.client.channelTree.hide_channel_tree(); /* dont perform channel inserts on the dom to prevent style recalculations */
console.log(tr("Got %d new channels"), json.length);
log.debug(LogCategory.NETWORKING, tr("Got %d new channels"), json.length);
for(let index = 0; index < json.length; index++)
this.createChannelFromJson(json[index], true);
}
@ -297,12 +320,12 @@ namespace connection {
let tree = this.connection.client.channelTree;
const conversations = this.connection.client.side_bar.channel_conversations();
console.log(tr("Got %d channel deletions"), json.length);
log.info(LogCategory.NETWORKING, tr("Got %d channel deletions"), json.length);
for(let index = 0; index < json.length; index++) {
conversations.delete_conversation(parseInt(json[index]["cid"]));
let channel = tree.findChannel(json[index]["cid"]);
if(!channel) {
console.error(tr("Invalid channel onDelete (Unknown channel)"));
log.error(LogCategory.NETWORKING, tr("Invalid channel onDelete (Unknown channel)"));
continue;
}
tree.deleteChannel(channel);
@ -313,12 +336,12 @@ namespace connection {
let tree = this.connection.client.channelTree;
const conversations = this.connection.client.side_bar.channel_conversations();
console.log(tr("Got %d channel hides"), json.length);
log.info(LogCategory.NETWORKING, tr("Got %d channel hides"), json.length);
for(let index = 0; index < json.length; index++) {
conversations.delete_conversation(parseInt(json[index]["cid"]));
let channel = tree.findChannel(json[index]["cid"]);
if(!channel) {
console.error(tr("Invalid channel on hide (Unknown channel)"));
log.error(LogCategory.NETWORKING, tr("Invalid channel on hide (Unknown channel)"));
continue;
}
tree.deleteChannel(channel);
@ -443,7 +466,7 @@ namespace connection {
let tree = this.connection.client.channelTree;
let client = tree.findClient(entry["clid"]);
if(!client) {
console.error(tr("Unknown client left!"));
log.error(LogCategory.NETWORKING, tr("Unknown client left!"));
return 0;
}
if(client == this.connection.client.getClient()) {
@ -500,7 +523,7 @@ namespace connection {
if(channel_from == own_channel)
this.connection_handler.sound.play(Sound.USER_LEFT_TIMEOUT);
} else {
console.error(tr("Unknown client left reason!"));
log.error(LogCategory.NETWORKING, tr("Unknown client left reason!"));
}
if(!channel_to) {
@ -531,16 +554,16 @@ namespace connection {
let channel_from = tree.findChannel(json["cfid"]);
if(!client) {
console.error(tr("Unknown client move (Client)!"));
log.error(LogCategory.NETWORKING, tr("Unknown client move (Client)!"));
return 0;
}
if(!channel_to) {
console.error(tr("Unknown client move (Channel to)!"));
log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel to)!"));
return 0;
}
if(!channel_from) //Not critical
console.error(tr("Unknown client move (Channel from)!"));
log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel from)!"));
let self = client instanceof LocalClientEntry;
let current_clients: ClientEntry[];
@ -626,25 +649,23 @@ namespace connection {
handleNotifyChannelMoved(json) {
json = json[0]; //Only one bulk
for(let key in json)
console.log("Key: " + key + " Value: " + json[key]);
let tree = this.connection.client.channelTree;
let channel = tree.findChannel(json["cid"]);
if(!channel) {
console.error(tr("Unknown channel move (Channel)!"));
log.error(LogCategory.NETWORKING, tr("Unknown channel move (Channel)!"));
return 0;
}
let prev = tree.findChannel(json["order"]);
if(!prev && json["order"] != 0) {
console.error(tr("Unknown channel move (prev)!"));
log.error(LogCategory.NETWORKING, tr("Unknown channel move (prev)!"));
return 0;
}
let parent = tree.findChannel(json["cpid"]);
if(!parent && json["cpid"] != 0) {
console.error(tr("Unknown channel move (parent)!"));
log.error(LogCategory.NETWORKING, tr("Unknown channel move (parent)!"));
return 0;
}
@ -657,7 +678,7 @@ namespace connection {
let tree = this.connection.client.channelTree;
let channel = tree.findChannel(json["cid"]);
if(!channel) {
console.error(tr("Unknown channel edit (Channel)!"));
log.error(LogCategory.NETWORKING, tr("Unknown channel edit (Channel)!"));
return 0;
}
@ -686,7 +707,7 @@ namespace connection {
const target_own = target_client_id === this.connection.client.getClientId();
if(target_own && target_client_id === json["invokerid"]) {
console.error(tr("Received conversation message from invalid client id. Data: %o", json));
log.error(LogCategory.NETWORKING, tr("Received conversation message from invalid client id. Data: %o", json));
return;
}
@ -700,7 +721,7 @@ namespace connection {
attach: target_own
});
if(!conversation) {
console.error(tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message"));
log.error(LogCategory.NETWORKING, tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message"));
return;
}
@ -764,6 +785,24 @@ namespace connection {
}
}
notifyClientChatComposing(json) {
json = json[0];
const conversation_manager = this.connection_handler.side_bar.private_conversations();
const conversation = conversation_manager.find_conversation({
client_id: parseInt(json["clid"]),
unique_id: json["cluid"],
name: undefined
}, {
create: false,
attach: false
});
if(!conversation)
return;
conversation.trigger_typing();
}
handleNotifyClientChatClosed(json) {
json = json[0]; //Only one bulk
@ -793,7 +832,7 @@ namespace connection {
let client = this.connection.client.channelTree.findClient(json["clid"]);
if(!client) {
console.error(tr("Tried to update an non existing client"));
log.error(LogCategory.NETWORKING, tr("Tried to update an non existing client"));
return;
}
@ -806,8 +845,6 @@ namespace connection {
updates.push({key: key, value: json[key]});
}
client.updateVariables(...updates);
if(this.connection.client.select_info.currentSelected == client)
this.connection.client.select_info.update();
}
handleNotifyServerEdited(json) {
@ -826,8 +863,6 @@ namespace connection {
updates.push({key: key, value: json[key]});
}
this.connection.client.channelTree.server.updateVariables(false, ...updates);
if(this.connection.client.select_info.currentSelected == this.connection.client.channelTree.server)
this.connection.client.select_info.update();
}
handleNotifyServerUpdated(json) {
@ -846,9 +881,6 @@ namespace connection {
updates.push({key: key, value: json[key]});
}
this.connection.client.channelTree.server.updateVariables(true, ...updates);
let info = this.connection.client.select_info;
if(info.currentSelected instanceof ServerEntry)
info.update();
}
handleNotifyMusicPlayerInfo(json) {

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,17 @@ enum LogCategory {
CHANNEL,
CHANNEL_PROPERTIES, /* separating channel and channel properties because on channel init logging is a big bottleneck */
CLIENT,
BOOKMARKS,
SERVER,
PERMISSIONS,
GENERAL,
NETWORKING,
VOICE,
AUDIO,
I18N,
IPC,
IDENTITIES
IDENTITIES,
STATISTICS
}
namespace log {
@ -26,13 +29,16 @@ namespace log {
[LogCategory.CHANNEL_PROPERTIES, "Channel "],
[LogCategory.CLIENT, "Client "],
[LogCategory.SERVER, "Server "],
[LogCategory.BOOKMARKS, "Bookmark "],
[LogCategory.PERMISSIONS, "Permission "],
[LogCategory.GENERAL, "General "],
[LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "],
[LogCategory.AUDIO, "Audio "],
[LogCategory.I18N, "I18N "],
[LogCategory.IDENTITIES, "IDENTITIES "],
[LogCategory.IPC, "IPC "]
[LogCategory.IDENTITIES, "Identities "],
[LogCategory.IPC, "IPC "],
[LogCategory.STATISTICS, "Statistics "]
]);
export let enabled_mapping = new Map<number, boolean>([
@ -40,13 +46,25 @@ namespace log {
[LogCategory.CHANNEL_PROPERTIES, false],
[LogCategory.CLIENT, true],
[LogCategory.SERVER, true],
[LogCategory.BOOKMARKS, true],
[LogCategory.PERMISSIONS, true],
[LogCategory.GENERAL, true],
[LogCategory.NETWORKING, true],
[LogCategory.VOICE, true],
[LogCategory.AUDIO, true],
[LogCategory.I18N, false],
[LogCategory.IDENTITIES, true],
[LogCategory.IPC, true]
[LogCategory.IPC, true],
[LogCategory.STATISTICS, true]
]);
//Values will be overridden by initialize()
export let level_mapping = new Map<LogType, boolean>([
[LogType.TRACE, true],
[LogType.DEBUG, true],
[LogType.INFO, true],
[LogType.WARNING, true],
[LogType.ERROR, true]
]);
enum GroupMode {
@ -55,22 +73,36 @@ namespace log {
}
const group_mode: GroupMode = GroupMode.PREFIX;
loader.register_task(loader.Stage.LOADED, {
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "log enabled initialisation",
function: async () => initialize(),
priority: 10
priority: 150
});
//Example: <url>?log.i18n.enabled=0
//Category Example: <url>?log.i18n.enabled=0
//Level Example A: <url>?log.level.trace.enabled=0
//Level Example B: <url>?log.level=0
export function initialize() {
for(const category of Object.keys(LogCategory).map(e => parseInt(e))) {
if(isNaN(category)) continue;
const category_name = LogCategory[category];
enabled_mapping[category] = settings.static_global<boolean>("log." + category_name.toLowerCase() + ".enabled", enabled_mapping.get(category));
const category_name = LogCategory[category].toLowerCase();
enabled_mapping.set(category, settings.static_global<boolean>("log." + category_name.toLowerCase() + ".enabled", enabled_mapping.get(category)));
}
const base_level = settings.static_global<number>("log.level", app.type === app.Type.CLIENT_DEBUG || app.type === app.Type.WEB_DEBUG ? LogType.TRACE : LogType.INFO);
for(const level of Object.keys(LogType).map(e => parseInt(e))) {
if(isNaN(level)) continue;
const level_name = LogType[level].toLowerCase();
level_mapping.set(level, settings.static_global<boolean>("log." + level_name + ".enabled", level >= base_level));
}
}
function logDirect(type: LogType, message: string, ...optionalParams: any[]) {
if(!level_mapping.get(type))
return;
switch (type) {
case LogType.TRACE:
case LogType.DEBUG:
@ -86,11 +118,10 @@ namespace log {
console.error(message, ...optionalParams);
break;
}
//console.log("This is %cMy stylish message", "color: yellow; font-style: italic; background-color: blue;padding: 2px");
}
export function log(type: LogType, category: LogCategory, message: string, ...optionalParams: any[]) {
if(!enabled_mapping[category]) return;
if(!enabled_mapping.get(category)) return;
optionalParams.unshift(category_mapping.get(category));
message = "[%s] " + message;
@ -124,13 +155,15 @@ namespace log {
return new Group(group_mode, level, category, name, optionalParams);
}
export function table(title: string, arguments: any) {
export function table(level: LogType, category: LogCategory, title: string, arguments: any) {
if(group_mode == GroupMode.NATIVE) {
console.groupCollapsed(title);
console.table(arguments);
console.groupEnd();
} else {
console.log("Snipped table %s", title);
if(!enabled_mapping.get(category) || !level_mapping.get(level))
return;
logDirect(level, tr("Snipped table \"%s\""), title);
}
}
@ -154,7 +187,7 @@ namespace log {
this.category = category;
this.name = name;
this.optionalParams = optionalParams;
this.enabled = enabled_mapping[category];
this.enabled = enabled_mapping.get(category);
}
group(level: LogType, name: string, ...optionalParams: any[]) : Group {
@ -190,8 +223,9 @@ namespace log {
}
if(this.mode == GroupMode.NATIVE)
logDirect(this.level, message, ...optionalParams);
else
logDirect(this.level, this._log_prefix + message, ...optionalParams);
else {
logDirect(this.level, "[%s] " + this._log_prefix + message, category_mapping.get(this.category), ...optionalParams);
}
return this;
}

View File

@ -84,11 +84,11 @@ function setup_close() {
declare function moment(...arguments) : any;
function setup_jsrender() : boolean {
if(!js_render) {
displayCriticalError("Missing jsrender extension!");
loader.critical_error("Missing jsrender extension!");
return false;
}
if(!js_render.views) {
displayCriticalError("Missing jsrender viewer extension!");
loader.critical_error("Missing jsrender viewer extension!");
return false;
}
js_render.views.settings.allowCode(true);
@ -108,10 +108,10 @@ function setup_jsrender() : boolean {
});
$(".jsrender-template").each((idx, _entry) => {
if(!js_render.templates(_entry.id, _entry.innerHTML)) { //, _entry.innerHTML
console.error("Failed to cache template " + _entry.id + " for js render!");
if(!js_render.templates(_entry.id, _entry.innerHTML)) {
log.error(LogCategory.GENERAL, tr("Failed to setup cache for js renderer template %s!"), _entry.id);
} else
console.debug("Successfully loaded jsrender template " + _entry.id);
log.info(LogCategory.GENERAL, tr("Successfully loaded jsrender template %s"), _entry.id);
});
return true;
}
@ -123,7 +123,7 @@ async function initialize() {
await i18n.initialize();
} catch(error) {
console.error(tr("Failed to initialized the translation system!\nError: %o"), error);
displayCriticalError("Failed to setup the translation system");
loader.critical_error("Failed to setup the translation system");
return;
}
@ -131,13 +131,6 @@ async function initialize() {
}
async function initialize_app() {
const display_load_error = message => {
if(typeof(display_critical_load) !== "undefined")
display_critical_load(message);
else
displayCriticalError(message);
};
try { //Initialize main template
const main = $("#tmpl_main").renderTag({
multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
@ -146,8 +139,8 @@ async function initialize_app() {
$("body").append(main);
} catch(error) {
console.error(error);
display_load_error(tr("Failed to setup main page!"));
log.error(LogCategory.GENERAL, error);
loader.critical_error(tr("Failed to setup main page!"));
return;
}
@ -158,7 +151,7 @@ async function initialize_app() {
if(audio.player.set_master_volume)
audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100);
else
console.warn("Client does not support audio.player.set_master_volume()... May client is too old?");
log.warn(LogCategory.GENERAL, tr("Client does not support audio.player.set_master_volume()... May client is too old?"));
if(audio.recorder.device_refresh_available())
await audio.recorder.refresh_devices();
@ -166,7 +159,7 @@ async function initialize_app() {
await default_recorder.initialize();
sound.initialize().then(() => {
console.log(tr("Sounds initialitzed"));
log.info(LogCategory.AUDIO, tr("Sounds initialized"));
});
sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100);
@ -175,8 +168,8 @@ async function initialize_app() {
try {
await ppt.initialize();
} catch(error) {
console.error(tr("Failed to initialize ppt!\nError: %o"), error);
displayCriticalError(tr("Failed to initialize ppt!"));
log.error(LogCategory.GENERAL, tr("Failed to initialize ppt!\nError: %o"), error);
loader.critical_error(tr("Failed to initialize ppt!"));
return;
}
@ -274,15 +267,15 @@ class TestProxy extends bipc.MethodProxy {
}
protected on_connected() {
console.log("Test proxy connected");
log.info(LogCategory.IPC, "Test proxy connected");
}
protected on_disconnected() {
console.log("Test proxy disconnected");
log.info(LogCategory.IPC, "Test proxy disconnected");
}
private async say_hello() : Promise<void> {
console.log("Hello World");
log.info(LogCategory.IPC, "Hello World");
}
private async add_slave(a: number, b: number) : Promise<number> {
@ -363,7 +356,7 @@ function main() {
volatile_collection_only: false
});
stats.register_user_count_listener(status => {
console.log("Received user count update: %o", status);
log.info(LogCategory.STATISTICS, tr("Received user count update: %o"), status);
});
(<any>window).test_upload = (message?: string) => {
@ -377,7 +370,6 @@ function main() {
name: '/HelloWorld.txt',
path: ''
}).then(key => {
console.log("Got key: %o", key);
const upload = new RequestFileUpload(key);
const buffer = new Uint8Array(message.length);
@ -396,7 +388,6 @@ function main() {
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
console.log("UUID: %s", profile_uuid);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
@ -431,9 +422,10 @@ function main() {
});
*/
// Modals.openServerInfo(connection.channelTree.server);
//Modals.createServerModal(connection.channelTree.server, properties => Promise.resolve());
}, 1000);
//Modals.spawnSettingsModal("audio-sounds");
Modals.spawnSettingsModal("identity-profiles");
//Modals.spawnKeySelect(console.log);
}
@ -454,7 +446,7 @@ const task_teaweb_starter: loader.Task = {
console.error(ex.stack);
if(ex instanceof ReferenceError || ex instanceof TypeError)
ex = ex.name + ": " + ex.message;
displayCriticalError("Failed to invoke main function:<br>" + ex);
loader.critical_error("Failed to invoke main function:<br>" + ex);
}
},
priority: 10
@ -519,7 +511,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
if(!setup_jsrender())
throw "invalid load";
} catch (error) {
displayCriticalError(tr("Failed to setup jsrender"));
loader.critical_error(tr("Failed to setup jsrender"));
console.error(tr("Failed to load jsrender! %o"), error);
return;
}
@ -543,7 +535,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
if(ex instanceof ReferenceError || ex instanceof TypeError)
ex = ex.name + ": " + ex.message;
displayCriticalError("Failed to boot app function:<br>" + ex);
loader.critical_error("Failed to boot app function:<br>" + ex);
}
},
priority: 1000

View File

@ -51,7 +51,6 @@ class Group {
if(key == "iconid") {
this.properties.iconid = (new Uint32Array([this.properties.iconid]))[0];
console.log("Icon id " + this.properties.iconid);
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
client.updateGroupIcon(this);
});
@ -139,7 +138,7 @@ class GroupManager extends connection.AbstractCommandHandler {
if(json[0]["sgid"]) target = GroupTarget.SERVER;
else if(json[0]["cgid"]) target = GroupTarget.CHANNEL;
else {
console.error(tr("Could not resolve group target! => %o"), json[0]);
log.error(LogCategory.CLIENT, tr("Could not resolve group target! => %o"), json[0]);
return;
}
@ -155,8 +154,7 @@ class GroupManager extends connection.AbstractCommandHandler {
case 1: type = GroupType.NORMAL; break;
case 2: type = GroupType.QUERY; break;
default:
//TODO tr
console.error("Invalid group type: " + groupData["type"] + " for group " + groupData["name"]);
log.error(LogCategory.CLIENT, tr("Invalid group type: %o for group %s"), groupData["type"],groupData["name"]);
continue;
}
@ -180,7 +178,6 @@ class GroupManager extends connection.AbstractCommandHandler {
this.channelGroups.push(group);
}
console.log("Got " + json.length + " new " + target + " groups:");
for(const client of this.handle.channelTree.clients)
client.update_displayed_client_groups();
}

View File

@ -1,6 +1,8 @@
/// <reference path="../ConnectionHandler.ts" />
/// <reference path="../connection/ConnectionBase.ts" />
import LogType = log.LogType;
enum PermissionType {
B_SERVERINSTANCE_HELP_VIEW = "b_serverinstance_help_view", /* Permission ID: 1 */
B_SERVERINSTANCE_VERSION_VIEW = "b_serverinstance_version_view", /* Permission ID: 2 */
@ -601,7 +603,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
"description": perm.description
});
}
log.table("Permission list", table_entries);
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Permission list", table_entries);
group.end();
log.info(LogCategory.PERMISSIONS, tr("Got %i permissions"), this.permissionList.length);
@ -658,7 +660,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
});
}
log.table("Needed client permissions", table_entries);
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Needed client permissions", table_entries);
group.end();
log.debug(LogCategory.PERMISSIONS, tr("Dropping %o needed permissions and added %o permissions."), copy.length, addcount);

View File

@ -201,7 +201,7 @@ namespace sound {
manager = new SoundManager(undefined);
audio.player.on_ready(reinitialisize_audio);
return new Promise<void>(resolve => {
return new Promise<void>((resolve, reject) => {
$.ajax({
url: "audio/speech/mapping.json",
success: response => {
@ -211,9 +211,9 @@ namespace sound {
register_sound(entry.key, "speech/" + entry.file);
resolve();
},
error: () => {
console.log("error!");
console.dir(...arguments);
error: error => {
log.error(LogCategory.AUDIO, "error: %o", error);
reject();
},
timeout: 5000,
async: true,
@ -254,19 +254,17 @@ namespace sound {
if(context.decodeAudioData) {
if(!file.cached) {
const decode_data = buffer => {
console.log(buffer);
try {
console.log(tr("Decoding data"));
log.info(LogCategory.AUDIO, tr("Decoding data"));
context.decodeAudioData(buffer, result => {
file.cached = result;
}, error => {
console.error(tr("Failed to decode audio data for %o"), sound);
console.error(error);
log.error(LogCategory.AUDIO, tr("Failed to decode audio data for %o: %o"), sound, error);
file.not_supported = true;
file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again!
})
} catch (error) {
console.error(error);
log.error(LogCategory.AUDIO, error);
file.not_supported = true;
file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again!
}
@ -288,15 +286,15 @@ namespace sound {
if (xhr.status != 200)
throw "invalid response code (" + xhr.status + ")";
console.log(tr("Decoding data"));
log.debug(LogCategory.AUDIO, tr("Decoding data"));
try {
file.cached = await context.decodeAudioData(xhr.response);
} catch(error) {
console.error(error);
log.error(LogCategory.AUDIO, error);
throw "failed to decode audio data";
}
} catch(error) {
console.error(tr("Failed to load audio file %s. Error: %o"), sound, error);
log.error(LogCategory.AUDIO, tr("Failed to load audio file %s. Error: %o"), sound, error);
file.not_supported = true;
file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again!
}
@ -305,7 +303,7 @@ namespace sound {
if(!file.node) {
if(!warned) {
warned = true;
console.warn(tr("Your browser does not support decodeAudioData! Using a node to playback! This bypasses the audio output and volume regulation!"));
log.warn(LogCategory.AUDIO, tr("Your browser does not support decodeAudioData! Using a node to playback! This bypasses the audio output and volume regulation!"));
}
const container = $("#sounds");
const node = $.spawn("audio").attr("src", path);
@ -332,7 +330,7 @@ namespace sound {
options = options || {};
const volume = get_sound_volume(_sound, options.default_volume);
console.log(tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume);
log.info(LogCategory.AUDIO, tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume);
if(volume == 0 || master_volume == 0)
return;
@ -342,7 +340,7 @@ namespace sound {
const context = audio.player.context();
if(!context) {
console.warn(tr("Tried to replay a sound without an audio context (Sound: %o). Dropping playback"), _sound);
log.warn(LogCategory.AUDIO, tr("Tried to replay a sound without an audio context (Sound: %o). Dropping playback"), _sound);
return;
}
@ -351,7 +349,7 @@ namespace sound {
return;
if(!options.ignore_overlap && (this._playing_sounds[_sound] > 0) && !sound.overlap_activated()) {
console.log(tr("Dropping requested playback for sound %s because it would overlap."), _sound);
log.info(LogCategory.AUDIO, tr("Dropping requested playback for sound %s because it would overlap."), _sound);
return;
}
@ -387,18 +385,18 @@ namespace sound {
if(options.callback)
options.callback(true);
}).catch(error => {
console.warn(tr("Sound playback for sound %o resulted in an error: %o"), sound, error);
log.warn(LogCategory.AUDIO, tr("Sound playback for sound %o resulted in an error: %o"), sound, error);
if(options.callback)
options.callback(false);
});
} else {
console.warn(tr("Failed to replay sound %o because of missing handles."), sound);
log.warn(LogCategory.AUDIO, tr("Failed to replay sound %o because of missing handles."), sound);
if(options.callback)
options.callback(false);
return;
}
}).catch(error => {
console.warn(tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error);
log.warn(LogCategory.AUDIO, tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error);
if(options.callback)
options.callback(false);
});

View File

@ -73,7 +73,7 @@ namespace stats {
export function initialize(config: Config) {
current_config = initialize_config_object(config || {}, DEFAULT_CONFIG);
if(current_config.verbose)
console.log(LOG_PREFIX + tr("Initializing statistics with this config: %o"), current_config);
log.info(LogCategory.STATISTICS, tr("Initializing statistics with this config: %o"), current_config);
connection.start_connection();
}
@ -110,7 +110,7 @@ namespace stats {
if(connection_copy !== connection) return;
if(current_config.verbose)
console.log(LOG_PREFIX + tr("Lost connection to statistics server (Connection closed). Reason: %o. Event object: %o"), CloseCodes[event.code] || event.code, event);
log.warn(LogCategory.STATISTICS, tr("Lost connection to statistics server (Connection closed). Reason: %o. Event object: %o"), CloseCodes[event.code] || event.code, event);
if(event.code != CloseCodes.BANNED)
invoke_reconnect();
@ -120,7 +120,7 @@ namespace stats {
if(connection_copy !== connection) return;
if(current_config.verbose)
console.log(LOG_PREFIX + tr("Successfully connected to server. Initializing session."));
log.info(LogCategory.STATISTICS, tr("Successfully connected to server. Initializing session."));
connection_state = ConnectionState.INITIALIZING;
initialize_session();
@ -130,7 +130,7 @@ namespace stats {
if(connection_copy !== connection) return;
if(current_config.verbose)
console.log(LOG_PREFIX + tr("Received an error. Closing connection. Object: %o"), event);
log.warn(LogCategory.STATISTICS, tr("Received an error. Closing connection. Object: %o"), event);
connection.close(CloseCodes.INTERNAL_ERROR);
invoke_reconnect();
@ -141,7 +141,7 @@ namespace stats {
if(typeof(event.data) !== 'string') {
if(current_config.verbose)
console.warn(LOG_PREFIX + tr("Received an message which isn't a string. Event object: %o"), event);
log.info(LogCategory.STATISTICS, tr("Received an message which isn't a string. Event object: %o"), event);
return;
}
@ -170,11 +170,11 @@ namespace stats {
}
if(current_config.verbose)
console.log(LOG_PREFIX + tr("Scheduled reconnect in %dms"), current_config.reconnect_interval);
log.info(LogCategory.STATISTICS, tr("Scheduled reconnect in %dms"), current_config.reconnect_interval);
reconnect_timer = setTimeout(() => {
if(current_config.verbose)
console.log(LOG_PREFIX + tr("Reconnecting"));
log.info(LogCategory.STATISTICS, tr("Reconnecting"));
start_connection();
}, current_config.reconnect_interval);
}
@ -212,10 +212,10 @@ namespace stats {
if(typeof(handler[type]) === 'function') {
if(current_config.verbose)
console.debug(LOG_PREFIX + tr("Handling message of type %s"), type);
log.debug(LogCategory.STATISTICS, tr("Handling message of type %s"), type);
handler[type](data);
} else if(current_config.verbose) {
console.warn(LOG_PREFIX + tr("Received message with an unknown type (%s). Dropping message. Full message: %o"), type, data_object);
log.warn(LogCategory.STATISTICS, tr("Received message with an unknown type (%s). Dropping message. Full message: %o"), type, data_object);
}
}
@ -231,7 +231,7 @@ namespace stats {
interface NotifyInitialized {}
function handle_notify_initialized(json: NotifyInitialized) {
if(current_config.verbose)
console.log(LOG_PREFIX + tr("Session successfully initialized."));
log.info(LogCategory.STATISTICS, tr("Session successfully initialized."));
connection_state = ConnectionState.CONNECTED;
}

View File

@ -393,8 +393,9 @@ class ChannelEntry {
for(let index = 0; index + 1 < clients.length; index++)
clients[index].tag.before(clients[index + 1].tag);
log.debug(LogCategory.CHANNEL, tr("Reordered channel clients: %d"), clients.length);
for(let client of clients) {
console.log("- %i %s", client.properties.client_talk_power, client.properties.client_nickname);
log.debug(LogCategory.CHANNEL, "- %i %s", client.properties.client_talk_power, client.properties.client_nickname);
}
}
}
@ -624,6 +625,7 @@ class ChannelEntry {
}
handle_frame_resized() {
if(this._channel_name_formatted === "align-repetitive")
this.__updateChannelName();
}
@ -631,16 +633,15 @@ class ChannelEntry {
private __updateChannelName() {
this._channel_name_formatted = undefined;
parseType:
parse_type:
if(this.parent_channel() == null && this.properties.channel_name.charAt(0) == '[') {
let end = this.properties.channel_name.indexOf(']');
if(end == -1) break parseType;
if(end == -1) break parse_type;
let options = this.properties.channel_name.substr(1, end - 1);
if(options.indexOf("spacer") == -1) break parseType;
if(options.indexOf("spacer") == -1) break parse_type;
options = options.substr(0, options.indexOf("spacer"));
console.log(tr("Channel options: '%o'"), options);
if(options.length == 0)
options = "l";
else if(options.length > 1)
@ -661,11 +662,10 @@ class ChannelEntry {
break;
default:
this._channel_name_alignment = undefined;
break parseType;
break parse_type;
}
this._channel_name_formatted = this.properties.channel_name.substr(end + 1) || "";
console.log(tr("Got formated channel name: %o"), this._channel_name_formatted);
}
this._tag_channel.find(".show-channel-normal-only").toggleClass("channel-normal", this._channel_name_formatted === undefined);
@ -696,14 +696,13 @@ class ChannelEntry {
index = 63;
} while (tag_name.parent().width() >= tag_name.width() && ++index < 64);
if(index == 64)
console.warn(LogCategory.CHANNEL, tr("Repeating spacer took too much repeats!"));
log.warn(LogCategory.CHANNEL, tr("Repeating spacer took too much repeats!"));
if(lastSuccess.length > 0) {
tag_name.text(lastSuccess);
}
}
}
}
console.log(tr("Align: %s"), this._channel_name_alignment);
}
recalculate_repetitive_name() {
@ -722,7 +721,7 @@ class ChannelEntry {
value: variable.value,
type: typeof (this.properties[variable.key])
});
log.table("Clannel update properties", entries);
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Clannel update properties", entries);
}
let info_update = false;

View File

@ -25,6 +25,8 @@ class ClientProperties {
client_channel_group_id: number = 0;
client_lastconnected: number = 0;
client_created: number = 0;
client_totalconnections: number = 0;
client_flag_avatar: string = "";
client_icon_id: number = 0;
@ -44,9 +46,68 @@ class ClientProperties {
client_teaforo_name: string = "";
client_teaforo_flags: number = 0; /* 0x01 := Banned | 0x02 := Stuff | 0x04 := Premium */
/* not updated in view! */
client_month_bytes_uploaded: number = 0;
client_month_bytes_downloaded: number = 0;
client_total_bytes_uploaded: number = 0;
client_total_bytes_downloaded: number = 0;
client_talk_power: number = 0;
}
class ClientConnectionInfo {
connection_bandwidth_received_last_minute_control: number = -1;
connection_bandwidth_received_last_minute_keepalive: number = -1;
connection_bandwidth_received_last_minute_speech: number = -1;
connection_bandwidth_received_last_second_control: number = -1;
connection_bandwidth_received_last_second_keepalive: number = -1;
connection_bandwidth_received_last_second_speech: number = -1;
connection_bandwidth_sent_last_minute_control: number = -1;
connection_bandwidth_sent_last_minute_keepalive: number = -1;
connection_bandwidth_sent_last_minute_speech: number = -1;
connection_bandwidth_sent_last_second_control: number = -1;
connection_bandwidth_sent_last_second_keepalive: number = -1;
connection_bandwidth_sent_last_second_speech: number = -1;
connection_bytes_received_control: number = -1;
connection_bytes_received_keepalive: number = -1;
connection_bytes_received_speech: number = -1;
connection_bytes_sent_control: number = -1;
connection_bytes_sent_keepalive: number = -1;
connection_bytes_sent_speech: number = -1;
connection_packets_received_control: number = -1;
connection_packets_received_keepalive: number = -1;
connection_packets_received_speech: number = -1;
connection_packets_sent_control: number = -1;
connection_packets_sent_keepalive: number = -1;
connection_packets_sent_speech: number = -1;
connection_ping: number = -1;
connection_ping_deviation: number = -1;
connection_server2client_packetloss_control: number = -1;
connection_server2client_packetloss_keepalive: number = -1;
connection_server2client_packetloss_speech: number = -1;
connection_server2client_packetloss_total: number = -1;
connection_client2server_packetloss_speech: number = -1;
connection_client2server_packetloss_keepalive: number = -1;
connection_client2server_packetloss_control: number = -1;
connection_client2server_packetloss_total: number = -1;
connection_filetransfer_bandwidth_sent: number = -1;
connection_filetransfer_bandwidth_received: number = -1;
connection_connected_time: number = -1;
connection_idle_time: number = -1;
connection_client_ip: string | undefined;
connection_client_port: number = -1;
}
class ClientEntry {
protected _clientId: number;
protected _channel: ChannelEntry;
@ -61,6 +122,14 @@ class ClientEntry {
protected _audio_volume: number;
protected _audio_muted: boolean;
private _info_variables_promise: Promise<void>;
private _info_variables_promise_timestamp: number;
private _info_connection_promise: Promise<ClientConnectionInfo>;
private _info_connection_promise_timestamp: number;
private _info_connection_promise_resolve: any;
private _info_connection_promise_reject: any;
channelTree: ChannelTree;
constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) {
@ -176,10 +245,6 @@ class ClientEntry {
}
});
this.tag.on('click', event => {
console.log("I've been clicked!");
});
if(!(this instanceof LocalClientEntry) && !(this instanceof MusicClientEntry))
this.tag.dblclick(event => {
if($.isArray(this.channelTree.currently_selected)) { //Multiselect
@ -524,8 +589,7 @@ class ClientEntry {
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume);
if(this._audio_handle)
this._audio_handle.set_volume(volume);
if(this.channelTree.client.select_info.currentSelected == this)
this.channelTree.client.select_info.update();
//TODO: Update in info
});
}
}, {
@ -710,7 +774,7 @@ class ClientEntry {
value: variable.value,
type: typeof (this.properties[variable.key])
});
log.table("Client update properties", entries);
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Client update properties", entries);
}
for(const variable of variables) {
@ -848,11 +912,17 @@ class ClientEntry {
}
}
updateClientVariables(){
if(this.lastVariableUpdate == 0 || new Date().getTime() - 10 * 60 * 1000 > this.lastVariableUpdate){ //Cache these only 10 min
this.lastVariableUpdate = new Date().getTime();
this.channelTree.client.serverConnection.send_command("clientgetvariables", {clid: this.clientId()});
}
updateClientVariables(force_update?: boolean) : Promise<void> {
if(Date.now() - 10 * 60 * 1000 < this._info_variables_promise_timestamp && this._info_variables_promise && (typeof(force_update) !== "boolean" || force_update))
return this._info_variables_promise;
this._info_variables_promise_timestamp = Date.now();
return (this._info_variables_promise = new Promise<void>((resolve, reject) => {
this.channelTree.client.serverConnection.send_command("clientgetvariables", {clid: this.clientId()}).then(() => resolve()).catch(error => {
this._info_connection_promise_timestamp = 0; /* not succeeded */
reject(error);
});
}));
}
updateClientIcon() {
@ -957,6 +1027,34 @@ class ClientEntry {
client_id: this._clientId
}
}
/* max 1s ago, so we could update every second */
request_connection_info() : Promise<ClientConnectionInfo> {
if(Date.now() - 900 < this._info_connection_promise_timestamp && this._info_connection_promise)
return this._info_connection_promise;
if(this._info_connection_promise_reject)
this._info_connection_promise_resolve("timeout");
let _local_reject; /* to ensure we're using the right resolve! */
this._info_connection_promise = new Promise<ClientConnectionInfo>((resolve, reject) => {
this._info_connection_promise_resolve = resolve;
this._info_connection_promise_reject = reject;
_local_reject = reject;
});
this._info_connection_promise_timestamp = Date.now();
this.channelTree.client.serverConnection.send_command("getconnectioninfo", {clid: this._clientId}).catch(error => _local_reject(error));
return this._info_connection_promise;
}
set_connection_info(info: ClientConnectionInfo) {
if(!this._info_connection_promise_resolve)
return;
this._info_connection_promise_resolve(info);
this._info_connection_promise_resolve = undefined;
this._info_connection_promise_reject = undefined;
}
}
class LocalClientEntry extends ClientEntry {
@ -1243,8 +1341,6 @@ class MusicClientEntry extends ClientEntry {
Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => {
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume);
this._audio_handle.set_volume(volume);
if(this.channelTree.client.select_info.currentSelected == this)
(<MusicInfoManager>this.channelTree.client.select_info.current_manager()).update_local_volume(volume);
});
}
},
@ -1265,8 +1361,7 @@ class MusicClientEntry extends ClientEntry {
clid: this.clientId(),
player_volume: value,
}).then(() => {
if(this.channelTree.client.select_info.currentSelected == this)
(<MusicInfoManager>this.channelTree.client.select_info.current_manager()).update_remote_volume(value);
//TODO: Update in info
});
});
}

View File

@ -50,7 +50,7 @@ class ClientMover {
this.selected_client = client;
this.callback = callback;
console.log(tr("Starting mouse move"));
log.debug(LogCategory.GENERAL, tr("Starting mouse move"));
ClientMover.listener_root.on('mouseup', this._bound_finish = this.finish_listener.bind(this)).on('mousemove', this._bound_move = this.move_listener.bind(this));
@ -112,6 +112,7 @@ class ClientMover {
private finish_listener(event) {
ClientMover.move_element.hide();
log.debug(LogCategory.GENERAL, tr("Finishing mouse move"));
const channel_id = this.hovered_channel ? parseInt(this.hovered_channel.getAttribute("channel-id")) : 0;
ClientMover.listener_root.unbind('mouseleave', this._bound_finish);

View File

@ -16,9 +16,18 @@ if(!$.fn.dividerfy) {
const seperator_id = element.attr("seperator-id");
const vertical = element.hasClass("vertical");
const apply_view = (property: string, previous: number, next: number) => {
previous_element.css(property, "calc(" + previous + "% - " + (vertical ? element.width() : element.height()) + "px)");
next_element.css(property, "calc(" +next + "% - " + (vertical ? element.width() : element.height()) + "px)");
const apply_view = (property: "width" | "height", previous: number, next: number) => {
const value_a = "calc(" + previous + "% - " + (vertical ? element.width() : element.height()) + "px)";
const value_b = "calc(" + next + "% - " + (vertical ? element.width() : element.height()) + "px)";
/* dont cause a reflow here */
if(property === "height") {
(previous_element[0] as HTMLElement).style.height = value_a;
(next_element[0] as HTMLElement).style.height = value_b;
} else {
(previous_element[0] as HTMLElement).style.width = value_a;
(next_element[0] as HTMLElement).style.width = value_b;
}
};
const listener_move = (event: MouseEvent | TouchEvent) => {
@ -105,12 +114,12 @@ if(!$.fn.dividerfy) {
try {
const config = JSON.parse(settings.global("seperator-settings-" + seperator_id));
if(config) {
console.log("Apply previous changed: %o", config);
log.debug(LogCategory.GENERAL, tr("Applying previous changed sperator settings for %s: %o"), seperator_id, config);
apply_view(config.property, config.previous, config.next);
}
} catch(e) {
if(!(e instanceof SyntaxError))
console.error(e);
log.error(LogCategory.GENERAL, tr("Failed to parse seperator settings for sperator %s: %o"), seperator_id, e);
}
}
});

View File

@ -80,14 +80,15 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
private _global_click_listener: (event) => any;
private _context_menu: JQuery;
private _close_callbacks: (() => any)[] = [];
private _visible = false;
despawn_context_menu() {
let menu = this._context_menu || (this._context_menu = $(".context-menu"));
if(!menu.is(":visible"))
if(!this._visible)
return;
let menu = this._context_menu || (this._context_menu = $(".context-menu"));
menu.animate({opacity: 0}, 100, () => menu.css("display", "none"));
this._visible = false;
for(const callback of this._close_callbacks) {
if(typeof(callback) !== "function") {
console.error(tr("Given close callback is not a function!. Callback: %o"), callback);
@ -185,6 +186,8 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
}
spawn_context_menu(x: number, y: number, ...entries: contextmenu.MenuEntry[]) {
this._visible = true;
let menu_tag = this._context_menu || (this._context_menu = $(".context-menu"));
menu_tag.finish().empty().css("opacity", "0");

View File

@ -1,700 +0,0 @@
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../../../vendor/xbbcode/src/parser.ts" />
abstract class InfoManagerBase {
private timers: NodeJS.Timer[] = [];
private intervals: number[] = [];
protected resetTimers() {
for(let timer of this.timers)
clearTimeout(timer);
this.timers = [];
}
protected resetIntervals() {
for(let interval of this.intervals)
clearInterval(interval);
this.intervals = [];
}
protected registerTimer(timer: NodeJS.Timer) {
this.timers.push(timer);
}
protected registerInterval<T extends number | NodeJS.Timer>(interval: T) {
this.intervals.push(interval as number);
}
abstract available<V>(object: V) : boolean;
}
abstract class InfoManager<T> extends InfoManagerBase {
protected handle?: InfoBar<undefined>;
createFrame<_>(handle: InfoBar<_>, object: T, html_tag: JQuery<HTMLElement>) {
this.handle = handle as InfoBar<undefined>;
}
abstract updateFrame(object: T, html_tag: JQuery<HTMLElement>);
finalizeFrame(object: T, frame: JQuery<HTMLElement>) {
this.resetIntervals();
this.resetTimers();
this.handle = undefined;
}
protected triggerUpdate() {
if(this.handle)
this.handle.update();
}
}
class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefined> {
readonly handle: ConnectionHandler;
private current_selected?: AvailableTypes;
private _tag: JQuery<HTMLElement>;
private readonly _tag_info: JQuery<HTMLElement>;
private readonly _tag_banner: JQuery<HTMLElement>;
private _current_manager: InfoManagerBase = undefined;
private managers: InfoManagerBase[] = [];
constructor(client: ConnectionHandler) {
this.handle = client;
this._tag = $("#tmpl_select_info").renderTag();
this._tag_info = this._tag.find(".container-select-info");
this._tag_banner = this._tag.find(".container-banner");
this.managers.push(new MusicInfoManager());
this.managers.push(new ClientInfoManager());
this.managers.push(new ChannelInfoManager());
this.managers.push(new ServerInfoManager());
this._tag.find("button.close").on('click', () => this.close_popover());
}
get_tag() : JQuery {
return this._tag;
}
destroy() {
this._tag && this._tag.remove();
this._tag = undefined;
this.managers = undefined;
this._current_manager = undefined;
this.current_selected = undefined;
}
handle_resize() {
/* test if the popover isn't a popover anymore */
if(this._tag.parent().hasClass('shown')) {
this._tag.parent().removeClass('shown');
if(this.is_popover())
this._tag.parent().addClass('shown');
}
}
setCurrentSelected(entry: AvailableTypes) {
if(this.current_selected == entry) return;
if(this._current_manager) {
(this._current_manager as InfoManager<AvailableTypes>).finalizeFrame(this.current_selected, this._tag_info);
this._current_manager = null;
this.current_selected = null;
}
this._tag_info.empty();
this.current_selected = entry;
for(let manager of this.managers) {
if(manager.available(this.current_selected)) {
this._current_manager = manager;
break;
}
}
console.log(tr("Using info manager: %o"), this._current_manager);
if(this._current_manager)
(this._current_manager as InfoManager<AvailableTypes>).createFrame(this, this.current_selected, this._tag_info);
}
get currentSelected() {
return this.current_selected;
}
update(){
if(this._current_manager && this.current_selected)
(this._current_manager as InfoManager<AvailableTypes>).updateFrame(this.current_selected, this._tag_info);
}
current_manager() { return this._current_manager; }
is_popover() : boolean {
return !this._tag.parent().is(':visible') || this._tag.parent().hasClass('shown');
}
open_popover() {
this._tag.parent().toggleClass('shown', true);
}
close_popover() {
this._tag.parent().toggleClass('shown', false);
}
rendered_tag() {
return this._tag_info;
}
}
interface Window {
Image: typeof HTMLImageElement;
HTMLImageElement: typeof HTMLImageElement;
}
class ClientInfoManager extends InfoManager<ClientEntry> {
available<V>(object: V): boolean {
return typeof object == "object" && object instanceof ClientEntry;
}
createFrame<_>(handle: InfoBar<_>, client: ClientEntry, html_tag: JQuery<HTMLElement>) {
super.createFrame(handle, client, html_tag);
client.updateClientVariables();
this.updateFrame(client, html_tag);
}
updateFrame(client: ClientEntry, html_tag: JQuery<HTMLElement>) {
this.resetIntervals();
html_tag.empty();
let properties = this.buildProperties(client);
let rendered = $("#tmpl_selected_client").renderTag(properties);
html_tag.append(rendered);
this.registerInterval(setInterval(() => {
html_tag.find(".update_onlinetime").text(formatDate(client.calculateOnlineTime()));
}, 1000));
}
buildProperties(client: ClientEntry) : any {
let properties: any = {};
properties["client_name"] = client.createChatTag()[0];
properties["client_onlinetime"] = formatDate(client.calculateOnlineTime());
properties["sound_volume"] = client.get_audio_handle() ? client.get_audio_handle().get_volume() * 100 : -1;
properties["client_is_query"] = client.properties.client_type == ClientType.CLIENT_QUERY;
properties["client_is_web"] = client.properties.client_type_exact == ClientType.CLIENT_WEB;
properties["group_server"] = [];
for(let groupId of client.assignedServerGroupIds()) {
let group = client.channelTree.client.groups.serverGroup(groupId);
if(!group) continue;
let group_property = {};
group_property["group_id"] = groupId;
group_property["group_name"] = group.name;
properties["group_server"].push(group_property);
properties["group_" + groupId + "_icon"] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid);
}
let group = client.channelTree.client.groups.channelGroup(client.assignedChannelGroup());
if(group) {
properties["group_channel"] = group.id;
properties["group_" + group.id + "_name"] = group.name;
properties["group_" + group.id + "_icon"] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid);
}
for(let key in client.properties)
properties["property_" + key] = client.properties[key];
if(client.properties.client_teaforo_id > 0) {
properties["teaspeak_forum"] = $.spawn("a")
.attr("href", "//forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id)
.attr("target", "_blank")
.text(client.properties.client_teaforo_id);
}
if(client.properties.client_flag_avatar && client.properties.client_flag_avatar.length > 0) {
properties["client_avatar"] = client.channelTree.client.fileManager.avatars.generate_client_tag(client);
}
return properties;
}
}
class ServerInfoManager extends InfoManager<ServerEntry> {
createFrame<_>(handle: InfoBar<_>, server: ServerEntry, html_tag: JQuery<HTMLElement>) {
super.createFrame(handle, server, html_tag);
if(server.shouldUpdateProperties()) server.updateProperties();
this.updateFrame(server, html_tag);
}
updateFrame(server: ServerEntry, html_tag: JQuery<HTMLElement>) {
this.resetIntervals();
html_tag.empty();
let properties: any = {};
properties["server_name"] = $.spawn("a").text(server.properties.virtualserver_name);
properties["server_onlinetime"] = formatDate(server.calculateUptime());
properties["server_address"] = server.remote_address.host + (server.remote_address.port == 9987 ? "" : ":" + server.remote_address.port);
const peer_address = server.channelTree.client.serverConnection.remote_address();
properties["server_peer_address"] = peer_address.host + (peer_address.port == 9987 ? "" : ":" + server.remote_address.port);
properties["hidden_clients"] = Math.max(0, server.properties.virtualserver_clientsonline - server.channelTree.clients.length);
for(let key in server.properties)
properties["property_" + key] = server.properties[key];
let rendered = $("#tmpl_selected_server").renderTag([properties]);
this.registerInterval(setInterval(() => {
html_tag.find(".update_onlinetime").text(formatDate(server.calculateUptime()));
}, 1000));
{
const disabled = !server.shouldUpdateProperties();
let requestUpdate = rendered.find(".button-update");
requestUpdate
.prop("disabled", disabled)
.toggleClass('btn-success', !disabled)
.toggleClass('btn-danger', disabled);
requestUpdate.click(() => {
server.updateProperties();
this.triggerUpdate();
});
this.registerTimer(setTimeout(function () {
requestUpdate
.prop("disabled", false)
.toggleClass('btn-success', true)
.toggleClass('btn-danger', false);
}, server.nextInfoRequest - Date.now()));
}
html_tag.append(rendered);
}
available<V>(object: V): boolean {
return typeof object == "object" && object instanceof ServerEntry;
}
}
class ChannelInfoManager extends InfoManager<ChannelEntry> {
createFrame<_>(handle: InfoBar<_>, channel: ChannelEntry, html_tag: JQuery<HTMLElement>) {
super.createFrame(handle, channel, html_tag);
this.updateFrame(channel, html_tag);
}
updateFrame(channel: ChannelEntry, html_tag: JQuery<HTMLElement>) {
this.resetIntervals();
html_tag.empty();
let properties: any = {};
properties["channel_name"] = channel.generate_tag(false);
properties["channel_type"] = ChannelType.normalize(channel.channelType());
properties["channel_clients"] = channel.channelTree.clientsByChannel(channel).length;
properties["channel_subscribed"] = channel.flag_subscribed;
properties["server_encryption"] = channel.channelTree.server.properties.virtualserver_codec_encryption_mode;
for(let key in channel.properties)
properties["property_" + key] = channel.properties[key];
let tag_channel_description = $.spawn("div");
properties["bbcode_channel_description"] = tag_channel_description;
channel.getChannelDescription().then(description => {
const result = xbbcode.parse(description, {});
/*
if(result.error) {
console.log("BBCode parse error: %o", result.errorQueue);
}
*/
tag_channel_description.empty()
.append($.spawn("div").html(result.build_html()).contents())
.css("overflow-y", "auto")
.css("flex-grow", "1");
});
let rendered = $("#tmpl_selected_channel").renderTag([properties]);
html_tag.append(rendered);
}
available<V>(object: V): boolean {
return typeof object == "object" && object instanceof ChannelEntry;
}
}
function format_time(time: number) {
let hours: any = 0, minutes: any = 0, seconds: any = 0;
if(time >= 60 * 60) {
hours = Math.floor(time / (60 * 60));
time -= hours * 60 * 60;
}
if(time >= 60) {
minutes = Math.floor(time / 60);
time -= minutes * 60;
}
seconds = time;
if(hours > 9)
hours = hours.toString();
else if(hours > 0)
hours = '0' + hours.toString();
else hours = '';
if(minutes > 9)
minutes = minutes.toString();
else if(minutes > 0)
minutes = '0' + minutes.toString();
else
minutes = '00';
if(seconds > 9)
seconds = seconds.toString();
else if(seconds > 0)
seconds = '0' + seconds.toString();
else
seconds = '00';
return (hours ? hours + ":" : "") + minutes + ':' + seconds;
}
enum MusicPlayerState {
SLEEPING,
LOADING,
PLAYING,
PAUSED,
STOPPED
}
class MusicInfoManager extends ClientInfoManager {
single_handler: connection.SingleCommandHandler;
createFrame<_>(handle: InfoBar<_>, channel: MusicClientEntry, html_tag: JQuery<HTMLElement>) {
super.createFrame(handle, channel, html_tag);
this.updateFrame(channel, html_tag);
}
updateFrame(bot: MusicClientEntry, html_tag: JQuery<HTMLElement>) {
if(this.single_handler) {
this.handle.handle.serverConnection.command_handler_boss().remove_single_handler(this.single_handler);
this.single_handler = undefined;
}
this.resetIntervals();
html_tag.empty();
let properties = super.buildProperties(bot);
{ //Render info frame
if(bot.properties.player_state < MusicPlayerState.PLAYING) {
properties["music_player"] = $("#tmpl_music_frame_empty").renderTag().css("align-self", "center");
} else {
let frame = $.spawn("div").text(tr("loading...")) as JQuery<HTMLElement>;
properties["music_player"] = frame;
properties["song_url"] = $.spawn("a").text(tr("loading..."));
bot.requestPlayerInfo().then(info => {
let timestamp = Date.now();
console.log(info);
let _frame = $("#tmpl_music_frame").renderTag({
song_name: info.player_title ? info.player_title :
info.song_url ? info.song_url : tr("No title or url"),
song_url: info.song_url,
thumbnail: info.song_thumbnail && info.song_thumbnail.length > 0 ? info.song_thumbnail : undefined
}).css("align-self", "center");
properties["song_url"].text(info.song_url);
frame.replaceWith(_frame);
frame = _frame;
/* Play/Pause logic */
{
let button_play = frame.find(".button_play");
let button_pause = frame.find(".button_pause");
let button_stop = frame.find('.button_stop');
button_play.click(handler => {
if(!button_play.hasClass("active")) {
this.handle.handle.serverConnection.send_command("musicbotplayeraction", {
bot_id: bot.properties.client_database_id,
action: 1
}).then(updated => this.triggerUpdate()).catch(error => {
createErrorModal(tr("Failed to execute play"), MessageHelper.formatMessage(tr("Failed to execute play.<br>{}"), error)).open();
this.triggerUpdate();
});
}
button_pause.show();
button_play.hide();
});
button_pause.click(handler => {
if(!button_pause.hasClass("active")) {
this.handle.handle.serverConnection.send_command("musicbotplayeraction", {
bot_id: bot.properties.client_database_id,
action: 2
}).then(updated => this.triggerUpdate()).catch(error => {
createErrorModal(tr("Failed to execute pause"), MessageHelper.formatMessage(tr("Failed to execute pause.<br>{}"), error)).open();
this.triggerUpdate();
});
}
button_play.show();
button_pause.hide();
});
button_stop.click(handler => {
this.handle.handle.serverConnection.send_command("musicbotplayeraction", {
bot_id: bot.properties.client_database_id,
action: 0
}).then(updated => this.triggerUpdate()).catch(error => {
createErrorModal(tr("Failed to execute stop"), MessageHelper.formatMessage(tr("Failed to execute stop.<br>{}"), error)).open();
this.triggerUpdate();
});
});
if(bot.properties.player_state == 2) {
button_play.hide();
button_pause.show();
} else if(bot.properties.player_state == 3) {
button_pause.hide();
button_play.show();
} else if(bot.properties.player_state == 4) {
button_pause.hide();
button_play.show();
}
}
{ /* Button functions */
_frame.find(".btn-forward").click(() => {
this.handle.handle.serverConnection.send_command("musicbotplayeraction", {
bot_id: bot.properties.client_database_id,
action: 3
}).then(updated => this.triggerUpdate()).catch(error => {
createErrorModal(tr("Failed to execute forward"), tr("Failed to execute pause.<br>{}").format(error)).open();
this.triggerUpdate();
});
});
_frame.find(".btn-rewind").click(() => {
this.handle.handle.serverConnection.send_command("musicbotplayeraction", {
bot_id: bot.properties.client_database_id,
action: 4
}).then(updated => this.triggerUpdate()).catch(error => {
createErrorModal(tr("Failed to execute rewind"),tr( "Failed to execute pause.<br>{}").format(error)).open();
this.triggerUpdate();
});
});
_frame.find(".btn-settings").click(() => {
this.handle.handle.serverConnection.command_helper.request_playlist_list().then(lists => {
for(const entry of lists) {
if(entry.playlist_id == bot.properties.client_playlist_id) {
Modals.spawnPlaylistEdit(bot.channelTree.client, entry);
return;
}
}
createErrorModal(tr("Invalid permissions"), tr("You don't have to see the bots playlist.")).open();
}).catch(error => {
createErrorModal(tr("Failed to query playlist."), tr("Failed to query playlist info.")).open();
});
});
}
/* Required flip card javascript */
frame.find(".right").mouseenter(() => {
frame.find(".controls-overlay").addClass("flipped");
});
frame.find(".right").mouseleave(() => {
frame.find(".controls-overlay").removeClass("flipped");
});
/* Slider */
frame.find(".timeline .slider").on('mousedown', ev => {
let timeline = frame.find(".timeline");
let time = frame.find(".time");
let slider = timeline.find(".slider");
let slider_old = slider.css("margin-left");
let time_max = parseInt(timeline.attr("time-max"));
slider.prop("editing", true);
let target_timestamp = 0;
let move_handler = (event: MouseEvent) => {
let max = timeline.width();
let current = event.pageX - timeline.offset().left - slider.width() / 2;
if(current < 0) current = 0;
else if(current > max) current = max;
target_timestamp = current / max * time_max;
time.text(format_time(Math.floor(target_timestamp / 1000)));
slider.css("margin-left", current / max * 100 + "%");
};
let finish_handler = event => {
console.log("Event (%i | %s): %o", event.button, event.type, event);
if(event.type == "mousedown" && event.button != 2) return;
$(document).unbind("mousemove", move_handler as any);
$(document).unbind("mouseup mouseleave mousedown", finish_handler as any);
if(event.type != "mousedown") {
slider.prop("editing", false);
slider.prop("edited", true);
let current_timestamp = info.player_replay_index + Date.now() - timestamp;
this.handle.handle.serverConnection.send_command("musicbotplayeraction", {
bot_id: bot.properties.client_database_id,
action: current_timestamp > target_timestamp ? 6 : 5,
units: current_timestamp < target_timestamp ? target_timestamp - current_timestamp : current_timestamp - target_timestamp
}).then(() => this.triggerUpdate()).catch(error => {
slider.prop("edited", false);
});
} else { //Restore old
event.preventDefault();
slider.css("margin-left", slider_old + "%");
}
};
$(document).on('mousemove', move_handler as any);
$(document).on('mouseup mouseleave mousedown', finish_handler as any);
ev.preventDefault();
return false;
});
{
frame.find(".timeline").attr("time-max", info.player_max_index);
let timeline = frame.find(".timeline");
let time_bar = timeline.find(".played");
let buffered_bar = timeline.find(".buffered");
let slider = timeline.find(".slider");
let player_time = _frame.find(".player_time");
let update_handler = (played?: number, buffered?: number) => {
let time_index = played || info.player_replay_index + (bot.properties.player_state == 2 ? Date.now() - timestamp : 0);
let buffered_index = buffered || 0;
time_bar.css("width", time_index / info.player_max_index * 100 + "%");
buffered_bar.css("width", buffered_index / info.player_max_index * 100 + "%");
if(!slider.prop("editing") && !slider.prop("edited")) {
player_time.text(format_time(Math.floor(time_index / 1000)));
slider.css("margin-left", time_index / info.player_max_index * 100 + "%");
}
};
let interval = setInterval(update_handler, 1000);
this.registerInterval(interval);
update_handler();
/* register subscription */
this.handle.handle.serverConnection.send_command("musicbotsetsubscription", {bot_id: bot.properties.client_database_id}).catch(error => {
console.error("Failed to subscribe to displayed music bot! Using pseudo timeline");
}).then(() => {
clearInterval(interval);
});
this.single_handler = {
command: "notifymusicstatusupdate",
function: command => {
const json = command.arguments[0];
update_handler(parseInt(json["player_replay_index"]), parseInt(json["player_buffered_index"]));
return false; /* do not unregister me! */
}
};
this.handle.handle.serverConnection.command_handler_boss().register_single_handler(this.single_handler);
}
});
}
const player = properties["music_player"];
const player_transformer = $.spawn("div").append(player);
player_transformer.css({
'display': 'block',
//'width': "100%",
'height': '100%'
});
properties["music_player"] = player_transformer;
}
let rendered = $("#tmpl_selected_music").renderTag([properties]);
html_tag.append(rendered);
{
const player = properties["music_player"] as JQuery;
const player_width = 400; //player.width();
const player_height = 400; //player.height();
const parent = player.parent();
parent.css({
'flex-grow': 1,
'display': 'flex',
'flex-direction': 'row',
'justify-content': 'space-around',
});
const padding = 14;
const scale_x = Math.min((parent.width() - padding) / player_width, 1.5);
const scale_y = Math.min((parent.height() - padding) / player_height, 1.5);
let scale = Math.min(scale_x, scale_y);
let translate_x = 50, translate_y = 50;
if(scale_x == scale_y && scale_x == scale) {
//Equal scale
} else if(scale_x == scale) {
//We scale on the x-Axis
//We have to adjust the y-Axis
} else {
//We scale on the y-Axis
//We have to adjust the x-Axis
}
//1 => 0 | 0
//1.5 => 0 | 25
//0.5 => 0 | -25
//const translate_x = scale_x != scale ? 0 : undefined || 50 - (50 * ((parent.width() - padding) / player_width));
//const translate_y = scale_y != scale || scale_y > 1 ? 0 : undefined || 50 - (50 * ((parent.height() - padding) / player_height));
const transform = ("translate(0%, " + (scale * 50 - 50) + "%) scale(" + scale.toPrecision(2) + ")");
console.log(tr("Parents: %o | %o"), parent.width(), parent.height());
console.log(tr("Player: %o | %o"), player_width, player_height);
console.log(tr("Scale: %f => translate: %o | %o"), scale, translate_x, translate_y);
player.css({
transform: transform
});
console.log("Transform: " + transform);
}
this.registerInterval(setInterval(() => {
html_tag.find(".update_onlinetime").text(formatDate(bot.calculateOnlineTime()));
}, 1000));
}
update_local_volume(volume: number) {
this.handle.rendered_tag().find(".property-volume-local").text(Math.floor(volume * 100) + "%");
}
update_remote_volume(volume: number) {
this.handle.rendered_tag().find(".property-volume-remote").text(Math.floor(volume * 100) + "%")
}
available<V>(object: V): boolean {
return typeof object == "object" && object instanceof MusicClientEntry;
}
finalizeFrame(object: ClientEntry, frame: JQuery<HTMLElement>) {
if(this.single_handler) {
this.handle.handle.serverConnection.command_handler_boss().remove_single_handler(this.single_handler);
this.single_handler = undefined;
}
super.finalizeFrame(object, frame);
}
}

View File

@ -5,6 +5,7 @@ enum ChatType {
CLIENT
}
declare const xbbcode: any;
namespace MessageHelper {
export function htmlEscape(message: string) : string[] {
const div = document.createElement('div');
@ -90,7 +91,7 @@ namespace MessageHelper {
}
export function bbcode_chat(message: string) : JQuery[] {
const result: xbbcode.Result = xbbcode.parse(message, {
const result = xbbcode.parse(message, {
/* TODO make this configurable and allow IMG */
tag_whitelist: [
"b", "big",
@ -133,6 +134,123 @@ namespace MessageHelper {
//return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? "&nbsp;" : entry));
}
export namespace network {
export const KB = 1024;
export const MB = 1024 * KB;
export const GB = 1024 * MB;
export const TB = 1024 * GB;
export function format_bytes(value: number, options?: {
time?: string,
unit?: string,
exact?: boolean
}) : string {
options = Object.assign(options || {}, {
time: undefined,
unit: "Bytes",
exact: true
});
let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
let v, unit;
if(value > 2 * TB) {
unit = "TB";
v = value / TB;
} else if(value > GB) {
unit = "GB";
v = value / GB;
} else if(value > MB) {
unit = "MB";
v = value / MB;
} else if(value > KB) {
unit = "KB";
v = value / KB;
} else {
unit = "";
v = value;
}
if(unit && options.time)
unit = unit + "/" + options.time;
return (options.exact || !unit ? (points + " " + (options.unit || "")) : "") + (unit ? (" / " + v.toFixed(2) + " " + unit) : "");
}
}
export const K = 1000;
export const M = 1000 * K;
export const G = 1000 * M;
export const T = 1000 * G;
export function format_number(value: number, options?: {
time?: string,
unit?: string
}) {
options = Object.assign(options || {}, {});
let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
let v, unit;
if(value > 2 * T) {
unit = "T";
v = value / T;
} else if(value > G) {
unit = "G";
v = value / G;
} else if(value > M) {
unit = "M";
v = value / M;
} else if(value > K) {
unit = "K";
v = value / K;
} else {
unit = "";
v = value;
}
if(unit && options.time)
unit = unit + "/" + options.time;
return points + " " + (options.unit || "") + (unit ? (" / " + v.toFixed(2) + " " + unit) : "");
}
export const TIME_SECOND = 1000;
export const TIME_MINUTE = 60 * TIME_SECOND;
export const TIME_HOUR = 60 * TIME_MINUTE;
export const TIME_DAY = 24 * TIME_HOUR;
export const TIME_WEEK = 7 * TIME_DAY;
export function format_time(time: number, default_value: string) {
let result = "";
if(time > TIME_WEEK) {
const amount = Math.floor(time / TIME_WEEK);
result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week"));
time -= amount * TIME_WEEK;
}
if(time > TIME_DAY) {
const amount = Math.floor(time / TIME_DAY);
result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day"));
time -= amount * TIME_DAY;
}
if(time > TIME_HOUR) {
const amount = Math.floor(time / TIME_HOUR);
result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour"));
time -= amount * TIME_HOUR;
}
if(time > TIME_MINUTE) {
const amount = Math.floor(time / TIME_MINUTE);
result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute"));
time -= amount * TIME_MINUTE;
}
if(time > TIME_SECOND) {
const amount = Math.floor(time / TIME_SECOND);
result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second"));
time -= amount * TIME_SECOND;
}
return result.length > 0 ? result.substring(1) : default_value;
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "XBBCode code tag init",
function: async () => {
@ -141,7 +259,7 @@ namespace MessageHelper {
tag: ["code", "icode", "i-code"],
content_tags_whitelist: [],
build_html(layer: xbbcode.TagLayer) : string {
build_html(layer) : string {
const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
const language = (layer.options || "").replace("\"", "'").toLowerCase();

View File

@ -1,7 +1,6 @@
/* the bar on the right with the chats (Channel & Client) */
namespace chat {
/* Fix some declare issues... */
import instantiate = WebAssembly.instantiate;
import Event = JQuery.Event;
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
@ -20,6 +19,8 @@ namespace chat {
private _value_ping: JQuery;
private _ping_updater: number;
private _button_conversation: HTMLElement;
constructor(handle: Frame) {
this.handle = handle;
this._build_html_tag();
@ -44,6 +45,20 @@ namespace chat {
this._html_tag.find(".button-switch-chat-channel").on('click', () => this.handle.show_channel_conversations());
this._value_ping = this._html_tag.find(".value-ping");
this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations());
this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => {
const selected_client = this.handle.client_info().current_client();
if(!selected_client) return;
const conversation = selected_client ? this.handle.private_conversations().find_conversation({
name: selected_client.properties.client_nickname,
unique_id: selected_client.properties.client_unique_identifier,
client_id: selected_client.clientId()
}, { create: true, attach: true }) : undefined;
if(!conversation) return;
this.handle.private_conversations().set_selected_conversation(conversation);
this.handle.show_private_conversations();
})[0];
}
update_ping() {
@ -115,7 +130,7 @@ namespace chat {
const current_channel_id = channel_tree ? this.handle.channel_conversations().current_channel() : 0;
const channel = channel_tree ? channel_tree.findChannel(current_channel_id) : undefined;
const tag_container = this._html_tag.find(".mode-channel_chat");
const tag_container = this._html_tag.find(".mode-channel_chat.channel");
const html_tag_title = tag_container.find(".title");
const html_tag = tag_container.find(".value-text-channel");
const html_limit_tag = tag_container.find(".value-text-limit");
@ -179,12 +194,31 @@ namespace chat {
}
set_mode(mode: InfoFrameMode) {
if(this._mode === mode)
return;
if(this._mode !== mode) {
this._mode = mode;
this._html_tag.find(".mode-based").hide();
this._html_tag.find(".mode-" + mode).show();
}
if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) {
//Will be called every time a client is shown
const selected_client = this.handle.client_info().current_client();
const conversation = selected_client ? this.handle.private_conversations().find_conversation({
name: selected_client.properties.client_nickname,
unique_id: selected_client.properties.client_unique_identifier,
client_id: selected_client.clientId()
}, { create: false, attach: false }) : undefined;
const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.clientId) ? "visible" : "hidden";
if(this._button_conversation.style.visibility !== visibility)
this._button_conversation.style.visibility = visibility;
if(conversation) {
this._button_conversation.innerText = tr("Open conversation");
} else {
this._button_conversation.innerText = tr("Start a conversation");
}
}
}
}
class ChatBox {
@ -195,6 +229,11 @@ namespace chat {
private __callback_key_down;
private __callback_paste;
private _typing_timeout: number; /* ID when the next callback_typing will be called */
private _typing_last_event: number; /* timestamp of the last typing event */
typing_interval: number = 2000; /* update frequency */
callback_typing: () => any;
callback_text: (text: string) => any;
constructor() {
@ -216,15 +255,18 @@ namespace chat {
this._html_tag = undefined;
this._html_input = undefined;
clearTimeout(this._typing_timeout);
this.__callback_text_changed = undefined;
this.__callback_key_down = undefined;
this.__callback_paste = undefined;
this.callback_text = undefined;
this.callback_typing = undefined;
}
private _initialize_listener() {
this._html_input.on("cut paste drop key keyup", (event) => this.__callback_text_changed(event));
this._html_input.on("cut paste drop keydown keyup", (event) => this.__callback_text_changed(event));
this._html_input.on("change", this.__callback_text_changed);
this._html_input.on("keydown", this.__callback_key_down);
this._html_input.on("paste", this.__callback_paste);
@ -246,7 +288,7 @@ namespace chat {
});
}
private _callback_text_changed(event) {
private _callback_text_changed(event: Event) {
if(event && event.isDefaultPrevented())
return;
@ -254,6 +296,28 @@ namespace chat {
const text = this._html_input[0];
text.style.height = "1em";
text.style.height = text.scrollHeight + 'px';
if(!event || (event.type !== "keydown" && event.type !== "keyup" && event.type !== "change"))
return;
this._typing_last_event = Date.now();
if(this._typing_timeout)
return;
const _trigger_typing = (last_time: number) => {
if(this._typing_last_event <= last_time) {
this._typing_timeout = 0;
return;
}
try {
if(this.callback_typing)
this.callback_typing();
} finally {
this._typing_timeout = setTimeout(_trigger_typing, this.typing_interval, this._typing_last_event);
}
};
_trigger_typing(0); /* We def want that*/
}
private _text(element: HTMLElement) {
@ -331,8 +395,14 @@ namespace chat {
this.callback_text(helpers.preprocess_chat_message(text));
}
if(this._typing_timeout)
clearTimeout(this._typing_timeout);
this._typing_timeout = 1; /* enforce no typing update while sending */
this._html_input.text("");
setTimeout(() => this.__callback_text_changed());
setTimeout(() => {
this.__callback_text_changed();
this._typing_timeout = 0; /* enable text change listener again */
});
}
}
@ -392,7 +462,7 @@ namespace chat {
}
namespace md2bbc {
type RemarkToken = {
export type RemarkToken = {
type: string;
tight: boolean;
lines: number[];
@ -764,11 +834,11 @@ test
}
}
type PrivateConversationViewEntry = {
export type PrivateConversationViewEntry = {
html_tag: JQuery;
}
type PrivateConversationMessageData = {
export type PrivateConversationMessageData = {
message_id: string;
message: string;
sender: "self" | "partner";
@ -780,10 +850,10 @@ test
timestamp: number;
};
type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & {
export type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & {
time_update_id: number;
};
type PrivateConversationViewSpacer = PrivateConversationViewEntry;
export type PrivateConversationViewSpacer = PrivateConversationViewEntry;
export enum PrivateConversationState {
OPEN,
@ -791,7 +861,7 @@ test
DISCONNECTED,
}
type DisplayedMessage = {
export type DisplayedMessage = {
timestamp: number;
message: PrivateConversationViewMessage | PrivateConversationViewEntry;
@ -817,6 +887,9 @@ test
private _state: PrivateConversationState;
private _last_message_updater_id: number;
private _last_typing: number = 0;
private _typing_timeout: number = 4000;
private _typing_timeout_task: number;
_scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */
_html_message_container: JQuery; /* only set when this chat is selected! */
@ -888,6 +961,8 @@ test
this._html_entry_tag = undefined;
this._message_history = undefined;
if(this._typing_timeout_task)
clearTimeout(this._typing_timeout_task);
}
private _2d_flat<T>(array: T[][]) : T[] {
@ -943,7 +1018,20 @@ test
while(this._message_history.length > 100)
this._message_history.pop();
}
if(sender.type === "partner") {
clearTimeout(this._typing_timeout_task);
this._typing_timeout_task = 0;
if(this.typing_active()) {
this._last_typing = 0;
this.typing_expired();
} else {
this._update_message_timestamp();
}
} else {
this._update_message_timestamp();
}
if(typeof(save_history) !== "boolean" || save_history)
this.save_history();
@ -1039,6 +1127,15 @@ test
private _update_message_timestamp() {
if(this._last_message_updater_id)
clearTimeout(this._last_message_updater_id);
if(!this._html_entry_tag)
return; /* we got deleted, not need for updates */
if(this.typing_active()) {
this._html_entry_tag.find(".last-message").text(tr("currently typing..."));
return;
}
const last_message = this._message_history[0];
if(!last_message) {
this._html_entry_tag.find(".last-message").text(tr("no history"));
@ -1312,9 +1409,10 @@ test
if(this._state == state)
return;
if(state == PrivateConversationState.DISCONNECTED)
if(state == PrivateConversationState.DISCONNECTED) {
this._append_state_change("disconnect");
else if(state == PrivateConversationState.OPEN && this._state != PrivateConversationState.CLOSED)
this.client_id = 0;
} else if(state == PrivateConversationState.OPEN && this._state != PrivateConversationState.CLOSED)
this._append_state_change("reconnect");
else if(state == PrivateConversationState.CLOSED)
this._append_state_change("closed");
@ -1355,6 +1453,31 @@ test
});
}
}
private typing_expired() {
this._update_message_timestamp();
if(this.handle.current_conversation() === this)
this.handle.update_typing_state();
}
trigger_typing() {
let _new = Date.now() - this._last_typing > this._typing_timeout;
this._last_typing = Date.now();
if(this._typing_timeout_task)
clearTimeout(this._typing_timeout_task);
if(_new)
this._update_message_timestamp();
if(this.handle.current_conversation() === this)
this.handle.update_typing_state();
this._typing_timeout_task = setTimeout(() => this.typing_expired(), this._typing_timeout);
}
typing_active() {
return Date.now() - this._last_typing < this._typing_timeout;
}
}
export class PrivateConverations {
@ -1365,6 +1488,7 @@ test
private _container_conversation: JQuery;
private _container_conversation_messages: JQuery;
private _container_conversation_list: JQuery;
private _container_typing: JQuery;
private _html_no_chats: JQuery;
private _conversations: PrivateConveration[] = [];
@ -1378,12 +1502,29 @@ test
this._build_html_tag();
this.update_chatbox_state();
this.update_typing_state();
this._chat_box.callback_text = message => {
if(!this._current_conversation) {
console.warn(tr("Dropping conversation message because of no active conversation."));
return;
}
this._current_conversation.call_message(message);
};
this._chat_box.callback_typing = () => {
if(!this._current_conversation) {
console.warn(tr("Dropping conversation typing action because of no active conversation."));
return;
}
console.log("TYPING!");
const connection = this.handle.handle.serverConnection;
if(!connection || !connection.connected())
return;
connection.send_command("clientchatcomposing", {
clid: this._current_conversation.client_id
});
}
}
@ -1408,6 +1549,8 @@ test
}
current_conversation() : PrivateConveration | undefined { return this._current_conversation; }
conversations() : PrivateConveration[] { return this._conversations; }
create_conversation(client_uid: string, client_name: string, client_id: number) : PrivateConveration {
const conv = new PrivateConveration(this, client_uid, client_name, client_id);
@ -1523,8 +1666,14 @@ test
this._chat_box.set_enabled(!!this._current_conversation && this._current_conversation.chat_enabled());
}
update_typing_state() {
this._container_typing.toggleClass("hidden", !this._current_conversation || !this._current_conversation.typing_active());
}
private _build_html_tag() {
this._html_tag = $("#tmpl_frame_chat_private").renderTag().dividerfy();
this._html_tag = $("#tmpl_frame_chat_private").renderTag({
chatbox: this._chat_box.html_tag()
}).dividerfy();
this._container_conversation = this._html_tag.find(".conversation");
this._container_conversation.on('click', event => { /* lets think if a user clicks within that field that he has read the messages */
if(this._current_conversation)
@ -1542,10 +1691,10 @@ test
else
this._current_conversation._scroll_position = this._container_conversation_messages[0].scrollTop;
});
this._container_conversation.find(".chatbox").append(this._chat_box.html_tag());
this._container_conversation_list = this._html_tag.find(".conversation-list");
this._html_no_chats = this._container_conversation_list.find(".no-chats");
this._container_typing = this._html_tag.find(".container-typing");
}
try_input_focus() {
@ -1637,7 +1786,17 @@ test
} else {
this._scroll_position = this._container_messages[0].scrollTop;
}
this._container_new_message.toggleClass("shown",!!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position);
const will_visible = !!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position;
const is_visible = this._container_new_message[0].classList.contains("shown");
if(!is_visible && will_visible)
this._container_new_message[0].classList.add("shown");
if(is_visible && !will_visible)
this._container_new_message[0].classList.remove("shown");
//This causes a Layout recalc (Forced reflow)
//this._container_new_message.toggleClass("shown",!!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position);
});
this._view_older_messages = this._generate_view_spacer(tr("Load older messages"), "old");
@ -2147,6 +2306,12 @@ test
this.handle.set_content(this.previous_frame_content);
});
this._html_tag.find(".button-more").on('click', () => {
if(!this._current_client)
return;
Modals.openClientInfo(this._current_client);
});
this._html_tag.find('.container-avatar-edit').on('click', () => this.handle.handle.update_avatar());
}
@ -2221,7 +2386,7 @@ test
}
const volume = this._html_tag.find(".client-local-volume");
volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1) + "%");
volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1).toFixed(0) + "%");
}
/* teaspeak forum */
@ -2471,6 +2636,7 @@ test
show_client_info(client: ClientEntry) {
this._client_info.set_current_client(client);
this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */
if(this._content_type === FrameContent.CLIENT_INFO)
return;
@ -2479,7 +2645,6 @@ test
this._clear();
this._content_type = FrameContent.CLIENT_INFO;
this._container_chat.append(this._client_info.html_tag());
this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO);
}
set_content(type: FrameContent) {

View File

@ -8,7 +8,6 @@ class ServerConnectionManager {
private _container_log_server: JQuery;
private _container_channel_tree: JQuery;
private _container_hostbanner: JQuery;
private _container_select_info: JQuery;
private _container_chat: JQuery;
private _tag: JQuery;
@ -34,7 +33,6 @@ class ServerConnectionManager {
this._container_log_server = $("#server-log");
this._container_channel_tree = $("#channelTree");
this._container_hostbanner = $("#hostbanner");
this._container_select_info = $("#select_info");
this._container_chat = $("#chat");
this.set_active_connection_handler(undefined);
@ -75,11 +73,8 @@ class ServerConnectionManager {
if(handler && this.connection_handlers.indexOf(handler) == -1)
throw "Handler hasn't been registered or is already obsolete!";
if(this.active_handler)
this.active_handler.select_info.close_popover();
this._tag_connection_entries.find(".active").removeClass("active");
this._container_channel_tree.children().detach();
this._container_select_info.children().detach();
this._container_chat.children().detach();
this._container_log_server.children().detach();
this._container_hostbanner.children().detach();
@ -90,7 +85,6 @@ class ServerConnectionManager {
this._container_hostbanner.append(handler.hostbanner.html_tag);
this._container_channel_tree.append(handler.channelTree.tag_tree());
this._container_select_info.append(handler.select_info.get_tag());
this._container_chat.append(handler.side_bar.html_tag());
this._container_log_server.append(handler.log.html_tag());

View File

@ -68,20 +68,14 @@ class Hostbanner {
this.html_tag.attr('title', server ? server.properties.virtualserver_hostbanner_url : undefined);
}
private async generate_tag?() : Promise<JQuery | undefined> {
if(!this.client.connected)
return undefined;
public static async generate_tag(banner_url: string | undefined, gfx_interval: number, mode: number) : Promise<JQuery | undefined> {
if(!banner_url) return undefined;
const server = this.client.channelTree.server;
if(!server) return undefined;
if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined;
let banner_url = server.properties.virtualserver_hostbanner_gfx_url;
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) {
const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60);
if(gfx_interval > 0) {
const update_interval = Math.max(gfx_interval, 60);
const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString();
try {
const url = new URL(server.properties.virtualserver_hostbanner_gfx_url);
const url = new URL(banner_url);
if(url.search.length == 0)
banner_url += "?_ts=" + update_timestamp;
else
@ -90,8 +84,6 @@ class Hostbanner {
console.warn(tr("Failed to parse banner URL: %o. Using default '&' append."), error);
banner_url += "&_ts=" + update_timestamp;
}
this.updater = setTimeout(() => this.update(), update_interval * 1000);
}
/* first now load the image */
@ -102,11 +94,27 @@ class Hostbanner {
image_element.src = banner_url;
image_element.style.display = 'none';
document.body.append(image_element);
console.log("Loading image!");
console.log("Loading hostbanner image!");
});
image_element.parentNode.removeChild(image_element);
image_element.style.display = 'unset';
return $.spawn("div").addClass("hostbanner-image-container hostbanner-mode-" + server.properties.virtualserver_hostbanner_mode).append($(image_element));
return $.spawn("div").addClass("hostbanner-image-container hostbanner-mode-" + mode).append($(image_element));
}
private async generate_tag?() : Promise<JQuery | undefined> {
if(!this.client.connected)
return undefined;
const server = this.client.channelTree.server;
if(!server) return undefined;
if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined;
const timeout = server.properties.virtualserver_hostbanner_gfx_interval;
const tag = Hostbanner.generate_tag(server.properties.virtualserver_hostbanner_gfx_url, server.properties.virtualserver_hostbanner_gfx_interval, server.properties.virtualserver_hostbanner_mode);
if(timeout > 0)
this.updater = setTimeout(() => this.update(), timeout * 1000);
return tag;
}
}

View File

@ -251,8 +251,8 @@ namespace log {
"channel_delete": event.ChannelDelete;
}
type MessageBuilderOptions = {};
type MessageBuilder<T extends keyof server.TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined;
export type MessageBuilderOptions = {};
export type MessageBuilder<T extends keyof server.TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined;
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
"error_custom": (data: event.ErrorCustom, options) => {

View File

@ -182,7 +182,7 @@ namespace htmltags {
const origin_url = xbbcode.register.find_parser('url');
xbbcode.register.register_parser({
tag: 'url',
build_html_tag_open(layer: xbbcode.TagLayer): string {
build_html_tag_open(layer): string {
if(layer.options) {
if(layer.options.match(url_channel_regex)) {
const groups = url_channel_regex.exec(layer.options);
@ -205,7 +205,7 @@ namespace htmltags {
}
return origin_url.build_html_tag_open(layer);
},
build_html_tag_close(layer: xbbcode.TagLayer): string {
build_html_tag_close(layer): string {
if(layer.options) {
if(layer.options.match(url_client_regex))
return "</div>";

View File

@ -0,0 +1,523 @@
namespace Modals {
type InfoUpdateCallback = (info: ClientConnectionInfo) => any;
export function openClientInfo(client: ClientEntry) {
let modal: Modal;
let update_callbacks: InfoUpdateCallback[] = [];
modal = createModal({
header: tr("Profile Information: ") + client.clientNickName(),
body: () => {
const template = $("#tmpl_client_info").renderTag();
/* the tab functionality */
{
const container_tabs = template.find(".container-categories");
container_tabs.find(".categories .entry").on('click', event => {
const entry = $(event.target);
container_tabs.find(".bodies > .body").addClass("hidden");
container_tabs.find(".categories > .selected").removeClass("selected");
entry.addClass("selected");
container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden");
});
container_tabs.find(".entry").first().trigger('click');
}
apply_static_info(client, template, modal, update_callbacks);
apply_client_status(client, template, modal, update_callbacks);
apply_basic_info(client, template.find(".container-basic"), modal, update_callbacks);
apply_groups(client, template.find(".container-groups"), modal, update_callbacks);
apply_packets(client, template.find(".container-packets"), modal, update_callbacks);
tooltip(template);
return template.children();
},
footer: null
});
const updater = setInterval(() => {
client.request_connection_info().then(info => update_callbacks.forEach(e => e(info)));
}, 1000);
modal.htmlTag.find(".modal-body").addClass("modal-client-info");
modal.open();
modal.close_listener.push(() => clearInterval(updater));
}
const TIME_SECOND = 1000;
const TIME_MINUTE = 60 * TIME_SECOND;
const TIME_HOUR = 60 * TIME_MINUTE;
const TIME_DAY = 24 * TIME_HOUR;
const TIME_WEEK = 7 * TIME_DAY;
function format_time(time: number, default_value: string) {
let result = "";
if(time > TIME_WEEK) {
const amount = Math.floor(time / TIME_WEEK);
result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week"));
time -= amount * TIME_WEEK;
}
if(time > TIME_DAY) {
const amount = Math.floor(time / TIME_DAY);
result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day"));
time -= amount * TIME_DAY;
}
if(time > TIME_HOUR) {
const amount = Math.floor(time / TIME_HOUR);
result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour"));
time -= amount * TIME_HOUR;
}
if(time > TIME_MINUTE) {
const amount = Math.floor(time / TIME_MINUTE);
result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute"));
time -= amount * TIME_MINUTE;
}
if(time > TIME_SECOND) {
const amount = Math.floor(time / TIME_SECOND);
result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second"));
time -= amount * TIME_SECOND;
}
return result.length > 0 ? result.substring(1) : default_value;
}
function apply_static_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
tag.find(".container-avatar").append(
client.channelTree.client.fileManager.avatars.generate_chat_tag({database_id: client.properties.client_database_id, id: client.clientId()}, client.properties.client_unique_identifier)
);
tag.find(".container-name").append(
client.createChatTag()
);
tag.find(".client-description").text(
client.properties.client_description
);
}
function apply_client_status(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
tag.find(".status-output-disabled").toggle(!client.properties.client_output_hardware);
tag.find(".status-input-disabled").toggle(!client.properties.client_input_hardware);
tag.find(".status-output-muted").toggle(client.properties.client_output_muted);
tag.find(".status-input-muted").toggle(client.properties.client_input_muted);
tag.find(".status-away").toggle(client.properties.client_away);
if(client.properties.client_away_message) {
tag.find(".container-away-message").show().find("a").text(client.properties.client_away_message);
} else {
tag.find(".container-away-message").hide();
}
}
function apply_basic_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* Unique ID */
{
const container = tag.find(".property-unique-id");
container.find(".value a").text(client.clientUid());
container.find(".value-dbid").text(client.properties.client_database_id);
container.find(".button-copy").on('click', event => {
copy_to_clipboard(client.clientUid());
createInfoModal(tr("Unique ID copied"), tr("The unique id has been copied to your clipboard!")).open();
});
}
/* TeaForo */
{
const container = tag.find(".property-teaforo .value").empty();
if(client.properties.client_teaforo_id) {
container.children().remove();
let text = client.properties.client_teaforo_name;
if((client.properties.client_teaforo_flags & 0x01) > 0)
text += " (" + tr("Banned") + ")";
if((client.properties.client_teaforo_flags & 0x02) > 0)
text += " (" + tr("Stuff") + ")";
if((client.properties.client_teaforo_flags & 0x04) > 0)
text += " (" + tr("Premium") + ")";
$.spawn("a")
.attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id)
.attr("target", "_blank")
.text(text)
.appendTo(container);
} else {
container.append($.spawn("a").text(tr("Not connected")));
}
}
/* Version */
{
const container = tag.find(".property-version");
let version_full = client.properties.client_version;
let version = version_full.substring(0, version_full.indexOf(" "));
container.find(".value").empty().append(
$.spawn("a").attr("title", version_full).text(version),
$.spawn("a").addClass("a-on").text("on"),
$.spawn("a").text(client.properties.client_platform)
);
const container_timestamp = container.find(".container-tooltip");
let timestamp = -1;
version_full.replace(/\[build: ?([0-9]+)]/gmi, (group, ts) => {
timestamp = parseInt(ts);
return "";
});
if(timestamp > 0) {
container_timestamp.find(".value-timestamp").text(moment(timestamp * 1000).format('MMMM Do YYYY, h:mm:ss a'));
container_timestamp.show();
} else {
container_timestamp.hide();
}
}
/* Country */
{
const container = tag.find(".property-country");
container.find(".value").empty().append(
$.spawn("div").addClass("country flag-" + client.properties.client_country.toLowerCase()),
$.spawn("a").text(i18n.country_name(client.properties.client_country, tr("Unknown")))
);
}
/* IP Address */
{
const container = tag.find(".property-ip");
const value = container.find(".value a");
value.text(tr("loading..."));
container.find(".button-copy").on('click', event => {
copy_to_clipboard(value.text());
createInfoModal(tr("Client IP copied"), tr("The client IP has been copied to your clipboard!")).open();
});
callbacks.push(info => {
value.text(info.connection_client_ip ? (info.connection_client_ip + ":" + info.connection_client_port) : tr("Hidden"));
});
}
/* first connected */
{
const container = tag.find(".property-first-connected");
container.find(".value a").text(tr("loading..."));
client.updateClientVariables().then(() => {
container.find(".value a").text(moment(client.properties.client_created * 1000).format('MMMM Do YYYY, h:mm:ss a'));
}).catch(error => {
container.find(".value a").text(tr("error"));
});
}
/* connect count */
{
const container = tag.find(".property-connect-count");
container.find(".value a").text(tr("loading..."));
client.updateClientVariables().then(() => {
container.find(".value a").text(client.properties.client_totalconnections);
}).catch(error => {
container.find(".value a").text(tr("error"));
});
}
/* Online since */
{
const container = tag.find(".property-online-since");
const node = container.find(".value a")[0];
if(node) {
const update = () => {
node.innerText = format_time(client.calculateOnlineTime() * 1000, tr("0 Seconds"));
};
callbacks.push(update); /* keep it in sync with all other updates. Else it looks wired */
update();
}
}
/* Idle time */
{
const container = tag.find(".property-idle-time");
const node = container.find(".value a")[0];
if(node) {
callbacks.push(info => {
node.innerText = format_time(info.connection_idle_time, tr("Currently active"));
});
node.innerText = tr("loading...");
}
}
/* ping */
{
const container = tag.find(".property-ping");
const node = container.find(".value a")[0];
if(node) {
callbacks.push(info => {
if(info.connection_ping >= 0)
node.innerText = info.connection_ping.toFixed(0) + "ms ± " + info.connection_ping_deviation.toFixed(2) + "ms";
else if(info.connection_ping == -1 && info.connection_ping_deviation == -1)
node.innerText = tr("Not calculated");
else
node.innerText = tr("loading...");
});
node.innerText = tr("loading...");
}
}
}
function apply_groups(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* server groups */
{
const container_entries = tag.find(".entries");
const container_empty = tag.find(".container-default-groups");
const update_groups = () => {
container_entries.empty();
container_empty.show();
for(const group_id of client.assignedServerGroupIds()) {
if(group_id == client.channelTree.server.properties.virtualserver_default_server_group)
continue;
const group = client.channelTree.client.groups.serverGroup(group_id);
if(!group) continue; //This shall never happen!
container_empty.hide();
container_entries.append($.spawn("div").addClass("entry").append(
client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid),
$.spawn("a").addClass("name").text(group.name + " (" + group.id + ")"),
$.spawn("div").addClass("button-delete").append(
$.spawn("div").addClass("icon_em client-delete").attr("title", tr("Delete group")).on('click', event => {
client.channelTree.client.serverConnection.send_command("servergroupdelclient", {
sgid: group.id,
cldbid: client.properties.client_database_id
}).then(result => update_groups());
})
).toggleClass("visible",
client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberRemovePower) ||
client.clientId() == client.channelTree.client.getClientId() && client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_SELF_REMOVE_POWER).granted(group.requiredMemberRemovePower)
)
))
}
};
tag.find(".button-group-add").on('click', event => {
Modals.createServerGroupAssignmentModal(client, (group, flag) => {
if(flag) {
return client.channelTree.client.serverConnection.send_command("servergroupaddclient", {
sgid: group.id,
cldbid: client.properties.client_database_id
}).then(result => { update_groups(); return true; });
} else
return client.channelTree.client.serverConnection.send_command("servergroupdelclient", {
sgid: group.id,
cldbid: client.properties.client_database_id
}).then(result => { update_groups(); return true; });
});
});
update_groups();
}
}
function apply_packets(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) {
/* Packet Loss */
{
const container = tag.find(".statistic-packet-loss");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
node_downstream.innerText = info.connection_server2client_packetloss_control < 0 ? tr("Not calculated") : (info.connection_server2client_packetloss_control || 0).toFixed();
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
node_upstream.innerText = info.connection_client2server_packetloss_total < 0 ? tr("Not calculated") : (info.connection_client2server_packetloss_total || 0).toFixed();
});
node_upstream.innerText = tr("loading...");
}
}
/* Packets transmitted */
{
const container = tag.find(".statistic-transmitted-packets");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let packets = 0;
packets += info.connection_packets_received_speech > 0 ? info.connection_packets_received_speech : 0;
packets += info.connection_packets_received_control > 0 ? info.connection_packets_received_control : 0;
packets += info.connection_packets_received_keepalive > 0 ? info.connection_packets_received_keepalive : 0;
if(packets == 0 && info.connection_packets_received_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = MessageHelper.format_number(packets, {unit: "Packets"});
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let packets = 0;
packets += info.connection_packets_sent_speech > 0 ? info.connection_packets_sent_speech : 0;
packets += info.connection_packets_sent_control > 0 ? info.connection_packets_sent_control : 0;
packets += info.connection_packets_sent_keepalive > 0 ? info.connection_packets_sent_keepalive : 0;
if(packets == 0 && info.connection_packets_sent_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = MessageHelper.format_number(packets, {unit: "Packets"});
});
node_upstream.innerText = tr("loading...");
}
}
/* Bytes transmitted */
{
const container = tag.find(".statistic-transmitted-bytes");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bytes_received_speech > 0 ? info.connection_bytes_received_speech : 0;
bytes += info.connection_bytes_received_control > 0 ? info.connection_bytes_received_control : 0;
bytes += info.connection_bytes_received_keepalive > 0 ? info.connection_bytes_received_keepalive : 0;
if(bytes == 0 && info.connection_bytes_received_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = MessageHelper.network.format_bytes(bytes);
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bytes_sent_speech > 0 ? info.connection_bytes_sent_speech : 0;
bytes += info.connection_bytes_sent_control > 0 ? info.connection_bytes_sent_control : 0;
bytes += info.connection_bytes_sent_keepalive > 0 ? info.connection_bytes_sent_keepalive : 0;
if(bytes == 0 && info.connection_bytes_sent_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = MessageHelper.network.format_bytes(bytes);
});
node_upstream.innerText = tr("loading...");
}
}
/* Bandwidth second */
{
const container = tag.find(".statistic-bandwidth-second");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_received_last_second_speech > 0 ? info.connection_bandwidth_received_last_second_speech : 0;
bytes += info.connection_bandwidth_received_last_second_control > 0 ? info.connection_bandwidth_received_last_second_control : 0;
bytes += info.connection_bandwidth_received_last_second_keepalive > 0 ? info.connection_bandwidth_received_last_second_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_received_last_second_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"});
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_sent_last_second_speech > 0 ? info.connection_bandwidth_sent_last_second_speech : 0;
bytes += info.connection_bandwidth_sent_last_second_control > 0 ? info.connection_bandwidth_sent_last_second_control : 0;
bytes += info.connection_bandwidth_sent_last_second_keepalive > 0 ? info.connection_bandwidth_sent_last_second_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_sent_last_second_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"});
});
node_upstream.innerText = tr("loading...");
}
}
/* Bandwidth minute */
{
const container = tag.find(".statistic-bandwidth-minute");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_received_last_minute_speech > 0 ? info.connection_bandwidth_received_last_minute_speech : 0;
bytes += info.connection_bandwidth_received_last_minute_control > 0 ? info.connection_bandwidth_received_last_minute_control : 0;
bytes += info.connection_bandwidth_received_last_minute_keepalive > 0 ? info.connection_bandwidth_received_last_minute_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_received_last_minute_keepalive == -1)
node_downstream.innerText = tr("Not calculated");
else
node_downstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"});
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
callbacks.push(info => {
let bytes = 0;
bytes += info.connection_bandwidth_sent_last_minute_speech > 0 ? info.connection_bandwidth_sent_last_minute_speech : 0;
bytes += info.connection_bandwidth_sent_last_minute_control > 0 ? info.connection_bandwidth_sent_last_minute_control : 0;
bytes += info.connection_bandwidth_sent_last_minute_keepalive > 0 ? info.connection_bandwidth_sent_last_minute_keepalive : 0;
if(bytes == 0 && info.connection_bandwidth_sent_last_minute_keepalive == -1)
node_upstream.innerText = tr("Not calculated");
else
node_upstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"});
});
node_upstream.innerText = tr("loading...");
}
}
/* quota */
{
const container = tag.find(".statistic-quota");
const node_downstream = container.find(".downstream .value")[0];
const node_upstream = container.find(".upstream .value")[0];
if(node_downstream) {
client.updateClientVariables().then(info => {
//TODO: Test for own client info and if so then show the max quota (needed permission)
node_downstream.innerText = MessageHelper.network.format_bytes(client.properties.client_month_bytes_downloaded, {exact: false});
});
node_downstream.innerText = tr("loading...");
}
if(node_upstream) {
client.updateClientVariables().then(info => {
//TODO: Test for own client info and if so then show the max quota (needed permission)
node_upstream.innerText = MessageHelper.network.format_bytes(client.properties.client_month_bytes_uploaded, {exact: false});
});
node_upstream.innerText = tr("loading...");
}
}
}
}

View File

@ -1,12 +1,14 @@
namespace Modals {
export function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity): Modal {
export function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity, name: string): Modal {
let modal: Modal;
let elapsed_timer: NodeJS.Timer;
modal = createModal({
header: tr("Improve identity"),
body: () => {
let template = $("#tmpl_settings-teamspeak_improve").renderTag();
let template = $("#tmpl_settings-teamspeak_improve").renderTag({
identity_name: name
});
template = $.spawn("div").append(template);
let active;
@ -103,11 +105,14 @@ namespace Modals {
}).catch(error => {
input_current_level.val("error: " + error);
});
return template;
tooltip(template);
return template.children();
},
footer: undefined,
width: 750
});
modal.htmlTag.find(".modal-body").addClass("modal-identity-improve modal-green");
modal.close_listener.push(() => modal.htmlTag.find(".button-close").trigger('click'));
modal.open();
return modal;
@ -115,78 +120,104 @@ namespace Modals {
export function spawnTeamSpeakIdentityImport(callback: (identity: profiles.identities.TeaSpeakIdentity) => any): Modal {
let modal: Modal;
let loaded_identity: profiles.identities.TeaSpeakIdentity;
let selected_type: string;
let identities: {[key: string]: profiles.identities.TeaSpeakIdentity} = {};
modal = createModal({
header: tr("Import identity"),
body: () => {
let template = $("#tmpl_settings-teamspeak_import").renderTag();
template = $.spawn("div").append(template);
template.find(".button-load-file").on('click', event => template.find(".input-file").trigger('click'));
const button_import = template.find(".button-import");
const set_error = message => {
template.find(".success").hide();
const button_file_select = template.find(".button-load-file");
const container_status = template.find(".container-status");
const input_text = template.find(".input-identity-text");
const input_file = template.find(".file-selector");
const set_status = (message: string | undefined, type: "error" | "loading" | "success") => {
container_status.toggleClass("hidden", !message);
if(message) {
template.find(".error").text(message).show();
button_import.prop("disabled", true);
} else
template.find(".error").hide();
container_status.toggleClass("error", type === "error");
container_status.toggleClass("loading", type === "loading");
container_status.find("a").text(message);
}
};
button_file_select.on('click', event => input_file.trigger('click'));
template.find("input[name='type']").on('change', event => {
const type = (event.target as HTMLInputElement).value;
button_file_select.prop("disabled", type !== "file");
input_text.prop("disabled", type !== "text");
selected_type = type;
button_import.prop("disabled", !identities[type]);
});
template.find("input[name='type'][value='file']").prop("checked", true).trigger("change");
const import_identity = (data: string, ini: boolean) => {
set_status(tr("Parsing identity"), "loading");
profiles.identities.TeaSpeakIdentity.import_ts(data, ini).then(identity => {
loaded_identity = identity;
set_error("");
identities[selected_type] = identity;
set_status("Identity parsed successfully.", "success");
button_import.prop("disabled", false);
template.find(".success").show();
}).catch(error => {
set_error("Failed to load identity: " + error);
set_status(tr("Failed to parse identity: ") + error, "error");
});
};
{ /* file select button */
template.find(".input-file").on('change', event => {
/* file select button */
input_file.on('change', event => {
const element = event.target as HTMLInputElement;
const file_reader = new FileReader();
set_status(tr("Loading file"), "loading");
file_reader.onload = function () {
import_identity(file_reader.result as string, true);
};
file_reader.onerror = ev => {
console.error(tr("Failed to read give identity file: %o"), ev);
set_error(tr("Failed to read file!"));
set_status(tr("Failed to read the identity file."), "error");
return;
};
if (element.files && element.files.length > 0)
file_reader.readAsText(element.files[0]);
});
input_text.on('change keyup', event => {
const text = input_text.val() as string;
if(!text) {
set_status("", "success");
return;
}
{ /* text input */
template.find(".button-load-text").on('click', event => {
createInputModal("Import identity from text", "Please paste your idenity bellow<br>", text => text.length > 0 && text.indexOf('V') != -1, result => {
if (result)
import_identity(result as string, false);
}).open();
});
if(text.indexOf('V') == -1) {
set_status(tr("Invalid identity string"), "error");
return;
}
import_identity(text, false);
});
button_import.on('click', event => {
modal.close();
callback(loaded_identity);
callback(identities[selected_type]);
});
set_error("");
set_status("", "success");
button_import.prop("disabled", true);
return template;
return template.children();
},
footer: undefined,
width: 750
});
modal.htmlTag.find(".modal-body").addClass("modal-identity-import modal-green");
modal.open();
return modal;
}

View File

@ -79,7 +79,7 @@ namespace Modals {
}
}
type PokeInvoker = {
export type PokeInvoker = {
name: string,
id: number,
unique_id: string

View File

@ -327,42 +327,14 @@ namespace Modals {
}
}
const format_unit = (value: number) => {
const KB = 1024;
const MB = 1024 * KB;
const GB = 1024 * MB;
const TB = 1024 * GB;
let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
let v, unit;
if(value > 2 * TB) {
unit = "TB";
v = value / TB;
} else if(value > GB) {
unit = "GB";
v = value / GB;
} else if(value > MB) {
unit = "MB";
v = value / MB;
} else if(value > KB) {
unit = "KB";
v = value / KB;
} else {
unit = "";
v = value;
}
return points + " Bytes" + (unit ? (" / " + v.toFixed(2) + " " + unit) : "");
};
/* quota info */
{
server.updateProperties().then(() => {
tag.find(".value.virtualserver_month_bytes_downloaded").text(format_unit(server.properties.virtualserver_month_bytes_downloaded));
tag.find(".value.virtualserver_month_bytes_uploaded").text(format_unit(server.properties.virtualserver_month_bytes_uploaded));
tag.find(".value.virtualserver_month_bytes_downloaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_month_bytes_downloaded));
tag.find(".value.virtualserver_month_bytes_uploaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_month_bytes_uploaded));
tag.find(".value.virtualserver_total_bytes_downloaded").text(format_unit(server.properties.virtualserver_total_bytes_downloaded));
tag.find(".value.virtualserver_total_bytes_uploaded").text(format_unit(server.properties.virtualserver_total_bytes_uploaded));
tag.find(".value.virtualserver_total_bytes_downloaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_total_bytes_downloaded));
tag.find(".value.virtualserver_total_bytes_uploaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_total_bytes_uploaded));
});
}
@ -381,14 +353,14 @@ namespace Modals {
server.request_connection_info().then(info => {
if(info.connection_filetransfer_bytes_sent_month && month_bytes_downloaded)
month_bytes_downloaded.innerText = format_unit(info.connection_filetransfer_bytes_sent_month);
month_bytes_downloaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_sent_month);
if(info.connection_filetransfer_bytes_received_month && month_bytes_uploaded)
month_bytes_uploaded.innerText = format_unit(info.connection_filetransfer_bytes_received_month);
month_bytes_uploaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_received_month);
if(info.connection_filetransfer_bytes_sent_total && total_bytes_downloaded)
total_bytes_downloaded.innerText = format_unit(info.connection_filetransfer_bytes_sent_total);
total_bytes_downloaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_sent_total);
if(info.connection_filetransfer_bytes_received_total && total_bytes_uploaded)
total_bytes_uploaded.innerText = format_unit(info.connection_filetransfer_bytes_received_total);
total_bytes_uploaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_received_total);
});
}, 1000);
modal.close_listener.push(() => clearInterval(id));

View File

@ -0,0 +1,195 @@
namespace Modals {
type InfoUpdateCallback = (info: ServerConnectionInfo | boolean) => any;
export function openServerInfo(server: ServerEntry) {
let modal: Modal;
let update_callbacks: InfoUpdateCallback[] = [];
modal = createModal({
header: tr("Server Information: ") + server.properties.virtualserver_name,
body: () => {
const template = $("#tmpl_server_info").renderTag();
apply_hostbanner(server, template.find(".container-top"));
apply_category_1(server, template, update_callbacks);
apply_category_2(server, template, update_callbacks);
apply_category_3(server, template, update_callbacks);
tooltip(template);
return template.children();
},
footer: null,
min_width: "25em"
});
const updater = setInterval(() => {
server.request_connection_info().then(info => update_callbacks.forEach(e => e(info))).catch(error => update_callbacks.forEach(e => e(false)));
}, 1000);
modal.htmlTag.find(".button-close").on('click', event => modal.close());
modal.htmlTag.find(".button-show-bandwidth").on('click', event => {
//TODO!
});
modal.htmlTag.find(".modal-body").addClass("modal-server-info");
modal.open();
modal.close_listener.push(() => clearInterval(updater));
}
function apply_hostbanner(server: ServerEntry, tag: JQuery) {
let container: JQuery;
tag.empty().append(
container = $.spawn("div").addClass("container-hostbanner")
).addClass("hidden");
const htag = Hostbanner.generate_tag(server.properties.virtualserver_hostbanner_gfx_url, server.properties.virtualserver_hostbanner_gfx_interval, server.properties.virtualserver_hostbanner_mode);
htag.then(t => {
if(!t) return;
tag.removeClass("hidden");
container.append(t);
});
}
function apply_category_1(server: ServerEntry, tag: JQuery, update_callbacks: InfoUpdateCallback[]) {
/* server name */
{
const container = tag.find(".server-name");
container.text(server.properties.virtualserver_name);
}
/* server region */
{
const container = tag.find(".server-region").empty();
container.append(
$.spawn("div").addClass("country flag-" + server.properties.virtualserver_country_code.toLowerCase()),
$.spawn("a").text(i18n.country_name(server.properties.virtualserver_country_code, tr("Global")))
);
}
/* slots */
{
const container = tag.find(".server-slots");
let text = server.properties.virtualserver_clientsonline + "/" + server.properties.virtualserver_maxclients;
if(server.properties.virtualserver_queryclientsonline)
text += " +" + (server.properties.virtualserver_queryclientsonline > 1 ?
server.properties.virtualserver_queryclientsonline + " " + tr("Queries") :
server.properties.virtualserver_queryclientsonline + " " + tr("Query"));
if(server.properties.virtualserver_reserved_slots)
text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")";
container.text(text);
}
/* first run */
{
const container = tag.find(".server-first-run");
container.text(
server.properties.virtualserver_created > 0 ?
moment(server.properties.virtualserver_created * 1000).format('MMMM Do YYYY, h:mm:ss a') :
tr("Unknown")
);
}
/* uptime */
{
const container = tag.find(".server-uptime");
const update = () => container.text(MessageHelper.format_time(server.calculateUptime() * 1000, tr("just started")));
update_callbacks.push(update);
update();
}
}
function apply_category_2(server: ServerEntry, tag: JQuery, update_callbacks: InfoUpdateCallback[]) {
/* ip */
{
const container = tag.find(".server-ip");
container.text(server.remote_address.host + (server.remote_address.port == 9987 ? "" : (":" + server.remote_address.port)))
}
/* version */
{
const container = tag.find(".server-version");
let timestamp = -1;
const version = (server.properties.virtualserver_version || "unknwon").replace(/ ?\[build: ?([0-9]+)]/gmi, (group, ts) => {
timestamp = parseInt(ts);
return "";
});
container.find("a").text(version);
container.find(".container-tooltip").toggle(timestamp > 0).find(".tooltip a").text(
moment(timestamp * 1000).format('[Build timestamp:] YYYY-MM-DD HH:mm Z')
);
}
/* platform */
{
const container = tag.find(".server-platform");
container.text(server.properties.virtualserver_platform);
}
/* ping */
{
const container = tag.find(".server-ping");
container.text(tr("calculating..."));
update_callbacks.push(data => {
if(typeof(data) === "boolean")
container.text(tr("No Permissions"));
else
container.text(data.connection_ping.toFixed(0) + " " + "ms");
});
}
/* packet loss */
{
const container = tag.find(".server-packet-loss");
container.text(tr("calculating..."));
update_callbacks.push(data => {
if(typeof(data) === "boolean")
container.text(tr("No Permissions"));
else
container.text(data.connection_packetloss_total.toFixed(2) + "%");
});
}
}
function apply_category_3(server: ServerEntry, tag: JQuery, update_callbacks: InfoUpdateCallback[]) {
/* unique id */
{
const container = tag.find(".server-unique-id");
container.text(server.properties.virtualserver_unique_identifier || tr("Unknown"));
}
/* voice encryption */
{
const container = tag.find(".server-voice-encryption");
if(server.properties.virtualserver_codec_encryption_mode == 0)
container.text(tr("Globally off"));
else if(server.properties.virtualserver_codec_encryption_mode == 1)
container.text(tr("Individually configured per channel"));
else
container.text(tr("Globally on"));
}
/* channel count */
{
const container = tag.find(".server-channel-count");
container.text(server.properties.virtualserver_channelsonline);
}
/* minimal security level */
{
const container = tag.find(".server-min-security-level");
container.text(server.properties.virtualserver_needed_identity_security_level);
}
/* complains */
{
const container = tag.find(".server-complains");
container.text(server.properties.virtualserver_complain_autoban_count);
}
}
}

View File

@ -1005,7 +1005,7 @@ namespace Modals {
const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity;
if (!profile) return;
Modals.spawnTeamSpeakIdentityImprove(profile).close_listener.push(() => {
Modals.spawnTeamSpeakIdentityImprove(profile, selected_profile.identity.profile_name).close_listener.push(() => {
profiles.mark_need_save();
for(const listener of profile_identity_changed)
listener();

View File

@ -240,7 +240,6 @@ namespace Modals {
{
const pe_client = tab_right.find(".permission-editor");
tab_right.on('show', event => {
console.error("client channel tab show");
editor.set_toggle_button(undefined, undefined);
pe_client.append(editor.html_tag());
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) {
@ -407,8 +406,6 @@ namespace Modals {
{
const pe_client = tab_right.find("permission-editor.client");
tab_right.on('show', event => {
console.error("Channel tab show");
editor.set_toggle_button(undefined, undefined);
pe_client.append(editor.html_tag());
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) {
@ -557,7 +554,6 @@ namespace Modals {
{
const pe_channel = tab_right.find(".permission-editor");
tab_right.on('show', event => {
console.error("Channel tab show");
editor.set_toggle_button(undefined, undefined);
pe_channel.append(editor.html_tag());
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST).granted(1))
@ -672,7 +668,6 @@ namespace Modals {
{
const pe_server = tab_right.find(".permission-editor");
tab_right.on('show', event => {
console.error("Channel group tab show");
editor.set_toggle_button(undefined, undefined);
pe_server.append(editor.html_tag());
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST).granted(1))
@ -1162,7 +1157,6 @@ namespace Modals {
{
const pe_server = tab_right.find(".permission-editor");
tab_right.on('show', event => {
console.error("Server tab show");
pe_server.append(editor.html_tag());
if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST).granted(1))
editor.set_mode(PermissionEditorMode.VISIBLE);

View File

@ -16,6 +16,7 @@ class ServerProperties {
virtualserver_queryclientsonline: number = 0;
virtualserver_channelsonline: number = 0;
virtualserver_uptime: number = 0;
virtualserver_created: number = 0;
virtualserver_maxclients: number = 0;
virtualserver_reserved_slots: number = 0;
@ -198,12 +199,17 @@ class ServerEntry {
name: tr("Show server info"),
callback: () => {
trigger_close = false;
//TODO
alert("inplement me");
Modals.openServerInfo(this);
},
icon_class: "client-about",
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
icon_class: "client-about"
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-invite_buddy",
name: tr("Invite buddy"),
callback: () => Modals.spawnInviteEditor(this.channelTree.client)
}, {
type: contextmenu.MenuEntryType.HR,
name: ''
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-channel_switch",
@ -213,10 +219,6 @@ class ServerEntry {
this.channelTree.client.side_bar.show_channel_conversations();
},
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
}, {
type: contextmenu.MenuEntryType.HR,
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT),
name: ''
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-virtualserver_edit",
@ -235,6 +237,10 @@ class ServerEntry {
return Promise.resolve();
});
}
}, {
type: contextmenu.MenuEntryType.HR,
visible: true,
name: ''
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-iconviewer",
@ -245,13 +251,8 @@ class ServerEntry {
icon_class: 'client-iconsview',
name: tr("View avatars"),
callback: () => Modals.spawnAvatarList(this.channelTree.client)
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-invite_buddy",
name: tr("Invite buddy"),
callback: () => Modals.spawnInviteEditor(this.channelTree.client)
},
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})())
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))())
);
}
@ -266,7 +267,7 @@ class ServerEntry {
value: variable.value,
type: typeof (this.properties[variable.key])
});
log.table("Server update properties", entries);
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Server update properties", entries);
}
let update_bannner = false, update_button = false;
@ -366,7 +367,7 @@ class ServerEntry {
});
this._info_connection_promise_timestamp = Date.now();
this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo").catch(error => _local_reject(error));
this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo", {}, {process_result: false}).catch(error => _local_reject(error));
return this._info_connection_promise;
}

View File

@ -494,7 +494,6 @@ class ChannelTree {
this.client.side_bar.show_channel_conversations();
}
}
this.client.select_info.setCurrentSelected($.isArray(this.currently_selected) ? undefined : entry);
}
private callback_multiselect_channel(event) {
@ -748,7 +747,7 @@ class ChannelTree {
}
handle_key_press(event: KeyboardEvent) {
console.log("Keydown: %o | %o | %o", this._focused, this.currently_selected, Array.isArray(this.currently_selected));
//console.log("Keydown: %o | %o | %o", this._focused, this.currently_selected, Array.isArray(this.currently_selected));
if(!this._focused || !this.currently_selected || Array.isArray(this.currently_selected)) return;
if(event.keyCode == KeyCode.KEY_UP) {

View File

@ -97,13 +97,13 @@ class RecorderProfile {
private initialize_input() {
this.input = audio.recorder.create_input();
this.input.callback_begin = () => {
console.log("Voice start");
log.debug(LogCategory.VOICE, "Voice start");
if(this.callback_start)
this.callback_start();
};
this.input.callback_end = () => {
console.log("Voice end");
log.debug(LogCategory.VOICE, "Voice end");
if(this.callback_stop)
this.callback_stop();
};
@ -153,11 +153,11 @@ class RecorderProfile {
const devices = all_devices.filter(e => e.default_input || e.unique_id === this.config.device_id);
const device = devices.find(e => e.unique_id === this.config.device_id) || devices[0];
console.log(tr("Loaded record profile device %s | %o (%o)"), this.config.device_id, device, all_devices);
log.info(LogCategory.VOICE, tr("Loaded record profile device %s | %o (%o)"), this.config.device_id, device, all_devices);
try {
await this.input.set_device(device);
} catch(error) {
console.error(tr("Failed to set input device (%o)"), error);
log.error(LogCategory.VOICE, tr("Failed to set input device (%o)"), error);
}
}
}
@ -207,7 +207,7 @@ class RecorderProfile {
try {
await this.input.set_consumer(undefined);
} catch(error) {
console.warn(tr("Failed to unmount input consumer for profile (%o)"), error);
log.warn(LogCategory.VOICE, tr("Failed to unmount input consumer for profile (%o)"), error);
}
}

605
shared/loader/app.ts Normal file
View File

@ -0,0 +1,605 @@
/// <reference path="loader.ts" />
interface Window {
$: JQuery;
}
namespace app {
export enum Type {
UNKNOWN,
CLIENT_RELEASE,
CLIENT_DEBUG,
WEB_DEBUG,
WEB_RELEASE
}
export let type: Type = Type.UNKNOWN;
export function is_web() {
return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG;
}
let _ui_version;
export function ui_version() {
if(typeof(_ui_version) !== "string") {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
return (_ui_version = version);
}
return _ui_version;
}
}
/* all javascript loaders */
const loader_javascript = {
detect_type: async () => {
if(window.require) {
const request = new Request("js/proto.js");
let file_path = request.url;
if(!file_path.startsWith("file://"))
throw "Invalid file path (" + file_path + ")";
file_path = file_path.substring(process.platform === "win32" ? 8 : 7);
const fs = require('fs');
if(fs.existsSync(file_path)) {
app.type = app.Type.CLIENT_DEBUG;
} else {
app.type = app.Type.CLIENT_RELEASE;
}
} else {
/* test if js/proto.js is available. If so we're in debug mode */
const request = new XMLHttpRequest();
request.open('GET', 'js/proto.js', true);
await new Promise((resolve, reject) => {
request.onreadystatechange = () => {
if (request.readyState === 4){
if (request.status === 404) {
app.type = app.Type.WEB_RELEASE;
} else {
app.type = app.Type.WEB_DEBUG;
}
resolve();
}
};
request.onerror = () => {
reject("Failed to detect app type");
};
request.send();
});
}
},
load_scripts: async () => {
/*
if(window.require !== undefined) {
console.log("Loading node specific things");
const remote = require('electron').remote;
module.paths.push(remote.app.getAppPath() + "/node_modules");
module.paths.push(remote.app.getAppPath() + "/app");
module.paths.push(remote.getGlobal("browser-root") + "js/");
window.$ = require("assets/jquery.min.js");
require("native/loader_adapter.js");
}
*/
if(!window.require) {
await loader.load_script(["vendor/jquery/jquery.min.js"]);
} else {
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "forum sync",
priority: 10,
function: async () => {
forum.sync_main();
}
});
*/
}
await loader.load_script(["vendor/DOMPurify/purify.min.js"]);
/* bootstrap material design and libs */
//await loader.load_script(["vendor/popper/popper.js"]);
//depends on popper
//await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]);
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "materialize body",
priority: 10,
function: async () => { $(document).ready(function() { $('body').bootstrapMaterialDesign(); }); }
});
*/
await loader.load_script("vendor/jsrender/jsrender.min.js");
await loader.load_scripts([
["vendor/xbbcode/src/parser.js"],
["vendor/moment/moment.js"],
["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */
["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */
["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */
["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"]
]);
await loader.load_scripts([
["vendor/emoji-picker/src/jquery.lsxemojipicker.js"]
]);
if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts release",
priority: 20,
function: loader_javascript.load_release
});
} else {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts debug",
priority: 20,
function: loader_javascript.load_scripts_debug
});
}
},
load_scripts_debug: async () => {
/* test if we're loading as TeaClient or WebClient */
if(!window.require) {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "javascript web",
priority: 10,
function: loader_javascript.load_scripts_debug_web
});
} else {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "javascript client",
priority: 10,
function: loader_javascript.load_scripts_debug_client
});
}
/* load some extends classes */
await loader.load_scripts([
["js/connection/ConnectionBase.js"]
]);
/* load the main app */
await loader.load_scripts([
//Load general API's
"js/proto.js",
"js/i18n/localize.js",
"js/i18n/country.js",
"js/log.js",
"js/sound/Sounds.js",
"js/utils/helpers.js",
"js/crypto/sha.js",
"js/crypto/hex.js",
"js/crypto/asn1.js",
"js/crypto/crc32.js",
//load the profiles
"js/profiles/ConnectionProfile.js",
"js/profiles/Identity.js",
"js/profiles/identities/teaspeak-forum.js",
//Basic UI elements
"js/ui/elements/context_divider.js",
"js/ui/elements/context_menu.js",
"js/ui/elements/modal.js",
"js/ui/elements/tab.js",
"js/ui/elements/slider.js",
"js/ui/elements/tooltip.js",
//Load UI
"js/ui/modal/ModalAbout.js",
"js/ui/modal/ModalAvatar.js",
"js/ui/modal/ModalAvatarList.js",
"js/ui/modal/ModalClientInfo.js",
"js/ui/modal/ModalQuery.js",
"js/ui/modal/ModalQueryManage.js",
"js/ui/modal/ModalPlaylistList.js",
"js/ui/modal/ModalPlaylistEdit.js",
"js/ui/modal/ModalBookmarks.js",
"js/ui/modal/ModalConnect.js",
"js/ui/modal/ModalSettings.js",
"js/ui/modal/ModalCreateChannel.js",
"js/ui/modal/ModalServerEdit.js",
"js/ui/modal/ModalServerInfo.js",
"js/ui/modal/ModalChangeVolume.js",
"js/ui/modal/ModalBanClient.js",
"js/ui/modal/ModalIconSelect.js",
"js/ui/modal/ModalInvite.js",
"js/ui/modal/ModalIdentity.js",
"js/ui/modal/ModalBanCreate.js",
"js/ui/modal/ModalBanList.js",
"js/ui/modal/ModalYesNo.js",
"js/ui/modal/ModalPoke.js",
"js/ui/modal/ModalKeySelect.js",
"js/ui/modal/ModalGroupAssignment.js",
"js/ui/modal/permission/ModalPermissionEdit.js",
{url: "js/ui/modal/permission/CanvasPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]},
{url: "js/ui/modal/permission/HTMLPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]},
"js/ui/channel.js",
"js/ui/client.js",
"js/ui/server.js",
"js/ui/view.js",
"js/ui/client_move.js",
"js/ui/htmltags.js",
"js/ui/frames/ControlBar.js",
"js/ui/frames/chat.js",
"js/ui/frames/chat_frame.js",
"js/ui/frames/connection_handlers.js",
"js/ui/frames/server_log.js",
"js/ui/frames/hostbanner.js",
"js/ui/frames/MenuBar.js",
//Load permissions
"js/permission/PermissionManager.js",
"js/permission/GroupManager.js",
//Load audio
"js/voice/RecorderBase.js",
"js/voice/RecorderProfile.js",
//Load general stuff
"js/settings.js",
"js/bookmarks.js",
"js/FileManager.js",
"js/ConnectionHandler.js",
"js/BrowserIPC.js",
"js/dns.js",
//Connection
"js/connection/CommandHandler.js",
"js/connection/CommandHelper.js",
"js/connection/HandshakeHandler.js",
"js/connection/ServerConnectionDeclaration.js",
"js/stats.js",
"js/PPTListener.js",
"js/profiles/identities/NameIdentity.js", //Depends on Identity
"js/profiles/identities/TeaForumIdentity.js", //Depends on Identity
"js/profiles/identities/TeamSpeakIdentity.js", //Depends on Identity
]);
await loader.load_script("js/main.js");
},
load_scripts_debug_web: async () => {
await loader.load_scripts([
["js/audio/AudioPlayer.js"],
["js/audio/WebCodec.js"],
["js/WebPPTListener.js"],
"js/voice/AudioResampler.js",
"js/voice/JavascriptRecorder.js",
"js/voice/VoiceHandler.js",
"js/voice/VoiceClient.js",
//Connection
"js/connection/ServerConnection.js",
//Load codec
"js/codec/Codec.js",
"js/codec/BasicCodec.js",
{url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]},
]);
},
load_scripts_debug_client: async () => {
await loader.load_scripts([
]);
},
load_release: async () => {
console.log("Load for release!");
await loader.load_scripts([
//Load general API's
["js/client.min.js", "js/client.js"]
]);
}
};
const loader_webassembly = {
test_webassembly: async () => {
/* We dont required WebAssembly anymore for fundamental functions, only for auto decoding
if(typeof (WebAssembly) === "undefined" || typeof (WebAssembly.compile) === "undefined") {
console.log(navigator.browserSpecs);
if (navigator.browserSpecs.name == 'Safari') {
if (parseInt(navigator.browserSpecs.version) < 11) {
displayCriticalError("You require Safari 11 or higher to use the web client!<br>Safari " + navigator.browserSpecs.version + " does not support WebAssambly!");
return;
}
}
else {
// Do something for all other browsers.
}
displayCriticalError("You require WebAssembly for TeaSpeak-Web!");
throw "Missing web assembly";
}
*/
}
};
const loader_style = {
load_style: async () => {
await loader.load_styles([
"vendor/xbbcode/src/xbbcode.css"
]);
await loader.load_styles([
"vendor/emoji-picker/src/jquery.lsxemojipicker.css"
]);
await loader.load_styles([
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
]);
if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) {
await loader_style.load_style_debug();
} else {
await loader_style.load_style_release();
}
},
load_style_debug: async () => {
await loader.load_styles([
"css/static/main.css",
"css/static/main-layout.css",
"css/static/helptag.css",
"css/static/scroll.css",
"css/static/channel-tree.css",
"css/static/ts/tab.css",
"css/static/ts/chat.css",
"css/static/ts/icons.css",
"css/static/ts/icons_em.css",
"css/static/ts/country.css",
"css/static/general.css",
"css/static/modal.css",
"css/static/modals.css",
"css/static/modal-about.css",
"css/static/modal-avatar.css",
"css/static/modal-icons.css",
"css/static/modal-bookmarks.css",
"css/static/modal-connect.css",
"css/static/modal-channel.css",
"css/static/modal-query.css",
"css/static/modal-invite.css",
"css/static/modal-playlist.css",
"css/static/modal-banlist.css",
"css/static/modal-bancreate.css",
"css/static/modal-clientinfo.css",
"css/static/modal-serverinfo.css",
"css/static/modal-identity.css",
"css/static/modal-settings.css",
"css/static/modal-poke.css",
"css/static/modal-server.css",
"css/static/modal-keyselect.css",
"css/static/modal-permissions.css",
"css/static/modal-group-assignment.css",
"css/static/music/info_plate.css",
"css/static/frame/SelectInfo.css",
"css/static/control_bar.css",
"css/static/context_menu.css",
"css/static/frame-chat.css",
"css/static/connection_handlers.css",
"css/static/server-log.css",
"css/static/htmltags.css",
"css/static/hostbanner.css",
"css/static/menu-bar.css"
]);
},
load_style_release: async () => {
await loader.load_styles([
"css/static/base.css",
"css/static/main.css",
]);
}
};
async function load_templates() {
try {
const response = await $.ajax("templates.html" + loader.get_cache_version());
let node = document.createElement("html");
node.innerHTML = response;
let tags: HTMLCollection;
if(node.getElementsByTagName("body").length > 0)
tags = node.getElementsByTagName("body")[0].children;
else
tags = node.children;
let root = document.getElementById("templates");
if(!root) {
loader.critical_error("Failed to find template tag!");
return;
}
while(tags.length > 0){
let tag = tags.item(0);
root.appendChild(tag);
}
} catch(error) {
loader.critical_error("Failed to find template tag!");
throw "template error";
}
}
//FUN: loader_ignore_age=0&loader_default_duration=1500&loader_default_age=5000
let _fadeout_warned = false;
function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = undefined) {
if(typeof($) === "undefined") {
if(!_fadeout_warned)
console.warn("Could not fadeout loader screen. Missing jquery functions.");
_fadeout_warned = true;
return;
}
let settingsDefined = typeof(StaticSettings) !== "undefined";
if(!duration) {
if(settingsDefined)
duration = StaticSettings.instance.static("loader_default_duration", 750);
else duration = 750;
}
if(!minAge) {
if(settingsDefined)
minAge = StaticSettings.instance.static("loader_default_age", 1750);
else minAge = 750;
}
if(!ignoreAge) {
if(settingsDefined)
ignoreAge = StaticSettings.instance.static("loader_ignore_age", false);
else ignoreAge = false;
}
/*
let age = Date.now() - app.appLoaded;
if(age < minAge && !ignoreAge) {
setTimeout(() => fadeoutLoader(duration, 0, true), minAge - age);
return;
}
*/
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration);
$(".loader .half").animate({width: 0}, duration, () => {
$(".loader").detach();
});
}
/* register tasks */
loader.register_task(loader.Stage.INITIALIZING, {
name: "safari fix",
function: async () => {
/* safari remove "fix" */
if(Element.prototype.remove === undefined)
Object.defineProperty(Element.prototype, "remove", {
enumerable: false,
configurable: false,
writable: false,
value: function(){
this.parentElement.removeChild(this);
}
});
},
priority: 50
});
loader.register_task(loader.Stage.INITIALIZING, {
name: "Browser detection",
function: async () => {
navigator.browserSpecs = (function(){
let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if(/trident/i.test(M[1])){
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return {name:'IE',version:(tem[1] || '')};
}
if(M[1]=== 'Chrome'){
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
if(tem != null) return {name:tem[1].replace('OPR', 'Opera'),version:tem[2]};
}
M = M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?'];
if((tem = ua.match(/version\/(\d+)/i))!= null)
M.splice(1, 1, tem[1]);
return {name:M[0], version:M[1]};
})();
console.log("Resolved browser specs: %o", navigator.browserSpecs); //Object { name: "Firefox", version: "42" }
},
priority: 30
});
loader.register_task(loader.Stage.INITIALIZING, {
name: "secure tester",
function: async () => {
/* we need https or localhost to use some things like the storage API */
if(typeof isSecureContext === "undefined")
(<any>window)["isSecureContext"] = location.protocol !== 'https:' && location.hostname !== 'localhost';
if(!isSecureContext) {
loader.critical_error("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!");
throw "App requires a secure context!"
}
},
priority: 20
});
loader.register_task(loader.Stage.INITIALIZING, {
name: "webassembly tester",
function: loader_webassembly.test_webassembly,
priority: 20
});
loader.register_task(loader.Stage.INITIALIZING, {
name: "app type test",
function: loader_javascript.detect_type,
priority: 20
});
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "javascript",
function: loader_javascript.load_scripts,
priority: 10
});
loader.register_task(loader.Stage.STYLE, {
name: "style",
function: loader_style.load_style,
priority: 10
});
loader.register_task(loader.Stage.TEMPLATES, {
name: "templates",
function: load_templates,
priority: 10
});
loader.register_task(loader.Stage.LOADED, {
name: "loaded handler",
function: async () => {
fadeoutLoader();
},
priority: 10
});
loader.register_task(loader.Stage.LOADED, {
name: "error task",
function: async () => {
if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) {
loader.critical_error("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good.");
throw "The tea is cold!";
}
},
priority: 20
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "lsx emoji picker setup",
function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}),
priority: 10
});
window["Module"] = (window["Module"] || {}) as any;
/* TeaClient */
if(window.require) {
const path = require("path");
const remote = require('electron').remote;
module.paths.push(path.join(remote.app.getAppPath(), "/modules"));
module.paths.push(path.join(path.dirname(remote.getGlobal("browser-root")), "js"));
const connector = require("renderer");
console.log(connector);
loader.register_task(loader.Stage.INITIALIZING, {
name: "teaclient initialize",
function: connector.initialize,
priority: 40
});
}
if(!loader.running()) {
/* we know that we want to load the app */
loader.execute_managed();
}

643
shared/loader/loader.ts Normal file
View File

@ -0,0 +1,643 @@
interface Window {
tr(message: string) : string;
}
namespace loader {
export namespace config {
export const loader_groups = false;
export const verbose = false;
export const error = true;
}
export type Task = {
name: string,
priority: number, /* tasks with the same priority will be executed in sync */
function: () => Promise<void>
};
export enum Stage {
/*
loading loader required files (incl this)
*/
INITIALIZING,
/*
setting up the loading process
*/
SETUP,
/*
loading all style sheet files
*/
STYLE,
/*
loading all javascript files
*/
JAVASCRIPT,
/*
loading all template files
*/
TEMPLATES,
/*
initializing static/global stuff
*/
JAVASCRIPT_INITIALIZING,
/*
finalizing load process
*/
FINALIZING,
/*
invoking main task
*/
LOADED,
DONE
}
let cache_tag: string | undefined;
let current_stage: Stage = undefined;
const tasks: {[key:number]:Task[]} = {};
/* test if all files shall be load from cache or fetch again */
function loader_cache_tag() {
const app_version = (() => {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
if(!version || version == "unknown" || version.replace(/0+/, "").length == 0)
return undefined;
return version;
})();
if(config.verbose) console.log("Found current app version: %o", app_version);
if(!app_version) {
/* TODO add warning */
cache_tag = "?_ts=" + Date.now();
return;
}
const cached_version = localStorage.getItem("cached_version");
if(!cached_version || cached_version != app_version) {
loader.register_task(loader.Stage.LOADED, {
priority: 0,
name: "cached version updater",
function: async () => {
localStorage.setItem("cached_version", app_version);
}
});
}
cache_tag = "?_version=" + app_version;
}
export function get_cache_version() { return cache_tag; }
export function finished() {
return current_stage == Stage.DONE;
}
export function running() { return typeof(current_stage) !== "undefined"; }
export function register_task(stage: Stage, task: Task) {
if(current_stage > stage) {
if(config.error)
console.warn("Register loading task, but it had already been finished. Executing task anyways!");
task.function().catch(error => {
if(config.error) {
console.error("Failed to execute delayed loader task!");
console.log(" - %s: %o", task.name, error);
}
loader.critical_error(error);
});
return;
}
const task_array = tasks[stage] || [];
task_array.push(task);
tasks[stage] = task_array.sort((a, b) => a.priority - b.priority);
}
export async function execute() {
document.getElementById("loader-overlay").classList.add("started");
loader_cache_tag();
const load_begin = Date.now();
let begin: number = 0;
let end: number = Date.now();
while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") {
let current_tasks: Task[] = [];
while((tasks[current_stage] || []).length > 0) {
if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) {
current_tasks.push(tasks[current_stage].pop());
} else break;
}
const errors: {
error: any,
task: Task
}[] = [];
const promises: Promise<void>[] = [];
for(const task of current_tasks) {
try {
if(config.verbose) console.debug("Executing loader %s (%d)", task.name, task.priority);
promises.push(task.function().catch(error => {
errors.push({
task: task,
error: error
});
return Promise.resolve();
}));
} catch(error) {
errors.push({
task: task,
error: error
});
}
}
if(promises.length > 0) {
await Promise.all([...promises]);
}
if(errors.length > 0) {
if(config.loader_groups) console.groupEnd();
console.error("Failed to execute loader. The following tasks failed (%d):", errors.length);
for(const error of errors)
console.error(" - %s: %o", error.task.name, error.error);
throw "failed to process step " + Stage[current_stage];
}
if(current_tasks.length == 0) {
if(typeof(current_stage) === "undefined") {
current_stage = -1;
if(config.verbose) console.debug("[loader] Booting app");
} else if(current_stage < Stage.INITIALIZING) {
if(config.loader_groups) console.groupEnd();
if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin);
} else {
if(config.loader_groups) console.groupEnd();
if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin);
}
begin = end;
current_stage += 1;
if(current_stage != Stage.DONE && config.loader_groups)
console.groupCollapsed("Executing loading stage %s", Stage[current_stage]);
}
}
/* cleanup */
{
_script_promises = {};
}
if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
}
export function execute_managed() {
loader.execute().then(() => {
if(config.verbose) {
let message;
if(typeof(window.tr) !== "undefined")
message = tr("App loaded successfully!");
else
message = "App loaded successfully!";
if(typeof(log) !== "undefined") {
/* We're having our log module */
log.info(LogCategory.GENERAL, message);
} else {
console.log(message);
}
}
}).catch(error => {
if(config.error) {
console.error("App loading failed: %o", error);
}
loader.critical_error("Failed to execute loader", "Lookup the console for more detail");
});
}
export type DependSource = {
url: string;
depends: string[];
}
export type SourcePath = string | DependSource | string[];
function script_name(path: SourcePath) {
if(Array.isArray(path)) {
let buffer = "";
let _or = " or ";
for(let entry of path)
buffer += _or + script_name(entry);
return buffer.slice(_or.length);
} else if(typeof(path) === "string")
return "<code>" + path + "</code>";
else
return "<code>" + path.url + "</code>";
}
class SyntaxError {
source: any;
constructor(source: any) {
this.source = source;
}
}
let _script_promises: {[key: string]: Promise<void>} = {};
export async function load_script(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_script(path[0]).catch(error => {
if(error instanceof SyntaxError)
return Promise.reject(error.source);
if(path.length > 1)
return load_script(path.slice(1));
return Promise.reject(error);
});
} else {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
return _script_promises[source.url] = (async () => {
/* await depends */
for(const depend of source.depends) {
if(!_script_promises[depend])
throw "Missing dependency " + depend;
await _script_promises[depend];
}
const tag: HTMLScriptElement = document.createElement("script");
await new Promise((resolve, reject) => {
let error = false;
const error_handler = (event: ErrorEvent) => {
if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.type = "application/javascript";
tag.async = true;
tag.defer = true;
tag.onerror = error => {
cleanup();
tag.remove();
reject(error);
};
tag.onload = () => {
cleanup();
if(config.verbose) console.debug("Script %o loaded", path);
setTimeout(resolve, 100);
};
document.getElementById("scripts").appendChild(tag);
tag.src = source.url + (cache_tag || "");
});
})();
}
}
export async function load_scripts(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
script: SourcePath,
error: any
}[] = [];
for(const script of paths)
promises.push(load_script(script).catch(error => {
errors.push({
script: script,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if(config.error) {
console.error("Failed to load the following scripts:");
for(const script of errors)
console.log(" - %o: %o", script.script, script.error);
}
loader.critical_error("Failed to load script " + script_name(errors[0].script) + " <br>" + "View the browser console for more information!");
throw "failed to load script " + script_name(errors[0].script);
}
}
export async function load_style(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_style(path[0]).catch(error => {
if(error instanceof SyntaxError)
return Promise.reject(error.source);
if(path.length > 1)
return load_script(path.slice(1));
return Promise.reject(error);
});
} else {
if(!path) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link");
let error = false;
const error_handler = (event: ErrorEvent) => {
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
if(event.filename == tag.href) { //FIXME!
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
tag.type = "text/css";
tag.rel = "stylesheet";
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.onerror = error => {
cleanup();
tag.remove();
if(config.error)
console.error("File load error for file %s: %o", path, error);
reject("failed to load file " + path);
};
tag.onload = () => {
cleanup();
{
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
const rules = css.cssRules;
const rules_remove: number[] = [];
const rules_add: string[] = [];
for(let index = 0; index < rules.length; index++) {
const rule = rules.item(index);
let rule_text = rule.cssText;
if(rule.cssText.indexOf("%%base_path%%") != -1) {
rules_remove.push(index);
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
}
}
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
if(css.removeRule)
css.removeRule(index);
else
css.deleteRule(index);
}
for(const rule of rules_add)
css.insertRule(rule, rules_remove[0]);
}
if(config.verbose) console.debug("Style sheet %o loaded", path);
setTimeout(resolve, 100);
};
document.getElementById("style").appendChild(tag);
tag.href = path + (cache_tag || "");
});
}
}
export async function load_styles(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
sheet: SourcePath,
error: any
}[] = [];
for(const sheet of paths)
promises.push(load_style(sheet).catch(error => {
errors.push({
sheet: sheet,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if(loader.config.error) {
console.error("Failed to load the following style sheet:");
for(const sheet of errors)
console.log(" - %o: %o", sheet.sheet, sheet.error);
}
loader.critical_error("Failed to load style sheet " + script_name(errors[0].sheet) + " <br>" + "View the browser console for more information!");
throw "failed to load style sheet " + script_name(errors[0].sheet);
}
}
export type ErrorHandler = (message: string, detail: string) => void;
let _callback_critical_error: ErrorHandler;
let _callback_critical_called: boolean = false;
export function critical_error(message: string, detail?: string) {
if(_callback_critical_called) {
console.warn("[CRITICAL] %s", message);
if(typeof(detail) === "string")
console.warn("[CRITICAL] %s", detail);
return;
}
if(_callback_critical_error) {
_callback_critical_error(message, detail);
return;
}
/* default handling */
let tag = document.getElementById("critical-load");
{
const error_tags = tag.getElementsByClassName("error");
error_tags[0].innerHTML = message;
}
if(typeof(detail) === "string") {
let node_detail = tag.getElementsByClassName("detail")[0];
node_detail.innerHTML = detail;
}
tag.style.display = "block";
}
export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler {
if((typeof(handler) === "object" && handler !== _callback_critical_error) || override)
_callback_critical_error = handler;
return _callback_critical_error;
}
}
{
const hello_world = () => {
const clog = console.log;
const print_security = () => {
{
const css = [
"display: block",
"text-align: center",
"font-size: 42px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: red"
].join(";");
clog("%c ", "font-size: 100px;");
clog("%cSecurity warning:", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 18px",
"font-weight: bold"
].join(";");
clog("%cPasting anything in here could give attackers access to your data.", css);
clog("%cUnless you understand exactly what you are doing, close this window and stay safe.", css);
clog("%c ", "font-size: 100px;");
}
};
/* print the hello world */
{
const css = [
"display: block",
"text-align: center",
"font-size: 72px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: #18BC9C"
].join(";");
clog("%cHey, hold on!", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 26px",
"font-weight: bold"
].join(";");
const css_2 = [
"display: block",
"text-align: center",
"font-size: 26px",
"font-weight: bold",
"color: blue"
].join(";");
const display_detect = /./;
display_detect.toString = function() { print_security(); return ""; };
clog("%cLovely to see you using and debugging the TeaSpeak Web client.", css);
clog("%cIf you have some good ideas or already done some incredible changes,", css);
clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2);
clog("%c ", display_detect);
}
};
try { /* lets try to print it as VM code :)*/
let hello_world_code = hello_world.toString();
hello_world_code = hello_world_code.substr(hello_world_code.indexOf('() => {') + 8);
hello_world_code = hello_world_code.substring(0, hello_world_code.lastIndexOf("}"));
//Look aheads are not possible with firefox
//hello_world_code = hello_world_code.replace(/(?<!const|let)(?<=^([^"'/]|"[^"]*"|'[^']*'|`[^`]*`|\/[^/]*\/)*) /gm, ""); /* replace all spaces */
hello_world_code = hello_world_code.replace(/[\n\r]/g, ""); /* replace as new lines */
eval(hello_world_code);
} catch(e) {
console.error(e);
hello_world();
}
}
/* set a timeout here, so if this script is merged with the actual loader (like in rel mode) the actual could load this manually here */
setTimeout(() => {
if(loader.running()) {
if(loader.config.verbose)
console.debug("Do not execute debug loading");
return;
}
loader.register_task(loader.Stage.INITIALIZING, {
priority: 100,
function: async () => {
let loader_type;
location.search.replace(/(?:^\?|&)([a-zA-Z_]+)=([.a-zA-Z0-9]+)(?=$|&)/g, (_, key: string, value: string) => {
if(key.toLowerCase() == "loader_target")
loader_type = value;
return "";
});
loader_type = loader_type || "app";
if(loader_type === "app") {
try {
await loader.load_scripts(["loader/app.js"]);
} catch (error) {
console.error("Failed to load main app script: %o", error);
loader.critical_error("Failed to load main app script", error);
throw "app loader failed";
}
} else {
loader.critical_error("Missing loader target: " + loader_type);
throw "Missing loader target: " + loader_type;
}
},
name: "loading target"
});
loader.execute_managed();
}, 0);

View File

@ -1,6 +0,0 @@
{
"source_files": [
"../js/load.ts"
],
"target_file": "../declarations/exports_loader.d.ts"
}

View File

@ -0,0 +1,7 @@
{
"source_files": [
"../loader/loader.ts",
"../loader/app.ts"
],
"target_file": "../declarations/exports_loader_app.d.ts"
}

View File

@ -3,8 +3,7 @@
"../js/**/*.ts"
],
"exclude": [
"../js/workers/**/*.ts",
"../js/load.ts"
"../js/workers/**/*.ts"
],
"target_file": "../declarations/exports_packed.d.ts"
}

View File

@ -20,6 +20,7 @@
"include": [
"../types",
"../declarations/imports_*.d.ts",
"../declarations/exports_loader_app.d.ts",
"../backend",
"../js/**/*.ts"
]

View File

@ -17,13 +17,12 @@
]
},
"exclude": [
"../js/workers",
"../js/load.ts"
"../js/workers"
],
"include": [
"../types",
"../declarations/imports_*.d.ts",
"../declarations/exports_loader.d.ts",
"../declarations/exports_loader_app.d.ts",
"../js/**/*.ts",
"../backend"
]

View File

@ -1,14 +1,16 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"sourceMap": true
"module": "none",
"sourceMap": true,
"outFile": "../generated/loader_app.js"
},
"include": [
"../types",
"../declarations/imports_*.d.ts",
"../declarations/exports_packed.d.ts",
"../js/load.ts",
"../loader/loader.ts",
"../loader/app.ts",
"../backend"
]
}

View File

@ -15,22 +15,63 @@
- Icon uploiad
- Avatar list
- Server group assignments checkbox
- Invite buddy
- Identity improve
- Identity import
- Ban Liste
- Übersicht
- Suchfunktion
- Text
- "Hightlisht own bans"
- "Show only own bans"
Pro Ban:
- Globaler ban oder server ban
- Name (Nicht immer gegeben)
- IP (Nicht immer gegeben)
- UID (Nicht immer gegeben)
- HWID (Nicht immer gegeben)
- Reason
- Creator
- Created/Expires
- "More info" Button (Dort sieht man auch dann die "Enforcements")
- Application Options
- Crash
- Focus crash window on crash
- Add a notification (Like the browser notifications)
- Reload button
- Hinzufügen
- Bearbeiten
- Löschen
- Ban "More Info" Dialog
Alle daten wie bei der liste, und eine liste mit "trigger" events (enforcements)
Pro entry:
- Unqiue ID
- Hardware ID
- Client Name
- Connection IP
- Timestamp
- Modal Bookmarks
- Bookmark list (+ Subdirectories)
- Bookmark Settings
- Bookmark/Directory name
- Only for bookmarks:
- Connect profile
- Server address
- Server port
- Server password
- Only for directories
- Parent directory
- Modal Bookmark create
- Type: Bookmark | Directory
- Parent directory
- Name
- Client info popup
- Basic Info
- Name/Unique ID (Database ID vil auch? Oder als hover irgendwo)
- TeaForo connected? Ist premium ja nein?
- Country
- Avatar
- IP (Wenn permission)
@ -44,15 +85,30 @@
- Description
- Server groups
- Channel group
- Connect count (Wie oft der client schon connectet ist)
- Online seit | Idle time
- Connect count (Wie oft der client schon connected ist)
- Online seid | Idle time
- Ping
- Client version
- First connected
Nur TeaClient; Nicht für WebClient clients
- Bandwidth
- Packets send/received | Packet Loss
- Up/Download Quota
- Server
- Server region/country
- Bandwidths & Transferred data
- Channel
- Audio Codec (Opus (4))
- Channel type (Wenn TEMP dann delete delay (nur wenn empty))
- Channel Topic
- Current clients "2/Unlimited"
- Description
- Password protected
- "Texting mode" | Private | "No saving" | "Logged (+ history length)"
- Server "short" info?
@ -71,6 +127,12 @@
- File Transfer bytes transferred, month & global (Up + Download)
TODO:
Server Info (Bandwidth usage)
Fix these icons: https://img.did.science/Screenshot_20-11-06.png
Fix auto TS compile for vendor emoji-picker
Make identity settings a bit smaller (Even scroll on my Laptop)
- Application Options
- Crash
- Focus crash window on crash
- Add a notification (Like the browser notifications)

15
tools/dtsgen/out.d.ts vendored
View File

@ -1,13 +1,6 @@
/* File: C:\Users\WolverinDEV\TeaSpeak\TeaWeb\tools\dtsgen\test\test_03.ts */
declare enum YY {
H = "C",
B = "Y"
}
declare interface X {
type: any;
c: YY.B;
}
declare class X {
static x();
/* File: /home/wolverindev/TeaSpeak/Web-Client/tools/dtsgen/test/test_07.ts */
declare namespace C { }
declare namespace C {
export function test(arg: string);
}

2
vendor/xbbcode vendored

@ -1 +1 @@
Subproject commit d75eb02ef8038595da5cc82d5953b170df9c49eb
Subproject commit 983c9d59542a003df38e80fa1680ab9cfda4c530

View File

@ -4,30 +4,34 @@ BASEDIR=$(dirname "$0")
cd "$BASEDIR"
source ../scripts/resolve_commands.sh
if [ ! -e declarations/imports_shared.d.ts ]; then
if [[ ! -e declarations/imports_shared.d.ts ]]; then
echo "generate the declarations first!"
echo "Execute: /scripts/build_declarations.sh"
exit 1
fi
if [ ! -e ../shared/generated/shared.js ]; then
if [[ ! -e ../shared/generated/shared.js ]]; then
echo "generate the shared packed file first!"
echo "Execute: /shared/generate_packed.sh"
exit 1
fi
execute_tsc -p tsconfig/tsconfig_packed.json
if [ $? -ne 0 ]; then
if [[ $? -ne 0 ]]; then
echo "Failed to build file"
exit 1
fi
echo "Mergin files"
echo "Merging files"
if [ -e generated/client.js ]; then
if [[ -e generated/client.js ]]; then
rm generated/client.js
fi
cat ../shared/generated/shared.js > generated/client.js
cat generated/web.js >> generated/client.js
npm run minify-web-rel-file
if [[ -e generated/client.min.js ]]; then
rm generated/client.min.js
fi
npm run minify-web-rel-file `pwd`/generated/client.min.js `pwd`/generated/client.js

View File

@ -120,7 +120,7 @@ namespace ppt {
new_hooks.push(hook);
if(!old_hooks.remove(hook) && hook.callback_press) {
hook.callback_press();
console.debug("Trigger key press for %o!", hook);
log.trace(LogCategory.GENERAL, tr("Trigger key press for %o!"), hook);
}
}
}
@ -129,7 +129,7 @@ namespace ppt {
for(const hook of old_hooks)
if(hook.callback_release) {
hook.callback_release();
console.debug("Trigger key release for %o!", hook);
log.trace(LogCategory.GENERAL, tr("Trigger key release for %o!"), hook);
}
key_hooks_active = new_hooks;
}

View File

@ -17,7 +17,7 @@ namespace audio.player {
}
function fire_initialized() {
console.log("Fire initialized: %o", _initialized_listener);
log.info(LogCategory.AUDIO, tr("File initialized for %d listeners"), _initialized_listener.length);
while(_initialized_listener.length > 0)
_initialized_listener.pop_front()();
}
@ -39,7 +39,7 @@ namespace audio.player {
(_globalContextPromise = _globalContext.resume()).then(() => {
fire_initialized();
}).catch(error => {
displayCriticalError("Failed to initialize global audio context! (" + error + ")");
loader.critical_error("Failed to initialize global audio context! (" + error + ")");
});
}
_globalContext.resume(); //We already have our listener

View File

@ -49,7 +49,7 @@ abstract class BasicCodec implements Codec {
encodeSamples(cache: CodecClientCache, pcm: AudioBuffer) {
this._encodeResampler.resample(pcm).catch(error => console.error(tr("Could not resample PCM data for codec. Error: %o"), error))
this._encodeResampler.resample(pcm).catch(error => log.error(LogCategory.VOICE, tr("Could not resample PCM data for codec. Error: %o"), error))
.then(buffer => this.encodeSamples0(cache, buffer as any)).catch(error => console.error(tr("Could not encode PCM data for codec. Error: %o"), error))
}
@ -74,12 +74,12 @@ abstract class BasicCodec implements Codec {
if(result instanceof Uint8Array) {
let time = Date.now() - encodeBegin;
if(time > 20)
console.error(tr("Required time: %d"), time);
log.warn(LogCategory.VOICE, tr("Voice buffer stalled in WorkerPipe longer then expected: %d"), time);
//if(time > 20)
// chat.serverChat().appendMessage("Required decode time: " + time);
this.on_encoded_data(result);
}
else console.error("[Codec][" + this.name() + "] Could not encode buffer. Result: " + result); //TODO tr
else log.error(LogCategory.VOICE, "[Codec][" + this.name() + "] Could not encode buffer. Result: " + result); //TODO tr
});
}
return true;

View File

@ -152,16 +152,14 @@ class CodecWrapperWorker extends BasicCodec {
}
private onWorkerMessage(message: any) {
if(Date.now() - message["timestamp"] > 5)
console.warn(tr("Worker message stock time: %d"), Date.now() - message["timestamp"]);
if(!message["token"]) {
console.error(tr("Invalid worker token!"));
log.error(LogCategory.VOICE, tr("Invalid worker token!"));
return;
}
if(message["token"] == this._workerCallbackToken) {
if(message["type"] == "loaded") {
console.log(tr("[Codec] Got worker init response: Success: %o Message: %o"), message["success"], message["message"]);
log.info(LogCategory.VOICE, tr("[Codec] Got worker init response: Success: %o Message: %o"), message["success"], message["message"]);
if(message["success"]) {
if(this._workerCallbackResolve)
this._workerCallbackResolve();
@ -176,10 +174,14 @@ class CodecWrapperWorker extends BasicCodec {
//FIXME?
return;
}
console.log(tr("Costume callback! (%o)"), message);
log.debug(LogCategory.VOICE, tr("Costume callback! (%o)"), message);
return;
}
/* lets warn on general packets. Control packets are allowed to "stuck" a bit longer */
if(Date.now() - message["timestamp"] > 5)
log.warn(LogCategory.VOICE, tr("Worker message stock time: %d"), Date.now() - message["timestamp"]);
for(let entry of this._workerListener) {
if(entry.token == message["token"]) {
entry.resolve(message);
@ -188,8 +190,7 @@ class CodecWrapperWorker extends BasicCodec {
}
}
//TODO tr
console.error("Could not find worker token entry! (" + message["token"] + ")");
log.error(LogCategory.VOICE, tr("Could not find worker token entry! (%o)"), message["token"]);
}
private spawnWorker() : Promise<Boolean> {

View File

@ -58,7 +58,7 @@ namespace connection {
destroy() {
this.disconnect("handle destroyed").catch(error => {
console.warn(tr("Failed to disconnect on server connection destroy: %o"), error);
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect on server connection destroy: %o"), error);
}).then(() => {
clearInterval(this._ping.thread_id);
clearTimeout(this._connect_timeout_timer);
@ -67,7 +67,7 @@ namespace connection {
try {
listener.reject("handler destroyed");
} catch(error) {
console.warn(tr("Failed to reject command promise: %o"), error);
log.warn(LogCategory.NETWORKING, tr("Failed to reject command promise: %o"), error);
}
}
this._retListener = undefined;
@ -86,7 +86,7 @@ namespace connection {
}
on_connect: () => void = () => {
console.log(tr("Socket connected"));
log.info(LogCategory.NETWORKING, tr("Socket connected"));
this.client.log.log(log.server.Type.CONNECTION_LOGIN, {});
this._handshakeHandler.initialize();
this._handshakeHandler.startHandshake();
@ -105,7 +105,7 @@ namespace connection {
try {
await this.disconnect()
} catch(error) {
console.error(tr("Failed to close old connection properly. Error: %o"), error);
log.error(LogCategory.NETWORKING, tr("Failed to close old connection properly. Error: %o"), error);
throw "failed to cleanup old connection";
}
}
@ -177,7 +177,7 @@ namespace connection {
local_socket.onerror = e => {
if(this._socket != local_socket) return; /* this socket isn't from interest anymore */
console.log(tr("Received web socket error: (%o)"), e);
log.warn(LogCategory.NETWORKING, tr("Received web socket error: (%o)"), e);
};
local_socket.onmessage = msg => {
@ -241,12 +241,12 @@ namespace connection {
try {
json = JSON.parse(data);
} catch(e) {
console.error(tr("Could not parse message json!"));
log.warn(LogCategory.NETWORKING, tr("Could not parse message json!"));
alert(e); // error in the above string (in this case, yes)!
return;
}
if(json["type"] === undefined) {
console.log(tr("Missing data type!"));
log.warn(LogCategory.NETWORKING, tr("Missing data type in message!"));
return;
}
if(json["type"] === "command") {
@ -271,7 +271,7 @@ namespace connection {
if(this._voice_connection)
this._voice_connection.handleControlPacket(json);
else
console.log(tr("Dropping WebRTC command packet, because we haven't a bridge."))
log.warn(LogCategory.NETWORKING, tr("Dropping WebRTC command packet, because we haven't a bridge."))
} else if(json["type"] === "ping") {
this.sendData(JSON.stringify({
type: 'pong',
@ -288,7 +288,7 @@ namespace connection {
log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3));
}
} else {
console.log(tr("Unknown command type %o"), json["type"]);
log.warn(LogCategory.NETWORKING, tr("Unknown command type %o"), json["type"]);
}
} else {
log.warn(LogCategory.NETWORKING, tr("Received unknown message of type %s. Dropping message"), typeof(data));
@ -317,7 +317,7 @@ namespace connection {
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
if(!this._socket || !this.connected()) {
console.warn(tr("Tried to send a command without a valid connection."));
log.warn(LogCategory.NETWORKING, tr("Tried to send a command without a valid connection."));
return Promise.reject(tr("not connected"));
}

View File

@ -9,7 +9,7 @@ class AudioResampler {
resample(buffer: AudioBuffer) : Promise<AudioBuffer> {
if(!buffer) {
console.warn(tr("Received empty buffer as input! Returning empty output!"));
log.warn(LogCategory.AUDIO, tr("Received empty buffer as input! Returning empty output!"));
return Promise.resolve(buffer);
}
//console.log("Encode from %i to %i", buffer.sampleRate, this.targetSampleRate);

View File

@ -46,7 +46,7 @@ namespace audio {
if(_queried_devices.length > 0 && _queried_devices.filter(e => e.default_input).length == 0)
_queried_devices[0].default_input = true;
} catch(error) {
console.warn(tr("Failed to query microphone devices (%o)"), error);
log.error(LogCategory.AUDIO, tr("Failed to query microphone devices (%o)"), error);
_queried_devices = [];
}
}
@ -357,7 +357,7 @@ namespace audio {
if(callback.callback_audio)
callback.callback_audio(event.inputBuffer);
if(callback.callback_buffer) {
console.warn(tr("AudioInput has callback buffer, but this isn't supported yet!"));
log.warn(LogCategory.AUDIO, tr("AudioInput has callback buffer, but this isn't supported yet!"));
}
}
@ -371,7 +371,7 @@ namespace audio {
if(this._state != InputState.PAUSED)
return;
} catch(error) {
console.debug(tr("JavascriptInput:start() Start promise await resulted in an error: %o"), error);
log.debug(LogCategory.AUDIO, tr("JavascriptInput:start() Start promise await resulted in an error: %o"), error);
}
}
@ -400,7 +400,7 @@ namespace audio {
if(!media_function) return InputStartResult.ENOTSUPPORTED;
try {
console.info(tr("Requesting a microphone stream for device %s in group %s"), device_id, group_id);
log.info(LogCategory.AUDIO, tr("Requesting a microphone stream for device %s in group %s"), device_id, group_id);
const audio_constrains: MediaTrackConstraints = {};
audio_constrains.deviceId = device_id;
@ -420,13 +420,13 @@ namespace audio {
//createErrorModal(tr("Failed to create microphone"), tr("Microphone recording failed. Please allow TeaWeb access to your microphone")).open();
//FIXME: Move this to somewhere else!
console.warn(tr("Microphone request failed (No permissions). Browser message: %o"), error.message);
log.warn(LogCategory.AUDIO, tr("Microphone request failed (No permissions). Browser message: %o"), error.message);
return InputStartResult.ENOTALLOWED;
} else {
console.warn(tr("Microphone request failed. Request resulted in error: %o: %o"), error.name, error);
log.warn(LogCategory.AUDIO, tr("Microphone request failed. Request resulted in error: %o: %o"), error.name, error);
}
} else {
console.warn(tr("Failed to initialize recording stream (%o)"), error);
log.warn(LogCategory.AUDIO, tr("Failed to initialize recording stream (%o)"), error);
}
return InputStartResult.EUNKNOWN;
}
@ -506,7 +506,7 @@ namespace audio {
try {
await this.stop();
} catch(error) {
console.warn(tr("Failed to stop previous record session (%o)"), error);
log.warn(LogCategory.AUDIO, tr("Failed to stop previous record session (%o)"), error);
}
this._current_device = device as any; /* TODO: Test for device_id and device_group */
@ -519,7 +519,7 @@ namespace audio {
try {
await this.start()
} catch(error) {
console.warn(tr("Failed to start new recording stream (%o)"), error);
log.warn(LogCategory.AUDIO, tr("Failed to start new recording stream (%o)"), error);
throw "failed to start record";
}
}

View File

@ -34,23 +34,23 @@ namespace audio {
playback_buffer(buffer: AudioBuffer) {
if(!buffer) {
console.warn(tr("[AudioController] Got empty or undefined buffer! Dropping it"));
log.warn(LogCategory.VOICE, tr("[AudioController] Got empty or undefined buffer! Dropping it"));
return;
}
if(!this.speakerContext) {
console.warn(tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!"));
log.warn(LogCategory.VOICE, tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!"));
return;
}
if (buffer.sampleRate != this.speakerContext.sampleRate)
console.warn(tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate);
log.warn(LogCategory.VOICE, tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate);
this.apply_volume_to_buffer(buffer);
this._buffered_samples.push(buffer);
if(this._player_state == connection.voice.PlayerState.STOPPED || this._player_state == connection.voice.PlayerState.STOPPING) {
console.log(tr("[Audio] Starting new playback"));
log.info(LogCategory.VOICE, tr("[Audio] Starting new playback"));
this.set_state(connection.voice.PlayerState.PREBUFFERING);
}
@ -67,11 +67,11 @@ namespace audio {
break;
}
if(this._player_state == connection.voice.PlayerState.PREBUFFERING) {
console.log(tr("[Audio] Prebuffering succeeded (Replaying now)"));
log.info(LogCategory.VOICE, tr("[Audio] Prebuffering succeeded (Replaying now)"));
if(this.callback_playback)
this.callback_playback();
} else if(this.allowBuffering) {
console.log(tr("[Audio] Buffering succeeded (Replaying now)"));
log.info(LogCategory.VOICE, tr("[Audio] Buffering succeeded (Replaying now)"));
}
this._player_state = connection.voice.PlayerState.PLAYING;
case connection.voice.PlayerState.PLAYING:
@ -86,7 +86,7 @@ namespace audio {
let buffer: AudioBuffer;
while((buffer = this._buffered_samples.pop_front())) {
if(this._playing_nodes.length >= this._latency_buffer_length * 1.5 + 3) {
console.log(tr("Dropping buffer because playing queue grows to much"));
log.info(LogCategory.VOICE, tr("Dropping buffer because playing queue grows to much"));
continue; /* drop the data (we're behind) */
}
if(this._time_index < this.speakerContext.currentTime)
@ -135,7 +135,7 @@ namespace audio {
this._player_state = connection.voice.PlayerState.BUFFERING;
if(!this.allowBuffering)
console.warn(tr("[Audio] Detected a buffer underflow!"));
log.warn(LogCategory.VOICE, tr("[Audio] Detected a buffer underflow!"));
this.reset_buffer_timeout(true);
} else {
this._player_state = connection.voice.PlayerState.STOPPED;
@ -152,7 +152,7 @@ namespace audio {
if(restart)
this._buffer_timeout = setTimeout(() => {
if(this._player_state == connection.voice.PlayerState.PREBUFFERING || this._player_state == connection.voice.PlayerState.BUFFERING) {
console.warn(tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay"));
log.warn(LogCategory.VOICE, tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay"));
this.stopAudio();
}
this._buffer_timeout = undefined;

View File

@ -26,16 +26,16 @@ namespace audio {
const dummy_client_id = 0xFFEF;
this.ownCodec(dummy_client_id, _ => {}).then(codec => {
console.log(tr("Release again! (%o)"), codec);
log.info(LogCategory.VOICE, tr("Release again! (%o)"), codec);
this.releaseCodec(dummy_client_id);
}).catch(error => {
if(this._supported) {
console.warn(tr("Disabling codec support for "), this.name);
log.warn(LogCategory.VOICE, tr("Disabling codec support for "), this.name);
createErrorModal(tr("Could not load codec driver"), tr("Could not load or initialize codec ") + this.name + "<br>" +
"Error: <code>" + JSON.stringify(error) + "</code>").open();
console.error(tr("Failed to initialize the opus codec. Error: %o"), error);
log.error(LogCategory.VOICE, tr("Failed to initialize the opus codec. Error: %o"), error);
} else {
console.debug(tr("Failed to initialize already disabled codec. Error: %o"), error);
log.debug(LogCategory.VOICE, tr("Failed to initialize already disabled codec. Error: %o"), error);
}
this._supported = false;
});
@ -61,7 +61,7 @@ namespace audio {
//TODO test success flag
this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject);
}).catch(error => {
console.error(tr("Could not initialize codec!\nError: %o"), error);
log.error(LogCategory.VOICE, tr("Could not initialize codec!\nError: %o"), error);
reject(typeof(error) === 'string' ? error : tr("Could not initialize codec!"));
});
}
@ -149,7 +149,7 @@ namespace audio {
clearInterval(this.send_task);
this.dropSession();
this.acquire_voice_recorder(undefined, true).catch(error => {
console.warn(tr("Failed to release voice recorder: %o"), error);
log.warn(LogCategory.VOICE, tr("Failed to release voice recorder: %o"), error);
}).then(() => {
for(const client of this._audio_clients) {
client.abort_replay();
@ -306,17 +306,17 @@ namespace audio {
try {
this.dataChannel.send(packet);
} catch (error) {
console.warn(tr("Failed to send voice packet. Error: %o"), error);
log.warn(LogCategory.VOICE, tr("Failed to send voice packet. Error: %o"), error);
}
} else {
console.warn(tr("Could not transfer audio (not connected)"));
log.warn(LogCategory.VOICE, tr("Could not transfer audio (not connected)"));
}
}
createSession() {
if(!audio.player.initialized()) {
console.log(tr("Audio player isn't initialized yet. Waiting for gesture."));
log.info(LogCategory.VOICE, tr("Audio player isn't initialized yet. Waiting for gesture."));
audio.player.on_ready(() => this.createSession());
return;
}
@ -352,13 +352,13 @@ namespace audio {
this.rtcPeerConnection.onicecandidate = this.on_local_ice_candidate.bind(this);
if(this.local_audio_stream) { //May a typecheck?
this.rtcPeerConnection.addStream(this.local_audio_stream.stream);
console.log(tr("Adding stream (%o)!"), this.local_audio_stream.stream);
log.info(LogCategory.VOICE, tr("Adding stream (%o)!"), this.local_audio_stream.stream);
}
this.rtcPeerConnection.createOffer(sdpConstraints).then(offer => {
this.on_local_offer_created(offer);
}).catch(error => {
console.error(tr("Could not create ice offer! error: %o"), error);
log.error(LogCategory.VOICE, tr("Could not create ice offer! error: %o"), error);
});
}
@ -384,27 +384,27 @@ namespace audio {
handleControlPacket(json) {
if(json["request"] === "answer") {
const session_description = new RTCSessionDescription(json["msg"]);
console.log(tr("Received answer to our offer. Answer: %o"), session_description);
log.info(LogCategory.VOICE, tr("Received answer to our offer. Answer: %o"), session_description);
this.rtcPeerConnection.setRemoteDescription(session_description).then(() => {
console.log(tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length);
log.info(LogCategory.VOICE, tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length);
this._ice_use_cache = false;
for(let msg of this._ice_cache) {
this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(msg)).catch(error => {
console.log(tr("Failed to add remote cached ice candidate %s: %o"), msg, error);
log.info(LogCategory.VOICE, tr("Failed to add remote cached ice candidate %s: %o"), msg, error);
});
}
this._ice_cache = [];
}).catch(error => {
console.log(tr("Failed to apply remote description: %o"), error); //FIXME error handling!
log.info(LogCategory.VOICE, tr("Failed to apply remote description: %o"), error); //FIXME error handling!
});
} else if(json["request"] === "ice") {
if(!this._ice_use_cache) {
console.log(tr("Add remote ice! (%o)"), json["msg"]);
log.info(LogCategory.VOICE, tr("Add remote ice! (%o)"), json["msg"]);
this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(json["msg"])).catch(error => {
console.log(tr("Failed to add remote ice candidate %s: %o"), json["msg"], error);
log.info(LogCategory.VOICE, tr("Failed to add remote ice candidate %s: %o"), json["msg"], error);
});
} else {
console.log(tr("Cache remote ice! (%o)"), json["msg"]);
log.info(LogCategory.VOICE, tr("Cache remote ice! (%o)"), json["msg"]);
this._ice_cache.push(json["msg"]);
}
} else if(json["request"] == "status") {
@ -429,7 +429,7 @@ namespace audio {
//if(event.candidate && event.candidate.protocol !== "udp")
// return;
console.log(tr("Gathered local ice candidate %o."), event.candidate);
log.info(LogCategory.VOICE, tr("Gathered local ice candidate %o."), event.candidate);
if(event.candidate) {
this.connection.sendData(JSON.stringify({
type: 'WebRTC',
@ -446,18 +446,18 @@ namespace audio {
}
private on_local_offer_created(localSession) {
console.log(tr("Local offer created. Setting up local description. (%o)"), localSession);
log.info(LogCategory.VOICE, tr("Local offer created. Setting up local description. (%o)"), localSession);
this.rtcPeerConnection.setLocalDescription(localSession).then(() => {
console.log(tr("Offer applied successfully. Sending offer to server."));
log.info(LogCategory.VOICE, tr("Offer applied successfully. Sending offer to server."));
this.connection.sendData(JSON.stringify({type: 'WebRTC', request: "create", msg: localSession}));
}).catch(error => {
console.log(tr("Failed to apply local description: %o"), error);
log.info(LogCategory.VOICE, tr("Failed to apply local description: %o"), error);
//FIXME error handling
});
}
private on_data_channel(channel) {
console.log(tr("Got new data channel! (%s)"), this.dataChannel.readyState);
log.info(LogCategory.VOICE, tr("Got new data channel! (%s)"), this.dataChannel.readyState);
this.connection.client.update_voice_status();
}
@ -471,16 +471,16 @@ namespace audio {
let clientId = bin[2] << 8 | bin[3];
let packetId = bin[0] << 8 | bin[1];
let codec = bin[4];
//console.log("Client id " + clientId + " PacketID " + packetId + " Codec: " + codec);
//log.info(LogCategory.VOICE, "Client id " + clientId + " PacketID " + packetId + " Codec: " + codec);
let client = this.find_client(clientId);
if(!client) {
console.error(tr("Having voice from unknown audio client? (ClientID: %o)"), clientId);
log.error(LogCategory.VOICE, tr("Having voice from unknown audio client? (ClientID: %o)"), clientId);
return;
}
let codec_pool = VoiceConnection.codec_pool[codec];
if(!codec_pool) {
console.error(tr("Could not playback codec %o"), codec);
log.error(LogCategory.VOICE, tr("Could not playback codec %o"), codec);
return;
}
@ -496,9 +496,9 @@ namespace audio {
codec_pool.ownCodec(clientId, e => this.handleEncodedVoicePacket(e, codec), true)
.then(decoder => decoder.decodeSamples(client.get_codec_cache(codec), encodedData))
.then(buffer => client.playback_buffer(buffer)).catch(error => {
console.error(tr("Could not playback client's (%o) audio (%o)"), clientId, error);
log.error(LogCategory.VOICE, tr("Could not playback client's (%o) audio (%o)"), clientId, error);
if(error instanceof Error)
console.error(error.stack);
log.error(LogCategory.VOICE, error.stack);
});
}
}
@ -539,7 +539,7 @@ namespace audio {
return false;
if(chandler.client_status.input_muted)
return false;
console.log(tr("Local voice ended"));
log.info(LogCategory.VOICE, tr("Local voice ended"));
if(this.dataChannel)
this.send_voice_packet(new Uint8Array(0), this.current_channel_codec());
@ -547,14 +547,14 @@ namespace audio {
private handleVoiceStarted() {
const chandler = this.connection.client;
console.log(tr("Local voice started"));
log.info(LogCategory.VOICE, tr("Local voice started"));
const ch = chandler.getClient();
if(ch) ch.speaking = true;
}
private on_recoder_yield() {
console.log("Lost recorder!");
log.info(LogCategory.VOICE, "Lost recorder!");
this._audio_source = undefined;
this.acquire_voice_recorder(undefined, true); /* we can ignore the promise because we should finish this directly */
}