yande.re refine

Refining yande.re

Від 09.05.2020. Дивіться остання версія.

// ==UserScript==
// @name           yande.re refine
// @namespace      https://greasyfork.org/scripts/397612-yande-re-refine
// @description    Refining yande.re
// @include        *://behoimi.org/*
// @include        *://www.behoimi.org/*
// @include        *://*.donmai.us/*
// @include        *://konachan.tld/*
// @include        *://yande.re/*
// @include        *://chan.sankakucomplex.com/*
// @version        2020.04.10.a
// @grant          none
// ==/UserScript==

const STORAGE_KEY = "yandere-refine-liked";
const SUGGEST_WIDTH = 300;
//Minimum amount of window left to scroll, maintained by loading more pages.
const scrollBuffer = 600;
//Time (in ms) the script will wait for a response from the next page before attempting to fetch the page again.  If the script gets trapped in a loop trying to load the next page, increase this value.
const timeToFailure = 15000;

//============================================================================
//=========================Script initialization==============================
//============================================================================

var nextPage, mainTable, mainParent, timeout, iframe;
let previewIframe, previewImage, previewImageDiv, previewDialog;
let imagesList = [];
let currentImage = 0;
let pending = ref(false, v =>
  document.getElementById("loader").classList.toggle("hidden", !v)
);
let likedList = readLikeList();
let viewingFavorites = isViewingFavorites();
let db;

injectGlobalStyle();
initialize();


function initialize() {
  //Stop if inside an iframe
  if (window != window.top || scrollBuffer == 0) return;

  //Stop if no "table"
  mainTable = getMainTable(document);
  if (!mainTable) return;

  injectStyle();
  initDOM();
  addImages(getImages());

  //Stop if no more pages
  nextPage = getNextPage(document);
  if (!nextPage) return;

  //Hide the blacklist sidebar, since this script breaks the tag totals and post unhiding.
  var sidebar = document.getElementById("blacklisted-sidebar");
  if (sidebar) sidebar.style.display = "none";

  //Other important variables:
  mainParent = mainTable.parentNode;
  pending.value = false;

  iframe = document.createElement("iframe");
  iframe.width = iframe.height = 0;
  iframe.style.visibility = "hidden";
  document.body.appendChild(iframe);

  //Slight delay so that Danbooru's initialize_edit_links() has time to hide all the edit boxes on the Comment index
  iframe.addEventListener(
    "load",
    function(e) {
      setTimeout(appendNewContent, 100);
    },
    false
  );

  window.addEventListener("scroll", testScrollPosition, false);
  testScrollPosition();
}

//============================================================================
//============================Script functions================================
//============================================================================

//Some pages match multiple "tables", so order is important.
function getMainTable(source) {
  //Special case: Sankaku post index with Auto Paging enabled
  if (
    /sankaku/.test(location.host) &&
    /auto_page=1/.test(document.cookie) &&
    /^(post(\/|\/index\/?)?|\/)$/.test(location.pathname)
  )
    return null;

  var xpath = [
    ".//div[@id='c-favorites']//div[@id='posts']", // Danbooru (/favorites)
    ".//div[@id='posts']/div", // Danbooru; don't want to fall through to the wrong xpath if no posts ("<article>") on first page.
    ".//div[@id='c-pools']//section/article/..", // Danbooru (/pools/####)

    ".//div[@id='a-index']/table[not(contains(@class,'search'))]", // Danbooru (/forum_topics, ...), take care that this doesn't catch comments containing tables
    ".//div[@id='a-index']", // Danbooru (/comments, ...)

    ".//table[contains(@class,'highlight')]", // large number of pages
    ".//div[@id='content']/div/div/div/div/span[@class='author']/../../../..", // Sankaku: note search
    ".//div[contains(@id,'comment-list')]/div/..", // comment index
    ".//*[not(contains(@id,'popular'))]/span[contains(@class,'thumb')]/a/../..", // post/index, pool/show, note/index
    ".//li/div/a[contains(@class,'thumb')]/../../..", // post/index, note/index
    ".//div[@id='content']//table/tbody/tr[@class='even']/../..", // user/index, wiki/history
    ".//div[@id='content']/div/table", // 3dbooru user records
    ".//div[@id='forum']" // forum/show
  ];

  for (var i = 0; i < xpath.length; i++) {
    getMainTable = (function(query) {
      return function(source) {
        var mTable = new XPathEvaluator().evaluate(
          query,
          source,
          null,
          XPathResult.FIRST_ORDERED_NODE_TYPE,
          null
        ).singleNodeValue;
        if (!mTable) return mTable;

        //Special case: Danbooru's /favorites lacks the extra DIV that /posts has, which causes issues with the paginator/page break.
        var xDiv = document.createElement("div");
        xDiv.style.overflow = "hidden";
        mTable.parentNode.insertBefore(xDiv, mTable);
        xDiv.appendChild(mTable);
        return xDiv;
      };
    })(xpath[i]);

    var result = getMainTable(source);
    if (result) {
      //alert("UPW main table query: "+xpath[i]+"\n\n"+location.pathname);
      return result;
    }
  }

  return null;
}

