// ==UserScript==
// @name [obj mult] A Better Book Highlighter (Integrated Read Extractor)
// @version 2.0
// @description Highlights tiered lists and fades books read by extracting per-pet read-lists from books_read pages (no network requests). Tampermonkey/Violentmonkey compatible. Includes export/import using IndexedDB for reliable storage.
// @namespace https://github.com/uxillary/neo-qol
// @author valkryie1248
// @license MIT
// @match https://www.neopets.com/objects.phtml?type=shop&obj_type=7
// @match https://www.neopets.com/objects.phtml?obj_type=7&type=shop
// @match https://www.neopets.com/objects.phtml?*obj_type=38*
// @match https://www.neopets.com/objects.phtml?*obj_type=51*
// @match https://www.neopets.com/objects.phtml?*obj_type=70*
// @match https://www.neopets.com/objects.phtml?*obj_type=77*
// @match https://www.neopets.com/objects.phtml?*obj_type=92*
// @match https://www.neopets.com/objects.phtml?*obj_type=106*
// @match https://www.neopets.com/objects.phtml?*obj_type=114*
// @match https://www.neopets.com/books_read.phtml?pet_name=*
// @match https://www.neopets.com/moon/books_read.phtml?pet_name=*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.registerMenuCommand
// @run-at document-end
// ==/UserScript==
(() => {
/* Safe error logging */
function safeLogError(err) {
try {
console.error("[BM Script Error]", err);
} catch (e) {}
}
/* ---------------------- CONSTANTS ----------------------- */
const CSS_CLASSES = {
OVERLAY: "ddg-img-overlay",
READ_HIGHLIGHT: "ListRead-highlight",
LIST1: "List1-highlight",
LIST2: "List2-highlight",
LIST3: "List3-highlight",
LIST4: "List4-highlight",
};
/* ---------------------- CSS + UI key ----------------------- */
const css = `
.ddg-img-overlay { position: relative; }
.ddg-img-overlay::after{
content: "";
position: absolute;
inset: 0;
background: rgba(0,255,255,0.25);
pointer-events: none;
transition: opacity .15s;
opacity: 1;
}
.ddg-img-overlay.no-overlay::after { opacity: 0; }
.${CSS_CLASSES.LIST4}{ color: red; font-weight: 800; text-decoration: underline;}
:has(>.${CSS_CLASSES.LIST4}) {border: 5px solid red; padding: 5px; box-shadow: 0 4px 8px rgba(255, 0, 0, 0.2); }
.${CSS_CLASSES.LIST3}{ color:red; }
:has(> .${CSS_CLASSES.LIST3}) { border: 2px dashed red; padding-top: 5px; box-shadow: 0 4px 8px rgba(255, 0, 0, 0.1);}
.${CSS_CLASSES.LIST2} { color: orange; }
:has(> .${CSS_CLASSES.LIST2}) { border:2px solid orange; padding-top:5px; box-shadow:0 4px 8px rgba(255,165,0,0.2);}
.${CSS_CLASSES.LIST1} { color:green; }
:has(> .${CSS_CLASSES.LIST1}) { border:1px dotted green; padding-top:5px; box-shadow:0 4px 8px rgba(0,255,0,0.2);}
.${CSS_CLASSES.READ_HIGHLIGHT} { color:grey; text-decoration: line-through;}
:has(> .${CSS_CLASSES.READ_HIGHLIGHT}) { padding-top:5px; box-shadow:0 4px 8px rgba(0,255,255,0.6);}
:where(.item-name) b { font-weight: normal; }`;
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
/* Price/key UI setup */
let keyDiv;
try {
keyDiv = document.createElement("div");
keyDiv.style.position = "fixed";
keyDiv.style.top = "180px";
keyDiv.style.right = "3px";
keyDiv.style.backgroundColor = "rgba(255, 255, 255, 0.9)";
keyDiv.style.border = "1px solid #ccc";
keyDiv.style.padding = "10px";
keyDiv.style.fontSize = "11px";
keyDiv.style.fontFamily = "Arial, sans-serif";
keyDiv.style.zIndex = "1000";
keyDiv.style.width = "110px";
keyDiv.style.maxHeight = "90vh";
keyDiv.style.overflowY = "auto";
keyDiv.style.opacity = "0.4";
keyDiv.style.transition = "opacity 0.3s";
keyDiv.onmouseenter = () => (keyDiv.style.opacity = "1.0");
keyDiv.onmouseleave = () => (keyDiv.style.opacity = "0.4");
// Inner HTML is set later by updateKeyUI()
} catch (err) {
safeLogError(err);
}
// Function to dynamically build and update the Key UI based on non-empty lists
function updateKeyUI() {
if (!keyDiv) return;
const listDefinitions = [
{
name: "List 4 (100k+ NP)",
style: "border: 5px solid red;",
check: NORM_LIST4,
margin: true,
},
{
name: "List 3 (50k-100k NP)",
style: "border: 2px dashed red;",
check: NORM_LIST3,
margin: false,
},
{
name: "List 2 (25k-50k NP)",
style: "border: 2px solid orange;",
check: NORM_LIST2,
margin: false,
},
{
name: "List 1 (10k-25k NP)",
style: "border: 1px dotted green;",
check: NORM_LIST1,
margin: false,
},
];
let html = `
<div style="display: flex; align-items: center;">
<strong>- Highlight Key -</strong><br><br>
</div>
<div style="display: flex; align-items: center; margin-bottom: 6px;">
<div style="width: 12px; height: 12px; background-color: rgba(0,255,255,0.25); border: 1px solid #00ffff; margin-right: 5px;"></div>
Read!
</div>`;
for (const def of listDefinitions) {
if (def.check.size > 0) {
const marginTop = def.margin ? "margin-top: 5px;" : "";
html += `
<div style="display: flex; align-items: center; margin-bottom: 6px;">
<div style="width: 12px; height: 12px; ${def.style} margin-right: 5px; ${marginTop}"></div>
${def.name}
</div>`;
}
}
keyDiv.innerHTML = html;
}
/* ---------------------- Normalizer ------------------------- */
const normalize = (s) =>
(s || "")
.toString()
.toLowerCase()
.normalize("NFKD")
.replace(/\p{Diacritic}/gu, "")
.replace(/[.,`'’"():/–—\-_]/g, " ")
.replace(/\b(the|a|an|of|and|&)\b/g, " ")
.replace(/\s+/g, " ")
.trim();
/* ---------------------- Lists (empty Sets for runtime data) ------------------------ */
const NORM_LISTBOOKTASTICREAD = new Set();
const NORM_LISTREAD = new Set();
// Tiered Highlight Lists: List 0 was removed as it had no special styling.
// Add your purchase preferences (ideally based on JN estimated values) directly into these sets
const NORM_LIST1 = new Set();
const NORM_LIST2 = new Set();
const NORM_LIST3 = new Set();
const NORM_LIST4 = new Set();
/* ---------------------- Storage helpers (IndexedDB for Read Lists) --------------------- */
const TIER_LISTS_KEY = "bm_tier_lists_v1";
const DB_NAME = "BookMarkerDB";
const STORE_NAME = "PetReadLists";
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onerror = (event) => {
console.error(
"[IDB Error] Failed to open IndexedDB:",
event.target.error
);
reject(new Error("IndexedDB error"));
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: "pet_name" });
}
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
});
}
async function loadReadListsForPet(petName) {
try {
const db = await openDB();
const transaction = db.transaction([STORE_NAME], "readonly");
const store = transaction.objectStore(STORE_NAME);
return new Promise((resolve) => {
const request = store.get(petName);
request.onsuccess = (event) => {
const result = event.target.result || {
booktastic: [],
regular: [],
pet_name: petName,
};
resolve(result);
};
request.onerror = (event) => {
console.error("[IDB Load Error]", event.target.error);
resolve({ booktastic: [], regular: [], pet_name: petName });
};
});
} catch (e) {
console.error("IndexedDB initialization failed during load:", e);
return { booktastic: [], regular: [], pet_name: petName };
}
}
async function saveReadListsForPet(petName, payload) {
try {
const db = await openDB();
const transaction = db.transaction([STORE_NAME], "readwrite");
const store = transaction.objectStore(STORE_NAME);
const dataToStore = {
pet_name: petName,
booktastic: payload.booktastic,
regular: payload.regular,
};
return new Promise((resolve, reject) => {
const request = store.put(dataToStore);
request.onsuccess = () => resolve();
request.onerror = (event) => {
console.error("[IDB Save Error]", event.target.error);
reject(new Error("Failed to save data to IndexedDB."));
};
});
} catch (e) {
console.error("IndexedDB initialization failed during save:", e);
throw new Error("Could not access database for saving.");
}
}
// Gets all records from IndexedDB
async function getAllReadLists() {
try {
const db = await openDB();
const transaction = db.transaction([STORE_NAME], "readonly");
const store = transaction.objectStore(STORE_NAME);
return new Promise((resolve) => {
const request = store.getAll();
request.onsuccess = (event) => {
resolve(event.target.result || []);
};
request.onerror = (event) => {
console.error("[IDB Load All Error]", event.target.error);
resolve([]);
};
});
} catch (e) {
safeLogError(e);
return [];
}
}
// Clears all records from IndexedDB
async function clearAllReadLists() {
if (
!confirm(
"WARNING: This will permanently DELETE ALL stored pet read lists (both regular and booktastic). Are you sure?"
)
) {
return;
}
try {
// Clears all records from the IndexedDB store
const db = await openDB();
const transaction = db.transaction([STORE_NAME], "readwrite");
const store = transaction.objectStore(STORE_NAME);
await new Promise((resolve, reject) => {
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = (event) => reject(event.target.error);
});
alert(
"All stored pet read lists have been deleted. Please reload the shop page."
);
console.log("[BM Debug Log] Successfully cleared all read lists.");
} catch (e) {
safeLogError(e);
alert(`Failed to clear read lists. Error: ${e.message}`);
}
}
// Imports multiple records into IndexedDB
async function importAllReadLists(data) {
try {
const db = await openDB();
const transaction = db.transaction([STORE_NAME], "readwrite");
const store = transaction.objectStore(STORE_NAME);
return new Promise((resolve, reject) => {
const promises = data.map((item) => {
return new Promise((res, rej) => {
const request = store.put(item);
request.onsuccess = () => res();
request.onerror = () => rej();
});
});
Promise.all(promises)
.then(() => resolve())
.catch((e) => {
console.error("[IDB Import Error]", e);
reject(new Error("Failed to import all data into IndexedDB."));
});
});
} catch (e) {
safeLogError(e);
throw new Error("Could not access database for importing.");
}
}
/* ---------------------- Matching / apply classes --------------------- */
const applyListClass = (element, nameNorm) => {
try {
if (!element) return;
element.classList.remove(
CSS_CLASSES.LIST4,
CSS_CLASSES.LIST3,
CSS_CLASSES.LIST2,
CSS_CLASSES.LIST1,
CSS_CLASSES.READ_HIGHLIGHT
);
// Tiered Highlighting is applied in reverse order (List 4 first)
if (NORM_LIST4.has(nameNorm)) element.classList.add(CSS_CLASSES.LIST4);
else if (NORM_LIST3.has(nameNorm))
element.classList.add(CSS_CLASSES.LIST3);
else if (NORM_LIST2.has(nameNorm))
element.classList.add(CSS_CLASSES.LIST2);
// List 0 check is absent; books not in List 1-4 receive default unstyled display.
else if (NORM_LIST1.has(nameNorm))
element.classList.add(CSS_CLASSES.LIST1);
// Read Highlighting
if (
NORM_LISTREAD.has(nameNorm) ||
NORM_LISTBOOKTASTICREAD.has(nameNorm)
) {
element.classList.add(CSS_CLASSES.READ_HIGHLIGHT);
if (element.parentNode)
element.parentNode.classList.add(CSS_CLASSES.OVERLAY);
} else {
if (element.parentNode)
element.parentNode.classList.remove(CSS_CLASSES.OVERLAY);
}
} catch (e) {
safeLogError(e);
}
};
const applyBooktasticByTitle = () => {
try {
const divs = document.querySelectorAll("div[title]");
for (const d of divs) {
try {
const title = (d.getAttribute("title") || "").trim();
if (!title) continue;
const titleNorm = normalize(title);
if (NORM_LISTBOOKTASTICREAD.has(titleNorm)) {
const target = d.querySelector(".item-name") || d;
target.classList.add(CSS_CLASSES.READ_HIGHLIGHT);
if (target.parentNode)
target.parentNode.classList.add(CSS_CLASSES.OVERLAY);
}
} catch (inner) {
safeLogError(inner);
}
}
} catch (err) {
safeLogError(err);
}
};
function applyStylesToItems() {
try {
const itemNames = document.getElementsByClassName("item-name");
for (let i = 0; i < itemNames.length; i++) {
try {
const raw = (itemNames[i].textContent || "").trim();
if (!raw) continue;
const nameNorm = normalize(raw);
applyListClass(itemNames[i], nameNorm);
} catch (inner) {
safeLogError(inner);
}
}
applyBooktasticByTitle();
} catch (err) {
safeLogError(err);
}
}
/* ---------------------- Extraction logic for books_read pages -------------------- */
function getQueryParam(name) {
try {
const u = new URL(location.href);
return u.searchParams.get(name);
} catch (e) {
return null;
}
}
function extractFromBooksReadDOM() {
try {
const log = [];
const booktasticRaw = [];
const tdsA = document.querySelectorAll('td[align="center"]');
for (const td of tdsA) {
if (td.querySelector("i") && !td.hasAttribute("style")) {
const txt = (td.textContent || "").trim();
if (txt) {
booktasticRaw.push(txt);
log.push({
source: "Variant A (Booktastic)",
raw: txt,
key: normalize(txt),
});
}
}
}
const regularRaw = [];
const tdsB = document.querySelectorAll(
'td[align="center"][style="border:1px solid black;"]'
);
for (const td of tdsB) {
const raw = (td.textContent || "").trim();
if (!raw || !raw.includes(":")) continue;
let title = raw;
const split = raw.split(/:\s*\u00A0*\s*/);
if (split && split.length > 1) {
title = split[0].trim();
} else {
title = raw.trim();
}
if (title) {
regularRaw.push(title);
log.push({
source: "Variant B (Regular)",
raw: raw,
key: normalize(title),
});
}
}
const seen = new Map();
booktasticRaw.forEach((o) => {
const key = normalize(o);
if (key) seen.set(key, { orig: o, key, classification: "booktastic" });
});
regularRaw.forEach((o) => {
const key = normalize(o);
if (key && !seen.has(key)) {
seen.set(key, { orig: o, key, classification: "regular" });
}
});
const booktastic = [];
const regular = [];
for (const item of seen.values()) {
if (item.classification === "booktastic") {
booktastic.push({ orig: item.orig, key: item.key });
} else {
regular.push({ orig: item.orig, key: item.key });
}
}
return { booktastic, regular, log };
} catch (e) {
safeLogError(e);
return { booktastic: [], regular: [], log: [{ error: e.message }] };
}
}
async function handleBooksReadPage() {
try {
const petName = getQueryParam("pet_name");
if (!petName) {
console.warn(
"[BM Debug Log] Could not find 'pet_name' query parameter. Aborting read page handling."
);
return;
}
const extracted = extractFromBooksReadDOM();
const extractedBooktasticCount = extracted.booktastic.length || 0;
const extractedRegularCount = extracted.regular.length || 0;
let pageType = "Unknown";
let booksFound = 0;
if (extractedBooktasticCount > 0 && extractedRegularCount === 0) {
pageType = "Booktastic";
booksFound = extractedBooktasticCount;
} else if (extractedRegularCount > 0 && extractedBooktasticCount === 0) {
pageType = "Regular";
booksFound = extractedRegularCount;
} else {
const isBooktasticPage = location.href.includes(
"booktastic_read.phtml"
);
pageType = isBooktasticPage ? "Booktastic" : "Regular";
booksFound = isBooktasticPage
? extractedBooktasticCount
: extractedRegularCount;
}
console.groupCollapsed(
`[BM Debug Log] Extracted ${
extractedBooktasticCount + extractedRegularCount
} books for ${petName} (${pageType} Page). Click to expand raw data.`
);
console.table(extracted.log);
console.log("Extracted Booktastic List:", extracted.booktastic);
console.log("Extracted Regular List:", extracted.regular);
console.groupEnd();
if (booksFound === 0) {
console.warn(
`[BM Debug Log] Found 0 relevant books (${pageType}). Aborting save.`
);
return;
}
const existing = await loadReadListsForPet(petName);
const existingBooktastic = new Map(
(existing.booktastic || []).map((e) => [e.key, e.orig])
);
const existingRegular = new Map(
(existing.regular || []).map((e) => [e.key, e.orig])
);
if (pageType === "Booktastic") {
existingBooktastic.clear();
extracted.booktastic.forEach((e) =>
existingBooktastic.set(e.key, e.orig)
);
} else if (pageType === "Regular") {
existingRegular.clear();
extracted.regular.forEach((e) => existingRegular.set(e.key, e.orig));
}
const shouldSave = confirm(
`Detected ${booksFound} ${pageType} entries on this page. ` +
`This will REPLACE the stored ${pageType} list for pet "${petName}". Save new lists?`
);
if (!shouldSave) return;
const payload = {
booktastic: Array.from(existingBooktastic.entries()).map(
([key, orig]) => ({ orig, key })
),
regular: Array.from(existingRegular.entries()).map(([key, orig]) => ({
orig,
key,
})),
};
await saveReadListsForPet(petName, payload);
alert(
`Saved ${payload.booktastic.length} Booktastic and ${payload.regular.length} Regular entries for pet "${petName}".`
);
} catch (e) {
console.error("[BM Script Fatal Error on books_read.phtml]", e);
safeLogError(e);
}
}
/* ---------------------- Load stored lists into runtime Sets -------------------- */
async function loadStoredListsToSetsForPet(petName) {
try {
NORM_LISTBOOKTASTICREAD.clear();
NORM_LISTREAD.clear();
if (!petName) return;
const raw = await loadReadListsForPet(petName);
(raw.booktastic || []).forEach((item) =>
NORM_LISTBOOKTASTICREAD.add(item.key)
);
(raw.regular || []).forEach((item) => NORM_LISTREAD.add(item.key));
} catch (e) {
safeLogError(e);
}
}
/* ---------------------- Tiered List Management (GM Storage) -------------------- */
async function loadTieredListsToSets() {
try {
const defaultLists = {
List1: [],
List2: [],
List3: [],
List4: [],
};
const raw = await GM.getValue(TIER_LISTS_KEY, defaultLists);
// Clear and populate runtime sets
NORM_LIST1.clear();
NORM_LIST2.clear();
NORM_LIST3.clear();
NORM_LIST4.clear();
(raw.List1 || []).forEach((item) => NORM_LIST1.add(normalize(item)));
(raw.List2 || []).forEach((item) => NORM_LIST2.add(normalize(item)));
(raw.List3 || []).forEach((item) => NORM_LIST3.add(normalize(item)));
(raw.List4 || []).forEach((item) => NORM_LIST4.add(normalize(item)));
} catch (e) {
safeLogError(e);
}
}
async function manageTieredLists() {
try {
const currentData = await GM.getValue(TIER_LISTS_KEY, {
List1: [],
List2: [],
List3: [],
List4: [],
});
const json = JSON.stringify(currentData, null, 2);
const promptText =
`Paste your complete tiered highlight lists in JSON format below. This will REPLACE the existing lists.\n\n` +
`Lists are typically based on Neopoint (NP) value:\n` +
`List1: 10k-25k NP (Lowest Priority)\n` +
`List2: 25k-50k NP\n` +
`List3: 50k-100k NP\n` +
`List4: 100k+ NP (Highest Priority)\n\n` +
`Format example (use arrays of raw book names):\n` +
`{\n "List1": ["Book Title A", "Book Title B"],\n "List2": ["Book Title C"],\n "List3": [],\n "List4": []\n}`;
const input = prompt(promptText, json);
if (input === null || input === json) return;
const parsed = JSON.parse(input);
if (
!Array.isArray(parsed.List1) ||
!Array.isArray(parsed.List2) ||
!Array.isArray(parsed.List3) ||
!Array.isArray(parsed.List4)
) {
alert(
"Invalid JSON structure. Must contain keys: List1, List2, List3, List4, each with an array value."
);
return;
}
await GM.setValue(TIER_LISTS_KEY, parsed);
await loadTieredListsToSets();
applyStylesToItems();
updateKeyUI(); // Update key after saving/loading new lists
alert("Tiered highlight lists updated successfully!");
} catch (e) {
safeLogError(e);
alert(`Failed to parse or save lists. Error: ${e.message}`);
}
}
/* ---------------------- Menu Command Logic (Import/Export/Clear) -------------------- */
async function exportReadLists() {
try {
const allData = await getAllReadLists();
if (allData.length === 0) {
alert("No read list data found to export.");
return;
}
const json = JSON.stringify(allData, null, 2);
// Use prompt as a temporary text area for easy copying
prompt(
`Copy the complete JSON data below (Contains all pet read lists):`,
json
);
} catch (e) {
safeLogError(e);
alert(`Failed to export read lists. Error: ${e.message}`);
}
}
async function importReadLists() {
try {
const input = prompt(
"Paste the complete JSON export data (containing all pet read lists) below. This will REPLACE existing data for matching pet names:",
""
);
if (!input) return;
const parsed = JSON.parse(input);
if (!Array.isArray(parsed)) {
alert("Invalid JSON format. Expected an array of pet objects.");
return;
}
// Basic validation: ensure each item has required keys
const validData = parsed.filter(
(item) =>
item.pet_name &&
Array.isArray(item.regular) &&
Array.isArray(item.booktastic)
);
if (validData.length === 0) {
alert("No valid pet list data found in the imported JSON.");
return;
}
const shouldProceed = confirm(
`Found ${validData.length} valid pet list(s) to import. This will OVERWRITE existing lists. Proceed?`
);
if (!shouldProceed) return;
await importAllReadLists(validData);
alert(
`Successfully imported ${validData.length} pet read list(s). Please reload the shop page to see the changes.`
);
} catch (e) {
safeLogError(e);
alert(`Failed to import read lists. Error: ${e.message}`);
}
}
try {
GM.registerMenuCommand(
"Manage Tiered Highlight Lists (JSON)",
manageTieredLists
);
GM.registerMenuCommand("Export ALL Pet Read Lists (JSON)", exportReadLists);
GM.registerMenuCommand("Import Pet Read Lists (JSON)", importReadLists);
GM.registerMenuCommand(
"Clear ALL Pet Read Lists (IndexedDB)",
clearReadLists
);
} catch (e) {
/* Ignore if GM functions are not supported */
}
/* ---------------------- Observe shop pages for dynamic content -------------------- */
let shopObserver;
let shopDebounce = null;
function initShopObserver() {
try {
if (shopObserver) return;
shopObserver = new MutationObserver((mutations) => {
if (shopDebounce) clearTimeout(shopDebounce);
shopDebounce = setTimeout(() => {
applyStylesToItems();
}, 120);
});
shopObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
});
} catch (e) {
safeLogError(e);
}
}
/* ---------------------- On load: route behavior -------------------- */
(async () => {
try {
const url = location.href;
if (url.includes("books_read.phtml")) {
await handleBooksReadPage();
} else if (url.includes("objects.phtml")) {
let petName = getQueryParam("pet_name"); // (1) URL param (highest priority)
if (!petName) {
// (2) Selector for H5 Profile Dropdown
const petLookupLink = document.querySelector(
'.nav-profile-dropdown-text a[href*="petlookup.phtml?pet="]'
);
if (petLookupLink) {
// Extract from the href's query parameter (parameter name is 'pet')
const lookupURL = new URL(petLookupLink.href, location.origin);
petName = lookupURL.searchParams.get("pet");
}
}
if (!petName) {
// (3) Fallback to previous selectors (H5 Status Name, Pet Image Alt, User Name)
petName =
document
.querySelector(".active-pet-status .active-pet-name")
?.textContent?.trim() ||
document
.querySelector(".active-pet-status img[alt]")
?.alt?.split(/\s+/)?.[0] ||
document
.querySelector(".user")
?.textContent?.trim()
?.split(/\s+/)?.[0] ||
null;
}
if (!petName) {
console.warn(
"[BM Debug Log] FAILED to detect active pet name on shop page. Cannot load read list."
);
} else {
console.log(
`[BM Debug Log] Detected active pet name: ${petName}. Loading read list.`
);
}
// Load both Read Lists (using petName) and Highlight Lists (global)
await loadStoredListsToSetsForPet(petName);
await loadTieredListsToSets();
applyStylesToItems();
applyBooktasticByTitle();
updateKeyUI(); // Generates dynamic Key content based on loaded lists
initShopObserver();
if (keyDiv) document.body.appendChild(keyDiv);
}
} catch (e) {
safeLogError(e);
}
})();
})();