A lot of updates

canary
WolverinDEV 2019-09-19 01:25:57 +02:00
parent 6d2ecc3c69
commit e9384bcf18
32 changed files with 1279 additions and 1030 deletions

View File

@ -68,6 +68,8 @@
flex-direction: row;
justify-content: stretch;
position: relative;
cursor: pointer;
margin-left: 0;
@ -114,39 +116,6 @@
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;
@ -308,6 +277,41 @@
}
}
}
&.channel .container-channel, &.client, &.server {
.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);
}
}
}
}

View File

@ -42,6 +42,7 @@
display: flex;
&.disabled {
pointer-events: none;
background-color: lightgray;
cursor: not-allowed;
}

View File

@ -474,6 +474,10 @@ $client_info_avatar_size: 10em;
//padding: .5em 1em;
color: #565353;
&.type-disconnect_self {
color: #565353; /* not really critical at all */
}
&.type-new {
color: darkred; /* TODO: Evaluate color */
}

View File

@ -141,6 +141,16 @@ $animation_length: .5s;
align-self: center;
}
> span {
display: flex;
flex-direction: row;
justify-content: flex-start;
> a {
margin-right: .5em;
}
}
a[href], a[href]:visited {
color: #353535!important;
}

View File

@ -0,0 +1,293 @@
@import "mixin";
@import "properties";
//TODO: Resize style!
.modal-body.modal-ban-client {
padding: 0!important;
display: flex;
flex-direction: column!important;
justify-content: stretch!important;
//min-width: 30em!important;
max-height: calc(100vh - 10em);
width: 40em;
min-height: 20em;
.container-tooltip {
flex-shrink: 0;
flex-grow: 0;
position: relative;
width: 1.6em;
margin-left: .5em;
margin-right: .25em;
font-size: .9em;
display: flex;
flex-direction: column;
justify-content: center;
img {
height: 1em;
width: 1em;
align-self: center;
font-size: 1.2em;
}
.tooltip {
display: none;
}
}
.container-info {
flex-shrink: 0;
flex-grow: 0;
padding: .5em;
display: flex;
flex-direction: row;
justify-content: stretch;
.container {
flex-grow: 1;
flex-shrink: 1;
min-width: 4em;
width: 10em;
display: flex;
flex-direction: column;
justify-content: stretch;
.title {
text-transform: uppercase;
color: #557edc;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.value {
flex-grow: 1;
flex-shrink: 1;
min-height: 2em;
padding: .5em;
border-radius: 0.2em;
border: 1px solid #111112;
background-color: #121213;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@include chat-scrollbar-vertical();
}
}
}
.container-duration {
margin: 1em;
margin-top: 0em;
display: flex;
flex-direction: column;
justify-content: flex-start;
> a {
flex-grow: 0;
flex-shrink: 0;
}
.container-value {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
.input-boxed.value {
flex-grow: 1;
flex-shrink: 1;
min-width: 6em;
margin-right: 1em;
}
select {
width: 7em;
padding-left: .5em;
}
}
}
.container-reason {
margin: 1em;
margin-top: 0em;
position: relative;
flex-grow: 0;
flex-shrink: 1;
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;
}
}
.container-criteria {
margin: 1em;
margin-top: 0em;
padding: .5em;
border-radius: 0.2em;
border: 1px solid #111112;
background-color: #121213;
.criteria {
display: flex;
flex-direction: row;
justify-content: space-between;
a {
flex-shrink: 1;
min-width: 4em;
text-transform: uppercase;
color: #557edc;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
label {
}
}
}
.container-buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin: 1em;
margin-top: 0em;
button:not(:first-of-type) {
margin-left: 1em;
width: 6em;
}
}
}

View File

@ -1,62 +0,0 @@
.bancreate {
display: flex;
flex-direction: column;
.frame-container {
display: flex;
flex-direction: column;
select.form-control {
height: 2rem!important;
}
.form-row {
margin-right: 0;
margin-left: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
div:first-of-type {
flex-grow: 1;
flex-shrink: 1;
margin-right: 10px;
}
div:nth-of-type(2) {
min-width: 150px;
}
}
.form-group, .form-row {
flex-grow: 0;
flex-shrink: 0;
&.container-reason {
flex-grow: 1;
flex-shrink: 1;
overflow-y: auto;
}
}
.footer {
display: flex;
flex-direction: row;
justify-content: space-between;
.container-global {
display: inline-block;
.input-global {
vertical-align: bottom;
}
}
.container-buttons {
display: inline-block;
}
}
}
}

View File

@ -0,0 +1,161 @@
@import "mixin";
@import "properties";
.modal-body.modal-channel-info {
display: flex;
flex-direction: column;
justify-content: stretch;
min-width: 30em!important;
max-height: calc(100vh - 10em);
padding: 0em!important;
.row {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
padding-top: 1em;
padding-left: .5em;
padding-right: .5em;
.column {
flex-grow: 1;
flex-shrink: 1;
min-width: 6em;
width: 10em;
margin-right: .5em;
margin-left: .5em;
.title {
text-transform: uppercase;
color: #557edc;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.value {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.audio-encrypted {
/* looks better */
.value {
height: 1.6em;
overflow: visible;
}
}
}
}
.container-description {
flex-grow: 1;
flex-shrink: 1;
min-height: 8em; /* description plus title */
display: flex;
flex-direction: column;
justify-content: stretch;
padding-top: 1em;
padding-left: 1em;
padding-right: 1em;
.title {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-grow: 0;
flex-shrink: 0;
text-transform: uppercase;
color: #557edc;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.button-copy {
display: flex;
flex-direction: column;
justify-content: center;
margin-top: .1em; /* looks a bit better */
margin-left: .5em;
border-radius: .2em;
width: 1.3em;
height: 1.3em;
cursor: pointer;
div {
align-self: center;
}
&:hover {
background-color: #313135;
}
@include transition($button_hover_animation_time ease-in-out);
}
}
.value {
display: block;
flex-grow: 1;
flex-shrink: 1;
border-radius: 0.2em;
border: 1px solid #212324;
background-color: #3a3b3f;
padding: .5em;
height: max-content;
min-height: 6em;
max-height: 40em;
overflow-y: auto;
overflow-x: hidden;
@include chat-scrollbar-vertical();
}
.no-value {
flex-grow: 0;
flex-shrink: 0;
font-size: 1.25em;
height: (6em / 1.25); /* min value height and a bit more */
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
color: #666666;
}
}
.container-buttons {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 1em;
}
}

View File

@ -6,13 +6,21 @@
min-width: 25em;
max-height: calc(100vh - 10rem);
min-height: 10em;
width: 30em;
display: flex;
flex-direction: column;
justify-content: stretch;
background-color: #2f2f35;
padding: .5em!important;
.group-assignment-list {
flex-grow: 1;
flex-shrink: 1;
min-height: 6em;
display: flex;
flex-direction: column;
@ -33,12 +41,15 @@
.group-list {
flex-shrink: 1;
flex-grow: 1;
min-height: 4em;
border: none;
border-radius: $border_radius_middle;
padding: 3px;
overflow-y: auto;
border: 1px #161616 solid;
border-radius: $border_radius_middle;
background-color: #28292b;
@include chat-scrollbar-vertical();
.group-entry {
@ -47,7 +58,30 @@
display: flex;
flex-direction: row;
height: max-content;
justify-content: stretch;
height: 1.75em;
> * {
flex-shrink: 0;
flex-grow: 0;
align-self: center;
}
a {
flex-shrink: 1;
flex-grow: 1;
min-width: 6em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
label {
margin-right: .25em;
}
}
.icon-container {
@ -60,79 +94,6 @@
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;
}
}
}
}
}
}

View File

