Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name YouTube - Subscription Box Cleanup
- // @namespace youtube.com
- // @description Their more modern idea of a "grid view" is a tad messy.
- // @include https://www.youtube.com/feed/subscriptions*
- // @version 1.2
- // @grant none
- // ==/UserScript==
- /* THINGS THIS SCRIPT DOES
- *
- * 1. Moves videos from the sections they're in, to the top-most section,
- * preserving chronological order. Sections where we can't hide videos are
- * ignored. If the top-most section is one of these sections, then this step
- * and step 2 are skipped entirely.
- *
- * 2. Removes any now-empty sections from the page.
- *
- * 3. Collapses all the sections where we can't hide videos, and provides a
- * button you can click to expand and collapse these sections at will.
- *
- * 4. Since more of the sections where we can't hide videos can be added after
- * the page loads, detects those sections being added and performs step 3 on
- * them.
- *
- * THINGS THIS SCRIPT DOES NOT DO
- *
- * 1. Anything nefarious.
- *
- * 2. Anything that would break page functionality, which would include removing
- * the sections that contain videos that we can't hide, or neutering the
- * "load more" functionality.
- *
- * 3. Anything to the List View. Grid View Master Race!
- *
- * 4. Use any n00by jQuery. All vanilla JavaScript, none of the cruft.
- *
- */
- /* Version history
- *
- * Version Date What's up
- * 1.0 2016-02-29 * Original version
- * 1.1 2016-03-01 * Tweaked Expand/Collapse buttons so they look like the
- * rest of YouTube's buttons
- * 1.2 2016-03-14 * Moved some code around to make things more efficient
- * * Inject some CSS so that toggling a collapsed section
- * is far simpler
- * * If the first section is one that should be extended
- * with the Expand/Collapse button, do so.
- */
- // constants
- const stage1init = { characterData: true, attributes: true, childList: true, subtree: true };
- const stage2init = { characterData: false, attributes: false, childList: true, subtree: true };
- // because cloneNode() takes a nondescript boolean that determines whether the
- // node is deep-copied (true) or shallow-copied (false)
- // and I'd like to be able to tell what my code is doing at a glance without
- // having to look up documentation on cloneNode().
- const DEEP_COPY = true;
- const GRID_VIEW = true; // GRID_VIEW is true because it's the One True Way
- const LIST_VIEW = false; // LIST_VIEW is false because if you use it you suck
- // if I was an oop-monger I'd have a Views object with these as static properties
- // and a static method named detect
- // and you'd call Views.detect() and it would return either Views.GRID_VIEW or
- // Views.LIST_VIEW
- // but I'm not
- // so I don't
- // and it doesn't
- // Selectors and classes and IDs, oh my!
- // jokes aside, if youtube changes things, this is where to start at
- // bitchslapping them for doing so
- // then you get the fun of inspecting elements to make sure there's the right
- // number of .parentNode and .children[0] in the chains found in fixVideos() (and toggleSection())
- const GRID_SELECTOR = 'li.yt-uix-menu-top-level-button [title="Grid"]';
- const LIST_SELECTOR = 'li.yt-uix-menu-top-level-button [title="List"]';
- const SECTION_LIST_ID = 'browse-items-primary';
- const SECTION_CLASS_NAME = 'feed-item-container';
- const SECTION_TITLE_CLASS_NAME = 'branded-page-module-title-text';
- const SECTION_CONTENT_CLASS_NAME = 'shelf-content';
- const COLLAPSE_SECTIONS = [ 'Older', 'More recent uploads', 'Most recent uploads' ];
- // detects which view we're in, so the script can act accordingly
- function detectView() {
- var gridViewButton = document.querySelector( GRID_SELECTOR );
- var listViewButton = document.querySelector( LIST_SELECTOR );
- // whichever button is disabled is the one whose mode we're in
- if( gridViewButton.hasAttribute( 'disabled' ) ) {
- return GRID_VIEW;
- }
- else if( listViewButton.hasAttribute( 'disabled' ) ) {
- return LIST_VIEW;
- }
- else {
- throw new Error( 'Mystery case in detectView() encountered!' );
- }
- }
- // this gets everything going by reacting to a document mutation
- // fortunately for us, the document mutates a lot during the page load
- // we only do anything if grid view is selected
- // we don't have to worry about redoing anything if the view is toggled to list
- // view and then back to grid view, because toggling the view triggers a page
- // load and runs the script again
- function stage1( record, observer ) {
- var o, css;
- // regardless of anything, we want to disconnect the observer now
- observer.disconnect();
- if( detectView() === GRID_VIEW ) {
- fixVideos();
- addExpandCollapse();
- stage2.element = document.getElementById( SECTION_LIST_ID );
- o = new MutationObserver( stage2 );
- o.observe( stage2.element, stage2init );
- // adding this CSS makes the expanding and collapsing of sections far simpler
- css = document.createElement( 'style' );
- css.setAttribute( 'type', 'text/css' );
- css.textContent = '.xt-extended .xt-expand-collapse-button .xt-expand-text { display: inline; }\n';
- css.textContent += '.xt-extended .xt-expand-collapse-button .xt-collapse-text { display: none; }\n';
- css.textContent += '.xt-extended .multirow-shelf { display: none; }\n';
- css.textContent += '.xt-extended.xt-expanded .xt-expand-collapse-button .xt-expand-text { display: none; }\n';
- css.textContent += '.xt-extended.xt-expanded .xt-expand-collapse-button .xt-collapse-text { display: inline; }\n';
- css.textContent += '.xt-extended.xt-expanded .multirow-shelf { display: block; }';
- document.head.appendChild( css );
- }
- }
- // this reacts to new sections being added (via the Load More button or
- // scrolling down), so we can collapse them by default
- function stage2( record, observer ) {
- observer.disconnect();
- addExpandCollapse();
- observer.observe( stage2.element, stage2init );
- }
- // meat and potatoes
- // collapses the sections containing videos you actually care about into one
- // section
- // removes the now-empty sections
- // adds expand/collapse button to sections whose names are in COLLAPSE_SECTIONS,
- // collapsing those sections by default
- function fixVideos() {
- var sections = document.getElementsByClassName( SECTION_CLASS_NAME );
- var target, current, sectionTitle, i, button;
- var loadmore, emptynote, sectioncontent;
- // hide top section name, regardless of anything.
- sections[0].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0].style.display = 'none';
- // determine if any moving of videos needs to happen
- if( COLLAPSE_SECTIONS.indexOf( sections[0].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0].textContent ) == -1 ) {
- target = sections[0].getElementsByClassName( SECTION_CONTENT_CLASS_NAME )[0];
- for( i = 1; i < sections.length; i++ ) {
- sectionTitle = sections[i].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0];
- if( COLLAPSE_SECTIONS.indexOf( sectionTitle.textContent ) == -1 ) {
- // move video list to sections[0]
- current = sections[i].getElementsByClassName( SECTION_CONTENT_CLASS_NAME )[0];
- while( current.children.length > 0 ) {
- target.appendChild( current.children[0].cloneNode( DEEP_COPY ) );
- current.removeChild( current.children[0] );
- }
- }
- }
- // remove empty sections
- // sections[i] is actually deep within the actual element that encompasses the entire section, so we get to .parentNode our way to success
- for( i = sections.length - 1; i > 0; i-- ) {
- if( sections[i].getElementsByClassName( SECTION_CONTENT_CLASS_NAME )[0].children.length == 0 ) {
- // janky as shit
- sections[i].parentNode.parentNode.parentNode.parentNode.removeChild( sections[i].parentNode.parentNode.parentNode );
- }
- }
- }
- // *whistles innocently*
- if( remove_irrelevant_sections || disable_load_more ) {
- loadmore = document.querySelector( '#browse-items-primary .load-more-button' );
- loadmore.setAttribute( 'disabled', 'disabled' );
- loadmore.style.display = 'none';
- }
- sectioncontent = sections[0].getElementsByClassName( SECTION_CONTENT_CLASS_NAME )[0];
- if( fake_empty_section ) {
- if( ( sectioncontent.children.length == 0 ) || ( COLLAPSE_SECTIONS.indexOf( sections[0].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0].textContent ) != -1 ) ) {
- emptynote = document.createElement( 'div' );
- emptynote.textContent = 'Move along now, nothing to see here.';
- emptynote.style.color = '#767676';
- emptynote.style.padding = '5px';
- emptynote.style.fontSize = '13.3333px';
- emptynote.style.fontStyle = 'italic';
- emptynote.style.textAlign = 'center';
- emptynote.style.marginTop = '75.5px';
- emptynote.style.marginBottom = '75.5px';
- sectioncontent.parentNode.replaceChild( emptynote, sectioncontent );
- }
- }
- }
- // add expand/collapse controls to sections whose names are in COLLAPSE_SECTIONS
- function addExpandCollapse() {
- var sections = document.getElementsByClassName( SECTION_CLASS_NAME );
- var i, sectionTitle, button, clonedButton, buttonText, expandText, collapseText;
- // *whistles innocently*
- if( remove_irrelevant_sections ) {
- for( i = sections.length - 1; i > 0; i-- ) {
- sectionTitle = sections[i].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0];
- if( COLLAPSE_SECTIONS.indexOf( sectionTitle.textContent ) != -1 ) {
- sections[i].parentNode.parentNode.parentNode.parentNode.removeChild( sections[i].parentNode.parentNode.parentNode );
- }
- }
- }
- else {
- // create the majority of the button now
- button = document.createElement( 'button' );
- button.setAttribute( 'type', 'button' );
- // chuck some of YouTube's CSS classes on it so it looks like one of their buttons
- button.classList.add( 'yt-uix-button' );
- button.classList.add( 'yt-uix-button-size-default' );
- button.classList.add( 'yt-uix-button-default' );
- button.classList.add( 'xt-expand-collapse-button' );
- // let's also handle the button content the way they do, where all the text is
- // there and we just toggle which one is actually displayed via CSS
- buttonText = document.createElement( 'span' );
- buttonText.classList.add( 'yt-uix-button-content' );
- expandText = document.createElement( 'span' );
- expandText.classList.add( 'xt-expand-text' );
- expandText.textContent = 'Expand';
- collapseText = document.createElement( 'span' );
- collapseText.classList.add( 'xt-collapse-text' );
- collapseText.textContent = 'Collapse';
- buttonText.appendChild( expandText );
- buttonText.appendChild( collapseText );
- button.appendChild( buttonText );
- button.style.marginLeft = '1em';
- delete expandText;
- delete collapseText;
- delete buttonText;
- for( i = 0; i < sections.length; i++ ) {
- sectionTitle = sections[i].getElementsByClassName( SECTION_TITLE_CLASS_NAME )[0];
- if( ( COLLAPSE_SECTIONS.indexOf( sectionTitle.textContent ) != -1 ) && ( !sections[i].parentNode.classList.contains( 'xt-extended' ) ) ) {
- clonedButton = button.cloneNode( DEEP_COPY );
- clonedButton.addEventListener( 'click', toggleSection, false );
- sections[i].children[0].children[0].children[0].children[0].appendChild( clonedButton ); // adds the expand/collapse button next to the section title
- sections[i].parentNode.classList.add( 'xt-extended' ); // this makes it so our code won't try to re-extend the same section
- delete clonedButton;
- }
- }
- }
- }
- // toggles one of the sections whose name is in COLLAPSE_SECTIONS between its
- // collapsed and expanded states
- // all the chained .parentNode stuff is janky as hell, but it's the only way
- function toggleSection() {
- // all we need to do is toggle a class and CSS handles the rest
- this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.classList.toggle( 'xt-expanded' );
- }
- // set up the observer to run stage1() and trigger the entire thing
- var o = new MutationObserver( stage1 );
- o.observe( document.body, stage1init );
- /* Reward for those who scroll down this far!
- * These settings should be fairly self explanatory.
- * Though, behavior-wise, setting remove_irrelevant_sections to true implies
- * disable_load_more. fake_empty_section might become standard functionality in
- * a later version. Since these are either intended to break page
- * functionality, or can do so as a side effect, they aren't exposed to the
- * average user. If you're using any of them and notice wonky behavior, set
- * whichever ones you're using back to false and see if it clears up.
- */
- const disable_load_more = false;
- const remove_irrelevant_sections = false;
- const fake_empty_section = false;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement