diff --git a/pytorrent/db.py b/pytorrent/db.py index 2585655..672b665 100644 --- a/pytorrent/db.py +++ b/pytorrent/db.py @@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS user_preferences ( mobile_mode INTEGER DEFAULT 0, peers_refresh_seconds INTEGER DEFAULT 0, port_check_enabled INTEGER DEFAULT 0, + footer_items_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) @@ -241,6 +242,7 @@ MIGRATIONS = [ "ALTER TABLE user_preferences ADD COLUMN port_check_enabled INTEGER DEFAULT 0", "ALTER TABLE user_preferences ADD COLUMN bootstrap_theme TEXT DEFAULT 'default'", "ALTER TABLE user_preferences ADD COLUMN font_family TEXT DEFAULT 'default'", + "ALTER TABLE user_preferences ADD COLUMN footer_items_json TEXT", "ALTER TABLE rtorrent_profiles ADD COLUMN max_parallel_jobs INTEGER DEFAULT 5", "ALTER TABLE rtorrent_profiles ADD COLUMN is_remote INTEGER DEFAULT 0", "ALTER TABLE jobs ADD COLUMN attempts INTEGER DEFAULT 0", diff --git a/pytorrent/services/preferences.py b/pytorrent/services/preferences.py index 00c1125..e9f0da9 100644 --- a/pytorrent/services/preferences.py +++ b/pytorrent/services/preferences.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json + from ..db import connect, utcnow, default_user_id BOOTSTRAP_THEMES = { @@ -157,6 +159,7 @@ def save_preferences(data: dict, user_id: int | None = None): table_columns_json = data.get("table_columns_json") peers_refresh_seconds = data.get("peers_refresh_seconds") port_check_enabled = data.get("port_check_enabled") + footer_items_json = data.get("footer_items_json") with connect() as conn: now = utcnow() if allowed_theme: @@ -173,4 +176,11 @@ def save_preferences(data: dict, user_id: int | None = None): conn.execute("UPDATE user_preferences SET peers_refresh_seconds=?, updated_at=? WHERE user_id=?", (sec, now, user_id)) if port_check_enabled is not None: conn.execute("UPDATE user_preferences SET port_check_enabled=?, updated_at=? WHERE user_id=?", (1 if port_check_enabled else 0, now, user_id)) + if footer_items_json is not None: + # Note: Store only JSON objects so footer visibility can be extended without schema churn. + value = footer_items_json if isinstance(footer_items_json, str) else json.dumps(footer_items_json) + parsed = json.loads(value or "{}") + if not isinstance(parsed, dict): + parsed = {} + conn.execute("UPDATE user_preferences SET footer_items_json=?, updated_at=? WHERE user_id=?", (json.dumps(parsed), now, user_id)) return get_preferences(user_id) diff --git a/pytorrent/services/rtorrent.py b/pytorrent/services/rtorrent.py index 3432b9e..8464fed 100644 --- a/pytorrent/services/rtorrent.py +++ b/pytorrent/services/rtorrent.py @@ -435,6 +435,28 @@ def disk_usage_for_default_path(profile: dict) -> dict: return usage + +def _safe_rtorrent_int(callable_obj, default=None): + """Return an integer rTorrent metric without failing the whole status poll.""" + try: + value = callable_obj() + return int(value) + except Exception: + return default + + +def _safe_rtorrent_time(c): + """Read rTorrent server time when supported; otherwise let the browser clock remain authoritative.""" + candidates = ( + lambda: c.system.time_seconds(), + lambda: c.system.time(), + ) + for candidate in candidates: + value = _safe_rtorrent_int(candidate) + if value: + return value + return None + def system_status(profile: dict) -> dict: c = client_for(profile) version = str(c.system.client_version()) @@ -447,13 +469,19 @@ def system_status(profile: dict) -> dict: except Exception: up_limit = 0 rows = list_torrents(profile) + # Note: ruTorrent-style footer metrics. Missing XMLRPC methods are shown as unavailable instead of breaking polling. + open_sockets = _safe_rtorrent_int(lambda: c.network.open_sockets()) + max_open_sockets = _safe_rtorrent_int(lambda: c.network.max_open_sockets()) + rtorrent_time = _safe_rtorrent_time(c) + checking_count = sum(1 for t in rows if t.get("status") == "Checking" or int(t.get("hashing") or 0) > 0) return { "ok": True, "version": version, "total": len(rows), "active": sum(1 for t in rows if t["state"]), "seeding": sum(1 for t in rows if t["complete"] and t["state"] and not t.get("paused")), - "leeching": sum(1 for t in rows if not t["complete"] and t["state"] and not t.get("paused")), + "leeching": sum(1 for t in rows if not t["complete"] and t["state"] and not t.get("paused") and t.get("status") != "Checking"), + "checking": checking_count, "paused": sum(1 for t in rows if t.get("paused")), "stopped": sum(1 for t in rows if not t["state"]), "down_rate": sum(t["down_rate"] for t in rows), @@ -468,6 +496,9 @@ def system_status(profile: dict) -> dict: "total_up": sum(t["up_total"] for t in rows), "total_down_h": human_size(sum(t["down_total"] for t in rows)), "total_up_h": human_size(sum(t["up_total"] for t in rows)), + "open_sockets": open_sockets, + "max_open_sockets": max_open_sockets, + "rtorrent_time": rtorrent_time, "disk": disk_usage_for_default_path(profile), } diff --git a/pytorrent/static/app.js b/pytorrent/static/app.js index 67d6840..2a61305 100644 --- a/pytorrent/static/app.js +++ b/pytorrent/static/app.js @@ -17,6 +17,13 @@ let portCheckEnabled = !!Number(window.PYTORRENT?.portCheckEnabled || 0); let bootstrapTheme = window.PYTORRENT?.bootstrapTheme || "default"; let fontFamily = window.PYTORRENT?.fontFamily || "default"; + const FOOTER_ITEM_DEFS = [ + ["cpu", "CPU"], ["ram", "RAM"], ["usage_chart", "CPU/RAM chart"], ["disk", "Disk"], + ["version", "rTorrent version"], ["speed_down", "Download speed"], ["speed_up", "Upload speed"], + ["limits", "Speed limits"], ["totals", "Total transfer"], ["port_check", "Port check"], + ["clock", "Clock"], ["sockets", "Open sockets"], ["shown", "Shown torrents"], ["selected", "Selected torrents"], ["docs", "API docs"] + ]; + let footerItems = {...Object.fromEntries(FOOTER_ITEM_DEFS.map(([key]) => [key, true])), ...(window.PYTORRENT?.footerItems || {})}; let modalLabels = new Set(), defaultDownloadPath = null; let hasTorrentSnapshot = false, initialLoaderDone = false, rtConfigOriginal = new Map(), rtConfigFieldTypes = new Map(), rtConfigOriginalApplyOnStart = false; let torrentSummary = null; @@ -147,7 +154,8 @@ function labelValue(labels){ return [...new Set((labels||[]).map(x=>String(x||'').trim()).filter(Boolean))].join(', '); } function rowHasLabel(t,label){ return labelNames(t.label).includes(label); } function torrentHasError(t){ return !!torrentWarning(t); } - function rowVisible(t){ const q=($('searchBox')?.value||'').toLowerCase(); if(q && ![t.name,t.path,t.label,t.hash,t.ratio_group].join(' ').toLowerCase().includes(q)) return false; if(activeFilter==='downloading') return !t.complete && t.state && !t.paused; if(activeFilter==='seeding') return t.status!=='Checking' && t.complete && t.state && !t.paused; if(activeFilter==='paused') return !!t.paused || t.status==='Paused'; if(activeFilter==='checking') return t.status==='Checking' || Number(t.hashing||0)>0; if(activeFilter==='error') return torrentHasError(t); if(activeFilter==='stopped') return !t.state; if(activeFilter.startsWith('label:')) return rowHasLabel(t,activeFilter.slice(6)); return true; } + function isChecking(t){ return t?.status==='Checking' || Number(t?.hashing||0)>0; } + function rowVisible(t){ const q=($('searchBox')?.value||'').toLowerCase(); if(q && ![t.name,t.path,t.label,t.hash,t.ratio_group].join(' ').toLowerCase().includes(q)) return false; if(activeFilter==='downloading') return !isChecking(t) && !t.complete && t.state && !t.paused; if(activeFilter==='seeding') return !isChecking(t) && t.complete && t.state && !t.paused; if(activeFilter==='paused') return !!t.paused || t.status==='Paused'; if(activeFilter==='checking') return isChecking(t); if(activeFilter==='error') return torrentHasError(t); if(activeFilter==='stopped') return !t.state && !isChecking(t); if(activeFilter.startsWith('label:')) return rowHasLabel(t,activeFilter.slice(6)); return true; } function compareRows(a,b){ const k=sortState.key; let av=a[k], bv=b[k]; if(typeof av==='string'||typeof bv==='string') return String(av||'').localeCompare(String(bv||''))*sortState.dir; return ((Number(av||0)>Number(bv||0))?1:(Number(av||0)0?" ":" "; } function updateSortHeaders(){ document.querySelectorAll('.torrent-table thead th[data-sort]').forEach(th=>{ const base=th.dataset.baseText||th.textContent.trim(); th.dataset.baseText=base; th.innerHTML=`${esc(base)}${sortIcon(th.dataset.sort)}`; th.classList.toggle('sorted',sortState.key===th.dataset.sort); }); } @@ -490,6 +498,39 @@ function diagCard(label,value,extra=''){ return `
${esc(label)}${esc(value ?? '-')}
`; } + // Note: Centralizes footer visibility so Preferences can hide items without removing existing status logic. + function applyFooterPreferences(){ + document.querySelectorAll('[data-footer-item]').forEach(el=>{ + const key=el.dataset.footerItem; + el.classList.toggle('footer-pref-hidden', footerItems[key] === false); + }); + } + function renderFooterPreferences(){ + const box=$('footerPreferences'); + if(!box) return; + box.innerHTML=FOOTER_ITEM_DEFS.map(([key,label])=>``).join(''); + } + async function saveFooterPreferences(){ + document.querySelectorAll('.footer-pref-toggle').forEach(cb=>{ footerItems[cb.dataset.footerKey] = !!cb.checked; }); + applyFooterPreferences(); + renderFooterPreferences(); + try{ await post('/api/preferences',{footer_items_json:footerItems}); toast('Footer preferences saved','success'); } + catch(e){ toast(e.message,'danger'); } + } + function updateFooterClock(){ + const el=$('statClock'); + if(el) el.textContent=new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', second:'2-digit'}); + } + function updateSocketStatus(s={}){ + const el=$('statSockets'); + if(!el) return; + const open=s.open_sockets; + const max=s.max_open_sockets; + el.textContent=open == null ? '-' : (max == null ? String(open) : `${open}/${max}`); + const box=$('statusSockets'); + if(box) box.title=open == null ? 'Open sockets unavailable from this rTorrent build' : `Open rTorrent sockets${max == null ? '' : ' / max'}: ${el.textContent}`; + } + function portStatusLabel(st){ return st==='open'?'open':st==='closed'?'closed':st==='disabled'?'disabled':st==='error'?'error':'unknown'; } function portStatusClass(st){ return st==='open'?'port-ok':st==='closed'?'port-bad':'port-secondary'; } function portStatusIcon(st){ return st==='open'?'fa-circle-check':st==='closed'?'fa-circle-xmark':'fa-circle-question'; } @@ -508,7 +549,7 @@ } if($('statusPortCheckBadge')) $('statusPortCheckBadge').outerHTML=portStatusBadge(data,'id="statusPortCheckBadge" ',true); } - async function loadPreferences(){ if($('portCheckEnabled')) $('portCheckEnabled').checked=portCheckEnabled; applyBootstrapTheme(bootstrapTheme); applyFontFamily(fontFamily); await loadPortCheck(false); } + async function loadPreferences(){ if($('portCheckEnabled')) $('portCheckEnabled').checked=portCheckEnabled; applyBootstrapTheme(bootstrapTheme); applyFontFamily(fontFamily); renderFooterPreferences(); applyFooterPreferences(); await loadPortCheck(false); } async function savePortCheckPref(){ portCheckEnabled=!!$('portCheckEnabled')?.checked; try{ await post('/api/preferences',{port_check_enabled:portCheckEnabled}); toast('Preferences saved','success'); await loadPortCheck(false); }catch(e){ toast(e.message,'danger'); } } async function loadPortCheck(force=false){ try{ const res=force?await post('/api/port-check',{}):await (await fetch('/api/port-check')).json(); if(!res.ok) throw new Error(res.error||'Port check failed'); renderPortCheck(res.port_check||{}); }catch(e){ renderPortCheck({status:'error',enabled:portCheckEnabled,error:e.message}); } } async function loadAppStatus(){ @@ -545,7 +586,7 @@ function awaitMaybeRun(action){ runAction(action).catch?.(()=>{}); } document.addEventListener('click',e=>{ const ctx=$('ctxMenu'); if(!e.target.closest('#ctxMenu')) ctx.style.display='none'; const mobileFilter=e.target.closest('#mobileFilterBar .mobile-filter'); if(mobileFilter){ document.querySelectorAll('.filter').forEach(x=>x.classList.remove('active')); document.querySelectorAll('.filter').forEach(x=>{ if(x.dataset.filter===mobileFilter.dataset.filter) x.classList.add('active'); }); activeFilter=mobileFilter.dataset.filter; if($('tableWrap'))$('tableWrap').scrollTop=0; if($('mobileList'))$('mobileList').scrollTop=0; scheduleRender(true); return; } const mobileSelectAll=e.target.closest('#mobileSelectAll'); if(mobileSelectAll){ const all=visibleRows.length>0 && visibleRows.every(t=>selected.has(t.hash)); if(all) visibleRows.forEach(t=>selected.delete(t.hash)); else visibleRows.forEach(t=>selected.add(t.hash)); if(selected.size===0){selectedHash=null;lastSelectedHash=null;} else {selectedHash=[...selected][selected.size-1];lastSelectedHash=selectedHash;} scheduleRender(true); return; } const mobileClear=e.target.closest('#mobileClearSelection'); if(mobileClear){ selected.clear(); selectedHash=null; lastSelectedHash=null; scheduleRender(true); return; } const mobileAct=e.target.closest('.mobile-card [data-action]'); if(mobileAct){ const card0=mobileAct.closest('.mobile-card'); selected.clear(); selected.add(card0.dataset.hash); selectedHash=card0.dataset.hash; awaitMaybeRun(mobileAct.dataset.action); scheduleRender(true); return; } const card=e.target.closest('.mobile-card'); const tr=e.target.closest('tr[data-hash]'); const row=tr||card; if(row){ const h=row.dataset.hash; const additive=e.ctrlKey||e.metaKey; if(e.shiftKey){ setSelectionRange(h, additive); } else if(e.target.classList.contains('row-check')){ e.target.checked?selected.add(h):selected.delete(h); lastSelectedHash=h; selectedHash=h; } else { selectedHash=h; if(!additive)selected.clear(); selected.add(h); lastSelectedHash=h; loadDetails(activeTab()); } scheduleRender(true); } const copy=e.target.closest('[data-copy]'); if(copy) copySelected(copy.dataset.copy); const smartEx=e.target.closest('#smartExcludeCtx'); if(smartEx){ selectedHashes().forEach(h=>post('/api/smart-queue/exclusion',{hash:h,excluded:true,reason:'manual'}).catch(()=>{})); toast('Smart Queue exception saved','success'); loadSmartQueue().catch(()=>{}); } const act=e.target.closest('.torrent-action,[data-action]'); if(act&&act.dataset.action&&!act.closest('#detailTabs')&&!act.closest('.mobile-card')) runAction(act.dataset.action); }); document.addEventListener('contextmenu',e=>{ const tr=e.target.closest('tr[data-hash],.mobile-card'); if(!tr)return; e.preventDefault(); selectedHash=tr.dataset.hash; if(!selected.has(selectedHash)){selected.clear();selected.add(selectedHash);scheduleRender(true);} const m=$('ctxMenu'); m.style.left=`${e.pageX}px`; m.style.top=`${e.pageY}px`; m.style.display='block'; }); - document.querySelectorAll('.torrent-table thead th[data-sort]').forEach(th=>th.addEventListener('click',()=>{ const key=th.dataset.sort; if(sortState.key===key) sortState.dir*=-1; else sortState={key,dir:1}; scheduleRender(true); })); $('tableWrap')?.addEventListener('scroll',()=>scheduleRender(false),{passive:true}); $('selectAll')?.addEventListener('change',e=>{selected.clear(); if(e.target.checked)visibleRows.forEach(t=>selected.add(t.hash)); scheduleRender(true);}); $('searchBox')?.addEventListener('input',()=>{if($('tableWrap'))$('tableWrap').scrollTop=0;scheduleRender(true);}); document.querySelectorAll('.filter').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('.filter').forEach(x=>x.classList.remove('active')); b.classList.add('active'); activeFilter=b.dataset.filter; if($('tableWrap'))$('tableWrap').scrollTop=0; scheduleRender(true);})); document.querySelectorAll('#detailTabs .nav-link').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('#detailTabs .nav-link').forEach(x=>x.classList.remove('active')); b.classList.add('active'); loadDetails(b.dataset.tab);})); document.addEventListener('change',e=>{ const sel=e.target.closest('.file-priority'); if(sel){ setFilePriorities([{index:Number(sel.dataset.index),priority:Number(sel.value)}]); return; } if(e.target && e.target.id==='fileSelectAll'){ document.querySelectorAll('#detailPane .file-check').forEach(cb=>cb.checked=e.target.checked); } }); document.addEventListener('click',e=>{ const bulk=e.target.closest('.file-priority-bulk'); if(!bulk) return; const priority=Number(bulk.dataset.priority); const checked=[...document.querySelectorAll('#detailPane .file-check:checked')].map(cb=>({index:Number(cb.dataset.index),priority})); if(!checked.length) return toast('No files selected','warning'); setFilePriorities(checked); }); document.addEventListener('click',e=>{ const b=e.target.closest('.peer-action'); if(!b) return; peerAction(b.dataset.peerIndex,b.dataset.peerAction); }); document.addEventListener('click',e=>{ const add=e.target.closest('#trackerAddBtn'); if(add){ const url=$('trackerAddUrl')?.value||''; trackerAction('add',{url}); return; } const editStart=e.target.closest('.tracker-edit-start'); if(editStart){ setTrackerEdit(editStart.dataset.index,true); return; } const cancel=e.target.closest('.tracker-edit-cancel'); if(cancel){ setTrackerEdit(cancel.dataset.index,false); return; } const save=e.target.closest('.tracker-edit-save'); if(save){ const input=document.querySelector(`.tracker-url[data-tracker-index="${CSS.escape(String(save.dataset.index))}"]`); trackerAction('edit',{index:Number(save.dataset.index),url:input?.value||''}); return; } const rea=e.target.closest('#trackerReannounceBtn'); if(rea) trackerAction('reannounce',{}); }); $('appStatusRefreshBtn')?.addEventListener('click',loadAppStatus); $('portCheckEnabled')?.addEventListener('change',savePortCheckPref); $('portCheckNowBtn')?.addEventListener('click',()=>loadPortCheck(true)); $('bootstrapThemeSelect')?.addEventListener('change',saveAppearancePreferences); $('fontFamilySelect')?.addEventListener('change',saveAppearancePreferences); + document.querySelectorAll('.torrent-table thead th[data-sort]').forEach(th=>th.addEventListener('click',()=>{ const key=th.dataset.sort; if(sortState.key===key) sortState.dir*=-1; else sortState={key,dir:1}; scheduleRender(true); })); $('tableWrap')?.addEventListener('scroll',()=>scheduleRender(false),{passive:true}); $('selectAll')?.addEventListener('change',e=>{selected.clear(); if(e.target.checked)visibleRows.forEach(t=>selected.add(t.hash)); scheduleRender(true);}); $('searchBox')?.addEventListener('input',()=>{if($('tableWrap'))$('tableWrap').scrollTop=0;scheduleRender(true);}); document.querySelectorAll('.filter').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('.filter').forEach(x=>x.classList.remove('active')); b.classList.add('active'); activeFilter=b.dataset.filter; if($('tableWrap'))$('tableWrap').scrollTop=0; scheduleRender(true);})); document.querySelectorAll('#detailTabs .nav-link').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('#detailTabs .nav-link').forEach(x=>x.classList.remove('active')); b.classList.add('active'); loadDetails(b.dataset.tab);})); document.addEventListener('change',e=>{ const sel=e.target.closest('.file-priority'); if(sel){ setFilePriorities([{index:Number(sel.dataset.index),priority:Number(sel.value)}]); return; } if(e.target && e.target.id==='fileSelectAll'){ document.querySelectorAll('#detailPane .file-check').forEach(cb=>cb.checked=e.target.checked); } }); document.addEventListener('click',e=>{ const bulk=e.target.closest('.file-priority-bulk'); if(!bulk) return; const priority=Number(bulk.dataset.priority); const checked=[...document.querySelectorAll('#detailPane .file-check:checked')].map(cb=>({index:Number(cb.dataset.index),priority})); if(!checked.length) return toast('No files selected','warning'); setFilePriorities(checked); }); document.addEventListener('click',e=>{ const b=e.target.closest('.peer-action'); if(!b) return; peerAction(b.dataset.peerIndex,b.dataset.peerAction); }); document.addEventListener('click',e=>{ const add=e.target.closest('#trackerAddBtn'); if(add){ const url=$('trackerAddUrl')?.value||''; trackerAction('add',{url}); return; } const editStart=e.target.closest('.tracker-edit-start'); if(editStart){ setTrackerEdit(editStart.dataset.index,true); return; } const cancel=e.target.closest('.tracker-edit-cancel'); if(cancel){ setTrackerEdit(cancel.dataset.index,false); return; } const save=e.target.closest('.tracker-edit-save'); if(save){ const input=document.querySelector(`.tracker-url[data-tracker-index="${CSS.escape(String(save.dataset.index))}"]`); trackerAction('edit',{index:Number(save.dataset.index),url:input?.value||''}); return; } const rea=e.target.closest('#trackerReannounceBtn'); if(rea) trackerAction('reannounce',{}); }); $('appStatusRefreshBtn')?.addEventListener('click',loadAppStatus); $('portCheckEnabled')?.addEventListener('change',savePortCheckPref); $('portCheckNowBtn')?.addEventListener('click',()=>loadPortCheck(true)); $('bootstrapThemeSelect')?.addEventListener('change',saveAppearancePreferences); $('fontFamilySelect')?.addEventListener('change',saveAppearancePreferences); $('saveFooterPrefsBtn')?.addEventListener('click',saveFooterPreferences); document.addEventListener('keydown',e=>{ const tag=(e.target?.tagName||'').toLowerCase(); const editable=tag==='input'||tag==='textarea'||tag==='select'||e.target?.isContentEditable; if(editable){ if(e.key==='Enter' && e.target?.id==='labelInput'){ e.preventDefault(); $('addLabelToSelectionBtn')?.click(); } return; } if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='a'){e.preventDefault();selected.clear();visibleRows.forEach(t=>selected.add(t.hash));scheduleRender(true);} if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='i'){e.preventDefault();visibleRows.forEach(t=>selected.has(t.hash)?selected.delete(t.hash):selected.add(t.hash));scheduleRender(true);} if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='o'){e.preventDefault();new bootstrap.Modal($('addModal')).show();} if(e.key==='Escape'){selected.clear();scheduleRender(true);} if(e.key==='Delete') new bootstrap.Modal($('removeModal')).show(); if(e.key===' ') {e.preventDefault();runAction('start');} if(e.key.toLowerCase()==='p')runAction('pause'); if(e.key.toLowerCase()==='s')runAction('stop'); if(e.key.toLowerCase()==='r')runAction('resume'); if(e.key.toLowerCase()==='m')runAction('move'); }); $('removeModal')?.addEventListener('show.bs.modal',()=>{$('removeCount').textContent=selected.size;$('removeData').checked=true;}); $('confirmRemoveBtn')?.addEventListener('click',async()=>{await runAction('remove',{remove_data:$('removeData').checked});bootstrap.Modal.getInstance($('removeModal'))?.hide();}); $('addModal')?.addEventListener('show.bs.modal',()=>applyDefaultDownloadPath(true)); @@ -705,6 +746,6 @@ ${disk.error}`:''}`; b.classList.add("btn-primary"); b.classList.remove("btn-outline-secondary"); loadTrafficHistory(b.dataset.range||"7d"); })); - socket.on('connect',()=>{ if(!hasActiveProfile){ showFirstRunSetup(); return; } $('connBadge').className='badge text-bg-success'; $('connBadge').textContent='online'; setInitialLoader('Loading torrents...','Connection is ready. Waiting for the first torrent snapshot.'); socket.emit('select_profile',{profile_id:window.PYTORRENT.activeProfile}); }); socket.on('disconnect',()=>{ $('connBadge').className='badge text-bg-danger'; $('connBadge').textContent='offline'; setInitialLoader('Waiting for connection...','pyTorrent is not connected yet. The application will open after data is received.'); }); socket.io.on('reconnect_attempt',()=>{ $('connBadge').className='badge text-bg-warning'; $('connBadge').textContent='reconnecting'; setInitialLoader('Reconnecting...','Trying to restore the live connection and load torrent data.'); }); socket.io.on('reconnect',()=>{ if(!hasActiveProfile){ showFirstRunSetup(); return; } $('connBadge').className='badge text-bg-success'; $('connBadge').textContent='online'; setInitialLoader('Loading torrents...','Connection restored. Waiting for the first torrent snapshot.'); socket.emit('select_profile',{profile_id:window.PYTORRENT.activeProfile}); }); socket.on('profile_required',()=>showFirstRunSetup()); socket.on('torrent_snapshot',msg=>{hasTorrentSnapshot=true;torrentSummary=msg.summary||null;torrents.clear();(msg.torrents||[]).forEach(t=>torrents.set(t.hash,t));scheduleRender(true);hideInitialLoader();}); socket.on('torrent_patch',patchRows); socket.on('job_update',()=>{ if(document.body.classList.contains('modal-open')) loadJobs().catch(()=>{}); }); socket.on('operation_started',msg=>{setBusy(true);markTorrentOperation(msg.hashes||[],msg.action,msg.job_id,'running');toast(`${msg.action} started`,'secondary');}); socket.on('operation_finished',msg=>{setBusy(false);clearJobOperation(msg.job_id,msg.hashes||[]);toast(`${msg.action} done`,'success');}); socket.on('operation_failed',msg=>{setBusy(false);clearJobOperation(msg.job_id,msg.hashes||[]);toast(`${msg.action}: ${msg.error}`,'danger');}); socket.on('rtorrent_error',msg=>{ if(msg.error){$('connBadge').className='badge badge-degraded';$('connBadge').textContent='degraded'; setInitialLoader('Waiting for rTorrent...','rTorrent is not ready yet. Data will appear automatically after it responds.');} }); socket.on('heartbeat',msg=>{ if(msg.error){$('connBadge').className='badge badge-degraded';$('connBadge').textContent='degraded'; setInitialLoader('Waiting for rTorrent...','rTorrent is not ready yet. Data will appear automatically after it responds.');} else if(socket.connected){$('connBadge').className='badge text-bg-success';$('connBadge').textContent='online';} }); socket.on('smart_queue_update',msg=>{ if(msg && msg.enabled) toast(`Smart Queue: paused ${msg.paused?.length||0}, resumed ${msg.resumed?.length||0}`,'secondary'); }); socket.on('automation_update',msg=>{ if(msg?.applied?.length) toast(`Automations applied ${msg.applied.length} item(s)`,'secondary'); }); socket.on('rtorrent_config_applied',msg=>{ if(msg?.result?.updated?.length) toast(`Startup rTorrent config applied (${msg.result.updated.length})`,'success'); if(msg?.error) toast(`Startup rTorrent config: ${msg.error}`,'danger'); }); socket.on('system_stats',s=>{ const usageAvailable=s.usage_available!==false && s.cpu!==undefined && s.ram!==undefined; $('statCpuBox')?.classList.toggle('d-none',!usageAvailable);$('statRamBox')?.classList.toggle('d-none',!usageAvailable);$('systemChart')?.classList.toggle('d-none',!usageAvailable); if(usageAvailable){$('statCpu').textContent=s.cpu??'-';$('statRam').textContent=s.ram??'-';drawSystemUsage(s.cpu,s.ram);} $('statVersion').textContent=s.version||'-';$('statDl').textContent=s.down_rate_h||'0 B/s';$('statUl').textContent=s.up_rate_h||'0 B/s';if($('mobileSpeedDl')) $('mobileSpeedDl').textContent=s.down_rate_h||'0 B/s';if($('mobileSpeedUl')) $('mobileSpeedUl').textContent=s.up_rate_h||'0 B/s';lastLimits={down:Number(s.down_limit||0),up:Number(s.up_limit||0)};$('statDlLimit').textContent=s.down_limit_h||'∞';$('statUlLimit').textContent=s.up_limit_h||'∞';$('statTotalDl').textContent=compactTransferText(s.total_down_h);$('statTotalUl').textContent=compactTransferText(s.total_up_h);drawTraffic(s.down_rate,s.up_rate);drawDiskUsage(s.disk);}); - updateSortHeaders(); applyColumnVisibility(); renderColumnManager(); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); + socket.on('connect',()=>{ if(!hasActiveProfile){ showFirstRunSetup(); return; } $('connBadge').className='badge text-bg-success'; $('connBadge').textContent='online'; setInitialLoader('Loading torrents...','Connection is ready. Waiting for the first torrent snapshot.'); socket.emit('select_profile',{profile_id:window.PYTORRENT.activeProfile}); }); socket.on('disconnect',()=>{ $('connBadge').className='badge text-bg-danger'; $('connBadge').textContent='offline'; setInitialLoader('Waiting for connection...','pyTorrent is not connected yet. The application will open after data is received.'); }); socket.io.on('reconnect_attempt',()=>{ $('connBadge').className='badge text-bg-warning'; $('connBadge').textContent='reconnecting'; setInitialLoader('Reconnecting...','Trying to restore the live connection and load torrent data.'); }); socket.io.on('reconnect',()=>{ if(!hasActiveProfile){ showFirstRunSetup(); return; } $('connBadge').className='badge text-bg-success'; $('connBadge').textContent='online'; setInitialLoader('Loading torrents...','Connection restored. Waiting for the first torrent snapshot.'); socket.emit('select_profile',{profile_id:window.PYTORRENT.activeProfile}); }); socket.on('profile_required',()=>showFirstRunSetup()); socket.on('torrent_snapshot',msg=>{hasTorrentSnapshot=true;torrentSummary=msg.summary||null;torrents.clear();(msg.torrents||[]).forEach(t=>torrents.set(t.hash,t));scheduleRender(true);hideInitialLoader();}); socket.on('torrent_patch',patchRows); socket.on('job_update',()=>{ if(document.body.classList.contains('modal-open')) loadJobs().catch(()=>{}); }); socket.on('operation_started',msg=>{setBusy(true);markTorrentOperation(msg.hashes||[],msg.action,msg.job_id,'running');toast(`${msg.action} started`,'secondary');}); socket.on('operation_finished',msg=>{setBusy(false);clearJobOperation(msg.job_id,msg.hashes||[]);toast(`${msg.action} done`,'success');}); socket.on('operation_failed',msg=>{setBusy(false);clearJobOperation(msg.job_id,msg.hashes||[]);toast(`${msg.action}: ${msg.error}`,'danger');}); socket.on('rtorrent_error',msg=>{ if(msg.error){$('connBadge').className='badge badge-degraded';$('connBadge').textContent='degraded'; setInitialLoader('Waiting for rTorrent...','rTorrent is not ready yet. Data will appear automatically after it responds.');} }); socket.on('heartbeat',msg=>{ if(msg.error){$('connBadge').className='badge badge-degraded';$('connBadge').textContent='degraded'; setInitialLoader('Waiting for rTorrent...','rTorrent is not ready yet. Data will appear automatically after it responds.');} else if(socket.connected){$('connBadge').className='badge text-bg-success';$('connBadge').textContent='online';} }); socket.on('smart_queue_update',msg=>{ if(msg && msg.enabled) toast(`Smart Queue: paused ${msg.paused?.length||0}, resumed ${msg.resumed?.length||0}`,'secondary'); }); socket.on('automation_update',msg=>{ if(msg?.applied?.length) toast(`Automations applied ${msg.applied.length} item(s)`,'secondary'); }); socket.on('rtorrent_config_applied',msg=>{ if(msg?.result?.updated?.length) toast(`Startup rTorrent config applied (${msg.result.updated.length})`,'success'); if(msg?.error) toast(`Startup rTorrent config: ${msg.error}`,'danger'); }); socket.on('system_stats',s=>{ const usageAvailable=s.usage_available!==false && s.cpu!==undefined && s.ram!==undefined; $('statCpuBox')?.classList.toggle('d-none',!usageAvailable);$('statRamBox')?.classList.toggle('d-none',!usageAvailable);$('systemChart')?.classList.toggle('d-none',!usageAvailable); if(usageAvailable){$('statCpu').textContent=s.cpu??'-';$('statRam').textContent=s.ram??'-';drawSystemUsage(s.cpu,s.ram);} $('statVersion').textContent=s.version||'-';$('statDl').textContent=s.down_rate_h||'0 B/s';$('statUl').textContent=s.up_rate_h||'0 B/s';if($('mobileSpeedDl')) $('mobileSpeedDl').textContent=s.down_rate_h||'0 B/s';if($('mobileSpeedUl')) $('mobileSpeedUl').textContent=s.up_rate_h||'0 B/s';lastLimits={down:Number(s.down_limit||0),up:Number(s.up_limit||0)};$('statDlLimit').textContent=s.down_limit_h||'∞';$('statUlLimit').textContent=s.up_limit_h||'∞';$('statTotalDl').textContent=compactTransferText(s.total_down_h);$('statTotalUl').textContent=compactTransferText(s.total_up_h);drawTraffic(s.down_rate,s.up_rate);drawDiskUsage(s.disk);updateSocketStatus(s);applyFooterPreferences();}); + updateSortHeaders(); applyColumnVisibility(); renderColumnManager(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); })(); diff --git a/pytorrent/static/styles.css b/pytorrent/static/styles.css index 9ef82e6..4762fc8 100644 --- a/pytorrent/static/styles.css +++ b/pytorrent/static/styles.css @@ -936,3 +936,35 @@ body.mobile-mode #mobileList { padding-top: 5.2rem !important; } .empty-state span { color: var(--bs-secondary-color); } + +.footer-pref-hidden { + display: none !important; +} + +/* Footer preferences */ +.footer-preferences { + display: grid; + gap: .5rem; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); +} + +.footer-pref-card { + align-items: center; + border: 1px solid var(--bs-border-color); + border-radius: .75rem; + display: flex; + gap: .55rem; + margin: 0; + padding: .6rem .7rem; +} + +.footer-pref-card.active { + background: rgba(var(--bs-primary-rgb), .08); + border-color: rgba(var(--bs-primary-rgb), .35); +} + +#statusClock, +#statusSockets { + white-space: nowrap; +} + diff --git a/pytorrent/templates/index.html b/pytorrent/templates/index.html index 7e818fd..acb00c3 100644 --- a/pytorrent/templates/index.html +++ b/pytorrent/templates/index.html @@ -98,11 +98,11 @@ @@ -145,13 +145,13 @@ - +
- +