/**
* @module filterBar
* @file filterBar.js - Setup I2PSnark torrent display filter buttons and AJAX filter loading.
* @description Manages the filter bar UI for showing/hiding torrents by status (all, seeding,
* downloading, etc.). Handles badge count display, filter state persistence to localStorage,
* URL updates for sort icons, and AJAX-based loading of filtered content.
* @author dr|z3d
* @license AGPL3 or later
*/
import {doRefresh} from "./refreshTorrents.js"; // NOSONAR S1128
/**
* @type {?HTMLElement}
* @description The #torrents container element.
*/
const torrents = document.getElementById("torrents");
/**
* @type {?HTMLElement}
* @description The filter bar element.
*/
let filterbar;
/**
* @type {?MutationObserver}
* @description MutationObserver watching the torrents container for child list changes.
*/
let observer;
/**
* @type {boolean}
* @description Guards against scheduling multiple badge count updates per animation frame.
*/
let countPending = false;
/**
* @type {number}
* @description Count of snark torrents matching the current filter.
*/
let snarkCount;
/**
* @type {?string}
* @description The id of the currently active filter, used to detect filter changes.
*/
let activeFilterId = null;
/**
* @function scheduleBadgeUpdate
* @description Coalesces torrent-count badge updates triggered by DOM mutations into a
* single requestAnimationFrame callback. Re-queries the active filter's badge by id so
* the observer stays valid across filter-bar re-renders.
* @returns {void}
*/
function scheduleBadgeUpdate() {
if (countPending) {return;}
countPending = true;
requestAnimationFrame(() => {
countPending = false;
const filterElement = document.getElementById(activeFilterId);
const badge = filterElement ? filterElement.querySelector(".badge") : null;
if (!filterElement || !badge) {return;}
if (activeFilterId !== "all") {badge.textContent = countSnarks();}
badge.hidden = false;
});
}
/**
* @function ensureObserver
* @description Creates the badge-count MutationObserver once and attaches it to the
* torrents container. Reused across showBadge calls because its callback reads module
* state instead of capturing filter elements.
* @returns {void}
*/
function ensureObserver() {
if (observer || !torrents) {return;}
observer = new MutationObserver(scheduleBadgeUpdate);
observer.observe(torrents, { childList: true, subtree: true });
}
/**
* @async
* @function showBadge
* @description Updates the filter bar badge display based on the current URL filter/search
* parameters. Highlights the active filter, disables inactive filters, sets up a
* MutationObserver on the active filter to track torrent count changes, and persists
* the active filter to localStorage.
* @returns {Promise<void>}
*/
async function showBadge() {
const filterbar = document.getElementById("filterBar");
if (!filterbar) {return;}
const urlParams = new URLSearchParams(window.location.search);
const filterQuery = urlParams.get("filter");
const searchQuery = urlParams.get("search");
const filterId = searchQuery !== null ? "search" : (filterQuery || "all");
const allFilters = filterbar.querySelectorAll(".filter");
const activeFilter = document.getElementById(filterId) || filterbar.querySelector(".filter#all");
if (!activeFilter) {return;}
const filterChanged = activeFilterId !== activeFilter.id;
activeFilterId = activeFilter.id;
const activeBadge = activeFilter.querySelector(".badge");
if (!activeBadge) {return;}
activeBadge.id = "filtercount";
if (activeFilter.id === "all") {
activeBadge.hidden = false;
} else {activeBadge.hidden = true;}
allFilters.forEach(filter => {
if (filter !== activeFilter) {
filter.classList.remove("enabled");
if (filterChanged) {
filter.style.pointerEvents = "none";
filter.style.opacity = ".5";
}
const badges = filter.querySelectorAll(".badge");
badges.forEach(badge => {
const filterAll = badge.closest(".filter#all");
if (filterAll) { badge.hidden = true; badge.id = ""; }
else if (filter && filter.id !== "all") { badge.hidden = true; badge.textContent = ""; badge.id = ""; }
});
} else {
ensureObserver();
}
if (filter !== activeFilter && filterChanged) {
setTimeout(() => {
filter.style.pointerEvents = "";
filter.style.opacity = "";
}, 1000);
}
});
if (activeFilter) {
if (!activeFilter.classList.contains("enabled")) {
activeFilter.classList.add("enabled");
window.localStorage.setItem("snarkFilter", activeFilter.id);
}
}
}
/**
* @function countSnarks
* @description Counts the number of visible torrent rows (volatile, non-peerinfo rows)
* in the current table.
* @returns {number} The count of visible torrent rows.
*/
function countSnarks() {
return torrents?.querySelectorAll("#snarkTbody tr.volatile:not(.peerinfo)").length;
}
/**
* @function updateURLs
* @description Sets up click handlers on sort icon elements to save the current query string
* to localStorage when a sort is applied.
* @returns {void}
*/
function updateURLs() {
const sortIcon = document.querySelectorAll(".sorter");
sortIcon.forEach((item) => { item.addEventListener("click", () => { setQuery(); }); });
/**
* @function setQuery
* @description Saves the current URL query string to localStorage for persistence across
* page loads.
* @returns {void}
*/
function setQuery() {
const params = window.location.search;
if (params) {window.localStorage.setItem("queryString", params);}
}
}
/**
* @async
* @function filterNav
* @description Sets up the filter bar click handler. Intercepts clicks on filter elements,
* constructs an AJAX-compatible URL, updates the browser history, refreshes the badge,
* and loads filtered content via doRefresh. Retries with a delay if the filter bar
* is not yet available.
* @returns {Promise<void>}
*/
async function filterNav() {
const filterbar = document.getElementById("filterBar");
if (!filterbar) { setTimeout(filterNav, 1500); return; }
const pagenavtop = document.getElementById("pagenavtop");
filterbar.addEventListener("click", async function(event) {
const filterElement = event.target.closest(".filter");
if (filterElement) {
event.preventDefault();
if (!filterElement.classList.contains("enabled")) {filterElement.classList.add("enabled");}
const filterURL = new URL(filterElement.href);
const xhrURL = "/i2psnark/.ajax/xhr1.html" + filterURL.search;
history.replaceState({}, "", filterURL);
showBadge();
try {await doRefresh({url: xhrURL, forceFetch: true});}
catch {}
if (pagenavtop) {pagenavtop.hidden = filterElement.id !== "all";}
}
});
}
document.addEventListener("DOMContentLoaded", function() { updateURLs(); filterNav(); countSnarks(); showBadge(); });
export {updateURLs, showBadge};