Greasy Fork

Greasy Fork is available in English.

KissCleaner

Cleans up KissAnime pages. Tested to work with Firefox and Greasemonkey.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            KissCleaner
// @namespace       juici.github.io
// @description     Cleans up KissAnime pages. Tested to work with Firefox and Greasemonkey.
// @author          Juici, crapier
// @version         1.5.2
// @license         https://github.com/Juici/KissCleaner/blob/master/LICENSE
// @icon            https://juici.github.io/KissCleaner/icon.png
// @homepage        https://github.com/Juici/KissCleaner
// @contactURL      https://github.com/Juici/KissCleaner/issues
// @supportURL      https://github.com/Juici/KissCleaner/issues
// @contributionURL https://github.com/Juici/KissCleaner#donate
// @include         /^https?:\/\/kiss(?:anime\.(?:to|ru|ac)|cartoon\.(?:me|se|ac)|asian\.(?:com|se))(?:\/.*|)/
// @grant           unsafeWindow
// @grant           GM_addStyle
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_registerMenuCommand
// @grant           GM_getResourceText
// @resource        settings https://juici.github.io/KissCleaner/settings.html?v=1
// @resource        css https://juici.github.io/KissCleaner/style.css?v=4
// @resource        resizeVideo https://juici.github.io/KissCleaner/resize-video.css?v=1
// @run-at          document-start
// @noframes
// ==/UserScript==

// current page url
const url = window.location.href;
// regex to check against for determining what type page currently on and what to clean
const rHome = /https?:\/\/(kiss(?:anime\.(?:to|ru|ac)|cartoon\.(?:me|se|ac)|asian\.(?:com|se)))\/$/;
const rAnimeList = /https?:\/\/(kiss(?:anime\.(?:to|ru|ac)|cartoon\.(?:me|se|ac)|asian\.(?:com|se)))\/(AnimeList|Genre|Status|Search|UpcomingAnime|CartoonList|DramaList|Country)/;
const rAnimePage = /https?:\/\/(kiss(?:anime\.(?:to|ru|ac)|cartoon\.(?:me|se|ac)|asian\.(?:com|se)))\/(Anime|Cartoon|Drama)\/[^/]*$/;
const rVideoPage = /https?:\/\/(kiss(?:anime\.(?:to|ru|ac)|cartoon\.(?:me|se|ac)|asian\.(?:com|se)))\/(Anime|Cartoon|Drama)\/[^/]*\/[^/]*(?:\?id=\d*)?/;

// player type constants
const PLAYER = {
  FLASH: 'flash',
  HTML5: 'html5'
};

// site type
const SITE = {
  ANIME: false,
  CARTOON: false,
  ASIAN: false
};
const kissSite = /:\/\/kiss([^.]+)\./.exec(window.location.href)[1].toUpperCase();
SITE[kissSite] = true;

// settings
const settings = {
  // auto pause on video load
  autoPause: GM_getValue('autoPause', false),
  // auto advance to next video on playback end
  autoAdvance: GM_getValue('autoAdvance', true),
  // auto scroll to video area
  autoScroll: GM_getValue('autoScroll', true),

  // resize the video
  resizeVideo: GM_getValue('resizeVideo', true),

  // remove login
  removeLogin: GM_getValue('removeLogin', true),
  // remove comments area
  removeComments: GM_getValue('removeComments', true),

  // video player
  player: GM_getValue('player', PLAYER.HTML5),
  // video quality
  quality: GM_getValue('quality', '1080'),
  // video volume
  volume: GM_getValue('volume', 100)
};

