// ABOUT & DISCLAIMER MODAL - Enhanced
// =============================================================================
/**
* Shows the About modal and populates it with current data
*/
const showAboutModal = () => {
if (elements.aboutModal) {
populateAboutModal();
if (window.openModalById) openModalById('aboutModal');
else {
elements.aboutModal.style.display = "flex";
document.body.style.overflow = "hidden";
}
}
};
/**
* Hides the About modal
*/
const hideAboutModal = () => {
if (elements.aboutModal) {
if (window.closeModalById) closeModalById('aboutModal');
else {
elements.aboutModal.style.display = "none";
document.body.style.overflow = "";
}
}
};
/**
* Shows the acknowledgment modal on load
*/
const showAckModal = () => {
const ackModal = document.getElementById("ackModal");
if (ackModal && !localStorage.getItem(ACK_DISMISSED_KEY)) {
populateAckModal();
if (window.openModalById) openModalById('ackModal');
else {
ackModal.style.display = "flex";
document.body.style.overflow = "hidden";
}
}
};
/**
* Hides the acknowledgment modal
*/
const hideAckModal = () => {
const ackModal = document.getElementById("ackModal");
if (ackModal) {
if (window.closeModalById) closeModalById('ackModal');
else {
ackModal.style.display = "none";
document.body.style.overflow = "";
}
}
};
/**
* Accepts the acknowledgment and hides the modal
*/
const acceptAck = () => {
localStorage.setItem(ACK_DISMISSED_KEY, "1");
hideAckModal();
};
/**
* Populates the about modal with current version and changelog information
*/
const populateAboutModal = () => {
// Update version displays
const aboutVersion = document.getElementById("aboutVersion");
const aboutCurrentVersion = document.getElementById("aboutCurrentVersion");
const aboutAppName = document.getElementById("aboutAppName");
if (aboutVersion && typeof APP_VERSION !== "undefined") {
aboutVersion.textContent = `v${APP_VERSION}`;
}
if (aboutCurrentVersion && typeof APP_VERSION !== "undefined") {
aboutCurrentVersion.textContent = `v${APP_VERSION}`;
}
if (aboutAppName) {
aboutAppName.textContent = getBrandingName();
}
// Load announcements for latest changes and roadmap
loadAnnouncements();
};
/**
* Populates the acknowledgment modal with version information
*/
const populateAckModal = () => {
const ackVersion = document.getElementById("ackVersion");
const ackAppName = document.getElementById("ackAppName");
if (ackVersion && typeof APP_VERSION !== "undefined") {
ackVersion.textContent = `v${APP_VERSION}`;
}
if (ackAppName) {
ackAppName.textContent = getBrandingName();
}
};
/**
* Loads announcements and populates changelog and roadmap sections
*/
const loadAnnouncements = async () => {
const whatsNewTargets = [
document.getElementById("aboutChangelogLatest"),
document.getElementById("versionChanges"),
].filter(Boolean);
const roadmapTargets = [
document.getElementById("aboutRoadmapList"),
document.getElementById("versionRoadmapList"),
].filter(Boolean);
if (!whatsNewTargets.length && !roadmapTargets.length) return;
try {
const res = await fetch("docs/announcements.md");
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const text = await res.text();
const section = (name) => {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`##\\s+${escaped}\\n([\\s\\S]*?)(?=##|$)`, "i");
const match = text.match(regex);
return match ? match[1] : "";
};
const parseList = (content) =>
content
.split("\n")
.filter((l) => l.trim().startsWith("-"))
.map((l) => sanitizeHtml(l.replace(/^[-*]\s*/, ""))
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>"));
const whatsNewItems = parseList(section("What's New"));
if (whatsNewTargets.length) {
// Filter to current version branch (e.g., 3.31.x) before slicing
const versionBranch = typeof APP_VERSION !== 'undefined'
? APP_VERSION.split('.').slice(0, 2).join('.')
: null;
const filteredWhatsNew = versionBranch
? whatsNewItems.filter(i => i.includes(`v${versionBranch}.`) || i.includes(`(v${versionBranch}`))
: whatsNewItems;
const displayItems = filteredWhatsNew.length > 0 ? filteredWhatsNew : whatsNewItems;
const html =
displayItems.length > 0
? displayItems
.slice(0, 5)
.map((i) => `<li>${i}</li>`)
.join("")
: "<li>No recent announcements</li>";
// nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
whatsNewTargets.forEach((el) => (el.innerHTML = html));
}
const roadmapItems = parseList(section("Development Roadmap"));
if (roadmapTargets.length) {
const html =
roadmapItems.length > 0
? roadmapItems
.slice(0, 3)
.map((i) => `<li>${i}</li>`)
.join("")
: "<li>Roadmap information unavailable</li>";
// nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
roadmapTargets.forEach((el) => (el.innerHTML = html));
}
} catch (e) {
console.warn("Could not load announcements, using embedded data:", e);
// Fallback to embedded announcements data
const embeddedWhatsNew = getEmbeddedWhatsNew();
const embeddedRoadmap = getEmbeddedRoadmap();
// nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
whatsNewTargets.forEach((el) => (el.innerHTML = embeddedWhatsNew));
// nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
roadmapTargets.forEach((el) => (el.innerHTML = embeddedRoadmap));
}
};
/**
* Shows full changelog in a new window or navigates to documentation
*/
const showFullChangelog = () => {
// Try to open changelog documentation
window.open(
"https://github.com/lbruton/StakTrakr/blob/main/CHANGELOG.md",
"_blank",
"noopener,noreferrer",
);
};
/**
* Sets up event listeners for about modal elements
*/
const setupAboutModalEvents = () => {
const aboutCloseBtn = document.getElementById("aboutCloseBtn");
const aboutShowChangelogBtn = document.getElementById(
"aboutShowChangelogBtn",
);
const versionShowChangelogBtn = document.getElementById(
"versionShowChangelogBtn",
);
const aboutModal = document.getElementById("aboutModal");
// Close button
if (aboutCloseBtn) {
aboutCloseBtn.addEventListener("click", hideAboutModal);
}
// Show changelog button
if (aboutShowChangelogBtn) {
aboutShowChangelogBtn.addEventListener("click", showFullChangelog);
}
if (versionShowChangelogBtn) {
versionShowChangelogBtn.addEventListener("click", showFullChangelog);
}
// Click outside to close
if (aboutModal) {
aboutModal.addEventListener("click", (e) => {
if (e.target === aboutModal) {
hideAboutModal();
}
});
}
// Escape key to close
document.addEventListener("keydown", (e) => {
if (
e.key === "Escape" &&
aboutModal &&
aboutModal.style.display === "flex"
) {
hideAboutModal();
}
});
};
/**
* Sets up event listeners for acknowledgment modal elements
*/
const setupAckModalEvents = () => {
const ackCloseBtn = document.getElementById("ackCloseBtn");
const ackAcceptBtn = document.getElementById("ackAcceptBtn");
const ackModal = document.getElementById("ackModal");
if (ackCloseBtn) {
ackCloseBtn.addEventListener("click", hideAckModal);
}
if (ackAcceptBtn) {
ackAcceptBtn.addEventListener("click", acceptAck);
}
if (ackModal) {
ackModal.addEventListener("click", (e) => {
if (e.target === ackModal) {
hideAckModal();
}
});
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && ackModal && ackModal.style.display === "flex") {
hideAckModal();
}
});
};
/**
* Provides embedded "What's New" data as fallback when file fetch fails
* @returns {string} HTML string of recent announcements
*/
const getEmbeddedWhatsNew = () => {
return `
<li><strong>v3.31.5 – Cloud Auto-Sync & Bulk Edit Fixes</strong>: Real-time encrypted auto-sync to Dropbox — inventory changes push automatically and other devices see an “Update Available” modal. Bulk Edit Delete/Copy/Apply now work correctly inside the modal. isCollectable field removed (superseded by tag system) (STAK-149)</li>
<li><strong>v3.31.4 – Vendored Libraries & True Offline Support</strong>: All CDN dependencies (PapaParse, jsPDF, Chart.js, JSZip, Forge) are now bundled locally — the app works fully offline and on file:// protocol with no internet required. CDN fallback fires automatically if a local file fails</li>
<li><strong>v3.31.3 – Filter Chip Active-State UX</strong>: Filter chips now hide × on idle — only active/search chips show a remove button and themed border ring. Clicking × on an active chip now correctly removes the filter. Card view pagination, mobile image tap, and bulk popover rendering polished</li>
<li><strong>v3.31.2 – Numista Metadata Pipeline Fixes</strong>: Tags now write eagerly on bulk sync and restore correctly after vault restore. View modal skips API when metadata is already cached. Weight pre-fills automatically from Numista search results (STAK-168)</li>
<li><strong>v3.31.1 – FAQ Modal & Privacy Improvements</strong>: Interactive FAQ with 13 questions added to Settings sidebar tab, About modal, and footer. ZIP export/import exposed in Settings. Files tab merged into Inventory. privacy.html theme and back-link fixed. pCloud and Box added as coming-soon cloud providers. r/Silverbugs community credit in footer</li>
`;
};
/**
* Provides embedded roadmap data as fallback when file fetch fails
* @returns {string} HTML string of development roadmap
*/
const getEmbeddedRoadmap = () => {
return `
<li><strong>Cloud Backup Conflict Detection (STAK-150)</strong>: Smarter conflict resolution using item count direction, not just timestamps</li>
<li><strong>Accessible Table Mode (STAK-144)</strong>: Style D with horizontal scroll, long-press to edit, 300% zoom support</li>
<li><strong>Custom Theme Editor (STAK-121)</strong>: User-defined color themes with CSS variable overrides</li>
`;
};
// Expose globally for access from other modules
if (typeof window !== "undefined") {
window.showAboutModal = showAboutModal;
window.hideAboutModal = hideAboutModal;
window.showAckModal = showAckModal;
window.hideAckModal = hideAckModal;
window.acceptAck = acceptAck;
window.loadAnnouncements = loadAnnouncements;
window.setupAboutModalEvents = setupAboutModalEvents;
window.setupAckModalEvents = setupAckModalEvents;
window.populateAboutModal = populateAboutModal;
window.populateAckModal = populateAckModal;
window.getEmbeddedWhatsNew = getEmbeddedWhatsNew;
window.getEmbeddedRoadmap = getEmbeddedRoadmap;
}