A lot of new style changes
20
ChangeLog.md
|
@ -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
|
||||||
|
|
|
@ -22,4 +22,8 @@ html, body {
|
||||||
|
|
||||||
display: flex; flex-direction: column; resize: both;
|
display: flex; flex-direction: column; resize: both;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: none!important;
|
||||||
}
|
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
13
files.php
|
@ -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/",
|
||||||
|
|
|
@ -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"}]
|
|
@ -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,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);
|
||||||
}
|
}
|
|
@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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",
|
||||||
|
|
After Width: | Height: | Size: 7.6 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
/* TODO: Use a global event bus as event distribute system */
|
||||||
|
namespace event {
|
||||||
|
namespace global {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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"] || {};
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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 : (() => {}))())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 */
|
||||||
|
|
|
@ -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 || (() => {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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 ? " " : 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 ? " " : 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));'";
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|