您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Keyboard navigation, inertial drag scrolling, chapter preloading and chapter tracking
当前为
// ==UserScript== // @name Usability Tweaks for Asura Scans // @namespace Itsnotlupus Industries // @match https://www.asurascans.com/* // @match https://asura.gg/* // @noframes // @version 1.4 // @author Itsnotlupus // @license MIT // @description Keyboard navigation, inertial drag scrolling, chapter preloading and chapter tracking // @require https://greasyfork.org/scripts/468394-itsnotlupus-tiny-utilities/code/utils.js // ==/UserScript== /* jshint esversion:11 */ // fixConsole(); 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, .postbody>article>#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 badge on bookmark items showing the number of unread chapters */ .unread-badge { position: absolute; top: 0; right: 0; z-index: 9999; display: block; padding: 2px; margin: 5px; border: 1px solid #0005b1; border-radius: 12px; background: #ffc700; color: #0005b1; font-weight: bold; font-family: cursive; transform: rotate(10deg); width: 24px; height: 24px; line-height: 18px; text-align: center; } .soralist .unread-badge { position: initial; display: inline-block; zoom: 0.8; } `); // 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 // XXX If we used GM APIs to store this instead (and stored localStorage.bookmark there too), TamperMonkey would be able to sync across browsers and devices. const LAST_READ_CHAPTER_KEY = "lastReadChapter"; const SERIES_ID_HREF_MAP = "seriesIdHrefMap"; const SERIES_ID_LATEST_MAP = "seriesIdLatestMap"; const lastReadChapters = JSON.parse(localStorage.getItem(LAST_READ_CHAPTER_KEY) ?? "{}"); const seriesIdHrefMap = JSON.parse(localStorage.getItem(SERIES_ID_HREF_MAP) ?? "{}"); const seriesIdLatestMap = JSON.parse(localStorage.getItem(SERIES_ID_LATEST_MAP) ?? "{}"); 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 getSeriesId(post_id, href) { if (post_id) { seriesIdHrefMap[href] = post_id; localStorage.setItem(SERIES_ID_HREF_MAP, JSON.stringify(seriesIdHrefMap)); } else { post_id = seriesIdHrefMap[href]; } return post_id; } function getLatestChapter(post_id, chapter) { if (chapter) { seriesIdLatestMap[post_id] = chapter; localStorage.setItem(SERIES_ID_LATEST_MAP, JSON.stringify(seriesIdLatestMap)); } else { chapter = seriesIdLatestMap[post_id]; } return chapter; } // new UI elements 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); } // series card decorations, used in bookmarks and manga lists pages. const CHAPTER_REGEX = /\bChapter (?<chapter>\d+)\b/i; function decorateCards(reorder = true) { $$$("//div[contains(@class, 'listupd')]//div[contains(@class, 'bsx')]/..").reverse().forEach(b => { const post_id = getSeriesId(b.firstElementChild.dataset.id, $('a', b).href); const latest_chapter = getLatestChapter(post_id, +$('.epxs',b).textContent.match(CHAPTER_REGEX)?.groups.chapter); const { number, id } = getLastReadChapter(post_id); if (id) { const unreadChapters = latest_chapter - number; if (unreadChapters) { // reorder bookmark, link directly to last read chapter and slap an unread count badge. if (reorder) b.parentElement.prepend(b); $('a',b).href = '/?p=' + id; $('.limit',b).prepend(crel('div', { className: 'unread-badge', textContent: unreadChapters<100 ? unreadChapters : '💀', title: `${unreadChapters} unread chapter${unreadChapters>1?'s':''}` })) } 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. } }); } // text-mode /manga/ page. put badges at the end of each series title, and strike through what's already read. function decorateText() { $$`.soralist a.series`.forEach(a => { const post_id = getSeriesId(a.rel, a.href); const latest_chapter = getLatestChapter(post_id); const { number, id } = getLastReadChapter(post_id); if (id) { const unreadChapters = latest_chapter - number; if (unreadChapters) { a.href = '/?p=' + id; a.append(crel('div', { className: 'unread-badge', textContent: unreadChapters<100 ? unreadChapters : '💀', title: `${unreadChapters} unread chapter${unreadChapters>1?'s':''}` })) } else { // nothing new to read here. gray it out. a.style = 'text-decoration: line-through;color: #777' } } }) } // page specific tweaks 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); } } 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 yet from bookmark API. show a fallback. $`#bookmark-pool`.innerHTML = localStorage.bookmarkHTML ?? ''; // add a marker so we know this is just a cached rendering. $`#bookmark-pool [data-id]`.classList.add('cached'); // decorate what we have. decorateCards(); } }, 1000); // wait until we get bookmark markup from the server, not cached. await untilDOM("#bookmark-pool .bs:first-child [data-id]:not(.cached)"); // bookmarks' ajax API is flaky (/aggressively rate-limited) - mitigate. localStorage.bookmarkHTML = $`#bookmark-pool`.innerHTML; decorateCards(); })(); else { // try generic decorations on any non-bookmark page decorateCards(false); decorateText(); } if ($`#chapterlist`) { // Add a "Continue Reading" button on main series pages. const post_id = $`.bookmark`.dataset.id; const { number, id } = getLastReadChapter(post_id); // add a "Continue Reading" button for series we recognize if (id) { $`.lastend`.prepend(crel('div', { className: 'inepcx', style: 'width: 100%' }, crel('a', { href: '/?p=' + id }, crel('span', {}, 'Continue Reading'), crel('span', { className: 'epcur' }, 'Chapter ' + number)) )); } } // Tweak footer content on any page that has them // 1. collapse related series. const related = $$$("//span[text()='Related Series']/../../..")[0]; if (related) { makeCollapsedFooter({label: 'Show Related Series', section: related}); related.style.display = 'none'; } // 2. collapse comments. const comments = $`#comments`; if (comments) makeCollapsedFooter({label: 'Show Comments', section: comments});