F95zone marker

Marks F95Zone threads for future reference

Versione datata 16/12/2024. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         F95zone marker
// @namespace    http://tampermonkey.net/
// @version      0.1.8
// @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', {});

  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}`);

  $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-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}`);
    new MutationObserver(mutations => mutations.forEach(m => Array.from(m.addedNodes).filter(e => e.tagName).forEach(node => {
      console.debug(node, node.children);
      $find_("a.resource-tile_link", node).forEach(e => {
        let _id = urlId(e.href||"");
        console.log(e.href, _id)
        e.append($editIcon(_id, e));
        _id && $updData(_id, e);
        bookmarks[_id] && $bookmark($find('.resource-tile_info-header_title', e));
      });
    }))).observe(document.body, {subtree: true, childList: true});
  }

  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}));
  }
})();