diff --git a/pytorrent/static/app.js b/pytorrent/static/app.js
index d968a78..4256bd0 100644
--- a/pytorrent/static/app.js
+++ b/pytorrent/static/app.js
@@ -24,6 +24,8 @@
const hasActiveProfile = !!window.PYTORRENT?.activeProfile;
let firstRunSetupShown = false;
const activeOperations = new Map();
+ // Note: Keeps live filter tooltips stable while the pointer is over a filter button.
+ const filterTooltipState = new WeakMap();
function toast(msg, type="secondary") { const h=$('toastHost'); if(!h) return; const el=document.createElement('div'); el.className=`toast-item text-bg-${type}`; el.innerHTML=esc(msg); h.appendChild(el); setTimeout(()=>el.remove(),3500); }
function setBusy(on){ pendingBusy += on ? 1 : -1; if(pendingBusy<0) pendingBusy=0; $('globalLoader')?.classList.toggle('d-none', pendingBusy===0); $('busyBadge')?.classList.toggle('d-none', pendingBusy===0); }
@@ -93,6 +95,41 @@
}
return lines.join('\n');
}
+ function applyFilterTooltip(button, tooltip, ariaLabel){
+ if(tooltip){
+ button.title = tooltip;
+ button.setAttribute('aria-label', ariaLabel);
+ } else {
+ button.removeAttribute('title');
+ button.removeAttribute('aria-label');
+ }
+ }
+ function ensureStableFilterTooltip(button){
+ if(filterTooltipState.has(button)) return filterTooltipState.get(button);
+ const state = {hovering:false, pending:null};
+ filterTooltipState.set(button, state);
+ button.addEventListener('mouseenter', () => {
+ state.hovering = true;
+ state.pending = null;
+ });
+ button.addEventListener('mouseleave', () => {
+ state.hovering = false;
+ if(state.pending){
+ applyFilterTooltip(button, state.pending.tooltip, state.pending.ariaLabel);
+ state.pending = null;
+ }
+ });
+ return state;
+ }
+ // Note: Freezes tooltip content during hover; the next hover receives the newest live summary.
+ function setStableFilterTooltip(button, tooltip, ariaLabel){
+ const state = ensureStableFilterTooltip(button);
+ if(state.hovering){
+ state.pending = {tooltip, ariaLabel};
+ return;
+ }
+ applyFilterTooltip(button, tooltip, ariaLabel);
+ }
function setFilterSummary(type){
const el=$(FILTER_COUNT_IDS[type]);
if(!el) return;
@@ -102,13 +139,8 @@
el.innerHTML=`${esc(bucket.count||0)}${meta?`${esc(meta)}`:''}`;
const button=el.closest('.filter');
if(button){
- if(tooltip){
- button.title=tooltip;
- button.setAttribute('aria-label', `${button.dataset.filter || type}: ${tooltip.replace(/\n/g, ', ')}`);
- } else {
- button.removeAttribute('title');
- button.removeAttribute('aria-label');
- }
+ const ariaLabel = tooltip ? `${button.dataset.filter || type}: ${tooltip.replace(/\n/g, ', ')}` : '';
+ setStableFilterTooltip(button, tooltip, ariaLabel);
}
}
function labelNames(value){ return String(value||'').split(/[,;|]+/).map(x=>x.trim()).filter(Boolean).filter((x,i,a)=>a.indexOf(x)===i); }