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: # Changelog:
* **XX.XX.XX** * **XX.XX.XX**
- Removed icon size restriction for SVGs - Removed icon size restriction for SVGs
- Fixed permission editor icon select for not granted icon permissions - 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** * **22.06.19**
- Fixed channel create not working issue - Fixed channel create not working issue

View File

@ -22,4 +22,8 @@ html, body {
display: flex; flex-direction: column; resize: both; 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/", "path" => "js/",
"local-path" => "./shared/js/", "local-path" => "./shared/js/",
"req-parm" => ["-js-map"] "req-parm" => ["--mappings"]
], ],
[ /* shared generated worker codec */ [ /* shared generated worker codec */
"type" => "js", "type" => "js",
@ -52,6 +52,15 @@
"path" => "css/", "path" => "css/",
"local-path" => "./shared/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 */ [ /* shared release css files */
"type" => "css", "type" => "css",
"search-pattern" => "/.*\.css$/", "search-pattern" => "/.*\.css$/",
@ -137,7 +146,7 @@
$APP_FILE_LIST_SHARED_VENDORS = [ $APP_FILE_LIST_SHARED_VENDORS = [
[ [
"type" => "js", "type" => "js",
"search-pattern" => "/.*\.js$/", "search-pattern" => "/.*(\.min)?\.js$/",
"build-target" => "dev|rel", "build-target" => "dev|rel",
"path" => "vendor/", "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_activated;See you soon
away_deactivated;Welcome back away_deactivated;Welcome back
#Microphone
microphone.muted;Microphone muted
microphone.activated;Microphone activated
#Sound
sound.muted;Sound muted
sound.activated;Sound activated
#Connection #Connection
connection.connected;Connected connection.connected;Connected
connection.disconnected;Disconnected 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.moved;User was moved out of your channel
user.left.disconnect;User disconnected from your channel user.left.disconnect;User disconnected from your channel
user.left.banned;User in your channel was banned from the server user.left.banned;User in your channel was banned from the server
user.left.timeout;User in your channel timed out
#Error #Error
error.insufficient_permissions;insufficient permissions 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 { declare namespace connection {
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection; export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection;
export function destroy_server_connection(handle: AbstractServerConnection);
} }

View File

@ -1,4 +1,15 @@
@import "properties"; @import "properties";
@import "mixin";
.channel-tree-container {
height: 100%;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
overflow-y: auto;
}
/* the channel tree */ /* the channel tree */
.channel-tree { .channel-tree {
@ -71,6 +82,11 @@
align-self: center; align-self: center;
color: $channel_tree_entry_text_color; color: $channel_tree_entry_text_color;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.icon_property { .icon_property {
@ -84,6 +100,8 @@
flex-direction: column; flex-direction: column;
.container-channel { .container-channel {
position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: stretch; justify-content: stretch;
@ -94,6 +112,39 @@
align-items: center; align-items: center;
cursor: pointer; 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 { .channel-type {
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
@ -125,6 +176,17 @@
.channel-name { .channel-name {
align-self: center; align-self: center;
color: $channel_tree_entry_text_color; 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 { .client-name {
line-height: 16px;
flex-grow: 0;
flex-shrink: 1;
padding-right: .25em;
color: $channel_tree_entry_text_color;
&.client-name-own { &.client-name-own {
font-weight: bold; font-weight: bold;
} }
}
line-height: 16px; .client-away-message {
flex-grow: 1;
flex-shrink: 1;
min-width: 75px;
color: $channel_tree_entry_text_color; color: $channel_tree_entry_text_color;
} }
@ -191,7 +257,8 @@
margin-right: 0; /* override from previous thing */ margin-right: 0; /* override from previous thing */
position: absolute; position: absolute;
right: 5px; right: 0;
padding-right: 5px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -211,14 +278,30 @@
} }
&.selected { &.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 { .client-name {
&:focus { &:focus {
position: absolute;
color: black; color: black;
padding-top: 1px; padding-top: 1px;
padding-bottom: 1px; padding-bottom: 1px;
z-index: 1000; z-index: 1000;
margin-right: -10px;
margin-left: 18px;
width: 100%;
} }
} }
} }

View File

@ -1,3 +1,5 @@
@import "mixin";
.container-connection-handlers { .container-connection-handlers {
$animation_length: .25s; $animation_length: .25s;
@ -14,10 +16,7 @@
background-color: transparent; background-color: transparent;
-webkit-user-select: none; @include user-select(none);
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative; position: relative;
@ -32,6 +31,8 @@
overflow-x: auto; overflow-x: auto;
overflow-y: visible; overflow-y: visible;
max-width: 100%;
.connection-container { .connection-container {
padding-top: 4px; padding-top: 4px;
position: relative; position: relative;
@ -57,25 +58,13 @@
color: #a8a8a8; color: #a8a8a8;
align-self: center; align-self: center;
margin-right: -5px; /* 5px padding which have to be overcommed */ margin-right: 20px;
position: relative; position: relative;
max-width: 16em;
overflow: visible; overflow: visible;
text-overflow: clip; text-overflow: clip;
white-space: nowrap; 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 { .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 { &:hover {
background-color: #242425; background-color: #242425;
} }

View File

@ -86,65 +86,66 @@
position: absolute; position: absolute;
margin-left: 3px; margin-left: 3px;
} }
}
}
.checkbox { /* we call it "ccheckbox" else it will be messed up the the global checkbox */
margin-top: 1px; .ccheckbox {
margin-left: 1px; margin-top: 1px;
display: block; margin-left: 1px;
position: relative; display: block;
padding-left: 14px; position: relative;
margin-bottom: 12px; padding-left: 14px;
cursor: pointer; margin-bottom: 12px;
font-size: 22px; cursor: pointer;
-webkit-user-select: none; font-size: 22px;
-moz-user-select: none; -webkit-user-select: none;
-ms-user-select: none; -moz-user-select: none;
user-select: none; -ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */ /* Hide the browser's default checkbox */
input { input {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
cursor: pointer; cursor: pointer;
display: none; display: none;
} }
.checkmark { .checkmark {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
height: 11px; height: 11px;
width: 11px; width: 11px;
background-color: #eee; background-color: #eee;
&:after { &:after {
content: ""; content: "";
position: absolute; position: absolute;
display: none; display: none;
left: 4px; left: 4px;
top: 1px; top: 1px;
width: 3px; width: 3px;
height: 7px; height: 7px;
border: solid white; border: solid white;
border-width: 0 2px 2px 0; border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg); -webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg); -ms-transform: rotate(45deg);
transform: rotate(45deg); transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
} }
} }
&: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); $border_color_activated: rgba(255, 255, 255, .75);
/* max height is 2em */
.control_bar { .control_bar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
-webkit-user-select: none; @include user-select(none);
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
height: 100%; height: 100%;
align-items: center; align-items: center;
@ -25,8 +26,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
.button, .dropdown-arrow { .button, .dropdown-arrow {
text-align: center; text-align: center;
border: 1px solid rgba(0, 0, 0, 0); border: .05em solid rgba(0, 0, 0, 0);
border-radius: 3px; border-radius: $border_radius_small;
background-color: #454545; 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 { > .icon_x24 {
vertical-align: middle; vertical-align: middle;
} }
@ -69,9 +72,25 @@ $border_color_activated: rgba(255, 255, 255, .75);
margin-left: 5px; margin-left: 5px;
&:not(.icon_x24) { &:not(.icon_x24) {
min-width: 28px; min-width: 2em;
max-width: 28px; max-width: 2em;
height: 28px; 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; position: relative;
.buttons { .buttons {
margin-top: 1px; height: 2em;
height: 28px;
align-items: center; align-items: center;
@ -89,14 +107,14 @@ $border_color_activated: rgba(255, 255, 255, .75);
flex-direction: row; flex-direction: row;
.dropdown-arrow { .dropdown-arrow {
height: 28px; height: 2em;
display: inline-flex; display: inline-flex;
justify-content: space-around; justify-content: space-around;
width: 18px; width: 1.5em;
cursor: pointer; cursor: pointer;
border-radius: 0 3px 3px 0; border-radius: 0 $border_radius_small $border_radius_small 0;
align-items: center; align-items: center;
border-left: 0; border-left: 0;
} }
@ -131,8 +149,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
background-color: #2d3032; background-color: #2d3032;
align-items: center; align-items: center;
border: 1px solid #2c2525; border: .05em solid #2c2525;
border-radius: 0 5px 5px 5px; border-radius: 0 $border_radius_middle $border_radius_middle $border_radius_middle;
width: 230px; width: 230px;
@ -143,7 +161,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
right: 0; right: 0;
} }
.icon { .icon, .icon-container, .icon_em {
vertical-align: middle; vertical-align: middle;
margin-right: 5px; margin-right: 5px;
} }
@ -159,16 +177,16 @@ $border_color_activated: rgba(255, 255, 255, .75);
} }
& > div:first-of-type { & > div:first-of-type {
border-radius: 2px 2px 0 0; border-radius: .1em .1em 0 0;
} }
& > div:last-of-type { & > div:last-of-type {
border-radius: 0 0 2px 2px; border-radius: 0 0 .1em .1em;
} }
&.display_left { &.display_left {
margin-left: -179px; 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; flex-direction: column;
padding-right: 0; padding-right: 0;
padding-left: 0; padding-left: 0;
.hostbanner {
overflow: hidden;
}
} }
} }
@ -84,42 +80,6 @@
display: none; display: none;
margin-bottom: 5px; 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 { .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; min-height: 330px;
.container-app-main { .container-app-main {
height: 100%;
width: 100%;
min-height: 500px;
margin-top: 5px; margin-top: 5px;
position: relative; position: relative;
@ -15,14 +19,14 @@ $animation_length: .5s;
flex-direction: column; flex-direction: column;
justify-content: stretch; justify-content: stretch;
height: 100%;
width: 100%;
.container-channel-chat { .container-channel-chat {
min-height: 200px; height: 80%; /* "default" settings */
min-width: 100px;
width: 100%; width: 100%;
min-height: 25em;
min-width: 100px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: stretch; justify-content: stretch;
@ -34,25 +38,44 @@ $animation_length: .5s;
border-radius: 5px; border-radius: 5px;
} }
.container-channel-tree { > .container-channel-tree {
width: 50%; /* "default" settings */
height: 100%;
background: #353535; background: #353535;
min-width: 200px; min-width: 200px;
display: flex; display: flex;
flex-direction: column;
justify-content: stretch; justify-content: stretch;
height: 100%;
min-height: 100px; min-height: 100px;
padding-top: 5px;
/*
overflow: auto;
overflow-x: visible;
*/
overflow: hidden; 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; background: #353535;
min-width: 350px; min-width: 350px;
@ -62,16 +85,66 @@ $animation_length: .5s;
} }
} }
.container-server-log {
min-height: 0; > .container-bottom {
height: 250px; height: 20%;
min-height: 1.5em;
width: 100%; width: 100%;
border-radius: 5px; display: flex;
padding-right: 5px; flex-direction: column;
padding-left: 5px; 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; border-radius: 5px;
height: 30px; height: 2em;
width: 100%; width: 100%;
background-color: #454545; background-color: #454545;
@ -121,39 +194,6 @@ $animation_length: .5s;
} }
@media only screen and (max-width: $small_device) { @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 { .hide-small {
display: none; display: none;
opacity: 0; 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 { .modal .modal-connect {
@include user-select(none);
/* font-size: 1rem;
margin-top: 5px; max-width: 100000px; /* max 100000px width, else we shrink the modal */
padding: 0!important; /* override the default padding */
> div:not(:first-of-type) { display: flex;
margin-top: 5px; flex-direction: column;
} justify-content: stretch;
.profile-select-container { .container-connect-input {
display: flex; flex-grow: 0;
flex-direction: row; flex-shrink: 0;
justify-content: space-between;
select { /* apply the default padding */
width: 150px; 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; display: flex;
flex-direction: column; 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; justify-content: stretch;
.container-address { max-height: 0;
flex-grow: 1; opacity: 0;
flex-shrink: 1; 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 { hr {
flex-grow: 0; height: 0;
flex-shrink: 4; 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 { @media all and (max-width: 55rem) {
display: flex; .modal .modal-connect {
flex-direction: row; min-width: calc(21.25em + 24px * 2)!important;
justify-content: stretch; width: 1000em; /* allocate space */
.container-select-profile { .container-address-password {
flex-grow: 1; .container-password {
flex-shrink: 1; min-width: unset!important;
margin-left: 1em!important;
}
} }
.container-manage { .container-buttons {
flex-grow: 0; justify-content: flex-end!important;
flex-shrink: 4;
margin-left: 15px; .button-toggle-last-servers {
display: none;
}
} }
}
.invalid-feedback { .container-profile-name {
position: absolute; 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 { .channel_perm_tbl .value {
width: 60px; width: 60px;
} }
@ -217,9 +88,13 @@ modal-body {
.arrow { .arrow {
display: inline-block; display: inline-block;
border: solid black; border: solid black;
border-width: 0 3px 3px 0; //border-width: 0 3px 3px 0;
padding: 3px; //padding: 3px;
height: 10px; //height: 10px;
border-width: 0 .2em .2em 0;
padding: .21em;
height: .5em;
&.right { &.right {
transform: rotate(-45deg); 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_selected: #2d2d2d;
$channel_tree_entry_hovered: #393939; $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 { .container-log {
display: block; display: block;
overflow-y: auto; overflow-y: auto;
@ -5,6 +7,9 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
@include chat-scrollbar-vertical();
@include chat-scrollbar-horizontal();
.container-messages { .container-messages {
width: 100%; width: 100%;
line-height: 16px; line-height: 16px;
@ -33,7 +38,7 @@
font-family: sans-serif; font-family: sans-serif;
font-size: 13px; font-size: 13px;
line-height: 1; line-height: initial;
} }
> .timestamp { > .timestamp {

View File

@ -5,6 +5,8 @@
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
background-position: 0 -2717px; /* by default use global flag */
} }
.country.flag-ad { .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) This should be executed with python 2.7 (because of pydub)
Used voice: UK-Graham
""" """
import os import os
import os.path
import string
import base64
import sys
import requests import requests
import json import json
import csv import csv
@ -12,7 +18,8 @@ from pydub import AudioSegment
TARGET_DIRECTORY = "audio/speech" TARGET_DIRECTORY = "audio/speech"
SOURCE_FILE = "audio/speech_sentences.csv" 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): def tts(text, file):
voice_id = 4 voice_id = 4
language_id = 1 language_id = 1
@ -43,33 +50,102 @@ def tts(text, file):
sound.export(file, format="wav") sound.export(file, format="wav")
os.remove(file + ".mp3") os.remove(file + ".mp3")
"""
def main(): def main():
if os.path.exists(TARGET_DIRECTORY): if False:
print("Deleting old speach directory (%s)!" % TARGET_DIRECTORY) if os.path.exists(TARGET_DIRECTORY):
try: print("Deleting old speach directory (%s)!" % TARGET_DIRECTORY)
shutil.rmtree(TARGET_DIRECTORY) try:
except e: shutil.rmtree(TARGET_DIRECTORY)
print("Cant delete old dir!") except e:
os.makedirs(TARGET_DIRECTORY) print("Cant delete old dir!")
try:
os.makedirs(TARGET_DIRECTORY)
except:
pass
mapping_file = 'audio/speech/mapping.json'
mapping = [] mapping = []
with open(mapping_file, "r") as fstream:
mapping = json.loads(fstream.read())
tts_queue = []
with open(SOURCE_FILE, 'r') as input: with open(SOURCE_FILE, 'r') as input:
reader = csv.reader(filter(lambda row: len(row) != 0 and row[0] != '#', input), delimiter=';', quotechar='#') reader = csv.reader(filter(lambda row: len(row) != 0 and row[0] != '#', input), delimiter=';', quotechar='#')
for row in reader: for row in reader:
if len(row) != 2: if len(row) != 2:
continue 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}) file = TARGET_DIRECTORY + "/" + "{}.wav".format(row[0])
except e:
print(e)
print("Failed to generate {}", 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.write(json.dumps(mapping))
fstream.close() fstream.close()

View File

@ -1,46 +1,17 @@
<?php <?php
$testXF = false; ini_set('display_errors', 1);
$localhost = false; ini_set('display_startup_errors', 1);
$_INCLIDE_ONLY = true; error_reporting(E_ALL);
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();
}
$WEB_CLIENT = http_response_code() !== false; $WEB_CLIENT = http_response_code() !== false;
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
<meta charset="UTF-8"> <meta charset="UTF-8">
<!-- App min width: 450px --> <!-- 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="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, min-zoom=1, max-zoom: 1, user-scalable=no">
<meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." /> <meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." />
<link rel="icon" href="img/favicon/teacup.png">
<meta name="keywords" content="TeaSpeak, TeaWeb, TeaSpeak-Web,Web client TeaSpeak, веб клієнт TeaSpeak, TSDNS, багатомовність, мультимовність, теми, функціонал"/> <meta name="keywords" content="TeaSpeak, TeaWeb, TeaSpeak-Web,Web client TeaSpeak, веб клієнт TeaSpeak, TSDNS, багатомовність, мультимовність, теми, функціонал"/>
<!-- TODO Needs some fix --> <!-- TODO Needs some fix -->
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
@ -50,6 +21,7 @@
echo "<title>TeaClient</title>"; echo "<title>TeaClient</title>";
} else { } else {
echo "<title>TeaSpeak-Web</title>"; 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('connect_default_host', $localhost ? "localhost" : "ts.TeaSpeak.de");
spawn_property('localhost_debug', $localhost ? "true" : "false"); 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"); $version = file_get_contents("./version");
if ($version === false) if ($version === false)
@ -171,8 +136,23 @@
</div> </div>
<div id="spoiler-style" style="z-index: 1000000; position: absolute; display: block; background: white; right: 5px; left: 5px; top: 34px;"> <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/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> </div>
<button class="toggle-spoiler-style" style="height: 30px; width: 100px; z-index: 100000000; position: absolute; bottom: 2px;">toggle style</button> <button class="toggle-spoiler-style" style="height: 30px; width: 100px; z-index: 100000000; position: absolute; bottom: 2px;">toggle style</button>
<script> <script>
@ -181,7 +161,7 @@
$(".toggle-spoiler-style").on('click', () => { $(".toggle-spoiler-style").on('click', () => {
$("#spoiler-style").toggle(); $("#spoiler-style").toggle();
}); });
}, 1000); }, 2500);
</script> </script>
<div id="music-test"></div> <div id="music-test"></div>
@ -191,28 +171,10 @@
<div class="container"> <div class="container">
</div> </div>
</div> </div>
<div id="global-tooltip">
<a></a>
</div>
</body> </body>
<?php <div id="top-menu-bar"></div>
$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>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -74,7 +74,7 @@
] ]
}, { }, {
"key": "ru_gt", "key": "ru_gt",
"country_code": "gt", "country_code": "ru",
"path": "ru_google_translate.translation", "path": "ru_google_translate.translation",
"name": "Auto translated messages for language ru", "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, HANDSHAKE_BANNED,
SERVER_CLOSED, SERVER_CLOSED,
SERVER_REQUIRES_PASSWORD, SERVER_REQUIRES_PASSWORD,
SERVER_HOSTMESSAGE,
IDENTITY_TOO_LOW, IDENTITY_TOO_LOW,
UNKNOWN UNKNOWN
} }
@ -88,14 +89,15 @@ class ConnectionHandler {
permissions: PermissionManager; permissions: PermissionManager;
groups: GroupManager; groups: GroupManager;
chat_frame: chat.Frame; side_bar: chat.Frame;
select_info: InfoBar; select_info: InfoBar;
chat: ChatBox;
settings: ServerSettings; settings: ServerSettings;
sound: sound.SoundManager; sound: sound.SoundManager;
readonly tag_connection_handler: JQuery; hostbanner: Hostbanner;
tag_connection_handler: JQuery;
private _clientId: number = 0; private _clientId: number = 0;
private _local_client: LocalClientEntry; private _local_client: LocalClientEntry;
@ -126,42 +128,50 @@ class ConnectionHandler {
this.log = new log.ServerLog(this); this.log = new log.ServerLog(this);
this.select_info = new InfoBar(this); this.select_info = new InfoBar(this);
this.channelTree = new ChannelTree(this); this.channelTree = new ChannelTree(this);
this.chat = new ChatBox(this); this.side_bar = new chat.Frame(this);
this.chat_frame = new chat.Frame(this);
this.sound = new sound.SoundManager(this); this.sound = new sound.SoundManager(this);
this.hostbanner = new Hostbanner(this);
this.serverConnection = connection.spawn_server_connection(this); this.serverConnection = connection.spawn_server_connection(this);
this.serverConnection.onconnectionstatechanged = this.on_connection_state_changed.bind(this); this.serverConnection.onconnectionstatechanged = this.on_connection_state_changed.bind(this);
this.fileManager = new FileManager(this); this.fileManager = new FileManager(this);
this.permissions = new PermissionManager(this); this.permissions = new PermissionManager(this);
this.side_bar.channel_conversations().initialize_needed_listener();
this.groups = new GroupManager(this); this.groups = new GroupManager(this);
this._local_client = new LocalClientEntry(this); this._local_client = new LocalClientEntry(this);
this.channelTree.registerClient(this._local_client);
//settings.static_global(Settings.KEY_DISABLE_VOICE, false) /* initialize connection handler tab entry */
this.chat.initialize(); {
this.tag_connection_handler = $.spawn("div").addClass("connection-container"); 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-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("server-name").appendTo(this.tag_connection_handler);
$.spawn("div").addClass("button-close icon client-tab_close_button").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 => { this.tag_connection_handler.on('click', event => {
if(event.isDefaultPrevented()) if(event.isDefaultPrevented())
return; return;
server_connections.set_active_connection_handler(this); server_connections.set_active_connection_handler(this);
}); });
this.tag_connection_handler.find(".button-close").on('click', event => { this.tag_connection_handler.find(".button-close").on('click', event => {
server_connections.destroy_server_connection_handler(this); server_connections.destroy_server_connection_handler(this);
event.preventDefault(); 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() { } setup() { }
async startConnection(addr: string, profile: profiles.ConnectionProfile, parameters: ConnectParameters) { async startConnection(addr: string, profile: profiles.ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
this.tag_connection_handler.find(".server-name").text(tr("Connecting")); this.tab_set_name(tr("Connecting"));
this.cancel_reconnect(); this.cancel_reconnect(false);
this._reconnect_attempt = false; this._reconnect_attempt = false;
if(this.serverConnection) if(this.serverConnection)
this.handleDisconnect(DisconnectReason.REQUESTED); this.handleDisconnect(DisconnectReason.REQUESTED);
@ -172,8 +182,9 @@ class ConnectionHandler {
port: -1 port: -1
}; };
{ {
let _v6_end = addr.indexOf(']');
let idx = addr.lastIndexOf(':'); let idx = addr.lastIndexOf(':');
if(idx != -1) { if(idx != -1 && idx > _v6_end) {
server_address.port = parseInt(addr.substr(idx + 1)); server_address.port = parseInt(addr.substr(idx + 1));
server_address.host = addr.substr(0, idx); server_address.host = addr.substr(0, idx);
} else { } else {
@ -203,7 +214,14 @@ class ConnectionHandler {
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open(); 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)) { 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; const id = ++this._connect_initialize_id;
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE, {}); 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)); 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() { onConnected() {
console.log("Client connected!"); console.log("Client connected!");
this.channelTree.registerClient(this._local_client);
this.permissions.requestPermissionList(); this.permissions.requestPermissionList();
if(this.groups.serverGroups.length == 0) if(this.groups.serverGroups.length == 0)
this.groups.requestGroups(); this.groups.requestGroups();
@ -352,7 +378,7 @@ class ConnectionHandler {
const profile = profiles.find_profile(properties.connect_profile) || profiles.default_profile(); const profile = profiles.find_profile(properties.connect_profile) || profiles.default_profile();
const cprops = this.reconnect_properties(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); const url = build_url(properties);
@ -379,10 +405,11 @@ class ConnectionHandler {
handleDisconnect(type: DisconnectReason, data: any = {}) { handleDisconnect(type: DisconnectReason, data: any = {}) {
this._connect_initialize_id++; 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; let auto_reconnect = false;
switch (type) { switch (type) {
case DisconnectReason.REQUESTED: case DisconnectReason.REQUESTED:
case DisconnectReason.SERVER_HOSTMESSAGE: /* already handled */
break; break;
case DisconnectReason.HANDLER_DESTROYED: case DisconnectReason.HANDLER_DESTROYED:
if(data) if(data)
@ -468,7 +495,8 @@ class ConnectionHandler {
break; break;
case DisconnectReason.SERVER_CLOSED: 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( createErrorModal(
tr("Server closed"), tr("Server closed"),
"The server is closed.<br>" + //TODO tr "The server is closed.<br>" + //TODO tr
@ -479,15 +507,23 @@ class ConnectionHandler {
auto_reconnect = true; auto_reconnect = true;
break; break;
case DisconnectReason.SERVER_REQUIRES_PASSWORD: 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 => { createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
if(!(typeof password === "string")) return; 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}; 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, connection_log.update_address_info({
cprops); 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(); }).open();
break; break;
case DisconnectReason.CLIENT_KICKED: case DisconnectReason.CLIENT_KICKED:
@ -499,15 +535,29 @@ class ConnectionHandler {
auto_reconnect = false; auto_reconnect = false;
break; break;
case DisconnectReason.HANDSHAKE_BANNED: case DisconnectReason.HANDSHAKE_BANNED:
this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"), this.log.log(log.server.Type.SERVER_BANNED, {
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]), invoker: {
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : ""); 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 this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
break; break;
case DisconnectReason.CLIENT_BANNED: case DisconnectReason.CLIENT_BANNED:
this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"), this.log.log(log.server.Type.SERVER_BANNED, {
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]), invoker: {
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : ""); 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 this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
break; break;
default: default:
@ -517,6 +567,7 @@ class ConnectionHandler {
break; break;
} }
this.channelTree.unregisterClient(this._local_client); /* if we dont unregister our client here the client will be destroyed */
this.channelTree.reset(); this.channelTree.reset();
if(this.serverConnection) if(this.serverConnection)
this.serverConnection.disconnect(); this.serverConnection.disconnect();
@ -524,7 +575,8 @@ class ConnectionHandler {
if(control_bar.current_connection_handler() == this) if(control_bar.current_connection_handler() == this)
control_bar.update_connection_state(); control_bar.update_connection_state();
this.select_info.setCurrentSelected(null); 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(auto_reconnect) {
if(!this.serverConnection) { if(!this.serverConnection) {
@ -542,15 +594,15 @@ class ConnectionHandler {
this.log.log(log.server.Type.RECONNECT_CANCELED, {}); this.log.log(log.server.Type.RECONNECT_CANCELED, {});
log.info(LogCategory.NETWORKING, tr("Reconnecting...")); 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; this._reconnect_attempt = true;
}, 5000); }, 5000);
} }
} }
cancel_reconnect() { cancel_reconnect(log_event: boolean) {
if(this._reconnect_timer) { 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); clearTimeout(this._reconnect_timer);
this._reconnect_timer = undefined; this._reconnect_timer = undefined;
} }
@ -562,6 +614,8 @@ class ConnectionHandler {
} }
update_voice_status(targetChannel?: ChannelEntry) { update_voice_status(targetChannel?: ChannelEntry) {
if(!this._local_client) return; /* we've been destroyed */
targetChannel = targetChannel || this.getClient().currentChannel(); targetChannel = targetChannel || this.getClient().currentChannel();
const vconnection = this.serverConnection.voice_connection(); const vconnection = this.serverConnection.voice_connection();
@ -636,10 +690,21 @@ class ConnectionHandler {
if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().record_supported) { if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().record_supported) {
const active = !this.client_status.input_muted && !this.client_status.output_muted; const active = !this.client_status.input_muted && !this.client_status.output_muted;
if(active) if(active) {
vconnection.voice_recorder().input.start(); if(vconnection.voice_recorder().input.current_state() === audio.recorder.InputState.PAUSED) {
else 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(); vconnection.voice_recorder().input.stop();
}
} }
if(control_bar.current_connection_handler() === this) if(control_bar.current_connection_handler() === this)
@ -665,6 +730,12 @@ class ConnectionHandler {
if(this.client_status.away === state) if(this.client_status.away === state)
return; return;
if(state) {
this.sound.play(Sound.AWAY_ACTIVATED);
} else {
this.sound.play(Sound.AWAY_DEACTIVATED);
}
this.client_status.away = state; this.client_status.away = state;
this.serverConnection.send_command("clientupdate", { this.serverConnection.send_command("clientupdate", {
client_away: typeof(this.client_status.away) === "string" || this.client_status.away, 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 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; this.transfer_key = key;
} }
async put_data(data: BufferSource | File) { async put_data(data: BlobPart | File) {
const form_data = new FormData(); const form_data = new FormData();
if(data instanceof File) { if(data instanceof File) {
@ -110,6 +110,10 @@ class RequestFileUpload {
throw "invalid size"; throw "invalid size";
form_data.append("file", data); 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 { } else {
const buffer = <BufferSource>data; const buffer = <BufferSource>data;
if(buffer.byteLength != this.transfer_key.total_size) 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); 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 { handle_command(command: connection.ServerCommand): boolean {
switch (command.command) { switch (command.command) {
case "notifyfilelist": case "notifyfilelist":
@ -262,7 +284,7 @@ class FileManager extends connection.AbstractCommandHandler {
"clientftfid": transfer_data.client_transfer_id, "clientftfid": transfer_data.client_transfer_id,
"seekpos": 0, "seekpos": 0,
"proto": 1 "proto": 1
}).catch(reason => { }, {process_result: false}).catch(reason => {
this.pending_download_requests.remove(transfer_data); this.pending_download_requests.remove(transfer_data);
reject(reason); 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 ab2str10 = () => {
const buf = new Uint8Array(base64 as ArrayBuffer); const buf = new Uint8Array(encoded_data as ArrayBuffer);
if(buf.byteLength < 10) if(buf.byteLength < 10)
return ""; return "";
@ -422,7 +444,7 @@ function image_type(base64: string | ArrayBuffer) {
return result; 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.length < 10) return ImageType.UNKNOWN;
if(bin[0] == String.fromCharCode(66) && bin[1] == String.fromCharCode(77)) { 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> { async resolve_cached(key: string, max_age?: number) : Promise<Response | undefined> {
max_age = typeof(max_age) === "number" ? max_age : -1; 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("https://_local_cache/cache_request_" + key);
const cached_response = await this._cache_category.match(request);
if(!cached_response) if(!cached_response)
return undefined; return undefined;
@ -491,8 +512,6 @@ class CacheManager {
} }
async put_cache(key: string, value: Response, type?: string, headers?: {[key: string]:string}) { 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(); const new_headers = new Headers();
for(const key of value.headers.keys()) for(const key of value.headers.keys())
new_headers.set(key, value.headers.get(key)); new_headers.set(key, value.headers.get(key));
@ -501,14 +520,25 @@ class CacheManager {
for(const key of Object.keys(headers || {})) for(const key of Object.keys(headers || {}))
new_headers.set(key, headers[key]); 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 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 { class IconManager {
private static cache: CacheManager; private static cache: CacheManager = new CacheManager("icons");
handle: FileManager; handle: FileManager;
private _id_urls: {[id:number]:string} = {}; private _id_urls: {[id:number]:string} = {};
@ -516,9 +546,15 @@ class IconManager {
constructor(handle: FileManager) { constructor(handle: FileManager) {
this.handle = handle; this.handle = handle;
}
if(!IconManager.cache) destroy() {
IconManager.cache = new CacheManager("icons"); 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() { async clear_cache() {
@ -548,7 +584,7 @@ class IconManager {
return this.handle.download_file("", "/icon_" + id); 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')) if(!response.headers.has('X-media-bytes'))
throw "missing media bytes"; throw "missing media bytes";
@ -573,14 +609,50 @@ class IconManager {
await IconManager.cache.setup(); await IconManager.cache.setup();
const response = await IconManager.cache.resolve_cached('icon_' + id); //TODO age! 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 { return {
id: id, id: id,
url: (this._id_urls[id] = await this._response_url(response)) url: url
}; };
}
return undefined; 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> { private async _load_icon(id: number) : Promise<Icon> {
try { try {
let download_key: transfer.DownloadKey; let download_key: transfer.DownloadKey;
@ -604,7 +676,10 @@ class IconManager {
const media = media_image_type(type); const media = media_image_type(type);
await IconManager.cache.put_cache('icon_' + id, response.clone(), "image/" + media); 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; this._loading_promises[id] = undefined;
return { return {
@ -644,6 +719,58 @@ class IconManager {
throw "icon not found"; 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?: { generateTag(id: number, options?: {
animate?: boolean animate?: boolean
}) : JQuery<HTMLDivElement> { }) : JQuery<HTMLDivElement> {
@ -651,44 +778,16 @@ class IconManager {
id = id >>> 0; id = id >>> 0;
if(id == 0 || !id) if(id == 0 || !id)
return $.spawn("div").addClass("icon_empty"); return IconManager.generate_tag({id: id, url: ""}, options);
else if(id < 1000) 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]) { if(this._id_urls[id]) {
icon_image.attr("src", this._id_urls[id]).appendTo(icon_container); return IconManager.generate_tag({id: id, url: this._id_urls[id]}, options);
icon_container.removeClass("icon_empty");
} else { } else {
const icon_load_image = $.spawn("div").addClass("icon_loading"); return IconManager.generate_tag(this.resolve_icon(id), options);
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 icon_container;
} }
} }
@ -713,6 +812,11 @@ class AvatarManager {
AvatarManager.cache = new CacheManager("avatars"); AvatarManager.cache = new CacheManager("avatars");
} }
destroy() {
this._cached_avatars = undefined;
this._loading_promises = undefined;
}
private async _response_url(response: Response, type: ImageType) : Promise<string> { private async _response_url(response: Response, type: ImageType) : Promise<string> {
if(!response.headers.has('X-media-bytes')) if(!response.headers.has('X-media-bytes'))
throw "missing media bytes"; throw "missing media bytes";
@ -725,12 +829,12 @@ class AvatarManager {
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
} }
async resolved_cached?(client_avatar_id: string, avatar_id?: string) : Promise<Avatar> { async resolved_cached?(client_avatar_id: string, avatar_version?: string) : Promise<Avatar> {
let avatar: Avatar = this._cached_avatars[avatar_id]; let avatar: Avatar = this._cached_avatars[avatar_version];
if(avatar) { if(avatar) {
if(typeof(avatar_id) !== "string" || avatar.avatar_id == avatar_id) if(typeof(avatar_version) !== "string" || avatar.avatar_id == avatar_version)
return avatar; return avatar;
this._cached_avatars[avatar_id] = (avatar = undefined); avatar = undefined;
} }
if(!AvatarManager.cache.setupped()) if(!AvatarManager.cache.setupped())
@ -740,14 +844,14 @@ class AvatarManager {
if(!response) if(!response)
return undefined; return undefined;
let response_avatar_id = response.headers.has("X-avatar-id") ? response.headers.get("X-avatar-id") : undefined; let response_avatar_version = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined;
if(typeof(avatar_id) === "string" && response_avatar_id != avatar_id) if(typeof(avatar_version) === "string" && response_avatar_version != avatar_version)
return undefined; return undefined;
const type = image_type(response.headers.get('X-media-bytes')); const type = image_type(response.headers.get('X-media-bytes'));
return this._cached_avatars[client_avatar_id] = { return this._cached_avatars[client_avatar_id] = {
client_avatar_id: 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), url: await this._response_url(response, type),
type: type type: type
}; };
@ -758,49 +862,76 @@ class AvatarManager {
return this.handle.download_file("", "/avatar_" + client_avatar_id); return this.handle.download_file("", "/avatar_" + client_avatar_id);
} }
private async _load_avatar(client_avatar_id: string, avatar_id: string) { private async _load_avatar(client_avatar_id: string, avatar_version: string) {
let download_key: transfer.DownloadKey;
try { try {
download_key = await this.create_avatar_download(client_avatar_id); let download_key: transfer.DownloadKey;
} catch(error) { try {
console.error(tr("Could not request download for avatar %s: %o"), client_avatar_id, error); download_key = await this.create_avatar_download(client_avatar_id);
throw "Failed to request icon"; } 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> { /* loads an avatar by the avatar id and optional with the avatar version */
return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_id)); 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 { generate_client_tag(client: ClientEntry) : JQuery {
return this.generate_tag(client.avatarId(), client.properties.client_flag_avatar); 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?: { generate_tag(client_avatar_id: string, avatar_id?: string, options?: {
callback_image?: (tag: JQuery<HTMLImageElement>) => any, callback_image?: (tag: JQuery<HTMLImageElement>) => any,
callback_avatar?: (avatar: Avatar) => any callback_avatar?: (avatar: Avatar) => any
@ -811,7 +942,9 @@ class AvatarManager {
let avatar_image = $.spawn("img").attr("alt", tr("Client avatar")); let avatar_image = $.spawn("img").attr("alt", tr("Client avatar"));
let cached_avatar: Avatar = this._cached_avatars[client_avatar_id]; 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_image.attr("src", cached_avatar.url);
avatar_container.append(avatar_image); avatar_container.append(avatar_image);
if(options.callback_image) if(options.callback_image)
@ -832,7 +965,7 @@ class AvatarManager {
} }
if(!avatar) if(!avatar)
avatar = await this.loadAvatar(client_avatar_id, avatar_id) avatar = await this.load_avatar(client_avatar_id, avatar_id);
if(!avatar) if(!avatar)
throw "failed to load avatar"; throw "failed to load avatar";
@ -844,7 +977,7 @@ class AvatarManager {
avatar_image.css("opacity", 0); avatar_image.css("opacity", 0);
avatar_container.append(avatar_image); avatar_container.append(avatar_image);
loader_image.animate({opacity: 0}, 50, () => { loader_image.animate({opacity: 0}, 50, () => {
loader_image.detach(); loader_image.remove();
avatar_image.animate({opacity: 1}, 150, () => { avatar_image.animate({opacity: 1}, 150, () => {
if(options.callback_image) if(options.callback_image)
options.callback_image(avatar_image); options.callback_image(avatar_image);
@ -859,4 +992,109 @@ class AvatarManager {
return avatar_container; 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) if(key.key_windows)
result += " + " + tr("Win"); 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); return result.substr(3);
} }
} }

View File

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

View File

@ -9,6 +9,37 @@ namespace bookmarks {
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); 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 { export interface ServerProperties {
server_address: string; server_address: string;
server_port: number; server_port: number;
@ -35,6 +66,8 @@ namespace bookmarks {
default_channel_password?: string; default_channel_password?: string;
connect_profile: string; connect_profile: string;
last_icon_id?: number;
} }
export interface DirectoryBookmark { export interface DirectoryBookmark {
@ -88,6 +121,19 @@ namespace bookmarks {
return bookmark_config().root_bookmark; 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 { function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark {
for(const entry of parent.content) { for(const entry of parent.content) {
if(entry.unique_id == uuid) if(entry.unique_id == uuid)
@ -169,4 +215,27 @@ namespace bookmarks {
export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) { export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
delete_bookmark_recursive(bookmarks(), bookmark) 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" /> /// <reference path="ConnectionBase.ts" />
namespace connection { namespace connection {
import Conversation = chat.channel.Conversation;
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
constructor(connection: AbstractServerConnection) { constructor(connection: AbstractServerConnection) {
super(connection); super(connection);
@ -23,6 +25,7 @@ namespace connection {
this["notifychannelhide"] = this.handleCommandChannelHide; this["notifychannelhide"] = this.handleCommandChannelHide;
this["notifychannelshow"] = this.handleCommandChannelShow; this["notifychannelshow"] = this.handleCommandChannelShow;
this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo;
this["notifycliententerview"] = this.handleCommandClientEnterView; this["notifycliententerview"] = this.handleCommandClientEnterView;
this["notifyclientleftview"] = this.handleCommandClientLeftView; this["notifyclientleftview"] = this.handleCommandClientLeftView;
this["notifyclientmoved"] = this.handleNotifyClientMoved; this["notifyclientmoved"] = this.handleNotifyClientMoved;
@ -45,6 +48,9 @@ namespace connection {
this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed; this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed;
this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed; this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed;
this["notifyconversationhistory"] = this.handleNotifyConversationHistory;
this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete;
} }
proxy_command_promise(promise: Promise<CommandResult>, options: connection.CommandOptions) { proxy_command_promise(promise: Promise<CommandResult>, options: connection.CommandOptions) {
@ -56,20 +62,21 @@ namespace connection {
if(ex instanceof CommandResult) { if(ex instanceof CommandResult) {
let res = ex; let res = ex;
if(!res.success) { if(!res.success) {
if(res.id == 2568) { //Permission error if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error
res.message = tr("Insufficient client permissions. Failed on permission ") + this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number).name; 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, { this.connection_handler.log.log(log.server.Type.ERROR_PERMISSION, {
permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number) permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number)
}); });
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); 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, { this.connection_handler.log.log(log.server.Type.ERROR_CUSTOM, {
message: res.extra_message.length == 0 ? res.message : res.extra_message message: res.extra_message.length == 0 ? res.message : res.extra_message
}); });
} }
} }
} else if(typeof(ex) === "string") { } 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 { } else {
console.error(tr("Invalid promise result type: %o. Result:"), typeof (ex)); console.error(tr("Invalid promise result type: %o. Result:"), typeof (ex));
console.error(ex); console.error(ex);
@ -131,6 +138,8 @@ namespace connection {
json = json[0]; //Only one bulk 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.clientId = parseInt(json["aclid"]);
this.connection.client.getClient().updateVariables({key: "client_nickname", value: json["acn"]}); 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); 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, { this.connection_handler.log.log(log.server.Type.CONNECTION_CONNECTED, {
own_client: this.connection_handler.getClient().log_data() own_client: this.connection_handler.getClient().log_data()
}); });
@ -155,6 +217,16 @@ namespace connection {
this.connection.client.onConnected(); 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) { private createChannelFromJson(json, ignoreOrder: boolean = false) {
let tree = this.connection.client.channelTree; let tree = this.connection.client.channelTree;
@ -223,9 +295,11 @@ namespace connection {
handleCommandChannelDelete(json) { handleCommandChannelDelete(json) {
let tree = this.connection.client.channelTree; let tree = this.connection.client.channelTree;
const conversations = this.connection.client.side_bar.channel_conversations();
console.log(tr("Got %d channel deletions"), json.length); console.log(tr("Got %d channel deletions"), json.length);
for(let index = 0; index < json.length; index++) { for(let index = 0; index < json.length; index++) {
conversations.delete_conversation(parseInt(json[index]["cid"]));
let channel = tree.findChannel(json[index]["cid"]); let channel = tree.findChannel(json[index]["cid"]);
if(!channel) { if(!channel) {
console.error(tr("Invalid channel onDelete (Unknown channel)")); console.error(tr("Invalid channel onDelete (Unknown channel)"));
@ -237,9 +311,11 @@ namespace connection {
handleCommandChannelHide(json) { handleCommandChannelHide(json) {
let tree = this.connection.client.channelTree; let tree = this.connection.client.channelTree;
const conversations = this.connection.client.side_bar.channel_conversations();
console.log(tr("Got %d channel hides"), json.length); console.log(tr("Got %d channel hides"), json.length);
for(let index = 0; index < json.length; index++) { for(let index = 0; index < json.length; index++) {
conversations.delete_conversation(parseInt(json[index]["cid"]));
let channel = tree.findChannel(json[index]["cid"]); let channel = tree.findChannel(json[index]["cid"]);
if(!channel) { if(!channel) {
console.error(tr("Invalid channel on hide (Unknown 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.properties.client_type = parseInt(entry["client_type"]);
client = tree.insertClient(client, channel); client = tree.insertClient(client, channel);
} else { } else {
if(client == this.connection.client.getClient())
this.connection_handler.chat.channelChat().name = channel.channelName();
tree.moveClient(client, channel); tree.moveClient(client, channel);
} }
@ -338,32 +412,25 @@ namespace connection {
client.updateVariables(...updates); client.updateVariables(...updates);
{ if(!old_channel) {
let client_chat = client.chat(false); /* client new join */
if(!client_chat) { const conversation_manager = this.connection_handler.side_bar.private_conversations();
for(const c of this.connection_handler.chat.open_chats()) { const conversation = conversation_manager.find_conversation({
if(c.owner_unique_id == client.properties.client_unique_identifier && c.flag_offline) { unique_id: client.properties.client_unique_identifier,
client_chat = c; client_id: client.clientId(),
break; name: client.clientNickName()
} }, {
} create: false,
} attach: true
});
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(client instanceof LocalClientEntry) { if(client instanceof LocalClientEntry) {
client.initializeListener();
this.connection_handler.update_voice_status(); 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); this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry);
} else } else
this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry); 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; return;
} }
@ -436,19 +503,19 @@ namespace connection {
console.error(tr("Unknown client left reason!")); console.error(tr("Unknown client left reason!"));
} }
{ if(!channel_to) {
const chat = client.chat(false); /* client left the server */
if(chat) { const conversation_manager = this.connection_handler.side_bar.private_conversations();
chat.flag_offline = true; const conversation = conversation_manager.find_conversation({
chat.onMessageSend = undefined; unique_id: client.properties.client_unique_identifier,
chat.onClose = undefined; client_id: client.clientId(),
chat.appendMessage( name: client.clientNickName()
"{0}", true, }, {
$.spawn("div") create: false,
.addClass("event-message event-partner-disconnect") attach: false
.text(tr("Your chat partner has disconnected")) });
); if(conversation)
} conversation.set_state(chat.PrivateConversationState.DISCONNECTED);
} }
} }
@ -478,7 +545,6 @@ namespace connection {
let self = client instanceof LocalClientEntry; let self = client instanceof LocalClientEntry;
let current_clients: ClientEntry[]; let current_clients: ClientEntry[];
if(self) { if(self) {
this.connection_handler.chat.channelChat().name = channel_to.channelName();
current_clients = client.channelTree.clientsByChannel(client.currentChannel()); current_clients = client.channelTree.clientsByChannel(client.currentChannel());
this.connection_handler.update_voice_status(channel_to); this.connection_handler.update_voice_status(channel_to);
} }
@ -488,8 +554,20 @@ namespace connection {
if(entry !== client && entry.get_audio_handle()) if(entry !== client && entry.get_audio_handle())
entry.get_audio_handle().abort_replay(); entry.get_audio_handle().abort_replay();
if(self) if(self) {
this.connection_handler.chat_frame.info_frame().update_channel_talk(); 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(); const own_channel = this.connection.client.getClient().currentChannel();
this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_MOVE, { this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_MOVE, {
@ -603,29 +681,65 @@ namespace connection {
let mode = json["targetmode"]; let mode = json["targetmode"];
if(mode == 1){ if(mode == 1){
let invoker = this.connection.client.channelTree.findClient(json["invokerid"]); //json["invokerid"], json["invokername"], json["invokeruid"]
let target = this.connection.client.channelTree.findClient(json["target"]); const target_client_id = parseInt(json["target"]);
if(!invoker) { //TODO spawn chat (Client is may invisible) const target_own = target_client_id === this.connection.client.getClientId();
console.error(tr("Got private message from invalid client!"));
if(target_own && target_client_id === json["invokerid"]) {
console.error(tr("Received conversation message from invalid client id. Data: %o", json));
return; 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; return;
} }
if(invoker == this.connection.client.getClient()) {
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); conversation.append_message(json["msg"], {
target.chat(true).appendMessage("{0}: {1}", true, this.connection.client.getClient().createChatTag(true), MessageHelper.bbcode_chat(json["msg"])); type: target_own ? "partner" : "self",
} else { 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}); 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) { } 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) if(json["invokerid"] == this.connection.client.clientId)
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); 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.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) { } else if(mode == 3) {
this.connection_handler.log.log(log.server.Type.GLOBAL_MESSAGE, { this.connection_handler.log.log(log.server.Type.GLOBAL_MESSAGE, {
message: json["msg"], message: json["msg"],
@ -635,6 +749,18 @@ namespace connection {
client_id: parseInt(json["invokerid"]) 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" //clid: "6"
//cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY=" //cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY="
const client = this.connection.client.channelTree.findClient(json["clid"]); const conversation_manager = this.connection_handler.side_bar.private_conversations();
if(!client) { const conversation = conversation_manager.find_conversation({
log.warn(LogCategory.GENERAL, tr("Received chat close for unknown client")); client_id: parseInt(json["clid"]),
return; unique_id: json["cluid"],
} name: undefined
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"]); create: false,
return; attach: false
} });
if(!conversation) {
const chat = client.chat(false);
if(!chat) {
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open.")); log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open."));
return; return;
} }
chat.flag_offline = true; conversation.set_state(chat.PrivateConversationState.CLOSED);
chat.appendMessage(
"{0}", true,
$.spawn("div")
.addClass("event-message event-partner-closed")
.text(tr("Your chat partner has close the conversation"))
);
} }
handleNotifyClientUpdated(json) { handleNotifyClientUpdated(json) {
@ -811,5 +929,38 @@ namespace connection {
this.connection.client.channelTree.deleteClient(client); 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() { initialize() {
this.connection.command_handler_boss().register_handler(this); 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 { handle_command(command: connection.ServerCommand): boolean {

View File

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

View File

@ -4,6 +4,12 @@ enum ErrorID {
PLAYLIST_IS_IN_USE = 0x2103, PLAYLIST_IS_IN_USE = 0x2103,
FILE_ALREADY_EXISTS = 2050, FILE_ALREADY_EXISTS = 2050,
CLIENT_INVALID_ID = 0x0200,
CONVERSATION_INVALID_ID = 0x2200,
CONVERSATION_MORE_DATA = 0x2201,
CONVERSATION_IS_PRIVATE = 0x2202
} }
class CommandResult { 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.full_url = url;
file.path = path; file.path = path;
//TODO validate file
//TODO: Validate file
resolve(file); resolve(file);
} catch(error) { } catch(error) {
log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, 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> { 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); log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
translations = result.translations; translations = result.translations;
return Promise.resolve();
}).catch(error => { }).catch(error => {
log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error); log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error);
return Promise.reject(error); return Promise.reject(error);
@ -292,6 +299,7 @@ namespace i18n {
try { try {
await load_file(cfg.current_translation_url, cfg.current_translation_path); await load_file(cfg.current_translation_url, cfg.current_translation_path);
} catch (error) { } 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(); 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 // @ts-ignore
const tr: typeof i18n.tr = i18n.tr; 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() { export function is_web() {
return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG; 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 { namespace loader {
@ -150,19 +164,31 @@ namespace loader {
console.groupCollapsed("Executing loading stage %s", Stage[current_stage]); console.groupCollapsed("Executing loading stage %s", Stage[current_stage]);
} }
} }
/* cleanup */
{
_script_promises = {};
}
console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); 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)) { if(Array.isArray(path)) {
let buffer = ""; let buffer = "";
let _or = " or "; let _or = " or ";
for(let entry of path) for(let entry of path)
buffer += _or + script_name(entry); buffer += _or + script_name(entry);
return buffer.slice(_or.length); 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 { class SyntaxError {
@ -173,6 +199,7 @@ namespace loader {
} }
} }
let _script_promises: {[key: string]: Promise<void>} = {};
export async function load_script(path: SourcePath) : Promise<void> { export async function load_script(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback if(Array.isArray(path)) { //We have some fallback
return load_script(path[0]).catch(error => { return load_script(path[0]).catch(error => {
@ -185,45 +212,64 @@ namespace loader {
return Promise.reject(error); return Promise.reject(error);
}); });
} else { } 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"); const tag: HTMLScriptElement = document.createElement("script");
let error = false; await new Promise((resolve, reject) => {
const error_handler = (event: ErrorEvent) => { let error = false;
if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error const error_handler = (event: ErrorEvent) => {
console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); if(event.filename == tag.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); 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)); console.debug("Script %o loaded", path);
event.preventDefault(); setTimeout(resolve, 100);
error = true; };
}
};
window.addEventListener('error', error_handler as any);
const timeout_handle = setTimeout(() => { document.getElementById("scripts").appendChild(tag);
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); tag.src = source.url + (cache_tag || "");
});
tag.src = path + (cache_tag || ""); })();
});
} }
} }
@ -257,7 +303,7 @@ namespace loader {
export async function load_style(path: SourcePath) : Promise<void> { export async function load_style(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback 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) if(error instanceof SyntaxError)
return Promise.reject(error.source); return Promise.reject(error.source);
@ -267,6 +313,10 @@ namespace loader {
return Promise.reject(error); return Promise.reject(error);
}); });
} else { } else {
if(!path) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link"); const tag: HTMLLinkElement = document.createElement("link");
@ -283,21 +333,30 @@ namespace loader {
}; };
window.addEventListener('error', error_handler as any); 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(() => { const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout"); reject("timeout");
}, 5000); }, 5000);
tag.type = "text/css";
tag.rel="stylesheet";
tag.onerror = error => { tag.onerror = error => {
clearTimeout(timeout_handle); cleanup();
window.removeEventListener('error', error_handler as any);
tag.remove(); tag.remove();
console.error("File load error for file %s: %o", path, error); console.error("File load error for file %s: %o", path, error);
reject("failed to load file " + path); reject("failed to load file " + path);
}; };
tag.onload = () => { tag.onload = () => {
cleanup();
{ {
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet; const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
const rules = css.cssRules; const rules = css.cssRules;
@ -324,8 +383,6 @@ namespace loader {
css.insertRule(rule, rules_remove[0]); css.insertRule(rule, rules_remove[0]);
} }
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
console.debug("Style sheet %o loaded", path); console.debug("Style sheet %o loaded", path);
setTimeout(resolve, 100); setTimeout(resolve, 100);
}; };
@ -464,6 +521,7 @@ const loader_javascript = {
if(!window.require) { if(!window.require) {
await loader.load_script(["vendor/jquery/jquery.min.js"]); await loader.load_script(["vendor/jquery/jquery.min.js"]);
} else { } else {
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "forum sync", name: "forum sync",
priority: 10, priority: 10,
@ -471,26 +529,36 @@ const loader_javascript = {
forum.sync_main(); forum.sync_main();
} }
}); });
*/
} }
await loader.load_script(["vendor/DOMPurify/purify.min.js"]);
/* bootstrap material design and libs */ /* bootstrap material design and libs */
await loader.load_script(["vendor/popper/popper.js"]); //await loader.load_script(["vendor/popper/popper.js"]);
//depends on popper //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, { loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "materialize body", name: "materialize body",
priority: 10, priority: 10,
function: async () => { $(document).ready(function() { $('body').bootstrapMaterialDesign(); }); } function: async () => { $(document).ready(function() { $('body').bootstrapMaterialDesign(); }); }
}); });
*/
await loader.load_script("vendor/jsrender/jsrender.min.js"); await loader.load_script("vendor/jsrender/jsrender.min.js");
await loader.load_scripts([ await loader.load_scripts([
["vendor/xbbcode/src/parser.js"], ["vendor/xbbcode/src/parser.js"],
["vendor/moment/moment.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"] ["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) { if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) {
loader.register_task(loader.Stage.JAVASCRIPT, { loader.register_task(loader.Stage.JAVASCRIPT, {
@ -547,14 +615,19 @@ const loader_javascript = {
//load the profiles //load the profiles
"js/profiles/ConnectionProfile.js", "js/profiles/ConnectionProfile.js",
"js/profiles/Identity.js", "js/profiles/Identity.js",
"js/profiles/identities/teaspeak-forum.js",
//Basic UI elements //Basic UI elements
"js/ui/elements/context_divider.js", "js/ui/elements/context_divider.js",
"js/ui/elements/context_menu.js", "js/ui/elements/context_menu.js",
"js/ui/elements/modal.js", "js/ui/elements/modal.js",
"js/ui/elements/tab.js", "js/ui/elements/tab.js",
"js/ui/elements/slider.js",
"js/ui/elements/tooltip.js",
//Load UI //Load UI
"js/ui/modal/ModalAbout.js",
"js/ui/modal/ModalAvatar.js",
"js/ui/modal/ModalAvatarList.js", "js/ui/modal/ModalAvatarList.js",
"js/ui/modal/ModalQuery.js", "js/ui/modal/ModalQuery.js",
"js/ui/modal/ModalQueryManage.js", "js/ui/modal/ModalQueryManage.js",
@ -569,13 +642,16 @@ const loader_javascript = {
"js/ui/modal/ModalBanClient.js", "js/ui/modal/ModalBanClient.js",
"js/ui/modal/ModalIconSelect.js", "js/ui/modal/ModalIconSelect.js",
"js/ui/modal/ModalInvite.js", "js/ui/modal/ModalInvite.js",
"js/ui/modal/ModalIdentity.js",
"js/ui/modal/ModalBanCreate.js", "js/ui/modal/ModalBanCreate.js",
"js/ui/modal/ModalBanList.js", "js/ui/modal/ModalBanList.js",
"js/ui/modal/ModalYesNo.js", "js/ui/modal/ModalYesNo.js",
"js/ui/modal/ModalPoke.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/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/channel.js",
"js/ui/client.js", "js/ui/client.js",
@ -590,6 +666,8 @@ const loader_javascript = {
"js/ui/frames/chat_frame.js", "js/ui/frames/chat_frame.js",
"js/ui/frames/connection_handlers.js", "js/ui/frames/connection_handlers.js",
"js/ui/frames/server_log.js", "js/ui/frames/server_log.js",
"js/ui/frames/hostbanner.js",
"js/ui/frames/MenuBar.js",
//Load permissions //Load permissions
"js/permission/PermissionManager.js", "js/permission/PermissionManager.js",
@ -640,12 +718,11 @@ const loader_javascript = {
//Load codec //Load codec
"js/codec/Codec.js", "js/codec/Codec.js",
"js/codec/BasicCodec.js", "js/codec/BasicCodec.js",
"js/codec/CodecWrapperWorker.js", {url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]},
]); ]);
}, },
load_scripts_debug_client: async () => { load_scripts_debug_client: async () => {
await loader.load_scripts([ await loader.load_scripts([
["js/teaforo.js"]
]); ]);
}, },
@ -685,15 +762,18 @@ const loader_style = {
await loader.load_styles([ await loader.load_styles([
"vendor/xbbcode/src/xbbcode.css" "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) { if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) {
await loader_style.load_style_debug(); await loader_style.load_style_debug();
} else { } else {
await loader_style.load_style_release(); await loader_style.load_style_release();
} }
/* the material design */
await loader.load_style("css/theme/bootstrap-material-design.css");
}, },
load_style_debug: async () => { load_style_debug: async () => {
@ -706,9 +786,12 @@ const loader_style = {
"css/static/ts/tab.css", "css/static/ts/tab.css",
"css/static/ts/chat.css", "css/static/ts/chat.css",
"css/static/ts/icons.css", "css/static/ts/icons.css",
"css/static/ts/icons_em.css",
"css/static/ts/country.css", "css/static/ts/country.css",
"css/static/general.css", "css/static/general.css",
"css/static/modal.css",
"css/static/modals.css", "css/static/modals.css",
"css/static/modal-about.css",
"css/static/modal-avatar.css", "css/static/modal-avatar.css",
"css/static/modal-icons.css", "css/static/modal-icons.css",
"css/static/modal-bookmarks.css", "css/static/modal-bookmarks.css",
@ -722,7 +805,9 @@ const loader_style = {
"css/static/modal-settings.css", "css/static/modal-settings.css",
"css/static/modal-poke.css", "css/static/modal-poke.css",
"css/static/modal-server.css", "css/static/modal-server.css",
"css/static/modal-keyselect.css",
"css/static/modal-permissions.css", "css/static/modal-permissions.css",
"css/static/modal-group-assignment.css",
"css/static/music/info_plate.css", "css/static/music/info_plate.css",
"css/static/frame/SelectInfo.css", "css/static/frame/SelectInfo.css",
"css/static/control_bar.css", "css/static/control_bar.css",
@ -730,7 +815,9 @@ const loader_style = {
"css/static/frame-chat.css", "css/static/frame-chat.css",
"css/static/connection_handlers.css", "css/static/connection_handlers.css",
"css/static/server-log.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", "css/static/main.css",
]); ]);
} }
} };
async function load_templates() { async function load_templates() {
try { try {
@ -773,13 +860,9 @@ async function load_templates() {
/* test if all files shall be load from cache or fetch again */ /* test if all files shall be load from cache or fetch again */
async function check_updates() { async function check_updates() {
const app_version = (() => { const app_version = (() => {
const version_node = document.getElementById("app_version"); const version = app.ui_version();
if(!version_node) return undefined;
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; if(!version || version == "unknown" || version.replace(/0+/, "").length == 0)
if(!version) return undefined;
if(version == "unknown" || version.replace(/0+/, "").length == 0)
return undefined; return undefined;
return version; return version;
@ -960,6 +1043,11 @@ loader.register_task(loader.Stage.LOADED, {
}, },
priority: 20 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"] || {}; window["Module"] = window["Module"] || {};

View File

@ -14,10 +14,15 @@ let settings: Settings;
const js_render = window.jsrender || $; const js_render = window.jsrender || $;
const native_client = window.require !== undefined; const native_client = window.require !== undefined;
function getUserMediaFunction() : (constraints: MediaStreamConstraints, success: (stream: MediaStream) => any, fail: (error: any) => any) => any { function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise<MediaStream> {
if((navigator as any).mediaDevices && (navigator as any).mediaDevices.getUserMedia) if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices)
return (settings, success, fail) => { (navigator as any).mediaDevices.getUserMedia(settings).then(success).catch(fail); }; return constraints => navigator.mediaDevices.getUserMedia(constraints);
return (navigator as any).getUserMedia || (navigator as any).webkitGetUserMedia || (navigator as any).mozGetUserMedia;
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 { interface Window {
@ -36,18 +41,41 @@ function setup_close() {
if(!native_client) { if(!native_client) {
event.returnValue = "Are you really sure?<br>You're still connected!"; event.returnValue = "Are you really sure?<br>You're still connected!";
} else { } 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) { if(window.open_connected_question) {
event.preventDefault(); event.preventDefault();
event.returnValue = "question"; event.returnValue = "question";
window.open_connected_question().then(result => { window.open_connected_question().then(result => {
if(result) { if(result) {
window.onbeforeunload = undefined; /* prevent quitting because we try to disconnect */
window.onbeforeunload = e => e.preventDefault();
const {remote} = require('electron'); /* allow a force quit after 5 seconds */
remote.getCurrentWindow().close(); 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(); bipc.setup();
} }
async function initialize_app() { async function initialize_app() {
const display_load_error = message => { const display_load_error = message => {
if(typeof(display_critical_load) !== "undefined") if(typeof(display_critical_load) !== "undefined")
@ -112,7 +139,10 @@ async function initialize_app() {
}; };
try { //Initialize main template 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); $("body").append(main);
} catch(error) { } catch(error) {
@ -126,7 +156,7 @@ async function initialize_app() {
if(!audio.player.initialize()) if(!audio.player.initialize())
console.warn(tr("Failed to initialize audio controller!")); console.warn(tr("Failed to initialize audio controller!"));
if(audio.player.set_master_volume) 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 else
console.warn("Client does not support audio.player.set_master_volume()... May client is too old?"); console.warn("Client does not support audio.player.set_master_volume()... May client is too old?");
if(audio.recorder.device_refresh_available()) if(audio.recorder.device_refresh_available())
@ -138,7 +168,7 @@ async function initialize_app() {
sound.initialize().then(() => { sound.initialize().then(() => {
console.log(tr("Sounds initialitzed")); 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(); await profiles.load();
@ -153,10 +183,6 @@ async function initialize_app() {
setup_close(); setup_close();
} }
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
function str2ab8(str) { function str2ab8(str) {
const buf = new ArrayBuffer(str.length); const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf); const bufView = new Uint8Array(buf);
@ -177,68 +203,58 @@ function arrayBufferBase64(base64: string) {
return buf; return buf;
} }
function base64ArrayBuffer(arrayBuffer) { function base64_encode_ab(source: ArrayBufferLike) {
var base64 = '' const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' let base64 = "";
var bytes = new Uint8Array(arrayBuffer) const bytes = new Uint8Array(source);
var byteLength = bytes.byteLength const byte_length = bytes.byteLength;
var byteRemainder = byteLength % 3 const byte_reminder = byte_length % 3;
var mainLength = byteLength - byteRemainder const main_length = byte_length - byte_reminder;
var a, b, c, d let a, b, c, d;
var chunk let chunk;
// Main loop deals with bytes in chunks of 3 // 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 // 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 // Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18 a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12 b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6 c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63 // 63 = 2^6 - 1 d = (chunk & 63) >> 0; // 63 = (2^6 - 1) << 0
// Convert the raw binary segments to the appropriate ASCII encoding // 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 // Deal with the remaining bytes and padding
if (byteRemainder == 1) { if (byte_reminder == 1) {
chunk = bytes[mainLength] 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 // 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] + '==' base64 += encodings[a] + encodings[b] + '==';
} else if (byteRemainder == 2) { } else if (byte_reminder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] chunk = (bytes[main_length] << 8) | bytes[main_length + 1];
a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero // 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 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 { class TestProxy extends bipc.MethodProxy {
constructor(params: bipc.MethodProxyConnectParameters) { constructor(params: bipc.MethodProxyConnectParameters) {
@ -282,7 +298,6 @@ interface Window {
} }
*/ */
function main() { function main() {
/* /*
window.proxy_instance = new TestProxy({ 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 //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")); server_connections = new ServerConnectionManager($("#connection-handlers"));
control_bar.initialise(); /* before connection handler to allow property apply */ control_bar.initialise(); /* before connection handler to allow property apply */
@ -306,7 +338,7 @@ function main() {
initial_handler.acquire_recorder(default_recorder, false); initial_handler.acquire_recorder(default_recorder, false);
control_bar.set_connection_handler(initial_handler); control_bar.set_connection_handler(initial_handler);
/** Setup the XF forum identity **/ /** Setup the XF forum identity **/
profiles.identities.setup_forum(); profiles.identities.update_forum();
let _resize_timeout: NodeJS.Timer; let _resize_timeout: NodeJS.Timer;
$(window).on('resize', event => { $(window).on('resize', event => {
@ -334,11 +366,6 @@ function main() {
console.log("Received user count update: %o", status); console.log("Received user count update: %o", status);
}); });
/*
setTimeout(() => {
Modals.spawnAvatarList(globalClient);
}, 1000);
*/
(<any>window).test_upload = (message?: string) => { (<any>window).test_upload = (message?: string) => {
message = message || "Hello World"; message = message || "Hello World";
@ -366,16 +393,6 @@ function main() {
}; };
server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]); 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, "")) { 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); 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()) { if(profile && profile.valid()) {
const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler(); const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler();
connection.startConnection(address, profile, { connection.startConnection(address, profile, true, {
nickname: username, nickname: username,
password: password.length > 0 ? { password: password.length > 0 ? {
password: password, password: password,
@ -397,7 +414,7 @@ function main() {
} : undefined } : undefined
}); });
} else { } else {
Modals.spawnConnectModal({ Modals.spawnConnectModal({},{
url: address, url: address,
enforce: true 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 = { const task_teaweb_starter: loader.Task = {

View File

@ -55,7 +55,11 @@ class Group {
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => { this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
client.updateGroupIcon(this); 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; 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 { handle_command(command: connection.ServerCommand): boolean {
switch (command.command) { switch (command.command) {
case "notifyservergrouplist": case "notifyservergrouplist":
@ -94,6 +104,11 @@ class GroupManager extends connection.AbstractCommandHandler {
static sorter() : (a: Group, b: Group) => number { static sorter() : (a: Group, b: Group) => number {
return (a, b) => { return (a, b) => {
if(!a)
return b ? 1 : 0;
if(!b)
return a ? -1 : 0;
if(a.properties.sortid > b.properties.sortid) if(a.properties.sortid > b.properties.sortid)
return 1; return 1;
if(a.properties.sortid < b.properties.sortid) if(a.properties.sortid < b.properties.sortid)

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ namespace profiles.identities {
this.connection.send_command("handshakebegin", { this.connection.send_command("handshakebegin", {
intention: 0, intention: 0,
authentication_method: this.identity.type(), authentication_method: this.identity.type(),
data: this.identity.data_json() data: this.identity.data().data_json()
}).catch(error => { }).catch(error => {
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), 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) { private handle_proof(json) {
this.connection.send_command("handshakeindentityproof", { this.connection.send_command("handshakeindentityproof", {
proof: this.identity.data_sign() proof: this.identity.data().data_sign()
}).catch(error => { }).catch(error => {
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), 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 { export class TeaForumIdentity implements Identity {
private identity_data: string; private readonly identity_data: forum.Data;
private identity_data_raw: string;
private identity_data_sign: string;
valid() : boolean { 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) { constructor(data: forum.Data) {
this.identity_data_raw = data; this.identity_data = data;
this.identity_data_sign = sign;
try {
this.identity_data = data ? JSON.parse(this.identity_data_raw) : undefined;
} catch(error) { }
} }
data_json() : string { return this.identity_data_raw; } data() : forum.Data {
data_sign() : string { return this.identity_data_sign; } return this.identity_data;
}
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();
*/
decode(data) : Promise<void> { decode(data) : Promise<void> {
data = JSON.parse(data); data = JSON.parse(data);
if(data.version !== 1) if(data.version !== 1)
throw "invalid version"; 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; return;
} }
encode?() : string { encode() : string {
return JSON.stringify({ return JSON.stringify({
version: 1, version: 1
identity_data: this.identity_data_raw,
identity_sign: this.identity_data_sign
}); });
} }
spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler { spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler {
return new TeaForumHandshakeHandler(connection, this); 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; let static_identity: TeaForumIdentity;
@ -127,12 +104,10 @@ namespace profiles.identities {
static_identity = identity; static_identity = identity;
} }
export function setup_forum() { export function update_forum() {
const user_data = settings.static("forum_user_data") as string; if(forum.logged_in() && (!static_identity || static_identity.data() !== forum.data())) {
const user_sign = settings.static("forum_user_sign") as string; static_identity = new TeaForumIdentity(forum.data());
}
if(user_data && user_sign)
static_identity = new TeaForumIdentity(user_data, user_sign);
} }
export function valid_static_forum_identity() : boolean { export function valid_static_forum_identity() : boolean {

View File

@ -2,6 +2,20 @@
namespace profiles.identities { namespace profiles.identities {
export namespace CryptoHelper { 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) { export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) {
/* /*
Tomcrypt public key export: Tomcrypt public key export:
@ -50,7 +64,7 @@ namespace profiles.identities {
buffer[index++] = 0x02; /* type */ buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */ 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) { if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1; buffer[index - 1] += 1;
buffer[index++] = 0; buffer[index++] = 0;
@ -68,7 +82,7 @@ namespace profiles.identities {
buffer[index++] = 0x02; /* type */ buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */ 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) { if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1; buffer[index - 1] += 1;
buffer[index++] = 0; buffer[index++] = 0;
@ -87,7 +101,7 @@ namespace profiles.identities {
buffer[index++] = 0x02; /* type */ buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */ 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) { if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1; buffer[index - 1] += 1;
buffer[index++] = 0; buffer[index++] = 0;
@ -104,7 +118,7 @@ namespace profiles.identities {
buffer[1] = index - 2; /* set the final sequence length */ 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"; const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e";
@ -125,7 +139,7 @@ namespace profiles.identities {
for(let i = 0; i < length; i++) for(let i = 0; i < length; i++)
buffer[i] ^= crypt_key.charCodeAt(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> { export async function encrypt_ts_identity(buffer: Uint8Array) : Promise<string> {
@ -137,7 +151,7 @@ namespace profiles.identities {
for(let i = 0; i < 20; i++) for(let i = 0; i < 20; i++)
buffer[i] ^= hash[i]; buffer[i] ^= hash[i];
return base64ArrayBuffer(buffer); return base64_encode_ab(buffer);
} }
/** /**
@ -185,9 +199,9 @@ namespace profiles.identities {
*/ */
return { return {
crv: "P-256", crv: "P-256",
d: Base64EncodeUrl(btoa(k)), d: base64_url_encode(btoa(k)),
x: Base64EncodeUrl(btoa(x)), x: base64_url_encode(btoa(x)),
y: Base64EncodeUrl(btoa(y)), y: base64_url_encode(btoa(y)),
ext: true, ext: true,
key_ops:["deriveKey", "sign"], key_ops:["deriveKey", "sign"],
@ -587,7 +601,7 @@ namespace profiles.identities {
if(carry) if(carry)
char_result.push(49); 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 { try {
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); 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) { } catch(error) {
log.error(LogCategory.IDENTITIES, error); log.error(LogCategory.IDENTITIES, error);
throw "failed to calculate unique id"; throw "failed to calculate unique id";
@ -840,7 +854,7 @@ namespace profiles.identities {
} }
buffer[1] = index - 2; 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 { 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>; alert() : JQuery<TElement>;
modal(properties: any) : this; modal(properties: any) : this;
bootstrapMaterialDesign() : 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> { interface JQueryStatic<TElement extends Node = HTMLElement> {
@ -184,6 +187,12 @@ if(typeof ($) !== "undefined") {
this.attr("style", original_style || ""); this.attr("style", original_style || "");
return result; 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) { if (!String.prototype.format) {
@ -247,8 +256,39 @@ function calculate_width(text: string) : number {
return size; 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 webkitAudioContext extends AudioContext {}
declare class webkitOfflineAudioContext extends OfflineAudioContext {} declare class webkitOfflineAudioContext extends OfflineAudioContext {}
interface Window { interface Window {
readonly webkitAudioContext: typeof webkitAudioContext; readonly webkitAudioContext: typeof webkitAudioContext;
readonly AudioContext: typeof webkitAudioContext; readonly AudioContext: typeof webkitAudioContext;
@ -258,6 +298,10 @@ interface Window {
readonly Pointer_stringify: any; readonly Pointer_stringify: any;
readonly jsrender: any; readonly jsrender: any;
twemoji: Twemoji;
hljs: HighlightJS;
remarkable: any;
require(id: string): any; require(id: string): any;
} }

View File

@ -18,6 +18,8 @@ interface SettingsKey<T> {
fallback_imports?: {[key: string]:(value: string) => T}; fallback_imports?: {[key: string]:(value: string) => T};
description?: string; description?: string;
default_value?: T; default_value?: T;
require_restart?: boolean;
} }
class SettingsBase { class SettingsBase {
@ -144,6 +146,13 @@ class Settings extends StaticSettings {
key: 'disableContextMenu', key: 'disableContextMenu',
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier' 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> = { static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey<boolean> = {
key: 'disableUnloadDialog', key: 'disableUnloadDialog',
description: 'Disables the unload popup on side closing' description: 'Disables the unload popup on side closing'
@ -154,6 +163,8 @@ class Settings extends StaticSettings {
}; };
static readonly KEY_DISABLE_MULTI_SESSION: SettingsKey<boolean> = { static readonly KEY_DISABLE_MULTI_SESSION: SettingsKey<boolean> = {
key: 'disableMultiSession', key: 'disableMultiSession',
default_value: false,
require_restart: true
}; };
static readonly KEY_LOAD_DUMMY_ERROR: SettingsKey<boolean> = { static readonly KEY_LOAD_DUMMY_ERROR: SettingsKey<boolean> = {
@ -194,6 +205,9 @@ class Settings extends StaticSettings {
static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey<boolean> = { static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey<boolean> = {
key: 'connect_password_hashed' key: 'connect_password_hashed'
}; };
static readonly KEY_CONNECT_HISTORY: SettingsKey<string> = {
key: 'connect_history'
};
static readonly KEY_CERTIFICATE_CALLBACK: SettingsKey<string> = { static readonly KEY_CERTIFICATE_CALLBACK: SettingsKey<string> = {
key: 'certificate_callback' key: 'certificate_callback'
@ -201,11 +215,82 @@ class Settings extends StaticSettings {
/* sounds */ /* sounds */
static readonly KEY_SOUND_MASTER: SettingsKey<number> = { 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> = { 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 => { 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 { 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; const default_object = { seed: Math.random() } as any;
let _static = this.static(key, default_object, typeof _default); let _static = this.static(key, default_object, typeof _default);
if(_static !== default_object) return StaticSettings.transformStO(_static, _default); if(_static !== default_object) return StaticSettings.transformStO(_static, actual_default);
return this.global<T>(key, _default); return this.global<T>(key, actual_default);
} }
global?<T>(key: string | SettingsKey<T>, _default?: T) : T { 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){ changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
@ -287,6 +375,7 @@ class ServerSettings extends SettingsBase {
private currentServer: ServerEntry; private currentServer: ServerEntry;
private _server_save_worker: NodeJS.Timer; private _server_save_worker: NodeJS.Timer;
private _server_settings_updated: boolean = false; private _server_settings_updated: boolean = false;
private _destroyed = false;
constructor() { constructor() {
super(); super();
@ -296,11 +385,23 @@ class ServerSettings extends SettingsBase {
}, 5 * 1000); }, 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 { 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]); return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
} }
changeServer<T>(key: string | SettingsKey<T>, value?: T) { changeServer<T>(key: string | SettingsKey<T>, value?: T) {
if(this._destroyed) throw "destroyed";
key = Settings.keyify(key); key = Settings.keyify(key);
if(this.cacheServer[key.key] == value) return; if(this.cacheServer[key.key] == value) return;
@ -313,6 +414,7 @@ class ServerSettings extends SettingsBase {
} }
setServer(server: ServerEntry) { setServer(server: ServerEntry) {
if(this._destroyed) throw "destroyed";
if(this.currentServer) { if(this.currentServer) {
this.save(); this.save();
this.cacheServer = {}; this.cacheServer = {};
@ -329,6 +431,7 @@ class ServerSettings extends SettingsBase {
} }
save() { save() {
if(this._destroyed) throw "destroyed";
this._server_settings_updated = false; this._server_settings_updated = false;
if(this.currentServer) { if(this.currentServer) {

View File

@ -5,6 +5,12 @@ enum Sound {
AWAY_ACTIVATED = "away_activated", AWAY_ACTIVATED = "away_activated",
AWAY_DEACTIVATED = "away_deactivated", 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_CONNECTED = "connection.connected",
CONNECTION_DISCONNECTED = "connection.disconnected", CONNECTION_DISCONNECTED = "connection.disconnected",
CONNECTION_BANNED = "connection.banned", CONNECTION_BANNED = "connection.banned",
@ -155,23 +161,22 @@ namespace sound {
const data: any = {}; const data: any = {};
data.version = 1; data.version = 1;
for(const sound in Sound) { for(const key in Sound) {
if(typeof(speech_volume[sound]) !== "undefined") if(typeof(speech_volume[Sound[key]]) !== "undefined")
data[sound] = speech_volume[sound]; data[Sound[key]] = speech_volume[Sound[key]];
} }
data.master = master_volume; data.master = master_volume;
data.overlap = overlap_sounds; data.overlap = overlap_sounds;
data.ignore_muted = ignore_muted; data.ignore_muted = ignore_muted;
settings.changeGlobal("sound_volume", JSON.stringify(data)); settings.changeGlobal("sound_volume", JSON.stringify(data));
console.error(data);
} }
} }
export function initialize() : Promise<void> { export function initialize() : Promise<void> {
$.ajaxSetup({ $.ajaxSetup({
beforeSend: function(jqXHR,settings){ beforeSend: function(jqXHR,settings){
if (settings.dataType === 'binary'){ if (settings.dataType === 'binary') {
settings.xhr().responseType = 'arraybuffer'; settings.xhr().responseType = 'arraybuffer';
settings.processData = false; settings.processData = false;
} }
@ -181,12 +186,11 @@ namespace sound {
/* volumes */ /* volumes */
{ {
const data = JSON.parse(settings.static_global("sound_volume", "{}")); const data = JSON.parse(settings.static_global("sound_volume", "{}"));
for(const sound in Sound) { for(const sound_key in Sound) {
if(typeof(data[sound]) !== "undefined") if(typeof(data[Sound[sound_key]]) !== "undefined")
speech_volume[sound] = data[sound]; speech_volume[Sound[sound_key]] = data[Sound[sound_key]];
} }
console.error(data);
master_volume = data.master || 1; master_volume = data.master || 1;
overlap_sounds = data.overlap || true; overlap_sounds = data.overlap || true;
ignore_muted = data.ignore_muted || true; ignore_muted = data.ignore_muted || true;
@ -223,6 +227,8 @@ namespace sound {
ignore_overlap?: boolean; ignore_overlap?: boolean;
default_volume?: number; default_volume?: number;
callback?: (flag: boolean) => any;
} }
export async function resolve_sound(sound: Sound) : Promise<SoundHandle> { export async function resolve_sound(sound: Sound) : Promise<SoundHandle> {
@ -358,6 +364,8 @@ namespace sound {
handle.replaying = true; handle.replaying = true;
player.onended = event => { player.onended = event => {
if(options.callback)
options.callback(true);
delete this._playing_sounds[_sound]; delete this._playing_sounds[_sound];
}; };
@ -375,11 +383,24 @@ namespace sound {
} }
} else if(handle.node) { } else if(handle.node) {
handle.node.currentTime = 0; 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 { } 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; 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 //Only after request
channel_description: string = ""; channel_description: string = "";
channel_flag_conversation_private: boolean = false;
} }
class ChannelEntry { class ChannelEntry {
@ -70,6 +72,7 @@ class ChannelEntry {
private _tag_siblings: JQuery<HTMLElement>; /* container for all sub channels */ private _tag_siblings: JQuery<HTMLElement>; /* container for all sub channels */
private _tag_clients: JQuery<HTMLElement>; /* container for all clients */ private _tag_clients: JQuery<HTMLElement>; /* container for all clients */
private _tag_channel: JQuery<HTMLElement>; /* container for the channel info itself */ private _tag_channel: JQuery<HTMLElement>; /* container for the channel info itself */
private _destroyed = false;
private _cachedPassword: string; private _cachedPassword: string;
private _cached_channel_description: string = undefined; private _cached_channel_description: string = undefined;
@ -91,6 +94,26 @@ class ChannelEntry {
this.__updateChannelName(); 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(){ channelName(){
return this.properties.channel_name; return this.properties.channel_name;
} }
@ -186,7 +209,7 @@ class ChannelEntry {
if(current_index == new_index && !enforce) return; if(current_index == new_index && !enforce) return;
this._tag_channel.css("z-index", this._family_index); 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 { calculate_family_index(enforce_recalculate: boolean = false) : number {
@ -213,6 +236,15 @@ class ChannelEntry {
container_entry.attr("channel-id", this.channelId); container_entry.attr("channel-id", this.channelId);
container_entry.addClass(this._channel_name_alignment); 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) */ /* channel icon (type) */
{ {
container_entry.append( container_entry.append(
@ -317,7 +349,7 @@ class ChannelEntry {
/* /*
setInterval(() => { setInterval(() => {
let color = (Math.random() * 10000000).toString(16).substr(0, 6); let color = (Math.random() * 10000000).toString(16).substr(0, 6);
bg.css("background", "#" + color); tag_channel.css("background", "#" + color);
}, 150); }, 150);
*/ */
@ -455,23 +487,31 @@ class ChannelEntry {
const bold = text => contextmenu.get_provider().html_format_enabled() ? "<b>" + text + "</b>" : text; const bold = text => contextmenu.get_provider().html_format_enabled() ? "<b>" + text + "</b>" : text;
contextmenu.spawn_context_menu(x, y, { 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, type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-channel_switch", icon_class: "client-channel_switch",
name: bold(tr("Switch to channel")), name: bold(tr("Switch to channel")),
callback: () => this.joinChannel() 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(); const local_client = this.channelTree.client.getClient();
@ -734,13 +774,19 @@ class ChannelEntry {
this.updateChannelTypeIcon(); this.updateChannelTypeIcon();
info_update = true; 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(); group.end();
if(info_update) { if(info_update) {
const _client = this.channelTree.client.getClient(); const _client = this.channelTree.client.getClient();
if(_client.currentChannel() === this) 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! //TODO chat channel!
} }
} }
@ -855,6 +901,7 @@ class ChannelEntry {
get flag_subscribed() : boolean { get flag_subscribed() : boolean {
return this._flag_subscribed; return this._flag_subscribed;
} }
set flag_subscribed(flag: boolean) { set flag_subscribed(flag: boolean) {
if(this._flag_subscribed == flag) if(this._flag_subscribed == flag)
return; return;
@ -875,6 +922,10 @@ class ChannelEntry {
this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode); 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 { log_data() : log.server.base.Channel {
return { return {
channel_name: this.channelName(), channel_name: this.channelName(),

View File

@ -1,10 +1,7 @@
/// <reference path="channel.ts" /> /// <reference path="channel.ts" />
/// <reference path="modal/ModalChangeVolume.ts" /> /// <reference path="modal/ModalChangeVolume.ts" />
/// <reference path="modal/ModalServerGroupDialog.ts" />
/// <reference path="client_move.ts" /> /// <reference path="client_move.ts" />
import KeyEvent = ppt.KeyEvent;
enum ClientType { enum ClientType {
CLIENT_VOICE, CLIENT_VOICE,
CLIENT_QUERY, CLIENT_QUERY,
@ -35,6 +32,7 @@ class ClientProperties {
client_away_message: string = ""; client_away_message: string = "";
client_away: boolean = false; client_away: boolean = false;
client_country: string = "";
client_input_hardware: boolean = false; client_input_hardware: boolean = false;
client_output_hardware: boolean = false; client_output_hardware: boolean = false;
@ -42,8 +40,9 @@ class ClientProperties {
client_output_muted: boolean = false; client_output_muted: boolean = false;
client_is_channel_commander: boolean = false; client_is_channel_commander: boolean = false;
client_teaforum_id: number = 0; client_teaforo_id: number = 0;
client_teaforum_name: string = ""; client_teaforo_name: string = "";
client_teaforo_flags: number = 0; /* 0x01 := Banned | 0x02 := Stuff | 0x04 := Premium */
client_talk_power: number = 0; client_talk_power: number = 0;
} }
@ -55,9 +54,12 @@ class ClientEntry {
protected _properties: ClientProperties; protected _properties: ClientProperties;
protected lastVariableUpdate: number = 0; protected lastVariableUpdate: number = 0;
protected _speaking: boolean = false; protected _speaking: boolean;
protected _listener_initialized: boolean; protected _listener_initialized: boolean;
protected _audio_handle: connection.voice.VoiceClient; protected _audio_handle: connection.voice.VoiceClient;
protected _audio_volume: number;
protected _audio_muted: boolean;
channelTree: ChannelTree; channelTree: ChannelTree;
@ -69,10 +71,42 @@ class ClientEntry {
this._channel = null; 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) { set_audio_handle(handle: connection.voice.VoiceClient) {
if(this._audio_handle === handle) if(this._audio_handle === handle)
return; 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? //TODO may ensure that the id is the same?
this._audio_handle = handle; this._audio_handle = handle;
if(!handle) { if(!handle) {
@ -97,6 +131,41 @@ class ClientEntry {
clientUid(){ return this.properties.client_unique_identifier; } clientUid(){ return this.properties.client_unique_identifier; }
clientId(){ return this._clientId; } 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(){ protected initializeListener(){
if(this._listener_initialized) return; if(this._listener_initialized) return;
this._listener_initialized = true; this._listener_initialized = true;
@ -116,7 +185,7 @@ class ClientEntry {
if($.isArray(this.channelTree.currently_selected)) { //Multiselect if($.isArray(this.channelTree.currently_selected)) { //Multiselect
return; return;
} }
this.chat(true).focus(); this.open_text_chat();
}); });
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { 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[] { protected assignment_context() : contextmenu.MenuEntry[] {
let server_groups: contextmenu.MenuEntry[] = []; let server_groups: contextmenu.MenuEntry[] = [];
for(let group of this.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { for(let group of this.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) {
@ -229,7 +317,7 @@ class ClientEntry {
sub_menu: [ sub_menu: [
{ {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
icon: "client-permission_server_groups", icon_class: "client-permission_server_groups",
name: "Server groups dialog", name: "Server groups dialog",
callback: () => { callback: () => {
Modals.createServerGroupAssignmentModal(this, (group, flag) => { Modals.createServerGroupAssignmentModal(this, (group, flag) => {
@ -260,36 +348,50 @@ class ClientEntry {
type: contextmenu.MenuEntryType.SUB_MENU, type: contextmenu.MenuEntryType.SUB_MENU,
icon_class: "client-permission_client", icon_class: "client-permission_client",
name: tr("Permissions"), 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) { showContextMenu(x: number, y: number, on_close: () => void = undefined) {
let trigger_close = true; let trigger_close = true;
contextmenu.spawn_context_menu(x, y, contextmenu.spawn_context_menu(x, y,
{ ...this.contextmenu_info(), {
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: ''
}, {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-change_nickname", icon_class: "client-change_nickname",
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") + name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
tr("Open text chat") + tr("Open text chat") +
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""), (contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
callback: () => { callback: () => {
this.channelTree.client.chat.activeChat = this.chat(true); this.open_text_chat();
this.channelTree.client.chat.focus();
} }
}, { }, {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
@ -417,15 +519,29 @@ class ClientEntry {
icon_class: "client-volume", icon_class: "client-volume",
name: tr("Change Volume"), name: tr("Change Volume"),
callback: () => { 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.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) if(this.channelTree.client.select_info.currentSelected == this)
this.channelTree.client.select_info.update(); 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) { set speaking(flag) {
if(flag == this._speaking) return; if(flag === this._speaking) return;
this._speaking = flag; this._speaking = flag;
this.updateClientSpeakIcon(); this.updateClientSpeakIcon();
} }
@ -531,8 +647,10 @@ class ClientEntry {
icon = "client-server_query"; icon = "client-server_query";
console.log("Server query!"); console.log("Server query!");
} else { } else {
if(this.properties.client_away) { if (this.properties.client_away) {
icon = "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) { } else if(!this.properties.client_output_hardware) {
icon = "client-hardware_output_muted"; icon = "client-hardware_output_muted";
} else if(this.properties.client_output_muted) { } else if(this.properties.client_output_muted) {
@ -582,6 +700,7 @@ class ClientEntry {
let update_icon_speech = false; let update_icon_speech = false;
let update_away = false; let update_away = false;
let reorder_channel = false; let reorder_channel = false;
let update_avatar = false;
{ {
const entries = []; const entries = [];
@ -595,13 +714,34 @@ class ClientEntry {
} }
for(const variable of variables) { for(const variable of variables) {
const old_value = this._properties[variable.key];
JSON.map_field_to(this._properties, variable.value, variable.key); JSON.map_field_to(this._properties, variable.value, variable.key);
if(variable.key == "client_nickname") { if(variable.key == "client_nickname") {
this.tag.find(".client-name").text(variable.value); if(variable.value !== old_value && typeof(old_value) === "string") {
let chat = this.chat(false); if(!(this instanceof LocalClientEntry)) { /* own changes will be logged somewhere else */
if(chat) chat.name = variable.value; 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; reorder_channel = true;
} }
if( if(
@ -617,13 +757,15 @@ class ClientEntry {
update_away = true; update_away = true;
} }
if(variable.key == "client_unique_identifier") { if(variable.key == "client_unique_identifier") {
if(this._audio_handle) { this._audio_volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1"));
const 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._audio_handle.set_volume(volume); this.set_muted(mute_status, false, mute_status); /* force only needed when we want to mute the client */
log.debug(LogCategory.CLIENT, tr("Loaded client volume %d for client %s from config."), volume, this.clientUid());
} else { if(this._audio_handle)
log.warn(LogCategory.CLIENT, tr("Visible client got unique id assigned, but hasn't yet an audio handle. Ignoring volume assignment.")); 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") { if(variable.key == "client_talk_power") {
reorder_channel = true; reorder_channel = true;
@ -639,6 +781,8 @@ class ClientEntry {
} }
if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups") if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups")
this.update_displayed_client_groups(); this.update_displayed_client_groups();
else if(variable.key == "client_flag_avatar")
update_avatar = true;
} }
/* process updates after variables have been set */ /* process updates after variables have been set */
@ -651,15 +795,30 @@ class ClientEntry {
if(update_away) if(update_away)
this.updateAwayMessage(); 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(); group.end();
} }
update_displayed_client_groups() { 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()) for(let id of this.assignedServerGroupIds())
this.updateGroupIcon(this.channelTree.client.groups.serverGroup(id)); 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)); this.updateGroupIcon(this.channelTree.client.groups.channelGroup(this.properties.client_channel_group_id));
let prefix_groups: string[] = []; 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() { updateClientIcon() {
this.tag.find(".container-icon-client").children().detach(); this.tag.find(".container-icon-client").children().remove();
if(this.properties.client_icon_id > 0) { if(this.properties.client_icon_id > 0) {
this.channelTree.client.fileManager.icons.generateTag(this.properties.client_icon_id).attr("title", "Client icon") this.channelTree.client.fileManager.icons.generateTag(this.properties.client_icon_id).attr("title", "Client icon")
.appendTo(this.tag.find(".container-icon-client")); .appendTo(this.tag.find(".container-icon-client"));
@ -742,18 +865,25 @@ class ClientEntry {
updateGroupIcon(group: Group) { updateGroupIcon(group: Group) {
if(!group) return; 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) { if (group.properties.iconid > 0) {
this.tag.find(".container-icons-group").append( container.append(
$.spawn("div") $.spawn("div").attr('group-power', group.properties.sortid)
.addClass("container-group-icon icon_group_" + group.id) .addClass("container-group-icon icon_group_" + group.id)
.append(this.channelTree.client.fileManager.icons.generateTag(group.properties.iconid)).attr("title", group.name) .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[] { assignedServerGroupIds() : number[] {
let result = []; let result = [];
for(let id of this.properties.client_servergroups.split(",")){ for(let id of this.properties.client_servergroups.split(",")){
@ -843,7 +973,7 @@ class LocalClientEntry extends ClientEntry {
const _self = this; const _self = this;
contextmenu.spawn_context_menu(x, y, contextmenu.spawn_context_menu(x, y,
{ ...this.contextmenu_info(), {
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") + name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
tr("Change name") + tr("Change name") +
@ -875,6 +1005,7 @@ class LocalClientEntry extends ClientEntry {
} }
initializeListener(): void { initializeListener(): void {
this._listener_initialized = false; /* could there be a better system */
super.initializeListener(); super.initializeListener();
this.tag.find(".client-name").addClass("client-name-own"); this.tag.find(".client-name").addClass("client-name-own");
@ -918,11 +1049,14 @@ class LocalClientEntry extends ClientEntry {
if(_self.clientNickName() == text) return; if(_self.clientNickName() == text) return;
elm.text(_self.clientNickName()); elm.text(_self.clientNickName());
const old_name = _self.clientNickName();
_self.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => { _self.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => {
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, text); settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, text);
this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, { this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, {
client: this.log_data(), client: this.log_data(),
own_action: true old_name: old_name,
new_name: text,
own_client: true
}); });
}).catch((e: CommandResult) => { }).catch((e: CommandResult) => {
this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGE_FAILED, { 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()); 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 { get properties() : MusicClientProperties {
return this._properties as MusicClientProperties; return this._properties as MusicClientProperties;
} }
@ -980,20 +1121,7 @@ class MusicClientEntry extends ClientEntry {
showContextMenu(x: number, y: number, on_close: () => void = undefined): void { showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
let trigger_close = true; let trigger_close = true;
contextmenu.spawn_context_menu(x, y, contextmenu.spawn_context_menu(x, y,
{ ...this.contextmenu_info(), {
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: ''
}, {
name: tr("<b>Change bot name</b>"), name: tr("<b>Change bot name</b>"),
icon_class: "client-change_nickname", icon_class: "client-change_nickname",
disabled: false, disabled: false,
@ -1160,7 +1288,7 @@ class MusicClientEntry extends ClientEntry {
}, },
type: contextmenu.MenuEntryType.ENTRY 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; return;
menu.animate({opacity: 0}, 100, () => menu.css("display", "none")); 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(); callback();
}
this._close_callbacks = []; this._close_callbacks = [];
} }
@ -135,7 +140,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
} }
return tag; return tag;
} else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) { } 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("input").attr("type", "checkbox").prop("checked", !!entry.checkbox_checked).appendTo(checkbox);
$.spawn("span").addClass("checkmark").appendTo(checkbox); $.spawn("span").addClass("checkmark").appendTo(checkbox);
@ -191,7 +196,8 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
continue; continue;
if(entry.type == contextmenu.MenuEntryType.CLOSE) { if(entry.type == contextmenu.MenuEntryType.CLOSE) {
this._close_callbacks.push(entry.callback); if(entry.callback)
this._close_callbacks.push(entry.callback);
} else } else
menu_container.append(this.generate_tag(entry)); menu_container.append(this.generate_tag(entry));
} }

View File

@ -1,3 +1,5 @@
import ClickEvent = JQuery.ClickEvent;
enum ElementType { enum ElementType {
HEADER, HEADER,
BODY, BODY,
@ -22,7 +24,7 @@ const ModalFunctions = {
switch (typeof val){ switch (typeof val){
case "string": case "string":
if(type == ElementType.HEADER) if(type == ElementType.HEADER)
return $.spawn("h5").addClass("modal-title").text(val); return $.spawn("div").addClass("modal-title").text(val);
return $("<div>" + val + "</div>"); return $("<div>" + val + "</div>");
case "object": return val as JQuery; case "object": return val as JQuery;
case "undefined": case "undefined":
@ -61,6 +63,7 @@ class ModalProperties {
return this; return this;
} }
width: number | string = "60%"; width: number | string = "60%";
min_width?: number | string;
height: number | string = "auto"; height: number | string = "auto";
closeable: boolean = true; closeable: boolean = true;
@ -78,8 +81,33 @@ class ModalProperties {
full_size?: boolean = false; 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; private _htmlTag: JQuery;
properties: ModalProperties; properties: ModalProperties;
shown: boolean; shown: boolean;
@ -119,32 +147,57 @@ class Modal {
Object.assign(properties, this.properties.template_properties); Object.assign(properties, this.properties.template_properties);
const tag = template.renderTag(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 = 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 = 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('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() { open() {
if(this.shown)
return;
_global_modal_last_time = Date.now();
_global_modal_last = this.htmlTag[0];
this.shown = true; this.shown = true;
this.htmlTag.appendTo($("body")); this.htmlTag.appendTo($("body"));
this.htmlTag.bootstrapMaterialDesign().modal(this.properties.closeable ? 'show' : { _global_modal_count++;
backdrop: 'static', this.htmlTag.show();
keyboard: false, setTimeout(() => this.htmlTag.addClass('shown'), 0);
});
if(this.properties.trigger_tab) setTimeout(() => {
this.htmlTag.one('shown.bs.modal', () => this.htmlTag.find(".tab").trigger('tab.resize')); for(const listener of this.open_listener) listener();
this.htmlTag.find(".tab").trigger('tab.resize');
}, 300);
} }
close() { close() {
if(!this.shown) return; if(!this.shown) return;
_global_modal_count--;
this.shown = false; this.shown = false;
this.htmlTag.modal('hide'); this.htmlTag.removeClass('shown');
setTimeout(() => {
this.htmlTag.remove();
this._htmlTag = undefined;
}, 300);
this.properties.triggerClose(); this.properties.triggerClose();
for(const listener of this.close_listener) for(const listener of this.close_listener)
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")) if(header_tag.attr("x-entry-class"))
tag_header.addClass(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); tag_header.append(header_data);
/* listener if the tab might got removed */ /* 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 connection_handler: ConnectionHandler | undefined;
private _button_hostbanner: JQuery;
htmlTag: JQuery; htmlTag: JQuery;
constructor(htmlTag: JQuery) { constructor(htmlTag: JQuery) {
this.htmlTag = htmlTag; this.htmlTag = htmlTag;
@ -47,6 +49,7 @@ class ControlBar {
this.connection_handler = handler; this.connection_handler = handler;
this.apply_server_state(); this.apply_server_state();
this.update_connection_state();
} }
apply_server_state() { apply_server_state() {
@ -63,15 +66,30 @@ class ControlBar {
this.button_query_visible = this.connection_handler.client_status.queries_visible; this.button_query_visible = this.connection_handler.client_status.queries_visible;
this.button_subscribe_all = this.connection_handler.client_status.channel_subscribe_all; this.button_subscribe_all = this.connection_handler.client_status.channel_subscribe_all;
this.apply_server_hostbutton();
this.apply_server_voice_state(); 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() { apply_server_voice_state() {
if(!this.connection_handler) if(!this.connection_handler)
return; return;
this.button_microphone = !this.connection_handler.client_status.input_hardware ? "disabled" : this.connection_handler.client_status.input_muted ? "muted" : "enabled"; 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"; this.button_speaker = this.connection_handler.client_status.output_muted ? "muted" : "enabled";
top_menu.update_state(); //TODO: Only run "small" update?
} }
current_connection_handler() { 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").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_disconnect").on('click', this.on_execute_disconnect.bind(this));
this.htmlTag.find(".btn_mute_input").on('click', this.on_toggle_microphone.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_use").on('click', this.on_token_use.bind(this));
this.htmlTag.find(".btn_token_list").on('click', this.on_token_list.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)); 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)); 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(".container-disconnect"));
dropdownify(this.htmlTag.find(".btn_token")); dropdownify(this.htmlTag.find(".btn_token"));
dropdownify(this.htmlTag.find(".btn_away")); dropdownify(this.htmlTag.find(".btn_away"));
@ -202,13 +231,20 @@ class ControlBar {
this._button_microphone = state; this._button_microphone = state;
let tag = this.htmlTag.find(".btn_mute_input"); 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.toggleClass('activated', state === "muted");
/*
tag_icon tag_icon
.toggleClass('client-input_muted', state === "muted") .toggleClass('client-input_muted', state === "muted")
.toggleClass('client-capture', state === "enabled") .toggleClass('client-capture', state === "enabled")
.toggleClass('client-activate_microphone', state === "disabled"); .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") if(state === "disabled")
tag_icon.attr('title', tr("Enable your microphone on this server")); tag_icon.attr('title', tr("Enable your microphone on this server"));
@ -224,12 +260,17 @@ class ControlBar {
this._button_speakers = state; this._button_speakers = state;
let tag = this.htmlTag.find(".btn_mute_output"); 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.toggleClass('activated', state === "muted");
/*
tag_icon tag_icon
.toggleClass('client-output_muted', state !== "enabled") .toggleClass('client-output_muted', state !== "enabled")
.toggleClass('client-volume', state === "enabled"); .toggleClass('client-volume', state === "enabled");
*/
tag_icon
.toggleClass('client-output_muted', true)
.toggleClass('client-volume', false);
if(state === "enabled") if(state === "enabled")
tag_icon.attr('title', tr("Mute sound")); tag_icon.attr('title', tr("Mute sound"));
@ -245,7 +286,7 @@ class ControlBar {
this.htmlTag this.htmlTag
.find(".button-subscribe-mode") .find(".button-subscribe-mode")
.toggleClass('activated', this._button_subscribe_all) .toggleClass('activated', this._button_subscribe_all)
.find('.icon_x32') .find('.icon_em')
.toggleClass('client-unsubscribe_from_all_channels', !this._button_subscribe_all) .toggleClass('client-unsubscribe_from_all_channels', !this._button_subscribe_all)
.toggleClass('client-subscribe_to_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() { 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"; this.button_microphone = "enabled";
else sound.manager.play(Sound.MICROPHONE_ACTIVATED);
} else {
this.button_microphone = "muted"; this.button_microphone = "muted";
sound.manager.play(Sound.MICROPHONE_MUTED);
}
if(this.connection_handler) { if(this.connection_handler) {
this.connection_handler.client_status.input_muted = this._button_microphone !== "enabled"; this.connection_handler.client_status.input_muted = this._button_microphone !== "enabled";
@ -338,10 +382,13 @@ class ControlBar {
} }
private on_toggle_sound() { private on_toggle_sound() {
if(this._button_speakers === "muted") if(this._button_speakers === "muted") {
this.button_speaker = "enabled"; this.button_speaker = "enabled";
else sound.manager.play(Sound.SOUND_ACTIVATED);
} else {
this.button_speaker = "muted"; this.button_speaker = "muted";
sound.manager.play(Sound.SOUND_MUTED);
}
if(this.connection_handler) { if(this.connection_handler) {
this.connection_handler.client_status.output_muted = this._button_speakers !== "enabled"; this.connection_handler.client_status.output_muted = this._button_speakers !== "enabled";
@ -379,8 +426,17 @@ class ControlBar {
private on_open_connect() { private on_open_connect() {
if(this.connection_handler) 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({ Modals.spawnConnectModal({
default_connect_new_tab: true
}, {
url: "ts.TeaSpeak.de", url: "ts.TeaSpeak.de",
enforce: false enforce: false
}); });
@ -410,7 +466,7 @@ class ControlBar {
} }
private on_execute_disconnect() { private on_execute_disconnect() {
this.connection_handler.cancel_reconnect(); this.connection_handler.cancel_reconnect(true);
this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message? this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
this.update_connection_state(); this.update_connection_state();
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED); this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
@ -426,7 +482,7 @@ class ControlBar {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open(); createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
}).catch(error => { }).catch(error => {
//TODO tr //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(); }).open();
} }
@ -459,23 +515,7 @@ class ControlBar {
} }
private on_bookmark_server_add() { private on_bookmark_server_add() {
if(this.connection_handler && this.connection_handler.connected) { bookmarks.add_current_server();
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();
}
} }
update_bookmark_status() { update_bookmark_status() {
@ -486,8 +526,8 @@ class ControlBar {
update_bookmarks() { update_bookmarks() {
//<div class="btn_bookmark_connect" target="localhost"><a>Localhost</a></div> //<div class="btn_bookmark_connect" target="localhost"><a>Localhost</a></div>
let tag_bookmark = this.htmlTag.find(".btn_bookmark .dropdown"); let tag_bookmark = this.htmlTag.find(".btn_bookmark > .dropdown");
tag_bookmark.find(".bookmark, .directory").detach(); tag_bookmark.find(".bookmark, .directory").remove();
const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => { const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => {
if(bookmark.type == bookmarks.BookmarkType.ENTRY) { if(bookmark.type == bookmarks.BookmarkType.ENTRY) {
@ -495,37 +535,14 @@ class ControlBar {
const bookmark_connect = (new_tab: boolean) => { const bookmark_connect = (new_tab: boolean) => {
this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working
bookmarks.boorkmak_connect(mark, new_tab);
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
})
}
}; };
return $.spawn("div") return $.spawn("div")
.addClass("bookmark") .addClass("bookmark")
.append( .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( .append(
$.spawn("div") $.spawn("div")
@ -550,7 +567,8 @@ class ControlBar {
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
name: tr("Connect in a new tab"), name: tr("Connect in a new tab"),
icon_class: 'client-connect', icon_class: 'client-connect',
callback: () => bookmark_connect(true) callback: () => bookmark_connect(true),
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
}, contextmenu.Entry.CLOSE(() => { }, contextmenu.Entry.CLOSE(() => {
setTimeout(() => { setTimeout(() => {
this.htmlTag.find(".btn_bookmark.dropdown-arrow").removeClass("force-show") this.htmlTag.find(".btn_bookmark.dropdown-arrow").removeClass("force-show")
@ -564,10 +582,7 @@ class ControlBar {
const mark = <bookmarks.DirectoryBookmark>bookmark; const mark = <bookmarks.DirectoryBookmark>bookmark;
const container = $.spawn("div").addClass("sub-menu dropdown"); const container = $.spawn("div").addClass("sub-menu dropdown");
for(const member of mark.content) const result = $.spawn("div")
container.append(build_entry(member));
return $.spawn("div")
.addClass("directory") .addClass("directory")
.append( .append(
$.spawn("div").addClass("icon client-folder") $.spawn("div").addClass("icon client-folder")
@ -583,7 +598,13 @@ class ControlBar {
.append( .append(
$.spawn("div").addClass("sub-container") $.spawn("div").addClass("sub-container")
.append(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 _current_manager: InfoManagerBase = undefined;
private managers: InfoManagerBase[] = []; private managers: InfoManagerBase[] = [];
private banner_manager: Hostbanner;
constructor(client: ConnectionHandler) { constructor(client: ConnectionHandler) {
this.handle = client; this.handle = client;
@ -74,8 +73,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
this.managers.push(new ChannelInfoManager()); this.managers.push(new ChannelInfoManager());
this.managers.push(new ServerInfoManager()); this.managers.push(new ServerInfoManager());
this.banner_manager = new Hostbanner(client, this._tag_banner);
this._tag.find("button.close").on('click', () => this.close_popover()); this._tag.find("button.close").on('click', () => this.close_popover());
} }
@ -83,6 +80,16 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
return this._tag; 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() { handle_resize() {
/* test if the popover isn't a popover anymore */ /* test if the popover isn't a popover anymore */
if(this._tag.parent().hasClass('shown')) { if(this._tag.parent().hasClass('shown')) {
@ -90,8 +97,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
if(this.is_popover()) if(this.is_popover())
this._tag.parent().addClass('shown'); this._tag.parent().addClass('shown');
} }
this.banner_manager.handle_resize();
} }
setCurrentSelected(entry: AvailableTypes) { 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); (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; } current_manager() { return this._current_manager; }
is_popover() : boolean { is_popover() : boolean {
@ -138,7 +139,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
open_popover() { open_popover() {
this._tag.parent().toggleClass('shown', true); this._tag.parent().toggleClass('shown', true);
this.banner_manager.handle_resize();
} }
close_popover() { close_popover() {
@ -155,161 +155,6 @@ interface Window {
HTMLImageElement: typeof HTMLImageElement; 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> { class ClientInfoManager extends InfoManager<ClientEntry> {
available<V>(object: V): boolean { available<V>(object: V): boolean {
return typeof object == "object" && object instanceof ClientEntry; return typeof object == "object" && object instanceof ClientEntry;

View File

@ -93,12 +93,19 @@ namespace MessageHelper {
const result: xbbcode.Result = xbbcode.parse(message, { const result: xbbcode.Result = xbbcode.parse(message, {
/* TODO make this configurable and allow IMG */ /* TODO make this configurable and allow IMG */
tag_whitelist: [ tag_whitelist: [
"b", "b", "big",
"i", "i", "italic",
"u", "u", "underlined",
"color", "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) { if(result.error) {
@ -106,470 +113,58 @@ namespace MessageHelper {
return formatElement(message); 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)); //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 { loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
date: Date; name: "XBBCode code tag init",
message: JQuery[]; function: async () => {
private _html_tag: JQuery<HTMLElement>; /* override default parser */
xbbcode.register.register_parser( {
tag: ["code", "icode", "i-code"],
content_tags_whitelist: [],
constructor(message: JQuery[]) { build_html(layer: xbbcode.TagLayer) : string {
this.date = new Date(); const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
this.message = message; const language = (layer.options || "").replace("\"", "'").toLowerCase();
}
private num(num: number) : string { /* remove heading empty lines */
let str = num.toString(); let text = layer.content.map(e => e.build_text())
while(str.length < 2) str = '0' + str; .reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "")
return str; .replace(/^([ \n\r\t]*)(?=\n)+/g, "");
} if(text.startsWith("\r") || text.startsWith("\n"))
text = text.substr(1);
get html_tag() { let result: HighlightJSResult;
if(this._html_tag) return this._html_tag; if(window.hljs.getLanguage(language))
result = window.hljs.highlight(language, text, true);
else
result = window.hljs.highlightAuto(text);
let tag = $.spawn("div"); let html = '<pre class="' + klass + '">';
tag.addClass("message"); html += '<code class="hljs" x-code-type="' + language + '" x-highlight-type="' + result.language + '">';
html += result.value;
let dateTag = $.spawn("div"); return html + "</code></pre>";
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();
} }
}); });
if(this.flag_closeable) { },
actions.push({ priority: 10
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;
}
} }

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_log_server: JQuery;
private _container_channel_tree: JQuery; private _container_channel_tree: JQuery;
private _container_hostbanner: JQuery;
private _container_select_info: JQuery; private _container_select_info: JQuery;
private _container_chat: JQuery; private _container_chat: JQuery;
@ -32,6 +33,7 @@ class ServerConnectionManager {
this._container_log_server = $("#server-log"); this._container_log_server = $("#server-log");
this._container_channel_tree = $("#channelTree"); this._container_channel_tree = $("#channelTree");
this._container_hostbanner = $("#hostbanner");
this._container_select_info = $("#select_info"); this._container_select_info = $("#select_info");
this._container_chat = $("#chat"); this._container_chat = $("#chat");
@ -52,7 +54,7 @@ class ServerConnectionManager {
destroy_server_connection_handler(handler: ConnectionHandler) { destroy_server_connection_handler(handler: ConnectionHandler) {
this.connection_handlers.remove(handler); this.connection_handlers.remove(handler);
handler.tag_connection_handler.detach(); handler.tag_connection_handler.remove();
this._update_scroll(); this._update_scroll();
this._tag.toggleClass("shown", this.connection_handlers.length > 1); this._tag.toggleClass("shown", this.connection_handlers.length > 1);
@ -64,11 +66,14 @@ class ServerConnectionManager {
if(handler === this.active_handler) if(handler === this.active_handler)
this.set_active_connection_handler(this.connection_handlers[0]); this.set_active_connection_handler(this.connection_handlers[0]);
/* destroy all elements */
handler.destroy();
} }
set_active_connection_handler(handler: ConnectionHandler) { set_active_connection_handler(handler: ConnectionHandler) {
if(handler && this.connection_handlers.indexOf(handler) == -1) 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) if(this.active_handler)
this.active_handler.select_info.close_popover(); this.active_handler.select_info.close_popover();
@ -77,19 +82,22 @@ class ServerConnectionManager {
this._container_select_info.children().detach(); this._container_select_info.children().detach();
this._container_chat.children().detach(); this._container_chat.children().detach();
this._container_log_server.children().detach(); this._container_log_server.children().detach();
this._container_hostbanner.children().detach();
control_bar.set_connection_handler(handler); control_bar.set_connection_handler(handler);
if(handler) { if(handler) {
handler.tag_connection_handler.addClass("active"); 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_channel_tree.append(handler.channelTree.tag_tree());
this._container_select_info.append(handler.select_info.get_tag()); 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()); this._container_log_server.append(handler.log.html_tag());
if(handler.invoke_resized_on_activate) if(handler.invoke_resized_on_activate)
handler.resize_elements(); handler.resize_elements();
} }
top_menu.update_state();
this.active_handler = handler; 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_FAILED = "connection_failed",
CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed", CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed",
CONNECTION_COMMAND_ERROR = "connection_command_error",
GLOBAL_MESSAGE = "global_message", 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_ENTER = "client_view_enter",
CLIENT_VIEW_LEAVE = "client_view_leave", CLIENT_VIEW_LEAVE = "client_view_leave",
CLIENT_VIEW_MOVE = "client_view_move", CLIENT_VIEW_MOVE = "client_view_move",
@ -79,6 +88,14 @@ namespace log {
permission: PermissionInfo; 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 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 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}") //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 */ reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */
} }
export type ConnectionCommandError = {
error: any;
}
export type ClientNicknameChanged = { export type ClientNicknameChanged = {
own_action: boolean; own_client: boolean;
client: base.Client; client: base.Client;
old_name: string;
new_name: string;
} }
export type ClientNicknameChangeFailed = { export type ClientNicknameChangeFailed = {
reason: string; reason: string;
} }
export type ServerClosed = {
message: string;
}
export type ServerRequiresPassword = {}
export type ServerBanned = {
message: string;
time: number;
invoker: base.Client;
}
} }
export type LogMessage = { export type LogMessage = {
@ -188,11 +226,20 @@ namespace log {
"connection_login": event.ConnectionLogin; "connection_login": event.ConnectionLogin;
"connection_connected": event.ConnectionConnected; "connection_connected": event.ConnectionConnected;
"connection_voice_setup_failed": event.ConnectionVoiceSetupFailed; "connection_voice_setup_failed": event.ConnectionVoiceSetupFailed;
"connection_command_error": event.ConnectionCommandError;
"reconnect_scheduled": event.ReconnectScheduled; "reconnect_scheduled": event.ReconnectScheduled;
"reconnect_canceled": event.ReconnectCanceled; "reconnect_canceled": event.ReconnectCanceled;
"reconnect_execute": event.ReconnectExecute; "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_enter": event.ClientEnter;
"client_view_move": event.ClientMove; "client_view_move": event.ClientMove;
"client_view_leave": event.ClientLeave; "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; type MessageBuilder<T extends keyof server.TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined;
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = { export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
"global_message": (data: event.GlobalMessage, options) => {
return [];
},
"error_custom": (data: event.ErrorCustom, options) => { "error_custom": (data: event.ErrorCustom, options) => {
return [$.spawn("div").addClass("log-error").text(data.message)] 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; 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]) { log<T extends keyof server.TypeInfo>(type: T, data: server.TypeInfo[T]) {
@ -263,6 +307,14 @@ namespace log {
return this._html_tag; 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) { private append_log(message: server.LogMessage) {
let container = $.spawn("div").addClass("log-message"); 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)); MessageHelper.formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container));
} else { } else {
const elements = builder(message.data, {}); const elements = builder(message.data, {});
if(!elements) if(!elements || elements.length == 0)
return; /* discard message */ return; /* discard message */
container.append(...elements); container.append(...elements);
} }
@ -297,7 +349,7 @@ namespace log {
while(messages.length - index > this.history_length) while(messages.length - index > this.history_length)
index++; index++;
const hide_elements = messages.filter(idx => idx < 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) if(this.auto_follow)
this._html_tag.scrollTop(this._html_tag[0].scrollHeight); this._html_tag.scrollTop(this._html_tag[0].scrollHeight);
@ -339,7 +391,7 @@ namespace log {
}; };
MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => { 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) => { 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 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 + ")")]; 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) if(properties.client_id)
result = result + "client-id='" + properties.client_id + "' "; result = result + "client-id='" + properties.client_id + "' ";
if(properties.client_unique_id && properties.client_unique_id != "unknown") if(properties.client_unique_id && properties.client_unique_id != "unknown") {
result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' "; 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) if(properties.client_name) {
result = result + "client-name='" + encodeURIComponent(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 */ /* add the click handler */
result += "oncontextmenu='return htmltags.callbacks.callback_context_client($(this));'"; 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; const lower_nibble = id.charCodeAt(index + 1) - 97;
buffer[index / 2] = (upper_nibble << 4) | lower_nibble; buffer[index / 2] = (upper_nibble << 4) | lower_nibble;
} }
return base64ArrayBuffer(buffer); return base64_encode_ab(buffer);
}; };
export const human_file_size = (size: number) => { export const human_file_size = (size: number) => {
@ -80,7 +80,7 @@ namespace Modals {
.css("display", "none") .css("display", "none")
.appendTo($("body")); .appendTo($("body"));
element[0].click(); element[0].click();
element.detach(); element.remove();
}; };
} }
})); }));

View File

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

View File

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

View File

@ -1,7 +1,100 @@
/// <reference path="../../ui/elements/modal.ts" /> /// <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 { 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; let selected_profile: profiles.ConnectionProfile;
const random_id = (() => { const random_id = (() => {
@ -10,12 +103,41 @@ namespace Modals {
return array.join(""); return array.join("");
})(); })();
const connect_modal = $("#tmpl_connect").renderTag({ const modal = createModal({
client: native_client, header: tr("Connect to a server"),
forum_path: settings.static("forum_path"), body: $("#tmpl_connect").renderTag({
password_id: random_id, client: native_client,
multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false) forum_path: settings.static("forum_path"),
}).modalize((header, body, footer) => { 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 = footer.find(".button-connect");
const button_connect_tab = footer.find(".button-connect-new-tab"); const button_connect_tab = footer.find(".button-connect-new-tab");
const button_manage = body.find(".button-manage-profiles"); const button_manage = body.find(".button-manage-profiles");
@ -25,7 +147,12 @@ namespace Modals {
const input_nickname = body.find(".container-nickname input"); const input_nickname = body.find(".container-nickname input");
const input_password = body.find(".container-password 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"); console.log("Updating");
if(selected_profile) if(selected_profile)
input_nickname.attr("placeholder", selected_profile.default_username); input_nickname.attr("placeholder", selected_profile.default_username);
@ -34,7 +161,7 @@ namespace Modals {
let address = input_address.val().toString(); let address = input_address.val().toString();
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address); 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(); let nickname = input_nickname.val().toString();
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname); 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.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
input_address input_address
.on("keyup", () => updateFields()) .on("keyup", () => updateFields(true))
.on('keydown', event => { .on('keydown', event => {
if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey) if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey)
button_connect.trigger('click'); button_connect.trigger('click');
}); });
button_manage.on('click', event => { button_manage.on('click', event => {
const modal = Modals.spawnSettingsModal(); const modal = Modals.spawnSettingsModal("identity-profiles");
setTimeout(() => {
modal.htmlTag.find(".tab-profiles").parent(".entry").trigger('click');
}, 100);
modal.close_listener.push(() => { modal.close_listener.push(() => {
input_profile.trigger('change'); input_profile.trigger('change');
}); });
@ -82,7 +206,7 @@ namespace Modals {
input_nickname.val(selected_profile.default_username); input_nickname.val(selected_profile.default_username);
} }
input_profile.toggleClass("is-invalid", !selected_profile || !selected_profile.valid()); 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'); 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); settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, last_nickname);
input_nickname.val(last_nickname); input_nickname.val(last_nickname);
input_nickname.on("keyup", () => updateFields()); input_nickname.on("keyup", () => updateFields(true));
setTimeout(() => updateFields(), 100); 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 => { button_connect.on('click', event => {
connect_modal.close(); modal.close();
const connection = server_connections.active_connection_handler(); const connection = server_connections.active_connection_handler();
if(connection) { if(connection) {
connection.startConnection( connection.startConnection(
input_address.val().toString(), current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
selected_profile, selected_profile,
true,
{ {
nickname: input_nickname.val().toString() || selected_profile.default_username, 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 { } else {
@ -111,24 +242,77 @@ namespace Modals {
} }
}); });
button_connect_tab.on('click', event => { button_connect_tab.on('click', event => {
connect_modal.close(); modal.close();
const connection = server_connections.spawn_server_connection_handler(); const connection = server_connections.spawn_server_connection_handler();
server_connections.set_active_connection_handler(connection); server_connections.set_active_connection_handler(connection);
connection.startConnection( connection.startConnection(
input_address.val().toString(), current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
selected_profile, selected_profile,
true,
{ {
nickname: input_nickname.val().toString() || selected_profile.default_username, 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; 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_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["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); let template = $("#tmpl_channel_edit").renderTag(render_properties);
return template.tabify();
}, /* the tab functionality */
footer: () => { {
let footer = $.spawn("div"); const container_tabs = template.find(".container-advanced");
footer.addClass("modal-button-group"); container_tabs.find(".categories .entry").on('click', event => {
footer.css("margin", "5px"); const entry = $(event.target);
let buttonCancel = $.spawn("button"); container_tabs.find(".bodies > .body").addClass("hidden");
buttonCancel.text(tr("Cancel")).addClass("button_cancel"); container_tabs.find(".categories > .selected").removeClass("selected");
let buttonOk = $.spawn("button"); entry.addClass("selected");
buttonOk.text(tr("Ok")).addClass("button_ok"); container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden");
});
footer.append(buttonCancel);
footer.append(buttonOk); container_tabs.find(".entry").first().trigger('click');
}
return footer;
/* 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 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); applyGeneralListener(connection, properties, modal.htmlTag.find(".container-general"), modal.htmlTag.find(".button_ok"), channel);
applyStandardListener(connection, properties, modal.htmlTag.find(".settings_standard"), modal.htmlTag.find(".button_ok"), parent, !channel); applyStandardListener(connection, properties, modal.htmlTag.find(".container-standard"), modal.htmlTag.find(".container-simple"), parent, channel);
applyPermissionListener(connection, properties, modal.htmlTag.find(".settings_permissions"), modal.htmlTag.find(".button_ok"), permissions, channel); applyPermissionListener(connection, properties, modal.htmlTag.find(".container-permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
applyAudioListener(connection, properties, modal.htmlTag.find(".container-channel-settings-audio"), modal.htmlTag.find(".button_ok"), channel); applyAudioListener(connection, properties, modal.htmlTag.find(".container-audio"), modal.htmlTag.find(".container-simple"), channel);
applyAdvancedListener(connection, properties, modal.htmlTag.find(".settings_advanced"), modal.htmlTag.find(".button_ok"), channel); applyAdvancedListener(connection, properties, modal.htmlTag.find(".container-misc"), modal.htmlTag.find(".button_ok"), channel);
let updated: PermissionValue[] = []; let updated: PermissionValue[] = [];
modal.htmlTag.find(".button_ok").click(() => { 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); let element = $(_element);
if(!element.prop("changed")) return; if(element.val() == element.attr("original-value")) return;
let permission = permissions.resolveInfo(element.attr("permission")); let permission = permissions.resolveInfo(element.attr("permission"));
if(!permission) { if(!permission) {
log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("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); console.log(tr("Updated permissions %o"), updated);
}).click(() => { }).click(() => {
modal.close(); 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 callback(properties, updated); //First may create the channel
}); });
tooltip(modal.htmlTag);
modal.htmlTag.find(".button_cancel").click(() => { modal.htmlTag.find(".button_cancel").click(() => {
modal.close(); modal.close();
callback(); callback();
@ -92,8 +122,8 @@ namespace Modals {
tag.find(".button-select-icon").on('click', event => { tag.find(".button-select-icon").on('click', event => {
Modals.spawnIconSelect(connection, id => { Modals.spawnIconSelect(connection, id => {
const icon_node = tag.find(".button-select-icon").find(".icon-node"); const icon_node = tag.find(".icon-preview");
icon_node.empty(); icon_node.children().remove();
icon_node.append(connection.fileManager.icons.generateTag(id)); icon_node.append(connection.fileManager.icons.generateTag(id));
console.log("Selected icon ID: %d", id); console.log("Selected icon ID: %d", id);
@ -101,6 +131,15 @@ namespace Modals {
}, channel ? channel.properties.channel_icon_id : 0); }, 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"); const channel_password = tag.find(".channel_password");
tag.find(".channel_password").change(function (this: HTMLInputElement) { tag.find(".channel_password").change(function (this: HTMLInputElement) {
@ -120,6 +159,42 @@ namespace Modals {
properties.channel_topic = this.value; properties.channel_topic = this.value;
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1)); }).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) { tag.find(".channel_description").change(function (this: HTMLInputElement) {
properties.channel_description = this.value; properties.channel_description = this.value;
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1)); }).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) { function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, parent: ChannelEntry, channel: ChannelEntry) {
tag.find("input[name=\"channel_type\"]").change(function (this: HTMLInputElement) { /* Channel type */
switch(this.value) { {
case "semi": const input_advanced_type = tag.find("input[name='channel_type']");
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = true; let _in_update = false;
break; const update_simple_type = () => {
case "perm": if(_in_update)
properties.channel_flag_permanent = true; return;
properties.channel_flag_semi_permanent = false;
break; let type;
default: if(properties.channel_flag_default || (typeof(properties.channel_flag_default) === "undefined" && channel && channel.properties.channel_flag_default))
properties.channel_flag_permanent = false; type = "def";
properties.channel_flag_semi_permanent = false; else if(properties.channel_flag_permanent || (typeof(properties.channel_flag_permanent) === "undefined" && channel && channel.properties.channel_flag_permanent))
break; 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) { /* simple */
console.log(this.checked); {
properties.channel_flag_default = this.checked; 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\"]"); simple.find("select[name='channel-type']").on('change', event => {
elements.prop("disabled", this.checked); try {
if(this.checked) { _in_update = true;
elements.prop("checked", false); switch ((event.target as HTMLSelectElement).value) {
tag.find("input[name=\"channel_type\"][value=\"perm\"]").prop("checked", true).trigger("change"); 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) { /* Talk power */
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)); 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"); input_advanced.on('change', event => {
for(let channel of (parent ? parent.children() : connection.channelTree.rootChannel())) properties.channel_needed_talk_power = parseInt(input_advanced.val() as string);
$.spawn("option").attr("channelId", channel.channelId.toString()).text(channel.channelName()).appendTo(orderTag); input_simple.val(input_advanced.val());
});
orderTag.change(function (this: HTMLSelectElement) { input_simple.on('change', event => {
let selected = $(this.options.item(this.selectedIndex)); properties.channel_needed_talk_power = parseInt(input_simple.val() as string);
properties.channel_order = parseInt(selected.attr("channelId")); input_advanced.val(input_simple.val());
}).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); }
/* 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) { function applyPermissionListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) {
let apply_permissions = (channel_permissions: PermissionValue[]) => { let apply_permissions = (channel_permissions: PermissionValue[]) => {
console.log(tr("Got permissions: %o"), channel_permissions); console.log(tr("Got permissions: %o"), channel_permissions);
@ -200,6 +491,9 @@ namespace Modals {
tag.find("input[permission]").each((index, _element) => { tag.find("input[permission]").each((index, _element) => {
let element = $(_element); let element = $(_element);
element.attr("original-value", 0);
element.val(0);
let permission = permissions.resolveInfo(element.attr("permission")); let permission = permissions.resolveInfo(element.attr("permission"));
if(!permission) { if(!permission) {
log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission")); log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission"));
@ -207,23 +501,16 @@ namespace Modals {
return; 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) for(let cperm of channel_permissions)
if(cperm.type == permission) { if(cperm.type == permission) {
element.val(old_value = cperm.value); element.val(cperm.value);
element.attr("original-value", cperm.value);
return; return;
} }
element.val(0);
}); });
if(!permissions.neededPermission(PermissionType.I_CHANNEL_MODIFY_POWER).granted(required_power, false)) { const permission = permissions.neededPermission(PermissionType.I_CHANNEL_MODIFY_POWER).granted(required_power, false);
tag.find("input[permission]").prop("disabled", false); //No permissions tag.find("input[permission]").prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); //No permissions
}
}; };
if(channel) { if(channel) {
@ -234,7 +521,17 @@ namespace Modals {
} else apply_permissions([]); } 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 update_template = () => {
let codec = properties.channel_codec; let codec = properties.channel_codec;
if(!codec && channel) if(!codec && channel)
@ -246,14 +543,25 @@ namespace Modals {
quality = channel.properties.channel_codec_quality; quality = channel.properties.channel_codec_quality;
if(!quality) return; if(!quality) return;
if(codec == 4 && quality == 4) let template_name = "custom";
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); if(codec == 4 && quality == 4)
else if(codec == 5 && quality == 6) template_name = "voice_mobile";
tag.find("input[name=\"voice_template\"][value=\"music\"]").prop("checked", true); 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 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 => { let change_codec = codec => {
@ -264,20 +572,30 @@ namespace Modals {
update_template(); update_template();
}; };
let quality_slider = tag.find(".voice_quality_slider"); const container_quality = tag.find(".container-quality");
let quality_number = tag.find(".voice_quality_number"); 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) => { let change_quality = (quality: number) => {
if(properties.channel_codec_quality == quality) return; if(properties.channel_codec_quality == quality) return;
properties.channel_codec_quality = quality; properties.channel_codec_quality = quality;
if(quality_slider.val() != quality) slider_quality.value(quality);
quality_slider.val(quality);
if(parseInt(quality_number.text()) != quality)
quality_number.text(quality);
update_template(); 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) { switch(this.value) {
case "custom": case "custom":
break; break;
@ -295,12 +613,43 @@ namespace Modals {
break; 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)); simple.find("select[name='voice_template']").change(function (this: HTMLInputElement) {
tag.find("input[name=\"voice_template\"][value=\"voice_desktop\"]") switch(this.value) {
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); case "custom":
tag.find("input[name=\"voice_template\"][value=\"music\"]") break;
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); 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"); 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)); 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); change_quality(channel.properties.channel_codec_quality);
} }
update_template(); 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) { function applyAdvancedListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
@ -332,58 +679,26 @@ namespace Modals {
properties.channel_topic = this.value; 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\"]"); const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1);
let tag_limited = tag.find("input[name=\"max_users\"][value=\"limited\"]"); tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) {
let tag_limited_value = tag.find(".channel_maxclients"); properties.channel_delete_delay = parseInt(this.value);
}).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission);
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');
}
} }
{ {
let tag_inherited = tag.find("input[name=\"max_users_family\"][value=\"inherited\"]"); tag.find(".button-delete-max").on('click', event => {
let tag_infinity = tag.find("input[name=\"max_users_family\"][value=\"infinity\"]"); const power = connection.permissions.neededPermission(PermissionType.I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY).value;
let tag_limited = tag.find("input[name=\"max_users_family\"][value=\"limited\"]"); let value = power == -2 ? 0 : power == -1 ? (7 * 24 * 60 * 60) : power;
let tag_limited_value = tag.find(".channel_maxfamilyclients"); 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); const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1);
tag_infinity.prop("disabled", true); tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) {
tag_limited.prop("disabled", true); properties.channel_codec_is_unencrypted = parseInt(this.value) == 0;
tag_limited_value.prop("disabled", true); }).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission);
} 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');
}
} }
} }
} }

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