function getNextPage(doc = document) {
  return (doc.querySelector("a.next_page") || {}).href;
}

function testScrollPosition() {
  if (!nextPage) return;

  //Take the max of the two heights for browser compatibility
  if (
    !pending.value &&
    window.pageYOffset + window.innerHeight + scrollBuffer >
      Math.max(
        document.documentElement.scrollHeight,
        document.documentElement.offsetHeight
      )
  ) {
    console.log("loading " + nextPage);
    pending.value = true;
    timeout = setTimeout(function() {
      pending.value = false;
      testScrollPosition();
    }, timeToFailure);
    iframe.contentDocument.location.replace(nextPage);
  }
}

function appendNewContent() {
  //Make sure page is correct.  Using 'indexOf' instead of '!=' because links like "https://danbooru.donmai.us/pools?page=2&search%5Border%5D=" become "https://danbooru.donmai.us/pools?page=2" in the iframe href.
  clearTimeout(timeout);
  if (nextPage.indexOf(iframe.contentDocument.location.href) < 0) {
    setTimeout(function() {
      pending.value = false;
    }, 1000);
    return;
  }

  let images = getImages(iframe.contentDocument);
  addImages(images);

  if (!images.length) nextPage = null;
  else {
    nextPage = getNextPage(iframe.contentDocument);
  }

  if (nextPage) {
    history.pushState({}, iframe.contentDocument, nextPage);
  } else {
    // TODO: end of pages
    console.log("End of pages");
  }

  pending.value = false;
  testScrollPosition();
}

function injectGlobalStyle() {
  const s = document.createElement("style");
  s.innerHTML = `
body { padding: 0; }
#header { margin: 0 !important; text-align: center; }
#header ul { float: none !important; display: inline-block;}
#content > div:first-child > div.sidebar { position: fixed; left: 0; top: 0; bottom: 0; overflow: auto !important; z-index: 2; width: 200px !important; transform: translate(-200px, calc(-100vh + 30px)); border-bottom-right-radius: 30px; background: #171717dd; transition: all .2s ease-out; float: none !important; padding: 15px; }
#content > div:first-child > div.sidebar:hover { transform: translateX(0); }
div.content { width: 100vw; text-align: center; float: none }
div.footer { clear: both !important; }
div#paginator a { border: none; }
#comments { max-width: unset !important; width: unset !important; padding: 20px; }
.avatar { border-radius: 1000px; }
form textarea { color: white; background: inherit; padding: 10px 5px; }
.comment .content { text-align: left; }
`;
  document.body.appendChild(s);
}

function injectStyle() {
  const s = document.createElement("style");
  s.innerHTML = `
#gallery .row { width: 100vw; white-space: nowrap; height: var(--image-height); --image-height: 300px; }
#gallery .row .thumb { position: relative; display: inline-block; transition: .2s ease-out; overflow: hidden; }
#gallery .row .thumb.liked::after { position: absolute; top: 3px; right: 3px; content: url('https://api.iconify.design/mdi:cards-heart.svg?color=%23f37e92&height=20'); vertical-align: -0.125em; }
#gallery .row .thumb:first-child { transform-origin: left; }
#gallery .row .thumb:last-child { transform-origin: right; }
#gallery .row .thumb img { height: var(--image-height); }
#gallery .row:hover { z-index: 1; }
#gallery .row .thumb:hover { transform: scale(1.3); z-index: 1; opacity: 1; box-shadow: 8px 8px 100px 10px rgba(0, 0, 0, 0.8); border-radius: 5px;  }

#loader { padding: 10px; text-align: center; }

.hidden { display: none !important; }
.preivew-dialog { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; background: rgba(0,0,0,0.7); z-index: 100; }
.preivew-dialog iframe { position: absolute; height: 90vh; width: 80vw; top: 50%; left: 50%; transform: translate(-50%, -50%); background: grey; border: none; border-radius: 5px; overflow: hidden; }
.preivew-dialog .image-host { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; overflow: auto; text-align: center; }
.preivew-dialog .image-host img { margin: auto; }
.preivew-dialog .image-host img.loading { filter: blur(3px); height: 100vh; }
.preivew-dialog .image-host.full { overflow: hidden }
.preivew-dialog .image-host.full img { max-width: 100vw; max-height: 100vh; }
`;
  document.body.appendChild(s);
}

function initPreviewIframe() {
  previewDialog = document.createElement("div");
  previewDialog.addClassName("preivew-dialog hidden");
  previewDialog.onclick = e => {
    if (e.target === previewDialog)
      previewDialog.classList.toggle("hidden", true);
  };
  window.onkeydown = e => {
    if (!previewDialog.classList.contains("hidden")) {
      if (e.key === "ArrowLeft") {
        currentImage = Math.max(0, currentImage - 1);
        openImage(currentImage);
        e.preventDefault();
      }
      if (e.key === "ArrowRight") {
        currentImage = Math.min(imagesList.length - 1, currentImage + 1);
        openImage(currentImage);
        e.preventDefault();
      }
      if (e.key === "Escape") {
        previewDialog.classList.toggle("hidden", true);
        e.preventDefault();
      }
      if (e.key === "Tab") {
        openImage(currentImage, "page");
        e.preventDefault();
      }
      if (e.code === "Space") {
        previewImageDiv.classList.toggle("full");
        e.preventDefault();
      }
      if (e.code === "KeyL") {
        like(currentImage, 3);
        e.preventDefault();
      }
      if (e.code === "KeyU") {
        unlike(currentImage, 2);
        e.preventDefault();
      }
    }
  };

  previewIframe = document.createElement("iframe");
  previewImageDiv = document.createElement("div");
  previewImageDiv.className = "image-host full";
  previewImage = document.createElement("img");

  previewImageDiv.onclick = e => {
    previewDialog.classList.toggle("hidden", true);
  };

  previewDialog.appendChild(previewIframe);
  previewImageDiv.appendChild(previewImage);
  previewDialog.appendChild(previewImageDiv);
  document.body.appendChild(previewDialog);
}

function ref(v, handler) {
  let value = v;
  return new Proxy(
    {},
    {
      get(obj, prop) {
        return value;
      },
      set(obj, prop, v) {
        if (value !== v) {
          value = v;
          handler(value);
        }
      }
    }
  );
}

function getImages(doc = document) {
  const result = Array.from(
    doc.querySelectorAll("ul#post-list-posts > li")
  ).map(li => {
    const page = (li.querySelector("a.thumb") || {}).href;
    const thumb = (li.querySelector("a.thumb img") || {}).src;
    const large = (li.querySelector("a.largeimg") || {}).href || (li.querySelector("a.smallimg") || {}).href;
    const id = page.split("/").slice(-1)[0];
    const resText = (li.querySelector(".directlink-res") || {}).innerText;
    let res = undefined;
    if (resText && resText.includes("x")) {
      let [height, width] = resText.split(" x ").map(i => +i);
      res = { height, width, radio: width / height };
    }
    if (viewingFavorites) setLiked(id, true);
    let liked = isLiked(id);

    return { page, thumb, large, id, res, liked };
  });
  doc.getElementById("post-list-posts").remove();
  return result;
}

function initDOM() {
  const list = document.getElementById("post-list");
  const gallery = document.createElement("div");
  gallery.id = "gallery";
  list.appendChild(gallery);
  const loader = document.createElement("div");
  loader.id = "loader";
  loader.innerText = "Loading...";
  list.appendChild(loader);
}

