NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Deezer Drop
// @version 0.7.1
// @description Simple tracks and playlists downloader from Deezer
// @updateURL http://93.179.68.67/drop/drop.meta.js
// @downloadURL http://93.179.68.67/drop/drop.user.js
// @include http://www.deezer.com/*
// @include https://www.deezer.com/*
// @require https://cdn.jsdelivr.net/npm/aes-js@3.1.2/index.min.js
// @require https://cdn.jsdelivr.net/npm/egoroof-blowfish@2.1.0/dist/blowfish.min.js
// @require https://cdn.jsdelivr.net/npm/browser-id3-writer@4.1.0/dist/browser-id3-writer.min.js
// @require https://cdn.jsdelivr.net/npm/spark-md5@3.0.0/spark-md5.min.js
// @require https://cdn.jsdelivr.net/npm/file-saver@2.0.0/dist/FileSaver.min.js
// @author LittleCrabby
// @connect deezer.com
// @connect dzcdn.net
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @licence MIT
// @copyright 2018, LittleCrabby (https://openuserjs.org/users/LittleCrabby)
// ==/UserScript==
(() => {
'use strict';
//Check if we are on Deezer App page. We don't want script to work on Deezer homepage for example.
if (!document.getElementById('dzr-app')) {
return;
}
const keys = {
aesKey: "jo6aey6haid2Teih",
bfIv: "\x00\x01\x02\x03\x04\x05\x06\x07",
bfSecret: "g4el58wc0zvf9na1"
};
const urls = {
apiUrl: "https://www.deezer.com/ajax/gw-light.php?method={0}&input=3&api_version=1.0&api_token={1}",
trackUrl: "https://e-cdns-proxy-{0}.dzcdn.net/mobile/1/{1}",
coverUrl: "https://e-cdns-images.dzcdn.net/images/cover/{0}/{1}x{2}.jpg"
};
const regexes = {
playlist: /https:\/\/www\.deezer\.com\/\w*\/playlist\/(\d*)$/,
album: /https:\/\/www\.deezer\.com\/\w*\/album\/(\d*)$/
};
class UI {
constructor(history, queue, urls, regexes) {
this.history = history;
this.queue = queue;
this.urls = urls;
this.regexes = regexes;
this.dropModal = document.createElement("div");
this.queueContainer = document.createElement("div");
this.historyContainer = document.createElement("div");
this.dropBtn = document.createElement("span");
this._addDropModal();
this.history.items.forEach(h => {
this.addHistoryRow(h);
});
}
_switchTab(target) {
const containers = this.dropModal.querySelectorAll(".drop-container");
const tabs = target.parentElement.childNodes;
const tab = target;
if (!tab.classList.contains("active")) {
containers.forEach(x => x.classList.remove("active"));
tabs.forEach(x => x.classList.remove("active"));
tab.classList.add("active");
this[target.dataset.container].classList.add("active");
}
}
_addDropModal() {
this.dropModal.id = "drop";
this.dropModal.classList.add("drop");
const emptyQueue = document.createElement("span");
const emptyHistory = document.createElement("span");
emptyQueue.innerText = "Click drop button to add tracks to queue";
emptyQueue.classList.add("drop", "drop-empty");
emptyHistory.innerText = "Previously downloaded tracks will be here";
emptyHistory.classList.add("drop", "drop-empty");
const dropHeader = document.createElement("div");
const queueTab = document.createElement("span");
const historyTab = document.createElement("span");
queueTab.innerText = "Queue";
queueTab.classList.add("drop", "drop-tab", "active");
queueTab.id = "queue-tab";
queueTab.dataset.container = "queueContainer";
historyTab.innerText = "History";
historyTab.classList.add("drop", "drop-tab");
historyTab.id = "history-tab";
historyTab.dataset.container = "historyContainer";
dropHeader.classList.add("drop", "drop-header");
dropHeader.appendChild(queueTab);
dropHeader.appendChild(historyTab);
const clearQueue = document.createElement("span");
const clearHistory = document.createElement("span");
clearQueue.innerText = "๐ clear queue";
clearQueue.classList.add("drop", "drop-clear");
clearQueue.id = "clear-queue";
clearHistory.innerText = "๐ clear history";
clearHistory.classList.add("drop", "drop-clear");
clearHistory.id = "clear-history";
this.queueContainer.classList.add("drop", "drop-container", "active");
this.queueContainer.id = "queue-container";
this.queueContainer.appendChild(emptyQueue);
this.queueContainer.appendChild(clearQueue);
this.historyContainer.classList.add("drop", "drop-container");
this.historyContainer.id = "history-container";
this.historyContainer.appendChild(emptyHistory);
this.historyContainer.appendChild(clearHistory);
this.dropBtn.classList.add("drop");
this.dropBtn.id = "drop-btn";
this.dropBtn.dataset.after = 0;
this.dropBtn.dataset.before = 0;
this.dropBtn.classList.add("after-hidden", "before-hidden");
this.dropModal.appendChild(dropHeader);
this.dropModal.appendChild(this.queueContainer);
this.dropModal.appendChild(this.historyContainer);
this.dropModal.appendChild(this.dropBtn);
document.body.appendChild(this.dropModal);
this.dropBtn.addEventListener("click", () => {
if (this.dropModal.classList.contains("expanded")) {
this.dropModal.classList.remove("expanded");
} else {
this.dropModal.classList.add("expanded");
}
if (this.historyContainer.classList.contains("active")) {
this.resetHistoryBadge();
}
});
queueTab.addEventListener("click", e => this._switchTab(e.target));
historyTab.addEventListener("click", e => {
this.resetHistoryBadge();
this._switchTab(e.target);
});
clearHistory.addEventListener("click", () => {
this.historyContainer.querySelectorAll('.drop-row').forEach(r => r.remove());
this.resetHistoryBadge();
this.history.clear();
});
clearQueue.addEventListener("click", () => {
this.queueContainer.querySelectorAll('.drop-row').forEach(r => r.remove());
this.resetQueueBadge();
this.queue.clear();
});
}
addToolbarButton(item) {
const ar = this.regexes.album.exec(location.href);
const pr = this.regexes.playlist.exec(location.href);
if (item && (pr || ar)) {
if (!item.getElementsByClassName("drop-list-button").length) {
const toolbarItem = document.createElement("div");
const listBtn = document.createElement("button");
const imgSpan = document.createElement("span");
const txtSpan = document.createElement("span");
const text = document.createTextNode("Drop");
imgSpan.classList.add("drop", "drop-icon");
txtSpan.classList.add("drop", "drop-button-span");
txtSpan.appendChild(text);
listBtn.classList.add("drop", "drop-list-button");
listBtn.appendChild(imgSpan);
listBtn.appendChild(txtSpan);
toolbarItem.classList.add("drop", "toolbar-item");
toolbarItem.appendChild(listBtn);
item.insertBefore(toolbarItem, item.childNodes[2]);
if (pr) {
listBtn.addEventListener("click", () => this.queue.addPlaylist(pr[1]));
}
if (ar) {
listBtn.addEventListener("click", () => this.queue.addAlbum(ar[1]));
}
}
}
}
addTrackButton(item) {
let cell = item.getElementsByClassName("cell-love").item(0);
if (!cell.getElementsByClassName("drop-button").length) {
const downloadBtn = document.createElement("button");
const downloadImg = document.createElement("span");
downloadImg.classList.add("drop", "drop-icon");
downloadBtn.classList.add("drop", "datagrid-action", "drop-button");
downloadBtn.setAttribute('aria-label', "Download");
downloadBtn.appendChild(downloadImg);
downloadBtn.addEventListener("click", () => this.queue.add(item.dataset.key));
cell.appendChild(downloadBtn);
}
}
addQueueRow(trackInfo) {
const row = document.createElement("div");
row.classList.add("drop", "drop-row");
row.dataset.key = trackInfo.SNG_ID;
const picContainer = document.createElement("div");
const trackImg = document.createElement("img");
trackImg.src = this.urls.coverUrl.format(trackInfo.ALB_PICTURE, 60, 60);
trackImg.classList.add("drop");
picContainer.appendChild(trackImg);
picContainer.classList.add("drop", "drop-queue-pic");
row.appendChild(picContainer);
const trackTitle = document.createElement("p");
const trackSize = document.createElement("p");
const trackStatus = document.createElement("p");
const infoContainer = document.createElement("div");
trackTitle.classList.add("drop", "drop-queue-track-title");
trackTitle.innerText = trackInfo.ART_NAME + " - " + trackInfo.SNG_TITLE;
trackSize.classList.add("drop", "drop-queue-track-size");
trackSize.innerText = "" + (trackInfo.FILESIZE_MP3_320 / 1048576).toFixed(2) + " MB";
trackStatus.classList.add("drop", "drop-queue-track-status");
trackStatus.innerText = "In queue";
infoContainer.appendChild(trackTitle);
infoContainer.appendChild(trackSize);
infoContainer.appendChild(trackStatus);
infoContainer.classList.add("drop", "drop-queue-info");
row.appendChild(infoContainer);
const actionContainer = document.createElement("div");
const cancelAction = document.createElement("a");
cancelAction.classList.add("drop", "drop-queue-track-cancel");
cancelAction.innerText = "๐";
actionContainer.appendChild(cancelAction);
actionContainer.classList.add("drop", "drop-queue-actions");
row.appendChild(actionContainer);
this.queueContainer.insertBefore(row, this.queueContainer.querySelector('.drop-empty'));
cancelAction.addEventListener("click", () => {
this.deleteRow(trackInfo.SNG_ID)
this.queue.cancel(trackInfo.SNG_ID);
});
}
addHistoryRow(trackInfo) {
const row = document.createElement("div");
row.classList.add("drop", "drop-row");
row.dataset.key = trackInfo.SNG_ID;
const picContainer = document.createElement("div");
const trackImg = document.createElement("img");
trackImg.src = this.urls.coverUrl.format(trackInfo.ALB_PICTURE, 60, 60);
trackImg.classList.add("drop");
picContainer.appendChild(trackImg);
picContainer.classList.add("drop", "drop-queue-pic");
row.appendChild(picContainer);
const trackTitle = document.createElement("p");
const trackSize = document.createElement("p");
const trackStatus = document.createElement("p");
const infoContainer = document.createElement("div");
trackTitle.classList.add("drop", "drop-queue-track-title");
trackTitle.innerText = trackInfo.ART_NAME + " - " + trackInfo.SNG_TITLE;
trackSize.classList.add("drop", "drop-queue-track-size");
trackSize.innerText = "" + (trackInfo.FILESIZE_MP3_320 / 1048576).toFixed(2) + " MB";
trackStatus.classList.add("drop", "drop-queue-track-status");
trackStatus.innerText = "Downloaded " + trackInfo.timestamp;
infoContainer.appendChild(trackTitle);
infoContainer.appendChild(trackSize);
infoContainer.appendChild(trackStatus);
infoContainer.classList.add("drop", "drop-queue-info");
row.appendChild(infoContainer);
const actionContainer = document.createElement("div");
const restartAction = document.createElement("a");
restartAction.classList.add("drop", "drop-queue-track-restart");
restartAction.innerText = "โฒ";
actionContainer.appendChild(restartAction);
actionContainer.classList.add("drop", "drop-queue-actions");
row.appendChild(actionContainer);
this.historyContainer.insertBefore(row, this.historyContainer.firstChild);
restartAction.addEventListener("click", () => {
this.queue.add(trackInfo.SNG_ID);
});
if (this.history.length > 50) {
this.deleteHistoryRow();
}
}
updateRow(key, status) {
const row = this.queueContainer.querySelector(`.drop-row[data-key='${key}']`);
if (row) {
row.querySelector(".drop-queue-track-status").innerText = status;
}
}
deleteRow(key) {
const row = this.queueContainer.querySelector(`.drop-row[data-key='${key}']`);
if (row) {
row.remove();
}
this.decQueueBadge();
}
deleteHistoryRow() {
const row = this.historyContainer.querySelector(`.drop-row:last-child`);
if (row) {
row.remove();
}
}
incQueueBadge() {
let cnt = parseInt(this.dropBtn.dataset.before);
this.dropBtn.dataset.before = ++cnt;
this.showQueueBadge();
}
incHistoryBadge() {
let cnt = parseInt(this.dropBtn.dataset.after);
this.dropBtn.dataset.after = ++cnt;
this.showHistoryBadge();
}
decQueueBadge() {
let cnt = parseInt(this.dropBtn.dataset.before);
this.dropBtn.dataset.before = --cnt;
if (cnt == 0) {
this.dropBtn.classList.add("before-hidden");
}
}
resetQueueBadge() {
this.dropBtn.dataset.before = 0;
this.hideQueueBadge();
}
resetHistoryBadge() {
this.dropBtn.dataset.after = 0;
this.hideHistoryBadge();
}
hideHistoryBadge() {
this.dropBtn.classList.add("after-hidden");
}
showHistoryBadge() {
this.dropBtn.classList.remove("after-hidden");
}
hideQueueBadge() {
this.dropBtn.classList.add("before-hidden");
}
showQueueBadge() {
this.dropBtn.classList.remove("before-hidden");
}
}
class Queue {
constructor(deezer) {
this.deezer = deezer;
this.items = [];
// init arrays of listeners callbacks
this.addListeners = [];
this.startListeners = [];
this.finishListeners = [];
}
set onItemAdd(listener) {
this.addListeners.push(listener);
}
set onStartDownload(listener) {
this.startListeners.push(listener);
}
set onFinishDownload(listener) {
this.finishListeners.push(listener);
}
async add(item) {
if (Array.isArray(item)) {
// if argument is array, push each item to queue
item.forEach(i => this.push(i));
} else {
this.push(item);
}
}
async push(key) {
const trackInfo = await this.deezer.getTrackInfo(key);
this.addListeners.forEach(f => f(trackInfo));
// each item will contain info about track and XHR object
this.items.push({ti: trackInfo, xhr: new XMLHttpRequest()});
if (this.items.length === 1) {
await this.startDownload();
}
}
async startDownload() {
if (!this.items.length) {
// return if no items left in queue
return;
}
const qItem = this.items[0];
this.startListeners.forEach(f => f(qItem.ti));
// track downloading and decrypting operations
const encryptedBuffer = await this.deezer.downloadTrack(qItem.ti, qItem.xhr);
const decryptedBuffer = await this.deezer.decryptTrack(qItem.ti, encryptedBuffer);
const coverBuffer = await this.deezer.downloadCover(qItem.ti);
// add mp3 tags to file and generate blob
const blob = this.deezer.addTags(decryptedBuffer, coverBuffer, qItem.ti);
// save mp3 file
saveAs(blob, `${qItem.ti.ART_NAME} - ${qItem.ti.SNG_TITLE}.mp3`);
// save timestamp to display in history tab
qItem.ti.timestamp = new Date().toLocaleDateString() + " " + new Date().toLocaleTimeString();
this.finishListeners.forEach(f => f(qItem.ti));
// shift one item from queue and try again
this.items.shift();
await this.startDownload();
}
async addPlaylist(key) {
const playlist = await this.deezer.getPlaylist(key);
this.add(playlist.SONGS.data.map(x => x.SNG_ID));
}
async addAlbum(key) {
const album = await this.deezer.getAlbum(key);
this.add(album.SONGS.data.map(x => x.SNG_ID));
}
cancel(key) {
const i = this.items.findIndex(item => item.ti.SNG_ID == key);
// if key found in queue, remove and abort downloading, start download next track
if (i !== -1) {
this.items[i].xhr.abort();
this.items.splice(i, 1);
this.startDownload();
}
}
clear() {
this.items.forEach(i => i.xhr.abort());
this.items = [];
}
}
class History {
constructor() {
this.history = GM_getValue('history', []);
}
get length() {
return this.history.length;
}
get items() {
return this.history;
}
push(item) {
this.history.push(item);
if (this.history.length > 50) {
this.history.shift();
}
GM_setValue('history', this.history);
}
clear() {
GM_setValue('history', []);
}
}
class Deezer {
constructor(crypt, urls) {
this.crypt = crypt;
this.apiUrl = urls.apiUrl;
this.apiTrackUrl = urls.apiTrackUrl;
this.apiPlaylistUrl = urls.apiPlaylistUrl;
this.apiAlbumUrl = urls.apiAlbumUrl;
this.trackUrl = urls.trackUrl;
this.coverUrl = urls.coverUrl;
this.progressListeners = [];
this.apiToken = "";
}
set onDownloadProgress(listener) {
this.progressListeners.push(listener);
}
get apiKey() {
return new Promise((resolve, reject) => {
if (!this.apiToken) {
// if apiToken is empty, fetch it from deezer API and save
fetch(this.apiUrl.format("deezer.getUserData", ""), {
method: "POST"
})
.then(response => response.json())
.then(data => {
this.apiToken = data.results.checkForm;
resolve(this.apiToken)
});
} else {
resolve(this.apiToken);
}
});
}
async getTrackInfo(id) {
const response = await fetch(this.apiUrl.format("song.getData", await this.apiKey, id), {
method: "POST",
body: JSON.stringify({sng_id: id})
});
const data = await response.json();
return data.results;
}
async getAlbum(id) {
const response = await fetch(this.apiUrl.format("deezer.pageAlbum", await this.apiKey, id), {
method: "POST",
body: JSON.stringify({
alb_id: id,
lang: "en"
})
});
const data = await response.json();
return data.results;
}
async getPlaylist(id) {
const response = await fetch(this.apiUrl.format("deezer.pagePlaylist", await this.apiKey, id), {
method: "POST",
body: JSON.stringify({
playlist_id: id,
lang: "en"
})
});
const data = await response.json();
return data.results;
}
async downloadCover(trackInfo) {
const response = await fetch(this.coverUrl.format(trackInfo.ALB_PICTURE, 500, 500));
return response.arrayBuffer();
}
getTrackUrl(trackInfos) {
// select file format according to track info
const bitRate = trackInfos.FILESIZE_MP3_320 ? 3 : trackInfos.FILESIZE_MP3_256 ? 5 : 1;
// prepare string to be hashed
const toHash = [trackInfos.MD5_ORIGIN, bitRate, trackInfos.SNG_ID, trackInfos.MEDIA_VERSION].join('ยค');
// encrypt using md5 and aes algorithms
const hash = this.crypt.aes(this.crypt.md5(toHash) + 'ยค' + toHash + 'ยค');
return this.trackUrl.format(trackInfos.MD5_ORIGIN[0], hash);
}
downloadTrack(trackInfo, r) {
return new Promise((resolve, reject) => {
r.onload = e => resolve(r.response);
r.onprogress = xhr => this.progressListeners.forEach(f => f(xhr, trackInfo))
r.open("GET", this.getTrackUrl(trackInfo));
r.responseType = "arraybuffer";
r.send();
});
}
decryptTrack(trackInfo, buffer) {
return new Promise((resolve, reject) => {
const bfKey = this.crypt.getBfKey(trackInfo.SNG_ID);
const data = new Uint8Array(buffer);
// work with buffer as with 2048 bytes blocks
for (let i = 0, j = 2048, n = 0; j < data.length; i += 2048, j += 2048, n++) {
if (n % 3 > 0 || data.length - j < 2048) {
// skip and don't decrypt blocks except 3rd and
// skip block that has less than 2048 bytes
continue;
}
// decrypt selected block and save it back to Uint8Array
data.set(this.crypt.bfDecrypt(data.slice(i, j), bfKey), i);
}
resolve(data.buffer);
});
}
addTags(songBuffer, coverBuffer, trackInfo) {
const writer = new ID3Writer(songBuffer);
let TPE1;
// set correct TPE1 tag
trackInfo.SNG_CONTRIBUTORS.featuring ? TPE1 = trackInfo.SNG_CONTRIBUTORS.featuring :
trackInfo.SNG_CONTRIBUTORS.mainartist ? TPE1 = trackInfo.SNG_CONTRIBUTORS.mainartist :
TPE1 = [trackInfo.ART_NAME];
// write tags and cover to mp3 file
writer.setFrame('TIT2', trackInfo.SNG_TITLE)
.setFrame('TPE1', TPE1)
.setFrame('TPE2', trackInfo.ART_NAME)
.setFrame('TALB', trackInfo.ALB_TITLE)
.setFrame('TYER', parseInt(trackInfo.PHYSICAL_RELEASE_DATE))
.setFrame('TRCK', trackInfo.TRACK_NUMBER)
.setFrame('TPOS', trackInfo.DISK_NUMBER)
.setFrame('APIC', { type: 3, data: coverBuffer, description: 'Cover' });
writer.addTag();
return writer.getBlob();
}
}
class Crypt {
constructor(keys) {
this.aesKey = keys.aesKey;
this.bfIv = keys.bfIv;
this.bfSecret = keys.bfSecret;
}
md5(value) {
return SparkMD5.hashBinary(value);
}
bfDecrypt(value, bfKey) {
const bf = new Blowfish(bfKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL);
bf.setIv(this.bfIv);
return bf.decode(value, Blowfish.TYPE.UINT8_ARRAY);
}
aes(value) {
while (value.length % 16 > 0) {
value += ' ';
}
const aesEcb = new aesjs.ModeOfOperation.ecb(this.strToBytes(this.aesKey));
const encryptedBytes = aesEcb.encrypt(this.strToBytes(value));
return aesjs.utils.hex.fromBytes(encryptedBytes);
}
getBfKey(songId) {
let key = "";
const idMd5 = this.md5(songId);
for (let i = 0; i < 16; i++) {
key += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ this.bfSecret.charCodeAt(i));
}
return key;
}
strToBytes(str) {
let bytes = [];
for (let i = 0; i < str.length; i++) {
bytes.push(str.charCodeAt(i));
}
return bytes;
}
}
const deezer = new Deezer(new Crypt(keys), urls);
const history = new History();
const queue = new Queue(deezer);
const ui = new UI(history, queue, urls, regexes);
queue.onItemAdd = (trackInfo) => {
ui.addQueueRow(trackInfo);
ui.incQueueBadge();
};
queue.onStartDownload = (trackInfo) => {
ui.updateRow(trackInfo.SNG_ID, "Downloading track...");
};
queue.onFinishDownload = (trackInfo) => {
history.push(trackInfo);
ui.addHistoryRow(trackInfo);
ui.deleteRow(trackInfo.SNG_ID);
ui.incHistoryBadge();
};
deezer.onDownloadProgress = (xhr, trackInfo) => {
const p = (xhr.loaded / (xhr.total / 100)).toFixed(2);
if (p < 99.9) {
ui.updateRow(trackInfo.SNG_ID, `Downloaded ${p}%`);
} else {
ui.updateRow(trackInfo.SNG_ID, `Processing track...`);
}
};
// Create an observer that will add drop buttons to each dynamically created song row and toolbar
const observer = new MutationObserver(mutationsList => {
for(let mutation of mutationsList) {
if (mutation.type == 'childList') {
let node = mutation.addedNodes[0];
if (node instanceof HTMLElement && typeof node.classList !== "undefined") {
if (node.classList.contains("song")) {
ui.addTrackButton(node);
} else {
const songRows = node.getElementsByClassName("song");
const toolbar = node.getElementsByClassName("toolbar-wrapper").item(0);
ui.addToolbarButton(toolbar);
for (let i = 0; i < songRows.length; i++) {
ui.addTrackButton(songRows[i]);
}
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Add listener to collapse Drop menu when clicking outside of it
document.body.addEventListener("click", e => {
if (!e.target.classList.contains("drop")) {
ui.dropModal.classList.remove("expanded");
}
});
String.prototype.format = function() {
let k, a = this;
for (k in arguments) {
a = a.replace("{" + k + "}", arguments[k])
}
return a
}
GM_addStyle(`
.datagrid-cell.datagrid-cell-action.cell-love {
width: 56px;
padding-left: 5px;
}
div.datagrid-cell.cell-title {
padding-left: 8px;
}
.drop-icon {
display: block;
width: 16px;
height:16px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAA3QAAAN0BcFOiBwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAOGSURBVFiFvZY/bxpJGId/Mzs7ywqJVLFk5MpAEYkq+GxrU9A7rqC6wmn9GSgi+9xcsQVWihRbWE6EdEJyvgOlN3drNzQ0RqI8pQEJOwy7814R1rqz+Q+5n7TFaqT3eWZ239HLiAjLhDEmpZR/AIBS6lciUksVIqKFHwAGY+xLMplUyWRSMca+ADCWqrWkwEchRNhoNKjRaJAQIgTw8X8RAHAMQNfrdYpTr9cJgAZw/FMFALxhjA1d143oSVzXjRhjQwBvfooAgC0hxLdSqaSewuOUSiUlhPgGYGutAgASpmneZjIZ1ev1JvGp1+tRJpNRpmneAkisU+BDIpEIm83mRHicZrNJiUQiBPBhLQIAXjPGolqtNhMep1arEWMsAvB6JQEA3DTNoFwuD+amj1IulwemaQYA+CoCx1LKsNPpLMqnTqdDUsrhrNacBn8phOidnp4uDI9zdnZGQogegJcLC5im+TmdTg/v7++XFnh4eKCtrS1lmubnhQQA7D697ZbN1dVVfEvuzi3AOf+0s7Oz8I83Kfv7+wPO+ae5BAC8MAzj+8XFxbr4dHl5SYZhfAfwYh6BYyll1O121ybQ7XZJShmN6wg+ZkRwisWiTqVSS80X45JKpVAsFjUA5+naMwHLsvL5fF6sjT5KPp8XlmXlZwporXPZbHbdfGSzWWitczMFGGNqMBg8vqfTaZyfn68sMBgMwBh7NjeOOwE/CILHSbVQKODm5mZlgSAISGvtzxQIw/Cr7/uPpoVCAdfX19BaryTg+74Kw/Drs4UxbfiWc677/T4REbVaLbJtm1zXXboN+/0+cc41gLfPeGMENjjnoed5jwWq1SpZlkVBECwl4Hkecc5DABszBUYS723bHrbbbSIi0lrTwcEBWZZFrutSFD2bSSem3W6TbdtDAO/HsiYICCnlreM4KoZpralarZJt25TL5ejo6Iiq1epUeBRF5DiOklLeAhBzC4wkXhmGoSqVyn923Gq16OTkhA4PD2lzc3MqvFKpkGEYCsCriZxJCyOJd4ZhKMdxVPw55km73SbHcdQI/m4qY9pifBJSylvbtoee51HcHePS7/fJ8zyybXs4OvaJO48fNoJMDWNMAKhwzk8A8O3tbbW3tycLhQIDflwyvu+ru7s7CUBrrX8D8DsRhTNrzyPwL5ENAL8A2BFC7HLO9/CD6I8umb8A/ElEf89b8x96fwU2MHVN0AAAAABJRU5ErkJggg==);
background-size: 102% 100%;
}
.drop-button {
width: 28px;
padding: 0 4px;
}
.drop-button .drop-icon {
margin-left: 2px;
}
.drop-list-button {
color: #23232D;
background-color: #F8F8F9;
border: 1px solid #D1D1D7;
cursor: pointer;
height: 32px;
display: inline-flex;
transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
align-items: center;
font-family: Open Sans;
font-weight: 600;
border-radius: 3px;
justify-content: center;
}
.drop-list-button:hover {
background-color: #FFFFFF;
}
.drop-list-button .drop-icon {
margin-right: 6px;
}
.toolbar-item button {
padding: 0 10px;
}
#drop {
position: fixed;
padding: 2px;
right: 20px;
top: 68px;
width:36px;
height:36px;
background-color: #fff;
border-radius:5px;
box-shadow: 0 2px 10px 0 rgba(25,25,34,.24);
z-index: 999;
}
#drop.expanded {
width:400px;
height:72%;
display:flex;
flex-direction:column;
}
#drop-btn {
position: absolute;
top: 5px;
right: 5px;
width:26px;
height:26px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAA3QAAAN0BcFOiBwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAOdSURBVFiFvZdPaBxVHMc/vzd/ktY0KdUtNNnFVhE2JNtom8Y2wV6EmpDaVgy0KUWDWFNYPejNILSB2EBLoGBT/zSCWNCLRUIOORm8qE2zQeofPJcNEepl8RLZnZ2fh9mSqLuZnc3iF+Yw8958v5/5vTeP90RVqUUi4mK5XwJQzA+rar4mI1WNfAEWyG3c7Xnc7XmQ24BVk1eNADcwtsfIrDIyqxjbA278LwDAKOAzNKNc/CO4hmYU8IHRqH4m4rj3IXKdY5eUjpPrDR0n4dglReS6iPRF8awaQETiGGuW5IByJP3f946kDckBxVizIhKvK4CINGKcOVoSzZyadip2PDXt0JJoxjhzItJYNwDgCsakGL7l4DZV7uU2wfAtB2NSwJW6AIjIAUTSnLhmEUuGO8aScOKahUhaRA5sCUBEDMa5SXLQIzUUHv5QqSFIDnoY56aIbJoRVoHzCF30T7jVp5fUP+Eish84XxOAiMQw9lWee9uiuS1yPs1tcPQdG2NfFZFYZACMNcUjj22j763o4Q/V+yY07W7EWFORAESkB794jhcmbOyq/qbyshug/30Hv3hORHqqBsCYNG1PF/6x2tWq9uOQ6C5gTLoqABFpATlN92vRJ14lHXzVBTkdeIcAAGcQ49B+vG75JAdBjAOcqQagl719Pg076gfQsAP29vlAbziA7XYSS9r1Sy8plrSx3c5wAN9/il376p7Prn2BdyiASJ7ihu3dVCfc+XjrAMV84B0KgC6yem99p9raBb//tHWA1XsKuhgOUPTusrK8TrqnC1YyoP7WAFaW8xS9u+EAkCF336WwFtylXoY/V+GHD2sPL6xB7r4LZKoBWAJ8fv4quHv0SXj+PVi4XPtQBF5+yXtzAFV9gPrjzI955LLBw8NvwBNH4dMB+H462nDksjA/5qH+uKo++HezlDsZiYiNcZaIP9PByJyDGEDhzifwzQQ0t0K8G/bsh8OjlcPVh89eLLDy46/4hUOq6oVWAEBVPfzCWbIZWJgsfbEEYRe+DebFXzn47oPNwxcmIZsBv3C2XHjFCmyoxCuImSHRDS995LAzUTlwo3JZ+PpCgWwG1H9dVT+vmBF2OBWRdozzBcbqZOCyTWoInG3lOxfWggk3P+bhF38pfflvm/pXczoWERt4FzEXAcPOx/PED7q0dgkQLDIry/nSr+aj/jgwWanskQE2gOwGDgHdWHYPyLNBiy6WFpkMsFRutlfS3yH6DlSLMq+6AAAAAElFTkSuQmCC);
background-size: 26px;
cursor: pointer;
}
#drop-btn:hover {
opacity: 0.8;
}
#drop-btn::before {
content: attr(data-before);
background-color: rgb(255, 251, 151);
width: 16px;
height: 16px;
display: block;
position: absolute;
text-align: center;
top: -10px;
left: -10px;
border-radius: 5px;
box-shadow: 0 2px 2px 0 rgba(25,25,34,.24);
}
#drop-btn.before-hidden::before {
display:none;
}
#drop-btn::after {
content: attr(data-after);
background-color: rgb(151, 255, 192);
width: 16px;
height: 16px;
display: block;
position: absolute;
text-align: center;
top: -10px;
right: -10px;
border-radius: 5px;
box-shadow: 0 2px 2px 0 rgba(25,25,34,.24);
}
#drop-btn.after-hidden::after {
display:none;
}
.drop-container {
display: none;
height: 100%;
overflow: auto;
margin: 7px 7px;
}
#drop.expanded .drop-container.active {
display: block;
}
#drop.expanded .drop-header {
display: block;
}
.drop-header {
display: none;
font-size: 16px;
margin: 6px 8px;
width: 350px;
}
.drop-tab {
padding: 8px 20px;
cursor: pointer;
}
.drop-tab:hover {
color: black;
border-bottom: 2px solid gray;
}
.drop-tab.active {
color: black;
border-bottom: 2px solid #007feb;
}
.toolbar-wrapper .c0113 {
padding: 0 8px;
}
.toolbar-wrapper .c0117 {
margin-right: 6px;
}
.drop-row {
display: flex;
margin: 5px 0;
}
.drop-queue-pic img {
width: 60px;
height: 60px;
}
.drop-queue-info {
flex: 1;
display: flex;
justify-content: space-around;
border-bottom: 1px solid lightgray;
flex-direction: column;
padding-left: 5px;
}
.drop-queue-actions {
width: 30px;
text-align: center;
display: flex;
justify-content: space-around;
flex-direction: column;
border-bottom: 1px solid lightgray;
}
.drop-empty {
display: block;
padding: 20px;
font-size: 14px;
}
.drop-row ~ .drop-empty {
display: none;
}
.drop-clear {
padding: 5px;
display: none;
text-align: center;
font-size: 14px;
cursor: pointer;
}
.drop-row ~ .drop-clear {
display: block;
}
.drop-clear:hover {
color: black;
}
#page_topbar > div.popper-wrapper.topbar-entrypoints > div {
display: none;
}
`);
})();