Greasy Fork

Greasy Fork is available in English.

Minor cleanups - asurascans.com

Keyboard navigation, inertial drag scrolling, chapter preloading and chapter-tracking bookmarks

当前为 2023-07-14 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Minor cleanups - asurascans.com
// @namespace   Itsnotlupus Industries
// @match       https://www.asurascans.com/*
// @noframes
// @version     1.2
// @author      Itsnotlupus
// @license     MIT
// @description Keyboard navigation, inertial drag scrolling, chapter preloading and chapter-tracking bookmarks
// @require     http://greasyfork.icu/scripts/468394-itsnotlupus-tiny-utilities/code/utils.js
// ==/UserScript==

/* jshint esversion:11 */

// yin yang SVG derived from https://icons8.com/preloaders/en/filtered-search/all/free;svg/
const loading_svg = 'data:image/svg+xml;base64,'+btoa`
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="128" viewBox="0 -128 128 256">
  <circle cx="64" cy="64" r="63.31" fill="#fff"/>
  <g>
    <path d="M3.13 44.22a64 64 0 1 0 80.65-41.1 64 64 0 0 0-80.65 41.1zm34.15-4.83a10.63 10.63 0 1 1-13.4 6.8 10.63 10.63 0 0 1 13.4-6.8zm7.85 82.66A61.06 61.06 0 0 1 5.7 45.86 30.53 30.53 0 0 0 64 64a30.53 30.53 0 0 1 58.3 18.12l.35-1.14-.58 1.9a61.06 61.06 0 0 1-76.94 39.2zM106.9 73.2A10.63 10.63 0 1 0 93.5 80a10.63 10.63 0 0 0 13.4-6.8z"/>
    <animateTransform attributeName="transform" dur="1200ms" from="0 64 64" repeatCount="indefinite" to="-360 64 64" type="rotate"/>
  </g>
</svg>`;

addStyles(`
/* remove ads and blank space between images were ads would have been */
[class^="ai-viewport"], .code-block, .blox, .kln, [id^="teaser"] {
  display: none !important;
}

/* hide various header and footer content. */
.socialts, .chdesc, .chaptertags, .postarea >#comments {
  display: none;
}

/* style a custom button to expand collapsed footer areas */
button.expand {
  float: right;
  border: 0;
  border-radius: 20px;
  padding: 2px 15px;
  font-size: 13px;
  line-height: 25px;
  background: #333;
  color: #888;
  font-weight: bold;
  cursor: pointer;
}
button.expand:hover {
  background: #444;
}

/* disable builtin drag behavior to allow drag scrolling */
* {
  user-select: none;
  -webkit-user-drag: none;
}
body.drag {
  cursor: grabbing;
}

/* add a loading state to the bookmark page so that it doesn't look broken. */
#bookmark-pool {
  /* add a loading animation to avoid image jumps. */
  min-height: 180px;
  background: no-repeat center url('${loading_svg}');
}
#bookmark-pool.loaded {
  background: none;
}
`);

// keyboard navigation. good for long strips, which is apparently all this site has.
const prev = () => $`.ch-prev-btn`?.click();
const next = () => $`.ch-next-btn`?.click();
addEventListener('keydown', e => ({
  ArrowLeft: prev,
  ArrowRight: next,
  KeyA: prev,
  KeyD: next
}[e.code]?.()), true);

// inertial drag scrolling
let [ delta, drag, dragged ] = [0, false, false];
events({
  mousedown() {
    [ delta, drag, dragged ] = [0, true, false];
  },
  mousemove(e) {
    if (drag) {
      scrollBy(0, delta=-e.movementY);
      if (Math.abs(delta)>3) {
        dragged = true;
        document.body.classList.add('drag');
      }
    }
  },
  mouseup(e) {
    if (drag) {
      drag=false;
      rAF((_, next) => Math.abs(delta*=0.95)>1 && next(scrollBy(0, delta)));
    }
    if (dragged) {
      dragged = false;
      document.body.classList.remove('drag');
      const preventClick = e => {
        e.preventDefault();
        e.stopPropagation();
        removeEventListener('click', preventClick, true);
      };
      addEventListener('click', preventClick, true);
    }
  }
});

// don't be shy about loading an entire chapter
$$`img[loading="lazy"]`.forEach(img => img.loading="eager");

