From 59120ed96eb1af41616f80f8298ebde1947a6e7d Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Sun, 12 Jul 2026 19:00:58 +0200 Subject: [PATCH] altering the url uploads --- public/s/css/upload.css | 60 ++++------ public/s/js/upload.js | 195 ++++++++++++++++++++------------ src/inc/locales/de.json | 12 +- src/inc/locales/en.json | 12 +- src/inc/locales/nl.json | 12 +- src/inc/locales/zange.json | 12 +- src/inc/routes/apiv2/upload.mjs | 2 +- views/snippets/footer.html | 13 ++- 8 files changed, 202 insertions(+), 116 deletions(-) diff --git a/public/s/css/upload.css b/public/s/css/upload.css index 9bb9588..0544dd6 100644 --- a/public/s/css/upload.css +++ b/public/s/css/upload.css @@ -1652,57 +1652,39 @@ } @keyframes uutShimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } -/* ── Divider between active + history ────────────── */ -.uut-divider { - display: flex; - align-items: center; - gap: 8px; - padding: 5px 12px; - opacity: 0.22; - font-size: 0.62rem; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--white, #fff); -} -.uut-divider::before, -.uut-divider::after { content: ''; flex: 1; height: 1px; background: currentColor; } - -/* ── History rows ────────────────────────────────── */ -.uut-hist-row { - display: flex; - align-items: center; - gap: 7px; - padding: 5px 12px; - border-bottom: 1px solid rgba(var(--accent-rgb,120,120,120), 0.05); - transition: background 0.15s; -} -.uut-hist-row:last-child { border-bottom: none; } -.uut-hist-row:hover { background: rgba(var(--accent-rgb,120,120,120), 0.05); } +/* ── Completion states (inline, auto-dismiss) ────── */ .uut-ok { color: #51cf66; font-size: 10px; flex-shrink: 0; } .uut-err { color: #ff5050; font-size: 10px; flex-shrink: 0; } -.uut-hist-right { margin-left: auto; flex-shrink: 0; } +.uut-job-row--done { align-items: center; } + +.uut-job--success .uut-bar-fill, +.uut-bar-fill--ok { + background: #51cf66; + background-image: none; + animation: none; +} + +.uut-job--error .uut-bar-fill, +.uut-bar-fill--err { + background: #ff5050; + background-image: none; + animation: none; +} + +.uut-stage--ok { color: #51cf66; } +.uut-stage--err { color: #ff5050; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; display: block; } .uut-link { font-size: 0.69rem; color: var(--accent); text-decoration: none; - opacity: 0.8; + opacity: 0.85; white-space: nowrap; transition: opacity 0.15s; + margin-left: 5px; } .uut-link:hover { opacity: 1; text-decoration: underline; } -.uut-errtxt { - font-size: 0.67rem; - color: #ff5050; - max-width: 90px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: block; - opacity: 0.8; -} diff --git a/public/s/js/upload.js b/public/s/js/upload.js index cfb0175..6635a6a 100644 --- a/public/s/js/upload.js +++ b/public/s/js/upload.js @@ -8,22 +8,21 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => { }); // ============================================================ -// URL Upload Tracker — single persistent panel with queue + history +// URL Upload Tracker — single panel, active jobs only // ============================================================ (function () { - const POLL_INTERVAL = 2000; - const MAX_HISTORY = 10; - const STORAGE_KEY = 'f0ck_url_upload_history'; + const POLL_INTERVAL = 2000; + const BC_CHANNEL = 'f0ck_uut'; + const i18n = () => window.f0ckI18n || {}; - // active jobs: jobId → { url, percent, stage, speed, eta, resolved, pollTimer } + // active jobs: jobId → { url, percent, stage, speed, eta, resolved, pollTimer, _remote } const _active = new Map(); - // history: [{ id, url, type, itemId, error, ts }] — newest first - let _history = (() => { - try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch { return []; } - })(); + // ── BroadcastChannel (cross-tab sync) ───────────────────────────────────── + const _bc = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel(BC_CHANNEL) : null; + const _broadcast = (msg) => { try { _bc?.postMessage(msg); } catch {} }; - let _collapsed = false; + let _closed = false; // ── Panel DOM ───────────────────────────────────────────────────────────── const _$ = id => document.getElementById(id); @@ -35,29 +34,19 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => { el.innerHTML = `
- URL Uploads + ${i18n().url_tracker_title || 'URL Uploads'} -
- - -
+
-
Recent
-
`; document.body.appendChild(el); - _$('uut-btn-toggle').addEventListener('click', () => { - _collapsed = !_collapsed; - _$('uut-body').style.display = _collapsed ? 'none' : ''; - _$('uut-btn-toggle').textContent = _collapsed ? '+' : '−'; - }); - _$('uut-btn-clear').addEventListener('click', () => { - _history = []; - try { localStorage.removeItem(STORAGE_KEY); } catch {} - _renderHistory(); + _$('uut-btn-close').addEventListener('click', () => { + _closed = true; + // Remove all resolved entries so they don't reappear next time + for (const [id, j] of _active) { if (j.resolved) _active.delete(id); } _sync(); }); }; @@ -65,18 +54,15 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => { // ── Visibility + badge ──────────────────────────────────────────────────── const _sync = () => { _ensurePanel(); - const panel = _$('url-upload-tracker'); - const hasContent = _active.size > 0 || _history.length > 0; - panel.classList.toggle('uut-visible', hasContent); + const panel = _$('url-upload-tracker'); + panel.classList.toggle('uut-visible', _active.size > 0 && !_closed); const badge = _$('uut-badge'); - if (badge) { badge.textContent = _active.size || ''; badge.style.display = _active.size ? '' : 'none'; } - - const div = _$('uut-divider'); - if (div) div.style.display = (_active.size > 0 && _history.length > 0) ? '' : 'none'; - - const clearBtn = _$('uut-btn-clear'); - if (clearBtn) clearBtn.style.display = _history.length > 0 ? '' : 'none'; + if (badge) { + const active = [..._active.values()].filter(j => !j.resolved).length; + badge.textContent = active || ''; + badge.style.display = active ? '' : 'none'; + } }; // ── Active job row ──────────────────────────────────────────────────────── @@ -95,7 +81,15 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => { } const pct = Math.min(Math.max(job.percent || 0, 0), 100); - const labels = { queued:'Queued', downloading:'Downloading', analyzing:'Analyzing', saving:'Saving', processing:'Processing', extracting:'Extracting' }; + const t = i18n(); + const labels = { + queued: t.url_tracker_queued || 'Queued', + downloading: t.url_tracker_downloading || 'Downloading', + analyzing: t.url_tracker_analyzing || 'Analyzing', + saving: t.url_tracker_saving || 'Saving', + processing: t.url_tracker_processing || 'Processing', + extracting: t.url_tracker_extracting || 'Extracting', + }; const label = labels[job.stage] || job.stage || 'Processing'; const status = (job.stage === 'downloading' && pct > 0) ? `${label} ${pct.toFixed(1)}%` : label; const speed = (job.speed && job.eta) ? `${job.speed} · ETA ${job.eta}` : (job.speed || ''); @@ -113,31 +107,45 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => {
`; + + if (!job._remote) { + _broadcast({ type: 'job_update', jobId, state: { url: job.url, percent: job.percent, stage: job.stage, speed: job.speed, eta: job.eta } }); + } }; - // ── History ─────────────────────────────────────────────────────────────── - const _saveHistory = () => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(_history)); } catch {} }; - - const _pushHistory = (entry) => { - _history.unshift(entry); - if (_history.length > MAX_HISTORY) _history.length = MAX_HISTORY; - _saveHistory(); - _renderHistory(); - }; - - const _renderHistory = () => { - _ensurePanel(); - const list = _$('uut-history-list'); + const _renderDone = (jobId, success, itemId, errMsg) => { + const list = _$('uut-active-list'); if (!list) return; - if (!_history.length) { list.innerHTML = ''; return; } - list.innerHTML = _history.map(h => { - const ok = h.type === 'upload_success'; - const icon = ok ? '' : ''; - const right = ok && h.itemId - ? `View →` - : (h.error ? `${window.escapeHtmlUpload(h.error)}` : ''); - return `
${icon}${window.escapeHtmlUpload(h.url)}
${right}
`; - }).join(''); + const row = list.querySelector(`[data-jid="${CSS.escape(jobId)}"]`); + if (!row) return; + const t = i18n(); + const url = window.escapeHtmlUpload(_active.get(jobId)?.url || ''); + + if (success) { + row.classList.add('uut-job--success'); + const linkText = t.url_tracker_view || 'View →'; + const link = itemId ? ` ${window.escapeHtmlUpload(linkText)}` : ''; + const completeText = t.url_tracker_complete || 'Complete!'; + row.innerHTML = ` +
+
+ ${url} +
${window.escapeHtmlUpload(completeText)}${link}
+
+
+ `; + } else { + row.classList.add('uut-job--error'); + const safeErr = window.escapeHtmlUpload(errMsg || t.url_tracker_failed || 'Upload failed'); + row.innerHTML = ` +
+
+ ${url} +
${safeErr}
+
+
+ `; + } }; // ── Polling ─────────────────────────────────────────────────────────────── @@ -163,7 +171,15 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => { _renderJob(jobId); if (state.done) { job.pollTimer = null; - setTimeout(() => { if (!job.resolved) _resolveJob(jobId, { type: state.error ? 'upload_error' : 'upload_success', item_id: null, data: state.error ? { msg: state.error } : null }); }, 2000); + setTimeout(() => { + if (!job.resolved) { + _resolveJob(jobId, { + type: state.error ? 'upload_error' : 'upload_success', + item_id: state.itemId || null, + data: state.error ? { msg: state.error } : null + }); + } + }, 2000); return; } } catch {} @@ -178,19 +194,23 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => { if (!job || job.resolved) return; job.resolved = true; _stopPoll(jobId); + _broadcast({ type: 'job_resolved', jobId, notif }); - _pushHistory({ id: jobId, url: job.url, type: notif.type, itemId: notif.item_id || null, error: notif.data?.msg || (notif.type === 'upload_error' ? 'Upload failed' : null), ts: Date.now() }); + const success = notif.type === 'upload_success'; + const itemId = notif.item_id || null; + const errMsg = notif.data?.msg || 'Upload failed'; - // Fade out the active row then remove - const row = _$('uut-active-list')?.querySelector(`[data-jid="${CSS.escape(jobId)}"]`); - if (row) { row.classList.add('uut-job--fading'); setTimeout(() => row.remove(), 300); } - setTimeout(() => { _active.delete(jobId); _sync(); }, 350); + _renderDone(jobId, success, itemId, errMsg); + _sync(); // update badge count }; // ── Public API ──────────────────────────────────────────────────────────── const addJob = (url, jobId) => { if (!jobId) jobId = `local_${Date.now()}`; _ensurePanel(); + _closed = false; + // Clean up any stale resolved entries from a previous session + for (const [id, j] of _active) { if (j.resolved) _active.delete(id); } _active.set(jobId, { url, percent: 0, stage: 'queued', speed: null, eta: null, resolved: false, pollTimer: null }); _renderJob(jobId); _sync(); @@ -202,7 +222,7 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => { const resolveByUrl = (notif, url) => { for (const [id, j] of _active) { if (!j.resolved && j.url === url) { _resolveJob(id, notif); return true; } } return false; }; const resolveOldest = (notif) => { for (const [id, j] of _active) { if (!j.resolved) { _resolveJob(id, notif); return true; } } return false; }; - // ── Notifications ───────────────────────────────────────────────────────── + // ── SSE Notifications ───────────────────────────────────────────────────── document.addEventListener('f0ck:upload_bg_update', (e) => { (e.detail || []).forEach(notif => { let matched = false; @@ -212,19 +232,52 @@ window.escapeHtmlUpload = window.escapeHtmlUpload || ((unsafe) => { }); }); - // ── Init: render persisted history on load ──────────────────────────────── - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { if (_history.length) { _ensurePanel(); _renderHistory(); _sync(); } }); - } else { - if (_history.length) { _ensurePanel(); _renderHistory(); _sync(); } + // ── BroadcastChannel ────────────────────────────────────────────────────── + if (_bc) { + _bc.onmessage = ({ data }) => { + switch (data.type) { + case 'hello': { + for (const [jobId, job] of _active) { + if (!job._remote) _broadcast({ type: 'job_update', jobId, state: { url: job.url, percent: job.percent, stage: job.stage, speed: job.speed, eta: job.eta } }); + } + break; + } + case 'job_update': { + const { jobId, state } = data; + if (_active.has(jobId) && !_active.get(jobId)._remote) break; + if (!_active.has(jobId)) { + _active.set(jobId, { ...state, resolved: false, pollTimer: null, _remote: true }); + } else { + Object.assign(_active.get(jobId), state); + } + _closed = false; + _renderJob(jobId); + _sync(); + break; + } + case 'job_resolved': { + const job = _active.get(data.jobId); + if (!job?._remote) break; + job.resolved = true; + const n = data.notif || {}; + _renderDone(data.jobId, n.type === 'upload_success', n.item_id || null, n.data?.msg || 'Upload failed'); + _sync(); + break; + } + } + }; } + // ── Init ────────────────────────────────────────────────────────────────── + _broadcast({ type: 'hello' }); + window.urlUploadTracker = { addJob, resolveByJobId, resolveByUrl, resolveOldest }; })(); + // Throttled queue to capture the first frame of video files asynchronously without blocking the browser class VideoThumbnailQueue { constructor(concurrency = 3) { diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index 62a2de5..63fd680 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -70,7 +70,17 @@ "btn_add_urls": "URL(s) hinzufügen", "tags_required_shitpost": "Alle Beiträge benötigen Tags", "url_queued_background": "URL Verarbeitung im Hintergrund gestartet.", - "add_more": "Mehr hinzufügen" + "add_more": "Mehr hinzufügen", + "url_tracker_title": "URL-Uploads", + "url_tracker_queued": "Warteschlange", + "url_tracker_downloading": "Wird heruntergeladen", + "url_tracker_analyzing": "Wird analysiert", + "url_tracker_saving": "Wird gespeichert", + "url_tracker_processing": "Wird verarbeitet", + "url_tracker_extracting": "Wird extrahiert", + "url_tracker_complete": "Fertig!", + "url_tracker_failed": "Upload fehlgeschlagen", + "url_tracker_view": "Ansehen →" }, "auth": { "registering": "Wird registriert...", diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index d150fa1..dba7a63 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -70,7 +70,17 @@ "btn_add_urls": "Add URL(s)", "tags_required_shitpost": "All items need tags", "url_queued_background": "URL processing started in background.", - "add_more": "Add more" + "add_more": "Add more", + "url_tracker_title": "URL Uploads", + "url_tracker_queued": "Queued", + "url_tracker_downloading": "Downloading", + "url_tracker_analyzing": "Analyzing", + "url_tracker_saving": "Saving", + "url_tracker_processing": "Processing", + "url_tracker_extracting": "Extracting", + "url_tracker_complete": "Complete!", + "url_tracker_failed": "Upload failed", + "url_tracker_view": "View →" }, "auth": { "registering": "Registering...", diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index 2e850b0..4cd4438 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -70,7 +70,17 @@ "btn_add_urls": "URL(s) toevoegen", "tags_required_shitpost": "Alle items hebben tags nodig", "url_queued_background": "URL wordt op de achtergrond verwerkt.", - "add_more": "Meer toevoegen" + "add_more": "Meer toevoegen", + "url_tracker_title": "URL Uploads", + "url_tracker_queued": "In wachtrij", + "url_tracker_downloading": "Downloaden", + "url_tracker_analyzing": "Analyseren", + "url_tracker_saving": "Opslaan", + "url_tracker_processing": "Verwerken", + "url_tracker_extracting": "Extraheren", + "url_tracker_complete": "Klaar!", + "url_tracker_failed": "Upload mislukt", + "url_tracker_view": "Bekijken →" }, "auth": { "registering": "Registreren...", diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index 5482f0f..823c44b 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -70,7 +70,17 @@ "btn_add_urls": "Elfe hinzufügen", "tags_required_shitpost": "Alle Fetzen brauchen Etiketten", "url_queued_background": "Elfen-Hintergrundverarbeitung gestartet.", - "add_more": "Meer hinzufügen" + "add_more": "Meer hinzufügen", + "url_tracker_title": "Elfen-Aufladierungen", + "url_tracker_queued": "Warteschlange", + "url_tracker_downloading": "Wird hinunterladiert", + "url_tracker_analyzing": "Wird analysiert", + "url_tracker_saving": "Wird gespeichert", + "url_tracker_processing": "Wird verarbeitet", + "url_tracker_extracting": "Wird extrahiert", + "url_tracker_complete": "Fertig!", + "url_tracker_failed": "Aufladierung fehlgeschlagen", + "url_tracker_view": "Ansehen →" }, "auth": { "registering": "Registrierung wird in die Wege geleitet...", diff --git a/src/inc/routes/apiv2/upload.mjs b/src/inc/routes/apiv2/upload.mjs index 7676fb5..eac1d52 100644 --- a/src/inc/routes/apiv2/upload.mjs +++ b/src/inc/routes/apiv2/upload.mjs @@ -751,7 +751,7 @@ export default router => { } // Completion notification - setProgress(jobId, { stage: 'done', percent: 100, done: true, error: null }); + setProgress(jobId, { stage: 'done', percent: 100, done: true, error: null, itemId: itemid }); cleanupJob(jobId); await db`INSERT INTO notifications (user_id, type, reference_id, item_id, data) VALUES (${session.id}, 'upload_success', 0, ${itemid}, ${db.json({ jobId })})`; diff --git a/views/snippets/footer.html b/views/snippets/footer.html index 29ebd7a..8be1e0b 100644 --- a/views/snippets/footer.html +++ b/views/snippets/footer.html @@ -586,7 +586,18 @@ poll_delete: "{{ t('comments.poll_delete') || 'Delete poll' }}", poll_expired: "{{ t('comments.poll_expired') || 'Poll closed' }}", poll_anonymous: "{{ t('comments.poll_anonymous') || 'Anonymous' }}", - poll_public: "{{ t('comments.poll_public') || 'Public votes' }}" + poll_public: "{{ t('comments.poll_public') || 'Public votes' }}", + // url upload tracker + url_tracker_title: "{{ t('upload.url_tracker_title') || 'URL Uploads' }}", + url_tracker_queued: "{{ t('upload.url_tracker_queued') || 'Queued' }}", + url_tracker_downloading: "{{ t('upload.url_tracker_downloading') || 'Downloading' }}", + url_tracker_analyzing: "{{ t('upload.url_tracker_analyzing') || 'Analyzing' }}", + url_tracker_saving: "{{ t('upload.url_tracker_saving') || 'Saving' }}", + url_tracker_processing: "{{ t('upload.url_tracker_processing') || 'Processing' }}", + url_tracker_extracting: "{{ t('upload.url_tracker_extracting') || 'Extracting' }}", + url_tracker_complete: "{{ t('upload.url_tracker_complete') || 'Complete!' }}", + url_tracker_failed: "{{ t('upload.url_tracker_failed') || 'Upload failed' }}", + url_tracker_view: "{{ t('upload.url_tracker_view') || 'View →' }}" };