forked from hamcha/tghandbook
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 { getPageHTML } from "./wiki";
|
||||||
import { processHTML, bindFunctions, CURRENT_VERSION } from "./userscript";
|
import { processHTML, bindFunctions, CURRENT_VERSION } from "./userscript";
|
||||||
import cache from "./cache";
|
import cache from "./cache";
|
||||||
import { nextAnimationFrame } from "./utils";
|
import { nextAnimationFrame, delay } from "./utils";
|
||||||
|
import { TabInfo } from "./sections";
|
||||||
|
|
||||||
// @ts-expect-error: Parcel image import
|
// @ts-expect-error: Parcel image import
|
||||||
import unknown from "~/assets/images/tab-icons/unknown.svg";
|
import unknown from "~/assets/images/tab-icons/unknown.svg";
|
||||||
|
@ -15,15 +16,10 @@ function initWaiting(elem: HTMLElement) {
|
||||||
const spinnerImg = document.createElement("img");
|
const spinnerImg = document.createElement("img");
|
||||||
spinnerImg.src = speen;
|
spinnerImg.src = speen;
|
||||||
spinnerContainer.appendChild(spinnerImg);
|
spinnerContainer.appendChild(spinnerImg);
|
||||||
const spinnerText = document.createElement("p");
|
|
||||||
spinnerText.appendChild(
|
|
||||||
document.createTextNode("You start skimming through the manual...")
|
|
||||||
);
|
|
||||||
spinnerContainer.appendChild(spinnerText);
|
|
||||||
elem.appendChild(spinnerContainer);
|
elem.appendChild(spinnerContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPage(page: string, elem: HTMLElement) {
|
async function loadPage(page: string, elem: HTMLElement): Promise<HTMLElement> {
|
||||||
let html: string | null = null;
|
let html: string | null = null;
|
||||||
const key = `page:${page}`;
|
const key = `page:${page}`;
|
||||||
|
|
||||||
|
@ -45,7 +41,18 @@ async function loadPage(page: string, elem: HTMLElement) {
|
||||||
// Fetch page content
|
// Fetch page content
|
||||||
if (!html) {
|
if (!html) {
|
||||||
console.log(`${page}: fetching`);
|
console.log(`${page}: fetching`);
|
||||||
|
let retries = 0;
|
||||||
|
while (retries < 5) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
html = await getPageHTML(page);
|
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)
|
// Convert relative links to absolute (and proxied)
|
||||||
html = html.replace(/"\/wiki/gi, '"//tgproxy.ovo.ovh/wiki');
|
html = html.replace(/"\/wiki/gi, '"//tgproxy.ovo.ovh/wiki');
|
||||||
|
@ -53,15 +60,14 @@ async function loadPage(page: string, elem: HTMLElement) {
|
||||||
await nextAnimationFrame();
|
await nextAnimationFrame();
|
||||||
|
|
||||||
// Set as HTML content and run HTML manipulations on it
|
// Set as HTML content and run HTML manipulations on it
|
||||||
const div = document.createElement("div");
|
const div = elem.cloneNode(false) as HTMLDivElement;
|
||||||
div.className = elem.className;
|
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
|
|
||||||
console.log(`${page}: processing`);
|
console.log(`${page}: processing`);
|
||||||
processHTML(div, page);
|
processHTML(div, page);
|
||||||
|
|
||||||
// Save result to cache
|
// 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`);
|
console.log(`${page}: saved to cache`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -69,12 +75,13 @@ async function loadPage(page: string, elem: HTMLElement) {
|
||||||
elem = div;
|
elem = div;
|
||||||
} else {
|
} else {
|
||||||
// Set cached content as HTML
|
// Set cached content as HTML
|
||||||
elem.outerHTML = html;
|
elem.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindFunctions(elem, page);
|
bindFunctions(elem, page);
|
||||||
console.log(`${page}: userscript applied`);
|
|
||||||
elem.classList.remove("waiting");
|
elem.classList.remove("waiting");
|
||||||
|
|
||||||
|
return elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabElements = {
|
type TabElements = {
|
||||||
|
@ -97,6 +104,8 @@ export default class TabManager {
|
||||||
|
|
||||||
sections: Record<string, Section> = {};
|
sections: Record<string, Section> = {};
|
||||||
|
|
||||||
|
loading: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
sectionlist: HTMLElement,
|
sectionlist: HTMLElement,
|
||||||
tablist: HTMLElement,
|
tablist: HTMLElement,
|
||||||
|
@ -107,6 +116,25 @@ export default class TabManager {
|
||||||
this.tabContentContainer = tabcontent;
|
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
|
* Create section and add it to the section list
|
||||||
* @param name Section name
|
* @param name Section name
|
||||||
|
@ -161,7 +189,7 @@ export default class TabManager {
|
||||||
* @param icon Icon to show
|
* @param icon Icon to show
|
||||||
* @param setActive Also set the tab as active
|
* @param setActive Also set the tab as active
|
||||||
*/
|
*/
|
||||||
openTab(
|
async openTab(
|
||||||
section: string,
|
section: string,
|
||||||
page: string,
|
page: string,
|
||||||
options: {
|
options: {
|
||||||
|
@ -169,7 +197,8 @@ export default class TabManager {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
}
|
}
|
||||||
): void {
|
): Promise<void> {
|
||||||
|
const { icon, active, text } = options;
|
||||||
// Create tab list item
|
// Create tab list item
|
||||||
const tabListItem = document.createElement("div");
|
const tabListItem = document.createElement("div");
|
||||||
tabListItem.className = "tab";
|
tabListItem.className = "tab";
|
||||||
|
@ -182,11 +211,10 @@ export default class TabManager {
|
||||||
this.setActive(section, page);
|
this.setActive(section, page);
|
||||||
});
|
});
|
||||||
const iconElement = document.createElement("img");
|
const iconElement = document.createElement("img");
|
||||||
iconElement.src = options.icon || unknown;
|
iconElement.src = icon || unknown;
|
||||||
tabListItem.title = page.replace(/_/gi, " ");
|
tabListItem.title = page.replace(/_/gi, " ");
|
||||||
tabListItem.appendChild(iconElement);
|
tabListItem.appendChild(iconElement);
|
||||||
const shortTitle =
|
const shortTitle = text || page.substr(page.lastIndexOf("_") + 1, 4);
|
||||||
options.text || page.substr(page.lastIndexOf("_") + 1, 4);
|
|
||||||
tabListItem.appendChild(document.createTextNode(shortTitle));
|
tabListItem.appendChild(document.createTextNode(shortTitle));
|
||||||
this.tabListContainer.appendChild(tabListItem);
|
this.tabListContainer.appendChild(tabListItem);
|
||||||
|
|
||||||
|
@ -196,9 +224,6 @@ export default class TabManager {
|
||||||
tabContentItem.dataset.tab = page;
|
tabContentItem.dataset.tab = page;
|
||||||
initWaiting(tabContentItem);
|
initWaiting(tabContentItem);
|
||||||
|
|
||||||
// Start loading page for new tab
|
|
||||||
loadPage(page, tabContentItem);
|
|
||||||
|
|
||||||
this.tabContentContainer.appendChild(tabContentItem);
|
this.tabContentContainer.appendChild(tabContentItem);
|
||||||
|
|
||||||
// Create tab entry
|
// Create tab entry
|
||||||
|
@ -209,8 +234,15 @@ export default class TabManager {
|
||||||
tabListItem.classList.add("hidden");
|
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 asked for, set it to active
|
||||||
if (options.active) {
|
if (active) {
|
||||||
this.setActive(section, page);
|
this.setActive(section, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
59
src/index.ts
59
src/index.ts
|
@ -1,25 +1,58 @@
|
||||||
import TabManager from "./TabManager";
|
import TabManager from "./TabManager";
|
||||||
import sections from "./sections";
|
import sections from "./sections";
|
||||||
|
import { nextAnimationFrame } from "./utils";
|
||||||
|
|
||||||
const sectionListContainer = document.getElementById("section-list");
|
// @ts-expect-error: Parcel image import
|
||||||
const tabListContainer = document.getElementById("tab-list");
|
import unknown from "~/assets/images/tab-icons/unknown.svg";
|
||||||
const tabContentContainer = document.getElementById("tabs");
|
|
||||||
const manager = new TabManager(
|
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,
|
sectionListContainer,
|
||||||
tabListContainer,
|
tabListContainer,
|
||||||
tabContentContainer
|
tabContentContainer
|
||||||
);
|
);
|
||||||
|
manager.setLoading(true);
|
||||||
|
|
||||||
sections.forEach((section) => {
|
await nextAnimationFrame();
|
||||||
manager.createSection(section.name);
|
|
||||||
|
// 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) => {
|
section.tabs.forEach((tab) => {
|
||||||
manager.openTab(section.name, tab.page, { icon: tab.icon, text: tab.text });
|
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) {
|
if ("serviceWorker" in navigator) {
|
||||||
const x = process.env.SUBPATH ? `${process.env.SUBPATH}/sw.js` : "sw.js";
|
const x = process.env.SUBPATH ? `${process.env.SUBPATH}/sw.js` : "sw.js";
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
|
@ -31,3 +64,5 @@ if ("serviceWorker" in navigator) {
|
||||||
console.log("Service worker registration failed, error:", error);
|
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.
|
// This is used for cache busting when userscript changes significantly.
|
||||||
// Only change it when you made changes to the processHTML part!
|
// 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) {
|
function chemistryFixups(root: HTMLElement) {
|
||||||
// Enable page-specific CSS rules
|
|
||||||
root.classList.add("bchem");
|
|
||||||
|
|
||||||
// Fix inconsistencies with <p> on random parts
|
// 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
|
// Ideally I'd like a <p> or something on every part, wrapping it completely, but for now let's just kill 'em
|
||||||
root
|
root
|
||||||
|
|
|
@ -16,4 +16,10 @@ export function nextAnimationFrame(): Promise<void> {
|
||||||
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
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;
|
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%;
|
width: 45%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
.bchem table.wikitable {
|
div[data-tab="Guide_to_chemistry"] table.wikitable {
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
.table-head {
|
.table-head {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -101,7 +101,7 @@ span.bgus_nested_element:not(.bgus_collapsed) + div.tooltiptext {
|
||||||
background-color: darken($nanotrasen, 5%) !important;
|
background-color: darken($nanotrasen, 5%) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div.bchem .bgus_fz_selected {
|
div[data-tab="Guide_to_chemistry"] .bgus_fz_selected {
|
||||||
background: $nanotrasen !important;
|
background: $nanotrasen !important;
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
|
|
|
@ -20,6 +20,7 @@ body {
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
img {
|
img {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
|
width: 80vmin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +38,13 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.waiting {
|
||||||
|
#tab-list,
|
||||||
|
#section-list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@ -59,6 +67,20 @@ body {
|
||||||
border: 1px solid lighten($nanotrasen, 10%);
|
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 {
|
#tabs {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
@ -85,17 +107,6 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
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;
|
background: transparent;
|
||||||
width: 100%;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue