import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame"; import * as events from "tc-shared/events"; import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client"; import {voice} from "tc-shared/connection/ConnectionBase"; import PlayerState = voice.PlayerState; import {LogCategory} from "tc-shared/log"; import {CommandResult, ErrorID, PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration"; import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal"; import * as log from "tc-shared/log"; import * as image_preview from "../image_preview"; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; interface LoadedSongData { description: string; title: string; url: string; length: number; thumbnail?: string; metadata: {[key: string]: string}; } export class MusicInfo { readonly events: events.Registry; readonly handle: Frame; private _html_tag: JQuery; private _container_playlist: JQuery; private _current_bot: MusicClientEntry | undefined; private update_song_info: number = 0; /* timestamp when we force update the info */ private time_select: { active: boolean, max_time: number, current_select_time: number, current_player_time: number } = { active: false, current_select_time: 0, max_time: 0, current_player_time: 0}; private song_reorder: { active: boolean, song_id: number, previous_entry: number, html: JQuery, mouse?: {x: number, y: number}, indicator: JQuery } = { active: false, song_id: 0, previous_entry: 0, html: undefined, indicator: $.spawn("div").addClass("reorder-indicator") }; previous_frame_content: FrameContent; constructor(handle: Frame) { this.events = new events.Registry(); this.handle = handle; this.events.enable_debug("music-info"); this.initialize_listener(); this._build_html_tag(); this.set_current_bot(undefined, true); } html_tag() : JQuery { return this._html_tag; } destroy() { this.set_current_bot(undefined); this.events.destroy(); this._html_tag && this._html_tag.remove(); this._html_tag = undefined; this._current_bot = undefined; this.previous_frame_content = undefined; } private format_time(value: number) { if(value == 0) return "--:--:--"; value /= 1000; let hours = 0, minutes = 0; while(value >= 60 * 60) { hours++; value -= 60 * 60; } while(value >= 60) { minutes++; value -= 60; } return ("0" + hours).substr(-2) + ":" + ("0" + minutes).substr(-2) + ":" + ("0" + value.toFixed(0)).substr(-2); }; private _build_html_tag() { this._html_tag = $("#tmpl_frame_chat_music_info").renderTag(); this._container_playlist = this._html_tag.find(".container-playlist"); this._html_tag.find(".button-close").on('click', () => { if(this.previous_frame_content === FrameContent.CLIENT_INFO) this.previous_frame_content = FrameContent.NONE; this.handle.set_content(this.previous_frame_content); }); this._html_tag.find(".button-reload-playlist").on('click', () => this.events.fire("action_playlist_reload")); this._html_tag.find(".button-song-add").on('click', () => this.events.fire("action_song_add")); this._html_tag.find(".thumbnail").on('click', event => { const image = this._html_tag.find(".thumbnail img"); const url = image.attr("x-thumbnail-url"); if(!url) return; image_preview.preview_image(decodeURIComponent(url), decodeURIComponent(url)); }); { const button_play = this._html_tag.find(".control-buttons .button-play"); const button_pause = this._html_tag.find(".control-buttons .button-pause"); button_play.on('click', () => this.events.fire("action_play")); button_pause.on('click', () => this.events.fire("action_pause")); this.events.on(["bot_change", "bot_property_update"], event => { if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return; button_play.toggleClass("hidden", this._current_bot === undefined || this._current_bot.properties.player_state < PlayerState.STOPPING); }); this.events.on(["bot_change", "bot_property_update"], event => { if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return; button_pause.toggleClass("hidden", this._current_bot !== undefined && this._current_bot.properties.player_state >= PlayerState.STOPPING); }); this._html_tag.find(".control-buttons .button-rewind").on('click', () => this.events.fire("action_rewind")); this._html_tag.find(".control-buttons .button-forward").on('click', () => this.events.fire("action_forward")); } /* timeline updaters */ { const container = this._html_tag.find(".container-timeline"); const timeline = container.find(".timeline"); const indicator_playtime = container.find(".indicator-playtime"); const indicator_buffered = container.find(".indicator-buffered"); const thumb = container.find(".thumb"); const timestamp_current = container.find(".timestamps .current"); const timestamp_max = container.find(".timestamps .max"); thumb.on('mousedown', event => event.button === 0 && this.events.fire("playtime_move_begin")); this.events.on(["bot_change", "player_song_change", "player_time_update", "playtime_move_end"], event => { if(!this._current_bot) { this.time_select.max_time = 0; indicator_buffered.each((_, e) => { e.style.width = "0%"; }); indicator_playtime.each((_, e) => { e.style.width = "0%"; }); thumb.each((_, e) => { e.style.marginLeft = "0%"; }); timestamp_current.text("--:--:--"); timestamp_max.text("--:--:--"); return; } if(event.type === "playtime_move_end" && !event.as<"playtime_move_end">().canceled) return; const update_info = Date.now() > this.update_song_info; this._current_bot.requestPlayerInfo(update_info ? 1000 : 60 * 1000).then(data => { if(update_info) this.display_song_info(data); let played, buffered; if(event.type !== "player_time_update") { played = data.player_replay_index; buffered = data.player_buffered_index; } else { played = event.as<"player_time_update">().player_replay_index; buffered = event.as<"player_time_update">().player_buffered_index; } this.time_select.current_player_time = played; this.time_select.max_time = data.player_max_index; timestamp_max.text(data.player_max_index ? this.format_time(data.player_max_index) : "--:--:--"); if(this.time_select.active) return; let wplayed, wbuffered; if(data.player_max_index) { wplayed = (played * 100 / data.player_max_index).toFixed(2) + "%"; wbuffered = (buffered * 100 / data.player_max_index).toFixed(2) + "%"; timestamp_current.text(this.format_time(played)); } else { wplayed = "100%"; wbuffered = "100%"; timestamp_current.text(this.format_time(played)); } indicator_buffered.each((_, e) => { e.style.width = wbuffered; }); indicator_playtime.each((_, e) => { e.style.width = wplayed; }); thumb.each((_, e) => { e.style.marginLeft = wplayed; }); }); }); const move_callback = (event: MouseEvent) => { const x_min = timeline.offset().left; const x_max = x_min + timeline.width(); let current = event.pageX; if(current < x_min) current = x_min; else if(current > x_max) current = x_max; const percent = (current - x_min) / (x_max - x_min); this.time_select.current_select_time = percent * this.time_select.max_time; timestamp_current.text(this.format_time(this.time_select.current_select_time)); const w = (percent * 100).toFixed(2) + "%"; indicator_playtime.each((_, e) => { e.style.width = w; }); thumb.each((_, e) => { e.style.marginLeft = w; }); }; const up_callback = (event: MouseEvent | FocusEvent) => { if(event.type === "mouseup") if((event as MouseEvent).button !== 0) return; this.events.fire("playtime_move_end", { canceled: event.type !== "mouseup", target_time: this.time_select.current_select_time }); }; this.events.on("playtime_move_begin", event => { if(this.time_select.max_time <= 0) return; this.time_select.active = true; indicator_buffered.each((_, e) => { e.style.width = "0"; }); document.addEventListener("mousemove", move_callback); document.addEventListener("mouseleave", up_callback); document.addEventListener("blur", up_callback); document.addEventListener("mouseup", up_callback); document.body.style.userSelect = "none"; }); this.events.on(["bot_change", "player_song_change", "playtime_move_end"], event => { document.removeEventListener("mousemove", move_callback); document.removeEventListener("mouseleave", up_callback); document.removeEventListener("blur", up_callback); document.removeEventListener("mouseup", up_callback); document.body.style.userSelect = undefined; this.time_select.active = false; if(event.type === "playtime_move_end") { const data = event.as<"playtime_move_end">(); if(data.canceled) return; const offset = data.target_time - this.time_select.current_player_time; this.events.fire(offset > 0 ? "action_forward_ms" : "action_rewind_ms", {units: Math.abs(offset) }); } }); } /* song info handlers */ this.events.on(["bot_change", "player_song_change"], event => { let song: SongInfo; /* update the player info so we dont get old data */ if(this._current_bot) { this.update_song_info = 0; this._current_bot.requestPlayerInfo(1000).then(data => { this.display_song_info(data); }).catch(error => { log.warn(LogCategory.CLIENT, tr("Failed to update current song for side bar: %o"), error); }); } if(event.type === "bot_change") { song = undefined; } else { song = event.as<"player_song_change">().song; } this.display_song_info(song); }); } private display_song_info(song: SongInfo) { if(song) { if(!song.song_loaded) { console.log("Awaiting a loaded song info."); this.update_song_info = 0; } else { console.log("Song info loaded."); this.update_song_info = Date.now() + 60 * 1000; } } if(!song) song = new SongInfo(); const container_thumbnail = this._html_tag.find(".player .container-thumbnail"); const container_info = this._html_tag.find(".player .container-song-info"); container_thumbnail.find("img") .attr("src", song.song_thumbnail || "img/music/no-thumbnail.png") .attr("x-thumbnail-url", encodeURIComponent(song.song_thumbnail)) .css("cursor", song.song_thumbnail ? "pointer" : null); if(song.song_id) container_info.find(".song-name").text(song.song_title || song.song_url).attr("title", song.song_title || song.song_url); else container_info.find(".song-name").text(tr("No song selected")); if(song.song_description) { container_info.find(".song-description").removeClass("hidden").text(song.song_description).attr("title", song.song_description); } else { container_info.find(".song-description").addClass("hidden").text(tr("Song has no description")).attr("title", tr("Song has no description")); } } private initialize_listener() { //Must come at first! this.events.on("player_song_change", event => { if(!this._current_bot) return; this._current_bot.requestPlayerInfo(0); /* enforce an info refresh */ }); /* bot property listener */ const callback_property = (event: ClientEvents["notify_properties_updated"]) => this.events.fire("bot_property_update", { properties: Object.keys(event.updated_properties) }); const callback_time_update = (event: ClientEvents["music_status_update"]) => this.events.fire("player_time_update", event, true); const callback_song_change = (event: ClientEvents["music_song_change"]) => this.events.fire("player_song_change", event, true); this.events.on("bot_change", event => { if(event.old) { event.old.events.off(callback_property); event.old.events.off(callback_time_update); event.old.events.off(callback_song_change); event.old.events.disconnect_all(this.events); } if(event.new) { event.new.events.on("notify_properties_updated", callback_property); event.new.events.on("music_status_update", callback_time_update); event.new.events.on("music_song_change", callback_song_change); event.new.events.connect("playlist_song_add", this.events); event.new.events.connect("playlist_song_remove", this.events); event.new.events.connect("playlist_song_reorder", this.events); event.new.events.connect("playlist_song_loaded", this.events); } }); /* basic player actions */ { const action_map = { "action_play": 1, "action_pause": 2, "action_forward": 3, "action_rewind": 4, "action_forward_ms": 5, "action_rewind_ms": 6 }; this.events.on(Object.keys(action_map) as any, event => { if(!this._current_bot) return; const action_id = action_map[event.type]; if(typeof action_id === "undefined") { log.warn(LogCategory.GENERAL, tr("Invalid music bot action event detected: %s. This should not happen!"), event.type); return; } const data = { bot_id: this._current_bot.properties.client_database_id, action: action_id, units: event.units }; this.handle.handle.serverConnection.send_command("musicbotplayeraction", data).catch(error => { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; log.error(LogCategory.CLIENT, tr("Failed to perform action %s on bot: %o"), event.type, error); //TODO: Better error dialog createErrorModal(tr("Failed to perform action."), tr("Failed to perform action for music bot.")).open(); }); }); } this.events.on("action_song_set", event => { if(!this._current_bot) return; const connection = this.handle.handle.serverConnection; if(!connection || !connection.connected()) return; connection.send_command("playlistsongsetcurrent", { playlist_id: this._current_bot.properties.client_playlist_id, song_id: event.song_id }).catch(error => { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; log.error(LogCategory.CLIENT, tr("Failed to set current song on bot: %o"), event.type, error); //TODO: Better error dialog createErrorModal(tr("Failed to set song."), tr("Failed to set current replaying song.")).open(); }) }); this.events.on("action_song_add", () => { if(!this._current_bot) return; createInputModal(tr("Enter song URL"), tr("Please enter the target song URL"), text => { try { new URL(text); return true; } catch(error) { return false; } }, result => { if(!result || !this._current_bot) return; const connection = this.handle.handle.serverConnection; connection.send_command("playlistsongadd", { playlist_id: this._current_bot.properties.client_playlist_id, url: result }).catch(error => { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; log.error(LogCategory.CLIENT, tr("Failed to add song to bot playlist: %o"), error); //TODO: Better error description createErrorModal(tr("Failed to insert song"), tr("Failed to append song to the playlist.")).open(); }); }).open(); }); this.events.on("action_song_delete", event => { if(!this._current_bot) return; const connection = this.handle.handle.serverConnection; if(!connection || !connection.connected()) return; connection.send_command("playlistsongremove", { playlist_id: this._current_bot.properties.client_playlist_id, song_id: event.song_id }).catch(error => { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; log.error(LogCategory.CLIENT, tr("Failed to delete song from bot playlist: %o"), error); //TODO: Better error description createErrorModal(tr("Failed to delete song"), tr("Failed to remove song from the playlist.")).open(); }); }); /* bot subscription */ this.events.on("bot_change", () => { const connection = this.handle.handle.serverConnection; if(!connection || !connection.connected()) return; const bot_id = this._current_bot ? this._current_bot.properties.client_database_id : 0; this.handle.handle.serverConnection.send_command("musicbotsetsubscription", { bot_id: bot_id }).catch(error => { log.warn(LogCategory.CLIENT, tr("Failed to subscribe to displayed bot within the side bar: %o"), error); }); }); /* playlist stuff */ this.events.on(["bot_change", "action_playlist_reload"], event => { this.playlist_subscribe(true); this.update_playlist(); }); this.events.on("playlist_song_add", event => { const animation = typeof event.insert_effect === "boolean" ? event.insert_effect : true; const html_entry = this.build_playlist_entry(event.song, animation); const playlist = this._container_playlist.find(".playlist"); const previous = playlist.find(".entry[song-id=" + event.song.song_previous_song_id + "]"); if(previous.length) html_entry.insertAfter(previous); else html_entry.appendTo(playlist); if(event.song.song_loaded) this.events.fire("playlist_song_loaded", { html_entry: html_entry, metadata: event.song.song_metadata, success: true, song_id: event.song.song_id }); if(animation) setTimeout(() => html_entry.addClass("shown"), 50); }); this.events.on("playlist_song_remove", event => { const playlist = this._container_playlist.find(".playlist"); const song = playlist.find(".entry[song-id=" + event.song_id + "]"); song.addClass("deleted"); setTimeout(() => song.remove(), 5000); /* to play some animations */ }); this.events.on("playlist_song_reorder", event => { const playlist = this._container_playlist.find(".playlist"); const entry = playlist.find(".entry[song-id=" + event.song_id + "]"); if(!entry) return; console.log(event); const previous = playlist.find(".entry[song-id=" + event.previous_song_id + "]"); if(previous.length) { entry.insertAfter(previous); } else { entry.insertBefore(playlist.find(".entry")[0]); } }); this.events.on("playlist_song_loaded", event => { const entry = event.html_entry || this._container_playlist.find(".playlist .entry[song-id=" + event.song_id + "]"); const thumbnail = entry.find(".container-thumbnail img"); const name = entry.find(".name"); const description = entry.find(".description"); const length = entry.find(".length"); if(event.success) { let meta: LoadedSongData; try { meta = JSON.parse(event.metadata); } catch(error) { log.warn(LogCategory.CLIENT, tr("Failed to decode song metadata")); meta = { description: "", title: "", metadata: {}, length: 0, url: entry.attr("song-url") } } if(!meta.title && meta.description) { meta.title = meta.description.split("\n")[0]; meta.description = meta.description.split("\n").slice(1).join("\n"); } meta.title = meta.title || meta.url; name.text(meta.title); description.text(meta.description); length.text(this.format_time(meta.length || 0)); if(meta.thumbnail) { thumbnail.attr("src", meta.thumbnail) .attr("x-thumbnail-url", encodeURIComponent(meta.thumbnail)); } } else { name.text(tr("failed to load ") + entry.attr("song-url")).attr("title", tr("failed to load ") + entry.attr("song-url")); description.text(event.error_msg || tr("unknown error")).attr("title", event.error_msg || tr("unknown error")); } }); /* song reorder */ { const move_callback = (event: MouseEvent) => { if(!this.song_reorder.html) return; this.song_reorder.html.each((_, e) => { e.style.left = (event.pageX - this.song_reorder.mouse.x) + "px"; e.style.top = (event.pageY - this.song_reorder.mouse.y) + "px"; }); const entries = this._container_playlist.find(".playlist .entry"); let before: HTMLElement; for(const entry of entries) { const off = $(entry).offset().top; if(off > event.pageY) { this.song_reorder.indicator.insertBefore(entry); this.song_reorder.previous_entry = before ? parseInt(before.attributes.getNamedItem("song-id").value) : 0; return; } before = entry; } this.song_reorder.indicator.insertAfter(entries.last()); this.song_reorder.previous_entry = before ? parseInt(before.attributes.getNamedItem("song-id").value) : 0; }; const up_callback = (event: MouseEvent | FocusEvent) => { if(event.type === "mouseup") if((event as MouseEvent).button !== 0) return; this.events.fire("reorder_end", { canceled: event.type !== "mouseup", song_id: this.song_reorder.song_id, entry: this.song_reorder.html, previous_entry: this.song_reorder.previous_entry }); }; this.events.on("reorder_begin", event => { this.song_reorder.song_id = event.song_id; this.song_reorder.html = event.entry; const width = this.song_reorder.html.width() + "px"; this.song_reorder.html.each((_, e) => { e.style.width = width; }); this.song_reorder.active = true; this.song_reorder.html.addClass("reordering"); document.addEventListener("mousemove", move_callback); document.addEventListener("mouseleave", up_callback); document.addEventListener("blur", up_callback); document.addEventListener("mouseup", up_callback); document.body.style.userSelect = "none"; }); this.events.on(["bot_change", "playlist_song_remove", "reorder_end"], event => { if(event.type === "playlist_song_remove" && event.as<"playlist_song_remove">().song_id !== this.song_reorder.song_id) return; document.removeEventListener("mousemove", move_callback); document.removeEventListener("mouseleave", up_callback); document.removeEventListener("blur", up_callback); document.removeEventListener("mouseup", up_callback); document.body.style.userSelect = undefined; this.song_reorder.active = false; this.song_reorder.indicator.remove(); if(this.song_reorder.html) { this.song_reorder.html.each((_, e) => { e.style.width = null; e.style.left = null; e.style.top = null; }); this.song_reorder.html.removeClass("reordering"); } if(event.type === "reorder_end") { const data = event.as<"reorder_end">(); if(data.canceled) return; const connection = this.handle.handle.serverConnection; if(!connection || !connection.connected()) return; if(!this._current_bot) return; connection.send_command("playlistsongreorder", { playlist_id: this._current_bot.properties.client_playlist_id, song_id: data.song_id, song_previous_song_id: data.previous_entry }).catch(error => { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; log.error(LogCategory.CLIENT, tr("Failed to add song to bot playlist: %o"), error); //TODO: Better error description createErrorModal(tr("Failed to reorder song"), tr("Failed to reorder song within the playlist.")).open(); }); console.log("Reorder to %d", data.previous_entry); } }); this.events.on(["bot_change", "player_song_change"], event => { if(!this._current_bot) { this._html_tag.find(".playlist .current-song").removeClass("current-song"); return; } this._current_bot.requestPlayerInfo(1000).then(data => { const song_id = data ? data.song_id : 0; this._html_tag.find(".playlist .current-song").removeClass("current-song"); this._html_tag.find(".playlist .entry[song-id=" + song_id + "]").addClass("current-song"); }); }); } } set_current_bot(client: MusicClientEntry | undefined, enforce?: boolean) { if(client) client.updateClientVariables(); /* just to ensure */ if(client === this._current_bot && (typeof(enforce) === "undefined" || !enforce)) return; const old = this._current_bot; this._current_bot = client; this.events.fire("bot_change", { new: client, old: old }); } current_bot() : MusicClientEntry | undefined { return this._current_bot; } private sort_songs(data: PlaylistSong[]) { const result = []; let appendable: PlaylistSong[] = []; for(const song of data) { if(song.song_id == 0 || data.findIndex(e => e.song_id === song.song_previous_song_id) == -1) result.push(song); else appendable.push(song); } let iters; while (appendable.length) { do { iters = 0; const left: PlaylistSong[] = []; for(const song of appendable) { const index = data.findIndex(e => e.song_id === song.song_previous_song_id); if(index == -1) { left.push(song); continue; } result.splice(index + 1, 0, song); iters++; } appendable = left; } while(iters > 0); if(appendable.length) result.push(appendable.pop_front()); } return result; } /* playlist stuff */ update_playlist() { this.playlist_subscribe(true); this._container_playlist.find(".overlay").toggleClass("hidden", true); const playlist = this._container_playlist.find(".playlist"); playlist.empty(); if(!this.handle.handle.serverConnection || !this.handle.handle.serverConnection.connected() || !this._current_bot) { this._container_playlist.find(".overlay-empty").removeClass("hidden"); return; } const overlay_loading = this._container_playlist.find(".overlay-loading"); overlay_loading.removeClass("hidden"); this._current_bot.updateClientVariables(true).catch(error => { log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error); }).then(() => { this.handle.handle.serverConnection.command_helper.request_playlist_songs(this._current_bot.properties.client_playlist_id, false).then(songs => { this.playlist_subscribe(false); /* we're allowed to see the playlist */ if(!songs) { this._container_playlist.find(".overlay-empty").removeClass("hidden"); return; } for(const song of this.sort_songs(songs)) this.events.fire("playlist_song_add", { song: song, insert_effect: false }); }).catch(error => { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) { this._container_playlist.find(".overlay-no-permissions").removeClass("hidden"); return; } log.error(LogCategory.CLIENT, tr("Failed to load bot playlist: %o"), error); this._container_playlist.find(".overlay.overlay-error").removeClass("hidden"); }).then(() => { overlay_loading.addClass("hidden"); }); }); } private _playlist_subscribed = false; private playlist_subscribe(unsubscribe: boolean) { if(!this.handle.handle.serverConnection) return; if(unsubscribe || !this._current_bot) { if(!this._playlist_subscribed) return; this._playlist_subscribed = false; this.handle.handle.serverConnection.send_command("playlistsetsubscription", {playlist_id: 0}).catch(error => { log.warn(LogCategory.CLIENT, tr("Failed to unsubscribe from last playlist: %o"), error); }); } else { this.handle.handle.serverConnection.send_command("playlistsetsubscription", { playlist_id: this._current_bot.properties.client_playlist_id }).then(() => this._playlist_subscribed = true).catch(error => { log.warn(LogCategory.CLIENT, tr("Failed to subscribe to bots playlist: %o"), error); }); } } private build_playlist_entry(data: PlaylistSong, insert_effect: boolean) : JQuery { const tag = $("#tmpl_frame_music_playlist_entry").renderTag(); tag.attr({ "song-id": data.song_id, "song-url": data.song_url }); const thumbnail = tag.find(".container-thumbnail img"); const name = tag.find(".name"); const description = tag.find(".description"); const length = tag.find(".length"); tag.find(".button-delete").on('click', () => this.events.fire("action_song_delete", { song_id: data.song_id })); tag.find(".container-thumbnail").on('click', event => { const target = tag.find(".container-thumbnail img"); const url = target.attr("x-thumbnail-url"); if(!url) return; image_preview.preview_image(decodeURIComponent(url), decodeURIComponent(url)); }); tag.on('dblclick', event => this.events.fire("action_song_set", { song_id: data.song_id })); name.text(tr("loading...")); description.text(data.song_url); tag.on('mousedown', event => { if(event.button !== 0) return; this.song_reorder.mouse = { x: event.pageX, y: event.pageY }; const baseOff = tag.offset(); const off = { x: event.pageX - baseOff.left, y: event.pageY - baseOff.top }; const move_listener = (event: MouseEvent) => { const distance = Math.pow(event.pageX - this.song_reorder.mouse.x, 2) + Math.pow(event.pageY - this.song_reorder.mouse.y, 2); if(distance < 50) return; document.removeEventListener("blur", up_listener); document.removeEventListener("mouseup", up_listener); document.removeEventListener("mousemove", move_listener); this.song_reorder.mouse = off; this.events.fire("reorder_begin", { entry: tag, song_id: data.song_id }); }; const up_listener = event => { if(event.type === "mouseup" && event.button !== 0) return; document.removeEventListener("blur", up_listener); document.removeEventListener("mouseup", up_listener); document.removeEventListener("mousemove", move_listener); }; document.addEventListener("blur", up_listener); document.addEventListener("mouseup", up_listener); document.addEventListener("mousemove", move_listener); }); if(this._current_bot) { this._current_bot.requestPlayerInfo(60 * 1000).then(pdata => { if(pdata.song_id === data.song_id) tag.addClass("current-song"); }); } if(insert_effect) { tag.removeClass("shown"); tag.addClass("animation"); } return tag; } }