A lot of new style changes

canary
WolverinDEV 2019-08-21 10:00:01 +02:00
parent 8fd38ff5f1
commit 016f7cec9a
234 changed files with 37252 additions and 17928 deletions

View File

@ -1,7 +1,27 @@
# Changelog:
* **XX.XX.XX**
- Removed icon size restriction for SVGs
- Fixed permission editor icon select for not granted icon permissions
- Fixed "disconnect" button not showing up after beeing connected
- Improved handling of `disableMultiSession` settings (Connect in a new tab does not show up anymore)
- Implemented avatar upload
- Sorting server group icons within client channel tree
- Fixed buggy away message position
- Logging the servers welcome message [#54](https://github.com/TeaSpeak/TeaWeb/issues/54)
- Showing servers hostbutton
- Fixed microphone and sound action sounds [#67](https://github.com/TeaSpeak/TeaWeb/issues/67)
- Added option to mute clients [#64](https://github.com/TeaSpeak/TeaWeb/issues/64)
- Improved debug loader (no dependency faults anymore)
- Saving private conversations and showing the messages again after client restart
- Fixed some general memory leaks
- Implemented the hostmessage functions
- Fixed bookmark server password
Big UI Improvement:
- New "dark theme" design
- All elements are responsive to the font-size (Supporting now large & small screens (No mobile support yet))
- Implemented an active ping calculation
* **22.06.19**
- Fixed channel create not working issue

View File

@ -22,4 +22,8 @@ html, body {
display: flex; flex-direction: column; resize: both;
}
}
footer {
display: none!important;
}

0
client/js/.keepalive Normal file
View File

View File

@ -1,33 +0,0 @@
namespace forum {
const ipc = require("electron").ipcRenderer;
let callback_listener: (() => any)[] = [];
ipc.on('teaforo-update', (event, data) => {
console.log("Got data update: %o", data);
profiles.identities.set_static_identity(data ? new profiles.identities.TeaForumIdentity(data.application_data, data.application_data_sign) : undefined);
try {
for(let listener of callback_listener)
setImmediate(listener);
} catch(e) {
console.log(e);
}
callback_listener = [];
});
export function register_callback(callback: () => any) {
callback_listener.push(callback);
}
export function open() {
ipc.send("teaforo-login");
}
export function logout() {
ipc.send("teaforo-logout");
}
export function sync_main() {
ipc.send("teaforo-update");
}
}

View File

@ -34,7 +34,7 @@
"path" => "js/",
"local-path" => "./shared/js/",
"req-parm" => ["-js-map"]
"req-parm" => ["--mappings"]
],
[ /* shared generated worker codec */
"type" => "js",
@ -52,6 +52,15 @@
"path" => "css/",
"local-path" => "./shared/css/"
],
[ /* shared css mapping files (development mode only) */
"type" => "css",
"search-pattern" => "/.*\.(css.map|scss)$/",
"build-target" => "dev",
"path" => "css/",
"local-path" => "./shared/css/",
"req-parm" => ["--mappings"]
],
[ /* shared release css files */
"type" => "css",
"search-pattern" => "/.*\.css$/",
@ -137,7 +146,7 @@
$APP_FILE_LIST_SHARED_VENDORS = [
[
"type" => "js",
"search-pattern" => "/.*\.js$/",
"search-pattern" => "/.*(\.min)?\.js$/",
"build-target" => "dev|rel",
"path" => "vendor/",

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,6 +6,14 @@ sound.egg;WolverinDEV is the best and I love TeaSpeak!
away_activated;See you soon
away_deactivated;Welcome back
#Microphone
microphone.muted;Microphone muted
microphone.activated;Microphone activated
#Sound
sound.muted;Sound muted
sound.activated;Sound activated
#Connection
connection.connected;Connected
connection.disconnected;Disconnected
@ -44,6 +52,7 @@ user.left.kicked.server;User in your channel got kicked from the server
user.left.moved;User was moved out of your channel
user.left.disconnect;User disconnected from your channel
user.left.banned;User in your channel was banned from the server
user.left.timeout;User in your channel timed out
#Error
error.insufficient_permissions;insufficient permissions

1 #Sound test
6 away_deactivated;Welcome back
7 #Connection #Microphone
8 connection.connected;Connected microphone.muted;Microphone muted
9 microphone.activated;Microphone activated
10 #Sound
11 sound.muted;Sound muted
12 sound.activated;Sound activated
13 #Connection
14 connection.connected;Connected
15 connection.disconnected;Disconnected
16 connection.disconnected.timeout;Connection to server lost
17 connection.disconnected;Disconnected connection.refused;Connect failed
18 connection.disconnected.timeout;Connection to server lost connection.banned;You got banned from this server
19 connection.refused;Connect failed #Server
52 group.server.revoked;Server group revoked group.channel.changed;Channel group changed
53 group.channel.changed;Channel group changed group.server.assigned.self;Server group assigned
54 group.server.assigned.self;Server group assigned group.server.revoked.self;Server group revoked
55 group.channel.changed.self;Channel group changed
56
57
58

View File

@ -1,3 +1,4 @@
declare namespace connection {
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection;
export function destroy_server_connection(handle: AbstractServerConnection);
}

View File

@ -1,4 +1,15 @@
@import "properties";
@import "mixin";
.channel-tree-container {
height: 100%;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
overflow-y: auto;
}
/* the channel tree */
.channel-tree {
@ -71,6 +82,11 @@
align-self: center;
color: $channel_tree_entry_text_color;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.icon_property {
@ -84,6 +100,8 @@
flex-direction: column;
.container-channel {
position: relative;
display: flex;
flex-direction: row;
justify-content: stretch;
@ -94,6 +112,39 @@
align-items: center;
cursor: pointer;
.marker-text-unread {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1px;
background-color: #a814147F;
opacity: 1;
&:before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 24px;
background: -moz-linear-gradient(left, rgba(168,20,20,.18) 0%, rgba(168,20,20,0) 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(left, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to right, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
}
&.hidden {
opacity: 0;
}
@include transition(opacity $button_hover_animation_time);
}
.channel-type {
flex-grow: 0;
flex-shrink: 0;
@ -125,6 +176,17 @@
.channel-name {
align-self: center;
color: $channel_tree_entry_text_color;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.align-repetitive {
.channel-name {
text-overflow: clip;
}
}
}
@ -174,16 +236,20 @@
}
.client-name {
line-height: 16px;
flex-grow: 0;
flex-shrink: 1;
padding-right: .25em;
color: $channel_tree_entry_text_color;
&.client-name-own {
font-weight: bold;
}
}
line-height: 16px;
flex-grow: 1;
flex-shrink: 1;
min-width: 75px;
.client-away-message {
color: $channel_tree_entry_text_color;
}
@ -191,7 +257,8 @@
margin-right: 0; /* override from previous thing */
position: absolute;
right: 5px;
right: 0;
padding-right: 5px;
display: flex;
flex-direction: row;
@ -211,14 +278,30 @@
}
&.selected {
&:focus-within {
.container-icons {
background-color: $channel_tree_entry_selected;
padding-left: 5px;
z-index: 1001; /* show before client name */
height: 18px;
}
}
.client-name {
&:focus {
position: absolute;
color: black;
padding-top: 1px;
padding-bottom: 1px;
z-index: 1000;
margin-right: -10px;
margin-left: 18px;
width: 100%;
}
}
}

View File

@ -1,3 +1,5 @@
@import "mixin";
.container-connection-handlers {
$animation_length: .25s;
@ -14,10 +16,7 @@
background-color: transparent;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
@include user-select(none);
position: relative;
@ -32,6 +31,8 @@
overflow-x: auto;
overflow-y: visible;
max-width: 100%;
.connection-container {
padding-top: 4px;
position: relative;
@ -57,25 +58,13 @@
color: #a8a8a8;
align-self: center;
margin-right: -5px; /* 5px padding which have to be overcommed */
margin-right: 20px;
position: relative;
max-width: 16em;
overflow: visible;
text-overflow: clip;
white-space: nowrap;
&:before {
content: '';
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: linear-gradient(to right, transparent, #1e1e1e calc(100% - 20px));
}
}
.button-close {
@ -89,6 +78,23 @@
}
}
&.cutoff-name {
.server-name {
max-width: 10em;
margin-right: -5px; /* 5px padding which have to be overcommed */
&:before {
content: '';
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: linear-gradient(to right, transparent, #1e1e1e calc(100% - 20px));
}
}
}
&:hover {
background-color: #242425;
}

View File

@ -86,65 +86,66 @@
position: absolute;
margin-left: 3px;
}
}
}
.checkbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 14px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* we call it "ccheckbox" else it will be messed up the the global checkbox */
.ccheckbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 14px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 11px;
width: 11px;
background-color: #eee;
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 11px;
width: 11px;
background-color: #eee;
&:after {
content: "";
position: absolute;
display: none;
&:after {
content: "";
position: absolute;
display: none;
left: 4px;
top: 1px;
width: 3px;
height: 7px;
border: solid white;
border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
left: 4px;
top: 1px;
width: 3px;
height: 7px;
border: solid white;
border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
}

View File

@ -1,13 +1,14 @@
@import "properties";
@import "mixin";
$border_color_activated: rgba(255, 255, 255, .75);
/* max height is 2em */
.control_bar {
display: flex;
flex-direction: row;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
@include user-select(none);
height: 100%;
align-items: center;
@ -25,8 +26,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
.button, .dropdown-arrow {
text-align: center;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 3px;
border: .05em solid rgba(0, 0, 0, 0);
border-radius: $border_radius_small;
background-color: #454545;
@ -56,6 +57,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
}
}
@include transition(background-color $button_hover_animation_time ease-in-out, border-color $button_hover_animation_time ease-in-out);
> .icon_x24 {
vertical-align: middle;
}
@ -69,9 +72,25 @@ $border_color_activated: rgba(255, 255, 255, .75);
margin-left: 5px;
&:not(.icon_x24) {
min-width: 28px;
max-width: 28px;
height: 28px;
min-width: 2em;
max-width: 2em;
height: 2em;
}
.icon_em {
vertical-align: text-top;
}
&.button-hostbutton {
img {
min-width: 1.5em;
max-width: 1.5em;
height: 1.5em;
width: 1.5em;
}
padding: .25em;
}
}
@ -80,8 +99,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
position: relative;
.buttons {
margin-top: 1px;
height: 28px;
height: 2em;
align-items: center;
@ -89,14 +107,14 @@ $border_color_activated: rgba(255, 255, 255, .75);
flex-direction: row;
.dropdown-arrow {
height: 28px;
height: 2em;
display: inline-flex;
justify-content: space-around;
width: 18px;
width: 1.5em;
cursor: pointer;
border-radius: 0 3px 3px 0;
border-radius: 0 $border_radius_small $border_radius_small 0;
align-items: center;
border-left: 0;
}
@ -131,8 +149,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
background-color: #2d3032;
align-items: center;
border: 1px solid #2c2525;
border-radius: 0 5px 5px 5px;
border: .05em solid #2c2525;
border-radius: 0 $border_radius_middle $border_radius_middle $border_radius_middle;
width: 230px;
@ -143,7 +161,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
right: 0;
}
.icon {
.icon, .icon-container, .icon_em {
vertical-align: middle;
margin-right: 5px;
}
@ -159,16 +177,16 @@ $border_color_activated: rgba(255, 255, 255, .75);
}
& > div:first-of-type {
border-radius: 2px 2px 0 0;
border-radius: .1em .1em 0 0;
}
& > div:last-of-type {
border-radius: 0 0 2px 2px;
border-radius: 0 0 .1em .1em;
}
&.display_left {
margin-left: -179px;
border-radius: 5px 0 5px 5px;
border-radius: $border_radius_middle 0 $border_radius_middle $border_radius_middle;
}
}
@ -263,4 +281,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
}
}
}
.icon_em {
font-size: 1.5em;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -41,10 +41,6 @@
flex-direction: column;
padding-right: 0;
padding-left: 0;
.hostbanner {
overflow: hidden;
}
}
}
@ -84,42 +80,6 @@
display: none;
margin-bottom: 5px;
}
.hostbanner {
position: relative;
flex-grow: 1;
flex-shrink: 1;
.meta-image {
display: none;
}
.image-container {
display: flex;
flex-direction: row;
justify-content: center;
height: 100%;
div {
background-position: center;
&.hostbanner-mode-0 { }
&.hostbanner-mode-1 {
width: 100%;
height: auto;
}
&.hostbanner-mode-2 {
background-size: cover!important;
background-position: top center !important;
width:100%;
height:100%
}
}
}
}
}
.container-select-info {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,78 @@
@import "./mixin.scss";
#hostbanner {
.container-hostbanner {
position: relative;
overflow: hidden;
height: 1000px; /* allocate some height to be truncated by the flex :) */
display: flex;
flex-direction: column;
justify-content: stretch;
cursor: pointer;
&:not(.no-background) {
background-color: #2e2e2e;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
-moz-box-shadow: inset 0 0 5px #00000040;
-webkit-box-shadow: inset 0 0 5px #00000040;
box-shadow: inset 0 0 5px #00000040;
padding-bottom: 5px;
}
&.disabled {
padding-bottom: 0;
height: 0;
}
@include transition(height 0.5s ease-in-out);
.hostbanner-image-container {
height: 100%;
width: 100%;
flex-grow: 1;
flex-shrink: 1;
text-align: center;
&.hostbanner-mode-0 {
/* do not adjust */
display: block;
}
&.hostbanner-mode-1 {
/* do adjust and ignore ration */
display: flex;
height: 100%;
width: 100%;
> img {
width: 100%;
height: 100%;
}
}
&.hostbanner-mode-2 {
display: flex;
flex-direction: row;
justify-content: space-around;
> img {
object-fit: contain;
max-height: 100%;
/* "Normal" third more */
//max-width: 100%;
/* better adoptable mode */
width: min-content;
}
}
}
}
}

View File

@ -8,6 +8,10 @@ $animation_length: .5s;
min-height: 330px;
.container-app-main {
height: 100%;
width: 100%;
min-height: 500px;
margin-top: 5px;
position: relative;
@ -15,14 +19,14 @@ $animation_length: .5s;
flex-direction: column;
justify-content: stretch;
height: 100%;
width: 100%;
.container-channel-chat {
min-height: 200px;
min-width: 100px;
height: 80%; /* "default" settings */
width: 100%;
min-height: 25em;
min-width: 100px;
display: flex;
flex-direction: row;
justify-content: stretch;
@ -34,25 +38,44 @@ $animation_length: .5s;
border-radius: 5px;
}
.container-channel-tree {
> .container-channel-tree {
width: 50%; /* "default" settings */
height: 100%;
background: #353535;
min-width: 200px;
display: flex;
flex-direction: column;
justify-content: stretch;
height: 100%;
min-height: 100px;
padding-top: 5px;
/*
overflow: auto;
overflow-x: visible;
*/
overflow: hidden;
overflow-y: auto;
> .hostbanner {
flex-grow: 0;
flex-shrink: 0;
max-height: 9em; /* same size as the info pannel */
display: flex;
flex-direction: column;
justify-content: stretch;
}
> .channel-tree {
padding-top: 5px;
flex-grow: 1;
flex-shrink: 1;
}
}
.container-chat {
> .container-chat {
width: 50%; /* "default" settings */
height: 100%;
background: #353535;
min-width: 350px;
@ -62,16 +85,66 @@ $animation_length: .5s;
}
}
.container-server-log {
min-height: 0;
height: 250px;
> .container-bottom {
height: 20%;
min-height: 1.5em;
width: 100%;
border-radius: 5px;
padding-right: 5px;
padding-left: 5px;
display: flex;
flex-direction: column;
justify-content: stretch;
background: #353535;
> .container-server-log {
display: flex;
flex-direction: column;
justify-content: stretch;
flex-shrink: 1;
flex-grow: 1;
min-height: 0;
width: 100%;
border-radius: 5px 5px 0 0;
padding-right: 5px;
padding-left: 5px;
background: #353535;
}
> .container-footer {
flex-shrink: 0;
flex-grow: 0;
height: 1.5em;
background: #252525;
color: #353535;
border-radius: 0 0 5px 5px;
padding-right: 5px;
padding-left: 5px;
padding-top: 2px;
-webkit-box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125);
-moz-box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125);
box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125);
display: flex;
flex-direction: row;
justify-content: space-between;
> * {
align-self: center;
}
a[href], a[href]:visited {
color: #353535!important;
}
}
}
}
@ -82,7 +155,7 @@ $animation_length: .5s;
border-radius: 5px;
height: 30px;
height: 2em;
width: 100%;
background-color: #454545;
@ -121,39 +194,6 @@ $animation_length: .5s;
}
@media only screen and (max-width: $small_device) {
.app {
.container-app-main {
.container-info {
display: none;
position: absolute;
width: 100% !important; /* override the seperator property */
height: 100%;
z-index: 1000;
&.shown {
display: block;
}
.select_info {
> .close {
display: block;
}
}
}
.container-channel-chat + .container-seperator {
display: none;
animation: fadeout $animation_length linear;
}
.container-channel-chat {
width: 100% !important; /* override the seperator property */
}
}
}
.hide-small {
display: none;
opacity: 0;

View File

@ -0,0 +1,125 @@
@import "mixin";
.top-menu-bar {
@include user-select(none);
height: 1.5em;
width: 100%;
background: #fafafa;
display: flex;
flex-direction: row;
justify-content: flex-start;
position: fixed;
top: 0;
z-index: 201;
font-family: Arial, serif;
.container-menu-item {
position: relative;
.menu-item {
cursor: pointer;
padding-left: .4em;
padding-right: .4em;
height: 100%;
display: flex;
flex-direction: row;
width: max-content;
> * {
vertical-align: middle;
}
.container-icon {
height: 1.2em;
width: 1.2em;
padding: .1em;
font-size: 1em;
margin-right: .2em;
display: inline-block;
}
.container-label {
display: inline-block;
align-self: center;
}
}
&:hover:not(.disabled) {
background-color: #00000044;
}
&.disabled {
background-color: #00000022;
}
.sub-menu {
z-index: 1000;
display: none;
background: white;
position: absolute;
top: 100%;
border: 1px solid black;
> .container-menu-item {
padding-right: .5em;
}
}
&.type-side {
&.sub-entries:after {
position: absolute;
display: block;
content: '>';
top: 0;
bottom: 0;
right: .4em;
}
> .sub-menu {
top: -1px; /* border */
left: 100%;
}
&:hover {
> .sub-menu {
display: block;
}
}
}
&.active {
background-color: #00000044;
> .sub-menu {
display: block;
}
}
}
> .container-menu-item {
> .menu-item {
.container-icon {
display: none;
}
}
}
hr {
margin-top: .125em;
margin-bottom: .125em;
}
}

View File

@ -0,0 +1,145 @@
/* Some general browser helpers */
@mixin transition($transition...) {
-moz-transition: $transition;
-o-transition: $transition;
-webkit-transition: $transition;
transition: $transition;
}
@mixin transition-property($property...) {
-moz-transition-property: $property;
-o-transition-property: $property;
-webkit-transition-property: $property;
transition-property: $property;
}
@mixin transition-duration($duration...) {
-moz-transition-property: $duration;
-o-transition-property: $duration;
-webkit-transition-property: $duration;
transition-property: $duration;
}
@mixin transition-timing-function($timing...) {
-moz-transition-timing-function: $timing;
-o-transition-timing-function: $timing;
-webkit-transition-timing-function: $timing;
transition-timing-function: $timing;
}
@mixin transition-delay($delay...) {
-moz-transition-delay: $delay;
-o-transition-delay: $delay;
-webkit-transition-delay: $delay;
transition-delay: $delay;
}
@mixin transform($transform...) {
-webkit-transform: $transform;
-o-transform: $transform;
-ms-transform: $transform;
transform: $transform;
}
@mixin placeholder($element) {
#{$element}::-webkit-input-placeholder {
@content;
}
#{$element}:-moz-placeholder {
@content;
}
#{$element}::-moz-placeholder {
@content;
}
#{$element}:-ms-input-placeholder {
@content;
}
#{$element}::-ms-input-placeholder {
@content;
}
#{$element}::placeholder {
@content;
}
}
@mixin user-select($mode) {
-webkit-user-select: $mode;
-moz-user-select: $mode;
-ms-user-select: $mode;
user-select: $mode;
}
@mixin chat-scrollbar() {
& {
/* for moz */
scrollbar-color: #353535 #555;
}
&::-webkit-scrollbar-track {
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
border-radius: .25em;
background-color: transparent;
}
&::-webkit-scrollbar {
width: .5em;
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: .25em;
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555;
}
}
@mixin chat-scrollbar-vertical() {
& {
/* for moz */
scrollbar-color: #353535 #555;
scrollbarWidth: .5em;
}
&::-webkit-scrollbar-track {
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
border-radius: .25em;
background-color: transparent;
cursor: pointer;
}
&::-webkit-scrollbar {
width: .5em;
background-color: transparent;
cursor: pointer;
}
&::-webkit-scrollbar-thumb {
border-radius: .25em;
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555;
}
}
@mixin chat-scrollbar-horizontal() {
& {
/* for moz */
scrollbar-color: #353535 #555;
scrollbarWidth: .5em;
}
&::-webkit-scrollbar-track {
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
border-radius: .25em;
background-color: transparent;
cursor: pointer;
}
&::-webkit-scrollbar {
height: .5em;
background-color: transparent;
cursor: pointer;
}
&::-webkit-scrollbar-thumb {
border-radius: .25em;
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555;
}
}

View File

@ -0,0 +1,53 @@
.modal-about {
display: flex!important;
flex-direction: row!important;
text-align: center;
color: #999999;
.container-right {
text-align: left;
padding-left: 2em;
h1 {
font-size: 1.5em;
margin-block-start: 0.35em;
margin-block-end: 0.35em;
}
h2 {
font-size: 1.25em;
margin-block-start: 0.10em;
margin-block-end: 0.10em;
}
p {
margin-block-start: .25em;
margin-block-end: .25em;
}
}
.version {
width: 100%;
display: flex;
flex-direction: row;
justify-content: stretch;
a {
width: 50%;
flex-grow: 1;
flex-shrink: 1;
text-align: right;
}
.value {
padding-left: .25em;
text-align: left;
flex-grow: 1;
flex-shrink: 1;
}
}
}

View File

