Try multiple times if a page request fail, add initial loading screen
This commit is contained in:
parent
bb7abd544a
commit
a571960b5c
6 changed files with 159 additions and 57 deletions
|
@ -3,7 +3,8 @@ import speen from "~/assets/images/speen.svg";
|
|||
import { getPageHTML } from "./wiki";
|
||||
import { processHTML, bindFunctions, CURRENT_VERSION } from "./userscript";
|
||||
import cache from "./cache";
|
||||
import { nextAnimationFrame } from "./utils";
|
||||
import { nextAnimationFrame, delay } from "./utils";
|
||||
import { TabInfo } from "./sections";
|
||||
|
||||
// @ts-expect-error: Parcel image import
|
||||
import unknown from "~/assets/images/tab-icons/unknown.svg";
|
||||
|
@ -15,15 +16,10 @@ function initWaiting(elem: HTMLElement) {
|
|||
const spinnerImg = document.createElement("img");
|
||||
spinnerImg.src = speen;
|
||||
spinnerContainer.appendChild(spinnerImg);
|
||||
const spinnerText = document.createElement("p");
|
||||
spinnerText.appendChild(
|
||||
document.createTextNode("You start skimming through the manual...")
|
||||
);
|
||||
spinnerContainer.appendChild(spinnerText);
|
||||
elem.appendChild(spinnerContainer);
|
||||
}
|
||||
|
||||
async function loadPage(page: string, elem: HTMLElement) {
|
||||
async function loadPage(page: string, elem: HTMLElement): Promise<HTMLElement> {
|
||||
let html: string | null = null;
|
||||
const key = `page:${page}`;
|
||||
|
||||
|
@ -45,7 +41,18 @@ async function loadPage(page: string, elem: HTMLElement) {
|
|||
// Fetch page content
|
||||
if (!html) {
|
||||
console.log(`${page}: fetching`);
|
||||
html = await getPageHTML(page);
|
||||
let retries = 0;
|
||||
while (retries < 5) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
html = await getPageHTML(page);
|
||||
break;
|
||||
} catch (e) {
|
||||
retries += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert relative links to absolute (and proxied)
|
||||
html = html.replace(/"\/wiki/gi, '"//tgproxy.ovo.ovh/wiki');
|
||||
|
@ -53,15 +60,14 @@ async function loadPage(page: string, elem: HTMLElement) {
|
|||
await nextAnimationFrame();
|
||||
|
||||
// Set as HTML content and run HTML manipulations on it
|
||||
const div = document.createElement("div");
|
||||
div.className = elem.className;
|
||||
const div = elem.cloneNode(false) as HTMLDivElement;
|
||||
div.innerHTML = html;
|
||||
|
||||
console.log(`${page}: processing`);
|
||||
processHTML(div, page);
|
||||
|
||||
// Save result to cache
|
||||
cache.set(key, div.outerHTML, CURRENT_VERSION).then(() => {
|
||||
cache.set(key, div.innerHTML, CURRENT_VERSION).then(() => {
|
||||
console.log(`${page}: saved to cache`);
|
||||
});
|
||||
|
||||
|
@ -69,12 +75,13 @@ async function loadPage(page: string, elem: HTMLElement) {
|
|||
elem = div;
|
||||
} else {
|
||||
// Set cached content as HTML
|
||||
elem.outerHTML = html;
|
||||
elem.innerHTML = html;
|
||||
}
|
||||
|
||||
bindFunctions(elem, page);
|
||||
console.log(`${page}: userscript applied`);
|
||||
elem.classList.remove("waiting");
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
type TabElements = {
|
||||
|
@ -97,6 +104,8 @@ export default class TabManager {
|
|||
|
||||
sections: Record<string, Section> = {};
|
||||
|
||||
loading: boolean;
|
||||
|
||||
constructor(
|
||||
sectionlist: HTMLElement,
|
||||
tablist: HTMLElement,
|
||||
|
@ -107,6 +116,25 @@ export default class TabManager {
|
|||
this.tabContentContainer = tabcontent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set app-wide loading state
|
||||
* @param value is app still loading?
|
||||
*/
|
||||
setLoading(value: boolean): void {
|
||||
if (value) {
|
||||
document.getElementById("app").classList.add("waiting");
|
||||
initWaiting(this.tabContentContainer);
|
||||
const spinnerContainer = this.tabContentContainer.querySelector(".speen");
|
||||
spinnerContainer.appendChild(
|
||||
document.createTextNode("Loading wiki pages")
|
||||
);
|
||||
} else {
|
||||
document.getElementById("app").classList.remove("waiting");
|
||||
const elem = this.tabContentContainer.querySelector(".speen");
|
||||
this.tabContentContainer.removeChild(elem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create section and add it to the section list
|
||||
* @param name Section name
|
||||
|
@ -161,7 +189,7 @@ export default class TabManager {
|
|||
* @param icon Icon to show
|
||||
* @param setActive Also set the tab as active
|
||||
*/
|
||||
openTab(
|
||||
async openTab(
|
||||
section: string,
|
||||
page: string,
|
||||
options: {
|
||||
|
@ -169,7 +197,8 @@ export default class TabManager {
|
|||
active?: boolean;
|
||||
text?: string;
|
||||
}
|
||||
): void {
|
||||
): Promise<void> {
|
||||
const { icon, active, text } = options;
|
||||
// Create tab list item
|
||||
const tabListItem = document.createElement("div");
|
||||
tabListItem.className = "tab";
|
||||
|
@ -182,11 +211,10 @@ export default class TabManager {
|
|||
this.setActive(section, page);
|
||||
});
|
||||
const iconElement = document.createElement("img");
|
||||
iconElement.src = options.icon || unknown;
|
||||
iconElement.src = icon || unknown;
|
||||
tabListItem.title = page.replace(/_/gi, " ");
|
||||
tabListItem.appendChild(iconElement);
|
||||
const shortTitle =
|
||||
options.text || page.substr(page.lastIndexOf("_") + 1, 4);
|
||||
const shortTitle = text || page.substr(page.lastIndexOf("_") + 1, 4);
|
||||
tabListItem.appendChild(document.createTextNode(shortTitle));
|
||||
this.tabListContainer.appendChild(tabListItem);
|
||||
|
||||
|
@ -196,9 +224,6 @@ export default class TabManager {
|
|||
tabContentItem.dataset.tab = page;
|
||||
initWaiting(tabContentItem);
|
||||
|
||||
// Start loading page for new tab
|
||||
loadPage(page, tabContentItem);
|
||||
|
||||
this.tabContentContainer.appendChild(tabContentItem);
|
||||
|
||||
// Create tab entry
|
||||
|
@ -209,8 +234,15 @@ export default class TabManager {
|
|||
tabListItem.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Start loading page for new tab
|
||||
const elem = await loadPage(page, tabContentItem);
|
||||
// Since element can be replaced (when loading for the first time), make sure the reference is updated
|
||||
if (elem !== tabContentItem) {
|
||||
this.sections[section].tabs[page].tabContentItem = elem;
|
||||
}
|
||||
|
||||
// If asked for, set it to active
|
||||
if (options.active) {
|
||||
if (active) {
|
||||
this.setActive(section, page);
|
||||
}
|
||||
}
|
||||
|
|
67
src/index.ts
67
src/index.ts
|
@ -1,25 +1,58 @@
|
|||
import TabManager from "./TabManager";
|
||||
import sections from "./sections";
|
||||
import { nextAnimationFrame } from "./utils";
|
||||
|
||||
const sectionListContainer = document.getElementById("section-list");
|
||||
const tabListContainer = document.getElementById("tab-list");
|
||||
const tabContentContainer = document.getElementById("tabs");
|
||||
const manager = new TabManager(
|
||||
sectionListContainer,
|
||||
tabListContainer,
|
||||
tabContentContainer
|
||||
);
|
||||
// @ts-expect-error: Parcel image import
|
||||
import unknown from "~/assets/images/tab-icons/unknown.svg";
|
||||
|
||||
sections.forEach((section) => {
|
||||
manager.createSection(section.name);
|
||||
section.tabs.forEach((tab) => {
|
||||
manager.openTab(section.name, tab.page, { icon: tab.icon, text: tab.text });
|
||||
async function load() {
|
||||
const sectionListContainer = document.getElementById("section-list");
|
||||
const tabListContainer = document.getElementById("tab-list");
|
||||
const tabContentContainer = document.getElementById("tabs");
|
||||
const manager = new TabManager(
|
||||
sectionListContainer,
|
||||
tabListContainer,
|
||||
tabContentContainer
|
||||
);
|
||||
manager.setLoading(true);
|
||||
|
||||
await nextAnimationFrame();
|
||||
|
||||
// Add loading "bar"
|
||||
const spinnerContainer = document.querySelector("#tabs > .speen");
|
||||
const icons = document.createElement("div");
|
||||
icons.className = "loading-icons";
|
||||
sections.forEach((section) =>
|
||||
section.tabs.forEach((tab) => {
|
||||
const iconElement = document.createElement("img");
|
||||
iconElement.dataset.tab = tab.page;
|
||||
iconElement.src = tab.icon || unknown;
|
||||
iconElement.title = tab.page.replace(/_/gi, " ");
|
||||
icons.appendChild(iconElement);
|
||||
})
|
||||
);
|
||||
spinnerContainer.appendChild(icons);
|
||||
|
||||
const promises = sections.flatMap((section) => {
|
||||
manager.createSection(section.name);
|
||||
return section.tabs.map(async (tab) => {
|
||||
// Load page
|
||||
await manager.openTab(section.name, tab.page, {
|
||||
icon: tab.icon,
|
||||
text: tab.text,
|
||||
});
|
||||
// Remove icon from loading
|
||||
icons.removeChild(icons.querySelector(`img[data-tab=${tab.page}]`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Set first page as active
|
||||
manager.setActive("Medical", "Guide_to_chemistry");
|
||||
Promise.all(promises).then(() => {
|
||||
// Remove app-wide loading
|
||||
manager.setLoading(false);
|
||||
|
||||
// Set first page as active
|
||||
manager.setActive("Medical", "Guide_to_chemistry");
|
||||
});
|
||||
}
|
||||
if ("serviceWorker" in navigator) {
|
||||
const x = process.env.SUBPATH ? `${process.env.SUBPATH}/sw.js` : "sw.js";
|
||||
navigator.serviceWorker
|
||||
|
@ -31,3 +64,5 @@ if ("serviceWorker" in navigator) {
|
|||
console.log("Service worker registration failed, error:", error);
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
|
|
|
@ -4,12 +4,9 @@ import { findParent } from "./utils";
|
|||
|
||||
// This is used for cache busting when userscript changes significantly.
|
||||
// Only change it when you made changes to the processHTML part!
|
||||
export const CURRENT_VERSION = "e700bb9c309627b944618152a7d8e936ae7a05db";
|
||||
export const CURRENT_VERSION = "bb7abd544a19369d4b6b7e3dde3eb3cc34c023d4";
|
||||
|
||||
function chemistryFixups(root: HTMLElement) {
|
||||
// Enable page-specific CSS rules
|
||||
root.classList.add("bchem");
|
||||
|
||||
// Fix inconsistencies with <p> on random parts
|
||||
// Ideally I'd like a <p> or something on every part, wrapping it completely, but for now let's just kill 'em
|
||||
root
|
||||
|
|
|
@ -16,4 +16,10 @@ export function nextAnimationFrame(): Promise<void> {
|
|||
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
||||
}
|
||||
|
||||
export default { findParent, nextAnimationFrame };
|
||||
export function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), ms);
|
||||
});
|
||||
}
|
||||
|
||||
export default { findParent, nextAnimationFrame, delay };
|
||||
|
|
|
@ -88,11 +88,11 @@ span.bgus_nested_element:not(.bgus_collapsed) + div.tooltiptext {
|
|||
margin-left: -5px;
|
||||
}
|
||||
}
|
||||
.bchem table.wikitable > tbody > tr > td:nth-child(2) {
|
||||
div[data-tab="Guide_to_chemistry"] table.wikitable > tbody > tr > td:nth-child(2) {
|
||||
width: 45%;
|
||||
padding: 10px;
|
||||
}
|
||||
.bchem table.wikitable {
|
||||
div[data-tab="Guide_to_chemistry"] table.wikitable {
|
||||
border: 0 !important;
|
||||
.table-head {
|
||||
text-align: center;
|
||||
|
@ -101,7 +101,7 @@ span.bgus_nested_element:not(.bgus_collapsed) + div.tooltiptext {
|
|||
background-color: darken($nanotrasen, 5%) !important;
|
||||
}
|
||||
}
|
||||
div.bchem .bgus_fz_selected {
|
||||
div[data-tab="Guide_to_chemistry"] .bgus_fz_selected {
|
||||
background: $nanotrasen !important;
|
||||
th,
|
||||
td {
|
||||
|
|
|
@ -20,6 +20,7 @@ body {
|
|||
z-index: 0;
|
||||
img {
|
||||
opacity: 0.4;
|
||||
width: 80vmin;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +38,13 @@ body {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.waiting {
|
||||
#tab-list,
|
||||
#section-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
@ -59,6 +67,20 @@ body {
|
|||
border: 1px solid lighten($nanotrasen, 10%);
|
||||
}
|
||||
|
||||
.speen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12pt;
|
||||
img {
|
||||
width: 60vmin;
|
||||
opacity: 0.9;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
#tabs {
|
||||
flex: 1;
|
||||
z-index: 1;
|
||||
|
@ -85,17 +107,6 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.speen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
img {
|
||||
width: 60vmin;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,3 +222,24 @@ noscript {
|
|||
background: transparent;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading-icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
font-size: 9pt;
|
||||
padding: 3pt 7pt;
|
||||
text-transform: uppercase;
|
||||
flex-wrap: wrap;
|
||||
div {
|
||||
border: 1px solid red;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
max-width: 32px;
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue