2 lines
9.6 KiB
JavaScript
2 lines
9.6 KiB
JavaScript
export const stateCoreSource = " const $ = (id) => document.getElementById(id);\n const esc = (s) => String(s ?? \"\").replace(/[&<>'\"]/g, c => ({\"&\":\"&\",\"<\":\"<\",\">\":\">\",\"'\":\"'\",'\"':\""\"}[c]));\n // Note: Footer transfer totals can arrive as already formatted strings, so keep this helper tolerant and side-effect free.\n function compactTransferText(value){\n const text = String(value ?? \"\").trim();\n if(!text) return \"-\";\n return text.replace(/\\\\s+/g, \" \");\n }\n function clampTorrentListFontSize(value){ value = Number(value || 13); if(!Number.isFinite(value)) value = 13; return Math.max(11, Math.min(16, Math.round(value))); }\n const ROW_HEIGHT = 32, COMPACT_ROW_HEIGHT = 24, OVERSCAN = 14;\n const torrents = new Map();\n function browserViewPrefsKey(){\n const profileId = String(window.PYTORRENT?.activeProfile || activeProfileId || 'none');\n return `pyTorrent.mobileViewPrefs.${profileId}`;\n }\n function loadBrowserViewPrefs(){\n try{\n const scoped = JSON.parse(localStorage.getItem(browserViewPrefsKey())||'{}')||{};\n if(Object.keys(scoped).length) return scoped;\n const legacy = JSON.parse(localStorage.getItem('pyTorrent.mobileViewPrefs')||'{}')||{};\n return String(window.PYTORRENT?.activeProfile || activeProfileId || 'none') === 'none' ? legacy : {};\n }catch(e){ return {}; }\n }\n function isActiveProfilePayload(payload){\n const payloadProfile = Number(payload?.profile_id || 0);\n const activeProfile = Number(window.PYTORRENT?.activeProfile || activeProfileId || 0);\n return !payloadProfile || !activeProfile || payloadProfile === activeProfile;\n }\n let browserViewPrefs = loadBrowserViewPrefs();\n function restoreProfileScopedViewPrefs(){\n // Note: Profile switch must reload browser-only filters/sort from the active profile key; otherwise stale values stay until hard refresh.\n browserViewPrefs = loadBrowserViewPrefs();\n const restoredFilter = String(browserViewPrefs.activeFilter || window.PYTORRENT?.activeFilter || \"all\");\n mobileActiveFilterKey = String(browserViewPrefs.mobileFilterKey || restoredFilter || \"all\");\n activeFilter = restoredFilter.startsWith(\"tracker:\") ? \"all\" : (restoredFilter || \"all\");\n activeTrackerFilter = restoredFilter.startsWith(\"tracker:\") ? restoredFilter.slice(8) : \"\";\n const restoredSort = browserViewPrefs.sortState || window.PYTORRENT?.torrentSort || {};\n sortState = {key: SORT_KEYS.has(restoredSort.key) ? restoredSort.key : \"name\", dir: Number(restoredSort.dir) < 0 ? -1 : 1};\n if(browserViewPrefs.columnWidths) columnWidths = normalizeColumnWidths({...columnWidths, ...browserViewPrefs.columnWidths});\n if(browserViewPrefs.mobileColumns) mobileColumns = normalizeMobileColumns({...mobileColumns, ...browserViewPrefs.mobileColumns});\n if(browserViewPrefs.mobileSortFilters) mobileSortFilters = normalizeMobileSortFilters({...mobileSortFilters, ...browserViewPrefs.mobileSortFilters});\n if($('searchBox')) $('searchBox').value = '';\n if($('tableWrap')) $('tableWrap').scrollTop = 0;\n if($('mobileList')) $('mobileList').scrollTop = 0;\n lastRenderSignature = '';\n lastLabelFiltersSignature = '';\n lastTrackerFiltersSignature = '';\n }\n function clearProfileScopedTorrentView(reason=''){\n // Note: Profile switches clear visible torrent/filter state synchronously so cached rows from the previous profile cannot flash in the UI.\n profileViewGeneration += 1;\n clearTimeout(trackerSummaryTimer);\n hasTorrentSnapshot = false;\n torrentSummary = null;\n trackerSummary = {hashes:{}, trackers:[], scanned:0, errors:[], pending:0, cached:0};\n trackerSummaryStatus = 'idle';\n trackerSummarySignature = '';\n lastLabelFiltersSignature = '';\n lastTrackerFiltersSignature = '';\n lastMobileFiltersSignature = '';\n lastRenderSignature = '';\n visibleRows = [];\n torrents.clear();\n selected.clear();\n activeOperations.clear();\n selectedHash = null;\n lastSelectedHash = null;\n const body = $('torrentBody');\n if(body){\n body.classList.remove('torrent-virtual-body');\n body.style.height = '';\n body.innerHTML = loadingTableRow(reason || 'Loading torrents...');\n }\n const mobileList = $('mobileList');\n if(mobileList) mobileList.innerHTML = loadingMarkup(reason || 'Loading torrents...');\n const detail = $('detailPane');\n if(detail) detail.innerHTML = 'Select a torrent to load details.';\n if($('tableWrap')) $('tableWrap').scrollTop = 0;\n if($('mobileList')) $('mobileList').scrollTop = 0;\n renderCounts();\n renderLabelFilters(true);\n renderTrackerFilters(true);\n updateBulkBar();\n }\n const savedFilter = String(browserViewPrefs.activeFilter || window.PYTORRENT?.activeFilter || \"all\");\n let sidebarLabelsExpanded = Number(window.PYTORRENT?.sidebarLabelsExpanded || 0) !== 0;\n let sidebarShortcutsExpanded = Number(window.PYTORRENT?.sidebarShortcutsExpanded || 0) !== 0;\n const SMART_QUEUE_TECHNICAL_LABEL = String(window.PYTORRENT?.smartQueueTechnicalLabel || \"Smart Queue Stopped\").trim();\n const SMART_QUEUE_STALLED_LABEL = String(window.PYTORRENT?.smartQueueStalledLabel || \"Stalled\").trim();\n // Note: Mobile has both \"All\" and \"All trackers\" options, so keep the exact selected option separate from the shared filter state.\n let mobileActiveFilterKey = String(browserViewPrefs.mobileFilterKey || savedFilter || \"all\");\n let visibleRows = [], selected = new Set(), selectedHash = null, lastSelectedHash = null, activeFilter = savedFilter.startsWith(\"tracker:\") ? \"all\" : (savedFilter || \"all\");\n let activeTrackerFilter = savedFilter.startsWith(\"tracker:\") ? savedFilter.slice(8) : \"\";\n const SORT_KEYS = new Set([\"name\", \"status\", \"size\", \"progress\", \"down_rate\", \"up_rate\", \"eta\", \"seeds\", \"peers\", \"ratio\", \"path\", \"label\", \"ratio_group\", \"down_total\", \"to_download\", \"up_total\", \"created\", \"last_activity\", \"priority\", \"state\", \"active\", \"complete\", \"hashing\", \"message\", \"hash\"]);\n const savedSort = browserViewPrefs.sortState || window.PYTORRENT?.torrentSort || {};\n let sortState = {key: SORT_KEYS.has(savedSort.key) ? savedSort.key : \"name\", dir: Number(savedSort.dir) < 0 ? -1 : 1}, renderPending = false, renderVersion = 0, lastRenderSignature = \"\";\n let compactTorrentListEnabled = Number(window.PYTORRENT?.compactTorrentListEnabled || 0) !== 0;\n let torrentListFontSize = clampTorrentListFontSize(window.PYTORRENT?.torrentListFontSize || 13);\n // Note: Mobile sort filters are configurable because the full sortable list is too large for quick phone use.\n const DEFAULT_MOBILE_SORT_FILTER_IDS = new Set([\"seeds:-1\", \"up_rate:-1\", \"down_rate:-1\", \"progress:-1\"]);\n const MOBILE_SORT_STEPS = [\n {key:\"down_rate\", dir:-1, label:\"DL\"},\n {key:\"down_rate\", dir:1, label:\"DL\"},\n {key:\"up_rate\", dir:-1, label:\"UL\"},\n {key:\"up_rate\", dir:1, label:\"UL\"},\n {key:\"progress\", dir:-1, label:\"Progress\"},\n {key:\"progress\", dir:1, label:\"Progress\"},\n {key:\"eta\", dir:-1, label:\"ETA\"},\n {key:\"eta\", dir:1, label:\"ETA\"},\n {key:\"ratio\", dir:-1, label:\"Ratio\"},\n {key:\"ratio\", dir:1, label:\"Ratio\"},\n {key:\"size\", dir:-1, label:\"Size\"},\n {key:\"size\", dir:1, label:\"Size\"},\n {key:\"seeds\", dir:-1, label:\"Seeds\"},\n {key:\"seeds\", dir:1, label:\"Seeds\"},\n {key:\"peers\", dir:-1, label:\"Peers\"},\n {key:\"peers\", dir:1, label:\"Peers\"},\n {key:\"status\", dir:1, label:\"Status\"},\n {key:\"status\", dir:-1, label:\"Status\"},\n {key:\"label\", dir:1, label:\"Label\"},\n {key:\"label\", dir:-1, label:\"Label\"},\n {key:\"ratio_group\", dir:1, label:\"Ratio group\"},\n {key:\"ratio_group\", dir:-1, label:\"Ratio group\"},\n {key:\"down_total\", dir:-1, label:\"Downloaded\"},\n {key:\"down_total\", dir:1, label:\"Downloaded\"},\n {key:\"to_download\", dir:-1, label:\"To download\"},\n {key:\"to_download\", dir:1, label:\"To download\"},\n {key:\"up_total\", dir:-1, label:\"Uploaded\"},\n {key:\"up_total\", dir:1, label:\"Uploaded\"},\n {key:\"created\", dir:-1, label:\"Created\"},\n {key:\"created\", dir:1, label:\"Created\"},\n {key:\"last_activity\", dir:-1, label:\"Last activity\"},\n {key:\"last_activity\", dir:1, label:\"Last activity\"},\n {key:\"priority\", dir:-1, label:\"Priority\"},\n {key:\"priority\", dir:1, label:\"Priority\"},\n {key:\"state\", dir:-1, label:\"State\"},\n {key:\"state\", dir:1, label:\"State\"},\n {key:\"active\", dir:-1, label:\"Active\"},\n {key:\"active\", dir:1, label:\"Active\"},\n {key:\"complete\", dir:-1, label:\"Complete\"},\n {key:\"complete\", dir:1, label:\"Complete\"},\n {key:\"hashing\", dir:-1, label:\"Hashing\"},\n {key:\"hashing\", dir:1, label:\"Hashing\"},\n {key:\"message\", dir:1, label:\"Message\"},\n {key:\"message\", dir:-1, label:\"Message\"},\n {key:\"path\", dir:1, label:\"Path\"},\n {key:\"path\", dir:-1, label:\"Path\"},\n {key:\"hash\", dir:1, label:\"Hash\"},\n {key:\"hash\", dir:-1, label:\"Hash\"},\n {key:\"name\", dir:1, label:\"Name\"},\n {key:\"name\", dir:-1, label:\"Name\"}\n ];\n let lastLimits = {down: 0, up: 0}, pendingBusy = 0, pathTarget = null, lastPathParent = \"/\";\n const traffic = [], systemUsage = [];\n const socket = (typeof io === \"function\") ? io({transports:[\"polling\"], reconnection:true, reconnectionAttempts:Infinity, reconnectionDelay:700, reconnectionDelayMax:5000, timeout:8000}) : {connected:false,on(){},emit(){},io:{on(){}}};\n";
|