f95zone tweaks

f95zone exclude tags and min like filter.

As of 2021-12-20. See the latest version.

// ==UserScript==
// @name        f95zone tweaks
// @namespace   f95zone tweaks
// @description f95zone exclude tags and min like filter.
// @match       https://f95zone.to/sam/latest_alpha/
// @version     1.4.1
// @author      3xd_Tango
// @require     https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/[email protected]
// @require     https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @license     GNU GPLv3
// ==/UserScript==

(function () {
'use strict';

var css_248z = ".customTags{border-radius:2px;box-shadow:0 0 0 0 transparent,0 1px 1px 0 rgba(0,0,0,.15),0 1px 2px 0 rgba(0,0,0,.15);display:inline-block;font-size:.8em;line-height:1.8em;margin:4px 4px 0 0;padding:0 6px;text-shadow:1px 1px 2px rgba(0,0,0,.75)}.selected-tags-wrap span:hover{background-color:#ba4545}.selected-tags-wrap span{background-color:#181a1d;border-radius:3px;cursor:pointer;display:inline-block;font-size:.9em;line-height:24px;margin:4px 4px 0 0;padding:0 7px;transition:.2s}.input-tag{left:0;opacity:1;position:relative;width:127px}.border-gradient{border:3px solid;border-image-slice:1}.flxgrow{flex-grow:1}";

const likes = GM_getValue('likes', [['incest', '#ff0000', '30'], ['harem', '#0011ff', '254'], ['futa/trans', '#EE82EE', '191'], ['loli', '#00f5ffff', '639'], ['futa/trans protagonist', '#EE82EE', '2255']]);
const dislikes = GM_getValue('dislikes', [['female protagonist', '392'], ['text based', '522'], ['mind control', '111']]);
const totalTags = GM_getValue('tags', [['2d game', '2214'], ['2dcg', '1507'], ['3d game', '1434'], ['3dcg', '107'], ['adventure', '162'], ['ahegao', '916'], ['anal sex', '2241'], ['animated', '783'], ['bdsm', '264'], ['bestiality', '105'], ['big ass', '817'], ['big tits', '130'], ['blackmail', '339'], ['bukkake', '216'], ['censored', '2247'], ['character creation', '2246'], ['cheating', '924'], ['combat', '550'], ['corruption', '103'], ['cosplay', '606'], ['creampie', '278'], ['dating sim', '348'], ['dilf', '1407'], ['drugs', '2217'], ['dystopian setting', '2249'], ['exhibitionism', '384'], ['fantasy', '179'], ['female domination', '2252'], ['female protagonist', '392'], ['footjob', '553'], ['furry', '382'], ['futa/trans', '191'], ['futa/trans protagonist', '2255'], ['gay', '360'], ['graphic violence', '728'], ['groping', '535'], ['group sex', '498'], ['handjob', '259'], ['harem', '254'], ['horror', '708'], ['humiliation', '871'], ['humor', '361'], ['incest', '30'], ['internal view', '1483'], ['interracial', '894'], ['japanese game', '736'], ['kinetic novel', '1111'], ['lactation', '290'], ['lesbian', '181'], ['loli', '639'], ['male domination', '174'], ['male protagonist', '173'], ['management', '449'], ['masturbation', '176'], ['milf', '75'], ['mind control', '111'], ['mobile game', '2229'], ['monster', '182'], ['monster girl', '394'], ['multiple endings', '322'], ['multiple penetration', '1556'], ['multiple protagonist', '2242'], ['necrophilia', '1828'], ['no sexual content', '324'], ['ntr', '258'], ['oral sex', '237'], ['paranormal', '408'], ['parody', '505'], ['platformer', '1508'], ['point & click', '1525'], ['possession', '1476'], ['pov', '1766'], ['pregnancy', '225'], ['prostitution', '374'], ['puzzle', '1471'], ['rape', '417'], ['real porn', '1707'], ['religion', '2218'], ['romance', '330'], ['rpg', '45'], ['sandbox', '2257'], ['scat', '689'], ['school setting', '547'], ['sci-fi', '141'], ['sex toys', '2216'], ['sexual harassment', '670'], ['shooter', '1079'], ['shota', '749'], ['side-scroller', '776'], ['simulator', '448'], ['sissification', '2215'], ['slave', '44'], ['sleep sex', '1305'], ['spanking', '769'], ['strategy', '628'], ['stripping', '480'], ['superpowers', '354'], ['swinging', '2234'], ['teasing', '351'], ['tentacles', '215'], ['text based', '522'], ['titfuck', '411'], ['trainer', '199'], ['transformation', '875'], ['trap', '362'], ['turn based combat', '452'], ['twins', '327'], ['urination', '1254'], ['vaginal sex', '2209'], ['virgin', '833'], ['virtual reality', '895'], ['voiced', '1506'], ['vore', '757'], ['voyeurism', '485']]);

function likeFilter() {
  const element = document.querySelectorAll('.resource-tile');
  document.querySelectorAll('.customTags').forEach(e => e.parentNode.remove());

  for (let i = 0; i < element.length; i += 1) {
    element[i].style = '';
    element[i].classList.remove('border-gradient');
    element[i].setAttribute('isDisplay', 'false');
    const colors = [];
    const node = document.createElement('DIV');
    const dataTags = element[i].dataset.tags.split(',');
    node.style.padding = '.5rem 0';

    for (let j = likes.length - 1; j >= 0; j -= 1) {
      if (dataTags.includes(likes[j][2])) {
        console.debug(likes[j][0], likes[j][1]);

        if (!element[i].classList.contains('border-gradient')) {
          element[i].classList.add('border-gradient');
        }

        const node1 = document.createElement('SPAN');
        colors.push(likes[j][1]);
        node1.classList.add('customTags');
        node1.style.borderLeft = `${likes[j][1]} solid`;
        node1.appendChild(document.createTextNode(likes[j][0]));
        node.appendChild(node1);
        element[i].setAttribute('isDisplay', 'true');
      } else if (element[i].getAttribute('isDisplay') === null) {
        element[i].setAttribute('isDisplay', 'false');
      }
    }

    if (colors.length > 1) {
      element[i].style.borderImageSource = `linear-gradient(to bottom right, ${colors.toString()})`;
    } else {
      element[i].style.color = colors[0];
    }

    if (element[i].getAttribute('isDisplay') === 'true') {
      try {
        element[i].childNodes[0].childNodes[1].childNodes[1].appendChild(node);
      } catch (e) {
        console.error(e);
        setTimeout(likeFilter, 200);
      }
    }
  }
}

function likeLimit() {
  const element = document.querySelectorAll('.resource-tile_info'); // if (likesLimit === '') {
  //   likesLimit = -1;
  // }

  for (let i = 0; i < element.length; i++) {
    const elementLikes = element[i].childNodes[1].childNodes[1].innerText;
    element[i].childNodes[0].childNodes[0].childNodes[0].innerText;
    const checkFilter = Number(elementLikes) <= Number(GM_getValue('like_limit', 200)); // console.debug(`we get Like limit of ${elementName} is ${elementLikes} with check ${checkFilter} with display attribute of ${(element[i].parentNode.parentNode.parentNode.getAttribute('isDisplay').toLowerCase() !== 'false')}`);

    if (checkFilter) {
      if (element[i].parentNode.parentNode.parentNode.getAttribute('isDisplay') === 'false') {
        element[i].parentNode.parentNode.parentNode.setAttribute('isLikeLimit', 'true');
        continue;
      }

      if (GM_getValue('favLikeFilter', false)) {
        element[i].parentNode.parentNode.parentNode.setAttribute('isLikeLimit', 'true');
        continue;
      }
    }

    element[i].parentNode.parentNode.parentNode.setAttribute('isLikeLimit', 'false');
  }
}

function filterOut() {
  const element = document.querySelectorAll('.resource-tile');

  for (let i = 0; i < element.length; i++) {
    if (element[i].getAttribute('isDislike') === 'true' || element[i].getAttribute('isLikeLimit') === 'true') {
      element[i].style.setProperty('display', 'none', 'important');
      element[i].style.setProperty('height', '0px', 'important');
      element[i].style.setProperty('margin', '0px', 'important');
    } else {
      element[i].style.setProperty('display', 'block', 'important');
      element[i].style.setProperty('height', 'unset', 'important');
      element[i].style.setProperty('margin', 'unset', 'important');
    }
  }
}

function checkTags(element, dataTags) {
  let tagProt = [['female domination', '2252'], ['male protagonist', '173'], ['futa/trans protagonist', '2255']];
  const tagProtNames = dislikes.filter(e => e[0].includes('protagonist'));
  console.log(tagProtNames, 'tagProtNames');
  const b = [];
  let c = 0;
  dataTags.forEach(e => {
    if (tagProtNames.length >= 1) {
      for (let i = 0; i < tagProtNames.length; i += 1) {
        if (tagProtNames[i][1].includes(e)) {
          return b.push(e);
        }
      }
    }

    if (dislikes.includes(e)) {
      c += 1;
    }
  }); // console.debug(`b = ${b}`, c, element);

  if (c > 0) {
    return true;
  }

  if (b.length > 0) {
    b.forEach(e => {
      tagProt = tagProt.filter(r => r !== e);
    });

    for (let i = 0; i <= tagProt.length - 1; i += 1) {
      console.debug(`datatags includes? = ${dataTags.includes(tagProt[i][1])} for ${tagProt[i][0]} `);

      if (dataTags.includes(tagProt[i])) {
        return false;
      }

      if (i === tagProt.length - 1) {
        return true;
      }
    }
  }

  console.debug(`tagProt = ${tagProt}`);
  return false;
}

function dislikeFilter() {
  const element = document.querySelectorAll('.resource-tile');

  for (let i = 0; i < element.length; i++) {
    const dataTags = element[i].dataset.tags.split(',');
    const isFP = checkTags(element[i], dataTags);

    if (isFP) {
      element[i].setAttribute('isDislike', 'true');
    } else {
      element[i].setAttribute('isDislike', 'false');
    }
  }
}

function tagRemoveHandler(event, isLikes) {
  let filtered = [];

  if (isLikes) {
    filtered = likes.filter(r => r[0] !== event.target.textContent);
    console.debug(`You have clicked the Like Tag to Remove it and the filtered is: ${filtered} within ${likes}`);

    const removed = _.remove(likes, n => {
      return n[0] === event.target.textContent;
    });

    console.debug(`after filtred you got likes tags: ${likes} and you removed ${removed}`);
    likeFilter();
    likeLimit();
  } else {
    filtered = dislikes.filter(r => r[0] !== event.target.textContent);
    console.debug(`You have clicked the dislike Tag to Remove it and the filtered is: ${filtered} within ${dislikes}`);

    const removed = _.remove(dislikes, n => {
      return n[0] === event.target.textContent;
    });

    console.debug(`after filtred you got dislike tags: ${dislikes} and you removed ${removed}`);
    dislikeFilter();
  }

  event.target.remove();
  GM_setValue(isLikes ? 'likes' : 'dislikes', filtered);
  filterOut();
} // function tagRemoveHandler1(event) {
//   likes = likes.filter(r => r[0] !== event.target.textContent);
//   event.target.remove();
//   GM_setValue('likes', likes);
//   likeFilter();
//   likeLimit();
//   filterOut();
// }

function likeHandler(event) {
  const [tagName, tagCode] = event.target.value.split(':');
  event.target.value = '';
  let x;

  do {
    x = prompt("Please Enter the Color in Hex like '#FFFFFF'");
  } while (!x.includes('#') && x === '' && x.length <= 1);

  console.debug(`you have entered color in hex: ${x} with tagName: ${tagName} and the condition ${x !== null}`);

  if (x !== null) {
    const tag = [tagName, x, tagCode];
    likes.push(tag);
    console.debug(`the likes after pushing the ${tag} is ${likes}`);
    GM_setValue('likes', _.sortBy(likes, item => item[0]));
    const node = document.createElement('SPAN');

    node.onclick = e => tagRemoveHandler(e, true);

    node.style.borderLeft = `${x} solid`;
    node.appendChild(document.createTextNode(tagName));
    event.target.parentNode.parentNode.childNodes[2].append(node);
    likeFilter();
    likeLimit();
    filterOut();
  }
}

function dislikeHandler(event) {
  const [tagName, tagCode] = event.target.value.split(':');
  dislikes.push([tagName, tagCode]);
  GM_setValue('dislikes', _.sortBy(dislikes));
  event.target.value = '';
  const node = document.createElement('SPAN');

  node.onclick = e => tagRemoveHandler(e, false);

  node.appendChild(document.createTextNode(tagName));
  event.target.parentNode.parentNode.childNodes[2].append(node);
  dislikeFilter();
  filterOut();
}

function limitHandler(event) {
  GM_setValue('like_limit', event.target.value);
  likeLimit();
  filterOut();
}

function clickTotalTags() {
  const y = document.querySelector('.selectize-control');

  if (y !== null) {
    y.addEventListener('click', e => {
      const x = document.querySelectorAll('div.option');
      console.log('clicked', x);

      if (x !== []) {
        const totalTags = [];
        x.forEach(e => totalTags.push([e.childNodes[0].innerHTML, e.dataset.value]));
        GM_setValue('tags', totalTags);
      }
    });
  }
}

function init() {
  const element = document.querySelectorAll('.resource-tile');
  const elementLength = element.length;

  if (element[0]) {
    likeFilter();
    clickTotalTags();

    for (let i = 0; i < 3; i++) {
      dislikeFilter();
      likeLimit();
      filterOut(); // if(document.querySelectorAll('.resource-tile'))

      const filtered = _.filter(document.querySelectorAll('.resource-tile'), e => {
        return e.getAttribute('isDisplay') === 'true';
      });

      console.debug('Filtered', filtered);

      if (filtered.length < elementLength) {
        break;
      }
    }
  } else {
    setTimeout(init, 100);
  }
}

function waitForElementToDisplayAndAppend(selector, callback, checkFrequencyInMs, timeoutInMs) {
  const startTimeInMs = Date.now();

  (function loopSearch() {
    if (document.querySelector(selector).childNodes.length > 0) {
      document.querySelector(selector).appendChild(callback());
    } else {
      setTimeout(() => {
        if (timeoutInMs && Date.now() - startTimeInMs > timeoutInMs) return;
        loopSearch();
      }, checkFrequencyInMs);
    }
  })();
}

function LikesTagFilter() {
  // filter-block accordion-block filter-block_prefix-group
  return VM.hm("div", {
    className: "filter-block accordion-block filter-block_prefix-group"
  }, VM.hm("h4", {
    className: "filter-block_title accordion-toggle"
  }, "Like Tags"), VM.hm("div", {
    className: "filter-block_content filter-block_v accordion-content"
  }, VM.hm("div", {
    className: "selectize-input"
  }, VM.hm("input", {
    autoComplete: "on",
    placeholder: "Enter a tag to filter...",
    className: "input-tag",
    onChange: likeHandler,
    list: "totalTagName"
  })), VM.hm("datalist", {
    id: "totalTagName"
  }, totalTags.map(item => {
    return VM.hm("option", {
      value: `${item[0]}:${item[1]}`
    });
  })), VM.hm("div", {
    className: "selected-tags-wrap"
  }, likes.map(dl => {
    const node = document.createElement('SPAN');

    node.onclick = e => tagRemoveHandler(e, true);

    node.style.borderLeft = `${dl[1]} solid`;
    node.appendChild(document.createTextNode(dl[0]));
    return node; // <span
    //   style={{ borderLeft: `${dl[1]} solid` }}
    //   onClick={(e) => tagRemoveHandler(e, true)}
    // >
    //   {dl[0]}
    // </span>;
  }))));
}

function DislikeTagFilter() {
  return VM.hm("div", {
    className: "filter-block accordion-block filter-block_prefix-group"
  }, VM.hm("h4", {
    className: "filter-block_title accordion-toggle"
  }, "Dislike Tags"), VM.hm("div", {
    className: "filter-block_content filter-block_v accordion-content"
  }, VM.hm("div", {
    className: "selectize-input"
  }, VM.hm("input", {
    autoComplete: "off",
    placeholder: "Enter a tag to filter...",
    className: "input-tag",
    onChange: dislikeHandler,
    list: "totalTagName1"
  })), VM.hm("datalist", {
    id: "totalTagName1"
  }, totalTags.map(item => {
    return VM.hm("option", {
      value: `${item[0]}:${item[1]}`
    });
  })), VM.hm("div", {
    className: "selected-tags-wrap"
  }, dislikes.map(dl => VM.hm("span", {
    onClick: e => tagRemoveHandler(e, false)
  }, dl[0])))));
}

function LikeLimitDisplay() {
  return VM.hm("div", {
    className: "filter-block"
  }, VM.hm("h4", {
    className: "filter-block_title"
  }, "Tags Like Limits"), VM.hm("div", {
    className: "selectize-input"
  }, VM.hm("input", {
    value: GM_getValue('like_limit', 200),
    autoComplete: "off",
    style: "width: 127px;",
    onChange: limitHandler
  })));
}

function FilterFavLikeHandler(event) {
  GM_setValue('favLikeFilter', event.target.checked);
  likeLimit();
  filterOut();
}

function mainFilter() {
  init();
  return VM.hm(VM.Fragment, null, LikeLimitDisplay(), VM.hm("div", {
    className: "filter-block"
  }, VM.hm("div", {
    className: "filter-block_button-wrap",
    style: {
      display: 'flex'
    }
  }, VM.hm("span", {
    className: "filter-block_title flxgrow"
  }, "Filter Fav likes:"), VM.hm("input", {
    type: "checkbox",
    id: "filter-fav-likes",
    onChange: FilterFavLikeHandler,
    checked: GM_getValue('favLikeFilter', false)
  }))), VM.hm("style", null, css_248z));
}

document.querySelector('.content-block_filter').appendChild(mainFilter());
waitForElementToDisplayAndAppend('#filter-block_prefixes', LikesTagFilter, 1000, 10000);
waitForElementToDisplayAndAppend('#filter-block_prefixes', DislikeTagFilter, 1000, 10000);

history.onpushstate = function (state) {
  setTimeout(init, 200);
};

(function (history) {
  const pushState = history.pushState;

  history.pushState = function (state) {
    if (typeof history.onpushstate === 'function') {
      history.onpushstate({
        state
      });
    }

    return pushState.apply(history);
  };
})(window.history);

})();