A lots of updates
parent
658c3506d7
commit
add76cbd20
|
@ -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:
|
||||
|
|
|
@ -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" => "./",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -8,9 +8,8 @@
|
|||
"sizes": "256x256"
|
||||
}
|
||||
],
|
||||
"start_url": "?",
|
||||
"start_url": "/?",
|
||||
"background_color": "#18BC9C",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#18BC9C"
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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") {
|
||||
|
|
Loading…
Reference in New Issue