A lots of updates

canary
WolverinDEV 2019-03-25 20:04:04 +01:00
parent 658c3506d7
commit add76cbd20
29 changed files with 1529 additions and 316 deletions

View File

@ -1,4 +1,13 @@
# Changelog:
* **XX.XX.XX**
- Improved icon and avatar cache handling
- Added an icon manager
- Fixed bookmark create modal style
- Fixed control bar drop downs going over the edge
- Fixed context menu overflowing and going out of the side
- Improved host banner url revoke (only revoke after a new one has been generated)
- Added some fancy console messages
* **17.03.19**
- Using VAD by default instead of PPT
- Improved mobile experience:

View File

@ -4,7 +4,7 @@
/* shared part */
[ /* shared html and php files */
"type" => "html",
"search-pattern" => "/^([a-zA-Z]+)\.(html|php)$/",
"search-pattern" => "/^([a-zA-Z]+)\.(html|php|json)$/",
"build-target" => "dev|rel",
"path" => "./",

View File

@ -3,140 +3,148 @@
display: none;
z-index: 2000;
position: absolute;
border: 1px solid #CCC;
white-space: nowrap;
font-family: sans-serif;
background: #FFF;
color: #333;
padding: 3px;
* {
font-family: Arial, serif;
font-size: 12px;
white-space: pre;
line-height: 1;
vertical-align: middle;
}
.context-menu-container {
border: 1px solid #CCC;
white-space: nowrap;
font-family: sans-serif;
background: #FFF;
color: #333;
padding: 3px;
hr {
margin-top: 8px;
margin-bottom: 8px;
}
.entry {
/*padding: 8px 12px;*/
padding-right: 12px;
cursor: pointer;
list-style-type: none;
transition: all .3s ease;
user-select: none;
align-items: center;
display: flex;
&.disabled {
background-color: lightgray;
cursor: not-allowed;
&.left {
margin-left: -100%;
width: 100%;
}
&:hover:not(.disabled) {
background-color: #DEF;
* {
font-family: Arial, serif;
font-size: 12px;
white-space: pre;
line-height: 1;
vertical-align: middle;
}
}
.icon_empty, .icon {
margin-right: 4px;
}
hr {
margin-top: 8px;
margin-bottom: 8px;
}
.arrow {
cursor: pointer;
pointer-events: all;
width: 7px;
height: 7px;
padding: 0;
margin-right: 5px;
margin-left: 5px;
.entry {
/*padding: 8px 12px;*/
padding-right: 12px;
cursor: pointer;
list-style-type: none;
transition: all .3s ease;
user-select: none;
align-items: center;
position: absolute;
right: 3px;
}
display: flex;
.sub-container {
padding-right: 3px;
position: relative;
&.disabled {
background-color: lightgray;
cursor: not-allowed;
}
&:hover {
.sub-menu {
&:hover:not(.disabled) {
background-color: #DEF;
}
}
.icon_empty, .icon {
margin-right: 4px;
}
.arrow {
cursor: pointer;
pointer-events: all;
width: 7px;
height: 7px;
padding: 0;
margin-right: 5px;
margin-left: 5px;
position: absolute;
right: 3px;
}
.sub-container {
padding-right: 3px;
position: relative;
&:hover {
.sub-menu {
display: block;
}
}
}
.sub-menu {
display: none;
left: 100%;
top: -4px;
position: absolute;
margin-left: 3px;
}
.checkbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 14px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 11px;
width: 11px;
background-color: #eee;
&:after {
content: "";
position: absolute;
display: none;
left: 4px;
top: 1px;
width: 3px;
height: 7px;
border: solid white;
border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
}
}
.sub-menu {
display: none;
left: 100%;
top: -4px;
position: absolute;
margin-left: 3px;
}
.checkbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 14px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 11px;
width: 11px;
background-color: #eee;
&:after {
content: "";
position: absolute;
display: none;
left: 4px;
top: 1px;
width: 3px;
height: 7px;
border: solid white;
border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
}
}

View File

@ -7,8 +7,7 @@ $background:lightgray;
flex-direction: row;
/* tmp fix for ultra small devices */
overflow-x: auto;
overflow-y: hidden;
overflow-y: visible;
.divider {
border-left:2px solid gray;
@ -46,6 +45,8 @@ $background:lightgray;
}
.button-dropdown {
position: relative;
.buttons {
display: flex;
flex-direction: row;
@ -104,7 +105,7 @@ $background:lightgray;
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
&.right {
right: 0;
}
.icon {

View File

@ -0,0 +1,175 @@
.modal-avatar-list {
display: flex;
flex-direction: row;
.container-list {
width: 50%;
margin-top: 5px;
display: flex;
flex-direction: column;
justify-content: stretch;
.column {
&.column-username {
width: calc(50% - 100px);
overflow: hidden;
text-overflow: ellipsis;
}
&.column-unique-id {
width: calc(50% - 100px);
overflow: hidden;
text-overflow: ellipsis;
}
&.column-size {
width: 75px;
flex-grow: 0;
flex-shrink: 0;
text-align: center;
}
&.column-timestamp {
width: 150px;
flex-grow: 0;
flex-shrink: 0;
text-align: center;
}
}
.list-header {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
.column {
border: 1px solid lightgray;
text-align: center;
}
}
.list-entries-container {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: start;
overflow-y: auto;
min-height: 250px;
.entry {
display: flex;
flex-direction: row;
.column {
margin-left: 2px;
}
cursor: pointer;
&.selected {
background-color: lightblue;
}
}
&.scrollbar {
.column-username {
width: calc(50% - 100px + 30px)
}
.column-unique-id {
width: calc(50% - 100px + 30px)
}
}
}
}
.container-info {
margin-left: 10px;
position: relative;
width: 50%;
.container-data {
width: 100%;
}
.container-preview {
display: flex;
flex-direction: row;
justify-content: stretch;
.container-image {
flex-shrink: 0;
flex-grow: 0;
width: 302px;
height: 302px;
background-color: whitesmoke;
border: 1px solid black;
border-radius: 2px;
position: relative;
overflow: hidden;
> div {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
}
}
.container-image-data {
margin-left: 10px;
flex-shrink: 1;
flex-grow: 1;
a {
text-align: center;
}
.form-group {
width: 100%;
margin-bottom: 0px;
}
}
}
.container-buttons {
width: 100%;
margin-top: 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.disabled-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: gray;
display: flex;
flex-direction: column;
justify-content: space-around;
a {
text-align: center;
}
}
}
}

View File

@ -1,3 +1,52 @@
.container-channel-edit-general {
.container-name-icon {
display: flex;
flex-direction: row;
justify-content: stretch;
.container-name {
flex-grow: 1;
flex-shrink: 1;
}
.container-icon {
flex-grow: 0;
flex-shrink: 0;
}
}
.container-icon {
width: 30px;
margin-left: 10px;
.button-select-icon {
left: 0;
right: 0;
top: 0;
bottom: 0;
position: absolute;
.icon-node {
cursor: pointer;
height: 100%;
width: 100%;
&:hover {
background-color: #00000011;
}
> div {
vertical-align: middle;
text-align: center;
}
}
}
}
}
.container-channel-settings-standard {
flex-grow: 1;
display: flex;

View File

@ -43,7 +43,7 @@
.container-password {
flex-grow: 0;
flex-shrink: 0;
flex-shrink: 4;
margin-left: 15px;
}
@ -61,7 +61,7 @@
.container-manage {
flex-grow: 0;
flex-shrink: 0;
flex-shrink: 4;
margin-left: 15px;
}

View File

@ -0,0 +1,150 @@
.modal-icon-select {
display: flex;
flex-direction: column;
justify-content: stretch;
.container-icons {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: row;
justify-content: stretch;
> div {
width: 50%;
&:not(:first-of-type) {
margin-left: 10px;
}
}
.content, .container-icons-list {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
}
.container-icons-list {
position: relative;
> div {
border-radius: 3px;
}
.container-icons-remote, .container-icons-local {
width: 100%;
min-height: 300px;
overflow-x: hidden;
overflow-y: auto;
background-color: whitesmoke;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.icon-container, .icon {
margin-left: 1px;
margin-right: 1px;
}
&.icon-select {
.icon-container, .icon {
cursor: pointer;
&:hover {
background-color: #00000011;
border: 1px solid black;
}
&.selected {
background-color: #00330011;
border: 1px solid red;
}
&:hover, &.selected {
width: 18px;
height: 18px;
margin: -1px 0px;
}
}
}
}
.container-loading, .container-no-permissions, .container-error {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
background-color: grey;
cursor: not-allowed;
text-align: center;
display: flex;
flex-direction: column;
justify-content: space-around;
> a {
padding-bottom: 30px;
}
}
.container-loading {
z-index: 40;
}
.container-error {
z-index: 30;
}
.container-no-permissions {
z-index: 20;
}
}
}
.container-buttons {
margin-top: 20px;
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
.spacer {
flex-grow: 1;
flex-shrink: 1;
}
.btn {
flex-grow: 0;
flex-shrink: 0;
}
.button-select {
margin-left: 10px;
display: flex;
align-items: center;
flex-direction: row;
.selected-item-container {
height: 16px;
vertical-align: sub;
}
}
.button-select-no-icon {
margin-left: 10px;
}
}
}

View File

@ -19,6 +19,53 @@
flex-shrink: 70;
}
}
.container-name-icon {
display: flex;
flex-direction: row;
justify-content: stretch;
.container-name {
flex-grow: 1;
flex-shrink: 1;
}
.container-icon {
flex-grow: 0;
flex-shrink: 0;
}
}
.container-icon {
width: 30px;
margin-left: 10px;
.button-select-icon {
left: 0;
right: 0;
top: 0;
bottom: 0;
position: absolute;
.icon-node {
cursor: pointer;
height: 100%;
width: 100%;
&:hover {
background-color: #00000011;
}
> div {
vertical-align: middle;
text-align: center;
}
}
}
}
}
.container-server-settings-host {
padding: 5px;

View File

@ -35,11 +35,12 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- App min width: 450px -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, min-zoom=1, max-zoom: 1, user-scalable=no">
<meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." />
<link rel="icon" href="img/favicon/teacup.png">
<!-- TODO Needs some fix -->
<!-- <link rel="manifest" href="manifest.json"> -->
<link rel="manifest" href="manifest.json">
<?php
if(!$WEB_CLIENT) {
@ -161,7 +162,7 @@
<div class="fulloverlay" id="critical-load">
<div class="container">
<img src="img/loading_error_right.svg" height="192px">
<h1 style="color: red">Ooops, we encountered some trouble while loading important files!</h1>
<h1 class="error" style="color: red">Ooops, we encountered some trouble while loading important files!</h1>
<h3 class="detail"></h3>
</div>
</div>

View File

@ -8,9 +8,8 @@
"sizes": "256x256"
}
],
"start_url": "?",
"start_url": "/?",
"background_color": "#18BC9C",
"display": "standalone",
"scope": "/",
"theme_color": "#18BC9C"
}

View File

@ -113,6 +113,14 @@
<div class="icon client-permission_overview"></div>
<a>{{tr "View/edit permissions" /}}</a>
</div>
<div class="btn_query_toggle" title="{{tr 'Show/hide server queries' /}}">
<div class="icon client-toggle_server_query_clients"></div>
<a class="query-text"></a>
</div>
<div class="btn_query_manage" title="{{tr 'Manage server queries' /}}">
<div class="icon client-server_query"></div>
<a>{{tr "Manage server queries" /}}</a>
</div>
</div>
</div>
@ -127,7 +135,7 @@
</div>
<!-- the query button -->
<div class="button-dropdown btn_query" title="{{tr 'Show/hide server queries' /}}">
<div class="hide-small button-dropdown btn_query" title="{{tr 'Show/hide server queries' /}}">
<div class="buttons">
<div class="button icon_x32 client-server_query btn_query_toggle"></div>
<div class="button-dropdown">
@ -135,7 +143,7 @@
</div>
</div>
<div class="dropdown display_left">
<div class="btn_query_toggle"><div class="icon client-toggle_server_query_clients"></div><a>{{tr "Show/hide server queries" /}}</a></div>
<div class="btn_query_toggle"><div class="icon client-toggle_server_query_clients"></div><a class="query-text"></a></div>
<div class="btn_query_manage"><div class="icon client-server_query"></div><a>{{tr "Manage server queries" /}}</a></div>
<!-- <div class="btn_query_create"><div class="icon client-away"></div><a>{{tr "Create server query login" /}}</a></div> -->
</div>
@ -317,7 +325,7 @@
<div class="invalid-feedback">{{tr "Selected profile is invalid. Select another one or fix the profile." /}}</div>
</div>
<span class="form-group bmd-form-group container-manage"> <!-- needed to match padding for floating labels -->
<button type="button" class="btn btn-raised button-manage-profiles">{{tr "Manage profiles" /}}</button>
<button type="button" class="btn btn-raised button-manage-profiles">{{tr "Profiles" /}}</button>
</span>
</div>
</modal-body>
@ -330,11 +338,23 @@
<!-- Template for channel create & edit-->
<script class="jsrender-template" id="tmpl_channel_edit" type="text/html">
<div class="align_column general_properties">
<div class="form-group">
<label class="bmd-label-static">{{tr "Name:" /}}</label>
<input class="form-control channel_name" value="{{>channel_name}}"/>
<div class="align_column general_properties container-channel-edit-general">
<div class="container-name-icon form-row">
<div class="container-name form-group">
<label>{{tr "Name:" /}}</label>
<input class="form-control channel_name" value="{{>channel_name}}"/>
</div>
<div class="container-icon form-group">
<label>{{tr "Icon:" /}}</label>
<input class="form-control">
<span class="bmd-form-group button-select-icon">
<div class="icon-node">
<node key="channel_icon"/>
</div>
</span>
</div>
</div>
<div class="form-group">
<label class="bmd-label-floating">{{tr "Channel password" /}}</label>
@ -696,9 +716,20 @@
</modal-header>
<modal-body>
<div class="container-server-settings-general">
<div class="form-group">
<label>{{tr "Name:" /}}</label>
<input class="form-control virtualserver_name" value="{{>virtualserver_name}}"/>
<div class="container-name-icon form-row">
<div class="container-name form-group">
<label>{{tr "Name:" /}}</label>
<input class="form-control virtualserver_name" value="{{>virtualserver_name}}"/>
</div>
<div class="container-icon form-group">
<label>{{tr "Icon:" /}}</label>
<input class="form-control">
<span class="bmd-form-group button-select-icon">
<div class="icon-node">
<node key="virtualserver_icon"/>
</div>
</span>
</div>
</div>
<div class="form-group">
<label>{{tr "Phonetic Name:" /}}</label>
@ -2912,28 +2943,201 @@
<script class="jsrender-template" id="tmpl_manage_bookmarks-create" type="text/html">
<div class="modal-bookmark-create">
<div class="property">
<div class="key">Bookmark Type:</div>
<select class="bookmark-type">
<div class="form-group">
<label class="bmd-label-floating">{{tr "Bookmark type:" /}}</label>
<select class="form-control bookmark-type">
<option value="bookmark">Bookmark</option>
<option value="directory">Directory</option>
</select>
</div>
<div class="property">
<div class="key">Parent Directory:</div>
<select class="bookmark-parent">
<div class="form-group">
<label class="bmd-label-floating">{{tr "Parent directory:" /}}</label>
<select class="form-control bookmark-parent">
<option bookmark-uuid=""></option>
</select>
</div>
<div class="property">
<div class="key">Bookmark Name:</div>
<input class="bookmark-name">
<div class="form-group">
<label class="bmd-label-floating">{{tr "Bookmark name" /}}</label>
<input class="form-control bookmark-name">
</div>
<hr>
<div class="buttons">
<button class="button-create">Create</button>
<button class="btn btn-success button-create">Create</button>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_icon_select" type="text/html">
<div class="modal-icon-select">
<div class="container-icons">
<div class="group_box">
<div class="header">{{tr "Remote" /}}</div>
<div class="content">
<div class="container-icons-list">
<div class="container-icons-remote {{if enable_select}}icon-select{{/if}}"></div>
<div class="container-loading">
<a>{{tr "loading..." /}}</a>
</div>
<div class="container-no-permissions">
<a>{{tr "You dont have permissions the view the icons" /}}</a>
</div>
<div class="container-error">
<a class="error-message">{{ŧr "An error occured" /}}</a>
</div>
</div>
<!--
<div class="container-buttons">
<button class="btn btn-success button-upload">{{tr "Upload" /}}</button>
<button class="btn btn-danger button-upload">{{tr "Delete" /}}</button>
</div>
-->
</div>
</div>
<div class="group_box">
<div class="header">{{tr "Local" /}}</div>
<div class="content">
<div class="container-icons-list">
<div class="container-icons-local {{if enable_select}}icon-select{{/if}}"></div>
</div>
</div>
</div>
</div>
<div class="container-buttons">
<button class="btn btn-primary btn-raised button-reload">{{tr "Reload" /}}</button>
<div class="spacer"></div>
{{if enable_select}}
<button class="btn btn-success btn-raised button-select-no-icon">{{tr "Remove icon" /}}</button>
<button class="btn btn-success btn-raised button-select"><a>{{tr "Select " /}}</a><div class="selected-item-container"></div></button>
{{/if}}
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_avatar_list" type="text/html">
<div class="modal-avatar-list">
<div class="container-list">
<div class="list-header">
<div class="column column-username">{{tr "Username" /}}</div>
<div class="column column-unique-id">{{tr "Unique ID" /}}</div>
<div class="column column-size">{{tr "Size" /}}</div>
<div class="column column-timestamp">{{tr "Date" /}}</div>
</div>
<div class="list-entries-container">
<div class="list-entries">
</div>
</div>
</div>
<div class="container-info">
<div class="container-data">
<div class="form-group">
<label class="bmd-label-static">{{tr "Username" /}}</label>
<input class="form-control property-username" disabled>
</div>
<div class="form-group">
<label class="bmd-label-static">{{tr "Unique ID" /}}</label>
<input class="form-control property-unique-id" disabled>
</div>
<div class="form-group">
<label class="bmd-label-static">{{tr "Avatar ID" /}}</label>
<input class="form-control property-avatar-id" disabled>
</div>
</div>
<div class="container-preview">
<div class="container-image"></div>
<div class="container-image-data">
<a>{{tr "Image info:" /}}</a>
<div class="form-group">
<label class="bmd-label-static">{{tr "Bytes" /}}</label>
<input class="form-control property-image-size" disabled>
</div>
<div class="form-group">
<label class="bmd-label-static">{{tr "Width" /}}</label>
<input class="form-control property-image-width" disabled>
</div>
<div class="form-group">
<label class="bmd-label-static">{{tr "Height" /}}</label>
<input class="form-control property-image-height" disabled>
</div>
<div class="form-group">
<label class="bmd-label-static">{{tr "Type" /}}</label>
<input class="form-control property-image-type" disabled>
</div>
</div>
</div>
<div class="container-buttons">
<button class="btn btn-danger button-delete">{{tr "Delete" /}}</button>
<button class="btn btn-success button-download">{{tr "Download" /}}</button>
</div>
<div class="disabled-overlay">
<a>{{tr "Please select a user" /}}</a>
</div>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_avatar_list-list_entry" type="text/html">
<div class="entry">
<div class="column column-username">{{>username}}</div>
<div class="column column-unique-id">{{>unique_id}}</div>
<div class="column column-size">{{>size}}</div>
<div class="column column-timestamp">{{>timestamp}}</div>
</div>
</script>
</body>
</html>
</html>
<!--
<script class="jsrender-template" id="tmpl_query_manager-list_entry" type="text/html">
<div class="entry">
<div class="column column-username">{{>username}}</div>
<div class="column column-unique-id">{{>unique_id}}</div>
<div class="column column-bound-server">{{>bounded_server}}</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_playlist_list" type="text/html">
<div class="playlist-management">
<div class="header">
<div class="form-group bmd-form-group buttons">
<button class="btn btn-success button button-playlist-create">{{tr "Create playlist" /}}</button>
<button class="btn btn-danger button button-playlist-delete">{{tr "Delete playlist" /}}</button>
<button class="btn btn-primary button-playlist-edit">{{tr "Edit playlist" /}}</button>
</div>
<div class="form-group search">
<label class="bmd-label-floating">{{tr "search" /}}</label>
<input class="form-control input input-search" type="text">
</div>
</div>
<div class="playlist-list">
<div class="playlist-list-header">
<div class="column column-id">{{tr "ID" /}}</div>
<div class="column column-title">{{tr "Title" /}}</div>
<div class="column column-creator">{{tr "Creator" /}}</div>
<div class="column column-type">{{tr "Type" /}}</div>
<div class="column column-used">{{tr "Used" /}}</div>
</div>
<div class="playlist-list-entries-container">
<div class="playlist-list-entries">
</div>
</div>
</div>
<div class="footer">
<div class="info">
<a>{{tr "loading..." /}}</a>
</div>
<div class="buttons">
<div class="form-group highlight-own">
<div class="switch">
<label>
<input type="checkbox" class="button-highlight-own">
{{tr "Highlight own playlists" /}}
</label>
</div>
</div>
<div class="form-group bmd-form-group">
<button class="btn btn-secondary btn-raised button-refresh">{{tr "Refresh" /}}</button>
</div>
</div>
</div>
</div>
</script>

View File

@ -233,8 +233,12 @@ class FileManager extends connection.AbstractCommandHandler {
console.error(tr("Invalid file list entry. Path: %s"), json[0]["path"]);
return;
}
for(let e of (json as Array<FileEntry>))
for(let e of (json as Array<FileEntry>)) {
e.datetime = parseInt(e.datetime + "");
e.size = parseInt(e.size + "");
e.type = parseInt(e.type + "");
entry.entries.push(e);
}
}
private notifyFileListFinished(json) {
@ -326,14 +330,14 @@ enum ImageType {
JPEG
}
function media_image_type(type: ImageType) {
function media_image_type(type: ImageType, file?: boolean) {
switch (type) {
case ImageType.BITMAP:
return "bmp";
case ImageType.GIF:
return "gif";
case ImageType.SVG:
return "svg+xml";
return file ? "svg" : "svg+xml";
case ImageType.JPEG:
return "jpeg";
case ImageType.UNKNOWN:
@ -440,7 +444,10 @@ class IconManager {
const media = media_image_type(type);
const blob = await response.blob();
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
if(blob.type !== "image/" + media)
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
else
return URL.createObjectURL(blob)
}
async resolved_cached?(id: number) : Promise<Icon> {
@ -463,41 +470,52 @@ class IconManager {
}
private async _load_icon(id: number) : Promise<Icon> {
let download_key: transfer.DownloadKey;
try {
download_key = await this.create_icon_download(id);
let download_key: transfer.DownloadKey;
try {
download_key = await this.create_icon_download(id);
} catch(error) {
console.error(tr("Could not request download for icon %d: %o"), id, error);
throw "Failed to request icon";
}
const downloader = new RequestFileDownload(download_key);
let response: Response;
try {
response = await downloader.request_file();
} catch(error) {
console.error(tr("Could not download icon %d: %o"), id, error);
throw "failed to download icon";
}
const type = image_type(response.headers.get('X-media-bytes'));
const media = media_image_type(type);
await this.cache.put_cache('icon_' + id, response.clone(), "image/" + media);
const url = (this._id_urls[id] = await this._response_url(response.clone()));
this._loading_promises[id] = undefined;
return {
id: id,
url: url
};
} catch(error) {
console.error(tr("Could not request download for icon %d: %o"), id, error);
throw "Failed to request icon";
setTimeout(() => {
this._loading_promises[id] = undefined;
}, 1000 * 60); /* try again in 60 seconds */
throw error;
}
const downloader = new RequestFileDownload(download_key);
let response: Response;
try {
response = await downloader.request_file();
} catch(error) {
console.error(tr("Could not download icon %d: %o"), id, error);
throw "failed to download icon";
}
const type = image_type(response.headers.get('X-media-bytes'));
const media = media_image_type(type);
await this.cache.put_cache('icon_' + id, response.clone(), "image/" + media);
const url = (this._id_urls[id] = await this._response_url(response.clone()));
this._loading_promises[id] = undefined;
return {
id: id,
url: url
};
}
loadIcon(id: number) : Promise<Icon> {
return this._loading_promises[id] || (this._loading_promises[id] = this._load_icon(id));
}
generateTag(id: number) : JQuery<HTMLDivElement> {
generateTag(id: number, options?: {
animate?: boolean
}) : JQuery<HTMLDivElement> {
options = options || {};
if(id == 0)
return $.spawn("div").addClass("icon_empty");
else if(id < 1000)
@ -529,13 +547,18 @@ class IconManager {
throw "failed to download icon";
icon_image.attr("src", icon.url);
icon_image.css("opacity", 0);
icon_container.append(icon_image).removeClass("icon_empty");
icon_load_image.animate({opacity: 0}, 50, function () {
if(typeof(options.animate) !== "boolean" || options.animate) {
icon_image.css("opacity", 0);
icon_load_image.animate({opacity: 0}, 50, function () {
icon_load_image.detach();
icon_image.animate({opacity: 1}, 150);
});
} else {
icon_load_image.detach();
icon_image.animate({opacity: 1}, 150);
});
}
})().catch(reason => {
console.error(tr("Could not load icon %o. Reason: %s"), id, reason);
icon_load_image.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon " + id);
@ -550,6 +573,7 @@ class Avatar {
client_avatar_id: string; /* the base64 uid thing from a-m */
avatar_id: string; /* client_flag_avatar */
url: string;
type: ImageType;
}
class AvatarManager {
@ -565,15 +589,16 @@ class AvatarManager {
this.cache = new CacheManager("avatars");
}
private async _response_url(response: Response) : Promise<string> {
private async _response_url(response: Response, type: ImageType) : Promise<string> {
if(!response.headers.has('X-media-bytes'))
throw "missing media bytes";
const type = image_type(response.headers.get('X-media-bytes'));
const media = media_image_type(type);
const blob = await response.blob();
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
if(blob.type !== "image/" + media)
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
else
return URL.createObjectURL(blob);
}
async resolved_cached?(client_avatar_id: string, avatar_id?: string) : Promise<Avatar> {
@ -595,10 +620,12 @@ class AvatarManager {
if(typeof(avatar_id) === "string" && response_avatar_id != avatar_id)
return undefined;
const type = image_type(response.headers.get('X-media-bytes'));
return this._cached_avatars[client_avatar_id] = {
client_avatar_id: client_avatar_id,
avatar_id: avatar_id || response_avatar_id,
url: await this._response_url(response)
url: await this._response_url(response, type),
type: type
};
}
@ -631,13 +658,14 @@ class AvatarManager {
await this.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, {
"X-avatar-id": avatar_id
});
const url = await this._response_url(response.clone());
const url = await this._response_url(response.clone(), type);
this._loading_promises[client_avatar_id] = undefined;
return this._cached_avatars[client_avatar_id] = {
client_avatar_id: client_avatar_id,
avatar_id: avatar_id,
url: url
url: url,
type: type
};
}
@ -645,9 +673,15 @@ class AvatarManager {
return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_id));
}
generateTag(client: ClientEntry) : JQuery {
const client_avatar_id = client.avatarId();
const avatar_id = client.properties.client_flag_avatar;
generate_client_tag(client: ClientEntry) : JQuery {
return this.generate_tag(client.avatarId(), client.properties.client_flag_avatar);
}
generate_tag(client_avatar_id: string, avatar_id?: string, options?: {
callback_image?: (tag: JQuery<HTMLImageElement>) => any,
callback_avatar?: (avatar: Avatar) => any
}) : JQuery {
options = options || {};
let avatar_container = $.spawn("div");
let avatar_image = $.spawn("img").attr("alt", tr("Client avatar"));
@ -656,6 +690,10 @@ class AvatarManager {
if(cached_avatar && cached_avatar.avatar_id == avatar_id) {
avatar_image.attr("src", cached_avatar.url);
avatar_container.append(avatar_image);
if(options.callback_image)
options.callback_image(avatar_image);
if(options.callback_avatar)
options.callback_avatar(cached_avatar);
} else {
let loader_image = $.spawn("img");
loader_image.attr("src", "img/loading_image.svg").css("width", "75%");
@ -672,17 +710,26 @@ class AvatarManager {
if(!avatar)
avatar = await this.loadAvatar(client_avatar_id, avatar_id)
if(!avatar)
throw "failed to load avatar";
if(options.callback_avatar)
options.callback_avatar(avatar);
avatar_image.attr("src", avatar.url);
avatar_image.css("opacity", 0);
avatar_container.append(avatar_image);
loader_image.animate({opacity: 0}, 50, function () {
$(this).detach();
avatar_image.animate({opacity: 1}, 150);
loader_image.animate({opacity: 0}, 50, () => {
loader_image.detach();
avatar_image.animate({opacity: 1}, 150, () => {
if(options.callback_image)
options.callback_image(avatar_image);
});
});
})().catch(reason => {
console.error(tr("Could not load avatar for %s. Reason: %s"), client.clientNickName(), reason);
console.error(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.clientNickName());
loader_image.addClass("icon client-warning").attr("tag", tr("Could not load avatar ") + client_avatar_id);
})
}

View File

@ -1,7 +1,7 @@
namespace connection {
export class CommandHelper extends AbstractCommandHandler {
private _callbacks_namefromuid: ClientNameFromUid[] = [];
private _who_am_i: any;
private _awaiters_unique_ids: {[unique_id: string]:((resolved: ClientNameInfo) => any)[]} = {};
constructor(connection) {
super(connection);
@ -46,24 +46,50 @@ namespace connection {
return this.connection.send_command("clientupdate", data);
}
info_from_uid(...uid: string[]) : Promise<ClientNameInfo[]> {
let uids = [...uid];
for(let p of this._callbacks_namefromuid)
if(p.keys == uids) return p.promise;
async info_from_uid(..._unique_ids: string[]) : Promise<ClientNameInfo[]> {
const response: ClientNameInfo[] = [];
const request = [];
const unique_ids = new Set(_unique_ids);
const unique_id_resolvers: {[unique_id: string]: (resolved: ClientNameInfo) => any} = {};
let req: ClientNameFromUid = {} as any;
req.keys = uids;
req.response = new Array(uids.length);
req.promise = new LaterPromise<ClientNameInfo[]>();
for(let uid of uids) {
this.connection.send_command("clientgetnamefromuid", {
cluid: uid
}).catch(req.promise.function_rejected());
for(const unique_id of unique_ids) {
request.push({'cluid': unique_id});
(this._awaiters_unique_ids[unique_id] || (this._awaiters_unique_ids[unique_id] = []))
.push(unique_id_resolvers[unique_id] = info => response.push(info));
}
this._callbacks_namefromuid.push(req);
return req.promise;
try {
await this.connection.send_command("clientgetnamefromuid", request);
} catch(error) {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) {
/* nothing */
} else {
throw error;
}
} finally {
/* cleanup */
for(const unique_id of Object.keys(unique_id_resolvers))
(this._awaiters_unique_ids[unique_id] || []).remove(unique_id_resolvers[unique_id]);
}
return response;
}
private handle_notifyclientnamefromuid(json: any[]) {
for(const entry of json) {
const info: ClientNameInfo = {
client_unique_id: entry["cluid"],
client_nickname: entry["clname"],
client_database_id: parseInt(entry["cldbid"])
};
const functions = this._awaiters_unique_ids[entry["cluid"]] || [];
delete this._awaiters_unique_ids[entry["cluid"]];
for(const fn of functions)
fn(info);
}
}
request_query_list(server_id: number = undefined) : Promise<QueryList> {
@ -284,28 +310,5 @@ namespace connection {
});
});
}
private handle_notifyclientnamefromuid(json: any[]) {
for(let entry of json) {
let info: ClientNameInfo = {} as any;
info.client_unique_id = entry["cluid"];
info.client_nickname = entry["clname"];
info.client_database_id = parseInt(entry["cldbid"]);
for(let elm of this._callbacks_namefromuid.slice(0)) {
let unset = 0;
for(let index = 0; index < elm.keys.length; index++) {
if(elm.keys[index] == info.client_unique_id) {
elm.response[index] = info;
}
if(elm.response[index] == undefined) unset++;
}
if(unset == 0) {
this._callbacks_namefromuid.remove(elm);
elm.promise.resolved(elm.response);
}
}
}
}
}
}

View File

@ -15,7 +15,7 @@ function despawn_context_menu() {
let menu = context_menu || (context_menu = $(".context-menu"));
if(!menu.is(":visible")) return;
menu.hide(100);
menu.animate({opacity: 0}, 100, () => menu.css("display", "none"));
if(contextMenuCloseFn) contextMenuCloseFn();
}
@ -110,9 +110,10 @@ function generate_tag(entry: ContextMenuEntry) : JQuery {
}
function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) {
let menu = context_menu || (context_menu = $(".context-menu"));
menu.finish().empty();
let menu_tag = context_menu || (context_menu = $(".context-menu"));
menu_tag.finish().empty().css("opacity", "0");
const menu_container = $.spawn("div").addClass("context-menu-container");
contextMenuCloseFn = undefined;
for(const entry of entries){
@ -122,12 +123,18 @@ function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) {
if(entry.type == MenuEntryType.CLOSE) {
contextMenuCloseFn = entry.callback;
} else
menu.append(generate_tag(entry));
menu_container.append(generate_tag(entry));
}
menu.show(100);
menu_tag.append(menu_container);
menu_tag.animate({opacity: 1}, 100).css("display", "block");
const width = menu_container.visible_width();
if(x + width + 5 > window.innerWidth)
menu_container.addClass("left");
// In the right position (the mouse)
menu.css({
menu_tag.css({
"top": y + "px",
"left": x + "px"
});

View File

@ -351,16 +351,23 @@ namespace loader {
/* define that here */
let _critical_triggered = false;
const display_critical_load = message => {
const display_critical_load = (message: string, error?: string) => {
if(_critical_triggered) return; /* only show the first error */
_critical_triggered = true;
let tag = document.getElementById("critical-load");
let detail = tag.getElementsByClassName("detail")[0];
detail.innerHTML = message;
if(error) {
const error_tags = tag.getElementsByClassName("error");
error_tags[0].innerHTML = error;
}
//error-message
tag.style.display = "block";
fadeoutLoader();
_fadeout_warned = true; /* we know that JQuery hasn't been loaded, else this function would be replaced by something else */
};
const loader_impl_display_critical_error = message => {
@ -498,6 +505,7 @@ const loader_javascript = {
"js/profiles/Identity.js",
//Load UI
"js/ui/modal/ModalAvatarList.js",
"js/ui/modal/ModalQuery.js",
"js/ui/modal/ModalQueryManage.js",
"js/ui/modal/ModalPlaylistList.js",
@ -509,7 +517,7 @@ const loader_javascript = {
"js/ui/modal/ModalServerEdit.js",
"js/ui/modal/ModalChangeVolume.js",
"js/ui/modal/ModalBanClient.js",
"js/ui/modal/ModalIconSelect.js",
"js/ui/modal/ModalBanCreate.js",
"js/ui/modal/ModalBanList.js",
"js/ui/modal/ModalYesNo.js",
@ -636,6 +644,8 @@ const loader_style = {
"css/static/ts/country.css",
"css/static/general.css",
"css/static/modals.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",
@ -770,26 +780,6 @@ function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = und
});
}
window["Module"] = window["Module"] || {};
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(navigator.browserSpecs); //Object { name: "Firefox", version: "42" }
/* register tasks */
loader.register_task(loader.Stage.INITIALIZING, {
name: "safari fix",
@ -808,6 +798,7 @@ loader.register_task(loader.Stage.INITIALIZING, {
priority: 50
});
window["Module"] = window["Module"] || {};
/* TeaClient */
if(window.require) {
const path = require("path");
@ -825,6 +816,42 @@ if(window.require) {
});
}
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(location.protocol !== 'https:' && location.hostname !== 'localhost') {
display_critical_load("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!");
throw "App requires to be loaded via HTTPS!"
}
},
priority: 20
});
loader.register_task(loader.Stage.INITIALIZING, {
name: "webassembly tester",
function: loader_webassembly.test_webassembly,
@ -872,17 +899,92 @@ loader.register_task(loader.Stage.LOADED, {
loader.register_task(loader.Stage.LOADED, {
name: "error task",
function: async () => {
if(Settings.instance.static("dummy_load_error", false)) {
displayCriticalError("The tea is cold!");
if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) {
display_critical_load("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good.");
throw "The tea is cold!";
}
},
priority: 20
});
const hello_world = () => {
const print_security = () => {
{
const css = [
"display: block",
"text-align: center",
"font-size: 42px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: red"
].join(";");
console.log("%c ", "font-size: 100px;");
console.log("%cSecurity warning:", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 18px",
"font-weight: bold"
].join(";");
console.log("%cPasting anything in here could give attackers access to your data.", css);
console.log("%cUnless you understand exactly what you are doing, close this window and stay safe.", css);
console.log("%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(";");
console.log("%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 ""; }
console.log("%cLovely to see you using and debugging the TeaSpeak Web client.", css);
console.log("%cIf you have some good ideas or already done some incredible changes,", css);
console.log("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2);
console.log("%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("}"));
eval(hello_world_code);
} catch(e) {
hello_world();
}
loader.execute().then(() => {
console.log("app successfully loaded!");
}).catch(error => {
displayCriticalError("failed to load app!<br>Please lookup the browser console for more details");
console.error("Failed to load app!\nError: %o", error);
/* console.error("Failed to load app!\nError: %o", error); */ //Error should be already printed by the loader
});

View File

@ -46,6 +46,12 @@ namespace log {
[LogCategory.IDENTITIES, true]
]);
enum GroupMode {
NATIVE,
PREFIX
}
const group_mode: GroupMode = GroupMode.NATIVE;
loader.register_task(loader.Stage.LOADED, {
name: "log enabled initialisation",
function: async () => initialize(),
@ -112,12 +118,17 @@ namespace log {
name = "[%s] " + name;
optionalParams.unshift(category_mapping.get(category));
return new Group(GroupMode.PREFIX, level, category, name, optionalParams);
return new Group(group_mode, level, category, name, optionalParams);
}
enum GroupMode {
NATIVE,
PREFIX
export function table(title: string, arguments: any) {
if(group_mode == GroupMode.NATIVE) {
console.groupCollapsed(title);
console.table(arguments);
console.groupEnd();
} else {
console.log("Snipped table %s", title);
}
}
export class Group {
@ -130,7 +141,7 @@ namespace log {
private readonly name: string;
private readonly optionalParams: any[][];
private _collapsed: boolean = true;
private _collapsed: boolean = false;
private initialized = false;
private _log_prefix: string;

View File

@ -282,6 +282,12 @@ function main() {
stats.register_user_count_listener(status => {
console.log("Received user count update: %o", status);
});
/*
setTimeout(() => {
Modals.spawnAvatarList(globalClient);
}, 1000);
*/
}
loader.register_task(loader.Stage.LOADED, {

View File

@ -550,6 +550,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
this._group_mapping = PermissionManager.group_mapping.slice();
let group = log.group(log.LogType.TRACE, LogCategory.PERMISSIONS, tr("Permission mapping"));
const table_entries = [];
for(let e of json) {
if(e["group_id_end"]) {
let group = new PermissionGroup();
@ -564,15 +565,22 @@ class PermissionManager extends connection.AbstractCommandHandler {
group.deep = info.deep;
}
this.permissionGroups.push(group);
continue;
}
let perm = new PermissionInfo();
perm.name = e["permname"];
perm.id = parseInt(e["permid"]);
perm.description = e["permdesc"];
group.log(tr("%i <> %s -> %s"), perm.id, perm.name, perm.description);
this.permissionList.push(perm);
table_entries.push({
"id": perm.id,
"name": perm.name,
"description": perm.description
});
}
log.table("Permission list", table_entries);
group.end();
log.info(LogCategory.PERMISSIONS, tr("Got %i permissions"), this.permissionList.length);
@ -594,6 +602,8 @@ class PermissionManager extends connection.AbstractCommandHandler {
let addcount = 0;
let group = log.group(log.LogType.TRACE, LogCategory.PERMISSIONS, tr("Got %d needed permissions."), json.length);
const table_entries = [];
for(let e of json) {
let entry: NeededPermissionValue = undefined;
for(let p of copy) {
@ -618,11 +628,16 @@ class PermissionManager extends connection.AbstractCommandHandler {
if(entry.value == parseInt(e["permvalue"])) continue;
entry.value = parseInt(e["permvalue"]);
//TODO tr
group.log("Update needed permission " + entry.type.name + " to " + entry.value);
for(let listener of entry.changeListener)
listener(entry.value);
table_entries.push({
"permission": entry.type.name,
"value": entry.value
});
}
log.table("Needed client permissions", table_entries);
group.end();
//TODO tr

View File

@ -137,6 +137,11 @@ class Settings extends StaticSettings {
description: 'Disables the voice bridge. If disabled, the audio and codec workers aren\'t required anymore'
};
static readonly KEY_LOAD_DUMMY_ERROR: SettingsKey<boolean> = {
key: 'dummy_load_error',
description: 'Triggers a loading error at the end of the loading process.'
};
/* Control bar */
static readonly KEY_CONTROL_MUTE_INPUT: SettingsKey<boolean> = {
key: 'mute_input'

View File

@ -663,13 +663,22 @@ class ChannelEntry {
updateVariables(...variables: {key: string, value: string}[]) {
let group = log.group(log.LogType.DEBUG, LogCategory.CHANNEL_PROPERTIES, tr("Update properties (%i) of %s (%i)"), variables.length, this.channelName(), this.getChannelId());
{
const entries = [];
for(const variable of variables)
entries.push({
key: variable.key,
value: variable.value,
type: typeof (this.properties[variable.key])
});
log.table("Clannel update properties", entries);
}
for(let variable of variables) {
let key = variable.key;
let value = variable.value;
JSON.map_field_to(this.properties, value, variable.key);
group.log(tr("Updating property %s = '%s' -> %o"), key, value, this.properties[key]);
if(key == "channel_name") {
this.__updateChannelName();
} else if(key == "channel_order") {

View File

@ -581,11 +581,20 @@ class ClientEntry {
let update_away = false;
let reorder_channel = false;
{
const entries = [];
for(const variable of variables)
entries.push({
key: variable.key,
value: variable.value,
type: typeof (this.properties[variable.key])
});
log.table("Client update properties", entries);
}
for(let variable of variables) {
JSON.map_field_to(this._properties, variable.value, variable.key);
//TODO tr
group.log("Updating client " + this.clientId() + ". Key " + variable.key + " Value: '" + variable.value + "' (" + typeof (this.properties[variable.key]) + ")");
if(variable.key == "client_nickname") {
this.tag.find(".client-name").text(variable.value);
let chat = this.chat(false);

View File

@ -48,12 +48,10 @@ class ControlBar {
tag.find(".button-dropdown").on('click', () => {
tag.addClass("displayed");
}).hover(() => {
console.log("Add");
tag.addClass("displayed");
}, () => {
if(tag.find(".dropdown:hover").length > 0)
return;
console.log("Removed");
tag.removeClass("displayed");
});
tag.on('mouseleave', () => {
@ -88,9 +86,10 @@ class ControlBar {
let query = this.htmlTag.find(".btn_query");
dropdownify(query);
query.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
query.find(".btn_query_create").on('click', this.on_query_create.bind(this));
query.find(".btn_query_manage").on('click', this.on_query_manage.bind(this));
/* search for query buttons not only on the large device button */
this.htmlTag.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
this.htmlTag.find(".btn_query_create").on('click', this.on_query_create.bind(this));
this.htmlTag.find(".btn_query_manage").on('click', this.on_query_manage.bind(this));
}
/* Mobile dropdowns */
@ -429,7 +428,6 @@ class ControlBar {
}
set query_visible(flag: boolean) {
console.error(flag);
if(this._query_visible == flag) return;
this._query_visible = flag;
@ -444,7 +442,12 @@ class ControlBar {
}
private update_query_visibility_button() {
this.htmlTag.find(".btn_query_toggle").toggleClass('activated', this._query_visible);
const button = this.htmlTag.find(".btn_query_toggle");
button.toggleClass('activated', this._query_visible);
if(this._query_visible)
button.find(".query-text").text(tr("Hide server queries"));
else
button.find(".query-text").text(tr("Show server queries"));
}
private on_query_create() {

View File

@ -149,6 +149,7 @@ class Hostbanner {
readonly client: TSClient;
private updater: NodeJS.Timer;
private _hostbanner_url: string;
constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) {
this.client = client;
@ -237,16 +238,13 @@ class Hostbanner {
}
}
const url = URL.createObjectURL(await result.blob());
if(this._hostbanner_url) {
log.debug(LogCategory.SERVER, tr("Revoked old hostbanner url %s"), this._hostbanner_url);
URL.revokeObjectURL(this._hostbanner_url);
}
const url = (this._hostbanner_url = URL.createObjectURL(await result.blob()));
tag_image.css('background-image', 'url(' + url + ')');
log.debug(LogCategory.SERVER, tr("Fetsched hostbanner successfully (%o, type: %o, url: %o)"), Date.now() - start, result.type, url);
if(URL.revokeObjectURL) {
setTimeout(() => {
log.debug(LogCategory.SERVER, tr("Revoked hostbanner url %s"), url);
URL.revokeObjectURL(url);
}, 10000);
}
} catch(error) {
log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), error);
}
@ -325,7 +323,7 @@ class ClientInfoManager extends InfoManager<ClientEntry> {
}
if(client.properties.client_flag_avatar && client.properties.client_flag_avatar.length > 0) {
properties["client_avatar"] = client.channelTree.client.fileManager.avatars.generateTag(client);
properties["client_avatar"] = client.channelTree.client.fileManager.avatars.generate_client_tag(client);
}
return properties;
}

View File

@ -0,0 +1,162 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
const avatar_to_uid = (id: string) => {
const buffer = new Uint8Array(id.length / 2);
for(let index = 0; index < id.length; index += 2) {
const upper_nibble = id.charCodeAt(index) - 97;
const lower_nibble = id.charCodeAt(index + 1) - 97;
buffer[index / 2] = (upper_nibble << 4) | lower_nibble;
}
return base64ArrayBuffer(buffer);
};
export const human_file_size = (size: number) => {
if(size < 1000)
return size + "B";
const exp = Math.floor(Math.log2(size) / 10);
return (size / Math.pow(1024, exp)).toFixed(2) + 'KMGTPE'.charAt(exp - 1) + "iB";
};
export function spawnAvatarList(client: TSClient) {
const modal = createModal({
header: tr("Avatars"),
footer: undefined,
body: () => {
const template = $("#tmpl_avatar_list").renderTag({});
return template;
}
});
let callback_download: () => any;
let callback_delete: () => any;
const button_download = modal.htmlTag.find(".button-download");
const button_delete = modal.htmlTag.find(".button-delete");
const container_list = modal.htmlTag.find(".container-list .list-entries-container");
const list_entries = container_list.find(".list-entries");
const container_info = modal.htmlTag.find(".container-info");
const overlay_no_user = container_info.find(".disabled-overlay").show();
const set_selected_avatar = (unique_id: string, avatar_id: string, size: number) => {
button_download.prop("disabled", true);
callback_download = undefined;
if(!unique_id) {
overlay_no_user.show();
return;
}
const tag_username = container_info.find(".property-username");
const tag_unique_id = container_info.find(".property-unique-id");
const tag_avatar_id = container_info.find(".property-avatar-id");
const container_avatar = container_info.find(".container-image");
const tag_image_bytes = container_info.find(".property-image-size");
const tag_image_width = container_info.find(".property-image-width").val(tr("loading..."));
const tag_image_height = container_info.find(".property-image-height").val(tr("loading..."));
const tag_image_type = container_info.find(".property-image-type").val(tr("loading..."));
tag_username.val("unknown");
tag_unique_id.val(unique_id);
tag_avatar_id.val(avatar_id);
tag_image_bytes.val(size);
container_avatar.empty().append(client.fileManager.avatars.generate_tag(avatar_id, undefined, {
callback_image: image => {
tag_image_width.val(image[0].naturalWidth + 'px');
tag_image_height.val(image[0].naturalHeight + 'px');
},
callback_avatar: avatar => {
tag_image_type.val(media_image_type(avatar.type));
button_download.prop("disabled", false);
callback_download = () => {
const element = $.spawn("a")
.text("download")
.attr("href", avatar.url)
.attr("download", "avatar-" + unique_id + "." + media_image_type(avatar.type, true))
.css("display", "none")
.appendTo($("body"));
element[0].click();
element.detach();
};
}
}));
callback_delete = () => {
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this avatar?"), result => {
if(result) {
createErrorModal(tr("Not implemented"), tr("Avatar delete hasn't implemented yet")).open();
//TODO Implement avatar delete
}
});
};
overlay_no_user.hide();
};
set_selected_avatar(undefined, undefined, 0);
const update_avatar_list = () => {
const template_entry = $("#tmpl_avatar_list-list_entry");
list_entries.empty();
client.fileManager.requestFileList("/").then(files => {
const username_resolve: {[unique_id: string]:((name:string) => any)[]} = {};
for(const entry of files) {
const avatar_id = entry.name.substr('avatar_'.length);
const unique_id = avatar_to_uid(avatar_id);
const tag = template_entry.renderTag({
username: 'loading',
unique_id: unique_id,
size: human_file_size(entry.size),
timestamp: moment(entry.datetime * 1000).format('YY-MM-DD HH:mm')
});
(username_resolve[unique_id] || (username_resolve[unique_id] = [])).push(name => {
const tag_username = tag.find(".column-username").empty();
if(name) {
tag_username.append(ClientEntry.chatTag(0, name, unique_id, false));
} else {
tag_username.text("unknown");
}
});
list_entries.append(tag);
tag.on('click', () => {
list_entries.find('.selected').removeClass('selected');
tag.addClass('selected');
set_selected_avatar(unique_id, avatar_id, entry.size);
});
}
if(container_list.hasScrollBar())
container_list.addClass("scrollbar");
client.serverConnection.command_helper.info_from_uid(...Object.keys(username_resolve)).then(result => {
for(const info of result) {
username_resolve[info.client_unique_id].forEach(e => e(info.client_nickname));
delete username_resolve[info.client_unique_id];
}
for(const uid of Object.keys(username_resolve)) {
(username_resolve[uid] || []).forEach(e => e(undefined));
}
}).catch(error => {
log.error(LogCategory.GENERAL, tr("Failed to fetch usernames from avatar names. Error: %o"), error);
createErrorModal(tr("Failed to fetch usernames"), tr("Failed to fetch usernames related to their avatar names"), undefined).open();
})
}).catch(error => {
//TODO: Display no perms error
log.error(LogCategory.GENERAL, tr("Failed to receive avatar list. Error: %o"), error);
createErrorModal(tr("Failed to list avatars"), tr("Failed to receive avatar list."), undefined).open();
});
};
button_download.on('click', () => (callback_download || (() => {}))());
button_delete.on('click', () => (callback_delete || (() => {}))());
setTimeout(() => update_avatar_list(), 250);
modal.open();
}
}

View File

@ -6,10 +6,14 @@ namespace Modals {
const modal = createModal({
header: channel ? tr("Edit channel") : tr("Create channel"),
body: () => {
let template = $("#tmpl_channel_edit").renderTag(channel ? channel.properties : {
const render_properties = {};
Object.assign(render_properties, channel ? channel.properties : {
channel_flag_maxfamilyclients_unlimited: true,
channel_flag_maxclients_unlimited: true
} as ChannelProperties);
channel_flag_maxclients_unlimited: true,
});
render_properties["channel_icon"] = globalClient.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0);
let template = $("#tmpl_channel_edit").renderTag(render_properties);
return template.tabify();
},
footer: () => {
@ -32,7 +36,7 @@ namespace Modals {
});
applyGeneralListener(properties, modal.htmlTag.find(".general_properties"), modal.htmlTag.find(".button_ok"), !channel);
applyGeneralListener(properties, modal.htmlTag.find(".general_properties"), modal.htmlTag.find(".button_ok"), channel);
applyStandardListener(properties, modal.htmlTag.find(".settings_standard"), modal.htmlTag.find(".button_ok"), parent, !channel);
applyPermissionListener(properties, modal.htmlTag.find(".settings_permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
applyAudioListener(properties, modal.htmlTag.find(".container-channel-settings-audio"), modal.htmlTag.find(".button_ok"), channel);
@ -68,7 +72,7 @@ namespace Modals {
modal.htmlTag.find(".channel_name").focus();
}
function applyGeneralListener(properties: ChannelProperties, tag: JQuery, button: JQuery, create: boolean) {
function applyGeneralListener(properties: ChannelProperties, tag: JQuery, button: JQuery, channel: ChannelEntry | undefined) {
let updateButton = () => {
if(tag.find(".input_error").length == 0)
button.removeAttr("disabled");
@ -82,7 +86,18 @@ namespace Modals {
if(this.value.length < 1 || this.value.length > 40)
$(this).addClass("input_error");
updateButton();
}).prop("disabled", !create && !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NAME).granted(1));
}).prop("disabled", channel && !globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NAME).granted(1));
tag.find(".button-select-icon").on('click', event => {
Modals.spawnIconSelect(globalClient, id => {
const icon_node = tag.find(".button-select-icon").find(".icon-node");
icon_node.empty();
icon_node.append(globalClient.fileManager.icons.generateTag(id));
console.log("Selected icon ID: %d", id);
properties.channel_icon_id = id;
}, channel ? channel.properties.channel_icon_id : 0);
});
tag.find(".channel_password").change(function (this: HTMLInputElement) {
properties.channel_flag_password = this.value.length != 0;
@ -94,17 +109,17 @@ namespace Modals {
if(globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1))
$(this).addClass("input_error");
updateButton();
}).prop("disabled", !globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_PASSWORD : PermissionType.B_CHANNEL_MODIFY_PASSWORD).granted(1));
}).prop("disabled", !globalClient.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_PASSWORD : PermissionType.B_CHANNEL_MODIFY_PASSWORD).granted(1));
tag.find(".channel_topic").change(function (this: HTMLInputElement) {
properties.channel_topic = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1));
}).prop("disabled", !globalClient.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1));
tag.find(".channel_description").change(function (this: HTMLInputElement) {
properties.channel_description = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1));
}).prop("disabled", !globalClient.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1));
if(create) {
if(!channel) {
setTimeout(() => {
tag.find(".channel_name").trigger("change");
tag.find(".channel_password").trigger('change');

View File

@ -0,0 +1,144 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
//TODO Upload/delete button
export function spawnIconSelect(client: TSClient, callback_icon?: (id: number) => any, selected_icon?: number) {
callback_icon = callback_icon || (() => {});
selected_icon = selected_icon || 0;
const modal = createModal({
header: tr("Icons"),
footer: undefined,
body: () => {
const template = $("#tmpl_icon_select").renderTag({
enable_select: !!callback_icon
});
return template;
}
});
const button_select = modal.htmlTag.find(".button-select");
const container_loading = modal.htmlTag.find(".container-loading").hide();
const container_no_permissions = modal.htmlTag.find(".container-no-permissions").hide();
const container_error = modal.htmlTag.find(".container-error").hide();
const selected_container = modal.htmlTag.find(".selected-item-container");
const update_local_icons = (icons: number[]) => {
const container_icons = modal.htmlTag.find(".container-icons .container-icons-local");
container_icons.empty();
for(const icon_id of icons) {
const tag = client.fileManager.icons.generateTag(icon_id, {animate: false}).attr('title', "Icon " + icon_id);
if(callback_icon) {
tag.on('click', event => {
container_icons.find(".selected").removeClass("selected");
tag.addClass("selected");
selected_container.empty().append(tag.clone());
selected_icon = icon_id;
button_select.prop("disabled", false);
});
tag.on('dblclick', event => {
callback_icon(icon_id);
modal.close();
});
if(icon_id == selected_icon)
tag.trigger('click');
}
tag.appendTo(container_icons);
}
};
const update_remote_icons = () => {
container_no_permissions.hide();
container_error.hide();
container_loading.show();
const display_remote_error = (error?: string) => {
if(typeof(error) === "string") {
container_error.find(".error-message").text(error);
container_error.show();
} else {
container_error.hide();
}
};
client.fileManager.requestFileList("/icons").then(icons => {
const container_icons = modal.htmlTag.find(".container-icons");
const container_icons_remote = container_icons.find(".container-icons-remote");
const container_icons_remote_parent = container_icons_remote.parent();
container_icons_remote.detach().empty();
const chunk_size = 50;
const icon_chunks: FileEntry[][] = [];
let index = 0;
while(icons.length > index) {
icon_chunks.push(icons.slice(index, index + chunk_size));
index += chunk_size;
}
const process_next_chunk = () => {
const chunk = icon_chunks.pop_front();
if(!chunk) return;
for(const icon of chunk) {
const icon_id = parseInt(icon.name.substr("icon_".length));
if(icon_id == NaN) {
log.warn(LogCategory.GENERAL, tr("Received an unparsable icon within icon list (%o)"), icon);
continue;
}
const tag = client.fileManager.icons.generateTag(icon_id, {animate: false}).attr('title', "Icon " + icon_id);
if(callback_icon) {
tag.on('click', event => {
container_icons.find(".selected").removeClass("selected");
tag.addClass("selected");
selected_container.empty().append(tag.clone());
selected_icon = icon_id;
button_select.prop("disabled", false);
});
tag.on('dblclick', event => {
callback_icon(icon_id);
modal.close();
});
if(icon_id == selected_icon)
tag.trigger('click');
}
tag.appendTo(container_icons_remote);
}
setTimeout(process_next_chunk, 100);
};
process_next_chunk();
container_icons_remote_parent.append(container_icons_remote);
container_error.hide();
container_loading.hide();
container_no_permissions.hide();
}).catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR) {
container_no_permissions.show();
} else {
log.error(LogCategory.GENERAL, tr("Failed to fetch icon list. Error: %o"), error);
display_remote_error(tr("Failed to fetch icon list"));
}
container_loading.hide();
});
};
update_local_icons([100, 200, 300, 500, 600]);
update_remote_icons();
modal.htmlTag.find('.button-reload').on('click', () => update_remote_icons());
button_select.prop("disabled", true).on('click', () => {
callback_icon(selected_icon);
modal.close();
});
modal.htmlTag.find(".button-select-no-icon").on('click', () => {
callback_icon(0);
modal.close();
});
modal.open();
}
}

View File

@ -4,14 +4,18 @@ namespace Modals {
export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => any) {
let properties: ServerProperties = {} as ServerProperties; //The changes properties
const modal_template = $("#tmpl_server_edit").renderTag(server.properties);
const render_properties = {};
Object.assign(render_properties, properties);
render_properties["virtualserver_icon"] = server.channelTree.client.fileManager.icons.generateTag(server.properties.virtualserver_icon_id);
const modal_template = $("#tmpl_server_edit").renderTag(render_properties);
const modal = modal_template.modalize((header, body, footer) => {
return {
body: body.tabify()
}
});
server_applyGeneralListener(properties, modal.htmlTag.find(".properties_general"), modal.htmlTag.find(".button_ok"));
server_applyGeneralListener(properties, server, modal.htmlTag.find(".container-server-settings-general"), modal.htmlTag.find(".button_ok"));
server_applyTransferListener(properties, server, modal.htmlTag.find('.properties_transfer'));
server_applyHostListener(server, properties, server.properties, modal.htmlTag.find(".properties_host"), modal.htmlTag.find(".button_ok"));
server_applyMessages(properties, server, modal.htmlTag.find(".properties_messages"));
@ -32,7 +36,7 @@ namespace Modals {
modal.open();
}
function server_applyGeneralListener(properties: ServerProperties, tag: JQuery, button: JQuery) {
function server_applyGeneralListener(properties: ServerProperties, server: ServerEntry, tag: JQuery, button: JQuery) {
let updateButton = () => {
if(tag.find(".input_error").length == 0)
button.removeAttr("disabled");
@ -81,6 +85,17 @@ namespace Modals {
tag.find(".virtualserver_welcomemessage").change(function (this: HTMLInputElement) {
properties.virtualserver_welcomemessage = this.value;
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE).granted(1));
tag.find(".button-select-icon").on('click', event => {
Modals.spawnIconSelect(globalClient, id => {
const icon_node = tag.find(".button-select-icon").find(".icon-node");
icon_node.empty();
icon_node.append(globalClient.fileManager.icons.generateTag(id));
console.log("Selected icon ID: %d", id);
properties.virtualserver_icon_id = id;
}, server.properties.virtualserver_icon_id);
})
}

View File

@ -163,6 +163,16 @@ class ServerEntry {
});
});
}
}, {
type: MenuEntryType.ENTRY,
icon: "client-iconviewer",
name: tr("View icons"),
callback: () => Modals.spawnIconSelect(this.channelTree.client)
}, {
type: MenuEntryType.ENTRY,
icon: 'client-iconsview',
name: tr("View avatars"),
callback: () => Modals.spawnAvatarList(this.channelTree.client)
}, {
type: MenuEntryType.ENTRY,
icon: "client-invite_buddy",
@ -183,12 +193,21 @@ class ServerEntry {
updateVariables(is_self_notify: boolean, ...variables: {key: string, value: string}[]) {
let group = log.group(log.LogType.DEBUG, LogCategory.SERVER, tr("Update properties (%i)"), variables.length);
{
const entries = [];
for(const variable of variables)
entries.push({
key: variable.key,
value: variable.value,
type: typeof (this.properties[variable.key])
});
log.table("Server update properties", entries);
}
let update_bannner = false;
for(let variable of variables) {
JSON.map_field_to(this.properties, variable.value, variable.key);
//TODO tr
group.log("Updating server " + this.properties.virtualserver_name + ". Key " + variable.key + " Value: '" + variable.value + "' (" + typeof (this.properties[variable.key]) + ")");
if(variable.key == "virtualserver_name") {
this.htmlTag.find(".name").text(variable.value);
} else if(variable.key == "virtualserver_icon_id") {