// ==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 https://gf.qytechs.cn/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.
}
});
})();