Advertisement
XT-8147

YouTube - Subscription Box Cleanup.user

Feb 29th, 2016
297
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name        YouTube - Subscription Box Cleanup
  3. // @namespace   youtube.com
  4. // @description Their more modern idea of a "grid view" is a tad messy.
  5. // @include     https://www.youtube.com/feed/subscriptions*
  6. // @version     1.2
  7. // @grant       none
  8. // ==/UserScript==
  9.  
  10. /* THINGS THIS SCRIPT DOES
  11.  *
  12.  * 1. Moves videos from the sections they're in, to the top-most section,
  13.  *    preserving chronological order.  Sections where we can't hide videos are
  14.  *    ignored.  If the top-most section is one of these sections, then this step
  15.  *    and step 2 are skipped entirely.
  16.  *
  17.  * 2. Removes any now-empty sections from the page.
  18.  *
  19.  * 3. Collapses all the sections where we can't hide videos, and provides a
  20.  *    button you can click to expand and collapse these sections at will.
  21.  *
  22.  * 4. Since more of the sections where we can't hide videos can be added after
  23.  *    the page loads, detects those sections being added and performs step 3 on
  24.  *    them.
  25.  *
  26.  * THINGS THIS SCRIPT DOES NOT DO
  27.  *
  28.  * 1. Anything nefarious.
  29.  *
  30.  * 2. Anything that would break page functionality, which would include removing
  31.  *    the sections that contain videos that we can't hide, or neutering the
  32.  *    "load more" functionality.
  33.  *
  34.  * 3. Anything to the List View.  Grid View Master Race!
  35.  *
  36.  * 4. Use any n00by jQuery.  All vanilla JavaScript, none of the cruft.
  37.  *
  38.  */
  39.  
  40. /* Version history
  41.  *
  42.  * Version   Date        What's up
  43.  *   1.0     2016-02-29  * Original version
  44.  *   1.1     2016-03-01  * Tweaked Expand/Collapse buttons so they look like the
  45.  *                         rest of YouTube's buttons
  46.  *   1.2     2016-03-14  * Moved some code around to make things more efficient
  47.  *                       * Inject some CSS so that toggling a collapsed section
  48.  *                         is far simpler
  49.  *                       * If the first section is one that should be extended
  50.  *                         with the Expand/Collapse button, do so.
  51.  */
  52.  
  53. // constants
  54. const stage1init = { characterData: true, attributes: true, childList: true, subtree: true };
  55. const stage2init = { characterData: false, attributes: false, childList: true, subtree: true };
  56.  
  57. // because cloneNode() takes a nondescript boolean that determines whether the
  58. // node is deep-copied (true) or shallow-copied (false)
  59. // and I'd like to be able to tell what my code is doing at a glance without
  60. // having to look up documentation on cloneNode().
  61. const DEEP_COPY = true;
  62.  
  63. const GRID_VIEW = true; // GRID_VIEW is true because it's the One True Way
  64. const LIST_VIEW = false; // LIST_VIEW is false because if you use it you suck
  65. // if I was an oop-monger I'd have a Views object with these as static properties
  66. // and a static method named detect
  67. // and you'd call Views.detect() and it would return either Views.GRID_VIEW or
  68. // Views.LIST_VIEW
  69. // but I'm not
  70. // so I don't
  71. // and it doesn't
  72.  
  73. // Selectors and classes and IDs, oh my!
  74. // jokes aside, if youtube changes things, this is where to start at
  75. // bitchslapping them for doing so
  76. // then you get the fun of inspecting elements to make sure there's the right
  77. // number of .parentNode and .children[0] in the chains found in fixVideos() (and toggleSection())
  78. const GRID_SELECTOR = 'li.yt-uix-menu-top-level-button [title="Grid"]';
  79. const LIST_SELECTOR = 'li.yt-uix-menu-top-level-button [title="List"]';
  80.  
  81. const SECTION_LIST_ID = 'browse-items-primary';
  82. const SECTION_CLASS_NAME = 'feed-item-container';
  83. const SECTION_TITLE_CLASS_NAME = 'branded-page-module-title-text';
  84. const SECTION_CONTENT_CLASS_NAME = 'shelf-content';
  85.  
  86. const COLLAPSE_SECTIONS = [ 'Older', 'More recent uploads', 'Most recent uploads' ];
  87.  
  88. // detects which view we're in, so the script can act accordingly
  89. function detectView() {
  90.     var gridViewButton = document.querySelector( GRID_SELECTOR );
  91.     var listViewButton = document.querySelector( LIST_SELECTOR );
  92.     // whichever button is disabled is the one whose mode we're in
  93.     if( gridViewButton.hasAttribute( 'disabled' ) ) {
  94.         return GRID_VIEW;
  95.     }
  96.     else if( listViewButton.hasAttribute( 'disabled' ) ) {
  97.         return LIST_VIEW;
  98.     }
  99.     else {
  100.         throw new Error( 'Mystery case in detectView() encountered!' );
  101.     }
  102. }
  103.  
  104. // this gets everything going by reacting to a document mutation
  105. // fortunately for us, the document mutates a lot during the page load
  106. // we only do anything if grid view is selected
  107. // we don't have to worry about redoing anything if the view is toggled to list
  108. // view and then back to grid view, because toggling the view triggers a page
  109. // load and runs the script again
  110. function stage1( record, observer ) {
  111.     var o, css;
  112.     // regardless of anything, we want to disconnect the observer now
  113.     observer.disconnect();
  114.     if( detectView() === GRID_VIEW ) {
  115.         fixVideos();
  116.         addExpandCollapse();
  117.         stage2.element = document.getElementById( SECTION_LIST_ID );
  118.         o = new MutationObserver( stage2 );
  119.         o.observe( stage2.element, stage2init );
  120.         // adding this CSS makes the expanding and collapsing of sections far simpler
  121.         css = document.createElement( 'style' );
  122.         css.setAttribute( 'type', 'text/css' );
  123.         css.textContent =  '.xt-extended .xt-expand-collapse-button .xt-expand-text { display: inline; }\n';
  124.         css.textContent += '.xt-extended .xt-expand-collapse-button .xt-collapse-text { display: none; }\n';
  125.         css.textContent += '.xt-extended .multirow-shelf { display: none; }\n';
  126.         css.textContent += '.xt-extended.xt-expanded .xt-expand-collapse-button .xt-expand-text { display: none; }\n';
  127.         css.textContent += '.xt-extended.xt-expanded .xt-expand-collapse-button .xt-collapse-text { display: inline; }\n';
  128.         css.textContent += '.xt-extended.xt-expanded .multirow-shelf { display: block; }';
  129.         document.head.appendChild( css );
  130.     }
  131. }
  132.  
  133. // this reacts to new sections being added (via the Load More button or
  134. // scrolling down), so we can collapse them by default
  135. function stage2( record, observer ) {
  136.     observer.disconnect();
  137.     addExpandCollapse();
  138.     observer.observe( stage2.element, stage2init );
  139. }
  140.  
  141. // meat and potatoes
  142. // collapses the sections containing videos you actually care about into one
  143. // section
  144. // removes the now-empty sections
  145. // adds expand/collapse button to sections whose names are in COLLAPSE_SECTIONS,
  146. // collapsing those sections by default
  147. function fixVideos() {
  148.     var sections = document.getElementsByClassName( SECTION_CLASS_NAME );
  149.     var target, current, sectionTitle, i, button;
  150.     var loadmore, emptynote, sectioncontent;
  151.     // hide top section name, regardless of anything.
  152.     sections[0].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0].style.display = 'none';
  153.     // determine if any moving of videos needs to happen
  154.     if( COLLAPSE_SECTIONS.indexOf( sections[0].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0].textContent ) == -1 ) {
  155.         target = sections[0].getElementsByClassName( SECTION_CONTENT_CLASS_NAME )[0];
  156.         for( i = 1; i < sections.length; i++ ) {
  157.             sectionTitle = sections[i].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0];
  158.             if( COLLAPSE_SECTIONS.indexOf( sectionTitle.textContent ) == -1 ) {
  159.                 // move video list to sections[0]
  160.                 current = sections[i].getElementsByClassName( SECTION_CONTENT_CLASS_NAME )[0];
  161.                 while( current.children.length > 0 ) {
  162.                     target.appendChild( current.children[0].cloneNode( DEEP_COPY ) );
  163.                     current.removeChild( current.children[0] );
  164.                 }
  165.             }
  166.         }
  167.         // remove empty sections
  168.         // sections[i] is actually deep within the actual element that encompasses the entire section, so we get to .parentNode our way to success
  169.         for( i = sections.length - 1; i > 0; i-- ) {
  170.             if( sections[i].getElementsByClassName( SECTION_CONTENT_CLASS_NAME )[0].children.length == 0 ) {
  171.                 // janky as shit
  172.                 sections[i].parentNode.parentNode.parentNode.parentNode.removeChild( sections[i].parentNode.parentNode.parentNode );
  173.             }
  174.         }
  175.     }
  176.     // *whistles innocently*
  177.     if( remove_irrelevant_sections || disable_load_more ) {
  178.         loadmore = document.querySelector( '#browse-items-primary .load-more-button' );
  179.         loadmore.setAttribute( 'disabled', 'disabled' );
  180.         loadmore.style.display = 'none';
  181.     }
  182.     sectioncontent = sections[0].getElementsByClassName( SECTION_CONTENT_CLASS_NAME )[0];
  183.     if( fake_empty_section ) {
  184.         if( ( sectioncontent.children.length == 0 ) || ( COLLAPSE_SECTIONS.indexOf( sections[0].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0].textContent ) != -1 ) ) {
  185.             emptynote = document.createElement( 'div' );
  186.             emptynote.textContent = 'Move along now, nothing to see here.';
  187.             emptynote.style.color = '#767676';
  188.             emptynote.style.padding = '5px';
  189.             emptynote.style.fontSize = '13.3333px';
  190.             emptynote.style.fontStyle = 'italic';
  191.             emptynote.style.textAlign = 'center';
  192.             emptynote.style.marginTop = '75.5px';
  193.             emptynote.style.marginBottom = '75.5px';
  194.             sectioncontent.parentNode.replaceChild( emptynote, sectioncontent );
  195.         }
  196.     }
  197. }
  198.  
  199. // add expand/collapse controls to sections whose names are in COLLAPSE_SECTIONS
  200. function addExpandCollapse() {
  201.     var sections = document.getElementsByClassName( SECTION_CLASS_NAME );
  202.     var i, sectionTitle, button, clonedButton, buttonText, expandText, collapseText;
  203.     // *whistles innocently*
  204.     if( remove_irrelevant_sections ) {
  205.         for( i = sections.length - 1; i > 0; i-- ) {
  206.             sectionTitle = sections[i].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0];
  207.             if( COLLAPSE_SECTIONS.indexOf( sectionTitle.textContent ) != -1 ) {
  208.                 sections[i].parentNode.parentNode.parentNode.parentNode.removeChild( sections[i].parentNode.parentNode.parentNode );
  209.             }
  210.         }
  211.     }
  212.     else {
  213.         // create the majority of the button now
  214.         button = document.createElement( 'button' );
  215.         button.setAttribute( 'type', 'button' );
  216.         // chuck some of YouTube's CSS classes on it so it looks like one of their buttons
  217.         button.classList.add( 'yt-uix-button' );
  218.         button.classList.add( 'yt-uix-button-size-default' );
  219.         button.classList.add( 'yt-uix-button-default' );
  220.         button.classList.add( 'xt-expand-collapse-button' );
  221.         // let's also handle the button content the way they do, where all the text is
  222.         // there and we just toggle which one is actually displayed via CSS
  223.         buttonText = document.createElement( 'span' );
  224.         buttonText.classList.add( 'yt-uix-button-content' );
  225.         expandText = document.createElement( 'span' );
  226.         expandText.classList.add( 'xt-expand-text' );
  227.         expandText.textContent = 'Expand';
  228.         collapseText = document.createElement( 'span' );
  229.         collapseText.classList.add( 'xt-collapse-text' );
  230.         collapseText.textContent = 'Collapse';
  231.         buttonText.appendChild( expandText );
  232.         buttonText.appendChild( collapseText );
  233.         button.appendChild( buttonText );
  234.         button.style.marginLeft = '1em';
  235.         delete expandText;
  236.         delete collapseText;
  237.         delete buttonText;
  238.         for( i = 0; i < sections.length; i++ ) {
  239.             sectionTitle = sections[i].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0];
  240.             if( ( COLLAPSE_SECTIONS.indexOf( sectionTitle.textContent ) != -1 ) && ( !sections[i].parentNode.classList.contains( 'xt-extended' ) ) ) {
  241.                 clonedButton = button.cloneNode( DEEP_COPY );
  242.                 clonedButton.addEventListener( 'click', toggleSection, false );
  243.                 sections[i].children[0].children[0].children[0].children[0].appendChild( clonedButton ); // adds the expand/collapse button next to the section title
  244.                 sections[i].parentNode.classList.add( 'xt-extended' ); // this makes it so our code won't try to re-extend the same section
  245.                 delete clonedButton;
  246.             }
  247.         }
  248.     }
  249. }
  250.  
  251. // toggles one of the sections whose name is in COLLAPSE_SECTIONS between its
  252. // collapsed and expanded states
  253. // all the chained .parentNode stuff is janky as hell, but it's the only way
  254. function toggleSection() {
  255.     // all we need to do is toggle a class and CSS handles the rest
  256.     this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.classList.toggle( 'xt-expanded' );
  257. }
  258.  
  259. // set up the observer to run stage1() and trigger the entire thing
  260. var o = new MutationObserver( stage1 );
  261. o.observe( document.body, stage1init );
  262.  
  263. /* Reward for those who scroll down this far!
  264.  * These settings should be fairly self explanatory.
  265.  * Though, behavior-wise, setting remove_irrelevant_sections to true implies
  266.  * disable_load_more.  fake_empty_section might become standard functionality in
  267.  * a later version.  Since these are either intended to break page
  268.  * functionality, or can do so as a side effect, they aren't exposed to the
  269.  * average user.  If you're using any of them and notice wonky behavior, set
  270.  * whichever ones you're using back to false and see if it clears up.
  271.  */
  272. const disable_load_more = false;
  273. const remove_irrelevant_sections = false;
  274. const fake_empty_section = false;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement