yande.re refine

Refining yande.re

Verze ze dne 09. 05. 2020. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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/*2
// @include        *://yande.re/*
// @include        *://chan.sankakucomplex.com/*
// @version        2020.04.10.b
// @grant          none
// ==/UserScript==

// You can found them on https://gist.github.com/jimmywarting/ac1be6ea0297c16c477e17f8fbe51347
const CORS_PROXY = 'https://cors-anywhere.herokuapp.com/'
const CACHE_ENABLED = true
const CORS_ENABLED = true
const CACHE_ONLY_LIKE = true

const STORAGE_KEY = "yandere-refine-liked";
const INDEXED_DB_NAME = "yandere-refine-cache";
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;
let memcache = {}

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

    if (CACHE_ENABLED){
    const script = document.createElement('script')
    script.src = 'https://unpkg.com/dexie@latest/dist/dexie.js'
    script.onload = ()=>{
        db = new Dexie(INDEXED_DB_NAME);

        db.version(1).stores({
            images: 'id, blob'
        });
    }
    document.head.appendChild(script)
    }
}

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

function awaitImage(img){
    if (img.completed)
      return
    return new Promise((resolve)=>{
      img.onload = ()=>{
          resolve(img)
          img.onload = undefined
      }
    })
}

async function setCache(id, img) {
    if (!db)
        return false

    console.log('Caching ' + id)
    const canvas = document.createElement("canvas");
    canvas.width = img.naturalWidth;
    canvas.height = img.naturalHeight;

    const ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);


    const blob = await new Promise((resolve)=>{
         canvas.toBlob(resolve);
    })
    delete canvas

    await db.images.put({id, blob})
    return true
}

async function getCache(id){
    if (memcache[id])
      return URL.createObjectURL(memcache[id]);
    if (!db)
      return
    const data = await db.images.get(id)
    if (data && data.blob) {
        console.log('Loading cache of ' + id)
        memcache[id] = data.blob
        return URL.createObjectURL(data.blob);
    }
}

async function removeCache(id){
    if (!db)
      return
    const data = await db.images.delete(id)
}

//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 getProxiedUrl(url){
    if (!CORS_ENABLED)
        return url;
    return CORS_PROXY +url;
}


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

  if (!large) type = "page";

  previewDialog.classList.toggle("hidden", false);
  previewImage.dataset.id = id;

  if (type === "image") {
      previewImage.crossOrigin = null
      const cache = await getCache(id);
      if (cache) {
           previewImage.src = cache;
           // show image
           previewIframe.classList.toggle("hidden", true);
           previewImageDiv.classList.toggle("hidden", false);
      }
      else {
          // show image
          previewIframe.classList.toggle("hidden", true);
          previewImageDiv.classList.toggle("hidden", false);

          // thumbnail
          previewImage.classList.toggle("loading", true);
          previewImage.src = thumb;
          await awaitImage(previewImage)

          // full image
          if (CORS_ENABLED)
              previewImage.crossOrigin = "Anonymous"
          let url = getProxiedUrl(large)
          previewImage.src = url
          await awaitImage(previewImage)
          previewImage.classList.toggle("loading", false);

          // image changed
          if (previewImage.dataset.id != id)
              return

          // cache
          if (!CACHE_ONLY_LIKE || isLiked(id))
              await setCache(id, previewImage);
      }
  } else {
    previewIframe.src = page;
    previewIframe.classList.toggle("hidden", false);
    previewImageDiv.classList.toggle("hidden", true);
    await awaitImage(previewIframe);
      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;
      }
  }
}

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);
  removeCache(image.id)
}