const _ = {
  // document query returning array
  queryAll: function (...selector) {
    return Array.from(document.querySelectorAll(selector instanceof Array ? selector.join(',') : selector));
  },
  // remove element matching css selector (first or index)
  removeAd: function (selector, index) {
    const ads = _.queryAll(selector);

    // make sure index is within bounds
    index = index || 0;
    if (index < 0 || index > ads.length - 1) {
      index = 0;
    }

    ads[index].remove();
  },
  // remove all elements matching css selectors
  removeAds: function (...selectors) {
    const ads = _.queryAll(selectors);
    ads.forEach(elt => elt.remove());
  },
  // clean up the empty space left by ad removal
  cleanupAdspace: function () {
    // get the ads frame
    const adspace = document.getElementById('adsIfrme1');
    if (adspace) {
      // check and remove the clear before the adspace
      const clearBefore = adspace.parentElement.previousElementSibling;
      if (clearBefore && clearBefore.matches('.clear')) {
        clearBefore.remove();
      }
      // check and remove the clear a bit after the adspace
      const clearAfter = adspace.parentElement.nextElementSibling && adspace.parentElement.nextElementSibling.nextElementSibling &&
        adspace.parentElement.nextElementSibling.nextElementSibling.nextElementSibling ? adspace.parentElement.nextElementSibling.nextElementSibling.nextElementSibling : null;
      if (clearAfter && clearAfter.matches('.clear')) {
        clearAfter.remove();
      }
      // remove the adspace's parent (and thus it)
      adspace.parentElement.remove();
    }
  },
  // remove or hide stubborn ads
  hideAds: function () {
    let count = 0;
    const adremover = setInterval(() => {
      count++;

      // remove extra elements after #containerRoot in body
      const rootAds = _.queryAll('#containerRoot ~ *');
      rootAds.forEach(elt => elt.remove());

      // hide elements after #container in #containerRoot
      const containerAds = _.queryAll('#container ~ *:not(#kisscleaner-settings-container)');
      containerAds.forEach(elt => _.hideElement(elt, true));

      // hide elements in the #rightside that aren't content
      const rightsideAds = _.queryAll('#rightside > div:not(.rightBox):not(.clear):not(.clear2)');
      rightsideAds.forEach(elt => _.hideElement(elt, true));

      if (count === 50) {
        clearInterval(adremover);
      }
    }, 100);
  },
  // inject javascript into page
  injectScript: function (js) {
    // create script to inject
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.innerHTML = (typeof js === 'function' ? `(${js.toString()})();` : js);
    // inject the script
    document.head.appendChild(script);
  },
  // hide an element, soft param doesn't use 'display: none;' instead 'visibility: hidden; height: 0; width: 0;'
  hideElement: function (element, soft) {
    if (soft === true) {
      element.style.setProperty('visibility', 'hidden', 'important');
      element.style.setProperty('height', '0', 'important');
      element.style.setProperty('width', '0', 'important');
    } else {
      element.style.setProperty('display', 'none', 'important');
    }
  },
  // navigate to previous video
  previousVideo: function () {
    const btnPrevious = document.getElementById('btnPrevious');
    if (btnPrevious) {
      window.location.href = btnPrevious.parentElement.href;
    }
  },
  // navigate to next video
  nextVideo: function () {
    const btnNext = document.getElementById('btnNext');
    if (btnNext) {
      window.location.href = btnNext.parentElement.href;
    }
  },
  // check if should advance to next video
  checkAutoAdvance: function (evt) {
    settings.autoAdvance && _.nextVideo();
  }
};

// add custom css
GM_addStyle(GM_getResourceText('css'));