// retry loading broken images
const imgBackoff = new Map();
const imgNextRetry = new Map();
const retryImage = img => {
  const now = Date.now();
  const nextRetry = imgNextRetry.has(img) ? imgNextRetry.get(img) : (imgNextRetry.set(img, now),now);
  if (nextRetry <= now) {
    // exponential backoff between retries: 0ms, 250ms, 500ms, 1s, 2s, 4s, 8s, 10s, 10s, ...
    imgBackoff.set(img, Math.min(10000,(imgBackoff.get(img)??125)*2));
    imgNextRetry.set(img, now + imgBackoff.get(img));
    img.src=img.src;
  } else {
    setTimeout(()=>retryImage(img), nextRetry - now);
  }
}
observeDOM(() => {
  [...document.images].filter(img=>img.complete && !img.naturalHeight).forEach(retryImage);
});

// and prefetch the next chapter's images for even less waiting.
const nextURL = $`.ch-next-btn`?.href;
if (nextURL) fetchHTML(nextURL).then(d => [...d.images].forEach(img => prefetch(img.src)));


// have bookmarks track the last chapter you read
const LAST_READ_CHAPTER_KEY = "lastReadChapter";
const lastReadChapters = JSON.parse(localStorage.getItem(LAST_READ_CHAPTER_KEY) ?? "{}");

function getLastReadChapter(post_id, defaultValue = {}) {
  return lastReadChapters[post_id] ?? defaultValue;
}

function setLastReadChapter(post_id, chapter_id, chapter_number) {
  lastReadChapters[post_id] = {
    id: chapter_id,
    number: chapter_number
  };
  localStorage.setItem(LAST_READ_CHAPTER_KEY, JSON.stringify(lastReadChapters));
}

function makeCollapsedFooter({ label, section }) {
  const elt = crel('div', {
    className: 'bixbox',
    style: 'padding: 8px 15px'
  }, crel('button', {
    className: 'expand',
    textContent: label,
    onclick() {
      section.style.display = 'block';
      elt.style.display = 'none';
    }
  }));
  section.parentElement.insertBefore(elt, section);
}

const CHAPTER_REGEX = /\bChapter (?<chapter>\d+)\b/i;

const chapterMatch = document.title.match(CHAPTER_REGEX);
if (chapterMatch) {
  // We're on a chapter page. Save chapter number and id if greater than last saved chapter number.
  const chapter_number = +chapterMatch.groups.chapter;
  const { post_id, chapter_id } = window;
  const { number = 0 } = getLastReadChapter(post_id);
  if (number<chapter_number) {
    setLastReadChapter(post_id, chapter_id, chapter_number);
  }
  // Tweak footer content:
  // 2. collapse related series.
  const related = $`.bixbox > .releases`.parentElement;
  makeCollapsedFooter({label: 'Show Related Series', section: related});
  related.style.display = 'none';
  // 3. collapse comments.
  const comments = $`#comments`;
  makeCollapsedFooter({label: 'Show Comments', section: comments});
}

if (location.pathname == '/bookmark/') (async () => {
  // We're on a bookmark page. Wait for them to load, then tweak them to point to last read chapter, and gray out the ones that are fully read so far.
  setTimeout(()=> {
    if (!$`#bookmark-pool [data-id]`) {
      // no data seen from bookmark API. try a fallback.
        $`#bookmark-pool`.innerHTML = localStorage.bookmarkHTML;
    }
  }, 5000);
  await untilDOM("#bookmark-pool [data-id]");
  // bookmarks' ajax API is flaky (/aggressively rate-limited) - mitigate.
  localStorage.bookmarkHTML = $`#bookmark-pool`.innerHTML;
  // stop loading animation
  $`#bookmark-pool`.classList.add('loaded');
  $$`#bookmark-pool [data-id]`.forEach(b => {
    const post_id = b.dataset.id;
    const latest_chapter = +$('.epxs',b).textContent.match(CHAPTER_REGEX)?.groups.chapter;
    const { number, id } = getLastReadChapter(post_id);
    if (id) {
      if (number < latest_chapter) {
        // change link to last read chapter and move to front of the line.
        const a = $('a',b);
        a.href = '/?p=' + id;
        const holder = b.parentElement;
        holder.parentElement.prepend(holder);
      } else {
        // nothing new to read here. gray it out.
        b.style = 'filter: grayscale(70%);opacity:.9';
      }
    } else {
      // we don't have data on that series. leave it alone.
    }
  });
})();