mobile ux
This commit is contained in:
@@ -984,9 +984,39 @@ def _tracker_int(value, default=None):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _tracker_rows(c: ScgiRtorrentClient, torrent_hash: str) -> list[list]:
|
||||||
|
fields = ("t.url=", "t.is_enabled=", "t.scrape_complete=", "t.scrape_incomplete=", "t.scrape_downloaded=")
|
||||||
|
errors: list[str] = []
|
||||||
|
for args in ((torrent_hash, "", *fields), ("", torrent_hash, *fields)):
|
||||||
|
try:
|
||||||
|
rows = c.call("t.multicall", *args)
|
||||||
|
return [list(r) for r in (rows or [])]
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append(f"t.multicall{args[:2]}: {exc}")
|
||||||
|
# Note: Fallback keeps the sidebar tracker filter usable on rTorrent builds without t.multicall scrape fields.
|
||||||
|
total = _tracker_int(_safe_tracker_call(c, "d.tracker_size", torrent_hash, 0), 0) or 0
|
||||||
|
rows: list[list] = []
|
||||||
|
for index in range(max(0, total)):
|
||||||
|
target = _tracker_target(torrent_hash, index)
|
||||||
|
url = _safe_tracker_call(c, "t.url", target, "")
|
||||||
|
if not url:
|
||||||
|
for args in ((torrent_hash, index), ("", torrent_hash, index)):
|
||||||
|
try:
|
||||||
|
url = c.call("t.url", *args)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if url:
|
||||||
|
enabled = _safe_tracker_call(c, "t.is_enabled", target, 1)
|
||||||
|
rows.append([url, enabled, None, None, None])
|
||||||
|
if rows:
|
||||||
|
return rows
|
||||||
|
raise RuntimeError("Cannot read trackers: " + "; ".join(errors))
|
||||||
|
|
||||||
|
|
||||||
def torrent_trackers(profile: dict, torrent_hash: str) -> list[dict]:
|
def torrent_trackers(profile: dict, torrent_hash: str) -> list[dict]:
|
||||||
c = client_for(profile)
|
c = client_for(profile)
|
||||||
rows = c.t.multicall(torrent_hash, "", "t.url=", "t.is_enabled=", "t.scrape_complete=", "t.scrape_incomplete=", "t.scrape_downloaded=")
|
rows = _tracker_rows(c, torrent_hash)
|
||||||
trackers = []
|
trackers = []
|
||||||
for idx, r in enumerate(rows):
|
for idx, r in enumerate(rows):
|
||||||
target = _tracker_target(torrent_hash, idx)
|
target = _tracker_target(torrent_hash, idx)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
let titleSpeedEnabled = !!Number(window.PYTORRENT?.titleSpeedEnabled || 0);
|
let titleSpeedEnabled = !!Number(window.PYTORRENT?.titleSpeedEnabled || 0);
|
||||||
let trackerFaviconsEnabled = !!Number(window.PYTORRENT?.trackerFaviconsEnabled || 0);
|
let trackerFaviconsEnabled = !!Number(window.PYTORRENT?.trackerFaviconsEnabled || 0);
|
||||||
let trackerSummary = {hashes:{}, trackers:[], scanned:0, errors:[]};
|
let trackerSummary = {hashes:{}, trackers:[], scanned:0, errors:[]};
|
||||||
|
let trackerSummaryStatus = 'idle';
|
||||||
let trackerSummarySignature = "";
|
let trackerSummarySignature = "";
|
||||||
let trackerSummaryTimer = null;
|
let trackerSummaryTimer = null;
|
||||||
const BASE_TITLE = document.title || "pyTorrent";
|
const BASE_TITLE = document.title || "pyTorrent";
|
||||||
@@ -240,13 +241,22 @@
|
|||||||
const src=`https://${encodeURIComponent(domain).replace(/%2E/g,'.')}/favicon.ico`;
|
const src=`https://${encodeURIComponent(domain).replace(/%2E/g,'.')}/favicon.ico`;
|
||||||
return `<img class="tracker-favicon" src="${esc(src)}" alt="" loading="lazy" onerror="this.classList.add('d-none')"><i class="fa-solid fa-bullseye tracker-fallback-icon"></i>`;
|
return `<img class="tracker-favicon" src="${esc(src)}" alt="" loading="lazy" onerror="this.classList.add('d-none')"><i class="fa-solid fa-bullseye tracker-fallback-icon"></i>`;
|
||||||
}
|
}
|
||||||
|
function trackerFilterPlaceholder(){
|
||||||
|
if(trackerSummaryStatus==='loading') return '<div class="tracker-filter-empty"><span class="spinner-border spinner-border-xs"></span> Loading trackers...</div>';
|
||||||
|
if(trackerSummaryStatus==='error') return '<div class="tracker-filter-empty text-warning"><i class="fa-solid fa-triangle-exclamation"></i> Tracker list unavailable</div>';
|
||||||
|
if(hasTorrentSnapshot && torrents.size) return '<div class="tracker-filter-empty">No trackers found</div>';
|
||||||
|
return '<div class="tracker-filter-empty">Waiting for torrents...</div>';
|
||||||
|
}
|
||||||
function renderTrackerFilters(){
|
function renderTrackerFilters(){
|
||||||
const box=$('trackerFilters');
|
const box=$('trackerFilters');
|
||||||
if(!box) return;
|
if(!box) return;
|
||||||
const trackers=trackerSummary.trackers || [];
|
const trackers=trackerSummary.trackers || [];
|
||||||
if(activeFilter.startsWith('tracker:') && !trackers.some(t=>t.domain===activeFilter.slice(8))) activeFilter='all';
|
if(activeFilter.startsWith('tracker:') && !trackers.some(t=>t.domain===activeFilter.slice(8))) activeFilter='all';
|
||||||
// Note: Tracker filters are appended below status and label filters, using read-only tracker summary data.
|
// Note: Tracker filter section is always visible, so an empty or failed tracker scan does not look like a missing feature.
|
||||||
box.innerHTML=trackers.length?`<div class="small text-muted px-2 mb-1">Trackers</div>${trackers.map(t=>`<button class="filter tracker-filter ${activeFilter==='tracker:'+t.domain?'active':''}" data-filter="tracker:${esc(t.domain)}"><span>${trackerFavicon(t.domain)} ${esc(t.domain)}</span><span>${esc(t.count||0)}</span></button>`).join('')}`:'';
|
const rows=trackers.length
|
||||||
|
? trackers.map(t=>`<button class="filter tracker-filter ${activeFilter==='tracker:'+t.domain?'active':''}" data-filter="tracker:${esc(t.domain)}"><span>${trackerFavicon(t.domain)} ${esc(t.domain)}</span><span>${esc(t.count||0)}</span></button>`).join('')
|
||||||
|
: trackerFilterPlaceholder();
|
||||||
|
box.innerHTML=`<div class="small text-muted px-2 mb-1">Trackers</div>${rows}`;
|
||||||
bindSidebarFilterClicks(box);
|
bindSidebarFilterClicks(box);
|
||||||
}
|
}
|
||||||
async function refreshTrackerSummary(force=false){
|
async function refreshTrackerSummary(force=false){
|
||||||
@@ -254,14 +264,20 @@
|
|||||||
const sig=`${hashes.length}:${hashes.slice(0,2000).join(',')}:${trackerFaviconsEnabled?1:0}`;
|
const sig=`${hashes.length}:${hashes.slice(0,2000).join(',')}:${trackerFaviconsEnabled?1:0}`;
|
||||||
if(!force && sig===trackerSummarySignature) return;
|
if(!force && sig===trackerSummarySignature) return;
|
||||||
trackerSummarySignature=sig;
|
trackerSummarySignature=sig;
|
||||||
if(!hashes.length){ trackerSummary={hashes:{},trackers:[],scanned:0,errors:[]}; renderTrackerFilters(); return; }
|
if(!hashes.length){ trackerSummary={hashes:{},trackers:[],scanned:0,errors:[]}; trackerSummaryStatus='empty'; renderTrackerFilters(); return; }
|
||||||
|
trackerSummaryStatus='loading';
|
||||||
|
renderTrackerFilters();
|
||||||
try{
|
try{
|
||||||
const j=await (await fetch(`/api/trackers/summary?limit=2000`)).json();
|
const qs=new URLSearchParams({limit:'2000'});
|
||||||
|
// Note: Browser sends currently visible torrent hashes, avoiding an empty cache race on the backend.
|
||||||
|
hashes.slice(0,2000).forEach(h=>qs.append('hash',h));
|
||||||
|
const j=await (await fetch(`/api/trackers/summary?${qs.toString()}`)).json();
|
||||||
if(!j.ok) throw new Error(j.error||'Tracker summary failed');
|
if(!j.ok) throw new Error(j.error||'Tracker summary failed');
|
||||||
trackerSummary=j.summary||{hashes:{},trackers:[],scanned:0,errors:[]};
|
trackerSummary=j.summary||{hashes:{},trackers:[],scanned:0,errors:[]};
|
||||||
|
trackerSummaryStatus=(trackerSummary.trackers||[]).length?'ready':'empty';
|
||||||
renderTrackerFilters();
|
renderTrackerFilters();
|
||||||
scheduleRender(true);
|
scheduleRender(true);
|
||||||
}catch(e){ console.warn('Tracker summary failed', e); }
|
}catch(e){ trackerSummaryStatus='error'; renderTrackerFilters(); console.warn('Tracker summary failed', e); }
|
||||||
}
|
}
|
||||||
function scheduleTrackerSummary(force=false){
|
function scheduleTrackerSummary(force=false){
|
||||||
clearTimeout(trackerSummaryTimer);
|
clearTimeout(trackerSummaryTimer);
|
||||||
|
|||||||
@@ -892,6 +892,20 @@ body.mobile-mode .main-grid {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tracker-filter-empty {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
display: flex;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Note: Empty tracker state uses the same sidebar spacing as regular filter rows. */
|
||||||
|
.tracker-filter-empty .spinner-border-xs {
|
||||||
|
height: 0.65rem;
|
||||||
|
width: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
.column-manager {
|
.column-manager {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user