\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) {
// 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});
});
// Add ability to render as landscape, since long form strips have to known to switch to it.
// should there be an upside-down mode too? that seems useless, but it'd be consistent.
// TODO: some of the math here is off. track down and fix.
async function toggleLandscape(dir=1) {
const html = document.documentElement;
function to270deg() {
return new Promise(r => {
landscape = NaN;
const originX = html.clientWidth/2;
const originY = html.scrollTop + html.clientHeight/2;
html.style.transformOrigin = `${originX}px ${originY}px`;
html.style.transform=`rotate(-90deg)`;
html.addEventListener('transitionend', () => {
// after the pretty rotation, shift everything at once to get the scrollbars where they need to be.
html.style.transition='initial';
html.style.transform=`rotate(-90deg) translate(0,${html.scrollTop}px)`;
scrollTo({left: html.scrollTop, behavior:"instant"});
rAF(()=>html.style.transition='');
// scrollbar adjusting. not great.
const w0 = html.clientWidth;
html.style.overflowY="hidden";
const w1 = html.clientWidth;
scrollBy({top:(w0-w1)/2});
landscape = 1;
r();
}, {once: true});
});
}
function from270deg() {
return new Promise(r => {
landscape = NaN;
// shift back to a position where we do a pretty rotation
html.style.overflowY="";
const oldOriginY = parseInt(html.style.transformOrigin.split(" ")[1])
const originX = html.clientWidth/2;
const originY = html.scrollLeft + html.clientWidth/2;
html.style.transition = "initial";
html.style.transformOrigin = `${originX}px ${originY}px`;
html.style.transform=`rotate(-90deg)`;
scrollBy({left: originY - oldOriginY, top: originY - oldOriginY, behavior:"instant"});
scrollTo({left: 0, behavior: "instant"});
// ready to rotate back to portrait
html.style.transition='';
html.style.transform="";
html.addEventListener('transitionend', () => {
landscape = false;
r();
}, {once: true});
});
}
async function to90deg() {
return new Promise(r => {
landscape = NaN;
const originX = html.clientWidth/2;
const originY = html.scrollTop + html.clientHeight/2;
html.style.transformOrigin = `${originX}px ${originY}px`;
html.style.transform=`rotate(90deg)`;
html.style.overflowY="hidden";
html.addEventListener('transitionend', () => {
// after the pretty rotation, shift everything at once to get the scrollbars where they need to be.
html.style.transition='initial';
html.style.transform=`rotate(90deg) translate(0,${originY + innerWidth/2 - html.scrollHeight}px)`;
scrollTo({left: html.scrollHeight - html.scrollTop - innerWidth + (innerWidth - innerHeight)/2, behavior:"instant"});
rAF(()=>html.style.transition='');
// scrollbar adjusting. not great.
const w0 = html.clientWidth;
html.style.overflowY="hidden";
const w1 = html.clientWidth;
scrollBy({top:(w1-w0)/2});
landscape = 2;
r();
}, {once: true});
});
}
async function from90deg() {
return new Promise(r => {
landscape = NaN;
// shift back to a position where we do a pretty rotation
const oldOriginY = parseInt(html.style.transformOrigin.split(" ")[1])
const originX = html.clientWidth/2;
const originY = html.scrollHeight - html.scrollLeft - html.clientWidth/2;
html.style.transition = "initial";
html.style.transformOrigin = `${originX}px ${originY}px`;
html.style.transform=`rotate(90deg)`;
scrollBy({left: originY - oldOriginY, top: originY - oldOriginY, behavior:"instant"});
scrollTo({left: 0, behavior: "instant"});
// ready to rotate back to portrait
html.style.transition='';
html.style.transform="";
html.style.overflowY="";
html.addEventListener('transitionend', () => {
landscape = false;
r();
}, {once: true});
});
}
switch (landscape) {
case false:
dir == 1 ? await to270deg() : await to90deg();
break;
case 1:
await from270deg();
if (dir == 1) await to90deg();
break;
case 2:
await from90deg();
if (dir != 1) await to270deg();
break;
}
}