@ -745,7 +745,7 @@ label.disabled > .radio-button, .radio-button.disabled, .radio-button:disabled {
label.disabled > .checkbox, .checkbox:disabled, .checkbox.disabled {
&.checkbox, > .checkbox {
pointer-events: none!important;
background-color: #222227;
background-color: #1a1a1e;
}
}

View File

@ -19,7 +19,7 @@
<meta name="og:description" content="The TeaSpeak Web client is a in the browser running client for the VoIP communication software TeaSpeak." />
<meta name="og:url" content="https://web.teaspeak.de/">
<meta name="og:image" content="https://www.whatsapp.com/img/whatsapp-promo.png">
<!-- WHAT THE HELL? <meta name="og:image" content="https://www.whatsapp.com/img/whatsapp-promo.png"> -->
<!-- TODO Needs some fix -->
<link rel="manifest" href="manifest.json">
@ -208,22 +208,31 @@
serveredit_1.png https://www.hypixel-koo.cf/tsapoijdsadpoijsadsapj.png
serveredit_2.png https://www.hypixel-koo.cf/tsandljsandljsamndoj3oiwejlkjmnlksandljsadmnlmsadnlsa.png
serveredit_3.png https://www.hypixel-koo.cf/toiuhsadouhgdsapoiugdsapouhdsapouhdsaouhwouhwwouhwwoiuhwoihwwoihwoijhwwoknw.png
Query accounts: https://puu.sh/EhvkJ/7551f548e3.png
Channel info: https://puu.sh/EhuVH/1e21540589.png
-->
<!-- <img src="http://puu.sh/E6NXv/eb2f19c7c3.png"> -->
<!-- <img src="http://puu.sh/E9jT6/302912ae34.png"> -->
<!-- <img src="http://puu.sh/E9jTe/b41f6386de.png"> -->
<!-- <img src="img/style/ban-list.png"> -->
<img src="http://puu.sh/E9jTe/b41f6386de.png">
<!-- <img src="http://puu.sh/E9jTe/b41f6386de.png"> -->
<img src="https://puu.sh/EhuVH/1e21540589.png">
</div>
<button class="toggle-spoiler-style" style="height: 30px; width: 100px; z-index: 100000000; position: absolute; bottom: 2px;">toggle style</button>
<script>
setTimeout(() => {
$("#spoiler-style").hide();
$(".toggle-spoiler-style").on('click', () => {
$("#spoiler-style").toggle();
const init = (jQuery) => {
if(typeof jQuery === "undefined") {
setTimeout(() => init($), 1000);
return;
}
jQuery("#spoiler-style").hide();
jQuery(".toggle-spoiler-style").on('click', () => {
jQuery("#spoiler-style").toggle();
});
}, 2500);
};
setTimeout(() => init($), 1000);
</script>
<?php } ?>
</body>

View File

@ -208,11 +208,14 @@
<div class="container-bottom">
<div class="container-server-log" id="server-log"></div>
<div class="container-footer">
<div class="hide-small">
Open source on <a href="https://github.com/TeaSpeak/TeaSpeak-Web"
style="display: inline-block; position: relative">github.com</a>
</div>
<a>{{tr "Version:" /}} {{>app_version}}</a>
<span>
<a>{{tr "Version:" /}} {{>app_version}}</a>
<div class="hide-small">
(Open source on
<a target="_blank" href="https://github.com/TeaSpeak/TeaSpeak-Web"
style="display: inline-block; position: relative">github.com</a>)
</div>
</span>
</div>
</div> <!-- Selection info -->
</div>
@ -2302,442 +2305,6 @@
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_settings_old" type="text/html">
<x-tab>
<x-entry>
<x-tag>{{tr "General"/}}</x-tag>
<x-content>
<div class="settings-general">
<div class="group_box">
<div class="header">{{tr "TeaSpeak Forum Connection" /}}</div>
<div class="content settings-teaspeak-forum">
<div class="not-connected">
<div>{{tr "You're currently not connected with your TeaSpeak Forum account"
/}}
</div>
<button class="btn btn-primary btn-raised button-login">{{tr "login" /}}
</button>
</div>
<!-- TODO new style! -->
<div class="connected">
<div class="connected-info">{{tr "You're connected via TeaSpeak forum" /}}</div>
<div class="container-info-action">
<div class="container-info">
<div class="property username">
<div class="key">{{tr "Username:" /}}</div>
<div class="value">WolverinDEV</div>
</div>
<div class="property premium">
<div class="key">{{tr "Premium:" /}}</div>
<div class="value">YES</div>
</div>
</div>
<div class="divider"></div>
<div class="container-actions">
<button class="button-logout">logout</button>
</div>
</div>
<!--
<div class="property synchronized">
<div class="key">{{tr "Synchronized:" /}}</div>
<div class="value">NO <button class="button-enable-disable-sync">{{tr "enable synchronization" /}}</button></div>
</div>
-->
</div>
</div>
</div>
</div>
</x-content>
</x-entry>
<x-entry>
<x-tag>{{tr "Audio" /}}</x-tag>
<x-content>
<div class="settings_audio">
<div class="settings-device-error alert alert-warning alert-dismissible fade show"
role="alert">
<div class="message"></div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="group_box">
<div class="header">{{tr "Microphone" /}}</div>
<div class="content settings-microphone {{if !voice_available}}disabled{{/if}}">
{{if voice_available}}
<div class="form-row settings-device settings-device-microphone">
<div class="form-group settings-device-select">
<label for="select-settings-microphone-device" class="bmd-label-static">{{tr
"Device:" /}}</label>
<select id="select-settings-microphone-device"
class="form-control audio-select-microphone"></select>
</div>
<div class="form-group bmd-form-group">
<button class="btn btn-secondary button-device-update">update</button>
</div>
</div>
<div class="settings-vad-container">
<div class="group_box">
<div class="header">{{tr "Voice Activity Detection"/}}</div>
<div class="content">
<fieldset>
<div class="custom-control custom-radio">
<input type="radio" id="select-settings-vad-type-1" value="pt"
name="vad_type" class="custom-control-input">
<label class="custom-control-label"
for="select-settings-vad-type-1">{{tr "Always active"
/}}</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="select-settings-vad-type-2" value="vad"
name="vad_type" class="custom-control-input">
<label class="custom-control-label"
for="select-settings-vad-type-2">{{tr "Voice activity
detection"/}}</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="select-settings-vad-type-3" value="ppt"
name="vad_type" class="custom-control-input">
<label class="custom-control-label"
for="select-settings-vad-type-3">{{tr "Push to
talk"/}}</label>
</div>
</fieldset>
</div>
</div>
<div class="settings-vad-impl">
<div class="settings-vad-impl-entry setting-vad-pt">
{{tr "There are no setting entries for an <b>always</b> online voice
detection."/}}
</div>
<div class="settings-vad-impl-entry setting-vad-ppt">
<div class="property ppt-key">
<div class="key">{{tr "Push to talk key:"/}}</div>
<div class="value">
<button type="button"
class="btn btn-raised btn-primary vat_ppt_key">{{tr
"Uninitialised"/}}
</button>
</div>
</div>
<div class="form-group ppt-delay">
<label for="input-settings-ppt-delay" class="bmd-label-static">{{tr
"Key release delay:" /}}</label>
<input id="input-settings-ppt-delay" class="form-control value"
type="number" min="0" max="5000"/>
</div>
</div>
<div class="settings-vad-impl-entry setting-vad-vad">
<div>{{tr "Voice activity threshold (<a
class='vad_vad_slider_value'>20</a>%)"/}}
</div>
<div class="vad_vad_threshold_selector">
<div class="vad_vad_bar">
<div class="container-hider">
<div class="hider vad_vad_bar_filler"></div>
</div>
<input type="range" min="0" max="100" value="50"
class="vad_vad_slider">
</div>
</div>
</div>
</div>
</div>
{{else}}
<div>{{tr "Voice had been disabled" /}}</div>
{{/if}}
</div>
</div>
<div class="group_box">
<div class="header">{{tr "Speaker" /}}</div>
<div class="content settings-speaker">
<div class="settings-device settings-device-speaker">
<div class="form-group settings-device-select">
<label for="select-settings-speaker-device" class="bmd-label-static">{{tr
"Device:" /}}</label>
<select id="select-settings-speaker-device"
class="form-control audio-select-speaker"></select>
</div>
<div class="form-group bmd-form-group">
<button class="btn btn-secondary button-device-update">update</button>
</div>
</div>
<div class="container-master-volume">
<div class="key">Master volume:</div>
<div class="value master-volume">
<input type="range" min="0" max="100" value="100">
<a>(66%)</a>
</div>
</div>
</div>
</div>
<hr>
<div class="group_box sound">
<div class="header">{{tr "Sound Settings" /}}</div>
<div class="content">
<div class="sound-settings">
<div class="property">
<div class="key">Sound Master volume:</div>
<div class="value sound-master-volume">
<input type="range" min="0" max="100" value="100">
<a>(66%)</a>
</div>
</div>
<div class="property">
<div class="key">
{{tr "Overlap same sounds:" /}}
</div>
<div class="value overlap-sounds">
<div class="switch">
<label>
<input type="checkbox" checked>
<div class="help-tip-container"> <!-- lets be absolute -->
<div class="help-tip tip-right tip-small">
<p>
{{tr "This options deferments if a sound overlaps
itself when played twice.<br>" +
"An example would be when you move multiple clients,
you hear that sound n-times.<br>" +
"If this option is disabled, you hear that sound
just once."
/}}
</p>
</div>
</div>
</label>
</div>
</div>
</div>
<div class="property">
<div class="key muted-sounds">
{{tr "Mute sounds when output is muted:" /}}
</div>
<div class="value muted-sounds">
<div class="switch">
<label>
<input type="checkbox" checked>
<div class="help-tip-container"> <!-- lets be absolute -->
<div class="help-tip tip-right tip-small">
<p>
{{tr "Mute all system sounds, when you've muted your
output.<br>If this option isn't disabled you'll
still receive system sounds like 'user joined your
channel'."/}}
</p>
</div>
</div>
</label>
</div>
</div>
</div>
<div class="sound-list">
<div class="sound-list-header">
<div class="column sound-name">{{tr "Name" /}}</div>
<div class="column sound-activated">{{tr "Activated" /}}</div>
</div>
<div class="sound-list-entries-container">
<div class="sound-list-entries">
</div>
</div>
</div>
<div class="form-group sound-list-filter">
<label for="input-settings-sounds-filter" class="bmd-label-floating">{{tr
"Filter" /}}</label>
<input id="input-settings-sounds-filter" class="form-control" type="text"/>
</div>
</div>
</div>
</div>
</div>
</x-content>
</x-entry>
<x-entry>
<x-tag>
<div class="container-tabname-translations">{{tr "Translations" /}}
<div class="country flag-en"></div>
</div>
</x-tag>
<x-content>
<div class="settings-translations">
<div class="group_box">
<div class="header">{{tr "Available translations" /}}</div>
<div class="content">
<div class="setting-list">
<div class="list">
</div>
<div class="management">
<div class="loading">Loading...</div>
<div class="space"></div>
<button class="btn btn-secondary button-add-repository">{{tr "Add
repository" /}}
</button>
</div>
<div class="restart-note">
<p>
{{tr "Attention: These settings get only affected after a restart or
reload!" /}}
</p>
<button class="button-reload">{{tr "reload now" /}}</button>
</div>
</div>
</div>
</div>
</div>
</x-content>
</x-entry>
<x-entry>
<x-tag class="tab-profiles">
{{tr "Profiles" /}}
</x-tag>
<x-content>
<div class="settings-profiles">
<div class="group_box">
<div class="header">{{tr "Available profiles" /}}</div>
<div class="content">
<div class="profile-list">
<div class="list">
</div>
<div class="management">
<button class="btn btn-primary button-set-default">{{tr "Set selected as
default" /}}
</button>
<button class="btn btn-danger button-delete">{{tr "Delete selected" /}}
</button>
<div class="space"></div>
<button class="btn btn-success button-add-profile">{{tr "Create profile"
/}}
</button>
</div>
</div>
</div>
</div>
<div class="group_box">
<div class="header">{{tr "Profile settings" /}}</div>
<div class="content">
<div class="profile-settings">
<div class="general-settings">
<div class="form-group">
<label for="input-settings-profile-name" class="bmd-label-static">{{tr
"Profile name:" /}}</label>
<input id="input-settings-profile-name"
class="form-control setting-name">
</div>
<div class="form-group">
<label for="input-settings-profile-default-name"
class="bmd-label-static">{{tr "Default nickname:" /}}</label>
<input id="input-settings-profile-default-name"
class="form-control setting-default-nickname">
</div>
<div class="form-group">
<!-- TODO use random ids -->
<label for="input-settings-profile-default-password"
class="bmd-label-floating">{{tr "Default server password"
/}}</label>
<input type="password" id="input-settings-profile-default-password"
class="form-control setting-default-password">
</div>
<div class="form-group select-container">
<label for="input-settings-profile-identity-type"
class="bmd-label-static">{{tr "Identify Type:" /}}</label>
<select id="input-settings-profile-identity-type" class="form-control">
<option name="identity-type" value="teaforo">{{tr "Forum Account"
/}}
</option>
<option name="identity-type" value="teamspeak">{{tr "TeamSpeak"
/}}
</option>
<option name="identity-type" value="nickname">{{tr "Nickname (Debug
purposes only!)" /}}
</option> <!-- Only available on localhost for debug -->
</select>
</div>
</div>
<hr>
<div class="identity-settings identity-settings-teamspeak">
<!--
<div class="import">
{{tr "Please enter your exported TS3 Identity string bellow or select your exported Identity"/}}<br>
<div style="width: 100%; display: flex; justify-content: stretch; flex-direction: row">
<input placeholder="Identity string" style="min-width: 60%; flex-shrink: 1; flex-grow: 2; margin: 5px;" class="identity_string">
<div style="max-width: 200px; flex-grow: 1; flex-shrink: 4; margin: 5px"><input style="display: flex; width: 100%;" class="identity_file" type="file"></div>
</div>
</div>
-->
<div class="identity-info" style="display: none">
<div class="form-group unique-id">
<label>{{tr "UniqueID:" /}}</label>
<input class="form-control" type="text"
value="xxjnc14LmvTk+Lyrm8OOeo4tOqw=" readonly/>
</div>
<div class="form-row level">
<div class="form-group container-input">
<label>{{tr "Level:" /}}</label>
<input class="form-control" type="text" value="39" readonly/>
</div>
<div class="form-group bmd-form-group">
<button class="btn btn-raised button-improve">{{tr "Improve"
/}}
</button>
</div>
</div>
</div>
<div class="identity-undefined">
<div>{{tr "You have'nt generated/imported an identity.<br>Generate a new
one or import one." /}}
</div>
</div>
<div class="manage">
<button class="btn btn-primary button-generate">{{tr "Generate new"
/}}
</button>
<div class="export-import">
<button class="btn btn-secondary button-import">{{tr "Import
identity" /}}
</button>
<button class="btn btn-primary button-export">{{tr "Export identity"
/}}
</button>
</div>
</div>
</div>
<div class="identity-settings identity-settings-teaforo">
<div class="connected">
{{tr "You're using your forum account as verification"/}}
</div>
<div class="disconnected">
<!-- TODO tr -->
<!-- <a href="#" class="native-teaforo-login">here</a> -->
<div class="error-forum-not-connected alert alert-warning fade show"
role="alert">
You cant use your TeaSpeak forum account. You're not connected with
your forum Account!<br>
Setup your connection <a class="forum-setup" href="#">here</a>.
</div>
</div>
</div>
<div class="identity-settings identity-settings-nickname">
<a>
{{tr "This is just for debug and uses the name as unique identifier" /}}
</a>
<div class="form-group">
<label for="input-settings-profile-name-name"
class="bmd-label-floating">{{tr "Username" /}}</label>
<input id="input-settings-profile-name-name"
class="form-control setting-name">
</div>
</div>
<div class="settings-profile-error alert alert-warning fade show" role="alert">
<div class="message"></div>
</div>
</div>
</div>
</div>
</div>
</x-content>
</x-entry>
</x-tab>
</script>
<script class="jsrender-template" id="tmpl_settings-sound_entry" type="text/html">
<div class="entry">
@ -3140,10 +2707,9 @@
<div class="group-list">
{{for groups}}
<div class="group-entry">
<label class="ccheckbox {{if disabled}}disabled{{/if}}">
<input type="checkbox" group-id="{{:id}}" {{if default}}default disabled="true" {{/if}} {{if
assigned}}checked{{/if}}>
<span class="checkmark"></span>
<label class="checkbox {{if default}}disabled{{/if}}">
<input type="checkbox" group-id="{{:id}}" {{if default}}default disabled="true" {{/if}}>
<div class="mark"></div>
</label>
<node key="icon_{{>id}}"></node>
<a>{{>name}} ({{>id}})</a>
@ -3564,21 +3130,38 @@
</script>
<script class="jsrender-template" id="tmpl_client_ban" type="text/html">
<div class="align_column">
<div class="align_column" style="margin: 5px">
<a>{{tr "Name:" /}}</a>
<input value="{{>client_name}}" readonly>
<div> <!-- for the renderer -->
<div class="container-info">
<div class="container container-name">
<div class="title">{{tr "Nickname" /}}</div>
<div class="value">
{{for entries}}
{{>name}}<br>
{{/for}}
</div>
</div>
<div class="container container-unique-id">
<div class="title">{{tr "Unique ID" /}}</div>
<div class="value">
{{for entries}}
{{>unique_id}}<br>
{{/for}}
</div>
</div>
</div>
<div class="align_column" style="margin: 5px">
<a>{{tr "Reason:" /}}</a>
<textarea style="height: 32px; resize: vertical; max-height: 150px; min-height: 32px"
maxlength="512" class="ban_reason"></textarea>
</div>
<div class="align_row" style="margin: 5px; justify-content: space-between">
<a>{{tr "Duration:" /}}</a>
<div class="align_row">
<input type="number" value="1" class="ban_duration" style="margin-right: 7px" min="1">
<select class="ban_duration_type">
<div class="container-duration">
<a>{{tr "Duration" /}}</a>
<div class="container-value">
<div class="input-boxed value">
<input type="number" value="1" min="1" />
<div class="container-tooltip tooltip-max-time">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a class="max">error: max duration</a>
</div>
</div>
</div>
<select class="input-boxed">
<option value="sec">{{tr "seconds"/}}</option>
<option value="min">{{tr "minutes"/}}</option>
<option value="hours">{{tr "hours"/}}</option>
@ -3587,38 +3170,44 @@
</select>
</div>
</div>
<div class="group_box container-ban-type">
<div class="header">{{tr "Ban client by"/}}</div>
<div class="content ban-types">
<div>
<input type="checkbox" class="ban-type-nickname">
<a>{{tr "Nickname"/}}</a>
<div class="help-tip tip-right tip-small" checked>
<p>
{{tr "Bans the client by his current nickname.<br>" +
"The currently nickname cant be used until the ban expired"/}}
</p>
</div>
</div>
<div>
<input type="checkbox" class="ban-type-hardware-id" checked>
<a>{{tr "Hardware ID" /}}</a>
<div class="help-tip tip-right tip-small">
<p>
{{tr "Bans the client by his hardware id.<br>" +
"The hardware id has different meanings, depends on the users agent<br>" +
"TeaClient: The hardware id will be equal to the mac address<br>" +
"TeaWeb: The TeaSpeak web client hasn't a hardware id, it will be random<br>" +
"TeamSpeak 3 client: The hardware id will be a result of some hashes from hardware
specific properties" /}}
</p>
</div>
</div>
<div>
<input type="checkbox" class="ban-type-ip" checked>
<a>{{tr "IP Address" /}}</a>
</div>
<div class="container-reason">
<div class="toolbar">
<div class="button button-bold">B</div>
<div class="button button-italic">I</div>
<div class="button button-underline">U</div>
<label class="button button-color">
<input type="color" value="#FF0000">
<a class="rainbow-letter">C</a>
</label>
</div>
<textarea class="input-boxed ban-reason" placeholder="{{tr 'Ban reason' /}}"></textarea>
</div>
<div class="container-criteria">
<div class="criteria nickname">
<a>{{tr "Nickname and Unique ID" /}}</a>
<label class="checkbox">
<input type="checkbox">
<div class="mark"></div>
</label>
</div>
<div class="criteria ip-address">
<a>{{tr "IP-Address" /}}</a>
<label class="checkbox">
<input type="checkbox">
<div class="mark"></div>
</label>
</div>
<div class="criteria hardware-id">
<a>{{tr "Hardware ID" /}}</a>
<label class="checkbox">
<input type="checkbox">
<div class="mark"></div>
</label>
</div>
</div>
<div class="container-buttons">
<button class="btn btn-danger button-cancel">{{tr "Cancel" /}}</button>
<button class="btn btn-success button-apply">{{tr "Ok" /}}</button>
</div>
</div>
</script>
@ -5285,6 +4874,60 @@
</div>
</script>
<script class="jsrender-template" id="tmpl_channel_info" type="text/html">
<div> <!-- Important for the renderer -->
<div class="row">
<div class="column channel-type">
<a class="title">{{tr "Channel Type" /}}</a>
<div class="value">error: channel type</div>
</div>
<div class="column chat-mode">
<a class="title">{{tr "Chat mode" /}}</a>
<div class="value">error: chat mode</div>
</div>
<div class="column current-clients">
<a class="title">{{tr "Current clients" /}}</a>
<div class="value">error: current clients</div>
</div>
</div>
<div class="row">
<div class="column audio-codec">
<a class="title">{{tr "Audio Codec" /}}</a>
<div class="value">error: audio codec</div>
</div>
<div class="column audio-encrypted">
<a class="title">{{tr "Audio encrypted" /}}</a>
<div class="value">error: audio encrypted</div>
</div>
<div class="column flag-password">
<a class="title">{{tr "Password protected" /}}</a>
<div class="value">error: password protected</div>
</div>
</div>
<div class="row topic"> <!-- only visible if set! -->
<div class="column">
<a class="title">{{tr "Topic" /}}</a>
<div class="value">error: channel topic</div>
</div>
</div>
<div class="container-description"> <!-- only visible if set -->
<a class="title">
{{tr "Description" /}}
<div class="button-copy">
<div class="icon client-copy"></div>
</div>
</a>
<div class="value">
error: channel description
</div>
<div class="no-value">{{tr "Channel has no description" /}}</div>
</div>
<div class="container-buttons">
<button class="btn btn-success button-update">{{tr "Refresh" /}}</button>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_server_info" type="text/html">
<div> <!-- required for the renderer -->
<div class="container-top hostbanner">
@ -5395,7 +5038,7 @@
</div>
</div>
<div class="row">
<a class="key">{{tr "Voice data encoded" /}}</a>
<a class="key">{{tr "Voice data encryption" /}}</a>
<div class="value server-voice-encryption">
error: voice encryption
</div>

View File

@ -437,7 +437,8 @@ namespace connection {
client.updateVariables(...updates);
if(!old_channel) {
/* if its a new client join, or a system reason (like we joined) */
if(!old_channel || reason_id == 2) {
/* client new join */
const conversation_manager = this.connection_handler.side_bar.private_conversations();
const conversation = conversation_manager.find_conversation({
@ -448,6 +449,8 @@ namespace connection {
create: false,
attach: true
});
if(conversation)
client.flag_text_unread = conversation.is_unread();
}
if(client instanceof LocalClientEntry) {
@ -754,6 +757,9 @@ namespace connection {
if(target_own) {
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
const client = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
if(client) /* the client itself might be invisible */
client.flag_text_unread = conversation.is_unread();
} else {
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
}
@ -802,6 +808,7 @@ namespace connection {
timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]),
message: json["msg"]
});
this.connection_handler.channelTree.server.flag_text_unread = conversation.is_unread();
}
}

View File

@ -1,7 +1,6 @@
/// <reference path="ui/frames/chat.ts" />
/// <reference path="ui/modal/ModalConnect.ts" />
/// <reference path="ui/modal/ModalCreateChannel.ts" />
/// <reference path="ui/modal/ModalBanCreate.ts" />
/// <reference path="ui/modal/ModalBanClient.ts" />
/// <reference path="ui/modal/ModalYesNo.ts" />
/// <reference path="ui/modal/ModalBanList.ts" />
@ -289,6 +288,37 @@ interface Window {
}
*/
function execute_default_connect() {
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
if(profile && profile.valid()) {
const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler();
connection.startConnection(address, profile, true, {
nickname: username,
password: password.length > 0 ? {
password: password,
hashed: password_hashed
} : undefined
});
} else {
Modals.spawnConnectModal({},{
url: address,
enforce: true
}, {
profile: profile,
enforce: true
});
}
}
}
function main() {
/*
window.proxy_instance = new TestProxy({
@ -357,6 +387,9 @@ function main() {
log.info(LogCategory.STATISTICS, tr("Received user count update: %o"), status);
});
server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]);
(<any>window).test_upload = (message?: string) => {
message = message || "Hello World";
@ -382,37 +415,8 @@ function main() {
})
};
server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]);
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
if(profile && profile.valid()) {
const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler();
connection.startConnection(address, profile, true, {
nickname: username,
password: password.length > 0 ? {
password: password,
hashed: password_hashed
} : undefined
});
} else {
Modals.spawnConnectModal({},{
url: address,
enforce: true
}, {
profile: profile,
enforce: true
});
}
}
/* schedule it a bit later then the main because the main function is still within the loader */
setTimeout(execute_default_connect, 5);
setTimeout(() => {
const connection = server_connections.active_connection_handler();
/*
@ -426,7 +430,15 @@ function main() {
//Modals.openClientInfo(connection.getClient());
//Modals.openServerInfoBandwidth(connection.channelTree.server);
Modals.openBanList(connection);
//Modals.openBanList(connection);
/*
Modals.spawnBanClient(connection,[
{name: "WolverinDEV", unique_id: "XXXX"},
{name: "WolverinDEV", unique_id: "XXXX"},
{name: "WolverinDEV", unique_id: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
{name: "WolverinDEV", unique_id: "YYY"}
], () => {});
*/
}, 4000);
//Modals.spawnSettingsModal("identity-profiles");
//Modals.spawnKeySelect(console.log);

View File

@ -51,7 +51,8 @@ class ChannelProperties {
//Only after request
channel_description: string = "";
channel_flag_conversation_private: boolean = false;
channel_flag_conversation_private: boolean = true; /* TeamSpeak mode */
channel_conversation_history_length: number = -1;
}
class ChannelEntry {
@ -460,30 +461,32 @@ class ChannelEntry {
}
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
let channelCreate =
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_TEMPORARY).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_PERMANENT).granted(1);
let channelCreate = !![
PermissionType.B_CHANNEL_CREATE_TEMPORARY,
PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT,
PermissionType.B_CHANNEL_CREATE_PERMANENT
].find(e => this.channelTree.client.permissions.neededPermission(e).granted(1));
let channelModify =
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NAME).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_PASSWORD).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_CODEC).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_CODEC_QUALITY).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_CODEC_LATENCY_FACTOR).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAXFAMILYCLIENTS).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1) ||
this.channelTree.client.permissions.neededPermission(PermissionType.B_ICON_MANAGE).granted(1);
let channelModify = !![
PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT,
PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT,
PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT,
PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY,
PermissionType.B_CHANNEL_MODIFY_NAME,
PermissionType.B_CHANNEL_MODIFY_TOPIC,
PermissionType.B_CHANNEL_MODIFY_DESCRIPTION,
PermissionType.B_CHANNEL_MODIFY_PASSWORD,
PermissionType.B_CHANNEL_MODIFY_CODEC,
PermissionType.B_CHANNEL_MODIFY_CODEC_QUALITY,
PermissionType.B_CHANNEL_MODIFY_CODEC_LATENCY_FACTOR,
PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS,
PermissionType.B_CHANNEL_MODIFY_MAXFAMILYCLIENTS,
PermissionType.B_CHANNEL_MODIFY_SORTORDER,
PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER,
PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED,
PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY,
PermissionType.B_ICON_MANAGE
].find(e => this.channelTree.client.permissions.neededPermission(e).granted(1));
let flagDelete = true;
if(this.clients(true).length > 0)
@ -522,8 +525,7 @@ class ChannelEntry {
name: tr("Show channel info"),
callback: () => {
trigger_close = false;
alert('TODO!');
Modals.openChannelInfo(this);
},
icon_class: "client-about"
},

View File

@ -384,20 +384,7 @@ class ClientEntry {
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-permission_server_groups",
name: "Server groups dialog",
callback: () => {
Modals.createServerGroupAssignmentModal(this, (group, flag) => {
if(flag) {
return this.channelTree.client.serverConnection.send_command("servergroupaddclient", {
sgid: group.id,
cldbid: this.properties.client_database_id
}).then(result => true);
} else
return this.channelTree.client.serverConnection.send_command("servergroupdelclient", {
sgid: group.id,
cldbid: this.properties.client_database_id
}).then(result => true);
});
}
callback: () => this.open_assignment_modal()
},
contextmenu.Entry.HR(),
...server_groups
@ -430,6 +417,33 @@ class ClientEntry {
}];
}
open_assignment_modal() {
Modals.createServerGroupAssignmentModal(this, (groups, flag) => {
if(groups.length == 0) return Promise.resolve(true);
if(groups.length == 1) {
if(flag) {
return this.channelTree.client.serverConnection.send_command("servergroupaddclient", {
sgid: groups[0],
cldbid: this.properties.client_database_id
}).then(result => true);
} else
return this.channelTree.client.serverConnection.send_command("servergroupdelclient", {
sgid: groups[0],
cldbid: this.properties.client_database_id
}).then(result => true);
} else {
const data = groups.map(e => { return {sgid: e}; });
data[0]["cldbid"] = this.properties.client_database_id;
if(flag) {
return this.channelTree.client.serverConnection.send_command("clientaddservergroup", data, {flagset: ["continueonerror"]}).then(result => true);
} else
return this.channelTree.client.serverConnection.send_command("clientdelservergroup", data, {flagset: ["continueonerror"]}).then(result => true);
}
});
}
open_text_chat() {
const chat = this.channelTree.client.side_bar;
const conversation = chat.private_conversations().find_conversation({
@ -441,7 +455,6 @@ class ClientEntry {
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();
}
@ -458,7 +471,16 @@ class ClientEntry {
callback: () => {
this.open_text_chat();
}
}, {
},
contextmenu.Entry.HR(),
{
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-about",
name: tr("Show client info"),
callback: () => Modals.openClientInfo(this)
},
contextmenu.Entry.HR(),
{
type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-poke",
name: tr("Poke client"),
@ -547,7 +569,10 @@ class ClientEntry {
name: tr("Ban client"),
invalidPermission: !this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1),
callback: () => {
Modals.spawnBanClient(this.properties.client_nickname, (data) => {
Modals.spawnBanClient(this.channelTree.client, [{
name: this.properties.client_nickname,
unique_id: this.properties.client_unique_identifier
}], (data) => {
this.channelTree.client.serverConnection.send_command("banclient", {
uid: this.properties.client_unique_identifier,
banreason: data.reason,
@ -617,6 +642,14 @@ class ClientEntry {
.attr("client-id", this.clientId());
/* unread marker */
{
container_client.append(
$.spawn("div")
.addClass("marker-text-unread hidden")
.attr("private-conversation", this._clientId)
);
}
container_client.append(
$.spawn("div")
.addClass("icon_client_state")
@ -1055,6 +1088,10 @@ class ClientEntry {
this._info_connection_promise_resolve = undefined;
this._info_connection_promise_reject = undefined;
}
set flag_text_unread(flag: boolean) {
this._tag.find(".marker-text-unread").toggleClass("hidden", !flag);
}
}
class LocalClientEntry extends ClientEntry {
@ -1383,7 +1420,7 @@ class MusicClientEntry extends ClientEntry {
},
type: contextmenu.MenuEntryType.ENTRY
},
contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))())
contextmenu.Entry.CLOSE(() => trigger_close && on_close())
);
}

View File

@ -130,11 +130,13 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
tag.append($.spawn("div").addClass(icon));
tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name));
if(entry.disabled || entry.invalidPermission) tag.addClass("disabled");
if(entry.disabled || entry.invalidPermission)
tag.addClass("disabled");
else {
tag.click( () => {
tag.on('click', () => {
if($.isFunction(entry.callback))
entry.callback();
entry.callback = undefined; /* for some reason despawn_context_menu() causes a second click event? */
this.despawn_context_menu();
});
}
@ -151,9 +153,10 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider {
if(entry.disabled || entry.invalidPermission)
tag.addClass("disabled");
else {
tag.click( () => {
tag.on('click', () => {
if($.isFunction(entry.callback))
entry.callback();
entry.callback = undefined; /* for some reason despawn_context_menu() causes a second click event? */
this.despawn_context_menu();
});
}

View File

@ -470,11 +470,11 @@ class ControlBar {
this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
this.update_connection_state();
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
this.log.log(log.server.Type.DISCONNECTED, {});
this.connection_handler.log.log(log.server.Type.DISCONNECTED, {});
}
private on_token_use() {
createInputModal(tr("Use token"), tr("Please enter your token/priviledge key"), message => message.length > 0, result => {
createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => {
if(!result) return;
if(this.connection_handler.serverConnection.connected)
this.connection_handler.serverConnection.send_command("tokenuse", {

View File

@ -395,7 +395,7 @@ namespace top_menu {
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 => {
createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => {
if(!result) return;
const scon = server_connections.active_connection_handler();

View File

@ -908,6 +908,7 @@ test
OPEN,
CLOSED,
DISCONNECTED,
DISCONNECTED_SELF,
}
export type DisplayedMessage = {
@ -1096,7 +1097,7 @@ test
tag_message: basic_view_entry.html_tag,
tag_timepointer: undefined,
tag_unread: undefined
});
}, true);
}
}
@ -1240,7 +1241,7 @@ test
return result;
}
private _build_spacer(message: string, type: "date" | "new" | "disconnect" | "reconnect" | "closed" | "error") : PrivateConversationViewSpacer {
private _build_spacer(message: string, type: "date" | "new" | "disconnect" | "disconnect_self" | "reconnect" | "closed" | "error") : PrivateConversationViewSpacer {
const tag = $("#tmpl_frame_chat_private_spacer").renderTag({
message: message
}).addClass("type-" + type);
@ -1249,7 +1250,7 @@ test
}
}
private _register_displayed_message(message: DisplayedMessage) {
private _register_displayed_message(message: DisplayedMessage, update_new: boolean) {
const message_date = new Date(message.timestamp);
/* before := older message; after := newer message */
@ -1277,7 +1278,7 @@ test
while(this._displayed_messages.length > this._displayed_messages_length)
this._destroy_displayed_message(this._displayed_messages.last(), true);
const flag_new_message = index == 0 && (message.message_type === "spacer" || (<PrivateConversationViewMessage>message.message).sender === "partner");
const flag_new_message = update_new && index == 0 && (message.message_type === "spacer" || (<PrivateConversationViewMessage>message.message).sender === "partner");
/* Timeline for before - now */
{
@ -1411,6 +1412,10 @@ test
this._spacer_unread_message.tag_unread.html_tag.insertBefore(this._spacer_unread_message.tag_message);
}
} else {
const ctree = this.handle.handle.handle.channelTree;
if(ctree && ctree.tag_tree() && this.client_id)
ctree.tag_tree().find(".marker-text-unread[private-conversation='" + this.client_id + "']").addClass("hidden");
if(this._spacer_unread_message) {
this._destroy_view_entry(this._spacer_unread_message.tag_unread);
this._spacer_unread_message.tag_unread = undefined;
@ -1427,14 +1432,16 @@ test
is_unread() : boolean { return !!this._spacer_unread_message; }
private _append_state_change(state: "disconnect" | "reconnect" | "closed") {
private _append_state_change(state: "disconnect" | "disconnect_self" | "reconnect" | "closed") {
let message;
if(state == "closed")
message = tr("Your chat partner has closed the conversation");
else if(state == "reconnect")
message = "Your chat partner has reconnected";
message = this._state === PrivateConversationState.DISCONNECTED_SELF ?tr("You've reconnected to the server") : tr("Your chat partner has reconnected");
else if(state === "disconnect")
message = tr("Your chat partner has disconnected");
else
message = "Your chat partner has disconnected";
message = tr("You've disconnected from the server");
const spacer = this._build_spacer(message, state);
this._register_displayed_message({
@ -1444,7 +1451,7 @@ test
tag_message: spacer.html_tag,
tag_timepointer: undefined,
tag_unread: undefined
});
}, state === "disconnect");
}
state() : PrivateConversationState {
@ -1462,6 +1469,8 @@ test
this._append_state_change("reconnect");
else if(state == PrivateConversationState.CLOSED)
this._append_state_change("closed");
else if(state == PrivateConversationState.DISCONNECTED_SELF)
this._append_state_change("disconnect_self");
this._state = state;
}
@ -1485,7 +1494,7 @@ test
tag_message: spacer.html_tag,
tag_timepointer: undefined,
tag_unread: undefined
});
}, true);
}
call_message(message: string) {
@ -1574,7 +1583,10 @@ test
}
clear_client_ids() {
this._conversations.forEach(e => e.client_id = 0);
this._conversations.forEach(e => {
e.client_id = 0;
e.set_state(PrivateConversationState.DISCONNECTED_SELF);
});
}
html_tag() : JQuery { return this._html_tag; }

View File

@ -3,102 +3,178 @@
/// <reference path="../../proto.ts" />
namespace Modals {
export function spawnBanClient(name: string | string[], callback: (data: {
export type BanEntry = {
name?: string;
unique_id: string;
}
export function spawnBanClient(client: ConnectionHandler, entries: BanEntry | BanEntry[], callback: (data: {
length: number,
reason: string,
no_name: boolean,
no_ip: boolean,
no_hwid: boolean
}) => void) {
const connectModal = createModal({
header: function() {
return tr("Ban client");
},
const max_ban_time = client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value;
const permission_criteria_hwid = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_HWID).granted(1);
const permission_criteria_ip = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_IP).granted(1);
const permission_criteria_name = client.permissions.neededPermission(PermissionType.B_CLIENT_BAN_NAME).granted(1);
const modal = createModal({
header: Array.isArray(entries) ? tr("Ban clients") : tr("Ban client"),
body: function () {
let tag = $("#tmpl_client_ban").renderTag({
client_name: $.isArray(name) ? '"' + name.join('", "') + '"' : name
});
let template = $("#tmpl_client_ban").renderTag({entries: entries});
let maxTime = 0; //globalClient.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value;
let unlimited = maxTime == 0 || maxTime == -1;
if(unlimited) maxTime = 0;
let update_duration;
let update_button_ok;
const button_ok = template.find(".button-apply");
const button_cancel = template.find(".button-cancel");
let banTag = tag.find(".ban_duration_type");
let durationTag = tag.find(".ban_duration");
banTag.find("option[value=\"sec\"]").prop("disabled", !unlimited && 1 > maxTime)
.attr("duration-scale", 1)
.attr("duration-max", maxTime);
banTag.find("option[value=\"min\"]").prop("disabled", !unlimited && 60 > maxTime)
.attr("duration-scale", 60)
.attr("duration-max", maxTime / 60);
banTag.find("option[value=\"hours\"]").prop("disabled", !unlimited && 60 * 60 > maxTime)
.attr("duration-scale", 60 * 60)
.attr("duration-max", maxTime / (60 * 60));
banTag.find("option[value=\"days\"]").prop("disabled", !unlimited && 60 * 60 * 24 > maxTime)
.attr("duration-scale", 60 * 60 * 24)
.attr("duration-max", maxTime / (60 * 60 * 24));
banTag.find("option[value=\"perm\"]").prop("disabled", !unlimited)
.attr("duration-scale", 0);
const input_duration_value = template.find(".container-duration input").on('change keyup', () => update_duration());
const input_duration_type = template.find(".container-duration select").on('change keyup', () => update_duration());
durationTag.change(() => banTag.trigger('change'));
const container_reason = template.find(".container-reason");
banTag.change(event => {
let element = $((event.target as HTMLSelectElement).selectedOptions.item(0));
if(element.val() !== "perm") {
durationTag.prop("disabled", false);
const criteria_nickname = template.find(".criteria.nickname input")
.prop('checked', permission_criteria_name).prop("disabled", !permission_criteria_name)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_name);
let current = durationTag.val() as number;
let max = parseInt(element.attr("duration-max"));
if (max > 0 && current > max)
durationTag.val(max);
else if(current <= 0)
durationTag.val(1);
durationTag.attr("max", max);
} else {
durationTag.prop("disabled", true);
}
});
const criteria_ip_address = template.find(".criteria.ip-address input")
.prop('checked', permission_criteria_ip).prop("disabled", !permission_criteria_ip)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_ip);
return tag;
const criteria_hardware_id = template.find(".criteria.hardware-id input")
.prop('checked', permission_criteria_hwid).prop("disabled", !permission_criteria_hwid)
.firstParent(".checkbox").toggleClass("disabled", !permission_criteria_hwid);
/* duration input handler */
{
const tooltip_duration_max = template.find(".tooltip-max-time a.max");
update_duration = () => {
const type = input_duration_type.val() as string;
const value = parseInt(input_duration_value.val() as string);
const disabled = input_duration_type.prop("disabled");
input_duration_value.prop("disabled", type === "perm" || disabled).firstParent(".input-boxed").toggleClass("disabled", type === "perm" || disabled);
if(type !== "perm") {
if(input_duration_value.attr("x-saved-value")) {
input_duration_value.val(parseInt(input_duration_value.attr("x-saved-value")));
input_duration_value.attr("x-saved-value", null);
}
const selected_option = input_duration_type.find("option[value='" + type + "']");
const max = parseInt(selected_option.attr("duration-max"));
input_duration_value.attr("max", max);
if((value > max && max != -1) || value < 1) {
input_duration_value.firstParent(".input-boxed").addClass("is-invalid");
} else {
input_duration_value.firstParent(".input-boxed").removeClass("is-invalid");
}
if(max != -1)
tooltip_duration_max.html(tr("You're allowed to ban a maximum of ") + "<b>" + max + " " + duration_data[type][max == 1 ? "1-text" : "text"] + "</b>");
else
tooltip_duration_max.html(tr("You're allowed to ban <b>permanent</b>."));
} else {
if(value && !Number.isNaN(value))
input_duration_value.attr("x-saved-value", value);
input_duration_value.attr("placeholder", tr("for ever")).val(null);
tooltip_duration_max.html(tr("You're allowed to ban <b>permanent</b>."));
}
update_button_ok && update_button_ok();
};
/* initialize ban time */
Promise.resolve(max_ban_time).catch(error => { /* TODO: Error handling? */ return 0; }).then(max_time => {
let unlimited = max_time == 0 || max_time == -1;
if(unlimited || typeof(max_time) === "undefined") max_time = 0;
for(const value of Object.keys(duration_data)) {
input_duration_type.find("option[value='" + value + "']")
.prop("disabled", !unlimited && max_time >= duration_data[value].scale)
.attr("duration-scale", duration_data[value].scale)
.attr("duration-max", unlimited ? -1 : Math.floor(max_time / duration_data[value].scale));
}
input_duration_type.find("option[value='perm']")
.prop("disabled", !unlimited)
.attr("duration-scale", 0)
.attr("duration-max", -1);
update_duration();
});
update_duration();
}
/* ban reason */
{
const input = container_reason.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');
};
container_reason.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]'));
container_reason.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]'));
container_reason.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]'));
container_reason.find(".button-color input").on('change', event => {
insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]')
});
}
/* buttons */
{
button_cancel.on('click', event => modal.close());
button_ok.on('click', event => {
const duration = input_duration_type.val() === "perm" ? 0 : (1000 * parseInt(input_duration_type.find("option[value='" + input_duration_type.val() + "']").attr("duration-scale")) * parseInt(input_duration_value.val() as string));
modal.close();
callback({
length: Math.floor(duration / 1000),
reason: container_reason.find("textarea").val() as string,
no_hwid: !criteria_hardware_id.find("input").prop("checked"),
no_ip: !criteria_ip_address.find("input").prop("checked"),
no_name: !criteria_nickname.find("input").prop("checked")
});
});
const inputs = template.find(".input-boxed");
update_button_ok = () => {
const invalid = [...inputs].find(e => $(e).hasClass("is-invalid"));
button_ok.prop('disabled', !!invalid);
};
update_button_ok();
}
tooltip(template);
return template.children();
},
footer: function () {
let tag = $.spawn("div");
tag.css("text-align", "right");
tag.css("margin-top", "3px");
tag.css("margin-bottom", "6px");
tag.addClass("modal-button-group");
footer: null,
let buttonCancel = $.spawn("button");
buttonCancel.text("Cancel");
buttonCancel.on("click", () => connectModal.close());
tag.append(buttonCancel);
let buttonOk = $.spawn("button");
buttonOk.text("OK").addClass("btn_success");
tag.append(buttonOk);
return tag;
},
width: 450
min_width: "10em",
width: "30em"
});
connectModal.open();
modal.open();
connectModal.htmlTag.find(".btn_success").on('click', () => {
connectModal.close();
let length = connectModal.htmlTag.find(".ban_duration").val() as number;
let duration = connectModal.htmlTag.find(".ban_duration_type option:selected");
console.log(duration);
console.log(length + "*" + duration.attr("duration-scale"));
callback({
length: length * parseInt(duration.attr("duration-scale")),
reason: connectModal.htmlTag.find(".ban_reason").val() as string,
no_hwid: !connectModal.htmlTag.find(".ban-type-hardware-id").prop("checked"),
no_ip: !connectModal.htmlTag.find(".ban-type-ip").prop("checked"),
no_name: !connectModal.htmlTag.find(".ban-type-nickname").prop("checked")
});
})
modal.htmlTag.find(".modal-body").addClass("modal-ban-client");
}
}

View File

@ -1,147 +0,0 @@
namespace Modals {
export function spawnBanCreate(connection: ConnectionHandler, base?: BanEntry, callback?: (entry?: BanEntry) => any) {
let result: BanEntry = {} as any;
result.banid = base ? base.banid : 0;
let modal: Modal;
modal = createModal({
header: base && base.banid > 0 ? tr("Edit ban") : tr("Add ban"),
body: () => {
let template = $("#tmpl_ban_create").renderTag();
const input_name = template.find(".input-name");
const input_name_type = template.find(".input-name-type");
const input_ip = template.find(".input-ip");
const input_uid = template.find(".input-uid");
const input_reason = template.find(".input-reason");
const input_time = template.find(".input-time");
const input_time_type = template.find(".input-time-unit");
const input_hwid = template.find(".input-hwid");
const input_global = template.find(".input-global");
{
let maxTime = 0; //globalClient.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value;
let unlimited = maxTime == 0 || maxTime == -1;
if(unlimited) maxTime = 0;
input_time_type.find("option[value=\"sec\"]").prop("disabled", !unlimited && 1 > maxTime)
.attr("duration-scale", 1)
.attr("duration-max", maxTime);
input_time_type.find("option[value=\"min\"]").prop("disabled", !unlimited && 60 > maxTime)
.attr("duration-scale", 60)
.attr("duration-max", maxTime / 60);
input_time_type.find("option[value=\"hours\"]").prop("disabled", !unlimited && 60 * 60 > maxTime)
.attr("duration-scale", 60 * 60)
.attr("duration-max", maxTime / (60 * 60));
input_time_type.find("option[value=\"days\"]").prop("disabled", !unlimited && 60 * 60 * 24 > maxTime)
.attr("duration-scale", 60 * 60 * 24)
.attr("duration-max", maxTime / (60 * 60 * 24));
input_time_type.find("option[value=\"perm\"]").prop("disabled", !unlimited)
.attr("duration-scale", 0);
input_time_type.change(event => {
let element = $((event.target as HTMLSelectElement).selectedOptions.item(0));
if(element.val() !== "perm") {
input_time.prop("disabled", false);
let current = input_time.val() as number;
let max = parseInt(element.attr("duration-max"));
if (max > 0 && current > max)
input_time.val(max);
else if(current <= 0)
input_time.val(1);
input_time.attr("max", max);
} else {
input_time.prop("disabled", true);
}
});
}
template.find('input, textarea').on('keyup change', event => {
let valid = false;
if(input_name.val() || input_ip.val() || input_uid.val())
valid = true;
modal.htmlTag.find(".button-success").prop("disabled", !valid);
});
if(base) {
input_ip.val(base.ip);
input_uid.val(base.unique_id);
input_name.val(base.name);
input_hwid.val(base.hardware_id);
(input_name_type[0] as HTMLSelectElement).selectedIndex = base.name_type || 0;
input_reason.val(base.reason);
if(base.timestamp_expire.getTime() == 0) {
input_time_type.find("option[value=\"perm\"]").prop("selected", true);
} else {
const time = (base.timestamp_expire.getTime() - base.timestamp_created.getTime()) / 1000;
if(time % (60 * 60 * 24) === 0) {
input_time_type.find("option[value=\"days\"]").prop("selected", true);
input_time.val(time / (60 * 60 * 24));
} else if(time % (60 * 60) === 0) {
input_time_type.find("option[value=\"hours\"]").prop("selected", true);
input_time.val(time / (60 * 60));
} else if(time % (60) === 0) {
input_time_type.find("option[value=\"min\"]").prop("selected", true);
input_time.val(time / (60));
} else {
input_time_type.find("option[value=\"sec\"]").prop("selected", true);
input_time.val(time);
}
}
template.find(".container-global").detach(); //We cant edit this
input_global.prop("checked", base.server_id == 0);
}
if(connection && connection.permissions)
input_global.prop("disabled", !connection.permissions.neededPermission(base ? PermissionType.B_CLIENT_BAN_EDIT_GLOBAL : PermissionType.B_CLIENT_BAN_CREATE_GLOBAL));
return template;
},
footer: undefined
});
modal.htmlTag.find(".button-close").on('click', () => modal.close());
modal.htmlTag.find(".button-success").on('click', () => {
{
let length = modal.htmlTag.find(".input-time").val() as number;
let duration = modal.htmlTag.find(".input-time-unit option:selected");
console.log(duration);
console.log(length + "*" + duration.attr("duration-scale"));
const time = length * parseInt(duration.attr("duration-scale"));
if(!result.timestamp_created)
result.timestamp_created = new Date();
if(time > 0)
result.timestamp_expire = new Date(result.timestamp_created.getTime() + time * 1000);
else
result.timestamp_expire = new Date(0);
}
{
result.name = modal.htmlTag.find(".input-name").val() as string;
{
const name_type = modal.htmlTag.find(".input-name-type") as JQuery<HTMLSelectElement>;
result.name_type = name_type[0].selectedIndex;
}
result.ip = modal.htmlTag.find(".input-ip").val() as string;
result.unique_id = modal.htmlTag.find(".input-uid").val() as string;
result.reason = modal.htmlTag.find(".input-reason").val() as string;
result.hardware_id = modal.htmlTag.find(".input-hwid").val() as string;
result.server_id = modal.htmlTag.find(".input-global").prop("checked") ? 0 : -1;
}
modal.close();
if(callback) callback(result);
});
modal.htmlTag.find("input").trigger("change");
modal.open();
}
}

View File

@ -233,7 +233,7 @@ namespace Modals {
}
//Note: This object must be sorted (from shortest to longest)!
const duration_data = {
export const duration_data = {
"sec": {
"text": tr("Seconds"),
"1-text": tr("Second"),

View File

@ -107,7 +107,7 @@ namespace Modals {
};
const container_bookmarks = template.find(".container-bookmarks");
const update_bookmark_list = () => {
const update_bookmark_list = (_current_selected: string) => {
container_bookmarks.empty();
selected_bookmark = undefined;
update_selected();
@ -155,6 +155,8 @@ namespace Modals {
update_buttons();
update_selected();
});
if(entry.unique_id === _current_selected)
container.trigger('click');
hide_links.push(sibling_data.last);
let cindex = 0;
@ -197,13 +199,13 @@ namespace Modals {
if(answer) {
bookmarks.delete_bookmark(selected_bookmark);
bookmarks.save_bookmark(selected_bookmark);
update_bookmark_list();
update_bookmark_list(undefined);
}
});
} else {
bookmarks.delete_bookmark(selected_bookmark);
bookmarks.save_bookmark(selected_bookmark);
update_bookmark_list();
update_bookmark_list(undefined);
}
});
@ -221,7 +223,7 @@ namespace Modals {
result as string
);
bookmarks.save_bookmark(mark);
update_bookmark_list();
update_bookmark_list(mark.unique_id);
}
}).open();
});
@ -243,7 +245,7 @@ namespace Modals {
server_password_hash: ""
}, "");
bookmarks.save_bookmark(mark);
update_bookmark_list();
update_bookmark_list(mark.unique_id);
}
}).open();
});
@ -305,10 +307,49 @@ namespace Modals {
})
}
/* Arrow key navigation for the bookmark list */
{
let _focused = false;
let _focus_listener;
let _key_listener;
update_bookmark_list();
_focus_listener = event => {
_focused = false;
let element = event.target as HTMLElement;
while(element) {
if(element === container_bookmarks[0]) {
_focused = true;
break;
}
element = element.parentNode as HTMLElement;
}
};
_key_listener = event => {
if(!_focused) return;
if(event.key.toLowerCase() === "arrowdown") {
container_bookmarks.find(".selected").next().trigger('click');
} else if(event.key.toLowerCase() === "arrowup") {
container_bookmarks.find(".selected").prev().trigger('click');
}
};
document.addEventListener('click', _focus_listener);
document.addEventListener('keydown', _key_listener);
modal.close_listener.push(() => {
document.removeEventListener('click', _focus_listener);
document.removeEventListener('keydown', _key_listener);
})
}
update_bookmark_list(undefined);
update_buttons();
template.find(".container-bookmarks").on('keydown', event => {
console.error(event.key);
});
template.find(".button-close").on('click', event => modal.close());
return template.children();
},
@ -321,6 +362,7 @@ namespace Modals {
control_bar.update_bookmarks();
top_menu.rebuild_bookmarks();
});
modal.open();
}
}

View File

@ -0,0 +1,152 @@
namespace Modals {
export function openChannelInfo(channel: ChannelEntry) {
let modal: Modal;
modal = createModal({
header: tr("Channel information: ") + channel.channelName(),
body: () => {
const template = $("#tmpl_channel_info").renderTag();
const update_values = (container) => {
apply_channel_description(container.find(".container-description"), channel);
apply_general(container, channel);
};
template.find(".button-copy").on('click', event => {
copy_to_clipboard(channel.properties.channel_description);
createInfoModal(tr("Description copied"), tr("The channel description has been copied to your clipboard!")).open();
});
const button_update = template.find(".button-update");
button_update.on('click', event => update_values(modal.htmlTag));
update_values(template);
tooltip(template);
return template.children();
},
footer: null,
width: "65em"
});
modal.htmlTag.find(".button-close").on('click', event => modal.close());
modal.htmlTag.find(".modal-body").addClass("modal-channel-info");
modal.open();
}
function apply_channel_description(container: JQuery, channel: ChannelEntry) {
const container_value = container.find(".value");
const container_no_value = container.find(".no-value");
channel.getChannelDescription().then(description => {
if(description) {
const result = xbbcode.parse(description, {});
container_value[0].innerHTML = result.build_html();
container_no_value.hide();
container_value.show();
} else {
container_no_value.text(tr("Channel has no description"));
}
});
container_value.hide();
container_no_value.text(tr("loading...")).show();
}
const codec_names = [
tr("Speex Narrowband"),
tr("Speex Wideband"),
tr("Speex Ultra-Wideband"),
tr("CELT Mono"),
tr("Opus Voice"),
tr("Opus Music")
];
function apply_general(container: JQuery, channel: ChannelEntry) {
/* channel type */
{
const tag = container.find(".channel-type .value").empty();
if(channel.properties.channel_flag_permanent)
tag.text(tr("Permanent"));
else if(channel.properties.channel_flag_semi_permanent)
tag.text(tr("Semi permanent"));
else
//TODO: Channel delete delay!
tag.text(tr("Temporary"));
}
/* chat mode */
{
const tag = container.find(".chat-mode .value").empty();
if(channel.properties.channel_flag_conversation_private || channel.properties.channel_flag_password) {
tag.text(tr("Private"));
} else {
if(channel.properties.channel_conversation_history_length == -1)
tag.text(tr("Public; Semi permanent message saving"));
else if(channel.properties.channel_conversation_history_length == 0)
tag.text(tr("Public; Permanent message saving"));
else
tag.append(MessageHelper.formatMessage(tr("Public; Saving last {} messages"), channel.properties.channel_conversation_history_length));
}
}
/* current clients */
{
const tag = container.find(".current-clients .value").empty();
if(channel.flag_subscribed) {
const current = channel.clients().length;
let channel_limit = tr("Unlimited");
if(!channel.properties.channel_flag_maxclients_unlimited)
channel_limit = "" + channel.properties.channel_maxclients;
else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) {
if(channel.properties.channel_maxfamilyclients >= 0)
channel_limit = "" + channel.properties.channel_maxfamilyclients;
}
tag.text(current + " / " + channel_limit);
} else {
tag.text(tr("Channel not subscribed"));
}
}
/* audio codec */
{
const tag = container.find(".audio-codec .value").empty();
tag.text((codec_names[channel.properties.channel_codec] || tr("Unknown")) + " (" + channel.properties.channel_codec_quality + ")")
}
/* audio encrypted */
{
const tag = container.find(".audio-encrypted .value").empty();
const mode = channel.channelTree.server.properties.virtualserver_codec_encryption_mode;
let appendix;
if(mode == 1)
appendix = tr("Overridden by the server with Unencrypted!");
else if(mode == 2)
appendix = tr("Overridden by the server with Encrypted!");
tag.html((channel.properties.channel_codec_is_unencrypted ? tr("Unencrypted") : tr("Encrypted")) + (appendix ? "<br>" + appendix : ""))
}
/* flag password */
{
const tag = container.find(".flag-password .value").empty();
if(channel.properties.channel_flag_password)
tag.text(tr("Yes"));
else
tag.text(tr("No"));
}
/* topic */
{
const container_tag = container.find(".topic");
const tag = container_tag.find(".value").empty();
if(channel.properties.channel_topic) {
container_tag.show();
tag.text(channel.properties.channel_topic);
} else {
container_tag.hide();
}
}
}
}

View File

@ -317,20 +317,7 @@ namespace Modals {
}
};
tag.find(".button-group-add").on('click', event => {
Modals.createServerGroupAssignmentModal(client, (group, flag) => {
if(flag) {
return client.channelTree.client.serverConnection.send_command("servergroupaddclient", {
sgid: group.id,
cldbid: client.properties.client_database_id
}).then(result => { update_groups(); return true; });
} else
return client.channelTree.client.serverConnection.send_command("servergroupdelclient", {
sgid: group.id,
cldbid: client.properties.client_database_id
}).then(result => { update_groups(); return true; });
});
});
tag.find(".button-group-add").on('click', () => client.open_assignment_modal());
update_groups();
}

