Merge pull request #37 from TeaSpeak/develop
This commit is contained in:
commit
019ec5e87e
49 changed files with 11127 additions and 720 deletions
11
ChangeLog.md
11
ChangeLog.md
|
@ -1,4 +1,15 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **28.03.19**
|
||||||
|
- 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
|
||||||
|
- Added country icons to the translation tab
|
||||||
|
- Decreased required bandwidth on translation loading
|
||||||
|
|
||||||
* **17.03.19**
|
* **17.03.19**
|
||||||
- Using VAD by default instead of PPT
|
- Using VAD by default instead of PPT
|
||||||
- Improved mobile experience:
|
- Improved mobile experience:
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
/* shared part */
|
/* shared part */
|
||||||
[ /* shared html and php files */
|
[ /* shared html and php files */
|
||||||
"type" => "html",
|
"type" => "html",
|
||||||
"search-pattern" => "/^([a-zA-Z]+)\.(html|php)$/",
|
"search-pattern" => "/^([a-zA-Z]+)\.(html|php|json)$/",
|
||||||
"build-target" => "dev|rel",
|
"build-target" => "dev|rel",
|
||||||
|
|
||||||
"path" => "./",
|
"path" => "./",
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
display: none;
|
display: none;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
|
.context-menu-container {
|
||||||
border: 1px solid #CCC;
|
border: 1px solid #CCC;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
@ -10,6 +12,11 @@
|
||||||
color: #333;
|
color: #333;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
margin-left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
font-family: Arial, serif;
|
font-family: Arial, serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
@ -140,3 +147,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
|
@ -7,8 +7,7 @@ $background:lightgray;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
/* tmp fix for ultra small devices */
|
/* tmp fix for ultra small devices */
|
||||||
overflow-x: auto;
|
overflow-y: visible;
|
||||||
overflow-y: hidden;
|
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
border-left:2px solid gray;
|
border-left:2px solid gray;
|
||||||
|
@ -46,6 +45,8 @@ $background:lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-dropdown {
|
.button-dropdown {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
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);*/
|
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -43,6 +43,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@viewport {
|
||||||
|
width: device-width;
|
||||||
|
user-zoom:fixed;
|
||||||
|
}
|
||||||
|
|
||||||
.select_info {
|
.select_info {
|
||||||
font-family: Arial;
|
font-family: Arial;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
175
shared/css/static/modal-avatar.scss
Normal file
175
shared/css/static/modal-avatar.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
.container-channel-settings-standard {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
.container-password {
|
.container-password {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 4;
|
||||||
|
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
.container-manage {
|
.container-manage {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 4;
|
||||||
|
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
150
shared/css/static/modal-icons.scss
Normal file
150
shared/css/static/modal-icons.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
permission-editor {
|
permission-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -260,6 +263,7 @@ permission-editor {
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
&.container-mode-unset {
|
&.container-mode-unset {
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
|
|
|
@ -155,6 +155,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
padding: 0; /* override tab-content setting */
|
padding: 0; /* override tab-content setting */
|
||||||
|
@ -228,13 +229,14 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content, x-content {
|
x-content {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-songs {
|
.container-songs {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
|
@ -19,6 +19,53 @@
|
||||||
flex-shrink: 70;
|
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 {
|
.container-server-settings-host {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
|
@ -412,6 +412,16 @@ $small_device: 800px; /* tested out via audio tab */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal .container-tabname-translations {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.country {
|
||||||
|
align-self: center;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal .settings-translations {
|
.modal .settings-translations {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
|
|
||||||
|
@ -475,6 +485,12 @@ $small_device: 800px; /* tested out via audio tab */
|
||||||
background-color: #00000010;
|
background-color: #00000010;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.country {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ body {
|
||||||
modal-body {
|
modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
min-height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
|
@ -64,6 +66,7 @@ modal-body {
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
/* max-height: 500px; */
|
/* max-height: 500px; */
|
||||||
|
min-height: 0; /* required for moz */
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
|
||||||
|
@ -90,6 +93,7 @@ modal-body {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
input.is-invalid {
|
input.is-invalid {
|
||||||
background-image: linear-gradient(0deg, #d50000 2px, rgba(213, 0, 0, 0) 0), linear-gradient(0deg, rgba(241, 1, 1, 0.61) 1px, transparent 0);
|
background-image: linear-gradient(0deg, #d50000 2px, rgba(213, 0, 0, 0) 0), linear-gradient(0deg, rgba(241, 1, 1, 0.61) 1px, transparent 0);
|
||||||
|
|
|
@ -6,6 +6,8 @@ x-tab { display:none }
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
min-height: 220px; /* min the header */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab div * {
|
.tab div * {
|
||||||
|
@ -25,9 +27,17 @@ x-tab { display:none }
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
x-content {
|
x-content {
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
x-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -35,9 +35,12 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<!-- 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." />
|
<meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." />
|
||||||
<link rel="icon" href="img/favicon/teacup.png">
|
<link rel="icon" href="img/favicon/teacup.png">
|
||||||
|
<!-- TODO Needs some fix -->
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
if(!$WEB_CLIENT) {
|
if(!$WEB_CLIENT) {
|
||||||
|
@ -159,7 +162,7 @@
|
||||||
<div class="fulloverlay" id="critical-load">
|
<div class="fulloverlay" id="critical-load">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img src="img/loading_error_right.svg" height="192px">
|
<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>
|
<h3 class="detail"></h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
15
shared/html/manifest.json
Normal file
15
shared/html/manifest.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"short_name": "TeaWeb",
|
||||||
|
"name": "TeaSpeak Web",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "img/favicon/teacup.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "256x256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/?",
|
||||||
|
"background_color": "#18BC9C",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#18BC9C"
|
||||||
|
}
|
|
@ -113,6 +113,14 @@
|
||||||
<div class="icon client-permission_overview"></div>
|
<div class="icon client-permission_overview"></div>
|
||||||
<a>{{tr "View/edit permissions" /}}</a>
|
<a>{{tr "View/edit permissions" /}}</a>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -127,7 +135,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- the query button -->
|
<!-- 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="buttons">
|
||||||
<div class="button icon_x32 client-server_query btn_query_toggle"></div>
|
<div class="button icon_x32 client-server_query btn_query_toggle"></div>
|
||||||
<div class="button-dropdown">
|
<div class="button-dropdown">
|
||||||
|
@ -135,7 +143,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown display_left">
|
<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_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 class="btn_query_create"><div class="icon client-away"></div><a>{{tr "Create server query login" /}}</a></div> -->
|
||||||
</div>
|
</div>
|
||||||
|
@ -317,7 +325,7 @@
|
||||||
<div class="invalid-feedback">{{tr "Selected profile is invalid. Select another one or fix the profile." /}}</div>
|
<div class="invalid-feedback">{{tr "Selected profile is invalid. Select another one or fix the profile." /}}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="form-group bmd-form-group container-manage"> <!-- needed to match padding for floating labels -->
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</modal-body>
|
</modal-body>
|
||||||
|
@ -330,11 +338,23 @@
|
||||||
|
|
||||||
<!-- Template for channel create & edit-->
|
<!-- Template for channel create & edit-->
|
||||||
<script class="jsrender-template" id="tmpl_channel_edit" type="text/html">
|
<script class="jsrender-template" id="tmpl_channel_edit" type="text/html">
|
||||||
<div class="align_column general_properties">
|
<div class="align_column general_properties container-channel-edit-general">
|
||||||
<div class="form-group">
|
<div class="container-name-icon form-row">
|
||||||
<label class="bmd-label-static">{{tr "Name:" /}}</label>
|
<div class="container-name form-group">
|
||||||
|
<label>{{tr "Name:" /}}</label>
|
||||||
<input class="form-control channel_name" value="{{>channel_name}}"/>
|
<input class="form-control channel_name" value="{{>channel_name}}"/>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label class="bmd-label-floating">{{tr "Channel password" /}}</label>
|
<label class="bmd-label-floating">{{tr "Channel password" /}}</label>
|
||||||
|
|
||||||
|
@ -696,10 +716,21 @@
|
||||||
</modal-header>
|
</modal-header>
|
||||||
<modal-body>
|
<modal-body>
|
||||||
<div class="container-server-settings-general">
|
<div class="container-server-settings-general">
|
||||||
<div class="form-group">
|
<div class="container-name-icon form-row">
|
||||||
|
<div class="container-name form-group">
|
||||||
<label>{{tr "Name:" /}}</label>
|
<label>{{tr "Name:" /}}</label>
|
||||||
<input class="form-control virtualserver_name" value="{{>virtualserver_name}}"/>
|
<input class="form-control virtualserver_name" value="{{>virtualserver_name}}"/>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label>{{tr "Phonetic Name:" /}}</label>
|
<label>{{tr "Phonetic Name:" /}}</label>
|
||||||
<input class="form-control virtualserver_name_phonetic" value="{{>virtualserver_name_phonetic}}"/>
|
<input class="form-control virtualserver_name_phonetic" value="{{>virtualserver_name_phonetic}}"/>
|
||||||
|
@ -1231,7 +1262,7 @@
|
||||||
</x-entry>
|
</x-entry>
|
||||||
<x-entry>
|
<x-entry>
|
||||||
<x-tag>
|
<x-tag>
|
||||||
{{tr "Translations" /}}
|
<div class="container-tabname-translations">{{tr "Translations" /}} <div class="country flag-en"></div></div>
|
||||||
</x-tag>
|
</x-tag>
|
||||||
<x-content>
|
<x-content>
|
||||||
<div class="settings-translations">
|
<div class="settings-translations">
|
||||||
|
@ -1240,18 +1271,6 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="setting-list">
|
<div class="setting-list">
|
||||||
<div class="list">
|
<div class="list">
|
||||||
<!--
|
|
||||||
<div class="entry default">{{tr "English (Default / Fallback)" /}}</div>
|
|
||||||
<div class="entry repository">
|
|
||||||
<div class="name">TeaSpeak Official</div>
|
|
||||||
<div class="button button-delete"><div class="icon client-delete"></div></div>
|
|
||||||
<div class="button button-info"><div class="icon client-about"></div></div>
|
|
||||||
</div>
|
|
||||||
<div class="entry translation selected">
|
|
||||||
<div class="name">German (Google Translate)</div>
|
|
||||||
<div class="button button-info"><div class="icon client-about"></div></div>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="management">
|
<div class="management">
|
||||||
<div class="loading">Loading...</div>
|
<div class="loading">Loading...</div>
|
||||||
|
@ -1281,18 +1300,6 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="profile-list">
|
<div class="profile-list">
|
||||||
<div class="list">
|
<div class="list">
|
||||||
<!--
|
|
||||||
<div class="entry default">{{tr "English (Default / Fallback)" /}}</div>
|
|
||||||
<div class="entry repository">
|
|
||||||
<div class="name">TeaSpeak Official</div>
|
|
||||||
<div class="button button-delete"><div class="icon client-delete"></div></div>
|
|
||||||
<div class="button button-info"><div class="icon client-about"></div></div>
|
|
||||||
</div>
|
|
||||||
<div class="entry translation selected">
|
|
||||||
<div class="name">German (Google Translate)</div>
|
|
||||||
<div class="button button-info"><div class="icon client-about"></div></div>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="management">
|
<div class="management">
|
||||||
<button class="btn btn-primary button-set-default">{{tr "Set selected as default" /}}</button>
|
<button class="btn btn-primary button-set-default">{{tr "Set selected as default" /}}</button>
|
||||||
|
@ -1505,9 +1512,13 @@
|
||||||
<div class="button button-info"><div class="icon client-about"></div></div>
|
<div class="button button-info"><div class="icon client-about"></div></div>
|
||||||
</div>
|
</div>
|
||||||
{{else type == "default" }}
|
{{else type == "default" }}
|
||||||
<div class="entry default {{if selected}}selected{{/if}}">{{tr "English (Default / Fallback)" /}}</div>
|
<div class="entry default {{if selected}}selected{{/if}}">
|
||||||
|
<div class="country flag-gb" title="{{*:i18n.country_name('gb')}}"></div>
|
||||||
|
<div class="name">{{tr "English (Default / Fallback)" /}}</div>
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="entry translation {{if selected}}selected{{/if}}" parent-repository="{{:id}}">
|
<div class="entry translation {{if selected}}selected{{/if}}" parent-repository="{{:id}}">
|
||||||
|
<div class="country flag-{{:country_code}}" title="{{*:i18n.country_name(data.country_code || 'XX')}}"></div>
|
||||||
<div class="name">{{> name}}</div>
|
<div class="name">{{> name}}</div>
|
||||||
<div class="button button-info"><div class="icon client-about"></div></div>
|
<div class="button button-info"><div class="icon client-about"></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1768,7 +1779,7 @@
|
||||||
<!-- <small class="form-text text-muted">{{tr "Filter permissions by permission name" /}}</small> -->
|
<!-- <small class="form-text text-muted">{{tr "Filter permissions by permission name" /}}</small> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group bmd-form-group">
|
<div class="form-group bmd-form-group">
|
||||||
<div class="checkbox">
|
<div class="switch">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" value="" class="filter-granted">
|
<input type="checkbox" value="" class="filter-granted">
|
||||||
{{tr "Show granted permissions only" /}}
|
{{tr "Show granted permissions only" /}}
|
||||||
|
@ -2912,28 +2923,201 @@
|
||||||
|
|
||||||
<script class="jsrender-template" id="tmpl_manage_bookmarks-create" type="text/html">
|
<script class="jsrender-template" id="tmpl_manage_bookmarks-create" type="text/html">
|
||||||
<div class="modal-bookmark-create">
|
<div class="modal-bookmark-create">
|
||||||
<div class="property">
|
<div class="form-group">
|
||||||
<div class="key">Bookmark Type:</div>
|
<label class="bmd-label-floating">{{tr "Bookmark type:" /}}</label>
|
||||||
<select class="bookmark-type">
|
<select class="form-control bookmark-type">
|
||||||
<option value="bookmark">Bookmark</option>
|
<option value="bookmark">Bookmark</option>
|
||||||
<option value="directory">Directory</option>
|
<option value="directory">Directory</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="property">
|
|
||||||
<div class="key">Parent Directory:</div>
|
<div class="form-group">
|
||||||
<select class="bookmark-parent">
|
<label class="bmd-label-floating">{{tr "Parent directory:" /}}</label>
|
||||||
|
<select class="form-control bookmark-parent">
|
||||||
<option bookmark-uuid=""></option>
|
<option bookmark-uuid=""></option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="property">
|
|
||||||
<div class="key">Bookmark Name:</div>
|
<div class="form-group">
|
||||||
<input class="bookmark-name">
|
<label class="bmd-label-floating">{{tr "Bookmark name" /}}</label>
|
||||||
|
<input class="form-control bookmark-name">
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button class="button-create">Create</button>
|
<button class="btn btn-success button-create">Create</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</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>
|
</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>
|
|
@ -1,32 +0,0 @@
|
||||||
{
|
|
||||||
"info": {
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"name": "Markus Hadenfeldt",
|
|
||||||
"email": "i18n.client@teaspeak.de"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "German translations"
|
|
||||||
},
|
|
||||||
"translations": [
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"message": "Show permission description",
|
|
||||||
"line": 374,
|
|
||||||
"character": 30,
|
|
||||||
"filename": "/home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/shared/js/ui/modal/ModalPermissionEdit.ts"
|
|
||||||
},
|
|
||||||
"translated": "Berechtigungsbeschreibung anzeigen",
|
|
||||||
"flags": [
|
|
||||||
"google-translate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"message": "Create a new connection"
|
|
||||||
},
|
|
||||||
"translated": "Verbinden",
|
|
||||||
"flags": [ ]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -2,23 +2,88 @@
|
||||||
"translations": [
|
"translations": [
|
||||||
{
|
{
|
||||||
"key": "de_gt",
|
"key": "de_gt",
|
||||||
"path": "de_google_translate.translation"
|
"country_code": "de",
|
||||||
|
"path": "de_google_translate.translation",
|
||||||
|
|
||||||
|
"name": "German translation, based on Google Translate",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||||
|
"email": "gtr.i18n.client@teaspeak.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Markus Hadenfeldt",
|
||||||
|
"email": "i18n.client@teaspeak.de"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "pl_gt",
|
"key": "pl_gt",
|
||||||
"path": "pl_google_translate.translation"
|
"country_code": "pl",
|
||||||
|
"path": "pl_google_translate.translation",
|
||||||
|
|
||||||
|
"name": "Polish translation, based on Google Translate",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||||
|
"email": "gtr.i18n.client@teaspeak.de"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "tr_gt",
|
"key": "tr_gt",
|
||||||
"path": "tr_google_translate.translation"
|
"country_code": "tr",
|
||||||
|
"path": "tr_google_translate.translation",
|
||||||
|
|
||||||
|
"name": "Turkey translation, based on Google Translate",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||||
|
"email": "gtr.i18n.client@teaspeak.de"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "fr_gt",
|
"key": "fr_gt",
|
||||||
"path": "fr_google_translate.translation"
|
"country_code": "fr",
|
||||||
|
"path": "fr_google_translate.translation",
|
||||||
|
|
||||||
|
"name": "Auto translated messages for language fr",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||||
|
"email": "gtr.i18n.client@teaspeak.de"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"key": "ru",
|
||||||
|
"country_code": "ru",
|
||||||
|
"path": "ru_translate_vafin.translation",
|
||||||
|
|
||||||
|
"name": "Russion translation by Vafin, baste on google translate",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||||
|
"email": "gtr.i18n.client@teaspeak.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vafin",
|
||||||
|
"email": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
"key": "ru_gt",
|
"key": "ru_gt",
|
||||||
"path": "ru_google_translate.translation"
|
"country_code": "gt",
|
||||||
|
"path": "ru_google_translate.translation",
|
||||||
|
|
||||||
|
"name": "Auto translated messages for language ru",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||||
|
"email": "gtr.i18n.client@teaspeak.de"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "Default TeaSpeak repository",
|
"name": "Default TeaSpeak repository",
|
||||||
|
|
8819
shared/i18n/ru_translate_vafin.translation
Normal file
8819
shared/i18n/ru_translate_vafin.translation
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
/* --------------- legacy! --------------- */
|
||||||
"info": {
|
"info": {
|
||||||
"contributors": [
|
"contributors": [
|
||||||
/* add yourself if you have done anything :) */
|
/* add yourself if you have done anything :) */
|
||||||
|
@ -9,6 +10,8 @@
|
||||||
],
|
],
|
||||||
"name": "A template translation file" /* this field is required */
|
"name": "A template translation file" /* this field is required */
|
||||||
},
|
},
|
||||||
|
/* --------------- legacy --------------- */
|
||||||
|
|
||||||
"translations": [ /* Array with all translation objects */
|
"translations": [ /* Array with all translation objects */
|
||||||
{ /* translation object */
|
{ /* translation object */
|
||||||
"key": { /* the key */
|
"key": { /* the key */
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
FIXME: Dont use item storage with base64! Use the larger cache API and drop IE support!
|
FIXME: Dont use item storage with base64! Use the larger cache API and drop IE support!
|
||||||
https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage#Browser_compatibility
|
https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage#Browser_compatibility
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class FileEntry {
|
class FileEntry {
|
||||||
name: string;
|
name: string;
|
||||||
datetime: number;
|
datetime: number;
|
||||||
|
@ -19,43 +20,68 @@ class FileListRequest {
|
||||||
callback: (entries: FileEntry[]) => void;
|
callback: (entries: FileEntry[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadFileTransfer {
|
namespace transfer {
|
||||||
transferId: number;
|
export interface TransferKey {
|
||||||
serverTransferId: number;
|
client_transfer_id: number;
|
||||||
transferKey: string;
|
server_transfer_id: number;
|
||||||
|
|
||||||
totalSize: number;
|
key: string;
|
||||||
|
|
||||||
|
file_path: string;
|
||||||
|
file_name: string;
|
||||||
|
|
||||||
|
peer: {
|
||||||
|
hosts: string[],
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
total_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadOptions {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
channel?: ChannelEntry;
|
||||||
|
channel_password?: string;
|
||||||
|
|
||||||
|
size: number;
|
||||||
|
overwrite: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DownloadKey = TransferKey;
|
||||||
|
export type UploadKey = TransferKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamedFileDownload {
|
||||||
|
readonly transfer_key: transfer.DownloadKey;
|
||||||
currentSize: number = 0;
|
currentSize: number = 0;
|
||||||
|
|
||||||
remotePort: number;
|
|
||||||
remoteHost: string;
|
|
||||||
|
|
||||||
on_start: () => void = () => {};
|
on_start: () => void = () => {};
|
||||||
on_complete: () => void = () => {};
|
on_complete: () => void = () => {};
|
||||||
on_fail: (reason: string) => void = (_) => {};
|
on_fail: (reason: string) => void = (_) => {};
|
||||||
on_data: (data: Uint8Array) => void = (_) => {};
|
on_data: (data: Uint8Array) => void = (_) => {};
|
||||||
|
|
||||||
private _handle: FileManager;
|
private _handle: FileManager;
|
||||||
private _promiseCallback: (value: DownloadFileTransfer) => void;
|
private _promiseCallback: (value: StreamedFileDownload) => void;
|
||||||
private _socket: WebSocket;
|
private _socket: WebSocket;
|
||||||
private _active: boolean;
|
private _active: boolean;
|
||||||
private _succeed: boolean;
|
private _succeed: boolean;
|
||||||
private _parseActive: boolean;
|
private _parseActive: boolean;
|
||||||
|
|
||||||
constructor(handle: FileManager, id: number) {
|
constructor(key: transfer.DownloadKey) {
|
||||||
this.transferId = id;
|
this.transfer_key = key;
|
||||||
this._handle = handle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransfer() {
|
start() {
|
||||||
if(!this.remoteHost || !this.remotePort || !this.transferKey || !this.totalSize) {
|
if(!this.transfer_key) {
|
||||||
this.on_fail("Missing data!");
|
this.on_fail("Missing data!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(tr("Create new file download to %s:%s (Key: %s, Expect %d bytes)"), this.remoteHost, this.remotePort, this.transferId, this.totalSize);
|
console.debug(tr("Create new file download to %s:%s (Key: %s, Expect %d bytes)"), this.transfer_key.peer.hosts[0], this.transfer_key.peer.port, this.transfer_key.key, this.transfer_key.total_size);
|
||||||
this._active = true;
|
this._active = true;
|
||||||
this._socket = new WebSocket("wss://" + this.remoteHost + ":" + this.remotePort);
|
this._socket = new WebSocket("wss://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port);
|
||||||
this._socket.onopen = this.onOpen.bind(this);
|
this._socket.onopen = this.onOpen.bind(this);
|
||||||
this._socket.onclose = this.onClose.bind(this);
|
this._socket.onclose = this.onClose.bind(this);
|
||||||
this._socket.onmessage = this.onMessage.bind(this);
|
this._socket.onmessage = this.onMessage.bind(this);
|
||||||
|
@ -65,7 +91,7 @@ class DownloadFileTransfer {
|
||||||
private onOpen() {
|
private onOpen() {
|
||||||
if(!this._active) return;
|
if(!this._active) return;
|
||||||
|
|
||||||
this._socket.send(this.transferKey);
|
this._socket.send(this.transfer_key.key);
|
||||||
this.on_start();
|
this.on_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +114,7 @@ class DownloadFileTransfer {
|
||||||
private onBinaryData(data: Uint8Array) {
|
private onBinaryData(data: Uint8Array) {
|
||||||
this.currentSize += data.length;
|
this.currentSize += data.length;
|
||||||
this.on_data(data);
|
this.on_data(data);
|
||||||
if(this.currentSize == this.totalSize) {
|
if(this.currentSize == this.transfer_key.total_size) {
|
||||||
this._succeed = true;
|
this._succeed = true;
|
||||||
this.on_complete();
|
this.on_complete();
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
|
@ -113,6 +139,83 @@ class DownloadFileTransfer {
|
||||||
//this._socket.close();
|
//this._socket.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class RequestFileDownload {
|
||||||
|
readonly transfer_key: transfer.DownloadKey;
|
||||||
|
|
||||||
|
constructor(key: transfer.DownloadKey) {
|
||||||
|
this.transfer_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request_file() : Promise<Response> {
|
||||||
|
return await this.try_fetch("https://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
response.setHeader("Access-Control-Allow-Methods", {"GET, POST"});
|
||||||
|
response.setHeader("Access-Control-Allow-Origin", {"*"});
|
||||||
|
response.setHeader("Access-Control-Allow-Headers", {"*"});
|
||||||
|
response.setHeader("Access-Control-Max-Age", {"86400"});
|
||||||
|
response.setHeader("Access-Control-Expose-Headers", {"X-media-bytes"});
|
||||||
|
*/
|
||||||
|
private async try_fetch(url: string) : Promise<Response> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
cache: "no-cache",
|
||||||
|
mode: 'cors',
|
||||||
|
headers: {
|
||||||
|
'transfer-key': this.transfer_key.key,
|
||||||
|
'download-name': this.transfer_key.file_name,
|
||||||
|
'Access-Control-Allow-Headers': '*',
|
||||||
|
'Access-Control-Expose-Headers': '*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!response.ok)
|
||||||
|
throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestFileUpload {
|
||||||
|
readonly transfer_key: transfer.UploadKey;
|
||||||
|
constructor(key: transfer.DownloadKey) {
|
||||||
|
this.transfer_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async put_data(data: BufferSource | File) {
|
||||||
|
const form_data = new FormData();
|
||||||
|
|
||||||
|
if(data instanceof File) {
|
||||||
|
if(data.size != this.transfer_key.total_size)
|
||||||
|
throw "invalid size";
|
||||||
|
|
||||||
|
form_data.append("file", data);
|
||||||
|
} else {
|
||||||
|
const buffer = <BufferSource>data;
|
||||||
|
if(buffer.byteLength != this.transfer_key.total_size)
|
||||||
|
throw "invalid size";
|
||||||
|
|
||||||
|
form_data.append("file", new Blob([buffer], { type: "application/octet-stream" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.try_put(form_data, "https://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
async try_put(data: FormData, url: string) : Promise<void> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
cache: "no-cache",
|
||||||
|
mode: 'cors',
|
||||||
|
body: data,
|
||||||
|
headers: {
|
||||||
|
'transfer-key': this.transfer_key.key,
|
||||||
|
'Access-Control-Allow-Headers': '*',
|
||||||
|
'Access-Control-Expose-Headers': '*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!response.ok)
|
||||||
|
throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class FileManager extends connection.AbstractCommandHandler {
|
class FileManager extends connection.AbstractCommandHandler {
|
||||||
handle: TSClient;
|
handle: TSClient;
|
||||||
|
@ -120,8 +223,10 @@ class FileManager extends connection.AbstractCommandHandler {
|
||||||
avatars: AvatarManager;
|
avatars: AvatarManager;
|
||||||
|
|
||||||
private listRequests: FileListRequest[] = [];
|
private listRequests: FileListRequest[] = [];
|
||||||
private pendingDownloadTransfers: DownloadFileTransfer[] = [];
|
private pending_download_requests: transfer.DownloadKey[] = [];
|
||||||
private downloadCounter : number = 0;
|
private pending_upload_requests: transfer.UploadKey[] = [];
|
||||||
|
|
||||||
|
private transfer_counter : number = 0;
|
||||||
|
|
||||||
constructor(client: TSClient) {
|
constructor(client: TSClient) {
|
||||||
super(client.serverConnection);
|
super(client.serverConnection);
|
||||||
|
@ -144,6 +249,9 @@ class FileManager extends connection.AbstractCommandHandler {
|
||||||
case "notifystartdownload":
|
case "notifystartdownload":
|
||||||
this.notifyStartDownload(command.arguments);
|
this.notifyStartDownload(command.arguments);
|
||||||
return true;
|
return true;
|
||||||
|
case "notifystartupload":
|
||||||
|
this.notifyStartUpload(command.arguments);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -187,9 +295,13 @@ class FileManager extends connection.AbstractCommandHandler {
|
||||||
console.error(tr("Invalid file list entry. Path: %s"), json[0]["path"]);
|
console.error(tr("Invalid file list entry. Path: %s"), json[0]["path"]);
|
||||||
return;
|
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);
|
entry.entries.push(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private notifyFileListFinished(json) {
|
private notifyFileListFinished(json) {
|
||||||
let entry : FileListRequest = undefined;
|
let entry : FileListRequest = undefined;
|
||||||
|
@ -210,21 +322,52 @@ class FileManager extends connection.AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/******************************** File download ********************************/
|
/******************************** File download/upload ********************************/
|
||||||
requestFileDownload(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise<DownloadFileTransfer> {
|
download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise<transfer.DownloadKey> {
|
||||||
const _this = this;
|
const transfer_data: transfer.DownloadKey = {
|
||||||
let transfer = new DownloadFileTransfer(this, this.downloadCounter++);
|
file_name: file,
|
||||||
this.pendingDownloadTransfers.push(transfer);
|
file_path: path,
|
||||||
return new Promise<DownloadFileTransfer>((resolve, reject) => {
|
client_transfer_id: this.transfer_counter++
|
||||||
transfer["_promiseCallback"] = resolve;
|
} as any;
|
||||||
_this.handle.serverConnection.send_command("ftinitdownload", {
|
|
||||||
|
this.pending_download_requests.push(transfer_data);
|
||||||
|
return new Promise<transfer.DownloadKey>((resolve, reject) => {
|
||||||
|
transfer_data["_callback"] = resolve;
|
||||||
|
this.handle.serverConnection.send_command("ftinitdownload", {
|
||||||
"path": path,
|
"path": path,
|
||||||
"name": file,
|
"name": file,
|
||||||
"cid": (channel ? channel.channelId : "0"),
|
"cid": (channel ? channel.channelId : "0"),
|
||||||
"cpw": (password ? password : ""),
|
"cpw": (password ? password : ""),
|
||||||
"clientftfid": transfer.transferId
|
"clientftfid": transfer_data.client_transfer_id
|
||||||
}).catch(reason => {
|
}).catch(reason => {
|
||||||
_this.pendingDownloadTransfers.remove(transfer);
|
this.pending_download_requests.remove(transfer_data);
|
||||||
|
reject(reason);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_file(options: transfer.UploadOptions) : Promise<transfer.UploadKey> {
|
||||||
|
const transfer_data: transfer.UploadKey = {
|
||||||
|
file_path: options.path,
|
||||||
|
file_name: options.name,
|
||||||
|
client_transfer_id: this.transfer_counter++,
|
||||||
|
total_size: options.size
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
this.pending_upload_requests.push(transfer_data);
|
||||||
|
return new Promise<transfer.UploadKey>((resolve, reject) => {
|
||||||
|
transfer_data["_callback"] = resolve;
|
||||||
|
this.handle.serverConnection.send_command("ftinitupload", {
|
||||||
|
"path": options.path,
|
||||||
|
"name": options.name,
|
||||||
|
"cid": (options.channel ? options.channel.channelId : "0"),
|
||||||
|
"cpw": options.channel_password || "",
|
||||||
|
"clientftfid": transfer_data.client_transfer_id,
|
||||||
|
"size": options.size,
|
||||||
|
"overwrite": options.overwrite,
|
||||||
|
"resume": false
|
||||||
|
}).catch(reason => {
|
||||||
|
this.pending_upload_requests.remove(transfer_data);
|
||||||
reject(reason);
|
reject(reason);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
@ -233,31 +376,64 @@ class FileManager extends connection.AbstractCommandHandler {
|
||||||
private notifyStartDownload(json) {
|
private notifyStartDownload(json) {
|
||||||
json = json[0];
|
json = json[0];
|
||||||
|
|
||||||
let transfer: DownloadFileTransfer;
|
let transfer: transfer.DownloadKey;
|
||||||
for(let e of this.pendingDownloadTransfers)
|
for(let e of this.pending_download_requests)
|
||||||
if(e.transferId == json["clientftfid"]) {
|
if(e.client_transfer_id == json["clientftfid"]) {
|
||||||
transfer = e;
|
transfer = e;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer.serverTransferId = json["serverftfid"];
|
transfer.server_transfer_id = json["serverftfid"];
|
||||||
transfer.transferKey = json["ftkey"];
|
transfer.key = json["ftkey"];
|
||||||
transfer.totalSize = json["size"];
|
transfer.total_size = json["size"];
|
||||||
|
|
||||||
transfer.remotePort = json["port"];
|
transfer.peer = {
|
||||||
transfer.remoteHost = (json["ip"] ? json["ip"] : "").replace(/,/g, "");
|
hosts: (json["ip"] || "").split(","),
|
||||||
if(!transfer.remoteHost || transfer.remoteHost == '0.0.0.0' || transfer.remoteHost == '127.168.0.0')
|
port: json["port"]
|
||||||
transfer.remoteHost = this.handle.serverConnection._remote_address.host;
|
};
|
||||||
|
|
||||||
(transfer["_promiseCallback"] as (val: DownloadFileTransfer) => void)(transfer);
|
if(transfer.peer.hosts.length == 0)
|
||||||
this.pendingDownloadTransfers.remove(transfer);
|
transfer.peer.hosts.push("0.0.0.0");
|
||||||
|
|
||||||
|
if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0')
|
||||||
|
transfer.peer.hosts[0] = this.handle.serverConnection._remote_address.host;
|
||||||
|
|
||||||
|
(transfer["_callback"] as (val: transfer.DownloadKey) => void)(transfer);
|
||||||
|
this.pending_download_requests.remove(transfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyStartUpload(json) {
|
||||||
|
json = json[0];
|
||||||
|
|
||||||
|
let transfer: transfer.UploadKey;
|
||||||
|
for(let e of this.pending_upload_requests)
|
||||||
|
if(e.client_transfer_id == json["clientftfid"]) {
|
||||||
|
transfer = e;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer.server_transfer_id = json["serverftfid"];
|
||||||
|
transfer.key = json["ftkey"];
|
||||||
|
|
||||||
|
transfer.peer = {
|
||||||
|
hosts: (json["ip"] || "").split(","),
|
||||||
|
port: json["port"]
|
||||||
|
};
|
||||||
|
|
||||||
|
if(transfer.peer.hosts.length == 0)
|
||||||
|
transfer.peer.hosts.push("0.0.0.0");
|
||||||
|
|
||||||
|
if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0')
|
||||||
|
transfer.peer.hosts[0] = this.handle.serverConnection._remote_address.host;
|
||||||
|
|
||||||
|
(transfer["_callback"] as (val: transfer.UploadKey) => void)(transfer);
|
||||||
|
this.pending_upload_requests.remove(transfer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Icon {
|
class Icon {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
url: string;
|
||||||
base64: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ImageType {
|
enum ImageType {
|
||||||
|
@ -269,14 +445,14 @@ enum ImageType {
|
||||||
JPEG
|
JPEG
|
||||||
}
|
}
|
||||||
|
|
||||||
function media_image_type(type: ImageType) {
|
function media_image_type(type: ImageType, file?: boolean) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ImageType.BITMAP:
|
case ImageType.BITMAP:
|
||||||
return "bmp";
|
return "bmp";
|
||||||
case ImageType.GIF:
|
case ImageType.GIF:
|
||||||
return "gif";
|
return "gif";
|
||||||
case ImageType.SVG:
|
case ImageType.SVG:
|
||||||
return "svg+xml";
|
return file ? "svg" : "svg+xml";
|
||||||
case ImageType.JPEG:
|
case ImageType.JPEG:
|
||||||
return "jpeg";
|
return "jpeg";
|
||||||
case ImageType.UNKNOWN:
|
case ImageType.UNKNOWN:
|
||||||
|
@ -305,286 +481,373 @@ function image_type(base64: string) {
|
||||||
return ImageType.UNKNOWN;
|
return ImageType.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CacheManager {
|
||||||
|
readonly cache_name: string;
|
||||||
|
|
||||||
|
private _cache_category: Cache;
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.cache_name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupped() : boolean { return !!this._cache_category; }
|
||||||
|
|
||||||
|
async setup() {
|
||||||
|
if(!window.caches)
|
||||||
|
throw "Missing caches!";
|
||||||
|
|
||||||
|
this._cache_category = await caches.open(this.cache_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(max_age: number) {
|
||||||
|
/* FIXME: TODO */
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolve_cached(key: string, max_age?: number) : Promise<Response | undefined> {
|
||||||
|
max_age = typeof(max_age) === "number" ? max_age : -1;
|
||||||
|
|
||||||
|
const request = new Request("cache_request_" + key);
|
||||||
|
const cached_response = await this._cache_category.match(request);
|
||||||
|
if(!cached_response)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
/* FIXME: Max age */
|
||||||
|
return cached_response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async put_cache(key: string, value: Response, type?: string, headers?: {[key: string]:string}) {
|
||||||
|
const request = new Request("cache_request_" + key);
|
||||||
|
|
||||||
|
const new_headers = new Headers();
|
||||||
|
for(const key of value.headers.keys())
|
||||||
|
new_headers.set(key, value.headers.get(key));
|
||||||
|
if(type)
|
||||||
|
new_headers.set("Content-type", type);
|
||||||
|
for(const key of Object.keys(headers || {}))
|
||||||
|
new_headers.set(key, headers[key]);
|
||||||
|
|
||||||
|
await this._cache_category.put(request, new Response(value.body, {
|
||||||
|
headers: new_headers
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class IconManager {
|
class IconManager {
|
||||||
handle: FileManager;
|
handle: FileManager;
|
||||||
private loading_icons: {promise: Promise<Icon>, id: number}[] = [];
|
private cache: CacheManager;
|
||||||
|
private _id_urls: {[id:number]:string} = {};
|
||||||
|
private _loading_promises: {[id:number]:Promise<Icon>} = {};
|
||||||
|
|
||||||
constructor(handle: FileManager) {
|
constructor(handle: FileManager) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
|
this.cache = new CacheManager("icons");
|
||||||
}
|
}
|
||||||
|
|
||||||
iconList() : Promise<FileEntry[]> {
|
iconList() : Promise<FileEntry[]> {
|
||||||
return this.handle.requestFileList("/icons");
|
return this.handle.requestFileList("/icons");
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadIcon(id: number) : Promise<DownloadFileTransfer> {
|
create_icon_download(id: number) : Promise<transfer.DownloadKey> {
|
||||||
return this.handle.requestFileDownload("", "/icon_" + id);
|
return this.handle.download_file("", "/icon_" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveCached?(id: number) : Icon {
|
private async _response_url(response: Response) {
|
||||||
let icon = localStorage.getItem("icon_" + id);
|
if(!response.headers.has('X-media-bytes'))
|
||||||
if(icon) {
|
throw "missing media bytes";
|
||||||
let i = JSON.parse(icon) as Icon;
|
|
||||||
if(i.base64.length > 0) { //TODO timestamp?
|
const type = image_type(response.headers.get('X-media-bytes'));
|
||||||
return i;
|
const media = media_image_type(type);
|
||||||
}
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
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> {
|
||||||
|
if(this._id_urls[id])
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
url: this._id_urls[id]
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!this.cache.setupped())
|
||||||
|
await this.cache.setup();
|
||||||
|
|
||||||
|
const response = await this.cache.resolve_cached('icon_' + id); //TODO age!
|
||||||
|
if(response)
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
url: (this._id_urls[id] = await this._response_url(response))
|
||||||
|
};
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private load_finished(id: number) {
|
private async _load_icon(id: number) : Promise<Icon> {
|
||||||
for(let entry of this.loading_icons)
|
try {
|
||||||
if(entry.id == id)
|
let download_key: transfer.DownloadKey;
|
||||||
this.loading_icons.remove(entry);
|
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) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._loading_promises[id] = undefined;
|
||||||
|
}, 1000 * 60); /* try again in 60 seconds */
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadIcon(id: number) : Promise<Icon> {
|
loadIcon(id: number) : Promise<Icon> {
|
||||||
for(let entry of this.loading_icons)
|
return this._loading_promises[id] || (this._loading_promises[id] = this._load_icon(id));
|
||||||
if(entry.id == id) return entry.promise;
|
|
||||||
|
|
||||||
let promise = new Promise<Icon>((resolve, reject) => {
|
|
||||||
let icon = this.resolveCached(id);
|
|
||||||
if(icon){
|
|
||||||
this.load_finished(id);
|
|
||||||
resolve(icon);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.downloadIcon(id).then(ft => {
|
generateTag(id: number, options?: {
|
||||||
let array = new Uint8Array(0);
|
animate?: boolean
|
||||||
ft.on_fail = reason => {
|
}) : JQuery<HTMLDivElement> {
|
||||||
this.load_finished(id);
|
options = options || {};
|
||||||
console.error(tr("Could not download icon %s -> %s"), id, tr(reason));
|
|
||||||
chat.serverChat().appendError(tr("Fail to download icon {0}. ({1})"), id, JSON.stringify(reason));
|
|
||||||
reject(reason);
|
|
||||||
};
|
|
||||||
ft.on_start = () => {};
|
|
||||||
ft.on_data = (data: Uint8Array) => {
|
|
||||||
array = concatenate(Uint8Array, array, data);
|
|
||||||
};
|
|
||||||
ft.on_complete = () => {
|
|
||||||
let base64 = btoa(String.fromCharCode.apply(null, array));
|
|
||||||
let icon = new Icon();
|
|
||||||
icon.base64 = base64;
|
|
||||||
icon.id = id;
|
|
||||||
icon.name = "icon_" + id;
|
|
||||||
|
|
||||||
localStorage.setItem("icon_" + id, JSON.stringify(icon));
|
|
||||||
this.load_finished(id);
|
|
||||||
resolve(icon);
|
|
||||||
};
|
|
||||||
|
|
||||||
ft.startTransfer();
|
|
||||||
}).catch(reason => {
|
|
||||||
console.error(tr("Error while downloading icon! (%s)"), tr(JSON.stringify(reason)));
|
|
||||||
chat.serverChat().appendError(tr("Failed to request download for icon {0}. ({1})"), id, tr(JSON.stringify(reason)));
|
|
||||||
reject(reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loading_icons.push({promise: promise, id: id});
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
//$("<img width=\"16\" height=\"16\" alt=\"tick\" src=\"data:image/png;base64," + value.base64 + "\">")
|
|
||||||
generateTag(id: number) : JQuery<HTMLDivElement> {
|
|
||||||
if(id == 0)
|
if(id == 0)
|
||||||
return $.spawn("div").addClass("icon_empty");
|
return $.spawn("div").addClass("icon_empty");
|
||||||
else if(id < 1000)
|
else if(id < 1000)
|
||||||
return $.spawn("div").addClass("icon client-group_" + id);
|
return $.spawn("div").addClass("icon client-group_" + id);
|
||||||
|
|
||||||
let tag = $.spawn("div");
|
|
||||||
tag.addClass("icon-container icon_empty");
|
|
||||||
|
|
||||||
let img = $.spawn("img");
|
const icon_container = $.spawn("div").addClass("icon-container icon_empty");
|
||||||
img.attr("width", 16).attr("height", 16).attr("alt", "");
|
const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", "");
|
||||||
|
|
||||||
let icon = this.resolveCached(id);
|
if(this._id_urls[id]) {
|
||||||
if(icon) {
|
icon_image.attr("src", this._id_urls[id]).appendTo(icon_container);
|
||||||
const type = image_type(icon.base64);
|
icon_container.removeClass("icon_empty");
|
||||||
const media = media_image_type(type);
|
|
||||||
console.debug(tr("Icon has an image type of %o (media: %o)"), type, media);
|
|
||||||
img.attr("src", "data:image/" + media + ";base64," + icon.base64);
|
|
||||||
tag.append(img).removeClass("icon_empty");
|
|
||||||
} else {
|
} else {
|
||||||
img.attr("src", "file://null");
|
const icon_load_image = $.spawn("div").addClass("icon_loading");
|
||||||
|
icon_load_image.appendTo(icon_container);
|
||||||
|
|
||||||
let loader = $.spawn("div");
|
(async () => {
|
||||||
loader.addClass("icon_loading");
|
let icon: Icon;
|
||||||
tag.append(loader);
|
try {
|
||||||
|
icon = await this.resolved_cached(id);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
this.loadIcon(id).then(icon => {
|
if(!icon)
|
||||||
const type = image_type(icon.base64);
|
icon = await this.loadIcon(id);
|
||||||
const media = media_image_type(type);
|
|
||||||
console.debug(tr("Icon has an image type of %o (media: %o)"), type, media);
|
|
||||||
img.attr("src", "data:image/" + media + ";base64," + icon.base64);
|
|
||||||
console.debug(tr("Icon %o loaded :)"), id);
|
|
||||||
|
|
||||||
img.css("opacity", 0);
|
if(!icon)
|
||||||
tag.append(img).removeClass("icon_empty");
|
throw "failed to download icon";
|
||||||
loader.animate({opacity: 0}, 50, function () {
|
|
||||||
$(this).detach();
|
icon_image.attr("src", icon.url);
|
||||||
img.animate({opacity: 1}, 150);
|
icon_container.append(icon_image).removeClass("icon_empty");
|
||||||
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
}).catch(reason => {
|
} else {
|
||||||
console.error(tr("Could not load icon %o. Reason: %p"), id, reason);
|
icon_load_image.detach();
|
||||||
loader.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon " + id);
|
}
|
||||||
|
})().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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return tag;
|
return icon_container;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Avatar {
|
class Avatar {
|
||||||
clientUid: string;
|
client_avatar_id: string; /* the base64 uid thing from a-m */
|
||||||
avatarId: string;
|
avatar_id: string; /* client_flag_avatar */
|
||||||
base64?: string;
|
url: string;
|
||||||
url?: string;
|
type: ImageType;
|
||||||
blob?: Blob;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvatarManager {
|
class AvatarManager {
|
||||||
handle: FileManager;
|
handle: FileManager;
|
||||||
private loading_avatars: {promise: Promise<Avatar>, name: string}[] = [];
|
|
||||||
private loaded_urls: string[] = [];
|
private cache: CacheManager;
|
||||||
|
private _cached_avatars: {[response_avatar_id:number]:Avatar} = {};
|
||||||
|
private _loading_promises: {[response_avatar_id:number]:Promise<Icon>} = {};
|
||||||
|
|
||||||
constructor(handle: FileManager) {
|
constructor(handle: FileManager) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
|
|
||||||
|
this.cache = new CacheManager("avatars");
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadAvatar(client: ClientEntry) : Promise<DownloadFileTransfer> {
|
private async _response_url(response: Response, type: ImageType) : Promise<string> {
|
||||||
console.log(tr("Downloading avatar %s"), client.avatarId());
|
if(!response.headers.has('X-media-bytes'))
|
||||||
return this.handle.requestFileDownload("", "/avatar_" + client.avatarId());
|
throw "missing media bytes";
|
||||||
|
|
||||||
|
const media = media_image_type(type);
|
||||||
|
const blob = await response.blob();
|
||||||
|
if(blob.type !== "image/" + media)
|
||||||
|
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
|
||||||
|
else
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveCached?(client: ClientEntry) : Avatar {
|
async resolved_cached?(client_avatar_id: string, avatar_id?: string) : Promise<Avatar> {
|
||||||
let avatar = localStorage.getItem("avatar_" + client.properties.client_unique_identifier);
|
let avatar: Avatar = this._cached_avatars[avatar_id];
|
||||||
if(avatar) {
|
if(avatar) {
|
||||||
let i = JSON.parse(avatar) as Avatar;
|
if(typeof(avatar_id) !== "string" || avatar.avatar_id == avatar_id)
|
||||||
//TODO timestamp?
|
return avatar;
|
||||||
|
this._cached_avatars[avatar_id] = (avatar = undefined);
|
||||||
|
}
|
||||||
|
|
||||||
if(i.avatarId != client.properties.client_flag_avatar) return undefined;
|
if(!this.cache.setupped())
|
||||||
|
await this.cache.setup();
|
||||||
|
|
||||||
if(i.base64) {
|
const response = await this.cache.resolve_cached('avatar_' + client_avatar_id); //TODO age!
|
||||||
if(i.base64.length > 0)
|
if(!response)
|
||||||
return i;
|
|
||||||
else i.base64 = undefined;
|
|
||||||
}
|
|
||||||
if(i.url) {
|
|
||||||
for(let url of this.loaded_urls)
|
|
||||||
if(url == i.url) return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
|
||||||
|
|
||||||
private load_finished(name: string) {
|
let response_avatar_id = response.headers.has("X-avatar-id") ? response.headers.get("X-avatar-id") : undefined;
|
||||||
for(let entry of this.loading_avatars)
|
if(typeof(avatar_id) === "string" && response_avatar_id != avatar_id)
|
||||||
if(entry.name == name)
|
return undefined;
|
||||||
this.loading_avatars.remove(entry);
|
|
||||||
}
|
|
||||||
loadAvatar(client: ClientEntry) : Promise<Avatar> {
|
|
||||||
let name = client.avatarId();
|
|
||||||
for(let promise of this.loading_avatars)
|
|
||||||
if(promise.name == name) return promise.promise;
|
|
||||||
|
|
||||||
let promise = new Promise<Avatar>((resolve, reject) => {
|
const type = image_type(response.headers.get('X-media-bytes'));
|
||||||
let avatar = this.resolveCached(client);
|
return this._cached_avatars[client_avatar_id] = {
|
||||||
if(avatar){
|
client_avatar_id: client_avatar_id,
|
||||||
this.load_finished(name);
|
avatar_id: avatar_id || response_avatar_id,
|
||||||
resolve(avatar);
|
url: await this._response_url(response, type),
|
||||||
return;
|
type: type
|
||||||
}
|
|
||||||
|
|
||||||
this.downloadAvatar(client).then(ft => {
|
|
||||||
let array = new Uint8Array(0);
|
|
||||||
ft.on_fail = reason => {
|
|
||||||
this.load_finished(name);
|
|
||||||
console.error(tr("Could not download avatar %o -> %s"), client.properties.client_flag_avatar, reason);
|
|
||||||
chat.serverChat().appendError(tr("Fail to download avatar for {0}. ({1})"), client.clientNickName(), JSON.stringify(reason));
|
|
||||||
reject(reason);
|
|
||||||
};
|
};
|
||||||
ft.on_start = () => {};
|
|
||||||
ft.on_data = (data: Uint8Array) => {
|
|
||||||
array = concatenate(Uint8Array, array, data);
|
|
||||||
};
|
|
||||||
ft.on_complete = () => {
|
|
||||||
let avatar = new Avatar();
|
|
||||||
if(array.length >= 1024 * 1024) {
|
|
||||||
let blob_image = new Blob([array]);
|
|
||||||
avatar.url = URL.createObjectURL(blob_image);
|
|
||||||
avatar.blob = blob_image;
|
|
||||||
this.loaded_urls.push(avatar.url);
|
|
||||||
} else {
|
|
||||||
avatar.base64 = btoa(String.fromCharCode.apply(null, array));
|
|
||||||
}
|
|
||||||
avatar.clientUid = client.clientUid();
|
|
||||||
avatar.avatarId = client.properties.client_flag_avatar;
|
|
||||||
|
|
||||||
localStorage.setItem("avatar_" + client.properties.client_unique_identifier, JSON.stringify(avatar));
|
|
||||||
this.load_finished(name);
|
|
||||||
resolve(avatar);
|
|
||||||
};
|
|
||||||
|
|
||||||
ft.startTransfer();
|
|
||||||
}).catch(reason => {
|
|
||||||
this.load_finished(name);
|
|
||||||
console.error(tr("Error while downloading avatar! (%s)"), JSON.stringify(reason));
|
|
||||||
chat.serverChat().appendError(tr("Failed to request avatar download for {0}. ({1})"), client.clientNickName(), JSON.stringify(reason));
|
|
||||||
reject(reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loading_avatars.push({promise: promise, name: name});
|
|
||||||
return promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generateTag(client: ClientEntry) {
|
create_avatar_download(client_avatar_id: string) : Promise<transfer.DownloadKey> {
|
||||||
let tag = $.spawn("div");
|
console.log(tr("Downloading avatar %s"), client_avatar_id);
|
||||||
|
return this.handle.download_file("", "/avatar_" + client_avatar_id);
|
||||||
|
}
|
||||||
|
|
||||||
let img = $.spawn("img");
|
private async _load_avatar(client_avatar_id: string, avatar_id: string) {
|
||||||
img.attr("alt", "");
|
let download_key: transfer.DownloadKey;
|
||||||
|
try {
|
||||||
|
download_key = await this.create_avatar_download(client_avatar_id);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(tr("Could not request download for avatar %s: %o"), client_avatar_id, error);
|
||||||
|
throw "Failed to request icon";
|
||||||
|
}
|
||||||
|
|
||||||
let avatar = this.resolveCached(client);
|
const downloader = new RequestFileDownload(download_key);
|
||||||
if(avatar) {
|
let response: Response;
|
||||||
if(avatar.url)
|
try {
|
||||||
img.attr("src", avatar.url);
|
response = await downloader.request_file();
|
||||||
else {
|
} catch(error) {
|
||||||
const type = image_type(avatar.base64);
|
console.error(tr("Could not download avatar %s: %o"), client_avatar_id, error);
|
||||||
|
throw "failed to download avatar";
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = image_type(response.headers.get('X-media-bytes'));
|
||||||
const media = media_image_type(type);
|
const media = media_image_type(type);
|
||||||
console.debug(tr("avatar has an image type of %o (media: %o)"), type, media);
|
|
||||||
img.attr("src", "data:image/" + media + ";base64," + avatar.base64);
|
|
||||||
}
|
|
||||||
tag.append(img);
|
|
||||||
} else {
|
|
||||||
let loader = $.spawn("img");
|
|
||||||
loader.attr("src", "img/loading_image.svg").css("width", "75%");
|
|
||||||
tag.append(loader);
|
|
||||||
|
|
||||||
this.loadAvatar(client).then(avatar => {
|
await this.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, {
|
||||||
if(avatar.url)
|
"X-avatar-id": avatar_id
|
||||||
img.attr("src", avatar.url);
|
|
||||||
else {
|
|
||||||
const type = image_type(avatar.base64);
|
|
||||||
const media = media_image_type(type);
|
|
||||||
console.debug(tr("Avatar has an image type of %o (media: %o)"), type, media);
|
|
||||||
img.attr("src", "data:image/" + media + ";base64," + avatar.base64);
|
|
||||||
}
|
|
||||||
console.debug("Avatar " + client.clientNickName() + " loaded :)");
|
|
||||||
|
|
||||||
img.css("opacity", 0);
|
|
||||||
tag.append(img);
|
|
||||||
loader.animate({opacity: 0}, 50, function () {
|
|
||||||
$(this).detach();
|
|
||||||
img.animate({opacity: 1}, 150);
|
|
||||||
});
|
});
|
||||||
}).catch(reason => {
|
const url = await this._response_url(response.clone(), type);
|
||||||
console.error(tr("Could not load avatar for %s. Reason: %s"), client.clientNickName(), reason);
|
|
||||||
|
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,
|
||||||
|
type: type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAvatar(client_avatar_id: string, avatar_id: string) : Promise<Avatar> {
|
||||||
|
return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
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"));
|
||||||
|
|
||||||
|
let cached_avatar: Avatar = this._cached_avatars[client_avatar_id];
|
||||||
|
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%");
|
||||||
|
avatar_container.append(loader_image);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let avatar: Avatar;
|
||||||
|
try {
|
||||||
|
avatar = await this.resolved_cached(client_avatar_id, avatar_id);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, () => {
|
||||||
|
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 id %s. Reason: %s"), client_avatar_id, reason);
|
||||||
//TODO Broken image
|
//TODO Broken image
|
||||||
loader.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);
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return tag;
|
return avatar_container;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
namespace connection {
|
namespace connection {
|
||||||
export class CommandHelper extends AbstractCommandHandler {
|
export class CommandHelper extends AbstractCommandHandler {
|
||||||
private _callbacks_namefromuid: ClientNameFromUid[] = [];
|
|
||||||
private _who_am_i: any;
|
private _who_am_i: any;
|
||||||
|
private _awaiters_unique_ids: {[unique_id: string]:((resolved: ClientNameInfo) => any)[]} = {};
|
||||||
|
|
||||||
constructor(connection) {
|
constructor(connection) {
|
||||||
super(connection);
|
super(connection);
|
||||||
|
@ -46,24 +46,50 @@ namespace connection {
|
||||||
return this.connection.send_command("clientupdate", data);
|
return this.connection.send_command("clientupdate", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
info_from_uid(...uid: string[]) : Promise<ClientNameInfo[]> {
|
async info_from_uid(..._unique_ids: string[]) : Promise<ClientNameInfo[]> {
|
||||||
let uids = [...uid];
|
const response: ClientNameInfo[] = [];
|
||||||
for(let p of this._callbacks_namefromuid)
|
const request = [];
|
||||||
if(p.keys == uids) return p.promise;
|
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) {
|
for(const unique_id of unique_ids) {
|
||||||
this.connection.send_command("clientgetnamefromuid", {
|
request.push({'cluid': unique_id});
|
||||||
cluid: uid
|
(this._awaiters_unique_ids[unique_id] || (this._awaiters_unique_ids[unique_id] = []))
|
||||||
}).catch(req.promise.function_rejected());
|
.push(unique_id_resolvers[unique_id] = info => response.push(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
this._callbacks_namefromuid.push(req);
|
try {
|
||||||
return req.promise;
|
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> {
|
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"));
|
let menu = context_menu || (context_menu = $(".context-menu"));
|
||||||
|
|
||||||
if(!menu.is(":visible")) return;
|
if(!menu.is(":visible")) return;
|
||||||
menu.hide(100);
|
menu.animate({opacity: 0}, 100, () => menu.css("display", "none"));
|
||||||
if(contextMenuCloseFn) contextMenuCloseFn();
|
if(contextMenuCloseFn) contextMenuCloseFn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,9 +110,10 @@ function generate_tag(entry: ContextMenuEntry) : JQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) {
|
function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) {
|
||||||
let menu = context_menu || (context_menu = $(".context-menu"));
|
let menu_tag = context_menu || (context_menu = $(".context-menu"));
|
||||||
menu.finish().empty();
|
menu_tag.finish().empty().css("opacity", "0");
|
||||||
|
|
||||||
|
const menu_container = $.spawn("div").addClass("context-menu-container");
|
||||||
contextMenuCloseFn = undefined;
|
contextMenuCloseFn = undefined;
|
||||||
|
|
||||||
for(const entry of entries){
|
for(const entry of entries){
|
||||||
|
@ -122,12 +123,18 @@ function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) {
|
||||||
if(entry.type == MenuEntryType.CLOSE) {
|
if(entry.type == MenuEntryType.CLOSE) {
|
||||||
contextMenuCloseFn = entry.callback;
|
contextMenuCloseFn = entry.callback;
|
||||||
} else
|
} 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)
|
// In the right position (the mouse)
|
||||||
menu.css({
|
menu_tag.css({
|
||||||
"top": y + "px",
|
"top": y + "px",
|
||||||
"left": x + "px"
|
"left": x + "px"
|
||||||
});
|
});
|
||||||
|
|
88
shared/js/crypto/src32.ts
Normal file
88
shared/js/crypto/src32.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
class Crc32 {
|
||||||
|
private static readonly lookup = [
|
||||||
|
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
|
||||||
|
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
|
||||||
|
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
|
||||||
|
0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
|
||||||
|
0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE,
|
||||||
|
0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
|
||||||
|
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
|
||||||
|
0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
|
||||||
|
0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
|
||||||
|
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
|
||||||
|
0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940,
|
||||||
|
0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
|
||||||
|
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116,
|
||||||
|
0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
|
||||||
|
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
|
||||||
|
0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
|
||||||
|
0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A,
|
||||||
|
0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
|
||||||
|
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
|
||||||
|
0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
|
||||||
|
0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
|
||||||
|
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
|
||||||
|
0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C,
|
||||||
|
0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
|
||||||
|
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2,
|
||||||
|
0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
|
||||||
|
0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
|
||||||
|
0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
|
||||||
|
0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
|
||||||
|
0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
|
||||||
|
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4,
|
||||||
|
0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
|
||||||
|
0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
|
||||||
|
0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
|
||||||
|
0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
|
||||||
|
0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
|
||||||
|
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE,
|
||||||
|
0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
|
||||||
|
0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
|
||||||
|
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
|
||||||
|
0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252,
|
||||||
|
0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
|
||||||
|
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60,
|
||||||
|
0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
|
||||||
|
0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
|
||||||
|
0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
|
||||||
|
0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04,
|
||||||
|
0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
|
||||||
|
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
|
||||||
|
0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
|
||||||
|
0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
|
||||||
|
0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
|
||||||
|
0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E,
|
||||||
|
0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
|
||||||
|
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C,
|
||||||
|
0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
|
||||||
|
0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
|
||||||
|
0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
|
||||||
|
0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
|
||||||
|
0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
|
||||||
|
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6,
|
||||||
|
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
|
||||||
|
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
|
||||||
|
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
|
||||||
|
];
|
||||||
|
|
||||||
|
private crc: number;
|
||||||
|
constructor() {
|
||||||
|
this.crc = -1 >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data: ArrayBufferLike) {
|
||||||
|
const dataView = new Uint8Array(data, 0);
|
||||||
|
const len = dataView.length;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
this.crc = (this.crc >>> 8) ^ Crc32.lookup[(this.crc ^ dataView[i]) & 0xFF];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
digest(radix: number) {
|
||||||
|
const buffer = new ArrayBuffer(4);
|
||||||
|
const dv = new DataView(buffer);
|
||||||
|
dv.setUint32(0, ~this.crc >>> 0, false);
|
||||||
|
return dv.getUint32(0).toString(radix || 16);
|
||||||
|
};
|
||||||
|
}
|
|
@ -39,21 +39,20 @@ namespace i18n {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileInfo {
|
|
||||||
name: string;
|
|
||||||
contributors: Contributor[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TranslationFile {
|
export interface TranslationFile {
|
||||||
url: string;
|
path: string;
|
||||||
|
full_url: string;
|
||||||
|
|
||||||
info: FileInfo;
|
|
||||||
translations: Translation[];
|
translations: Translation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepositoryTranslation {
|
export interface RepositoryTranslation {
|
||||||
key: string;
|
key: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
||||||
|
country_code: string;
|
||||||
|
name: string;
|
||||||
|
contributors: Contributor[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TranslationRepository {
|
export interface TranslationRepository {
|
||||||
|
@ -85,7 +84,7 @@ namespace i18n {
|
||||||
return translated;
|
return translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load_translation_file(url: string) : Promise<TranslationFile> {
|
async function load_translation_file(url: string, path: string) : Promise<TranslationFile> {
|
||||||
return new Promise<TranslationFile>((resolve, reject) => {
|
return new Promise<TranslationFile>((resolve, reject) => {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
|
@ -98,7 +97,8 @@ namespace i18n {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.url = url;
|
file.full_url = url;
|
||||||
|
file.path = path;
|
||||||
//TODO validate file
|
//TODO validate file
|
||||||
resolve(file);
|
resolve(file);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
|
@ -113,8 +113,8 @@ namespace i18n {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function load_file(url: string) : Promise<void> {
|
export function load_file(url: string, path: string) : Promise<void> {
|
||||||
return load_translation_file(url).then(result => {
|
return load_translation_file(url, path).then(result => {
|
||||||
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
|
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
|
||||||
translations = result.translations;
|
translations = result.translations;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -169,6 +169,7 @@ namespace i18n {
|
||||||
current_language?: string;
|
current_language?: string;
|
||||||
|
|
||||||
current_translation_url: string;
|
current_translation_url: string;
|
||||||
|
current_translation_path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepositoryConfig {
|
export interface RepositoryConfig {
|
||||||
|
@ -248,65 +249,31 @@ namespace i18n {
|
||||||
config.save_repository_config();
|
config.save_repository_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function iterate_translations(callback_entry: (repository: TranslationRepository, entry: TranslationFile) => any, callback_finish: () => any) {
|
export async function iterate_repositories(callback_entry: (repository: TranslationRepository) => any) {
|
||||||
let count = 0;
|
const promises = [];
|
||||||
const update_finish = () => {
|
|
||||||
if(count == 0 && callback_finish)
|
|
||||||
callback_finish();
|
|
||||||
};
|
|
||||||
|
|
||||||
for(const repo of registered_repositories()) {
|
for(const repository of registered_repositories()) {
|
||||||
count++;
|
promises.push(load_repository0(repository, false).then(() => callback_entry(repository)).catch(error => {
|
||||||
load_repository0(repo, false).then(() => {
|
log.warn(LogCategory.I18N, "Failed to fetch repository %s. error: %o", repository.url, error);
|
||||||
for(const translation of repo.translations || []) {
|
}));
|
||||||
const translation_path = repo.url + "/" + translation.path;
|
|
||||||
count++;
|
|
||||||
|
|
||||||
load_translation_file(translation_path).then(file => {
|
|
||||||
if(callback_entry) {
|
|
||||||
try {
|
|
||||||
callback_entry(repo, file);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
//TODO more error handling?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
count--;
|
await Promise.all(promises);
|
||||||
update_finish();
|
|
||||||
}).catch(error => {
|
|
||||||
log.warn(LogCategory.I18N, tr("Failed to load translation file for repository %s. Translation: %s (%s) Error: %o"), repo.name, translation.key, translation_path, error);
|
|
||||||
|
|
||||||
count--;
|
|
||||||
update_finish();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
count--;
|
export function select_translation(repository: TranslationRepository, entry: RepositoryTranslation) {
|
||||||
update_finish();
|
|
||||||
}).catch(error => {
|
|
||||||
log.warn(LogCategory.I18N, tr("Failed to load repository while iteration: %s (%s). Error: %o"), (repo || {name: "unknown"}).name, (repo || {url: "unknown"}).url, error);
|
|
||||||
|
|
||||||
count--;
|
|
||||||
update_finish();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
update_finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function select_translation(repository: TranslationRepository, entry: TranslationFile) {
|
|
||||||
const cfg = config.translation_config();
|
const cfg = config.translation_config();
|
||||||
|
|
||||||
if(entry && repository) {
|
if(entry && repository) {
|
||||||
cfg.current_language = entry.info.name;
|
cfg.current_language = entry.name;
|
||||||
cfg.current_repository_url = repository.url;
|
cfg.current_repository_url = repository.url;
|
||||||
cfg.current_translation_url = entry.url;
|
cfg.current_translation_url = repository.url + entry.path;
|
||||||
|
cfg.current_translation_path = entry.path;
|
||||||
} else {
|
} else {
|
||||||
cfg.current_language = undefined;
|
cfg.current_language = undefined;
|
||||||
cfg.current_repository_url = undefined;
|
cfg.current_repository_url = undefined;
|
||||||
cfg.current_translation_url = undefined;
|
cfg.current_translation_url = undefined;
|
||||||
|
cfg.current_translation_path = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.save_translation_config();
|
config.save_translation_config();
|
||||||
|
@ -318,7 +285,7 @@ namespace i18n {
|
||||||
|
|
||||||
if(cfg.current_translation_url) {
|
if(cfg.current_translation_url) {
|
||||||
try {
|
try {
|
||||||
await load_file(cfg.current_translation_url);
|
await load_file(cfg.current_translation_url, cfg.current_translation_path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
createErrorModal(tr("Translation System"), tr("Failed to load current selected translation file.") + "<br>File: " + cfg.current_translation_url + "<br>Error: " + error + "<br>" + tr("Using default fallback translations.")).open();
|
createErrorModal(tr("Translation System"), tr("Failed to load current selected translation file.") + "<br>File: " + cfg.current_translation_url + "<br>Error: " + error + "<br>" + tr("Using default fallback translations.")).open();
|
||||||
}
|
}
|
||||||
|
|
|
@ -351,16 +351,23 @@ namespace loader {
|
||||||
|
|
||||||
/* define that here */
|
/* define that here */
|
||||||
let _critical_triggered = false;
|
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 */
|
if(_critical_triggered) return; /* only show the first error */
|
||||||
_critical_triggered = true;
|
_critical_triggered = true;
|
||||||
|
|
||||||
let tag = document.getElementById("critical-load");
|
let tag = document.getElementById("critical-load");
|
||||||
|
|
||||||
let detail = tag.getElementsByClassName("detail")[0];
|
let detail = tag.getElementsByClassName("detail")[0];
|
||||||
detail.innerHTML = message;
|
detail.innerHTML = message;
|
||||||
|
|
||||||
|
if(error) {
|
||||||
|
const error_tags = tag.getElementsByClassName("error");
|
||||||
|
error_tags[0].innerHTML = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
//error-message
|
||||||
tag.style.display = "block";
|
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 => {
|
const loader_impl_display_critical_error = message => {
|
||||||
|
@ -498,6 +505,7 @@ const loader_javascript = {
|
||||||
"js/profiles/Identity.js",
|
"js/profiles/Identity.js",
|
||||||
|
|
||||||
//Load UI
|
//Load UI
|
||||||
|
"js/ui/modal/ModalAvatarList.js",
|
||||||
"js/ui/modal/ModalQuery.js",
|
"js/ui/modal/ModalQuery.js",
|
||||||
"js/ui/modal/ModalQueryManage.js",
|
"js/ui/modal/ModalQueryManage.js",
|
||||||
"js/ui/modal/ModalPlaylistList.js",
|
"js/ui/modal/ModalPlaylistList.js",
|
||||||
|
@ -509,7 +517,7 @@ const loader_javascript = {
|
||||||
"js/ui/modal/ModalServerEdit.js",
|
"js/ui/modal/ModalServerEdit.js",
|
||||||
"js/ui/modal/ModalChangeVolume.js",
|
"js/ui/modal/ModalChangeVolume.js",
|
||||||
"js/ui/modal/ModalBanClient.js",
|
"js/ui/modal/ModalBanClient.js",
|
||||||
|
"js/ui/modal/ModalIconSelect.js",
|
||||||
"js/ui/modal/ModalBanCreate.js",
|
"js/ui/modal/ModalBanCreate.js",
|
||||||
"js/ui/modal/ModalBanList.js",
|
"js/ui/modal/ModalBanList.js",
|
||||||
"js/ui/modal/ModalYesNo.js",
|
"js/ui/modal/ModalYesNo.js",
|
||||||
|
@ -636,6 +644,8 @@ const loader_style = {
|
||||||
"css/static/ts/country.css",
|
"css/static/ts/country.css",
|
||||||
"css/static/general.css",
|
"css/static/general.css",
|
||||||
"css/static/modals.css",
|
"css/static/modals.css",
|
||||||
|
"css/static/modal-avatar.css",
|
||||||
|
"css/static/modal-icons.css",
|
||||||
"css/static/modal-bookmarks.css",
|
"css/static/modal-bookmarks.css",
|
||||||
"css/static/modal-connect.css",
|
"css/static/modal-connect.css",
|
||||||
"css/static/modal-channel.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 */
|
/* register tasks */
|
||||||
loader.register_task(loader.Stage.INITIALIZING, {
|
loader.register_task(loader.Stage.INITIALIZING, {
|
||||||
name: "safari fix",
|
name: "safari fix",
|
||||||
|
@ -808,6 +798,7 @@ loader.register_task(loader.Stage.INITIALIZING, {
|
||||||
priority: 50
|
priority: 50
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window["Module"] = window["Module"] || {};
|
||||||
/* TeaClient */
|
/* TeaClient */
|
||||||
if(window.require) {
|
if(window.require) {
|
||||||
const path = require("path");
|
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, {
|
loader.register_task(loader.Stage.INITIALIZING, {
|
||||||
name: "webassembly tester",
|
name: "webassembly tester",
|
||||||
function: loader_webassembly.test_webassembly,
|
function: loader_webassembly.test_webassembly,
|
||||||
|
@ -872,17 +899,92 @@ loader.register_task(loader.Stage.LOADED, {
|
||||||
loader.register_task(loader.Stage.LOADED, {
|
loader.register_task(loader.Stage.LOADED, {
|
||||||
name: "error task",
|
name: "error task",
|
||||||
function: async () => {
|
function: async () => {
|
||||||
if(Settings.instance.static("dummy_load_error", false)) {
|
if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) {
|
||||||
displayCriticalError("The tea is cold!");
|
display_critical_load("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good.");
|
||||||
throw "The tea is cold!";
|
throw "The tea is cold!";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
priority: 20
|
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(() => {
|
loader.execute().then(() => {
|
||||||
console.log("app successfully loaded!");
|
console.log("app successfully loaded!");
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
displayCriticalError("failed to load app!<br>Please lookup the browser console for more details");
|
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]
|
[LogCategory.IDENTITIES, true]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
enum GroupMode {
|
||||||
|
NATIVE,
|
||||||
|
PREFIX
|
||||||
|
}
|
||||||
|
const group_mode: GroupMode = GroupMode.NATIVE;
|
||||||
|
|
||||||
loader.register_task(loader.Stage.LOADED, {
|
loader.register_task(loader.Stage.LOADED, {
|
||||||
name: "log enabled initialisation",
|
name: "log enabled initialisation",
|
||||||
function: async () => initialize(),
|
function: async () => initialize(),
|
||||||
|
@ -112,12 +118,17 @@ namespace log {
|
||||||
name = "[%s] " + name;
|
name = "[%s] " + name;
|
||||||
optionalParams.unshift(category_mapping.get(category));
|
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 {
|
export function table(title: string, arguments: any) {
|
||||||
NATIVE,
|
if(group_mode == GroupMode.NATIVE) {
|
||||||
PREFIX
|
console.groupCollapsed(title);
|
||||||
|
console.table(arguments);
|
||||||
|
console.groupEnd();
|
||||||
|
} else {
|
||||||
|
console.log("Snipped table %s", title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Group {
|
export class Group {
|
||||||
|
@ -130,7 +141,7 @@ namespace log {
|
||||||
|
|
||||||
private readonly name: string;
|
private readonly name: string;
|
||||||
private readonly optionalParams: any[][];
|
private readonly optionalParams: any[][];
|
||||||
private _collapsed: boolean = true;
|
private _collapsed: boolean = false;
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
private _log_prefix: string;
|
private _log_prefix: string;
|
||||||
|
|
||||||
|
|
|
@ -282,6 +282,35 @@ function main() {
|
||||||
stats.register_user_count_listener(status => {
|
stats.register_user_count_listener(status => {
|
||||||
console.log("Received user count update: %o", status);
|
console.log("Received user count update: %o", status);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
setTimeout(() => {
|
||||||
|
Modals.spawnAvatarList(globalClient);
|
||||||
|
}, 1000);
|
||||||
|
*/
|
||||||
|
(<any>window).test_upload = () => {
|
||||||
|
const data = "Hello World";
|
||||||
|
globalClient.fileManager.upload_file({
|
||||||
|
size: data.length,
|
||||||
|
overwrite: true,
|
||||||
|
channel: globalClient.getClient().currentChannel(),
|
||||||
|
name: '/HelloWorld.txt',
|
||||||
|
path: ''
|
||||||
|
}).then(key => {
|
||||||
|
console.log("Got key: %o", key);
|
||||||
|
const upload = new RequestFileUpload(key);
|
||||||
|
|
||||||
|
const buffer = new Uint8Array(data.length);
|
||||||
|
{
|
||||||
|
for(let index = 0; index < data.length; index++)
|
||||||
|
buffer[index] = data.charCodeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
upload.put_data(buffer).catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
loader.register_task(loader.Stage.LOADED, {
|
loader.register_task(loader.Stage.LOADED, {
|
||||||
|
|
|
@ -550,6 +550,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
||||||
this._group_mapping = PermissionManager.group_mapping.slice();
|
this._group_mapping = PermissionManager.group_mapping.slice();
|
||||||
|
|
||||||
let group = log.group(log.LogType.TRACE, LogCategory.PERMISSIONS, tr("Permission mapping"));
|
let group = log.group(log.LogType.TRACE, LogCategory.PERMISSIONS, tr("Permission mapping"));
|
||||||
|
const table_entries = [];
|
||||||
for(let e of json) {
|
for(let e of json) {
|
||||||
if(e["group_id_end"]) {
|
if(e["group_id_end"]) {
|
||||||
let group = new PermissionGroup();
|
let group = new PermissionGroup();
|
||||||
|
@ -564,15 +565,22 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
||||||
group.deep = info.deep;
|
group.deep = info.deep;
|
||||||
}
|
}
|
||||||
this.permissionGroups.push(group);
|
this.permissionGroups.push(group);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let perm = new PermissionInfo();
|
let perm = new PermissionInfo();
|
||||||
perm.name = e["permname"];
|
perm.name = e["permname"];
|
||||||
perm.id = parseInt(e["permid"]);
|
perm.id = parseInt(e["permid"]);
|
||||||
perm.description = e["permdesc"];
|
perm.description = e["permdesc"];
|
||||||
group.log(tr("%i <> %s -> %s"), perm.id, perm.name, perm.description);
|
|
||||||
this.permissionList.push(perm);
|
this.permissionList.push(perm);
|
||||||
|
|
||||||
|
table_entries.push({
|
||||||
|
"id": perm.id,
|
||||||
|
"name": perm.name,
|
||||||
|
"description": perm.description
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
log.table("Permission list", table_entries);
|
||||||
group.end();
|
group.end();
|
||||||
|
|
||||||
log.info(LogCategory.PERMISSIONS, tr("Got %i permissions"), this.permissionList.length);
|
log.info(LogCategory.PERMISSIONS, tr("Got %i permissions"), this.permissionList.length);
|
||||||
|
@ -594,6 +602,8 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
||||||
let addcount = 0;
|
let addcount = 0;
|
||||||
|
|
||||||
let group = log.group(log.LogType.TRACE, LogCategory.PERMISSIONS, tr("Got %d needed permissions."), json.length);
|
let group = log.group(log.LogType.TRACE, LogCategory.PERMISSIONS, tr("Got %d needed permissions."), json.length);
|
||||||
|
const table_entries = [];
|
||||||
|
|
||||||
for(let e of json) {
|
for(let e of json) {
|
||||||
let entry: NeededPermissionValue = undefined;
|
let entry: NeededPermissionValue = undefined;
|
||||||
for(let p of copy) {
|
for(let p of copy) {
|
||||||
|
@ -618,11 +628,16 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
||||||
if(entry.value == parseInt(e["permvalue"])) continue;
|
if(entry.value == parseInt(e["permvalue"])) continue;
|
||||||
entry.value = parseInt(e["permvalue"]);
|
entry.value = parseInt(e["permvalue"]);
|
||||||
|
|
||||||
//TODO tr
|
|
||||||
group.log("Update needed permission " + entry.type.name + " to " + entry.value);
|
|
||||||
for(let listener of entry.changeListener)
|
for(let listener of entry.changeListener)
|
||||||
listener(entry.value);
|
listener(entry.value);
|
||||||
|
|
||||||
|
table_entries.push({
|
||||||
|
"permission": entry.type.name,
|
||||||
|
"value": entry.value
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.table("Needed client permissions", table_entries);
|
||||||
group.end();
|
group.end();
|
||||||
|
|
||||||
//TODO tr
|
//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'
|
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 */
|
/* Control bar */
|
||||||
static readonly KEY_CONTROL_MUTE_INPUT: SettingsKey<boolean> = {
|
static readonly KEY_CONTROL_MUTE_INPUT: SettingsKey<boolean> = {
|
||||||
key: 'mute_input'
|
key: 'mute_input'
|
||||||
|
|
|
@ -663,13 +663,22 @@ class ChannelEntry {
|
||||||
updateVariables(...variables: {key: string, value: string}[]) {
|
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());
|
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) {
|
for(let variable of variables) {
|
||||||
let key = variable.key;
|
let key = variable.key;
|
||||||
let value = variable.value;
|
let value = variable.value;
|
||||||
JSON.map_field_to(this.properties, value, variable.key);
|
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") {
|
if(key == "channel_name") {
|
||||||
this.__updateChannelName();
|
this.__updateChannelName();
|
||||||
} else if(key == "channel_order") {
|
} else if(key == "channel_order") {
|
||||||
|
|
|
@ -581,11 +581,20 @@ class ClientEntry {
|
||||||
let update_away = false;
|
let update_away = false;
|
||||||
let reorder_channel = 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) {
|
for(let variable of variables) {
|
||||||
JSON.map_field_to(this._properties, variable.value, variable.key);
|
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") {
|
if(variable.key == "client_nickname") {
|
||||||
this.tag.find(".client-name").text(variable.value);
|
this.tag.find(".client-name").text(variable.value);
|
||||||
let chat = this.chat(false);
|
let chat = this.chat(false);
|
||||||
|
|
|
@ -48,12 +48,10 @@ class ControlBar {
|
||||||
tag.find(".button-dropdown").on('click', () => {
|
tag.find(".button-dropdown").on('click', () => {
|
||||||
tag.addClass("displayed");
|
tag.addClass("displayed");
|
||||||
}).hover(() => {
|
}).hover(() => {
|
||||||
console.log("Add");
|
|
||||||
tag.addClass("displayed");
|
tag.addClass("displayed");
|
||||||
}, () => {
|
}, () => {
|
||||||
if(tag.find(".dropdown:hover").length > 0)
|
if(tag.find(".dropdown:hover").length > 0)
|
||||||
return;
|
return;
|
||||||
console.log("Removed");
|
|
||||||
tag.removeClass("displayed");
|
tag.removeClass("displayed");
|
||||||
});
|
});
|
||||||
tag.on('mouseleave', () => {
|
tag.on('mouseleave', () => {
|
||||||
|
@ -88,9 +86,10 @@ class ControlBar {
|
||||||
let query = this.htmlTag.find(".btn_query");
|
let query = this.htmlTag.find(".btn_query");
|
||||||
dropdownify(query);
|
dropdownify(query);
|
||||||
|
|
||||||
query.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
|
/* search for query buttons not only on the large device button */
|
||||||
query.find(".btn_query_create").on('click', this.on_query_create.bind(this));
|
this.htmlTag.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
|
||||||
query.find(".btn_query_manage").on('click', this.on_query_manage.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 */
|
/* Mobile dropdowns */
|
||||||
|
@ -429,7 +428,6 @@ class ControlBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
set query_visible(flag: boolean) {
|
set query_visible(flag: boolean) {
|
||||||
console.error(flag);
|
|
||||||
if(this._query_visible == flag) return;
|
if(this._query_visible == flag) return;
|
||||||
|
|
||||||
this._query_visible = flag;
|
this._query_visible = flag;
|
||||||
|
@ -444,7 +442,12 @@ class ControlBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
private update_query_visibility_button() {
|
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() {
|
private on_query_create() {
|
||||||
|
|
|
@ -149,6 +149,7 @@ class Hostbanner {
|
||||||
readonly client: TSClient;
|
readonly client: TSClient;
|
||||||
|
|
||||||
private updater: NodeJS.Timer;
|
private updater: NodeJS.Timer;
|
||||||
|
private _hostbanner_url: string;
|
||||||
|
|
||||||
constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) {
|
constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) {
|
||||||
this.client = client;
|
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 + ')');
|
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);
|
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) {
|
} catch(error) {
|
||||||
log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), 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) {
|
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;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
162
shared/js/ui/modal/ModalAvatarList.ts
Normal file
162
shared/js/ui/modal/ModalAvatarList.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -157,7 +157,7 @@ namespace Modals {
|
||||||
|
|
||||||
let Regex = {
|
let Regex = {
|
||||||
//DOMAIN<:port>
|
//DOMAIN<:port>
|
||||||
DOMAIN: /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,5}))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,4}))$/,
|
DOMAIN: /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,46}))$/,
|
||||||
//IP<:port>
|
//IP<:port>
|
||||||
IP_V4: /(^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,4}))$/,
|
IP_V4: /(^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[0-5]?[0-9]{1,4}))$/,
|
||||||
IP_V6: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
|
IP_V6: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
|
||||||
|
|
|
@ -6,10 +6,14 @@ namespace Modals {
|
||||||
const modal = createModal({
|
const modal = createModal({
|
||||||
header: channel ? tr("Edit channel") : tr("Create channel"),
|
header: channel ? tr("Edit channel") : tr("Create channel"),
|
||||||
body: () => {
|
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_maxfamilyclients_unlimited: true,
|
||||||
channel_flag_maxclients_unlimited: true
|
channel_flag_maxclients_unlimited: true,
|
||||||
} as ChannelProperties);
|
});
|
||||||
|
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();
|
return template.tabify();
|
||||||
},
|
},
|
||||||
footer: () => {
|
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);
|
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);
|
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);
|
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();
|
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 = () => {
|
let updateButton = () => {
|
||||||
if(tag.find(".input_error").length == 0)
|
if(tag.find(".input_error").length == 0)
|
||||||
button.removeAttr("disabled");
|
button.removeAttr("disabled");
|
||||||
|
@ -82,7 +86,18 @@ namespace Modals {
|
||||||
if(this.value.length < 1 || this.value.length > 40)
|
if(this.value.length < 1 || this.value.length > 40)
|
||||||
$(this).addClass("input_error");
|
$(this).addClass("input_error");
|
||||||
updateButton();
|
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) {
|
tag.find(".channel_password").change(function (this: HTMLInputElement) {
|
||||||
properties.channel_flag_password = this.value.length != 0;
|
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))
|
if(globalClient.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1))
|
||||||
$(this).addClass("input_error");
|
$(this).addClass("input_error");
|
||||||
updateButton();
|
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) {
|
tag.find(".channel_topic").change(function (this: HTMLInputElement) {
|
||||||
properties.channel_topic = this.value;
|
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) {
|
tag.find(".channel_description").change(function (this: HTMLInputElement) {
|
||||||
properties.channel_description = this.value;
|
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(() => {
|
setTimeout(() => {
|
||||||
tag.find(".channel_name").trigger("change");
|
tag.find(".channel_name").trigger("change");
|
||||||
tag.find(".channel_password").trigger('change');
|
tag.find(".channel_password").trigger('change');
|
||||||
|
|
144
shared/js/ui/modal/ModalIconSelect.ts
Normal file
144
shared/js/ui/modal/ModalIconSelect.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,14 +4,18 @@ namespace Modals {
|
||||||
export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => any) {
|
export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => any) {
|
||||||
let properties: ServerProperties = {} as ServerProperties; //The changes properties
|
let properties: ServerProperties = {} as ServerProperties; //The changes properties
|
||||||
|
|
||||||
const modal_template = $("#tmpl_server_edit").renderTag(server.properties);
|
const render_properties = {};
|
||||||
|
Object.assign(render_properties, server.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) => {
|
const modal = modal_template.modalize((header, body, footer) => {
|
||||||
return {
|
return {
|
||||||
body: body.tabify()
|
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_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_applyHostListener(server, properties, server.properties, modal.htmlTag.find(".properties_host"), modal.htmlTag.find(".button_ok"));
|
||||||
server_applyMessages(properties, server, modal.htmlTag.find(".properties_messages"));
|
server_applyMessages(properties, server, modal.htmlTag.find(".properties_messages"));
|
||||||
|
@ -32,7 +36,7 @@ namespace Modals {
|
||||||
modal.open();
|
modal.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function server_applyGeneralListener(properties: ServerProperties, tag: JQuery, button: JQuery) {
|
function server_applyGeneralListener(properties: ServerProperties, server: ServerEntry, tag: JQuery, button: JQuery) {
|
||||||
let updateButton = () => {
|
let updateButton = () => {
|
||||||
if(tag.find(".input_error").length == 0)
|
if(tag.find(".input_error").length == 0)
|
||||||
button.removeAttr("disabled");
|
button.removeAttr("disabled");
|
||||||
|
@ -81,6 +85,17 @@ namespace Modals {
|
||||||
tag.find(".virtualserver_welcomemessage").change(function (this: HTMLInputElement) {
|
tag.find(".virtualserver_welcomemessage").change(function (this: HTMLInputElement) {
|
||||||
properties.virtualserver_welcomemessage = this.value;
|
properties.virtualserver_welcomemessage = this.value;
|
||||||
}).prop("disabled", !globalClient.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE).granted(1));
|
}).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);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -611,7 +611,7 @@ namespace Modals {
|
||||||
};
|
};
|
||||||
|
|
||||||
tag_loading.show();
|
tag_loading.show();
|
||||||
i18n.iterate_translations((repo, entry) => {
|
i18n.iterate_repositories(repo => {
|
||||||
let repo_tag = tag_list.find("[repository=\"" + repo.unique_id + "\"]");
|
let repo_tag = tag_list.find("[repository=\"" + repo.unique_id + "\"]");
|
||||||
if (repo_tag.length == 0) {
|
if (repo_tag.length == 0) {
|
||||||
repo_tag = template.renderTag({
|
repo_tag = template.renderTag({
|
||||||
|
@ -639,11 +639,13 @@ namespace Modals {
|
||||||
tag_list.append(repo_tag);
|
tag_list.append(repo_tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(const translation of repo.translations) {
|
||||||
const tag = template.renderTag({
|
const tag = template.renderTag({
|
||||||
type: "translation",
|
type: "translation",
|
||||||
name: entry.info.name || entry.url,
|
name: translation.name || translation.path,
|
||||||
id: repo.unique_id,
|
id: repo.unique_id,
|
||||||
selected: i18n.config.translation_config().current_translation_url == entry.url
|
country_code: translation.country_code,
|
||||||
|
selected: i18n.config.translation_config().current_translation_path == translation.path
|
||||||
});
|
});
|
||||||
tag.find(".button-info").on('click', e => {
|
tag.find(".button-info").on('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -653,10 +655,10 @@ namespace Modals {
|
||||||
body: () => {
|
body: () => {
|
||||||
const tag = $("#settings-translations-list-entry-info").renderTag({
|
const tag = $("#settings-translations-list-entry-info").renderTag({
|
||||||
type: "translation",
|
type: "translation",
|
||||||
name: entry.info.name,
|
name: translation.name,
|
||||||
url: entry.url,
|
url: translation.path,
|
||||||
repository_name: repo.name,
|
repository_name: repo.name,
|
||||||
contributors: entry.info.contributors || []
|
contributors: translation.contributors || []
|
||||||
});
|
});
|
||||||
|
|
||||||
tag.find(".button-info").on('click', () => display_repository_info(repo));
|
tag.find(".button-info").on('click', () => display_repository_info(repo));
|
||||||
|
@ -682,16 +684,18 @@ namespace Modals {
|
||||||
});
|
});
|
||||||
tag.on('click', e => {
|
tag.on('click', e => {
|
||||||
if (e.isDefaultPrevented()) return;
|
if (e.isDefaultPrevented()) return;
|
||||||
i18n.select_translation(repo, entry);
|
i18n.select_translation(repo, translation);
|
||||||
tag_list.find(".selected").removeClass("selected");
|
tag_list.find(".selected").removeClass("selected");
|
||||||
tag.addClass("selected");
|
tag.addClass("selected");
|
||||||
|
|
||||||
restart_hint.show();
|
restart_hint.show();
|
||||||
});
|
});
|
||||||
tag.insertAfter(repo_tag)
|
tag.insertAfter(repo_tag);
|
||||||
}, () => {
|
}
|
||||||
tag_loading.hide();
|
}).then(() => tag_loading.hide()).catch(error => {
|
||||||
});
|
console.error(error);
|
||||||
|
/* this should NEVER happen */
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
type: MenuEntryType.ENTRY,
|
||||||
icon: "client-invite_buddy",
|
icon: "client-invite_buddy",
|
||||||
|
@ -183,12 +193,21 @@ class ServerEntry {
|
||||||
updateVariables(is_self_notify: boolean, ...variables: {key: string, value: string}[]) {
|
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);
|
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;
|
let update_bannner = false;
|
||||||
for(let variable of variables) {
|
for(let variable of variables) {
|
||||||
JSON.map_field_to(this.properties, variable.value, variable.key);
|
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") {
|
if(variable.key == "virtualserver_name") {
|
||||||
this.htmlTag.find(".name").text(variable.value);
|
this.htmlTag.find(".name").text(variable.value);
|
||||||
} else if(variable.key == "virtualserver_icon_id") {
|
} else if(variable.key == "virtualserver_icon_id") {
|
||||||
|
|
|
@ -3819,7 +3819,7 @@ class ClientEntry {
|
||||||
static chatTag(id, name, uid, braces = false) {
|
static chatTag(id, name, uid, braces = false) {
|
||||||
return $(htmltags.generate_client({
|
return $(htmltags.generate_client({
|
||||||
client_name: name,
|
client_name: name,
|
||||||
client_id: id,
|
client_avatar_id: id,
|
||||||
client_unique_id: uid,
|
client_unique_id: uid,
|
||||||
add_braces: braces
|
add_braces: braces
|
||||||
}));
|
}));
|
||||||
|
@ -7668,7 +7668,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
||||||
let client = parseInt(json[0]["cldbid"]);
|
let client = parseInt(json[0]["cldbid"]);
|
||||||
let permissions = PermissionManager.parse_permission_bulk(json, this);
|
let permissions = PermissionManager.parse_permission_bulk(json, this);
|
||||||
for (let req of this.requests_client_permissions.slice(0)) {
|
for (let req of this.requests_client_permissions.slice(0)) {
|
||||||
if (req.client_id == client) {
|
if (req.client_avatar_id == client) {
|
||||||
this.requests_client_permissions.remove(req);
|
this.requests_client_permissions.remove(req);
|
||||||
req.promise.resolved(permissions);
|
req.promise.resolved(permissions);
|
||||||
}
|
}
|
||||||
|
@ -7676,7 +7676,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
requestClientPermissions(client_id) {
|
requestClientPermissions(client_id) {
|
||||||
for (let request of this.requests_client_permissions)
|
for (let request of this.requests_client_permissions)
|
||||||
if (request.client_id == client_id && request.promise.time() + 1000 > Date.now())
|
if (request.client_avatar_id == client_id && request.promise.time() + 1000 > Date.now())
|
||||||
return request.promise;
|
return request.promise;
|
||||||
let request = {};
|
let request = {};
|
||||||
request.client_id = client_id;
|
request.client_id = client_id;
|
||||||
|
@ -7692,7 +7692,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
requestClientChannelPermissions(client_id, channel_id) {
|
requestClientChannelPermissions(client_id, channel_id) {
|
||||||
for (let request of this.requests_client_channel_permissions)
|
for (let request of this.requests_client_channel_permissions)
|
||||||
if (request.client_id == client_id && request.channel_id == channel_id && request.promise.time() + 1000 > Date.now())
|
if (request.client_avatar_id == client_id && request.channel_id == channel_id && request.promise.time() + 1000 > Date.now())
|
||||||
return request.promise;
|
return request.promise;
|
||||||
let request = {};
|
let request = {};
|
||||||
request.client_id = client_id;
|
request.client_id = client_id;
|
||||||
|
@ -10761,7 +10761,7 @@ class IconManager {
|
||||||
return this.handle.requestFileList("/icons");
|
return this.handle.requestFileList("/icons");
|
||||||
}
|
}
|
||||||
downloadIcon(id) {
|
downloadIcon(id) {
|
||||||
return this.handle.requestFileDownload("", "/icon_" + id);
|
return this.handle.download_file("", "/icon_" + id);
|
||||||
}
|
}
|
||||||
resolveCached(id) {
|
resolveCached(id) {
|
||||||
let icon = localStorage.getItem("icon_" + id);
|
let icon = localStorage.getItem("icon_" + id);
|
||||||
|
@ -10811,7 +10811,7 @@ class IconManager {
|
||||||
this.load_finished(id);
|
this.load_finished(id);
|
||||||
resolve(icon);
|
resolve(icon);
|
||||||
};
|
};
|
||||||
ft.startTransfer();
|
ft.start();
|
||||||
}).catch(reason => {
|
}).catch(reason => {
|
||||||
console.error(_translations.DtVBEPYe || (_translations.DtVBEPYe = tr("Error while downloading icon! (%s)")), tr(JSON.stringify(reason)));
|
console.error(_translations.DtVBEPYe || (_translations.DtVBEPYe = tr("Error while downloading icon! (%s)")), tr(JSON.stringify(reason)));
|
||||||
chat.serverChat().appendError(_translations.z_jAHQFu || (_translations.z_jAHQFu = tr("Failed to request download for icon {0}. ({1})")), id, tr(JSON.stringify(reason)));
|
chat.serverChat().appendError(_translations.z_jAHQFu || (_translations.z_jAHQFu = tr("Failed to request download for icon {0}. ({1})")), id, tr(JSON.stringify(reason)));
|
||||||
|
@ -10874,7 +10874,7 @@ class AvatarManager {
|
||||||
}
|
}
|
||||||
downloadAvatar(client) {
|
downloadAvatar(client) {
|
||||||
console.log(_translations.FXXPuv5A || (_translations.FXXPuv5A = tr("Downloading avatar %s")), client.avatarId());
|
console.log(_translations.FXXPuv5A || (_translations.FXXPuv5A = tr("Downloading avatar %s")), client.avatarId());
|
||||||
return this.handle.requestFileDownload("", "/avatar_" + client.avatarId());
|
return this.handle.download_file("", "/avatar_" + client.avatarId());
|
||||||
}
|
}
|
||||||
resolveCached(client) {
|
resolveCached(client) {
|
||||||
let avatar = localStorage.getItem("avatar_" + client.properties.client_unique_identifier);
|
let avatar = localStorage.getItem("avatar_" + client.properties.client_unique_identifier);
|
||||||
|
@ -10943,7 +10943,7 @@ class AvatarManager {
|
||||||
this.load_finished(name);
|
this.load_finished(name);
|
||||||
resolve(avatar);
|
resolve(avatar);
|
||||||
};
|
};
|
||||||
ft.startTransfer();
|
ft.start();
|
||||||
}).catch(reason => {
|
}).catch(reason => {
|
||||||
this.load_finished(name);
|
this.load_finished(name);
|
||||||
console.error(_translations.ynLLTaB0 || (_translations.ynLLTaB0 = tr("Error while downloading avatar! (%s)")), JSON.stringify(reason));
|
console.error(_translations.ynLLTaB0 || (_translations.ynLLTaB0 = tr("Error while downloading avatar! (%s)")), JSON.stringify(reason));
|
||||||
|
@ -16401,8 +16401,8 @@ var htmltags;
|
||||||
let result = "";
|
let result = "";
|
||||||
/* build the opening tag: <div ...> */
|
/* build the opening tag: <div ...> */
|
||||||
result = result + "<div class='htmltag-client' ";
|
result = result + "<div class='htmltag-client' ";
|
||||||
if (properties.client_id)
|
if (properties.client_avatar_id)
|
||||||
result = result + "client-id='" + properties.client_id + "' ";
|
result = result + "client-id='" + properties.client_avatar_id + "' ";
|
||||||
if (properties.client_unique_id && properties.client_unique_id != "unknown")
|
if (properties.client_unique_id && properties.client_unique_id != "unknown")
|
||||||
result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' ";
|
result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' ";
|
||||||
if (properties.client_name)
|
if (properties.client_name)
|
||||||
|
@ -16523,7 +16523,7 @@ var htmltags;
|
||||||
const groups = url_client_regex.exec(params);
|
const groups = url_client_regex.exec(params);
|
||||||
return generate_client_open({
|
return generate_client_open({
|
||||||
add_braces: false,
|
add_braces: false,
|
||||||
client_id: parseInt(groups[1]),
|
client_avatar_id: parseInt(groups[1]),
|
||||||
client_unique_id: groups[2],
|
client_unique_id: groups[2],
|
||||||
client_name: decodeURIComponent(groups[3])
|
client_name: decodeURIComponent(groups[3])
|
||||||
});
|
});
|
||||||
|
|
2
vendor/bbcode
vendored
2
vendor/bbcode
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 5773a1301b6c4b218496da5d04b4dfc2e25a4fa9
|
Subproject commit 454f0a8069fd842ff967b0a7f127f2d89e97f2be
|
Loading…
Add table
Reference in a new issue