function addImages(images) {
  const gallery = document.getElementById("gallery");
  const RADIO = Math.round(window.innerWidth / SUGGEST_WIDTH);
  images.forEach((info, i) => {
    let idx = imagesList.length + i;
    let row = gallery.querySelector(".row:last-child:not(.full)");
    if (!row) {
      row = document.createElement("div");
      row.className = "row";
      gallery.appendChild(row);
    }
    row.dataset.width = +(row.dataset.width || 0) + 1 / info.res.radio;

    if (+row.dataset.width >= RADIO) {
      row.classList.toggle("full", true);
      row.style = `--image-height: calc(100vw / ${row.dataset.width})`;
    }

    const thumb = document.createElement("div");
    thumb.className = "thumb";
    row.appendChild(thumb);

    const img = document.createElement("img");
    img.src = info.thumb;
    thumb.appendChild(img);
    info.dom = thumb;
    thumb.classList.toggle("liked", info.liked);

    let lastClicked = -Infinity;
    let timer = null;
    img.onclick = e => {
      e.preventDefault();
      // double click
      if (Date.now() - lastClicked < 300) {
        if (info.liked) unlike(idx);
        else like(idx);

        clearTimeout(timer);
        // click
      } else {
        lastClicked = +Date.now();
        timer = setTimeout(() => openImage(idx), 400);
      }
      return false;
    };
  });
  imagesList.push(...images);
}

function openImage(idx, type = "image") {
  currentImage = idx;
  const img = imagesList[idx];
  const { page, large, id, thumb } = img;
  if (!previewIframe) initPreviewIframe();

  if (!large) type = "page";

  if (type === "image") {
    previewImage.classList.toggle("loading", true);
    previewImage.src = thumb;
    previewImage.onload = () => {
      previewImage.src = large;
      previewImage.onload = () => {
        previewImage.classList.toggle("loading", false);
        previewImage.onload = null;
      };
    };
    previewIframe.classList.toggle("hidden", true);
    previewImageDiv.classList.toggle("hidden", false);
  } else {
    previewIframe.onload = () => {
      if (
        previewIframe.contentWindow.location.href !== page &&
        !previewIframe.contentWindow.location.pathname.startsWith("/post/show/")
      ) {
        location.href = previewIframe.contentWindow.location.href;
        previewDialog.classList.toggle("hidden", true);
        previewIframe.onload = null;
      }
    };
    previewIframe.src = page;
    previewIframe.classList.toggle("hidden", false);
    previewImageDiv.classList.toggle("hidden", true);
  }
  previewDialog.classList.toggle("hidden", false);
}

function getFavoritesLike() {
  return document.querySelector(".user .submenu li:nth-child(3) a").href;
}

function isViewingFavorites() {
  let fav = getFavoritesLike();
  if (!fav) return false;

  let a = (new URL(fav).searchParams.get("tags") || "")
    .toLowerCase()
    .split(" ")
    .sort();
  let b = (new URL(location.href).searchParams.get("tags") || "")
    .toLowerCase()
    .split(" ")
    .sort();

  return a[0] == b[0] && a[1] == b[1];
}

async function vote(id, score) {
  let body = new FormData();
  body.append("id", id);
  body.append("score", score);
  const rawResponse = await fetch("https://yande.re/post/vote.json", {
    method: "POST",
    headers: {
      "X-CSRF-Token": document.querySelector("meta[name=csrf-token]").attributes
        .content.value
    },
    body
  });
  const content = await rawResponse.json();
}

function readLikeList() {
  return Object.fromEntries(
    (localStorage.getItem(STORAGE_KEY) || "").split(",").map(i => [i, true])
  );
}

function isLiked(id) {
  return !!likedList[id];
}

function setLiked(id, v) {
  likedList[id] = v;
  localStorage.setItem(
    STORAGE_KEY,
    Object.entries(likedList)
      .map(([i, v]) => (v ? i : null))
      .filter(i => i)
      .join(",")
  );
}

function like(idx) {
  let image = imagesList[idx];
  vote(image.id, 3);
  image.liked = true;
  setLiked(image.id, true);
  image.dom.classList.toggle("liked", image.liked);
}

function unlike(idx) {
  let image = imagesList[idx];
  vote(image.id, 2);
  image.liked = false;
  setLiked(image.id, false);
  image.dom.classList.toggle("liked", image.liked);
}