raingart / YouTube SuperNova

// ==UserScript==
// @version      0.5.1
// @name         YouTube SuperNova
// @namespace    https://github.com/raingart/Nova-YouTube-extension/
// @description  Youtube extension plugins
// @include      https://*.youtube.com/*
// @include      https://*.youtube-nocookie.com/*
// @include      https://raingart.github.io/options.html
// @exclude      https://www.youtube.com/html5
// @exclude      https://www.youtube.com/*/*.xml*
// @author       raingart
// @homepageURL  https://github.com/raingart/Nova-YouTube-extension
// @supportURL   https://openuserjs.org/scripts/raingart/YouTube_SuperNova/issues
// @icon         https://raw.github.com/raingart/Nova-YouTube-extension/master/icons/48.png
// @license      AGPL-3.0-or-later
// @run-at       document-start
// @require      https://gist.githubusercontent.com/raingart/b863b65347b1674a87daeb822c511adf/raw/plugins_init.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/js/plugins.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/ytc_lib.js
// @resource     https://gist.githubusercontent.com/raingart/ff6711fafbc46e5646d4d251a79d1118/raw/youtube_api_keys.json

// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/ad-skip-button.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/speed.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/volume.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/hud.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/quality.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/pause.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/theater-mode.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/tab-pause.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/focused.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/pin.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/time-jump.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/remaining-time.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/player/fly-progress-bar.js

// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/other/scroll-to-top.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/other/rating-bars.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/other/normalize-video-title.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/other/thumbnail-clear.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/other/channel-tab.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/other/clear-redirect.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/other/wake-up.js

// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/details/expand-description.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/details/channel-video-count.js

// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/comments/disable-comments.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/comments/expand-comments.js

// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/sidebar/playlist-duration.js
// @require      https://raw.github.com/raingart/Nova-YouTube-extension/master/plugins/sidebar/livechat-hide.js
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_getResourceURL
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        GM_listValues
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @compatible   Chrome >=80 + Violentmonkey
// ==/UserScript==
/*jshint esversion: 6 */

