changes_2
This commit is contained in:
@@ -1 +1 @@
|
|||||||
export const apiSource = " async function post(url,data,method='POST'){\n const res=await fetch(url,{method,headers:{'Content-Type':'application/json','Accept':'application/json'},body:JSON.stringify(data||{})});\n const text=await res.text();\n let json;\n try{ json=JSON.parse(text); }\n catch(e){\n const clean=(text||'').replace(/<[^>]+>/g,' ').replace(/\\s+/g,' ').trim().slice(0,180);\n throw new Error(clean?`Invalid server response (${res.status}): ${clean}`:`Invalid server response (${res.status})`);\n }\n if(!res.ok || !json.ok) throw new Error(json.error||`Operation failed (${res.status})`);\n return json;\n }\n\n async function runAction(action, extra={}){ const hashes=selectedHashes(); if(!hashes.length) return toast('No torrents selected','warning'); let payload={hashes,...extra}; if(action==='move'){ openPathPicker('move'); return; } setBusy(true); try{ const j=await post(`/api/torrents/${action}`,payload); markQueuedJobs(j, hashes, action); if(action==='recheck'){ hashes.forEach(h=>{ const t=torrents.get(h); if(t) torrents.set(h,{...t,status:'Checking',hashing:1,message:'Force recheck queued'}); }); scheduleRender(true); } const parts=Number(j.bulk_parts||1); toast(parts>1?`${action} queued in ${parts} bulk parts`:`${action} queued`,'success'); if(action==='set_label') await loadLabels(); }catch(e){toast(e.message,'danger');} finally{setBusy(false);} }\n function flag(iso){ const code=String(iso||'').toLowerCase(); return code?`<span class=\"fi fi-${esc(code)}\"></span> <span>${esc(code.toUpperCase())}</span>`:'-'; }\n function table(headers,rows,extraClass=''){ const cls=extraClass?` ${extraClass}`:''; return `<table class=\"table table-sm detail-table${cls}\"><thead><tr>${headers.map(h=>`<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${rows.map(r=>`<tr>${r.map(c=>`<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table>`; }\n function responsiveTable(headers,rows,extraClass=''){ return `<div class=\"responsive-table-wrap\">${table(headers,rows,extraClass)}</div>`; }\n function downloadJson(filename, data){ const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(url),500); }\n function filenameFromResponse(res, fallback){ const cd=res.headers.get('Content-Disposition')||''; const m=cd.match(/filename\\*=UTF-8''([^;]+)|filename=\"?([^\";]+)\"?/i); try{ return decodeURIComponent(m?.[1]||m?.[2]||fallback); }catch(e){ return m?.[1]||m?.[2]||fallback; } }\n async function downloadResponse(url, options={}, fallback='download.bin', label='Preparing download...'){\n setBusy(true,label);\n try{\n const res=await fetch(url,options);\n if(!res.ok){ const j=await res.json().catch(()=>({})); throw new Error(j.error||`Download failed: HTTP ${res.status}`); }\n const total=Number(res.headers.get('Content-Length')||0);\n const name=filenameFromResponse(res,fallback);\n let blob;\n if(res.body){\n const reader=res.body.getReader();\n const chunks=[]; let received=0;\n while(true){\n const {done,value}=await reader.read();\n if(done) break;\n chunks.push(value); received += value.length;\n const loader=$('globalLoader');\n const span=loader?.querySelector('span:last-child');\n if(span){\n if(total){\n const pct=Math.max(0,Math.min(100,Math.round((received/total)*100)));\n span.textContent=`Downloading ${pct}%`;\n } else {\n span.textContent=`Downloading ${(received/1024/1024).toFixed(1)} MB`;\n }\n }\n }\n blob=new Blob(chunks);\n } else {\n blob=await res.blob();\n }\n const obj=URL.createObjectURL(blob);\n const a=document.createElement('a'); a.href=obj; a.download=name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(obj),1000);\n toast('Download started','success');\n } finally { setBusy(false); }\n }\n async function downloadTorrentFiles(hashes=null){\n const list=hashes||selectedHashes();\n if(!list.length) return toast('No torrents selected','warning');\n if(list.length===1) return downloadResponse(`/api/torrents/${encodeURIComponent(list[0])}/torrent-file`,{},`${list[0]}.torrent`,'Preparing .torrent...').catch(e=>toast(e.message,'danger'));\n return downloadResponse('/api/torrents/torrent-files.zip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({hashes:list})},'pytorrent-torrents.zip','Preparing torrent ZIP...').catch(e=>toast(e.message,'danger'));\n }\n";
|
export const apiSource = " async function post(url,data,method='POST'){\n const res=await fetch(url,{method,headers:{'Content-Type':'application/json','Accept':'application/json'},body:JSON.stringify(data||{})});\n const text=await res.text();\n let json;\n try{ json=JSON.parse(text); }\n catch(e){\n const clean=(text||'').replace(/<[^>]+>/g,' ').replace(/\\s+/g,' ').trim().slice(0,180);\n throw new Error(clean?`Invalid server response (${res.status}): ${clean}`:`Invalid server response (${res.status})`);\n }\n if(!res.ok || !json.ok) throw new Error(json.error||`Operation failed (${res.status})`);\n return json;\n }\n\n async function runAction(action, extra={}){ const hashes=selectedHashes(); if(!hashes.length) return toastMessage('toast.noTorrentsSelected','warning'); let payload={hashes,...extra}; if(action==='move'){ openPathPicker('move'); return; } setBusy(true); try{ const j=await post(`/api/torrents/${action}`,payload); markQueuedJobs(j, hashes, action); if(action==='recheck'){ hashes.forEach(h=>{ const t=torrents.get(h); if(t) torrents.set(h,{...t,status:'Checking',hashing:1,message:'Force recheck queued'}); }); scheduleRender(true); } const parts=Number(j.bulk_parts||1); toastMessage('toast.actionQueued','success',{action,parts}); if(action==='set_label') await loadLabels(); }catch(e){toast(e.message,'danger');} finally{setBusy(false);} }\n function flag(iso){ const code=String(iso||'').toLowerCase(); return code?`<span class=\"fi fi-${esc(code)}\"></span> <span>${esc(code.toUpperCase())}</span>`:'-'; }\n function table(headers,rows,extraClass=''){ const cls=extraClass?` ${extraClass}`:''; return `<table class=\"table table-sm detail-table${cls}\"><thead><tr>${headers.map(h=>`<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${rows.map(r=>`<tr>${r.map(c=>`<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table>`; }\n function responsiveTable(headers,rows,extraClass=''){ return `<div class=\"responsive-table-wrap\">${table(headers,rows,extraClass)}</div>`; }\n function downloadJson(filename, data){ const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(url),500); }\n function filenameFromResponse(res, fallback){ const cd=res.headers.get('Content-Disposition')||''; const m=cd.match(/filename\\*=UTF-8''([^;]+)|filename=\"?([^\";]+)\"?/i); try{ return decodeURIComponent(m?.[1]||m?.[2]||fallback); }catch(e){ return m?.[1]||m?.[2]||fallback; } }\n async function downloadResponse(url, options={}, fallback='download.bin', label='Preparing download...'){\n setBusy(true,label);\n try{\n const res=await fetch(url,options);\n if(!res.ok){ const j=await res.json().catch(()=>({})); throw new Error(j.error||`Download failed: HTTP ${res.status}`); }\n const total=Number(res.headers.get('Content-Length')||0);\n const name=filenameFromResponse(res,fallback);\n let blob;\n if(res.body){\n const reader=res.body.getReader();\n const chunks=[]; let received=0;\n while(true){\n const {done,value}=await reader.read();\n if(done) break;\n chunks.push(value); received += value.length;\n const loader=$('globalLoader');\n const span=loader?.querySelector('span:last-child');\n if(span){\n if(total){\n const pct=Math.max(0,Math.min(100,Math.round((received/total)*100)));\n span.textContent=`Downloading ${pct}%`;\n } else {\n span.textContent=`Downloading ${(received/1024/1024).toFixed(1)} MB`;\n }\n }\n }\n blob=new Blob(chunks);\n } else {\n blob=await res.blob();\n }\n const obj=URL.createObjectURL(blob);\n const a=document.createElement('a'); a.href=obj; a.download=name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(obj),1000);\n toastMessage('toast.downloadStarted','success');\n } finally { setBusy(false); }\n }\n async function downloadTorrentFiles(hashes=null){\n const list=hashes||selectedHashes();\n if(!list.length) return toastMessage('toast.noTorrentsSelected','warning');\n if(list.length===1) return downloadResponse(`/api/torrents/${encodeURIComponent(list[0])}/torrent-file`,{},`${list[0]}.torrent`,'Preparing .torrent...').catch(e=>toast(e.message,'danger'));\n return downloadResponse('/api/torrents/torrent-files.zip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({hashes:list})},'pytorrent-torrents.zip','Preparing torrent ZIP...').catch(e=>toast(e.message,'danger'));\n }\n";
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { stateSource } from './state.js';
|
import { stateSource } from './state.js';
|
||||||
import { torrentsSource } from './torrents.js';
|
import { torrentsSource } from './torrents.js';
|
||||||
|
import { mobileSource } from './mobile.js';
|
||||||
|
import { messagesSource } from './messages.js';
|
||||||
|
import { torrentAddSource } from './torrentAdd.js';
|
||||||
import { apiSource } from './api.js';
|
import { apiSource } from './api.js';
|
||||||
import { createTorrentSource } from './createTorrent.js';
|
import { createTorrentSource } from './createTorrent.js';
|
||||||
import { torrentDetailsSource } from './torrentDetails.js';
|
import { torrentDetailsSource } from './torrentDetails.js';
|
||||||
@@ -16,6 +19,9 @@ import { bootstrapSource } from './bootstrap.js';
|
|||||||
export const moduleSources = [
|
export const moduleSources = [
|
||||||
stateSource,
|
stateSource,
|
||||||
torrentsSource,
|
torrentsSource,
|
||||||
|
mobileSource,
|
||||||
|
messagesSource,
|
||||||
|
torrentAddSource,
|
||||||
apiSource,
|
apiSource,
|
||||||
createTorrentSource,
|
createTorrentSource,
|
||||||
torrentDetailsSource,
|
torrentDetailsSource,
|
||||||
|
|||||||
2
pytorrent/static/js/bootstrap.js
vendored
2
pytorrent/static/js/bootstrap.js
vendored
File diff suppressed because one or more lines are too long
156
pytorrent/static/js/messages.js
Normal file
156
pytorrent/static/js/messages.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// Note: User-facing toast and modal copy is centralized here.
|
||||||
|
const APP_MESSAGES = {
|
||||||
|
actions: {
|
||||||
|
raw_torrent: 'Add torrent',
|
||||||
|
add: 'Add torrent',
|
||||||
|
start: 'Start torrent',
|
||||||
|
pause: 'Pause torrent',
|
||||||
|
stop: 'Stop torrent',
|
||||||
|
resume: 'Resume torrent',
|
||||||
|
remove: 'Remove torrent',
|
||||||
|
erase: 'Delete torrent',
|
||||||
|
delete: 'Delete torrent',
|
||||||
|
move: 'Move torrent',
|
||||||
|
recheck: 'Force recheck',
|
||||||
|
reannounce: 'Reannounce',
|
||||||
|
set_label: 'Update label',
|
||||||
|
label: 'Update label'
|
||||||
|
},
|
||||||
|
|
||||||
|
toast: {
|
||||||
|
operationStarted: ({ action }) => `${actionLabel(action)} started`,
|
||||||
|
|
||||||
|
operationDone: ({ action }) => `${actionLabel(action)} done`,
|
||||||
|
|
||||||
|
operationFailed: ({ action, error }) =>
|
||||||
|
`${actionLabel(action)} failed: ${error || 'unknown error'}`,
|
||||||
|
|
||||||
|
actionQueued: ({ action, parts }) =>
|
||||||
|
Number(parts || 1) > 1
|
||||||
|
? `${actionLabel(action)} queued in ${parts} parts`
|
||||||
|
: `${actionLabel(action)} queued`,
|
||||||
|
|
||||||
|
moveQueued: ({ parts, physical }) =>
|
||||||
|
Number(parts || 1) > 1
|
||||||
|
? `Move queued in ${parts} parts`
|
||||||
|
: physical
|
||||||
|
? 'Physical move queued'
|
||||||
|
: 'Move queued',
|
||||||
|
|
||||||
|
addQueued: () => 'Torrent add queued',
|
||||||
|
|
||||||
|
addQueuedSkipped: ({ count }) =>
|
||||||
|
`Torrent add queued, skipped ${count} duplicate torrent(s)`,
|
||||||
|
|
||||||
|
addTooLarge: () =>
|
||||||
|
'One or more .torrent files exceed the current rTorrent XML-RPC upload limit. Open rTorrent config and set network.xmlrpc.size_limit to e.g. 16M.',
|
||||||
|
|
||||||
|
dropOnlyTorrents: () => 'Drop .torrent files only',
|
||||||
|
|
||||||
|
droppedAddedSkipped: ({ queued, skipped }) =>
|
||||||
|
`Added ${queued} torrent(s), skipped ${skipped} duplicate(s)`,
|
||||||
|
|
||||||
|
droppedAdded: ({ queued }) => `Added ${queued} torrent(s)`,
|
||||||
|
|
||||||
|
droppedSkipped: ({ skipped }) =>
|
||||||
|
`Skipped ${skipped} duplicate torrent(s)`,
|
||||||
|
|
||||||
|
droppedNone: () => 'No torrents were added',
|
||||||
|
|
||||||
|
noTorrentsSelected: () => 'No torrents selected',
|
||||||
|
|
||||||
|
noTorrentSelected: () => 'No torrent selected',
|
||||||
|
|
||||||
|
noFilesSelected: () => 'No files selected',
|
||||||
|
|
||||||
|
downloadStarted: () => 'Download started',
|
||||||
|
|
||||||
|
chunkActionDone: ({ action }) => `${actionLabel(action)} done`,
|
||||||
|
|
||||||
|
trackerActionDone: ({ action }) => `${actionLabel(action)} done`,
|
||||||
|
|
||||||
|
pathPickerUnavailable: () => 'Path picker is unavailable',
|
||||||
|
|
||||||
|
pathEmpty: () => 'Path is empty',
|
||||||
|
|
||||||
|
columnsSaved: () => 'Columns saved',
|
||||||
|
|
||||||
|
recommendedColumnsApplied: () => 'Recommended columns applied',
|
||||||
|
|
||||||
|
jobLogsCleared: ({ deleted }) =>
|
||||||
|
`Cleared ${deleted || 0} finished job log(s)`,
|
||||||
|
|
||||||
|
emergencyJobLogsCleared: ({ deleted }) =>
|
||||||
|
`Emergency cleanup removed ${deleted || 0} job log(s)`,
|
||||||
|
|
||||||
|
rtorrentConfigSaved: ({ updated }) =>
|
||||||
|
`rTorrent config saved (${updated || 0})`,
|
||||||
|
|
||||||
|
rtorrentConfigReset: ({ removed }) =>
|
||||||
|
`rTorrent config reset (${removed || 0} override(s) removed)`,
|
||||||
|
|
||||||
|
automationLogsDeleted: ({ deleted }) =>
|
||||||
|
`Automation logs deleted: ${deleted || 0}`,
|
||||||
|
|
||||||
|
cleanupDone: ({ deleted }) => `Cleanup done (${deleted})`,
|
||||||
|
|
||||||
|
plannerApplied: ({ dryRun, paused, resumed, limitsChanged }) =>
|
||||||
|
`${dryRun ? 'Planner dry-run' : 'Planner applied'}: paused ${
|
||||||
|
paused || 0
|
||||||
|
}, resumed ${resumed || 0}, limits ${
|
||||||
|
limitsChanged ? 'changed' : 'unchanged'
|
||||||
|
}`,
|
||||||
|
|
||||||
|
rssQueued: ({ queued }) => `RSS queued ${queued || 0} item(s)`,
|
||||||
|
|
||||||
|
smartQueueCheckQueued: () =>
|
||||||
|
'Smart Queue check queued. It will continue in the background.',
|
||||||
|
|
||||||
|
automationForceRunDone: ({ count }) =>
|
||||||
|
`Automation force run done (${count || 0} torrent item(s))`,
|
||||||
|
|
||||||
|
automationsApplied: ({ count, batches }) =>
|
||||||
|
batches
|
||||||
|
? `Automations applied ${count || 0} torrent(s) in ${
|
||||||
|
batches || 0
|
||||||
|
} batch(es)`
|
||||||
|
: `Automations applied ${count || 0} item(s)`,
|
||||||
|
|
||||||
|
torrentStatsError: ({ error }) => `Torrent stats: ${error}`,
|
||||||
|
|
||||||
|
startupConfigApplied: ({ count }) =>
|
||||||
|
`Startup rTorrent config applied (${count || 0})`,
|
||||||
|
|
||||||
|
startupConfigFailed: ({ error }) =>
|
||||||
|
`Startup rTorrent config: ${error}`,
|
||||||
|
|
||||||
|
plannerSocketResult: ({ paused, resumed, dryRun }) =>
|
||||||
|
`Planner: paused ${paused || 0}, resumed ${resumed || 0}${
|
||||||
|
dryRun ? ' dry-run' : ''
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function actionLabel(action) {
|
||||||
|
const key = String(action || '').trim();
|
||||||
|
|
||||||
|
if (APP_MESSAGES.actions[key]) {
|
||||||
|
return APP_MESSAGES.actions[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
? key.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
||||||
|
: 'Operation';
|
||||||
|
}
|
||||||
|
|
||||||
|
function appMessage(key, params = {}) {
|
||||||
|
const fn = key
|
||||||
|
.split('.')
|
||||||
|
.reduce((acc, part) => acc && acc[part], APP_MESSAGES);
|
||||||
|
|
||||||
|
return typeof fn === 'function' ? fn(params) : String(fn || key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toastMessage(key, type = 'secondary', params = {}) {
|
||||||
|
toast(appMessage(key, params), type);
|
||||||
|
}
|
||||||
1
pytorrent/static/js/mobile.js
Normal file
1
pytorrent/static/js/mobile.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
pytorrent/static/js/torrentAdd.js
Normal file
1
pytorrent/static/js/torrentAdd.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user