From 88d956676e108d2d06feb191e5b174f9c1a2649b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 5 Jun 2026 13:26:29 +0200 Subject: [PATCH] tracker details --- pytorrent/static/js/mobileTorrentDetails.js | 2 +- pytorrent/static/js/torrentTrackerDetails.js | 2 +- pytorrent/static/styles.css | 54 ++++++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/pytorrent/static/js/mobileTorrentDetails.js b/pytorrent/static/js/mobileTorrentDetails.js index 87410a1..f046fd4 100644 --- a/pytorrent/static/js/mobileTorrentDetails.js +++ b/pytorrent/static/js/mobileTorrentDetails.js @@ -1 +1 @@ -export const mobileTorrentDetailsSource = " function mobileDetailValue(value, fallback='-'){\n const text = value === null || value === undefined || value === '' ? fallback : String(value);\n return esc(text);\n }\n function mobileDetailsStatCards(t){\n const stats = [\n ['Status', t.status || '-'],\n ['Progress', `${Number(t.progress || 0)}%`],\n ['Size', t.size_h || '-'],\n ['Downloaded', t.down_total_h || '-'],\n ['Uploaded', t.up_total_h || '-'],\n ['DL / UL', `${t.down_rate_h || '-'} / ${t.up_rate_h || '-'}`],\n ['Seeds / Peers', `${t.seeds ?? 0} / ${t.peers ?? 0}`],\n ['Ratio', t.ratio ?? '-'],\n ['ETA', t.eta_h || '-'],\n ['Added', formatDateTime(t.created)],\n ];\n return stats.map(([label,value]) => `
${esc(label)}${mobileDetailValue(value)}
`).join('');\n }\n function mobileDetailsPeerRows(peers){\n // Note: Mobile peers use the same responsive table wrapper as desktop details for consistent spacing and scrolling.\n return (peers || []).slice(0, 40).map(p => {\n const location = [p.country, p.city].filter(Boolean).join(', ') || '-';\n const ip = `${esc(p.ip || '-')}`;\n const row = [flag(p.country_iso), ip];\n if(reverseDnsEnabled) row.push(peerHostCell(p));\n row.push(esc(location), esc(p.client || '-'), progressBar(p.completed || 0, 'peer-progress peer-progress-wide'), esc(p.down_rate_h || '-'), esc(p.up_rate_h || '-'), esc(p.port || '-'), peerBadges(p));\n return row;\n });\n }\n function scheduleMobileReverseDnsRefresh(hash, torrent, payload){\n // Note: Mobile details refresh only the peers payload while preserving already-loaded files and trackers.\n clearTimeout(mobileReverseDnsRefreshTimer);\n if(!hasPendingReverseDns(payload.peers?.value?.peers || [])){ mobileReverseDnsRefreshAttempts=0; return; }\n if(!hash || mobileReverseDnsRefreshAttempts>=REVERSE_DNS_REFRESH_MAX_ATTEMPTS) return;\n mobileReverseDnsRefreshAttempts+=1;\n mobileReverseDnsRefreshTimer=setTimeout(async()=>{\n if(selectedHash!==hash || !$('mobileDetailsModal')?.classList.contains('show')) return;\n try{\n const peersJson=await fetchMobileDetailsJson(hash, 'peers');\n const nextPayload={...payload, peers:{status:'fulfilled', value:peersJson}};\n const body=$('mobileDetailsBody');\n if(body && selectedHash===hash){\n body.innerHTML=renderMobileDetailsContent(torrent, nextPayload);\n scheduleMobileReverseDnsRefresh(hash, torrent, nextPayload);\n }\n }catch(_){ }\n }, REVERSE_DNS_REFRESH_SECONDS*1000);\n }\n function mobileDetailsPeerTable(peers){\n const headers = ['Flag', 'IP'];\n if(reverseDnsEnabled) headers.push('Host');\n headers.push('Location', 'Client', '%', 'DL', 'UL', 'Port', 'Flags');\n const rows = mobileDetailsPeerRows(peers);\n if(!rows.length) return '
No peers returned by rTorrent.
';\n return responsiveTable(headers, rows, reverseDnsEnabled ? 'peers-table mobile-details-peers-table peers-table-hosts' : 'peers-table mobile-details-peers-table');\n }\n function mobileDetailsFileTable(files){\n const rows = (files || []).map(file => {\n const priority = FILE_PRIORITY_LABELS[Number(file.priority || 0)] || file.priority || '-';\n const actions = `
${renderFileInfoButton(file)}
`;\n return [\n `${esc(file.path || file.name || '-')}`,\n esc(file.size_h || '-'),\n progressBar(file.progress ?? 0, 'file-progress'),\n `${esc(priority)}`,\n renderFilePrioritySelect(file),\n actions,\n ];\n });\n // Note: Mobile files now reuse the same compact table pattern as peers, with per-file priority, state, info and download actions.\n if(!rows.length) return '
No files returned by rTorrent.
';\n return responsiveTable(['Path', 'Size', 'Done', 'Priority', 'Set priority', 'Actions'], rows, 'file-priority-table mobile-details-files-table');\n }\n function mobileDetailsTrackerItem(t){\n return `
  • ${esc(t.url || '-')}Seeds / Peers: ${esc(trackerSeedsPeers(t))}
  • `;\n }\n function mobileDetailsSection(title, icon, body, meta='', options={}){\n const collapsed = !!options.collapsed;\n const titleMarkup = `
    ${esc(title)}${meta ? `${esc(meta)}` : ''}
    `;\n if(collapsed){\n // Note: Heavy mobile sections start collapsed to keep torrent details quick to scan on phones.\n return `
    ${titleMarkup}${body}
    `;\n }\n return `
    ${titleMarkup}${body}
    `;\n }\n function ensureMobileDetailsModal(){\n let modal = $('mobileDetailsModal');\n if(modal) return modal;\n // Note: Mobile torrent details are lazy-created so the desktop details pane and existing tabs stay unchanged.\n modal = document.createElement('div');\n modal.id = 'mobileDetailsModal';\n modal.className = 'modal fade mobile-details-modal';\n modal.tabIndex = -1;\n modal.innerHTML = `
    Torrent details
    Loading torrent details...
    `;\n document.body.appendChild(modal);\n return modal;\n }\n function renderMobileDetailsContent(t, payload){\n const peers = payload.peers?.status === 'fulfilled' ? (payload.peers.value.peers || []) : [];\n const files = payload.files?.status === 'fulfilled' ? (payload.files.value.files || []) : [];\n const trackers = payload.trackers?.status === 'fulfilled' ? (payload.trackers.value.trackers || []) : [];\n const failures = ['peers','files','trackers'].filter(key => payload[key]?.status === 'rejected').map(key => `${key}: ${payload[key].reason?.message || 'failed'}`);\n const fullPath = joinRemotePath(t.path, t.name);\n const peerTable = mobileDetailsPeerTable(peers);\n const fileTable = mobileDetailsFileTable(files);\n const trackerList = trackers.slice(0, 12).map(mobileDetailsTrackerItem).join('') || '
  • No trackers returned by rTorrent.
  • ';\n const generalBody = `
    ${esc(t.name || '-')}
    Path${esc(fullPath)}
    Hash${esc(t.hash || '-')}
    ${mobileDetailsStatCards(t)}
    `;\n const messageBody = `
    ${esc(t.message || 'No message.')}
    `;\n const errorBox = failures.length ? `
    Partial details loaded
    ${esc(failures.join(' | '))}
    ` : '';\n // Note: General and heavy lists start collapsed on mobile so the modal opens cleanly and the user expands only the section needed.\n return `${errorBox}${mobileDetailsSection('General', 'fa-circle-info', generalBody, '', {collapsed:true})}${mobileDetailsSection('Peers', 'fa-users', peerTable, peers.length > 40 ? `showing 40 of ${peers.length}` : `${peers.length} total`, {collapsed:true})}${mobileDetailsSection('Files', 'fa-folder-tree', fileTable, files.length ? `${files.length} total` : '', {collapsed:true})}${mobileDetailsSection('Trackers', 'fa-bullhorn', ``, trackers.length > 12 ? `showing 12 of ${trackers.length}` : `${trackers.length} total`, {collapsed:true})}${mobileDetailsSection('Message', 'fa-message', messageBody, '', {collapsed:true})}`;\n }\n async function fetchMobileDetailsJson(hash, tab){\n const res = await fetch(`/api/torrents/${encodeURIComponent(hash)}/${tab}`, {headers:{'Accept':'application/json'}});\n const json = await res.json().catch(() => ({}));\n if(!res.ok || !json.ok) throw new Error(json.error || `HTTP ${res.status}`);\n return json;\n }\n async function openMobileDetails(hash){\n const t = torrents.get(hash);\n if(!t) return toast('Torrent is no longer available.','warning');\n selectedHash = hash;\n lastSelectedHash = hash;\n const modal = ensureMobileDetailsModal();\n const title = $('mobileDetailsTitle');\n const subtitle = $('mobileDetailsSubtitle');\n const body = $('mobileDetailsBody');\n if(title) title.innerHTML = ' Torrent details';\n if(subtitle) subtitle.textContent = t.name || hash;\n if(body) body.innerHTML = '
    Loading peers, files and trackers...
    ';\n new bootstrap.Modal(modal).show();\n try{\n // Note: The mobile modal reads existing lightweight detail endpoints without changing the desktop details tabs.\n const [peers, files, trackers] = await Promise.allSettled([\n fetchMobileDetailsJson(hash, 'peers'),\n fetchMobileDetailsJson(hash, 'files'),\n fetchMobileDetailsJson(hash, 'trackers'),\n ]);\n if(body) body.innerHTML = renderMobileDetailsContent(t, {peers, files, trackers});\n mobileReverseDnsRefreshAttempts=0;\n scheduleMobileReverseDnsRefresh(hash, t, {peers, files, trackers});\n }catch(e){\n if(body) body.innerHTML = `
    Details failed
    ${esc(e.message)}
    `;\n }\n }\n\n"; +export const mobileTorrentDetailsSource = " function mobileDetailValue(value, fallback='-'){\n const text = value === null || value === undefined || value === '' ? fallback : String(value);\n return esc(text);\n }\n function mobileDetailsStatCards(t){\n const stats = [\n ['Status', t.status || '-'],\n ['Progress', `${Number(t.progress || 0)}%`],\n ['Size', t.size_h || '-'],\n ['Downloaded', t.down_total_h || '-'],\n ['Uploaded', t.up_total_h || '-'],\n ['DL / UL', `${t.down_rate_h || '-'} / ${t.up_rate_h || '-'}`],\n ['Seeds / Peers', `${t.seeds ?? 0} / ${t.peers ?? 0}`],\n ['Ratio', t.ratio ?? '-'],\n ['ETA', t.eta_h || '-'],\n ['Added', formatDateTime(t.created)],\n ];\n return stats.map(([label,value]) => `
    ${esc(label)}${mobileDetailValue(value)}
    `).join('');\n }\n function mobileDetailsPeerRows(peers){\n // Note: Mobile peers use the same responsive table wrapper as desktop details for consistent spacing and scrolling.\n return (peers || []).slice(0, 40).map(p => {\n const location = [p.country, p.city].filter(Boolean).join(', ') || '-';\n const ip = `${esc(p.ip || '-')}`;\n const row = [flag(p.country_iso), ip];\n if(reverseDnsEnabled) row.push(peerHostCell(p));\n row.push(esc(location), esc(p.client || '-'), progressBar(p.completed || 0, 'peer-progress peer-progress-wide'), esc(p.down_rate_h || '-'), esc(p.up_rate_h || '-'), esc(p.port || '-'), peerBadges(p));\n return row;\n });\n }\n function scheduleMobileReverseDnsRefresh(hash, torrent, payload){\n // Note: Mobile details refresh only the peers payload while preserving already-loaded files and trackers.\n clearTimeout(mobileReverseDnsRefreshTimer);\n if(!hasPendingReverseDns(payload.peers?.value?.peers || [])){ mobileReverseDnsRefreshAttempts=0; return; }\n if(!hash || mobileReverseDnsRefreshAttempts>=REVERSE_DNS_REFRESH_MAX_ATTEMPTS) return;\n mobileReverseDnsRefreshAttempts+=1;\n mobileReverseDnsRefreshTimer=setTimeout(async()=>{\n if(selectedHash!==hash || !$('mobileDetailsModal')?.classList.contains('show')) return;\n try{\n const peersJson=await fetchMobileDetailsJson(hash, 'peers');\n const nextPayload={...payload, peers:{status:'fulfilled', value:peersJson}};\n const body=$('mobileDetailsBody');\n if(body && selectedHash===hash){\n body.innerHTML=renderMobileDetailsContent(torrent, nextPayload);\n scheduleMobileReverseDnsRefresh(hash, torrent, nextPayload);\n }\n }catch(_){ }\n }, REVERSE_DNS_REFRESH_SECONDS*1000);\n }\n function mobileDetailsPeerTable(peers){\n const headers = ['Flag', 'IP'];\n if(reverseDnsEnabled) headers.push('Host');\n headers.push('Location', 'Client', '%', 'DL', 'UL', 'Port', 'Flags');\n const rows = mobileDetailsPeerRows(peers);\n if(!rows.length) return '
    No peers returned by rTorrent.
    ';\n return responsiveTable(headers, rows, reverseDnsEnabled ? 'peers-table mobile-details-peers-table peers-table-hosts' : 'peers-table mobile-details-peers-table');\n }\n function mobileDetailsFileTable(files){\n const rows = (files || []).map(file => {\n const priority = FILE_PRIORITY_LABELS[Number(file.priority || 0)] || file.priority || '-';\n const actions = `
    ${renderFileInfoButton(file)}
    `;\n return [\n `${esc(file.path || file.name || '-')}`,\n esc(file.size_h || '-'),\n progressBar(file.progress ?? 0, 'file-progress'),\n `${esc(priority)}`,\n renderFilePrioritySelect(file),\n actions,\n ];\n });\n // Note: Mobile files now reuse the same compact table pattern as peers, with per-file priority, state, info and download actions.\n if(!rows.length) return '
    No files returned by rTorrent.
    ';\n return responsiveTable(['Path', 'Size', 'Done', 'Priority', 'Set priority', 'Actions'], rows, 'file-priority-table mobile-details-files-table');\n }\n function mobileDetailsTrackerRows(trackers){\n // Note: Mobile trackers mirror the desktop tracker table instead of using a separate list layout.\n return (trackers || []).slice(0, 12).map(t => [\n `#${esc(t.index)}`,\n trackerUrlCell(t),\n trackerEnabledCell(t.enabled),\n esc(trackerSeedsPeers(t)),\n esc(t.downloaded ?? '-'),\n fmtTs(t.last_announce),\n ]);\n }\n function mobileDetailsTrackerTable(trackers){\n const rows = mobileDetailsTrackerRows(trackers);\n if(!rows.length) return '
    No trackers returned by rTorrent.
    ';\n return responsiveTable(['#', 'URL', 'On', 'Seeds / Peers', 'Done', 'Last announce'], rows, 'tracker-table mobile-details-trackers-table');\n }\n function mobileDetailsSection(title, icon, body, meta='', options={}){\n const collapsed = !!options.collapsed;\n const titleMarkup = `
    ${esc(title)}${meta ? `${esc(meta)}` : ''}
    `;\n if(collapsed){\n // Note: Heavy mobile sections start collapsed to keep torrent details quick to scan on phones.\n return `
    ${titleMarkup}${body}
    `;\n }\n return `
    ${titleMarkup}${body}
    `;\n }\n function ensureMobileDetailsModal(){\n let modal = $('mobileDetailsModal');\n if(modal) return modal;\n // Note: Mobile torrent details are lazy-created so the desktop details pane and existing tabs stay unchanged.\n modal = document.createElement('div');\n modal.id = 'mobileDetailsModal';\n modal.className = 'modal fade mobile-details-modal';\n modal.tabIndex = -1;\n modal.innerHTML = `
    Torrent details
    Loading torrent details...
    `;\n document.body.appendChild(modal);\n return modal;\n }\n function renderMobileDetailsContent(t, payload){\n const peers = payload.peers?.status === 'fulfilled' ? (payload.peers.value.peers || []) : [];\n const files = payload.files?.status === 'fulfilled' ? (payload.files.value.files || []) : [];\n const trackers = payload.trackers?.status === 'fulfilled' ? (payload.trackers.value.trackers || []) : [];\n const failures = ['peers','files','trackers'].filter(key => payload[key]?.status === 'rejected').map(key => `${key}: ${payload[key].reason?.message || 'failed'}`);\n const fullPath = joinRemotePath(t.path, t.name);\n const peerTable = mobileDetailsPeerTable(peers);\n const fileTable = mobileDetailsFileTable(files);\n const trackerTable = mobileDetailsTrackerTable(trackers);\n const generalBody = `
    ${esc(t.name || '-')}
    Path${esc(fullPath)}
    Hash${esc(t.hash || '-')}
    ${mobileDetailsStatCards(t)}
    `;\n const messageBody = `
    ${esc(t.message || 'No message.')}
    `;\n const errorBox = failures.length ? `
    Partial details loaded
    ${esc(failures.join(' | '))}
    ` : '';\n // Note: General and heavy lists start collapsed on mobile so the modal opens cleanly and the user expands only the section needed.\n return `${errorBox}${mobileDetailsSection('General', 'fa-circle-info', generalBody, '', {collapsed:true})}${mobileDetailsSection('Peers', 'fa-users', peerTable, peers.length > 40 ? `showing 40 of ${peers.length}` : `${peers.length} total`, {collapsed:true})}${mobileDetailsSection('Files', 'fa-folder-tree', fileTable, files.length ? `${files.length} total` : '', {collapsed:true})}${mobileDetailsSection('Trackers', 'fa-bullhorn', trackerTable, trackers.length > 12 ? `showing 12 of ${trackers.length}` : `${trackers.length} total`, {collapsed:true})}${mobileDetailsSection('Message', 'fa-message', messageBody, '', {collapsed:true})}`;\n }\n async function fetchMobileDetailsJson(hash, tab){\n const res = await fetch(`/api/torrents/${encodeURIComponent(hash)}/${tab}`, {headers:{'Accept':'application/json'}});\n const json = await res.json().catch(() => ({}));\n if(!res.ok || !json.ok) throw new Error(json.error || `HTTP ${res.status}`);\n return json;\n }\n async function openMobileDetails(hash){\n const t = torrents.get(hash);\n if(!t) return toast('Torrent is no longer available.','warning');\n selectedHash = hash;\n lastSelectedHash = hash;\n const modal = ensureMobileDetailsModal();\n const title = $('mobileDetailsTitle');\n const subtitle = $('mobileDetailsSubtitle');\n const body = $('mobileDetailsBody');\n if(title) title.innerHTML = ' Torrent details';\n if(subtitle) subtitle.textContent = t.name || hash;\n if(body) body.innerHTML = '
    Loading peers, files and trackers...
    ';\n new bootstrap.Modal(modal).show();\n try{\n // Note: The mobile modal reads existing lightweight detail endpoints without changing the desktop details tabs.\n const [peers, files, trackers] = await Promise.allSettled([\n fetchMobileDetailsJson(hash, 'peers'),\n fetchMobileDetailsJson(hash, 'files'),\n fetchMobileDetailsJson(hash, 'trackers'),\n ]);\n if(body) body.innerHTML = renderMobileDetailsContent(t, {peers, files, trackers});\n mobileReverseDnsRefreshAttempts=0;\n scheduleMobileReverseDnsRefresh(hash, t, {peers, files, trackers});\n }catch(e){\n if(body) body.innerHTML = `
    Details failed
    ${esc(e.message)}
    `;\n }\n }\n\n"; diff --git a/pytorrent/static/js/torrentTrackerDetails.js b/pytorrent/static/js/torrentTrackerDetails.js index f93ca1c..ffae31b 100644 --- a/pytorrent/static/js/torrentTrackerDetails.js +++ b/pytorrent/static/js/torrentTrackerDetails.js @@ -1 +1 @@ -export const torrentTrackerDetailsSource = " function fmtTs(value){ const n=Number(value||0); if(!n) return '-'; try{return new Date(n*1000).toLocaleString();}catch(e){return String(n);} }\n function trackerSeedsPeers(t){ const hasScrape = t.seeds !== null || t.peers !== null; return hasScrape ? `${t.seeds ?? \"-\"} / ${t.peers ?? \"-\"}` : \"-\"; }\n function renderTrackers(trackers){\n // Note: Tracker URL editing is intentionally replaced by safe deletion; adding trackers remains unchanged.\n const pane=$('detailPane');\n const list=trackers||[];\n const canDelete=list.length>1;\n const rows=list.map(t=>{\n const idx=esc(t.index), url=esc(t.url);\n const deleteDisabled=canDelete ? '' : ' disabled title=\"At least one tracker must remain\"';\n return [`#${idx}`, `${url || '-'}`, t.enabled?'yes':'no', esc(trackerSeedsPeers(t)), esc(t.downloaded ?? '-'), fmtTs(t.last_announce), `
    `];\n });\n // Note: Trackers share the responsive wrapper so long URLs do not break the details pane.\n pane.innerHTML=`
    ${responsiveTable(['#','URL','On','Seeds / Peers','Done','Last announce','Actions'], rows.length?rows:[[ '-','No trackers.','','','','','' ]], 'tracker-table')}`;\n }\n async function trackerAction(action,payload={}){\n if(!selectedHash) return toastMessage('toast.noTorrentSelected','warning');\n setBusy(true);\n try{\n const j=await post(`/api/torrents/${encodeURIComponent(selectedHash)}/trackers/${action}`,payload);\n toast(j.message || appMessage('toast.trackerActionDone',{action}),'success');\n await loadDetails('trackers');\n }catch(e){toast(e.message,'danger');}\n finally{setBusy(false);}\n }\n\n"; +export const torrentTrackerDetailsSource = " function fmtTs(value){ const n=Number(value||0); if(!n) return '-'; try{return new Date(n*1000).toLocaleString();}catch(e){return String(n);} }\n function trackerSeedsPeers(t){ const hasScrape = t.seeds !== null || t.peers !== null; return hasScrape ? `${t.seeds ?? \"-\"} / ${t.peers ?? \"-\"}` : \"-\"; }\n function trackerUrlCell(t){\n const url=String(t.url||'').trim();\n // Note: Tracker URLs now use the same single-line ellipsis behavior as peer table cells.\n return url ? `${esc(url)}` : '-';\n }\n function trackerEnabledCell(enabled){\n // Note: Tracker enabled state is rendered as a compact badge to keep row height aligned with peers.\n return enabled ? 'yes' : 'no';\n }\n function renderTrackers(trackers){\n // Note: Tracker URL editing is intentionally replaced by safe deletion; adding trackers remains unchanged.\n const pane=$('detailPane');\n const list=trackers||[];\n const canDelete=list.length>1;\n const rows=list.map(t=>{\n const idx=esc(t.index);\n const deleteDisabled=canDelete ? '' : ' disabled title=\"At least one tracker must remain\"';\n return [`#${idx}`, trackerUrlCell(t), trackerEnabledCell(t.enabled), esc(trackerSeedsPeers(t)), esc(t.downloaded ?? '-'), fmtTs(t.last_announce), `
    `];\n });\n // Note: Trackers now use the same fixed responsive table pattern as peers for consistent row text and spacing.\n pane.innerHTML=`
    ${responsiveTable(['#','URL','On','Seeds / Peers','Done','Last announce','Actions'], rows.length?rows:[[ '-','No trackers.','','','','','' ]], 'tracker-table')}`;\n }\n async function trackerAction(action,payload={}){\n if(!selectedHash) return toastMessage('toast.noTorrentSelected','warning');\n setBusy(true);\n try{\n const j=await post(`/api/torrents/${encodeURIComponent(selectedHash)}/trackers/${action}`,payload);\n toast(j.message || appMessage('toast.trackerActionDone',{action}),'success');\n await loadDetails('trackers');\n }catch(e){toast(e.message,'danger');}\n finally{setBusy(false);}\n }\n\n"; diff --git a/pytorrent/static/styles.css b/pytorrent/static/styles.css index 8c15a66..2f9c3f7 100644 --- a/pytorrent/static/styles.css +++ b/pytorrent/static/styles.css @@ -2221,8 +2221,55 @@ body.mobile-mode .mobile-filter-bar { max-width: 520px; } +.tracker-table { + min-width: 960px; + table-layout: fixed; + width: 100%; +} + +.tracker-table th, +.tracker-table td { + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; +} + +.tracker-table th:nth-child(1), +.tracker-table td:nth-child(1) { + width: 6%; +} + +.tracker-table th:nth-child(2), +.tracker-table td:nth-child(2) { + width: 38%; +} + +.tracker-table th:nth-child(3), +.tracker-table td:nth-child(3), +.tracker-table th:nth-child(4), +.tracker-table td:nth-child(4), +.tracker-table th:nth-child(5), +.tracker-table td:nth-child(5) { + width: 10%; +} + +.tracker-table th:nth-child(6), +.tracker-table td:nth-child(6) { + width: 16%; +} + +.tracker-table th:nth-child(7), +.tracker-table td:nth-child(7) { + width: 10%; +} + .tracker-url-text { - word-break: break-all; + display: block; + max-width: 100%; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .tool-note { @@ -5450,10 +5497,11 @@ body, .mobile-details-files-table { margin-bottom: 0; + min-width: 760px; } -.mobile-details-files-table { - min-width: 760px; +.mobile-details-trackers-table { + margin-bottom: 0; } .mobile-details-files-table .file-progress {