Property and channel tree improvements

This commit is contained in:
WolverinDEV 2018-04-30 23:57:21 +02:00
parent 09229a8e9d
commit 4689cdd4e0
21 changed files with 1056 additions and 235 deletions

View file

@ -0,0 +1,249 @@
$animtime: .5s;
$ease: cubic-bezier(.45, 0, .55, 1);
.music-wrapper {
display: flex;
position: relative;
width: 400px;
height: 400px;
user-select: none;
.container {
.right:hover {
.flip-card {
transform: rotateY(-60deg);
}
z-index: 120;
}
}
.left, .right {
position: absolute;
width: 50%;
height: 100%;
perspective-origin: 50% 50%;
perspective: 1200px;
.flip-card,
.static-card {
background: white;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
border: 7px solid #dedede;
img {
width: calc(100% * 2);
height: 100%;
}
}
.static-card {
border-right: none;
}
.flip-card {
border-left: none;
transform-origin: 0% 50%;
transition: transform $animtime $ease;
transform: rotateY(0);
&:before {
position: absolute;
content: '';
width: 100%;
height: 100%;
top: 0;
right: -20px;
box-shadow: 29px 0px 52px 6px rgba(186, 186, 186, 1);
}
img {
position: absolute;
background-color: #fff;
right: 0;
}
}
}
.left {
left: 0;
}
.right {
right: 0;
}
.controls {
position: absolute;
right: 0;
width: 50%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
cursor: pointer;
&:after {
position: absolute;
content: '';
right: 0;
top: 0;
width: 100%;
height: 100%;
box-shadow: inset 20px 0px 37px -10px rgba(0, 0, 0, 0.75);
pointer-events: none;
transition: width $animtime $ease;
}
input[type="radio"] {
position: absolute;
left: -1000px;
}
label {
flex-grow: 1;
display: block;
width: 100%;
border-top: 1px #e6e6e6 solid;
border-bottom: 1px #9c9c9c solid;
box-sizing: border-box;
cursor: pointer;
background-color: #dcdcdc;
span {
background-repeat: no-repeat;
background-position: 16px 42px;
width: 80px;
height: 125px;
display: block;
pointer-events: none;
}
}
input:checked + label,
label:active {
background-color: #BCBCBC;
box-shadow: inset 0px 0px 10px 5px rgba(120, 120, 120, 0.2);
border: 1px solid #fff;
}
.btn-forward span {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABP0lEQVRoQ+2YwW3DMAxFX4ZNT7knE7QrNB0iWSKZIZ2g9/aYS4oPxECPpUSKFkCdZEAW+Pit729vmHxsJq+fAshWsBQoBTo7UI9QZwO7by8FgBOwA76629mwgYcCD+AbOABHQNfDhhfAUvAFeAFuowi8AVT3HXgDXp/zUJYIgKVgqbAFrpEEkQCqW+dB52IP/ESARAMsNcuh5FRnb4hRAEvdAnC13NEAAnG13AwAV8vNBHCx3GyAbstdC0Cz5a4JoMlyC8Axfeqt/f5Mtf9+a69FgebclA2g5KrUqvSquXlkAujbQWn101z1nxsyABQllE4/PM7PaIBpw9y0cbrJGi1nIvIRarbGbIBua8wEcLHGDABXaxwNMP2vRUvD3Nd6uJB7UZYNC8DSrYi1pUBEVy17lgKWbkWsLQUiumrZ8xeQiV4xsW8UvQAAAABJRU5ErkJggg==");
}
.btn-rewind span {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAsElEQVRoQ+2YSw6AIAxEh9vqifS2utAF0YgtpBCS57raz0wHxqTJnzR5/aKB0QiCAAg0TgAKNQ6w+fUaBI5C1lXS/lPVImkrxLhqcgXfSWkABDL+QaHHMrLEku+CCYWg0PtEc7HCFcxBdk2Aq0TOOiiECqFCfk+EH8AP4Ae+9wY/gB8wqCoyiowio8jo2J+7BqHqF1LjyPpVZ8hEA4YhhYaAQOh4DR8HAcOQQkOmR+AEspRaMYlt9skAAAAASUVORK5CYII=");
}
.btn-settings span {
background-size: 42px 42px;
background-position: 22px 42px;
background-image: url("../../img/music/settings.svg");
}
}
.controls-overlay {
position: absolute;
display: block;
top: calc(100% - 40px);
width: 100%;
height: 40px;
z-index: 100;
.timer {
margin-left: 20px;
height: 15px;
z-index: 200;
width: 360px;
display: inline-flex;
justify-content: space-between;
vertical-align: center;
.button {
width: 10px;
height: 12px;
margin-left: 2px;
fill: none;
stroke: #4c4c4c;;
stroke-width: 0.5;
stroke-miterlimit: 10;
cursor: pointer;
//box-shadow: 20px 20px 20px 20px rgb(186, 0, 12);
}
.button:hover {
animation: bounce 500ms alternate;
transform: scale(1.1);
transition: transform 150ms;
}
.timeline * {
border: gray 0;
border-radius: 8px;
}
//TODO box SHADOW
.timeline {
width: 90%;
height: 4px;
float: right;
background: #DBE3E3;
position: relative;
align-self: center;
border: gray 0;
border-radius: 8px;
.buffered {
position: absolute;
width: 80%;
height: 100%;
background: #a0a0a0;
}
.played {
position: absolute;
width: 60%;
height: 100%;
background: #1fe2e3;
}
.slider {
position: absolute;
width: 4px;
height: 12px;
top: -4px;
background: #303030;
cursor: pointer;
}
}
}
}
}
.music-wrapper.empty {
border: 7px solid #dedede;
display: flex;
flex-direction: column;
background: white;
}
.music-wrapper.empty img {
margin: 5px;
-webkit-animation: rotation 5s infinite linear;
}
@-webkit-keyframes rotation {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(359deg);
}
}
.music-wrapper.empty a {
text-align: center;
margin: 5px;
margin-top: 20px;
font-size: 20px;
font-family: Arial;
}

View file

@ -30,11 +30,12 @@
#chat .message {
width: 100%;
display: inline-flex;
display: inline-block;
}
#chat .message *{
display: inline-flex;
display: inline-block;
vertical-align: top;
}
#chat .chats {

View file

@ -14,6 +14,7 @@ class AudioController {
this._volume = 1;
this._codecCache = [];
this._timeIndex = 0;
this._latencyBufferLength = 3;
this.allowBuffering = true;
this.speakerContext = AudioController.globalContext;
this.onSpeaking = function () { };
@ -37,7 +38,7 @@ class AudioController {
playBuffer(buffer) {
if (buffer.sampleRate != this.speakerContext.sampleRate)
console.warn("[AudioController] Source sample rate isn't equal to playback sample rate! (" + buffer.sampleRate + " | " + this.speakerContext.sampleRate + ")");
this.applayVolume(buffer);
this.applyVolume(buffer);
this.audioCache.push(buffer);
if (this.playerState == PlayerState.STOPPED || this.playerState == PlayerState.STOPPING) {
console.log("[Audio] Starting new playback");
@ -47,7 +48,7 @@ class AudioController {
switch (this.playerState) {
case PlayerState.PREBUFFERING:
case PlayerState.BUFFERING:
if (this.audioCache.length < 3) {
if (this.audioCache.length <= this._latencyBufferLength) {
if (this.playerState == PlayerState.BUFFERING) {
if (this.allowBuffering)
break;
@ -119,9 +120,9 @@ class AudioController {
return;
this._volume = val;
for (let buffer of this.audioCache)
this.applayVolume(buffer);
this.applyVolume(buffer);
}
applayVolume(buffer) {
applyVolume(buffer) {
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
let data = buffer.getChannelData(channel);
for (let sample = 0; sample < data.length; sample++) {
@ -721,7 +722,6 @@ class CodecPool {
else {
this.entries[index].instance.initialise().then((flag) => {
//TODO test success flag
console.error(flag);
this.ownCodec(clientId, false).then(resolve).catch(reject);
}).catch(error => {
console.error("Could not initialize codec!\nError: %o", error);
@ -1409,7 +1409,7 @@ class ChannelEntry {
}
createChatTag(braces = false) {
let tag = $.spawn("div");
tag.css("display", "table");
tag.css("display", "inline-block");
tag.css("cursor", "pointer");
tag.css("font-weight", "bold");
tag.css("color", "darkblue");
@ -1417,10 +1417,15 @@ class ChannelEntry {
tag.text("\"" + this.channelName() + "\"");
else
tag.text(this.channelName());
tag.attr("oncontextmenu", "chat_channel_contextmenu(this, ...arguments);");
tag.contextmenu(event => {
if (event.isDefaultPrevented())
return;
event.preventDefault();
this.showContextMenu(event.pageX, event.pageY);
});
tag.attr("channelId", this.channelId);
tag.attr("channelName", this.channelName());
return tag.wrap("<p/>").parent();
return tag;
}
channelType() {
if (this.properties.channel_flag_permanent == true)
@ -1546,6 +1551,8 @@ class ClientProperties {
this.client_input_hardware = false;
this.client_input_muted = false;
this.client_is_channel_commander = false;
this.client_teaforum_id = 0;
this.client_teaforum_name = "";
}
}
class ClientEntry {
@ -1676,8 +1683,16 @@ class ClientEntry {
type: MenuEntryType.ENTRY,
icon: "client-ban_client",
name: "Ban client",
disabled: true,
callback: () => { }
invalidPermission: !this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1),
callback: () => {
Modals.spawnBanClient(this.properties.client_nickname, (duration, reason) => {
this.channelTree.client.serverConnection.sendCommand("banclient", {
uid: this.properties.client_unique_identifier,
banreason: reason,
time: duration
});
});
}
}, MenuEntry.HR(), {
type: MenuEntryType.ENTRY,
icon: "client-volume",
@ -1708,19 +1723,30 @@ class ClientEntry {
}
static chatTag(id, name, uid, braces = false) {
let tag = $.spawn("div");
tag.css("cursor", "pointer");
tag.css("font-weight", "bold");
tag.css("color", "darkblue");
tag.css("display", "table");
tag.css("cursor", "pointer")
.css("font-weight", "bold")
.css("color", "darkblue")
.css("display", "inline-block")
.css("margin", 0);
if (braces)
tag.text("\"" + name + "\"");
else
tag.text(name);
tag.attr("oncontextmenu", "chat_client_contextmenu(this, ...arguments);");
tag.contextmenu(event => {
if (event.isDefaultPrevented())
return;
event.preventDefault();
let client = globalClient.channelTree.findClient(id);
if (!client)
return;
if (client.properties.client_unique_identifier != uid)
return;
client.showContextMenu(event.pageX, event.pageY);
});
tag.attr("clientId", id);
tag.attr("clientUid", uid);
tag.attr("clientName", name);
return tag.wrap("<p/>").parent();
return tag;
}
createChatTag(braces = false) {
return ClientEntry.chatTag(this.clientId(), this.clientNickName(), this.clientUid(), braces);
@ -1865,7 +1891,7 @@ class ClientEntry {
this.audioController = undefined;
}
calculateOnlineTime() {
return new Date().getTime() / 1000 - this.properties.client_lastconnected;
return Date.now() / 1000 - this.properties.client_lastconnected;
}
avatarId() {
function str2ab(str) {
@ -2988,30 +3014,11 @@ if (typeof (customElements) !== "undefined") {
customElements.define('x-properties', X_Properties, { extends: 'div' });
customElements.define('x-property', X_Property, { extends: 'div' });
}
class Settings {
constructor() {
this.cacheGlobal = {};
this.cacheServer = {};
this.updated = false;
this._staticPropsTag = $("#properties");
this.cacheGlobal = JSON.parse(localStorage.getItem("settings.global"));
if (!this.cacheGlobal)
this.cacheGlobal = {};
const _this = this;
this.saveWorker = setInterval(() => {
if (_this.updated)
_this.save();
}, 5 * 1000);
this.initializeStatic();
}
initializeStatic() {
location.search.substr(1).split("&").forEach(part => {
let item = part.split("=");
$("<x-property></x-property>")
.attr("key", item[0])
.attr("value", item[1])
.appendTo(this._staticPropsTag);
});
class StaticSettings {
static get instance() {
if (!this._instance)
this._instance = new StaticSettings(true);
return this._instance;
}
static transformStO(input, _default) {
if (typeof input === "undefined")
@ -3037,24 +3044,68 @@ class Settings {
return undefined;
return JSON.stringify(input);
}
constructor(_reserved = undefined) {
if (_reserved && !StaticSettings._instance) {
this._staticPropsTag = $("#properties");
this.initializeStatic();
}
else {
this._handle = StaticSettings.instance;
}
}
initializeStatic() {
location.search.substr(1).split("&").forEach(part => {
let item = part.split("=");
$("<x-property></x-property>")
.attr("key", item[0])
.attr("value", item[1])
.appendTo(this._staticPropsTag);
});
}
static(key, _default) {
if (this._handle)
return this._handle.static(key, _default);
let result = this._staticPropsTag.find("[key='" + key + "']");
console.log("%d | %o", result.length, result);
return StaticSettings.transformStO(result.length > 0 ? decodeURIComponent(result.last().attr("value")) : undefined, _default);
}
deleteStatic(key) {
if (this._handle) {
this._handle.deleteStatic(key);
return;
}
let result = this._staticPropsTag.find("[key='" + key + "']");
if (result.length != 0)
result.detach();
}
}
class Settings extends StaticSettings {
constructor() {
super();
this.cacheGlobal = {};
this.cacheServer = {};
this.updated = false;
this.cacheGlobal = JSON.parse(localStorage.getItem("settings.global"));
if (!this.cacheGlobal)
this.cacheGlobal = {};
this.saveWorker = setInterval(() => {
if (this.updated)
this.save();
}, 5 * 1000);
}
global(key, _default) {
let result = this.cacheGlobal[key];
return Settings.transformStO(result, _default);
return StaticSettings.transformStO(result, _default);
}
server(key, _default) {
let result = this.cacheServer[key];
return Settings.transformStO(result, _default);
}
static(key, _default) {
let result = this._staticPropsTag.find("[key='" + key + "']");
console.log("%d | %o", result.length, result);
return Settings.transformStO(result.length > 0 ? decodeURIComponent(result.last().attr("value")) : undefined, _default);
return StaticSettings.transformStO(result, _default);
}
changeGlobal(key, value) {
if (this.cacheGlobal[key] == value)
return;
this.updated = true;
this.cacheGlobal[key] = Settings.transformOtS(value);
this.cacheGlobal[key] = StaticSettings.transformOtS(value);
if (Settings.UPDATE_DIRECT)
this.save();
}
@ -3062,7 +3113,7 @@ class Settings {
if (this.cacheServer[key] == value)
return;
this.updated = true;
this.cacheServer[key] = Settings.transformOtS(value);
this.cacheServer[key] = StaticSettings.transformOtS(value);
if (Settings.UPDATE_DIRECT)
this.save();
}
@ -3090,11 +3141,6 @@ class Settings {
let global = JSON.stringify(this.cacheGlobal);
localStorage.setItem("settings.global", global);
}
deleteStatic(key) {
let result = this._staticPropsTag.find("[key='" + key + "']");
if (result.length != 0)
result.detach();
}
}
Settings.KEY_DISABLE_CONTEXT_MENU = "disableContextMenu";
Settings.KEY_DISABLE_UNLOAD_DIALOG = "disableUnloadDialog";
@ -3108,12 +3154,16 @@ class InfoBar {
this._htmlTag = htmlTag;
}
createInfoTable(infos) {
let table = $("<table/>");
for (let e in infos) {
console.log("Display info " + e);
let entry = $("<tr/>");
entry.append("<td class='info_key'>" + e + ":</td>");
entry.append("<td>" + infos[e] + "</td>");
let table = $.spawn("table");
for (let key in infos) {
console.log("Display info " + key);
let entry = $.spawn("tr");
entry.append($.spawn("td").addClass("info_key").html(key + ":"));
let value = $.spawn("td");
console.log(infos[key]);
console.log(MessageHelper.formatElement(infos[key]));
MessageHelper.formatElement(infos[key]).forEach(e => e.appendTo(value));
entry.append(value);
table.append(entry);
}
return table;
@ -3187,7 +3237,7 @@ class InfoBar {
else if (this._currentSelected instanceof ChannelEntry) {
let props = this._currentSelected.properties;
this._htmlTag.append(this.createInfoTable({
"Name": this._currentSelected.createChatTag().html(),
"Name": this._currentSelected.createChatTag(),
"Topic": this._currentSelected.properties.channel_topic,
"Codec": this._currentSelected.properties.channel_codec,
"Codec Quality": this._currentSelected.properties.channel_codec_quality,
@ -3198,19 +3248,22 @@ class InfoBar {
}));
}
else if (this._currentSelected instanceof ClientEntry) {
this._currentSelected.updateVariables();
this._currentSelected.updateClientVariables();
let version = this._currentSelected.properties.client_version;
if (!version)
version = "";
let infos = {
"Name": this._currentSelected.createChatTag().html(),
"Name": this._currentSelected.createChatTag(),
"Description": this._currentSelected.properties.client_description,
"Version": "<a title='" + ChatMessage.formatMessage(version) + "'>" + version.split(" ")[0] + "</a>" + " on " + this._currentSelected.properties.client_platform,
"Online since": "<a class='online'>" + formatDate(this._currentSelected.calculateOnlineTime()) + "</a>",
"Version": MessageHelper.formatMessage("{0} on {1}", $.spawn("a").attr("title", version).text(version.split(" ")[0]), this._currentSelected.properties.client_platform),
"Online since": $.spawn("a").addClass("online").text(formatDate(this._currentSelected.calculateOnlineTime())),
"Volume": this._currentSelected.audioController.volume * 100 + " %"
};
if (this._currentSelected.properties["client_teaforum_id"] > 0) {
infos["TeaSpeak Account"] = "<a href='//forum.teaspeak.de/index.php?members/{1}/' target='_blank'>{0}</a>".format(this._currentSelected.properties["client_teaforum_name"], this._currentSelected.properties["client_teaforum_id"]);
if (this._currentSelected.properties.client_teaforum_id > 0) {
infos["TeaSpeak Account"] = $.spawn("a")
.attr("href", "//forum.teaspeak.de/index.php?members/" + this._currentSelected.properties.client_teaforum_id)
.attr("target", "_blank")
.text(this._currentSelected.properties.client_teaforum_id);
}
this._htmlTag.append(this.createInfoTable(infos));
{
@ -3264,13 +3317,17 @@ class InfoBar {
.css("margin-left", "10px")
.css("align-items", "center");
this.handle.fileManager.icons.generateTag(group.properties.iconid).appendTo(groupTag);
$.spawn("div").text(group.name).css("margin-left", "3px").appendTo(groupTag);
$.spawn("div").text(group.name)
.css("margin-left", "3px").appendTo(groupTag);
groupTag.appendTo(channelGroup);
}
this._htmlTag.append(channelGroup);
}
if (this._currentSelected.properties.client_flag_avatar.length > 0)
this.handle.fileManager.avatars.generateTag(this._currentSelected).appendTo(this._htmlTag);
this.handle.fileManager.avatars.generateTag(this._currentSelected)
.css("margin-top", "20px")
.css("max-height", "90%")
.css("max-width", "100%").appendTo(this._htmlTag);
this.intervals.push(setInterval(this.updateClientTimings.bind(this), 1000));
}
}
@ -4628,14 +4685,14 @@ class AvatarManager {
let img = $.spawn("img");
img.attr("alt", "");
let avatar = this.resolveCached(client);
avatar = undefined;
if (avatar) {
img.attr("src", "data:image/png;base64," + avatar.base64);
tag.append(img);
}
else {
img.attr("src", "file://null");
let loader = $.spawn("div");
loader.addClass("avatar_loading");
let loader = $.spawn("img");
loader.attr("src", "img/loading_image.svg").css("width", "75%");
tag.append(loader);
this.loadAvatar(client).then(avatar => {
img.attr("src", "data:image/png;base64," + avatar.base64);
@ -4649,7 +4706,7 @@ class AvatarManager {
}).catch(reason => {
console.error("Could not load avatar for " + client.clientNickName() + ". Reason: " + reason);
//TODO Broken image
loader.removeClass("avatar_loading").addClass("icon client-warning").attr("tag", "Could not load avatar " + client.clientNickName());
loader.addClass("icon client-warning").attr("tag", "Could not load avatar " + client.clientNickName());
});
}
return tag;
@ -4748,6 +4805,73 @@ var ChatType;
ChatType[ChatType["CHANNEL"] = 2] = "CHANNEL";
ChatType[ChatType["CLIENT"] = 3] = "CLIENT";
})(ChatType || (ChatType = {}));
var MessageHelper;
(function (MessageHelper) {
function htmlEscape(message) {
const div = document.createElement('div');
div.innerText = message;
message = div.innerHTML;
return message.replace(/ /g, '&nbsp;');
}
MessageHelper.htmlEscape = htmlEscape;
function formatElement(object) {
if ($.isArray(object)) {
let result = [];
for (let element of object)
result.push(...this.formatElement(element));
return result;
}
else if (typeof (object) == "string") {
if (object.length == 0)
return [];
return [$.spawn("a").html(this.htmlEscape(object))];
}
else if (typeof (object) === "object") {
if (object instanceof jQuery)
return [object];
return this.formatElement("<unknwon object>");
}
else if (typeof (object) === "function")
return this.formatElement(object());
else if (typeof (object) === "undefined")
return this.formatElement("<undefined>");
return this.formatElement("<unknown object type " + typeof object + ">");
}
MessageHelper.formatElement = formatElement;
function formatMessage(pattern, ...objects) {
let begin = 0, found = 0;
let result = [];
do {
found = pattern.indexOf('{', found);
if (found == -1 || pattern.length <= found + 1) {
result.push(...this.formatElement(pattern.substr(begin)));
break;
}
if (found > 0 && pattern[found - 1] == '\\') {
//TODO remove the escape!
found++;
continue;
}
result.push(...this.formatElement(pattern.substr(begin, found - begin))); //Append the text
let number;
let offset = 0;
while ("0123456789".includes(pattern[found + 1 + offset]))
offset++;
number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0");
if (pattern[found + offset + 1] != '}') {
found++;
continue;
}
if (objects.length < number)
console.warn("Message to format contains invalid index (" + number + ")");
result.push(...this.formatElement(objects[number]));
begin = found = found + 2 + offset;
console.log("Offset: " + offset + " Number: " + number);
} while (found++);
return result;
}
MessageHelper.formatMessage = formatMessage;
})(MessageHelper || (MessageHelper = {}));
class ChatMessage {
constructor(message) {
this.date = new Date();
@ -4768,27 +4892,12 @@ class ChatMessage {
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");
let messageTag = $.spawn("div");
messageTag.html(this.message);
messageTag.css("color", "blue");
this._htmlTag = tag;
tag.append(dateTag);
tag.append(messageTag);
this.message.forEach(e => e.appendTo(tag));
tag.hide();
return tag;
}
static formatMessage(message) {
/*
message = message
.replace(/ /g, '&nbsp;')
.replace(/\n/g, "<br/>");
*/
const div = document.createElement('div');
div.innerText = message;
message = div.innerHTML;
console.log(message + "->" + div.innerHTML);
return message;
}
}
class ChatEntry {
constructor(handle, type, key) {
@ -4800,25 +4909,15 @@ class ChatEntry {
this.onClose = function () { return true; };
}
appendError(message, ...args) {
this.appendMessage("<a style='color: red'>{0}</a>".format(ChatMessage.formatMessage(message).format(...args)), false);
let entries = MessageHelper.formatMessage(message, ...args);
entries.forEach(e => e.css("color", "red"));
this.pushChatMessage(new ChatMessage(entries));
}
appendMessage(message, fmt = true, ...args) {
let parms = [];
for (let index = 2; index < arguments.length; index++) {
if (typeof arguments[index] == "string")
arguments[index] = ChatMessage.formatMessage(arguments[index]);
else if (arguments[index] instanceof jQuery)
arguments[index] = arguments[index].html();
else {
console.error("Invalid type " + typeof arguments[index] + "|" + arguments[index].prototype);
arguments[index] = arguments[index].toString();
}
parms.push(arguments[index]);
}
let msg = fmt ? ChatMessage.formatMessage(message) : message;
msg = msg.format(parms);
let elm = new ChatMessage(msg);
this.history.push(elm);
this.pushChatMessage(new ChatMessage(MessageHelper.formatMessage(message, ...args)));
}
pushChatMessage(entry) {
this.history.push(entry);
while (this.history.length > 100) {
let elm = this.history.pop_front();
elm.htmlTag.animate({ opacity: 0 }, 200, function () {
@ -4829,8 +4928,8 @@ class ChatEntry {
let box = $(this.handle.htmlTag).find(".messages");
let mbox = $(this.handle.htmlTag).find(".message_box");
let bottom = box.scrollTop() + box.height() + 1 >= mbox.height();
mbox.append(elm.htmlTag);
elm.htmlTag.show().css("opacity", "0").animate({ opacity: 1 }, 100);
mbox.append(entry.htmlTag);
entry.htmlTag.show().css("opacity", "0").animate({ opacity: 1 }, 100);
if (bottom)
box.scrollTop(mbox.height());
}
@ -5341,6 +5440,88 @@ var Modals;
Identity isnt valid!
</div>
*/
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
var Modals;
(function (Modals) {
function spawnBanClient(name, callback) {
const connectModal = createModal({
header: function () {
return "Ban client";
},
body: function () {
let tag = $("#tmpl_client_ban").tmpl({
client_name: name
});
let maxTime = 0; //globalClient.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value;
let unlimited = maxTime == 0 || maxTime == -1;
if (unlimited)
maxTime = 0;
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);
durationTag.change(() => banTag.trigger('change'));
banTag.change(event => {
let element = $(event.target.selectedOptions.item(0));
if (element.val() !== "perm") {
durationTag.prop("disabled", false);
let current = durationTag.val();
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);
}
});
return tag;
},
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");
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
});
connectModal.open();
connectModal.htmlTag.find(".btn_success").on('click', () => {
connectModal.close();
let length = connectModal.htmlTag.find(".ban_duration").val();
let duration = connectModal.htmlTag.find(".ban_duration_type option:selected");
console.log(duration);
console.log(length + "*" + duration.attr("duration-scale"));
callback(length * parseInt(duration.attr("duration-scale")), connectModal.htmlTag.find(".ban_reason").val());
});
}
Modals.spawnBanClient = spawnBanClient;
})(Modals || (Modals = {}));
/// <reference path="Codec.ts"/>
class BasicCodec {
constructor(codecSampleRate) {
@ -5561,6 +5742,7 @@ class CodecWrapper extends BasicCodec {
/// <reference path="utils/modal.ts" />
/// <reference path="ui/modal/ModalConnect.ts" />
/// <reference path="ui/modal/ModalCreateChannel.ts" />
/// <reference path="ui/modal/ModalBanClient.ts" />
/// <reference path="codec/CodecWrapper.ts" />
/// <reference path="settings.ts" />
/// <reference path="log.ts" />
@ -5569,7 +5751,9 @@ let globalClient;
let chat;
let forumIdentity;
function main() {
//console.log(ChatEntry.formatMessage("Hello World '{0}' | '{1}' | {12} X", "XXX"));
//localhost:63343/Web-Client/index.php?disableUnloadDialog=1&default_connect_type=forum&default_connect_url=localhost
//disableUnloadDialog=1&default_connect_type=forum&default_connect_url=localhost&loader_ignore_age=1
AudioController.initializeAudioController();
if (!TSIdentityHelper.setup()) {
console.error("Could not setup the TeamSpeak identity parser!");
@ -5595,7 +5779,7 @@ function main() {
//Modals.spawnSettingsModal();
//Modals.createChannelModal(undefined);
if (settings.static("default_connect_url")) {
if (settings.static("default_connect_type", "forum")) {
if (settings.static("default_connect_type", "forum") == "forum") {
globalClient.startConnection(settings.static("default_connect_url"), forumIdentity);
}
else
@ -5683,10 +5867,10 @@ class ServerEntry {
tag.attr("id", "server");
tag.addClass("server");
tag.append($.spawn("div").addClass("server_type icon client-server_green"));
tag.append("<a class='name'>" + this.properties.virtualserver_name + "</a>");
tag.append($.spawn("a").addClass("name").text(this.properties.virtualserver_name));
const serverIcon = $("<span/>");
//we cant spawn an icon on creation :)
serverIcon.append("<div class='icon_property icon_empty'></div>");
serverIcon.append($.spawn("div").addClass("icon_property icon_empty"));
tag.append(serverIcon);
return this._htmlTag = tag;
}
@ -5728,7 +5912,7 @@ class ServerEntry {
this.channelTree.client.serverConnection.sendCommand("servergetvariables");
}
shouldUpdateProperties() {
return this.nextInfoRequest < new Date().getTime();
return this.nextInfoRequest < Date.now();
}
calculateUptime() {
if (this.properties.virtualserver_uptime == 0 || this.lastInfoRequest == 0)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -103,4 +103,5 @@
id="path13674" /><path
d="M104.397,50.073L118.944,50.073L118.944,54.193L109.293,54.193L109.293,60.077L118.17,60.077L118.17,64.057L109.293,64.057L109.293,70.538L119.05,70.538L119.05,74.657L104.398,74.657L104.398,50.073L104.397,50.073Z"
style="fill:#a9aaac;fill-rule:nonzero;fill-opacity:1"
id="path13676" /></g></g></g></g></svg>
id="path13676" /></g></g></g></g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -30,6 +30,7 @@
<link rel="stylesheet" href="css/general.css" type="text/css">
<link rel="stylesheet" href="css/modals.css" type="text/css">
<link rel="stylesheet" href="css/loader.css" type="text/css">
<link rel="stylesheet" href="css/music/info_plate.css" type="text/css">
<!-- PHP generated properies -->
<!-- localhost:63342/TeaSpeak-Web/index.php?_ijt=o48hmliefjoa8cer8v7mpl98pj&connect_default_host=192.168.43.141 -->
@ -105,6 +106,7 @@
<div style="height: 45px; width: 100%; border-radius: 2px 0px 0px 0px; border-bottom-width: 0px; background-color: lightgrey" class="main_container">
<div id="control_bar" class="control_bar">
<div class="button btn_connect"><div class="icon_x32 client-connect"></div></div>
<!--<div class="button btn_disconnect"><div class="icon_x32 client-disconnect"></div></div>-->
<div style="border-left:2px solid gray;height: auto; margin-left: 5px; margin-right: 5px"></div>
<div class="button btn_client_away"><div class="icon_x32 client-away"></div></div>
<div class="button btn_mute_input"><div class="icon_x32 client-input_muted"></div></div>
@ -117,9 +119,7 @@
<div style="flex-direction: row; height: 100%; width: 100%; display: flex">
<div style="width: 60%; flex-direction: column;">
<div style="height: 60%; border-radius: 0px 0px 0px 0px; border-right-width: 0px; overflow: auto; overflow-x: visible" class="main_container">
<div class="channelTree" id="channelTree">
<div class="server l"><div class="icon client-server_green"></div> TeaSpeak web!</div>
</div>
<div class="channelTree" id="channelTree"></div>
</div> <!-- Channel tree -->
<div style="height: 40%; border-radius: 0px 0px 0px 2px; border-top-width: 0px; border-right-width: 0px;" class="main_container">
<div id="chat">
@ -136,7 +136,7 @@
</div> <!-- Chat window -->
</div>
<div style="width: 40%; border-radius: 0px 0px 2px 0px;" class="main_container">
<div id="select_info" class="select_info">
<div id="select_info" class="select_info" style="width: 100%; max-width: 100%">
</div>
</div> <!-- Selection info -->
</div>
@ -148,6 +148,9 @@
<div id="scripts">
<script src="js/load.js"></script>
</div>
<div id="music-test"></div>
<div style="height: 100px"></div>
</body>
<footer>
<div class="container">

View file

@ -425,14 +425,13 @@ class AvatarManager {
img.attr("alt", "");
let avatar = this.resolveCached(client);
avatar = undefined;
if(avatar) {
img.attr("src", "data:image/png;base64," + avatar.base64);
tag.append(img);
} else {
img.attr("src", "file://null");
let loader = $.spawn("div");
loader.addClass("avatar_loading");
let loader = $.spawn("img");
loader.attr("src", "img/loading_image.svg").css("width", "75%");
tag.append(loader);
this.loadAvatar(client).then(avatar => {
@ -448,7 +447,7 @@ class AvatarManager {
}).catch(reason => {
console.error("Could not load avatar for " + client.clientNickName() + ". Reason: " + reason);
//TODO Broken image
loader.removeClass("avatar_loading").addClass("icon client-warning").attr("tag", "Could not load avatar " + client.clientNickName());
loader.addClass("icon client-warning").attr("tag", "Could not load avatar " + client.clientNickName());
});
}

View file

@ -16,13 +16,17 @@ class InfoBar {
private createInfoTable(infos: any) : JQuery<HTMLElement> {
let table = $("<table/>");
let table = $.spawn("table");
for(let e in infos) {
console.log("Display info " + e);
let entry = $("<tr/>");
entry.append("<td class='info_key'>" + e + ":</td>");
entry.append("<td>" + infos[e] + "</td>");
for(let key in infos) {
console.log("Display info " + key);
let entry = $.spawn("tr");
entry.append($.spawn("td").addClass("info_key").html(key + ":"));
let value = $.spawn("td");
console.log(infos[key]);
console.log( MessageHelper.formatElement(infos[key]));
MessageHelper.formatElement(infos[key]).forEach(e => e.appendTo(value));
entry.append(value);
table.append(entry);
}
@ -106,7 +110,7 @@ class InfoBar {
} else if(this._currentSelected instanceof ChannelEntry) {
let props = this._currentSelected.properties;
this._htmlTag.append(this.createInfoTable({
"Name": this._currentSelected.createChatTag().html(),
"Name": this._currentSelected.createChatTag(),
"Topic": this._currentSelected.properties.channel_topic,
"Codec": this._currentSelected.properties.channel_codec,
"Codec Quality": this._currentSelected.properties.channel_codec_quality,
@ -115,20 +119,35 @@ class InfoBar {
"Subscription Status": "unknown",
"Voice Data Encryption": "unknown"
}));
} else if(this._currentSelected instanceof ClientEntry) {
this._currentSelected.updateVariables();
} else if(this._currentSelected instanceof MusicClientEntry) {
this._htmlTag.append("Im a music bot!");
let frame = $("#tmpl_music_frame" + (this._currentSelected.properties.music_track_id == 0 ? "_empty" : "")).tmpl({
thumbnail: "img/loading_image.svg"
}).css("align-self", "center");
if(this._currentSelected.properties.music_track_id == 0) {
} else {
}
this._htmlTag.append(frame);
//TODO
} else if(this._currentSelected instanceof ClientEntry) { this._currentSelected.updateClientVariables();
let version: string = this._currentSelected.properties.client_version;
if(!version) version = "";
let infos = {
"Name": this._currentSelected.createChatTag().html(),
"Name": this._currentSelected.createChatTag(),
"Description": this._currentSelected.properties.client_description,
"Version": "<a title='" + ChatMessage.formatMessage(version) + "'>" + version.split(" ")[0] + "</a>" + " on " + this._currentSelected.properties.client_platform,
"Online since": "<a class='online'>" + formatDate(this._currentSelected.calculateOnlineTime()) + "</a>",
"Version": MessageHelper.formatMessage("{0} on {1}", $.spawn("a").attr("title", version).text(version.split(" ")[0]), this._currentSelected.properties.client_platform),
"Online since": $.spawn("a").addClass("online").text(formatDate(this._currentSelected.calculateOnlineTime())),
"Volume": this._currentSelected.audioController.volume * 100 + " %"
};
if(this._currentSelected.properties["client_teaforum_id"] > 0) {
infos["TeaSpeak Account"] = "<a href='//forum.teaspeak.de/index.php?members/{1}/' target='_blank'>{0}</a>".format(this._currentSelected.properties["client_teaforum_name"], this._currentSelected.properties["client_teaforum_id"]);
if(this._currentSelected.properties.client_teaforum_id > 0) {
infos["TeaSpeak Account"] = $.spawn("a")
.attr("href", "//forum.teaspeak.de/index.php?members/" + this._currentSelected.properties.client_teaforum_id)
.attr("target", "_blank")
.text(this._currentSelected.properties.client_teaforum_id);
}
this._htmlTag.append(this.createInfoTable(infos));
@ -169,7 +188,8 @@ class InfoBar {
let channelGroup = $.spawn("div");
channelGroup
.css("display", "flex")
.css("flex-direction", "column");
.css("flex-direction", "column")
.css("margin-bottom", "20px");
let header = $.spawn("div");
header
@ -189,15 +209,42 @@ class InfoBar {
.css("margin-left", "10px")
.css("align-items", "center");
this.handle.fileManager.icons.generateTag(group.properties.iconid).appendTo(groupTag);
$.spawn("div").text(group.name).css("margin-left", "3px").appendTo(groupTag);
$.spawn("div").text(group.name)
.css("margin-left", "3px").appendTo(groupTag);
groupTag.appendTo(channelGroup);
}
this._htmlTag.append(channelGroup);
}
if(this._currentSelected.properties.client_flag_avatar.length > 0)
this.handle.fileManager.avatars.generateTag(this._currentSelected).appendTo(this._htmlTag);
{
if(this._currentSelected.properties.client_flag_avatar.length > 0)
this.handle.fileManager.avatars.generateTag(this._currentSelected)
.css("max-height", "90%")
.css("max-width", "100%").appendTo(this._htmlTag);
}
{
let spawnTag = (type: string, description: string) : JQuery => {
return $.spawn("div").css("display", "inline-flex")
.append($.spawn("div").addClass("icon_x32 client-" + type).css("margin-right", "5px"))
.append($.spawn("a").text(description).css("align-self", "center"));
};
if(!this._currentSelected.properties.client_output_hardware)
spawnTag("hardware_output_muted", "Speakers/Headphones disabled").appendTo(this._htmlTag);
if(!this._currentSelected.properties.client_input_hardware)
spawnTag("hardware_input_muted", "Microphone disabled").appendTo(this._htmlTag);
if(this._currentSelected.properties.client_output_muted)
spawnTag("output_muted", "Speakers/Headphones Muted").appendTo(this._htmlTag);
if(this._currentSelected.properties.client_input_muted)
spawnTag("input_muted", "Microphone Muted").appendTo(this._htmlTag);
}
this.intervals.push(setInterval(this.updateClientTimings.bind(this),1000));
}
}

View file

@ -5,12 +5,79 @@ enum ChatType {
CLIENT
}
namespace MessageHelper {
export function htmlEscape(message: string) : string {
const div = document.createElement('div');
div.innerText = message;
message = div.innerHTML;
return message.replace(/ /g, '&nbsp;');
}
export function formatElement(object: any) : JQuery[] {
if($.isArray(object)) {
let result = [];
for(let element of object)
result.push(...this.formatElement(element));
return result;
} else if(typeof(object) == "string") {
if(object.length == 0) return [];
return [$.spawn("a").html(this.htmlEscape(object))];
} else if(typeof(object) === "object") {
if(object instanceof jQuery)
return [object];
return this.formatElement("<unknwon object>");
} else if(typeof(object) === "function") return this.formatElement(object());
else if(typeof(object) === "undefined") return this.formatElement("<undefined>");
return this.formatElement("<unknown object type " + typeof object + ">");
}
export function formatMessage(pattern: string, ...objects: any[]) : JQuery[] {
let begin = 0, found = 0;
let result: JQuery[] = [];
do {
found = pattern.indexOf('{', found);
if(found == -1 || pattern.length <= found + 1) {
result.push(...this.formatElement(pattern.substr(begin)));
break;
}
if(found > 0 && pattern[found - 1] == '\\') {
//TODO remove the escape!
found++;
continue;
}
result.push(...this.formatElement(pattern.substr(begin, found - begin))); //Append the text
let number;
let offset = 0;
while ("0123456789".includes(pattern[found + 1 + offset])) offset++;
number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0");
if(pattern[found + offset + 1] != '}') {
found++;
continue;
}
if(objects.length < number)
console.warn("Message to format contains invalid index (" + number + ")");
result.push(...this.formatElement(objects[number]));
begin = found = found + 2 + offset;
console.log("Offset: " + offset + " Number: " + number);
} while(found++);
return result;
}
}
class ChatMessage {
date: Date;
message: string;
message: JQuery[];
private _htmlTag: JQuery<HTMLElement>;
constructor(message) {
constructor(message: JQuery[]) {
this.date = new Date();
this.message = message;
}
@ -32,29 +99,12 @@ class ChatMessage {
dateTag.css("margin-right", "4px");
dateTag.css("color", "dodgerblue");
let messageTag = $.spawn("div");
messageTag.html(this.message);
messageTag.css("color", "blue");
this._htmlTag = tag;
tag.append(dateTag);
tag.append(messageTag);
this.message.forEach(e => e.appendTo(tag));
tag.hide();
return tag;
}
static formatMessage(message: string) : string {
/*
message = message
.replace(/ /g, '&nbsp;')
.replace(/\n/g, "<br/>");
*/
const div = document.createElement('div');
div.innerText = message;
message = div.innerHTML;
console.log(message + "->" + div.innerHTML);
return message;
}
}
class ChatEntry {
@ -81,24 +131,17 @@ class ChatEntry {
}
appendError(message: string, ...args) {
this.appendMessage("<a style='color: red'>{0}</a>".format(ChatMessage.formatMessage(message).format(...args)), false);
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) {
let parms: any[] = [];
for(let index = 2; index < arguments.length; index++) {
if(typeof arguments[index] == "string") arguments[index] = ChatMessage.formatMessage(arguments[index]);
else if(arguments[index] instanceof jQuery) arguments[index] = arguments[index].html();
else {
console.error("Invalid type " + typeof arguments[index] + "|" + arguments[index].prototype);
arguments[index] = arguments[index].toString();
}
parms.push(arguments[index]);
}
let msg : string = fmt ? ChatMessage.formatMessage(message) : message;
msg = msg.format(parms);
let elm = new ChatMessage(msg);
this.history.push(elm);
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.htmlTag.animate({opacity: 0}, 200, function () {
@ -109,8 +152,8 @@ class ChatEntry {
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(elm.htmlTag);
elm.htmlTag.show().css("opacity", "0").animate({opacity: 1}, 100);
mbox.append(entry.htmlTag);
entry.htmlTag.show().css("opacity", "0").animate({opacity: 1}, 100);
if(bottom) box.scrollTop(mbox.height());
} else {
this.unread = true;

View file

@ -1,5 +1,6 @@
/// <reference path="ui/channel.ts" />
/// <reference path="client.ts" />
/// <reference path="ui/MusicClient.ts" />
class CommandResult {
success: boolean;
@ -466,7 +467,11 @@ class ConnectionCommandHandler {
client = tree.findClient(json["clid"]);
if(!client) {
client = new ClientEntry(parseInt(json["clid"]), json["client_nickname"]);
if(parseInt(json["client_type_exact"]) == ClientType.CLIENT_MUSIC) {
client = new MusicClientEntry(parseInt(json["clid"]), json["client_nickname"]);
} else {
client = new ClientEntry(parseInt(json["clid"]), json["client_nickname"]);
}
client = tree.insertClient(client, channel);
} else {
if(client == this.connection._client.getClient())
@ -500,6 +505,9 @@ class ConnectionCommandHandler {
}
client.updateVariables(...updates);
if(client instanceof LocalClientEntry)
this.connection._client.controlBar.updateVoice();
}
handleCommandClientLeftView(json) {
@ -575,22 +583,24 @@ class ConnectionCommandHandler {
if(!channel_from) //Not critical
console.error("Unknown client move (Channel from)!");
if(client instanceof LocalClientEntry) {
let self = client instanceof LocalClientEntry;
if(self) {
chat.channelChat().name = channel_to.channelName();
for(let entry of client.channelTree.clientsByChannel(client.currentChannel()))
if(entry !== client) entry.getAudioController().stopAudio(true);
this.connection._client.controlBar.updateVoice(channel_to);
}
tree.moveClient(client, channel_to);
if(json["reasonid"] == ViewReasonId.VREASON_MOVED) {
chat.serverChat().appendMessage("{0} was moved from channel {1} to {2} by {3}", true,
chat.serverChat().appendMessage(self ? "You was moved by {3} from channel {1} to {2}" : "{0} was moved from channel {1} to {2} by {3}", true,
client.createChatTag(true),
channel_from ? channel_from.createChatTag(true) : undefined,
channel_to.createChatTag(true),
ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"])
);
} else if(json["reasonid"] == ViewReasonId.VREASON_USER_ACTION) {
chat.serverChat().appendMessage("{0} switched from channel {1} to {2}", true,
chat.serverChat().appendMessage(self ? "You switched from channel {1} to {2}" : "{0} switched from channel {1} to {2}", true,
client.createChatTag(true),
channel_from ? channel_from.createChatTag(true) : undefined,
channel_to.createChatTag(true)

View file

@ -145,8 +145,10 @@ function loadDebug() {
"js/ui/modal/ModalCreateChannel.js",
"js/ui/modal/ModalConnect.js",
"js/ui/modal/ModalChangeVolume.js",
"js/ui/modal/ModalBanClient.js",
"js/ui/channel.js",
"js/ui/client.js",
"js/ui/MusicClient.js",
"js/ui/server.js",
"js/ui/view.js",
"js/ui/ControlBar.js",

View file

@ -4,6 +4,7 @@
/// <reference path="utils/modal.ts" />
/// <reference path="ui/modal/ModalConnect.ts" />
/// <reference path="ui/modal/ModalCreateChannel.ts" />
/// <reference path="ui/modal/ModalBanClient.ts" />
/// <reference path="codec/CodecWrapper.ts" />
/// <reference path="settings.ts" />
/// <reference path="log.ts" />
@ -16,6 +17,7 @@ let forumIdentity: TeaForumIdentity;
function main() {
//localhost:63343/Web-Client/index.php?disableUnloadDialog=1&default_connect_type=forum&default_connect_url=localhost
//disableUnloadDialog=1&default_connect_type=forum&default_connect_url=localhost&loader_ignore_age=1
AudioController.initializeAudioController();
if(!TSIdentityHelper.setup()) { console.error("Could not setup the TeamSpeak identity parser!"); return; }
@ -43,11 +45,19 @@ function main() {
//Modals.createChannelModal(undefined);
if(settings.static("default_connect_url")) {
if(settings.static("default_connect_type", "forum")) {
if(settings.static("default_connect_type", "forum") == "forum") {
globalClient.startConnection(settings.static("default_connect_url"), forumIdentity);
} else
Modals.spawnConnectModal(settings.static("default_connect_url"));
}
/*
$("#music-test").replaceWith($("#tmpl_music_frame_empty").tmpl({
thumbnail: "img/loading_image.svg"
}));
*/
Modals.spawnSettingsModal();
}
app.loadedListener.push(() => main());

View file

@ -17,6 +17,8 @@ class ControlBar {
private _away: boolean;
private _awayMessage: string;
private _codecNotSupported: boolean = true;
readonly handle: TSClient;
htmlTag: JQuery;
@ -142,6 +144,24 @@ class ControlBar {
});
}
updateVoice(targetChannel?: ChannelEntry) {
if(!targetChannel)
targetChannel = this.handle.getClient().currentChannel();
let voiceSupport = this.handle.voiceConnection.codecSupported(targetChannel.properties.channel_codec);
if(voiceSupport == this._codecNotSupported) return;
this._codecNotSupported = voiceSupport;
this.htmlTag.find(".btn_mute_input").prop("disabled", !this._codecNotSupported);
this.htmlTag.find(".btn_mute_output").prop("disabled", !this._codecNotSupported);
this.handle.serverConnection.sendCommand("clientupdate", {
client_input_hardware: this._codecNotSupported,
client_output_hardware: this._codecNotSupported
});
if(!this._codecNotSupported)
createErrorModal("Channel codec unsupported", "This channel has an unsupported codec.<br>You cant speak or listen to anybody!").open();
}
private onOpenSettings() {
Modals.spawnSettingsModal();
}

View file

@ -0,0 +1,53 @@
/// <reference path="client.ts" />
class MusicClientProperties extends ClientProperties {
music_volume: number = 0;
music_track_id: number = 0;
}
class MusicClientEntry extends ClientEntry {
constructor(clientId, clientName) {
super(clientId, clientName, new MusicClientProperties());
}
get properties() : MusicClientProperties {
return this._properties as MusicClientProperties;
}
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
spawnMenu(x, y,
{
name: "<b>Change bot name</b>",
icon: "client-change_nickname",
disabled: true,
callback: () => {},
type: MenuEntryType.ENTRY
}, {
name: "Change bot description",
icon: "client-edit",
disabled: true,
callback: () => {},
type: MenuEntryType.ENTRY
}, {
name: "Open music panel",
icon: "client-edit",
disabled: true,
callback: () => {},
type: MenuEntryType.ENTRY
},
MenuEntry.HR(),
{
name: "Delete bot",
icon: "client-delete",
disabled: true,
callback: () => {},
type: MenuEntryType.ENTRY
},
MenuEntry.CLOSE(on_close)
);
}
initializeListener(): void {
super.initializeListener();
}
}

View file

@ -447,7 +447,7 @@ class ChannelEntry {
createChatTag(braces: boolean = false) : JQuery {
let tag = $.spawn("div");
tag.css("display", "table");
tag.css("display", "inline-block");
tag.css("cursor", "pointer");
tag.css("font-weight", "bold");
tag.css("color", "darkblue");
@ -455,10 +455,15 @@ class ChannelEntry {
tag.text("\"" + this.channelName() + "\"");
else
tag.text(this.channelName());
tag.attr("oncontextmenu", "chat_channel_contextmenu(this, ...arguments);");
tag.contextmenu(event => {
if(event.isDefaultPrevented()) return;
event.preventDefault();
this.showContextMenu(event.pageX, event.pageY);
});
tag.attr("channelId", this.channelId);
tag.attr("channelName", this.channelName());
return tag.wrap("<p/>").parent();
return tag;
}
channelType() : ChannelType {

View file

@ -1,7 +1,18 @@
/// <reference path="channel.ts" />
/// <reference path="modal/ModalChangeVolume.ts" />
enum ClientType {
CLIENT_VOICE,
CLIENT_QUERY,
CLIENT_INTERNAL,
CLIENT_WEB,
CLIENT_MUSIC,
CLIENT_UNDEFINED
}
class ClientProperties {
client_type: ClientType = ClientType.CLIENT_VOICE; //TeamSpeaks type
client_type_exact: ClientType = ClientType.CLIENT_VOICE;
client_version: string = "";
client_platform: string = "";
client_nickname: string = "unknown";
@ -14,32 +25,37 @@ class ClientProperties {
client_flag_avatar: string = "";
client_output_muted: boolean = false;
client_away_message: string = "";
client_away: boolean = false;
client_input_hardware: boolean = false;
client_output_hardware: boolean = false;
client_input_muted: boolean = false;
client_output_muted: boolean = false;
client_is_channel_commander: boolean = false;
client_teaforum_id: number = 0;
client_teaforum_name: string = "";
}
class ClientEntry {
private _clientId: number;
private _channel: ChannelEntry;
private _tag: JQuery<HTMLElement>;
protected _clientId: number;
protected _channel: ChannelEntry;
protected _tag: JQuery<HTMLElement>;
properties: ClientProperties = new ClientProperties();
private lastVariableUpdate: number = 0;
private _speaking: boolean = false;
protected _properties: ClientProperties;
protected lastVariableUpdate: number = 0;
protected _speaking: boolean = false;
channelTree: ChannelTree;
audioController: AudioController;
constructor(clientId, clientName) {
constructor(clientId, clientName, properties: ClientProperties = new ClientProperties()) {
this._properties = properties;
this._properties.client_nickname = clientName;
this._clientId = clientId;
this.properties.client_nickname = clientName;
this.channelTree = null;
this._channel = null;
this.audioController = new AudioController();
@ -55,6 +71,10 @@ class ClientEntry {
this.audioController.initialize();
}
get properties() : ClientProperties {
return this._properties;
}
currentChannel() { return this._channel; }
clientNickName(){ return this.properties.client_nickname; }
clientUid(){ return this.properties.client_unique_identifier; }
@ -175,8 +195,16 @@ class ClientEntry {
type: MenuEntryType.ENTRY,
icon: "client-ban_client",
name: "Ban client",
disabled: true,
callback: () => {}
invalidPermission: !this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1),
callback: () => {
Modals.spawnBanClient(this.properties.client_nickname, (duration, reason) => {
this.channelTree.client.serverConnection.sendCommand("banclient", {
uid: this.properties.client_unique_identifier,
banreason: reason,
time: duration
});
});
}
},
MenuEntry.HR(),
{
@ -219,20 +247,30 @@ class ClientEntry {
static chatTag(id: number, name: string, uid: string, braces: boolean = false) : JQuery {
let tag = $.spawn("div");
tag.css("cursor", "pointer");
tag.css("font-weight", "bold");
tag.css("color", "darkblue");
tag.css("display", "table");
tag.css("cursor", "pointer")
.css("font-weight", "bold")
.css("color", "darkblue")
.css("display", "inline-block")
.css("margin", 0);
if(braces)
tag.text("\"" + name + "\"");
else
tag.text(name);
tag.attr("oncontextmenu", "chat_client_contextmenu(this, ...arguments);");
tag.contextmenu(event => {
if(event.isDefaultPrevented()) return;
event.preventDefault();
let client = globalClient.channelTree.findClient(id);
if(!client) return;
if(client.properties.client_unique_identifier != uid) return;
client.showContextMenu(event.pageX, event.pageY);
});
tag.attr("clientId", id);
tag.attr("clientUid", uid);
tag.attr("clientName", name);
return tag.wrap("<p/>").parent();
return tag;
}
createChatTag(braces: boolean = false) : JQuery {
@ -384,7 +422,7 @@ class ClientEntry {
}
calculateOnlineTime() : number {
return new Date().getTime() / 1000 - this.properties.client_lastconnected;
return Date.now() / 1000 - this.properties.client_lastconnected;
}
avatarId?() : string {
@ -506,23 +544,4 @@ class LocalClientEntry extends ClientEntry {
});
});
}
}
//Global functions
function chat_client_contextmenu(_element: any, event: any) {
event.preventDefault();
let element = $(_element);
console.log("Context menue for " + element.attr("clientName"));
let clid : number = Number.parseInt(element.attr("clientId"));
let client = globalClient.channelTree.findClient(clid);
if(!client) {
//TODO
return;
}
if(client.clientUid() != element.attr("clientUid")) {
//TODO
return;
}
client.showContextMenu(event.pageX, event.pageY);
}

View file

@ -0,0 +1,91 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
export function spawnBanClient(name: string, callback: (length: number, reason: string) => void) {
const connectModal = createModal({
header: function() {
return "Ban client";
},
body: function () {
let tag = $("#tmpl_client_ban").tmpl({
client_name: name
});
let maxTime = 0; //globalClient.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value;
let unlimited = maxTime == 0 || maxTime == -1;
if(unlimited) maxTime = 0;
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);
durationTag.change(() => banTag.trigger('change'));
banTag.change(event => {
let element = $((event.target as HTMLSelectElement).selectedOptions.item(0));
if(element.val() !== "perm") {
durationTag.prop("disabled", false);
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);
}
});
return tag;
},
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");
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
});
connectModal.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 * parseInt(duration.attr("duration-scale")), connectModal.htmlTag.find(".ban_reason").val() as string);
})
}
}

View file

@ -33,12 +33,11 @@ class ServerEntry {
tag.attr("id", "server");
tag.addClass("server");
tag.append($.spawn("div").addClass("server_type icon client-server_green"));
tag.append("<a class='name'>" + this.properties.virtualserver_name + "</a>");
tag.append($.spawn("a").addClass("name").text(this.properties.virtualserver_name));
const serverIcon = $("<span/>");
//we cant spawn an icon on creation :)
serverIcon.append("<div class='icon_property icon_empty'></div>");
serverIcon.append($.spawn("div").addClass("icon_property icon_empty"));
tag.append(serverIcon);
return this._htmlTag = tag;
@ -89,7 +88,7 @@ class ServerEntry {
}
shouldUpdateProperties() : boolean {
return this.nextInfoRequest < new Date().getTime();
return this.nextInfoRequest < Date.now();
}
calculateUptime() : number {

View file

@ -166,9 +166,9 @@
<x-entry>
<x-tag>Voice</x-tag>
<x-content>
<div style="display: flex; flex-direction: column;" class="settings_voice">
<div>
<a>Microphone:</a>
<div class="settings_voice align_column">
<div style="justify-content: right">
<a style="margin-left: 20px">Microphone:</a>
<select class="voice_microphone_select"></select>
<hr>
</div>
@ -217,5 +217,90 @@
<div class="display_volume" style="width: 60px; align-self: center; text-align: center">&plusmn;0 %</div>
</div>
</template>
<template id="tmpl_client_ban">
<div class="align_column">
<div class="align_column" style="margin: 5px">
<a>Name:</a>
<input value="${client_name}" readonly>
</div>
<div class="align_column" style="margin: 5px">
<a>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>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">
<option value="sec">seconds</option>
<option value="min">minutes</option>
<option value="hours">hours</option>
<option value="days">days</option>
<option value="perm">permanent</option>
</select>
</div>
</div>
</div>
</template>
<template id="tmpl_music_frame">
<div class="music-wrapper">
<div class="container">
<div class="left">
<div class="static-card">
<img src="${thumbnail}" alt="Thumbnail" class="thumbnail">
</div>
</div>
<div class="right">
<div class="controls hover">
<label class="btn-forward"><span></span></label>
<label class="btn-rewind"><span></span></label>
<label class="btn-settings"><span></span></label>
</div>
<div class="flip-card">
<img src="${thumbnail}" alt="Thumbnail" class="thumbnail">
</div>
</div>
</div>
<div class="controls-overlay">
<div class="timer">
<div>
<svg class="button" x="0px" y="0px" viewBox="0 0 4.5 6.9" style="enable-background:new 0 0 4.5 6.9;">
<defs>
<filter id="shadow">
<feDropShadow dx="4" dy="8" stdDeviation="4"/>
</filter>
</defs>
<polyline style="filter:url(#shadow);" class="button" points="0.6,0.3 3.9,3.4 0.6,6.6 "></polyline>
</svg>
<svg class="button" x="0px" y="0px" viewBox="0 0 4.5 6.9" style="enable-background:new 0 0 4.5 6.9;">
<defs>
<filter id="shadow">
<feDropShadow dx="4" dy="8" stdDeviation="4"/>
</filter>
</defs>
<g style="filter:url(#shadow);">
<line x1="0.4" y1="0.1" x2="0.4" y2="6.8"></line>
<line x1="4.1" y1="0.1" x2="4.1" y2="6.8"></line>
</g>
</svg>
</div>
<div class="timeline">
<div class="buffered"></div>
<div class="played"></div>
<div class="slider"></div>
</div>
</div>
</div>
</div>
</template>
<template id="tmpl_music_frame_empty">
<div class="music-wrapper empty">
<img src="img/music/empty_disk.svg">
<a>Not playing any music</a>
</div>
</template>
</body>
</html>