View File

@ -1,6 +1,6 @@
namespace Modals {
let current_modal: Modal;
export function createServerGroupAssignmentModal(client: ClientEntry, callback: (group: Group, flag: boolean) => Promise<boolean>) {
export function createServerGroupAssignmentModal(client: ClientEntry, callback: (groups: number[], flag: boolean) => Promise<boolean>) {
if(current_modal)
current_modal.close();
@ -19,7 +19,6 @@ namespace Modals {
let entry = {} as any;
entry["id"] = group.id;
entry["name"] = group.name;
entry["assigned"] = client.groupAssigned(group);
entry["disabled"] = !client.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower);
entry["default"] = client.channelTree.server.properties.virtualserver_default_server_group == group.id;
tag["icon_" + group.id] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid);
@ -28,6 +27,12 @@ namespace Modals {
let template = $("#tmpl_server_group_assignment").renderTag(tag);
const update_groups = () => {
for(let group of _groups) {
template.find("input[group-id='" + group.id + "']").prop("checked", client.groupAssigned(group));
}
};
template.find(".group-entry input").each((_idx, _entry) => {
let entry = $(_entry);
@ -40,27 +45,32 @@ namespace Modals {
}
let target = entry.prop("checked");
callback(group, target).then(flag => flag ? Promise.resolve() : Promise.reject()).then(() => {
template.find(".group-entry input[default]").prop("checked", template.find(".group-entry input:checked").length == 0);
}).catch(error => entry.prop("checked", !target));
callback([group.id], target).catch(e => { log.warn(LogCategory.GENERAL, tr("Failed to change group assignment: %o"), e)}).then(update_groups);
});
});
template.find(".button-close").on('click', () => current_modal.close());
template.find(".button-remove-all").on('click', () => {
const group_ids = [];
template.find(".group-entry input").each((_idx, _entry) => {
let entry = $(_entry);
if(entry.attr("default") !== undefined || !entry.prop("checked"))
return;
entry.prop("checked", false).trigger('change');
group_ids.push(parseInt(entry.attr("group-id")));
});
callback(group_ids, false).catch(e => { log.warn(LogCategory.GENERAL, tr("Failed to remove all group assignments: %o"), e)}).then(update_groups);
});
template.find(".group-entry input[default]").prop("checked", template.find(".group-entry input:checked").length == 0);
update_groups();
return template;
},
footer: null,
width: "max-content"
min_width: "10em"
});
current_modal.htmlTag.find(".modal-body").addClass("modal-server-group-assignments");

