SoundCloud: Additional "Add to playlist" button

Adds an additional "Add to Playlist" buttons for easier and quicker access.

  1. // ==UserScript==
  2. // @name SoundCloud: Additional "Add to playlist" button
  3. // @description Adds an additional "Add to Playlist" buttons for easier and quicker access.
  4. // @version 0.9
  5. // @author iammordaty
  6. // @namespace https://github.com/iammordaty
  7. // @match https://soundcloud.com/*
  8. // @license MIT
  9. // @grant none
  10. // @icon https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico
  11. // ==/UserScript==
  12.  
  13. // This must be unique on the page
  14. const ANIMATION_NAME = 'IAMMORDATY-SC-AATPB-ON-NODE-INSTERTED';
  15.  
  16. // https://davidwalsh.name/detect-node-insertion
  17. document.head.insertAdjacentHTML('beforeend', `
  18. <style>
  19. @keyframes ${ANIMATION_NAME} {
  20. from { opacity: 0.99; }
  21. to { opacity: 1; }
  22. }
  23.  
  24. .soundList__item, .listenEngagement__footer, .trackList__item, .historicalPlays__item {
  25. animation-duration: 0.001s;
  26. animation-name: ${ANIMATION_NAME};
  27. }
  28. </style>
  29. `);
  30.  
  31. const BUTTON_CLASS_NAMES = [
  32. 'sc-button-medium',
  33. 'sc-button-secondary',
  34. 'sc-button-small',
  35. 'sc-button-icon',
  36. 'sc-button',
  37. 'sc-button-responsive',
  38. ];
  39.  
  40. const getButtonClassList = refNodeClassList => {
  41. const classList = BUTTON_CLASS_NAMES.filter(value => refNodeClassList.includes(value));
  42.  
  43. return [ ...classList, 'sc-button-add-to-playlist', 'sc-button-addtoset' ];
  44. }
  45.  
  46. const createButton = (container, refNode) => {
  47. const button = document.createElement('button');
  48.  
  49. button.setAttribute('role', 'button');
  50.  
  51. const classList = getButtonClassList([ ...refNode.classList ]);
  52. button.classList.add(...classList);
  53.  
  54. // button.innerHTML = 'Add to Playlist';
  55. button.setAttribute('title', 'Add this track to Playlist');
  56.  
  57. const innerDiv = document.createElement('div');
  58.  
  59. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  60. svg.setAttribute('viewBox', '0 0 16 16');
  61. svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  62. svg.setAttribute('aria-hidden', 'true');
  63.  
  64. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  65. 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');
  66. path.setAttribute('fill', 'currentColor');
  67.  
  68. svg.appendChild(path);
  69. innerDiv.appendChild(svg);
  70.  
  71. button.appendChild(innerDiv);
  72.  
  73. button.addEventListener('click', () => {
  74. // Ignoring case sensitiveness in querySelectorAll: https://stackoverflow.com/a/38399344
  75. container.querySelector('button[aria-label="more" i]').click();
  76.  
  77. document.querySelector('button[title="add to playlist" i]').click();
  78. }, false);
  79.  
  80. return button;
  81. }
  82.  
  83.  
  84. const insertAfter = (button, refButton) => refButton.parentNode.insertBefore(button, refButton);
  85.  
  86. const onNodeInsert = ({ animationName, target: container }) => {
  87. if (animationName !== ANIMATION_NAME) {
  88. return;
  89. }
  90.  
  91. const refButton = container.querySelector('.sc-button-more');
  92.  
  93. if (!refButton) {
  94. return;
  95. }
  96.  
  97. const button = createButton(container, refButton);
  98.  
  99. insertAfter(button, refButton);
  100. }
  101.  
  102. document.addEventListener('animationstart', onNodeInsert, false);