A lot of new style changes
20
ChangeLog.md
|
@ -1,7 +1,27 @@
|
|||
# Changelog:
|
||||
|
||||
* **XX.XX.XX**
|
||||
- Removed icon size restriction for SVGs
|
||||
- Fixed permission editor icon select for not granted icon permissions
|
||||
- Fixed "disconnect" button not showing up after beeing connected
|
||||
- Improved handling of `disableMultiSession` settings (Connect in a new tab does not show up anymore)
|
||||
- Implemented avatar upload
|
||||
- Sorting server group icons within client channel tree
|
||||
- Fixed buggy away message position
|
||||
- Logging the servers welcome message [#54](https://github.com/TeaSpeak/TeaWeb/issues/54)
|
||||
- Showing servers hostbutton
|
||||
- Fixed microphone and sound action sounds [#67](https://github.com/TeaSpeak/TeaWeb/issues/67)
|
||||
- Added option to mute clients [#64](https://github.com/TeaSpeak/TeaWeb/issues/64)
|
||||
- Improved debug loader (no dependency faults anymore)
|
||||
- Saving private conversations and showing the messages again after client restart
|
||||
- Fixed some general memory leaks
|
||||
- Implemented the hostmessage functions
|
||||
- Fixed bookmark server password
|
||||
|
||||
Big UI Improvement:
|
||||
- New "dark theme" design
|
||||
- All elements are responsive to the font-size (Supporting now large & small screens (No mobile support yet))
|
||||
- Implemented an active ping calculation
|
||||
|
||||
* **22.06.19**
|
||||
- Fixed channel create not working issue
|
||||
|
|
|
@ -23,3 +23,7 @@ html, body {
|
|||
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/",
|
||||
"local-path" => "./shared/js/",
|
||||
"req-parm" => ["-js-map"]
|
||||
"req-parm" => ["--mappings"]
|
||||
],
|
||||
[ /* shared generated worker codec */
|
||||
"type" => "js",
|
||||
|
@ -52,6 +52,15 @@
|
|||
"path" => "css/",
|
||||
"local-path" => "./shared/css/"
|
||||
],
|
||||
[ /* shared css mapping files (development mode only) */
|
||||
"type" => "css",
|
||||
"search-pattern" => "/.*\.(css.map|scss)$/",
|
||||
"build-target" => "dev",
|
||||
|
||||
"path" => "css/",
|
||||
"local-path" => "./shared/css/",
|
||||
"req-parm" => ["--mappings"]
|
||||
],
|
||||
[ /* shared release css files */
|
||||
"type" => "css",
|
||||
"search-pattern" => "/.*\.css$/",
|
||||
|
@ -137,7 +146,7 @@
|
|||
$APP_FILE_LIST_SHARED_VENDORS = [
|
||||
[
|
||||
"type" => "js",
|
||||
"search-pattern" => "/.*\.js$/",
|
||||
"search-pattern" => "/.*(\.min)?\.js$/",
|
||||
"build-target" => "dev|rel",
|
||||
|
||||
"path" => "vendor/",
|
||||
|
|
|
@ -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_deactivated;Welcome back
|
||||
|
||||
#Microphone
|
||||
microphone.muted;Microphone muted
|
||||
microphone.activated;Microphone activated
|
||||
|
||||
#Sound
|
||||
sound.muted;Sound muted
|
||||
sound.activated;Sound activated
|
||||
|
||||
#Connection
|
||||
connection.connected;Connected
|
||||
connection.disconnected;Disconnected
|
||||
|
@ -44,6 +52,7 @@ user.left.kicked.server;User in your channel got kicked from the server
|
|||
user.left.moved;User was moved out of your channel
|
||||
user.left.disconnect;User disconnected from your channel
|
||||
user.left.banned;User in your channel was banned from the server
|
||||
user.left.timeout;User in your channel timed out
|
||||
|
||||
#Error
|
||||
error.insufficient_permissions;insufficient permissions
|
||||
|
|
|
|
@ -1,3 +1,4 @@
|
|||
declare namespace connection {
|
||||
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection;
|
||||
export function destroy_server_connection(handle: AbstractServerConnection);
|
||||
}
|
|
@ -1,4 +1,15 @@
|
|||
@import "properties";
|
||||
@import "mixin";
|
||||
|
||||
.channel-tree-container {
|
||||
height: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* the channel tree */
|
||||
.channel-tree {
|
||||
|
@ -71,6 +82,11 @@
|
|||
|
||||
align-self: center;
|
||||
color: $channel_tree_entry_text_color;
|
||||
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon_property {
|
||||
|
@ -84,6 +100,8 @@
|
|||
flex-direction: column;
|
||||
|
||||
.container-channel {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
@ -94,6 +112,39 @@
|
|||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.marker-text-unread {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 1px;
|
||||
background-color: #a814147F;
|
||||
|
||||
opacity: 1;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 24px;
|
||||
|
||||
background: -moz-linear-gradient(left, rgba(168,20,20,.18) 0%, rgba(168,20,20,0) 100%); /* FF3.6-15 */
|
||||
background: -webkit-linear-gradient(left, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* Chrome10-25,Safari5.1-6 */
|
||||
background: linear-gradient(to right, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include transition(opacity $button_hover_animation_time);
|
||||
}
|
||||
|
||||
.channel-type {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
@ -125,6 +176,17 @@
|
|||
.channel-name {
|
||||
align-self: center;
|
||||
color: $channel_tree_entry_text_color;
|
||||
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.align-repetitive {
|
||||
.channel-name {
|
||||
text-overflow: clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,16 +236,20 @@
|
|||
}
|
||||
|
||||
.client-name {
|
||||
line-height: 16px;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
padding-right: .25em;
|
||||
color: $channel_tree_entry_text_color;
|
||||
|
||||
&.client-name-own {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
line-height: 16px;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 75px;
|
||||
.client-away-message {
|
||||
color: $channel_tree_entry_text_color;
|
||||
}
|
||||
|
||||
|
@ -191,7 +257,8 @@
|
|||
margin-right: 0; /* override from previous thing */
|
||||
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
right: 0;
|
||||
padding-right: 5px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -211,14 +278,30 @@
|
|||
}
|
||||
|
||||
&.selected {
|
||||
&:focus-within {
|
||||
.container-icons {
|
||||
background-color: $channel_tree_entry_selected;
|
||||
padding-left: 5px;
|
||||
z-index: 1001; /* show before client name */
|
||||
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.client-name {
|
||||
&:focus {
|
||||
position: absolute;
|
||||
color: black;
|
||||
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
margin-right: -10px;
|
||||
margin-left: 18px;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "mixin";
|
||||
|
||||
.container-connection-handlers {
|
||||
$animation_length: .25s;
|
||||
|
||||
|
@ -14,10 +16,7 @@
|
|||
|
||||
background-color: transparent;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
@include user-select(none);
|
||||
|
||||
position: relative;
|
||||
|
||||
|
@ -32,6 +31,8 @@
|
|||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
.connection-container {
|
||||
padding-top: 4px;
|
||||
position: relative;
|
||||
|
@ -57,25 +58,13 @@
|
|||
color: #a8a8a8;
|
||||
|
||||
align-self: center;
|
||||
margin-right: -5px; /* 5px padding which have to be overcommed */
|
||||
margin-right: 20px;
|
||||
|
||||
position: relative;
|
||||
|
||||
max-width: 16em;
|
||||
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: linear-gradient(to right, transparent, #1e1e1e calc(100% - 20px));
|
||||
}
|
||||
}
|
||||
|
||||
.button-close {
|
||||
|
@ -89,6 +78,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.cutoff-name {
|
||||
.server-name {
|
||||
max-width: 10em;
|
||||
margin-right: -5px; /* 5px padding which have to be overcommed */
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: linear-gradient(to right, transparent, #1e1e1e calc(100% - 20px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #242425;
|
||||
}
|
||||
|
|
|
@ -86,8 +86,11 @@
|
|||
position: absolute;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
/* we call it "ccheckbox" else it will be messed up the the global checkbox */
|
||||
.ccheckbox {
|
||||
margin-top: 1px;
|
||||
margin-left: 1px;
|
||||
display: block;
|
||||
|
@ -145,6 +148,4 @@
|
|||
input:checked ~ .checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
@import "properties";
|
||||
@import "mixin";
|
||||
|
||||
$border_color_activated: rgba(255, 255, 255, .75);
|
||||
|
||||
/* max height is 2em */
|
||||
.control_bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
@include user-select(none);
|
||||
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
@ -25,8 +26,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
.button, .dropdown-arrow {
|
||||
text-align: center;
|
||||
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
border-radius: 3px;
|
||||
border: .05em solid rgba(0, 0, 0, 0);
|
||||
border-radius: $border_radius_small;
|
||||
|
||||
background-color: #454545;
|
||||
|
||||
|
@ -56,6 +57,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
}
|
||||
}
|
||||
|
||||
@include transition(background-color $button_hover_animation_time ease-in-out, border-color $button_hover_animation_time ease-in-out);
|
||||
|
||||
> .icon_x24 {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
@ -69,9 +72,25 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
margin-left: 5px;
|
||||
|
||||
&:not(.icon_x24) {
|
||||
min-width: 28px;
|
||||
max-width: 28px;
|
||||
height: 28px;
|
||||
min-width: 2em;
|
||||
max-width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.icon_em {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
&.button-hostbutton {
|
||||
img {
|
||||
min-width: 1.5em;
|
||||
max-width: 1.5em;
|
||||
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
}
|
||||
|
||||
padding: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,8 +99,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
position: relative;
|
||||
|
||||
.buttons {
|
||||
margin-top: 1px;
|
||||
height: 28px;
|
||||
height: 2em;
|
||||
|
||||
align-items: center;
|
||||
|
||||
|
@ -89,14 +107,14 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
flex-direction: row;
|
||||
|
||||
.dropdown-arrow {
|
||||
height: 28px;
|
||||
height: 2em;
|
||||
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
width: 18px;
|
||||
width: 1.5em;
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 0 3px 3px 0;
|
||||
border-radius: 0 $border_radius_small $border_radius_small 0;
|
||||
align-items: center;
|
||||
border-left: 0;
|
||||
}
|
||||
|
@ -131,8 +149,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
|
||||
background-color: #2d3032;
|
||||
align-items: center;
|
||||
border: 1px solid #2c2525;
|
||||
border-radius: 0 5px 5px 5px;
|
||||
border: .05em solid #2c2525;
|
||||
border-radius: 0 $border_radius_middle $border_radius_middle $border_radius_middle;
|
||||
|
||||
width: 230px;
|
||||
|
||||
|
@ -143,7 +161,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
right: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
.icon, .icon-container, .icon_em {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
@ -159,16 +177,16 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
}
|
||||
|
||||
& > div:first-of-type {
|
||||
border-radius: 2px 2px 0 0;
|
||||
border-radius: .1em .1em 0 0;
|
||||
}
|
||||
|
||||
& > div:last-of-type {
|
||||
border-radius: 0 0 2px 2px;
|
||||
border-radius: 0 0 .1em .1em;
|
||||
}
|
||||
|
||||
&.display_left {
|
||||
margin-left: -179px;
|
||||
border-radius: 5px 0 5px 5px;
|
||||
border-radius: $border_radius_middle 0 $border_radius_middle $border_radius_middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -263,4 +281,8 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_em {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
|
@ -41,10 +41,6 @@
|
|||
flex-direction: column;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
|
||||
.hostbanner {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,42 +80,6 @@
|
|||
display: none;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.hostbanner {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.meta-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.image-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
div {
|
||||
background-position: center;
|
||||
|
||||
&.hostbanner-mode-0 { }
|
||||
|
||||
&.hostbanner-mode-1 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.hostbanner-mode-2 {
|
||||
background-size: cover!important;
|
||||
background-position: top center !important;
|
||||
width:100%;
|
||||
height:100%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-select-info {
|
||||
|
|
|
@ -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;
|
||||
|
||||
.container-app-main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
min-height: 500px;
|
||||
margin-top: 5px;
|
||||
|
||||
position: relative;
|
||||
|
@ -15,14 +19,14 @@ $animation_length: .5s;
|
|||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.container-channel-chat {
|
||||
min-height: 200px;
|
||||
min-width: 100px;
|
||||
height: 80%; /* "default" settings */
|
||||
width: 100%;
|
||||
|
||||
min-height: 25em;
|
||||
min-width: 100px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
@ -34,25 +38,44 @@ $animation_length: .5s;
|
|||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.container-channel-tree {
|
||||
> .container-channel-tree {
|
||||
width: 50%; /* "default" settings */
|
||||
height: 100%;
|
||||
|
||||
background: #353535;
|
||||
min-width: 200px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
height: 100%;
|
||||
|
||||
min-height: 100px;
|
||||
|
||||
padding-top: 5px;
|
||||
/*
|
||||
overflow: auto;
|
||||
overflow-x: visible;
|
||||
*/
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
> .hostbanner {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
max-height: 9em; /* same size as the info pannel */
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.container-chat {
|
||||
> .channel-tree {
|
||||
padding-top: 5px;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
> .container-chat {
|
||||
width: 50%; /* "default" settings */
|
||||
height: 100%;
|
||||
|
||||
background: #353535;
|
||||
min-width: 350px;
|
||||
|
||||
|
@ -62,17 +85,67 @@ $animation_length: .5s;
|
|||
}
|
||||
}
|
||||
|
||||
.container-server-log {
|
||||
min-height: 0;
|
||||
height: 250px;
|
||||
|
||||
> .container-bottom {
|
||||
height: 20%;
|
||||
|
||||
min-height: 1.5em;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
> .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-control-bar {
|
||||
|
@ -82,7 +155,7 @@ $animation_length: .5s;
|
|||
|
||||
border-radius: 5px;
|
||||
|
||||
height: 30px;
|
||||
height: 2em;
|
||||
width: 100%;
|
||||
|
||||
background-color: #454545;
|
||||
|
@ -121,39 +194,6 @@ $animation_length: .5s;
|
|||
}
|
||||
|
||||
@media only screen and (max-width: $small_device) {
|
||||
.app {
|
||||
.container-app-main {
|
||||
.container-info {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
width: 100% !important; /* override the seperator property */
|
||||
height: 100%;
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
&.shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.select_info {
|
||||
> .close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-channel-chat + .container-seperator {
|
||||
display: none;
|
||||
animation: fadeout $animation_length linear;
|
||||
}
|
||||
|
||||
.container-channel-chat {
|
||||
width: 100% !important; /* override the seperator property */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hide-small {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -173,3 +173,126 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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,255 +1,815 @@
|
|||
$required_notab_height: 800px;
|
||||
|
||||
.container-channel-edit-general, .tab-channel-edit-general {
|
||||
@import "mixin";
|
||||
@import "properties";
|
||||
|
||||
.modal-body.modal-channel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
max-height: calc(100vh - 10em);
|
||||
padding: 1em!important;
|
||||
|
||||
input, textarea, select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select {
|
||||
margin-left: 0!important;
|
||||
height: 2.5em!important;
|
||||
}
|
||||
|
||||
textarea {
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.container-general {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
> div:not(:first-of-type) {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.container-name-icon {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.container-name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.container-icon-select {
|
||||
position: relative;
|
||||
|
||||
.container-icon {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
height: 2.5em;
|
||||
border-radius: .2em;
|
||||
|
||||
.container-icon {
|
||||
width: 30px;
|
||||
margin-left: 1em;
|
||||
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.button-select-icon {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
position: absolute;
|
||||
|
||||
.icon-node {
|
||||
cursor: pointer;
|
||||
background-color: #121213;
|
||||
border: 1px solid #0d0d0d;
|
||||
|
||||
.icon-preview {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
width: 3em;
|
||||
|
||||
&:hover {
|
||||
background-color: #00000011;
|
||||
}
|
||||
border: none;
|
||||
border-right: 1px solid #0d0d0d;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
> div {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-tag-channel-edit-general {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
.tab-channel-edit-general {
|
||||
padding: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container-channel-edit-general {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-height: $required_notab_height) {
|
||||
.tab-tag-channel-edit-general {
|
||||
display: inline-block!important;
|
||||
}
|
||||
|
||||
.tab-channel-edit-general {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container-channel-edit-general {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.container-channel-settings-standard {
|
||||
min-height: 300px;
|
||||
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.container-divider {
|
||||
border-left:1px solid #000;
|
||||
height: auto;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.container-left, .container-right {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-self: center;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.container-right {
|
||||
@include transition(border-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
|
||||
.container-dropdown {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: stretch;
|
||||
vertical-align: center;
|
||||
justify-content: space-around;
|
||||
|
||||
margin: 20px 50px 20px 50px;
|
||||
height: 100%;
|
||||
width: 1.5em;
|
||||
|
||||
.button {
|
||||
text-align: center;
|
||||
|
||||
.arrow {
|
||||
border-color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.container-channel-type {
|
||||
padding: 5px;
|
||||
.dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
|
||||
border: lightgrey 2px solid;
|
||||
border-radius: 2px;
|
||||
text-align: left;
|
||||
top: calc(2.5em - 1px);
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
background-color: #121213;
|
||||
border: 1px solid #0d0d0d;
|
||||
border-radius: .2em 0 .2em .2em;
|
||||
|
||||
right: -1px;
|
||||
|
||||
.entry {
|
||||
padding: .5em;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border: none;
|
||||
border-bottom: 1px solid #0d0d0d;
|
||||
}
|
||||
}
|
||||
|
||||
.container-channel-settings-audio {
|
||||
&:hover {
|
||||
background-color: #17171a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-bottom-right-radius: 0;
|
||||
.dropdown {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #17171a;
|
||||
border-color: hsla(0, 0%, 20%, 1);
|
||||
|
||||
.icon-preview {
|
||||
border-color: hsla(0, 0%, 20%, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@include transition(border-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
.container-description {
|
||||
position: relative;
|
||||
|
||||
flex-grow: 1!important;
|
||||
flex-shrink: 1!important;
|
||||
|
||||
min-height: 5em;
|
||||
max-height: 22.5em;
|
||||
|
||||
border-radius: .2em;
|
||||
border: 1px solid #111112;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.toolbar {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
height: 2.5em;
|
||||
|
||||
background-color: #17171a;
|
||||
font-size: .8em;
|
||||
|
||||
padding: .25em;
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
|
||||
padding: .5em;
|
||||
&:not(:first-child) {
|
||||
margin-left: .25em;
|
||||
}
|
||||
|
||||
border-radius: .2em;
|
||||
border: 1px solid #111112;
|
||||
|
||||
background-color: #121213;
|
||||
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
|
||||
&.button-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.button-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.button-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.button-color {
|
||||
input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #0f0f0f;
|
||||
@include transition(background-color $button_hover_animation_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .input-boxed {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-height: 2.5em;
|
||||
height: 5em;
|
||||
max-height: 20em;
|
||||
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-top: 1px solid #111112;
|
||||
|
||||
|
||||
overflow-x: hidden;;
|
||||
overflow-y: auto;
|
||||
|
||||
resize: vertical;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
background-color: #131b22;
|
||||
//border-color: #284262;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: min-content;
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
@include transition(.25s ease-in-out);
|
||||
}
|
||||
|
||||
.container-advanced, .container-simple {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
margin-top: 1em;
|
||||
min-width: 20em;
|
||||
|
||||
width: 50em;
|
||||
|
||||
&.hidden {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
&.container-simple.hidden {
|
||||
transform: translate(-100%, -100%);
|
||||
}
|
||||
|
||||
&.container-advanced.hidden {
|
||||
transform: translate(100%, 100%);
|
||||
}
|
||||
@include transition(.25s ease-in-out);
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: #548abc;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.container-divider {
|
||||
border-left:1px solid #000;
|
||||
height: auto;
|
||||
/* total height 2.5em */
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
height: 1.5em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
* {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin: -.5em 0!important;
|
||||
|
||||
padding: 0!important;
|
||||
|
||||
input {
|
||||
height: 1.5em!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* radio buttons */
|
||||
$icon_width: 1.7em; /* equal to the label height */
|
||||
|
||||
.input-boxed {
|
||||
position: relative;
|
||||
|
||||
height: 1.7em;
|
||||
margin-left: 2.5em;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 4em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.container-tooltip {
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
position: relative;
|
||||
width: $icon_width;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
|
||||
align-self: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-type, .container-codec, .container-sort {
|
||||
padding-top: .5em;
|
||||
}
|
||||
|
||||
.container-talk {
|
||||
.input-boxed {
|
||||
margin-left: 0!important;
|
||||
height: 2.5em;
|
||||
|
||||
.container-tooltip {
|
||||
width: 2.5em!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-advanced {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
min-height: 5em;
|
||||
|
||||
border-radius: .2em;
|
||||
border: 1px solid #111112;
|
||||
|
||||
background-color: #17171a;
|
||||
|
||||
.categories {
|
||||
height: 2.5em;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.container-presets, .container-custom {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
text-align: left;
|
||||
align-self: center;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.container-custom {
|
||||
margin: 20px 50px 20px 50px;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
> .group_box {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
border-bottom: 1px solid #1d1d1d;
|
||||
|
||||
.container-channel-settings-permission {
|
||||
flex-grow: 1;
|
||||
.entry {
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
align-items: center;
|
||||
|
||||
|
||||
.container-left, .container-right {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #b6c4d6;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-bottom: 3px solid #245184;
|
||||
margin-bottom: -1px;
|
||||
|
||||
color: #245184;
|
||||
}
|
||||
|
||||
@include transition(color $button_hover_animation_time, border-bottom-color $button_hover_animation_time);
|
||||
}
|
||||
}
|
||||
|
||||
.bodies {
|
||||
position: relative;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
|
||||
min-height: 12em;
|
||||
height: 20em;
|
||||
|
||||
.body {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
|
||||
overflow: auto;
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.container-standard {
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
|
||||
.container-top, .container-bottom {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
min-height: 5em;
|
||||
}
|
||||
|
||||
.container-right, .container-left {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 3em;
|
||||
width: 50%;
|
||||
|
||||
> .group_box {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.form-placeholder {
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
.container-top {
|
||||
border-bottom: 2px solid #111113;
|
||||
.container-left, .container-right {
|
||||
padding-bottom: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.container-bottom {
|
||||
.container-left, .container-right {
|
||||
padding-top: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.container-left {
|
||||
margin-left: 10%;
|
||||
margin-right: 10px;
|
||||
border-right: 2px solid #111113;
|
||||
padding-right: .5em;
|
||||
}
|
||||
|
||||
.container-right {
|
||||
margin-right: 10%;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
padding-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.container-channel-settings-advanced {
|
||||
.container-perm-default {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.container-default-channel {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.container-permissions {
|
||||
flex-direction: row;
|
||||
overflow: visible;
|
||||
|
||||
.container-right, .container-left {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 3em;
|
||||
width: 50%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.container-max-users, .container-other {
|
||||
width: 100%;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.container-max-users {
|
||||
margin-top: 20px;
|
||||
.container-left {
|
||||
padding-right: .5em;
|
||||
border-right: 2px solid #111113;
|
||||
}
|
||||
|
||||
.container-right {
|
||||
padding-left: .5em;
|
||||
}
|
||||
|
||||
|
||||
.container-permission {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
|
||||
.name {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 8em;
|
||||
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.input-boxed {
|
||||
align-self: center;
|
||||
margin-left: 0!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.container-audio {
|
||||
overflow: visible;
|
||||
flex-direction: column;
|
||||
|
||||
.container-top {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.container-right, .container-left {
|
||||
border-bottom: 2px solid #111113;
|
||||
padding-bottom: .5em;
|
||||
}
|
||||
|
||||
.container- {
|
||||
border-right: 2px solid #111113;
|
||||
}
|
||||
}
|
||||
|
||||
.container-bottom {
|
||||
width: 100%;
|
||||
|
||||
padding-top: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
text-align: center;
|
||||
|
||||
.container-needed-bandwidth {
|
||||
padding-left: .5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #383838;
|
||||
font-size: .8em;
|
||||
}
|
||||
}
|
||||
|
||||
.container-right, .container-left {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
width: 50%;
|
||||
min-width: 3em;
|
||||
height: unset;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.container-left {
|
||||
padding-right: .5em;
|
||||
|
||||
border-right: 2px solid #111113;
|
||||
}
|
||||
|
||||
.container-right {
|
||||
border: none;
|
||||
padding-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.container-misc {
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
|
||||
|
||||
.container-other {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
.container-phonetic, .container-delay, .container-encrypt {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.group_box:not(:first-of-type) {
|
||||
margin-left: 40px;
|
||||
padding-top: .5em;
|
||||
padding-bottom: .5em;
|
||||
|
||||
> a {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 10em;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
> .group_box {
|
||||
> button {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 5em;
|
||||
|
||||
/* results in a height of 1.7em */
|
||||
height: 2em;
|
||||
font-size: .85em;
|
||||
|
||||
align-self: center;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
> input, .input-boxed {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
align-self: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-simple {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
min-height: 5em;
|
||||
border-radius: 0.2em;
|
||||
border: 1px solid #111112;
|
||||
background-color: #17171a;
|
||||
padding: .5em;
|
||||
|
||||
.container-left, .container-right {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.container-left {
|
||||
padding-right: .5em;
|
||||
border-right: 2px solid #111113;
|
||||
}
|
||||
|
||||
.container-right {
|
||||
padding-left: .5em;
|
||||
}
|
||||
|
||||
.container-perm-default {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.container-default-channel {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.container-talk {
|
||||
padding-top: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.container-buttons {
|
||||
margin-top: 1em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-left: 20px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.bmd-form-group {
|
||||
padding-top: 0;
|
||||
> *:not(.spacer) {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
> * {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
padding-left: .25em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +1,36 @@
|
|||
@import "mixin";
|
||||
|
||||
.modal .modal-connect {
|
||||
@include user-select(none);
|
||||
|
||||
/*
|
||||
margin-top: 5px;
|
||||
font-size: 1rem;
|
||||
max-width: 100000px; /* max 100000px width, else we shrink the modal */
|
||||
padding: 0!important; /* override the default padding */
|
||||
|
||||
> div:not(:first-of-type) {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.profile-select-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
select {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-invalid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
justify-content: stretch;
|
||||
|
||||
> div {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.container-connect-input {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
color: red;
|
||||
}
|
||||
*/
|
||||
/* apply the default padding */
|
||||
padding: .75em 24px;
|
||||
|
||||
.container-address-password {
|
||||
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;
|
||||
|
@ -45,11 +40,14 @@
|
|||
flex-grow: 0;
|
||||
flex-shrink: 4;
|
||||
|
||||
margin-left: 15px;
|
||||
min-width: 21.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.container-profile-manage {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 4;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
@ -57,6 +55,12 @@
|
|||
.container-select-profile {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 14em;
|
||||
|
||||
> .invalid-feedback {
|
||||
width: max-content; /* allow overflow here */
|
||||
}
|
||||
}
|
||||
|
||||
.container-manage {
|
||||
|
@ -65,9 +69,281 @@
|
|||
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.button-manage-profiles {
|
||||
min-width: 7em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
position: absolute;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.container-last-servers {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
|
||||
border: none;
|
||||
border-left: 2px solid #7a7a7a;
|
||||
|
||||
@include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out);
|
||||
&.shown {
|
||||
/* apply the default padding */
|
||||
padding: 0 24px 24px;
|
||||
|
||||
max-height: 100%;
|
||||
opacity: 1;
|
||||
|
||||
@include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out)
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
width: calc(100% + 46px);
|
||||
min-width: 0;
|
||||
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 55rem) {
|
||||
.modal .modal-connect {
|
||||
min-width: calc(21.25em + 24px * 2)!important;
|
||||
width: 1000em; /* allocate space */
|
||||
|
||||
.container-address-password {
|
||||
.container-password {
|
||||
min-width: unset!important;
|
||||
margin-left: 1em!important;
|
||||
}
|
||||
}
|
||||
|
||||
.container-buttons {
|
||||
justify-content: flex-end!important;
|
||||
|
||||
.button-toggle-last-servers {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container-profile-name {
|
||||
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 {
|
||||
width: 60px;
|
||||
}
|
||||
|
@ -217,9 +88,13 @@ modal-body {
|
|||
.arrow {
|
||||
display: inline-block;
|
||||
border: solid black;
|
||||
border-width: 0 3px 3px 0;
|
||||
padding: 3px;
|
||||
height: 10px;
|
||||
//border-width: 0 3px 3px 0;
|
||||
//padding: 3px;
|
||||
//height: 10px;
|
||||
|
||||
border-width: 0 .2em .2em 0;
|
||||
padding: .21em;
|
||||
height: .5em;
|
||||
|
||||
&.right {
|
||||
transform: rotate(-45deg);
|
||||
|
@ -328,105 +203,3 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,3 +2,9 @@ $channel_tree_entry_selected: #2d2d2d;
|
|||
$channel_tree_entry_hovered: #393939;
|
||||
|
||||
$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 {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
|
@ -5,6 +7,9 @@
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
@include chat-scrollbar-horizontal();
|
||||
|
||||
.container-messages {
|
||||
width: 100%;
|
||||
line-height: 16px;
|
||||
|
@ -33,7 +38,7 @@
|
|||
|
||||
font-family: sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
> .timestamp {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
background-position: 0 -2717px; /* by default use global flag */
|
||||
}
|
||||
|
||||
.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)
|
||||
|
||||
Used voice: UK-Graham
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import string
|
||||
import base64
|
||||
import sys
|
||||
import requests
|
||||
import json
|
||||
import csv
|
||||
|
@ -12,7 +18,8 @@ from pydub import AudioSegment
|
|||
TARGET_DIRECTORY = "audio/speech"
|
||||
SOURCE_FILE = "audio/speech_sentences.csv"
|
||||
|
||||
|
||||
"""
|
||||
We cant use the automated way because this now requires a security token and the AWS server does bot exists anymore
|
||||
def tts(text, file):
|
||||
voice_id = 4
|
||||
language_id = 1
|
||||
|
@ -43,33 +50,102 @@ def tts(text, file):
|
|||
sound.export(file, format="wav")
|
||||
|
||||
os.remove(file + ".mp3")
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
if False:
|
||||
if os.path.exists(TARGET_DIRECTORY):
|
||||
print("Deleting old speach directory (%s)!" % TARGET_DIRECTORY)
|
||||
try:
|
||||
shutil.rmtree(TARGET_DIRECTORY)
|
||||
except e:
|
||||
print("Cant delete old dir!")
|
||||
try:
|
||||
os.makedirs(TARGET_DIRECTORY)
|
||||
except:
|
||||
pass
|
||||
|
||||
mapping_file = 'audio/speech/mapping.json'
|
||||
mapping = []
|
||||
|
||||
with open(mapping_file, "r") as fstream:
|
||||
mapping = json.loads(fstream.read())
|
||||
|
||||
tts_queue = []
|
||||
with open(SOURCE_FILE, 'r') as input:
|
||||
reader = csv.reader(filter(lambda row: len(row) != 0 and row[0] != '#', input), delimiter=';', quotechar='#')
|
||||
for row in reader:
|
||||
if len(row) != 2:
|
||||
continue
|
||||
print("Generating speech for {}: {}".format(row[0], row[1]))
|
||||
try:
|
||||
file = "{}.wav".format(row[0])
|
||||
tts(row[1], TARGET_DIRECTORY + "/" + file)
|
||||
|
||||
mapping.append({'key': row[0], 'file': file})
|
||||
except e:
|
||||
print(e)
|
||||
print("Failed to generate {}", row[0])
|
||||
file = TARGET_DIRECTORY + "/" + "{}.wav".format(row[0])
|
||||
|
||||
with open("audio/speech/mapping.json", "w") as fstream:
|
||||
_object = filter(lambda e: e["key"] == row[0], mapping)
|
||||
if len(_object) > 0:
|
||||
_object = _object[0]
|
||||
if os.path.exists(TARGET_DIRECTORY + "/" + _object["file"]):
|
||||
print("Skipping speech generation for {} ({}). File already exists".format(row[0], file))
|
||||
continue
|
||||
|
||||
print("Enqueuing speech generation for {} ({}): {}".format(row[0], file, row[1]))
|
||||
tts_queue.append([row[0], file, row[1]])
|
||||
|
||||
if len(tts_queue) == 0:
|
||||
print("No sounds need to be generated!")
|
||||
return
|
||||
|
||||
print(tts_queue)
|
||||
print("Please generate HSR file for the following text:")
|
||||
for entry in tts_queue:
|
||||
print(entry[2])
|
||||
print("")
|
||||
|
||||
print("-" * 30)
|
||||
print("Enter the HSR file path")
|
||||
file = "" # /home/wolverindev/Downloads/www.naturalreaders.com.har
|
||||
while True:
|
||||
if len(file) > 0:
|
||||
if os.path.exists(file):
|
||||
break
|
||||
print("Invalid file try again")
|
||||
file = string.strip(sys.stdin.readline())
|
||||
print("Testing file {}".format(file))
|
||||
|
||||
with open(file, "r") as fstream:
|
||||
data = json.loads(fstream.read())
|
||||
entries = data["log"]["entries"]
|
||||
for entry in entries:
|
||||
if not entry["request"]["url"].startswith('https://pweb.naturalreaders.com/v0/tts?'):
|
||||
continue
|
||||
if not (entry["request"]["method"] == "POST"):
|
||||
continue
|
||||
|
||||
post_data = json.loads(entry["request"]["postData"]["text"])
|
||||
key = post_data["t"]
|
||||
tts_entry = filter(lambda e: e[2] == key, tts_queue)
|
||||
if len(tts_entry) == 0:
|
||||
print("Missing generated speech text handle for: {}".format(key))
|
||||
continue
|
||||
tts_entry = tts_entry[0]
|
||||
tts_queue.remove(tts_entry)
|
||||
|
||||
print(tts_entry)
|
||||
with open(tts_entry[1] + ".mp3", "wb") as mp3_tmp:
|
||||
mp3_tmp.write(base64.decodestring(entry["response"]["content"]["text"]))
|
||||
mp3_tmp.close()
|
||||
|
||||
sound = AudioSegment.from_mp3(tts_entry[1] + ".mp3")
|
||||
sound.export(tts_entry[1], format="wav")
|
||||
os.remove(tts_entry[1] + ".mp3")
|
||||
|
||||
mapping.append({
|
||||
'key': tts_entry[0],
|
||||
'file': "{}.wav".format(tts_entry[0])
|
||||
})
|
||||
|
||||
print("FILE DONE!")
|
||||
with open(mapping_file, "w") as fstream:
|
||||
fstream.write(json.dumps(mapping))
|
||||
fstream.close()
|
||||
|
||||
|
|
|
@ -1,46 +1,17 @@
|
|||
<?php
|
||||
$testXF = false;
|
||||
$localhost = false;
|
||||
$_INCLIDE_ONLY = true;
|
||||
|
||||
if (file_exists('auth.php'))
|
||||
include_once('auth.php');
|
||||
else if (file_exists('auth/auth.php'))
|
||||
include_once('auth/auth.php');
|
||||
else {
|
||||
function authPath() {
|
||||
return "";
|
||||
}
|
||||
|
||||
function redirectOnInvalidSession()
|
||||
{
|
||||
}
|
||||
|
||||
function logged_in() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(function_exists("setup_forum_auth"))
|
||||
setup_forum_auth();
|
||||
|
||||
$localhost |= gethostname() == "WolverinDEV";
|
||||
if(!$localhost || $testXF) {
|
||||
//redirectOnInvalidSession();
|
||||
}
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$WEB_CLIENT = http_response_code() !== false;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<!-- App min width: 450px -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, min-zoom=1, max-zoom: 1, user-scalable=no">
|
||||
<meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." />
|
||||
<link rel="icon" href="img/favicon/teacup.png">
|
||||
<meta name="keywords" content="TeaSpeak, TeaWeb, TeaSpeak-Web,Web client TeaSpeak, веб клієнт TeaSpeak, TSDNS, багатомовність, мультимовність, теми, функціонал"/>
|
||||
<!-- TODO Needs some fix -->
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
@ -50,6 +21,7 @@
|
|||
echo "<title>TeaClient</title>";
|
||||
} else {
|
||||
echo "<title>TeaSpeak-Web</title>";
|
||||
echo '<link rel="icon" href="img/favicon/teacup.png" type="image/x-icon">';
|
||||
}
|
||||
?>
|
||||
|
||||
|
@ -64,13 +36,6 @@
|
|||
|
||||
spawn_property('connect_default_host', $localhost ? "localhost" : "ts.TeaSpeak.de");
|
||||
spawn_property('localhost_debug', $localhost ? "true" : "false");
|
||||
if(isset($_COOKIE)) {
|
||||
if(array_key_exists("COOKIE_NAME_USER_DATA", $GLOBALS) && array_key_exists($GLOBALS["COOKIE_NAME_USER_DATA"], $_COOKIE))
|
||||
spawn_property('forum_user_data', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_DATA"]]);
|
||||
if(array_key_exists("COOKIE_NAME_USER_SIGN", $GLOBALS) && array_key_exists($GLOBALS["COOKIE_NAME_USER_SIGN"], $_COOKIE))
|
||||
spawn_property('forum_user_sign', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_SIGN"]]);
|
||||
}
|
||||
spawn_property('forum_path', authPath());
|
||||
|
||||
$version = file_get_contents("./version");
|
||||
if ($version === false)
|
||||
|
@ -171,8 +136,23 @@
|
|||
</div>
|
||||
|
||||
<div id="spoiler-style" style="z-index: 1000000; position: absolute; display: block; background: white; right: 5px; left: 5px; top: 34px;">
|
||||
<!-- <img src="https://www.chromatic-solutions.de/teaspeak/window/connect_opened.png"> -->
|
||||
<!-- <img src="http://puu.sh/DZDgO/9149c0a1aa.png"> -->
|
||||
<!-- <img src="http://puu.sh/E0QUb/ce5e3f93ae.png"> -->
|
||||
<!-- <img src="img/style/default.png"> -->
|
||||
<img src="img/style/user-selected.png">
|
||||
<!-- <img src="img/style/user-selected.png"> -->
|
||||
<!-- <img src="img/style/privat_chat.png"> -->
|
||||
<!-- <img src="http://puu.sh/E1aBL/3c40ae3c2c.png"> -->
|
||||
<!-- <img src="http://puu.sh/E2qb2/b27bb2fde5.png"> -->
|
||||
<!-- <img src="http://puu.sh/E2UQR/1e0d7e03a3.png"> -->
|
||||
<!-- <img src="http://puu.sh/E38yX/452e27864c.png"> -->
|
||||
<!-- <img src="http://puu.sh/E3fjq/e2b4447bcd.png"> -->
|
||||
<!-- <img src="http://puu.sh/E3WlW/f791a9e7b1.png"> -->
|
||||
<!-- <img src="http://puu.sh/E4lHJ/1a4afcdf0b.png"> -->
|
||||
<!-- <img src="http://puu.sh/E4HKK/5ee74d4cc7.png"> -->
|
||||
<!-- <img src="http://puu.sh/E6LN1/8518c10898.png"> -->
|
||||
|
||||
<img src="http://puu.sh/E6NXv/eb2f19c7c3.png">
|
||||
</div>
|
||||
<button class="toggle-spoiler-style" style="height: 30px; width: 100px; z-index: 100000000; position: absolute; bottom: 2px;">toggle style</button>
|
||||
<script>
|
||||
|
@ -181,7 +161,7 @@
|
|||
$(".toggle-spoiler-style").on('click', () => {
|
||||
$("#spoiler-style").toggle();
|
||||
});
|
||||
}, 1000);
|
||||
}, 2500);
|
||||
</script>
|
||||
|
||||
<div id="music-test"></div>
|
||||
|
@ -191,28 +171,10 @@
|
|||
<div class="container">
|
||||
</div>
|
||||
</div>
|
||||
<div id="global-tooltip">
|
||||
<a></a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<?php
|
||||
$footer_style = "display: none;";
|
||||
$footer_forum = '';
|
||||
|
||||
if($WEB_CLIENT) {
|
||||
$footer_style = "display: block;";
|
||||
|
||||
if (logged_in()) {
|
||||
$footer_forum = "<a href=\"" . authPath() . "auth.php?type=logout\">logout</a>";
|
||||
} else {
|
||||
$footer_forum = "<a href=\"" . authPath() . "login.php\">Login</a> via the TeaSpeak forum.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<footer style="<?php echo $footer_style; ?>">
|
||||
<div class="container" style="display: flex; flex-direction: row; align-content: space-between;">
|
||||
<div class="hide-small" style="align-self: center; position: fixed; left: 5px;">Open source on <a href="https://github.com/TeaSpeak/TeaSpeak-Web" style="display: inline-block; position: relative">github.com</a></div>
|
||||
<div style="align-self: center;">TeaSpeak Web (<?php echo $version; ?>) by WolverinDEV</div>
|
||||
<div class="hide-small" style="align-self: center; position: fixed; right: 5px;"><?php echo $footer_forum; ?></div>
|
||||
</div>
|
||||
</footer>
|
||||
<div id="top-menu-bar"></div>
|
||||
</html>
|
|
@ -74,7 +74,7 @@
|
|||
]
|
||||
}, {
|
||||
"key": "ru_gt",
|
||||
"country_code": "gt",
|
||||
"country_code": "ru",
|
||||
"path": "ru_google_translate.translation",
|
||||
|
||||
"name": "Auto translated messages for language ru",
|
||||
|
|
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,
|
||||
SERVER_CLOSED,
|
||||
SERVER_REQUIRES_PASSWORD,
|
||||
SERVER_HOSTMESSAGE,
|
||||
IDENTITY_TOO_LOW,
|
||||
UNKNOWN
|
||||
}
|
||||
|
@ -88,14 +89,15 @@ class ConnectionHandler {
|
|||
permissions: PermissionManager;
|
||||
groups: GroupManager;
|
||||
|
||||
chat_frame: chat.Frame;
|
||||
side_bar: chat.Frame;
|
||||
select_info: InfoBar;
|
||||
chat: ChatBox;
|
||||
|
||||
settings: ServerSettings;
|
||||
sound: sound.SoundManager;
|
||||
|
||||
readonly tag_connection_handler: JQuery;
|
||||
hostbanner: Hostbanner;
|
||||
|
||||
tag_connection_handler: JQuery;
|
||||
|
||||
private _clientId: number = 0;
|
||||
private _local_client: LocalClientEntry;
|
||||
|
@ -126,24 +128,25 @@ class ConnectionHandler {
|
|||
this.log = new log.ServerLog(this);
|
||||
this.select_info = new InfoBar(this);
|
||||
this.channelTree = new ChannelTree(this);
|
||||
this.chat = new ChatBox(this);
|
||||
this.chat_frame = new chat.Frame(this);
|
||||
this.side_bar = new chat.Frame(this);
|
||||
this.sound = new sound.SoundManager(this);
|
||||
this.hostbanner = new Hostbanner(this);
|
||||
|
||||
this.serverConnection = connection.spawn_server_connection(this);
|
||||
this.serverConnection.onconnectionstatechanged = this.on_connection_state_changed.bind(this);
|
||||
|
||||
this.fileManager = new FileManager(this);
|
||||
this.permissions = new PermissionManager(this);
|
||||
this.side_bar.channel_conversations().initialize_needed_listener();
|
||||
|
||||
this.groups = new GroupManager(this);
|
||||
this._local_client = new LocalClientEntry(this);
|
||||
this.channelTree.registerClient(this._local_client);
|
||||
|
||||
//settings.static_global(Settings.KEY_DISABLE_VOICE, false)
|
||||
this.chat.initialize();
|
||||
/* initialize connection handler tab entry */
|
||||
{
|
||||
this.tag_connection_handler = $.spawn("div").addClass("connection-container");
|
||||
$.spawn("div").addClass("server-icon icon client-server_green").appendTo(this.tag_connection_handler);
|
||||
$.spawn("div").addClass("server-name").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);
|
||||
this.tag_connection_handler.on('click', event => {
|
||||
if(event.isDefaultPrevented())
|
||||
|
@ -155,13 +158,20 @@ class ConnectionHandler {
|
|||
server_connections.destroy_server_connection_handler(this);
|
||||
event.preventDefault();
|
||||
});
|
||||
this.tab_set_name(tr("Not connected"));
|
||||
}
|
||||
}
|
||||
|
||||
tab_set_name(name: string) {
|
||||
this.tag_connection_handler.toggleClass('cutoff-name', name.length > 30);
|
||||
this.tag_connection_handler.find(".server-name").text(name);
|
||||
}
|
||||
|
||||
setup() { }
|
||||
|
||||
async startConnection(addr: string, profile: profiles.ConnectionProfile, parameters: ConnectParameters) {
|
||||
this.tag_connection_handler.find(".server-name").text(tr("Connecting"));
|
||||
this.cancel_reconnect();
|
||||
async startConnection(addr: string, profile: profiles.ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
|
||||
this.tab_set_name(tr("Connecting"));
|
||||
this.cancel_reconnect(false);
|
||||
this._reconnect_attempt = false;
|
||||
if(this.serverConnection)
|
||||
this.handleDisconnect(DisconnectReason.REQUESTED);
|
||||
|
@ -172,8 +182,9 @@ class ConnectionHandler {
|
|||
port: -1
|
||||
};
|
||||
{
|
||||
let _v6_end = addr.indexOf(']');
|
||||
let idx = addr.lastIndexOf(':');
|
||||
if(idx != -1) {
|
||||
if(idx != -1 && idx > _v6_end) {
|
||||
server_address.port = parseInt(addr.substr(idx + 1));
|
||||
server_address.host = addr.substr(0, idx);
|
||||
} else {
|
||||
|
@ -203,7 +214,14 @@ class ConnectionHandler {
|
|||
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open();
|
||||
}
|
||||
}
|
||||
if(parameters.password) {
|
||||
connection_log.update_address_password({
|
||||
hostname: server_address.host,
|
||||
port: server_address.port
|
||||
}, parameters.password.password);
|
||||
}
|
||||
|
||||
const original_address = {host: server_address.host, port: server_address.port};
|
||||
if(dns.supported() && !server_address.host.match(Modals.Regex.IP_V4) && !server_address.host.match(Modals.Regex.IP_V6)) {
|
||||
const id = ++this._connect_initialize_id;
|
||||
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE, {});
|
||||
|
@ -229,6 +247,15 @@ class ConnectionHandler {
|
|||
}
|
||||
|
||||
await this.serverConnection.connect(server_address, new connection.HandshakeHandler(profile, parameters));
|
||||
setTimeout(() => {
|
||||
const connected = this.serverConnection.connected();
|
||||
if(user_action && connected) {
|
||||
connection_log.log_connect({
|
||||
hostname: original_address.host,
|
||||
port: original_address.port
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
|
||||
|
@ -252,7 +279,6 @@ class ConnectionHandler {
|
|||
*/
|
||||
onConnected() {
|
||||
console.log("Client connected!");
|
||||
this.channelTree.registerClient(this._local_client);
|
||||
this.permissions.requestPermissionList();
|
||||
if(this.groups.serverGroups.length == 0)
|
||||
this.groups.requestGroups();
|
||||
|
@ -352,7 +378,7 @@ class ConnectionHandler {
|
|||
|
||||
const profile = profiles.find_profile(properties.connect_profile) || profiles.default_profile();
|
||||
const cprops = this.reconnect_properties(profile);
|
||||
this.startConnection(properties.connect_address, profile, cprops);
|
||||
this.startConnection(properties.connect_address, profile, true, cprops);
|
||||
});
|
||||
|
||||
const url = build_url(properties);
|
||||
|
@ -379,10 +405,11 @@ class ConnectionHandler {
|
|||
handleDisconnect(type: DisconnectReason, data: any = {}) {
|
||||
this._connect_initialize_id++;
|
||||
|
||||
this.tag_connection_handler.find(".server-name").text(tr("Not connected"));
|
||||
this.tab_set_name(tr("Not connected"));
|
||||
let auto_reconnect = false;
|
||||
switch (type) {
|
||||
case DisconnectReason.REQUESTED:
|
||||
case DisconnectReason.SERVER_HOSTMESSAGE: /* already handled */
|
||||
break;
|
||||
case DisconnectReason.HANDLER_DESTROYED:
|
||||
if(data)
|
||||
|
@ -468,7 +495,8 @@ class ConnectionHandler {
|
|||
|
||||
break;
|
||||
case DisconnectReason.SERVER_CLOSED:
|
||||
this.chat.serverChat().appendError(tr("Server closed ({0})"), data.reasonmsg);
|
||||
this.log.log(log.server.Type.SERVER_CLOSED, {message: data.reasonmsg});
|
||||
//this.chat.serverChat().appendError(tr("Server closed ({0})"), data.reasonmsg);
|
||||
createErrorModal(
|
||||
tr("Server closed"),
|
||||
"The server is closed.<br>" + //TODO tr
|
||||
|
@ -479,15 +507,23 @@ class ConnectionHandler {
|
|||
auto_reconnect = true;
|
||||
break;
|
||||
case DisconnectReason.SERVER_REQUIRES_PASSWORD:
|
||||
this.chat.serverChat().appendError(tr("Server requires password"));
|
||||
this.log.log(log.server.Type.SERVER_REQUIRES_PASSWORD, {});
|
||||
//this.chat.serverChat().appendError(tr("Server requires password"));
|
||||
|
||||
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
|
||||
if(!(typeof password === "string")) return;
|
||||
|
||||
const cprops = this.reconnect_properties(this.serverConnection.handshake_handler().profile);
|
||||
const profile = this.serverConnection.handshake_handler().profile;
|
||||
const cprops = this.reconnect_properties(profile);
|
||||
cprops.password = {password: password as string, hashed: false};
|
||||
this.startConnection(this.serverConnection.remote_address().host + ":" + this.serverConnection.remote_address().port,
|
||||
this.serverConnection.handshake_handler().profile,
|
||||
cprops);
|
||||
|
||||
connection_log.update_address_info({
|
||||
port: this.channelTree.server.remote_address.port,
|
||||
hostname: this.channelTree.server.remote_address.host
|
||||
}, {
|
||||
flag_password: true
|
||||
} as any);
|
||||
this.startConnection(this.channelTree.server.remote_address.host + ":" + this.channelTree.server.remote_address.port, profile, false, cprops);
|
||||
}).open();
|
||||
break;
|
||||
case DisconnectReason.CLIENT_KICKED:
|
||||
|
@ -499,15 +535,29 @@ class ConnectionHandler {
|
|||
auto_reconnect = false;
|
||||
break;
|
||||
case DisconnectReason.HANDSHAKE_BANNED:
|
||||
this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"),
|
||||
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]),
|
||||
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : "");
|
||||
this.log.log(log.server.Type.SERVER_BANNED, {
|
||||
invoker: {
|
||||
client_name: data["invokername"],
|
||||
client_id: parseInt(data["invokerid"]),
|
||||
client_unique_id: data["invokeruid"]
|
||||
},
|
||||
|
||||
message: data["reasonmsg"],
|
||||
time: parseInt(data["time"])
|
||||
});
|
||||
this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
|
||||
break;
|
||||
case DisconnectReason.CLIENT_BANNED:
|
||||
this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"),
|
||||
ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]),
|
||||
data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : "");
|
||||
this.log.log(log.server.Type.SERVER_BANNED, {
|
||||
invoker: {
|
||||
client_name: data["invokername"],
|
||||
client_id: parseInt(data["invokerid"]),
|
||||
client_unique_id: data["invokeruid"]
|
||||
},
|
||||
|
||||
message: data["reasonmsg"],
|
||||
time: parseInt(data["time"])
|
||||
});
|
||||
this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse
|
||||
break;
|
||||
default:
|
||||
|
@ -517,6 +567,7 @@ class ConnectionHandler {
|
|||
break;
|
||||
}
|
||||
|
||||
this.channelTree.unregisterClient(this._local_client); /* if we dont unregister our client here the client will be destroyed */
|
||||
this.channelTree.reset();
|
||||
if(this.serverConnection)
|
||||
this.serverConnection.disconnect();
|
||||
|
@ -524,7 +575,8 @@ class ConnectionHandler {
|
|||
if(control_bar.current_connection_handler() == this)
|
||||
control_bar.update_connection_state();
|
||||
this.select_info.setCurrentSelected(null);
|
||||
this.select_info.update_banner();
|
||||
this.side_bar.private_conversations().clear_client_ids();
|
||||
this.hostbanner.update();
|
||||
|
||||
if(auto_reconnect) {
|
||||
if(!this.serverConnection) {
|
||||
|
@ -542,15 +594,15 @@ class ConnectionHandler {
|
|||
this.log.log(log.server.Type.RECONNECT_CANCELED, {});
|
||||
log.info(LogCategory.NETWORKING, tr("Reconnecting..."));
|
||||
|
||||
this.startConnection(server_address.host + ":" + server_address.port, profile, this.reconnect_properties(profile));
|
||||
this.startConnection(server_address.host + ":" + server_address.port, profile, false, this.reconnect_properties(profile));
|
||||
this._reconnect_attempt = true;
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
cancel_reconnect() {
|
||||
cancel_reconnect(log_event: boolean) {
|
||||
if(this._reconnect_timer) {
|
||||
this.log.log(log.server.Type.RECONNECT_CANCELED, {});
|
||||
if(log_event) this.log.log(log.server.Type.RECONNECT_CANCELED, {});
|
||||
clearTimeout(this._reconnect_timer);
|
||||
this._reconnect_timer = undefined;
|
||||
}
|
||||
|
@ -562,6 +614,8 @@ class ConnectionHandler {
|
|||
}
|
||||
|
||||
update_voice_status(targetChannel?: ChannelEntry) {
|
||||
if(!this._local_client) return; /* we've been destroyed */
|
||||
|
||||
targetChannel = targetChannel || this.getClient().currentChannel();
|
||||
|
||||
const vconnection = this.serverConnection.voice_connection();
|
||||
|
@ -636,11 +690,22 @@ class ConnectionHandler {
|
|||
|
||||
if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().record_supported) {
|
||||
const active = !this.client_status.input_muted && !this.client_status.output_muted;
|
||||
if(active)
|
||||
vconnection.voice_recorder().input.start();
|
||||
else
|
||||
if(active) {
|
||||
if(vconnection.voice_recorder().input.current_state() === audio.recorder.InputState.PAUSED) {
|
||||
vconnection.voice_recorder().input.start().then(result => {
|
||||
if(result != audio.recorder.InputStartResult.EOK) {
|
||||
console.warn(tr("Failed to start microphone input (%s)."), result);
|
||||
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), result)).open();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn(tr("Failed to start microphone input (%s)."), error);
|
||||
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
vconnection.voice_recorder().input.stop();
|
||||
}
|
||||
}
|
||||
|
||||
if(control_bar.current_connection_handler() === this)
|
||||
control_bar.apply_server_voice_state();
|
||||
|
@ -665,6 +730,12 @@ class ConnectionHandler {
|
|||
if(this.client_status.away === state)
|
||||
return;
|
||||
|
||||
if(state) {
|
||||
this.sound.play(Sound.AWAY_ACTIVATED);
|
||||
} else {
|
||||
this.sound.play(Sound.AWAY_DEACTIVATED);
|
||||
}
|
||||
|
||||
this.client_status.away = state;
|
||||
this.serverConnection.send_command("clientupdate", {
|
||||
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
|
||||
|
@ -707,4 +778,141 @@ class ConnectionHandler {
|
|||
password: this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.password : undefined
|
||||
}
|
||||
}
|
||||
|
||||
update_avatar() {
|
||||
Modals.spawnAvatarUpload(data => {
|
||||
if(typeof(data) === "undefined")
|
||||
return;
|
||||
if(data === null) {
|
||||
console.log(tr("Deleting existing avatar"));
|
||||
this.serverConnection.send_command('ftdeletefile', {
|
||||
name: "/avatar_", /* delete own avatar */
|
||||
path: "",
|
||||
cid: 0
|
||||
}).then(() => {
|
||||
createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open();
|
||||
}).catch(error => {
|
||||
console.error(tr("Failed to reset avatar flag: %o"), error);
|
||||
|
||||
let message;
|
||||
if(error instanceof CommandResult)
|
||||
message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
if(!message)
|
||||
message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to delete avatar"), message).open();
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
console.log(tr("Uploading new avatar"));
|
||||
(async () => {
|
||||
let key: transfer.UploadKey;
|
||||
try {
|
||||
key = await this.fileManager.upload_file({
|
||||
size: data.byteLength,
|
||||
path: '',
|
||||
name: '/avatar',
|
||||
overwrite: true,
|
||||
channel: undefined,
|
||||
channel_password: undefined
|
||||
});
|
||||
} catch(error) {
|
||||
console.error(tr("Failed to initialize avatar upload: %o"), error);
|
||||
let message;
|
||||
if(error instanceof CommandResult) {
|
||||
//TODO: Resolve permission name
|
||||
//i_client_max_avatar_filesize
|
||||
if(error.id == ErrorID.PERMISSION_ERROR) {
|
||||
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]);
|
||||
} else {
|
||||
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
}
|
||||
}
|
||||
if(!message)
|
||||
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to upload avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await transfer.spawn_upload_transfer(key).put_data(data);
|
||||
} catch(error) {
|
||||
console.error(tr("Failed to upload avatar: %o"), error);
|
||||
|
||||
let message;
|
||||
if(typeof(error) === "string")
|
||||
message = MessageHelper.formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error);
|
||||
|
||||
if(!message)
|
||||
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to upload avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.serverConnection.send_command('clientupdate', {
|
||||
client_flag_avatar: guid()
|
||||
});
|
||||
} catch(error) {
|
||||
console.error(tr("Failed to update avatar flag: %o"), error);
|
||||
|
||||
let message;
|
||||
if(error instanceof CommandResult)
|
||||
message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
if(!message)
|
||||
message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to set avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
|
||||
createInfoModal(tr("Avatar successfully uploaded"), tr("Your avatar has been uploaded successfully!")).open();
|
||||
})();
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.cancel_reconnect(true);
|
||||
|
||||
this.tag_connection_handler && this.tag_connection_handler.remove();
|
||||
this.tag_connection_handler = undefined;
|
||||
|
||||
this.hostbanner && this.hostbanner.destroy();
|
||||
this.hostbanner = undefined;
|
||||
|
||||
this._local_client && this._local_client.destroy();
|
||||
this._local_client = undefined;
|
||||
|
||||
this.channelTree && this.channelTree.destroy();
|
||||
this.channelTree = undefined;
|
||||
|
||||
this.side_bar && this.side_bar.destroy();
|
||||
this.side_bar = undefined;
|
||||
|
||||
this.select_info && this.select_info.destroy();
|
||||
this.select_info = undefined;
|
||||
|
||||
this.log && this.log.destroy();
|
||||
this.log = undefined;
|
||||
|
||||
this.permissions && this.permissions.destroy();
|
||||
this.permissions = undefined;
|
||||
|
||||
this.groups && this.groups.destroy();
|
||||
this.groups = undefined;
|
||||
|
||||
this.fileManager && this.fileManager.destroy();
|
||||
this.fileManager = undefined;
|
||||
|
||||
this.settings && this.settings.destroy();
|
||||
this.settings = undefined;
|
||||
|
||||
if(this.serverConnection) {
|
||||
this.serverConnection.onconnectionstatechanged = undefined;
|
||||
connection.destroy_server_connection(this.serverConnection);
|
||||
}
|
||||
this.serverConnection = undefined;
|
||||
|
||||
this.sound = undefined;
|
||||
this._local_client = undefined;
|
||||
}
|
||||
}
|
|
@ -102,7 +102,7 @@ class RequestFileUpload {
|
|||
this.transfer_key = key;
|
||||
}
|
||||
|
||||
async put_data(data: BufferSource | File) {
|
||||
async put_data(data: BlobPart | File) {
|
||||
const form_data = new FormData();
|
||||
|
||||
if(data instanceof File) {
|
||||
|
@ -110,6 +110,10 @@ class RequestFileUpload {
|
|||
throw "invalid size";
|
||||
|
||||
form_data.append("file", data);
|
||||
} else if(typeof(data) === "string") {
|
||||
if(data.length != this.transfer_key.total_size)
|
||||
throw "invalid size";
|
||||
form_data.append("file", new Blob([data], { type: "application/octet-stream" }));
|
||||
} else {
|
||||
const buffer = <BufferSource>data;
|
||||
if(buffer.byteLength != this.transfer_key.total_size)
|
||||
|
@ -159,6 +163,24 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
this.connection.command_handler_boss().register_handler(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(this.connection) {
|
||||
const hboss = this.connection.command_handler_boss();
|
||||
if(hboss)
|
||||
hboss.unregister_handler(this);
|
||||
}
|
||||
|
||||
this.listRequests = undefined;
|
||||
this.pending_download_requests = undefined;
|
||||
this.pending_upload_requests = undefined;
|
||||
|
||||
this.icons && this.icons.destroy();
|
||||
this.icons = undefined;
|
||||
|
||||
this.avatars && this.avatars.destroy();
|
||||
this.avatars = undefined;
|
||||
}
|
||||
|
||||
handle_command(command: connection.ServerCommand): boolean {
|
||||
switch (command.command) {
|
||||
case "notifyfilelist":
|
||||
|
@ -262,7 +284,7 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
"clientftfid": transfer_data.client_transfer_id,
|
||||
"seekpos": 0,
|
||||
"proto": 1
|
||||
}).catch(reason => {
|
||||
}, {process_result: false}).catch(reason => {
|
||||
this.pending_download_requests.remove(transfer_data);
|
||||
reject(reason);
|
||||
})
|
||||
|
@ -410,9 +432,9 @@ function media_image_type(type: ImageType, file?: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
function image_type(base64: string | ArrayBuffer) {
|
||||
function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) {
|
||||
const ab2str10 = () => {
|
||||
const buf = new Uint8Array(base64 as ArrayBuffer);
|
||||
const buf = new Uint8Array(encoded_data as ArrayBuffer);
|
||||
if(buf.byteLength < 10)
|
||||
return "";
|
||||
|
||||
|
@ -422,7 +444,7 @@ function image_type(base64: string | ArrayBuffer) {
|
|||
return result;
|
||||
};
|
||||
|
||||
const bin = typeof(base64) === "string" ? atob(base64) : ab2str10();
|
||||
const bin = typeof(encoded_data) === "string" ? ((typeof(base64_encoded) === "undefined" || base64_encoded) ? atob(encoded_data) : encoded_data) : ab2str10();
|
||||
if(bin.length < 10) return ImageType.UNKNOWN;
|
||||
|
||||
if(bin[0] == String.fromCharCode(66) && bin[1] == String.fromCharCode(77)) {
|
||||
|
@ -481,8 +503,7 @@ class CacheManager {
|
|||
async resolve_cached(key: string, max_age?: number) : Promise<Response | undefined> {
|
||||
max_age = typeof(max_age) === "number" ? max_age : -1;
|
||||
|
||||
const request = new Request("https://_local_cache/cache_request_" + key);
|
||||
const cached_response = await this._cache_category.match(request);
|
||||
const cached_response = await this._cache_category.match("https://_local_cache/cache_request_" + key);
|
||||
if(!cached_response)
|
||||
return undefined;
|
||||
|
||||
|
@ -491,8 +512,6 @@ class CacheManager {
|
|||
}
|
||||
|
||||
async put_cache(key: string, value: Response, type?: string, headers?: {[key: string]:string}) {
|
||||
const request = new Request("https://_local_cache/cache_request_" + key);
|
||||
|
||||
const new_headers = new Headers();
|
||||
for(const key of value.headers.keys())
|
||||
new_headers.set(key, value.headers.get(key));
|
||||
|
@ -501,14 +520,25 @@ class CacheManager {
|
|||
for(const key of Object.keys(headers || {}))
|
||||
new_headers.set(key, headers[key]);
|
||||
|
||||
await this._cache_category.put(request, new Response(value.body, {
|
||||
await this._cache_category.put("https://_local_cache/cache_request_" + key, new Response(value.body, {
|
||||
headers: new_headers
|
||||
}));
|
||||
}
|
||||
|
||||
async delete(key: string) {
|
||||
const flag = await this._cache_category.delete("https://_local_cache/cache_request_" + key, {
|
||||
ignoreVary: true,
|
||||
ignoreMethod: true,
|
||||
ignoreSearch: true
|
||||
});
|
||||
if(!flag) {
|
||||
console.warn(tr("Failed to delete key %s from cache!"), flag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IconManager {
|
||||
private static cache: CacheManager;
|
||||
private static cache: CacheManager = new CacheManager("icons");
|
||||
|
||||
handle: FileManager;
|
||||
private _id_urls: {[id:number]:string} = {};
|
||||
|
@ -516,9 +546,15 @@ class IconManager {
|
|||
|
||||
constructor(handle: FileManager) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
if(!IconManager.cache)
|
||||
IconManager.cache = new CacheManager("icons");
|
||||
destroy() {
|
||||
if(URL.revokeObjectURL) {
|
||||
for(const id of Object.keys(this._id_urls))
|
||||
URL.revokeObjectURL(this._id_urls[id]);
|
||||
}
|
||||
this._id_urls = undefined;
|
||||
this._loading_promises = undefined;
|
||||
}
|
||||
|
||||
async clear_cache() {
|
||||
|
@ -548,7 +584,7 @@ class IconManager {
|
|||
return this.handle.download_file("", "/icon_" + id);
|
||||
}
|
||||
|
||||
private async _response_url(response: Response) {
|
||||
private static async _response_url(response: Response) {
|
||||
if(!response.headers.has('X-media-bytes'))
|
||||
throw "missing media bytes";
|
||||
|
||||
|
@ -573,14 +609,50 @@ class IconManager {
|
|||
await IconManager.cache.setup();
|
||||
|
||||
const response = await IconManager.cache.resolve_cached('icon_' + id); //TODO age!
|
||||
if(response)
|
||||
if(response) {
|
||||
const url = await IconManager._response_url(response);
|
||||
if(this._id_urls[id])
|
||||
URL.revokeObjectURL(this._id_urls[id]);
|
||||
return {
|
||||
id: id,
|
||||
url: (this._id_urls[id] = await this._response_url(response))
|
||||
url: url
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static _static_id_url: {[icon: number]:string} = {};
|
||||
private static _static_cached_promise: {[icon: number]:Promise<Icon>} = {};
|
||||
static load_cached_icon(id: number, ignore_age?: boolean) : Promise<Icon> | Icon {
|
||||
if(this._static_id_url[id]) {
|
||||
return {
|
||||
id: id,
|
||||
url: this._static_id_url[id]
|
||||
};
|
||||
}
|
||||
|
||||
if(this._static_cached_promise[id])
|
||||
return this._static_cached_promise[id];
|
||||
|
||||
return (this._static_cached_promise[id] = (async () => {
|
||||
if(!this.cache.setupped())
|
||||
await this.cache.setup();
|
||||
|
||||
const response = await this.cache.resolve_cached('icon_' + id); //TODO age!
|
||||
if(response) {
|
||||
const url = await this._response_url(response);
|
||||
if(this._static_id_url[id])
|
||||
URL.revokeObjectURL(this._static_id_url[id]);
|
||||
this._static_id_url[id] = url;
|
||||
|
||||
return {
|
||||
id: id,
|
||||
url: url
|
||||
};
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
private async _load_icon(id: number) : Promise<Icon> {
|
||||
try {
|
||||
let download_key: transfer.DownloadKey;
|
||||
|
@ -604,7 +676,10 @@ class IconManager {
|
|||
const media = media_image_type(type);
|
||||
|
||||
await IconManager.cache.put_cache('icon_' + id, response.clone(), "image/" + media);
|
||||
const url = (this._id_urls[id] = await this._response_url(response.clone()));
|
||||
const url = await IconManager._response_url(response.clone());
|
||||
if(this._id_urls[id])
|
||||
URL.revokeObjectURL(this._id_urls[id]);
|
||||
this._id_urls[id] = url;
|
||||
|
||||
this._loading_promises[id] = undefined;
|
||||
return {
|
||||
|
@ -644,6 +719,58 @@ class IconManager {
|
|||
throw "icon not found";
|
||||
}
|
||||
|
||||
static generate_tag(icon: Promise<Icon> | Icon, options?: {
|
||||
animate?: boolean
|
||||
}) : JQuery<HTMLDivElement> {
|
||||
options = options || {};
|
||||
|
||||
let icon_container = $.spawn("div").addClass("icon-container icon_empty");
|
||||
let icon_load_image = $.spawn("div").addClass("icon_loading");
|
||||
|
||||
const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", "");
|
||||
const _apply = (icon) => {
|
||||
let id = icon ? (icon.id >>> 0) : 0;
|
||||
if (!icon || id == 0) {
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
return;
|
||||
} else if (id < 1000) {
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
|
||||
icon_container.removeClass("icon_empty").addClass("icon_em client-group_" + id);
|
||||
return;
|
||||
}
|
||||
|
||||
icon_image.attr("src", icon.url);
|
||||
icon_container.append(icon_image).removeClass("icon_empty");
|
||||
|
||||
if (typeof (options.animate) !== "boolean" || options.animate) {
|
||||
icon_image.css("opacity", 0);
|
||||
|
||||
icon_load_image.animate({opacity: 0}, 50, function () {
|
||||
icon_load_image.remove();
|
||||
icon_image.animate({opacity: 1}, 150);
|
||||
});
|
||||
} else {
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
}
|
||||
};
|
||||
if(icon instanceof Promise) {
|
||||
icon.then(_apply).catch(error => {
|
||||
console.error(tr("Could not load icon. Reason: %s"), error);
|
||||
icon_load_image.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon");
|
||||
});
|
||||
} else {
|
||||
_apply(icon as Icon);
|
||||
}
|
||||
|
||||
if(icon_load_image)
|
||||
icon_load_image.appendTo(icon_container);
|
||||
return icon_container;
|
||||
}
|
||||
|
||||
generateTag(id: number, options?: {
|
||||
animate?: boolean
|
||||
}) : JQuery<HTMLDivElement> {
|
||||
|
@ -651,44 +778,16 @@ class IconManager {
|
|||
|
||||
id = id >>> 0;
|
||||
if(id == 0 || !id)
|
||||
return $.spawn("div").addClass("icon_empty");
|
||||
return IconManager.generate_tag({id: id, url: ""}, options);
|
||||
else if(id < 1000)
|
||||
return $.spawn("div").addClass("icon client-group_" + id);
|
||||
return IconManager.generate_tag({id: id, url: ""}, options);
|
||||
|
||||
|
||||
const icon_container = $.spawn("div").addClass("icon-container icon_empty");
|
||||
const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", "");
|
||||
|
||||
if(this._id_urls[id]) {
|
||||
icon_image.attr("src", this._id_urls[id]).appendTo(icon_container);
|
||||
icon_container.removeClass("icon_empty");
|
||||
return IconManager.generate_tag({id: id, url: this._id_urls[id]}, options);
|
||||
} else {
|
||||
const icon_load_image = $.spawn("div").addClass("icon_loading");
|
||||
icon_load_image.appendTo(icon_container);
|
||||
|
||||
(async () => {
|
||||
let icon: Icon = await this.resolve_icon(id);
|
||||
|
||||
icon_image.attr("src", icon.url);
|
||||
icon_container.append(icon_image).removeClass("icon_empty");
|
||||
|
||||
if(typeof(options.animate) !== "boolean" || options.animate) {
|
||||
icon_image.css("opacity", 0);
|
||||
|
||||
icon_load_image.animate({opacity: 0}, 50, function () {
|
||||
icon_load_image.detach();
|
||||
icon_image.animate({opacity: 1}, 150);
|
||||
});
|
||||
} else {
|
||||
icon_load_image.detach();
|
||||
return IconManager.generate_tag(this.resolve_icon(id), options);
|
||||
}
|
||||
})().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");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._cached_avatars = undefined;
|
||||
this._loading_promises = undefined;
|
||||
}
|
||||
|
||||
private async _response_url(response: Response, type: ImageType) : Promise<string> {
|
||||
if(!response.headers.has('X-media-bytes'))
|
||||
throw "missing media bytes";
|
||||
|
@ -725,12 +829,12 @@ class AvatarManager {
|
|||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async resolved_cached?(client_avatar_id: string, avatar_id?: string) : Promise<Avatar> {
|
||||
let avatar: Avatar = this._cached_avatars[avatar_id];
|
||||
async resolved_cached?(client_avatar_id: string, avatar_version?: string) : Promise<Avatar> {
|
||||
let avatar: Avatar = this._cached_avatars[avatar_version];
|
||||
if(avatar) {
|
||||
if(typeof(avatar_id) !== "string" || avatar.avatar_id == avatar_id)
|
||||
if(typeof(avatar_version) !== "string" || avatar.avatar_id == avatar_version)
|
||||
return avatar;
|
||||
this._cached_avatars[avatar_id] = (avatar = undefined);
|
||||
avatar = undefined;
|
||||
}
|
||||
|
||||
if(!AvatarManager.cache.setupped())
|
||||
|
@ -740,14 +844,14 @@ class AvatarManager {
|
|||
if(!response)
|
||||
return undefined;
|
||||
|
||||
let response_avatar_id = response.headers.has("X-avatar-id") ? response.headers.get("X-avatar-id") : undefined;
|
||||
if(typeof(avatar_id) === "string" && response_avatar_id != avatar_id)
|
||||
let response_avatar_version = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined;
|
||||
if(typeof(avatar_version) === "string" && response_avatar_version != avatar_version)
|
||||
return undefined;
|
||||
|
||||
const type = image_type(response.headers.get('X-media-bytes'));
|
||||
return this._cached_avatars[client_avatar_id] = {
|
||||
client_avatar_id: client_avatar_id,
|
||||
avatar_id: avatar_id || response_avatar_id,
|
||||
avatar_id: avatar_version || response_avatar_version,
|
||||
url: await this._response_url(response, type),
|
||||
type: type
|
||||
};
|
||||
|
@ -758,13 +862,14 @@ class AvatarManager {
|
|||
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) {
|
||||
try {
|
||||
let download_key: transfer.DownloadKey;
|
||||
try {
|
||||
download_key = await this.create_avatar_download(client_avatar_id);
|
||||
} catch(error) {
|
||||
console.error(tr("Could not request download for avatar %s: %o"), client_avatar_id, error);
|
||||
throw "Failed to request icon";
|
||||
throw "failed to request avatar download";
|
||||
}
|
||||
|
||||
const downloader = transfer.spawn_download_transfer(download_key);
|
||||
|
@ -780,27 +885,53 @@ class AvatarManager {
|
|||
const media = media_image_type(type);
|
||||
|
||||
await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, {
|
||||
"X-avatar-id": avatar_id
|
||||
"X-avatar-version": avatar_version
|
||||
});
|
||||
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,
|
||||
avatar_id: avatar_version,
|
||||
url: url,
|
||||
type: type
|
||||
};
|
||||
} finally {
|
||||
this._loading_promises[client_avatar_id] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
loadAvatar(client_avatar_id: string, avatar_id: string) : Promise<Avatar> {
|
||||
return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_id));
|
||||
/* loads an avatar by the avatar id and optional with the avatar version */
|
||||
load_avatar(client_avatar_id: string, avatar_version: string) : Promise<Avatar> {
|
||||
return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_version));
|
||||
}
|
||||
|
||||
generate_client_tag(client: ClientEntry) : JQuery {
|
||||
return this.generate_tag(client.avatarId(), client.properties.client_flag_avatar);
|
||||
}
|
||||
|
||||
update_cache(client_avatar_id: string, avatar_id: string) {
|
||||
const _cached: Avatar = this._cached_avatars[client_avatar_id];
|
||||
if(_cached) {
|
||||
if(_cached.avatar_id === avatar_id)
|
||||
return; /* cache is up2date */
|
||||
|
||||
console.log(tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id);
|
||||
delete this._cached_avatars[client_avatar_id];
|
||||
AvatarManager.cache.delete("avatar_" + client_avatar_id).catch(error => {
|
||||
log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o: %o"), client_avatar_id, error);
|
||||
});
|
||||
} else {
|
||||
this.resolved_cached(client_avatar_id).then(avatar => {
|
||||
if(avatar && avatar.avatar_id !== avatar_id) {
|
||||
/* this time we ensured that its cached */
|
||||
this.update_cache(client_avatar_id, avatar_id);
|
||||
}
|
||||
}).catch(error => {
|
||||
log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o (cache lookup failed): %o"), client_avatar_id, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generate_tag(client_avatar_id: string, avatar_id?: string, options?: {
|
||||
callback_image?: (tag: JQuery<HTMLImageElement>) => any,
|
||||
callback_avatar?: (avatar: Avatar) => any
|
||||
|
@ -811,7 +942,9 @@ class AvatarManager {
|
|||
let avatar_image = $.spawn("img").attr("alt", tr("Client avatar"));
|
||||
|
||||
let cached_avatar: Avatar = this._cached_avatars[client_avatar_id];
|
||||
if(cached_avatar && cached_avatar.avatar_id == avatar_id) {
|
||||
if(avatar_id === "") {
|
||||
avatar_container.append(this.generate_default_image());
|
||||
} else if(cached_avatar && cached_avatar.avatar_id == avatar_id) {
|
||||
avatar_image.attr("src", cached_avatar.url);
|
||||
avatar_container.append(avatar_image);
|
||||
if(options.callback_image)
|
||||
|
@ -832,7 +965,7 @@ class AvatarManager {
|
|||
}
|
||||
|
||||
if(!avatar)
|
||||
avatar = await this.loadAvatar(client_avatar_id, avatar_id)
|
||||
avatar = await this.load_avatar(client_avatar_id, avatar_id);
|
||||
|
||||
if(!avatar)
|
||||
throw "failed to load avatar";
|
||||
|
@ -844,7 +977,7 @@ class AvatarManager {
|
|||
avatar_image.css("opacity", 0);
|
||||
avatar_container.append(avatar_image);
|
||||
loader_image.animate({opacity: 0}, 50, () => {
|
||||
loader_image.detach();
|
||||
loader_image.remove();
|
||||
avatar_image.animate({opacity: 1}, 150, () => {
|
||||
if(options.callback_image)
|
||||
options.callback_image(avatar_image);
|
||||
|
@ -859,4 +992,109 @@ class AvatarManager {
|
|||
|
||||
return avatar_container;
|
||||
}
|
||||
|
||||
unique_id_2_avatar_id(unique_id: string) {
|
||||
function str2ab(str) {
|
||||
let buf = new ArrayBuffer(str.length); // 2 bytes for each char
|
||||
let bufView = new Uint8Array(buf);
|
||||
for (let i=0, strLen = str.length; i<strLen; i++) {
|
||||
bufView[i] = str.charCodeAt(i);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
try {
|
||||
let raw = atob(unique_id);
|
||||
let input = hex.encode(str2ab(raw));
|
||||
|
||||
let result: string = "";
|
||||
for(let index = 0; index < input.length; index++) {
|
||||
let c = input.charAt(index);
|
||||
let offset: number = 0;
|
||||
if(c >= '0' && c <= '9')
|
||||
offset = c.charCodeAt(0) - '0'.charCodeAt(0);
|
||||
else if(c >= 'A' && c <= 'F')
|
||||
offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A;
|
||||
else if(c >= 'a' && c <= 'f')
|
||||
offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A;
|
||||
result += String.fromCharCode('a'.charCodeAt(0) + offset);
|
||||
}
|
||||
return result;
|
||||
} catch (e) { //invalid base 64 (like music bot etc)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private generate_default_image() : JQuery {
|
||||
return $.spawn("img").attr("src", "img/style/avatar.png").css({width: '100%', height: '100%'});
|
||||
}
|
||||
|
||||
generate_chat_tag(client: { id?: number; database_id?: number; }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any) : JQuery {
|
||||
let client_handle;
|
||||
if(typeof(client.id) == "number")
|
||||
client_handle = this.handle.handle.channelTree.findClient(client.id);
|
||||
if(!client_handle && typeof(client.id) == "number") {
|
||||
client_handle = this.handle.handle.channelTree.find_client_by_dbid(client.database_id);
|
||||
}
|
||||
|
||||
if(client_handle && client_handle.clientUid() !== client_unique_id)
|
||||
client_handle = undefined;
|
||||
|
||||
const container = $.spawn("div").addClass("avatar");
|
||||
if(client_handle && !client_handle.properties.client_flag_avatar)
|
||||
return container.append(this.generate_default_image());
|
||||
|
||||
|
||||
const avatar_id = client_handle ? client_handle.avatarId() : this.unique_id_2_avatar_id(client_unique_id);
|
||||
if(avatar_id) {
|
||||
if(this._cached_avatars[avatar_id]) { /* Test if we're may able to load the client avatar sync without a loading screen */
|
||||
const cache: Avatar = this._cached_avatars[avatar_id];
|
||||
console.log("[AVATAR] Using cached avatar. ID: %o | Version: %o (Cached: %o)", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id);
|
||||
if(!client_handle || client_handle.properties.client_flag_avatar == cache.avatar_id) {
|
||||
const image = $.spawn("img").attr("src", cache.url).css({width: '100%', height: '100%'});
|
||||
return container.append(image);
|
||||
}
|
||||
}
|
||||
|
||||
const image_loading = $.spawn("img").attr("src", "img/loading_image.svg").css({width: '100%', height: '100%'});
|
||||
|
||||
/* lets actually load the avatar */
|
||||
(async () => {
|
||||
let avatar: Avatar;
|
||||
let loaded_image = this.generate_default_image();
|
||||
|
||||
console.log("[AVATAR] Resolving avatar. ID: %o | Version: %o", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
|
||||
try {
|
||||
//TODO: Cache if avatar load failed and try again in some minutes/may just even consider using the default avatar 'till restart
|
||||
try {
|
||||
avatar = await this.resolved_cached(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
|
||||
} catch(error) {
|
||||
console.error(tr("Failed to use cached avatar: %o"), error);
|
||||
}
|
||||
|
||||
if(!avatar)
|
||||
avatar = await this.load_avatar(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined);
|
||||
|
||||
if(!avatar)
|
||||
throw "no avatar present!";
|
||||
|
||||
loaded_image = $.spawn("img").attr("src", avatar.url).css({width: '100%', height: '100%'});
|
||||
} catch(error) {
|
||||
throw error;
|
||||
} finally {
|
||||
container.children().remove();
|
||||
container.append(loaded_image);
|
||||
}
|
||||
})().then(() => callback_loaded && callback_loaded(true)).catch(error => {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to load chat avatar for client %s. Error: %o"), client_unique_id, error);
|
||||
callback_loaded && callback_loaded(false, error);
|
||||
});
|
||||
|
||||
image_loading.appendTo(container);
|
||||
} else {
|
||||
this.generate_default_image().appendTo(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
|
@ -166,7 +166,11 @@ namespace ppt {
|
|||
if(key.key_windows)
|
||||
result += " + " + tr("Win");
|
||||
|
||||
result += " + " + (key.key_code ? key.key_code : tr("unset"));
|
||||
if(!result && !key.key_code)
|
||||
return tr("unset");
|
||||
|
||||
if(key.key_code)
|
||||
result += " + " + key.key_code;
|
||||
return result.substr(3);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@ namespace audio {
|
|||
export namespace player {
|
||||
export interface Device {
|
||||
device_id: string;
|
||||
|
||||
driver: string;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,37 @@ namespace bookmarks {
|
|||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
|
||||
const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile();
|
||||
if(profile.valid()) {
|
||||
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler();
|
||||
server_connections.set_active_connection_handler(connection);
|
||||
connection.startConnection(
|
||||
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
profile,
|
||||
true,
|
||||
{
|
||||
nickname: mark.nickname,
|
||||
password: mark.server_properties.server_password_hash ? {
|
||||
password: mark.server_properties.server_password_hash,
|
||||
hashed: true
|
||||
} : mark.server_properties.server_password ? {
|
||||
hashed: false,
|
||||
password: mark.server_properties.server_password
|
||||
} : undefined
|
||||
}
|
||||
);
|
||||
} else {
|
||||
Modals.spawnConnectModal({}, {
|
||||
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
enforce: true
|
||||
}, {
|
||||
profile: profile,
|
||||
enforce: true
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export interface ServerProperties {
|
||||
server_address: string;
|
||||
server_port: number;
|
||||
|
@ -35,6 +66,8 @@ namespace bookmarks {
|
|||
default_channel_password?: string;
|
||||
|
||||
connect_profile: string;
|
||||
|
||||
last_icon_id?: number;
|
||||
}
|
||||
|
||||
export interface DirectoryBookmark {
|
||||
|
@ -88,6 +121,19 @@ namespace bookmarks {
|
|||
return bookmark_config().root_bookmark;
|
||||
}
|
||||
|
||||
export function bookmarks_flat() : Bookmark[] {
|
||||
const result: Bookmark[] = [];
|
||||
const _flat = (bookmark: Bookmark | DirectoryBookmark) => {
|
||||
if(bookmark.type == BookmarkType.DIRECTORY)
|
||||
for(const book of (bookmark as DirectoryBookmark).content)
|
||||
_flat(book);
|
||||
else
|
||||
result.push(bookmark as Bookmark);
|
||||
};
|
||||
_flat(bookmark_config().root_bookmark);
|
||||
return result;
|
||||
}
|
||||
|
||||
function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark {
|
||||
for(const entry of parent.content) {
|
||||
if(entry.unique_id == uuid)
|
||||
|
@ -169,4 +215,27 @@ namespace bookmarks {
|
|||
export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
|
||||
delete_bookmark_recursive(bookmarks(), bookmark)
|
||||
}
|
||||
|
||||
export function add_current_server() {
|
||||
const ch = server_connections.active_connection_handler();
|
||||
if(ch && ch.connected) {
|
||||
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => true, result => {
|
||||
if(result) {
|
||||
const bookmark = create_bookmark(result as string, bookmarks(), {
|
||||
server_port: ch.serverConnection.remote_address().port,
|
||||
server_address: ch.serverConnection.remote_address().host,
|
||||
|
||||
server_password: "",
|
||||
server_password_hash: ""
|
||||
}, this.connection_handler.getClient().clientNickName());
|
||||
save_bookmark(bookmark);
|
||||
|
||||
control_bar.update_bookmarks();
|
||||
top_menu.rebuild_bookmarks();
|
||||
}
|
||||
}).open();
|
||||
} else {
|
||||
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
/// <reference path="ConnectionBase.ts" />
|
||||
|
||||
namespace connection {
|
||||
import Conversation = chat.channel.Conversation;
|
||||
|
||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||
constructor(connection: AbstractServerConnection) {
|
||||
super(connection);
|
||||
|
@ -23,6 +25,7 @@ namespace connection {
|
|||
this["notifychannelhide"] = this.handleCommandChannelHide;
|
||||
this["notifychannelshow"] = this.handleCommandChannelShow;
|
||||
|
||||
this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo;
|
||||
this["notifycliententerview"] = this.handleCommandClientEnterView;
|
||||
this["notifyclientleftview"] = this.handleCommandClientLeftView;
|
||||
this["notifyclientmoved"] = this.handleNotifyClientMoved;
|
||||
|
@ -45,6 +48,9 @@ namespace connection {
|
|||
|
||||
this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed;
|
||||
this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed;
|
||||
|
||||
this["notifyconversationhistory"] = this.handleNotifyConversationHistory;
|
||||
this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete;
|
||||
}
|
||||
|
||||
proxy_command_promise(promise: Promise<CommandResult>, options: connection.CommandOptions) {
|
||||
|
@ -56,20 +62,21 @@ namespace connection {
|
|||
if(ex instanceof CommandResult) {
|
||||
let res = ex;
|
||||
if(!res.success) {
|
||||
if(res.id == 2568) { //Permission error
|
||||
res.message = tr("Insufficient client permissions. Failed on permission ") + this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number).name;
|
||||
if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error
|
||||
const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number);
|
||||
res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown");
|
||||
this.connection_handler.log.log(log.server.Type.ERROR_PERMISSION, {
|
||||
permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number)
|
||||
});
|
||||
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
|
||||
} else {
|
||||
} else if(res.id != ErrorID.EMPTY_RESULT) {
|
||||
this.connection_handler.log.log(log.server.Type.ERROR_CUSTOM, {
|
||||
message: res.extra_message.length == 0 ? res.message : res.extra_message
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if(typeof(ex) === "string") {
|
||||
this.connection_handler.chat.serverChat().appendError(tr("Command execution results in ") + ex);
|
||||
this.connection_handler.log.log(log.server.Type.CONNECTION_COMMAND_ERROR, {error: ex});
|
||||
} else {
|
||||
console.error(tr("Invalid promise result type: %o. Result:"), typeof (ex));
|
||||
console.error(ex);
|
||||
|
@ -131,6 +138,8 @@ namespace connection {
|
|||
|
||||
json = json[0]; //Only one bulk
|
||||
|
||||
this.connection_handler.channelTree.registerClient(this.connection_handler.getClient());
|
||||
this.connection.client.side_bar.channel_conversations().reset();
|
||||
this.connection.client.clientId = parseInt(json["aclid"]);
|
||||
this.connection.client.getClient().updateVariables({key: "client_nickname", value: json["acn"]});
|
||||
|
||||
|
@ -146,8 +155,61 @@ namespace connection {
|
|||
}
|
||||
this.connection.client.channelTree.server.updateVariables(false, ...updates);
|
||||
|
||||
const properties = this.connection.client.channelTree.server.properties;
|
||||
/* host message */
|
||||
if(properties.virtualserver_hostmessage_mode > 0) {
|
||||
if(properties.virtualserver_hostmessage_mode == 1) {
|
||||
/* show in log */
|
||||
this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE, {
|
||||
message: properties.virtualserver_hostmessage
|
||||
});
|
||||
} else {
|
||||
/* create modal/create modal and quit */
|
||||
createModal({
|
||||
header: tr("Host message"),
|
||||
body: MessageHelper.bbcode_chat(properties.virtualserver_hostmessage),
|
||||
footer: undefined
|
||||
}).open();
|
||||
|
||||
if(properties.virtualserver_hostmessage_mode == 3) {
|
||||
/* first let the client initialize his stuff */
|
||||
setTimeout(() => {
|
||||
this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE_DISCONNECT, {
|
||||
message: properties.virtualserver_welcomemessage
|
||||
});
|
||||
|
||||
this.connection.disconnect("host message disconnect");
|
||||
this.connection_handler.handleDisconnect(DisconnectReason.SERVER_HOSTMESSAGE);
|
||||
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* welcome message */
|
||||
if(properties.virtualserver_welcomemessage) {
|
||||
this.connection_handler.log.log(log.server.Type.SERVER_WELCOME_MESSAGE, {
|
||||
message: properties.virtualserver_welcomemessage
|
||||
});
|
||||
}
|
||||
|
||||
/* priviledge key */
|
||||
if(properties.virtualserver_ask_for_privilegekey) {
|
||||
createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.<br>Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => {
|
||||
if(!result) return;
|
||||
const scon = server_connections.active_connection_handler();
|
||||
|
||||
if(scon.serverConnection.connected)
|
||||
scon.serverConnection.send_command("tokenuse", {
|
||||
token: result
|
||||
}).then(() => {
|
||||
createInfoModal(tr("Use privilege key"), tr("Privilege key successfully used!")).open();
|
||||
}).catch(error => {
|
||||
createErrorModal(tr("Use privilege key"), MessageHelper.formatMessage(tr("Failed to use privilege key: {}"), error instanceof CommandResult ? error.message : error)).open();
|
||||
});
|
||||
}, { field_placeholder: 'Enter Privilege Key' }).open();
|
||||
}
|
||||
|
||||
this.connection_handler.chat.serverChat().name = this.connection.client.channelTree.server.properties["virtualserver_name"];
|
||||
this.connection_handler.log.log(log.server.Type.CONNECTION_CONNECTED, {
|
||||
own_client: this.connection_handler.getClient().log_data()
|
||||
});
|
||||
|
@ -155,6 +217,16 @@ namespace connection {
|
|||
this.connection.client.onConnected();
|
||||
}
|
||||
|
||||
handleNotifyServerConnectionInfo(json) {
|
||||
json = json[0];
|
||||
|
||||
/* everything is a number, so lets parse it */
|
||||
for(const key of Object.keys(json))
|
||||
json[key] = parseInt(json[key]);
|
||||
|
||||
this.connection_handler.channelTree.server.set_connection_info(json);
|
||||
}
|
||||
|
||||
private createChannelFromJson(json, ignoreOrder: boolean = false) {
|
||||
let tree = this.connection.client.channelTree;
|
||||
|
||||
|
@ -223,9 +295,11 @@ namespace connection {
|
|||
|
||||
handleCommandChannelDelete(json) {
|
||||
let tree = this.connection.client.channelTree;
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
|
||||
console.log(tr("Got %d channel deletions"), json.length);
|
||||
for(let index = 0; index < json.length; index++) {
|
||||
conversations.delete_conversation(parseInt(json[index]["cid"]));
|
||||
let channel = tree.findChannel(json[index]["cid"]);
|
||||
if(!channel) {
|
||||
console.error(tr("Invalid channel onDelete (Unknown channel)"));
|
||||
|
@ -237,9 +311,11 @@ namespace connection {
|
|||
|
||||
handleCommandChannelHide(json) {
|
||||
let tree = this.connection.client.channelTree;
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
|
||||
console.log(tr("Got %d channel hides"), json.length);
|
||||
for(let index = 0; index < json.length; index++) {
|
||||
conversations.delete_conversation(parseInt(json[index]["cid"]));
|
||||
let channel = tree.findChannel(json[index]["cid"]);
|
||||
if(!channel) {
|
||||
console.error(tr("Invalid channel on hide (Unknown channel)"));
|
||||
|
@ -282,8 +358,6 @@ namespace connection {
|
|||
client.properties.client_type = parseInt(entry["client_type"]);
|
||||
client = tree.insertClient(client, channel);
|
||||
} else {
|
||||
if(client == this.connection.client.getClient())
|
||||
this.connection_handler.chat.channelChat().name = channel.channelName();
|
||||
tree.moveClient(client, channel);
|
||||
}
|
||||
|
||||
|
@ -338,32 +412,25 @@ namespace connection {
|
|||
|
||||
client.updateVariables(...updates);
|
||||
|
||||
{
|
||||
let client_chat = client.chat(false);
|
||||
if(!client_chat) {
|
||||
for(const c of this.connection_handler.chat.open_chats()) {
|
||||
if(c.owner_unique_id == client.properties.client_unique_identifier && c.flag_offline) {
|
||||
client_chat = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(client_chat) {
|
||||
client_chat.appendMessage(
|
||||
"{0}", true,
|
||||
$.spawn("div")
|
||||
.addClass("event-message event-partner-connect")
|
||||
.text(tr("Your chat partner has reconnected"))
|
||||
);
|
||||
client_chat.flag_offline = false;
|
||||
client.initialize_chat(client_chat);
|
||||
}
|
||||
if(!old_channel) {
|
||||
/* client new join */
|
||||
const conversation_manager = this.connection_handler.side_bar.private_conversations();
|
||||
const conversation = conversation_manager.find_conversation({
|
||||
unique_id: client.properties.client_unique_identifier,
|
||||
client_id: client.clientId(),
|
||||
name: client.clientNickName()
|
||||
}, {
|
||||
create: false,
|
||||
attach: true
|
||||
});
|
||||
}
|
||||
|
||||
if(client instanceof LocalClientEntry) {
|
||||
client.initializeListener();
|
||||
this.connection_handler.update_voice_status();
|
||||
this.connection_handler.chat_frame.info_frame().update_channel_talk();
|
||||
this.connection_handler.side_bar.info_frame().update_channel_talk();
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
conversations.set_current_channel(client.currentChannel().channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -390,7 +457,7 @@ namespace connection {
|
|||
this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry);
|
||||
} else
|
||||
this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry);
|
||||
this.connection_handler.chat_frame.info_frame().update_channel_talk();
|
||||
this.connection_handler.side_bar.info_frame().update_channel_talk();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -436,19 +503,19 @@ namespace connection {
|
|||
console.error(tr("Unknown client left reason!"));
|
||||
}
|
||||
|
||||
{
|
||||
const chat = client.chat(false);
|
||||
if(chat) {
|
||||
chat.flag_offline = true;
|
||||
chat.onMessageSend = undefined;
|
||||
chat.onClose = undefined;
|
||||
chat.appendMessage(
|
||||
"{0}", true,
|
||||
$.spawn("div")
|
||||
.addClass("event-message event-partner-disconnect")
|
||||
.text(tr("Your chat partner has disconnected"))
|
||||
);
|
||||
}
|
||||
if(!channel_to) {
|
||||
/* client left the server */
|
||||
const conversation_manager = this.connection_handler.side_bar.private_conversations();
|
||||
const conversation = conversation_manager.find_conversation({
|
||||
unique_id: client.properties.client_unique_identifier,
|
||||
client_id: client.clientId(),
|
||||
name: client.clientNickName()
|
||||
}, {
|
||||
create: false,
|
||||
attach: false
|
||||
});
|
||||
if(conversation)
|
||||
conversation.set_state(chat.PrivateConversationState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -478,7 +545,6 @@ namespace connection {
|
|||
let self = client instanceof LocalClientEntry;
|
||||
let current_clients: ClientEntry[];
|
||||
if(self) {
|
||||
this.connection_handler.chat.channelChat().name = channel_to.channelName();
|
||||
current_clients = client.channelTree.clientsByChannel(client.currentChannel());
|
||||
this.connection_handler.update_voice_status(channel_to);
|
||||
}
|
||||
|
@ -488,8 +554,20 @@ namespace connection {
|
|||
if(entry !== client && entry.get_audio_handle())
|
||||
entry.get_audio_handle().abort_replay();
|
||||
|
||||
if(self)
|
||||
this.connection_handler.chat_frame.info_frame().update_channel_talk();
|
||||
if(self) {
|
||||
const side_bar = this.connection_handler.side_bar;
|
||||
side_bar.info_frame().update_channel_talk();
|
||||
|
||||
const conversation_to = side_bar.channel_conversations().conversation(channel_to.channelId, false);
|
||||
if(conversation_to)
|
||||
conversation_to.update_private_state();
|
||||
|
||||
const conversation_from = side_bar.channel_conversations().conversation(channel_from.channelId, false);
|
||||
if(conversation_from)
|
||||
conversation_from.update_private_state();
|
||||
|
||||
side_bar.channel_conversations().update_chat_box();
|
||||
}
|
||||
|
||||
const own_channel = this.connection.client.getClient().currentChannel();
|
||||
this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_MOVE, {
|
||||
|
@ -603,29 +681,65 @@ namespace connection {
|
|||
|
||||
let mode = json["targetmode"];
|
||||
if(mode == 1){
|
||||
let invoker = this.connection.client.channelTree.findClient(json["invokerid"]);
|
||||
let target = this.connection.client.channelTree.findClient(json["target"]);
|
||||
if(!invoker) { //TODO spawn chat (Client is may invisible)
|
||||
console.error(tr("Got private message from invalid client!"));
|
||||
//json["invokerid"], json["invokername"], json["invokeruid"]
|
||||
const target_client_id = parseInt(json["target"]);
|
||||
const target_own = target_client_id === this.connection.client.getClientId();
|
||||
|
||||
if(target_own && target_client_id === json["invokerid"]) {
|
||||
console.error(tr("Received conversation message from invalid client id. Data: %o", json));
|
||||
return;
|
||||
}
|
||||
if(!target) { //TODO spawn chat (Client is may invisible)
|
||||
console.error(tr("Got private message from invalid client!"));
|
||||
|
||||
const conversation_manager = this.connection_handler.side_bar.private_conversations();
|
||||
const conversation = conversation_manager.find_conversation({
|
||||
client_id: target_own ? parseInt(json["invokerid"]) : target_client_id,
|
||||
unique_id: target_own ? json["invokeruid"] : undefined,
|
||||
name: target_own ? json["invokername"] : undefined
|
||||
}, {
|
||||
create: target_own,
|
||||
attach: target_own
|
||||
});
|
||||
if(!conversation) {
|
||||
console.error(tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message"));
|
||||
return;
|
||||
}
|
||||
if(invoker == this.connection.client.getClient()) {
|
||||
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
|
||||
target.chat(true).appendMessage("{0}: {1}", true, this.connection.client.getClient().createChatTag(true), MessageHelper.bbcode_chat(json["msg"]));
|
||||
} else {
|
||||
|
||||
conversation.append_message(json["msg"], {
|
||||
type: target_own ? "partner" : "self",
|
||||
name: json["invokername"],
|
||||
unique_id: json["invokeruid"],
|
||||
client_id: parseInt(json["invokerid"])
|
||||
});
|
||||
|
||||
if(target_own) {
|
||||
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
|
||||
invoker.chat(true).appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]));
|
||||
} else {
|
||||
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
|
||||
}
|
||||
} else if(mode == 2) {
|
||||
const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
|
||||
const own_channel_id = this.connection.client.getClient().currentChannel().channelId;
|
||||
const channel_id = typeof(json["cid"]) !== "undefined" ? parseInt(json["cid"]) : own_channel_id;
|
||||
const channel = this.connection_handler.channelTree.findChannel(channel_id);
|
||||
|
||||
if(json["invokerid"] == this.connection.client.clientId)
|
||||
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
|
||||
else
|
||||
else if(channel_id == own_channel_id) {
|
||||
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
|
||||
this.connection_handler.chat.channelChat().appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"]))
|
||||
}
|
||||
|
||||
const conversations = this.connection_handler.side_bar.channel_conversations();
|
||||
const conversation = conversations.conversation(channel_id);
|
||||
conversation.register_new_message({
|
||||
sender_database_id: invoker ? invoker.properties.client_database_id : 0,
|
||||
sender_name: json["invokername"],
|
||||
sender_unique_id: json["invokeruid"],
|
||||
|
||||
timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]),
|
||||
message: json["msg"]
|
||||
});
|
||||
if(conversation.is_unread())
|
||||
channel.flag_text_unread = true;
|
||||
} else if(mode == 3) {
|
||||
this.connection_handler.log.log(log.server.Type.GLOBAL_MESSAGE, {
|
||||
message: json["msg"],
|
||||
|
@ -635,6 +749,18 @@ namespace connection {
|
|||
client_id: parseInt(json["invokerid"])
|
||||
}
|
||||
});
|
||||
|
||||
const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
|
||||
const conversations = this.connection_handler.side_bar.channel_conversations();
|
||||
const conversation = conversations.conversation(0);
|
||||
conversation.register_new_message({
|
||||
sender_database_id: invoker ? invoker.properties.client_database_id : 0,
|
||||
sender_name: json["invokername"],
|
||||
sender_unique_id: json["invokeruid"],
|
||||
|
||||
timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]),
|
||||
message: json["msg"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -646,28 +772,20 @@ namespace connection {
|
|||
//clid: "6"
|
||||
//cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY="
|
||||
|
||||
const client = this.connection.client.channelTree.findClient(json["clid"]);
|
||||
if(!client) {
|
||||
log.warn(LogCategory.GENERAL, tr("Received chat close for unknown client"));
|
||||
return;
|
||||
}
|
||||
if(client.properties.client_unique_identifier !== json["cluid"]) {
|
||||
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but unique ids dosn't match. (expected %o, received %o)"), client.properties.client_unique_identifier, json["cluid"]);
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = client.chat(false);
|
||||
if(!chat) {
|
||||
const conversation_manager = this.connection_handler.side_bar.private_conversations();
|
||||
const conversation = conversation_manager.find_conversation({
|
||||
client_id: parseInt(json["clid"]),
|
||||
unique_id: json["cluid"],
|
||||
name: undefined
|
||||
}, {
|
||||
create: false,
|
||||
attach: false
|
||||
});
|
||||
if(!conversation) {
|
||||
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open."));
|
||||
return;
|
||||
}
|
||||
chat.flag_offline = true;
|
||||
chat.appendMessage(
|
||||
"{0}", true,
|
||||
$.spawn("div")
|
||||
.addClass("event-message event-partner-closed")
|
||||
.text(tr("Your chat partner has close the conversation"))
|
||||
);
|
||||
conversation.set_state(chat.PrivateConversationState.CLOSED);
|
||||
}
|
||||
|
||||
handleNotifyClientUpdated(json) {
|
||||
|
@ -811,5 +929,38 @@ namespace connection {
|
|||
this.connection.client.channelTree.deleteClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
handleNotifyConversationHistory(json: any[]) {
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
const conversation = conversations.conversation(parseInt(json[0]["cid"]));
|
||||
if(!conversation) {
|
||||
log.warn(LogCategory.NETWORKING, tr("Received conversation history for invalid or unknown conversation (%o)"), json[0]["cid"]);
|
||||
return;
|
||||
}
|
||||
|
||||
for(const entry of json) {
|
||||
conversation.register_new_message({
|
||||
message: entry["msg"],
|
||||
sender_unique_id: entry["sender_unique_id"],
|
||||
sender_name: entry["sender_name"],
|
||||
timestamp: parseInt(entry["timestamp"]),
|
||||
sender_database_id: parseInt(entry["sender_database_id"])
|
||||
}, false);
|
||||
}
|
||||
conversation.fix_scroll(true);
|
||||
}
|
||||
|
||||
handleNotifyConversationMessageDelete(json: any[]) {
|
||||
let conversation: Conversation;
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
for(const entry of json) {
|
||||
if(typeof(entry["cid"]) !== "undefined")
|
||||
conversation = conversations.conversation(parseInt(entry["cid"]), false);
|
||||
if(!conversation)
|
||||
continue;
|
||||
|
||||
conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,14 @@ namespace connection {
|
|||
|
||||
initialize() {
|
||||
this.connection.command_handler_boss().register_handler(this);
|
||||
/* notifyquerylist */
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(this.connection) {
|
||||
const hboss = this.connection.command_handler_boss();
|
||||
hboss && hboss.unregister_handler(this);
|
||||
}
|
||||
this._awaiters_unique_ids = undefined;
|
||||
}
|
||||
|
||||
handle_command(command: connection.ServerCommand): boolean {
|
||||
|
|
|
@ -39,6 +39,11 @@ namespace connection {
|
|||
|
||||
abstract remote_address() : ServerAddress; /* only valid when connected */
|
||||
abstract handshake_handler() : HandshakeHandler; /* only valid when connected */
|
||||
|
||||
abstract ping() : {
|
||||
native: number,
|
||||
javascript?: number
|
||||
};
|
||||
}
|
||||
|
||||
export namespace voice {
|
||||
|
@ -128,6 +133,11 @@ namespace connection {
|
|||
this.connection = connection;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.command_handlers = undefined;
|
||||
this.single_command_handler = undefined;
|
||||
}
|
||||
|
||||
register_handler(handler: AbstractCommandHandler) {
|
||||
if(!handler.volatile_handler_boss && handler.handler_boss)
|
||||
throw "handler already registered";
|
||||
|
|
|
@ -4,6 +4,12 @@ enum ErrorID {
|
|||
PLAYLIST_IS_IN_USE = 0x2103,
|
||||
|
||||
FILE_ALREADY_EXISTS = 2050,
|
||||
|
||||
CLIENT_INVALID_ID = 0x0200,
|
||||
|
||||
CONVERSATION_INVALID_ID = 0x2200,
|
||||
CONVERSATION_MORE_DATA = 0x2201,
|
||||
CONVERSATION_IS_PRIVATE = 0x2202
|
||||
}
|
||||
|
||||
class CommandResult {
|
||||
|
|
|
@ -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.path = path;
|
||||
//TODO validate file
|
||||
|
||||
//TODO: Validate file
|
||||
resolve(file);
|
||||
} catch(error) {
|
||||
log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, error);
|
||||
|
@ -119,10 +120,16 @@ namespace i18n {
|
|||
}
|
||||
|
||||
export function load_file(url: string, path: string) : Promise<void> {
|
||||
return load_translation_file(url, path).then(result => {
|
||||
return load_translation_file(url, path).then(async result => {
|
||||
/* TODO: Improve this test?!*/
|
||||
try {
|
||||
tr("Dummy translation test");
|
||||
} catch(error) {
|
||||
throw "dummy test failed";
|
||||
}
|
||||
|
||||
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
|
||||
translations = result.translations;
|
||||
return Promise.resolve();
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error);
|
||||
return Promise.reject(error);
|
||||
|
@ -292,6 +299,7 @@ namespace i18n {
|
|||
try {
|
||||
await load_file(cfg.current_translation_url, cfg.current_translation_path);
|
||||
} catch (error) {
|
||||
console.error(tr("Failed to initialize selected translation: %o"), error);
|
||||
createErrorModal(tr("Translation System"), tr("Failed to load current selected translation file.") + "<br>File: " + cfg.current_translation_url + "<br>Error: " + error + "<br>" + tr("Using default fallback translations.")).open();
|
||||
}
|
||||
}
|
||||
|
@ -303,3 +311,6 @@ namespace i18n {
|
|||
// @ts-ignore
|
||||
const tr: typeof i18n.tr = i18n.tr;
|
||||
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() {
|
||||
return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG;
|
||||
}
|
||||
|
||||
let _ui_version;
|
||||
export function ui_version() {
|
||||
if(typeof(_ui_version) !== "string") {
|
||||
const version_node = document.getElementById("app_version");
|
||||
if(!version_node) return undefined;
|
||||
|
||||
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
|
||||
if(!version) return undefined;
|
||||
|
||||
return (_ui_version = version);
|
||||
}
|
||||
return _ui_version;
|
||||
}
|
||||
}
|
||||
|
||||
namespace loader {
|
||||
|
@ -150,19 +164,31 @@ namespace loader {
|
|||
console.groupCollapsed("Executing loading stage %s", Stage[current_stage]);
|
||||
}
|
||||
}
|
||||
|
||||
/* cleanup */
|
||||
{
|
||||
_script_promises = {};
|
||||
}
|
||||
console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
|
||||
}
|
||||
|
||||
type SourcePath = string | string[];
|
||||
type DependSource = {
|
||||
url: string;
|
||||
depends: string[];
|
||||
}
|
||||
type SourcePath = string | DependSource | string[];
|
||||
|
||||
function script_name(path: string | string[]) {
|
||||
function script_name(path: SourcePath) {
|
||||
if(Array.isArray(path)) {
|
||||
let buffer = "";
|
||||
let _or = " or ";
|
||||
for(let entry of path)
|
||||
buffer += _or + script_name(entry);
|
||||
return buffer.slice(_or.length);
|
||||
} else return "<code>" + path + "</code>";
|
||||
} else if(typeof(path) === "string")
|
||||
return "<code>" + path + "</code>";
|
||||
else
|
||||
return "<code>" + path.url + "</code>";
|
||||
}
|
||||
|
||||
class SyntaxError {
|
||||
|
@ -173,6 +199,7 @@ namespace loader {
|
|||
}
|
||||
}
|
||||
|
||||
let _script_promises: {[key: string]: Promise<void>} = {};
|
||||
export async function load_script(path: SourcePath) : Promise<void> {
|
||||
if(Array.isArray(path)) { //We have some fallback
|
||||
return load_script(path[0]).catch(error => {
|
||||
|
@ -185,9 +212,20 @@ namespace loader {
|
|||
return Promise.reject(error);
|
||||
});
|
||||
} else {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
|
||||
if(source.url.length == 0) return Promise.resolve();
|
||||
|
||||
return _script_promises[source.url] = (async () => {
|
||||
/* await depends */
|
||||
for(const depend of source.depends) {
|
||||
if(!_script_promises[depend])
|
||||
throw "Missing dependency " + depend;
|
||||
await _script_promises[depend];
|
||||
}
|
||||
|
||||
const tag: HTMLScriptElement = document.createElement("script");
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let error = false;
|
||||
const error_handler = (event: ErrorEvent) => {
|
||||
if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
|
||||
|
@ -201,29 +239,37 @@ namespace loader {
|
|||
};
|
||||
window.addEventListener('error', error_handler as any);
|
||||
|
||||
const cleanup = () => {
|
||||
tag.onerror = undefined;
|
||||
tag.onload = undefined;
|
||||
|
||||
clearTimeout(timeout_handle);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
};
|
||||
const timeout_handle = setTimeout(() => {
|
||||
cleanup();
|
||||
reject("timeout");
|
||||
}, 5000);
|
||||
tag.type = "application/javascript";
|
||||
tag.async = true;
|
||||
tag.defer = true;
|
||||
tag.onerror = error => {
|
||||
clearTimeout(timeout_handle);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
cleanup();
|
||||
tag.remove();
|
||||
reject(error);
|
||||
};
|
||||
tag.onload = () => {
|
||||
clearTimeout(timeout_handle);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
cleanup();
|
||||
|
||||
console.debug("Script %o loaded", path);
|
||||
setTimeout(resolve, 100);
|
||||
};
|
||||
|
||||
document.getElementById("scripts").appendChild(tag);
|
||||
|
||||
tag.src = path + (cache_tag || "");
|
||||
tag.src = source.url + (cache_tag || "");
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,7 +303,7 @@ namespace loader {
|
|||
|
||||
export async function load_style(path: SourcePath) : Promise<void> {
|
||||
if(Array.isArray(path)) { //We have some fallback
|
||||
return load_script(path[0]).catch(error => {
|
||||
return load_style(path[0]).catch(error => {
|
||||
if(error instanceof SyntaxError)
|
||||
return Promise.reject(error.source);
|
||||
|
||||
|
@ -267,6 +313,10 @@ namespace loader {
|
|||
return Promise.reject(error);
|
||||
});
|
||||
} else {
|
||||
if(!path) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const tag: HTMLLinkElement = document.createElement("link");
|
||||
|
||||
|
@ -283,21 +333,30 @@ namespace loader {
|
|||
};
|
||||
window.addEventListener('error', error_handler as any);
|
||||
|
||||
tag.type = "text/css";
|
||||
tag.rel = "stylesheet";
|
||||
|
||||
const cleanup = () => {
|
||||
tag.onerror = undefined;
|
||||
tag.onload = undefined;
|
||||
|
||||
clearTimeout(timeout_handle);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
};
|
||||
|
||||
const timeout_handle = setTimeout(() => {
|
||||
cleanup();
|
||||
reject("timeout");
|
||||
}, 5000);
|
||||
|
||||
tag.type = "text/css";
|
||||
tag.rel="stylesheet";
|
||||
|
||||
tag.onerror = error => {
|
||||
clearTimeout(timeout_handle);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
cleanup();
|
||||
tag.remove();
|
||||
console.error("File load error for file %s: %o", path, error);
|
||||
reject("failed to load file " + path);
|
||||
};
|
||||
tag.onload = () => {
|
||||
cleanup();
|
||||
{
|
||||
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
|
||||
const rules = css.cssRules;
|
||||
|
@ -324,8 +383,6 @@ namespace loader {
|
|||
css.insertRule(rule, rules_remove[0]);
|
||||
}
|
||||
|
||||
clearTimeout(timeout_handle);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
console.debug("Style sheet %o loaded", path);
|
||||
setTimeout(resolve, 100);
|
||||
};
|
||||
|
@ -464,6 +521,7 @@ const loader_javascript = {
|
|||
if(!window.require) {
|
||||
await loader.load_script(["vendor/jquery/jquery.min.js"]);
|
||||
} else {
|
||||
/*
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "forum sync",
|
||||
priority: 10,
|
||||
|
@ -471,26 +529,36 @@ const loader_javascript = {
|
|||
forum.sync_main();
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
await loader.load_script(["vendor/DOMPurify/purify.min.js"]);
|
||||
|
||||
/* bootstrap material design and libs */
|
||||
await loader.load_script(["vendor/popper/popper.js"]);
|
||||
//await loader.load_script(["vendor/popper/popper.js"]);
|
||||
|
||||
//depends on popper
|
||||
await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]);
|
||||
//await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]);
|
||||
|
||||
/*
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "materialize body",
|
||||
priority: 10,
|
||||
function: async () => { $(document).ready(function() { $('body').bootstrapMaterialDesign(); }); }
|
||||
});
|
||||
*/
|
||||
|
||||
await loader.load_script("vendor/jsrender/jsrender.min.js");
|
||||
await loader.load_scripts([
|
||||
["vendor/xbbcode/src/parser.js"],
|
||||
["vendor/moment/moment.js"],
|
||||
["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */
|
||||
["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */
|
||||
["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */
|
||||
["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"]
|
||||
]);
|
||||
await loader.load_scripts([
|
||||
["vendor/emoji-picker/src/jquery.lsxemojipicker.js"]
|
||||
]);
|
||||
|
||||
if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) {
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
|
@ -547,14 +615,19 @@ const loader_javascript = {
|
|||
//load the profiles
|
||||
"js/profiles/ConnectionProfile.js",
|
||||
"js/profiles/Identity.js",
|
||||
"js/profiles/identities/teaspeak-forum.js",
|
||||
|
||||
//Basic UI elements
|
||||
"js/ui/elements/context_divider.js",
|
||||
"js/ui/elements/context_menu.js",
|
||||
"js/ui/elements/modal.js",
|
||||
"js/ui/elements/tab.js",
|
||||
"js/ui/elements/slider.js",
|
||||
"js/ui/elements/tooltip.js",
|
||||
|
||||
//Load UI
|
||||
"js/ui/modal/ModalAbout.js",
|
||||
"js/ui/modal/ModalAvatar.js",
|
||||
"js/ui/modal/ModalAvatarList.js",
|
||||
"js/ui/modal/ModalQuery.js",
|
||||
"js/ui/modal/ModalQueryManage.js",
|
||||
|
@ -569,13 +642,16 @@ const loader_javascript = {
|
|||
"js/ui/modal/ModalBanClient.js",
|
||||
"js/ui/modal/ModalIconSelect.js",
|
||||
"js/ui/modal/ModalInvite.js",
|
||||
"js/ui/modal/ModalIdentity.js",
|
||||
"js/ui/modal/ModalBanCreate.js",
|
||||
"js/ui/modal/ModalBanList.js",
|
||||
"js/ui/modal/ModalYesNo.js",
|
||||
"js/ui/modal/ModalPoke.js",
|
||||
"js/ui/modal/ModalServerGroupDialog.js",
|
||||
"js/ui/modal/ModalKeySelect.js",
|
||||
"js/ui/modal/ModalGroupAssignment.js",
|
||||
"js/ui/modal/permission/ModalPermissionEdit.js",
|
||||
"js/ui/modal/permission/PermissionEditor.js",
|
||||
{url: "js/ui/modal/permission/CanvasPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]},
|
||||
{url: "js/ui/modal/permission/HTMLPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]},
|
||||
|
||||
"js/ui/channel.js",
|
||||
"js/ui/client.js",
|
||||
|
@ -590,6 +666,8 @@ const loader_javascript = {
|
|||
"js/ui/frames/chat_frame.js",
|
||||
"js/ui/frames/connection_handlers.js",
|
||||
"js/ui/frames/server_log.js",
|
||||
"js/ui/frames/hostbanner.js",
|
||||
"js/ui/frames/MenuBar.js",
|
||||
|
||||
//Load permissions
|
||||
"js/permission/PermissionManager.js",
|
||||
|
@ -640,12 +718,11 @@ const loader_javascript = {
|
|||
//Load codec
|
||||
"js/codec/Codec.js",
|
||||
"js/codec/BasicCodec.js",
|
||||
"js/codec/CodecWrapperWorker.js",
|
||||
{url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]},
|
||||
]);
|
||||
},
|
||||
load_scripts_debug_client: async () => {
|
||||
await loader.load_scripts([
|
||||
["js/teaforo.js"]
|
||||
]);
|
||||
},
|
||||
|
||||
|
@ -685,15 +762,18 @@ const loader_style = {
|
|||
await loader.load_styles([
|
||||
"vendor/xbbcode/src/xbbcode.css"
|
||||
]);
|
||||
await loader.load_styles([
|
||||
"vendor/emoji-picker/src/jquery.lsxemojipicker.css"
|
||||
]);
|
||||
await loader.load_styles([
|
||||
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
|
||||
]);
|
||||
|
||||
if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) {
|
||||
await loader_style.load_style_debug();
|
||||
} else {
|
||||
await loader_style.load_style_release();
|
||||
}
|
||||
|
||||
/* the material design */
|
||||
await loader.load_style("css/theme/bootstrap-material-design.css");
|
||||
},
|
||||
|
||||
load_style_debug: async () => {
|
||||
|
@ -706,9 +786,12 @@ const loader_style = {
|
|||
"css/static/ts/tab.css",
|
||||
"css/static/ts/chat.css",
|
||||
"css/static/ts/icons.css",
|
||||
"css/static/ts/icons_em.css",
|
||||
"css/static/ts/country.css",
|
||||
"css/static/general.css",
|
||||
"css/static/modal.css",
|
||||
"css/static/modals.css",
|
||||
"css/static/modal-about.css",
|
||||
"css/static/modal-avatar.css",
|
||||
"css/static/modal-icons.css",
|
||||
"css/static/modal-bookmarks.css",
|
||||
|
@ -722,7 +805,9 @@ const loader_style = {
|
|||
"css/static/modal-settings.css",
|
||||
"css/static/modal-poke.css",
|
||||
"css/static/modal-server.css",
|
||||
"css/static/modal-keyselect.css",
|
||||
"css/static/modal-permissions.css",
|
||||
"css/static/modal-group-assignment.css",
|
||||
"css/static/music/info_plate.css",
|
||||
"css/static/frame/SelectInfo.css",
|
||||
"css/static/control_bar.css",
|
||||
|
@ -730,7 +815,9 @@ const loader_style = {
|
|||
"css/static/frame-chat.css",
|
||||
"css/static/connection_handlers.css",
|
||||
"css/static/server-log.css",
|
||||
"css/static/htmltags.css"
|
||||
"css/static/htmltags.css",
|
||||
"css/static/hostbanner.css",
|
||||
"css/static/menu-bar.css"
|
||||
]);
|
||||
},
|
||||
|
||||
|
@ -740,7 +827,7 @@ const loader_style = {
|
|||
"css/static/main.css",
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function load_templates() {
|
||||
try {
|
||||
|
@ -773,13 +860,9 @@ async function load_templates() {
|
|||
/* test if all files shall be load from cache or fetch again */
|
||||
async function check_updates() {
|
||||
const app_version = (() => {
|
||||
const version_node = document.getElementById("app_version");
|
||||
if(!version_node) return undefined;
|
||||
const version = app.ui_version();
|
||||
|
||||
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
|
||||
if(!version) return undefined;
|
||||
|
||||
if(version == "unknown" || version.replace(/0+/, "").length == 0)
|
||||
if(!version || version == "unknown" || version.replace(/0+/, "").length == 0)
|
||||
return undefined;
|
||||
|
||||
return version;
|
||||
|
@ -960,6 +1043,11 @@ loader.register_task(loader.Stage.LOADED, {
|
|||
},
|
||||
priority: 20
|
||||
});
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "lsx emoji picker setup",
|
||||
function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}),
|
||||
priority: 10
|
||||
});
|
||||
|
||||
|
||||
window["Module"] = window["Module"] || {};
|
||||
|
|
|
@ -14,10 +14,15 @@ let settings: Settings;
|
|||
const js_render = window.jsrender || $;
|
||||
const native_client = window.require !== undefined;
|
||||
|
||||
function getUserMediaFunction() : (constraints: MediaStreamConstraints, success: (stream: MediaStream) => any, fail: (error: any) => any) => any {
|
||||
if((navigator as any).mediaDevices && (navigator as any).mediaDevices.getUserMedia)
|
||||
return (settings, success, fail) => { (navigator as any).mediaDevices.getUserMedia(settings).then(success).catch(fail); };
|
||||
return (navigator as any).getUserMedia || (navigator as any).webkitGetUserMedia || (navigator as any).mozGetUserMedia;
|
||||
function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise<MediaStream> {
|
||||
if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices)
|
||||
return constraints => navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
const _callbacked_function = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
||||
if(!_callbacked_function)
|
||||
return undefined;
|
||||
|
||||
return constraints => new Promise<MediaStream>((resolve, reject) => _callbacked_function(constraints, resolve, reject));
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
@ -36,18 +41,41 @@ function setup_close() {
|
|||
if(!native_client) {
|
||||
event.returnValue = "Are you really sure?<br>You're still connected!";
|
||||
} else {
|
||||
const do_exit = () => {
|
||||
const dp = server_connections.server_connection_handlers().map(e => {
|
||||
if(e.serverConnection.connected())
|
||||
return e.serverConnection.disconnect(tr("client closed"));
|
||||
return Promise.resolve();
|
||||
}).map(e => e.catch(error => {
|
||||
console.warn(tr("Failed to disconnect from server on client close: %o"), e);
|
||||
}));
|
||||
|
||||
const exit = () => {
|
||||
const {remote} = require('electron');
|
||||
remote.getCurrentWindow().close();
|
||||
};
|
||||
|
||||
Promise.all(dp).then(exit);
|
||||
/* force exit after 2500ms */
|
||||
setTimeout(exit, 2500);
|
||||
};
|
||||
if(window.open_connected_question) {
|
||||
event.preventDefault();
|
||||
event.returnValue = "question";
|
||||
window.open_connected_question().then(result => {
|
||||
if(result) {
|
||||
window.onbeforeunload = undefined;
|
||||
/* prevent quitting because we try to disconnect */
|
||||
window.onbeforeunload = e => e.preventDefault();
|
||||
|
||||
const {remote} = require('electron');
|
||||
remote.getCurrentWindow().close();
|
||||
/* allow a force quit after 5 seconds */
|
||||
setTimeout(() => window.onbeforeunload, 5000);
|
||||
do_exit();
|
||||
}
|
||||
});
|
||||
} else { /* we're in debugging mode */ }
|
||||
} else {
|
||||
/* we're in debugging mode */
|
||||
do_exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -102,7 +130,6 @@ async function initialize() {
|
|||
bipc.setup();
|
||||
}
|
||||
|
||||
|
||||
async function initialize_app() {
|
||||
const display_load_error = message => {
|
||||
if(typeof(display_critical_load) !== "undefined")
|
||||
|
@ -112,7 +139,10 @@ async function initialize_app() {
|
|||
};
|
||||
|
||||
try { //Initialize main template
|
||||
const main = $("#tmpl_main").renderTag().dividerfy();
|
||||
const main = $("#tmpl_main").renderTag({
|
||||
multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
|
||||
app_version: app.ui_version()
|
||||
}).dividerfy();
|
||||
|
||||
$("body").append(main);
|
||||
} catch(error) {
|
||||
|
@ -126,7 +156,7 @@ async function initialize_app() {
|
|||
if(!audio.player.initialize())
|
||||
console.warn(tr("Failed to initialize audio controller!"));
|
||||
if(audio.player.set_master_volume)
|
||||
audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER, 1) / 100);
|
||||
audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100);
|
||||
else
|
||||
console.warn("Client does not support audio.player.set_master_volume()... May client is too old?");
|
||||
if(audio.recorder.device_refresh_available())
|
||||
|
@ -138,7 +168,7 @@ async function initialize_app() {
|
|||
sound.initialize().then(() => {
|
||||
console.log(tr("Sounds initialitzed"));
|
||||
});
|
||||
sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS, 1) / 100);
|
||||
sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100);
|
||||
|
||||
await profiles.load();
|
||||
|
||||
|
@ -153,10 +183,6 @@ async function initialize_app() {
|
|||
setup_close();
|
||||
}
|
||||
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||
}
|
||||
|
||||
function str2ab8(str) {
|
||||
const buf = new ArrayBuffer(str.length);
|
||||
const bufView = new Uint8Array(buf);
|
||||
|
@ -177,68 +203,58 @@ function arrayBufferBase64(base64: string) {
|
|||
return buf;
|
||||
}
|
||||
|
||||
function base64ArrayBuffer(arrayBuffer) {
|
||||
var base64 = ''
|
||||
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
function base64_encode_ab(source: ArrayBufferLike) {
|
||||
const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
let base64 = "";
|
||||
|
||||
var bytes = new Uint8Array(arrayBuffer)
|
||||
var byteLength = bytes.byteLength
|
||||
var byteRemainder = byteLength % 3
|
||||
var mainLength = byteLength - byteRemainder
|
||||
const bytes = new Uint8Array(source);
|
||||
const byte_length = bytes.byteLength;
|
||||
const byte_reminder = byte_length % 3;
|
||||
const main_length = byte_length - byte_reminder;
|
||||
|
||||
var a, b, c, d
|
||||
var chunk
|
||||
let a, b, c, d;
|
||||
let chunk;
|
||||
|
||||
// Main loop deals with bytes in chunks of 3
|
||||
for (var i = 0; i < mainLength; i = i + 3) {
|
||||
for (let i = 0; i < main_length; i = i + 3) {
|
||||
// Combine the three bytes into a single integer
|
||||
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
|
||||
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
||||
|
||||
// Use bitmasks to extract 6-bit segments from the triplet
|
||||
a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
|
||||
b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12
|
||||
c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6
|
||||
d = chunk & 63 // 63 = 2^6 - 1
|
||||
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
|
||||
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
|
||||
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
|
||||
d = (chunk & 63) >> 0; // 63 = (2^6 - 1) << 0
|
||||
|
||||
// Convert the raw binary segments to the appropriate ASCII encoding
|
||||
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
|
||||
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
|
||||
}
|
||||
|
||||
// Deal with the remaining bytes and padding
|
||||
if (byteRemainder == 1) {
|
||||
chunk = bytes[mainLength]
|
||||
if (byte_reminder == 1) {
|
||||
chunk = bytes[main_length];
|
||||
|
||||
a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
|
||||
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
|
||||
|
||||
// Set the 4 least significant bits to zero
|
||||
b = (chunk & 3) << 4 // 3 = 2^2 - 1
|
||||
b = (chunk & 3) << 4; // 3 = 2^2 - 1
|
||||
|
||||
base64 += encodings[a] + encodings[b] + '=='
|
||||
} else if (byteRemainder == 2) {
|
||||
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
|
||||
base64 += encodings[a] + encodings[b] + '==';
|
||||
} else if (byte_reminder == 2) {
|
||||
chunk = (bytes[main_length] << 8) | bytes[main_length + 1];
|
||||
|
||||
a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
|
||||
b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4
|
||||
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
|
||||
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
|
||||
|
||||
// Set the 2 least significant bits to zero
|
||||
c = (chunk & 15) << 2 // 15 = 2^4 - 1
|
||||
c = (chunk & 15) << 2; // 15 = 2^4 - 1
|
||||
|
||||
base64 += encodings[a] + encodings[b] + encodings[c] + '='
|
||||
base64 += encodings[a] + encodings[b] + encodings[c] + '=';
|
||||
}
|
||||
|
||||
return base64
|
||||
}
|
||||
|
||||
function Base64EncodeUrl(str){
|
||||
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
|
||||
}
|
||||
|
||||
function Base64DecodeUrl(str: string, pad?: boolean){
|
||||
if(typeof(pad) === 'undefined' || pad)
|
||||
str = (str + '===').slice(0, str.length + (str.length % 4));
|
||||
return str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
}
|
||||
|
||||
/*
|
||||
class TestProxy extends bipc.MethodProxy {
|
||||
constructor(params: bipc.MethodProxyConnectParameters) {
|
||||
|
@ -282,7 +298,6 @@ interface Window {
|
|||
}
|
||||
*/
|
||||
|
||||
|
||||
function main() {
|
||||
/*
|
||||
window.proxy_instance = new TestProxy({
|
||||
|
@ -299,6 +314,23 @@ function main() {
|
|||
*/
|
||||
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
|
||||
|
||||
/* initialize font */
|
||||
{
|
||||
const font = settings.static_global(Settings.KEY_FONT_SIZE, parseInt(getComputedStyle(document.body).fontSize));
|
||||
$(document.body).css("font-size", font + "px");
|
||||
}
|
||||
|
||||
/* context menu prevent */
|
||||
$(document).on('contextmenu', event => {
|
||||
if(event.isDefaultPrevented())
|
||||
return;
|
||||
|
||||
if(!settings.static_global(Settings.KEY_DISABLE_GLOBAL_CONTEXT_MENU))
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
top_menu.initialize();
|
||||
|
||||
server_connections = new ServerConnectionManager($("#connection-handlers"));
|
||||
control_bar.initialise(); /* before connection handler to allow property apply */
|
||||
|
||||
|
@ -306,7 +338,7 @@ function main() {
|
|||
initial_handler.acquire_recorder(default_recorder, false);
|
||||
control_bar.set_connection_handler(initial_handler);
|
||||
/** Setup the XF forum identity **/
|
||||
profiles.identities.setup_forum();
|
||||
profiles.identities.update_forum();
|
||||
|
||||
let _resize_timeout: NodeJS.Timer;
|
||||
$(window).on('resize', event => {
|
||||
|
@ -334,11 +366,6 @@ function main() {
|
|||
console.log("Received user count update: %o", status);
|
||||
});
|
||||
|
||||
/*
|
||||
setTimeout(() => {
|
||||
Modals.spawnAvatarList(globalClient);
|
||||
}, 1000);
|
||||
*/
|
||||
(<any>window).test_upload = (message?: string) => {
|
||||
message = message || "Hello World";
|
||||
|
||||
|
@ -366,16 +393,6 @@ function main() {
|
|||
};
|
||||
|
||||
server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]);
|
||||
const convs = server_connections.active_connection_handler().chat_frame.private_conversations();
|
||||
let conv = convs.create_conversation("xxxx0", "WolverinDEV");
|
||||
conv = convs.create_conversation("xxxx1", "Darkatzu");
|
||||
conv = convs.create_conversation("xxxx2", "ZameXxX");
|
||||
conv.set_unread_flag(true);
|
||||
|
||||
conv = convs.create_conversation("xxxx3", "Vagur");
|
||||
|
||||
//for(let i = 0; i < 100; i++)
|
||||
// convs.create_conversation('xx' + i, "WolverinDEV #" + i);
|
||||
|
||||
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
|
||||
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
|
||||
|
@ -389,7 +406,7 @@ function main() {
|
|||
|
||||
if(profile && profile.valid()) {
|
||||
const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler();
|
||||
connection.startConnection(address, profile, {
|
||||
connection.startConnection(address, profile, true, {
|
||||
nickname: username,
|
||||
password: password.length > 0 ? {
|
||||
password: password,
|
||||
|
@ -397,7 +414,7 @@ function main() {
|
|||
} : undefined
|
||||
});
|
||||
} else {
|
||||
Modals.spawnConnectModal({
|
||||
Modals.spawnConnectModal({},{
|
||||
url: address,
|
||||
enforce: true
|
||||
}, {
|
||||
|
@ -406,6 +423,18 @@ function main() {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const connection = server_connections.active_connection_handler();
|
||||
/*
|
||||
Modals.createChannelModal(connection, undefined, undefined, connection.permissions, (cb, perms) => {
|
||||
|
||||
});
|
||||
*/
|
||||
//Modals.createServerModal(connection.channelTree.server, properties => Promise.resolve());
|
||||
}, 1000);
|
||||
//Modals.spawnSettingsModal("audio-sounds");
|
||||
//Modals.spawnKeySelect(console.log);
|
||||
}
|
||||
|
||||
const task_teaweb_starter: loader.Task = {
|
||||
|
|
|
@ -55,7 +55,11 @@ class Group {
|
|||
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
|
||||
client.updateGroupIcon(this);
|
||||
});
|
||||
}
|
||||
} else if(key == "sortid")
|
||||
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
|
||||
client.update_group_icon_order();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,6 +77,12 @@ class GroupManager extends connection.AbstractCommandHandler {
|
|||
this.handle = client;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.handle.serverConnection && this.handle.serverConnection.command_handler_boss().unregister_handler(this);
|
||||
this.serverGroups = undefined;
|
||||
this.channelGroups = undefined;
|
||||
}
|
||||
|
||||
handle_command(command: connection.ServerCommand): boolean {
|
||||
switch (command.command) {
|
||||
case "notifyservergrouplist":
|
||||
|
@ -94,6 +104,11 @@ class GroupManager extends connection.AbstractCommandHandler {
|
|||
|
||||
static sorter() : (a: Group, b: Group) => number {
|
||||
return (a, b) => {
|
||||
if(!a)
|
||||
return b ? 1 : 0;
|
||||
if(!b)
|
||||
return a ? -1 : 0;
|
||||
|
||||
if(a.properties.sortid > b.properties.sortid)
|
||||
return 1;
|
||||
if(a.properties.sortid < b.properties.sortid)
|
||||
|
|
|
@ -396,8 +396,6 @@ class PermissionValue {
|
|||
}
|
||||
|
||||
class NeededPermissionValue extends PermissionValue {
|
||||
changeListener: ((newValue: number) => void)[] = [];
|
||||
|
||||
constructor(type, value) {
|
||||
super(type, value);
|
||||
}
|
||||
|
@ -424,6 +422,8 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
|||
permissionGroups: PermissionGroup[] = [];
|
||||
neededPermissions: NeededPermissionValue[] = [];
|
||||
|
||||
needed_permission_change_listener: {[permission: string]:(() => any)[]} = {};
|
||||
|
||||
requests_channel_permissions: ChannelPermissionRequest[] = [];
|
||||
requests_client_permissions: TeaPermissionRequest[] = [];
|
||||
requests_client_channel_permissions: TeaPermissionRequest[] = [];
|
||||
|
@ -515,6 +515,24 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
|||
this.handle = client;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.handle.serverConnection && this.handle.serverConnection.command_handler_boss().unregister_handler(this);
|
||||
this.needed_permission_change_listener = {};
|
||||
|
||||
this.permissionList = undefined;
|
||||
this.permissionGroups = undefined;
|
||||
|
||||
this.neededPermissions = undefined;
|
||||
|
||||
this.requests_channel_permissions = undefined;
|
||||
this.requests_client_permissions = undefined;
|
||||
this.requests_client_channel_permissions = undefined;
|
||||
this.requests_playlist_permissions = undefined;
|
||||
|
||||
this.initializedListener = undefined;
|
||||
this._cacheNeededPermissions = undefined;
|
||||
}
|
||||
|
||||
handle_command(command: connection.ServerCommand): boolean {
|
||||
switch (command.command) {
|
||||
case "notifyclientneededpermissions":
|
||||
|
@ -631,8 +649,8 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
|||
if(entry.value == parseInt(e["permvalue"])) continue;
|
||||
entry.value = parseInt(e["permvalue"]);
|
||||
|
||||
for(let listener of entry.changeListener)
|
||||
listener(entry.value);
|
||||
for(const listener of this.needed_permission_change_listener[entry.type.name] || [])
|
||||
listener();
|
||||
|
||||
table_entries.push({
|
||||
"permission": entry.type.name,
|
||||
|
@ -643,15 +661,28 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
|||
log.table("Needed client permissions", table_entries);
|
||||
group.end();
|
||||
|
||||
//TODO tr
|
||||
log.debug(LogCategory.PERMISSIONS, "Dropping " + copy.length + " needed permissions and added " + addcount + " permissions.");
|
||||
log.debug(LogCategory.PERMISSIONS, tr("Dropping %o needed permissions and added %o permissions."), copy.length, addcount);
|
||||
for(let e of copy) {
|
||||
e.value = -2;
|
||||
for(let listener of e.changeListener)
|
||||
listener(e.value);
|
||||
for(const listener of this.needed_permission_change_listener[e.type.name] || [])
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
register_needed_permission(key: PermissionType, listener: () => any) {
|
||||
const array = this.needed_permission_change_listener[key] || [];
|
||||
array.push(listener);
|
||||
this.needed_permission_change_listener[key] = array;
|
||||
}
|
||||
|
||||
unregister_needed_permission(key: PermissionType, listener: () => any) {
|
||||
const array = this.needed_permission_change_listener[key];
|
||||
if(!array) return;
|
||||
|
||||
array.remove(listener);
|
||||
this.needed_permission_change_listener[key] = array.length > 0 ? array : undefined;
|
||||
}
|
||||
|
||||
private onChannelPermList(json) {
|
||||
let channelId: number = parseInt(json[0]["cid"]);
|
||||
|
||||
|
@ -780,7 +811,7 @@ class PermissionManager extends connection.AbstractCommandHandler {
|
|||
return request.promise;
|
||||
}
|
||||
|
||||
neededPermission(key: number | string | PermissionType | PermissionInfo) : PermissionValue {
|
||||
neededPermission(key: number | string | PermissionType | PermissionInfo) : NeededPermissionValue {
|
||||
for(let perm of this.neededPermissions)
|
||||
if(perm.type.id == key || perm.type.name == key || perm.type == key)
|
||||
return perm;
|
||||
|
|
|
@ -66,7 +66,7 @@ namespace profiles {
|
|||
const identity = this.selected_identity();
|
||||
if(!identity || !identity.valid()) return false;
|
||||
|
||||
return this.default_username !== undefined;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ namespace profiles.identities {
|
|||
}
|
||||
|
||||
valid(): boolean {
|
||||
return this._name != undefined && this._name.length >= 3;
|
||||
return this._name != undefined && this._name.length >= 5;
|
||||
}
|
||||
|
||||
decode(data) : Promise<void> {
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace profiles.identities {
|
|||
this.connection.send_command("handshakebegin", {
|
||||
intention: 0,
|
||||
authentication_method: this.identity.type(),
|
||||
data: this.identity.data_json()
|
||||
data: this.identity.data().data_json()
|
||||
}).catch(error => {
|
||||
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
|
||||
|
||||
|
@ -30,7 +30,7 @@ namespace profiles.identities {
|
|||
|
||||
private handle_proof(json) {
|
||||
this.connection.send_command("handshakeindentityproof", {
|
||||
proof: this.identity.data_sign()
|
||||
proof: this.identity.data().data_sign()
|
||||
}).catch(error => {
|
||||
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
|
||||
|
||||
|
@ -52,73 +52,50 @@ namespace profiles.identities {
|
|||
}
|
||||
|
||||
export class TeaForumIdentity implements Identity {
|
||||
private identity_data: string;
|
||||
private identity_data_raw: string;
|
||||
private identity_data_sign: string;
|
||||
private readonly identity_data: forum.Data;
|
||||
|
||||
valid() : boolean {
|
||||
return this.identity_data_raw.length > 0 && this.identity_data_raw.length > 0 && this.identity_data_sign.length > 0;
|
||||
return !!this.identity_data && !this.identity_data.is_expired();
|
||||
}
|
||||
|
||||
constructor(data: string, sign: string) {
|
||||
this.identity_data_raw = data;
|
||||
this.identity_data_sign = sign;
|
||||
try {
|
||||
this.identity_data = data ? JSON.parse(this.identity_data_raw) : undefined;
|
||||
} catch(error) { }
|
||||
constructor(data: forum.Data) {
|
||||
this.identity_data = data;
|
||||
}
|
||||
|
||||
data_json() : string { return this.identity_data_raw; }
|
||||
data_sign() : string { return this.identity_data_sign; }
|
||||
|
||||
name() : string { return this.identity_data["user_name"]; }
|
||||
uid() : string { return "TeaForo#" + this.identity_data["user_id"]; }
|
||||
type() : IdentitifyType { return IdentitifyType.TEAFORO; }
|
||||
|
||||
forum_user_id() { return this.identity_data["user_id"]; }
|
||||
forum_user_group() { return this.identity_data["user_group_id"]; }
|
||||
is_stuff() : boolean { return this.identity_data["is_staff"]; }
|
||||
is_premium() : boolean { return (<number[]>this.identity_data["user_groups"]).indexOf(5) != -1; }
|
||||
data_age() : Date { return new Date(this.identity_data["data_age"]); }
|
||||
|
||||
/*
|
||||
$user_data["user_id"] = $user->user_id;
|
||||
$user_data["user_name"] = $user->username;
|
||||
$user_data["user_group"] = $user->user_group_id;
|
||||
$user_data["user_groups"] = $user->secondary_group_ids;
|
||||
|
||||
$user_data["trophy_points"] = $user->trophy_points;
|
||||
$user_data["register_date"] = $user->register_date;
|
||||
$user_data["is_staff"] = $user->is_staff;
|
||||
$user_data["is_admin"] = $user->is_admin;
|
||||
$user_data["is_super_admin"] = $user->is_super_admin;
|
||||
$user_data["is_banned"] = $user->is_banned;
|
||||
|
||||
$user_data["data_age"] = milliseconds();
|
||||
*/
|
||||
data() : forum.Data {
|
||||
return this.identity_data;
|
||||
}
|
||||
|
||||
decode(data) : Promise<void> {
|
||||
data = JSON.parse(data);
|
||||
if(data.version !== 1)
|
||||
throw "invalid version";
|
||||
|
||||
this.identity_data_raw = data["identity_data"];
|
||||
this.identity_data_sign = data["identity_sign"];
|
||||
this.identity_data = JSON.parse(this.identity_data);
|
||||
return;
|
||||
}
|
||||
|
||||
encode?() : string {
|
||||
encode() : string {
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
identity_data: this.identity_data_raw,
|
||||
identity_sign: this.identity_data_sign
|
||||
version: 1
|
||||
});
|
||||
}
|
||||
|
||||
spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler {
|
||||
return new TeaForumHandshakeHandler(connection, this);
|
||||
}
|
||||
|
||||
name(): string {
|
||||
return (this.identity_data ? this.identity_data.name() : "Another TeaSpeak user");
|
||||
}
|
||||
|
||||
type(): profiles.identities.IdentitifyType {
|
||||
return IdentitifyType.TEAFORO;
|
||||
}
|
||||
|
||||
uid(): string {
|
||||
//FIXME: Real UID!
|
||||
return "TeaForo#" + ((this.identity_data ? this.identity_data.name() : "Another TeaSpeak user"));
|
||||
}
|
||||
}
|
||||
|
||||
let static_identity: TeaForumIdentity;
|
||||
|
@ -127,12 +104,10 @@ namespace profiles.identities {
|
|||
static_identity = identity;
|
||||
}
|
||||
|
||||
export function setup_forum() {
|
||||
const user_data = settings.static("forum_user_data") as string;
|
||||
const user_sign = settings.static("forum_user_sign") as string;
|
||||
|
||||
if(user_data && user_sign)
|
||||
static_identity = new TeaForumIdentity(user_data, user_sign);
|
||||
export function update_forum() {
|
||||
if(forum.logged_in() && (!static_identity || static_identity.data() !== forum.data())) {
|
||||
static_identity = new TeaForumIdentity(forum.data());
|
||||
}
|
||||
}
|
||||
|
||||
export function valid_static_forum_identity() : boolean {
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
|
||||
namespace profiles.identities {
|
||||
export namespace CryptoHelper {
|
||||
export function base64_url_encode(str){
|
||||
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
|
||||
}
|
||||
|
||||
export function base64_url_decode(str: string, pad?: boolean){
|
||||
if(typeof(pad) === 'undefined' || pad)
|
||||
str = (str + '===').slice(0, str.length + (str.length % 4));
|
||||
return str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
}
|
||||
|
||||
export function arraybuffer_to_string(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||
}
|
||||
|
||||
export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) {
|
||||
/*
|
||||
Tomcrypt public key export:
|
||||
|
@ -50,7 +64,7 @@ namespace profiles.identities {
|
|||
buffer[index++] = 0x02; /* type */
|
||||
buffer[index++] = 0x20; /* length */
|
||||
|
||||
const raw = atob(Base64DecodeUrl(key_data.x, false));
|
||||
const raw = atob(base64_url_decode(key_data.x, false));
|
||||
if(raw.charCodeAt(0) > 0x7F) {
|
||||
buffer[index - 1] += 1;
|
||||
buffer[index++] = 0;
|
||||
|
@ -68,7 +82,7 @@ namespace profiles.identities {
|
|||
buffer[index++] = 0x02; /* type */
|
||||
buffer[index++] = 0x20; /* length */
|
||||
|
||||
const raw = atob(Base64DecodeUrl(key_data.y, false));
|
||||
const raw = atob(base64_url_decode(key_data.y, false));
|
||||
if(raw.charCodeAt(0) > 0x7F) {
|
||||
buffer[index - 1] += 1;
|
||||
buffer[index++] = 0;
|
||||
|
@ -87,7 +101,7 @@ namespace profiles.identities {
|
|||
buffer[index++] = 0x02; /* type */
|
||||
buffer[index++] = 0x20; /* length */
|
||||
|
||||
const raw = atob(Base64DecodeUrl(key_data.d, false));
|
||||
const raw = atob(base64_url_decode(key_data.d, false));
|
||||
if(raw.charCodeAt(0) > 0x7F) {
|
||||
buffer[index - 1] += 1;
|
||||
buffer[index++] = 0;
|
||||
|
@ -104,7 +118,7 @@ namespace profiles.identities {
|
|||
|
||||
buffer[1] = index - 2; /* set the final sequence length */
|
||||
|
||||
return base64ArrayBuffer(buffer.buffer.slice(0, index));
|
||||
return base64_encode_ab(buffer.buffer.slice(0, index));
|
||||
}
|
||||
|
||||
const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e";
|
||||
|
@ -125,7 +139,7 @@ namespace profiles.identities {
|
|||
for(let i = 0; i < length; i++)
|
||||
buffer[i] ^= crypt_key.charCodeAt(i);
|
||||
|
||||
return ab2str(buffer);
|
||||
return arraybuffer_to_string(buffer);
|
||||
}
|
||||
|
||||
export async function encrypt_ts_identity(buffer: Uint8Array) : Promise<string> {
|
||||
|
@ -137,7 +151,7 @@ namespace profiles.identities {
|
|||
for(let i = 0; i < 20; i++)
|
||||
buffer[i] ^= hash[i];
|
||||
|
||||
return base64ArrayBuffer(buffer);
|
||||
return base64_encode_ab(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -185,9 +199,9 @@ namespace profiles.identities {
|
|||
*/
|
||||
return {
|
||||
crv: "P-256",
|
||||
d: Base64EncodeUrl(btoa(k)),
|
||||
x: Base64EncodeUrl(btoa(x)),
|
||||
y: Base64EncodeUrl(btoa(y)),
|
||||
d: base64_url_encode(btoa(k)),
|
||||
x: base64_url_encode(btoa(x)),
|
||||
y: base64_url_encode(btoa(y)),
|
||||
|
||||
ext: true,
|
||||
key_ops:["deriveKey", "sign"],
|
||||
|
@ -587,7 +601,7 @@ namespace profiles.identities {
|
|||
if(carry)
|
||||
char_result.push(49);
|
||||
|
||||
return String.fromCharCode.apply(null, char_result.reverse());
|
||||
return String.fromCharCode.apply(null, char_result.slice().reverse());
|
||||
}
|
||||
|
||||
|
||||
|
@ -774,7 +788,7 @@ namespace profiles.identities {
|
|||
|
||||
try {
|
||||
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
|
||||
this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key));
|
||||
this._unique_id = base64_encode_ab(await sha.sha1(this.public_key));
|
||||
} catch(error) {
|
||||
log.error(LogCategory.IDENTITIES, error);
|
||||
throw "failed to calculate unique id";
|
||||
|
@ -840,7 +854,7 @@ namespace profiles.identities {
|
|||
}
|
||||
buffer[1] = index - 2;
|
||||
|
||||
return base64ArrayBuffer(buffer.subarray(0, index));
|
||||
return base64_encode_ab(buffer.subarray(0, index));
|
||||
}
|
||||
|
||||
spawn_identity_handshake_handler(connection: connection.AbstractServerConnection): connection.HandshakeIdentityHandler {
|
||||
|
|
|
@ -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>;
|
||||
modal(properties: any) : this;
|
||||
bootstrapMaterialDesign() : this;
|
||||
|
||||
/* first element which matches the selector, could be the element itself or a parent */
|
||||
firstParent(selector: string) : JQuery;
|
||||
}
|
||||
|
||||
interface JQueryStatic<TElement extends Node = HTMLElement> {
|
||||
|
@ -184,6 +187,12 @@ if(typeof ($) !== "undefined") {
|
|||
this.attr("style", original_style || "");
|
||||
return result;
|
||||
}
|
||||
if(!$.fn.firstParent)
|
||||
$.fn.firstParent = function (this: JQuery<HTMLElement>, selector: string) {
|
||||
if(this.is(selector))
|
||||
return this;
|
||||
return this.parent(selector);
|
||||
}
|
||||
}
|
||||
|
||||
if (!String.prototype.format) {
|
||||
|
@ -247,8 +256,39 @@ function calculate_width(text: string) : number {
|
|||
return size;
|
||||
}
|
||||
|
||||
interface Twemoji {
|
||||
parse(message: string) : string;
|
||||
}
|
||||
declare let twemoji: Twemoji;
|
||||
|
||||
interface HighlightJS {
|
||||
listLanguages() : string[];
|
||||
getLanguage(name: string) : any | undefined;
|
||||
|
||||
highlight(language: string, text: string, ignore_illegals?: boolean) : HighlightJSResult;
|
||||
highlightAuto(text: string) : HighlightJSResult;
|
||||
}
|
||||
|
||||
interface HighlightJSResult {
|
||||
language: string;
|
||||
relevance: number;
|
||||
|
||||
value: string;
|
||||
second_best?: any;
|
||||
}
|
||||
|
||||
interface DOMPurify {
|
||||
sanitize(html: string, config?: {
|
||||
ADD_ATTR?: string[]
|
||||
}) : string;
|
||||
}
|
||||
declare let DOMPurify: DOMPurify;
|
||||
|
||||
declare let remarkable: typeof window.remarkable;
|
||||
|
||||
declare class webkitAudioContext extends AudioContext {}
|
||||
declare class webkitOfflineAudioContext extends OfflineAudioContext {}
|
||||
|
||||
interface Window {
|
||||
readonly webkitAudioContext: typeof webkitAudioContext;
|
||||
readonly AudioContext: typeof webkitAudioContext;
|
||||
|
@ -258,6 +298,10 @@ interface Window {
|
|||
readonly Pointer_stringify: any;
|
||||
readonly jsrender: any;
|
||||
|
||||
twemoji: Twemoji;
|
||||
hljs: HighlightJS;
|
||||
remarkable: any;
|
||||
|
||||
require(id: string): any;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ interface SettingsKey<T> {
|
|||
fallback_imports?: {[key: string]:(value: string) => T};
|
||||
description?: string;
|
||||
default_value?: T;
|
||||
|
||||
require_restart?: boolean;
|
||||
}
|
||||
|
||||
class SettingsBase {
|
||||
|
@ -144,6 +146,13 @@ class Settings extends StaticSettings {
|
|||
key: 'disableContextMenu',
|
||||
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier'
|
||||
};
|
||||
|
||||
static readonly KEY_DISABLE_GLOBAL_CONTEXT_MENU: SettingsKey<boolean> = {
|
||||
key: 'disableGlobalContextMenu',
|
||||
description: 'Disable the general context menu prevention',
|
||||
default_value: false
|
||||
};
|
||||
|
||||
static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey<boolean> = {
|
||||
key: 'disableUnloadDialog',
|
||||
description: 'Disables the unload popup on side closing'
|
||||
|
@ -154,6 +163,8 @@ class Settings extends StaticSettings {
|
|||
};
|
||||
static readonly KEY_DISABLE_MULTI_SESSION: SettingsKey<boolean> = {
|
||||
key: 'disableMultiSession',
|
||||
default_value: false,
|
||||
require_restart: true
|
||||
};
|
||||
|
||||
static readonly KEY_LOAD_DUMMY_ERROR: SettingsKey<boolean> = {
|
||||
|
@ -194,6 +205,9 @@ class Settings extends StaticSettings {
|
|||
static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey<boolean> = {
|
||||
key: 'connect_password_hashed'
|
||||
};
|
||||
static readonly KEY_CONNECT_HISTORY: SettingsKey<string> = {
|
||||
key: 'connect_history'
|
||||
};
|
||||
|
||||
static readonly KEY_CERTIFICATE_CALLBACK: SettingsKey<string> = {
|
||||
key: 'certificate_callback'
|
||||
|
@ -201,11 +215,82 @@ class Settings extends StaticSettings {
|
|||
|
||||
/* sounds */
|
||||
static readonly KEY_SOUND_MASTER: SettingsKey<number> = {
|
||||
key: 'audio_master_volume'
|
||||
key: 'audio_master_volume',
|
||||
default_value: 100
|
||||
};
|
||||
|
||||
static readonly KEY_SOUND_MASTER_SOUNDS: SettingsKey<number> = {
|
||||
key: 'audio_master_volume_sounds'
|
||||
key: 'audio_master_volume_sounds',
|
||||
default_value: 100
|
||||
};
|
||||
|
||||
static readonly KEY_CHAT_FIXED_TIMESTAMPS: SettingsKey<boolean> = {
|
||||
key: 'chat_fixed_timestamps',
|
||||
default_value: false,
|
||||
description: 'Enables fixed timestamps for chat messages and disabled the updating once (2 seconds ago... etc)'
|
||||
};
|
||||
|
||||
static readonly KEY_CHAT_COLLOQUIAL_TIMESTAMPS: SettingsKey<boolean> = {
|
||||
key: 'chat_colloquial_timestamps',
|
||||
default_value: true,
|
||||
description: 'Enabled colloquial timestamp formatting like "Yesterday at ..." or "Today at ..."'
|
||||
};
|
||||
|
||||
static readonly KEY_CHAT_COLORED_EMOJIES: SettingsKey<boolean> = {
|
||||
key: 'chat_colored_emojies',
|
||||
default_value: true,
|
||||
description: 'Enables colored emojies powered by Twemoji'
|
||||
};
|
||||
|
||||
static readonly KEY_CHAT_TAG_URLS: SettingsKey<boolean> = {
|
||||
key: 'chat_tag_urls',
|
||||
default_value: true,
|
||||
description: 'Automatically link urls with [url]'
|
||||
};
|
||||
|
||||
static readonly KEY_CHAT_ENABLE_MARKDOWN: SettingsKey<boolean> = {
|
||||
key: 'chat_enable_markdown',
|
||||
default_value: true,
|
||||
description: 'Enabled markdown chat support.'
|
||||
};
|
||||
|
||||
static readonly KEY_CHAT_ENABLE_BBCODE: SettingsKey<boolean> = {
|
||||
key: 'chat_enable_bbcode',
|
||||
default_value: true,
|
||||
description: 'Enabled bbcode support in chat.'
|
||||
};
|
||||
|
||||
static readonly KEY_SWITCH_INSTANT_CHAT: SettingsKey<boolean> = {
|
||||
key: 'switch_instant_chat',
|
||||
default_value: true,
|
||||
description: 'Directly switch to channel chat on channel select'
|
||||
};
|
||||
|
||||
static readonly KEY_SWITCH_INSTANT_CLIENT: SettingsKey<boolean> = {
|
||||
key: 'switch_instant_client',
|
||||
default_value: true,
|
||||
description: 'Directly switch to client info on client select'
|
||||
};
|
||||
|
||||
static readonly KEY_HOSTBANNER_BACKGROUND: SettingsKey<boolean> = {
|
||||
key: 'hostbanner_background',
|
||||
default_value: false,
|
||||
description: 'Enables a default background begind the hostbanner'
|
||||
};
|
||||
|
||||
static readonly KEY_CHANNEL_EDIT_ADVANCED: SettingsKey<boolean> = {
|
||||
key: 'channel_edit_advanced',
|
||||
default_value: false,
|
||||
description: 'Edit channels in advanced mode with a lot more settings'
|
||||
};
|
||||
|
||||
static readonly KEY_TEAFORO_URL: SettingsKey<string> = {
|
||||
key: "teaforo_url",
|
||||
default_value: "https://forum.teaspeak.de/"
|
||||
};
|
||||
|
||||
static readonly KEY_FONT_SIZE: SettingsKey<number> = {
|
||||
key: "font_size"
|
||||
};
|
||||
|
||||
static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel: ChannelEntry) => SettingsKey<ChannelSubscribeMode> = channel => {
|
||||
|
@ -250,14 +335,17 @@ class Settings extends StaticSettings {
|
|||
}
|
||||
|
||||
static_global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||
const actual_default = typeof(_default) === "undefined" && typeof(key) === "object" && 'default_value' in key ? key.default_value : _default;
|
||||
|
||||
const default_object = { seed: Math.random() } as any;
|
||||
let _static = this.static(key, default_object, typeof _default);
|
||||
if(_static !== default_object) return StaticSettings.transformStO(_static, _default);
|
||||
return this.global<T>(key, _default);
|
||||
if(_static !== default_object) return StaticSettings.transformStO(_static, actual_default);
|
||||
return this.global<T>(key, actual_default);
|
||||
}
|
||||
|
||||
global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheGlobal[key]);
|
||||
const actual_default = typeof(_default) === "undefined" && typeof(key) === "object" && 'default_value' in key ? key.default_value : _default;
|
||||
return StaticSettings.resolveKey(Settings.keyify(key), actual_default, key => this.cacheGlobal[key]);
|
||||
}
|
||||
|
||||
changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
|
||||
|
@ -287,6 +375,7 @@ class ServerSettings extends SettingsBase {
|
|||
private currentServer: ServerEntry;
|
||||
private _server_save_worker: NodeJS.Timer;
|
||||
private _server_settings_updated: boolean = false;
|
||||
private _destroyed = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -296,11 +385,23 @@ class ServerSettings extends SettingsBase {
|
|||
}, 5 * 1000);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroyed = true;
|
||||
|
||||
this.currentServer = undefined;
|
||||
this.cacheServer = undefined;
|
||||
|
||||
clearInterval(this._server_save_worker);
|
||||
this._server_save_worker = undefined;
|
||||
}
|
||||
|
||||
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||
if(this._destroyed) throw "destroyed";
|
||||
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
|
||||
}
|
||||
|
||||
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
|
||||
if(this._destroyed) throw "destroyed";
|
||||
key = Settings.keyify(key);
|
||||
|
||||
if(this.cacheServer[key.key] == value) return;
|
||||
|
@ -313,6 +414,7 @@ class ServerSettings extends SettingsBase {
|
|||
}
|
||||
|
||||
setServer(server: ServerEntry) {
|
||||
if(this._destroyed) throw "destroyed";
|
||||
if(this.currentServer) {
|
||||
this.save();
|
||||
this.cacheServer = {};
|
||||
|
@ -329,6 +431,7 @@ class ServerSettings extends SettingsBase {
|
|||
}
|
||||
|
||||
save() {
|
||||
if(this._destroyed) throw "destroyed";
|
||||
this._server_settings_updated = false;
|
||||
|
||||
if(this.currentServer) {
|
||||
|
|
|
@ -5,6 +5,12 @@ enum Sound {
|
|||
AWAY_ACTIVATED = "away_activated",
|
||||
AWAY_DEACTIVATED = "away_deactivated",
|
||||
|
||||
MICROPHONE_MUTED = "microphone.muted",
|
||||
MICROPHONE_ACTIVATED = "microphone.activated",
|
||||
|
||||
SOUND_MUTED = "sound.muted",
|
||||
SOUND_ACTIVATED = "sound.activated",
|
||||
|
||||
CONNECTION_CONNECTED = "connection.connected",
|
||||
CONNECTION_DISCONNECTED = "connection.disconnected",
|
||||
CONNECTION_BANNED = "connection.banned",
|
||||
|
@ -155,23 +161,22 @@ namespace sound {
|
|||
const data: any = {};
|
||||
data.version = 1;
|
||||
|
||||
for(const sound in Sound) {
|
||||
if(typeof(speech_volume[sound]) !== "undefined")
|
||||
data[sound] = speech_volume[sound];
|
||||
for(const key in Sound) {
|
||||
if(typeof(speech_volume[Sound[key]]) !== "undefined")
|
||||
data[Sound[key]] = speech_volume[Sound[key]];
|
||||
}
|
||||
data.master = master_volume;
|
||||
data.overlap = overlap_sounds;
|
||||
data.ignore_muted = ignore_muted;
|
||||
|
||||
settings.changeGlobal("sound_volume", JSON.stringify(data));
|
||||
console.error(data);
|
||||
}
|
||||
}
|
||||
|
||||
export function initialize() : Promise<void> {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function(jqXHR,settings){
|
||||
if (settings.dataType === 'binary'){
|
||||
if (settings.dataType === 'binary') {
|
||||
settings.xhr().responseType = 'arraybuffer';
|
||||
settings.processData = false;
|
||||
}
|
||||
|
@ -181,12 +186,11 @@ namespace sound {
|
|||
/* volumes */
|
||||
{
|
||||
const data = JSON.parse(settings.static_global("sound_volume", "{}"));
|
||||
for(const sound in Sound) {
|
||||
if(typeof(data[sound]) !== "undefined")
|
||||
speech_volume[sound] = data[sound];
|
||||
for(const sound_key in Sound) {
|
||||
if(typeof(data[Sound[sound_key]]) !== "undefined")
|
||||
speech_volume[Sound[sound_key]] = data[Sound[sound_key]];
|
||||
}
|
||||
|
||||
console.error(data);
|
||||
master_volume = data.master || 1;
|
||||
overlap_sounds = data.overlap || true;
|
||||
ignore_muted = data.ignore_muted || true;
|
||||
|
@ -223,6 +227,8 @@ namespace sound {
|
|||
ignore_overlap?: boolean;
|
||||
|
||||
default_volume?: number;
|
||||
|
||||
callback?: (flag: boolean) => any;
|
||||
}
|
||||
|
||||
export async function resolve_sound(sound: Sound) : Promise<SoundHandle> {
|
||||
|
@ -358,6 +364,8 @@ namespace sound {
|
|||
|
||||
handle.replaying = true;
|
||||
player.onended = event => {
|
||||
if(options.callback)
|
||||
options.callback(true);
|
||||
delete this._playing_sounds[_sound];
|
||||
};
|
||||
|
||||
|
@ -375,11 +383,24 @@ namespace sound {
|
|||
}
|
||||
} else if(handle.node) {
|
||||
handle.node.currentTime = 0;
|
||||
handle.node.play();
|
||||
handle.node.play().then(() => {
|
||||
if(options.callback)
|
||||
options.callback(true);
|
||||
}).catch(error => {
|
||||
console.warn(tr("Sound playback for sound %o resulted in an error: %o"), sound, error);
|
||||
if(options.callback)
|
||||
options.callback(false);
|
||||
});
|
||||
} else {
|
||||
console.warn(tr("Failed to replay sound because of missing handles."), sound);
|
||||
console.warn(tr("Failed to replay sound %o because of missing handles."), sound);
|
||||
if(options.callback)
|
||||
options.callback(false);
|
||||
return;
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn(tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error);
|
||||
if(options.callback)
|
||||
options.callback(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@ class ChannelProperties {
|
|||
|
||||
//Only after request
|
||||
channel_description: string = "";
|
||||
|
||||
channel_flag_conversation_private: boolean = false;
|
||||
}
|
||||
|
||||
class ChannelEntry {
|
||||
|
@ -70,6 +72,7 @@ class ChannelEntry {
|
|||
private _tag_siblings: JQuery<HTMLElement>; /* container for all sub channels */
|
||||
private _tag_clients: JQuery<HTMLElement>; /* container for all clients */
|
||||
private _tag_channel: JQuery<HTMLElement>; /* container for the channel info itself */
|
||||
private _destroyed = false;
|
||||
|
||||
private _cachedPassword: string;
|
||||
private _cached_channel_description: string = undefined;
|
||||
|
@ -91,6 +94,26 @@ class ChannelEntry {
|
|||
this.__updateChannelName();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroyed = true;
|
||||
if(this._tag_root) {
|
||||
this._tag_root.remove(); /* removes also all other tags */
|
||||
this._tag_root = undefined;
|
||||
}
|
||||
this._tag_siblings = undefined;
|
||||
this._tag_channel = undefined;
|
||||
this._tag_clients = undefined;
|
||||
|
||||
this._cached_channel_description_promise = undefined;
|
||||
this._cached_channel_description_promise_resolve = undefined;
|
||||
this._cached_channel_description_promise_reject = undefined;
|
||||
|
||||
this.channel_previous = undefined;
|
||||
this.parent = undefined;
|
||||
this.channel_next = undefined;
|
||||
this.channelTree = undefined;
|
||||
}
|
||||
|
||||
channelName(){
|
||||
return this.properties.channel_name;
|
||||
}
|
||||
|
@ -186,7 +209,7 @@ class ChannelEntry {
|
|||
if(current_index == new_index && !enforce) return;
|
||||
|
||||
this._tag_channel.css("z-index", this._family_index);
|
||||
this._tag_channel.css("padding-left", (this._family_index + 1) * 16 + "px");
|
||||
this._tag_channel.css("padding-left", ((this._family_index + 1) * 16 + 10) + "px");
|
||||
}
|
||||
|
||||
calculate_family_index(enforce_recalculate: boolean = false) : number {
|
||||
|
@ -213,6 +236,15 @@ class ChannelEntry {
|
|||
container_entry.attr("channel-id", this.channelId);
|
||||
container_entry.addClass(this._channel_name_alignment);
|
||||
|
||||
/* unread marker */
|
||||
{
|
||||
container_entry.append(
|
||||
$.spawn("div")
|
||||
.addClass("marker-text-unread hidden")
|
||||
.attr("conversation", this.channelId)
|
||||
);
|
||||
}
|
||||
|
||||
/* channel icon (type) */
|
||||
{
|
||||
container_entry.append(
|
||||
|
@ -317,7 +349,7 @@ class ChannelEntry {
|
|||
/*
|
||||
setInterval(() => {
|
||||
let color = (Math.random() * 10000000).toString(16).substr(0, 6);
|
||||
bg.css("background", "#" + color);
|
||||
tag_channel.css("background", "#" + color);
|
||||
}, 150);
|
||||
*/
|
||||
|
||||
|
@ -455,23 +487,31 @@ class ChannelEntry {
|
|||
|
||||
const bold = text => contextmenu.get_provider().html_format_enabled() ? "<b>" + text + "</b>" : text;
|
||||
contextmenu.spawn_context_menu(x, y, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Show channel info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
this.channelTree.client.select_info.open_popover()
|
||||
},
|
||||
icon_class: "client-about",
|
||||
visible: this.channelTree.client.select_info.is_popover()
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.HR,
|
||||
visible: this.channelTree.client.select_info.is_popover(),
|
||||
name: ''
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-channel_switch",
|
||||
name: bold(tr("Switch to channel")),
|
||||
callback: () => this.joinChannel()
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-channel_switch",
|
||||
name: bold(tr("Join text channel")),
|
||||
callback: () => {
|
||||
this.channelTree.client.side_bar.channel_conversations().set_current_channel(this.getChannelId());
|
||||
this.channelTree.client.side_bar.show_channel_conversations();
|
||||
},
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.HR,
|
||||
name: ''
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Show channel info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
|
||||
alert('TODO!');
|
||||
},
|
||||
icon_class: "client-about"
|
||||
},
|
||||
...(() => {
|
||||
const local_client = this.channelTree.client.getClient();
|
||||
|
@ -734,13 +774,19 @@ class ChannelEntry {
|
|||
this.updateChannelTypeIcon();
|
||||
info_update = true;
|
||||
}
|
||||
if(key == "channel_flag_conversation_private") {
|
||||
const conversations = this.channelTree.client.side_bar.channel_conversations();
|
||||
const conversation = conversations.conversation(this.channelId, false);
|
||||
if(conversation)
|
||||
conversation.set_flag_private(this.properties.channel_flag_conversation_private);
|
||||
}
|
||||
}
|
||||
group.end();
|
||||
|
||||
if(info_update) {
|
||||
const _client = this.channelTree.client.getClient();
|
||||
if(_client.currentChannel() === this)
|
||||
this.channelTree.client.chat_frame.info_frame().update_channel_talk();
|
||||
this.channelTree.client.side_bar.info_frame().update_channel_talk();
|
||||
//TODO chat channel!
|
||||
}
|
||||
}
|
||||
|
@ -855,6 +901,7 @@ class ChannelEntry {
|
|||
get flag_subscribed() : boolean {
|
||||
return this._flag_subscribed;
|
||||
}
|
||||
|
||||
set flag_subscribed(flag: boolean) {
|
||||
if(this._flag_subscribed == flag)
|
||||
return;
|
||||
|
@ -875,6 +922,10 @@ class ChannelEntry {
|
|||
this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode);
|
||||
}
|
||||
|
||||
set flag_text_unread(flag: boolean) {
|
||||
this._tag_channel.find(".marker-text-unread").toggleClass("hidden", !flag);
|
||||
}
|
||||
|
||||
log_data() : log.server.base.Channel {
|
||||
return {
|
||||
channel_name: this.channelName(),
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
/// <reference path="channel.ts" />
|
||||
/// <reference path="modal/ModalChangeVolume.ts" />
|
||||
/// <reference path="modal/ModalServerGroupDialog.ts" />
|
||||
/// <reference path="client_move.ts" />
|
||||
|
||||
import KeyEvent = ppt.KeyEvent;
|
||||
|
||||
enum ClientType {
|
||||
CLIENT_VOICE,
|
||||
CLIENT_QUERY,
|
||||
|
@ -35,6 +32,7 @@ class ClientProperties {
|
|||
client_away_message: string = "";
|
||||
client_away: boolean = false;
|
||||
|
||||
client_country: string = "";
|
||||
|
||||
client_input_hardware: boolean = false;
|
||||
client_output_hardware: boolean = false;
|
||||
|
@ -42,8 +40,9 @@ class ClientProperties {
|
|||
client_output_muted: boolean = false;
|
||||
client_is_channel_commander: boolean = false;
|
||||
|
||||
client_teaforum_id: number = 0;
|
||||
client_teaforum_name: string = "";
|
||||
client_teaforo_id: number = 0;
|
||||
client_teaforo_name: string = "";
|
||||
client_teaforo_flags: number = 0; /* 0x01 := Banned | 0x02 := Stuff | 0x04 := Premium */
|
||||
|
||||
client_talk_power: number = 0;
|
||||
}
|
||||
|
@ -55,9 +54,12 @@ class ClientEntry {
|
|||
|
||||
protected _properties: ClientProperties;
|
||||
protected lastVariableUpdate: number = 0;
|
||||
protected _speaking: boolean = false;
|
||||
protected _speaking: boolean;
|
||||
protected _listener_initialized: boolean;
|
||||
|
||||
protected _audio_handle: connection.voice.VoiceClient;
|
||||
protected _audio_volume: number;
|
||||
protected _audio_muted: boolean;
|
||||
|
||||
channelTree: ChannelTree;
|
||||
|
||||
|
@ -69,10 +71,42 @@ class ClientEntry {
|
|||
this._channel = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(this._tag) {
|
||||
this._tag.remove();
|
||||
this._tag = undefined;
|
||||
}
|
||||
if(this._audio_handle) {
|
||||
console.warn(tr("Destroying client with an active audio handle. This could cause memory leaks!"));
|
||||
this._audio_handle.abort_replay();
|
||||
this._audio_handle.callback_playback = undefined;
|
||||
this._audio_handle.callback_stopped = undefined;
|
||||
this._audio_handle = undefined;
|
||||
}
|
||||
|
||||
this._channel = undefined;
|
||||
}
|
||||
|
||||
tree_unregistered() {
|
||||
this.channelTree = undefined;
|
||||
if(this._audio_handle) {
|
||||
this._audio_handle.abort_replay();
|
||||
this._audio_handle.callback_playback = undefined;
|
||||
this._audio_handle.callback_stopped = undefined;
|
||||
this._audio_handle = undefined;
|
||||
}
|
||||
|
||||
this._channel = undefined;
|
||||
}
|
||||
|
||||
set_audio_handle(handle: connection.voice.VoiceClient) {
|
||||
if(this._audio_handle === handle)
|
||||
return;
|
||||
|
||||
if(this._audio_handle) {
|
||||
this._audio_handle.callback_playback = undefined;
|
||||
this._audio_handle.callback_stopped = undefined;
|
||||
}
|
||||
//TODO may ensure that the id is the same?
|
||||
this._audio_handle = handle;
|
||||
if(!handle) {
|
||||
|
@ -97,6 +131,41 @@ class ClientEntry {
|
|||
clientUid(){ return this.properties.client_unique_identifier; }
|
||||
clientId(){ return this._clientId; }
|
||||
|
||||
is_muted() { return !!this._audio_muted; }
|
||||
set_muted(flag: boolean, update_icon: boolean, force?: boolean) {
|
||||
if(this._audio_muted === flag && !force)
|
||||
return;
|
||||
|
||||
if(flag) {
|
||||
this.channelTree.client.serverConnection.send_command('clientmute', {
|
||||
clid: this.clientId()
|
||||
});
|
||||
} else if(this._audio_muted) {
|
||||
this.channelTree.client.serverConnection.send_command('clientunmute', {
|
||||
clid: this.clientId()
|
||||
});
|
||||
}
|
||||
this._audio_muted = flag;
|
||||
|
||||
this.channelTree.client.settings.changeServer("mute_client_" + this.clientUid(), flag);
|
||||
if(this._audio_handle) {
|
||||
if(flag) {
|
||||
this._audio_handle.set_volume(0);
|
||||
} else {
|
||||
this._audio_handle.set_volume(this._audio_volume);
|
||||
}
|
||||
}
|
||||
|
||||
if(update_icon)
|
||||
this.updateClientSpeakIcon();
|
||||
|
||||
for(const client of this.channelTree.clients) {
|
||||
if(client === this || client.properties.client_unique_identifier != this.properties.client_unique_identifier)
|
||||
continue;
|
||||
client.set_muted(flag, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected initializeListener(){
|
||||
if(this._listener_initialized) return;
|
||||
this._listener_initialized = true;
|
||||
|
@ -116,7 +185,7 @@ class ClientEntry {
|
|||
if($.isArray(this.channelTree.currently_selected)) { //Multiselect
|
||||
return;
|
||||
}
|
||||
this.chat(true).focus();
|
||||
this.open_text_chat();
|
||||
});
|
||||
|
||||
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
||||
|
@ -169,6 +238,25 @@ class ClientEntry {
|
|||
});
|
||||
}
|
||||
|
||||
protected contextmenu_info() : contextmenu.MenuEntry[] {
|
||||
return [
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: this.properties.client_type_exact === ClientType.CLIENT_MUSIC ? tr("Show bot info") : tr("Show client info"),
|
||||
callback: () => {
|
||||
this.channelTree.client.side_bar.show_client_info(this);
|
||||
},
|
||||
icon_class: "client-about",
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)
|
||||
}, {
|
||||
callback: () => {},
|
||||
type: contextmenu.MenuEntryType.HR,
|
||||
name: "",
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
protected assignment_context() : contextmenu.MenuEntry[] {
|
||||
let server_groups: contextmenu.MenuEntry[] = [];
|
||||
for(let group of this.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) {
|
||||
|
@ -229,7 +317,7 @@ class ClientEntry {
|
|||
sub_menu: [
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon: "client-permission_server_groups",
|
||||
icon_class: "client-permission_server_groups",
|
||||
name: "Server groups dialog",
|
||||
callback: () => {
|
||||
Modals.createServerGroupAssignmentModal(this, (group, flag) => {
|
||||
|
@ -260,36 +348,50 @@ class ClientEntry {
|
|||
type: contextmenu.MenuEntryType.SUB_MENU,
|
||||
icon_class: "client-permission_client",
|
||||
name: tr("Permissions"),
|
||||
disabled: true,
|
||||
sub_menu: [ ]
|
||||
sub_menu: [
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-permission_client",
|
||||
name: tr("Client permissions"),
|
||||
callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clp", {unique_id: this.clientUid()}).open()
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-permission_client",
|
||||
name: tr("Client channel permissions"),
|
||||
callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clchp", {unique_id: this.clientUid(), channel_id: this._channel ? this._channel.channelId : undefined }).open()
|
||||
}
|
||||
]
|
||||
}];
|
||||
}
|
||||
|
||||
open_text_chat() {
|
||||
const chat = this.channelTree.client.side_bar;
|
||||
const conversation = chat.private_conversations().find_conversation({
|
||||
name: this.clientNickName(),
|
||||
client_id: this.clientId(),
|
||||
unique_id: this.clientUid()
|
||||
}, {
|
||||
attach: true,
|
||||
create: true
|
||||
});
|
||||
chat.private_conversations().set_selected_conversation(conversation);
|
||||
/* TODO: Check if auto switch to private conversations is enabled */
|
||||
chat.show_private_conversations();
|
||||
chat.private_conversations().try_input_focus();
|
||||
}
|
||||
|
||||
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
|
||||
let trigger_close = true;
|
||||
contextmenu.spawn_context_menu(x, y,
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Show client info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
this.channelTree.client.select_info.open_popover()
|
||||
},
|
||||
icon_class: "client-about",
|
||||
visible: this.channelTree.client.select_info.is_popover()
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.HR,
|
||||
visible: this.channelTree.client.select_info.is_popover(),
|
||||
name: ''
|
||||
}, {
|
||||
...this.contextmenu_info(), {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-change_nickname",
|
||||
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
|
||||
tr("Open text chat") +
|
||||
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
|
||||
callback: () => {
|
||||
this.channelTree.client.chat.activeChat = this.chat(true);
|
||||
this.channelTree.client.chat.focus();
|
||||
this.open_text_chat();
|
||||
}
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
|
@ -417,15 +519,29 @@ class ClientEntry {
|
|||
icon_class: "client-volume",
|
||||
name: tr("Change Volume"),
|
||||
callback: () => {
|
||||
Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => {
|
||||
Modals.spawnChangeVolume(this._audio_volume, volume => {
|
||||
this._audio_volume = volume;
|
||||
this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume);
|
||||
if(this._audio_handle)
|
||||
this._audio_handle.set_volume(volume);
|
||||
if(this.channelTree.client.select_info.currentSelected == this)
|
||||
this.channelTree.client.select_info.update();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-input_muted_local",
|
||||
name: tr("Mute client"),
|
||||
visible: !this._audio_muted,
|
||||
callback: () => this.set_muted(true, true)
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-input_muted_local",
|
||||
name: tr("Unmute client"),
|
||||
visible: this._audio_muted,
|
||||
callback: () => this.set_muted(false, true)
|
||||
},
|
||||
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})())
|
||||
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))())
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -510,7 +626,7 @@ class ClientEntry {
|
|||
}
|
||||
|
||||
set speaking(flag) {
|
||||
if(flag == this._speaking) return;
|
||||
if(flag === this._speaking) return;
|
||||
this._speaking = flag;
|
||||
this.updateClientSpeakIcon();
|
||||
}
|
||||
|
@ -531,8 +647,10 @@ class ClientEntry {
|
|||
icon = "client-server_query";
|
||||
console.log("Server query!");
|
||||
} else {
|
||||
if(this.properties.client_away) {
|
||||
if (this.properties.client_away) {
|
||||
icon = "client-away";
|
||||
} else if (this._audio_muted && !(this instanceof LocalClientEntry)) {
|
||||
icon = "client-input_muted_local";
|
||||
} else if(!this.properties.client_output_hardware) {
|
||||
icon = "client-hardware_output_muted";
|
||||
} else if(this.properties.client_output_muted) {
|
||||
|
@ -582,6 +700,7 @@ class ClientEntry {
|
|||
let update_icon_speech = false;
|
||||
let update_away = false;
|
||||
let reorder_channel = false;
|
||||
let update_avatar = false;
|
||||
|
||||
{
|
||||
const entries = [];
|
||||
|
@ -595,13 +714,34 @@ class ClientEntry {
|
|||
}
|
||||
|
||||
for(const variable of variables) {
|
||||
const old_value = this._properties[variable.key];
|
||||
JSON.map_field_to(this._properties, variable.value, variable.key);
|
||||
|
||||
if(variable.key == "client_nickname") {
|
||||
this.tag.find(".client-name").text(variable.value);
|
||||
let chat = this.chat(false);
|
||||
if(chat) chat.name = variable.value;
|
||||
if(variable.value !== old_value && typeof(old_value) === "string") {
|
||||
if(!(this instanceof LocalClientEntry)) { /* own changes will be logged somewhere else */
|
||||
this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, {
|
||||
own_client: false,
|
||||
client: this.log_data(),
|
||||
new_name: variable.value,
|
||||
old_name: old_value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.tag.find(".client-name").text(variable.value);
|
||||
|
||||
const chat = this.channelTree.client.side_bar;
|
||||
const conversation = chat.private_conversations().find_conversation({
|
||||
name: this.clientNickName(),
|
||||
client_id: this.clientId(),
|
||||
unique_id: this.clientUid()
|
||||
}, {
|
||||
attach: false,
|
||||
create: false
|
||||
});
|
||||
if(conversation)
|
||||
conversation.set_client_name(variable.value);
|
||||
reorder_channel = true;
|
||||
}
|
||||
if(
|
||||
|
@ -617,13 +757,15 @@ class ClientEntry {
|
|||
update_away = true;
|
||||
}
|
||||
if(variable.key == "client_unique_identifier") {
|
||||
if(this._audio_handle) {
|
||||
const volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1"));
|
||||
this._audio_handle.set_volume(volume);
|
||||
log.debug(LogCategory.CLIENT, tr("Loaded client volume %d for client %s from config."), volume, this.clientUid());
|
||||
} else {
|
||||
log.warn(LogCategory.CLIENT, tr("Visible client got unique id assigned, but hasn't yet an audio handle. Ignoring volume assignment."));
|
||||
}
|
||||
this._audio_volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1"));
|
||||
const mute_status = this.channelTree.client.settings.server("mute_client_" + this.clientUid(), false);
|
||||
this.set_muted(mute_status, false, mute_status); /* force only needed when we want to mute the client */
|
||||
|
||||
if(this._audio_handle)
|
||||
this._audio_handle.set_volume(this._audio_muted ? 0 : this._audio_volume);
|
||||
|
||||
update_icon_speech = true;
|
||||
log.debug(LogCategory.CLIENT, tr("Loaded client (%s) server specific properties. Volume: %o Muted: %o."), this.clientUid(), this._audio_volume, this._audio_muted);
|
||||
}
|
||||
if(variable.key == "client_talk_power") {
|
||||
reorder_channel = true;
|
||||
|
@ -639,6 +781,8 @@ class ClientEntry {
|
|||
}
|
||||
if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups")
|
||||
this.update_displayed_client_groups();
|
||||
else if(variable.key == "client_flag_avatar")
|
||||
update_avatar = true;
|
||||
}
|
||||
|
||||
/* process updates after variables have been set */
|
||||
|
@ -651,15 +795,30 @@ class ClientEntry {
|
|||
if(update_away)
|
||||
this.updateAwayMessage();
|
||||
|
||||
const side_bar = this.channelTree.client.side_bar;
|
||||
{
|
||||
const client_info = side_bar.client_info();
|
||||
if(client_info.current_client() === this)
|
||||
client_info.set_current_client(this, true); /* force an update */
|
||||
}
|
||||
if(update_avatar) {
|
||||
this.channelTree.client.fileManager.avatars.update_cache(this.avatarId(), this.properties.client_flag_avatar);
|
||||
|
||||
const conversations = side_bar.private_conversations();
|
||||
const conversation = conversations.find_conversation({name: this.clientNickName(), unique_id: this.clientUid(), client_id: this.clientId()}, {create: false, attach: false});
|
||||
if(conversation)
|
||||
conversation.update_avatar();
|
||||
}
|
||||
|
||||
group.end();
|
||||
}
|
||||
|
||||
update_displayed_client_groups() {
|
||||
this.tag.find(".container-icons-group").children().detach();
|
||||
this.tag.find(".container-icons-group").children().remove();
|
||||
|
||||
for(let id of this.assignedServerGroupIds())
|
||||
this.updateGroupIcon(this.channelTree.client.groups.serverGroup(id));
|
||||
|
||||
this.update_group_icon_order();
|
||||
this.updateGroupIcon(this.channelTree.client.groups.channelGroup(this.properties.client_channel_group_id));
|
||||
|
||||
let prefix_groups: string[] = [];
|
||||
|
@ -696,44 +855,8 @@ class ClientEntry {
|
|||
}
|
||||
}
|
||||
|
||||
private chat_name() {
|
||||
return "client_" + this.clientUid() + ":" + this.clientId();
|
||||
}
|
||||
|
||||
chat(create: boolean = false) : ChatEntry {
|
||||
let chatName = "client_" + this.clientUid() + ":" + this.clientId();
|
||||
let chat = this.channelTree.client.chat.findChat(chatName);
|
||||
if(!chat && create) {
|
||||
chat = this.channelTree.client.chat.createChat(chatName);
|
||||
chat.flag_closeable = true;
|
||||
chat.name = this.clientNickName();
|
||||
chat.owner_unique_id = this.properties.client_unique_identifier;
|
||||
}
|
||||
|
||||
this.initialize_chat(chat);
|
||||
return chat;
|
||||
}
|
||||
|
||||
initialize_chat(handle?: ChatEntry) {
|
||||
handle = handle || this.channelTree.client.chat.findChat(this.chat_name());
|
||||
if(!handle)
|
||||
return;
|
||||
|
||||
handle.onMessageSend = text => {
|
||||
this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, this);
|
||||
};
|
||||
|
||||
handle.onClose = () => {
|
||||
if(!handle.flag_offline)
|
||||
this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": this.clientId()}, {process_result: false}).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to notify chat participant (%o) that the chat has been closed. Error: %o"), this, error);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
updateClientIcon() {
|
||||
this.tag.find(".container-icon-client").children().detach();
|
||||
this.tag.find(".container-icon-client").children().remove();
|
||||
if(this.properties.client_icon_id > 0) {
|
||||
this.channelTree.client.fileManager.icons.generateTag(this.properties.client_icon_id).attr("title", "Client icon")
|
||||
.appendTo(this.tag.find(".container-icon-client"));
|
||||
|
@ -742,18 +865,25 @@ class ClientEntry {
|
|||
|
||||
updateGroupIcon(group: Group) {
|
||||
if(!group) return;
|
||||
//TODO group icon order
|
||||
this.tag.find(".container-icons-group .icon_group_" + group.id).detach();
|
||||
|
||||
const container = this.tag.find(".container-icons-group");
|
||||
container.find(".icon_group_" + group.id).remove();
|
||||
|
||||
if (group.properties.iconid > 0) {
|
||||
this.tag.find(".container-icons-group").append(
|
||||
$.spawn("div")
|
||||
container.append(
|
||||
$.spawn("div").attr('group-power', group.properties.sortid)
|
||||
.addClass("container-group-icon icon_group_" + group.id)
|
||||
.append(this.channelTree.client.fileManager.icons.generateTag(group.properties.iconid)).attr("title", group.name)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
update_group_icon_order() {
|
||||
const container = this.tag.find(".container-icons-group");
|
||||
|
||||
container.append(...[...container.children()].sort((a, b) => parseInt(a.getAttribute("group-power")) - parseInt(b.getAttribute("group-power"))));
|
||||
}
|
||||
|
||||
assignedServerGroupIds() : number[] {
|
||||
let result = [];
|
||||
for(let id of this.properties.client_servergroups.split(",")){
|
||||
|
@ -843,7 +973,7 @@ class LocalClientEntry extends ClientEntry {
|
|||
const _self = this;
|
||||
|
||||
contextmenu.spawn_context_menu(x, y,
|
||||
{
|
||||
...this.contextmenu_info(), {
|
||||
|
||||
name: (contextmenu.get_provider().html_format_enabled() ? "<b>" : "") +
|
||||
tr("Change name") +
|
||||
|
@ -875,6 +1005,7 @@ class LocalClientEntry extends ClientEntry {
|
|||
}
|
||||
|
||||
initializeListener(): void {
|
||||
this._listener_initialized = false; /* could there be a better system */
|
||||
super.initializeListener();
|
||||
this.tag.find(".client-name").addClass("client-name-own");
|
||||
|
||||
|
@ -918,11 +1049,14 @@ class LocalClientEntry extends ClientEntry {
|
|||
if(_self.clientNickName() == text) return;
|
||||
|
||||
elm.text(_self.clientNickName());
|
||||
const old_name = _self.clientNickName();
|
||||
_self.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => {
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, text);
|
||||
this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, {
|
||||
client: this.log_data(),
|
||||
own_action: true
|
||||
old_name: old_name,
|
||||
new_name: text,
|
||||
own_client: true
|
||||
});
|
||||
}).catch((e: CommandResult) => {
|
||||
this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGE_FAILED, {
|
||||
|
@ -973,6 +1107,13 @@ class MusicClientEntry extends ClientEntry {
|
|||
super(clientId, clientName, new MusicClientProperties());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this._info_promise = undefined;
|
||||
this._info_promise_reject = undefined;
|
||||
this._info_promise_resolve = undefined;
|
||||
}
|
||||
|
||||
get properties() : MusicClientProperties {
|
||||
return this._properties as MusicClientProperties;
|
||||
}
|
||||
|
@ -980,20 +1121,7 @@ class MusicClientEntry extends ClientEntry {
|
|||
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
|
||||
let trigger_close = true;
|
||||
contextmenu.spawn_context_menu(x, y,
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Show bot info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
this.channelTree.client.select_info.open_popover()
|
||||
},
|
||||
icon_class: "client-about",
|
||||
visible: this.channelTree.client.select_info.is_popover()
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.HR,
|
||||
visible: this.channelTree.client.select_info.is_popover(),
|
||||
name: ''
|
||||
}, {
|
||||
...this.contextmenu_info(), {
|
||||
name: tr("<b>Change bot name</b>"),
|
||||
icon_class: "client-change_nickname",
|
||||
disabled: false,
|
||||
|
@ -1160,7 +1288,7 @@ class MusicClientEntry extends ClientEntry {
|
|||
},
|
||||
type: contextmenu.MenuEntryType.ENTRY
|
||||
},
|
||||
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})())
|
||||
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -88,8 +88,13 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
|
|||
return;
|
||||
|
||||
menu.animate({opacity: 0}, 100, () => menu.css("display", "none"));
|
||||
for(const callback of this._close_callbacks)
|
||||
for(const callback of this._close_callbacks) {
|
||||
if(typeof(callback) !== "function") {
|
||||
console.error(tr("Given close callback is not a function!. Callback: %o"), callback);
|
||||
continue;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
this._close_callbacks = [];
|
||||
}
|
||||
|
||||
|
@ -135,7 +140,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
|
|||
}
|
||||
return tag;
|
||||
} else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) {
|
||||
let checkbox = $.spawn("label").addClass("checkbox");
|
||||
let checkbox = $.spawn("label").addClass("ccheckbox");
|
||||
$.spawn("input").attr("type", "checkbox").prop("checked", !!entry.checkbox_checked).appendTo(checkbox);
|
||||
$.spawn("span").addClass("checkmark").appendTo(checkbox);
|
||||
|
||||
|
@ -191,6 +196,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
|
|||
continue;
|
||||
|
||||
if(entry.type == contextmenu.MenuEntryType.CLOSE) {
|
||||
if(entry.callback)
|
||||
this._close_callbacks.push(entry.callback);
|
||||
} else
|
||||
menu_container.append(this.generate_tag(entry));
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import ClickEvent = JQuery.ClickEvent;
|
||||
|
||||
enum ElementType {
|
||||
HEADER,
|
||||
BODY,
|
||||
|
@ -22,7 +24,7 @@ const ModalFunctions = {
|
|||
switch (typeof val){
|
||||
case "string":
|
||||
if(type == ElementType.HEADER)
|
||||
return $.spawn("h5").addClass("modal-title").text(val);
|
||||
return $.spawn("div").addClass("modal-title").text(val);
|
||||
return $("<div>" + val + "</div>");
|
||||
case "object": return val as JQuery;
|
||||
case "undefined":
|
||||
|
@ -61,6 +63,7 @@ class ModalProperties {
|
|||
return this;
|
||||
}
|
||||
width: number | string = "60%";
|
||||
min_width?: number | string;
|
||||
height: number | string = "auto";
|
||||
|
||||
closeable: boolean = true;
|
||||
|
@ -78,8 +81,33 @@ class ModalProperties {
|
|||
full_size?: boolean = false;
|
||||
}
|
||||
|
||||
class Modal {
|
||||
$(document).on('mousedown', (event: MouseEvent) => {
|
||||
/* pageX or pageY are undefined if this is an event executed via .trigger('click'); */
|
||||
if(_global_modal_count == 0 || typeof(event.pageX) === "undefined" || typeof(event.pageY) === "undefined")
|
||||
return;
|
||||
|
||||
|
||||
let element = event.target as HTMLElement;
|
||||
do {
|
||||
if(element.classList.contains('modal-content'))
|
||||
break;
|
||||
|
||||
if(!element.classList.contains('modal'))
|
||||
continue;
|
||||
|
||||
if(element == _global_modal_last && _global_modal_last_time + 100 > Date.now())
|
||||
break;
|
||||
|
||||
$(element).find("> .modal-dialog > .modal-content > .modal-header .button-modal-close").trigger('click');
|
||||
break;
|
||||
} while((element = element.parentElement));
|
||||
});
|
||||
|
||||
let _global_modal_count = 0;
|
||||
let _global_modal_last: HTMLElement;
|
||||
let _global_modal_last_time: number;
|
||||
|
||||
class Modal {
|
||||
private _htmlTag: JQuery;
|
||||
properties: ModalProperties;
|
||||
shown: boolean;
|
||||
|
@ -119,32 +147,57 @@ class Modal {
|
|||
Object.assign(properties, this.properties.template_properties);
|
||||
|
||||
const tag = template.renderTag(properties);
|
||||
if(typeof(this.properties.width) !== "undefined")
|
||||
tag.find(".modal-content").css("min-width", this.properties.width);
|
||||
if(typeof(this.properties.min_width) !== "undefined")
|
||||
tag.find(".modal-content").css("min-width", this.properties.min_width);
|
||||
|
||||
this.close_elements = tag.find(".button-modal-close");
|
||||
this.close_elements.toggle(this.properties.closeable);
|
||||
this.close_elements.toggle(this.properties.closeable).on('click', event => {
|
||||
if(this.properties.closeable)
|
||||
this.close();
|
||||
});
|
||||
this._htmlTag = tag;
|
||||
this._htmlTag.on('shown.bs.modal', event => { for(const listener of this.open_listener) listener(); });
|
||||
|
||||
this._htmlTag.find("input").on('change', event => {
|
||||
$(event.target).parents(".form-group").toggleClass('is-filled', !!(event.target as HTMLInputElement).value);
|
||||
});
|
||||
|
||||
//TODO: After the animation!
|
||||
this._htmlTag.on('hide.bs.modal', event => !this.properties.closeable || this.close());
|
||||
this._htmlTag.on('hidden.bs.modal', event => this._htmlTag.detach());
|
||||
this._htmlTag.on('hidden.bs.modal', event => this._htmlTag.remove());
|
||||
}
|
||||
|
||||
open() {
|
||||
if(this.shown)
|
||||
return;
|
||||
|
||||
_global_modal_last_time = Date.now();
|
||||
_global_modal_last = this.htmlTag[0];
|
||||
|
||||
this.shown = true;
|
||||
this.htmlTag.appendTo($("body"));
|
||||
|
||||
this.htmlTag.bootstrapMaterialDesign().modal(this.properties.closeable ? 'show' : {
|
||||
backdrop: 'static',
|
||||
keyboard: false,
|
||||
});
|
||||
_global_modal_count++;
|
||||
this.htmlTag.show();
|
||||
setTimeout(() => this.htmlTag.addClass('shown'), 0);
|
||||
|
||||
if(this.properties.trigger_tab)
|
||||
this.htmlTag.one('shown.bs.modal', () => this.htmlTag.find(".tab").trigger('tab.resize'));
|
||||
setTimeout(() => {
|
||||
for(const listener of this.open_listener) listener();
|
||||
this.htmlTag.find(".tab").trigger('tab.resize');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
close() {
|
||||
if(!this.shown) return;
|
||||
|
||||
_global_modal_count--;
|
||||
this.shown = false;
|
||||
this.htmlTag.modal('hide');
|
||||
this.htmlTag.removeClass('shown');
|
||||
setTimeout(() => {
|
||||
this.htmlTag.remove();
|
||||
this._htmlTag = undefined;
|
||||
}, 300);
|
||||
this.properties.triggerClose();
|
||||
for(const listener of this.close_listener)
|
||||
listener();
|
||||
|
|
|
@ -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"))
|
||||
tag_header.addClass(header_tag.attr("x-entry-class"));
|
||||
if(header_tag.attr("x-entry-id"))
|
||||
tag_header.attr("x-id", header_tag.attr("x-entry-id"));
|
||||
|
||||
tag_header.append(header_data);
|
||||
|
||||
/* listener if the tab might got removed */
|
||||
|
|
|
@ -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 _button_hostbanner: JQuery;
|
||||
|
||||
htmlTag: JQuery;
|
||||
constructor(htmlTag: JQuery) {
|
||||
this.htmlTag = htmlTag;
|
||||
|
@ -47,6 +49,7 @@ class ControlBar {
|
|||
|
||||
this.connection_handler = handler;
|
||||
this.apply_server_state();
|
||||
this.update_connection_state();
|
||||
}
|
||||
|
||||
apply_server_state() {
|
||||
|
@ -63,15 +66,30 @@ class ControlBar {
|
|||
this.button_query_visible = this.connection_handler.client_status.queries_visible;
|
||||
this.button_subscribe_all = this.connection_handler.client_status.channel_subscribe_all;
|
||||
|
||||
this.apply_server_hostbutton();
|
||||
this.apply_server_voice_state();
|
||||
}
|
||||
|
||||
apply_server_hostbutton() {
|
||||
const server = this.connection_handler.channelTree.server;
|
||||
if(server && server.properties.virtualserver_hostbutton_gfx_url) {
|
||||
this._button_hostbanner
|
||||
.attr("title", server.properties.virtualserver_hostbutton_tooltip || server.properties.virtualserver_hostbutton_gfx_url)
|
||||
.attr("href", server.properties.virtualserver_hostbutton_url);
|
||||
this._button_hostbanner.find("img").attr("src", server.properties.virtualserver_hostbutton_gfx_url);
|
||||
this._button_hostbanner.show();
|
||||
} else {
|
||||
this._button_hostbanner.hide();
|
||||
}
|
||||
}
|
||||
|
||||
apply_server_voice_state() {
|
||||
if(!this.connection_handler)
|
||||
return;
|
||||
|
||||
this.button_microphone = !this.connection_handler.client_status.input_hardware ? "disabled" : this.connection_handler.client_status.input_muted ? "muted" : "enabled";
|
||||
this.button_speaker = this.connection_handler.client_status.output_muted ? "muted" : "enabled";
|
||||
top_menu.update_state(); //TODO: Only run "small" update?
|
||||
}
|
||||
|
||||
current_connection_handler() {
|
||||
|
@ -95,6 +113,7 @@ class ControlBar {
|
|||
};
|
||||
|
||||
this.htmlTag.find(".btn_connect").on('click', this.on_open_connect.bind(this));
|
||||
this.htmlTag.find(".btn_connect_new_tab").on('click', this.on_open_connect_new_tab.bind(this));
|
||||
this.htmlTag.find(".btn_disconnect").on('click', this.on_execute_disconnect.bind(this));
|
||||
|
||||
this.htmlTag.find(".btn_mute_input").on('click', this.on_toggle_microphone.bind(this));
|
||||
|
@ -110,6 +129,15 @@ class ControlBar {
|
|||
this.htmlTag.find(".btn_token_use").on('click', this.on_token_use.bind(this));
|
||||
this.htmlTag.find(".btn_token_list").on('click', this.on_token_list.bind(this));
|
||||
|
||||
(this._button_hostbanner = this.htmlTag.find(".button-hostbutton")).hide().on('click', () => {
|
||||
if(!this.connection_handler) return;
|
||||
|
||||
const server = this.connection_handler.channelTree.server;
|
||||
if(!server || !server.properties.virtualserver_hostbutton_url) return;
|
||||
|
||||
window.open(server.properties.virtualserver_hostbutton_url, '_blank');
|
||||
});
|
||||
|
||||
|
||||
{
|
||||
this.htmlTag.find(".btn_away_disable").on('click', this.on_away_disable.bind(this));
|
||||
|
@ -124,6 +152,7 @@ class ControlBar {
|
|||
this.htmlTag.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
|
||||
}
|
||||
|
||||
dropdownify(this.htmlTag.find(".container-connect"));
|
||||
dropdownify(this.htmlTag.find(".container-disconnect"));
|
||||
dropdownify(this.htmlTag.find(".btn_token"));
|
||||
dropdownify(this.htmlTag.find(".btn_away"));
|
||||
|
@ -202,13 +231,20 @@ class ControlBar {
|
|||
this._button_microphone = state;
|
||||
|
||||
let tag = this.htmlTag.find(".btn_mute_input");
|
||||
const tag_icon = tag.find(".icon_x32, .icon");
|
||||
const tag_icon = tag.find(".icon_em, .icon");
|
||||
tag.toggleClass('activated', state === "muted");
|
||||
|
||||
/*
|
||||
tag_icon
|
||||
.toggleClass('client-input_muted', state === "muted")
|
||||
.toggleClass('client-capture', state === "enabled")
|
||||
.toggleClass('client-activate_microphone', state === "disabled");
|
||||
*/
|
||||
|
||||
tag_icon
|
||||
.toggleClass('client-input_muted', state !== "disabled")
|
||||
.toggleClass('client-capture', false)
|
||||
.toggleClass('client-activate_microphone', state === "disabled");
|
||||
|
||||
if(state === "disabled")
|
||||
tag_icon.attr('title', tr("Enable your microphone on this server"));
|
||||
|
@ -224,12 +260,17 @@ class ControlBar {
|
|||
this._button_speakers = state;
|
||||
|
||||
let tag = this.htmlTag.find(".btn_mute_output");
|
||||
const tag_icon = tag.find(".icon_x32, .icon");
|
||||
const tag_icon = tag.find(".icon_em, .icon");
|
||||
|
||||
tag.toggleClass('activated', state === "muted");
|
||||
/*
|
||||
tag_icon
|
||||
.toggleClass('client-output_muted', state !== "enabled")
|
||||
.toggleClass('client-volume', state === "enabled");
|
||||
*/
|
||||
tag_icon
|
||||
.toggleClass('client-output_muted', true)
|
||||
.toggleClass('client-volume', false);
|
||||
|
||||
if(state === "enabled")
|
||||
tag_icon.attr('title', tr("Mute sound"));
|
||||
|
@ -245,7 +286,7 @@ class ControlBar {
|
|||
this.htmlTag
|
||||
.find(".button-subscribe-mode")
|
||||
.toggleClass('activated', this._button_subscribe_all)
|
||||
.find('.icon_x32')
|
||||
.find('.icon_em')
|
||||
.toggleClass('client-unsubscribe_from_all_channels', !this._button_subscribe_all)
|
||||
.toggleClass('client-subscribe_to_all_channels', this._button_subscribe_all);
|
||||
}
|
||||
|
@ -320,10 +361,13 @@ class ControlBar {
|
|||
|
||||
|
||||
private on_toggle_microphone() {
|
||||
if(this._button_microphone === "disabled" || this._button_microphone === "muted")
|
||||
if(this._button_microphone === "disabled" || this._button_microphone === "muted") {
|
||||
this.button_microphone = "enabled";
|
||||
else
|
||||
sound.manager.play(Sound.MICROPHONE_ACTIVATED);
|
||||
} else {
|
||||
this.button_microphone = "muted";
|
||||
sound.manager.play(Sound.MICROPHONE_MUTED);
|
||||
}
|
||||
|
||||
if(this.connection_handler) {
|
||||
this.connection_handler.client_status.input_muted = this._button_microphone !== "enabled";
|
||||
|
@ -338,10 +382,13 @@ class ControlBar {
|
|||
}
|
||||
|
||||
private on_toggle_sound() {
|
||||
if(this._button_speakers === "muted")
|
||||
if(this._button_speakers === "muted") {
|
||||
this.button_speaker = "enabled";
|
||||
else
|
||||
sound.manager.play(Sound.SOUND_ACTIVATED);
|
||||
} else {
|
||||
this.button_speaker = "muted";
|
||||
sound.manager.play(Sound.SOUND_MUTED);
|
||||
}
|
||||
|
||||
if(this.connection_handler) {
|
||||
this.connection_handler.client_status.output_muted = this._button_speakers !== "enabled";
|
||||
|
@ -379,8 +426,17 @@ class ControlBar {
|
|||
|
||||
private on_open_connect() {
|
||||
if(this.connection_handler)
|
||||
this.connection_handler.cancel_reconnect();
|
||||
this.connection_handler.cancel_reconnect(true);
|
||||
Modals.spawnConnectModal({}, {
|
||||
url: "ts.TeaSpeak.de",
|
||||
enforce: false
|
||||
});
|
||||
}
|
||||
|
||||
private on_open_connect_new_tab() {
|
||||
Modals.spawnConnectModal({
|
||||
default_connect_new_tab: true
|
||||
}, {
|
||||
url: "ts.TeaSpeak.de",
|
||||
enforce: false
|
||||
});
|
||||
|
@ -410,7 +466,7 @@ class ControlBar {
|
|||
}
|
||||
|
||||
private on_execute_disconnect() {
|
||||
this.connection_handler.cancel_reconnect();
|
||||
this.connection_handler.cancel_reconnect(true);
|
||||
this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
|
||||
this.update_connection_state();
|
||||
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||
|
@ -426,7 +482,7 @@ class ControlBar {
|
|||
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
|
||||
}).catch(error => {
|
||||
//TODO tr
|
||||
createErrorModal(tr("Use token"), "Failed to use token: " + (error instanceof CommandResult ? error.message : error)).open();
|
||||
createErrorModal(tr("Use token"), MessageHelper.formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
|
||||
});
|
||||
}).open();
|
||||
}
|
||||
|
@ -459,23 +515,7 @@ class ControlBar {
|
|||
}
|
||||
|
||||
private on_bookmark_server_add() {
|
||||
if(this.connection_handler && this.connection_handler.connected) {
|
||||
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => true, result => {
|
||||
if(result) {
|
||||
const bookmark = bookmarks.create_bookmark(result as string, bookmarks.bookmarks(), {
|
||||
server_port: this.connection_handler.serverConnection.remote_address().port,
|
||||
server_address: this.connection_handler.serverConnection.remote_address().host,
|
||||
|
||||
server_password: "",
|
||||
server_password_hash: ""
|
||||
}, this.connection_handler.getClient().clientNickName());
|
||||
bookmarks.save_bookmark(bookmark);
|
||||
this.update_bookmarks()
|
||||
}
|
||||
}).open();
|
||||
} else {
|
||||
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
|
||||
}
|
||||
bookmarks.add_current_server();
|
||||
}
|
||||
|
||||
update_bookmark_status() {
|
||||
|
@ -486,8 +526,8 @@ class ControlBar {
|
|||
|
||||
update_bookmarks() {
|
||||
//<div class="btn_bookmark_connect" target="localhost"><a>Localhost</a></div>
|
||||
let tag_bookmark = this.htmlTag.find(".btn_bookmark .dropdown");
|
||||
tag_bookmark.find(".bookmark, .directory").detach();
|
||||
let tag_bookmark = this.htmlTag.find(".btn_bookmark > .dropdown");
|
||||
tag_bookmark.find(".bookmark, .directory").remove();
|
||||
|
||||
const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => {
|
||||
if(bookmark.type == bookmarks.BookmarkType.ENTRY) {
|
||||
|
@ -495,37 +535,14 @@ class ControlBar {
|
|||
|
||||
const bookmark_connect = (new_tab: boolean) => {
|
||||
this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working
|
||||
|
||||
const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile();
|
||||
if(profile.valid()) {
|
||||
const connection = this.connection_handler && !new_tab ? this.connection_handler : server_connections.spawn_server_connection_handler();
|
||||
server_connections.set_active_connection_handler(connection);
|
||||
connection.startConnection(
|
||||
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
profile,
|
||||
{
|
||||
nickname: mark.nickname,
|
||||
password: {
|
||||
password: mark.server_properties.server_password_hash,
|
||||
hashed: true
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
Modals.spawnConnectModal({
|
||||
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
enforce: true
|
||||
}, {
|
||||
profile: profile,
|
||||
enforce: true
|
||||
})
|
||||
}
|
||||
bookmarks.boorkmak_connect(mark, new_tab);
|
||||
};
|
||||
|
||||
return $.spawn("div")
|
||||
.addClass("bookmark")
|
||||
.append(
|
||||
$.spawn("div").addClass("icon client-server")
|
||||
//$.spawn("div").addClass("icon client-server")
|
||||
IconManager.generate_tag(IconManager.load_cached_icon(mark.last_icon_id || 0), {animate: false}) /* must be false */
|
||||
)
|
||||
.append(
|
||||
$.spawn("div")
|
||||
|
@ -550,7 +567,8 @@ class ControlBar {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Connect in a new tab"),
|
||||
icon_class: 'client-connect',
|
||||
callback: () => bookmark_connect(true)
|
||||
callback: () => bookmark_connect(true),
|
||||
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
|
||||
}, contextmenu.Entry.CLOSE(() => {
|
||||
setTimeout(() => {
|
||||
this.htmlTag.find(".btn_bookmark.dropdown-arrow").removeClass("force-show")
|
||||
|
@ -564,10 +582,7 @@ class ControlBar {
|
|||
const mark = <bookmarks.DirectoryBookmark>bookmark;
|
||||
const container = $.spawn("div").addClass("sub-menu dropdown");
|
||||
|
||||
for(const member of mark.content)
|
||||
container.append(build_entry(member));
|
||||
|
||||
return $.spawn("div")
|
||||
const result = $.spawn("div")
|
||||
.addClass("directory")
|
||||
.append(
|
||||
$.spawn("div").addClass("icon client-folder")
|
||||
|
@ -583,7 +598,13 @@ class ControlBar {
|
|||
.append(
|
||||
$.spawn("div").addClass("sub-container")
|
||||
.append(container)
|
||||
)
|
||||
);
|
||||
|
||||
/* we've to keep it this order because we're then keeping the reference of the loading icons... */
|
||||
for(const member of mark.content)
|
||||
container.append(build_entry(member));
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 managers: InfoManagerBase[] = [];
|
||||
private banner_manager: Hostbanner;
|
||||
|
||||
constructor(client: ConnectionHandler) {
|
||||
this.handle = client;
|
||||
|
@ -74,8 +73,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
this.managers.push(new ChannelInfoManager());
|
||||
this.managers.push(new ServerInfoManager());
|
||||
|
||||
this.banner_manager = new Hostbanner(client, this._tag_banner);
|
||||
|
||||
this._tag.find("button.close").on('click', () => this.close_popover());
|
||||
}
|
||||
|
||||
|
@ -83,6 +80,16 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
return this._tag;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._tag && this._tag.remove();
|
||||
this._tag = undefined;
|
||||
|
||||
this.managers = undefined;
|
||||
this._current_manager = undefined;
|
||||
|
||||
this.current_selected = undefined;
|
||||
}
|
||||
|
||||
handle_resize() {
|
||||
/* test if the popover isn't a popover anymore */
|
||||
if(this._tag.parent().hasClass('shown')) {
|
||||
|
@ -90,8 +97,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
if(this.is_popover())
|
||||
this._tag.parent().addClass('shown');
|
||||
}
|
||||
|
||||
this.banner_manager.handle_resize();
|
||||
}
|
||||
|
||||
setCurrentSelected(entry: AvailableTypes) {
|
||||
|
@ -126,10 +131,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
(this._current_manager as InfoManager<AvailableTypes>).updateFrame(this.current_selected, this._tag_info);
|
||||
}
|
||||
|
||||
update_banner() {
|
||||
this.banner_manager.update();
|
||||
}
|
||||
|
||||
current_manager() { return this._current_manager; }
|
||||
|
||||
is_popover() : boolean {
|
||||
|
@ -138,7 +139,6 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
|
||||
open_popover() {
|
||||
this._tag.parent().toggleClass('shown', true);
|
||||
this.banner_manager.handle_resize();
|
||||
}
|
||||
|
||||
close_popover() {
|
||||
|
@ -155,161 +155,6 @@ interface Window {
|
|||
HTMLImageElement: typeof HTMLImageElement;
|
||||
}
|
||||
|
||||
class Hostbanner {
|
||||
readonly html_tag: JQuery<HTMLElement>;
|
||||
readonly client: ConnectionHandler;
|
||||
|
||||
private updater: NodeJS.Timer;
|
||||
private _hostbanner_url: string;
|
||||
|
||||
constructor(client: ConnectionHandler, htmlTag: JQuery<HTMLElement>) {
|
||||
this.client = client;
|
||||
this.html_tag = htmlTag;
|
||||
}
|
||||
|
||||
update() {
|
||||
if(this.updater) {
|
||||
clearTimeout(this.updater);
|
||||
this.updater = undefined;
|
||||
}
|
||||
|
||||
const tag = this.generate_tag();
|
||||
|
||||
if(tag) {
|
||||
tag.then(element => {
|
||||
const children = this.html_tag.children();
|
||||
this.html_tag.append(element).removeClass("disabled");
|
||||
|
||||
/* allow the new image be loaded from cache URL */
|
||||
{
|
||||
children
|
||||
.css('z-index', '2')
|
||||
.css('position', 'absolute')
|
||||
.css('height', '100%')
|
||||
.css('width', '100%');
|
||||
setTimeout(() => {
|
||||
children.detach();
|
||||
}, 250);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn(tr("Failed to load hostbanner: %o"), error);
|
||||
this.html_tag.empty().addClass("disabled");
|
||||
})
|
||||
} else {
|
||||
this.html_tag.empty().addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
handle_resize() {
|
||||
this.html_tag.find("[x-divider-require-resize]").trigger('resize');
|
||||
}
|
||||
|
||||
private generate_tag?() : Promise<JQuery<HTMLElement>> {
|
||||
if(!this.client.connected) return undefined;
|
||||
|
||||
const server = this.client.channelTree.server;
|
||||
if(!server) return undefined;
|
||||
if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined;
|
||||
|
||||
let properties: any = {};
|
||||
for(let key in server.properties)
|
||||
properties["property_" + key] = server.properties[key];
|
||||
|
||||
properties["hostbanner_gfx_url"] = server.properties.virtualserver_hostbanner_gfx_url;
|
||||
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) {
|
||||
const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60);
|
||||
const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString();
|
||||
try {
|
||||
const url = new URL(server.properties.virtualserver_hostbanner_gfx_url);
|
||||
if(url.search.length == 0)
|
||||
properties["hostbanner_gfx_url"] += "?_ts=" + update_timestamp;
|
||||
else
|
||||
properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to parse banner URL: %o"), error);
|
||||
properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
|
||||
}
|
||||
|
||||
this.updater = setTimeout(() => this.update(), update_interval * 1000);
|
||||
}
|
||||
|
||||
const rendered = $("#tmpl_selected_hostbanner").renderTag(properties);
|
||||
|
||||
/* ration watcher */
|
||||
if(server.properties.virtualserver_hostbanner_mode == 2) {
|
||||
const jimage = rendered.find(".meta-image");
|
||||
if(jimage.length == 0) {
|
||||
log.warn(LogCategory.SERVER, tr("Missing hostbanner meta image tag"));
|
||||
} else {
|
||||
const image = jimage[0];
|
||||
image.onload = event => {
|
||||
const image: HTMLImageElement = jimage[0] as any;
|
||||
rendered.on('resize', event => {
|
||||
const container = rendered.parent();
|
||||
container.css('height', null);
|
||||
container.css('flex-grow', '1');
|
||||
|
||||
const max_height = rendered.visible_height();
|
||||
const max_width = rendered.visible_width();
|
||||
container.css('flex-grow', '0');
|
||||
|
||||
|
||||
const original_height = image.naturalHeight;
|
||||
const original_width = image.naturalWidth;
|
||||
|
||||
const ratio_height = max_height / original_height;
|
||||
const ratio_width = max_width / original_width;
|
||||
|
||||
const ratio = Math.min(ratio_height, ratio_width);
|
||||
|
||||
if(ratio == 0)
|
||||
return;
|
||||
const hostbanner_height = ratio * original_height;
|
||||
container.css('height', Math.ceil(hostbanner_height) + "px");
|
||||
/* the width is ignorable*/
|
||||
});
|
||||
setTimeout(() => rendered.trigger('resize'), 100);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if(window.fetch) {
|
||||
return (async () => {
|
||||
const start = Date.now();
|
||||
|
||||
const tag_image = rendered.find(".hostbanner-image");
|
||||
|
||||
_fetch:
|
||||
try {
|
||||
const result = await fetch(properties["hostbanner_gfx_url"]);
|
||||
|
||||
if(!result.ok) {
|
||||
if(result.type === 'opaque' || result.type === 'opaqueredirect') {
|
||||
log.warn(LogCategory.SERVER, tr("Could not load hostbanner because 'Access-Control-Allow-Origin' isnt valid!"));
|
||||
break _fetch;
|
||||
}
|
||||
}
|
||||
|
||||
if(this._hostbanner_url) {
|
||||
log.debug(LogCategory.SERVER, tr("Revoked old hostbanner url %s"), this._hostbanner_url);
|
||||
URL.revokeObjectURL(this._hostbanner_url);
|
||||
}
|
||||
const url = (this._hostbanner_url = URL.createObjectURL(await result.blob()));
|
||||
tag_image.css('background-image', 'url(' + url + ')');
|
||||
tag_image.attr('src', url);
|
||||
log.debug(LogCategory.SERVER, tr("Fetsched hostbanner successfully (%o, type: %o, url: %o)"), Date.now() - start, result.type, url);
|
||||
} catch(error) {
|
||||
log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), error);
|
||||
}
|
||||
return rendered;
|
||||
})();
|
||||
} else {
|
||||
console.debug(tr("Hostbanner has been loaded"));
|
||||
return Promise.resolve(rendered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClientInfoManager extends InfoManager<ClientEntry> {
|
||||
available<V>(object: V): boolean {
|
||||
return typeof object == "object" && object instanceof ClientEntry;
|
||||
|
|
|
@ -93,12 +93,19 @@ namespace MessageHelper {
|
|||
const result: xbbcode.Result = xbbcode.parse(message, {
|
||||
/* TODO make this configurable and allow IMG */
|
||||
tag_whitelist: [
|
||||
"b",
|
||||
"i",
|
||||
"u",
|
||||
"b", "big",
|
||||
"i", "italic",
|
||||
"u", "underlined",
|
||||
"color",
|
||||
"url"
|
||||
]
|
||||
"url",
|
||||
"code",
|
||||
"icode",
|
||||
"i-code",
|
||||
|
||||
"ul", "ol", "list",
|
||||
"li",
|
||||
/* "img" */
|
||||
] //[img]https://i.ytimg.com/vi/kgeSTkZssPg/maxresdefault.jpg[/img]
|
||||
});
|
||||
/*
|
||||
if(result.error) {
|
||||
|
@ -106,470 +113,58 @@ namespace MessageHelper {
|
|||
return formatElement(message);
|
||||
}
|
||||
*/
|
||||
return [$.spawn("div").html(result.build_html()).contents() as any];
|
||||
|
||||
let html = result.build_html();
|
||||
|
||||
if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
|
||||
html = twemoji.parse(html);
|
||||
|
||||
const container = $.spawn("div");
|
||||
container[0].innerHTML = DOMPurify.sanitize(html, {
|
||||
ADD_ATTR: [
|
||||
"x-highlight-type",
|
||||
"x-code-type"
|
||||
]
|
||||
});
|
||||
|
||||
container.find("a").attr('target', "_blank");
|
||||
|
||||
return [container.contents() as JQuery];
|
||||
//return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry));
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessage {
|
||||
date: Date;
|
||||
message: JQuery[];
|
||||
private _html_tag: JQuery<HTMLElement>;
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode code tag init",
|
||||
function: async () => {
|
||||
/* override default parser */
|
||||
xbbcode.register.register_parser( {
|
||||
tag: ["code", "icode", "i-code"],
|
||||
content_tags_whitelist: [],
|
||||
|
||||
constructor(message: JQuery[]) {
|
||||
this.date = new Date();
|
||||
this.message = message;
|
||||
}
|
||||
build_html(layer: xbbcode.TagLayer) : string {
|
||||
const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
|
||||
const language = (layer.options || "").replace("\"", "'").toLowerCase();
|
||||
|
||||
private num(num: number) : string {
|
||||
let str = num.toString();
|
||||
while(str.length < 2) str = '0' + str;
|
||||
return str;
|
||||
}
|
||||
/* remove heading empty lines */
|
||||
let text = layer.content.map(e => e.build_text())
|
||||
.reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "")
|
||||
.replace(/^([ \n\r\t]*)(?=\n)+/g, "");
|
||||
if(text.startsWith("\r") || text.startsWith("\n"))
|
||||
text = text.substr(1);
|
||||
|
||||
get html_tag() {
|
||||
if(this._html_tag) return this._html_tag;
|
||||
let result: HighlightJSResult;
|
||||
if(window.hljs.getLanguage(language))
|
||||
result = window.hljs.highlight(language, text, true);
|
||||
else
|
||||
result = window.hljs.highlightAuto(text);
|
||||
|
||||
let tag = $.spawn("div");
|
||||
tag.addClass("message");
|
||||
|
||||
let dateTag = $.spawn("div");
|
||||
dateTag.text("<" + this.num(this.date.getUTCHours()) + ":" + this.num(this.date.getUTCMinutes()) + ":" + this.num(this.date.getUTCSeconds()) + "> ");
|
||||
dateTag.css("margin-right", "4px");
|
||||
dateTag.css("color", "dodgerblue");
|
||||
|
||||
this._html_tag = tag;
|
||||
tag.append(dateTag);
|
||||
this.message.forEach(e => e.appendTo(tag));
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
class ChatEntry {
|
||||
readonly handle: ChatBox;
|
||||
type: ChatType;
|
||||
key: string;
|
||||
history: ChatMessage[] = [];
|
||||
send_history: string[] = [];
|
||||
|
||||
owner_unique_id?: string;
|
||||
|
||||
private _name: string;
|
||||
private _html_tag: any;
|
||||
|
||||
private _flag_closeable: boolean = true;
|
||||
private _flag_unread : boolean = false;
|
||||
private _flag_offline: boolean = false;
|
||||
|
||||
onMessageSend: (text: string) => void;
|
||||
onClose: () => boolean = () => true;
|
||||
|
||||
constructor(handle, type : ChatType, key) {
|
||||
this.handle = handle;
|
||||
this.type = type;
|
||||
this.key = key;
|
||||
this._name = key;
|
||||
}
|
||||
|
||||
appendError(message: string, ...args) {
|
||||
let entries = MessageHelper.formatMessage(message, ...args);
|
||||
entries.forEach(e => e.css("color", "red"));
|
||||
this.pushChatMessage(new ChatMessage(entries));
|
||||
}
|
||||
|
||||
appendMessage(message : string, fmt: boolean = true, ...args) {
|
||||
this.pushChatMessage(new ChatMessage(MessageHelper.formatMessage(message, ...args)));
|
||||
}
|
||||
|
||||
private pushChatMessage(entry: ChatMessage) {
|
||||
this.history.push(entry);
|
||||
while(this.history.length > 100) {
|
||||
let elm = this.history.pop_front();
|
||||
elm.html_tag.animate({opacity: 0}, 200, function () {
|
||||
$(this).detach();
|
||||
});
|
||||
}
|
||||
if(this.handle.activeChat === this) {
|
||||
let box = $(this.handle.htmlTag).find(".messages");
|
||||
let mbox = $(this.handle.htmlTag).find(".message_box");
|
||||
let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height();
|
||||
mbox.append(entry.html_tag);
|
||||
entry.html_tag.css("opacity", "0").animate({opacity: 1}, 100);
|
||||
if(bottom) box.scrollTop(mbox.height());
|
||||
} else {
|
||||
this.flag_unread = true;
|
||||
}
|
||||
}
|
||||
|
||||
displayHistory() {
|
||||
this.flag_unread = false;
|
||||
let box = this.handle.htmlTag.find(".messages");
|
||||
let mbox = box.find(".message_box").detach(); /* detach the message box to improve performance */
|
||||
mbox.empty();
|
||||
|
||||
for(let e of this.history) {
|
||||
mbox.append(e.html_tag);
|
||||
/* TODO Is this really totally useless?
|
||||
Because its at least a performance bottleneck because is(...) recalculates the page style
|
||||
if(e.htmlTag.is(":hidden"))
|
||||
e.htmlTag.show();
|
||||
*/
|
||||
}
|
||||
|
||||
mbox.appendTo(box);
|
||||
box.scrollTop(mbox.height());
|
||||
}
|
||||
|
||||
get html_tag() {
|
||||
if(this._html_tag)
|
||||
return this._html_tag;
|
||||
|
||||
let tag = $.spawn("div");
|
||||
tag.addClass("chat");
|
||||
if(this._flag_unread)
|
||||
tag.addClass('unread');
|
||||
if(this._flag_offline)
|
||||
tag.addClass('offline');
|
||||
if(this._flag_closeable)
|
||||
tag.addClass('closeable');
|
||||
|
||||
tag.append($.spawn("div").addClass("chat-type icon " + this.chat_icon()));
|
||||
tag.append($.spawn("a").addClass("name").text(this._name));
|
||||
|
||||
let tag_close = $.spawn("div");
|
||||
tag_close.addClass("btn_close icon client-tab_close_button");
|
||||
if(!this._flag_closeable) tag_close.hide();
|
||||
tag.append(tag_close);
|
||||
|
||||
tag.click(() => { this.handle.activeChat = this; });
|
||||
tag.on("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let actions: contextmenu.MenuEntry[] = [];
|
||||
actions.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "",
|
||||
name: tr("Clear"),
|
||||
callback: () => {
|
||||
this.history = [];
|
||||
this.displayHistory();
|
||||
let html = '<pre class="' + klass + '">';
|
||||
html += '<code class="hljs" x-code-type="' + language + '" x-highlight-type="' + result.language + '">';
|
||||
html += result.value;
|
||||
return html + "</code></pre>";
|
||||
}
|
||||
});
|
||||
if(this.flag_closeable) {
|
||||
actions.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-tab_close_button",
|
||||
name: tr("Close"),
|
||||
callback: () => this.handle.deleteChat(this)
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-tab_close_button",
|
||||
name: tr("Close all private tabs"),
|
||||
callback: () => {
|
||||
//TODO Implement this?
|
||||
},
|
||||
visible: false
|
||||
});
|
||||
contextmenu.spawn_context_menu(e.pageX, e.pageY, ...actions);
|
||||
});
|
||||
|
||||
tag_close.click(() => {
|
||||
if($.isFunction(this.onClose) && !this.onClose())
|
||||
return;
|
||||
|
||||
this.handle.deleteChat(this);
|
||||
});
|
||||
|
||||
return this._html_tag = tag;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.handle.activeChat = this;
|
||||
this.handle.htmlTag.find(".input_box").focus();
|
||||
}
|
||||
|
||||
set name(newName : string) {
|
||||
this._name = newName;
|
||||
this.html_tag.find(".name").text(this._name);
|
||||
}
|
||||
|
||||
set flag_closeable(flag : boolean) {
|
||||
if(this._flag_closeable == flag) return;
|
||||
|
||||
this._flag_closeable = flag;
|
||||
|
||||
this.html_tag.toggleClass('closeable', flag);
|
||||
}
|
||||
|
||||
set flag_unread(flag : boolean) {
|
||||
if(this._flag_unread == flag) return;
|
||||
this._flag_unread = flag;
|
||||
this.html_tag.find(".chat-type").attr("class", "chat-type icon " + this.chat_icon());
|
||||
this.html_tag.toggleClass('unread', flag);
|
||||
}
|
||||
|
||||
get flag_offline() { return this._flag_offline; }
|
||||
|
||||
set flag_offline(flag: boolean) {
|
||||
if(flag == this._flag_offline)
|
||||
return;
|
||||
|
||||
this._flag_offline = flag;
|
||||
this.html_tag.toggleClass('offline', flag);
|
||||
}
|
||||
|
||||
private chat_icon() : string {
|
||||
if(this._flag_unread) {
|
||||
switch (this.type) {
|
||||
case ChatType.CLIENT:
|
||||
return "client-new_chat";
|
||||
}
|
||||
}
|
||||
switch (this.type) {
|
||||
case ChatType.SERVER:
|
||||
return "client-server_log";
|
||||
case ChatType.CHANNEL:
|
||||
return "client-channel_chat";
|
||||
case ChatType.CLIENT:
|
||||
return "client-player_chat";
|
||||
case ChatType.GENERAL:
|
||||
return "client-channel_chat";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ChatBox {
|
||||
//https://regex101.com/r/YQbfcX/2
|
||||
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
|
||||
static readonly URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
|
||||
|
||||
readonly connection_handler: ConnectionHandler;
|
||||
htmlTag: JQuery;
|
||||
chats: ChatEntry[];
|
||||
private _activeChat: ChatEntry;
|
||||
private _history_index: number = 0;
|
||||
|
||||
private _button_send: JQuery;
|
||||
private _input_message: JQuery;
|
||||
|
||||
constructor(connection_handler: ConnectionHandler) {
|
||||
this.connection_handler = connection_handler;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.htmlTag = $("#tmpl_frame_chat").renderTag();
|
||||
this._button_send = this.htmlTag.find(".button-send");
|
||||
this._input_message = this.htmlTag.find(".input-message");
|
||||
|
||||
this._button_send.click(this.onSend.bind(this));
|
||||
this._input_message.on('keypress',event => {
|
||||
if(!event.shiftKey) {
|
||||
console.log(event.keyCode);
|
||||
if(event.keyCode == KeyCode.KEY_RETURN) {
|
||||
this.onSend();
|
||||
return false;
|
||||
} else if(event.keyCode == KeyCode.KEY_UP || event.keyCode == KeyCode.KEY_DOWN) {
|
||||
if(this._activeChat) {
|
||||
const message = (this._input_message.val() || "").toString();
|
||||
const history = this._activeChat.send_history;
|
||||
|
||||
if(history.length == 0 || this._history_index > history.length)
|
||||
return;
|
||||
|
||||
if(message.replace(/[ \n\r\t]/, "").length == 0 || this._history_index == 0 || (this._history_index > 0 && message == this._activeChat.send_history[this._history_index - 1])) {
|
||||
if(event.keyCode == KeyCode.KEY_UP)
|
||||
this._history_index = Math.min(history.length, this._history_index + 1);
|
||||
else
|
||||
this._history_index = Math.max(0, this._history_index - 1);
|
||||
|
||||
if(this._history_index > 0)
|
||||
this._input_message.val(this._activeChat.send_history[this._history_index - 1]);
|
||||
else
|
||||
this._input_message.val("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).on('input', (event) => {
|
||||
let text = $(event.target).val().toString();
|
||||
if(this.testMessage(text))
|
||||
this._button_send.removeAttr("disabled");
|
||||
else
|
||||
this._button_send.attr("disabled", "true");
|
||||
}).trigger("input");
|
||||
|
||||
this.chats = [];
|
||||
this._activeChat = undefined;
|
||||
|
||||
this.createChat("chat_server", ChatType.SERVER).onMessageSend = (text: string) => {
|
||||
if(!this.connection_handler.serverConnection) {
|
||||
this.serverChat().appendError(tr("Could not send chat message (Not connected)"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.connection_handler.serverConnection.command_helper.sendMessage(text, ChatType.SERVER).catch(error => {
|
||||
if(error instanceof CommandResult)
|
||||
return;
|
||||
|
||||
this.serverChat().appendMessage(tr("Failed to send text message."));
|
||||
log.error(LogCategory.GENERAL, tr("Failed to send server text message: %o"), error);
|
||||
});
|
||||
};
|
||||
this.serverChat().name = tr("Server chat");
|
||||
this.serverChat().flag_closeable = false;
|
||||
|
||||
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
|
||||
if(!this.connection_handler.serverConnection) {
|
||||
this.channelChat().appendError(tr("Could not send chant message (Not connected)"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.connection_handler.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, this.connection_handler.getClient().currentChannel()).catch(error => {
|
||||
this.channelChat().appendMessage(tr("Failed to send text message."));
|
||||
log.error(LogCategory.GENERAL, tr("Failed to send channel text message: %o"), error);
|
||||
});
|
||||
};
|
||||
this.channelChat().name = tr("Channel chat");
|
||||
this.channelChat().flag_closeable = false;
|
||||
|
||||
this.connection_handler.permissions.initializedListener.push(flag => {
|
||||
if(flag) this.activeChat0(this._activeChat);
|
||||
});
|
||||
}
|
||||
|
||||
createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry {
|
||||
let chat = new ChatEntry(this, type, key);
|
||||
this.chats.push(chat);
|
||||
this.htmlTag.find(".chats").append(chat.html_tag);
|
||||
if(!this._activeChat) this.activeChat = chat;
|
||||
return chat;
|
||||
}
|
||||
|
||||
open_chats() : ChatEntry[] {
|
||||
return this.chats;
|
||||
}
|
||||
|
||||
findChat(key : string) : ChatEntry {
|
||||
for(let e of this.chats)
|
||||
if(e.key == key) return e;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
deleteChat(chat : ChatEntry) {
|
||||
this.chats.remove(chat);
|
||||
chat.html_tag.detach();
|
||||
if(this._activeChat === chat) {
|
||||
if(this.chats.length > 0)
|
||||
this.activeChat = this.chats.last();
|
||||
else
|
||||
this.activeChat = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onSend() {
|
||||
let text = this._input_message.val().toString();
|
||||
if(!this.testMessage(text)) return;
|
||||
this._input_message.val("");
|
||||
this._input_message.trigger("input");
|
||||
|
||||
/* preprocessing text */
|
||||
const words = text.split(/[ \n]/);
|
||||
for(let index = 0; index < words.length; index++) {
|
||||
const flag_escaped = words[index].startsWith('!');
|
||||
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
|
||||
|
||||
_try:
|
||||
try {
|
||||
const url = new URL(unescaped);
|
||||
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
|
||||
if(url.protocol !== 'http:' && url.protocol !== 'https:')
|
||||
break _try;
|
||||
if(flag_escaped)
|
||||
words[index] = unescaped;
|
||||
else {
|
||||
text = undefined;
|
||||
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
|
||||
}
|
||||
} catch(e) { /* word isn't an url */ }
|
||||
|
||||
if(unescaped.match(ChatBox.URL_REGEX)) {
|
||||
if(flag_escaped)
|
||||
words[index] = unescaped;
|
||||
else {
|
||||
text = undefined;
|
||||
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text = text || words.join(" ");
|
||||
if(this._activeChat.send_history.length == 0 || this._activeChat.send_history[0] != text)
|
||||
this._activeChat.send_history.unshift(text);
|
||||
while(this._activeChat.send_history.length > 100)
|
||||
this._activeChat.send_history.pop();
|
||||
this._history_index = 0;
|
||||
if(this._activeChat && $.isFunction(this._activeChat.onMessageSend))
|
||||
this._activeChat.onMessageSend(text);
|
||||
}
|
||||
|
||||
set activeChat(chat : ChatEntry) {
|
||||
if(this.chats.indexOf(chat) === -1) return;
|
||||
if(this._activeChat == chat) return;
|
||||
this.activeChat0(chat);
|
||||
}
|
||||
|
||||
private activeChat0(chat: ChatEntry) {
|
||||
this._activeChat = chat;
|
||||
for(let e of this.chats)
|
||||
e.html_tag.removeClass("active");
|
||||
|
||||
let disable_input = !chat;
|
||||
if(this._activeChat) {
|
||||
this._activeChat.html_tag.addClass("active");
|
||||
this._activeChat.displayHistory();
|
||||
|
||||
if(!disable_input && this.connection_handler && this.connection_handler.permissions && this.connection_handler.permissions.initialized())
|
||||
switch (this._activeChat.type) {
|
||||
case ChatType.CLIENT:
|
||||
disable_input = false;
|
||||
break;
|
||||
case ChatType.SERVER:
|
||||
disable_input = !this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
|
||||
break;
|
||||
case ChatType.CHANNEL:
|
||||
disable_input = !this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._input_message.prop("disabled", disable_input);
|
||||
}
|
||||
|
||||
get activeChat() : ChatEntry { return this._activeChat; }
|
||||
|
||||
channelChat() : ChatEntry {
|
||||
return this.findChat("chat_channel");
|
||||
}
|
||||
|
||||
serverChat() {
|
||||
return this.findChat("chat_server");
|
||||
}
|
||||
|
||||
focus(){
|
||||
this._input_message.focus();
|
||||
}
|
||||
|
||||
private testMessage(message: string) : boolean {
|
||||
message = message
|
||||
.replace(/ /gi, "")
|
||||
.replace(/<br>/gi, "")
|
||||
.replace(/\n/gi, "")
|
||||
.replace(/<br\/>/gi, "");
|
||||
return message.length > 0;
|
||||
}
|
||||
priority: 10
|
||||
})
|
||||
}
|
|
@ -7,6 +7,7 @@ class ServerConnectionManager {
|
|||
|
||||
private _container_log_server: JQuery;
|
||||
private _container_channel_tree: JQuery;
|
||||
private _container_hostbanner: JQuery;
|
||||
private _container_select_info: JQuery;
|
||||
private _container_chat: JQuery;
|
||||
|
||||
|
@ -32,6 +33,7 @@ class ServerConnectionManager {
|
|||
|
||||
this._container_log_server = $("#server-log");
|
||||
this._container_channel_tree = $("#channelTree");
|
||||
this._container_hostbanner = $("#hostbanner");
|
||||
this._container_select_info = $("#select_info");
|
||||
this._container_chat = $("#chat");
|
||||
|
||||
|
@ -52,7 +54,7 @@ class ServerConnectionManager {
|
|||
|
||||
destroy_server_connection_handler(handler: ConnectionHandler) {
|
||||
this.connection_handlers.remove(handler);
|
||||
handler.tag_connection_handler.detach();
|
||||
handler.tag_connection_handler.remove();
|
||||
this._update_scroll();
|
||||
this._tag.toggleClass("shown", this.connection_handlers.length > 1);
|
||||
|
||||
|
@ -64,11 +66,14 @@ class ServerConnectionManager {
|
|||
|
||||
if(handler === this.active_handler)
|
||||
this.set_active_connection_handler(this.connection_handlers[0]);
|
||||
|
||||
/* destroy all elements */
|
||||
handler.destroy();
|
||||
}
|
||||
|
||||
set_active_connection_handler(handler: ConnectionHandler) {
|
||||
if(handler && this.connection_handlers.indexOf(handler) == -1)
|
||||
throw "Handler hasn't been registrated or is already obsolete!";
|
||||
throw "Handler hasn't been registered or is already obsolete!";
|
||||
|
||||
if(this.active_handler)
|
||||
this.active_handler.select_info.close_popover();
|
||||
|
@ -77,19 +82,22 @@ class ServerConnectionManager {
|
|||
this._container_select_info.children().detach();
|
||||
this._container_chat.children().detach();
|
||||
this._container_log_server.children().detach();
|
||||
this._container_hostbanner.children().detach();
|
||||
|
||||
control_bar.set_connection_handler(handler);
|
||||
if(handler) {
|
||||
handler.tag_connection_handler.addClass("active");
|
||||
|
||||
this._container_hostbanner.append(handler.hostbanner.html_tag);
|
||||
this._container_channel_tree.append(handler.channelTree.tag_tree());
|
||||
this._container_select_info.append(handler.select_info.get_tag());
|
||||
this._container_chat.append(handler.chat_frame.html_tag());
|
||||
this._container_chat.append(handler.side_bar.html_tag());
|
||||
this._container_log_server.append(handler.log.html_tag());
|
||||
|
||||
if(handler.invoke_resized_on_activate)
|
||||
handler.resize_elements();
|
||||
}
|
||||
top_menu.update_state();
|
||||
this.active_handler = handler;
|
||||
}
|
||||
|
||||
|
|
|
@ -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_VOICE_SETUP_FAILED = "connection_voice_setup_failed",
|
||||
CONNECTION_COMMAND_ERROR = "connection_command_error",
|
||||
|
||||
GLOBAL_MESSAGE = "global_message",
|
||||
|
||||
SERVER_WELCOME_MESSAGE = "server_welcome_message",
|
||||
SERVER_HOST_MESSAGE = "server_host_message",
|
||||
SERVER_HOST_MESSAGE_DISCONNECT = "server_host_message_disconnect",
|
||||
|
||||
SERVER_CLOSED = "server_closed",
|
||||
SERVER_BANNED = "server_banned",
|
||||
SERVER_REQUIRES_PASSWORD = "server_requires_password",
|
||||
|
||||
CLIENT_VIEW_ENTER = "client_view_enter",
|
||||
CLIENT_VIEW_LEAVE = "client_view_leave",
|
||||
CLIENT_VIEW_MOVE = "client_view_move",
|
||||
|
@ -79,6 +88,14 @@ namespace log {
|
|||
permission: PermissionInfo;
|
||||
}
|
||||
|
||||
export type WelcomeMessage = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type HostMessageDisconnect = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
//tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}")
|
||||
//tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}")
|
||||
//tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}")
|
||||
|
@ -158,14 +175,35 @@ namespace log {
|
|||
reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */
|
||||
}
|
||||
|
||||
export type ConnectionCommandError = {
|
||||
error: any;
|
||||
}
|
||||
|
||||
export type ClientNicknameChanged = {
|
||||
own_action: boolean;
|
||||
own_client: boolean;
|
||||
|
||||
client: base.Client;
|
||||
|
||||
old_name: string;
|
||||
new_name: string;
|
||||
}
|
||||
|
||||
export type ClientNicknameChangeFailed = {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export type ServerClosed = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ServerRequiresPassword = {}
|
||||
|
||||
export type ServerBanned = {
|
||||
message: string;
|
||||
time: number;
|
||||
|
||||
invoker: base.Client;
|
||||
}
|
||||
}
|
||||
|
||||
export type LogMessage = {
|
||||
|
@ -188,11 +226,20 @@ namespace log {
|
|||
"connection_login": event.ConnectionLogin;
|
||||
"connection_connected": event.ConnectionConnected;
|
||||
"connection_voice_setup_failed": event.ConnectionVoiceSetupFailed;
|
||||
"connection_command_error": event.ConnectionCommandError;
|
||||
|
||||
"reconnect_scheduled": event.ReconnectScheduled;
|
||||
"reconnect_canceled": event.ReconnectCanceled;
|
||||
"reconnect_execute": event.ReconnectExecute;
|
||||
|
||||
"server_welcome_message": event.WelcomeMessage;
|
||||
"server_host_message": event.WelcomeMessage;
|
||||
"server_host_message_disconnect": event.HostMessageDisconnect;
|
||||
|
||||
"server_closed": event.ServerClosed;
|
||||
"server_requires_password": event.ServerRequiresPassword;
|
||||
"server_banned": event.ServerBanned;
|
||||
|
||||
"client_view_enter": event.ClientEnter;
|
||||
"client_view_move": event.ClientMove;
|
||||
"client_view_leave": event.ClientLeave;
|
||||
|
@ -208,9 +255,6 @@ namespace log {
|
|||
type MessageBuilder<T extends keyof server.TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined;
|
||||
|
||||
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
|
||||
"global_message": (data: event.GlobalMessage, options) => {
|
||||
return [];
|
||||
},
|
||||
"error_custom": (data: event.ErrorCustom, options) => {
|
||||
return [$.spawn("div").addClass("log-error").text(data.message)]
|
||||
}
|
||||
|
@ -242,7 +286,7 @@ namespace log {
|
|||
}
|
||||
|
||||
this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
log<T extends keyof server.TypeInfo>(type: T, data: server.TypeInfo[T]) {
|
||||
|
@ -263,6 +307,14 @@ namespace log {
|
|||
return this._html_tag;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._html_tag && this._html_tag.remove();
|
||||
this._html_tag = undefined;
|
||||
this._log_container = undefined;
|
||||
|
||||
this._log = undefined;
|
||||
}
|
||||
|
||||
private append_log(message: server.LogMessage) {
|
||||
let container = $.spawn("div").addClass("log-message");
|
||||
|
||||
|
@ -283,7 +335,7 @@ namespace log {
|
|||
MessageHelper.formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container));
|
||||
} else {
|
||||
const elements = builder(message.data, {});
|
||||
if(!elements)
|
||||
if(!elements || elements.length == 0)
|
||||
return; /* discard message */
|
||||
container.append(...elements);
|
||||
}
|
||||
|
@ -297,7 +349,7 @@ namespace log {
|
|||
while(messages.length - index > this.history_length)
|
||||
index++;
|
||||
const hide_elements = messages.filter(idx => idx < index);
|
||||
hide_elements.hide(250, () => hide_elements.detach());
|
||||
hide_elements.hide(250, () => hide_elements.remove());
|
||||
|
||||
if(this.auto_follow)
|
||||
this._html_tag.scrollTop(this._html_tag[0].scrollHeight);
|
||||
|
@ -339,7 +391,7 @@ namespace log {
|
|||
};
|
||||
|
||||
MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => {
|
||||
return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission.name).map(e => e.addClass("log-error"));
|
||||
return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error"));
|
||||
};
|
||||
|
||||
MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => {
|
||||
|
@ -442,6 +494,26 @@ namespace log {
|
|||
return MessageHelper.formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : "");
|
||||
}
|
||||
return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.message + ")")];
|
||||
};
|
||||
|
||||
MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => {
|
||||
return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]");
|
||||
};
|
||||
|
||||
MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => {
|
||||
return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]");
|
||||
};
|
||||
|
||||
MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => {
|
||||
if(data.own_client) {
|
||||
return MessageHelper.formatMessage(tr("Nickname successfully changed."));
|
||||
} else {
|
||||
return MessageHelper.formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name);
|
||||
}
|
||||
};
|
||||
|
||||
MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => {
|
||||
return []; /* we do not show global messages within log */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,11 +33,21 @@ namespace htmltags {
|
|||
if(properties.client_id)
|
||||
result = result + "client-id='" + properties.client_id + "' ";
|
||||
|
||||
if(properties.client_unique_id && properties.client_unique_id != "unknown")
|
||||
if(properties.client_unique_id && properties.client_unique_id != "unknown") {
|
||||
try {
|
||||
result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' ";
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to generate client tag attribute 'client-unique-id': %o"), error);
|
||||
}
|
||||
}
|
||||
|
||||
if(properties.client_name)
|
||||
if(properties.client_name) {
|
||||
try {
|
||||
result = result + "client-name='" + encodeURIComponent(properties.client_name) + "' ";
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to generate client tag attribute 'client-name': %o"), error);
|
||||
}
|
||||
}
|
||||
|
||||
/* add the click handler */
|
||||
result += "oncontextmenu='return htmltags.callbacks.callback_context_client($(this));'";
|
||||
|
|
|
@ -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;
|
||||
buffer[index / 2] = (upper_nibble << 4) | lower_nibble;
|
||||
}
|
||||
return base64ArrayBuffer(buffer);
|
||||
return base64_encode_ab(buffer);
|
||||
};
|
||||
|
||||
export const human_file_size = (size: number) => {
|
||||
|
@ -80,7 +80,7 @@ namespace Modals {
|
|||
.css("display", "none")
|
||||
.appendTo($("body"));
|
||||
element[0].click();
|
||||
element.detach();
|
||||
element.remove();
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -216,7 +216,7 @@ namespace Modals {
|
|||
};
|
||||
result.clear = () => {
|
||||
entries = [];
|
||||
modal.htmlTag.find(".entry-container .entries").children().detach();
|
||||
modal.htmlTag.find(".entry-container .entries").children().remove();
|
||||
update_function();
|
||||
};
|
||||
result.modal = modal;
|
||||
|
|
|
@ -252,7 +252,10 @@ namespace Modals {
|
|||
width: 750
|
||||
});
|
||||
|
||||
modal.close_listener.push(() => control_bar.update_bookmarks());
|
||||
modal.close_listener.push(() => {
|
||||
control_bar.update_bookmarks();
|
||||
top_menu.rebuild_bookmarks();
|
||||
});
|
||||
modal.open();
|
||||
}
|
||||
}
|
|
@ -1,7 +1,100 @@
|
|||
/// <reference path="../../ui/elements/modal.ts" />
|
||||
|
||||
//FIXME: Move this shit out of this file!
|
||||
namespace connection_log {
|
||||
//TODO: Save password data
|
||||
export type ConnectionData = {
|
||||
name: string;
|
||||
icon_id: number;
|
||||
country: string;
|
||||
clients_online: number;
|
||||
clients_total: number;
|
||||
|
||||
flag_password: boolean;
|
||||
password_hash: string;
|
||||
}
|
||||
|
||||
export type ConnectionEntry = ConnectionData & {
|
||||
address: { hostname: string; port: number },
|
||||
total_connection: number;
|
||||
|
||||
first_timestamp: number;
|
||||
last_timestamp: number;
|
||||
}
|
||||
|
||||
let _history: ConnectionEntry[] = [];
|
||||
export function log_connect(address: { hostname: string; port: number }) {
|
||||
let entry = _history.find(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port);
|
||||
if(!entry) {
|
||||
_history.push(entry = {
|
||||
last_timestamp: Date.now(),
|
||||
first_timestamp: Date.now(),
|
||||
address: address,
|
||||
clients_online: 0,
|
||||
clients_total: 0,
|
||||
country: 'unknown',
|
||||
name: 'Unknown',
|
||||
icon_id: 0,
|
||||
total_connection: 0,
|
||||
|
||||
flag_password: false,
|
||||
password_hash: undefined
|
||||
});
|
||||
}
|
||||
entry.last_timestamp = Date.now();
|
||||
entry.total_connection++;
|
||||
_save();
|
||||
}
|
||||
|
||||
export function update_address_info(address: { hostname: string; port: number }, data: ConnectionData) {
|
||||
_history.filter(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port).forEach(e => {
|
||||
for(const key of Object.keys(data)) {
|
||||
if(typeof(data[key]) !== "undefined") {
|
||||
e[key] = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
_save();
|
||||
}
|
||||
|
||||
export function update_address_password(address: { hostname: string; port: number }, password_hash: string) {
|
||||
_history.filter(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port).forEach(e => {
|
||||
e.password_hash = password_hash;
|
||||
});
|
||||
_save();
|
||||
}
|
||||
|
||||
function _save() {
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_HISTORY, JSON.stringify(_history));
|
||||
}
|
||||
|
||||
export function history() : ConnectionEntry[] {
|
||||
return _history.sort((a, b) => b.last_timestamp - a.last_timestamp);
|
||||
}
|
||||
|
||||
export function delete_entry(address: { hostname: string; port: number }) {
|
||||
_history = _history.filter(e => !(e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port));
|
||||
_save();
|
||||
}
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: 'connection history load',
|
||||
priority: 1,
|
||||
function: async () => {
|
||||
_history = [];
|
||||
try {
|
||||
_history = JSON.parse(settings.global(Settings.KEY_CONNECT_HISTORY, "[]"));
|
||||
} catch(error) {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to load connection history: {}"), error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
namespace Modals {
|
||||
export function spawnConnectModal(defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) {
|
||||
export function spawnConnectModal(options: {
|
||||
default_connect_new_tab?: boolean /* default false */
|
||||
}, defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) {
|
||||
let selected_profile: profiles.ConnectionProfile;
|
||||
|
||||
const random_id = (() => {
|
||||
|
@ -10,12 +103,41 @@ namespace Modals {
|
|||
return array.join("");
|
||||
})();
|
||||
|
||||
const connect_modal = $("#tmpl_connect").renderTag({
|
||||
const modal = createModal({
|
||||
header: tr("Connect to a server"),
|
||||
body: $("#tmpl_connect").renderTag({
|
||||
client: native_client,
|
||||
forum_path: settings.static("forum_path"),
|
||||
password_id: random_id,
|
||||
multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false)
|
||||
}).modalize((header, body, footer) => {
|
||||
multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
|
||||
default_connect_new_tab: typeof(options.default_connect_new_tab) === "boolean" && options.default_connect_new_tab
|
||||
}),
|
||||
footer: () => undefined,
|
||||
min_width: "25em"
|
||||
});
|
||||
|
||||
modal.htmlTag.find(".modal-body").addClass("modal-connect");
|
||||
|
||||
const container_last_servers = modal.htmlTag.find(".container-last-servers");
|
||||
/* server list toggle */
|
||||
{
|
||||
const button = modal.htmlTag.find(".button-toggle-last-servers");
|
||||
const set_show = shown => {
|
||||
container_last_servers.toggleClass('shown', shown);
|
||||
button.find(".arrow").toggleClass('down', shown).toggleClass('up', !shown);
|
||||
settings.changeGlobal("connect_show_last_servers", shown);
|
||||
};
|
||||
button.on('click', event => {
|
||||
set_show(!container_last_servers.hasClass("shown"));
|
||||
});
|
||||
set_show(settings.static_global("connect_show_last_servers", false));
|
||||
}
|
||||
|
||||
const apply = (header, body, footer) => {
|
||||
const container = modal.htmlTag.find(".container-last-servers .table .body");
|
||||
const container_empty = container.find(".body-empty");
|
||||
let current_connect_data: connection_log.ConnectionEntry;
|
||||
|
||||
const button_connect = footer.find(".button-connect");
|
||||
const button_connect_tab = footer.find(".button-connect-new-tab");
|
||||
const button_manage = body.find(".button-manage-profiles");
|
||||
|
@ -25,7 +147,12 @@ namespace Modals {
|
|||
const input_nickname = body.find(".container-nickname input");
|
||||
const input_password = body.find(".container-password input");
|
||||
|
||||
let updateFields = function () {
|
||||
let updateFields = (reset_current_data: boolean) => {
|
||||
if(reset_current_data) {
|
||||
current_connect_data = undefined;
|
||||
container.find(".selected").removeClass("selected");
|
||||
}
|
||||
|
||||
console.log("Updating");
|
||||
if(selected_profile)
|
||||
input_nickname.attr("placeholder", selected_profile.default_username);
|
||||
|
@ -34,7 +161,7 @@ namespace Modals {
|
|||
|
||||
let address = input_address.val().toString();
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address);
|
||||
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN);
|
||||
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN);
|
||||
|
||||
let nickname = input_nickname.val().toString();
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname);
|
||||
|
@ -50,17 +177,14 @@ namespace Modals {
|
|||
|
||||
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
|
||||
input_address
|
||||
.on("keyup", () => updateFields())
|
||||
.on("keyup", () => updateFields(true))
|
||||
.on('keydown', event => {
|
||||
if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey)
|
||||
button_connect.trigger('click');
|
||||
});
|
||||
|
||||
button_manage.on('click', event => {
|
||||
const modal = Modals.spawnSettingsModal();
|
||||
setTimeout(() => {
|
||||
modal.htmlTag.find(".tab-profiles").parent(".entry").trigger('click');
|
||||
}, 100);
|
||||
const modal = Modals.spawnSettingsModal("identity-profiles");
|
||||
modal.close_listener.push(() => {
|
||||
input_profile.trigger('change');
|
||||
});
|
||||
|
@ -82,7 +206,7 @@ namespace Modals {
|
|||
input_nickname.val(selected_profile.default_username);
|
||||
}
|
||||
input_profile.toggleClass("is-invalid", !selected_profile || !selected_profile.valid());
|
||||
updateFields();
|
||||
updateFields(true);
|
||||
});
|
||||
input_profile.val(connect_profile && connect_profile.enforce ? connect_profile.profile.id : connect_profile && connect_profile.profile ? connect_profile.profile.id : 'default').trigger('change');
|
||||
}
|
||||
|
@ -90,20 +214,27 @@ namespace Modals {
|
|||
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, last_nickname);
|
||||
|
||||
input_nickname.val(last_nickname);
|
||||
input_nickname.on("keyup", () => updateFields());
|
||||
setTimeout(() => updateFields(), 100);
|
||||
input_nickname.on("keyup", () => updateFields(true));
|
||||
setTimeout(() => updateFields(false), 100);
|
||||
|
||||
const server_address = () => {
|
||||
let address = input_address.val().toString();
|
||||
if(address.match(Regex.IP_V6) && !address.startsWith("["))
|
||||
return "[" + address + "]";
|
||||
return address;
|
||||
};
|
||||
button_connect.on('click', event => {
|
||||
connect_modal.close();
|
||||
modal.close();
|
||||
|
||||
const connection = server_connections.active_connection_handler();
|
||||
if(connection) {
|
||||
connection.startConnection(
|
||||
input_address.val().toString(),
|
||||
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
|
||||
selected_profile,
|
||||
true,
|
||||
{
|
||||
nickname: input_nickname.val().toString() || selected_profile.default_username,
|
||||
password: {password: input_password.val().toString(), hashed: false}
|
||||
password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
@ -111,24 +242,77 @@ namespace Modals {
|
|||
}
|
||||
});
|
||||
button_connect_tab.on('click', event => {
|
||||
connect_modal.close();
|
||||
modal.close();
|
||||
|
||||
const connection = server_connections.spawn_server_connection_handler();
|
||||
server_connections.set_active_connection_handler(connection);
|
||||
connection.startConnection(
|
||||
input_address.val().toString(),
|
||||
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
|
||||
selected_profile,
|
||||
true,
|
||||
{
|
||||
nickname: input_nickname.val().toString() || selected_profile.default_username,
|
||||
password: {password: input_password.val().toString(), hashed: false}
|
||||
password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false}
|
||||
}
|
||||
);
|
||||
});
|
||||
}, {
|
||||
width: '70%'
|
||||
});
|
||||
|
||||
connect_modal.open();
|
||||
|
||||
/* server list show */
|
||||
{
|
||||
for(const entry of connection_log.history().slice(0, 10)) {
|
||||
$.spawn("div").addClass("row").append(
|
||||
$.spawn("div").addClass("column delete").append($.spawn("div").addClass("icon_em client-delete")).on('click', event => {
|
||||
event.preventDefault();
|
||||
|
||||
const row = $(event.target).parents('.row');
|
||||
row.hide(250, () => {
|
||||
row.detach();
|
||||
});
|
||||
connection_log.delete_entry(entry.address);
|
||||
container_empty.toggle(container.children().length > 1);
|
||||
})
|
||||
).append(
|
||||
$.spawn("div").addClass("column name").append([
|
||||
IconManager.generate_tag(IconManager.load_cached_icon(entry.icon_id)),
|
||||
$.spawn("a").text(entry.name)
|
||||
])
|
||||
).append(
|
||||
$.spawn("div").addClass("column address").text(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : ""))
|
||||
).append(
|
||||
$.spawn("div").addClass("column password").text(entry.flag_password ? tr("Yes") : tr("No"))
|
||||
).append(
|
||||
$.spawn("div").addClass("column country-name").append([
|
||||
$.spawn("div").addClass("country flag-" + entry.country.toLowerCase()),
|
||||
$.spawn("a").text(i18n.country_name(entry.country, tr("Global")))
|
||||
])
|
||||
).append(
|
||||
$.spawn("div").addClass("column clients").text(entry.clients_online + "/" + entry.clients_total)
|
||||
).append(
|
||||
$.spawn("div").addClass("column connections").text(entry.total_connection + "")
|
||||
).on('click', event => {
|
||||
if(event.isDefaultPrevented())
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
current_connect_data = entry;
|
||||
container.find(".selected").removeClass("selected");
|
||||
$(event.target).parent('.row').addClass('selected');
|
||||
|
||||
input_address.val(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : ""));
|
||||
input_password.val(entry.password_hash ? "WolverinDEV Yeahr!" : "").trigger('change');
|
||||
}).on('dblclick', event => {
|
||||
current_connect_data = entry;
|
||||
button_connect.trigger('click');
|
||||
}).appendTo(container);
|
||||
container_empty.toggle(false);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
apply(modal.htmlTag, modal.htmlTag, modal.htmlTag);
|
||||
|
||||
modal.open();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,41 +13,67 @@ namespace Modals {
|
|||
});
|
||||
render_properties["channel_icon_tab"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0);
|
||||
render_properties["channel_icon_general"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0);
|
||||
render_properties["create"] = !channel;
|
||||
|
||||
let template = $("#tmpl_channel_edit").renderTag(render_properties);
|
||||
return template.tabify();
|
||||
},
|
||||
footer: () => {
|
||||
let footer = $.spawn("div");
|
||||
footer.addClass("modal-button-group");
|
||||
footer.css("margin", "5px");
|
||||
|
||||
let buttonCancel = $.spawn("button");
|
||||
buttonCancel.text(tr("Cancel")).addClass("button_cancel");
|
||||
/* the tab functionality */
|
||||
{
|
||||
const container_tabs = template.find(".container-advanced");
|
||||
container_tabs.find(".categories .entry").on('click', event => {
|
||||
const entry = $(event.target);
|
||||
|
||||
let buttonOk = $.spawn("button");
|
||||
buttonOk.text(tr("Ok")).addClass("button_ok");
|
||||
container_tabs.find(".bodies > .body").addClass("hidden");
|
||||
container_tabs.find(".categories > .selected").removeClass("selected");
|
||||
|
||||
footer.append(buttonCancel);
|
||||
footer.append(buttonOk);
|
||||
|
||||
return footer;
|
||||
},
|
||||
width: 500
|
||||
entry.addClass("selected");
|
||||
container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden");
|
||||
});
|
||||
|
||||
container_tabs.find(".entry").first().trigger('click');
|
||||
}
|
||||
|
||||
applyGeneralListener(connection, properties, modal.htmlTag.find(".general_properties"), modal.htmlTag.find(".button_ok"), channel);
|
||||
applyStandardListener(connection, properties, modal.htmlTag.find(".settings_standard"), modal.htmlTag.find(".button_ok"), parent, !channel);
|
||||
applyPermissionListener(connection, properties, modal.htmlTag.find(".settings_permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
|
||||
applyAudioListener(connection, properties, modal.htmlTag.find(".container-channel-settings-audio"), modal.htmlTag.find(".button_ok"), channel);
|
||||
applyAdvancedListener(connection, properties, modal.htmlTag.find(".settings_advanced"), modal.htmlTag.find(".button_ok"), channel);
|
||||
/* Advanced/normal switch */
|
||||
{
|
||||
const input = template.find(".input-advanced-mode");
|
||||
const container_mode = template.find(".mode-container");
|
||||
const container_advanced = container_mode.find(".container-advanced");
|
||||
const container_simple = container_mode.find(".container-simple");
|
||||
input.on('change', event => {
|
||||
const advanced = input.prop("checked");
|
||||
settings.changeGlobal(Settings.KEY_CHANNEL_EDIT_ADVANCED, advanced);
|
||||
|
||||
container_mode.css("overflow", "hidden");
|
||||
container_advanced.show().toggleClass("hidden", !advanced);
|
||||
container_simple.show().toggleClass("hidden", advanced);
|
||||
|
||||
setTimeout(() => {
|
||||
container_advanced.toggle(advanced);
|
||||
container_simple.toggle(!advanced);
|
||||
container_mode.css("overflow", "visible");
|
||||
}, 300);
|
||||
}).prop("checked", settings.static_global(Settings.KEY_CHANNEL_EDIT_ADVANCED)).trigger('change');
|
||||
}
|
||||
|
||||
return template.tabify().children(); /* the "render" div */
|
||||
},
|
||||
footer: null,
|
||||
width: 500
|
||||
});
|
||||
modal.htmlTag.find(".modal-body").addClass("modal-channel modal-blue");
|
||||
|
||||
|
||||
applyGeneralListener(connection, properties, modal.htmlTag.find(".container-general"), modal.htmlTag.find(".button_ok"), channel);
|
||||
applyStandardListener(connection, properties, modal.htmlTag.find(".container-standard"), modal.htmlTag.find(".container-simple"), parent, channel);
|
||||
applyPermissionListener(connection, properties, modal.htmlTag.find(".container-permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
|
||||
applyAudioListener(connection, properties, modal.htmlTag.find(".container-audio"), modal.htmlTag.find(".container-simple"), channel);
|
||||
applyAdvancedListener(connection, properties, modal.htmlTag.find(".container-misc"), modal.htmlTag.find(".button_ok"), channel);
|
||||
|
||||
let updated: PermissionValue[] = [];
|
||||
modal.htmlTag.find(".button_ok").click(() => {
|
||||
modal.htmlTag.find(".settings_permissions").find("input[permission]").each((index, _element) => {
|
||||
modal.htmlTag.find(".container-permissions").find("input[permission]").each((index, _element) => {
|
||||
let element = $(_element);
|
||||
if(!element.prop("changed")) return;
|
||||
if(element.val() == element.attr("original-value")) return;
|
||||
let permission = permissions.resolveInfo(element.attr("permission"));
|
||||
if(!permission) {
|
||||
log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission"));
|
||||
|
@ -60,9 +86,13 @@ namespace Modals {
|
|||
console.log(tr("Updated permissions %o"), updated);
|
||||
}).click(() => {
|
||||
modal.close();
|
||||
for(const key of Object.keys(channel ? channel.properties : {}))
|
||||
if(channel.properties[key] == properties[key])
|
||||
delete properties[key];
|
||||
callback(properties, updated); //First may create the channel
|
||||
});
|
||||
|
||||
tooltip(modal.htmlTag);
|
||||
modal.htmlTag.find(".button_cancel").click(() => {
|
||||
modal.close();
|
||||
callback();
|
||||
|
@ -92,8 +122,8 @@ namespace Modals {
|
|||
|
||||
tag.find(".button-select-icon").on('click', event => {
|
||||
Modals.spawnIconSelect(connection, id => {
|
||||
const icon_node = tag.find(".button-select-icon").find(".icon-node");
|
||||
icon_node.empty();
|
||||
const icon_node = tag.find(".icon-preview");
|
||||
icon_node.children().remove();
|
||||
icon_node.append(connection.fileManager.icons.generateTag(id));
|
||||
|
||||
console.log("Selected icon ID: %d", id);
|
||||
|
@ -101,6 +131,15 @@ namespace Modals {
|
|||
}, channel ? channel.properties.channel_icon_id : 0);
|
||||
});
|
||||
|
||||
tag.find(".button-icon-remove").on('click', event => {
|
||||
const icon_node = tag.find(".icon-preview");
|
||||
icon_node.children().remove();
|
||||
icon_node.append(connection.fileManager.icons.generateTag(0));
|
||||
|
||||
console.log("Remove channel icon");
|
||||
properties.channel_icon_id = 0;
|
||||
});
|
||||
|
||||
{
|
||||
const channel_password = tag.find(".channel_password");
|
||||
tag.find(".channel_password").change(function (this: HTMLInputElement) {
|
||||
|
@ -120,6 +159,42 @@ namespace Modals {
|
|||
properties.channel_topic = this.value;
|
||||
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1));
|
||||
|
||||
{
|
||||
const container = tag.find(".container-description");
|
||||
const input = container.find("textarea");
|
||||
|
||||
const insert_tag = (open: string, close: string) => {
|
||||
if(input.prop("disabled"))
|
||||
return;
|
||||
|
||||
const node = input[0] as HTMLTextAreaElement;
|
||||
if (node.selectionStart || node.selectionStart == 0) {
|
||||
const startPos = node.selectionStart;
|
||||
const endPos = node.selectionEnd;
|
||||
node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos);
|
||||
node.selectionEnd = endPos + open.length;
|
||||
node.selectionStart = node.selectionEnd;
|
||||
} else {
|
||||
node.value += open + close;
|
||||
node.selectionEnd = node.value.length - close.length;
|
||||
node.selectionStart = node.selectionEnd;
|
||||
}
|
||||
|
||||
input.focus().trigger('change');
|
||||
};
|
||||
|
||||
input.on('change', event => {
|
||||
console.log(tr("Channel description edited: %o"), input.val());
|
||||
properties.channel_description = input.val() as string;
|
||||
});
|
||||
|
||||
container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]'));
|
||||
container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]'));
|
||||
container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]'));
|
||||
container.find(".button-color input").on('change', event => {
|
||||
insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]')
|
||||
})
|
||||
}
|
||||
tag.find(".channel_description").change(function (this: HTMLInputElement) {
|
||||
properties.channel_description = this.value;
|
||||
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1));
|
||||
|
@ -132,9 +207,33 @@ namespace Modals {
|
|||
}
|
||||
}
|
||||
|
||||
function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, parent: ChannelEntry, create: boolean) {
|
||||
tag.find("input[name=\"channel_type\"]").change(function (this: HTMLInputElement) {
|
||||
switch(this.value) {
|
||||
function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, parent: ChannelEntry, channel: ChannelEntry) {
|
||||
/* Channel type */
|
||||
{
|
||||
const input_advanced_type = tag.find("input[name='channel_type']");
|
||||
|
||||
let _in_update = false;
|
||||
const update_simple_type = () => {
|
||||
if(_in_update)
|
||||
return;
|
||||
|
||||
let type;
|
||||
if(properties.channel_flag_default || (typeof(properties.channel_flag_default) === "undefined" && channel && channel.properties.channel_flag_default))
|
||||
type = "def";
|
||||
else if(properties.channel_flag_permanent || (typeof(properties.channel_flag_permanent) === "undefined" && channel && channel.properties.channel_flag_permanent))
|
||||
type = "perm";
|
||||
else if(properties.channel_flag_semi_permanent || (typeof(properties.channel_flag_semi_permanent) === "undefined" && channel && channel.properties.channel_flag_semi_permanent))
|
||||
type = "semi";
|
||||
else
|
||||
type = "temp";
|
||||
|
||||
console.log(type);
|
||||
console.log(Object.assign({}, properties));
|
||||
simple.find("option[name='channel-type'][value='" + type + "']").prop("selected", true);
|
||||
};
|
||||
|
||||
input_advanced_type.on('change', event => {
|
||||
switch(input_advanced_type.val()) {
|
||||
case "semi":
|
||||
properties.channel_flag_permanent = false;
|
||||
properties.channel_flag_semi_permanent = true;
|
||||
|
@ -148,46 +247,238 @@ namespace Modals {
|
|||
properties.channel_flag_semi_permanent = false;
|
||||
break;
|
||||
}
|
||||
update_simple_type();
|
||||
});
|
||||
tag.find("input[name=\"channel_type\"][value=\"temp\"]")
|
||||
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1));
|
||||
tag.find("input[name=\"channel_type\"][value=\"semi\"]")
|
||||
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1));
|
||||
tag.find("input[name=\"channel_type\"][value=\"perm\"]")
|
||||
.prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1));
|
||||
if(create)
|
||||
tag.find("input[name=\"channel_type\"]:not(:disabled)").last().prop("checked", true).trigger('change');
|
||||
|
||||
tag.find("input[name=\"channel_default\"]").change(function (this: HTMLInputElement) {
|
||||
console.log(this.checked);
|
||||
properties.channel_flag_default = this.checked;
|
||||
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);
|
||||
|
||||
let elements = tag.find("input[name=\"channel_type\"]");
|
||||
elements.prop("disabled", this.checked);
|
||||
if(this.checked) {
|
||||
elements.prop("checked", false);
|
||||
tag.find("input[name=\"channel_type\"][value=\"perm\"]").prop("checked", true).trigger("change");
|
||||
/* 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');
|
||||
}
|
||||
}).prop("disabled",
|
||||
!connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) ||
|
||||
!connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1));
|
||||
|
||||
tag.find("input[name=\"talk_power\"]").change(function (this: HTMLInputElement) {
|
||||
properties.channel_needed_talk_power = parseInt(this.value);
|
||||
}).prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1));
|
||||
select_default.on('change', event => {
|
||||
const node = select_default[0] as HTMLInputElement;
|
||||
console.log(node.checked);
|
||||
|
||||
let orderTag = tag.find(".order_id");
|
||||
for(let channel of (parent ? parent.children() : connection.channelTree.rootChannel()))
|
||||
$.spawn("option").attr("channelId", channel.channelId.toString()).text(channel.channelName()).appendTo(orderTag);
|
||||
properties.channel_flag_default = node.checked;
|
||||
|
||||
orderTag.change(function (this: HTMLSelectElement) {
|
||||
let selected = $(this.options.item(this.selectedIndex));
|
||||
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);
|
||||
}
|
||||
|
||||
/* simple */
|
||||
{
|
||||
simple.find("option[name='channel-type'][value='def']").prop("disabled", !permission_default);
|
||||
simple.find("option[name='channel-type'][value='perm']").prop("disabled", !permission_perm);
|
||||
simple.find("option[name='channel-type'][value='semi']").prop("disabled", !permission_semi);
|
||||
simple.find("option[name='channel-type'][value='temp']").prop("disabled", !permission_temp);
|
||||
|
||||
simple.find("select[name='channel-type']").on('change', event => {
|
||||
try {
|
||||
_in_update = true;
|
||||
switch ((event.target as HTMLSelectElement).value) {
|
||||
case "temp":
|
||||
properties.channel_flag_permanent = false;
|
||||
properties.channel_flag_semi_permanent = false;
|
||||
properties.channel_flag_default = false;
|
||||
select_default.prop("checked", false).trigger('change');
|
||||
tag_type_temp.trigger('click');
|
||||
break;
|
||||
case "semi":
|
||||
properties.channel_flag_permanent = false;
|
||||
properties.channel_flag_semi_permanent = true;
|
||||
properties.channel_flag_default = false;
|
||||
select_default.prop("checked", false).trigger('change');
|
||||
tag_type_semi.trigger('click');
|
||||
break;
|
||||
case "perm":
|
||||
properties.channel_flag_permanent = true;
|
||||
properties.channel_flag_semi_permanent = false;
|
||||
properties.channel_flag_default = false;
|
||||
select_default.prop("checked", false).trigger('change');
|
||||
tag_type_perm.trigger('click');
|
||||
break;
|
||||
case "def":
|
||||
properties.channel_flag_permanent = true;
|
||||
properties.channel_flag_semi_permanent = false;
|
||||
properties.channel_flag_default = true;
|
||||
select_default.prop("checked", true).trigger('change');
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
_in_update = false;
|
||||
/* We dont need to update the simple type because we changed the advanced part to the just changed simple part */
|
||||
//update_simple_type();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* Talk power */
|
||||
{
|
||||
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1);
|
||||
const input_advanced = tag.find("input[name='talk_power']").prop("disabled", !permission);
|
||||
const input_simple = simple.find("input[name='talk_power']").prop("disabled", !permission);
|
||||
|
||||
input_advanced.on('change', event => {
|
||||
properties.channel_needed_talk_power = parseInt(input_advanced.val() as string);
|
||||
input_simple.val(input_advanced.val());
|
||||
});
|
||||
|
||||
input_simple.on('change', event => {
|
||||
properties.channel_needed_talk_power = parseInt(input_simple.val() as string);
|
||||
input_advanced.val(input_simple.val());
|
||||
});
|
||||
}
|
||||
|
||||
/* Channel order */
|
||||
{
|
||||
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1);
|
||||
|
||||
const advanced_order_id = tag.find(".order_id").prop("disabled", !permission) as JQuery<HTMLSelectElement>;
|
||||
const simple_order_id = simple.find(".order_id").prop("disabled", !permission) as JQuery<HTMLSelectElement>;
|
||||
|
||||
for(let previous_channel of (parent ? parent.children() : connection.channelTree.rootChannel())) {
|
||||
let selected = channel && channel.properties.channel_order == previous_channel.channelId;
|
||||
$.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(advanced_order_id);
|
||||
$.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(simple_order_id);
|
||||
}
|
||||
|
||||
advanced_order_id.on('change', event => {
|
||||
simple_order_id[0].selectedIndex = advanced_order_id[0].selectedIndex;
|
||||
const selected = $(advanced_order_id[0].options.item(advanced_order_id[0].selectedIndex));
|
||||
properties.channel_order = parseInt(selected.attr("channelId"));
|
||||
}).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);
|
||||
});
|
||||
|
||||
simple_order_id.on('change', event => {
|
||||
advanced_order_id[0].selectedIndex = simple_order_id[0].selectedIndex;
|
||||
const selected = $(simple_order_id[0].options.item(simple_order_id[0].selectedIndex));
|
||||
properties.channel_order = parseInt(selected.attr("channelId"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* Advanced only */
|
||||
{
|
||||
const container_max_users = tag.find(".container-max-users");
|
||||
|
||||
const container_unlimited = container_max_users.find(".container-unlimited");
|
||||
const container_limited = container_max_users.find(".container-limited");
|
||||
|
||||
const input_unlimited = container_unlimited.find("input[value='unlimited']");
|
||||
const input_limited = container_limited.find("input[value='limited']");
|
||||
const input_limit = container_limited.find(".channel_maxclients");
|
||||
|
||||
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1);
|
||||
|
||||
if(!permission) {
|
||||
input_unlimited.prop("disabled", true);
|
||||
input_limited.prop("disabled", true);
|
||||
input_limit.prop("disabled", true);
|
||||
|
||||
container_limited.addClass("disabled");
|
||||
container_unlimited.addClass("disabled");
|
||||
} else {
|
||||
container_max_users.find("input[name='max_users']").on('change', event => {
|
||||
const node = event.target as HTMLInputElement;
|
||||
console.log(tr("Channel max user mode: %o"), node.value);
|
||||
|
||||
const flag = node.value === "unlimited";
|
||||
input_limit
|
||||
.prop("disabled", flag)
|
||||
.parent().toggleClass("disabled", flag);
|
||||
properties.channel_flag_maxclients_unlimited = flag;
|
||||
});
|
||||
|
||||
input_limit.on('change', event => {
|
||||
properties.channel_maxclients = parseInt(input_limit.val() as string);
|
||||
console.log(tr("Changed max user limit to %o"), properties.channel_maxclients);
|
||||
});
|
||||
|
||||
setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const container_max_users = tag.find(".container-max-family-users");
|
||||
|
||||
const container_unlimited = container_max_users.find(".container-unlimited");
|
||||
const container_inherited = container_max_users.find(".container-inherited");
|
||||
const container_limited = container_max_users.find(".container-limited");
|
||||
|
||||
const input_unlimited = container_unlimited.find("input[value='unlimited']");
|
||||
const input_inherited = container_inherited.find("input[value='inherited']");
|
||||
const input_limited = container_limited.find("input[value='limited']");
|
||||
const input_limit = container_limited.find(".channel_maxfamilyclients");
|
||||
|
||||
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1);
|
||||
|
||||
if(!permission) {
|
||||
input_unlimited.prop("disabled", true);
|
||||
input_inherited.prop("disabled", true);
|
||||
input_limited.prop("disabled", true);
|
||||
input_limit.prop("disabled", true);
|
||||
|
||||
container_limited.addClass("disabled");
|
||||
container_unlimited.addClass("disabled");
|
||||
container_inherited.addClass("disabled");
|
||||
} else {
|
||||
container_max_users.find("input[name='max_family_users']").on('change', event => {
|
||||
const node = event.target as HTMLInputElement;
|
||||
console.log(tr("Channel max family user mode: %o"), node.value);
|
||||
|
||||
const flag_unlimited = node.value === "unlimited";
|
||||
const flag_inherited = node.value === "inherited";
|
||||
input_limit
|
||||
.prop("disabled", flag_unlimited || flag_inherited)
|
||||
.parent().toggleClass("disabled", flag_unlimited || flag_inherited);
|
||||
properties.channel_flag_maxfamilyclients_unlimited = flag_unlimited;
|
||||
properties.channel_flag_maxfamilyclients_inherited = flag_inherited;
|
||||
});
|
||||
|
||||
input_limit.on('change', event => {
|
||||
properties.channel_maxfamilyclients = parseInt(input_limit.val() as string);
|
||||
console.log(tr("Changed max family user limit to %o"), properties.channel_maxfamilyclients);
|
||||
});
|
||||
|
||||
setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyPermissionListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) {
|
||||
let apply_permissions = (channel_permissions: PermissionValue[]) => {
|
||||
console.log(tr("Got permissions: %o"), channel_permissions);
|
||||
|
@ -200,6 +491,9 @@ namespace Modals {
|
|||
|
||||
tag.find("input[permission]").each((index, _element) => {
|
||||
let element = $(_element);
|
||||
element.attr("original-value", 0);
|
||||
element.val(0);
|
||||
|
||||
let permission = permissions.resolveInfo(element.attr("permission"));
|
||||
if(!permission) {
|
||||
log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission"));
|
||||
|
@ -207,23 +501,16 @@ namespace Modals {
|
|||
return;
|
||||
}
|
||||
|
||||
let old_value: number = 0;
|
||||
element.on("click keyup", () => {
|
||||
console.log(tr("Permission triggered! %o"), element.val() != old_value);
|
||||
element.prop("changed", element.val() != old_value);
|
||||
});
|
||||
|
||||
for(let cperm of channel_permissions)
|
||||
if(cperm.type == permission) {
|
||||
element.val(old_value = cperm.value);
|
||||
element.val(cperm.value);
|
||||
element.attr("original-value", cperm.value);
|
||||
return;
|
||||
}
|
||||
element.val(0);
|
||||
});
|
||||
|
||||
if(!permissions.neededPermission(PermissionType.I_CHANNEL_MODIFY_POWER).granted(required_power, false)) {
|
||||
tag.find("input[permission]").prop("disabled", false); //No permissions
|
||||
}
|
||||
const permission = permissions.neededPermission(PermissionType.I_CHANNEL_MODIFY_POWER).granted(required_power, false);
|
||||
tag.find("input[permission]").prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); //No permissions
|
||||
};
|
||||
|
||||
if(channel) {
|
||||
|
@ -234,7 +521,17 @@ namespace Modals {
|
|||
} else apply_permissions([]);
|
||||
}
|
||||
|
||||
function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
|
||||
function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, channel?: ChannelEntry) {
|
||||
const bandwidth_mapping = [
|
||||
/* SPEEX narrow */ [2.49, 2.69, 2.93, 3.17, 3.17, 3.56, 3.56, 4.05, 4.05, 4.44, 5.22],
|
||||
/* SPEEX wide */ [2.69, 2.93, 3.17, 3.42, 3.76, 4.25, 4.74, 5.13, 5.62, 6.40, 7.37],
|
||||
/* SPEEX ultra */ [2.73, 3.12, 3.37, 3.61, 4.00, 4.49, 4.93, 5.32, 5.81, 6.59, 7.57],
|
||||
/* CELT */ [6.10, 6.10, 7.08, 7.08, 7.08, 8.06, 8.06, 8.06, 8.06, 10.01, 13.92],
|
||||
|
||||
/* Opus Voice */ [2.73, 3.22, 3.71, 4.20, 4.74, 5.22, 5.71, 6.20, 6.74, 7.23, 7.71],
|
||||
/* Opus Music */ [3.08, 3.96, 4.83, 5.71, 6.59, 7.47, 8.35, 9.23, 10.11, 10.99, 11.87]
|
||||
];
|
||||
|
||||
let update_template = () => {
|
||||
let codec = properties.channel_codec;
|
||||
if(!codec && channel)
|
||||
|
@ -246,14 +543,25 @@ namespace Modals {
|
|||
quality = channel.properties.channel_codec_quality;
|
||||
if(!quality) return;
|
||||
|
||||
let template_name = "custom";
|
||||
|
||||
{
|
||||
if(codec == 4 && quality == 4)
|
||||
tag.find("input[name=\"voice_template\"][value=\"voice_mobile\"]").prop("checked", true);
|
||||
template_name = "voice_mobile";
|
||||
else if(codec == 4 && quality == 6)
|
||||
tag.find("input[name=\"voice_template\"][value=\"voice_desktop\"]").prop("checked", true);
|
||||
template_name = "voice_desktop";
|
||||
else if(codec == 5 && quality == 6)
|
||||
tag.find("input[name=\"voice_template\"][value=\"music\"]").prop("checked", true);
|
||||
template_name = "music";
|
||||
}
|
||||
tag.find("input[name='voice_template'][value='" + template_name + "']").prop("checked", true);
|
||||
simple.find("option[name='voice_template'][value='" + template_name + "']").prop("selected", true);
|
||||
|
||||
let bandwidth;
|
||||
if(codec < 0 || codec > bandwidth_mapping.length)
|
||||
bandwidth = 0;
|
||||
else
|
||||
tag.find("input[name=\"voice_template\"][value=\"custom\"]").prop("checked", true);
|
||||
bandwidth = bandwidth_mapping[codec][quality] || 0; /* OOB access results in undefined, but is allowed */
|
||||
tag.find(".container-needed-bandwidth").text(bandwidth.toFixed(2) + " KiB/s");
|
||||
};
|
||||
|
||||
let change_codec = codec => {
|
||||
|
@ -264,20 +572,30 @@ namespace Modals {
|
|||
update_template();
|
||||
};
|
||||
|
||||
let quality_slider = tag.find(".voice_quality_slider");
|
||||
let quality_number = tag.find(".voice_quality_number");
|
||||
const container_quality = tag.find(".container-quality");
|
||||
const slider_quality = sliderfy(container_quality.find(".container-slider"), {
|
||||
initial_value: properties.channel_codec_quality || 6,
|
||||
unit: "",
|
||||
min_value: 1,
|
||||
max_value: 10,
|
||||
step: 1,
|
||||
value_field: container_quality.find(".container-value")
|
||||
});
|
||||
|
||||
let change_quality = (quality: number) => {
|
||||
if(properties.channel_codec_quality == quality) return;
|
||||
|
||||
properties.channel_codec_quality = quality;
|
||||
if(quality_slider.val() != quality)
|
||||
quality_slider.val(quality);
|
||||
if(parseInt(quality_number.text()) != quality)
|
||||
quality_number.text(quality);
|
||||
slider_quality.value(quality);
|
||||
update_template();
|
||||
};
|
||||
|
||||
tag.find("input[name=\"voice_template\"]").change(function (this: HTMLInputElement) {
|
||||
container_quality.find(".container-slider").on('change', event => {
|
||||
properties.channel_codec_quality = slider_quality.value();
|
||||
update_template();
|
||||
});
|
||||
|
||||
tag.find("input[name='voice_template']").change(function (this: HTMLInputElement) {
|
||||
switch(this.value) {
|
||||
case "custom":
|
||||
break;
|
||||
|
@ -295,12 +613,43 @@ namespace Modals {
|
|||
break;
|
||||
}
|
||||
});
|
||||
tag.find("input[name=\"voice_template\"][value=\"voice_mobile\"]")
|
||||
|
||||
simple.find("select[name='voice_template']").change(function (this: HTMLInputElement) {
|
||||
switch(this.value) {
|
||||
case "custom":
|
||||
break;
|
||||
case "music":
|
||||
change_codec(5);
|
||||
change_quality(6);
|
||||
break;
|
||||
case "voice_desktop":
|
||||
change_codec(4);
|
||||
change_quality(6);
|
||||
break;
|
||||
case "voice_mobile":
|
||||
change_codec(4);
|
||||
change_quality(4);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
/* disable not granted templates */
|
||||
{
|
||||
tag.find("input[name='voice_template'][value='voice_mobile']")
|
||||
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
|
||||
tag.find("input[name=\"voice_template\"][value=\"voice_desktop\"]")
|
||||
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=\"music\"]")
|
||||
|
||||
tag.find("input[name='voice_template'][value=\"voice_desktop\"]")
|
||||
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
|
||||
simple.find("option[name='voice_template'][value=\"voice_desktop\"]")
|
||||
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
|
||||
|
||||
tag.find("input[name='voice_template'][value=\"music\"]")
|
||||
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
|
||||
simple.find("option[name='voice_template'][value=\"music\"]")
|
||||
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
|
||||
}
|
||||
|
||||
let codecs = tag.find(".voice_codec option");
|
||||
codecs.eq(0).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8).granted(1));
|
||||
|
@ -323,8 +672,6 @@ namespace Modals {
|
|||
change_quality(channel.properties.channel_codec_quality);
|
||||
}
|
||||
update_template();
|
||||
|
||||
quality_slider.on('input', event => change_quality(parseInt(quality_slider.val() as string)));
|
||||
}
|
||||
|
||||
function applyAdvancedListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
|
||||
|
@ -332,58 +679,26 @@ namespace Modals {
|
|||
properties.channel_topic = this.value;
|
||||
});
|
||||
|
||||
{
|
||||
const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1);
|
||||
tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) {
|
||||
properties.channel_delete_delay = parseInt(this.value);
|
||||
}).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1));
|
||||
}).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission);
|
||||
}
|
||||
|
||||
{
|
||||
tag.find(".button-delete-max").on('click', event => {
|
||||
const power = connection.permissions.neededPermission(PermissionType.I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY).value;
|
||||
let value = power == -2 ? 0 : power == -1 ? (7 * 24 * 60 * 60) : power;
|
||||
tag.find(".channel_delete_delay").val(value).trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1);
|
||||
tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) {
|
||||
properties.channel_codec_is_unencrypted = parseInt(this.value) == 0;
|
||||
}).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1));
|
||||
|
||||
{
|
||||
let tag_infinity = tag.find("input[name=\"max_users\"][value=\"infinity\"]");
|
||||
let tag_limited = tag.find("input[name=\"max_users\"][value=\"limited\"]");
|
||||
let tag_limited_value = tag.find(".channel_maxclients");
|
||||
|
||||
if(!connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) {
|
||||
tag_infinity.prop("disabled", true);
|
||||
tag_limited.prop("disabled", true);
|
||||
tag_limited_value.prop("disabled", true);
|
||||
} else {
|
||||
tag.find("input[name=\"max_users\"]").change(function (this: HTMLInputElement) {
|
||||
console.log(this.value);
|
||||
let infinity = this.value == "infinity";
|
||||
tag_limited_value.prop("disabled", infinity);
|
||||
properties.channel_flag_maxclients_unlimited = infinity;
|
||||
});
|
||||
|
||||
tag_limited_value.change(event => properties.channel_maxclients = parseInt(tag_limited_value.val() as string));
|
||||
tag.find("input[name=\"max_users\"]:checked").trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let tag_inherited = tag.find("input[name=\"max_users_family\"][value=\"inherited\"]");
|
||||
let tag_infinity = tag.find("input[name=\"max_users_family\"][value=\"infinity\"]");
|
||||
let tag_limited = tag.find("input[name=\"max_users_family\"][value=\"limited\"]");
|
||||
let tag_limited_value = tag.find(".channel_maxfamilyclients");
|
||||
|
||||
if(!connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) {
|
||||
tag_inherited.prop("disabled", true);
|
||||
tag_infinity.prop("disabled", true);
|
||||
tag_limited.prop("disabled", true);
|
||||
tag_limited_value.prop("disabled", true);
|
||||
} else {
|
||||
tag.find("input[name=\"max_users_family\"]").change(function (this: HTMLInputElement) {
|
||||
console.log(this.value);
|
||||
tag_limited_value.prop("disabled", this.value != "limited");
|
||||
properties.channel_flag_maxfamilyclients_unlimited = this.value == "infinity";
|
||||
properties.channel_flag_maxfamilyclients_inherited = this.value == "inherited";
|
||||
});
|
||||
|
||||
tag_limited_value.change(event => properties.channel_maxfamilyclients = parseInt(tag_limited_value.val() as string));
|
||||
tag.find("input[name=\"max_users_family\"]:checked").trigger('change');
|
||||
}
|
||||
}).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission);
|
||||
}
|
||||
}
|
||||
}
|