View File

@ -1,5 +1,5 @@
namespace Modals {
type InfoUpdateCallback = (info: ServerConnectionInfo | boolean) => any;
export type InfoUpdateCallback = (info: ServerConnectionInfo | boolean) => any;
export function openServerInfoBandwidth(server: ServerEntry, update_callbacks?: InfoUpdateCallback[]) : Modal {
let modal: Modal;
let own_callbacks = !update_callbacks;

View File

@ -73,7 +73,7 @@ namespace Modals {
select.on('change', event => {
const value = parseInt(select.val() as string);
settings.static_global(Settings.KEY_FONT_SIZE, value);
settings.changeGlobal(Settings.KEY_FONT_SIZE, value);
console.log("Changed font size of %dpx", value);
$(document.body).css("font-size", value + "px");

View File

@ -140,6 +140,15 @@ class ServerEntry {
let tag = $.spawn("div").addClass("tree-entry server");
/* unread marker */
{
tag.append(
$.spawn("div")
.addClass("marker-text-unread hidden")
.attr("conversation", 0)
);
}
tag.append(
$.spawn("div")
.addClass("server_type icon client-server_green")
@ -388,4 +397,8 @@ class ServerEntry {
if(this.properties.virtualserver_uptime == 0 || this.lastInfoRequest == 0) return this.properties.virtualserver_uptime;
return this.properties.virtualserver_uptime + (new Date().getTime() - this.lastInfoRequest) / 1000;
}
set flag_text_unread(flag: boolean) {
this._htmlTag.find(".marker-text-unread").toggleClass("hidden", !flag);
}
}

View File

@ -608,7 +608,12 @@ class ChannelTree {
name: tr("Ban clients"),
invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1),
callback: () => {
Modals.spawnBanClient((clients).map(entry => entry.clientNickName()), (data) => {
Modals.spawnBanClient(this.client, (clients).map(entry => {
return {
name: entry.clientNickName(),
unique_id: entry.properties.client_unique_identifier
}
}), (data) => {
for (const client of clients)
this.client.serverConnection.send_command("banclient", {
uid: client.properties.client_unique_identifier,

View File

@ -198,6 +198,9 @@ const loader_javascript = {
"js/ui/modal/ModalAvatar.js",
"js/ui/modal/ModalAvatarList.js",
"js/ui/modal/ModalClientInfo.js",
"js/ui/modal/ModalChannelInfo.js",
"js/ui/modal/ModalServerInfo.js",
"js/ui/modal/ModalServerInfoBandwidth.js",
"js/ui/modal/ModalQuery.js",
"js/ui/modal/ModalQueryManage.js",
"js/ui/modal/ModalPlaylistList.js",
@ -207,14 +210,11 @@ const loader_javascript = {
"js/ui/modal/ModalSettings.js",
"js/ui/modal/ModalCreateChannel.js",
"js/ui/modal/ModalServerEdit.js",
"js/ui/modal/ModalServerInfo.js",
"js/ui/modal/ModalServerInfoBandwidth.js",
"js/ui/modal/ModalChangeVolume.js",
"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",
@ -371,7 +371,8 @@ const loader_style = {
"css/static/modal-invite.css",
"css/static/modal-playlist.css",
"css/static/modal-banlist.css",
"css/static/modal-bancreate.css",
"css/static/modal-banclient.css",
"css/static/modal-channelinfo.css",
"css/static/modal-clientinfo.css",
"css/static/modal-serverinfo.css",
"css/static/modal-serverinfobandwidth.css",
@ -523,7 +524,7 @@ loader.register_task(loader.Stage.LOADED, {
function: async () => {
fadeoutLoader();
},
priority: 10
priority: 5
});
loader.register_task(loader.Stage.LOADED, {
@ -589,6 +590,19 @@ loader.register_task(loader.Stage.SETUP, {
priority: 10
});
/* test if we're getting loaded within a TeaClient preview window */
loader.register_task(loader.Stage.SETUP, {
name: "TeaClient tester",
function: async () => {
//@ts-ignore
if(typeof __teaclient_preview_notice !== "undefined" && typeof __teaclient_preview_error !== "undefined") {
loader.critical_error("Why you're opening TeaWeb within the TeaSpeak client?!");
throw "we're already a TeaClient!";
}
},
priority: 100
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "log enabled initialisation",
function: async () => log.initialize(app.type === app.Type.CLIENT_DEBUG || app.type === app.Type.WEB_DEBUG ? log.LogType.TRACE : log.LogType.INFO),

View File

@ -19,9 +19,8 @@
- Icon Select
- Icon upload
- Avatar list
- Server group assignments checkbox
- Invite buddy
- Change volume
- Ban Liste
- Übersicht
@ -132,6 +131,7 @@
- File Transfer bytes transferred, month & global (Up + Download)
TODO:
Context menu for clients => Group assignment => Not assignable groups sind assignable
Fix these icons: https://img.did.science/Screenshot_20-11-06.png
- Application Options
- Crash