F95zone marker

Marks F95Zone threads for future reference

Per 05-02-2025. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         F95zone marker
// @namespace    http://tampermonkey.net/
// @version      0.1.9
// @description  Marks F95Zone threads for future reference
// @author       agreg
// @license      MIT
// @match        https://f95zone.to/*
// @icon         https://www.google.com/s2/favicons?domain=f95zone.to
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// ==/UserScript==

/* jshint esversion: 6 */
/* eslint no-multi-spaces:off */
(function() {
  'use strict';

  const THREADS = /^(https:\/\/f95zone.to)?\/threads\//;
  const OP = '.message-threadStarterPost';
  const OP_ICONS = `${OP} header .message-attribution-opposite`;
  var bookmarks = GM_getValue('bookmarks', {});
  var tags = GM_getValue('tags', {});

  let urlId = s => (!THREADS.test(s) ? null : s.replace(THREADS, '').match(/^([^\/]+\.)?([0-9]+)/)[2]);
  let $find = (s, e=document) => e.querySelector(s);
  let $find_ = (s, e=document) => Array.from( e.querySelectorAll(s) );
  let $parent = (e, check=()=>true) => (check(e.parentNode) ? e.parentNode : $parent(e.parentNode, check));
  let $e = (tag, options={}, ...children) => {var e = Object.assign(document.createElement(tag), options);
                                              children.forEach(x => e.append(x));
                                              return e}
  let $assign = (o, vals) => Object.assign(o, ...Object.entries(vals).map(([k, v]) => ({[k]: v||undefined})));
  let $bookmark = e => {
    e.prepend( $e('i', {className: "bookmarkLink bookmarkLink--highlightable is-bookmarked"}) );
    e.classList.add('-marker-bookmark');
  };
  let $updData = (id, e, data=GM_getValue(id), upd={}) => {if (data) {
    GM_setValue(id, $assign(data, upd));
    e.classList[data.fade ? 'add' : 'remove']('-marker-fade');
    e.classList[data.mark ? 'add' : 'remove']('-marker-mark');
    e.title = data.info || "";
  } else {
    GM_deleteValue(id);
    e.classList.remove('-marker-fade', '-marker-mark');
    e.title = "";
  }};

  let _$getData = () => Object.fromEntries(GM_listValues().map(k => [k, GM_getValue(k)]));
  let $exportData = () => $e('a', {
    download: `F95zoneMarker_${new Date().toJSON().match(/(.*)T/)[1]}.json`,
    href: "data:application/json;base64," + btoa(JSON.stringify(_$getData(), null, 2) + "\n"),
  }).click();
  let $importData = () => $e('input', {type: 'file', accept: 'application/json', onchange() {
    this.files[0] && Object.assign(new FileReader(), {onload () {
      let data = JSON.parse(this.result);
      GM_listValues().forEach(GM_deleteValue);
      Object.entries(data).forEach(([k, v]) => GM_setValue(k, v));
      location.reload();
    }}).readAsText(this.files[0]);
  }}).click();

  let $editIcon = (id, e) => {
    var icon = $e('i', {className: "fas fa-star", title: "Edit mark"});
    var data = GM_getValue(id), edit = null;
    let _$updData = (upd={}) => {$updData(id, e, data, upd);
                                 icon.classList.add(`-marker-${data ? "" : "un"}marked`);
                                 icon.classList.remove(`-marker-${data ? "un" : ""}marked`)}
    let $deleteMark = () => {if (confirm("Delete mark?")) {
      $toggleEdit();
      data = null;
      _$updData();
    }};
    let $toggleEdit = () => {if (edit) {
      edit.remove();
      edit = null;
    } else {
      data = data || {};
      document.body.append(edit = $e('div', {className: '-marker-dialog'},
                                     $e('textarea', {placeholder: "Tooltip", value: data.info||"", oninput () {_$updData({info: this.value})}}),
                                     $e('div', {className: '-marker-row'},
                                        $e('label', {},
                                           $e('input', {type: 'checkbox', checked: data.fade, onchange () {_$updData({fade: this.checked})}}),
                                           $e('span', {innerText: "Fade"})),
                                        $e('label', {},
                                           $e('input', {type: 'checkbox', checked: data.mark, onchange () {_$updData({mark: this.checked})}}),
                                           $e('span', {innerText: "Mark"}))),
                                     $e('div', {className: '-marker-row'},
                                        $e('button', {innerText: "OK", onclick: $toggleEdit}),
                                        $e('button', {innerText: "Delete", onclick: $deleteMark}),
                                        $e('button', {style: "float:right", innerText: "Export Data", onclick() {$toggleEdit(); $exportData()}}),
                                        $e('button', {style: "float:right", innerText: "Import Data", onclick() {$toggleEdit(); $importData()}}))));
    }};
    _$updData();
    return $e('a', {className: '-marker-edit', href: location.href, onclick () {$toggleEdit();  return false}}, icon);
  };

  GM_addStyle(`.-marker-unmarked {opacity: 0.5;  color: rgb(147, 152, 160)}   .-marker-marked {color: rgb(193, 88, 88)}
               .-marker-fade:not(:hover) {opacity: 0.25}   .-marker-fade:hover {outline: 2px solid black}   .-marker-mark {outline: 2px solid grey}
               .-marker-bookmark {font-weight: bold;  text-shadow: 0 0, 0 0 7px !important}   .-marker-bookmark .bookmarkLink {padding-right: 1ex}
               .-marker-dialog {position: fixed;  top: 30%;  left: 30%;  width: 40%;  background: rgb(29, 31, 33);  border-radius: 5px;  z-index:1000}
               .-marker-dialog > * {margin: 1em}   .-marker-dialog textarea {width: calc(100% - 2em)}
               .-marker-dialog .-marker-row > * {margin-left: .5em;  margin-right: .5em}   .-marker-tag {box-shadow: 0 0 1px 2px !important}`);

  $find_(".structItem-title").forEach(e => {                     // forum, similar threads
    let _id = urlId(e.getAttribute('uix-data-href')||"");
    _id && $updData(_id, $parent(e, x => x.classList.contains('structItem--thread')));
    bookmarks[_id] && $bookmark(e);
  });

  if (['/search/', '/tags/'].some(s => location.pathname.startsWith(s))) {  // search results
    $find_(".contentRow-title a").forEach(e => {
      let _id = urlId(e.href||"");
      console.log(e.href, _id)
      _id && $updData(_id, $parent(e, x => /^li$/i.test(x.tagName)));
      bookmarks[_id] && $bookmark(e);
    });
  }

  if (['/latest', '/sam/latest_alpha'].some(s => location.pathname.startsWith(s))) {  // latest updates
    GM_addStyle(`.-marker-mark {display: block;/* Chrome bug */}   .-marker-hide :is(.-marker-edit, .-marker-tag) {display: none}
                 .-marker-dialog {top: 45%;  left: calc(30% - 125px - 10px)}
                 @media only screen and (max-width: 1240px) {.-marker-dialog {left: 30%}}
                 .-marker-edit {position: absolute;  top: 2%;  right: 1%;  z-index: 10;  opacity: 0.7;  background: black;  border-radius: 5px}
                 .-marker-edit > * {padding: 5px 3px;  opacity: 1}   .resource-tile:hover .-marker-tags {display: none}
                 .-marker-tags {position: absolute;  top: 2%;  left: 1%;  z-index: 10;  font-size: smaller;  width: calc(97% - 24px)}
                 .-marker-tags > * {display: inline-block;  background: #A44C;  border-radius: 3px;  padding: 0 5px;  margin: 1px}}`);
    let _allTagIds = (() => {try {return latestUpdates.tags} catch (e) {}})();  // eslint-disable-line no-undef
    let _markedTagIds = Object.fromEntries( Object.entries(_allTagIds||{}).filter(([_, k]) => tags[k]) );
    new MutationObserver(mutations => mutations.forEach(m => Array.from(m.addedNodes).filter(e => e.tagName).forEach(node => {
      console.debug(node, node.children);
      $find_(".resource-tile_tags > span", node).forEach(e => tags[e.innerText] && e.classList.add('-marker-tag'));
      $find_("a.resource-tile_link", node).forEach(e => {
        let _id = urlId(e.href||"");
        let _tags = (e.parentNode.getAttribute('data-tags')||"").split(',').map(id => _markedTagIds[id]).filter(Boolean);
        console.log(e.href, _id, _tags);
        e.append($editIcon(_id, e));
        e.append($e('div', {className: "-marker-tags"}, ..._tags.map(s => $e('span', {className: "-marker-tag", innerText: s}))));
        _id && $updData(_id, e);
        bookmarks[_id] && $bookmark($find('.resource-tile_info-header_title', e));
      });
    }))).observe(document.body, {subtree: true, childList: true});
    [['keydown', 'add'], ['keyup', 'remove']].forEach(([name, toggle]) =>  // hide extra elements when holding down Shift
      document.addEventListener(name, evt => (evt.key == 'Shift') && document.body.classList[toggle]('-marker-hide')));
    document.addEventListener('visibilitychange', () => document.body.classList.remove('-marker-hide'));  // unhide when switching tabs
  }

  if (location.pathname === '/account/bookmarks') {
    $find_("li.block-row").forEach(e => {
      let _id = urlId($find(".contentRow-title a", e).href||"");
      if (_id) {
        let toolbar = $find(".contentRow-extra", e);
        toolbar.insertBefore($editIcon(_id, e), toolbar.firstChild);
        bookmarks[_id] = true;
      }
    });
    GM_setValue('bookmarks', bookmarks);
  }

  if (location.pathname.startsWith('/threads/') && $find(OP)) {  // first page of a thread
    let _icons = $find(OP_ICONS), _id = urlId(location.href), _bookmarked = Boolean($find("a.is-bookmarked", _icons));
    if ( $find_('a', _icons).some(e => e.innerText == "#1") ) {
      _icons.insertBefore($e('li', {}, $editIcon(_id, $find(OP))),
                          _icons.firstChild);
    }
    (_bookmarked || bookmarks[_id]) && GM_setValue('bookmarks', $assign(bookmarks, {[_id]: _bookmarked}));
  }

  if (location.pathname.startsWith('/threads/')) {  // thread tags
    let _tagList = $find(".js-tagList"), _tags = $find_(".tagItem", _tagList);
    let _markTag = (e, id) => e.classList[tags[id] ? 'add' : 'remove']('-marker-tag');
    _tags.forEach(e => {
      let id = e.innerText;
      _markTag(e, id);
      e.title = "Middle Click to mark/unmark the tag as important";
      e.addEventListener('auxclick', evt => {evt.preventDefault();
                                             GM_setValue('tags', $assign(tags, {[id]: !tags[id]||void 0}));
                                             _markTag(e, id)});
    });
  }
})();