Adds an "Add to Playlist" button for easier and quicker access.
// ==UserScript==
// @name SoundCloud: Additional "Add to playlist" button
// @description Adds an "Add to Playlist" button for easier and quicker access.
// @version 1.1
// @author iammordaty
// @namespace https://github.com/iammordaty
// @match https://soundcloud.com/*
// @license MIT
// @grant none
// @icon https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico
// ==/UserScript==
const ELEMENTS = '.soundList__item, .soundBadgeList__item, .listenEngagement__footer, .trackList__item, .historicalPlays__item';
const BUTTON_CLASS_NAMES = [
'sc-button',
'sc-button-icon',
'sc-button-medium',
'sc-button-responsive',
'sc-button-secondary',
'sc-button-small',
];
const getButtonClassList = refNodeClassList =>
[...BUTTON_CLASS_NAMES.filter(value => refNodeClassList.includes(value)),
'sc-button-add-to-playlist',
'sc-button-addtoset'
];
const createButton = (container, refNode) => {
const button = document.createElement('button');
button.setAttribute('role', 'button');
const classList = getButtonClassList([...refNode.classList]);
button.classList.add(...classList);
button.setAttribute('title', 'Add this track to Playlist');
const innerDiv = document.createElement('div');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 16 16');
svg.setAttribute('aria-hidden', 'true');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M3.25 7V4.75H1v-1.5h2.25V1h1.5v2.25H7v1.5H4.75V7h-1.5zM9 4.75h6v-1.5H9v1.5zM15 9.875H1v-1.5h14v1.5zM1 15h14v-1.5H1V15z');
path.setAttribute('fill', 'currentColor');
svg.appendChild(path);
innerDiv.appendChild(svg);
button.appendChild(innerDiv);
button.addEventListener('click', () => {
container.querySelector('button[aria-label="more" i]').click();
document.querySelector('button[title="add to playlist" i]').click();
}, false);
return button;
};
const insertAfter = (button, refButton) => refButton.parentNode.insertBefore(button, refButton);
const pending = new Set();
let scheduled = false;
const scheduleProcess = () => {
if (scheduled) {
return;
}
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
pending.forEach(container => processContainer(container));
pending.clear();
});
};
const processContainer = container => {
if (!(container instanceof HTMLElement)) {
return;
}
const refButton = container.querySelector('.sc-button-more');
if (!refButton) {
return;
}
if (container.querySelector('.sc-button-add-to-playlist')) {
return;
}
const button = createButton(container, refButton);
insertAfter(button, refButton);
};
const scanPage = () => {
document.querySelectorAll(ELEMENTS).forEach(el => pending.add(el));
scheduleProcess();
};
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) {
continue;
}
if (node.matches(ELEMENTS)) {
pending.add(node);
}
node.querySelectorAll?.(ELEMENTS).forEach(el => {
pending.add(el);
});
}
}
scheduleProcess();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
document.addEventListener('DOMContentLoaded', scanPage, false);
window.addEventListener('load', scanPage, false);