\d+)\b/i;
async function decorateCards(reorder = true) {
const cards = await untilDOM(() => $$$("//div[contains(@class, 'listupd')]//div[contains(@class, 'bsx')]/.."));
cards.reverse().forEach(b => {
const post_id = getSeriesId(b.firstElementChild.dataset.id, $('a', b).href);
const epxs = $('.epxs',b)?.textContent ?? b.innerHTML.match(/(?.*?)<\/div>/)?.groups.epxs;
const latest_chapter = getLatestChapter(post_id, parseInt(epxs?.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.
async function decorateText() {
const links = await untilDOM(()=>$`.soralist a.series` && $$`.soralist a.series`);
links.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) {
until(()=>unsafeWindow.post_id).then(() => {
// We're on a chapter page. Save chapter number and id if greater than last saved chapter number.
const chapter_number = parseInt(chapterMatch.groups.chapter ?? chapterMatch.groups.ch);
const { post_id, chapter_id } = unsafeWindow;
const { number = 0 } = getLastReadChapter(post_id);
if (number {
// 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 = GM_getValue(BOOKMARK_HTML, 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.
GM_setValue(BOOKMARK_HTML, $`#bookmark-pool`.innerHTML);
decorateCards();
})(); else {
// try generic decorations on any non-bookmark page
decorateCards(false);
decorateText();
}
untilDOM(`#chapterlist`).then(() => {
// 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))
));
}
});
untilDOM(()=>$$$("//span[text()='Related Series' or text()='Similar Series']/../../..")[0]).then(related => {
// Tweak footer content on any page that has them
// 1. collapse related series.
makeCollapsedFooter({label: 'Show Related Series', section: related});
related.style.display = 'none';
});
untilDOM("#comments").then(comments => {
// 2. collapse comments.
makeCollapsedFooter({label: 'Show Comments', section: comments});
});
// This page rotation thingy actually feels good now.
async function rotatePage(clockwise) {
if (rotating) return;
rotating = true;
const html = document.documentElement;
html.style.overflow = "hidden";
const { scrollHeight, scrollTop, scrollWidth, scrollLeft, clientWidth, clientHeight, style } = html;
const oldOriginY = parseInt(style.transformOrigin.split(" ")[1]);
const from0 = (next) => () => {
const originY = scrollTop + clientHeight/2;
style.transformOrigin = `${clientWidth/2}px ${originY}px`;
return next(true);
};
const from90 = (next) => () => {
const originY = scrollHeight - scrollLeft - clientWidth/2 - 1; // rounding error accumulation compensation, or something.
style.transition = "initial";
style.transformOrigin = `${clientWidth/2}px ${originY}px`;
style.transform=`rotate(90deg)`;
scrollBy({top: originY - oldOriginY, behavior:"instant"});
scrollTo({left: 0, behavior: "instant"});
style.transition='';
return next();
};
const from180 = (next) => () => {
const originY = scrollHeight - (scrollTop + clientHeight/2);
style.transformOrigin = `${clientWidth/2}px ${originY}px`;
scrollBy({top: 2*(originY - oldOriginY), behavior:"instant"});
return next();
};
const from270 = (next) => () => {
const originY = scrollLeft + clientHeight/2;
style.transition = "initial";
style.transformOrigin = `${clientWidth/2}px ${originY}px`;
style.transform=`rotate(${ next == to0 ? "-90" : "270" }deg)`;
scrollBy({top: originY - oldOriginY, behavior:"instant"});
scrollTo({left: 0, behavior: "instant"});
style.transition='';
return next();
};
const to0 = () => new Promise(next => {
style.transform="";
html.addEventListener('transitionend', () => {
style.overflow = "";
next(0);
}, {once: true});
});
const to90 = () => new Promise(next => {
style.transform=`rotate(90deg)`;
html.addEventListener('transitionend', () => {
style.transition='initial';
style.transform=`rotate(90deg) translate(0,${html.scrollWidth - scrollHeight}px)`;
scrollTo({left: scrollHeight - html.scrollTop - clientWidth/2 - clientHeight/2, behavior:"instant"});
style.transition=''
style.overflow = "auto hidden";
next(90);
}, {once: true});
});
const to180 = () => new Promise(next => {
style.transform="rotate(180deg)";
html.addEventListener('transitionend', () => {
style.transition = "initial";
// we have to bring the transform origin in the middle of the page, or there will be misfits at the vertical edges (extra space or clipped page)
style.transformOrigin = `${clientWidth/2}px ${scrollHeight/2}px`;
scrollTo({top: scrollHeight - html.scrollTop - clientHeight, behavior:"instant"});
style.transition='';
style.overflow = "hidden auto";
next(180);
}, {once: true});
});
const to270 = (from0) => new Promise(next => {
style.transform=`rotate(${ from0 ? "-90" : "270" }deg)`;
html.addEventListener('transitionend', () => {
style.transition='initial';
style.transform=`rotate(-90deg) translate(0,${html.scrollTop}px)`;
scrollTo({left: html.scrollTop, behavior:"instant"});
style.transition='';
style.overflow = "auto hidden";
next(270);
}, {once: true});
});
const rotations = {
0: [from0(to270), from0(to90)],
90: [from90(to0), from90(to180)],
180: [from180(to90), from180(to270)],
270: [from270(to180), from270(to0)]
}
orientation = await rotations[orientation][~~clockwise]();
rotating = false;
}