const clean = function () {
  // pre init checks
  if (document.querySelector('.cf-browser-verification')) {
    // cloudflare browser verification
    console.log('Waiting for CloudFlare browser verification.');
    return;
  } else if (!document.getElementById('containerRoot')) {
    // some error with page
    console.log('Something went wrong loading page. Reloading to fix it...');
    setTimeout(() => {
      window.location.href = window.location.href;
    }, 500);
    return;
  }

  console.log('Initializing KissCleaner...');

  //---------------------------------------------------------------------------------------------------------------
  // Clean Home page
  //---------------------------------------------------------------------------------------------------------------
  if (rHome.test(url)) {
    console.log('Cleaning home page...');

    // remove sections from the right side of the page
    const rightsideSearch = [/remove ads/i, /like me please/i];
    const rightside = document.getElementById('rightside');
    if (rightside) {
      for (let i = rightside.children.length - 1; i >= 0; i--) {
        const child = rightside.children[i];
        // if child has children
        if (child.children.length > 0) {
          // check search patterns
          let remove = false;
          for (const pattern of rightsideSearch) {
            if (child.children[0].textContent.search(pattern) > 0) {
              remove = true;
              break;
            }
          }

          // remove because a pattern matched and remove following .clear2 (if exists)
          if (remove) {
            const clear2 = child.nextElementSibling;
            if (clear2 && clear2.matches('.clear2')) {
              clear2.remove();
            }
            child.remove();
          }
        }
      }
    }

    // remove register link in nav sub bar
    const navsub = document.querySelector('#navsubbar > p');
    if (navsub) {
      navsub.children[0].remove();
      navsub.childNodes[1].remove();
    }

    // remove ads
    _.removeAds('#divFloatLeft', '#divFloatRight', '#divAds2', '#divAds', '#adsIfrme1');

    // remove or hide stubborn ads
    _.hideAds();

    console.log('Finished cleaning home page.');
  }

  //---------------------------------------------------------------------------------------------------------------
  // Clean Anime List Pages
  //---------------------------------------------------------------------------------------------------------------
  if (rAnimeList.test(url)) {
    console.log('Cleaning anime list page...');

    // remove large spaces left by empty adspace
    _.cleanupAdspace();

    // remove ads
    _.removeAds('#divFloatLeft', '#divFloatRight', '#adsIfrme2');

    // remove or hide stubborn ads
    _.hideAds();

    console.log('Finished cleaning anime list page.');
  }

  //---------------------------------------------------------------------------------------------------------------
  // Clean Episode List Pages
  //---------------------------------------------------------------------------------------------------------------
  if (rAnimePage.test(url)) {
    console.log('Cleaning episode list page...');

    // remove large spaces left by empty adspace
    _.cleanupAdspace();

    // remove ads
    // remove bookmarks
    _.removeAds('#divFloatLeft', '#divFloatRight', '#divAds', '#spanBookmark');

    // remove fluff from episode list pages
    const eplist = document.querySelector('div.barContent.episodeList > div:not(.arrow-general)');
    if (eplist) {
      let countdown = document.querySelector('#nextEpisodeCountDown');
      if (countdown) {
        countdown = countdown.parentElement.parentElement;

        // remove clear before listing
        const clear = countdown.nextElementSibling;
        if (clear && clear.matches('.clear')) {
          clear.remove();
        }
      }

      // loop remove element if not listing or element containing counting
      let child;
      while (eplist.children.length > 0 && !((child = eplist.children[0]).matches('.listing') || (typeof countdown !== 'undefined' && countdown !== null && child === countdown))) {
        child.remove();
      }
    }

    // remove comments (different child location for kissasian)
    settings.removeComments && _.removeAd('div.bigBarContainer', SITE.ASIAN ? 3 : 2);

    // remove or hide stubborn ads
    _.hideAds();

    console.log('Finished cleaning episode list page.');
  }

  //---------------------------------------------------------------------------------------------------------------
  // Clean Video Page
  //---------------------------------------------------------------------------------------------------------------
  if (rVideoPage.test(url)) {
    console.log('Cleaning video page...');

    // override functions so they wont be do anything when called by the pages code
    _.injectScript(`
      isBlockAds2 = false;
      DoDetect2 = function () {};
      CheckAdImage = function () {};
      xaZlE = function () {};
    `);
    // doesn't appear to be randomly generated function name (leaving this here for now)
    // get around new anti-adblock on kisscartoon.se
    //const aab = document.querySelector('#container ~ iframe[src="/ucs.aspx"] ~ script');
    //if (aab) {
    //  const aabFn = aab.textContent.replace(/.*function (.*?)\(\).*/, '$1').trim();
    //  _.injectScript(`
    //    ${aabFn} = function () {};
    //  `);
    //}
    console.log('Overridden anti-adblock detects.');

    // remove ad spaces
    // remove lights off feature (pointless with ads removed)
    _.removeAds('#divFloatLeft', '#divFloatRight', '#adsIfrme6', '#adsIfrme7', '#adsIfrme8', '#adsIfrme10', '#adsIfrme11', '#adCheck1', '#adCheck2', '#adCheck3', '#divDownload', '#divFileName', '#switch', '#divTextQua', '#videoAd');
    _.queryAll(`iframe[src*="${window.location.hostname}/Ads"]:not(#videoAd)`).forEach(e => e.parentElement.remove());

    // hide ads
    _.hideAds();

    // remove empty spaces from video pages
    // remove clears
    const vid = document.getElementById('centerDivVideo');
    const vidParent = vid.parentElement;
    for (let i = 0; i < vidParent.children.length; i++) {
      if (vidParent.children[i].matches('.clear, .clear2')) {
        vidParent.removeChild(vidParent.children[i--]);
      }
    }

    // remove comments area
    if (settings.removeComments) {
      // get the comment section on the video pages
      let comments = document.getElementById('btnShowComments');
      comments = (comments ? comments.parentElement : document.getElementById('divComments'));
      // remove comments and elements just prior
      if (comments) {
        let temp;
        while ((temp = comments.previousElementSibling) && !temp.matches('iframe, script')) {
          temp.remove();
        }
        while ((temp = comments.nextElementSibling) && !temp.matches('iframe, script')) {
          temp.remove();
        }
        comments.remove();
      }
    }

    if (settings.resizeVideo) {
      GM_addStyle(GM_getResourceText('resizeVideo'));
    }

    // hide on page video controls
    const selectPlayer = document.getElementById('selectPlayer');
    if (selectPlayer) {
      _.hideElement(selectPlayer.parentElement.parentElement.parentElement);
    }

    // remove info above video
    const profileLink = document.querySelector('#adsIfrme a[href="/Profile"]');
    if (profileLink) {
      profileLink.parentElement.parentElement.remove();
    }

    // set player type
    let useFlash = (settings.player === PLAYER.FLASH);
    let youtubeFlash = false;

    // set player cookie and reload with correct player
    if (useFlash && document.cookie.indexOf('usingFlashV1') < 0) {
      document.cookie = 'usingFlashV1=true;path=/';
      document.cookie = 'usingHTML5V1=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/';
      window.location.href = window.location.href;
    } else if (document.cookie.indexOf('usingHTML5V1') < 0) {
      document.cookie = 'usingFlashV1=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/';
      document.cookie = 'usingHTML5V1=true;path=/';
      window.location.href = window.location.href;
    }

    const _jw = (typeof unsafeWindow.jwplayer === 'undefined' || unsafeWindow.jwplayer === null), _mp = (typeof unsafeWindow.myPlayer === 'undefined' || unsafeWindow.myPlayer === null);
    if ((useFlash && _jw) || (!useFlash && _mp)) {
      useFlash = !useFlash;
      youtubeFlash = _jw && _mp;
    }

    // start per player settings
    // flash player
    if (useFlash) {
      console.log('Using Flash player.');

      if (youtubeFlash) {
        console.log('Using YouTube player.');

        // functions to inject on page for flash video control
        // fires when youtube player is ready
        const ytHook = (playerId) => {
          console.log('Youtube player hooked.');

          const video = window.embedVideo;

          settings.autoPause && video.pauseVideo();
          video.addEventListener('onStateChange', _.checkAutoAdvance);

          // translate option into youtube quality strings values
          const ytQuality = (function (quality) {
            switch (quality) {
            case '360': return 'medium';
            case '480': return 'large';
            case '720': return 'hd720';
            default: return 'hd1080';
            }
          })(settings.quality);

          // set quality
          video.setPlaybackQuality(ytQuality);
          // set volume
          video.setVolume(settings.volume);

          // focus on the video (so pressing f will fullscreen)
          setTimeout(() => { video.focus(); }, 0);

          // force position to be absolute (compatibility with 'Turn Off the Lights')
          setTimeout(() => { video.style.position === 'relative' && video.style.removeProperty('position'); }, 100);
        };
        // inject function into page
        unsafeWindow.onYouTubePlayerReady = exportFunction(ytHook, unsafeWindow);
      } else {
        console.log('Using jwplayer.');
        // functions to inject on page for flash video control
        // fires when youtube player is ready
        const jwHook = () => {
          // wait till video is loaded into player
          window.jwplayer().onReady(() => {
            console.log('jwplayer hooked.');
            // change the quality to desired flash option
            const qualityLevels = window.jwplayer().getQualityLevels();
            const desiredQuality = parseInt(settings.quality, 10);
            let qualitySet = false;

            // try to find exact quality level
            for (let i = 0; i < qualityLevels.length; i++) {
              if (desiredQuality === parseInt(qualityLevels[i].label, 10)) {
                window.jwplayer().setCurrentQuality(i);
                qualitySet = true;
                break;
              }
            }

            // try to find best level alternate
            if (!qualitySet) {
              // check if desired level is lower than all available
              if (desiredQuality < parseInt(qualityLevels[0].label, 10)) {
                window.jwplayer().setCurrentQuality(0);
              }
              // check if desired level is higher than all available
              else if (desiredQuality > parseInt(qualityLevels[qualityLevels.length - 1].label, 10)) {
                window.jwplayer().setCurrentQuality(qualityLevels.length - 1);
              }
              // else find level that is next smallest
              else {
                for (let i = 0; i < qualityLevels.length; i++) {
                  if (desiredQuality < parseInt(qualityLevels[i].label, 10)) {
                    window.jwplayer().setCurrentQuality(i - 1);
                    break;
                  }
                }
              }
            }

            // pause the video if option is enabled
            settings.autoPause && window.jwplayer().pause();

            // setup callback for end of video checks
            window.jwplayer().onComplete(_.checkAutoAdvance);
          });
        };
        // inject function into page
        unsafeWindow.jwHook = exportFunction(jwHook, unsafeWindow);
        // call injected function, hopefully after jwplayer has been setup
        setTimeout(() => { unsafeWindow.jwHook(); }, 500);
      }

    }
    // html5 player
    else {
      console.log('Using HTML5 player.');

      // move quality select below player
      const selectQuality = document.getElementById('selectQuality') || document.getElementById('slcQualix');
      const videoArea = document.getElementById('centerDivVideo');
      if (selectQuality && videoArea) {
        const parent = selectQuality.parentElement;
        videoArea.parentElement.appendChild(document.createElement('div'), videoArea.nextSibling);
        videoArea.parentElement.appendChild(selectQuality, videoArea.nextSibling);
        parent.remove();
      }

      // functions to inject on page for html5 video control
      const html5Hook = () => {
        console.log('HTML5 player hooked.');
        // change the quality to desired flash option
        const qualityLevels = document.getElementById('selectQuality') || document.getElementById('slcQualix');
        const desiredQuality = parseInt(settings.quality, 10);
        let qualitySet = false;
        const setQuality = (index) => {
          qualityLevels.selectedIndex = index;
          qualityLevels.dispatchEvent(new Event('change'));
          _.removeAds('.clsTempMSg');
        };
        // try to find exact quality level
        for (let i = 0; i < qualityLevels.length; i++) {
          if (desiredQuality === parseInt(qualityLevels.options[i].innerHTML, 10)) {
            setQuality(i);
            qualitySet = true;
            break;
          }
        }

        // try to find best level alternate
        if (!qualitySet) {
          // check if desired level is higher than all available
          if (desiredQuality < parseInt(qualityLevels.options[qualityLevels.length - 1].innerHTML, 10)) {
            setQuality(qualityLevels.length - 1);
          }
          // check if desired level is lower than all available
          else if (desiredQuality > parseInt(qualityLevels.options[0].innerHTML, 10)) {
            setQuality(0);
          }
          // else find level that is next smallest
          else {
            for (let i = 0; i < qualityLevels.length; i++) {
              if (desiredQuality > parseInt(qualityLevels.options[i].innerHTML, 10)) {
                setQuality(i);
              }
            }
          }
        }

        const video = document.getElementById('my_video_1_html5_api');
        // auto pause
        if (settings.autoPause) {
          video.pause();
          video.parentElement.setAttribute('autoplay', 'false');
          video.autoplay = false;
        }
        video.volume = settings.volume / 100; // volume
        video.addEventListener('ended', _.checkAutoAdvance); // auto advance
      };
      // inject function into page
      unsafeWindow.html5Hook = exportFunction(html5Hook, unsafeWindow);
      // call the injected script
      unsafeWindow.html5Hook();

      // remove any ghost vjs-tip (html5 player)
      setInterval(() => { _.queryAll('#vjs-tip').slice(1).forEach(elt => elt.remove()); }, 500);
    }
    // end per player settings

    // scroll to the container
    settings.autoScroll && document.getElementById('container') && setTimeout(() => { document.getElementById('container').scrollIntoView(true); }, 0);

    // add listeners for keydown
    const windowListener = function (evt) {
      // prev / next video navigate
      if (evt.code === 'NumpadMultiply' || evt.code === 'NumpadSubtract') {
        (evt.code === 'NumpadMultiply') ? _.previousVideo() : _.nextVideo();
        evt.preventDefault();
      }
    };
    const videoListener = function (evt) {
      if (!useFlash) {
        const video = unsafeWindow.document.getElementById('my_video_1_html5_api');
        const videoFocused = (unsafeWindow.document.activeElement === video);

        // speed controls (html5)
        if (evt.code === 'Minus' || evt.code === 'Equal') {
          video.playbackRate += ((evt.code === 'Minus' && video.playbackRate > 0.25)? -0.25 : (evt.code === 'Equal' && video.playbackRate < 5) ? 0.25 : 0);
          evt.preventDefault();
        }

        // large seek (html5)
        const largeSeek = 20; // TODO add settings option
        if (evt.ctrlKey && (evt.code === 'ArrowLeft' || evt.code === 'ArrowRight')) {
          video.currentTime += ((evt.code === 'ArrowLeft') ? -largeSeek : largeSeek);
          evt.preventDefault();
        }
      }
    };
    const my_video_1 = document.getElementById('my_video_1');
    if (my_video_1) {
      window.addEventListener('keydown', videoListener);
    }
    window.addEventListener('keydown', windowListener);

    console.log('Finished cleaning video page.');
  }

  //---------------------------------------------------------------------------------------------------------------
  // Clean All pages
  //---------------------------------------------------------------------------------------------------------------
  console.log('Starting general page cleaning...');

  // remove share stuff next to search bar
  // get the search element near the top of the page
  const result_box = document.getElementById('result_box');
  if (result_box) {
    const ad = result_box.nextElementSibling;
    if (ad && (ad.children.length === 0 || !ad.children[0].matches('a[href*="AdvanceSearch"]'))) {
      ad.remove();
    }
  }

  // remove login at the top of the page
  settings.removeLogin && _.removeAd('#topHolderBox');

  // remove pointless tabs
  // remove footer
  // remove ad hides
  _.removeAds('#liMobile', '#liReportError', '#liRequest', '#liCommunity', '#liFAQ', '#liReadManga', '#footer', 'div.divCloseBut');

  // keys listener for script mnu otions
  // home key
  let settingsMenu;
  let isSettingsMenuOpen = false;
  const openSettingsMenu = () => {
    if (!isSettingsMenuOpen) {
      isSettingsMenuOpen = true;

      // create settings settingsMenu if it doesn't exist
      if (typeof settingsMenu === 'undefined' || settingsMenu === null) {
        const preview = document.implementation.createHTMLDocument('preview');
        preview.documentElement.innerHTML = GM_getResourceText('settings');
        settingsMenu = preview.getElementById('kisscleaner-settings-container');
      }

      // set settingsMenu option values from current settings
      const nodes = Array.from(settingsMenu.querySelectorAll('[name]'));
      nodes.forEach((node) => {
        if (settings.hasOwnProperty(node.name)) {
          if (node.type === 'checkbox') {
            node.checked = settings[node.name];
          } else {
            node.value = settings[node.name];
          }
        }
      });

      // add settings settingsMenu to page
      document.getElementById('containerRoot').appendChild(settingsMenu);

      // add save button listener
      document.getElementById('kisscleaner-settings-save').addEventListener('click', saveSettingsMenu);
    } else {
      saveSettingsMenu();
    }
  };
  const saveSettingsMenu = () => {
    // save values
    const nodes = Array.from(settingsMenu.querySelectorAll('[name]'));
    let changes = false;
    nodes.forEach((node) => {
      if (settings.hasOwnProperty(node.name)) {
        const value = ((node.type === 'checkbox') ? node.checked : node.value);
        if (settings[node.name] !== value) {
          changes = true;
        }
        GM_setValue(node.name, (settings[node.name] = value));
      }
    });

    // update player cookie
    if (settings.player === PLAYER.FLASH) {
      document.cookie = 'usingFlashV1=true;path=/';
      document.cookie = 'usingHTML5V1=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/';
    } else {
      document.cookie = 'usingFlashV1=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/';
      document.cookie = 'usingHTML5V1=true;path=/';
    }

    // reload page to make changes
    if (changes) {
      window.location.href = window.location.href;
    }

    settingsMenu.remove();
    isSettingsMenuOpen = false;
  };
  // create global key listener
  const globalKeyListener = (evt) => {
    if (evt.code === 'Home') {
      // prevent default action of scrolling to top of page
      evt.preventDefault();
      openSettingsMenu();
    }
  };
  // inject functions into page
  unsafeWindow.openSettingsMenu = exportFunction(openSettingsMenu, unsafeWindow);
  unsafeWindow.saveSettingsMenu = exportFunction(saveSettingsMenu, unsafeWindow);
  // add the listener for keypresses
  window.addEventListener('keydown', globalKeyListener);
  // also add as GM menu command
  GM_registerMenuCommand('KissCleaner Settings', unsafeWindow.openSettingsMenu);

  // search box navigation
  const searchResults = document.getElementById('searchResults'); // search results
  const  searchBox = document.getElementById('keyword'); // search form text box
  // TODO arrow scroll through search results

  console.log('Finished general page cleaning.');
  console.log('Finished initialization.');
};
// work around different greasemonkey and tampermonkey load times
window.addEventListener('DOMContentLoaded', clean);