@ -172,4 +172,127 @@
}
}
}
}
.modal-avatar-upload {
display: flex;
flex-direction: column;
justify-content: stretch;
.container-upload {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
.bmd-form-group {
padding-top: 0;
}
input[type="file"] {
display: none;
}
}
.container-preview {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
justify-content: stretch;
.title {
font-size: 1.2em;
font-weight: bold;
border-bottom: 1px solid gray;
}
.previews {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-self: center;
.preview {
flex-shrink: 1;
flex-grow: 1;
width: 11rem;
min-width: 11rem;
max-width: 11rem;
height: 13rem;
min-height: 13rem;
max-height: 13rem;
text-align: center;
display: flex;
flex-direction: column;
justify-content: flex-end;
.container-avatar {
display: flex;
flex-direction: row;
justify-content: space-around;
.avatar {
position: relative;
height: 1em;
width: 1em;
overflow: hidden;
border-radius: 50%;
> img {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
}
}
> a {
margin-top: 1em;
}
&.preview-client-info {
.container-avatar {
font-size: 10rem;
}
}
&.preview-chat {
.container-avatar {
font-size: 2.5rem;
}
}
&.preview-chat-entry {
.container-avatar {
font-size: 2rem;
}
}
}
}
}
}
@media all and (max-width: 40rem) {
.modal-avatar-upload .container-preview .previews {
flex-direction: column;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +1,349 @@
@import "mixin";
.modal .modal-connect {
@include user-select(none);
/*
margin-top: 5px;
font-size: 1rem;
max-width: 100000px; /* max 100000px width, else we shrink the modal */
padding: 0!important; /* override the default padding */
> div:not(:first-of-type) {
margin-top: 5px;
}
display: flex;
flex-direction: column;
justify-content: stretch;
.profile-select-container {
display: flex;
flex-direction: row;
justify-content: space-between;
.container-connect-input {
flex-grow: 0;
flex-shrink: 0;
select {
width: 150px;
/* apply the default padding */
padding: .75em 24px;
border-left: 2px solid #0073d4;
> .row {
display: flex;
flex-direction: row;
justify-content: stretch;
> *:not(:last-of-type) {
margin-right: 3em;
}
}
.container-address-password {
.container-address {
flex-grow: 1;
flex-shrink: 1;
}
.container-password {
flex-grow: 0;
flex-shrink: 4;
min-width: 21.5em;
}
}
.container-profile-manage {
flex-grow: 0;
flex-shrink: 4;
display: flex;
flex-direction: row;
justify-content: stretch;
.container-select-profile {
flex-grow: 1;
flex-shrink: 1;
min-width: 14em;
> .invalid-feedback {
width: max-content; /* allow overflow here */
}
}
.container-manage {
flex-grow: 0;
flex-shrink: 4;
margin-left: 15px;
}
.button-manage-profiles {
min-width: 7em;
margin-left: 0.5em;
}
}
.container-nickname {
flex-grow: 1;
flex-shrink: 1;
}
.container-buttons {
padding-top: 1em;
display: flex;
flex-direction: row;
justify-content: space-between;
.container-buttons-connect {
display: flex;
flex-direction: row;
}
.button-right {
min-width: 7em;
margin-left: 0.5em;
}
.button-left {
min-width: 14em;
}
}
.arrow {
border-color: #7a7a7a;
margin-left: .5em;
}
}
.profile-invalid {
.container-last-servers {
flex-grow: 0;
flex-shrink: 1;
display: flex;
flex-direction: column;
justify-content: start;
> div {
display: inline-flex;
flex-direction: row;
}
color: red;
}
*/
.container-address-password {
display: flex;
flex-direction: row;
justify-content: stretch;
.container-address {
flex-grow: 1;
flex-shrink: 1;
max-height: 0;
opacity: 0;
overflow: hidden;
padding: 0;
min-width: 0;
border: none;
border-left: 2px solid #7a7a7a;
@include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out);
&.shown {
/* apply the default padding */
padding: 0 24px 24px;
max-height: 100%;
opacity: 1;
@include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out)
}
.container-password {
flex-grow: 0;
flex-shrink: 4;
hr {
height: 0;
width: calc(100% + 46px);
min-width: 0;
margin-left: 15px;
margin: 0 0 0 -23px;
padding: 0;
border: none;
border-top: 1px solid #090909;
margin-bottom: .75em;
}
color: #7a7a7a;
/* general table class */
.table {
width: 100em;
max-width: 100%;
display: flex;
flex-direction: column;
justify-content: stretch;
.head {
display: flex;
flex-direction: row;
justify-content: stretch;
flex-grow: 0;
flex-shrink: 0;
border: none;
border-bottom: 1px solid #161618;
}
.body {
flex-grow: 0;
flex-shrink: 1;
display: flex;
flex-direction: column;
justify-content: stretch;
overflow: auto;
.row {
cursor: pointer;
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
&:hover {
background-color: #202022;
}
&.selected {
background-color: #131315;
}
}
.body-empty {
height: 3em;
text-align: center;
display: flex;
flex-direction: column;
justify-content: space-around;
font-size: 1.25em;
color: #7979797F;
}
}
.column {
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
white-space: nowrap;
padding-right: .25em;
padding-left: .25em;
display: flex;
flex-direction: row;
justify-content: flex-start;
&:not(:last-of-type) {
border-right: 1px solid #161618;
}
> a {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
/* connect table */
.table {
margin-left: -1.5em; /* the delete row */
.head {
margin-left: 1.5em; /* the delete row */
.column.delete {
display: none;
}
}
.column {
align-self: center;
.country, .icon-container {
align-self: center;
margin-right: 0.1em;
}
@mixin fixed-column($name, $width) {
&.#{$name} {
flex-grow: 0;
flex-shrink: 0;
width: $width;
}
}
@include fixed-column(delete, 1.5em);
@include fixed-column(password, 5em);
@include fixed-column(country-name, 7em);
@include fixed-column(clients, 4em);
@include fixed-column(connections, 6.5em);
&.delete {
opacity: 0;
border-right: none;
border-bottom: none;
text-align: center;
@include transition(opacity .25 ease-in-out);
&:hover {
opacity: 1;
@include transition(opacity .25 ease-in-out);
}
}
&.address {
flex-grow: 1;
flex-shrink: 1;
width: 40%;
}
&.name {
flex-grow: 1;
flex-shrink: 1;
width: 60%;
}
}
}
}
}
.container-profile-manage {
display: flex;
flex-direction: row;
justify-content: stretch;
@media all and (max-width: 55rem) {
.modal .modal-connect {
min-width: calc(21.25em + 24px * 2)!important;
width: 1000em; /* allocate space */
.container-select-profile {
flex-grow: 1;
flex-shrink: 1;
.container-address-password {
.container-password {
min-width: unset!important;
margin-left: 1em!important;
}
}
.container-manage {
flex-grow: 0;
flex-shrink: 4;
.container-buttons {
justify-content: flex-end!important;
margin-left: 15px;
.button-toggle-last-servers {
display: none;
}
}
}
.invalid-feedback {
position: absolute;
.container-profile-name {
flex-direction: column!important;
}
.container-connect-input {
> .row {
> div {
margin-right: 0!important;
}
}
}
.container-last-servers {
display: none;
}
}
}

View File

@ -0,0 +1,149 @@
@import "mixin";
@import "properties";
.modal-server-group-assignments {
@include user-select(none);
min-width: 25em;
max-height: calc(100vh - 10rem);
display: flex;
flex-direction: column;
justify-content: stretch;
.group-assignment-list {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: stretch;
color: #999999;
a {
flex-shrink: 0;
flex-grow: 0;
.htmltag-client {
display: inline;
color: #999999;
}
}
.group-list {
flex-shrink: 1;
flex-grow: 1;
border: none;
border-radius: $border_radius_middle;
padding: 3px;
overflow-y: auto;
@include chat-scrollbar-vertical();
.group-entry {
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
height: max-content;
}
.icon-container {
align-self: center;
margin-right: 4px;
margin-left: 2px;
margin-top: -2px;
}
a {
align-self: center;
}
.checkbox {
align-self: center;
height: 8px;
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 18px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
background-color: #eee;
margin-right: 4px;
&:after {
content: "";
position: absolute;
display: none;
left: 5px;
top: 1px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover:not(.disabled) input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
&.disabled {
user-select: none;
pointer-events: none;
cursor: not-allowed;
.checkmark {
background-color: #00000055;
&:after {
border-color: #00000055;
}
}
}
}
}
}
.container-buttons {
flex-grow: 0;
flex-shrink: 0;
padding-top: 1em;
display: flex;
flex-direction: row;
justify-content: space-between;
}
}

View File

@ -0,0 +1,50 @@
.modal-body.modal-keyselect {
width: max-content!important;
.body {
display: flex;
flex-direction: column;
justify-content: flex-start;
.container-select {
margin-top: .5em;
display: flex;
flex-direction: row;
justify-content: flex-start;
a {
align-self: center;
margin-right: .5em;
}
.container-key {
background-color: #272626;
border-radius: 0.15em;
-webkit-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
-moz-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
min-width: 12em;
height: 2em;
padding: 0 .5em;
display: flex;
flex-direction: row;
justify-content: center;
}
}
}
.container-buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 1em;
button {
margin-left: 1em;
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,938 @@
@import "properties";
@import "mixin";
.modal {
color: #999999; /* base color */
overflow: auto; /* allow scrolling if a modal is too big */
background-color: rgba(0, 0, 0, 0.8);
padding-right: 5%;
padding-left: 5%;
z-index: 1000;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
margin-top: -7em;
opacity: 0;
$animation_length: .3s;
@include transition(opacity $animation_length ease-in, margin-top $animation_length ease-in);
&.shown {
display: flex;
flex-direction: column;
justify-content: center;
margin-top: 0;
opacity: 1;
@include transition(opacity $animation_length ease-out, margin-top $animation_length ease-out);
}
.modal-dialog {
display: block;
margin: 1.75rem 0;
/* width calculations */
align-items: center;
/* height stuff */
max-height: calc(100% - 3.5em);
.modal-content {
background: #19191b;
border: 1px solid black;
border-radius: $border_radius_middle;
width: max-content;
max-width: 100%;
min-width: 20em;
min-height: min-content;
/* align us in the center */
margin-right: auto;
margin-left: auto;
flex-shrink: 1;
flex-grow: 0; /* we dont want a grow over the limit set within the content, but we want to shrink the content if necessary */
align-self: center;
display: flex;
flex-direction: column;
justify-content: stretch;
.modal-header, .modal-footer {
flex-grow: 0;
flex-shrink: 0;
}
.modal-header {
background-color: #222224;
display: flex;
flex-direction: row;
justify-content: stretch;
padding: .25em;
.container-icon, .container-close {
flex-grow: 0;
flex-shrink: 0;
}
.container-close {
height: 1.4em;
width: 1.4em;
padding: .2em;
border-radius: .2em;
cursor: pointer;
&:hover {
background-color: #1b1b1c;
}
}
.container-icon {
margin-right: .25em;
img {
height: 1em;
width: 1em;
}
}
.modal-title, modal-header {
flex-grow: 1;
flex-shrink: 1;
color: #9d9d9e;
}
h5 {
margin: 0;
padding: 0;
}
}
.modal-body {
max-width: 100%;
min-width: 20em; /* may adjust if needed */
overflow-y: auto;
overflow-x: auto;
}
}
}
}
.modal {
//General style
.properties {
display: grid;
grid-template-columns: minmax(min-content, max-content) auto;
grid-column-gap: 10px;
grid-row-gap: 3px;
box-sizing: border-box;
}
hr {
border-top: 3px double #8c8b8b;
width: 100%;
}
.input_error {
border-radius: 1px;
border: solid red;
}
.properties_misc {
.complains {
display: grid;
grid-template-columns: auto auto auto;
grid-template-rows: auto auto;
grid-column-gap: 5px;
margin-bottom: 10px;
}
}
.container {
padding: 6px;
}
.modal-dialog {
display: flex;
flex-direction: column;
justify-content: stretch;
&.modal-dialog-centered {
justify-content: stretch;
}
}
.modal-content {
/* max-height: 500px; */
min-height: 0; /* required for moz */
flex-direction: column;
justify-content: stretch;
.modal-header {
flex-shrink: 0;
flex-grow: 0;
&.modal-header-error {
background-color: #ce0000;
}
&.modal-header-info {
background-color: #03a9f4;
}
&.modal-header-warning, &.modal-header-info, &.modal-header-error {
border-top-left-radius: .125rem;
border-top-right-radius: .125rem;
}
}
.modal-body {
padding: 20px 24px 24px;
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
min-height: 0;
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);
}
}
.modal-footer {
flex-shrink: 0;
flex-grow: 0;
&.modal-footer-button-group {
button {
min-width: 100px;
}
button:not(:first-of-type) {
margin-left: 15px;
};
}
}
}
}
/* special general modals */
.modal {
.modal-body.modal-blue {
border-left: 2px solid #0a73d2;
}
.modal-body.modal-green {
border-left: 2px solid #00d400;
}
.modal-body.modal-body-input {
color: #999999;
width: 100%;
.form-group:not(.with-title) {
padding-top: .75rem;
}
input.is-invalid ~ .container-help-feedback > .invalid-feedback {
display: block;
}
.container-help-feedback {
position: absolute;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
button {
width: 6em;
&:not(:last-of-type) {
margin-right: 1em;
}
}
}
}
.modal-body.modal-body-yesno {
color: #999999;
border: none;
border-left: 2px solid #d50000;
width: 100%;
.buttons {
padding-top: 2em;
display: flex;
flex-direction: row;
justify-content: flex-end;
button {
width: 6em;
&:not(:last-of-type) {
margin-right: 1em;
}
}
}
}
}
/* Input group */
.form-group {
position: relative;
padding-top: 1.75rem; /* the label above (might be floating) */
margin-bottom: 1rem; /* for invalid label/help label */
.form-control {
display: block;
width: 100%;
padding: .4375rem 0;
font-size: 1rem;
line-height: 1.5;
color: #cdd1d0;
background-color: transparent;
background-clip: padding-box;
border: 1px solid rgba(0, 0, 0, .26);
border-radius: 0;
box-shadow: none;
@include transition(border-color .15s ease-in-out, box-shadow .15s ease-in-out);
}
label {
color: #999999;
top: 1rem;
left: 0;
font-size: .75rem;
position: absolute;
pointer-events: none;
transition: all .3s ease;
line-height: 1;
&.bmd-label-floating {
will-change: left, top, contents;
color: #999999;
top: 2.42rem;
font-size: 1rem;
}
@include transition(color $button_hover_animation_time ease-in-out, top $button_hover_animation_time ease-in-out, font-size $button_hover_animation_time ease-in-out);
}
&:focus-within {
label {
color: #3c74a2;
&.bmd-label-floating {
//color: #343434;
}
}
}
&:focus-within, &.is-filled {
label.bmd-label-floating {
top: 1rem;
font-size: .75rem;
color: #3c74a2;
}
}
.form-control {
height: 2.25em;
background: no-repeat bottom, 50% calc(100% - 1px);
background-size: 0 100%, 100% 100%;
border: 0;
transition: background 0s ease-out;
padding-left: 0;
padding-right: 0;
background-image: linear-gradient(0deg, #008aff 2px, rgba(0, 150, 136, 0) 0), linear-gradient(0deg, #393939 1px, transparent 0);
&:focus {
background-size: 100% 100%, 100% 100%;
transition-duration: .3s;
color: #ced3d3;
background-color: transparent;
outline: 0;
}
&.is-invalid {
background-image: linear-gradient(0deg, #d50000 2px,rgba(213,0,0,0) 0),linear-gradient(0deg,rgba(241,1,1,.61) 1px,transparent 0);
}
}
.invalid-feedback {
position: absolute;
opacity: 0;
width: 100%;
margin-top: .25rem;
font-size: 80%;
color: #f44336;
@include transition(opacity .25s ease-in-out);
}
.form-control.is-invalid ~ .invalid-feedback {
opacity: 1;
}
&.is-invalid {
.form-control {
background-image: linear-gradient(0deg, #d50000 2px,rgba(213,0,0,0) 0),linear-gradient(0deg,rgba(241,1,1,.61) 1px,transparent 0);
}
.invalid-feedback {
opacity: 1;
}
label {
color: #f44336!important;
}
}
.bmd-help {
position: absolute;
opacity: 0;
width: 100%;
margin-top: .25rem;
font-size: .75em;
@include transition(opacity .25s ease-in-out);
}
.form-control:focus-within ~ .bmd-help {
opacity: 1;
}
}
/* button look */
.btn {
cursor: pointer;
background-color: #0000007F;
border-width: 0;
border-radius: $border_radius_middle;
border-style: solid;
color: #7c7c7c;
padding: .25em 1em;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12);
&:hover {
background-color: #151515;
}
&:disabled {
box-shadow: none;
background-color: #00000045;
&:hover {
background-color: #00000045;
}
}
&.btn-success {
border-bottom-width: 2px;
border-bottom-color: #389738;
}
&.btn-info {
border-bottom-width: 2px;
border-bottom-color: #386896;
}
&.btn-warning, &.btn-danger {
border-bottom-width: 2px;
border-bottom-color: #973838;
}
@include transition(background-color $button_hover_animation_time ease-in-out);
}
/* general switch look */
.switch {
$ball_outer_width: 1.5em; /* 1.5? */
$ball_inner_width: .4em;
$slider_height: .8em;
$slider_width: 2em;
$slider_border_size: .1em;
position: relative;
display: inline-block;
outline: none;
width: $slider_width;
height: $slider_height;
/* "allocate" space for the slider */
margin-top: ($ball_outer_width - $slider_height) / 2;
margin-bottom: ($ball_outer_width - $slider_height) / 2;
margin-left: $ball_outer_width / 2;
margin-right: $ball_outer_width / 2;
/* fix size */
flex-shrink: 0;
flex-grow: 0;
input {
/* "hide" the actual input node */
opacity: 0;
width: 0;
height: 0;
outline: none;
}
.slider {
pointer-events: all!important;
position: absolute;
cursor: pointer;
outline: none;
top: -$slider_border_size;
left: -$slider_border_size;
right: -$slider_border_size;
bottom: -$slider_border_size;
background-color: #252424;
border: $slider_border_size solid #262628;
border-radius: 5px;
&:before {
position: absolute;
content: "";
height: $ball_outer_width;
width: $ball_outer_width;
left: - $ball_outer_width / 2;
bottom: -($ball_outer_width - $slider_height) / 2;
background-color: #3d3a3a;
@include transition(.4s);
border-radius: 50%;
box-shadow: 0 0 .2em 1px #00000044;
}
.dot {
position: absolute;
height: $ball_inner_width;
width: $ball_inner_width;
left: -($ball_inner_width / 2);
bottom: $slider_height / 2 - $ball_inner_width / 2;
background-color: #a5a5a5;
box-shadow: 0 0 1em 1px #a5a5a566;
border-radius: 50%;
@include transition(.4s);
}
}
input:focus + .slider {
}
input:checked + .slider {
&:before {
@include transform(translateX($slider_width));
}
.dot {
@include transform(translateX($slider_width));
background-color: #46c0ec;
box-shadow: 0 0 1em 1px #46c0ec;
}
}
}
/* general ratio button look */
.ratio-button {
$button_size: 1.2em;
$mark_size: .6em;
position: relative;
width: $button_size;
height: $button_size;
cursor: pointer;
overflow: hidden;
background-color: #272626;
border-radius: 50%;
input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
}
//#07d1fe
.mark {
position: absolute;
opacity: 0;
top: ($button_size - $mark_size) / 2;
bottom: ($button_size - $mark_size) / 2;
right: ($button_size - $mark_size) / 2;
left: ($button_size - $mark_size) / 2;
background-color: #46c0ec;
box-shadow: 0 0 .5em 1px #46c0ec66;
border-radius: 50%;
@include transition(.4s);
}
input:checked + .mark {
opacity: 1;
}
@include transition(background-color $button_hover_animation_time);
-webkit-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
-moz-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
}
label:hover > .ratio-button, .ratio-button:hover {
&.ratio-button, > .ratio-button {
background-color: #2c2b2b;
}
}
label.disabled > .ratio-button, .ratio-button.disabled, .ratio-button:disabled {
&.ratio-button, > .ratio-button {
pointer-events: none!important;
background-color: #1a1919!important;
}
}
/*
<label class="checkbox">
<input type="checkbox">
<div class="mark"></div>
</label>
*/
.checkbox {
flex-shrink: 0;
flex-grow: 0;
position: relative;
width: 1.3em;
height: 1.3em;
cursor: pointer;
pointer-events: all;
overflow: hidden;
background-color: #272626;
border-radius: $border_radius_middle;
input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
}
//#07d1fe
.mark {
position: absolute;
opacity: 0;
height: .5em;
width: .8em;
margin-left: 0.25em;
margin-top: .3em;
border: none;
border-bottom: .2em solid #46c0ec;
border-left: .2em solid #46c0ec;
transform: rotateY(0deg) rotate(-45deg); /* needs Y at 0 deg to behave properly*/
@include transition(.4s);
}
input:checked + .mark {
opacity: 1;
}
-webkit-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
-moz-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
}
label.disabled > .checkbox, .checkbox:disabled, .checkbox.disabled {
&.checkbox, > .checkbox {
pointer-events: none!important;
background-color: #222227;
}
}
/* slider */
$track_height: .6em;
$thumb_width: .6em;
$thumb_height: 2em;
$tooltip_width: 4em;
$tooltip_height: 1.8em;
.container-slider {
font-size: .8em;
position: relative;
margin-top: .5em; /* for the track */
width: 100%;
height: $track_height;
cursor: pointer;
background-color: #242527;
border-radius: $border_radius_large;
overflow: visible;
.filler {
position: absolute;
left: 0;
top: 0;
bottom: 0;
background-color: #4370a2;
border-radius: $border_radius_large;
}
.thumb {
position: absolute;
top: 0;
right: 0;
height: $thumb_height;
width: $thumb_width;
margin-left: -($thumb_width / 2);
margin-right: -($thumb_width / 2);
margin-top: -($thumb_height - $track_height) / 2;
margin-bottom: -($thumb_height - $track_height) / 2;
background-color: #808080;
.tooltip {
display: none;
/*
position: absolute;
top: -($tooltip_height + .6em);
left: -($tooltip_width - $thumb_width) / 2;
line-height: 1em;
height: $tooltip_height;
width: $tooltip_width;
background-color: #232222;
border-radius: $border_radius_middle;
text-align: center;
display: flex;
flex-direction: column;
justify-content: space-around;
opacity: 0;
@include transition(opacity .5s ease-in-out);
&:before {
content: '';
position: absolute;
left: ($tooltip_width - $thumb_width) / 2 - .25em;
right: 0;
bottom: -.4em;
width: 0;
height: 0;
border-style: solid;
border-width: .5em .5em 0 .5em;
border-color: #232222 transparent transparent transparent;
}
*/
}
}
&:hover, &.active {
.tooltip {
opacity: 1;
}
}
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type=number] {
-moz-appearance:textfield; /* Firefox */
}
/* "Boxed input" Used in channeledit & serveredit */
.input-boxed {
height: 2.5em;
border-radius: .2em;
border: 1px solid #111112;
background-color: #121213;
display: flex;
flex-direction: row;
justify-content: stretch;
color: #b3b3b3;
@include placeholder(&) {
color: #606060;
};
.prefix {
flex-grow: 0;
flex-shrink: 0;
margin: 0;
line-height: initial;
align-self: center;
padding: 0 .5em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 1;
@include transition($button_hover_animation_time ease-in-out);
}
&.is-invalid {
background-color: #180d0d;
border-color: #721c1c;
background-image: unset!important;
}
&:focus, &:focus-within {
background-color: #131b22;
border-color: #284262;
color: #e1e2e3;
.prefix {
width: 0;
padding-left: 0;
padding-right: 0;
opacity: 0;
}
}
input, select {
flex-grow: 1;
flex-shrink: 1;
padding: 0 0.5em;
background: transparent;
border: none;
outline: none;
margin: 0;
color: #b3b3b3;
}
.prefix + input {
padding-left: 0;
}
&:focus, &:focus-within {
.prefix + input {
padding-left: .5em;
}
}
&.disabled, &:disabled {
background-color: #1a1819;
}
@include transition($button_hover_animation_time ease-in-out);
}
input.input-boxed {
padding: 0.5em;
}

View File

@ -1,132 +1,3 @@
/* backdrop fix */
.modal-backdrop {
visibility: hidden !important;
}
.modal {
background-color: rgba(0,0,0,0.5);
padding-right: 8% !important;
}
modal-body {
display: flex;
flex-direction: column;
min-height: 10px;
}
.modal {
//General style
.properties {
display: grid;
grid-template-columns: minmax(min-content, max-content) auto;
grid-column-gap: 10px;
grid-row-gap: 3px;
box-sizing: border-box;
}
hr {
border-top: 3px double #8c8b8b;
width: 100%;
}
.input_error {
border-radius: 1px;
border: solid red;
}
.properties_misc {
.complains {
display: grid;
grid-template-columns: auto auto auto;
grid-template-rows: auto auto;
grid-column-gap: 5px;
margin-bottom: 10px;
}
}
.container {
padding: 6px;
}
.modal-dialog {
max-height: 80%;
display: flex;
flex-direction: column;
justify-content: stretch;
&.modal-dialog-centered {
justify-content: stretch;
}
}
.modal-content {
/* max-height: 500px; */
min-height: 0; /* required for moz */
flex-direction: column;
justify-content: stretch;
.modal-header {
flex-shrink: 0;
flex-grow: 0;
&.modal-header-error {
background-color: #ce0000;
}
&.modal-header-info {
background-color: #03a9f4;
}
&.modal-header-warning, &.modal-header-info, &.modal-header-error {
border-top-left-radius: .125rem;
border-top-right-radius: .125rem;
}
}
.modal-body {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
min-height: 0;
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);
}
&.modal-body-input {
.form-group:not(.with-title) {
padding-top: .75rem;
}
input.is-invalid ~ .container-help-feedback > .invalid-feedback {
display: block;
}
.container-help-feedback {
position: absolute;
}
}
}
.modal-footer {
flex-shrink: 0;
flex-grow: 0;
&.modal-footer-button-group {
button {
min-width: 100px;
}
button:not(:first-of-type) {
margin-left: 15px;
};
}
}
}
}
.channel_perm_tbl .value {
width: 60px;
}
@ -217,9 +88,13 @@ modal-body {
.arrow {
display: inline-block;
border: solid black;
border-width: 0 3px 3px 0;
padding: 3px;
height: 10px;
//border-width: 0 3px 3px 0;
//padding: 3px;
//height: 10px;
border-width: 0 .2em .2em 0;
padding: .21em;
height: .5em;
&.right {
transform: rotate(-45deg);
@ -327,106 +202,4 @@ modal-body {
}
}
}
}
.group-assignment-list {
.group-list {
border: lightgray solid 1px;
padding: 3px;
overflow-y: auto;
.group-entry {
display: flex;
flex-direction: row;
height: max-content;
}
.icon-container {
align-self: center;
margin-right: 4px;
margin-left: 2px;
margin-top: -2px;
}
a {
align-self: center;
}
.checkbox {
align-self: center;
height: 8px;
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 18px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
background-color: #eee;
margin-right: 4px;
&:after {
content: "";
position: absolute;
display: none;
left: 5px;
top: 1px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover:not(.disabled) input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
&.disabled {
user-select: none;
pointer-events: none;
cursor: not-allowed;
.checkmark {
background-color: #00000055;
&:after {
border-color: #00000055;
}
}
}
}
}
}

View File

@ -1,4 +1,10 @@
$channel_tree_entry_selected: #2d2d2d;
$channel_tree_entry_hovered: #393939;
$channel_tree_entry_text_color: #828282;
$channel_tree_entry_text_color: #828282;
$border_radius_small: .1em;
$border_radius_middle: .15em;
$border_radius_large: .2em;
$button_hover_animation_time: .25s

View File

@ -1,3 +1,5 @@
@import "mixin";
.container-log {
display: block;
overflow-y: auto;
@ -5,6 +7,9 @@
height: 100%;
width: 100%;
@include chat-scrollbar-vertical();
@include chat-scrollbar-horizontal();
.container-messages {
width: 100%;
line-height: 16px;
@ -33,7 +38,7 @@
font-family: sans-serif;
font-size: 13px;
line-height: 1;
line-height: initial;
}
> .timestamp {

View File

@ -5,6 +5,8 @@
flex-shrink: 0;
flex-grow: 0;
background-position: 0 -2717px; /* by default use global flag */
}
.country.flag-ad {

View File

@ -0,0 +1,607 @@
/* sprite bounds (px): width="496" height="400" */
.icon_em {
display: inline-block;
background: url('../../../img/client_icon_sprite.svg'), url('../../img/client_icon_sprite.svg') no-repeat;
background-size: calc(496em / 16) calc(400em / 16);
height: 1em;
width: 1em;
}
/* Icons 1em */
.icon_em.client-d_sound {
background-position: calc(0em / 16) calc(0em / 16);
}
.icon_em.client-d_sound_me {
background-position: calc(-32em / 16) calc(0em / 16);
}
.icon_em.client-d_sound_user {
background-position: calc(-64em / 16) calc(0em / 16);
}
.icon_em.client-about {
background-position: calc(-96em / 16) calc(0em / 16);
}
.icon_em.client-activate_microphone {
background-position: calc(-128em / 16) calc(0em / 16);
}
.icon_em.client-add {
background-position: calc(-160em / 16) calc(0em / 16);
}
.icon_em.client-add_foe {
background-position: calc(-192em / 16) calc(0em / 16);
}
.icon_em.client-add_folder {
background-position: calc(-224em / 16) calc(0em / 16);
}
.icon_em.client-add_friend {
background-position: calc(-256em / 16) calc(0em / 16);
}
.icon_em.client-addon {
background-position: calc(-288em / 16) calc(0em / 16);
}
.icon_em.client-addon-collection {
background-position: calc(-320em / 16) calc(0em / 16);
}
.icon_em.client-apply {
background-position: calc(-352em / 16) calc(0em / 16);
}
.icon_em.client-arrow_down {
background-position: calc(-384em / 16) calc(0em / 16);
}
.icon_em.client-arrow_left {
background-position: calc(-416em / 16) calc(0em / 16);
}
.icon_em.client-arrow_right {
background-position: calc(-448em / 16) calc(0em / 16);
}
.icon_em.client-arrow_up {
background-position: calc(-480em / 16) calc(0em / 16);
}
.icon_em.client-away {
background-position: calc(0em / 16) calc(-32em / 16);
}
.icon_em.client-ban_client {
background-position: calc(-32em / 16) calc(-32em / 16);
}
.icon_em.client-ban_list {
background-position: calc(-64em / 16) calc(-32em / 16);
}
.icon_em.client-bookmark_add {
background-position: calc(-96em / 16) calc(-32em / 16);
}
.icon_em.client-bookmark_add_folder {
background-position: calc(-128em / 16) calc(-32em / 16);
}
.icon_em.client-bookmark_duplicate {
background-position: calc(-160em / 16) calc(-32em / 16);
}
.icon_em.client-bookmark_manager {
background-position: calc(-192em / 16) calc(-32em / 16);
}
.icon_em.client-bookmark_remove {
background-position: calc(-224em / 16) calc(-32em / 16);
}
.icon_em.client-broken_image {
background-position: calc(-256em / 16) calc(-32em / 16);
}
.icon_em.client-browse-addon-online {
background-position: calc(-288em / 16) calc(-32em / 16);
}
.icon_em.client-capture {
background-position: calc(-320em / 16) calc(-32em / 16);
}
.icon_em.client-changelog {
background-position: calc(-352em / 16) calc(-32em / 16);
}
.icon_em.client-change_nickname {
background-position: calc(-384em / 16) calc(-32em / 16);
}
.icon_em.client-channel_chat {
background-position: calc(-416em / 16) calc(-32em / 16);
}
.icon_em.client-channel_collapse_all {
background-position: calc(-448em / 16) calc(-32em / 16);
}
.icon_em.client-channel_commander {
background-position: calc(-480em / 16) calc(-32em / 16);
}
.icon_em.client-channel_create {
background-position: calc(0em / 16) calc(-64em / 16);
}
.icon_em.client-channel_create_sub {
background-position: calc(-32em / 16) calc(-64em / 16);
}
.icon_em.client-channel_default {
background-position: calc(-64em / 16) calc(-64em / 16);
}
.icon_em.client-channel_delete {
background-position: calc(-96em / 16) calc(-64em / 16);
}
.icon_em.client-channel_edit {
background-position: calc(-128em / 16) calc(-64em / 16);
}
.icon_em.client-channel_expand_all {
background-position: calc(-160em / 16) calc(-64em / 16);
}
.icon_em.client-channel_green {
background-position: calc(-192em / 16) calc(-64em / 16);
}
.icon_em.client-channel_green_subscribed {
background-position: calc(-224em / 16) calc(-64em / 16);
}
.icon_em.client-channel_private {
background-position: calc(-256em / 16) calc(-64em / 16);
}
.icon_em.client-channel_red {
background-position: calc(-288em / 16) calc(-64em / 16);
}
.icon_em.client-channel_red_subscribed {
background-position: calc(-320em / 16) calc(-64em / 16);
}
.icon_em.client-channel_switch {
background-position: calc(-352em / 16) calc(-64em / 16);
}
.icon_em.client-channel_unsubscribed {
background-position: calc(-384em / 16) calc(-64em / 16);
}
.icon_em.client-channel_yellow {
background-position: calc(-416em / 16) calc(-64em / 16);
}
.icon_em.client-channel_yellow_subscribed {
background-position: calc(-448em / 16) calc(-64em / 16);
}
.icon_em.client-check_update {
background-position: calc(-480em / 16) calc(-64em / 16);
}
.icon_em.client-client_hide {
background-position: calc(0em / 16) calc(-96em / 16);
}
.icon_em.client-client_show {
background-position: calc(-32em / 16) calc(-96em / 16);
}
.icon_em.client-close_button {
background-position: calc(-64em / 16) calc(-96em / 16);
}
.icon_em.client-complaint_list {
background-position: calc(-96em / 16) calc(-96em / 16);
}
.icon_em.client-conflict-icon {
background-position: calc(-128em / 16) calc(-96em / 16);
}
.icon_em.client-connect {
background-position: calc(-160em / 16) calc(-96em / 16);
}
.icon_em.client-contact {
background-position: calc(-192em / 16) calc(-96em / 16);
}
.icon_em.client-copy {
background-position: calc(-224em / 16) calc(-96em / 16);
}
.icon_em.client-copy_url {
background-position: calc(-256em / 16) calc(-96em / 16);
}
.icon_em.client-default {
background-position: calc(-288em / 16) calc(-96em / 16);
}
.icon_em.client-default_for_all_bookmarks {
background-position: calc(-320em / 16) calc(-96em / 16);
}
.icon_em.client-delete {
background-position: calc(-352em / 16) calc(-96em / 16);
}
.icon_em.client-delete_avatar {
background-position: calc(-384em / 16) calc(-96em / 16);
}
.icon_em.client-disconnect {
background-position: calc(-416em / 16) calc(-96em / 16);
}
.icon_em.client-down {
background-position: calc(-448em / 16) calc(-96em / 16);
}
.icon_em.client-download {
background-position: calc(-480em / 16) calc(-96em / 16);
}
.icon_em.client-edit {
background-position: calc(0em / 16) calc(-128em / 16);
}
.icon_em.client-edit_friend_foe_status {
background-position: calc(-32em / 16) calc(-128em / 16);
}
.icon_em.client-emoticon {
background-position: calc(-64em / 16) calc(-128em / 16);
}
.icon_em.client-error {
background-position: calc(-96em / 16) calc(-128em / 16);
}
.icon_em.client-file_home {
background-position: calc(-128em / 16) calc(-128em / 16);
}
.icon_em.client-file_refresh {
background-position: calc(-160em / 16) calc(-128em / 16);
}
.icon_em.client-filetransfer {
background-position: calc(-192em / 16) calc(-128em / 16);
}
.icon_em.client-find {
background-position: calc(-224em / 16) calc(-128em / 16);
}
.icon_em.client-folder {
background-position: calc(-256em / 16) calc(-128em / 16);
}
.icon_em.client-folder_up {
background-position: calc(-288em / 16) calc(-128em / 16);
}
.icon_em.client-group_100 {
background-position: calc(-320em / 16) calc(-128em / 16);
}
.icon_em.client-group_200 {
background-position: calc(-352em / 16) calc(-128em / 16);
}
.icon_em.client-group_300 {
background-position: calc(-384em / 16) calc(-128em / 16);
}
.icon_em.client-group_500 {
background-position: calc(-416em / 16) calc(-128em / 16);
}
.icon_em.client-group_600 {
background-position: calc(-448em / 16) calc(-128em / 16);
}
.icon_em.client-guisetup {
background-position: calc(-480em / 16) calc(-128em / 16);
}
.icon_em.client-hardware_input_muted {
background-position: calc(0em / 16) calc(-160em / 16);
}
.icon_em.client-hardware_output_muted {
background-position: calc(-32em / 16) calc(-160em / 16);
}
.icon_em.client-hoster_button {
background-position: calc(-64em / 16) calc(-160em / 16);
}
.icon_em.client-hotkeys {
background-position: calc(-96em / 16) calc(-160em / 16);
}
.icon_em.client-icon-pack {
background-position: calc(-128em / 16) calc(-160em / 16);
}
.icon_em.client-iconsview {
background-position: calc(-160em / 16) calc(-160em / 16);
}
.icon_em.client-iconviewer {
background-position: calc(-192em / 16) calc(-160em / 16);
}
.icon_em.client-identity_default {
background-position: calc(-224em / 16) calc(-160em / 16);
}
.icon_em.client-identity_export {
background-position: calc(-256em / 16) calc(-160em / 16);
}
.icon_em.client-identity_import {
background-position: calc(-288em / 16) calc(-160em / 16);
}
.icon_em.client-identity_manager {
background-position: calc(-320em / 16) calc(-160em / 16);
}
.icon_em.client-info {
background-position: calc(-352em / 16) calc(-160em / 16);
}
.icon_em.client-input_muted {
background-position: calc(-384em / 16) calc(-160em / 16);
}
.icon_em.client-input_muted_local {
background-position: calc(-416em / 16) calc(-160em / 16);
}
.icon_em.client-invite_buddy {
background-position: calc(-448em / 16) calc(-160em / 16);
}
.icon_em.client-is_talker {
background-position: calc(-480em / 16) calc(-160em / 16);
}
.icon_em.client-kick_channel {
background-position: calc(0em / 16) calc(-192em / 16);
}
.icon_em.client-kick_server {
background-position: calc(-32em / 16) calc(-192em / 16);
}
.icon_em.client-listview {
background-position: calc(-64em / 16) calc(-192em / 16);
}
.icon_em.client-loading_image {
background-position: calc(-96em / 16) calc(-192em / 16);
}
.icon_em.client-message_incoming {
background-position: calc(-128em / 16) calc(-192em / 16);
}
.icon_em.client-message_info {
background-position: calc(-160em / 16) calc(-192em / 16);
}
.icon_em.client-message_outgoing {
background-position: calc(-192em / 16) calc(-192em / 16);
}
.icon_em.client-messages {
background-position: calc(-224em / 16) calc(-192em / 16);
}
.icon_em.client-moderated {
background-position: calc(-256em / 16) calc(-192em / 16);
}
.icon_em.client-move_client_to_own_channel {
background-position: calc(-288em / 16) calc(-192em / 16);
}
.icon_em.client-music {
background-position: calc(-320em / 16) calc(-192em / 16);
}
.icon_em.client-new_chat {
background-position: calc(-352em / 16) calc(-192em / 16);
}
.icon_em.client-notifications {
background-position: calc(-384em / 16) calc(-192em / 16);
}
.icon_em.client-offline_messages {
background-position: calc(-416em / 16) calc(-192em / 16);
}
.icon_em.client-on_whisperlist {
background-position: calc(-448em / 16) calc(-192em / 16);
}
.icon_em.client-output_muted {
background-position: calc(-480em / 16) calc(-192em / 16);
}
.icon_em.client-permission_channel {
background-position: calc(0em / 16) calc(-224em / 16);
}
.icon_em.client-permission_client {
background-position: calc(-32em / 16) calc(-224em / 16);
}
.icon_em.client-permission_overview {
background-position: calc(-64em / 16) calc(-224em / 16);
}
.icon_em.client-permission_server_groups {
background-position: calc(-96em / 16) calc(-224em / 16);
}
.icon_em.client-phoneticsnickname {
background-position: calc(-128em / 16) calc(-224em / 16);
}
.icon_em.client-ping_1 {
background-position: calc(-160em / 16) calc(-224em / 16);
}
.icon_em.client-ping_2 {
background-position: calc(-192em / 16) calc(-224em / 16);
}
.icon_em.client-ping_3 {
background-position: calc(-224em / 16) calc(-224em / 16);
}
.icon_em.client-ping_4 {
background-position: calc(-256em / 16) calc(-224em / 16);
}
.icon_em.client-ping_calculating {
background-position: calc(-288em / 16) calc(-224em / 16);
}
.icon_em.client-ping_disconnected {
background-position: calc(-320em / 16) calc(-224em / 16);
}
.icon_em.client-play {
background-position: calc(-352em / 16) calc(-224em / 16);
}
.icon_em.client-player_chat {
background-position: calc(-384em / 16) calc(-224em / 16);
}
.icon_em.client-player_commander_off {
background-position: calc(-416em / 16) calc(-224em / 16);
}
.icon_em.client-player_commander_on {
background-position: calc(-448em / 16) calc(-224em / 16);
}
.icon_em.client-player_off {
background-position: calc(-480em / 16) calc(-224em / 16);
}
.icon_em.client-player_on {
background-position: calc(0em / 16) calc(-256em / 16);
}
.icon_em.client-player_whisper {
background-position: calc(-32em / 16) calc(-256em / 16);
}
.icon_em.client-plugins {
background-position: calc(-64em / 16) calc(-256em / 16);
}
.icon_em.client-poke {
background-position: calc(-96em / 16) calc(-256em / 16);
}
.icon_em.client-present {
background-position: calc(-128em / 16) calc(-256em / 16);
}
.icon_em.client-recording_start {
background-position: calc(-160em / 16) calc(-256em / 16);
}
.icon_em.client-recording_stop {
background-position: calc(-192em / 16) calc(-256em / 16);
}
.icon_em.client-refresh {
background-position: calc(-224em / 16) calc(-256em / 16);
}
.icon_em.client-register {
background-position: calc(-256em / 16) calc(-256em / 16);
}
.icon_em.client-reload {
background-position: calc(-288em / 16) calc(-256em / 16);
}
.icon_em.client-remove_foe {
background-position: calc(-320em / 16) calc(-256em / 16);
}
.icon_em.client-remove_friend {
background-position: calc(-352em / 16) calc(-256em / 16);
}
.icon_em.client-security {
background-position: calc(-384em / 16) calc(-256em / 16);
}
.icon_em.client-selectfolder {
background-position: calc(-416em / 16) calc(-256em / 16);
}
.icon_em.client-send_complaint {
background-position: calc(-448em / 16) calc(-256em / 16);
}
.icon_em.client-server_green {
background-position: calc(-480em / 16) calc(-256em / 16);
}
.icon_em.client-server_log {
background-position: calc(0em / 16) calc(-288em / 16);
}
.icon_em.client-server_query {
background-position: calc(-32em / 16) calc(-288em / 16);
}
.icon_em.client-settings {
background-position: calc(-64em / 16) calc(-288em / 16);
}
.icon_em.client-sort_by_name {
background-position: calc(-96em / 16) calc(-288em / 16);
}
.icon_em.client-soundpack {
background-position: calc(-128em / 16) calc(-288em / 16);
}
.icon_em.client-sound-pack {
background-position: calc(-160em / 16) calc(-288em / 16);
}
.icon_em.client-stop {
background-position: calc(-192em / 16) calc(-288em / 16);
}
.icon_em.client-subscribe_mode {
background-position: calc(-224em / 16) calc(-288em / 16);
}
.icon_em.client-subscribe_to_all_channels {
background-position: calc(-256em / 16) calc(-288em / 16);
}
.icon_em.client-subscribe_to_channel {
background-position: calc(-288em / 16) calc(-288em / 16);
}
.icon_em.client-subscribe_to_channel_family {
background-position: calc(-320em / 16) calc(-288em / 16);
}
.icon_em.client-switch_advanced {
background-position: calc(-352em / 16) calc(-288em / 16);
}
.icon_em.client-switch_standard {
background-position: calc(-384em / 16) calc(-288em / 16);
}
.icon_em.client-sync-disable {
background-position: calc(-416em / 16) calc(-288em / 16);
}
.icon_em.client-sync-enable {
background-position: calc(-448em / 16) calc(-288em / 16);
}
.icon_em.client-sync-icon {
background-position: calc(-480em / 16) calc(-288em / 16);
}
.icon_em.client-tab_close_button {
background-position: calc(0em / 16) calc(-320em / 16);
}
.icon_em.client-talk_power_grant {
background-position: calc(-32em / 16) calc(-320em / 16);
}
.icon_em.client-talk_power_grant_next {
background-position: calc(-64em / 16) calc(-320em / 16);
}
.icon_em.client-talk_power_request {
background-position: calc(-96em / 16) calc(-320em / 16);
}
.icon_em.client-talk_power_request_cancel {
background-position: calc(-128em / 16) calc(-320em / 16);
}
.icon_em.client-talk_power_revoke {
background-position: calc(-160em / 16) calc(-320em / 16);
}
.icon_em.client-talk_power_revoke_all_grant_next {
background-position: calc(-192em / 16) calc(-320em / 16);
}
.icon_em.client-temp_server_password {
background-position: calc(-224em / 16) calc(-320em / 16);
}
.icon_em.client-temp_server_password_add {
background-position: calc(-256em / 16) calc(-320em / 16);
}
.icon_em.client-textformat {
background-position: calc(-288em / 16) calc(-320em / 16);
}
.icon_em.client-textformat_bold {
background-position: calc(-320em / 16) calc(-320em / 16);
}
.icon_em.client-textformat_foreground {
background-position: calc(-352em / 16) calc(-320em / 16);
}
.icon_em.client-textformat_italic {
background-position: calc(-384em / 16) calc(-320em / 16);
}
.icon_em.client-textformat_underline {
background-position: calc(-416em / 16) calc(-320em / 16);
}
.icon_em.client-theme {
background-position: calc(-448em / 16) calc(-320em / 16);
}
.icon_em.client-toggle_server_query_clients {
background-position: calc(-480em / 16) calc(-320em / 16);
}
.icon_em.client-toggle_whisper {
background-position: calc(0em / 16) calc(-352em / 16);
}
.icon_em.client-token {
background-position: calc(-32em / 16) calc(-352em / 16);
}
.icon_em.client-token_use {
background-position: calc(-64em / 16) calc(-352em / 16);
}
.icon_em.client-translation {
background-position: calc(-96em / 16) calc(-352em / 16);
}
.icon_em.client-unsubscribe_from_all_channels {
background-position: calc(-128em / 16) calc(-352em / 16);
}
.icon_em.client-unsubscribe_from_channel_family {
background-position: calc(-160em / 16) calc(-352em / 16);
}
.icon_em.client-unsubscribe_mode {
background-position: calc(-192em / 16) calc(-352em / 16);
}
.icon_em.client-up {
background-position: calc(-224em / 16) calc(-352em / 16);
}
.icon_em.client-upload {
background-position: calc(-256em / 16) calc(-352em / 16);
}
.icon_em.client-upload_avatar {
background-position: calc(-288em / 16) calc(-352em / 16);
}
.icon_em.client-urlcatcher {
background-position: calc(-320em / 16) calc(-352em / 16);
}
.icon_em.client-user-account {
background-position: calc(-352em / 16) calc(-352em / 16);
}
.icon_em.client-virtualserver_edit {
background-position: calc(-384em / 16) calc(-352em / 16);
}
.icon_em.client-volume {
background-position: calc(-416em / 16) calc(-352em / 16);
}
.icon_em.client-warning {
background-position: calc(-448em / 16) calc(-352em / 16);
}
.icon_em.client-warning_external_link {
background-position: calc(-480em / 16) calc(-352em / 16);
}
.icon_em.client-warning_info {
background-position: calc(0em / 16) calc(-384em / 16);
}
.icon_em.client-warning_question {
background-position: calc(-32em / 16) calc(-384em / 16);
}
.icon_em.client-weblist {
background-position: calc(-64em / 16) calc(-384em / 16);
}
.icon_em.client-whisper {
background-position: calc(-96em / 16) calc(-384em / 16);
}
.icon_em.client-whisperlists {
background-position: calc(-128em / 16) calc(-384em / 16);
}
.icon_em.client-channel_green_subscribed2 {
background-position: calc(-160em / 16) calc(-384em / 16);
}
.icon_em.client-home {
background-position: calc(-192em / 16) calc(-384em / 16);
}

View File

@ -1,8 +1,14 @@
"""
This should be executed with python 2.7 (because of pydub)
Used voice: UK-Graham
"""
import os
import os.path
import string
import base64
import sys
import requests
import json
import csv
@ -12,7 +18,8 @@ from pydub import AudioSegment
TARGET_DIRECTORY = "audio/speech"
SOURCE_FILE = "audio/speech_sentences.csv"
"""
We cant use the automated way because this now requires a security token and the AWS server does bot exists anymore
def tts(text, file):
voice_id = 4
language_id = 1
@ -43,33 +50,102 @@ def tts(text, file):
sound.export(file, format="wav")
os.remove(file + ".mp3")
"""
def main():
if os.path.exists(TARGET_DIRECTORY):
print("Deleting old speach directory (%s)!" % TARGET_DIRECTORY)
try:
shutil.rmtree(TARGET_DIRECTORY)
except e:
print("Cant delete old dir!")
os.makedirs(TARGET_DIRECTORY)
if False:
if os.path.exists(TARGET_DIRECTORY):
print("Deleting old speach directory (%s)!" % TARGET_DIRECTORY)
try:
shutil.rmtree(TARGET_DIRECTORY)
except e:
print("Cant delete old dir!")
try:
os.makedirs(TARGET_DIRECTORY)
except:
pass
mapping_file = 'audio/speech/mapping.json'
mapping = []
with open(mapping_file, "r") as fstream:
mapping = json.loads(fstream.read())
tts_queue = []
with open(SOURCE_FILE, 'r') as input:
reader = csv.reader(filter(lambda row: len(row) != 0 and row[0] != '#', input), delimiter=';', quotechar='#')
for row in reader:
if len(row) != 2:
continue
print("Generating speech for {}: {}".format(row[0], row[1]))
try:
file = "{}.wav".format(row[0])
tts(row[1], TARGET_DIRECTORY + "/" + file)
mapping.append({'key': row[0], 'file': file})
except e:
print(e)
print("Failed to generate {}", row[0])
file = TARGET_DIRECTORY + "/" + "{}.wav".format(row[0])
with open("audio/speech/mapping.json", "w") as fstream:
_object = filter(lambda e: e["key"] == row[0], mapping)
if len(_object) > 0:
_object = _object[0]
if os.path.exists(TARGET_DIRECTORY + "/" + _object["file"]):
print("Skipping speech generation for {} ({}). File already exists".format(row[0], file))
continue
print("Enqueuing speech generation for {} ({}): {}".format(row[0], file, row[1]))
tts_queue.append([row[0], file, row[1]])
if len(tts_queue) == 0:
print("No sounds need to be generated!")
return
print(tts_queue)
print("Please generate HSR file for the following text:")
for entry in tts_queue:
print(entry[2])
print("")
print("-" * 30)
print("Enter the HSR file path")
file = "" # /home/wolverindev/Downloads/www.naturalreaders.com.har
while True:
if len(file) > 0:
if os.path.exists(file):
break
print("Invalid file try again")
file = string.strip(sys.stdin.readline())
print("Testing file {}".format(file))
with open(file, "r") as fstream:
data = json.loads(fstream.read())
entries = data["log"]["entries"]
for entry in entries:
if not entry["request"]["url"].startswith('https://pweb.naturalreaders.com/v0/tts?'):
continue
if not (entry["request"]["method"] == "POST"):
continue
post_data = json.loads(entry["request"]["postData"]["text"])
key = post_data["t"]
tts_entry = filter(lambda e: e[2] == key, tts_queue)
if len(tts_entry) == 0:
print("Missing generated speech text handle for: {}".format(key))
continue
tts_entry = tts_entry[0]
tts_queue.remove(tts_entry)
print(tts_entry)
with open(tts_entry[1] + ".mp3", "wb") as mp3_tmp:
mp3_tmp.write(base64.decodestring(entry["response"]["content"]["text"]))
mp3_tmp.close()
sound = AudioSegment.from_mp3(tts_entry[1] + ".mp3")
sound.export(tts_entry[1], format="wav")
os.remove(tts_entry[1] + ".mp3")
mapping.append({
'key': tts_entry[0],
'file': "{}.wav".format(tts_entry[0])
})
print("FILE DONE!")
with open(mapping_file, "w") as fstream:
fstream.write(json.dumps(mapping))
fstream.close()

View File

@ -1,46 +1,17 @@
<?php
$testXF = false;
$localhost = false;
$_INCLIDE_ONLY = true;
if (file_exists('auth.php'))
include_once('auth.php');
else if (file_exists('auth/auth.php'))
include_once('auth/auth.php');
else {
function authPath() {
return "";
}
function redirectOnInvalidSession()
{
}
function logged_in() {
return false;
}
}
if(function_exists("setup_forum_auth"))
setup_forum_auth();
$localhost |= gethostname() == "WolverinDEV";
if(!$localhost || $testXF) {
//redirectOnInvalidSession();
}
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$WEB_CLIENT = http_response_code() !== false;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
<meta charset="UTF-8">
<!-- App min width: 450px -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, min-zoom=1, max-zoom: 1, user-scalable=no">
<meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." />
<link rel="icon" href="img/favicon/teacup.png">
<meta name="keywords" content="TeaSpeak, TeaWeb, TeaSpeak-Web,Web client TeaSpeak, веб клієнт TeaSpeak, TSDNS, багатомовність, мультимовність, теми, функціонал"/>
<!-- TODO Needs some fix -->
<link rel="manifest" href="manifest.json">
@ -50,6 +21,7 @@
echo "<title>TeaClient</title>";
} else {
echo "<title>TeaSpeak-Web</title>";
echo '<link rel="icon" href="img/favicon/teacup.png" type="image/x-icon">';
}
?>
@ -64,13 +36,6 @@
spawn_property('connect_default_host', $localhost ? "localhost" : "ts.TeaSpeak.de");
spawn_property('localhost_debug', $localhost ? "true" : "false");
if(isset($_COOKIE)) {
if(array_key_exists("COOKIE_NAME_USER_DATA", $GLOBALS) && array_key_exists($GLOBALS["COOKIE_NAME_USER_DATA"], $_COOKIE))
spawn_property('forum_user_data', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_DATA"]]);
if(array_key_exists("COOKIE_NAME_USER_SIGN", $GLOBALS) && array_key_exists($GLOBALS["COOKIE_NAME_USER_SIGN"], $_COOKIE))
spawn_property('forum_user_sign', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_SIGN"]]);
}
spawn_property('forum_path', authPath());
$version = file_get_contents("./version");
if ($version === false)
@ -171,8 +136,23 @@
</div>
<div id="spoiler-style" style="z-index: 1000000; position: absolute; display: block; background: white; right: 5px; left: 5px; top: 34px;">
<!-- <img src="https://www.chromatic-solutions.de/teaspeak/window/connect_opened.png"> -->
<!-- <img src="http://puu.sh/DZDgO/9149c0a1aa.png"> -->
<!-- <img src="http://puu.sh/E0QUb/ce5e3f93ae.png"> -->
<!-- <img src="img/style/default.png"> -->
<img src="img/style/user-selected.png">
<!-- <img src="img/style/user-selected.png"> -->
<!-- <img src="img/style/privat_chat.png"> -->
<!-- <img src="http://puu.sh/E1aBL/3c40ae3c2c.png"> -->
<!-- <img src="http://puu.sh/E2qb2/b27bb2fde5.png"> -->
<!-- <img src="http://puu.sh/E2UQR/1e0d7e03a3.png"> -->
<!-- <img src="http://puu.sh/E38yX/452e27864c.png"> -->
<!-- <img src="http://puu.sh/E3fjq/e2b4447bcd.png"> -->
<!-- <img src="http://puu.sh/E3WlW/f791a9e7b1.png"> -->
<!-- <img src="http://puu.sh/E4lHJ/1a4afcdf0b.png"> -->
<!-- <img src="http://puu.sh/E4HKK/5ee74d4cc7.png"> -->
<!-- <img src="http://puu.sh/E6LN1/8518c10898.png"> -->
<img src="http://puu.sh/E6NXv/eb2f19c7c3.png">
</div>
<button class="toggle-spoiler-style" style="height: 30px; width: 100px; z-index: 100000000; position: absolute; bottom: 2px;">toggle style</button>
<script>
@ -181,7 +161,7 @@
$(".toggle-spoiler-style").on('click', () => {
$("#spoiler-style").toggle();
});
}, 1000);
}, 2500);
</script>
<div id="music-test"></div>
@ -191,28 +171,10 @@
<div class="container">
</div>
</div>
<div id="global-tooltip">
<a></a>
</div>
</body>
<?php
$footer_style = "display: none;";
$footer_forum = '';
if($WEB_CLIENT) {
$footer_style = "display: block;";
if (logged_in()) {
$footer_forum = "<a href=\"" . authPath() . "auth.php?type=logout\">logout</a>";
} else {
$footer_forum = "<a href=\"" . authPath() . "login.php\">Login</a> via the TeaSpeak forum.";
}
}
?>
<footer style="<?php echo $footer_style; ?>">
<div class="container" style="display: flex; flex-direction: row; align-content: space-between;">
<div class="hide-small" style="align-self: center; position: fixed; left: 5px;">Open source on <a href="https://github.com/TeaSpeak/TeaSpeak-Web" style="display: inline-block; position: relative">github.com</a></div>
<div style="align-self: center;">TeaSpeak Web (<?php echo $version; ?>) by WolverinDEV</div>
<div class="hide-small" style="align-self: center; position: fixed; right: 5px;"><?php echo $footer_forum; ?></div>
</div>
</footer>
<div id="top-menu-bar"></div>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -74,7 +74,7 @@
]
}, {
"key": "ru_gt",
"country_code": "gt",
"country_code": "ru",
"path": "ru_google_translate.translation",
"name": "Auto translated messages for language ru",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg preserveAspectRatio="xMinYMid" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g transform="matrix(0.933313,-4.93038e-32,-4.93038e-32,0.933313,2.134,2.13353)">
<path d="M16.272,21.06L16.3,50.447C16.3,52.687 18.113,54.501 20.354,54.501L43.648,54.501C45.888,54.501 47.702,52.689 47.702,50.448L47.728,21.06" style="fill:none;stroke-width:2px;stroke:rgb(211,84,68);"/>
</g>
<g transform="matrix(0.933313,-4.93038e-32,-4.93038e-32,0.933313,2.134,2.13353)">
<path d="M12.746,15.992L51.254,15.992" style="fill:none;stroke-width:3px;stroke:rgb(211,84,68);"/>
</g>
<g transform="matrix(0.933313,-4.93038e-32,-4.93038e-32,0.933313,2.134,2.13353)">
<path d="M38.334,15.992L38.334,12.245C38.334,10.729 37.104,9.5 35.589,9.5L28.409,9.5C26.894,9.5 25.666,10.73 25.666,12.245L25.666,15.992" style="fill:none;stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(211,84,68);"/>
</g>
<g transform="matrix(0.933313,-4.93038e-32,-4.93038e-32,0.933313,2.134,2.13353)">
<path d="M38.986,47.447L39.013,24.059M24.987,47.447L25.013,24.059M31.987,47.447L32.013,24.059" style="fill:none;stroke-width:2px;stroke:rgb(211,84,68);"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN'
'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg id="Layer_1" style="enable-background:new 0 0 64 64;" version="1.1" viewBox="0 0 64 64" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:#151515;}
</style>
<g><g id="Icon-Plus" transform="translate(28.000000, 278.000000)"><path class="st0" d="M4-222.1c-13.2,0-23.9-10.7-23.9-23.9c0-13.2,10.7-23.9,23.9-23.9s23.9,10.7,23.9,23.9 C27.9-232.8,17.2-222.1,4-222.1L4-222.1z M4-267.3c-11.7,0-21.3,9.6-21.3,21.3s9.6,21.3,21.3,21.3s21.3-9.6,21.3-21.3 S15.7-267.3,4-267.3L4-267.3z" id="Fill-38"/><polygon
class="st0" id="Fill-39" points="-8.7,-247.4 16.7,-247.4 16.7,-244.6 -8.7,-244.6 "/><polygon class="st0"
id="Fill-40"
points="2.6,-258.7 5.4,-258.7 5.4,-233.3 2.6,-233.3 "/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg id="Layer_1" style="enable-background:new 0 0 64 64;" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:#151515;}
</style><g><g id="Icon-Trash" transform="translate(232.000000, 228.000000)"><polygon class="st0" id="Fill-6" points="-207.5,-205.1 -204.5,-205.1 -204.5,-181.1 -207.5,-181.1 "/><polygon class="st0" id="Fill-7" points="-201.5,-205.1 -198.5,-205.1 -198.5,-181.1 -201.5,-181.1 "/><polygon class="st0" id="Fill-8" points="-195.5,-205.1 -192.5,-205.1 -192.5,-181.1 -195.5,-181.1 "/><polygon class="st0" id="Fill-9" points="-219.5,-214.1 -180.5,-214.1 -180.5,-211.1 -219.5,-211.1 "/><path class="st0" d="M-192.6-212.6h-2.8v-3c0-0.9-0.7-1.6-1.6-1.6h-6c-0.9,0-1.6,0.7-1.6,1.6v3h-2.8v-3 c0-2.4,2-4.4,4.4-4.4h6c2.4,0,4.4,2,4.4,4.4V-212.6" id="Fill-10"/><path class="st0" d="M-191-172.1h-18c-2.4,0-4.5-2-4.7-4.4l-2.8-36l3-0.2l2.8,36c0.1,0.9,0.9,1.6,1.7,1.6h18 c0.9,0,1.7-0.8,1.7-1.6l2.8-36l3,0.2l-2.8,36C-186.5-174-188.6-172.1-191-172.1" id="Fill-11"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN'
'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg height="512px" id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512"
width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style type="text/css">
.st0{fill:#151515;}
</style>
<g><polygon class="st0" points="304,96 288,96 288,176 368,176 368,160 304,160 "/><path class="st0"
d="M325.3,64H160v48h-48v336h240v-48h48V139L325.3,64z M336,432H128V128h32v272h176V432z M384,384H176V80h142.7l65.3,65.6V384 z"/></g></svg>

After

Width:  |  Height:  |  Size: 678 B

View File

@ -0,0 +1,3 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg id="Layer_1" style="enable-background:new 0 0 64 64;" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:#151515;}
</style><g><g id="Icon-Pencil" transform="translate(179.000000, 382.000000)"><path class="st0" d="M-168.2-328l3.7-14.9l22.7-22.7l11.2,11.2l-22.7,22.7L-168.2-328L-168.2-328z M-161.9-341.5 l-2.4,9.6l9.6-2.4l20.2-20.2l-7.2-7.2L-161.9-341.5L-161.9-341.5z" id="Fill-168"/><path class="st0" d="M-155.7-332.6c-1-3.9-4-6.9-7.9-7.9l0.7-2.8c4.9,1.2,8.7,5,9.9,9.9L-155.7-332.6" id="Fill-169"/><polyline class="st0" id="Fill-170" points="-156,-338.1 -158,-340.2 -138.1,-360.1 -136.1,-358.1 -156,-338.1 "/><path class="st0" d="M-166.2-330l4.4-1.1c-0.4-1.6-1.7-2.9-3.3-3.3L-166.2-330" id="Fill-171"/><path class="st0" d="M-129.5-355.5l-11.2-11.2l4.5-4.5l0.7,0.1c5.4,0.7,9.7,5,10.4,10.4l0.1,0.7L-129.5-355.5 L-129.5-355.5z M-136.6-366.7l7.2,7.2l1.4-1.4c-0.8-3.6-3.6-6.4-7.2-7.2L-136.6-366.7L-136.6-366.7z" id="Fill-172"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,2 @@
<svg id="icon_play" data-name="base" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M395,512a73.14,73.14,0,0,1-73.14-73.14V73.14a73.14,73.14,0,1,1,146.29,0V438.86A73.14,73.14,0,0,1,395,512Z" fill="#673535"/><path d="M117,512a73.14,73.14,0,0,1-73.14-73.14V73.14a73.14,73.14,0,1,1,146.29,0V438.86A73.14,73.14,0,0,1,117,512Z" fill="#673535"/></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -0,0 +1,2 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>play-glyph</title><path d="M60.54,512c-17.06,0-30.43-13.86-30.43-31.56V31.55C30.12,13.86,43.48,0,60.55,0A32.94,32.94,0,0,1,77,4.52L465.7,229c10.13,5.85,16.18,16,16.18,27s-6,21.2-16.18,27L77,507.48A32.92,32.92,0,0,1,60.55,512Z" fill="#376736"/></svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
<defs>
<style>
.cls-1 {
fill: #313131;
fill-rule: evenodd;
}
</style>
</defs>
<path id="icon" class="cls-1" d="M200.012,0c110.457,0,200,89.545,200,200s-89.54,200-200,200S0.015,310.456.015,200,89.557,0,200.012,0Zm0.01,38.636A160.629,160.629,0,1,1,39.395,199.268,160.632,160.632,0,0,1,200.022,38.639ZM159.407,153.478s42.867-5.468,19.8,68.014c-24.208,77.113-66.566,140.45,63.467,81.447,0,0-64.556,21.6-42.619-37.407,12.79-34.406,28.292-81.421,27.092-93.4C225.387,154.546,207.6,141.025,159.407,153.478Zm65.564-79a24.685,24.685,0,1,1-24.687,24.684A24.685,24.685,0,0,1,224.971,74.479Z"/>
</svg>

After

Width:  |  Height:  |  Size: 760 B

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!--
Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)
Editor: Markus Hadenfeldt <graphics@teaspeak.de>
-->
<svg version="1.1" id="camara_icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<g>
<g>
<path style="fill:#030104;" d="M50,40c-8.285,0-15,6.718-15,15c0,8.285,6.715,15,15,15c8.283,0,15-6.715,15-15
C65,46.718,58.283,40,50,40z M90,25H78c-1.65,0-3.428-1.28-3.949-2.846l-3.102-9.309C70.426,11.28,68.65,10,67,10H33
c-1.65,0-3.428,1.28-3.949,2.846l-3.102,9.309C25.426,23.72,23.65,25,22,25H10C4.5,25,0,29.5,0,35v45c0,5.5,4.5,10,10,10h80
c5.5,0,10-4.5,10-10V35C100,29.5,95.5,25,90,25z M50,80c-13.807,0-25-11.193-25-25c0-13.806,11.193-25,25-25
c13.805,0,25,11.194,25,25C75,68.807,63.805,80,50,80z M86.5,41.993c-1.932,0-3.5-1.566-3.5-3.5c0-1.932,1.568-3.5,3.5-3.5
c1.934,0,3.5,1.568,3.5,3.5C90,40.427,88.433,41.993,86.5,41.993z"/>
</g>
</g>
</svg>
<!--
Icon made by Daniel Bruce (https://www.flaticon.com/authors/daniel-bruce)
Licensed by Creative Commons BY 3.0 (http://creativecommons.org/licenses/by/3.0/)
Taken from: https://www.flaticon.com/free-icon/photo-camera_3901
-->

After

Width:  |  Height:  |  Size: 1.3 KiB

17
shared/img/style/urls.txt Normal file
View File

@ -0,0 +1,17 @@
18:56:34> "Nieme": http://puu.sh/E0Qut/3092386510.png <
18:59:26> "Nieme": http://puu.sh/E0QxG/3b4d7286ec.png <
19:00:16> "Nieme": http://puu.sh/E0QyI/8b66f7cf3b.png <
19:02:09> "Nieme": süß das er denkt das du das nicht weisst :D <
19:02:20> "Another TeaSpeak user": xD <
19:02:53> "Nieme": http://puu.sh/E0QBN/a49973e13f.png <
19:03:05> "Nieme": http://puu.sh/E0QC0/a668c9500c.png <
19:03:50> "Nieme": http://puu.sh/E0QCZ/e78dc1b3c0.png <
19:06:47> "Nieme": http://puu.sh/E0QGx/b21ace2d9a.png <
19:18:29> "Nieme": -> http://puu.sh/E0QUb/ce5e3f93ae.png <
19:20:45> "Nieme": http://puu.sh/E0QX2/af62f28320.png <
19:23:02> "Nieme": 82qtx5 <
19:23:06> "Nieme": 1 280 349 948 <
20:09:34> Your chat partner has disconnected <
20:10:08> Your chat partner has reconnected
https://www.iconfinder.com/iconsets/evil-icons-user-interface

View File

@ -24,6 +24,7 @@ enum DisconnectReason {
HANDSHAKE_BANNED,
SERVER_CLOSED,
SERVER_REQUIRES_PASSWORD,
SERVER_HOSTMESSAGE,
IDENTITY_TOO_LOW,
UNKNOWN
}
@ -88,14 +89,15 @@ class ConnectionHandler {
permissions: PermissionManager;
groups: GroupManager;
chat_frame: chat.Frame;
side_bar: chat.Frame;
select_info: InfoBar;
chat: ChatBox;
settings: ServerSettings;
sound: sound.SoundManager;
readonly tag_connection_handler: JQuery;
hostbanner: Hostbanner;
tag_connection_handler: JQuery;
private _clientId: number = 0;
private _local_client: LocalClientEntry;
@ -126,42 +128,50 @@ class ConnectionHandler {
this.log = new log.ServerLog(this);
this.select_info = new InfoBar(this);
this.channelTree = new ChannelTree(this);
this.chat = new ChatBox(this);
this.chat_frame = new chat.Frame(this);
this.side_bar = new chat.Frame(this);
this.sound = new sound.SoundManager(this);
this.hostbanner = new Hostbanner(this);
this.serverConnection = connection.spawn_server_connection(this);
this.serverConnection.onconnectionstatechanged = this.on_connection_state_changed.bind(this);
this.fileManager = new FileManager(this);
this.permissions = new PermissionManager(this);
this.side_bar.channel_conversations().initialize_needed_listener();
this.groups = new GroupManager(this);
this._local_client = new LocalClientEntry(this);
this.channelTree.registerClient(this._local_client);
//settings.static_global(Settings.KEY_DISABLE_VOICE, false)
this.chat.initialize();
this.tag_connection_handler = $.spawn("div").addClass("connection-container");
$.spawn("div").addClass("server-icon icon client-server_green").appendTo(this.tag_connection_handler);
$.spawn("div").addClass("server-name").text(tr("Not connected")).appendTo(this.tag_connection_handler);
$.spawn("div").addClass("button-close icon client-tab_close_button").appendTo(this.tag_connection_handler);
this.tag_connection_handler.on('click', event => {
if(event.isDefaultPrevented())
return;
/* initialize connection handler tab entry */
{
this.tag_connection_handler = $.spawn("div").addClass("connection-container");
$.spawn("div").addClass("server-icon icon client-server_green").appendTo(this.tag_connection_handler);
$.spawn("div").addClass("server-name").appendTo(this.tag_connection_handler);
$.spawn("div").addClass("button-close icon client-tab_close_button").appendTo(this.tag_connection_handler);
this.tag_connection_handler.on('click', event => {
if(event.isDefaultPrevented())
return;
server_connections.set_active_connection_handler(this);
});
this.tag_connection_handler.find(".button-close").on('click', event => {
server_connections.destroy_server_connection_handler(this);
event.preventDefault();
});
server_connections.set_active_connection_handler(this);
});
this.tag_connection_handler.find(".button-close").on('click', event => {
server_connections.destroy_server_connection_handler(this);
event.preventDefault();
});
this.tab_set_name(tr("Not connected"));
}
}
tab_set_name(name: string) {
this.tag_connection_handler.toggleClass('cutoff-name', name.length > 30);
this.tag_connection_handler.find(".server-name").text(name);
}
setup() { }
async startConnection(addr: string, profile: profiles.ConnectionProfile, parameters: ConnectParameters) {
this.tag_connection_handler.find(".server-name").text(tr("Connecting"));
this.cancel_reconnect();
async startConnection(addr: string, profile: profiles.ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
this.tab_set_name(tr("Connecting"));
this.cancel_reconnect(false);
this._reconnect_attempt = false;
if(this.serverConnection)
this.handleDisconnect(DisconnectReason.REQUESTED);
@ -172,8 +182,9 @@ class ConnectionHandler {
port: -1
};
{
let _v6_end = addr.indexOf(']');
let idx = addr.lastIndexOf(':');
if(idx != -1) {
if(idx != -1 && idx > _v6_end) {
server_address.port = parseInt(addr.substr(idx + 1));
server_address.host = addr.substr(0, idx);
} else {
@ -203,7 +214,14 @@ class ConnectionHandler {
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open();
}
}
if(parameters.password) {
connection_log.update_address_password({
hostname: server_address.host,
port: server_address.port
}, parameters.password.password);
}
const original_address = {host: server_address.host, port: server_address.port};
if(dns.supported() && !server_address.host.match(Modals.Regex.IP_V4) && !server_address.host.match(Modals.Regex.IP_V6)) {
const id = ++this._connect_initialize_id;
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE, {});
@ -229,6 +247,15 @@ class ConnectionHandler {
}
await this.serverConnection.connect(server_address, new connection.HandshakeHandler(profile, parameters));
setTimeout(() => {
const connected = this.serverConnection.connected();
if(user_action && connected) {
connection_log.log_connect({
hostname: original_address.host,
port: original_address.port
});
}
}, 50);
}
@ -252,7 +279,6 @@ class ConnectionHandler {
*/
onConnected() {
console.log("Client connected!");
this.channelTree.registerClient(this._local_client);
this.permissions.requestPermissionList();
if(this.groups.serverGroups.length == 0)
this.groups.requestGroups();
@ -352,7 +378,7 @@ class ConnectionHandler {
const profile = profiles.find_profile(properties.connect_profile) || profiles.default_profile();
const cprops = this.reconnect_properties(profile);
this.startConnection(properties.connect_address, profile, cprops);
this.startConnection(properties.connect_address, profile, true, cprops);
});
const url = build_url(properties);
@ -379,10 +405,11 @@ class ConnectionHandler {
handleDisconnect(type: DisconnectReason, data: any = {}) {
this._connect_initialize_id++;
this.tag_connection_handler.find(".server-name").text(tr("Not connected"));
this.tab_set_name(tr("Not connected"));
let auto_reconnect = false;
switch (type) {
case DisconnectReason.REQUESTED:
case DisconnectReason.SERVER_HOSTMESSAGE: /* already handled */
break;
case DisconnectReason.HANDLER_DESTROYED:
if(data)
@ -468,7 +495,8 @@ class ConnectionHandler {
break;
case DisconnectReason.SERVER_CLOSED:
this.chat.serverChat().appendError(tr("Server closed ({0})"), data.reasonmsg);
this.log.log(log.server.Type.SERVER_CLOSED, {message: data.reasonmsg});
//this.chat.serverChat().appendError(tr("Server closed ({0})"), data.reasonmsg);
createErrorModal(
tr("Server closed"),
"The server is closed.<br>" + //TODO tr
@ -479,15 +507,23 @@ class ConnectionHandler {
auto_reconnect = true;
break;
case DisconnectReason.SERVER_REQUIRES_PASSWORD:
this.chat.serverChat().appendError(tr("Server requires password"));
this.log.log(log.server.Type.SERVER_REQUIRES_PASSWORD, {});
//this.chat.serverChat().appendError(tr("Server requires password"));
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
if(!(typeof password === "string")) return;
const cprops = this.reconnect_properties(this.serverConnection.handshake_handler().profile);
const profile = this.serverConnection.handshake_handler().profile;
const cprops = this.reconnect_properties(profile);
cprops.password = {password: password as string, hashed: false};
this.startConnection(this.serverConnection.remote_address().host + ":" + this.serverConnection.remote_address().port,
this.serverConnection.handshake_handler().profile,
cprops);
connection_log.update_address_info({
port: this.channelTree.server.remote_address.port,
hostname: this.channelTree.server.remote_address.host
}, {
flag_password: true
} as any);
this.startConnection(this.channelTree.server.remote_address.host + ":" + this.channelTree.server.remote_address.port, profile, false, cprops);
}).open();
break;
case DisconnectReason.CLIENT_KICKED:
@ -499,15 +535,29 @@ class ConnectionHandler {
auto_reconnect = false;
break;
case DisconnectReason.HANDSHAKE_BANNED:
this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"),
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]),
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : "");
this.log.log(log.server.Type.SERVER_BANNED, {
invoker: {
client_name: data["invokername"],
client_id: parseInt(data["invokerid"]),
client_unique_id: data["invokeruid"]
},
message: data["reasonmsg"],
time: parseInt(data["time"])
});
this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
break;
case DisconnectReason.CLIENT_BANNED:
this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"),
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]),
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : "");
this.log.log(log.server.Type.SERVER_BANNED, {
invoker: {
client_name: data["invokername"],
client_id: parseInt(data["invokerid"]),
client_unique_id: data["invokeruid"]
},
message: data["reasonmsg"],
time: parseInt(data["time"])
});
this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
break;
default:
@ -517,6 +567,7 @@ class ConnectionHandler {
break;
}
this.channelTree.unregisterClient(this._local_client); /* if we dont unregister our client here the client will be destroyed */
this.channelTree.reset();
if(this.serverConnection)
this.serverConnection.disconnect();
@ -524,7 +575,8 @@ class ConnectionHandler {
if(control_bar.current_connection_handler() == this)
control_bar.update_connection_state();
this.select_info.setCurrentSelected(null);
this.select_info.update_banner();
this.side_bar.private_conversations().clear_client_ids();
this.hostbanner.update();
if(auto_reconnect) {
if(!this.serverConnection) {
@ -542,15 +594,15 @@ class ConnectionHandler {
this.log.log(log.server.Type.RECONNECT_CANCELED, {});
log.info(LogCategory.NETWORKING, tr("Reconnecting..."));
this.startConnection(server_address.host + ":" + server_address.port, profile, this.reconnect_properties(profile));
this.startConnection(server_address.host + ":" + server_address.port, profile, false, this.reconnect_properties(profile));
this._reconnect_attempt = true;
}, 5000);
}
}
cancel_reconnect() {
cancel_reconnect(log_event: boolean) {
if(this._reconnect_timer) {
this.log.log(log.server.Type.RECONNECT_CANCELED, {});
if(log_event) this.log.log(log.server.Type.RECONNECT_CANCELED, {});
clearTimeout(this._reconnect_timer);
this._reconnect_timer = undefined;
}
@ -562,6 +614,8 @@ class ConnectionHandler {
}
update_voice_status(targetChannel?: ChannelEntry) {
if(!this._local_client) return; /* we've been destroyed */
targetChannel = targetChannel || this.getClient().currentChannel();
const vconnection = this.serverConnection.voice_connection();
@ -636,10 +690,21 @@ class ConnectionHandler {
if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().record_supported) {
const active = !this.client_status.input_muted && !this.client_status.output_muted;
if(active)
vconnection.voice_recorder().input.start();
else
if(active) {
if(vconnection.voice_recorder().input.current_state() === audio.recorder.InputState.PAUSED) {
vconnection.voice_recorder().input.start().then(result => {
if(result != audio.recorder.InputStartResult.EOK) {
console.warn(tr("Failed to start microphone input (%s)."), result);
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), result)).open();
}
}).catch(error => {
console.warn(tr("Failed to start microphone input (%s)."), error);
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open();
});
}
} else {
vconnection.voice_recorder().input.stop();
}
}
if(control_bar.current_connection_handler() === this)
@ -665,6 +730,12 @@ class ConnectionHandler {
if(this.client_status.away === state)
return;
if(state) {
this.sound.play(Sound.AWAY_ACTIVATED);
} else {
this.sound.play(Sound.AWAY_DEACTIVATED);
}
this.client_status.away = state;
this.serverConnection.send_command("clientupdate", {
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
@ -707,4 +778,141 @@ class ConnectionHandler {
password: this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.password : undefined
}
}
update_avatar() {
Modals.spawnAvatarUpload(data => {
if(typeof(data) === "undefined")
return;
if(data === null) {
console.log(tr("Deleting existing avatar"));
this.serverConnection.send_command('ftdeletefile', {
name: "/avatar_", /* delete own avatar */
path: "",
cid: 0
}).then(() => {
createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open();
}).catch(error => {
console.error(tr("Failed to reset avatar flag: %o"), error);
let message;
if(error instanceof CommandResult)
message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message);
if(!message)
message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
createErrorModal(tr("Failed to delete avatar"), message).open();
return;
});
} else {
console.log(tr("Uploading new avatar"));
(async () => {
let key: transfer.UploadKey;
try {
key = await this.fileManager.upload_file({
size: data.byteLength,
path: '',
name: '/avatar',
overwrite: true,
channel: undefined,
channel_password: undefined
});
} catch(error) {
console.error(tr("Failed to initialize avatar upload: %o"), error);
let message;
if(error instanceof CommandResult) {
//TODO: Resolve permission name
//i_client_max_avatar_filesize
if(error.id == ErrorID.PERMISSION_ERROR) {
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]);
} else {
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message);
}
}
if(!message)
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
createErrorModal(tr("Failed to upload avatar"), message).open();
return;
}
try {
await transfer.spawn_upload_transfer(key).put_data(data);
} catch(error) {
console.error(tr("Failed to upload avatar: %o"), error);
let message;
if(typeof(error) === "string")
message = MessageHelper.formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error);
if(!message)
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
createErrorModal(tr("Failed to upload avatar"), message).open();
return;
}
try {
await this.serverConnection.send_command('clientupdate', {
client_flag_avatar: guid()
});
} catch(error) {
console.error(tr("Failed to update avatar flag: %o"), error);
let message;
if(error instanceof CommandResult)
message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message);
if(!message)
message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
createErrorModal(tr("Failed to set avatar"), message).open();
return;
}
createInfoModal(tr("Avatar successfully uploaded"), tr("Your avatar has been uploaded successfully!")).open();
})();
}
});
}
destroy() {
this.cancel_reconnect(true);
this.tag_connection_handler && this.tag_connection_handler.remove();
this.tag_connection_handler = undefined;
this.hostbanner && this.hostbanner.destroy();
this.hostbanner = undefined;
this._local_client && this._local_client.destroy();
this._local_client = undefined;
this.channelTree && this.channelTree.destroy();
this.channelTree = undefined;
this.side_bar && this.side_bar.destroy();
this.side_bar = undefined;
this.select_info && this.select_info.destroy();
this.select_info = undefined;
this.log && this.log.destroy();
this.log = undefined;
this.permissions && this.permissions.destroy();
this.permissions = undefined;
this.groups && this.groups.destroy();
this.groups = undefined;
this.fileManager && this.fileManager.destroy();
this.fileManager = undefined;
this.settings && this.settings.destroy();
this.settings = undefined;
if(this.serverConnection) {
this.serverConnection.onconnectionstatechanged = undefined;
connection.destroy_server_connection(this.serverConnection);
}
this.serverConnection = undefined;
this.sound = undefined;
this._local_client = undefined;
}
}

View File

@ -102,7 +102,7 @@ class RequestFileUpload {
this.transfer_key = key;
}
async put_data(data: BufferSource | File) {
async put_data(data: BlobPart | File) {
const form_data = new FormData();
if(data instanceof File) {
@ -110,6 +110,10 @@ class RequestFileUpload {
throw "invalid size";
form_data.append("file", data);
} else if(typeof(data) === "string") {
if(data.length != this.transfer_key.total_size)
throw "invalid size";
form_data.append("file", new Blob([data], { type: "application/octet-stream" }));
} else {
const buffer = <BufferSource>data;
if(buffer.byteLength != this.transfer_key.total_size)
@ -159,6 +163,24 @@ class FileManager extends connection.AbstractCommandHandler {
this.connection.command_handler_boss().register_handler(this);
}
destroy() {
if(this.connection) {
const hboss = this.connection.command_handler_boss();
if(hboss)
hboss.unregister_handler(this);
}
this.listRequests = undefined;
this.pending_download_requests = undefined;
this.pending_upload_requests = undefined;
this.icons && this.icons.destroy();
this.icons = undefined;
this.avatars && this.avatars.destroy();
this.avatars = undefined;
}
handle_command(command: connection.ServerCommand): boolean {
switch (command.command) {
case "notifyfilelist":
@ -262,7 +284,7 @@ class FileManager extends connection.AbstractCommandHandler {
"clientftfid": transfer_data.client_transfer_id,
"seekpos": 0,
"proto": 1
}).catch(reason => {
}, {process_result: false}).catch(reason => {
this.pending_download_requests.remove(transfer_data);
reject(reason);
})
@ -410,9 +432,9 @@ function media_image_type(type: ImageType, file?: boolean) {
}
}
function image_type(base64: string | ArrayBuffer) {
function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) {
const ab2str10 = () => {
const buf = new Uint8Array(base64 as ArrayBuffer);
const buf = new Uint8Array(encoded_data as ArrayBuffer);
if(buf.byteLength < 10)
return "";
@ -422,7 +444,7 @@ function image_type(base64: string | ArrayBuffer) {
return result;
};
const bin = typeof(base64) === "string" ? atob(base64) : ab2str10();
const bin = typeof(encoded_data) === "string" ? ((typeof(base64_encoded) === "undefined" || base64_encoded) ? atob(encoded_data) : encoded_data) : ab2str10();
if(bin.length < 10) return ImageType.UNKNOWN;
if(bin[0] == String.fromCharCode(66) && bin[1] == String.fromCharCode(77)) {
@ -481,8 +503,7 @@ class CacheManager {
async resolve_cached(key: string, max_age?: number) : Promise<Response | undefined> {
max_age = typeof(max_age) === "number" ? max_age : -1;
const request = new Request("https://_local_cache/cache_request_" + key);
const cached_response = await this._cache_category.match(request);
const cached_response = await this._cache_category.match("https://_local_cache/cache_request_" + key);
if(!cached_response)
return undefined;
@ -491,8 +512,6 @@ class CacheManager {
}
async put_cache(key: string, value: Response, type?: string, headers?: {[key: string]:string}) {
const request = new Request("https://_local_cache/cache_request_" + key);
const new_headers = new Headers();
for(const key of value.headers.keys())
new_headers.set(key, value.headers.get(key));
@ -501,14 +520,25 @@ class CacheManager {
for(const key of Object.keys(headers || {}))
new_headers.set(key, headers[key]);
await this._cache_category.put(request, new Response(value.body, {
await this._cache_category.put("https://_local_cache/cache_request_" + key, new Response(value.body, {
headers: new_headers
}));
}
async delete(key: string) {
const flag = await this._cache_category.delete("https://_local_cache/cache_request_" + key, {
ignoreVary: true,
ignoreMethod: true,
ignoreSearch: true
});
if(!flag) {
console.warn(tr("Failed to delete key %s from cache!"), flag);
}
}
}
class IconManager {
private static cache: CacheManager;
private static cache: CacheManager = new CacheManager("icons");
handle: FileManager;
private _id_urls: {[id:number]:string} = {};
@ -516,9 +546,15 @@ class IconManager {
constructor(handle: FileManager) {
this.handle = handle;
}
if(!IconManager.cache)
IconManager.cache = new CacheManager("icons");
destroy() {
if(URL.revokeObjectURL) {
for(const id of Object.keys(this._id_urls))
URL.revokeObjectURL(this._id_urls[id]);
}
this._id_urls = undefined;
this._loading_promises = undefined;
}
async clear_cache() {
@ -548,7 +584,7 @@ class IconManager {
return this.handle.download_file("", "/icon_" + id);
}
private async _response_url(response: Response) {
private static async _response_url(response: Response) {
if(!response.headers.has('X-media-bytes'))
throw "missing media bytes";
@ -573,14 +609,50 @@ class IconManager {
await IconManager.cache.setup();
const response = await IconManager.cache.resolve_cached('icon_' + id); //TODO age!
if(response)
if(response) {
const url = await IconManager._response_url(response);
if(this._id_urls[id])
URL.revokeObjectURL(this._id_urls[id]);
return {
id: id,
url: (this._id_urls[id] = await this._response_url(response))
url: url
};
}
return undefined;
}
private static _static_id_url: {[icon: number]:string} = {};
private static _static_cached_promise: {[icon: number]:Promise<Icon>} = {};
static load_cached_icon(id: number, ignore_age?: boolean) : Promise<Icon> | Icon {
if(this._static_id_url[id]) {
return {
id: id,
url: this._static_id_url[id]
};
}
if(this._static_cached_promise[id])
return this._static_cached_promise[id];
return (this._static_cached_promise[id] = (async () => {
if(!this.cache.setupped())
await this.cache.setup();
const response = await this.cache.resolve_cached('icon_' + id); //TODO age!
if(response) {
const url = await this._response_url(response);
if(this._static_id_url[id])
URL.revokeObjectURL(this._static_id_url[id]);
this._static_id_url[id] = url;
return {
id: id,
url: url
};
}
})());
}
private async _load_icon(id: number) : Promise<Icon> {
try {
let download_key: transfer.DownloadKey;
@ -604,7 +676,10 @@ class IconManager {
const media = media_image_type(type);
await IconManager.cache.put_cache('icon_' + id, response.clone(), "image/" + media);
const url = (this._id_urls[id] = await this._response_url(response.clone()));
const url = await IconManager._response_url(response.clone());
if(this._id_urls[id])
URL.revokeObjectURL(this._id_urls[id]);
this._id_urls[id] = url;
this._loading_promises[id] = undefined;
return {
@ -644,6 +719,58 @@ class IconManager {
throw "icon not found";
}
static generate_tag(icon: Promise<Icon> | Icon, options?: {
animate?: boolean
}) : JQuery<HTMLDivElement> {
options = options || {};
let icon_container = $.spawn("div").addClass("icon-container icon_empty");
let icon_load_image = $.spawn("div").addClass("icon_loading");
const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", "");
const _apply = (icon) => {
let id = icon ? (icon.id >>> 0) : 0;
if (!icon || id == 0) {
icon_load_image.remove();
icon_load_image = undefined;
return;
} else if (id < 1000) {
icon_load_image.remove();
icon_load_image = undefined;
icon_container.removeClass("icon_empty").addClass("icon_em client-group_" + id);
return;
}
icon_image.attr("src", icon.url);
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.remove();
icon_image.animate({opacity: 1}, 150);
});
} else {
icon_load_image.remove();
icon_load_image = undefined;
}
};
if(icon instanceof Promise) {
icon.then(_apply).catch(error => {
console.error(tr("Could not load icon. Reason: %s"), error);
icon_load_image.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon");
});
} else {
_apply(icon as Icon);
}
if(icon_load_image)
icon_load_image.appendTo(icon_container);
return icon_container;
}
generateTag(id: number, options?: {
animate?: boolean
}) : JQuery<HTMLDivElement> {
@ -651,44 +778,16 @@ class IconManager {
id = id >>> 0;
if(id == 0 || !id)
return $.spawn("div").addClass("icon_empty");
return IconManager.generate_tag({id: id, url: ""}, options);
else if(id < 1000)
return $.spawn("div").addClass("icon client-group_" + id);
return IconManager.generate_tag({id: id, url: ""}, options);
const icon_container = $.spawn("div").addClass("icon-container icon_empty");
const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", "");
if(this._id_urls[id]) {
icon_image.attr("src", this._id_urls[id]).appendTo(icon_container);
icon_container.removeClass("icon_empty");
return IconManager.generate_tag({id: id, url: this._id_urls[id]}, options);
} else {
const icon_load_image = $.spawn("div").addClass("icon_loading");
icon_load_image.appendTo(icon_container);
(async () => {
let icon: Icon = await this.resolve_icon(id);
icon_image.attr("src", icon.url);
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);
});
} else {
icon_load_image.detach();
}
})().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 IconManager.generate_tag(this.resolve_icon(id), options);
}
return icon_container;
}
}
@ -713,6 +812,11 @@ class AvatarManager {
AvatarManager.cache = new CacheManager("avatars");
}
destroy() {
this._cached_avatars = undefined;
this._loading_promises = undefined;
}
private async _response_url(response: Response, type: ImageType) : Promise<string> {
if(!response.headers.has('X-media-bytes'))
throw "missing media bytes";
@ -725,12 +829,12 @@ class AvatarManager {
return URL.createObjectURL(blob);
}
async resolved_cached?(client_avatar_id: string, avatar_id?: string) : Promise<Avatar> {
let avatar: Avatar = this._cached_avatars[avatar_id];
async resolved_cached?(client_avatar_id: string, avatar_version?: string) : Promise<Avatar> {
let avatar: Avatar = this._cached_avatars[avatar_version];
if(avatar) {
if(typeof(avatar_id) !== "string" || avatar.avatar_id == avatar_id)
if(typeof(avatar_version) !== "string" || avatar.avatar_id == avatar_version)
return avatar;
this._cached_avatars[avatar_id] = (avatar = undefined);
avatar = undefined;
}
if(!AvatarManager.cache.setupped())
@ -740,14 +844,14 @@ class AvatarManager {
if(!response)
return undefined;
let response_avatar_id = response.headers.has("X-avatar-id") ? response.headers.get("X-avatar-id") : undefined;
if(typeof(avatar_id) === "string" && response_avatar_id != avatar_id)
let response_avatar_version = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined;
if(typeof(avatar_version) === "string" && response_avatar_version != avatar_version)
return undefined;
const type = image_type(response.headers.get('X-media-bytes'));
return this._cached_avatars[client_avatar_id] = {
client_avatar_id: client_avatar_id,
avatar_id: avatar_id || response_avatar_id,
avatar_id: avatar_version || response_avatar_version,
url: await this._response_url(response, type),
type: type
};
@ -758,49 +862,76 @@ class AvatarManager {
return this.handle.download_file("", "/avatar_" + client_avatar_id);
}
private async _load_avatar(client_avatar_id: string, avatar_id: string) {
let download_key: transfer.DownloadKey;
private async _load_avatar(client_avatar_id: string, avatar_version: string) {
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 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 avatar download";
}
const downloader = transfer.spawn_download_transfer(download_key);
let response: Response;
try {
response = await downloader.request_file();
} catch(error) {
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);
await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, {
"X-avatar-version": avatar_version
});
const url = await this._response_url(response.clone(), type);
return this._cached_avatars[client_avatar_id] = {
client_avatar_id: client_avatar_id,
avatar_id: avatar_version,
url: url,
type: type
};
} finally {
this._loading_promises[client_avatar_id] = undefined;
}
const downloader = transfer.spawn_download_transfer(download_key);
let response: Response;
try {
response = await downloader.request_file();
} catch(error) {
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);
await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, {
"X-avatar-id": avatar_id
});
const url = await this._response_url(response.clone(), type);
this._loading_promises[client_avatar_id] = undefined;
return this._cached_avatars[client_avatar_id] = {
client_avatar_id: client_avatar_id,
avatar_id: avatar_id,
url: url,
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));
/* loads an avatar by the avatar id and optional with the avatar version */
load_avatar(client_avatar_id: string, avatar_version: string) : Promise<Avatar> {
return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_version));
}
generate_client_tag(client: ClientEntry) : JQuery {
return this.generate_tag(client.avatarId(), client.properties.client_flag_avatar);
}
update_cache(client_avatar_id: string, avatar_id: string) {
const _cached: Avatar = this._cached_avatars[client_avatar_id];
if(_cached) {
if(_cached.avatar_id === avatar_id)
return; /* cache is up2date */
console.log(tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id);
delete this._cached_avatars[client_avatar_id];
AvatarManager.cache.delete("avatar_" + client_avatar_id).catch(error => {
log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o: %o"), client_avatar_id, error);
});
} else {
this.resolved_cached(client_avatar_id).then(avatar => {
if(avatar && avatar.avatar_id !== avatar_id) {
/* this time we ensured that its cached */
this.update_cache(client_avatar_id, avatar_id);
}
}).catch(error => {
log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o (cache lookup failed): %o"), client_avatar_id, error);
});
}
}
generate_tag(client_avatar_id: string, avatar_id?: string, options?: {
callback_image?: (tag: JQuery<HTMLImageElement>) => any,
callback_avatar?: (avatar: Avatar) => any
@ -811,7 +942,9 @@ class AvatarManager {
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) {
if(avatar_id === "") {
avatar_container.append(this.generate_default_image());
} else 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)
@ -832,7 +965,7 @@ class AvatarManager {
}
if(!avatar)
avatar = await this.loadAvatar(client_avatar_id, avatar_id)
avatar = await this.load_avatar(client_avatar_id, avatar_id);
if(!avatar)
throw "failed to load avatar";
@ -844,7 +977,7 @@ class AvatarManager {
avatar_image.css("opacity", 0);
avatar_container.append(avatar_image);
loader_image.animate({opacity: 0}, 50, () => {
loader_image.detach();
loader_image.remove();
avatar_image.animate({opacity: 1}, 150, () => {
if(options.callback_image)
options.callback_image(avatar_image);
@ -859,4 +992,109 @@ class AvatarManager {
return avatar_container;
}
unique_id_2_avatar_id(unique_id: string) {
function str2ab(str) {
let buf = new ArrayBuffer(str.length); // 2 bytes for each char
let bufView = new Uint8Array(buf);
for (let i=0, strLen = str.length; i<strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
try {
let raw = atob(unique_id);
let input = hex.encode(str2ab(raw));
let result: string = "";
for(let index = 0; index < input.length; index++) {
let c = input.charAt(index);
let offset: number = 0;
if(c >= '0' && c <= '9')
offset = c.charCodeAt(0) - '0'.charCodeAt(0);
else if(c >= 'A' && c <= 'F')
offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A;
else if(c >= 'a' && c <= 'f')
offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A;
result += String.fromCharCode('a'.charCodeAt(0) + offset);
}
return result;
} catch (e) { //invalid base 64 (like music bot etc)
return undefined;
}
}
private generate_default_image() : JQuery {
return $.spawn("img").attr("src", "img/style/avatar.png").css({width: '100%', height: '100%'});
}
generate_chat_tag(client: { id?: number; database_id?: number; }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any) : JQuery {
let client_handle;
if(typeof(client.id) == "number")
client_handle = this.handle.handle.channelTree.findClient(client.id);
if(!client_handle && typeof(client.id) == "number") {
client_handle = this.handle.handle.channelTree.find_client_by_dbid(client.database_id);
}
if(client_handle && client_handle.clientUid() !== client_unique_id)
client_handle = undefined;
const container = $.spawn("div").addClass("avatar");
if(client_handle && !client_handle.properties.client_flag_avatar)
return container.append(this.generate_default_image());
const avatar_id = client_handle ? client_handle.avatarId() : this.unique_id_2_avatar_id(client_unique_id);
if(avatar_id) {
if(this._cached_avatars[avatar_id]) { /* Test if we're may able to load the client avatar sync without a loading screen */
const cache: Avatar = this._cached_avatars[avatar_id];
console.log("[AVATAR] Using cached avatar. ID: %o | Version: %o (Cached: %o)", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id);
if(!client_handle || client_handle.properties.client_flag_avatar == cache.avatar_id) {
const image = $.spawn("img").attr("src", cache.url).css({width: '100%', height: '100%'});
return container.append(image);
}
}
const image_loading = $.spawn("img").attr("src", "img/loading_image.svg").css({width: '100%', height: '100%'});
/* lets actually load the avatar */
(async () => {
let avatar: Avatar;
let loaded_image = this.generate_default_image();
console.log("[AVATAR] Resolving avatar. ID: %o | Version: %o", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
try {
//TODO: Cache if avatar load failed and try again in some minutes/may just even consider using the default avatar 'till restart
try {
avatar = await this.resolved_cached(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
} catch(error) {
console.error(tr("Failed to use cached avatar: %o"), error);
}
if(!avatar)
avatar = await this.load_avatar(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
if(!avatar)
throw "no avatar present!";
loaded_image = $.spawn("img").attr("src", avatar.url).css({width: '100%', height: '100%'});
} catch(error) {
throw error;
} finally {
container.children().remove();
container.append(loaded_image);
}
})().then(() => callback_loaded && callback_loaded(true)).catch(error => {
log.warn(LogCategory.CLIENT, tr("Failed to load chat avatar for client %s. Error: %o"), client_unique_id, error);
callback_loaded && callback_loaded(false, error);
});
image_loading.appendTo(container);
} else {
this.generate_default_image().appendTo(container);
}
return container;
}
}

View File

@ -166,7 +166,11 @@ namespace ppt {
if(key.key_windows)
result += " + " + tr("Win");
result += " + " + (key.key_code ? key.key_code : tr("unset"));
if(!result && !key.key_code)
return tr("unset");
if(key.key_code)
result += " + " + key.key_code;
return result.substr(3);
}
}

View File

@ -2,6 +2,8 @@ namespace audio {
export namespace player {
export interface Device {
device_id: string;
driver: string;
name: string;
}
}

View File

@ -9,6 +9,37 @@ namespace bookmarks {
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile();
if(profile.valid()) {
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
connection.startConnection(
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
profile,
true,
{
nickname: mark.nickname,
password: mark.server_properties.server_password_hash ? {
password: mark.server_properties.server_password_hash,
hashed: true
} : mark.server_properties.server_password ? {
hashed: false,
password: mark.server_properties.server_password
} : undefined
}
);
} else {
Modals.spawnConnectModal({}, {
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
enforce: true
}, {
profile: profile,
enforce: true
})
}
};
export interface ServerProperties {
server_address: string;
server_port: number;
@ -35,6 +66,8 @@ namespace bookmarks {
default_channel_password?: string;
connect_profile: string;
last_icon_id?: number;
}
export interface DirectoryBookmark {
@ -88,6 +121,19 @@ namespace bookmarks {
return bookmark_config().root_bookmark;
}
export function bookmarks_flat() : Bookmark[] {
const result: Bookmark[] = [];
const _flat = (bookmark: Bookmark | DirectoryBookmark) => {
if(bookmark.type == BookmarkType.DIRECTORY)
for(const book of (bookmark as DirectoryBookmark).content)
_flat(book);
else
result.push(bookmark as Bookmark);
};
_flat(bookmark_config().root_bookmark);
return result;
}
function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark {
for(const entry of parent.content) {
if(entry.unique_id == uuid)
@ -169,4 +215,27 @@ namespace bookmarks {
export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
delete_bookmark_recursive(bookmarks(), bookmark)
}
export function add_current_server() {
const ch = server_connections.active_connection_handler();
if(ch && ch.connected) {
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => true, result => {
if(result) {
const bookmark = create_bookmark(result as string, bookmarks(), {
server_port: ch.serverConnection.remote_address().port,
server_address: ch.serverConnection.remote_address().host,
server_password: "",
server_password_hash: ""
}, this.connection_handler.getClient().clientNickName());
save_bookmark(bookmark);
control_bar.update_bookmarks();
top_menu.rebuild_bookmarks();
}
}).open();
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
}
}
}

View File

@ -1,6 +1,8 @@
/// <reference path="ConnectionBase.ts" />
namespace connection {
import Conversation = chat.channel.Conversation;
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
constructor(connection: AbstractServerConnection) {
super(connection);
@ -23,6 +25,7 @@ namespace connection {
this["notifychannelhide"] = this.handleCommandChannelHide;
this["notifychannelshow"] = this.handleCommandChannelShow;
this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo;
this["notifycliententerview"] = this.handleCommandClientEnterView;
this["notifyclientleftview"] = this.handleCommandClientLeftView;
this["notifyclientmoved"] = this.handleNotifyClientMoved;
@ -45,6 +48,9 @@ namespace connection {
this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed;
this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed;
this["notifyconversationhistory"] = this.handleNotifyConversationHistory;
this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete;
}
proxy_command_promise(promise: Promise<CommandResult>, options: connection.CommandOptions) {
@ -56,20 +62,21 @@ namespace connection {
if(ex instanceof CommandResult) {
let res = ex;
if(!res.success) {
if(res.id == 2568) { //Permission error
res.message = tr("Insufficient client permissions. Failed on permission ") + this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number).name;
if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error
const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number);
res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown");
this.connection_handler.log.log(log.server.Type.ERROR_PERMISSION, {
permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number)
});
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
} else {
} else if(res.id != ErrorID.EMPTY_RESULT) {
this.connection_handler.log.log(log.server.Type.ERROR_CUSTOM, {
message: res.extra_message.length == 0 ? res.message : res.extra_message
});
}
}
} else if(typeof(ex) === "string") {
this.connection_handler.chat.serverChat().appendError(tr("Command execution results in ") + ex);
this.connection_handler.log.log(log.server.Type.CONNECTION_COMMAND_ERROR, {error: ex});
} else {
console.error(tr("Invalid promise result type: %o. Result:"), typeof (ex));
console.error(ex);
@ -131,6 +138,8 @@ namespace connection {
json = json[0]; //Only one bulk
this.connection_handler.channelTree.registerClient(this.connection_handler.getClient());
this.connection.client.side_bar.channel_conversations().reset();
this.connection.client.clientId = parseInt(json["aclid"]);
this.connection.client.getClient().updateVariables({key: "client_nickname", value: json["acn"]});
@ -146,8 +155,61 @@ namespace connection {
}
this.connection.client.channelTree.server.updateVariables(false, ...updates);
const properties = this.connection.client.channelTree.server.properties;
/* host message */
if(properties.virtualserver_hostmessage_mode > 0) {
if(properties.virtualserver_hostmessage_mode == 1) {
/* show in log */
this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE, {
message: properties.virtualserver_hostmessage
});
} else {
/* create modal/create modal and quit */
createModal({
header: tr("Host message"),
body: MessageHelper.bbcode_chat(properties.virtualserver_hostmessage),
footer: undefined
}).open();
if(properties.virtualserver_hostmessage_mode == 3) {
/* first let the client initialize his stuff */
setTimeout(() => {
this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE_DISCONNECT, {
message: properties.virtualserver_welcomemessage
});
this.connection.disconnect("host message disconnect");
this.connection_handler.handleDisconnect(DisconnectReason.SERVER_HOSTMESSAGE);
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
}, 100);
}
}
}
/* welcome message */
if(properties.virtualserver_welcomemessage) {
this.connection_handler.log.log(log.server.Type.SERVER_WELCOME_MESSAGE, {
message: properties.virtualserver_welcomemessage
});
}
/* priviledge key */
if(properties.virtualserver_ask_for_privilegekey) {
createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.<br>Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => {
if(!result) return;
const scon = server_connections.active_connection_handler();
if(scon.serverConnection.connected)
scon.serverConnection.send_command("tokenuse", {
token: result
}).then(() => {
createInfoModal(tr("Use privilege key"), tr("Privilege key successfully used!")).open();
}).catch(error => {
createErrorModal(tr("Use privilege key"), MessageHelper.formatMessage(tr("Failed to use privilege key: {}"), error instanceof CommandResult ? error.message : error)).open();
});
}, { field_placeholder: 'Enter Privilege Key' }).open();
}
this.connection_handler.chat.serverChat().name = this.connection.client.channelTree.server.properties["virtualserver_name"];
this.connection_handler.log.log(log.server.Type.CONNECTION_CONNECTED, {
own_client: this.connection_handler.getClient().log_data()
});
@ -155,6 +217,16 @@ namespace connection {
this.connection.client.onConnected();
}
handleNotifyServerConnectionInfo(json) {
json = json[0];
/* everything is a number, so lets parse it */
for(const key of Object.keys(json))
json[key] = parseInt(json[key]);
this.connection_handler.channelTree.server.set_connection_info(json);
}
private createChannelFromJson(json, ignoreOrder: boolean = false) {
let tree = this.connection.client.channelTree;
@ -223,9 +295,11 @@ namespace connection {
handleCommandChannelDelete(json) {
let tree = this.connection.client.channelTree;
const conversations = this.connection.client.side_bar.channel_conversations();
console.log(tr("Got %d channel deletions"), json.length);
for(let index = 0; index < json.length; index++) {
conversations.delete_conversation(parseInt(json[index]["cid"]));
let channel = tree.findChannel(json[index]["cid"]);
if(!channel) {
console.error(tr("Invalid channel onDelete (Unknown channel)"));
@ -237,9 +311,11 @@ namespace connection {
handleCommandChannelHide(json) {
let tree = this.connection.client.channelTree;
const conversations = this.connection.client.side_bar.channel_conversations();
console.log(tr("Got %d channel hides"), json.length);
for(let index = 0; index < json.length; index++) {
conversations.delete_conversation(parseInt(json[index]["cid"]));
let channel = tree.findChannel(json[index]["cid"]);
if(!channel) {
console.error(tr("Invalid channel on hide (Unknown channel)"));
@ -282,8 +358,6 @@ namespace connection {
client.properties.client_type = parseInt(entry["client_type"]);
client = tree.insertClient(client, channel);
} else {
if(client == this.connection.client.getClient())
this.connection_handler.chat.channelChat().name = channel.channelName();
tree.moveClient(client, channel);
}
@ -338,32 +412,25 @@ namespace connection {
client.updateVariables(...updates);
{
let client_chat = client.chat(false);
if(!client_chat) {
for(const c of this.connection_handler.chat.open_chats()) {
if(c.owner_unique_id == client.properties.client_unique_identifier && c.flag_offline) {
client_chat = c;
break;
}
}
}
if(client_chat) {
client_chat.appendMessage(
"{0}", true,
$.spawn("div")
.addClass("event-message event-partner-connect")
.text(tr("Your chat partner has reconnected"))
);
client_chat.flag_offline = false;
client.initialize_chat(client_chat);
}
if(!old_channel) {
/* client new join */
const conversation_manager = this.connection_handler.side_bar.private_conversations();
const conversation = conversation_manager.find_conversation({
unique_id: client.properties.client_unique_identifier,
client_id: client.clientId(),
name: client.clientNickName()
}, {
create: false,
attach: true
});
}
if(client instanceof LocalClientEntry) {
client.initializeListener();
this.connection_handler.update_voice_status();
this.connection_handler.chat_frame.info_frame().update_channel_talk();
this.connection_handler.side_bar.info_frame().update_channel_talk();
const conversations = this.connection.client.side_bar.channel_conversations();
conversations.set_current_channel(client.currentChannel().channelId);
}
}
}
@ -390,7 +457,7 @@ namespace connection {
this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry);
} else
this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry);
this.connection_handler.chat_frame.info_frame().update_channel_talk();
this.connection_handler.side_bar.info_frame().update_channel_talk();
return;
}
@ -436,19 +503,19 @@ namespace connection {
console.error(tr("Unknown client left reason!"));
}
{
const chat = client.chat(false);
if(chat) {
chat.flag_offline = true;
chat.onMessageSend = undefined;
chat.onClose = undefined;
chat.appendMessage(
"{0}", true,
$.spawn("div")
.addClass("event-message event-partner-disconnect")
.text(tr("Your chat partner has disconnected"))
);
}
if(!channel_to) {
/* client left the server */
const conversation_manager = this.connection_handler.side_bar.private_conversations();
const conversation = conversation_manager.find_conversation({
unique_id: client.properties.client_unique_identifier,
client_id: client.clientId(),
name: client.clientNickName()
}, {
create: false,
attach: false
});
if(conversation)
conversation.set_state(chat.PrivateConversationState.DISCONNECTED);
}
}
@ -478,7 +545,6 @@ namespace connection {
let self = client instanceof LocalClientEntry;
let current_clients: ClientEntry[];
if(self) {
this.connection_handler.chat.channelChat().name = channel_to.channelName();
current_clients = client.channelTree.clientsByChannel(client.currentChannel());
this.connection_handler.update_voice_status(channel_to);
}
@ -488,8 +554,20 @@ namespace connection {
if(entry !== client && entry.get_audio_handle())
entry.get_audio_handle().abort_replay();
if(self)
this.connection_handler.chat_frame.info_frame().update_channel_talk();
if(self) {
const side_bar = this.connection_handler.side_bar;
side_bar.info_frame().update_channel_talk();
const conversation_to = side_bar.channel_conversations().conversation(channel_to.channelId, false);
if(conversation_to)
conversation_to.update_private_state();
const conversation_from = side_bar.channel_conversations().conversation(channel_from.channelId, false);
if(conversation_from)
conversation_from.update_private_state();
side_bar.channel_conversations().update_chat_box();
}
const own_channel = this.connection.client.getClient().currentChannel();
this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_MOVE, {
@ -603,29 +681,65 @@ namespace connection {
let mode = json["targetmode"];
if(mode == 1){
let invoker = this.connection.client.channelTree.findClient(json["invokerid"]);
let target = this.connection.client.channelTree.findClient(json["target"]);
if(!invoker) { //TODO spawn chat (Client is may invisible)
console.error(tr("Got private message from invalid client!"));
//json["invokerid"], json["invokername"], json["invokeruid"]
const target_client_id = parseInt(json["target"]);
const target_own = target_client_id === this.connection.client.getClientId();
if(target_own && target_client_id === json["invokerid"]) {
console.error(tr("Received conversation message from invalid client id. Data: %o", json));
return;
}
if(!target) { //TODO spawn chat (Client is may invisible)
console.error(tr("Got private message from invalid client!"));
const conversation_manager = this.connection_handler.side_bar.private_conversations();
const conversation = conversation_manager.find_conversation({
client_id: target_own ? parseInt(json["invokerid"]) : target_client_id,
unique_id: target_own ? json["invokeruid"] : undefined,
name: target_own ? json["invokername"] : undefined
}, {
create: target_own,
attach: target_own
});
if(!conversation) {
console.error(tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message"));
return;
}
if(invoker == this.connection.client.getClient()) {
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
target.chat(true).appendMessage("{0}: {1}", true, this.connection.client.getClient().createChatTag(true), MessageHelper.bbcode_chat(json["msg"]));
} else {
conversation.append_message(json["msg"], {
type: target_own ? "partner" : "self",
name: json["invokername"],
unique_id: json["invokeruid"],
client_id: parseInt(json["invokerid"])
});
if(target_own) {
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
invoker.chat(true).appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]));
} else {
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
}
} else if(mode == 2) {
const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
const own_channel_id = this.connection.client.getClient().currentChannel().channelId;
const channel_id = typeof(json["cid"]) !== "undefined" ? parseInt(json["cid"]) : own_channel_id;
const channel = this.connection_handler.channelTree.findChannel(channel_id);
if(json["invokerid"] == this.connection.client.clientId)
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
else
else if(channel_id == own_channel_id) {
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
this.connection_handler.chat.channelChat().appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]))
}
const conversations = this.connection_handler.side_bar.channel_conversations();
const conversation = conversations.conversation(channel_id);
conversation.register_new_message({
sender_database_id: invoker ? invoker.properties.client_database_id : 0,
sender_name: json["invokername"],
sender_unique_id: json["invokeruid"],
timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]),
message: json["msg"]
});
if(conversation.is_unread())
channel.flag_text_unread = true;
} else if(mode == 3) {
this.connection_handler.log.log(log.server.Type.GLOBAL_MESSAGE, {
message: json["msg"],
@ -635,6 +749,18 @@ namespace connection {
client_id: parseInt(json["invokerid"])
}
});
const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
const conversations = this.connection_handler.side_bar.channel_conversations();
const conversation = conversations.conversation(0);
conversation.register_new_message({
sender_database_id: invoker ? invoker.properties.client_database_id : 0,
sender_name: json["invokername"],
sender_unique_id: json["invokeruid"],
timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]),
message: json["msg"]
});
}
}
@ -646,28 +772,20 @@ namespace connection {
//clid: "6"
//cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY="
const client = this.connection.client.channelTree.findClient(json["clid"]);
if(!client) {
log.warn(LogCategory.GENERAL, tr("Received chat close for unknown client"));
return;
}
if(client.properties.client_unique_identifier !== json["cluid"]) {
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but unique ids dosn't match. (expected %o, received %o)"), client.properties.client_unique_identifier, json["cluid"]);
return;
}
const chat = client.chat(false);
if(!chat) {
const conversation_manager = this.connection_handler.side_bar.private_conversations();
const conversation = conversation_manager.find_conversation({
client_id: parseInt(json["clid"]),
unique_id: json["cluid"],
name: undefined
}, {
create: false,
attach: false
});
if(!conversation) {
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open."));
return;
}
chat.flag_offline = true;
chat.appendMessage(
"{0}", true,
$.spawn("div")
.addClass("event-message event-partner-closed")
.text(tr("Your chat partner has close the conversation"))
);
conversation.set_state(chat.PrivateConversationState.CLOSED);
}
handleNotifyClientUpdated(json) {
@ -811,5 +929,38 @@ namespace connection {
this.connection.client.channelTree.deleteClient(client);
}
}
handleNotifyConversationHistory(json: any[]) {
const conversations = this.connection.client.side_bar.channel_conversations();
const conversation = conversations.conversation(parseInt(json[0]["cid"]));
if(!conversation) {
log.warn(LogCategory.NETWORKING, tr("Received conversation history for invalid or unknown conversation (%o)"), json[0]["cid"]);
return;
}
for(const entry of json) {
conversation.register_new_message({
message: entry["msg"],
sender_unique_id: entry["sender_unique_id"],
sender_name: entry["sender_name"],
timestamp: parseInt(entry["timestamp"]),
sender_database_id: parseInt(entry["sender_database_id"])
}, false);
}
conversation.fix_scroll(true);
}
handleNotifyConversationMessageDelete(json: any[]) {
let conversation: Conversation;
const conversations = this.connection.client.side_bar.channel_conversations();
for(const entry of json) {
if(typeof(entry["cid"]) !== "undefined")
conversation = conversations.conversation(parseInt(entry["cid"]), false);
if(!conversation)
continue;
conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"]));
}
}
}
}

View File

@ -12,7 +12,14 @@ namespace connection {
initialize() {
this.connection.command_handler_boss().register_handler(this);
/* notifyquerylist */
}
destroy() {
if(this.connection) {
const hboss = this.connection.command_handler_boss();
hboss && hboss.unregister_handler(this);
}
this._awaiters_unique_ids = undefined;
}
handle_command(command: connection.ServerCommand): boolean {

View File

@ -39,6 +39,11 @@ namespace connection {
abstract remote_address() : ServerAddress; /* only valid when connected */
abstract handshake_handler() : HandshakeHandler; /* only valid when connected */
abstract ping() : {
native: number,
javascript?: number
};
}
export namespace voice {
@ -128,6 +133,11 @@ namespace connection {
this.connection = connection;
}
destroy() {
this.command_handlers = undefined;
this.single_command_handler = undefined;
}
register_handler(handler: AbstractCommandHandler) {
if(!handler.volatile_handler_boss && handler.handler_boss)
throw "handler already registered";

View File

@ -4,6 +4,12 @@ enum ErrorID {
PLAYLIST_IS_IN_USE = 0x2103,
FILE_ALREADY_EXISTS = 2050,
CLIENT_INVALID_ID = 0x0200,
CONVERSATION_INVALID_ID = 0x2200,
CONVERSATION_MORE_DATA = 0x2201,
CONVERSATION_IS_PRIVATE = 0x2202
}
class CommandResult {

7
shared/js/events.ts Normal file
View File

@ -0,0 +1,7 @@
/* TODO: Use a global event bus as event distribute system */
namespace event {
namespace global {
}
}

View File

@ -104,7 +104,8 @@ namespace i18n {
file.full_url = url;
file.path = path;
//TODO validate file
//TODO: Validate file
resolve(file);
} catch(error) {
log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, error);
@ -119,10 +120,16 @@ namespace i18n {
}
export function load_file(url: string, path: string) : Promise<void> {
return load_translation_file(url, path).then(result => {
return load_translation_file(url, path).then(async result => {
/* TODO: Improve this test?!*/
try {
tr("Dummy translation test");
} catch(error) {
throw "dummy test failed";
}
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
translations = result.translations;
return Promise.resolve();
}).catch(error => {
log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error);
return Promise.reject(error);
@ -292,6 +299,7 @@ namespace i18n {
try {
await load_file(cfg.current_translation_url, cfg.current_translation_path);
} catch (error) {
console.error(tr("Failed to initialize selected translation: %o"), 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();
}
}
@ -302,4 +310,7 @@ namespace i18n {
// @ts-ignore
const tr: typeof i18n.tr = i18n.tr;
const tra: typeof i18n.tra = i18n.tra;
const tra: typeof i18n.tra = i18n.tra;
(window as any).tr = i18n.tr;
(window as any).tra = i18n.tra;

View File

@ -11,6 +11,20 @@ namespace app {
export function is_web() {
return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG;
}
let _ui_version;
export function ui_version() {
if(typeof(_ui_version) !== "string") {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
return (_ui_version = version);
}
return _ui_version;
}
}
namespace loader {
@ -150,19 +164,31 @@ namespace loader {
console.groupCollapsed("Executing loading stage %s", Stage[current_stage]);
}
}
/* cleanup */
{
_script_promises = {};
}
console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
}
type SourcePath = string | string[];
type DependSource = {
url: string;
depends: string[];
}
type SourcePath = string | DependSource | string[];
function script_name(path: string | string[]) {
function script_name(path: SourcePath) {
if(Array.isArray(path)) {
let buffer = "";
let _or = " or ";
for(let entry of path)
buffer += _or + script_name(entry);
return buffer.slice(_or.length);
} else return "<code>" + path + "</code>";
} else if(typeof(path) === "string")
return "<code>" + path + "</code>";
else
return "<code>" + path.url + "</code>";
}
class SyntaxError {
@ -173,6 +199,7 @@ namespace loader {
}
}
let _script_promises: {[key: string]: Promise<void>} = {};
export async function load_script(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_script(path[0]).catch(error => {
@ -185,45 +212,64 @@ namespace loader {
return Promise.reject(error);
});
} else {
return new Promise<void>((resolve, reject) => {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
return _script_promises[source.url] = (async () => {
/* await depends */
for(const depend of source.depends) {
if(!_script_promises[depend])
throw "Missing dependency " + depend;
await _script_promises[depend];
}
const tag: HTMLScriptElement = document.createElement("script");
let error = false;
const error_handler = (event: ErrorEvent) => {
if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
await new Promise((resolve, reject) => {
let error = false;
const error_handler = (event: ErrorEvent) => {
if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.type = "application/javascript";
tag.async = true;
tag.defer = true;
tag.onerror = error => {
cleanup();
tag.remove();
reject(error);
};
tag.onload = () => {
cleanup();
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
console.debug("Script %o loaded", path);
setTimeout(resolve, 100);
};
const timeout_handle = setTimeout(() => {
reject("timeout");
}, 5000);
tag.type = "application/javascript";
tag.async = true;
tag.defer = true;
tag.onerror = error => {
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
tag.remove();
reject(error);
};
tag.onload = () => {
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
console.debug("Script %o loaded", path);
setTimeout(resolve, 100);
};
document.getElementById("scripts").appendChild(tag);
document.getElementById("scripts").appendChild(tag);
tag.src = path + (cache_tag || "");
});
tag.src = source.url + (cache_tag || "");
});
})();
}
}
@ -257,7 +303,7 @@ namespace loader {
export async function load_style(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_script(path[0]).catch(error => {
return load_style(path[0]).catch(error => {
if(error instanceof SyntaxError)
return Promise.reject(error.source);
@ -267,6 +313,10 @@ namespace loader {
return Promise.reject(error);
});
} else {
if(!path) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link");
@ -283,21 +333,30 @@ namespace loader {
};
window.addEventListener('error', error_handler as any);
tag.type = "text/css";
tag.rel = "stylesheet";
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.type = "text/css";
tag.rel="stylesheet";
tag.onerror = error => {
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
cleanup();
tag.remove();
console.error("File load error for file %s: %o", path, error);
reject("failed to load file " + path);
};
tag.onload = () => {
cleanup();
{
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
const rules = css.cssRules;
@ -324,8 +383,6 @@ namespace loader {
css.insertRule(rule, rules_remove[0]);
}
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
console.debug("Style sheet %o loaded", path);
setTimeout(resolve, 100);
};
@ -464,6 +521,7 @@ const loader_javascript = {
if(!window.require) {
await loader.load_script(["vendor/jquery/jquery.min.js"]);
} else {
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "forum sync",
priority: 10,
@ -471,26 +529,36 @@ const loader_javascript = {
forum.sync_main();
}
});
*/
}
await loader.load_script(["vendor/DOMPurify/purify.min.js"]);
/* bootstrap material design and libs */
await loader.load_script(["vendor/popper/popper.js"]);
//await loader.load_script(["vendor/popper/popper.js"]);
//depends on popper
await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]);
//await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]);
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "materialize body",
priority: 10,
function: async () => { $(document).ready(function() { $('body').bootstrapMaterialDesign(); }); }
});
*/
await loader.load_script("vendor/jsrender/jsrender.min.js");
await loader.load_scripts([
["vendor/xbbcode/src/parser.js"],
["vendor/moment/moment.js"],
["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */
["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */
["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */
["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"]
]);
await loader.load_scripts([
["vendor/emoji-picker/src/jquery.lsxemojipicker.js"]
]);
if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) {
loader.register_task(loader.Stage.JAVASCRIPT, {
@ -547,14 +615,19 @@ const loader_javascript = {
//load the profiles
"js/profiles/ConnectionProfile.js",
"js/profiles/Identity.js",
"js/profiles/identities/teaspeak-forum.js",
//Basic UI elements
"js/ui/elements/context_divider.js",
"js/ui/elements/context_menu.js",
"js/ui/elements/modal.js",
"js/ui/elements/tab.js",
"js/ui/elements/slider.js",
"js/ui/elements/tooltip.js",
//Load UI
"js/ui/modal/ModalAbout.js",
"js/ui/modal/ModalAvatar.js",
"js/ui/modal/ModalAvatarList.js",
"js/ui/modal/ModalQuery.js",
"js/ui/modal/ModalQueryManage.js",
@ -569,13 +642,16 @@ const loader_javascript = {
"js/ui/modal/ModalBanClient.js",
"js/ui/modal/ModalIconSelect.js",
"js/ui/modal/ModalInvite.js",
"js/ui/modal/ModalIdentity.js",
"js/ui/modal/ModalBanCreate.js",
"js/ui/modal/ModalBanList.js",
"js/ui/modal/ModalYesNo.js",
"js/ui/modal/ModalPoke.js",
"js/ui/modal/ModalServerGroupDialog.js",
"js/ui/modal/ModalKeySelect.js",
"js/ui/modal/ModalGroupAssignment.js",
"js/ui/modal/permission/ModalPermissionEdit.js",
"js/ui/modal/permission/PermissionEditor.js",
{url: "js/ui/modal/permission/CanvasPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]},
{url: "js/ui/modal/permission/HTMLPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]},
"js/ui/channel.js",
"js/ui/client.js",
@ -590,6 +666,8 @@ const loader_javascript = {
"js/ui/frames/chat_frame.js",
"js/ui/frames/connection_handlers.js",
"js/ui/frames/server_log.js",
"js/ui/frames/hostbanner.js",
"js/ui/frames/MenuBar.js",
//Load permissions
"js/permission/PermissionManager.js",
@ -640,12 +718,11 @@ const loader_javascript = {
//Load codec
"js/codec/Codec.js",
"js/codec/BasicCodec.js",
"js/codec/CodecWrapperWorker.js",
{url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]},
]);
},
load_scripts_debug_client: async () => {
await loader.load_scripts([
["js/teaforo.js"]
]);
},
@ -685,15 +762,18 @@ const loader_style = {
await loader.load_styles([
"vendor/xbbcode/src/xbbcode.css"
]);
await loader.load_styles([
"vendor/emoji-picker/src/jquery.lsxemojipicker.css"
]);
await loader.load_styles([
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
]);
if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) {
await loader_style.load_style_debug();
} else {
await loader_style.load_style_release();
}
/* the material design */
await loader.load_style("css/theme/bootstrap-material-design.css");
},
load_style_debug: async () => {
@ -706,9 +786,12 @@ const loader_style = {
"css/static/ts/tab.css",
"css/static/ts/chat.css",
"css/static/ts/icons.css",
"css/static/ts/icons_em.css",
"css/static/ts/country.css",
"css/static/general.css",
"css/static/modal.css",
"css/static/modals.css",
"css/static/modal-about.css",
"css/static/modal-avatar.css",
"css/static/modal-icons.css",
"css/static/modal-bookmarks.css",
@ -722,7 +805,9 @@ const loader_style = {
"css/static/modal-settings.css",
"css/static/modal-poke.css",
"css/static/modal-server.css",
"css/static/modal-keyselect.css",
"css/static/modal-permissions.css",
"css/static/modal-group-assignment.css",
"css/static/music/info_plate.css",
"css/static/frame/SelectInfo.css",
"css/static/control_bar.css",
@ -730,7 +815,9 @@ const loader_style = {
"css/static/frame-chat.css",
"css/static/connection_handlers.css",
"css/static/server-log.css",
"css/static/htmltags.css"
"css/static/htmltags.css",
"css/static/hostbanner.css",
"css/static/menu-bar.css"
]);
},
@ -740,7 +827,7 @@ const loader_style = {
"css/static/main.css",
]);
}
}
};
async function load_templates() {
try {
@ -773,13 +860,9 @@ async function load_templates() {
/* test if all files shall be load from cache or fetch again */
async function check_updates() {
const app_version = (() => {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = app.ui_version();
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
if(version == "unknown" || version.replace(/0+/, "").length == 0)
if(!version || version == "unknown" || version.replace(/0+/, "").length == 0)
return undefined;
return version;
@ -960,6 +1043,11 @@ loader.register_task(loader.Stage.LOADED, {
},
priority: 20
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "lsx emoji picker setup",
function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}),
priority: 10
});
window["Module"] = window["Module"] || {};

View File

@ -14,10 +14,15 @@ let settings: Settings;
const js_render = window.jsrender || $;
const native_client = window.require !== undefined;
function getUserMediaFunction() : (constraints: MediaStreamConstraints, success: (stream: MediaStream) => any, fail: (error: any) => any) => any {
if((navigator as any).mediaDevices && (navigator as any).mediaDevices.getUserMedia)
return (settings, success, fail) => { (navigator as any).mediaDevices.getUserMedia(settings).then(success).catch(fail); };
return (navigator as any).getUserMedia || (navigator as any).webkitGetUserMedia || (navigator as any).mozGetUserMedia;
function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise<MediaStream> {
if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices)
return constraints => navigator.mediaDevices.getUserMedia(constraints);
const _callbacked_function = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
if(!_callbacked_function)
return undefined;
return constraints => new Promise<MediaStream>((resolve, reject) => _callbacked_function(constraints, resolve, reject));
}
interface Window {
@ -36,18 +41,41 @@ function setup_close() {
if(!native_client) {
event.returnValue = "Are you really sure?<br>You're still connected!";
} else {
const do_exit = () => {
const dp = server_connections.server_connection_handlers().map(e => {
if(e.serverConnection.connected())
return e.serverConnection.disconnect(tr("client closed"));
return Promise.resolve();
}).map(e => e.catch(error => {
console.warn(tr("Failed to disconnect from server on client close: %o"), e);
}));
const exit = () => {
const {remote} = require('electron');
remote.getCurrentWindow().close();
};
Promise.all(dp).then(exit);
/* force exit after 2500ms */
setTimeout(exit, 2500);
};
if(window.open_connected_question) {
event.preventDefault();
event.returnValue = "question";
window.open_connected_question().then(result => {
if(result) {
window.onbeforeunload = undefined;
/* prevent quitting because we try to disconnect */
window.onbeforeunload = e => e.preventDefault();
const {remote} = require('electron');
remote.getCurrentWindow().close();
/* allow a force quit after 5 seconds */
setTimeout(() => window.onbeforeunload, 5000);
do_exit();
}
});
} else { /* we're in debugging mode */ }
} else {
/* we're in debugging mode */
do_exit();
}
}
}
};
@ -102,7 +130,6 @@ async function initialize() {
bipc.setup();
}
async function initialize_app() {
const display_load_error = message => {
if(typeof(display_critical_load) !== "undefined")
@ -112,7 +139,10 @@ async function initialize_app() {
};
try { //Initialize main template
const main = $("#tmpl_main").renderTag().dividerfy();
const main = $("#tmpl_main").renderTag({
multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
app_version: app.ui_version()
}).dividerfy();
$("body").append(main);
} catch(error) {
@ -126,7 +156,7 @@ async function initialize_app() {
if(!audio.player.initialize())
console.warn(tr("Failed to initialize audio controller!"));
if(audio.player.set_master_volume)
audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER, 1) / 100);
audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100);
else
console.warn("Client does not support audio.player.set_master_volume()... May client is too old?");
if(audio.recorder.device_refresh_available())
@ -138,7 +168,7 @@ async function initialize_app() {
sound.initialize().then(() => {
console.log(tr("Sounds initialitzed"));
});
sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS, 1) / 100);
sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100);
await profiles.load();
@ -153,10 +183,6 @@ async function initialize_app() {
setup_close();
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
function str2ab8(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
@ -177,68 +203,58 @@ function arrayBufferBase64(base64: string) {
return buf;
}
function base64ArrayBuffer(arrayBuffer) {
var base64 = ''
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
function base64_encode_ab(source: ArrayBufferLike) {
const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let base64 = "";
var bytes = new Uint8Array(arrayBuffer)
var byteLength = bytes.byteLength
var byteRemainder = byteLength % 3
var mainLength = byteLength - byteRemainder
const bytes = new Uint8Array(source);
const byte_length = bytes.byteLength;
const byte_reminder = byte_length % 3;
const main_length = byte_length - byte_reminder;
var a, b, c, d
var chunk
let a, b, c, d;
let chunk;
// Main loop deals with bytes in chunks of 3
for (var i = 0; i < mainLength; i = i + 3) {
for (let i = 0; i < main_length; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6
d = chunk & 63 // 63 = 2^6 - 1
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = (chunk & 63) >> 0; // 63 = (2^6 - 1) << 0
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength]
if (byte_reminder == 1) {
chunk = bytes[main_length];
a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4 // 3 = 2^2 - 1
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + '=='
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
base64 += encodings[a] + encodings[b] + '==';
} else if (byte_reminder == 2) {
chunk = (bytes[main_length] << 8) | bytes[main_length + 1];
a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2 // 15 = 2^4 - 1
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + '='
base64 += encodings[a] + encodings[b] + encodings[c] + '=';
}
return base64
}
function Base64EncodeUrl(str){
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
}
function Base64DecodeUrl(str: string, pad?: boolean){
if(typeof(pad) === 'undefined' || pad)
str = (str + '===').slice(0, str.length + (str.length % 4));
return str.replace(/-/g, '+').replace(/_/g, '/');
}
/*
class TestProxy extends bipc.MethodProxy {
constructor(params: bipc.MethodProxyConnectParameters) {
@ -282,7 +298,6 @@ interface Window {
}
*/
function main() {
/*
window.proxy_instance = new TestProxy({
@ -299,6 +314,23 @@ function main() {
*/
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
/* initialize font */
{
const font = settings.static_global(Settings.KEY_FONT_SIZE, parseInt(getComputedStyle(document.body).fontSize));
$(document.body).css("font-size", font + "px");
}
/* context menu prevent */
$(document).on('contextmenu', event => {
if(event.isDefaultPrevented())
return;
if(!settings.static_global(Settings.KEY_DISABLE_GLOBAL_CONTEXT_MENU))
event.preventDefault();
});
top_menu.initialize();
server_connections = new ServerConnectionManager($("#connection-handlers"));
control_bar.initialise(); /* before connection handler to allow property apply */
@ -306,7 +338,7 @@ function main() {
initial_handler.acquire_recorder(default_recorder, false);
control_bar.set_connection_handler(initial_handler);
/** Setup the XF forum identity **/
profiles.identities.setup_forum();
profiles.identities.update_forum();
let _resize_timeout: NodeJS.Timer;
$(window).on('resize', event => {
@ -334,11 +366,6 @@ function main() {
console.log("Received user count update: %o", status);
});
/*
setTimeout(() => {
Modals.spawnAvatarList(globalClient);
}, 1000);
*/
(<any>window).test_upload = (message?: string) => {
message = message || "Hello World";
@ -366,16 +393,6 @@ function main() {
};
server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]);
const convs = server_connections.active_connection_handler().chat_frame.private_conversations();
let conv = convs.create_conversation("xxxx0", "WolverinDEV");
conv = convs.create_conversation("xxxx1", "Darkatzu");
conv = convs.create_conversation("xxxx2", "ZameXxX");
conv.set_unread_flag(true);
conv = convs.create_conversation("xxxx3", "Vagur");
//for(let i = 0; i < 100; i++)
// convs.create_conversation('xx' + i, "WolverinDEV #" + i);
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
@ -389,7 +406,7 @@ function main() {
if(profile && profile.valid()) {
const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler();
connection.startConnection(address, profile, {
connection.startConnection(address, profile, true, {
nickname: username,
password: password.length > 0 ? {
password: password,
@ -397,7 +414,7 @@ function main() {
} : undefined
});
} else {
Modals.spawnConnectModal({
Modals.spawnConnectModal({},{
url: address,
enforce: true
}, {
@ -406,6 +423,18 @@ function main() {
});
}
}
setTimeout(() => {
const connection = server_connections.active_connection_handler();
/*
Modals.createChannelModal(connection, undefined, undefined, connection.permissions, (cb, perms) => {
});
*/
//Modals.createServerModal(connection.channelTree.server, properties => Promise.resolve());
}, 1000);
//Modals.spawnSettingsModal("audio-sounds");
//Modals.spawnKeySelect(console.log);
}
const task_teaweb_starter: loader.Task = {

View File

@ -55,7 +55,11 @@ class Group {
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
client.updateGroupIcon(this);
});
}
} else if(key == "sortid")
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
client.update_group_icon_order();
});
}
}
@ -73,6 +77,12 @@ class GroupManager extends connection.AbstractCommandHandler {
this.handle = client;
}
destroy() {
this.handle.serverConnection && this.handle.serverConnection.command_handler_boss().unregister_handler(this);
this.serverGroups = undefined;
this.channelGroups = undefined;
}
handle_command(command: connection.ServerCommand): boolean {
switch (command.command) {
case "notifyservergrouplist":
@ -94,6 +104,11 @@ class GroupManager extends connection.AbstractCommandHandler {
static sorter() : (a: Group, b: Group) => number {
return (a, b) => {
if(!a)
return b ? 1 : 0;
if(!b)
return a ? -1 : 0;
if(a.properties.sortid > b.properties.sortid)
return 1;
if(a.properties.sortid < b.properties.sortid)

View File

@ -396,8 +396,6 @@ class PermissionValue {
}
class NeededPermissionValue extends PermissionValue {
changeListener: ((newValue: number) => void)[] = [];
constructor(type, value) {
super(type, value);
}
@ -424,6 +422,8 @@ class PermissionManager extends connection.AbstractCommandHandler {
permissionGroups: PermissionGroup[] = [];
neededPermissions: NeededPermissionValue[] = [];
needed_permission_change_listener: {[permission: string]:(() => any)[]} = {};
requests_channel_permissions: ChannelPermissionRequest[] = [];
requests_client_permissions: TeaPermissionRequest[] = [];
requests_client_channel_permissions: TeaPermissionRequest[] = [];
@ -515,6 +515,24 @@ class PermissionManager extends connection.AbstractCommandHandler {
this.handle = client;
}
destroy() {
this.handle.serverConnection && this.handle.serverConnection.command_handler_boss().unregister_handler(this);
this.needed_permission_change_listener = {};
this.permissionList = undefined;
this.permissionGroups = undefined;
this.neededPermissions = undefined;
this.requests_channel_permissions = undefined;
this.requests_client_permissions = undefined;
this.requests_client_channel_permissions = undefined;
this.requests_playlist_permissions = undefined;
this.initializedListener = undefined;
this._cacheNeededPermissions = undefined;
}
handle_command(command: connection.ServerCommand): boolean {
switch (command.command) {
case "notifyclientneededpermissions":
@ -631,8 +649,8 @@ class PermissionManager extends connection.AbstractCommandHandler {
if(entry.value == parseInt(e["permvalue"])) continue;
entry.value = parseInt(e["permvalue"]);
for(let listener of entry.changeListener)
listener(entry.value);
for(const listener of this.needed_permission_change_listener[entry.type.name] || [])
listener();
table_entries.push({
"permission": entry.type.name,
@ -643,15 +661,28 @@ class PermissionManager extends connection.AbstractCommandHandler {
log.table("Needed client permissions", table_entries);
group.end();
//TODO tr
log.debug(LogCategory.PERMISSIONS, "Dropping " + copy.length + " needed permissions and added " + addcount + " permissions.");
log.debug(LogCategory.PERMISSIONS, tr("Dropping %o needed permissions and added %o permissions."), copy.length, addcount);
for(let e of copy) {
e.value = -2;
for(let listener of e.changeListener)
listener(e.value);
for(const listener of this.needed_permission_change_listener[e.type.name] || [])
listener();
}
}
register_needed_permission(key: PermissionType, listener: () => any) {
const array = this.needed_permission_change_listener[key] || [];
array.push(listener);
this.needed_permission_change_listener[key] = array;
}
unregister_needed_permission(key: PermissionType, listener: () => any) {
const array = this.needed_permission_change_listener[key];
if(!array) return;
array.remove(listener);
this.needed_permission_change_listener[key] = array.length > 0 ? array : undefined;
}
private onChannelPermList(json) {
let channelId: number = parseInt(json[0]["cid"]);
@ -780,7 +811,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
return request.promise;
}
neededPermission(key: number | string | PermissionType | PermissionInfo) : PermissionValue {
neededPermission(key: number | string | PermissionType | PermissionInfo) : NeededPermissionValue {
for(let perm of this.neededPermissions)
if(perm.type.id == key || perm.type.name == key || perm.type == key)
return perm;

View File

@ -66,7 +66,7 @@ namespace profiles {
const identity = this.selected_identity();
if(!identity || !identity.valid()) return false;
return this.default_username !== undefined;
return true;
}
}

View File

@ -60,7 +60,7 @@ namespace profiles.identities {
}
valid(): boolean {
return this._name != undefined && this._name.length >= 3;
return this._name != undefined && this._name.length >= 5;
}
decode(data) : Promise<void> {

View File

@ -17,7 +17,7 @@ namespace profiles.identities {
this.connection.send_command("handshakebegin", {
intention: 0,
authentication_method: this.identity.type(),
data: this.identity.data_json()
data: this.identity.data().data_json()
}).catch(error => {
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
@ -30,7 +30,7 @@ namespace profiles.identities {
private handle_proof(json) {
this.connection.send_command("handshakeindentityproof", {
proof: this.identity.data_sign()
proof: this.identity.data().data_sign()
}).catch(error => {
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
@ -52,73 +52,50 @@ namespace profiles.identities {
}
export class TeaForumIdentity implements Identity {
private identity_data: string;
private identity_data_raw: string;
private identity_data_sign: string;
private readonly identity_data: forum.Data;
valid() : boolean {
return this.identity_data_raw.length > 0 && this.identity_data_raw.length > 0 && this.identity_data_sign.length > 0;
return !!this.identity_data && !this.identity_data.is_expired();
}
constructor(data: string, sign: string) {
this.identity_data_raw = data;
this.identity_data_sign = sign;
try {
this.identity_data = data ? JSON.parse(this.identity_data_raw) : undefined;
} catch(error) { }
constructor(data: forum.Data) {
this.identity_data = data;
}
data_json() : string { return this.identity_data_raw; }
data_sign() : string { return this.identity_data_sign; }
name() : string { return this.identity_data["user_name"]; }
uid() : string { return "TeaForo#" + this.identity_data["user_id"]; }
type() : IdentitifyType { return IdentitifyType.TEAFORO; }
forum_user_id() { return this.identity_data["user_id"]; }
forum_user_group() { return this.identity_data["user_group_id"]; }
is_stuff() : boolean { return this.identity_data["is_staff"]; }
is_premium() : boolean { return (<number[]>this.identity_data["user_groups"]).indexOf(5) != -1; }
data_age() : Date { return new Date(this.identity_data["data_age"]); }
/*
$user_data["user_id"] = $user->user_id;
$user_data["user_name"] = $user->username;
$user_data["user_group"] = $user->user_group_id;
$user_data["user_groups"] = $user->secondary_group_ids;
$user_data["trophy_points"] = $user->trophy_points;
$user_data["register_date"] = $user->register_date;
$user_data["is_staff"] = $user->is_staff;
$user_data["is_admin"] = $user->is_admin;
$user_data["is_super_admin"] = $user->is_super_admin;
$user_data["is_banned"] = $user->is_banned;
$user_data["data_age"] = milliseconds();
*/
data() : forum.Data {
return this.identity_data;
}
decode(data) : Promise<void> {
data = JSON.parse(data);
if(data.version !== 1)
throw "invalid version";
this.identity_data_raw = data["identity_data"];
this.identity_data_sign = data["identity_sign"];
this.identity_data = JSON.parse(this.identity_data);
return;
}
encode?() : string {
encode() : string {
return JSON.stringify({
version: 1,
identity_data: this.identity_data_raw,
identity_sign: this.identity_data_sign
version: 1
});
}
spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler {
return new TeaForumHandshakeHandler(connection, this);
}
name(): string {
return (this.identity_data ? this.identity_data.name() : "Another TeaSpeak user");
}
type(): profiles.identities.IdentitifyType {
return IdentitifyType.TEAFORO;
}
uid(): string {
//FIXME: Real UID!
return "TeaForo#" + ((this.identity_data ? this.identity_data.name() : "Another TeaSpeak user"));
}
}
let static_identity: TeaForumIdentity;
@ -127,12 +104,10 @@ namespace profiles.identities {
static_identity = identity;
}
export function setup_forum() {
const user_data = settings.static("forum_user_data") as string;
const user_sign = settings.static("forum_user_sign") as string;
if(user_data && user_sign)
static_identity = new TeaForumIdentity(user_data, user_sign);
export function update_forum() {
if(forum.logged_in() && (!static_identity || static_identity.data() !== forum.data())) {
static_identity = new TeaForumIdentity(forum.data());
}
}
export function valid_static_forum_identity() : boolean {

View File

@ -2,6 +2,20 @@
namespace profiles.identities {
export namespace CryptoHelper {
export function base64_url_encode(str){
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
}
export function base64_url_decode(str: string, pad?: boolean){
if(typeof(pad) === 'undefined' || pad)
str = (str + '===').slice(0, str.length + (str.length % 4));
return str.replace(/-/g, '+').replace(/_/g, '/');
}
export function arraybuffer_to_string(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) {
/*
Tomcrypt public key export:
@ -50,7 +64,7 @@ namespace profiles.identities {
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(Base64DecodeUrl(key_data.x, false));
const raw = atob(base64_url_decode(key_data.x, false));
if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
@ -68,7 +82,7 @@ namespace profiles.identities {
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(Base64DecodeUrl(key_data.y, false));
const raw = atob(base64_url_decode(key_data.y, false));
if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
@ -87,7 +101,7 @@ namespace profiles.identities {
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(Base64DecodeUrl(key_data.d, false));
const raw = atob(base64_url_decode(key_data.d, false));
if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
@ -104,7 +118,7 @@ namespace profiles.identities {
buffer[1] = index - 2; /* set the final sequence length */
return base64ArrayBuffer(buffer.buffer.slice(0, index));
return base64_encode_ab(buffer.buffer.slice(0, index));
}
const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e";
@ -125,7 +139,7 @@ namespace profiles.identities {
for(let i = 0; i < length; i++)
buffer[i] ^= crypt_key.charCodeAt(i);
return ab2str(buffer);
return arraybuffer_to_string(buffer);
}
export async function encrypt_ts_identity(buffer: Uint8Array) : Promise<string> {
@ -137,7 +151,7 @@ namespace profiles.identities {
for(let i = 0; i < 20; i++)
buffer[i] ^= hash[i];
return base64ArrayBuffer(buffer);
return base64_encode_ab(buffer);
}
/**
@ -185,9 +199,9 @@ namespace profiles.identities {
*/
return {
crv: "P-256",
d: Base64EncodeUrl(btoa(k)),
x: Base64EncodeUrl(btoa(x)),
y: Base64EncodeUrl(btoa(y)),
d: base64_url_encode(btoa(k)),
x: base64_url_encode(btoa(x)),
y: base64_url_encode(btoa(y)),
ext: true,
key_ops:["deriveKey", "sign"],
@ -587,7 +601,7 @@ namespace profiles.identities {
if(carry)
char_result.push(49);
return String.fromCharCode.apply(null, char_result.reverse());
return String.fromCharCode.apply(null, char_result.slice().reverse());
}
@ -774,7 +788,7 @@ namespace profiles.identities {
try {
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key));
this._unique_id = base64_encode_ab(await sha.sha1(this.public_key));
} catch(error) {
log.error(LogCategory.IDENTITIES, error);
throw "failed to calculate unique id";
@ -840,7 +854,7 @@ namespace profiles.identities {
}
buffer[1] = index - 2;
return base64ArrayBuffer(buffer.subarray(0, index));
return base64_encode_ab(buffer.subarray(0, index));
}
spawn_identity_handshake_handler(connection: connection.AbstractServerConnection): connection.HandshakeIdentityHandler {

View File

@ -0,0 +1,366 @@
interface Window {
grecaptcha: GReCaptcha;
}
interface GReCaptcha {
render(container: string | HTMLElement, parameters: {
sitekey: string;
theme?: "dark" | "light";
size?: "compact" | "normal";
tabindex?: number;
callback?: (token: string) => any;
"expired-callback"?: () => any;
"error-callback"?: (error: any) => any;
}) : string; /* widget_id */
reset(widget_id?: string);
}
namespace forum {
export namespace gcaptcha {
export async function initialize() {
if(typeof(window.grecaptcha) === "undefined") {
let script = document.createElement("script");
script.async = true;
let timeout;
const callback_name = "captcha_callback_" + Math.random().toString().replace(".", "");
try {
await new Promise((resolve, reject) => {
script.onerror = reject;
window[callback_name] = resolve;
script.src = "https://www.google.com/recaptcha/api.js?onload=" + encodeURIComponent(callback_name) + "&render=explicit";
document.body.append(script);
timeout = setTimeout(() => reject("timeout"), 15000);
});
} catch(error) {
script.remove();
script = undefined;
console.error(tr("Failed to fetch recaptcha javascript source: %o"), error);
throw tr("failed to download source");
} finally {
if(script)
script.onerror = undefined;
delete window[callback_name];
clearTimeout(timeout);
}
}
if(typeof(window.grecaptcha) === "undefined")
throw tr("failed to load recaptcha");
}
export async function spawn(container: JQuery, key: string, callback_data: (token: string) => any) {
try {
await initialize();
} catch(error) {
console.error(tr("Failed to initialize G-Recaptcha. Error: %o"), error);
throw tr("initialisation failed");
}
if(container.attr("captcha-uuid"))
window.grecaptcha.reset(container.attr("captcha-uuid"));
else {
container.attr("captcha-uuid", window.grecaptcha.render(container[0], {
"sitekey": key,
callback: callback_data
}));
}
}
}
function api_url() {
return settings.static_global(Settings.KEY_TEAFORO_URL);
}
export class Data {
readonly auth_key: string;
readonly raw: string;
readonly sign: string;
parsed: {
user_id: number;
user_name: string;
data_age: number;
user_group_id: number;
is_staff: boolean;
user_groups: number[];
};
constructor(auth: string, raw: string, sign: string) {
this.auth_key = auth;
this.raw = raw;
this.sign = sign;
this.parsed = JSON.parse(raw);
}
data_json() : string { return this.raw; }
data_sign() : string { return this.sign; }
name() : string { return this.parsed.user_name; }
user_id() { return this.parsed.user_id; }
user_group() { return this.parsed.user_group_id; }
is_stuff() : boolean { return this.parsed.is_staff; }
is_premium() : boolean { return this.parsed.user_groups.indexOf(5) != -1; }
data_age() : Date { return new Date(this.parsed.data_age); }
is_expired() : boolean { return this.parsed.data_age + 48 * 60 * 60 * 1000 < Date.now(); }
should_renew() : boolean { return this.parsed.data_age + 24 * 60 * 60 * 1000 < Date.now(); } /* renew data all 24hrs */
}
let _data: Data | undefined;
export function logged_in() : boolean {
return !!_data && !_data.is_expired();
}
export function data() : Data { return _data; }
export interface LoginResult {
status: "success" | "captcha" | "error";
error_message?: string;
captcha?: {
type: "gre-captcha" | "unknown";
data: any; /* in case of gre-captcha it would be the side key */
};
}
export async function login(username: string, password: string, captcha?: any) : Promise<LoginResult> {
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/login",
type: "POST",
cache: false,
data: {
username: username,
password: password,
remember: true,
"g-recaptcha-response": captcha
},
crossDomain: true,
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Login request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
return {
status: "error",
error_message: tr("failed to send login request")
};
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
return {
status: "error",
error_message: (response["errors"] || [])[0] || tr("Unknown error")
};
}
if(!response["success"]) {
console.error(tr("Login failed. Response %o"), response);
let message = tr("failed to login");
let captcha;
/* user/password wrong | and maybe captcha required */
if(response["code"] == 1 || response["code"] == 3)
message = tr("Invalid username or password");
if(response["code"] == 2 || response["code"] == 3) {
captcha = {
type: response["captcha"]["type"],
data: response["captcha"]["siteKey"] //TODO: Why so static here?
};
if(response["code"] == 2)
message = tr("captcha required");
}
return {
status: typeof(captcha) !== "undefined" ? "captcha" : "error",
error_message: message,
captcha: captcha
};
}
//document.cookie = "user_data=" + response["data"] + ";path=/";
//document.cookie = "user_sign=" + response["sign"] + ";path=/";
try {
_data = new Data(response["auth-key"], response["data"], response["sign"]);
localStorage.setItem("teaspeak-forum-data", response["data"]);
localStorage.setItem("teaspeak-forum-sign", response["sign"]);
localStorage.setItem("teaspeak-forum-auth", response["auth-key"]);
profiles.identities.update_forum();
} catch(error) {
console.error(tr("Failed to parse forum given data: %o"), error);
return {
status: "error",
error_message: tr("Failed to parse response data")
}
}
return {
status: "success"
};
}
export async function renew_data() : Promise<"success" | "login-required"> {
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/renew-data",
type: "GET",
cache: false,
crossDomain: true,
data: {
"auth-key": _data.auth_key
},
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Renew request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
throw tr("failed to send renew request");
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
throw (response["errors"] || [])[0] || tr("Unknown error");
}
if(!response["success"]) {
if(response["code"] == 1) {
return "login-required";
}
throw "invalid error code (" + response["code"] + ")";
}
if(!response["data"] || !response["sign"])
throw tr("response missing data");
console.debug(tr("Renew succeeded. Parsing data."));
try {
_data = new Data(_data.auth_key, response["data"], response["sign"]);
localStorage.setItem("teaspeak-forum-data", response["data"]);
localStorage.setItem("teaspeak-forum-sign", response["sign"]);
profiles.identities.update_forum();
} catch(error) {
console.error(tr("Failed to parse forum given data: %o"), error);
throw tr("failed to parse data");
}
return "success";
}
export async function logout() : Promise<void> {
if(!logged_in())
return;
let response;
try {
response = await new Promise<any>((resolve, reject) => {
$.ajax({
url: api_url() + "?web-api/v1/logout",
type: "GET",
cache: false,
crossDomain: true,
data: {
"auth-key": _data.auth_key
},
success: resolve,
error: (xhr, status, error) => {
console.log(tr("Logout request failed %o: %o"), status, error);
reject(tr("request failed"));
}
})
});
} catch(error) {
throw tr("failed to send logout request");
}
if(response["status"] !== "ok") {
console.error(tr("Response status not okey. Error happend: %o"), response);
throw (response["errors"] || [])[0] || tr("Unknown error");
}
if(!response["success"]) {
/* code 1 means not logged in, its an success */
if(response["code"] != 1) {
throw "invalid error code (" + response["code"] + ")";
}
}
_data = undefined;
localStorage.removeItem("teaspeak-forum-data");
localStorage.removeItem("teaspeak-forum-sign");
localStorage.removeItem("teaspeak-forum-auth");
profiles.identities.update_forum();
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "TeaForo initialize",
priority: 10,
function: async () => {
const raw_data = localStorage.getItem("teaspeak-forum-data");
const raw_sign = localStorage.getItem("teaspeak-forum-sign");
const forum_auth = localStorage.getItem("teaspeak-forum-auth");
if(!raw_data || !raw_sign || !forum_auth) {
console.log(tr("No TeaForo authentification found. TeaForo connection status: unconnected"));
return;
}
try {
_data = new Data(forum_auth, raw_data, raw_sign);
} catch(error) {
console.error(tr("Failed to initialize TeaForo connection from local data. Error: %o"), error);
return;
}
if(_data.should_renew()) {
console.info(tr("TeaForo data should be renewed. Executing renew."));
renew_data().then(status => {
if(status === "success") {
console.info(tr("TeaForo data has been successfully renewed."));
} else {
console.warn(tr("Failed to renew TeaForo data. New login required."));
localStorage.removeItem("teaspeak-forum-data");
localStorage.removeItem("teaspeak-forum-sign");
localStorage.removeItem("teaspeak-forum-auth");
}
}).catch(error => {
console.warn(tr("Failed to renew TeaForo data. An error occurred: %o"), error);
});
return;
}
if(_data && _data.is_expired()) {
console.error(tr("TeaForo data is expired. TeaForo connection isn't available!"));
}
}
})
}

View File

@ -24,6 +24,9 @@ interface JQuery<TElement = HTMLElement> {
alert() : JQuery<TElement>;
modal(properties: any) : this;
bootstrapMaterialDesign() : this;
/* first element which matches the selector, could be the element itself or a parent */
firstParent(selector: string) : JQuery;
}
interface JQueryStatic<TElement extends Node = HTMLElement> {
@ -184,6 +187,12 @@ if(typeof ($) !== "undefined") {
this.attr("style", original_style || "");
return result;
}
if(!$.fn.firstParent)
$.fn.firstParent = function (this: JQuery<HTMLElement>, selector: string) {
if(this.is(selector))
return this;
return this.parent(selector);
}
}
if (!String.prototype.format) {
@ -247,8 +256,39 @@ function calculate_width(text: string) : number {
return size;
}
interface Twemoji {
parse(message: string) : string;
}
declare let twemoji: Twemoji;
interface HighlightJS {
listLanguages() : string[];
getLanguage(name: string) : any | undefined;
highlight(language: string, text: string, ignore_illegals?: boolean) : HighlightJSResult;
highlightAuto(text: string) : HighlightJSResult;
}
interface HighlightJSResult {
language: string;
relevance: number;
value: string;
second_best?: any;
}
interface DOMPurify {
sanitize(html: string, config?: {
ADD_ATTR?: string[]
}) : string;
}
declare let DOMPurify: DOMPurify;
declare let remarkable: typeof window.remarkable;
declare class webkitAudioContext extends AudioContext {}
declare class webkitOfflineAudioContext extends OfflineAudioContext {}
interface Window {
readonly webkitAudioContext: typeof webkitAudioContext;
readonly AudioContext: typeof webkitAudioContext;
@ -258,6 +298,10 @@ interface Window {
readonly Pointer_stringify: any;
readonly jsrender: any;
twemoji: Twemoji;
hljs: HighlightJS;
remarkable: any;
require(id: string): any;
}

View File

@ -18,6 +18,8 @@ interface SettingsKey<T> {
fallback_imports?: {[key: string]:(value: string) => T};
description?: string;
default_value?: T;
require_restart?: boolean;
}
class SettingsBase {
@ -144,6 +146,13 @@ class Settings extends StaticSettings {
key: 'disableContextMenu',
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier'
};
static readonly KEY_DISABLE_GLOBAL_CONTEXT_MENU: SettingsKey<boolean> = {
key: 'disableGlobalContextMenu',
description: 'Disable the general context menu prevention',
default_value: false
};
static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey<boolean> = {
key: 'disableUnloadDialog',
description: 'Disables the unload popup on side closing'
@ -154,6 +163,8 @@ class Settings extends StaticSettings {
};
static readonly KEY_DISABLE_MULTI_SESSION: SettingsKey<boolean> = {
key: 'disableMultiSession',
default_value: false,
require_restart: true
};
static readonly KEY_LOAD_DUMMY_ERROR: SettingsKey<boolean> = {
@ -194,6 +205,9 @@ class Settings extends StaticSettings {
static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey<boolean> = {
key: 'connect_password_hashed'
};
static readonly KEY_CONNECT_HISTORY: SettingsKey<string> = {
key: 'connect_history'
};
static readonly KEY_CERTIFICATE_CALLBACK: SettingsKey<string> = {
key: 'certificate_callback'
@ -201,11 +215,82 @@ class Settings extends StaticSettings {
/* sounds */
static readonly KEY_SOUND_MASTER: SettingsKey<number> = {
key: 'audio_master_volume'
key: 'audio_master_volume',
default_value: 100
};
static readonly KEY_SOUND_MASTER_SOUNDS: SettingsKey<number> = {
key: 'audio_master_volume_sounds'
key: 'audio_master_volume_sounds',
default_value: 100
};
static readonly KEY_CHAT_FIXED_TIMESTAMPS: SettingsKey<boolean> = {
key: 'chat_fixed_timestamps',
default_value: false,
description: 'Enables fixed timestamps for chat messages and disabled the updating once (2 seconds ago... etc)'
};
static readonly KEY_CHAT_COLLOQUIAL_TIMESTAMPS: SettingsKey<boolean> = {
key: 'chat_colloquial_timestamps',
default_value: true,
description: 'Enabled colloquial timestamp formatting like "Yesterday at ..." or "Today at ..."'
};
static readonly KEY_CHAT_COLORED_EMOJIES: SettingsKey<boolean> = {
key: 'chat_colored_emojies',
default_value: true,
description: 'Enables colored emojies powered by Twemoji'
};
static readonly KEY_CHAT_TAG_URLS: SettingsKey<boolean> = {
key: 'chat_tag_urls',
default_value: true,
description: 'Automatically link urls with [url]'
};
static readonly KEY_CHAT_ENABLE_MARKDOWN: SettingsKey<boolean> = {
key: 'chat_enable_markdown',
default_value: true,
description: 'Enabled markdown chat support.'
};
static readonly KEY_CHAT_ENABLE_BBCODE: SettingsKey<boolean> = {
key: 'chat_enable_bbcode',
default_value: true,
description: 'Enabled bbcode support in chat.'
};
static readonly KEY_SWITCH_INSTANT_CHAT: SettingsKey<boolean> = {
key: 'switch_instant_chat',
default_value: true,
description: 'Directly switch to channel chat on channel select'
};
static readonly KEY_SWITCH_INSTANT_CLIENT: SettingsKey<boolean> = {
key: 'switch_instant_client',
default_value: true,
description: 'Directly switch to client info on client select'
};
static readonly KEY_HOSTBANNER_BACKGROUND: SettingsKey<boolean> = {
key: 'hostbanner_background',
default_value: false,
description: 'Enables a default background begind the hostbanner'
};
static readonly KEY_CHANNEL_EDIT_ADVANCED: SettingsKey<boolean> = {
key: 'channel_edit_advanced',
default_value: false,
description: 'Edit channels in advanced mode with a lot more settings'
};
static readonly KEY_TEAFORO_URL: SettingsKey<string> = {
key: "teaforo_url",
default_value: "https://forum.teaspeak.de/"
};
static readonly KEY_FONT_SIZE: SettingsKey<number> = {
key: "font_size"
};
static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel: ChannelEntry) => SettingsKey<ChannelSubscribeMode> = channel => {
@ -250,14 +335,17 @@ class Settings extends StaticSettings {
}
static_global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
const actual_default = typeof(_default) === "undefined" && typeof(key) === "object" && 'default_value' in key ? key.default_value : _default;
const default_object = { seed: Math.random() } as any;
let _static = this.static(key, default_object, typeof _default);
if(_static !== default_object) return StaticSettings.transformStO(_static, _default);
return this.global<T>(key, _default);
if(_static !== default_object) return StaticSettings.transformStO(_static, actual_default);
return this.global<T>(key, actual_default);
}
global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheGlobal[key]);
const actual_default = typeof(_default) === "undefined" && typeof(key) === "object" && 'default_value' in key ? key.default_value : _default;
return StaticSettings.resolveKey(Settings.keyify(key), actual_default, key => this.cacheGlobal[key]);
}
changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
@ -287,6 +375,7 @@ class ServerSettings extends SettingsBase {
private currentServer: ServerEntry;
private _server_save_worker: NodeJS.Timer;
private _server_settings_updated: boolean = false;
private _destroyed = false;
constructor() {
super();
@ -296,11 +385,23 @@ class ServerSettings extends SettingsBase {
}, 5 * 1000);
}
destroy() {
this._destroyed = true;
this.currentServer = undefined;
this.cacheServer = undefined;
clearInterval(this._server_save_worker);
this._server_save_worker = undefined;
}
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
if(this._destroyed) throw "destroyed";
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
}
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
if(this._destroyed) throw "destroyed";
key = Settings.keyify(key);
if(this.cacheServer[key.key] == value) return;
@ -313,6 +414,7 @@ class ServerSettings extends SettingsBase {
}
setServer(server: ServerEntry) {
if(this._destroyed) throw "destroyed";
if(this.currentServer) {
this.save();
this.cacheServer = {};
@ -329,6 +431,7 @@ class ServerSettings extends SettingsBase {
}
save() {
if(this._destroyed) throw "destroyed";
this._server_settings_updated = false;
if(this.currentServer) {

View File

@ -5,6 +5,12 @@ enum Sound {
AWAY_ACTIVATED = "away_activated",
AWAY_DEACTIVATED = "away_deactivated",
MICROPHONE_MUTED = "microphone.muted",
MICROPHONE_ACTIVATED = "microphone.activated",
SOUND_MUTED = "sound.muted",
SOUND_ACTIVATED = "sound.activated",
CONNECTION_CONNECTED = "connection.connected",
CONNECTION_DISCONNECTED = "connection.disconnected",
CONNECTION_BANNED = "connection.banned",
@ -155,23 +161,22 @@ namespace sound {
const data: any = {};
data.version = 1;
for(const sound in Sound) {
if(typeof(speech_volume[sound]) !== "undefined")
data[sound] = speech_volume[sound];
for(const key in Sound) {
if(typeof(speech_volume[Sound[key]]) !== "undefined")
data[Sound[key]] = speech_volume[Sound[key]];
}
data.master = master_volume;
data.overlap = overlap_sounds;
data.ignore_muted = ignore_muted;
settings.changeGlobal("sound_volume", JSON.stringify(data));
console.error(data);
}
}
export function initialize() : Promise<void> {
$.ajaxSetup({
beforeSend: function(jqXHR,settings){
if (settings.dataType === 'binary'){
if (settings.dataType === 'binary') {
settings.xhr().responseType = 'arraybuffer';
settings.processData = false;
}
@ -181,12 +186,11 @@ namespace sound {
/* volumes */
{
const data = JSON.parse(settings.static_global("sound_volume", "{}"));
for(const sound in Sound) {
if(typeof(data[sound]) !== "undefined")
speech_volume[sound] = data[sound];
for(const sound_key in Sound) {
if(typeof(data[Sound[sound_key]]) !== "undefined")
speech_volume[Sound[sound_key]] = data[Sound[sound_key]];
}
console.error(data);
master_volume = data.master || 1;
overlap_sounds = data.overlap || true;
ignore_muted = data.ignore_muted || true;
@ -223,6 +227,8 @@ namespace sound {
ignore_overlap?: boolean;
default_volume?: number;
callback?: (flag: boolean) => any;
}
export async function resolve_sound(sound: Sound) : Promise<SoundHandle> {
@ -358,6 +364,8 @@ namespace sound {
handle.replaying = true;
player.onended = event => {
if(options.callback)
options.callback(true);
delete this._playing_sounds[_sound];
};
@ -375,11 +383,24 @@ namespace sound {
}
} else if(handle.node) {
handle.node.currentTime = 0;
handle.node.play();
handle.node.play().then(() => {
if(options.callback)
options.callback(true);
}).catch(error => {
console.warn(tr("Sound playback for sound %o resulted in an error: %o"), sound, error);
if(options.callback)
options.callback(false);
});
} else {
console.warn(tr("Failed to replay sound because of missing handles."), sound);
console.warn(tr("Failed to replay sound %o because of missing handles."), sound);
if(options.callback)
options.callback(false);
return;
}
}).catch(error => {
console.warn(tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error);
if(options.callback)
options.callback(false);
});
}
}

View File

@ -50,6 +50,8 @@ class ChannelProperties {
//Only after request
channel_description: string = "";
channel_flag_conversation_private: boolean = false;
}
class ChannelEntry {
@ -70,6 +72,7 @@ class ChannelEntry {
private _tag_siblings: JQuery<HTMLElement>; /* container for all sub channels */
private _tag_clients: JQuery<HTMLElement>; /* container for all clients */
private _tag_channel: JQuery<HTMLElement>; /* container for the channel info itself */
private _destroyed = false;
private _cachedPassword: string;
private _cached_channel_description: string = undefined;
@ -91,6 +94,26 @@ class ChannelEntry {
this.__updateChannelName();
}
destroy() {
this._destroyed = true;
if(this._tag_root) {
this._tag_root.remove(); /* removes also all other tags */
this._tag_root = undefined;
}
this._tag_siblings = undefined;
this._tag_channel = undefined;
this._tag_clients = undefined;
this._cached_channel_description_promise = undefined;
this._cached_channel_description_promise_resolve = undefined;
this._cached_channel_description_promise_reject = undefined;
this.channel_previous = undefined;
this.parent = undefined;
this.channel_next = undefined;
this.channelTree = undefined;
}
channelName(){
return this.properties.channel_name;
}
@ -186,7 +209,7 @@ class ChannelEntry {
if(current_index == new_index && !enforce) return;
this._tag_channel.css("z-index", this._family_index);
this._tag_channel.css("padding-left", (this._family_index + 1) * 16 + "px");
this._tag_channel.css("padding-left", ((this._family_index + 1) * 16 + 10) + "px");
}
calculate_family_index(enforce_recalculate: boolean = false) : number {
@ -213,6 +236,15 @@ class ChannelEntry {
container_entry.attr("channel-id", this.channelId);
container_entry.addClass(this._channel_name_alignment);
/* unread marker */
{
container_entry.append(
$.spawn("div")
.addClass("marker-text-unread hidden")
.attr("conversation", this.channelId)
);
}
/* channel icon (type) */
{
container_entry.append(
@ -317,7 +349,7 @@ class ChannelEntry {
/*
setInterval(() => {
let color = (Math.random() * 10000000).toString(16).substr(0, 6);
bg.css("background", "#" + color);
tag_channel.css("background", "#" + color);
}, 150);
*/
@ -455,23 +487,31 @@ class ChannelEntry {
const bold = text => contextmenu.get_provider().html_format_enabled() ? "<b>" + text + "</b>" : text;
contextmenu.spawn_context_menu(x, y, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Show channel info"),
callback: () => {
trigger_close = false;
this.channelTree.client.select_info.open_popover()
},
icon_class: "client-about",
visible: this.channelTree.client.select_info.is_popover()
}, {
type: contextmenu.MenuEntryType.HR,
visible: this.channelTree.client.select_info.is_popover(),
name: ''
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-channel_switch",
name: bold(tr("Switch to channel")),
callback: () => this.joinChannel()
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-channel_switch",
name: bold(tr("Join text channel")),
callback: () => {
this.channelTree.client.side_bar.channel_conversations().set_current_channel(this.getChannelId());
this.channelTree.client.side_bar.show_channel_conversations();
},
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
}, {
type: contextmenu.MenuEntryType.HR,
name: ''
}, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Show channel info"),
callback: () => {
trigger_close = false;
alert('TODO!');
},
icon_class: "client-about"
},
...(() => {
const local_client = this.channelTree.client.getClient();
@ -734,13 +774,19 @@ class ChannelEntry {
this.updateChannelTypeIcon();
info_update = true;
}
if(key == "channel_flag_conversation_private") {
const conversations = this.channelTree.client.side_bar.channel_conversations();
const conversation = conversations.conversation(this.channelId, false);
if(conversation)
conversation.set_flag_private(this.properties.channel_flag_conversation_private);
}
}
group.end();
if(info_update) {
const _client = this.channelTree.client.getClient();
if(_client.currentChannel() === this)
this.channelTree.client.chat_frame.info_frame().update_channel_talk();
this.channelTree.client.side_bar.info_frame().update_channel_talk();
//TODO chat channel!
}
}
@ -855,6 +901,7 @@ class ChannelEntry {
get flag_subscribed() : boolean {
return this._flag_subscribed;
}
set flag_subscribed(flag: boolean) {
if(this._flag_subscribed == flag)
return;
@ -875,6 +922,10 @@ class ChannelEntry {
this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode);
}
set flag_text_unread(flag: boolean) {
this._tag_channel.find(".marker-text-unread").toggleClass("hidden", !flag);
}
log_data() : log.server.base.Channel {
return {
channel_name: this.channelName(),

View File

@ -1,10 +1,7 @@
/// <reference path="channel.ts" />
/// <reference path="modal/ModalChangeVolume.ts" />
/// <reference path="modal/ModalServerGroupDialog.ts" />
/// <reference path="client_move.ts" />
import KeyEvent = ppt.KeyEvent;
enum ClientType {
CLIENT_VOICE,
CLIENT_QUERY,
@ -35,6 +32,7 @@ class ClientProperties {
client_away_message: string = "";
client_away: boolean = false;
client_country: string = "";
client_input_hardware: boolean = false;
client_output_hardware: boolean = false;
@ -42,8 +40,9 @@ class ClientProperties {
client_output_muted: boolean = false;
client_is_channel_commander: boolean = false;
client_teaforum_id: number = 0;
client_teaforum_name: string = "";
client_teaforo_id: number = 0;
client_teaforo_name: string = "";
client_teaforo_flags: number = 0; /* 0x01 := Banned | 0x02 := Stuff | 0x04 := Premium */
client_talk_power: number = 0;
}
@ -55,9 +54,12 @@ class ClientEntry {
protected _properties: ClientProperties;
protected lastVariableUpdate: number = 0;
protected _speaking: boolean = false;
protected _speaking: boolean;
protected _listener_initialized: boolean;
protected _audio_handle: connection.voice.VoiceClient;
protected _audio_volume: number;
protected _audio_muted: boolean;
channelTree: ChannelTree;
@ -69,10 +71,42 @@ class ClientEntry {
this._channel = null;
}
destroy() {
if(this._tag) {
this._tag.remove();
this._tag = undefined;
}
if(this._audio_handle) {
console.warn(tr("Destroying client with an active audio handle. This could cause memory leaks!"));
this._audio_handle.abort_replay();
this._audio_handle.callback_playback = undefined;
this._audio_handle.callback_stopped = undefined;
this._audio_handle = undefined;
}
this._channel = undefined;
}
tree_unregistered() {
this.channelTree = undefined;
if(this._audio_handle) {
this._audio_handle.abort_replay();
this._audio_handle.callback_playback = undefined;
this._audio_handle.callback_stopped = undefined;
this._audio_handle = undefined;
}
this._channel = undefined;
}
set_audio_handle(handle: connection.voice.VoiceClient) {
if(this._audio_handle === handle)
return;
if(this._audio_handle) {
this._audio_handle.callback_playback = undefined;
this._audio_handle.callback_stopped = undefined;
}
//TODO may ensure that the id is the same?
this._audio_handle = handle;
if(!handle) {
@ -97,6 +131,41 @@ class ClientEntry {
clientUid(){ return this.properties.client_unique_identifier; }
clientId(){ return this._clientId; }
is_muted() { return !!this._audio_muted; }
set_muted(flag: boolean, update_icon: boolean, force?: boolean) {
if(this._audio_muted === flag && !force)
return;
if(flag) {
this.channelTree.client.serverConnection.send_command('clientmute', {
clid: this.clientId()
});
} else if(this._audio_muted) {
this.channelTree.client.serverConnection.send_command('clientunmute', {
clid: this.clientId()
});
}
this._audio_muted = flag;
this.channelTree.client.settings.changeServer("mute_client_" + this.clientUid(), flag);
if(this._audio_handle) {
if(flag) {
this._audio_handle.set_volume(0);
} else {
this._audio_handle.set_volume(this._audio_volume);
}
}
if(update_icon)
this.updateClientSpeakIcon();
for(const client of this.channelTree.clients) {
if(client === this || client.properties.client_unique_identifier != this.properties.client_unique_identifier)
continue;
client.set_muted(flag, true);
}
}
protected initializeListener(){
if(this._listener_initialized) return;
this._listener_initialized = true;
@ -116,7 +185,7 @@ class ClientEntry {
if($.isArray(this.channelTree.currently_selected)) { //Multiselect
return;
}
this.chat(true).focus();
this.open_text_chat();
});
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
@ -169,6 +238,25 @@ class ClientEntry {
});
}
protected contextmenu_info() : contextmenu.MenuEntry[] {
return [
{
type: contextmenu.MenuEntryType.ENTRY,
name: this.properties.client_type_exact === ClientType.CLIENT_MUSIC ? tr("Show bot info") : tr("Show client info"),
callback: () => {
this.channelTree.client.side_bar.show_client_info(this);
},
icon_class: "client-about",
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)
}, {
callback: () => {},
type: contextmenu.MenuEntryType.HR,
name: "",
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)
}
]
}
protected assignment_context() : contextmenu.MenuEntry[] {
let server_groups: contextmenu.MenuEntry[] = [];
for(let group of this.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) {
@ -229,7 +317,7 @@ class ClientEntry {
sub_menu: [
{
type: contextmenu.MenuEntryType.ENTRY,
icon: "client-permission_server_groups",
icon_class: "client-permission_server_groups",
name: "Server groups dialog",
callback: () => {
Modals.createServerGroupAssignmentModal(this, (group, flag) => {
@ -260,36 +348,50 @@ class ClientEntry {
type: contextmenu.MenuEntryType.SUB_MENU,
icon_class: "client-permission_client",
name: tr("Permissions"),
disabled: true,
sub_menu: [ ]
sub_menu: [
{
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-permission_client",
name: tr("Client permissions"),
callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clp", {unique_id: this.clientUid()}).open()
},
{
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-permission_client",
name: tr("Client channel permissions"),
callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clchp", {unique_id: this.clientUid(), channel_id: this._channel ? this._channel.channelId : undefined }).open()
}
]
}];
}
open_text_chat() {
const chat = this.channelTree.client.side_bar;
const conversation = chat.private_conversations().find_conversation({
name: this.clientNickName(),
client_id: this.clientId(),
unique_id: this.clientUid()
}, {
attach: true,
create: true
});
chat.private_conversations().set_selected_conversation(conversation);
/* TODO: Check if auto switch to private conversations is enabled */
chat.show_private_conversations();
chat.private_conversations().try_input_focus();
}
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
let trigger_close = true;
contextmenu.spawn_context_menu(x, y,
{
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Show client info"),
callback: () => {
trigger_close = false;
this.channelTree.client.select_info.open_popover()
},
icon_class: "client-about",
visible: this.channelTree.client.select_info.is_popover()
}, {
type: contextmenu.MenuEntryType.HR,
visible: this.channelTree.client.select_info.is_popover(),
name: ''
}, {
...this.contextmenu_info(), {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-change_nickname",
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
tr("Open text chat") +
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
callback: () => {
this.channelTree.client.chat.activeChat = this.chat(true);
this.channelTree.client.chat.focus();
this.open_text_chat();
}
}, {
type: contextmenu.MenuEntryType.ENTRY,
@ -417,15 +519,29 @@ class ClientEntry {
icon_class: "client-volume",
name: tr("Change Volume"),
callback: () => {
Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => {
Modals.spawnChangeVolume(this._audio_volume, volume => {
this._audio_volume = volume;
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume);
this._audio_handle.set_volume(volume);
if(this._audio_handle)
this._audio_handle.set_volume(volume);
if(this.channelTree.client.select_info.currentSelected == this)
this.channelTree.client.select_info.update();
});
}
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-input_muted_local",
name: tr("Mute client"),
visible: !this._audio_muted,
callback: () => this.set_muted(true, true)
}, {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-input_muted_local",
name: tr("Unmute client"),
visible: this._audio_muted,
callback: () => this.set_muted(false, true)
},
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})())
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))())
);
}
@ -510,7 +626,7 @@ class ClientEntry {
}
set speaking(flag) {
if(flag == this._speaking) return;
if(flag === this._speaking) return;
this._speaking = flag;
this.updateClientSpeakIcon();
}
@ -531,8 +647,10 @@ class ClientEntry {
icon = "client-server_query";
console.log("Server query!");
} else {
if(this.properties.client_away) {
if (this.properties.client_away) {
icon = "client-away";
} else if (this._audio_muted && !(this instanceof LocalClientEntry)) {
icon = "client-input_muted_local";
} else if(!this.properties.client_output_hardware) {
icon = "client-hardware_output_muted";
} else if(this.properties.client_output_muted) {
@ -582,6 +700,7 @@ class ClientEntry {
let update_icon_speech = false;
let update_away = false;
let reorder_channel = false;
let update_avatar = false;
{
const entries = [];
@ -595,13 +714,34 @@ class ClientEntry {
}
for(const variable of variables) {
const old_value = this._properties[variable.key];
JSON.map_field_to(this._properties, variable.value, variable.key);
if(variable.key == "client_nickname") {
this.tag.find(".client-name").text(variable.value);
let chat = this.chat(false);
if(chat) chat.name = variable.value;
if(variable.value !== old_value && typeof(old_value) === "string") {
if(!(this instanceof LocalClientEntry)) { /* own changes will be logged somewhere else */
this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, {
own_client: false,
client: this.log_data(),
new_name: variable.value,
old_name: old_value
});
}
}
this.tag.find(".client-name").text(variable.value);
const chat = this.channelTree.client.side_bar;
const conversation = chat.private_conversations().find_conversation({
name: this.clientNickName(),
client_id: this.clientId(),
unique_id: this.clientUid()
}, {
attach: false,
create: false
});
if(conversation)
conversation.set_client_name(variable.value);
reorder_channel = true;
}
if(
@ -617,13 +757,15 @@ class ClientEntry {
update_away = true;
}
if(variable.key == "client_unique_identifier") {
if(this._audio_handle) {
const volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1"));
this._audio_handle.set_volume(volume);
log.debug(LogCategory.CLIENT, tr("Loaded client volume %d for client %s from config."), volume, this.clientUid());
} else {
log.warn(LogCategory.CLIENT, tr("Visible client got unique id assigned, but hasn't yet an audio handle. Ignoring volume assignment."));
}
this._audio_volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1"));
const mute_status = this.channelTree.client.settings.server("mute_client_" + this.clientUid(), false);
this.set_muted(mute_status, false, mute_status); /* force only needed when we want to mute the client */
if(this._audio_handle)
this._audio_handle.set_volume(this._audio_muted ? 0 : this._audio_volume);
update_icon_speech = true;
log.debug(LogCategory.CLIENT, tr("Loaded client (%s) server specific properties. Volume: %o Muted: %o."), this.clientUid(), this._audio_volume, this._audio_muted);
}
if(variable.key == "client_talk_power") {
reorder_channel = true;
@ -639,6 +781,8 @@ class ClientEntry {
}
if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups")
this.update_displayed_client_groups();
else if(variable.key == "client_flag_avatar")
update_avatar = true;
}
/* process updates after variables have been set */
@ -651,15 +795,30 @@ class ClientEntry {
if(update_away)
this.updateAwayMessage();
const side_bar = this.channelTree.client.side_bar;
{
const client_info = side_bar.client_info();
if(client_info.current_client() === this)
client_info.set_current_client(this, true); /* force an update */
}
if(update_avatar) {
this.channelTree.client.fileManager.avatars.update_cache(this.avatarId(), this.properties.client_flag_avatar);
const conversations = side_bar.private_conversations();
const conversation = conversations.find_conversation({name: this.clientNickName(), unique_id: this.clientUid(), client_id: this.clientId()}, {create: false, attach: false});
if(conversation)
conversation.update_avatar();
}
group.end();
}
update_displayed_client_groups() {
this.tag.find(".container-icons-group").children().detach();
this.tag.find(".container-icons-group").children().remove();
for(let id of this.assignedServerGroupIds())
this.updateGroupIcon(this.channelTree.client.groups.serverGroup(id));
this.update_group_icon_order();
this.updateGroupIcon(this.channelTree.client.groups.channelGroup(this.properties.client_channel_group_id));
let prefix_groups: string[] = [];
@ -696,44 +855,8 @@ class ClientEntry {
}
}
private chat_name() {
return "client_" + this.clientUid() + ":" + this.clientId();
}
chat(create: boolean = false) : ChatEntry {
let chatName = "client_" + this.clientUid() + ":" + this.clientId();
let chat = this.channelTree.client.chat.findChat(chatName);
if(!chat && create) {
chat = this.channelTree.client.chat.createChat(chatName);
chat.flag_closeable = true;
chat.name = this.clientNickName();
chat.owner_unique_id = this.properties.client_unique_identifier;
}
this.initialize_chat(chat);
return chat;
}
initialize_chat(handle?: ChatEntry) {
handle = handle || this.channelTree.client.chat.findChat(this.chat_name());
if(!handle)
return;
handle.onMessageSend = text => {
this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, this);
};
handle.onClose = () => {
if(!handle.flag_offline)
this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": this.clientId()}, {process_result: false}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to notify chat participant (%o) that the chat has been closed. Error: %o"), this, error);
});
return true;
};
}
updateClientIcon() {
this.tag.find(".container-icon-client").children().detach();
this.tag.find(".container-icon-client").children().remove();
if(this.properties.client_icon_id > 0) {
this.channelTree.client.fileManager.icons.generateTag(this.properties.client_icon_id).attr("title", "Client icon")
.appendTo(this.tag.find(".container-icon-client"));
@ -742,18 +865,25 @@ class ClientEntry {
updateGroupIcon(group: Group) {
if(!group) return;
//TODO group icon order
this.tag.find(".container-icons-group .icon_group_" + group.id).detach();
const container = this.tag.find(".container-icons-group");
container.find(".icon_group_" + group.id).remove();
if (group.properties.iconid > 0) {
this.tag.find(".container-icons-group").append(
$.spawn("div")
container.append(
$.spawn("div").attr('group-power', group.properties.sortid)
.addClass("container-group-icon icon_group_" + group.id)
.append(this.channelTree.client.fileManager.icons.generateTag(group.properties.iconid)).attr("title", group.name)
);
}
}
update_group_icon_order() {
const container = this.tag.find(".container-icons-group");
container.append(...[...container.children()].sort((a, b) => parseInt(a.getAttribute("group-power")) - parseInt(b.getAttribute("group-power"))));
}
assignedServerGroupIds() : number[] {
let result = [];
for(let id of this.properties.client_servergroups.split(",")){
@ -843,7 +973,7 @@ class LocalClientEntry extends ClientEntry {
const _self = this;
contextmenu.spawn_context_menu(x, y,
{
...this.contextmenu_info(), {
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
tr("Change name") +
@ -875,6 +1005,7 @@ class LocalClientEntry extends ClientEntry {
}
initializeListener(): void {
this._listener_initialized = false; /* could there be a better system */
super.initializeListener();
this.tag.find(".client-name").addClass("client-name-own");
@ -918,11 +1049,14 @@ class LocalClientEntry extends ClientEntry {
if(_self.clientNickName() == text) return;
elm.text(_self.clientNickName());
const old_name = _self.clientNickName();
_self.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => {
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, text);
this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, {
client: this.log_data(),
own_action: true
old_name: old_name,
new_name: text,
own_client: true
});
}).catch((e: CommandResult) => {
this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGE_FAILED, {
@ -973,6 +1107,13 @@ class MusicClientEntry extends ClientEntry {
super(clientId, clientName, new MusicClientProperties());
}
destroy() {
super.destroy();
this._info_promise = undefined;
this._info_promise_reject = undefined;
this._info_promise_resolve = undefined;
}
get properties() : MusicClientProperties {
return this._properties as MusicClientProperties;
}
@ -980,20 +1121,7 @@ class MusicClientEntry extends ClientEntry {
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
let trigger_close = true;
contextmenu.spawn_context_menu(x, y,
{
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Show bot info"),
callback: () => {
trigger_close = false;
this.channelTree.client.select_info.open_popover()
},
icon_class: "client-about",
visible: this.channelTree.client.select_info.is_popover()
}, {
type: contextmenu.MenuEntryType.HR,
visible: this.channelTree.client.select_info.is_popover(),
name: ''
}, {
...this.contextmenu_info(), {
name: tr("<b>Change bot name</b>"),
icon_class: "client-change_nickname",
disabled: false,
@ -1160,7 +1288,7 @@ class MusicClientEntry extends ClientEntry {
},
type: contextmenu.MenuEntryType.ENTRY
},
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})())
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))())
);
}

View File

@ -88,8 +88,13 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
return;
menu.animate({opacity: 0}, 100, () => menu.css("display", "none"));
for(const callback of this._close_callbacks)
for(const callback of this._close_callbacks) {
if(typeof(callback) !== "function") {
console.error(tr("Given close callback is not a function!. Callback: %o"), callback);
continue;
}
callback();
}
this._close_callbacks = [];
}
@ -135,7 +140,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
}
return tag;
} else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) {
let checkbox = $.spawn("label").addClass("checkbox");
let checkbox = $.spawn("label").addClass("ccheckbox");
$.spawn("input").attr("type", "checkbox").prop("checked", !!entry.checkbox_checked).appendTo(checkbox);
$.spawn("span").addClass("checkmark").appendTo(checkbox);
@ -191,7 +196,8 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
continue;
if(entry.type == contextmenu.MenuEntryType.CLOSE) {
this._close_callbacks.push(entry.callback);
if(entry.callback)
this._close_callbacks.push(entry.callback);
} else
menu_container.append(this.generate_tag(entry));
}

View File

@ -1,3 +1,5 @@
import ClickEvent = JQuery.ClickEvent;
enum ElementType {
HEADER,
BODY,
@ -22,7 +24,7 @@ const ModalFunctions = {
switch (typeof val){
case "string":
if(type == ElementType.HEADER)
return $.spawn("h5").addClass("modal-title").text(val);
return $.spawn("div").addClass("modal-title").text(val);
return $("<div>" + val + "</div>");
case "object": return val as JQuery;
case "undefined":
@ -61,6 +63,7 @@ class ModalProperties {
return this;
}
width: number | string = "60%";
min_width?: number | string;
height: number | string = "auto";
closeable: boolean = true;
@ -78,8 +81,33 @@ class ModalProperties {
full_size?: boolean = false;
}
class Modal {
$(document).on('mousedown', (event: MouseEvent) => {
/* pageX or pageY are undefined if this is an event executed via .trigger('click'); */
if(_global_modal_count == 0 || typeof(event.pageX) === "undefined" || typeof(event.pageY) === "undefined")
return;
let element = event.target as HTMLElement;
do {
if(element.classList.contains('modal-content'))
break;
if(!element.classList.contains('modal'))
continue;
if(element == _global_modal_last && _global_modal_last_time + 100 > Date.now())
break;
$(element).find("> .modal-dialog > .modal-content > .modal-header .button-modal-close").trigger('click');
break;
} while((element = element.parentElement));
});
let _global_modal_count = 0;
let _global_modal_last: HTMLElement;
let _global_modal_last_time: number;
class Modal {
private _htmlTag: JQuery;
properties: ModalProperties;
shown: boolean;
@ -119,32 +147,57 @@ class Modal {
Object.assign(properties, this.properties.template_properties);
const tag = template.renderTag(properties);
if(typeof(this.properties.width) !== "undefined")
tag.find(".modal-content").css("min-width", this.properties.width);
if(typeof(this.properties.min_width) !== "undefined")
tag.find(".modal-content").css("min-width", this.properties.min_width);
this.close_elements = tag.find(".button-modal-close");
this.close_elements.toggle(this.properties.closeable);
this.close_elements.toggle(this.properties.closeable).on('click', event => {
if(this.properties.closeable)
this.close();
});
this._htmlTag = tag;
this._htmlTag.on('shown.bs.modal', event => { for(const listener of this.open_listener) listener(); });
this._htmlTag.find("input").on('change', event => {
$(event.target).parents(".form-group").toggleClass('is-filled', !!(event.target as HTMLInputElement).value);
});
//TODO: After the animation!
this._htmlTag.on('hide.bs.modal', event => !this.properties.closeable || this.close());
this._htmlTag.on('hidden.bs.modal', event => this._htmlTag.detach());
this._htmlTag.on('hidden.bs.modal', event => this._htmlTag.remove());
}
open() {
if(this.shown)
return;
_global_modal_last_time = Date.now();
_global_modal_last = this.htmlTag[0];
this.shown = true;
this.htmlTag.appendTo($("body"));
this.htmlTag.bootstrapMaterialDesign().modal(this.properties.closeable ? 'show' : {
backdrop: 'static',
keyboard: false,
});
_global_modal_count++;
this.htmlTag.show();
setTimeout(() => this.htmlTag.addClass('shown'), 0);
if(this.properties.trigger_tab)
this.htmlTag.one('shown.bs.modal', () => this.htmlTag.find(".tab").trigger('tab.resize'));
setTimeout(() => {
for(const listener of this.open_listener) listener();
this.htmlTag.find(".tab").trigger('tab.resize');
}, 300);
}
close() {
if(!this.shown) return;
_global_modal_count--;
this.shown = false;
this.htmlTag.modal('hide');
this.htmlTag.removeClass('shown');
setTimeout(() => {
this.htmlTag.remove();
this._htmlTag = undefined;
}, 300);
this.properties.triggerClose();
for(const listener of this.close_listener)
listener();

View File

@ -0,0 +1,105 @@
interface SliderOptions {
min_value?: number;
max_value?: number;
initial_value?: number;
step?: number;
unit?: string;
value_field?: JQuery | JQuery[];
}
interface Slider {
value(value?: number) : number;
}
function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
options = Object.assign( {
initial_value: 0,
min_value: 0,
max_value: 100,
step: 1,
unit: '%',
value_field: []
}, options);
if(!Array.isArray(options.value_field))
options.value_field = [options.value_field];
if(options.min_value >= options.max_value)
throw "invalid range";
if(options.step > (options.max_value - options.min_value))
throw "invalid step size";
const tool = tooltip(slider); /* add the tooltip functionality */
const filler = slider.find(".filler");
const thumb = slider.find(".thumb");
const tooltip_text = slider.find(".tooltip a");
let _current_value;
const update_value = (value: number, trigger_change: boolean) => {
_current_value = value;
const offset = Math.min(100, Math.max(0, ((value - options.min_value) * 100) / (options.max_value - options.min_value)));
filler.css('width', offset + '%');
thumb.css('left', offset + '%');
tooltip_text.text(value.toFixed(0) + options.unit);
slider.attr("value", value);
if(trigger_change)
slider.trigger('change');
for(const field of options.value_field)
(field as JQuery).text(value + options.unit);
tool.update();
};
const mouse_up_listener = () => {
document.removeEventListener('mousemove', mouse_listener);
document.removeEventListener('touchmove', mouse_listener);
document.removeEventListener('mouseup', mouse_up_listener);
document.removeEventListener('touchend', mouse_up_listener);
document.removeEventListener('touchcancel', mouse_up_listener);
tool.hide();
slider.removeClass("active");
console.log("Events removed");
};
const mouse_listener = (event: MouseEvent | TouchEvent) => {
const parent_offset = slider.offset();
const min = parent_offset.left;
const max = parent_offset.left + slider.width();
const current = event instanceof MouseEvent ? event.pageX : event.touches[event.touches.length - 1].clientX;
const range = options.max_value - options.min_value;
const offset = Math.round(((current - min) * (range / options.step)) / (max - min)) * options.step;
let value = Math.min(options.max_value, Math.max(options.min_value, options.min_value + offset));
//console.log("Min: %o | Max: %o | %o (%o)", min, max, current, offset);
update_value(value, true);
};
slider.on('mousedown', event => {
document.addEventListener('mousemove', mouse_listener);
document.addEventListener('touchmove', mouse_listener);
document.addEventListener('mouseup', mouse_up_listener);
document.addEventListener('touchend', mouse_up_listener);
document.addEventListener('touchcancel', mouse_up_listener);
tool.show();
slider.addClass("active");
});
update_value(options.initial_value, false);
return {
value(value?: number) {
if(typeof(value) !== "undefined" && value !== _current_value)
update_value(value, true);
return _current_value;
}
}
}

View File

@ -75,6 +75,9 @@ var TabFunctions = {
if(header_tag.attr("x-entry-class"))
tag_header.addClass(header_tag.attr("x-entry-class"));
if(header_tag.attr("x-entry-id"))
tag_header.attr("x-id", header_tag.attr("x-entry-id"));
tag_header.append(header_data);
/* listener if the tab might got removed */

View File

@ -0,0 +1,78 @@
function tooltip(entry: JQuery) {
return tooltip.initialize(entry);
}
namespace tooltip {
let _global_tooltip: JQuery;
export type Handle = {
show();
is_shown();
hide();
update();
}
export function initialize(entry: JQuery) : Handle {
let _show;
let _hide;
let _shown;
let _update;
entry.find(".container-tooltip").each((index, _node) => {
const node = $(_node) as JQuery;
const node_content = node.find(".tooltip");
let _force_show = false, _flag_shown = false;
const mouseenter = (event?) => {
const bounds = node[0].getBoundingClientRect();
if(!_global_tooltip) {
_global_tooltip = $("#global-tooltip");
}
_global_tooltip[0].style.left = (bounds.left + bounds.width / 2) + "px";
_global_tooltip[0].style.top = bounds.top + "px";
_global_tooltip[0].classList.add("shown");
_global_tooltip[0].innerHTML = node_content[0].innerHTML;
_flag_shown = _flag_shown || !!event; /* if event is undefined then it has been triggered by hand */
};
const mouseexit = () => {
if(_global_tooltip) {
if(!_force_show) {
_global_tooltip[0].classList.remove("shown");
}
_flag_shown = false;
}
};
_node.addEventListener("mouseenter", mouseenter);
_node.addEventListener("mouseleave", mouseexit);
_show = () => {
_force_show = true;
mouseenter();
};
_hide = () => {
_force_show = false;
if(!_flag_shown)
mouseexit();
};
_update = () => {
if(_flag_shown || _force_show)
mouseenter();
};
_shown = () => _flag_shown || _force_show;
});
return {
hide: _hide || (() => {}),
show: _show || (() => {}),
is_shown: _shown || (() => false),
update: _update || (() => {})
};
}
}

View File

@ -27,6 +27,8 @@ class ControlBar {
private connection_handler: ConnectionHandler | undefined;
private _button_hostbanner: JQuery;
htmlTag: JQuery;
constructor(htmlTag: JQuery) {
this.htmlTag = htmlTag;
@ -47,6 +49,7 @@ class ControlBar {
this.connection_handler = handler;
this.apply_server_state();
this.update_connection_state();
}
apply_server_state() {
@ -63,15 +66,30 @@ class ControlBar {
this.button_query_visible = this.connection_handler.client_status.queries_visible;
this.button_subscribe_all = this.connection_handler.client_status.channel_subscribe_all;
this.apply_server_hostbutton();
this.apply_server_voice_state();
}
apply_server_hostbutton() {
const server = this.connection_handler.channelTree.server;
if(server && server.properties.virtualserver_hostbutton_gfx_url) {
this._button_hostbanner
.attr("title", server.properties.virtualserver_hostbutton_tooltip || server.properties.virtualserver_hostbutton_gfx_url)
.attr("href", server.properties.virtualserver_hostbutton_url);
this._button_hostbanner.find("img").attr("src", server.properties.virtualserver_hostbutton_gfx_url);
this._button_hostbanner.show();
} else {
this._button_hostbanner.hide();
}
}
apply_server_voice_state() {
if(!this.connection_handler)
return;
this.button_microphone = !this.connection_handler.client_status.input_hardware ? "disabled" : this.connection_handler.client_status.input_muted ? "muted" : "enabled";
this.button_speaker = this.connection_handler.client_status.output_muted ? "muted" : "enabled";
top_menu.update_state(); //TODO: Only run "small" update?
}
current_connection_handler() {
@ -95,6 +113,7 @@ class ControlBar {
};
this.htmlTag.find(".btn_connect").on('click', this.on_open_connect.bind(this));
this.htmlTag.find(".btn_connect_new_tab").on('click', this.on_open_connect_new_tab.bind(this));
this.htmlTag.find(".btn_disconnect").on('click', this.on_execute_disconnect.bind(this));
this.htmlTag.find(".btn_mute_input").on('click', this.on_toggle_microphone.bind(this));
@ -110,6 +129,15 @@ class ControlBar {
this.htmlTag.find(".btn_token_use").on('click', this.on_token_use.bind(this));
this.htmlTag.find(".btn_token_list").on('click', this.on_token_list.bind(this));
(this._button_hostbanner = this.htmlTag.find(".button-hostbutton")).hide().on('click', () => {
if(!this.connection_handler) return;
const server = this.connection_handler.channelTree.server;
if(!server || !server.properties.virtualserver_hostbutton_url) return;
window.open(server.properties.virtualserver_hostbutton_url, '_blank');
});
{
this.htmlTag.find(".btn_away_disable").on('click', this.on_away_disable.bind(this));
@ -124,6 +152,7 @@ class ControlBar {
this.htmlTag.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
}
dropdownify(this.htmlTag.find(".container-connect"));
dropdownify(this.htmlTag.find(".container-disconnect"));
dropdownify(this.htmlTag.find(".btn_token"));
dropdownify(this.htmlTag.find(".btn_away"));
@ -202,13 +231,20 @@ class ControlBar {
this._button_microphone = state;
let tag = this.htmlTag.find(".btn_mute_input");
const tag_icon = tag.find(".icon_x32, .icon");
const tag_icon = tag.find(".icon_em, .icon");
tag.toggleClass('activated', state === "muted");
/*
tag_icon
.toggleClass('client-input_muted', state === "muted")
.toggleClass('client-capture', state === "enabled")
.toggleClass('client-activate_microphone', state === "disabled");
*/
tag_icon
.toggleClass('client-input_muted', state !== "disabled")
.toggleClass('client-capture', false)
.toggleClass('client-activate_microphone', state === "disabled");
if(state === "disabled")
tag_icon.attr('title', tr("Enable your microphone on this server"));
@ -224,12 +260,17 @@ class ControlBar {
this._button_speakers = state;
let tag = this.htmlTag.find(".btn_mute_output");
const tag_icon = tag.find(".icon_x32, .icon");
const tag_icon = tag.find(".icon_em, .icon");
tag.toggleClass('activated', state === "muted");
/*
tag_icon
.toggleClass('client-output_muted', state !== "enabled")
.toggleClass('client-volume', state === "enabled");
*/
tag_icon
.toggleClass('client-output_muted', true)
.toggleClass('client-volume', false);
if(state === "enabled")
tag_icon.attr('title', tr("Mute sound"));
@ -245,7 +286,7 @@ class ControlBar {
this.htmlTag
.find(".button-subscribe-mode")
.toggleClass('activated', this._button_subscribe_all)
.find('.icon_x32')
.find('.icon_em')
.toggleClass('client-unsubscribe_from_all_channels', !this._button_subscribe_all)
.toggleClass('client-subscribe_to_all_channels', this._button_subscribe_all);
}
@ -320,10 +361,13 @@ class ControlBar {
private on_toggle_microphone() {
if(this._button_microphone === "disabled" || this._button_microphone === "muted")
if(this._button_microphone === "disabled" || this._button_microphone === "muted") {
this.button_microphone = "enabled";
else
sound.manager.play(Sound.MICROPHONE_ACTIVATED);
} else {
this.button_microphone = "muted";
sound.manager.play(Sound.MICROPHONE_MUTED);
}
if(this.connection_handler) {
this.connection_handler.client_status.input_muted = this._button_microphone !== "enabled";
@ -338,10 +382,13 @@ class ControlBar {
}
private on_toggle_sound() {
if(this._button_speakers === "muted")
if(this._button_speakers === "muted") {
this.button_speaker = "enabled";
else
sound.manager.play(Sound.SOUND_ACTIVATED);
} else {
this.button_speaker = "muted";
sound.manager.play(Sound.SOUND_MUTED);
}
if(this.connection_handler) {
this.connection_handler.client_status.output_muted = this._button_speakers !== "enabled";
@ -379,8 +426,17 @@ class ControlBar {
private on_open_connect() {
if(this.connection_handler)
this.connection_handler.cancel_reconnect();
this.connection_handler.cancel_reconnect(true);
Modals.spawnConnectModal({}, {
url: "ts.TeaSpeak.de",
enforce: false
});
}
private on_open_connect_new_tab() {
Modals.spawnConnectModal({
default_connect_new_tab: true
}, {
url: "ts.TeaSpeak.de",
enforce: false
});
@ -410,7 +466,7 @@ class ControlBar {
}
private on_execute_disconnect() {
this.connection_handler.cancel_reconnect();
this.connection_handler.cancel_reconnect(true);
this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
this.update_connection_state();
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
@ -426,7 +482,7 @@ class ControlBar {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
}).catch(error => {
//TODO tr
createErrorModal(tr("Use token"), "Failed to use token: " + (error instanceof CommandResult ? error.message : error)).open();
createErrorModal(tr("Use token"), MessageHelper.formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
});
}).open();
}
@ -459,23 +515,7 @@ class ControlBar {
}
private on_bookmark_server_add() {
if(this.connection_handler && this.connection_handler.connected) {
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => true, result => {
if(result) {
const bookmark = bookmarks.create_bookmark(result as string, bookmarks.bookmarks(), {
server_port: this.connection_handler.serverConnection.remote_address().port,
server_address: this.connection_handler.serverConnection.remote_address().host,
server_password: "",
server_password_hash: ""
}, this.connection_handler.getClient().clientNickName());
bookmarks.save_bookmark(bookmark);
this.update_bookmarks()
}
}).open();
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
}
bookmarks.add_current_server();
}
update_bookmark_status() {
@ -486,8 +526,8 @@ class ControlBar {
update_bookmarks() {
//<div class="btn_bookmark_connect" target="localhost"><a>Localhost</a></div>
let tag_bookmark = this.htmlTag.find(".btn_bookmark .dropdown");
tag_bookmark.find(".bookmark, .directory").detach();
let tag_bookmark = this.htmlTag.find(".btn_bookmark > .dropdown");
tag_bookmark.find(".bookmark, .directory").remove();
const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => {
if(bookmark.type == bookmarks.BookmarkType.ENTRY) {
@ -495,37 +535,14 @@ class ControlBar {
const bookmark_connect = (new_tab: boolean) => {
this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working
const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile();
if(profile.valid()) {
const connection = this.connection_handler && !new_tab ? this.connection_handler : server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
connection.startConnection(
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
profile,
{
nickname: mark.nickname,
password: {
password: mark.server_properties.server_password_hash,
hashed: true
}
}
);
} else {
Modals.spawnConnectModal({
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
enforce: true
}, {
profile: profile,
enforce: true
})
}
bookmarks.boorkmak_connect(mark, new_tab);
};
return $.spawn("div")
.addClass("bookmark")
.append(
$.spawn("div").addClass("icon client-server")
//$.spawn("div").addClass("icon client-server")
IconManager.generate_tag(IconManager.load_cached_icon(mark.last_icon_id || 0), {animate: false}) /* must be false */
)
.append(
$.spawn("div")
@ -550,7 +567,8 @@ class ControlBar {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Connect in a new tab"),
icon_class: 'client-connect',
callback: () => bookmark_connect(true)
callback: () => bookmark_connect(true),
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
}, contextmenu.Entry.CLOSE(() => {
setTimeout(() => {
this.htmlTag.find(".btn_bookmark.dropdown-arrow").removeClass("force-show")
@ -564,10 +582,7 @@ class ControlBar {
const mark = <bookmarks.DirectoryBookmark>bookmark;
const container = $.spawn("div").addClass("sub-menu dropdown");
for(const member of mark.content)
container.append(build_entry(member));
return $.spawn("div")
const result = $.spawn("div")
.addClass("directory")
.append(
$.spawn("div").addClass("icon client-folder")
@ -583,7 +598,13 @@ class ControlBar {
.append(
$.spawn("div").addClass("sub-container")
.append(container)
)
);
/* we've to keep it this order because we're then keeping the reference of the loading icons... */
for(const member of mark.content)
container.append(build_entry(member));
return result;
}
};

View File

@ -0,0 +1,517 @@
namespace top_menu {
export interface HRItem { }
export interface MenuItem {
append_item(label: string): MenuItem;
append_hr(): HRItem;
delete_item(item: MenuItem | HRItem);
items() : (MenuItem | HRItem)[];
icon(klass?: string | Promise<Icon> | Icon) : string;
label(value?: string) : string;
visible(value?: boolean) : boolean;
disabled(value?: boolean) : boolean;
click(callback: () => any) : this;
}
export interface MenuBarDriver {
initialize();
append_item(label: string) : MenuItem;
delete_item(item: MenuItem);
items() : MenuItem[];
flush_changes();
}
let _driver: MenuBarDriver;
export function driver() : MenuBarDriver {
return _driver;
}
export function set_driver(driver: MenuBarDriver) {
_driver = driver;
}
export interface NativeActions {
open_dev_tools();
reload_page();
check_native_update();
open_change_log();
quit();
}
export let native_actions: NativeActions;
namespace html {
class HTMLHrItem implements top_menu.HRItem {
readonly html_tag: JQuery;
constructor() {
this.html_tag = $.spawn("hr");
}
}
class HTMLMenuItem implements top_menu.MenuItem {
readonly html_tag: JQuery;
readonly _label_tag: JQuery;
readonly _label_icon_tag: JQuery;
readonly _label_text_tag: JQuery;
readonly _submenu_tag: JQuery;
private _items: (MenuItem | HRItem)[] = [];
private _label: string;
private _callback_click: () => any;
constructor(label: string, mode: "side" | "down") {
this._label = label;
this.html_tag = $.spawn("div").addClass("container-menu-item type-" + mode);
this._label_tag = $.spawn("div").addClass("menu-item");
this._label_icon_tag = $.spawn("div").addClass("container-icon").appendTo(this._label_tag);
$.spawn("div").addClass("container-label").append(
this._label_text_tag = $.spawn("a").text(label)
).appendTo(this._label_tag);
this._label_tag.on('click', event => {
if(event.isDefaultPrevented())
return;
const disabled = this.html_tag.hasClass("disabled");
if(this._callback_click && !disabled) {
this._callback_click();
}
event.preventDefault();
if(disabled) event.stopPropagation();
});
this._submenu_tag = $.spawn("div").addClass("sub-menu");
this.html_tag.append(this._label_tag);
this.html_tag.append(this._submenu_tag);
}
append_item(label: string): top_menu.MenuItem {
const item = new HTMLMenuItem(label, "side");
this._items.push(item);
this._submenu_tag.append(item.html_tag);
this.html_tag.addClass('sub-entries');
return item;
}
append_hr(): HRItem {
const item = new HTMLHrItem();
this._items.push(item);
this._submenu_tag.append(item.html_tag);
return item;
}
delete_item(item: top_menu.MenuItem | top_menu.HRItem) {
this._items.remove(item);
(item as any).html_tag.detach();
this.html_tag.toggleClass('sub-entries', this._items.length > 0);
}
disabled(value?: boolean): boolean {
if(typeof(value) === "undefined")
return this.html_tag.hasClass("disabled");
this.html_tag.toggleClass("disabled", value);
return value;
}
items(): (top_menu.MenuItem | top_menu.HRItem)[] {
return this._items;
}
label(value?: string): string {
if(typeof(value) === "undefined" || this._label === value)
return this._label;
return this._label;
}
visible(value?: boolean): boolean {
if(typeof(value) === "undefined")
return this.html_tag.is(':visible'); //FIXME!
this.html_tag.toggle(!!value);
return value;
}
click(callback: () => any): this {
this._callback_click = callback;
return this;
}
icon(klass?: string | Promise<Icon> | Icon): string {
this._label_icon_tag.children().remove();
if(typeof(klass) === "string")
$.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag);
else
IconManager.generate_tag(klass).appendTo(this._label_icon_tag);
return "";
}
}
export class HTMLMenuBarDriver implements MenuBarDriver {
private static _instance: HTMLMenuBarDriver;
public static instance() : HTMLMenuBarDriver {
if(!this._instance)
this._instance = new HTMLMenuBarDriver();
return this._instance;
}
readonly html_tag: JQuery;
private _items: MenuItem[] = [];
constructor() {
this.html_tag = $.spawn("div").addClass("top-menu-bar");
}
append_item(label: string): top_menu.MenuItem {
const item = new HTMLMenuItem(label, "down");
this._items.push(item);
this.html_tag.append(item.html_tag);
item._label_tag.on('click', event => {
event.preventDefault();
this.html_tag.find(".active").removeClass("active");
item.html_tag.addClass("active");
setTimeout(() => {
$(document).one('click focusout', event => item.html_tag.removeClass("active"));
}, 0);
});
return item;
}
delete_item(item: MenuItem) {
return undefined;
}
items(): top_menu.MenuItem[] {
return this._items;
}
flush_changes() { /* unused, all changed were made instantly */ }
initialize() {
$("#top-menu-bar").replaceWith(this.html_tag);
}
}
}
let _items_bookmark: {
root: MenuItem,
manage: MenuItem,
add_current: MenuItem
};
export function rebuild_bookmarks() {
if(!_items_bookmark) {
_items_bookmark = {
root: driver().append_item(tr("Favorites")),
add_current: undefined,
manage: undefined
};
_items_bookmark.manage = _items_bookmark.root.append_item(tr("Manage bookmarks"));
_items_bookmark.manage.icon("client-bookmark_manager");
_items_bookmark.manage.click(() => Modals.spawnBookmarkModal());
_items_bookmark.add_current = _items_bookmark.root.append_item(tr("Add current server to bookmarks"));
_items_bookmark.add_current.icon('client-bookmark_add');
_items_bookmark.add_current.click(() => bookmarks.add_current_server());
_state_updater["bookmarks.ac"] = { item: _items_bookmark.add_current, conditions: [condition_connected]};
}
_items_bookmark.root.items().filter(e => e !== _items_bookmark.add_current && e !== _items_bookmark.manage).forEach(e => {
_items_bookmark.root.delete_item(e);
});
_items_bookmark.root.append_hr();
const build_bookmark = (root: MenuItem, entry: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => {
if(entry.type == bookmarks.BookmarkType.DIRECTORY) {
const directory = entry as bookmarks.DirectoryBookmark;
const item = root.append_item(directory.display_name);
item.icon('client-folder');
for(const entry of directory.content)
build_bookmark(item, entry);
if(directory.content.length == 0)
item.disabled(true);
} else {
const bookmark = entry as bookmarks.Bookmark;
const item = root.append_item(bookmark.display_name);
item.icon(IconManager.load_cached_icon(bookmark.last_icon_id || 0));
item.click(() => bookmarks.boorkmak_connect(bookmark));
}
};
for(const entry of bookmarks.bookmarks().content)
build_bookmark(_items_bookmark.root, entry);
driver().flush_changes();
}
/* will be called on connection handler change or on client connect state or mic state change etc... */
let _state_updater: {[key: string]:{ item: MenuItem; conditions: (() => boolean)[], update_handler?: (item: MenuItem) => any }} = {};
export function update_state() {
for(const _key of Object.keys(_state_updater)) {
const item = _state_updater[_key];
if(item.update_handler) {
if(item.update_handler(item.item))
continue;
}
let enabled = true;
for(const condition of item.conditions)
if(!condition()) {
enabled = false;
break;
}
item.item.disabled(!enabled);
}
driver().flush_changes();
}
const condition_connected = () => {
const scon = server_connections ? server_connections.active_connection_handler() : undefined;
return scon && scon.connected;
};
declare namespace native {
export function initialize();
}
export function initialize() {
const driver = top_menu.driver();
driver.initialize();
/* build connection */
let item: MenuItem;
{
const menu = driver.append_item(tr("Connection"));
item = menu.append_item("Connect to a server");
item.icon('client-connect');
item.click(() => Modals.spawnConnectModal({}));
const do_disconnect = (handlers: ConnectionHandler[]) => {
for(const handler of handlers) {
handler.cancel_reconnect(true);
handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
server_connections.active_connection_handler().serverConnection.disconnect();
handler.sound.play(Sound.CONNECTION_DISCONNECTED);
}
control_bar.update_connection_state();
update_state();
};
item = menu.append_item("Disconnect from current server");
item.icon('client-disconnect');
item.disabled(true);
item.click(() => {
const handler = server_connections.active_connection_handler();
do_disconnect([handler]);
});
_state_updater["connection.dc"] = { item: item, conditions: [() => condition_connected()]};
item = menu.append_item("Disconnect from all servers");
item.icon('client-disconnect');
item.click(() => {
do_disconnect(server_connections.server_connection_handlers());
});
_state_updater["connection.dca"] = { item: item, conditions: [], update_handler: (item) => {
item.visible(server_connections && server_connections.server_connection_handlers().length > 1);
return true;
}};
if(!app.is_web()) {
menu.append_hr();
item = menu.append_item(tr("Quit"));
item.icon('client-close_button');
item.click(() => native_actions.quit());
}
}
{
rebuild_bookmarks();
}
if(false) {
const menu = driver.append_item("Self");
/* Microphone | Sound | Away */
}
{
const menu = driver.append_item("Rights");
item = menu.append_item(tr("Server Groups"));
item.icon("client-permission_server_groups");
item.click(() => {
Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "sg").open();
});
_state_updater["permission.sg"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Client Permissions"));
item.icon("client-permission_client");
item.click(() => {
Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "clp").open();
});
_state_updater["permission.clp"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Client Permissions"));
item.icon("client-permission_client");
item.click(() => {
Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "clchp").open();
});
_state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Groups"));
item.icon("client-permission_channel");
item.click(() => {
Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "cg").open();
});
_state_updater["permission.cg"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Permissions"));
item.icon("client-permission_channel");
item.click(() => {
Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "chp").open();
});
_state_updater["permission.cp"] = { item: item, conditions: [condition_connected]};
menu.append_hr();
item = menu.append_item(tr("List Privilege Keys"));
item.icon("client-token");
item.click(() => {
createErrorModal(tr("Not implemented"), tr("Privilege key list is not implemented yet!")).open();
});
_state_updater["permission.pk"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Use Privilege Key"));
item.icon("client-token_use");
item.click(() => {
//TODO: Fixeme use one method for the control bar and here!
createInputModal(tr("Use token"), tr("Please enter your token/priviledge key"), message => message.length > 0, result => {
if(!result) return;
const scon = server_connections.active_connection_handler();
if(scon.serverConnection.connected)
scon.serverConnection.send_command("tokenuse", {
token: result
}).then(() => {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
}).catch(error => {
//TODO tr
createErrorModal(tr("Use token"), MessageHelper.formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
});
}).open();
});
_state_updater["permission.upk"] = { item: item, conditions: [condition_connected]};
}
{
const menu = driver.append_item("Tools");
item = menu.append_item(tr("Manage Playlists"));
item.icon('client-music');
item.click(() => {
const scon = server_connections.active_connection_handler();
if(scon && scon.connected) {
Modals.spawnPlaylistManage(scon);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
});
_state_updater["tools.pl"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Ban List"));
item.icon('client-ban_list');
item.click(() => {
const scon = server_connections.active_connection_handler();
if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
Modals.openBanList(scon);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open();
scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
Modals.spawnPlaylistManage(scon);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
});
_state_updater["tools.bl"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Query List"));
item.icon('client-server_query');
item.click(() => {
const scon = server_connections.active_connection_handler();
if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST_OWN).granted(1)) {
Modals.spawnQueryManage(scon);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the server query list")).open();
scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
});
_state_updater["tools.ql"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Query Create"));
item.icon('client-server_query');
item.click(() => {
const scon = server_connections.active_connection_handler();
if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) {
Modals.spawnQueryManage(scon);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open();
scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
});
_state_updater["tools.qc"] = { item: item, conditions: [condition_connected]};
menu.append_hr();
item = menu.append_item(tr("Settings"));
item.icon("client-settings");
item.click(() => Modals.spawnSettingsModal());
}
{
const menu = driver.append_item("Help");
if(!app.is_web()) {
item = menu.append_item(tr("Check for updates"));
item.click(() => native_actions.check_native_update());
item = menu.append_item(tr("Open changelog"));
item.click(() => native_actions.open_change_log());
}
item = menu.append_item(tr("Visit TeaSpeak.de"));
//TODO: Client direct browser?
item.click(() => window.open('https://teaspeak.de/', '_blank'));
item = menu.append_item(tr("Visit TeaSpeak forum"));
//TODO: Client direct browser?
item.click(() => window.open('https://forum.teaspeak.de/', '_blank'));
menu.append_hr();
item = menu.append_item(app.is_web() ? tr("About TeaWeb") : tr("About TeaClient"));
item.click(() => Modals.spawnAbout())
}
update_state();
}
/* default is HTML, the client will override this */
set_driver(html.HTMLMenuBarDriver.instance());
}

View File

@ -60,7 +60,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
private _current_manager: InfoManagerBase = undefined;
private managers: InfoManagerBase[] = [];
private banner_manager: Hostbanner;
constructor(client: ConnectionHandler) {
this.handle = client;
@ -74,8 +73,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
this.managers.push(new ChannelInfoManager());
this.managers.push(new ServerInfoManager());
this.banner_manager = new Hostbanner(client, this._tag_banner);
this._tag.find("button.close").on('click', () => this.close_popover());
}
@ -83,6 +80,16 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
return this._tag;
}
destroy() {
this._tag && this._tag.remove();
this._tag = undefined;
this.managers = undefined;
this._current_manager = undefined;
this.current_selected = undefined;
}
handle_resize() {
/* test if the popover isn't a popover anymore */
if(this._tag.parent().hasClass('shown')) {
@ -90,8 +97,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
if(this.is_popover())
this._tag.parent().addClass('shown');
}
this.banner_manager.handle_resize();
}
setCurrentSelected(entry: AvailableTypes) {
@ -126,10 +131,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
(this._current_manager as InfoManager<AvailableTypes>).updateFrame(this.current_selected, this._tag_info);
}
update_banner() {
this.banner_manager.update();
}
current_manager() { return this._current_manager; }
is_popover() : boolean {
@ -138,7 +139,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
open_popover() {
this._tag.parent().toggleClass('shown', true);
this.banner_manager.handle_resize();
}
close_popover() {
@ -155,161 +155,6 @@ interface Window {
HTMLImageElement: typeof HTMLImageElement;
}
class Hostbanner {
readonly html_tag: JQuery<HTMLElement>;
readonly client: ConnectionHandler;
private updater: NodeJS.Timer;
private _hostbanner_url: string;
constructor(client: ConnectionHandler, htmlTag: JQuery<HTMLElement>) {
this.client = client;
this.html_tag = htmlTag;
}
update() {
if(this.updater) {
clearTimeout(this.updater);
this.updater = undefined;
}
const tag = this.generate_tag();
if(tag) {
tag.then(element => {
const children = this.html_tag.children();
this.html_tag.append(element).removeClass("disabled");
/* allow the new image be loaded from cache URL */
{
children
.css('z-index', '2')
.css('position', 'absolute')
.css('height', '100%')
.css('width', '100%');
setTimeout(() => {
children.detach();
}, 250);
}
}).catch(error => {
console.warn(tr("Failed to load hostbanner: %o"), error);
this.html_tag.empty().addClass("disabled");
})
} else {
this.html_tag.empty().addClass("disabled");
}
}
handle_resize() {
this.html_tag.find("[x-divider-require-resize]").trigger('resize');
}
private generate_tag?() : Promise<JQuery<HTMLElement>> {
if(!this.client.connected) return undefined;
const server = this.client.channelTree.server;
if(!server) return undefined;
if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined;
let properties: any = {};
for(let key in server.properties)
properties["property_" + key] = server.properties[key];
properties["hostbanner_gfx_url"] = server.properties.virtualserver_hostbanner_gfx_url;
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) {
const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60);
const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString();
try {
const url = new URL(server.properties.virtualserver_hostbanner_gfx_url);
if(url.search.length == 0)
properties["hostbanner_gfx_url"] += "?_ts=" + update_timestamp;
else
properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
} catch(error) {
console.warn(tr("Failed to parse banner URL: %o"), error);
properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
}
this.updater = setTimeout(() => this.update(), update_interval * 1000);
}
const rendered = $("#tmpl_selected_hostbanner").renderTag(properties);
/* ration watcher */
if(server.properties.virtualserver_hostbanner_mode == 2) {
const jimage = rendered.find(".meta-image");
if(jimage.length == 0) {
log.warn(LogCategory.SERVER, tr("Missing hostbanner meta image tag"));
} else {
const image = jimage[0];
image.onload = event => {
const image: HTMLImageElement = jimage[0] as any;
rendered.on('resize', event => {
const container = rendered.parent();
container.css('height', null);
container.css('flex-grow', '1');
const max_height = rendered.visible_height();
const max_width = rendered.visible_width();
container.css('flex-grow', '0');
const original_height = image.naturalHeight;
const original_width = image.naturalWidth;
const ratio_height = max_height / original_height;
const ratio_width = max_width / original_width;
const ratio = Math.min(ratio_height, ratio_width);
if(ratio == 0)
return;
const hostbanner_height = ratio * original_height;
container.css('height', Math.ceil(hostbanner_height) + "px");
/* the width is ignorable*/
});
setTimeout(() => rendered.trigger('resize'), 100);
};
}
}
if(window.fetch) {
return (async () => {
const start = Date.now();
const tag_image = rendered.find(".hostbanner-image");
_fetch:
try {
const result = await fetch(properties["hostbanner_gfx_url"]);
if(!result.ok) {
if(result.type === 'opaque' || result.type === 'opaqueredirect') {
log.warn(LogCategory.SERVER, tr("Could not load hostbanner because 'Access-Control-Allow-Origin' isnt valid!"));
break _fetch;
}
}
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.attr('src', url);
log.debug(LogCategory.SERVER, tr("Fetsched hostbanner successfully (%o, type: %o, url: %o)"), Date.now() - start, result.type, url);
} catch(error) {
log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), error);
}
return rendered;
})();
} else {
console.debug(tr("Hostbanner has been loaded"));
return Promise.resolve(rendered);
}
}
}
class ClientInfoManager extends InfoManager<ClientEntry> {
available<V>(object: V): boolean {
return typeof object == "object" && object instanceof ClientEntry;

View File

@ -93,12 +93,19 @@ namespace MessageHelper {
const result: xbbcode.Result = xbbcode.parse(message, {
/* TODO make this configurable and allow IMG */
tag_whitelist: [
"b",
"i",
"u",
"b", "big",
"i", "italic",
"u", "underlined",
"color",
"url"
]
"url",
"code",
"icode",
"i-code",
"ul", "ol", "list",
"li",
/* "img" */
] //[img]https://i.ytimg.com/vi/kgeSTkZssPg/maxresdefault.jpg[/img]
});
/*
if(result.error) {
@ -106,470 +113,58 @@ namespace MessageHelper {
return formatElement(message);
}
*/
return [$.spawn("div").html(result.build_html()).contents() as any];
let html = result.build_html();
if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
html = twemoji.parse(html);
const container = $.spawn("div");
container[0].innerHTML = DOMPurify.sanitize(html, {
ADD_ATTR: [
"x-highlight-type",
"x-code-type"
]
});
container.find("a").attr('target', "_blank");
return [container.contents() as JQuery];
//return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? "&nbsp;" : entry));
}
}
class ChatMessage {
date: Date;
message: JQuery[];
private _html_tag: JQuery<HTMLElement>;
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "XBBCode code tag init",
function: async () => {
/* override default parser */
xbbcode.register.register_parser( {
tag: ["code", "icode", "i-code"],
content_tags_whitelist: [],
constructor(message: JQuery[]) {
this.date = new Date();
this.message = message;
}
build_html(layer: xbbcode.TagLayer) : string {
const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
const language = (layer.options || "").replace("\"", "'").toLowerCase();
private num(num: number) : string {
let str = num.toString();
while(str.length < 2) str = '0' + str;
return str;
}
/* remove heading empty lines */
let text = layer.content.map(e => e.build_text())
.reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "")
.replace(/^([ \n\r\t]*)(?=\n)+/g, "");
if(text.startsWith("\r") || text.startsWith("\n"))
text = text.substr(1);
get html_tag() {
if(this._html_tag) return this._html_tag;
let result: HighlightJSResult;
if(window.hljs.getLanguage(language))
result = window.hljs.highlight(language, text, true);
else
result = window.hljs.highlightAuto(text);
let tag = $.spawn("div");
tag.addClass("message");
let dateTag = $.spawn("div");
dateTag.text("<" + this.num(this.date.getUTCHours()) + ":" + this.num(this.date.getUTCMinutes()) + ":" + this.num(this.date.getUTCSeconds()) + "> ");
dateTag.css("margin-right", "4px");
dateTag.css("color", "dodgerblue");
this._html_tag = tag;
tag.append(dateTag);
this.message.forEach(e => e.appendTo(tag));
return tag;
}
}
class ChatEntry {
readonly handle: ChatBox;
type: ChatType;
key: string;
history: ChatMessage[] = [];
send_history: string[] = [];
owner_unique_id?: string;
private _name: string;
private _html_tag: any;
private _flag_closeable: boolean = true;
private _flag_unread : boolean = false;
private _flag_offline: boolean = false;
onMessageSend: (text: string) => void;
onClose: () => boolean = () => true;
constructor(handle, type : ChatType, key) {
this.handle = handle;
this.type = type;
this.key = key;
this._name = key;
}
appendError(message: string, ...args) {
let entries = MessageHelper.formatMessage(message, ...args);
entries.forEach(e => e.css("color", "red"));
this.pushChatMessage(new ChatMessage(entries));
}
appendMessage(message : string, fmt: boolean = true, ...args) {
this.pushChatMessage(new ChatMessage(MessageHelper.formatMessage(message, ...args)));
}
private pushChatMessage(entry: ChatMessage) {
this.history.push(entry);
while(this.history.length > 100) {
let elm = this.history.pop_front();
elm.html_tag.animate({opacity: 0}, 200, function () {
$(this).detach();
});
}
if(this.handle.activeChat === this) {
let box = $(this.handle.htmlTag).find(".messages");
let mbox = $(this.handle.htmlTag).find(".message_box");
let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height();
mbox.append(entry.html_tag);
entry.html_tag.css("opacity", "0").animate({opacity: 1}, 100);
if(bottom) box.scrollTop(mbox.height());
} else {
this.flag_unread = true;
}
}
displayHistory() {
this.flag_unread = false;
let box = this.handle.htmlTag.find(".messages");
let mbox = box.find(".message_box").detach(); /* detach the message box to improve performance */
mbox.empty();
for(let e of this.history) {
mbox.append(e.html_tag);
/* TODO Is this really totally useless?
Because its at least a performance bottleneck because is(...) recalculates the page style
if(e.htmlTag.is(":hidden"))
e.htmlTag.show();
*/
}
mbox.appendTo(box);
box.scrollTop(mbox.height());
}
get html_tag() {
if(this._html_tag)
return this._html_tag;
let tag = $.spawn("div");
tag.addClass("chat");
if(this._flag_unread)
tag.addClass('unread');
if(this._flag_offline)
tag.addClass('offline');
if(this._flag_closeable)
tag.addClass('closeable');
tag.append($.spawn("div").addClass("chat-type icon " + this.chat_icon()));
tag.append($.spawn("a").addClass("name").text(this._name));
let tag_close = $.spawn("div");
tag_close.addClass("btn_close icon client-tab_close_button");
if(!this._flag_closeable) tag_close.hide();
tag.append(tag_close);
tag.click(() => { this.handle.activeChat = this; });
tag.on("contextmenu", (e) => {
e.preventDefault();
let actions: contextmenu.MenuEntry[] = [];
actions.push({
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "",
name: tr("Clear"),
callback: () => {
this.history = [];
this.displayHistory();
let html = '<pre class="' + klass + '">';
html += '<code class="hljs" x-code-type="' + language + '" x-highlight-type="' + result.language + '">';
html += result.value;
return html + "</code></pre>";
}
});
if(this.flag_closeable) {
actions.push({
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-tab_close_button",
name: tr("Close"),
callback: () => this.handle.deleteChat(this)
});
}
actions.push({
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-tab_close_button",
name: tr("Close all private tabs"),
callback: () => {
//TODO Implement this?
},
visible: false
});
contextmenu.spawn_context_menu(e.pageX, e.pageY, ...actions);
});
tag_close.click(() => {
if($.isFunction(this.onClose) && !this.onClose())
return;
this.handle.deleteChat(this);
});
return this._html_tag = tag;
}
focus() {
this.handle.activeChat = this;
this.handle.htmlTag.find(".input_box").focus();
}
set name(newName : string) {
this._name = newName;
this.html_tag.find(".name").text(this._name);
}
set flag_closeable(flag : boolean) {
if(this._flag_closeable == flag) return;
this._flag_closeable = flag;
this.html_tag.toggleClass('closeable', flag);
}
set flag_unread(flag : boolean) {
if(this._flag_unread == flag) return;
this._flag_unread = flag;
this.html_tag.find(".chat-type").attr("class", "chat-type icon " + this.chat_icon());
this.html_tag.toggleClass('unread', flag);
}
get flag_offline() { return this._flag_offline; }
set flag_offline(flag: boolean) {
if(flag == this._flag_offline)
return;
this._flag_offline = flag;
this.html_tag.toggleClass('offline', flag);
}
private chat_icon() : string {
if(this._flag_unread) {
switch (this.type) {
case ChatType.CLIENT:
return "client-new_chat";
}
}
switch (this.type) {
case ChatType.SERVER:
return "client-server_log";
case ChatType.CHANNEL:
return "client-channel_chat";
case ChatType.CLIENT:
return "client-player_chat";
case ChatType.GENERAL:
return "client-channel_chat";
}
return "";
}
}
class ChatBox {
//https://regex101.com/r/YQbfcX/2
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
static readonly URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
readonly connection_handler: ConnectionHandler;
htmlTag: JQuery;
chats: ChatEntry[];
private _activeChat: ChatEntry;
private _history_index: number = 0;
private _button_send: JQuery;
private _input_message: JQuery;
constructor(connection_handler: ConnectionHandler) {
this.connection_handler = connection_handler;
}
initialize() {
this.htmlTag = $("#tmpl_frame_chat").renderTag();
this._button_send = this.htmlTag.find(".button-send");
this._input_message = this.htmlTag.find(".input-message");
this._button_send.click(this.onSend.bind(this));
this._input_message.on('keypress',event => {
if(!event.shiftKey) {
console.log(event.keyCode);
if(event.keyCode == KeyCode.KEY_RETURN) {
this.onSend();
return false;
} else if(event.keyCode == KeyCode.KEY_UP || event.keyCode == KeyCode.KEY_DOWN) {
if(this._activeChat) {
const message = (this._input_message.val() || "").toString();
const history = this._activeChat.send_history;
if(history.length == 0 || this._history_index > history.length)
return;
if(message.replace(/[ \n\r\t]/, "").length == 0 || this._history_index == 0 || (this._history_index > 0 && message == this._activeChat.send_history[this._history_index - 1])) {
if(event.keyCode == KeyCode.KEY_UP)
this._history_index = Math.min(history.length, this._history_index + 1);
else
this._history_index = Math.max(0, this._history_index - 1);
if(this._history_index > 0)
this._input_message.val(this._activeChat.send_history[this._history_index - 1]);
else
this._input_message.val("");
}
}
}
}
}).on('input', (event) => {
let text = $(event.target).val().toString();
if(this.testMessage(text))
this._button_send.removeAttr("disabled");
else
this._button_send.attr("disabled", "true");
}).trigger("input");
this.chats = [];
this._activeChat = undefined;
this.createChat("chat_server", ChatType.SERVER).onMessageSend = (text: string) => {
if(!this.connection_handler.serverConnection) {
this.serverChat().appendError(tr("Could not send chat message (Not connected)"));
return;
}
this.connection_handler.serverConnection.command_helper.sendMessage(text, ChatType.SERVER).catch(error => {
if(error instanceof CommandResult)
return;
this.serverChat().appendMessage(tr("Failed to send text message."));
log.error(LogCategory.GENERAL, tr("Failed to send server text message: %o"), error);
});
};
this.serverChat().name = tr("Server chat");
this.serverChat().flag_closeable = false;
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
if(!this.connection_handler.serverConnection) {
this.channelChat().appendError(tr("Could not send chant message (Not connected)"));
return;
}
this.connection_handler.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, this.connection_handler.getClient().currentChannel()).catch(error => {
this.channelChat().appendMessage(tr("Failed to send text message."));
log.error(LogCategory.GENERAL, tr("Failed to send channel text message: %o"), error);
});
};
this.channelChat().name = tr("Channel chat");
this.channelChat().flag_closeable = false;
this.connection_handler.permissions.initializedListener.push(flag => {
if(flag) this.activeChat0(this._activeChat);
});
}
createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry {
let chat = new ChatEntry(this, type, key);
this.chats.push(chat);
this.htmlTag.find(".chats").append(chat.html_tag);
if(!this._activeChat) this.activeChat = chat;
return chat;
}
open_chats() : ChatEntry[] {
return this.chats;
}
findChat(key : string) : ChatEntry {
for(let e of this.chats)
if(e.key == key) return e;
return undefined;
}
deleteChat(chat : ChatEntry) {
this.chats.remove(chat);
chat.html_tag.detach();
if(this._activeChat === chat) {
if(this.chats.length > 0)
this.activeChat = this.chats.last();
else
this.activeChat = undefined;
}
}
onSend() {
let text = this._input_message.val().toString();
if(!this.testMessage(text)) return;
this._input_message.val("");
this._input_message.trigger("input");
/* preprocessing text */
const words = text.split(/[ \n]/);
for(let index = 0; index < words.length; index++) {
const flag_escaped = words[index].startsWith('!');
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
_try:
try {
const url = new URL(unescaped);
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
if(url.protocol !== 'http:' && url.protocol !== 'https:')
break _try;
if(flag_escaped)
words[index] = unescaped;
else {
text = undefined;
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
}
} catch(e) { /* word isn't an url */ }
if(unescaped.match(ChatBox.URL_REGEX)) {
if(flag_escaped)
words[index] = unescaped;
else {
text = undefined;
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
}
}
}
text = text || words.join(" ");
if(this._activeChat.send_history.length == 0 || this._activeChat.send_history[0] != text)
this._activeChat.send_history.unshift(text);
while(this._activeChat.send_history.length > 100)
this._activeChat.send_history.pop();
this._history_index = 0;
if(this._activeChat && $.isFunction(this._activeChat.onMessageSend))
this._activeChat.onMessageSend(text);
}
set activeChat(chat : ChatEntry) {
if(this.chats.indexOf(chat) === -1) return;
if(this._activeChat == chat) return;
this.activeChat0(chat);
}
private activeChat0(chat: ChatEntry) {
this._activeChat = chat;
for(let e of this.chats)
e.html_tag.removeClass("active");
let disable_input = !chat;
if(this._activeChat) {
this._activeChat.html_tag.addClass("active");
this._activeChat.displayHistory();
if(!disable_input && this.connection_handler && this.connection_handler.permissions && this.connection_handler.permissions.initialized())
switch (this._activeChat.type) {
case ChatType.CLIENT:
disable_input = false;
break;
case ChatType.SERVER:
disable_input = !this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
break;
case ChatType.CHANNEL:
disable_input = !this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
break;
}
}
this._input_message.prop("disabled", disable_input);
}
get activeChat() : ChatEntry { return this._activeChat; }
channelChat() : ChatEntry {
return this.findChat("chat_channel");
}
serverChat() {
return this.findChat("chat_server");
}
focus(){
this._input_message.focus();
}
private testMessage(message: string) : boolean {
message = message
.replace(/ /gi, "")
.replace(/<br>/gi, "")
.replace(/\n/gi, "")
.replace(/<br\/>/gi, "");
return message.length > 0;
}
},
priority: 10
})
}

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ class ServerConnectionManager {
private _container_log_server: JQuery;
private _container_channel_tree: JQuery;
private _container_hostbanner: JQuery;
private _container_select_info: JQuery;
private _container_chat: JQuery;
@ -32,6 +33,7 @@ class ServerConnectionManager {
this._container_log_server = $("#server-log");
this._container_channel_tree = $("#channelTree");
this._container_hostbanner = $("#hostbanner");
this._container_select_info = $("#select_info");
this._container_chat = $("#chat");
@ -52,7 +54,7 @@ class ServerConnectionManager {
destroy_server_connection_handler(handler: ConnectionHandler) {
this.connection_handlers.remove(handler);
handler.tag_connection_handler.detach();
handler.tag_connection_handler.remove();
this._update_scroll();
this._tag.toggleClass("shown", this.connection_handlers.length > 1);
@ -64,11 +66,14 @@ class ServerConnectionManager {
if(handler === this.active_handler)
this.set_active_connection_handler(this.connection_handlers[0]);
/* destroy all elements */
handler.destroy();
}
set_active_connection_handler(handler: ConnectionHandler) {
if(handler && this.connection_handlers.indexOf(handler) == -1)
throw "Handler hasn't been registrated or is already obsolete!";
throw "Handler hasn't been registered or is already obsolete!";
if(this.active_handler)
this.active_handler.select_info.close_popover();
@ -77,19 +82,22 @@ class ServerConnectionManager {
this._container_select_info.children().detach();
this._container_chat.children().detach();
this._container_log_server.children().detach();
this._container_hostbanner.children().detach();
control_bar.set_connection_handler(handler);
if(handler) {
handler.tag_connection_handler.addClass("active");
this._container_hostbanner.append(handler.hostbanner.html_tag);
this._container_channel_tree.append(handler.channelTree.tag_tree());
this._container_select_info.append(handler.select_info.get_tag());
this._container_chat.append(handler.chat_frame.html_tag());
this._container_chat.append(handler.side_bar.html_tag());
this._container_log_server.append(handler.log.html_tag());
if(handler.invoke_resized_on_activate)
handler.resize_elements();
}
top_menu.update_state();
this.active_handler = handler;
}

View File

@ -0,0 +1,112 @@
class Hostbanner {
readonly html_tag: JQuery<HTMLElement>;
readonly client: ConnectionHandler;
private _destryed = false;
private updater: NodeJS.Timer;
constructor(client: ConnectionHandler) {
this.client = client;
this.html_tag = $.spawn("div").addClass("container-hostbanner");
this.html_tag.on('click', event => {
const server = this.client.channelTree.server;
if(!server || !server.properties.virtualserver_hostbanner_url)
return;
window.open(server.properties.virtualserver_hostbanner_url, '_blank');
});
this.update();
}
destroy() {
if(this.updater) {
clearTimeout(this.updater);
this.updater = undefined;
}
if(this.html_tag) {
this.html_tag.remove();
}
this._destryed = true;
}
update() {
if(this._destryed) return;
if(this.updater) {
clearTimeout(this.updater);
this.updater = undefined;
}
this.html_tag.toggleClass("no-background", !settings.static_global(Settings.KEY_HOSTBANNER_BACKGROUND));
const tag = this.generate_tag();
tag.then(element => {
console.log("Regenrated result: %o", element);
if(!element) {
this.html_tag.empty().addClass("disabled");
return;
}
const children = this.html_tag.children();
this.html_tag.append(element).removeClass("disabled");
/* allow the new image be loaded from cache URL */
{
children
.css('z-index', '2')
.css('position', 'absolute')
.css('height', '100%')
.css('width', '100%');
setTimeout(() => {
children.detach();
}, 250);
}
}).catch(error => {
console.warn(tr("Failed to load hostbanner: %o"), error);
this.html_tag.empty().addClass("disabled");
});
const server = this.client.channelTree.server;
this.html_tag.attr('title', server ? server.properties.virtualserver_hostbanner_url : undefined);
}
private async generate_tag?() : Promise<JQuery | undefined> {
if(!this.client.connected)
return undefined;
const server = this.client.channelTree.server;
if(!server) return undefined;
if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined;
let banner_url = server.properties.virtualserver_hostbanner_gfx_url;
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) {
const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60);
const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString();
try {
const url = new URL(server.properties.virtualserver_hostbanner_gfx_url);
if(url.search.length == 0)
banner_url += "?_ts=" + update_timestamp;
else
banner_url += "&_ts=" + update_timestamp;
} catch(error) {
console.warn(tr("Failed to parse banner URL: %o. Using default '&' append."), error);
banner_url += "&_ts=" + update_timestamp;
}
this.updater = setTimeout(() => this.update(), update_interval * 1000);
}
/* first now load the image */
const image_element = document.createElement("img");
await new Promise((resolve, reject) => {
image_element.onload = resolve;
image_element.onerror = reject;
image_element.src = banner_url;
image_element.style.display = 'none';
document.body.append(image_element);
console.log("Loading image!");
});
image_element.parentNode.removeChild(image_element);
image_element.style.display = 'unset';
return $.spawn("div").addClass("hostbanner-image-container hostbanner-mode-" + server.properties.virtualserver_hostbanner_mode).append($(image_element));
}
}

View File

@ -10,9 +10,18 @@ namespace log {
CONNECTION_FAILED = "connection_failed",
CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed",
CONNECTION_COMMAND_ERROR = "connection_command_error",
GLOBAL_MESSAGE = "global_message",
SERVER_WELCOME_MESSAGE = "server_welcome_message",
SERVER_HOST_MESSAGE = "server_host_message",
SERVER_HOST_MESSAGE_DISCONNECT = "server_host_message_disconnect",
SERVER_CLOSED = "server_closed",
SERVER_BANNED = "server_banned",
SERVER_REQUIRES_PASSWORD = "server_requires_password",
CLIENT_VIEW_ENTER = "client_view_enter",
CLIENT_VIEW_LEAVE = "client_view_leave",
CLIENT_VIEW_MOVE = "client_view_move",
@ -79,6 +88,14 @@ namespace log {
permission: PermissionInfo;
}
export type WelcomeMessage = {
message: string;
}
export type HostMessageDisconnect = {
message: string;
}
//tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}")
//tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}")
//tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}")
@ -158,14 +175,35 @@ namespace log {
reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */
}
export type ConnectionCommandError = {
error: any;
}
export type ClientNicknameChanged = {
own_action: boolean;
own_client: boolean;
client: base.Client;
old_name: string;
new_name: string;
}
export type ClientNicknameChangeFailed = {
reason: string;
}
export type ServerClosed = {
message: string;
}
export type ServerRequiresPassword = {}
export type ServerBanned = {
message: string;
time: number;
invoker: base.Client;
}
}
export type LogMessage = {
@ -188,11 +226,20 @@ namespace log {
"connection_login": event.ConnectionLogin;
"connection_connected": event.ConnectionConnected;
"connection_voice_setup_failed": event.ConnectionVoiceSetupFailed;
"connection_command_error": event.ConnectionCommandError;
"reconnect_scheduled": event.ReconnectScheduled;
"reconnect_canceled": event.ReconnectCanceled;
"reconnect_execute": event.ReconnectExecute;
"server_welcome_message": event.WelcomeMessage;
"server_host_message": event.WelcomeMessage;
"server_host_message_disconnect": event.HostMessageDisconnect;
"server_closed": event.ServerClosed;
"server_requires_password": event.ServerRequiresPassword;
"server_banned": event.ServerBanned;
"client_view_enter": event.ClientEnter;
"client_view_move": event.ClientMove;
"client_view_leave": event.ClientLeave;
@ -208,9 +255,6 @@ namespace log {
type MessageBuilder<T extends keyof server.TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined;
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
"global_message": (data: event.GlobalMessage, options) => {
return [];
},
"error_custom": (data: event.ErrorCustom, options) => {
return [$.spawn("div").addClass("log-error").text(data.message)]
}
@ -242,7 +286,7 @@ namespace log {
}
this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight;
})
});
}
log<T extends keyof server.TypeInfo>(type: T, data: server.TypeInfo[T]) {
@ -263,6 +307,14 @@ namespace log {
return this._html_tag;
}
destroy() {
this._html_tag && this._html_tag.remove();
this._html_tag = undefined;
this._log_container = undefined;
this._log = undefined;
}
private append_log(message: server.LogMessage) {
let container = $.spawn("div").addClass("log-message");
@ -283,7 +335,7 @@ namespace log {
MessageHelper.formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container));
} else {
const elements = builder(message.data, {});
if(!elements)
if(!elements || elements.length == 0)
return; /* discard message */
container.append(...elements);
}
@ -297,7 +349,7 @@ namespace log {
while(messages.length - index > this.history_length)
index++;
const hide_elements = messages.filter(idx => idx < index);
hide_elements.hide(250, () => hide_elements.detach());
hide_elements.hide(250, () => hide_elements.remove());
if(this.auto_follow)
this._html_tag.scrollTop(this._html_tag[0].scrollHeight);
@ -339,7 +391,7 @@ namespace log {
};
MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => {
return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission.name).map(e => e.addClass("log-error"));
return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error"));
};
MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => {
@ -442,6 +494,26 @@ namespace log {
return MessageHelper.formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : "");
}
return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.message + ")")];
};
MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => {
return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]");
};
MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => {
return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]");
};
MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => {
if(data.own_client) {
return MessageHelper.formatMessage(tr("Nickname successfully changed."));
} else {
return MessageHelper.formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name);
}
};
MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => {
return []; /* we do not show global messages within log */
}
}
}

View File

@ -33,11 +33,21 @@ namespace htmltags {
if(properties.client_id)
result = result + "client-id='" + properties.client_id + "' ";
if(properties.client_unique_id && properties.client_unique_id != "unknown")
result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' ";
if(properties.client_unique_id && properties.client_unique_id != "unknown") {
try {
result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' ";
} catch(error) {
console.warn(tr("Failed to generate client tag attribute 'client-unique-id': %o"), error);
}
}
if(properties.client_name)
result = result + "client-name='" + encodeURIComponent(properties.client_name) + "' ";
if(properties.client_name) {
try {
result = result + "client-name='" + encodeURIComponent(properties.client_name) + "' ";
} catch(error) {
console.warn(tr("Failed to generate client tag attribute 'client-name': %o"), error);
}
}
/* add the click handler */
result += "oncontextmenu='return htmltags.callbacks.callback_context_client($(this));'";

View File

@ -0,0 +1,46 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
namespace Modals {
function format_date(date: number) {
const d = new Date(date);
return ('00' + d.getDay()).substr(-2) + "." + ('00' + d.getMonth()).substr(-2) + "." + d.getFullYear() + " - " + ('00' + d.getHours()).substr(-2) + ":" + ('00' + d.getMinutes()).substr(-2);
}
export function spawnAbout() {
const app_version = (() => {
const version_node = document.getElementById("app_version");
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
if(!version) return undefined;
if(version == "unknown" || version.replace(/0+/, "").length == 0)
return undefined;
return version;
})();
const connectModal = createModal({
header: tr("About"),
body: () => {
let tag = $("#tmpl_about").renderTag({
client: false,
version_client: app_version || "in-dev",
version_ui: app_version || "in-dev",
version_timestamp: !!app_version ? format_date(Date.now()) : "--"
});
return tag;
},
footer: null,
width: 600
});
connectModal.htmlTag.find(".modal-body").addClass("modal-about");
connectModal.open();
}
}

View File

@ -0,0 +1,74 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
namespace Modals {
//TODO: Test if we could render this image and not only the browser by knowing the type.
export function spawnAvatarUpload(callback_data: (data: ArrayBuffer | undefined | null) => any) {
const modal = createModal({
header: tr("Avatar Upload"),
footer: undefined,
body: () => {
return $("#tmpl_avatar_upload").renderTag({});
}
});
let _data_submitted = false;
let _current_avatar;
modal.htmlTag.find(".button-select").on('click', event => {
modal.htmlTag.find(".file-inputs").trigger('click');
});
modal.htmlTag.find(".button-delete").on('click', () => {
if(_data_submitted)
return;
_data_submitted = true;
modal.close();
callback_data(null);
});
modal.htmlTag.find(".button-cancel").on('click', () => modal.close());
const button_upload = modal.htmlTag.find(".button-upload");
button_upload.on('click', event => (!_data_submitted) && (_data_submitted = true, modal.close(), true) && callback_data(_current_avatar));
const set_avatar = (data: string | undefined, type?: string) => {
_current_avatar = data ? arrayBufferBase64(data) : undefined;
button_upload.prop("disabled", !_current_avatar);
modal.htmlTag.find(".preview img").attr("src", data ? ("data:image/" + type + ";base64," + data) : "img/style/avatar.png");
};
const input_node = modal.htmlTag.find(".file-inputs")[0] as HTMLInputElement;
input_node.multiple = false;
modal.htmlTag.find(".file-inputs").on('change', event => {
console.log("Files: %o", input_node.files);
const read_file = (file: File) => new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
reader.readAsDataURL(file);
});
(async () => {
const data = await read_file(input_node.files[0]);
if(!data.startsWith("data:image/")) {
console.error(tr("Failed to load file %s: Invalid data media type (%o)"), input_node.files[0].name, data);
createErrorModal(tr("Icon upload failed"), tra("Failed to select avatar {}.<br>File is not an image", input_node.files[0].name)).open();
return;
}
const semi = data.indexOf(';');
const type = data.substring(11, semi);
console.log(tr("Given image has type %s"), type);
set_avatar(data.substr(semi + 8 /* 8 bytes := base64, */), type);
})();
});
set_avatar(undefined);
modal.close_listener.push(() => !_data_submitted && callback_data(undefined));
modal.open();
}
}

View File

@ -10,7 +10,7 @@ namespace Modals {
const lower_nibble = id.charCodeAt(index + 1) - 97;
buffer[index / 2] = (upper_nibble << 4) | lower_nibble;
}
return base64ArrayBuffer(buffer);
return base64_encode_ab(buffer);
};
export const human_file_size = (size: number) => {
@ -80,7 +80,7 @@ namespace Modals {
.css("display", "none")
.appendTo($("body"));
element[0].click();
element.detach();
element.remove();
};
}
}));

View File

@ -216,7 +216,7 @@ namespace Modals {
};
result.clear = () => {
entries = [];
modal.htmlTag.find(".entry-container .entries").children().detach();
modal.htmlTag.find(".entry-container .entries").children().remove();
update_function();
};
result.modal = modal;

View File

@ -252,7 +252,10 @@ namespace Modals {
width: 750
});
modal.close_listener.push(() => control_bar.update_bookmarks());
modal.close_listener.push(() => {
control_bar.update_bookmarks();
top_menu.rebuild_bookmarks();
});
modal.open();
}
}

View File

@ -1,7 +1,100 @@
/// <reference path="../../ui/elements/modal.ts" />
//FIXME: Move this shit out of this file!
namespace connection_log {
//TODO: Save password data
export type ConnectionData = {
name: string;
icon_id: number;
country: string;
clients_online: number;
clients_total: number;
flag_password: boolean;
password_hash: string;
}
export type ConnectionEntry = ConnectionData & {
address: { hostname: string; port: number },
total_connection: number;
first_timestamp: number;
last_timestamp: number;
}
let _history: ConnectionEntry[] = [];
export function log_connect(address: { hostname: string; port: number }) {
let entry = _history.find(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port);
if(!entry) {
_history.push(entry = {
last_timestamp: Date.now(),
first_timestamp: Date.now(),
address: address,
clients_online: 0,
clients_total: 0,
country: 'unknown',
name: 'Unknown',
icon_id: 0,
total_connection: 0,
flag_password: false,
password_hash: undefined
});
}
entry.last_timestamp = Date.now();
entry.total_connection++;
_save();
}
export function update_address_info(address: { hostname: string; port: number }, data: ConnectionData) {
_history.filter(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port).forEach(e => {
for(const key of Object.keys(data)) {
if(typeof(data[key]) !== "undefined") {
e[key] = data[key];
}
}
});
_save();
}
export function update_address_password(address: { hostname: string; port: number }, password_hash: string) {
_history.filter(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port).forEach(e => {
e.password_hash = password_hash;
});
_save();
}
function _save() {
settings.changeGlobal(Settings.KEY_CONNECT_HISTORY, JSON.stringify(_history));
}
export function history() : ConnectionEntry[] {
return _history.sort((a, b) => b.last_timestamp - a.last_timestamp);
}
export function delete_entry(address: { hostname: string; port: number }) {
_history = _history.filter(e => !(e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port));
_save();
}
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: 'connection history load',
priority: 1,
function: async () => {
_history = [];
try {
_history = JSON.parse(settings.global(Settings.KEY_CONNECT_HISTORY, "[]"));
} catch(error) {
log.warn(LogCategory.CLIENT, tr("Failed to load connection history: {}"), error);
}
}
});
}
namespace Modals {
export function spawnConnectModal(defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) {
export function spawnConnectModal(options: {
default_connect_new_tab?: boolean /* default false */
}, defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) {
let selected_profile: profiles.ConnectionProfile;
const random_id = (() => {
@ -10,12 +103,41 @@ namespace Modals {
return array.join("");
})();
const connect_modal = $("#tmpl_connect").renderTag({
client: native_client,
forum_path: settings.static("forum_path"),
password_id: random_id,
multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false)
}).modalize((header, body, footer) => {
const modal = createModal({
header: tr("Connect to a server"),
body: $("#tmpl_connect").renderTag({
client: native_client,
forum_path: settings.static("forum_path"),
password_id: random_id,
multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
default_connect_new_tab: typeof(options.default_connect_new_tab) === "boolean" && options.default_connect_new_tab
}),
footer: () => undefined,
min_width: "25em"
});
modal.htmlTag.find(".modal-body").addClass("modal-connect");
const container_last_servers = modal.htmlTag.find(".container-last-servers");
/* server list toggle */
{
const button = modal.htmlTag.find(".button-toggle-last-servers");
const set_show = shown => {
container_last_servers.toggleClass('shown', shown);
button.find(".arrow").toggleClass('down', shown).toggleClass('up', !shown);
settings.changeGlobal("connect_show_last_servers", shown);
};
button.on('click', event => {
set_show(!container_last_servers.hasClass("shown"));
});
set_show(settings.static_global("connect_show_last_servers", false));
}
const apply = (header, body, footer) => {
const container = modal.htmlTag.find(".container-last-servers .table .body");
const container_empty = container.find(".body-empty");
let current_connect_data: connection_log.ConnectionEntry;
const button_connect = footer.find(".button-connect");
const button_connect_tab = footer.find(".button-connect-new-tab");
const button_manage = body.find(".button-manage-profiles");
@ -25,7 +147,12 @@ namespace Modals {
const input_nickname = body.find(".container-nickname input");
const input_password = body.find(".container-password input");
let updateFields = function () {
let updateFields = (reset_current_data: boolean) => {
if(reset_current_data) {
current_connect_data = undefined;
container.find(".selected").removeClass("selected");
}
console.log("Updating");
if(selected_profile)
input_nickname.attr("placeholder", selected_profile.default_username);
@ -34,7 +161,7 @@ namespace Modals {
let address = input_address.val().toString();
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address);
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN);
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN);
let nickname = input_nickname.val().toString();
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname);
@ -50,17 +177,14 @@ namespace Modals {
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
input_address
.on("keyup", () => updateFields())
.on("keyup", () => updateFields(true))
.on('keydown', event => {
if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey)
button_connect.trigger('click');
});
button_manage.on('click', event => {
const modal = Modals.spawnSettingsModal();
setTimeout(() => {
modal.htmlTag.find(".tab-profiles").parent(".entry").trigger('click');
}, 100);
const modal = Modals.spawnSettingsModal("identity-profiles");
modal.close_listener.push(() => {
input_profile.trigger('change');
});
@ -82,7 +206,7 @@ namespace Modals {
input_nickname.val(selected_profile.default_username);
}
input_profile.toggleClass("is-invalid", !selected_profile || !selected_profile.valid());
updateFields();
updateFields(true);
});
input_profile.val(connect_profile && connect_profile.enforce ? connect_profile.profile.id : connect_profile && connect_profile.profile ? connect_profile.profile.id : 'default').trigger('change');
}
@ -90,20 +214,27 @@ namespace Modals {
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, last_nickname);
input_nickname.val(last_nickname);
input_nickname.on("keyup", () => updateFields());
setTimeout(() => updateFields(), 100);
input_nickname.on("keyup", () => updateFields(true));
setTimeout(() => updateFields(false), 100);
const server_address = () => {
let address = input_address.val().toString();
if(address.match(Regex.IP_V6) && !address.startsWith("["))
return "[" + address + "]";
return address;
};
button_connect.on('click', event => {
connect_modal.close();
modal.close();
const connection = server_connections.active_connection_handler();
if(connection) {
connection.startConnection(
input_address.val().toString(),
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
selected_profile,
true,
{
nickname: input_nickname.val().toString() || selected_profile.default_username,
password: {password: input_password.val().toString(), hashed: false}
password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false}
}
);
} else {
@ -111,24 +242,77 @@ namespace Modals {
}
});
button_connect_tab.on('click', event => {
connect_modal.close();
modal.close();
const connection = server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection);
connection.startConnection(
input_address.val().toString(),
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
selected_profile,
true,
{
nickname: input_nickname.val().toString() || selected_profile.default_username,
password: {password: input_password.val().toString(), hashed: false}
password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false}
}
);
});
}, {
width: '70%'
});
connect_modal.open();
/* server list show */
{
for(const entry of connection_log.history().slice(0, 10)) {
$.spawn("div").addClass("row").append(
$.spawn("div").addClass("column delete").append($.spawn("div").addClass("icon_em client-delete")).on('click', event => {
event.preventDefault();
const row = $(event.target).parents('.row');
row.hide(250, () => {
row.detach();
});
connection_log.delete_entry(entry.address);
container_empty.toggle(container.children().length > 1);
})
).append(
$.spawn("div").addClass("column name").append([
IconManager.generate_tag(IconManager.load_cached_icon(entry.icon_id)),
$.spawn("a").text(entry.name)
])
).append(
$.spawn("div").addClass("column address").text(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : ""))
).append(
$.spawn("div").addClass("column password").text(entry.flag_password ? tr("Yes") : tr("No"))
).append(
$.spawn("div").addClass("column country-name").append([
$.spawn("div").addClass("country flag-" + entry.country.toLowerCase()),
$.spawn("a").text(i18n.country_name(entry.country, tr("Global")))
])
).append(
$.spawn("div").addClass("column clients").text(entry.clients_online + "/" + entry.clients_total)
).append(
$.spawn("div").addClass("column connections").text(entry.total_connection + "")
).on('click', event => {
if(event.isDefaultPrevented())
return;
event.preventDefault();
current_connect_data = entry;
container.find(".selected").removeClass("selected");
$(event.target).parent('.row').addClass('selected');
input_address.val(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : ""));
input_password.val(entry.password_hash ? "WolverinDEV Yeahr!" : "").trigger('change');
}).on('dblclick', event => {
current_connect_data = entry;
button_connect.trigger('click');
}).appendTo(container);
container_empty.toggle(false);
}
}
};
apply(modal.htmlTag, modal.htmlTag, modal.htmlTag);
modal.open();
return;
}

View File

@ -13,41 +13,67 @@ namespace Modals {
});
render_properties["channel_icon_tab"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0);
render_properties["channel_icon_general"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0);
render_properties["create"] = !channel;
let template = $("#tmpl_channel_edit").renderTag(render_properties);
return template.tabify();
},
footer: () => {
let footer = $.spawn("div");
footer.addClass("modal-button-group");
footer.css("margin", "5px");
let buttonCancel = $.spawn("button");
buttonCancel.text(tr("Cancel")).addClass("button_cancel");
let buttonOk = $.spawn("button");
buttonOk.text(tr("Ok")).addClass("button_ok");
footer.append(buttonCancel);
footer.append(buttonOk);
return footer;
/* the tab functionality */
{
const container_tabs = template.find(".container-advanced");
container_tabs.find(".categories .entry").on('click', event => {
const entry = $(event.target);
container_tabs.find(".bodies > .body").addClass("hidden");
container_tabs.find(".categories > .selected").removeClass("selected");
entry.addClass("selected");
container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden");
});
container_tabs.find(".entry").first().trigger('click');
}
/* Advanced/normal switch */
{
const input = template.find(".input-advanced-mode");
const container_mode = template.find(".mode-container");
const container_advanced = container_mode.find(".container-advanced");
const container_simple = container_mode.find(".container-simple");
input.on('change', event => {
const advanced = input.prop("checked");
settings.changeGlobal(Settings.KEY_CHANNEL_EDIT_ADVANCED, advanced);
container_mode.css("overflow", "hidden");
container_advanced.show().toggleClass("hidden", !advanced);
container_simple.show().toggleClass("hidden", advanced);
setTimeout(() => {
container_advanced.toggle(advanced);
container_simple.toggle(!advanced);
container_mode.css("overflow", "visible");
}, 300);
}).prop("checked", settings.static_global(Settings.KEY_CHANNEL_EDIT_ADVANCED)).trigger('change');
}
return template.tabify().children(); /* the "render" div */
},
footer: null,
width: 500
});
modal.htmlTag.find(".modal-body").addClass("modal-channel modal-blue");
applyGeneralListener(connection, properties, modal.htmlTag.find(".general_properties"), modal.htmlTag.find(".button_ok"), channel);
applyStandardListener(connection, properties, modal.htmlTag.find(".settings_standard"), modal.htmlTag.find(".button_ok"), parent, !channel);
applyPermissionListener(connection, properties, modal.htmlTag.find(".settings_permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
applyAudioListener(connection, properties, modal.htmlTag.find(".container-channel-settings-audio"), modal.htmlTag.find(".button_ok"), channel);
applyAdvancedListener(connection, properties, modal.htmlTag.find(".settings_advanced"), modal.htmlTag.find(".button_ok"), channel);
applyGeneralListener(connection, properties, modal.htmlTag.find(".container-general"), modal.htmlTag.find(".button_ok"), channel);
applyStandardListener(connection, properties, modal.htmlTag.find(".container-standard"), modal.htmlTag.find(".container-simple"), parent, channel);
applyPermissionListener(connection, properties, modal.htmlTag.find(".container-permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
applyAudioListener(connection, properties, modal.htmlTag.find(".container-audio"), modal.htmlTag.find(".container-simple"), channel);
applyAdvancedListener(connection, properties, modal.htmlTag.find(".container-misc"), modal.htmlTag.find(".button_ok"), channel);
let updated: PermissionValue[] = [];
modal.htmlTag.find(".button_ok").click(() => {
modal.htmlTag.find(".settings_permissions").find("input[permission]").each((index, _element) => {
modal.htmlTag.find(".container-permissions").find("input[permission]").each((index, _element) => {
let element = $(_element);
if(!element.prop("changed")) return;
if(element.val() == element.attr("original-value")) return;
let permission = permissions.resolveInfo(element.attr("permission"));
if(!permission) {
log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission"));
@ -60,9 +86,13 @@ namespace Modals {
console.log(tr("Updated permissions %o"), updated);
}).click(() => {
modal.close();
for(const key of Object.keys(channel ? channel.properties : {}))
if(channel.properties[key] == properties[key])
delete properties[key];
callback(properties, updated); //First may create the channel
});
tooltip(modal.htmlTag);
modal.htmlTag.find(".button_cancel").click(() => {
modal.close();
callback();
@ -92,8 +122,8 @@ namespace Modals {
tag.find(".button-select-icon").on('click', event => {
Modals.spawnIconSelect(connection, id => {
const icon_node = tag.find(".button-select-icon").find(".icon-node");
icon_node.empty();
const icon_node = tag.find(".icon-preview");
icon_node.children().remove();
icon_node.append(connection.fileManager.icons.generateTag(id));
console.log("Selected icon ID: %d", id);
@ -101,6 +131,15 @@ namespace Modals {
}, channel ? channel.properties.channel_icon_id : 0);
});
tag.find(".button-icon-remove").on('click', event => {
const icon_node = tag.find(".icon-preview");
icon_node.children().remove();
icon_node.append(connection.fileManager.icons.generateTag(0));
console.log("Remove channel icon");
properties.channel_icon_id = 0;
});
{
const channel_password = tag.find(".channel_password");
tag.find(".channel_password").change(function (this: HTMLInputElement) {
@ -120,6 +159,42 @@ namespace Modals {
properties.channel_topic = this.value;
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1));
{
const container = tag.find(".container-description");
const input = container.find("textarea");
const insert_tag = (open: string, close: string) => {
if(input.prop("disabled"))
return;
const node = input[0] as HTMLTextAreaElement;
if (node.selectionStart || node.selectionStart == 0) {
const startPos = node.selectionStart;
const endPos = node.selectionEnd;
node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos);
node.selectionEnd = endPos + open.length;
node.selectionStart = node.selectionEnd;
} else {
node.value += open + close;
node.selectionEnd = node.value.length - close.length;
node.selectionStart = node.selectionEnd;
}
input.focus().trigger('change');
};
input.on('change', event => {
console.log(tr("Channel description edited: %o"), input.val());
properties.channel_description = input.val() as string;
});
container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]'));
container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]'));
container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]'));
container.find(".button-color input").on('change', event => {
insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]')
})
}
tag.find(".channel_description").change(function (this: HTMLInputElement) {
properties.channel_description = this.value;
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1));
@ -132,62 +207,278 @@ namespace Modals {
}
}
function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, parent: ChannelEntry, create: boolean) {
tag.find("input[name=\"channel_type\"]").change(function (this: HTMLInputElement) {
switch(this.value) {
case "semi":
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = true;
break;
case "perm":
properties.channel_flag_permanent = true;
properties.channel_flag_semi_permanent = false;
break;
default:
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = false;
break;
function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, parent: ChannelEntry, channel: ChannelEntry) {
/* Channel type */
{
const input_advanced_type = tag.find("input[name='channel_type']");
let _in_update = false;
const update_simple_type = () => {
if(_in_update)
return;
let type;
if(properties.channel_flag_default || (typeof(properties.channel_flag_default) === "undefined" && channel && channel.properties.channel_flag_default))
type = "def";
else if(properties.channel_flag_permanent || (typeof(properties.channel_flag_permanent) === "undefined" && channel && channel.properties.channel_flag_permanent))
type = "perm";
else if(properties.channel_flag_semi_permanent || (typeof(properties.channel_flag_semi_permanent) === "undefined" && channel && channel.properties.channel_flag_semi_permanent))
type = "semi";
else
type = "temp";
console.log(type);
console.log(Object.assign({}, properties));
simple.find("option[name='channel-type'][value='" + type + "']").prop("selected", true);
};
input_advanced_type.on('change', event => {
switch(input_advanced_type.val()) {
case "semi":
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = true;
break;
case "perm":
properties.channel_flag_permanent = true;
properties.channel_flag_semi_permanent = false;
break;
default:
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = false;
break;
}
update_simple_type();
});
const permission_temp = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1);
const permission_semi = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1);
const permission_perm = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1);
const permission_default = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) &&
connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1);
/* advanced type listeners */
const container_types = tag.find(".container-channel-type");
const tag_type_temp = container_types.find(".type-temp");
const tag_type_semi = container_types.find(".type-semi");
const tag_type_perm = container_types.find(".type-perm");
const select_default = tag.find(".input-flag-default");
{
if(!channel) {
if(permission_perm)
tag_type_perm.find("input").trigger('click');
else if(permission_semi)
tag_type_semi.find("input").trigger('click');
else
tag_type_temp.find("input").trigger('click');
}
select_default.on('change', event => {
const node = select_default[0] as HTMLInputElement;
console.log(node.checked);
properties.channel_flag_default = node.checked;
if(node.checked)
tag_type_perm.find("input").prop("checked", true);
tag_type_temp
.toggleClass("disabled", node.checked || !permission_temp)
.find("input").prop("disabled", node.checked || !permission_temp);
tag_type_semi
.toggleClass("disabled", node.checked || !permission_semi)
.find("input").prop("disabled", node.checked || !permission_semi);
tag_type_perm
.toggleClass("disabled", node.checked || !permission_perm)
.find("input").prop("disabled", node.checked || !permission_perm);
update_simple_type();
}).prop("disabled", !permission_default).trigger('change').parent().toggleClass("disabled", !permission_default);
}
});
tag.find("input[name=\"channel_type\"][value=\"temp\"]")
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1));
tag.find("input[name=\"channel_type\"][value=\"semi\"]")
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1));
tag.find("input[name=\"channel_type\"][value=\"perm\"]")
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1));
if(create)
tag.find("input[name=\"channel_type\"]:not(:disabled)").last().prop("checked", true).trigger('change');
tag.find("input[name=\"channel_default\"]").change(function (this: HTMLInputElement) {
console.log(this.checked);
properties.channel_flag_default = this.checked;
/* simple */
{
simple.find("option[name='channel-type'][value='def']").prop("disabled", !permission_default);
simple.find("option[name='channel-type'][value='perm']").prop("disabled", !permission_perm);
simple.find("option[name='channel-type'][value='semi']").prop("disabled", !permission_semi);
simple.find("option[name='channel-type'][value='temp']").prop("disabled", !permission_temp);
let elements = tag.find("input[name=\"channel_type\"]");
elements.prop("disabled", this.checked);
if(this.checked) {
elements.prop("checked", false);
tag.find("input[name=\"channel_type\"][value=\"perm\"]").prop("checked", true).trigger("change");
simple.find("select[name='channel-type']").on('change', event => {
try {
_in_update = true;
switch ((event.target as HTMLSelectElement).value) {
case "temp":
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = false;
properties.channel_flag_default = false;
select_default.prop("checked", false).trigger('change');
tag_type_temp.trigger('click');
break;
case "semi":
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = true;
properties.channel_flag_default = false;
select_default.prop("checked", false).trigger('change');
tag_type_semi.trigger('click');
break;
case "perm":
properties.channel_flag_permanent = true;
properties.channel_flag_semi_permanent = false;
properties.channel_flag_default = false;
select_default.prop("checked", false).trigger('change');
tag_type_perm.trigger('click');
break;
case "def":
properties.channel_flag_permanent = true;
properties.channel_flag_semi_permanent = false;
properties.channel_flag_default = true;
select_default.prop("checked", true).trigger('change');
break;
}
} finally {
_in_update = false;
/* We dont need to update the simple type because we changed the advanced part to the just changed simple part */
//update_simple_type();
}
});
}
}).prop("disabled",
!connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) ||
!connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1));
}
tag.find("input[name=\"talk_power\"]").change(function (this: HTMLInputElement) {
properties.channel_needed_talk_power = parseInt(this.value);
}).prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1));
/* Talk power */
{
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1);
const input_advanced = tag.find("input[name='talk_power']").prop("disabled", !permission);
const input_simple = simple.find("input[name='talk_power']").prop("disabled", !permission);
let orderTag = tag.find(".order_id");
for(let channel of (parent ? parent.children() : connection.channelTree.rootChannel()))
$.spawn("option").attr("channelId", channel.channelId.toString()).text(channel.channelName()).appendTo(orderTag);
input_advanced.on('change', event => {
properties.channel_needed_talk_power = parseInt(input_advanced.val() as string);
input_simple.val(input_advanced.val());
});
orderTag.change(function (this: HTMLSelectElement) {
let selected = $(this.options.item(this.selectedIndex));
properties.channel_order = parseInt(selected.attr("channelId"));
}).prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1));
orderTag.find("option").last().prop("selected", true);
input_simple.on('change', event => {
properties.channel_needed_talk_power = parseInt(input_simple.val() as string);
input_advanced.val(input_simple.val());
});
}
/* Channel order */
{
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1);
const advanced_order_id = tag.find(".order_id").prop("disabled", !permission) as JQuery<HTMLSelectElement>;
const simple_order_id = simple.find(".order_id").prop("disabled", !permission) as JQuery<HTMLSelectElement>;
for(let previous_channel of (parent ? parent.children() : connection.channelTree.rootChannel())) {
let selected = channel && channel.properties.channel_order == previous_channel.channelId;
$.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(advanced_order_id);
$.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(simple_order_id);
}
advanced_order_id.on('change', event => {
simple_order_id[0].selectedIndex = advanced_order_id[0].selectedIndex;
const selected = $(advanced_order_id[0].options.item(advanced_order_id[0].selectedIndex));
properties.channel_order = parseInt(selected.attr("channelId"));
});
simple_order_id.on('change', event => {
advanced_order_id[0].selectedIndex = simple_order_id[0].selectedIndex;
const selected = $(simple_order_id[0].options.item(simple_order_id[0].selectedIndex));
properties.channel_order = parseInt(selected.attr("channelId"));
});
}
/* Advanced only */
{
const container_max_users = tag.find(".container-max-users");
const container_unlimited = container_max_users.find(".container-unlimited");
const container_limited = container_max_users.find(".container-limited");
const input_unlimited = container_unlimited.find("input[value='unlimited']");
const input_limited = container_limited.find("input[value='limited']");
const input_limit = container_limited.find(".channel_maxclients");
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1);
if(!permission) {
input_unlimited.prop("disabled", true);
input_limited.prop("disabled", true);
input_limit.prop("disabled", true);
container_limited.addClass("disabled");
container_unlimited.addClass("disabled");
} else {
container_max_users.find("input[name='max_users']").on('change', event => {
const node = event.target as HTMLInputElement;
console.log(tr("Channel max user mode: %o"), node.value);
const flag = node.value === "unlimited";
input_limit
.prop("disabled", flag)
.parent().toggleClass("disabled", flag);
properties.channel_flag_maxclients_unlimited = flag;
});
input_limit.on('change', event => {
properties.channel_maxclients = parseInt(input_limit.val() as string);
console.log(tr("Changed max user limit to %o"), properties.channel_maxclients);
});
setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100);
}
}
{
const container_max_users = tag.find(".container-max-family-users");
const container_unlimited = container_max_users.find(".container-unlimited");
const container_inherited = container_max_users.find(".container-inherited");
const container_limited = container_max_users.find(".container-limited");
const input_unlimited = container_unlimited.find("input[value='unlimited']");
const input_inherited = container_inherited.find("input[value='inherited']");
const input_limited = container_limited.find("input[value='limited']");
const input_limit = container_limited.find(".channel_maxfamilyclients");
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1);
if(!permission) {
input_unlimited.prop("disabled", true);
input_inherited.prop("disabled", true);
input_limited.prop("disabled", true);
input_limit.prop("disabled", true);
container_limited.addClass("disabled");
container_unlimited.addClass("disabled");
container_inherited.addClass("disabled");
} else {
container_max_users.find("input[name='max_family_users']").on('change', event => {
const node = event.target as HTMLInputElement;
console.log(tr("Channel max family user mode: %o"), node.value);
const flag_unlimited = node.value === "unlimited";
const flag_inherited = node.value === "inherited";
input_limit
.prop("disabled", flag_unlimited || flag_inherited)
.parent().toggleClass("disabled", flag_unlimited || flag_inherited);
properties.channel_flag_maxfamilyclients_unlimited = flag_unlimited;
properties.channel_flag_maxfamilyclients_inherited = flag_inherited;
});
input_limit.on('change', event => {
properties.channel_maxfamilyclients = parseInt(input_limit.val() as string);
console.log(tr("Changed max family user limit to %o"), properties.channel_maxfamilyclients);
});
setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100);
}
}
}
function applyPermissionListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) {
let apply_permissions = (channel_permissions: PermissionValue[]) => {
console.log(tr("Got permissions: %o"), channel_permissions);
@ -200,6 +491,9 @@ namespace Modals {
tag.find("input[permission]").each((index, _element) => {
let element = $(_element);
element.attr("original-value", 0);
element.val(0);
let permission = permissions.resolveInfo(element.attr("permission"));
if(!permission) {
log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission"));
@ -207,23 +501,16 @@ namespace Modals {
return;
}
let old_value: number = 0;
element.on("click keyup", () => {
console.log(tr("Permission triggered! %o"), element.val() != old_value);
element.prop("changed", element.val() != old_value);
});
for(let cperm of channel_permissions)
if(cperm.type == permission) {
element.val(old_value = cperm.value);
element.val(cperm.value);
element.attr("original-value", cperm.value);
return;
}
element.val(0);
});
if(!permissions.neededPermission(PermissionType.I_CHANNEL_MODIFY_POWER).granted(required_power, false)) {
tag.find("input[permission]").prop("disabled", false); //No permissions
}
const permission = permissions.neededPermission(PermissionType.I_CHANNEL_MODIFY_POWER).granted(required_power, false);
tag.find("input[permission]").prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); //No permissions
};
if(channel) {
@ -234,7 +521,17 @@ namespace Modals {
} else apply_permissions([]);
}
function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, channel?: ChannelEntry) {
const bandwidth_mapping = [
/* SPEEX narrow */ [2.49, 2.69, 2.93, 3.17, 3.17, 3.56, 3.56, 4.05, 4.05, 4.44, 5.22],
/* SPEEX wide */ [2.69, 2.93, 3.17, 3.42, 3.76, 4.25, 4.74, 5.13, 5.62, 6.40, 7.37],
/* SPEEX ultra */ [2.73, 3.12, 3.37, 3.61, 4.00, 4.49, 4.93, 5.32, 5.81, 6.59, 7.57],
/* CELT */ [6.10, 6.10, 7.08, 7.08, 7.08, 8.06, 8.06, 8.06, 8.06, 10.01, 13.92],
/* Opus Voice */ [2.73, 3.22, 3.71, 4.20, 4.74, 5.22, 5.71, 6.20, 6.74, 7.23, 7.71],
/* Opus Music */ [3.08, 3.96, 4.83, 5.71, 6.59, 7.47, 8.35, 9.23, 10.11, 10.99, 11.87]
];
let update_template = () => {
let codec = properties.channel_codec;
if(!codec && channel)
@ -246,14 +543,25 @@ namespace Modals {
quality = channel.properties.channel_codec_quality;
if(!quality) return;
if(codec == 4 && quality == 4)
tag.find("input[name=\"voice_template\"][value=\"voice_mobile\"]").prop("checked", true);
else if(codec == 4 && quality == 6)
tag.find("input[name=\"voice_template\"][value=\"voice_desktop\"]").prop("checked", true);
else if(codec == 5 && quality == 6)
tag.find("input[name=\"voice_template\"][value=\"music\"]").prop("checked", true);
let template_name = "custom";
{
if(codec == 4 && quality == 4)
template_name = "voice_mobile";
else if(codec == 4 && quality == 6)
template_name = "voice_desktop";
else if(codec == 5 && quality == 6)
template_name = "music";
}
tag.find("input[name='voice_template'][value='" + template_name + "']").prop("checked", true);
simple.find("option[name='voice_template'][value='" + template_name + "']").prop("selected", true);
let bandwidth;
if(codec < 0 || codec > bandwidth_mapping.length)
bandwidth = 0;
else
tag.find("input[name=\"voice_template\"][value=\"custom\"]").prop("checked", true);
bandwidth = bandwidth_mapping[codec][quality] || 0; /* OOB access results in undefined, but is allowed */
tag.find(".container-needed-bandwidth").text(bandwidth.toFixed(2) + " KiB/s");
};
let change_codec = codec => {
@ -264,20 +572,30 @@ namespace Modals {
update_template();
};
let quality_slider = tag.find(".voice_quality_slider");
let quality_number = tag.find(".voice_quality_number");
const container_quality = tag.find(".container-quality");
const slider_quality = sliderfy(container_quality.find(".container-slider"), {
initial_value: properties.channel_codec_quality || 6,
unit: "",
min_value: 1,
max_value: 10,
step: 1,
value_field: container_quality.find(".container-value")
});
let change_quality = (quality: number) => {
if(properties.channel_codec_quality == quality) return;
properties.channel_codec_quality = quality;
if(quality_slider.val() != quality)
quality_slider.val(quality);
if(parseInt(quality_number.text()) != quality)
quality_number.text(quality);
slider_quality.value(quality);
update_template();
};
tag.find("input[name=\"voice_template\"]").change(function (this: HTMLInputElement) {
container_quality.find(".container-slider").on('change', event => {
properties.channel_codec_quality = slider_quality.value();
update_template();
});
tag.find("input[name='voice_template']").change(function (this: HTMLInputElement) {
switch(this.value) {
case "custom":
break;
@ -295,12 +613,43 @@ namespace Modals {
break;
}
});
tag.find("input[name=\"voice_template\"][value=\"voice_mobile\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
tag.find("input[name=\"voice_template\"][value=\"voice_desktop\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
tag.find("input[name=\"voice_template\"][value=\"music\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
simple.find("select[name='voice_template']").change(function (this: HTMLInputElement) {
switch(this.value) {
case "custom":
break;
case "music":
change_codec(5);
change_quality(6);
break;
case "voice_desktop":
change_codec(4);
change_quality(6);
break;
case "voice_mobile":
change_codec(4);
change_quality(4);
break;
}
});
/* disable not granted templates */
{
tag.find("input[name='voice_template'][value='voice_mobile']")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
simple.find("option[name='voice_template'][value='voice_mobile']")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
tag.find("input[name='voice_template'][value=\"voice_desktop\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
simple.find("option[name='voice_template'][value=\"voice_desktop\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
tag.find("input[name='voice_template'][value=\"music\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
simple.find("option[name='voice_template'][value=\"music\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
}
let codecs = tag.find(".voice_codec option");
codecs.eq(0).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8).granted(1));
@ -323,8 +672,6 @@ namespace Modals {
change_quality(channel.properties.channel_codec_quality);
}
update_template();
quality_slider.on('input', event => change_quality(parseInt(quality_slider.val() as string)));
}
function applyAdvancedListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
@ -332,58 +679,26 @@ namespace Modals {
properties.channel_topic = this.value;
});
tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) {
properties.channel_delete_delay = parseInt(this.value);
}).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1));
tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) {
properties.channel_codec_is_unencrypted = parseInt(this.value) == 0;
}).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1));
{
let tag_infinity = tag.find("input[name=\"max_users\"][value=\"infinity\"]");
let tag_limited = tag.find("input[name=\"max_users\"][value=\"limited\"]");
let tag_limited_value = tag.find(".channel_maxclients");
if(!connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) {
tag_infinity.prop("disabled", true);
tag_limited.prop("disabled", true);
tag_limited_value.prop("disabled", true);
} else {
tag.find("input[name=\"max_users\"]").change(function (this: HTMLInputElement) {
console.log(this.value);
let infinity = this.value == "infinity";
tag_limited_value.prop("disabled", infinity);
properties.channel_flag_maxclients_unlimited = infinity;
});
tag_limited_value.change(event => properties.channel_maxclients = parseInt(tag_limited_value.val() as string));
tag.find("input[name=\"max_users\"]:checked").trigger('change');
}
const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1);
tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) {
properties.channel_delete_delay = parseInt(this.value);
}).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission);
}
{
let tag_inherited = tag.find("input[name=\"max_users_family\"][value=\"inherited\"]");
let tag_infinity = tag.find("input[name=\"max_users_family\"][value=\"infinity\"]");
let tag_limited = tag.find("input[name=\"max_users_family\"][value=\"limited\"]");
let tag_limited_value = tag.find(".channel_maxfamilyclients");
tag.find(".button-delete-max").on('click', event => {
const power = connection.permissions.neededPermission(PermissionType.I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY).value;
let value = power == -2 ? 0 : power == -1 ? (7 * 24 * 60 * 60) : power;
tag.find(".channel_delete_delay").val(value).trigger('change');
});
}
if(!connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) {
tag_inherited.prop("disabled", true);
tag_infinity.prop("disabled", true);
tag_limited.prop("disabled", true);
tag_limited_value.prop("disabled", true);
} else {
tag.find("input[name=\"max_users_family\"]").change(function (this: HTMLInputElement) {
console.log(this.value);
tag_limited_value.prop("disabled", this.value != "limited");
properties.channel_flag_maxfamilyclients_unlimited = this.value == "infinity";
properties.channel_flag_maxfamilyclients_inherited = this.value == "inherited";
});
tag_limited_value.change(event => properties.channel_maxfamilyclients = parseInt(tag_limited_value.val() as string));
tag.find("input[name=\"max_users_family\"]:checked").trigger('change');
}
{
const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1);
tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) {
properties.channel_codec_is_unencrypted = parseInt(this.value) == 0;
}).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission);
}
}
}

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