Add IDB-backed caching
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
7930f96171
commit
df914edcb5
11 changed files with 470 additions and 285 deletions
|
@ -19,6 +19,7 @@ module.exports = {
|
||||||
"no-param-reassign": "off",
|
"no-param-reassign": "off",
|
||||||
"no-alert": "off",
|
"no-alert": "off",
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
|
"func-names": "off",
|
||||||
"@typescript-eslint/ban-ts-comment": [
|
"@typescript-eslint/ban-ts-comment": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|
23
LICENSE
Normal file
23
LICENSE
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
This code is license under ISC, full copy below:
|
||||||
|
|
||||||
|
Copyright 2020, Alessandro Gatti
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
This code uses idb-keyval, licensed under Apache 2:
|
||||||
|
|
||||||
|
Copyright 2016, Jake Archibald
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -8,4 +8,4 @@
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
`yarn && yarn dev`
|
`yarn dev`
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"eslint-config-prettier": "^6.11.0",
|
"eslint-config-prettier": "^6.11.0",
|
||||||
"eslint-plugin-import": "2.21.2",
|
"eslint-plugin-import": "2.21.2",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"idb-keyval": "^3.2.0",
|
||||||
"parcel-bundler": "^1.12.4",
|
"parcel-bundler": "^1.12.4",
|
||||||
"parcel-plugin-sw-cache": "^0.3.1",
|
"parcel-plugin-sw-cache": "^0.3.1",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// @ts-expect-error: Asset imports are handled by parcel
|
// @ts-expect-error: Asset imports are handled by parcel
|
||||||
import speen from "~/assets/images/speen.svg";
|
import speen from "~/assets/images/speen.svg";
|
||||||
import { getPageHTML } from "./wiki";
|
import { getPageHTML } from "./wiki";
|
||||||
import userscript from "./userscript";
|
import { processHTML, bindFunctions, CURRENT_VERSION } from "./userscript";
|
||||||
|
import cache from "./cache";
|
||||||
|
import { nextAnimationFrame } from "./utils";
|
||||||
|
|
||||||
// @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";
|
||||||
|
@ -22,21 +24,52 @@ function initWaiting(elem: HTMLElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPage(page: string, elem: HTMLElement) {
|
async function loadPage(page: string, elem: HTMLElement) {
|
||||||
|
let html: string | null = null;
|
||||||
|
const key = `page:${page}`;
|
||||||
|
|
||||||
|
// Check cache for pre-processed page
|
||||||
|
try {
|
||||||
|
const cachedPage = await cache.get<string>(key);
|
||||||
|
if (cachedPage) {
|
||||||
|
if (cachedPage.version === CURRENT_VERSION) {
|
||||||
|
console.log(`${page}: found cached entry`);
|
||||||
|
html = cachedPage.value;
|
||||||
|
} else {
|
||||||
|
console.log(`${page}: found outdated cache entry`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`${page}: failed to retrieve cache entry:`, e);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch page content
|
// Fetch page content
|
||||||
console.log(`${page}: fetching`);
|
if (!html) {
|
||||||
let html = await getPageHTML(page);
|
console.log(`${page}: fetching`);
|
||||||
|
html = await getPageHTML(page);
|
||||||
|
|
||||||
// Convert relative links to absolute
|
// Convert relative links to absolute (and proxied)
|
||||||
html = html.replace(/"\/wiki/gi, '"//tgstation13.org/wiki');
|
html = html.replace(/"\/wiki/gi, '"//tgproxy.ovo.ovh/wiki');
|
||||||
|
|
||||||
// Set as HTML content and run HTML manipulations on it
|
await nextAnimationFrame();
|
||||||
requestAnimationFrame(() => {
|
|
||||||
|
// Set as HTML content and run HTML manipulations on it
|
||||||
elem.innerHTML = html;
|
elem.innerHTML = html;
|
||||||
|
|
||||||
console.log(`${page}: processing`);
|
console.log(`${page}: processing`);
|
||||||
userscript(elem, page);
|
processHTML(elem, page);
|
||||||
console.log(`${page}: userscript applied`);
|
|
||||||
elem.classList.remove("waiting");
|
// Save result to cache
|
||||||
});
|
cache.set(key, elem.innerHTML, CURRENT_VERSION).then(() => {
|
||||||
|
console.log(`${page}: saved to cache`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Set cached content as HTML
|
||||||
|
elem.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindFunctions(elem, page);
|
||||||
|
console.log(`${page}: userscript applied`);
|
||||||
|
elem.classList.remove("waiting");
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabElements = {
|
type TabElements = {
|
||||||
|
|
110
src/cache.ts
Normal file
110
src/cache.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
/* eslint-disable no-shadow */
|
||||||
|
|
||||||
|
interface CacheEntry<T> {
|
||||||
|
version: string;
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Store {
|
||||||
|
readonly dbp: Promise<IDBDatabase>;
|
||||||
|
|
||||||
|
constructor(dbName = "tg-cache", readonly storeName = "keyval") {
|
||||||
|
this.dbp = new Promise((resolve, reject) => {
|
||||||
|
const openreq = indexedDB.open(dbName, 1);
|
||||||
|
openreq.onerror = () => reject(openreq.error);
|
||||||
|
openreq.onsuccess = () => resolve(openreq.result);
|
||||||
|
|
||||||
|
// First time setup: create an empty object store
|
||||||
|
openreq.onupgradeneeded = () => {
|
||||||
|
openreq.result.createObjectStore(storeName);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
withIDBStore(
|
||||||
|
type: IDBTransactionMode,
|
||||||
|
callback: (store: IDBObjectStore) => void
|
||||||
|
): Promise<void> {
|
||||||
|
return this.dbp.then(
|
||||||
|
(db) =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(this.storeName, type);
|
||||||
|
transaction.oncomplete = () => resolve();
|
||||||
|
transaction.onabort = () => reject(transaction.error);
|
||||||
|
transaction.onerror = () => reject(transaction.error);
|
||||||
|
callback(transaction.objectStore(this.storeName));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultStore: Store;
|
||||||
|
|
||||||
|
function getDefaultStore() {
|
||||||
|
if (!defaultStore) defaultStore = new Store();
|
||||||
|
return defaultStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get<Type>(
|
||||||
|
key: IDBValidKey,
|
||||||
|
store = getDefaultStore()
|
||||||
|
): Promise<CacheEntry<Type>> {
|
||||||
|
let req: IDBRequest;
|
||||||
|
return store
|
||||||
|
.withIDBStore("readonly", (store) => {
|
||||||
|
req = store.get(key);
|
||||||
|
})
|
||||||
|
.then(() => req.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set<Type>(
|
||||||
|
key: IDBValidKey,
|
||||||
|
value: Type,
|
||||||
|
version: string,
|
||||||
|
store = getDefaultStore()
|
||||||
|
): Promise<void> {
|
||||||
|
return store.withIDBStore("readwrite", (store) => {
|
||||||
|
store.put({ version, value }, key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function del(
|
||||||
|
key: IDBValidKey,
|
||||||
|
store = getDefaultStore()
|
||||||
|
): Promise<void> {
|
||||||
|
return store.withIDBStore("readwrite", (store) => {
|
||||||
|
store.delete(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clear(store = getDefaultStore()): Promise<void> {
|
||||||
|
return store.withIDBStore("readwrite", (store) => {
|
||||||
|
store.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keys(store = getDefaultStore()): Promise<IDBValidKey[]> {
|
||||||
|
const dbkeys: IDBValidKey[] = [];
|
||||||
|
|
||||||
|
return store
|
||||||
|
.withIDBStore("readonly", (store) => {
|
||||||
|
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
|
||||||
|
// And openKeyCursor isn't supported by Safari.
|
||||||
|
(store.openKeyCursor || store.openCursor).call(
|
||||||
|
store
|
||||||
|
).onsuccess = function () {
|
||||||
|
if (!this.result) return;
|
||||||
|
dbkeys.push(this.result.key);
|
||||||
|
this.result.continue();
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.then(() => dbkeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
keys,
|
||||||
|
del,
|
||||||
|
clear,
|
||||||
|
};
|
|
@ -2,7 +2,145 @@ import { darken, ColorFmt, lighten } from "./darkmode";
|
||||||
import searchBox from "./search";
|
import searchBox from "./search";
|
||||||
import { findParent } from "./utils";
|
import { findParent } from "./utils";
|
||||||
|
|
||||||
export default function userscript(root: HTMLElement, docname: string): void {
|
// 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 = "7930f961710641f022ef0cd3ad3277ffd4eab7eb";
|
||||||
|
|
||||||
|
function chemistryFixups(root: HTMLElement) {
|
||||||
|
// 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
|
||||||
|
.querySelectorAll(
|
||||||
|
"table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2), .tooltiptext"
|
||||||
|
)
|
||||||
|
.forEach((td) => {
|
||||||
|
const tmp = td.cloneNode() as HTMLElement;
|
||||||
|
// The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop.
|
||||||
|
// Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index.
|
||||||
|
Array.from(td.childNodes).forEach((el) => {
|
||||||
|
if (el instanceof HTMLParagraphElement) {
|
||||||
|
tmp.append(...el.childNodes);
|
||||||
|
} else {
|
||||||
|
tmp.append(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
td.parentNode.replaceChild(tmp, td);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enrich "x part" with checkboxes and parts
|
||||||
|
Array.from(root.querySelectorAll("td"))
|
||||||
|
.filter((el) => el.textContent.indexOf(" part") >= 0)
|
||||||
|
.forEach((el) => {
|
||||||
|
el.innerHTML = el.innerHTML.replace(
|
||||||
|
/((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi,
|
||||||
|
(match, ...m) =>
|
||||||
|
`<label class="bgus_part ${
|
||||||
|
m[2].includes("</a>") ? "bgus_part_tooltip" : ""
|
||||||
|
}" data-amount="${
|
||||||
|
m[1]
|
||||||
|
}"><input type="checkbox" class='bgus_checkbox bgus_hidden'/> <span class="bgus_part_label" data-src="${
|
||||||
|
m[0]
|
||||||
|
}">${m[0]}</span></label>${m[2].replace(
|
||||||
|
/(<a .+?<\/a>)/gi,
|
||||||
|
'<span class="bgus_nobreak bgus_nested_element">$1<span class="bgus_twistie"></span></span>'
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wrap every recipe with extra metadata
|
||||||
|
root.querySelectorAll<HTMLElement>(".bgus_part").forEach((el) => {
|
||||||
|
if ("parts" in el.parentElement.dataset) {
|
||||||
|
el.parentElement.dataset.parts = (
|
||||||
|
parseInt(el.parentElement.dataset.parts, 10) +
|
||||||
|
parseInt(el.dataset.amount, 10)
|
||||||
|
).toString();
|
||||||
|
} else {
|
||||||
|
el.parentElement.dataset.parts = el.dataset.amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove "Removed medicines" section
|
||||||
|
const remTable = root.querySelector(
|
||||||
|
"#Non-craftable_Medicines + h4 + p + table"
|
||||||
|
);
|
||||||
|
remTable.parentElement.removeChild(remTable);
|
||||||
|
|
||||||
|
// Restructure recipes to work in a narrow window
|
||||||
|
root
|
||||||
|
.querySelectorAll<HTMLElement>("div[data-name] .wikitable.sortable tr")
|
||||||
|
.forEach((row) => {
|
||||||
|
const sectionEl = findParent(
|
||||||
|
row,
|
||||||
|
(sel) => "name" in sel.dataset && sel.dataset.name !== ""
|
||||||
|
);
|
||||||
|
const section = sectionEl.dataset.name;
|
||||||
|
if (row.querySelector("td") === null) {
|
||||||
|
// Remove unused rows if found
|
||||||
|
const headers = row.querySelectorAll("th");
|
||||||
|
headers.forEach((th, i) => {
|
||||||
|
if (i < 2) {
|
||||||
|
th.classList.add("table-head");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
th.parentElement.removeChild(th);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rows = Array.from(row.querySelectorAll("td")).slice(1);
|
||||||
|
let treatment = null;
|
||||||
|
let desc = null;
|
||||||
|
let metabolism = null;
|
||||||
|
let overdose = null;
|
||||||
|
let addiction = null;
|
||||||
|
// Handle special cases
|
||||||
|
switch (section) {
|
||||||
|
case "Components":
|
||||||
|
case "Virology Recipes":
|
||||||
|
[desc] = rows;
|
||||||
|
break;
|
||||||
|
case "Narcotics":
|
||||||
|
[desc, metabolism, overdose, addiction] = rows;
|
||||||
|
break;
|
||||||
|
case "Explosive Strength":
|
||||||
|
case "Other Reagents":
|
||||||
|
case "Mutation Toxins":
|
||||||
|
[desc, metabolism] = rows;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// All fields
|
||||||
|
[treatment, desc, metabolism, overdose, addiction] = rows;
|
||||||
|
}
|
||||||
|
const title = row.querySelector("th");
|
||||||
|
let content = `<div class="reagent-header">${title.innerHTML}</div>`;
|
||||||
|
if (treatment) {
|
||||||
|
content += `<p class="treatment">${treatment.innerHTML}</p>`;
|
||||||
|
}
|
||||||
|
if (metabolism) {
|
||||||
|
content += `<p class="metabolism">${metabolism.innerHTML}</p>`;
|
||||||
|
}
|
||||||
|
if (addiction && addiction.innerHTML.trim() !== "N/A") {
|
||||||
|
content += `<p class="addiction">${addiction.innerHTML}</p>`;
|
||||||
|
}
|
||||||
|
if (overdose && overdose.innerHTML.trim() !== "N/A") {
|
||||||
|
content += `<p class="overdose">${overdose.innerHTML}</p>`;
|
||||||
|
}
|
||||||
|
if (desc) {
|
||||||
|
content += `<p>${desc.innerHTML}</p>`;
|
||||||
|
}
|
||||||
|
title.classList.add("reagent-ext");
|
||||||
|
title.innerHTML = content;
|
||||||
|
if (desc) desc.parentElement.removeChild(desc);
|
||||||
|
if (treatment) treatment.parentElement.removeChild(treatment);
|
||||||
|
if (metabolism) metabolism.parentElement.removeChild(metabolism);
|
||||||
|
if (overdose) overdose.parentElement.removeChild(overdose);
|
||||||
|
if (addiction) addiction.parentElement.removeChild(addiction);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable page-specific CSS rules
|
||||||
|
root.classList.add("bchem");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processHTML(root: HTMLElement, docname: string): void {
|
||||||
// Add header
|
// Add header
|
||||||
const header = document.createElement("h1");
|
const header = document.createElement("h1");
|
||||||
header.className = "pageheader";
|
header.className = "pageheader";
|
||||||
|
@ -98,281 +236,149 @@ export default function userscript(root: HTMLElement, docname: string): void {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tell user that better chemistry is loading
|
|
||||||
const postbody = root;
|
|
||||||
const statusMessage = document.createElement("div");
|
|
||||||
statusMessage.innerHTML = `
|
|
||||||
<table style="background-color: black; margin-bottom:10px;" width="95%" align="center">
|
|
||||||
<tbody><tr><td align="center">
|
|
||||||
<b>Hang on...</b> Better guides is loading.
|
|
||||||
</td></tr></tbody>
|
|
||||||
</table>`;
|
|
||||||
postbody.insertBefore(statusMessage, postbody.firstChild);
|
|
||||||
|
|
||||||
function betterChemistry() {
|
|
||||||
// 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
|
|
||||||
document
|
|
||||||
.querySelectorAll(
|
|
||||||
"table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2), .tooltiptext"
|
|
||||||
)
|
|
||||||
.forEach((td) => {
|
|
||||||
const tmp = td.cloneNode() as HTMLElement;
|
|
||||||
// The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop.
|
|
||||||
// Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index.
|
|
||||||
Array.from(td.childNodes).forEach((el) => {
|
|
||||||
if (el instanceof HTMLParagraphElement) {
|
|
||||||
tmp.append(...el.childNodes);
|
|
||||||
} else {
|
|
||||||
tmp.append(el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
td.parentNode.replaceChild(tmp, td);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enrich "x part" with checkboxes and parts
|
|
||||||
Array.from(document.querySelectorAll("td"))
|
|
||||||
.filter((el) => el.textContent.indexOf(" part") >= 0)
|
|
||||||
.forEach((el) => {
|
|
||||||
el.innerHTML = el.innerHTML.replace(
|
|
||||||
/((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi,
|
|
||||||
(match, ...m) =>
|
|
||||||
`<label class="bgus_part ${
|
|
||||||
m[2].includes("</a>") ? "bgus_part_tooltip" : ""
|
|
||||||
}" data-amount="${
|
|
||||||
m[1]
|
|
||||||
}"><input type="checkbox" class='bgus_checkbox bgus_hidden'/> <span class="bgus_part_label" data-src="${
|
|
||||||
m[0]
|
|
||||||
}">${m[0]}</span></label>${m[2].replace(
|
|
||||||
/(<a .+?<\/a>)/gi,
|
|
||||||
'<span class="bgus_nobreak bgus_nested_element">$1<span class="bgus_twistie"></span></span>'
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// Add event to autofill child checkboxes
|
|
||||||
root
|
|
||||||
.querySelectorAll(".bgus_part_tooltip > .bgus_checkbox")
|
|
||||||
.forEach((box: HTMLInputElement) => {
|
|
||||||
const tooltip = box.parentElement.nextElementSibling;
|
|
||||||
box.addEventListener("click", () => {
|
|
||||||
tooltip
|
|
||||||
.querySelectorAll(".bgus_checkbox")
|
|
||||||
.forEach((el: HTMLInputElement) => {
|
|
||||||
el.checked = box.checked;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add event to collapse subsections
|
|
||||||
root.querySelectorAll(".bgus_nested_element").forEach((twistie) => {
|
|
||||||
twistie.addEventListener("click", () => {
|
|
||||||
twistie.classList.toggle("bgus_collapsed");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wrap every recipe with extra metadata
|
|
||||||
root.querySelectorAll<HTMLElement>(".bgus_part").forEach((el) => {
|
|
||||||
if ("parts" in el.parentElement.dataset) {
|
|
||||||
el.parentElement.dataset.parts = (
|
|
||||||
parseInt(el.parentElement.dataset.parts, 10) +
|
|
||||||
parseInt(el.dataset.amount, 10)
|
|
||||||
).toString();
|
|
||||||
} else {
|
|
||||||
el.parentElement.dataset.parts = el.dataset.amount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const setPartSize = (labels, ml) => {
|
|
||||||
labels.forEach((el) => {
|
|
||||||
const part = el.parentElement.dataset.amount;
|
|
||||||
const total = el.parentElement.parentElement.dataset.parts;
|
|
||||||
const amt = Math.ceil(ml * (part / total));
|
|
||||||
el.innerHTML = `${amt} ml`;
|
|
||||||
// Lookup tooltips
|
|
||||||
let next = el.parentElement.nextElementSibling;
|
|
||||||
while (next) {
|
|
||||||
if (next.classList.contains("tooltip")) {
|
|
||||||
const sublabels = [];
|
|
||||||
next.querySelector(".tooltiptext").childNodes.forEach((ch) => {
|
|
||||||
if (ch.classList && ch.classList.contains("bgus_part")) {
|
|
||||||
sublabels.push(ch.querySelector(".bgus_part_label"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setPartSize(sublabels, amt);
|
|
||||||
}
|
|
||||||
if (next.classList.contains("bgus_part")) {
|
|
||||||
// Done searching
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
next = next.nextElementSibling;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
root.classList.add("bchem");
|
|
||||||
// Init fuzzy search with elements
|
|
||||||
const el = Array.from(
|
|
||||||
root.querySelectorAll<HTMLElement>(
|
|
||||||
"table.wikitable > tbody > tr:not(:first-child) > th"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const name = el.map((elem) => {
|
|
||||||
let partial = "";
|
|
||||||
elem.childNodes.forEach((t) => {
|
|
||||||
if (t instanceof Text) {
|
|
||||||
partial += t.textContent;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return partial.trim();
|
|
||||||
});
|
|
||||||
const box = searchBox(
|
|
||||||
el,
|
|
||||||
name.map((e, i) => ({ id: i, str: e }))
|
|
||||||
);
|
|
||||||
document.body.appendChild(box);
|
|
||||||
|
|
||||||
// Remove "Removed medicines" section
|
|
||||||
const remTable = root.querySelector(
|
|
||||||
"#Non-craftable_Medicines + h4 + p + table"
|
|
||||||
);
|
|
||||||
remTable.parentElement.removeChild(remTable);
|
|
||||||
|
|
||||||
root
|
|
||||||
.querySelectorAll<HTMLElement>("div[data-name] .wikitable.sortable tr")
|
|
||||||
.forEach((row) => {
|
|
||||||
const sectionEl = findParent(
|
|
||||||
row,
|
|
||||||
(sel) => "name" in sel.dataset && sel.dataset.name !== ""
|
|
||||||
);
|
|
||||||
const section = sectionEl.dataset.name;
|
|
||||||
if (row.querySelector("td") === null) {
|
|
||||||
// Remove unused rows if found
|
|
||||||
const headers = row.querySelectorAll("th");
|
|
||||||
headers.forEach((th, i) => {
|
|
||||||
if (i < 2) {
|
|
||||||
th.classList.add("table-head");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
th.parentElement.removeChild(th);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rows = Array.from(row.querySelectorAll("td")).slice(1);
|
|
||||||
let treatment = null;
|
|
||||||
let desc = null;
|
|
||||||
let metabolism = null;
|
|
||||||
let overdose = null;
|
|
||||||
let addiction = null;
|
|
||||||
// Handle special cases
|
|
||||||
switch (section) {
|
|
||||||
case "Components":
|
|
||||||
case "Virology Recipes":
|
|
||||||
[desc] = rows;
|
|
||||||
break;
|
|
||||||
case "Narcotics":
|
|
||||||
[desc, metabolism, overdose, addiction] = rows;
|
|
||||||
break;
|
|
||||||
case "Explosive Strength":
|
|
||||||
case "Other Reagents":
|
|
||||||
case "Mutation Toxins":
|
|
||||||
[desc, metabolism] = rows;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// All fields
|
|
||||||
[treatment, desc, metabolism, overdose, addiction] = rows;
|
|
||||||
}
|
|
||||||
const title = row.querySelector("th");
|
|
||||||
let content = `<div class="reagent-header">${title.innerHTML}</div>`;
|
|
||||||
if (treatment) {
|
|
||||||
content += `<p class="treatment">${treatment.innerHTML}</p>`;
|
|
||||||
}
|
|
||||||
if (metabolism) {
|
|
||||||
content += `<p class="metabolism">${metabolism.innerHTML}</p>`;
|
|
||||||
}
|
|
||||||
if (addiction && addiction.innerHTML.trim() !== "N/A") {
|
|
||||||
content += `<p class="addiction">${addiction.innerHTML}</p>`;
|
|
||||||
}
|
|
||||||
if (overdose && overdose.innerHTML.trim() !== "N/A") {
|
|
||||||
content += `<p class="overdose">${overdose.innerHTML}</p>`;
|
|
||||||
}
|
|
||||||
if (desc) {
|
|
||||||
content += `<p>${desc.innerHTML}</p>`;
|
|
||||||
}
|
|
||||||
title.classList.add("reagent-ext");
|
|
||||||
title.innerHTML = content;
|
|
||||||
if (desc) desc.parentElement.removeChild(desc);
|
|
||||||
if (treatment) treatment.parentElement.removeChild(treatment);
|
|
||||||
if (metabolism) metabolism.parentElement.removeChild(metabolism);
|
|
||||||
if (overdose) overdose.parentElement.removeChild(overdose);
|
|
||||||
if (addiction) addiction.parentElement.removeChild(addiction);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.addEventListener("keydown", (ev) => {
|
|
||||||
if (ev.shiftKey) {
|
|
||||||
switch (ev.keyCode) {
|
|
||||||
// SHIFT+C = Toggle checkboxes
|
|
||||||
case 67: {
|
|
||||||
root.classList.toggle("bgus_cbox");
|
|
||||||
root
|
|
||||||
.querySelectorAll(".bgus_checkbox:checked")
|
|
||||||
.forEach((sel: HTMLInputElement) => {
|
|
||||||
sel.checked = false;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SHIFT+B = Set whole size (beaker?) for parts/units
|
|
||||||
case 66: {
|
|
||||||
const size = parseInt(
|
|
||||||
prompt("Write target ml (0 to reset)", "90"),
|
|
||||||
10
|
|
||||||
);
|
|
||||||
if (Number.isNaN(size) || size <= 0) {
|
|
||||||
// Reset to parts/unit
|
|
||||||
root
|
|
||||||
.querySelectorAll(".bgus_part_label")
|
|
||||||
.forEach((sel: HTMLElement) => {
|
|
||||||
sel.innerHTML = sel.dataset.src;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPartSize(
|
|
||||||
root.querySelectorAll("td > .bgus_part > .bgus_part_label"),
|
|
||||||
+size
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function betterGeneric() {
|
|
||||||
const el = Array.from(
|
|
||||||
root.querySelectorAll<HTMLElement>("div.mw-headline-cont[id][data-name]")
|
|
||||||
);
|
|
||||||
const name = el.map((elem: HTMLDivElement) => elem.dataset.name.trim());
|
|
||||||
|
|
||||||
// Init fuzzy search with headlines
|
|
||||||
const box = searchBox(
|
|
||||||
el,
|
|
||||||
name.map((e, i) => ({ id: i, str: e })),
|
|
||||||
{ alignment: "start" }
|
|
||||||
);
|
|
||||||
root.appendChild(box);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (docname) {
|
switch (docname) {
|
||||||
case "Guide_to_chemistry":
|
case "Guide_to_chemistry":
|
||||||
betterChemistry();
|
chemistryFixups(root);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
betterGeneric();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chemistryScript(root: HTMLElement) {
|
||||||
|
// Add event to autofill child checkboxes
|
||||||
|
root
|
||||||
|
.querySelectorAll(".bgus_part_tooltip > .bgus_checkbox")
|
||||||
|
.forEach((box: HTMLInputElement) => {
|
||||||
|
const tooltip = box.parentElement.nextElementSibling;
|
||||||
|
box.addEventListener("click", () => {
|
||||||
|
tooltip
|
||||||
|
.querySelectorAll(".bgus_checkbox")
|
||||||
|
.forEach((el: HTMLInputElement) => {
|
||||||
|
el.checked = box.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event to collapse subsections
|
||||||
|
root.querySelectorAll(".bgus_nested_element").forEach((twistie) => {
|
||||||
|
twistie.addEventListener("click", () => {
|
||||||
|
twistie.classList.toggle("bgus_collapsed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const setPartSize = (labels, ml) => {
|
||||||
|
labels.forEach((el) => {
|
||||||
|
const part = el.parentElement.dataset.amount;
|
||||||
|
const total = el.parentElement.parentElement.dataset.parts;
|
||||||
|
const amt = Math.ceil(ml * (part / total));
|
||||||
|
el.innerHTML = `${amt} ml`;
|
||||||
|
// Lookup tooltips
|
||||||
|
let next = el.parentElement.nextElementSibling;
|
||||||
|
while (next) {
|
||||||
|
if (next.classList.contains("tooltip")) {
|
||||||
|
const sublabels = [];
|
||||||
|
next.querySelector(".tooltiptext").childNodes.forEach((ch) => {
|
||||||
|
if (ch.classList && ch.classList.contains("bgus_part")) {
|
||||||
|
sublabels.push(ch.querySelector(".bgus_part_label"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setPartSize(sublabels, amt);
|
||||||
|
}
|
||||||
|
if (next.classList.contains("bgus_part")) {
|
||||||
|
// Done searching
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
next = next.nextElementSibling;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init fuzzy search with elements
|
||||||
|
const el = Array.from(
|
||||||
|
root.querySelectorAll<HTMLElement>(
|
||||||
|
"table.wikitable > tbody > tr:not(:first-child) > th"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const name = el.map((elem) =>
|
||||||
|
elem.querySelector(".reagent-header").textContent.trim().replace("▮", "")
|
||||||
|
);
|
||||||
|
const box = searchBox(
|
||||||
|
el,
|
||||||
|
name.map((e, i) => ({ id: i, str: e }))
|
||||||
|
);
|
||||||
|
document.body.appendChild(box);
|
||||||
|
|
||||||
|
document.body.addEventListener("keydown", (ev) => {
|
||||||
|
if (ev.shiftKey) {
|
||||||
|
switch (ev.keyCode) {
|
||||||
|
// SHIFT+C = Toggle checkboxes
|
||||||
|
case 67: {
|
||||||
|
root.classList.toggle("bgus_cbox");
|
||||||
|
root
|
||||||
|
.querySelectorAll(".bgus_checkbox:checked")
|
||||||
|
.forEach((sel: HTMLInputElement) => {
|
||||||
|
sel.checked = false;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHIFT+B = Set whole size (beaker?) for parts/units
|
||||||
|
case 66: {
|
||||||
|
const size = parseInt(
|
||||||
|
prompt("Write target ml (0 to reset)", "90"),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
if (Number.isNaN(size) || size <= 0) {
|
||||||
|
// Reset to parts/unit
|
||||||
|
root
|
||||||
|
.querySelectorAll(".bgus_part_label")
|
||||||
|
.forEach((sel: HTMLElement) => {
|
||||||
|
sel.innerHTML = sel.dataset.src;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPartSize(
|
||||||
|
root.querySelectorAll("td > .bgus_part > .bgus_part_label"),
|
||||||
|
+size
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function genericScript(root: HTMLElement) {
|
||||||
|
const el = Array.from(
|
||||||
|
root.querySelectorAll<HTMLElement>("div.mw-headline-cont[id][data-name]")
|
||||||
|
);
|
||||||
|
const name = el.map((elem: HTMLDivElement) => elem.dataset.name.trim());
|
||||||
|
|
||||||
|
// Init fuzzy search with headlines
|
||||||
|
const box = searchBox(
|
||||||
|
el,
|
||||||
|
name.map((e, i) => ({ id: i, str: e })),
|
||||||
|
{ alignment: "start" }
|
||||||
|
);
|
||||||
|
root.appendChild(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindFunctions(root: HTMLElement, docname: string): void {
|
||||||
|
switch (docname) {
|
||||||
|
case "Guide_to_chemistry":
|
||||||
|
chemistryScript(root);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
genericScript(root);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Everything is loaded, remove loading bar
|
|
||||||
statusMessage.innerHTML = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
CURRENT_VERSION,
|
||||||
|
processHTML,
|
||||||
|
bindFunctions,
|
||||||
|
};
|
||||||
|
|
|
@ -12,4 +12,8 @@ export function findParent(
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { findParent };
|
export function nextAnimationFrame(): Promise<void> {
|
||||||
|
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { findParent, nextAnimationFrame };
|
||||||
|
|
|
@ -66,6 +66,7 @@ body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.page {
|
.page {
|
||||||
//visibility: hidden;
|
//visibility: hidden;
|
||||||
|
will-change: display;
|
||||||
display: none;
|
display: none;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~*": ["./src/*"]
|
"~*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3230,6 +3230,11 @@ icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
||||||
integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
|
integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
|
||||||
|
|
||||||
|
idb-keyval@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-3.2.0.tgz#cbbf354deb5684b6cdc84376294fc05932845bd6"
|
||||||
|
integrity sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ==
|
||||||
|
|
||||||
ieee754@^1.1.4:
|
ieee754@^1.1.4:
|
||||||
version "1.1.13"
|
version "1.1.13"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||||
|
|
Reference in a new issue