From 48822c604a994495b79012ecd71aa35160f2dcb5 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Sun, 4 Nov 2018 13:54:18 +0100 Subject: [PATCH] Implementing a lot of new features again --- ChangeLog.md | 8 + shared/audio/speech_sentences.csv | 10 +- shared/css/music/info_plate.scss | 52 ++++-- shared/html/templates.html | 66 +++++--- shared/js/connection.ts | 41 ++++- shared/js/sound/Sounds.ts | 9 +- shared/js/ui/channel.ts | 34 +++- shared/js/ui/client.ts | 98 +++++++++-- shared/js/ui/client_move.ts | 4 +- shared/js/ui/frames/SelectedItemInfo.ts | 88 +++++++--- shared/js/ui/modal/ModalBanClient.ts | 4 +- shared/js/ui/server.ts | 17 +- shared/js/ui/view.ts | 216 ++++++++++++++++++++++-- shared/js/voice/AudioController.ts | 22 ++- 14 files changed, 559 insertions(+), 110 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index f3f14f69..0910107b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,12 @@ # Changelog: +* **04.11.18** + - Added basic music bot management (Create | Delete | Nickname/Description-change) + - Merged music bot pause and play. Added stop button + - Fixed voice "lamp" being on on channel switch + - Improved hostbanner reload (Not flicker anymore) + - Added sounds on servergroup assignment and on revoke as well for channel group changed + - Added client multiselect and multi actions + * **03.11.18** - Reworked on the basic overlay sizes - Added hostbanner to the UI diff --git a/shared/audio/speech_sentences.csv b/shared/audio/speech_sentences.csv index e758c102..b831093c 100644 --- a/shared/audio/speech_sentences.csv +++ b/shared/audio/speech_sentences.csv @@ -46,4 +46,12 @@ user.left.disconnect;User disconnected from your channel user.left.banned;User in your channel was banned from the server #Error -error.insufficient_permissions;insufficient permissions \ No newline at end of file +error.insufficient_permissions;insufficient permissions + +#Groups +group.server.assigned;Server group assigned +group.server.revoked;Server group revoked +group.channel.changed;Channel group changed +group.server.assigned.self;Server group assigned +group.server.revoked.self;Server group revoked +group.channel.changed.self;Channel group changed \ No newline at end of file diff --git a/shared/css/music/info_plate.scss b/shared/css/music/info_plate.scss index 02311109..8aa15737 100644 --- a/shared/css/music/info_plate.scss +++ b/shared/css/music/info_plate.scss @@ -170,37 +170,55 @@ $ease: cubic-bezier(.45, 0, .55, 1); justify-content: space-between; vertical-align: center; + .button-container{ + display: inline-block; + + > div { + display: inline-block; + } + } + .button { width: 10px; height: 12px; margin-left: 2px; - fill: none; - stroke: #4c4c4c;; - stroke-width: 0.5; - stroke-miterlimit: 10; - cursor: pointer; - color: white; - mix-blend-mode: difference; - //box-shadow: 20px 20px 20px 20px rgb(186, 0, 12); + svg { + + fill: none; + stroke: #4c4c4c;; + stroke-width: 0.5; + stroke-miterlimit: 10; + cursor: pointer; + + color: white; + mix-blend-mode: difference; + //box-shadow: 20px 20px 20px 20px rgb(186, 0, 12); + } } .button.active { - animation: bounce 500ms alternate; - transform: scale(1.3); - transition: transform 150ms; + svg { + animation: bounce 500ms alternate; + transform: scale(1.3); + transition: transform 150ms; + } } .button:hover { - animation: bounce 500ms alternate; - transform: scale(1.1); - transition: transform 150ms; + svg { + animation: bounce 500ms alternate; + transform: scale(1.1); + transition: transform 150ms; + } } .button.active:hover { - animation: bounce 500ms alternate; - transform: scale(1.5); - transition: transform 150ms; + svg { + animation: bounce 500ms alternate; + transform: scale(1.5); + transition: transform 150ms; + } } .timeline * { diff --git a/shared/html/templates.html b/shared/html/templates.html index fc36441a..39c67bfd 100644 --- a/shared/html/templates.html +++ b/shared/html/templates.html @@ -1103,26 +1103,46 @@
-
- - - - - - - - - - - - - - - - - - - +
+
+
+ + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+
+ + + + + + + + + + +
+
@@ -1286,6 +1306,12 @@ Local Volume: {{>sound_volume}}% + {{if song_url}} + + Currently replaying: + + + {{/if}} diff --git a/shared/js/connection.ts b/shared/js/connection.ts index 1bbfc543..f5cdf3c8 100644 --- a/shared/js/connection.ts +++ b/shared/js/connection.ts @@ -479,6 +479,10 @@ class ConnectionCommandHandler { this["notifyclientpoke"] = this.handleNotifyClientPoke; this["notifymusicplayerinfo"] = this.handleNotifyMusicPlayerInfo; + + this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd; + this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove; + this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged; } handleCommandResult(json) { @@ -802,14 +806,16 @@ class ConnectionCommandHandler { console.error("Unknown client move (Channel from)!"); let self = client instanceof LocalClientEntry; + let current_clients; if(self) { chat.channelChat().name = channel_to.channelName(); - for(let entry of client.channelTree.clientsByChannel(client.currentChannel())) - if(entry !== client) entry.getAudioController().stopAudio(true); + current_clients = client.channelTree.clientsByChannel(client.currentChannel()) this.connection._client.controlBar.updateVoice(channel_to); } tree.moveClient(client, channel_to); + for(const entry of current_clients || []) + if(entry !== client) entry.getAudioController().stopAudio(true); const own_channel = this.connection._client.getClient().currentChannel(); if(json["reasonid"] == ViewReasonId.VREASON_MOVED) { @@ -842,7 +848,7 @@ class ConnectionCommandHandler { channel_from ? channel_from.createChatTag(true) : undefined, channel_to.createChatTag(true), ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"]), - (json["reasonmsg"] || "").length > 0 ? " (" + json["msg"] + ")" : "" + json["reasonmsg"] ? " (" + json["reasonmsg"] + ")" : "" ); if(self) { sound.play(Sound.CHANNEL_KICKED); @@ -1026,4 +1032,33 @@ class ConnectionCommandHandler { sound.play(Sound.USER_POKED_SELF); } + + //TODO server chat message + handleNotifyServerGroupClientAdd(json) { + json = json[0]; + + const self = this.connection._client.getClient(); + if(json["clid"] == self.clientId()) + sound.play(Sound.GROUP_SERVER_ASSIGNED_SELF); + } + + //TODO server chat message + handleNotifyServerGroupClientRemove(json) { + json = json[0]; + + const self = this.connection._client.getClient(); + if(json["clid"] == self.clientId()) { + sound.play(Sound.GROUP_SERVER_REVOKED_SELF); + } else { + } + } + + //TODO server chat message + handleNotifyClientChannelGroupChanged(json) { + json = json[0]; + + const self = this.connection._client.getClient(); + if(json["clid"] == self.clientId()) + sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF); + } } \ No newline at end of file diff --git a/shared/js/sound/Sounds.ts b/shared/js/sound/Sounds.ts index 4cf272b3..de41108c 100644 --- a/shared/js/sound/Sounds.ts +++ b/shared/js/sound/Sounds.ts @@ -44,7 +44,14 @@ enum Sound { ERROR_INSUFFICIENT_PERMISSIONS = "error.insufficient_permissions", MESSAGE_SEND = "message.send", - MESSAGE_RECEIVED = "message.received" + MESSAGE_RECEIVED = "message.received", + + GROUP_SERVER_ASSIGNED = "group.server.assigned", + GROUP_SERVER_REVOKED = "group.server.revoked", + GROUP_CHANNEL_CHANGED = "group.channel.changed", + GROUP_SERVER_ASSIGNED_SELF = "group.server.assigned.self", + GROUP_SERVER_REVOKED_SELF = "group.server.revoked.self", + GROUP_CHANNEL_CHANGED_SELF = "group.channel.changed.self" } namespace sound { diff --git a/shared/js/ui/channel.ts b/shared/js/ui/channel.ts index 0eaa36fb..f0160372 100644 --- a/shared/js/ui/channel.ts +++ b/shared/js/ui/channel.ts @@ -293,14 +293,24 @@ class ChannelEntry { this.channelTag().click(function () { _this.channelTree.onSelect(_this); }); - this.channelTag().dblclick(() => this.joinChannel()); + this.channelTag().dblclick(() => { + if($.isArray(this.channelTree.currently_selected)) { //Multiselect + return; + } + this.joinChannel() + }); if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { - this.channelTag().on("contextmenu", function (event) { + this.channelTag().on("contextmenu", (event) => { event.preventDefault(); - _this.channelTree.onSelect(_this); + if($.isArray(this.channelTree.currently_selected)) { //Multiselect + (this.channelTree.currently_selected_context_callback || ((_) => null))(event); + return; + } + + _this.channelTree.onSelect(_this, true); _this.showContextMenu(event.pageX, event.pageY, () => { - _this.channelTree.onSelect(undefined); + _this.channelTree.onSelect(undefined, true); }); }); } @@ -395,6 +405,22 @@ class ChannelEntry { } }, MenuEntry.HR(), + { + type: MenuEntryType.ENTRY, + icon: "client-addon-collection", + name: "Create music bot", + callback: () => { + this.channelTree.client.serverConnection.sendCommand("musicbotcreate", {cid: this.channelId}).then(() => { + createInfoModal("Bot successfully created", "But has been successfully created.").open(); + }).catch(error => { + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal("Failed to create bot", "Failed to create the music bot:
" + error).open(); + }); + } + }, + MenuEntry.HR(), { type: MenuEntryType.ENTRY, icon: "client-channel_create_sub", diff --git a/shared/js/ui/client.ts b/shared/js/ui/client.ts index 4a9b5bea..b3a06c0e 100644 --- a/shared/js/ui/client.ts +++ b/shared/js/ui/client.ts @@ -54,6 +54,7 @@ class ClientEntry { protected _properties: ClientProperties; protected lastVariableUpdate: number = 0; protected _speaking: boolean = false; + private _listener_initialized: boolean; channelTree: ChannelTree; audioController: AudioController; @@ -91,28 +92,40 @@ class ClientEntry { } initializeListener(){ - const _this = this; + if(this._listener_initialized) return; + this._listener_initialized = true; + this.tag.click(event => { - _this.channelTree.onSelect(_this); + this.channelTree.onSelect(this); }); if(this.clientId() != this.channelTree.client.clientId && !(this instanceof MusicClientEntry)) this.tag.dblclick(event => { + if($.isArray(this.channelTree.currently_selected)) { //Multiselect + return; + } this.chat(true).focus(); }); if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { - this.tag.on("contextmenu", function (event) { + this.tag.on("contextmenu", (event) => { event.preventDefault(); - _this.channelTree.onSelect(_this); - _this.showContextMenu(event.pageX, event.pageY, () => { - _this.channelTree.onSelect(undefined); + if($.isArray(this.channelTree.currently_selected)) { //Multiselect + (this.channelTree.currently_selected_context_callback || ((_) => null))(event); + return; + } + + this.channelTree.onSelect(this, true); + this.showContextMenu(event.pageX, event.pageY, () => { + this.channelTree.onSelect(undefined, true); }); return false; }); } this.tag.mousedown(event => { + if(event.which != 1) return; //Only the left button + this.channelTree.client_mover.activate(this, target => { if(!target) return; if(target == this._channel) return; @@ -296,7 +309,7 @@ class ClientEntry { type: MenuEntryType.ENTRY, icon: "client-kick_channel", name: "Kick client from channel", - callback: function () { + callback: () => { createInputModal("Kick client from channel", "Kick reason:
", text => true, result => { if(result) { console.log("Kicking client " + _this.clientNickName() + " from channel with reason " + result); @@ -313,7 +326,7 @@ class ClientEntry { type: MenuEntryType.ENTRY, icon: "client-kick_server", name: "Kick client fom server", - callback: function () { + callback: () => { createInputModal("Kick client from server", "Kick reason:
", text => true, result => { if(result) { console.log("Kicking client " + _this.clientNickName() + " from server with reason " + result); @@ -700,9 +713,11 @@ class LocalClientEntry extends ClientEntry { super.initializeListener(); this.tag.find(".name").addClass("own_name"); - const _self = this; - this.tag.dblclick(function () { - _self.openRename(); + this.tag.dblclick(() => { + if($.isArray(this.channelTree.currently_selected)) { //Multiselect + return; + } + this.openRename(); }); } @@ -789,14 +804,34 @@ class MusicClientEntry extends ClientEntry { { name: "Change bot name", icon: "client-change_nickname", - disabled: true, - callback: () => {}, + disabled: false, + callback: () => { + createInputModal("Change music bots nickname", "New nickname:
", text => text.length >= 3 && text.length <= 31, result => { + if(result) { + this.channelTree.client.serverConnection.sendCommand("clientedit", { + clid: this.clientId(), + client_nickname: result + }); + + } + }, { width: 400, maxLength: 255 }).open(); + }, type: MenuEntryType.ENTRY }, { name: "Change bot description", icon: "client-edit", - disabled: true, - callback: () => {}, + disabled: false, + callback: () => { + createInputModal("Change music bots description", "New description:
", text => true, result => { + if(typeof(result) === 'string') { + this.channelTree.client.serverConnection.sendCommand("clientedit", { + clid: this.clientId(), + client_description: result + }); + + } + }, { width: 400, maxLength: 255 }).open(); + }, type: MenuEntryType.ENTRY }, { name: "Open music panel", @@ -804,6 +839,27 @@ class MusicClientEntry extends ClientEntry { disabled: true, callback: () => {}, type: MenuEntryType.ENTRY + }, { + name: "Quick url replay", + icon: "client-edit", + disabled: false, + callback: () => { + createInputModal("Please enter the URL", "URL:", text => true, result => { + if(result) { + this.channelTree.client.serverConnection.sendCommand("musicbotqueueadd", { + botid: this.properties.client_database_id, + type: "yt", //Its a hint not a force! + url: result + }).catch(error => { + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal("Failed to replay url", "Failed to enqueue url:
" + error).open(); + }); + } + }, { width: 400, maxLength: 255 }).open(); + }, + type: MenuEntryType.ENTRY }, MenuEntry.HR(), ...super.assignment_context(), @@ -830,7 +886,6 @@ class MusicClientEntry extends ClientEntry { reasonid: ViewReasonId.VREASON_CHANNEL_KICK, reasonmsg: result }); - } }, { width: 400, maxLength: 255 }).open(); } @@ -853,9 +908,16 @@ class MusicClientEntry extends ClientEntry { { name: "Delete bot", icon: "client-delete", - disabled: true, + disabled: false, callback: () => { - //TODO + const tag = $.spawn("div").append(MessageHelper.formatMessage("Do you really want to delete {0}", this.createChatTag(false))); + Modals.spawnYesNo("Are you sure?", $.spawn("div").append(tag), result => { + if(result) { + this.channelTree.client.serverConnection.sendCommand("musicbotdelete", { + botid: this.properties.client_database_id + }); + } + }); }, type: MenuEntryType.ENTRY }, diff --git a/shared/js/ui/client_move.ts b/shared/js/ui/client_move.ts index 45019574..ba0b617e 100644 --- a/shared/js/ui/client_move.ts +++ b/shared/js/ui/client_move.ts @@ -53,8 +53,10 @@ class ClientMover { const d_y = this.origin_point.y - event.pageY; this._active = Math.sqrt(d_x * d_x + d_y * d_y) > 5 * 5; - if(this._active) + if(this._active) { ClientMover.move_element.show(); + this.channel_tree.onSelect(this.selected_client, true); + } } const elements = document.elementsFromPoint(event.pageX, event.pageY); diff --git a/shared/js/ui/frames/SelectedItemInfo.ts b/shared/js/ui/frames/SelectedItemInfo.ts index 89d60eea..cafd3236 100644 --- a/shared/js/ui/frames/SelectedItemInfo.ts +++ b/shared/js/ui/frames/SelectedItemInfo.ts @@ -128,17 +128,25 @@ class Hostbanner { this.updater = undefined; } - this.html_tag.empty(); const tag = this.generate_tag(); if(tag) { - this.html_tag.append(tag); - this.html_tag.prop("disabled", false); - } else + tag.then(element => { + this.html_tag.empty(); + this.html_tag.append(element); + this.html_tag.prop("disabled", false); + }).catch(error => { + console.warn("Failed to load hostbanner: %o", error); + this.html_tag.empty(); + this.html_tag.prop("disabled", true); + }) + } else { + this.html_tag.empty(); this.html_tag.prop("disabled", true); + } } - private generate_tag?() : JQuery { + private generate_tag?() : Promise> { if(!this.client.connected) return undefined; const server = this.client.channelTree.server; if(!server) return undefined; @@ -149,10 +157,19 @@ class Hostbanner { properties["property_" + key] = server.properties[key]; const rendered = $("#tmpl_selected_hostbanner").renderTag(properties); - - if(server.properties.virtualserver_hostbanner_gfx_interval > 0) - this.updater = setTimeout(() => this.update(), Math.min(server.properties.virtualserver_hostbanner_gfx_interval, 60) * 1000); - return rendered; + const image = rendered.find("img"); + return new Promise>((resolve, reject) => { + const node_image = image[0] as HTMLImageElement; + node_image.onload = () => { + console.debug("Hostbanner has been loaded"); + if(server.properties.virtualserver_hostbanner_gfx_interval > 0) + this.updater = setTimeout(() => this.update(), Math.min(server.properties.virtualserver_hostbanner_gfx_interval, 60) * 1000); + resolve(rendered); + }; + node_image.onerror = event => { + reject(event); + } + }); } } @@ -354,6 +371,15 @@ function format_time(time: number) { return (hours ? hours + ":" : "") + minutes + ':' + seconds; } +enum MusicPlayerState { + SLEEPING, + LOADING, + + PLAYING, + PAUSED, + STOPPED +} + class MusicInfoManager extends ClientInfoManager { createFrame<_>(handle: InfoBar<_>, channel: MusicClientEntry, html_tag: JQuery) { super.createFrame(handle, channel, html_tag); @@ -366,12 +392,12 @@ class MusicInfoManager extends ClientInfoManager { let properties = super.buildProperties(bot); { //Render info frame - if(bot.properties.player_state != 2 && bot.properties.player_state != 3) { + if(bot.properties.player_state < MusicPlayerState.PLAYING) { properties["music_player"] = $("#tmpl_music_frame_empty").renderTag().css("align-self", "center"); } else { let frame = $.spawn("div").text("loading...") as JQuery; properties["music_player"] = frame; - + properties["song_url"] = $.spawn("a").text("loading..."); bot.requestPlayerInfo().then(info => { let timestamp = Date.now(); @@ -380,8 +406,11 @@ class MusicInfoManager extends ClientInfoManager { let _frame = $("#tmpl_music_frame").renderTag({ song_name: info.player_title ? info.player_title : info.song_url ? info.song_url : "No title or url", + song_url: info.song_url, thumbnail: info.song_thumbnail && info.song_thumbnail.length > 0 ? info.song_thumbnail : undefined }).css("align-self", "center"); + properties["song_url"].text(info.song_url); + frame.replaceWith(_frame); frame = _frame; @@ -389,8 +418,8 @@ class MusicInfoManager extends ClientInfoManager { { let button_play = frame.find(".button_play"); let button_pause = frame.find(".button_pause"); - - frame.find(".button_play").click(handler => { + let button_stop = frame.find('.button_stop'); + button_play.click(handler => { if(!button_play.hasClass("active")) { this.handle.handle.serverConnection.sendCommand("musicbotplayeraction", { botid: bot.properties.client_database_id, @@ -400,10 +429,10 @@ class MusicInfoManager extends ClientInfoManager { this.triggerUpdate(); }); } - button_pause.removeClass("active"); - button_play.addClass("active"); + button_pause.show(); + button_play.hide(); }); - frame.find(".button_pause").click(handler => { + button_pause.click(handler => { if(!button_pause.hasClass("active")) { this.handle.handle.serverConnection.sendCommand("musicbotplayeraction", { botid: bot.properties.client_database_id, @@ -413,14 +442,29 @@ class MusicInfoManager extends ClientInfoManager { this.triggerUpdate(); }); } - button_play.removeClass("active"); - button_pause.addClass("active"); + button_play.show(); + button_pause.hide(); + }); + button_stop.click(handler => { + this.handle.handle.serverConnection.sendCommand("musicbotplayeraction", { + botid: bot.properties.client_database_id, + action: 0 + }).then(updated => this.triggerUpdate()).catch(error => { + createErrorModal("Failed to execute stop", MessageHelper.formatMessage("Failed to execute stop.
{}", error)).open(); + this.triggerUpdate(); + }); }); - if(bot.properties.player_state == 2) - button_play.addClass("active"); - else if(bot.properties.player_state == 3) - button_pause.addClass("active"); + if(bot.properties.player_state == 2) { + button_play.hide(); + button_pause.show(); + } else if(bot.properties.player_state == 3) { + button_pause.hide(); + button_play.show(); + } else if(bot.properties.player_state == 4) { + button_pause.hide(); + button_play.show(); + } } { /* Button functions */ diff --git a/shared/js/ui/modal/ModalBanClient.ts b/shared/js/ui/modal/ModalBanClient.ts index 269b618c..ff9511ea 100644 --- a/shared/js/ui/modal/ModalBanClient.ts +++ b/shared/js/ui/modal/ModalBanClient.ts @@ -3,7 +3,7 @@ /// namespace Modals { - export function spawnBanClient(name: string, callback: (data: { + export function spawnBanClient(name: string | string[], callback: (data: { length: number, reason: string, no_name: boolean, @@ -16,7 +16,7 @@ namespace Modals { }, body: function () { let tag = $("#tmpl_client_ban").renderTag({ - client_name: name + client_name: $.isArray(name) ? '"' + name.join('", "') + '"' : name }); let maxTime = 0; //globalClient.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).value; diff --git a/shared/js/ui/server.ts b/shared/js/ui/server.ts index 39865839..87df8ac1 100644 --- a/shared/js/ui/server.ts +++ b/shared/js/ui/server.ts @@ -105,17 +105,20 @@ class ServerEntry { } initializeListener(){ - const _this = this; - - this._htmlTag.click(function () { - _this.channelTree.onSelect(_this); + this._htmlTag.click(() => { + this.channelTree.onSelect(this); }); if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { - this.htmlTag.on("contextmenu", function (event) { + this.htmlTag.on("contextmenu", (event) => { event.preventDefault(); - _this.channelTree.onSelect(_this); - _this.spawnContextMenu(event.pageX, event.pageY, () => { _this.channelTree.onSelect(undefined); }); + if($.isArray(this.channelTree.currently_selected)) { //Multiselect + (this.channelTree.currently_selected_context_callback || ((_) => null))(event); + return; + } + + this.channelTree.onSelect(this, true); + this.spawnContextMenu(event.pageX, event.pageY, () => { this.channelTree.onSelect(undefined, true); }); }); } } diff --git a/shared/js/ui/view.ts b/shared/js/ui/view.ts index 0058888e..9c4911ff 100644 --- a/shared/js/ui/view.ts +++ b/shared/js/ui/view.ts @@ -6,6 +6,11 @@ /// /// +let shift_pressed = false; +$(document).on('keyup keydown', function(e){ + shift_pressed = e.shiftKey; + console.log(shift_pressed); +}); class ChannelTree { client: TSClient; htmlTree: JQuery; @@ -13,6 +18,8 @@ class ChannelTree { channels: ChannelEntry[]; clients: ClientEntry[]; + currently_selected: ClientEntry | ServerEntry | ChannelEntry | (ClientEntry | ServerEntry)[] = undefined; + currently_selected_context_callback: (event) => any = undefined; readonly client_mover: ClientMover; constructor(client, htmlTree) { @@ -24,13 +31,16 @@ class ChannelTree { this.reset(); if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { - let _this = this; - this.htmlTree.parent().on("contextmenu", function (event) { + this.htmlTree.parent().on("contextmenu", (event) => { if(event.isDefaultPrevented()) return; event.preventDefault(); - _this.onSelect(undefined); - _this.showContextMenu(event.pageX, event.pageY); + if($.isArray(this.currently_selected)) { //Multiselect + (this.currently_selected_context_callback || ((_) => null))(event); + } else { + this.onSelect(undefined); + this.showContextMenu(event.pageX, event.pageY); + } }); } @@ -233,21 +243,72 @@ class ChannelTree { } findClient?(clientId: number) : ClientEntry { - for(let index = 0; index < this.clients.length; index++) - if(this.clients[index].clientId() == clientId) return this.clients[index]; + for(let index = 0; index < this.clients.length; index++) { + if(this.clients[index].clientId() == clientId) + return this.clients[index]; + } return undefined; } find_client_by_dbid?(client_dbid: number) : ClientEntry { - for(let index = 0; index < this.clients.length; index++) - if(this.clients[index].properties.client_database_id == client_dbid) return this.clients[index]; + for(let index = 0; index < this.clients.length; index++) { + if(this.clients[index].properties.client_database_id == client_dbid) + return this.clients[index]; + } return undefined; } - onSelect(entry?: ChannelEntry | ClientEntry | ServerEntry) { - this.htmlTree.find(".selected").each(function (idx, e) { - $(e).removeClass("selected"); - }); + private static same_selected_type(a, b) { + if(a instanceof ChannelEntry) + return b instanceof ChannelEntry; + if(a instanceof ClientEntry) + return b instanceof ClientEntry; + if(a instanceof ServerEntry) + return b instanceof ServerEntry; + return a == b; + } + + onSelect(entry?: ChannelEntry | ClientEntry | ServerEntry, enforce_single?: boolean) { + console.log(shift_pressed); + if(this.currently_selected && shift_pressed && entry instanceof ClientEntry) { //Currently we're only supporting client multiselects :D + if(!entry) return; //Nowhere + + if($.isArray(this.currently_selected)) { + if(!ChannelTree.same_selected_type(this.currently_selected[0], entry)) return; //Not the same type + } else if(ChannelTree.same_selected_type(this.currently_selected, entry)) { + this.currently_selected = [this.currently_selected] as any; + } + if(entry instanceof ChannelEntry) + this.currently_selected_context_callback = this.callback_multiselect_channel.bind(this); + if(entry instanceof ClientEntry) + this.currently_selected_context_callback = this.callback_multiselect_client.bind(this); + } else + this.currently_selected = undefined; + + if(!$.isArray(this.currently_selected) || enforce_single) { + this.currently_selected = entry; + this.htmlTree.find(".selected").each(function (idx, e) { + $(e).removeClass("selected"); + }); + } else { + for(const e of this.currently_selected) + if(e == entry) { + this.currently_selected.remove(e); + if(entry instanceof ChannelEntry) + (entry as ChannelEntry).rootTag().find("> .channelLine").removeClass("selected"); + else if(entry instanceof ClientEntry) + (entry as ClientEntry).tag.removeClass("selected"); + else if(entry instanceof ServerEntry) + (entry as ServerEntry).htmlTag.removeClass("selected"); + if(this.currently_selected.length == 1) + this.currently_selected = this.currently_selected[0]; + else if(this.currently_selected.length == 0) + this.currently_selected = undefined; + //Already selected + return; + } + this.currently_selected.push(entry as any); + } if(entry instanceof ChannelEntry) (entry as ChannelEntry).rootTag().find("> .channelLine").addClass("selected"); @@ -255,7 +316,136 @@ class ChannelTree { (entry as ClientEntry).tag.addClass("selected"); else if(entry instanceof ServerEntry) (entry as ServerEntry).htmlTag.addClass("selected"); - this.client.selectInfo.setCurrentSelected(entry); + + this.client.selectInfo.setCurrentSelected($.isArray(this.currently_selected) ? undefined : entry); + } + + private callback_multiselect_channel(event) { + console.log("Multiselect channel"); + } + private callback_multiselect_client(event) { + console.log("Multiselect client"); + const clients = this.currently_selected as ClientEntry[]; + const music_only = clients.map(e => e instanceof MusicClientEntry ? 0 : 1).reduce((a, b) => a + b, 0) == 0; + const music_entry = clients.map(e => e instanceof MusicClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0; + const local_client = clients.map(e => e instanceof LocalClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0; + console.log("Music only: %o | Container music: %o | Container local: %o", music_entry, music_entry, local_client); + let entries: ContextMenuEntry[] = []; + if (!music_entry && !local_client) { //Music bots or local client cant be poked + entries.push({ + type: MenuEntryType.ENTRY, + icon: "client-poke", + name: "Poke clients", + callback: () => { + createInputModal("Poke clients", "Poke message:
", text => true, result => { + if (typeof(result) === "string") { + for (const client of this.currently_selected as ClientEntry[]) + this.client.serverConnection.sendCommand("clientpoke", { + clid: client.clientId(), + msg: result + }); + + } + }, {width: 400, maxLength: 512}).open(); + } + }); + } + entries.push({ + type: MenuEntryType.ENTRY, + icon: "client-move_client_to_own_channel", + name: "Move clients to your channel", + callback: () => { + const target = this.client.getClient().currentChannel().getChannelId(); + for(const client of clients) + this.client.serverConnection.sendCommand("clientmove", { + clid: client.clientId(), + cid: target + }); + } + }); + if (!local_client) {//local client cant be kicked and/or banned or kicked + entries.push(MenuEntry.HR()); + entries.push({ + type: MenuEntryType.ENTRY, + icon: "client-kick_channel", + name: "Kick clients from channel", + callback: () => { + createInputModal("Kick clients from channel", "Kick reason:
", text => true, result => { + if (result) { + for (const client of clients) + this.client.serverConnection.sendCommand("clientkick", { + clid: client.clientId(), + reasonid: ViewReasonId.VREASON_CHANNEL_KICK, + reasonmsg: result + }); + + } + }, {width: 400, maxLength: 255}).open(); + } + }); + + if (!music_entry) { //Music bots cant be banned or kicked + entries.push({ + type: MenuEntryType.ENTRY, + icon: "client-kick_server", + name: "Kick clients fom server", + callback: () => { + createInputModal("Kick clients from server", "Kick reason:
", text => true, result => { + if (result) { + for (const client of clients) + this.client.serverConnection.sendCommand("clientkick", { + clid: client.clientId(), + reasonid: ViewReasonId.VREASON_SERVER_KICK, + reasonmsg: result + }); + + } + }, {width: 400, maxLength: 255}).open(); + } + }, { + type: MenuEntryType.ENTRY, + icon: "client-ban_client", + name: "Ban clients", + invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), + callback: () => { + Modals.spawnBanClient((clients).map(entry => entry.clientNickName()), (data) => { + for (const client of clients) + this.client.serverConnection.sendCommand("banclient", { + uid: client.properties.client_unique_identifier, + banreason: data.reason, + time: data.length + }, [data.no_ip ? "no-ip" : "", data.no_hwid ? "no-hardware-id" : "", data.no_name ? "no-nickname" : ""]).then(() => { + sound.play(Sound.USER_BANNED); + }); + }); + } + }); + } + if(music_only) { + entries.push(MenuEntry.HR()); + entries.push({ + name: "Delete bots", + icon: "client-delete", + disabled: false, + callback: () => { + const param_string = clients.map((_, index) => "{" + index + "}").join(', '); + const param_values = clients.map(client => client.createChatTag(true)); + const tag = $.spawn("div").append(...MessageHelper.formatMessage("Do you really want to delete " + param_string, ...param_values)); + const tag_container = $.spawn("div").append(tag); + Modals.spawnYesNo("Are you sure?", tag_container, result => { + if(result) { + for(const client of clients) + this.client.serverConnection.sendCommand("musicbotdelete", { + botid: client.properties.client_database_id + }); + } + }); + }, + type: MenuEntryType.ENTRY + }); + } + } + spawn_context_menu(event.pageX, event.pageY, ...entries); } clientsByGroup(group: Group) : ClientEntry[] { diff --git a/shared/js/voice/AudioController.ts b/shared/js/voice/AudioController.ts index 6e034c55..b47e1954 100644 --- a/shared/js/voice/AudioController.ts +++ b/shared/js/voice/AudioController.ts @@ -80,6 +80,8 @@ class AudioController { private _codecCache: CodecClientCache[] = []; private _timeIndex: number = 0; private _latencyBufferLength: number = 3; + private _buffer_timeout: NodeJS.Timer; + allowBuffering: boolean = true; //Events @@ -125,6 +127,7 @@ class AudioController { switch (this.playerState) { case PlayerState.PREBUFFERING: case PlayerState.BUFFERING: + this.reset_buffer_timeout(true); //Reset timeout, we got a new buffer if(this.audioCache.length <= this._latencyBufferLength) { if(this.playerState == PlayerState.BUFFERING) { if(this.allowBuffering) break; @@ -183,14 +186,18 @@ class AudioController { this.playingAudioCache = []; } this.testBufferQueue(); + this.playQueue(); //Flush queue } private testBufferQueue() { if(this.audioCache.length == 0 && this.playingAudioCache.length == 0) { - if(this.playerState != PlayerState.STOPPING) { + if(this.playerState != PlayerState.STOPPING && this.playerState != PlayerState.STOPPED) { + if(this.playerState == PlayerState.BUFFERING) return; //We're already buffering + this.playerState = PlayerState.BUFFERING; if(!this.allowBuffering) console.warn("[Audio] Detected a buffer underflow!"); + this.reset_buffer_timeout(true); } else { this.playerState = PlayerState.STOPPED; this.onSilence(); @@ -198,6 +205,19 @@ class AudioController { } } + private reset_buffer_timeout(restart: boolean) { + if(this._buffer_timeout) + clearTimeout(this._buffer_timeout); + if(restart) + this._buffer_timeout = setTimeout(() => { + if(this.playerState == PlayerState.PREBUFFERING || this.playerState == PlayerState.BUFFERING) { + console.warn("[Audio] Buffering exceeded timeout. Flushing and stopping replay"); + this.stopAudio(); + } + this._buffer_timeout = undefined; + }, 1000); + } + get volume() : number { return this._volume; } set volume(val: number) {