(() => {
   console.log('%c /* %s */', 'color: #0096fa; font-weight: bold;', GM_info.script.name + ' v.' + GM_info.script.version);
   const configStoreName = 'user_settings';
   const user_settings = GM_getValue(configStoreName);
   
   if (!isOptionsPage()) return;
   landerPlugins();
   reflectException();
   
   // ======
   function isOptionsPage() {
      const optionsPage = 'https://raingart.github.io/options.html';
   
      GM_registerMenuCommand('Settings', () => window.open(optionsPage));
      GM_registerMenuCommand('Export settings', () => {
         let d = document.createElement('a');
         d.style.display = 'none';
         d.setAttribute('download', 'nova_settings.json');
         d.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(user_settings)));
         document.body.appendChild(d);
         d.click();
         document.body.removeChild(d);
      });
      GM_registerMenuCommand('Import settings', () => {
         let f = document.createElement('input');
         f.type = 'file';
         f.style.display = 'none';
         f.addEventListener('change', function () {
            if (f.files.length !== 1) return;
            let rdr = new FileReader();
            rdr.addEventListener('load', function () {
               try {
                  GM_setValue(configStoreName, JSON.parse(rdr.result));
                  alert('Settings imported');
                  document.location.reload();
               }
               catch (ex) { alert('Error parsing settings\n' + ex); }
            });
            rdr.addEventListener('error', () => alert('Error loading file\n' + rdr.error));
            rdr.readAsText(f.files[0]);
         });
         document.body.appendChild(f);
         f.click();
         document.body.removeChild(f);
      });
   
      if (location.href === optionsPage) {
         // form submit
         document.addEventListener('submit', event => {
            // console.debug('submit', event.target);
            event.preventDefault();
   
            let obj = {};
            new FormData(event.target)
               .forEach((value, key) => {
                  // SerializedArray
                  if (obj.hasOwnProperty(key)) {
                     // adding another val
                     obj[key] += ';' + value; // add new
                     obj[key] = obj[key].split(';'); // to key = [old, new]
                  }
                  else obj[key] = value;
               });
            obj;
   
            console.debug(`update ${configStoreName}:`, obj);
            GM_setValue(configStoreName, obj);
         });
   
         window.addEventListener('load', () => {
            let interval_pagesync = setInterval(() => {
               //if (document.body.classList.contains("preload")) return console.debug('page loading..');
               if (!document.querySelector("[data-dependent]")) return console.debug('page loading..');
               clearTimeout(interval_pagesync);
   
               PopulateForm.fill(user_settings); // fill form
               attrDependencies();
               document.body.classList.remove("preload");
            });
         });
   
         function attrDependencies() {
            document.querySelectorAll("[data-dependent]")
               .forEach(dependentItem => {
                  const dependentsJson = JSON.parse(dependentItem.getAttribute('data-dependent').toString());
                  const handler = () => showOrHide(dependentItem, dependentsJson);
                  handler(); // init state
   
                  const dependentTag = document.getElementById(Object.keys(dependentsJson))
                  if (dependentTag) dependentTag.addEventListener("change", handler);
               });
   
            function showOrHide(dependentItem, dependentsList) {
               // console.debug('showOrHide', ...arguments);
               for (const name in dependentsList) {
                  const reqParent = document.getElementsByName(name)[0];
                  if (!reqParent) return console.error('error showOrHide:', name);
   
                  for (const values of [dependentsList[name]]) {
                     // console.debug('check', name, reqParent.value + '=' + values);
                     if ((reqParent.checked && values) || values.includes(reqParent.value)) {
                        // console.debug('show:', name);
                        dependentItem.classList.remove("hide");
   
                     } else {
                        // console.debug('hide:', name);
                        dependentItem.classList.add("hide");
                     }
                  }
               }
            }
         }
   
      } else if (!user_settings || !Object.keys(user_settings).length) {
         if (confirm('Active plugins undetected. Open the settings page?')) window.open(optionsPage);
   
      } else return true; // is not optionsPage
   }
   
   // ======
   function landerPlugins() {
      const plugins_count = Plugins.list.length;
   
      let forceLander = setTimeout(() => {
         console.debug('force lander:', _plugins_conteiner.length + '/' + plugins_count);
         processLander();
      }, 1000 * 5); // 5sec
   
      let plugins_lander = setInterval(() => {
         const domLoaded = document?.readyState !== 'loading';
   
         if (!domLoaded || document.querySelectorAll("#progress[style*=transition-duration], yt-page-navigation-progress:not([hidden])").length) {
            return console.debug('waiting, page loading..');
         }
   
         if (YDOM && _plugins_conteiner.length === plugins_count) {
            clearTimeout(forceLander);
            processLander();
         }
         else console.debug('loading:', _plugins_conteiner.length + '/' + plugins_count);
   
      }, 100); // 100ms
   
      function processLander() {
         console.groupCollapsed('plugins status');
         console.debug('loaded:', _plugins_conteiner.length + '/' + plugins_count);
         clearInterval(plugins_lander);
   
         //setTimeout(() => {
         Plugins.run({
            'user_settings': user_settings,
            'app_ver': GM_info.script.version,
         });
         //}, 300);
      }
   
      let lastUrl = location.href;
      const isChangeUrl = () => lastUrl == location.href ? false : lastUrl = location.href;
      // skip first run on page transition
      document.addEventListener('yt-navigate-start', () => isChangeUrl() && landerPlugins());
   }
   
   // ======
   function reflectException() {
      function _pluginsCaptureException({ trace_name, err_stack, confirm_msg, app_ver }) {
         if (confirm(confirm_msg || 'Error in Nova YouTube™. Open popup to report the bug?')) {
            window.open(
               'https://docs.google.com/forms/u/0/d/e/1FAIpQLScfpAvLoqWlD5fO3g-fRmj4aCeJP9ZkdzarWB8ge8oLpE5Cpg/viewform' +
               '?entry.35504208=' + encodeURIComponent(trace_name) +
               '&entry.151125768=' + encodeURIComponent(err_stack) +
               '&entry.744404568=' + encodeURIComponent(location.href) +
               '&entry.1416921320=' + encodeURIComponent(app_ver + ' | ' + navigator.userAgent), '_blank');
         }
      };
   
      window.addEventListener('unhandledrejection', err => {
         //if (!err.reason.stack.toString().includes(${JSON.stringify(chrome.runtime.id)})) return;
         console.error('[ERROR PROMISE]\n', err.reason, '\nPlease report the bug: https://github.com/raingart/Nova-YouTube-extension/issues/new/choose');
   
         if (user_settings.report_issues)
            _pluginsCaptureException({
               'trace_name': 'unhandledrejection',
               'err_stack': err.reason.stack,
               'app_ver': GM_info.script.version,
               'confirm_msg': 'Failure when async-call of one "Nova YouTube™" plugin.\n\nOpen tab to report the bug?',
            });
      });
   }
   
   })();