Remove background SW uploads, use simple direct fetch instead
SW was causing issues. Reverted to straightforward XHR uploads with progress bar, duplicate detection, and overwrite/skip. No IndexedDB, no SW messaging. SW is now minimal (cache-only for static assets). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6c4c21591f
commit
5aae2776ed
|
|
@ -1,17 +1,12 @@
|
|||
// rfiles.online Service Worker
|
||||
const CACHE_NAME = 'rfiles-upload-v2';
|
||||
const DB_NAME = 'rfiles-upload';
|
||||
const DB_VERSION = 2;
|
||||
// rfiles.online Service Worker — minimal cache-only
|
||||
const CACHE_NAME = 'rfiles-v3';
|
||||
|
||||
// Assets to cache for offline use
|
||||
const ASSETS_TO_CACHE = [
|
||||
'/',
|
||||
'/static/portal/manifest.json',
|
||||
'/static/portal/icon-192.png',
|
||||
'/static/portal/icon-512.png',
|
||||
];
|
||||
|
||||
// Install event - cache assets
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS_TO_CACHE))
|
||||
|
|
@ -19,226 +14,18 @@ self.addEventListener('install', (event) => {
|
|||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Activate event - clean old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) =>
|
||||
Promise.all(
|
||||
cacheNames.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n))
|
||||
)
|
||||
caches.keys().then((names) =>
|
||||
Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n)))
|
||||
)
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
// Fetch event - serve from cache, fallback to network
|
||||
self.addEventListener('fetch', (event) => {
|
||||
if (event.request.url.includes('/share-target/') || event.request.url.includes('/api/')) {
|
||||
return;
|
||||
}
|
||||
if (event.request.url.includes('/api/')) return;
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((r) => r || fetch(event.request))
|
||||
);
|
||||
});
|
||||
|
||||
// Handle share target
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
if (url.pathname === '/share-target/' && event.request.method === 'POST') {
|
||||
event.respondWith(handleShareTarget(event.request));
|
||||
}
|
||||
});
|
||||
|
||||
async function handleShareTarget(request) {
|
||||
const formData = await request.formData();
|
||||
const title = formData.get('title') || '';
|
||||
const text = formData.get('text') || '';
|
||||
const url = formData.get('url') || '';
|
||||
const files = formData.getAll('files');
|
||||
|
||||
if (files.length > 0) {
|
||||
try {
|
||||
const uploadPromises = files.map(async (file) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
fd.append('title', title || file.name);
|
||||
fd.append('description', text || url || '');
|
||||
return (await fetch('/api/upload/', { method: 'POST', body: fd })).json();
|
||||
});
|
||||
await Promise.all(uploadPromises);
|
||||
const successUrl = new URL('/', self.location.origin);
|
||||
successUrl.searchParams.set('shared', 'files');
|
||||
successUrl.searchParams.set('count', files.length);
|
||||
return Response.redirect(successUrl.toString(), 303);
|
||||
} catch (error) {
|
||||
const offlineUrl = new URL('/', self.location.origin);
|
||||
offlineUrl.searchParams.set('queued', 'true');
|
||||
return Response.redirect(offlineUrl.toString(), 303);
|
||||
}
|
||||
}
|
||||
|
||||
const redirectUrl = new URL('/', self.location.origin);
|
||||
if (url) redirectUrl.searchParams.set('url', url);
|
||||
if (text) redirectUrl.searchParams.set('text', text);
|
||||
if (title) redirectUrl.searchParams.set('title', title);
|
||||
redirectUrl.searchParams.set('shared', 'true');
|
||||
return Response.redirect(redirectUrl.toString(), 303);
|
||||
}
|
||||
|
||||
// --- IndexedDB helpers ---
|
||||
|
||||
function openDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onerror = () => reject(req.error);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('offline-queue')) {
|
||||
db.createObjectStore('offline-queue', { keyPath: 'queuedAt' });
|
||||
}
|
||||
if (!db.objectStoreNames.contains('uploads')) {
|
||||
db.createObjectStore('uploads', { keyPath: 'id' });
|
||||
}
|
||||
if (!db.objectStoreNames.contains('results')) {
|
||||
db.createObjectStore('results', { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function dbGet(storeName, key) {
|
||||
return openDB().then(db => new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const req = tx.objectStore(storeName).get(key);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
}));
|
||||
}
|
||||
|
||||
function dbPut(storeName, value) {
|
||||
return openDB().then(db => new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const req = tx.objectStore(storeName).put(value);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
}));
|
||||
}
|
||||
|
||||
function dbDelete(storeName, key) {
|
||||
return openDB().then(db => new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const req = tx.objectStore(storeName).delete(key);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
}));
|
||||
}
|
||||
|
||||
function dbGetAll(storeName) {
|
||||
return openDB().then(db => new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const req = tx.objectStore(storeName).getAll();
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Background upload handling ---
|
||||
|
||||
// Broadcast to all open pages
|
||||
async function broadcast(msg) {
|
||||
const clients = await self.clients.matchAll({ type: 'window' });
|
||||
clients.forEach(c => c.postMessage(msg));
|
||||
}
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'START_UPLOAD') {
|
||||
event.waitUntil(handleBackgroundUpload(event.data));
|
||||
}
|
||||
if (event.data && event.data.type === 'GET_RESULTS') {
|
||||
event.waitUntil(sendResults(event.source));
|
||||
}
|
||||
if (event.data && event.data.type === 'CLEAR_RESULT') {
|
||||
event.waitUntil(dbDelete('results', event.data.id));
|
||||
}
|
||||
});
|
||||
|
||||
async function handleBackgroundUpload(data) {
|
||||
const { id, uploadUrl, space, filename, action } = data;
|
||||
|
||||
try {
|
||||
// Read file blob from IndexedDB
|
||||
const record = await dbGet('uploads', id);
|
||||
if (!record) {
|
||||
await broadcast({ type: 'UPLOAD_ERROR', id, error: 'File not found in storage' });
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', record.blob, filename);
|
||||
formData.append('space', space);
|
||||
if (action) formData.append('action', action);
|
||||
|
||||
const response = await fetch(uploadUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
|
||||
// Clean up the stored blob
|
||||
await dbDelete('uploads', id);
|
||||
|
||||
if (response.status === 409 && result.duplicate) {
|
||||
// Store duplicate result for the page to handle
|
||||
await dbPut('results', { id, status: 'duplicate', data: result, filename, space });
|
||||
await broadcast({ type: 'UPLOAD_DUPLICATE', id, data: result, filename });
|
||||
} else if (response.ok && result.success) {
|
||||
await dbPut('results', { id, status: 'success', data: result, filename });
|
||||
await broadcast({ type: 'UPLOAD_COMPLETE', id, data: result });
|
||||
// Show notification if no page is focused
|
||||
const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: false });
|
||||
const hasFocus = clients.some(c => c.focused);
|
||||
if (!hasFocus) {
|
||||
self.registration.showNotification('Upload complete', {
|
||||
body: `${result.file.title} uploaded successfully`,
|
||||
icon: '/static/portal/icon-192.png',
|
||||
tag: 'upload-' + id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const error = result.error || 'Upload failed';
|
||||
await dbPut('results', { id, status: 'error', error, filename });
|
||||
await broadcast({ type: 'UPLOAD_ERROR', id, error });
|
||||
}
|
||||
} catch (err) {
|
||||
await dbPut('results', { id, status: 'error', error: err.message, filename });
|
||||
await broadcast({ type: 'UPLOAD_ERROR', id, error: err.message });
|
||||
// Keep the blob so user can retry
|
||||
}
|
||||
}
|
||||
|
||||
async function sendResults(client) {
|
||||
const results = await dbGetAll('results');
|
||||
client.postMessage({ type: 'PENDING_RESULTS', results });
|
||||
}
|
||||
|
||||
// Background sync
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'upload-queue') {
|
||||
event.waitUntil(processOfflineQueue());
|
||||
}
|
||||
});
|
||||
|
||||
async function processOfflineQueue() {
|
||||
const db = await openDB();
|
||||
const tx = db.transaction('offline-queue', 'readonly');
|
||||
const req = tx.objectStore('offline-queue').getAll();
|
||||
const items = await new Promise((resolve) => {
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
});
|
||||
for (const item of items) {
|
||||
try {
|
||||
const deleteTx = db.transaction('offline-queue', 'readwrite');
|
||||
deleteTx.objectStore('offline-queue').delete(item.queuedAt);
|
||||
} catch (error) {
|
||||
console.error('Failed to process queued item:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,18 +161,6 @@
|
|||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/static/portal/sw.js').then(reg => {
|
||||
// If the SW is installed but not yet controlling this page, reload once
|
||||
if (reg.active && !navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
// Controller is now active, no need to reload
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -246,15 +246,10 @@
|
|||
<script>
|
||||
const uploadZone = document.getElementById('uploadZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const results = document.getElementById('results');
|
||||
const resultsEl = document.getElementById('results');
|
||||
const maxSize = {{ space.max_file_size_mb }} * 1024 * 1024;
|
||||
const UPLOAD_URL = 'https://direct.rfiles.online/api/upload/';
|
||||
const SPACE = '{{ space.slug }}';
|
||||
const DB_NAME = 'rfiles-upload';
|
||||
const DB_VERSION = 2;
|
||||
|
||||
// Track active upload items by ID so SW messages can update them
|
||||
const uploadItems = {};
|
||||
|
||||
uploadZone.addEventListener('click', () => fileInput.click());
|
||||
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
|
||||
|
|
@ -271,60 +266,6 @@ function formatBytes(bytes) {
|
|||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// --- IndexedDB helpers (same schema as SW) ---
|
||||
function openDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onerror = () => reject(req.error);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('offline-queue'))
|
||||
db.createObjectStore('offline-queue', { keyPath: 'queuedAt' });
|
||||
if (!db.objectStoreNames.contains('uploads'))
|
||||
db.createObjectStore('uploads', { keyPath: 'id' });
|
||||
if (!db.objectStoreNames.contains('results'))
|
||||
db.createObjectStore('results', { keyPath: 'id' });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function dbPut(storeName, value) {
|
||||
return openDB().then(db => new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const req = tx.objectStore(storeName).put(value);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
}));
|
||||
}
|
||||
|
||||
function dbDelete(storeName, key) {
|
||||
return openDB().then(db => new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const req = tx.objectStore(storeName).delete(key);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Upload logic ---
|
||||
|
||||
function makeItem(id, filename, size) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'result-item';
|
||||
item.id = 'upload-' + id;
|
||||
item.innerHTML = `
|
||||
<div class="result-info">
|
||||
<h3>${filename}</h3>
|
||||
<div class="meta">${formatBytes(size)} - Uploading (continues in background)...</div>
|
||||
<div class="progress-bar"><div class="progress" style="width: 5%"></div></div>
|
||||
</div>
|
||||
`;
|
||||
results.insertBefore(item, results.firstChild);
|
||||
uploadItems[id] = item;
|
||||
return item;
|
||||
}
|
||||
|
||||
function showSuccess(item, data) {
|
||||
item.className = 'result-item success';
|
||||
item.innerHTML = `
|
||||
|
|
@ -339,13 +280,12 @@ function showSuccess(item, data) {
|
|||
`;
|
||||
}
|
||||
|
||||
function showDuplicate(item, id, data, filename) {
|
||||
item.id = 'dup-' + filename;
|
||||
function showDuplicate(item, file, data) {
|
||||
item.className = 'result-item duplicate';
|
||||
const existingSize = data.existing_file.size ? formatBytes(data.existing_file.size) : 'unknown size';
|
||||
item.innerHTML = `
|
||||
<div class="result-info">
|
||||
<h3>${filename}</h3>
|
||||
<h3>${file.name}</h3>
|
||||
<div class="meta">"${data.existing_file.title}" already exists (${existingSize})</div>
|
||||
</div>
|
||||
<div class="duplicate-actions">
|
||||
|
|
@ -353,31 +293,13 @@ function showDuplicate(item, id, data, filename) {
|
|||
<button class="btn-skip">Skip</button>
|
||||
</div>
|
||||
`;
|
||||
// For overwrite, we need the original file — re-read from file input won't work
|
||||
// So we keep the blob in IndexedDB and re-trigger with action=overwrite
|
||||
item.querySelector('.btn-overwrite').addEventListener('click', async () => {
|
||||
item.className = 'result-item';
|
||||
item.innerHTML = `<div class="result-info"><h3>${filename}</h3><div class="meta">Overwriting...</div><div class="progress-bar"><div class="progress" style="width: 5%"></div></div></div>`;
|
||||
// Clear the duplicate result
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'CLEAR_RESULT', id
|
||||
});
|
||||
// Re-store blob and re-trigger upload with overwrite
|
||||
// The blob was deleted after first upload attempt, so we need to handle this
|
||||
// For overwrite of duplicates detected by SW, the blob was already cleaned up
|
||||
// We need to use the original file reference if still available
|
||||
if (uploadItems['_file_' + id]) {
|
||||
const file = uploadItems['_file_' + id];
|
||||
await dbPut('uploads', { id, blob: file, filename: file.name });
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'START_UPLOAD', id, uploadUrl: UPLOAD_URL, space: SPACE, filename: file.name, action: 'overwrite'
|
||||
});
|
||||
}
|
||||
item.querySelector('.btn-overwrite').addEventListener('click', () => {
|
||||
uploadFile(file, 'overwrite');
|
||||
item.remove();
|
||||
});
|
||||
item.querySelector('.btn-skip').addEventListener('click', () => {
|
||||
item.className = 'result-item error';
|
||||
item.innerHTML = `<div class="result-info"><h3>${filename}</h3><div class="meta">Skipped (duplicate)</div></div>`;
|
||||
navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_RESULT', id });
|
||||
item.innerHTML = `<div class="result-info"><h3>${file.name}</h3><div class="meta">Skipped (duplicate)</div></div>`;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -391,123 +313,64 @@ async function uploadFile(file, action) {
|
|||
const item = document.createElement('div');
|
||||
item.className = 'result-item error';
|
||||
item.innerHTML = `<div class="result-info"><h3>${file.name}</h3><div class="meta">File too large (${formatBytes(file.size)}). Max: {{ space.max_file_size_mb }}MB</div></div>`;
|
||||
results.insertBefore(item, results.firstChild);
|
||||
resultsEl.insertBefore(item, resultsEl.firstChild);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const item = makeItem(id, file.name, file.size);
|
||||
const item = document.createElement('div');
|
||||
item.className = 'result-item';
|
||||
item.innerHTML = `
|
||||
<div class="result-info">
|
||||
<h3>${file.name}</h3>
|
||||
<div class="meta">${formatBytes(file.size)} - Uploading...</div>
|
||||
<div class="progress-bar"><div class="progress" style="width: 5%"></div></div>
|
||||
</div>
|
||||
`;
|
||||
resultsEl.insertBefore(item, resultsEl.firstChild);
|
||||
|
||||
// Keep file reference for potential overwrite
|
||||
uploadItems['_file_' + id] = file;
|
||||
|
||||
// Store blob in IndexedDB so service worker can access it
|
||||
await dbPut('uploads', { id, blob: file, filename: file.name });
|
||||
|
||||
try {
|
||||
// Wait for SW controller if not ready yet (first visit)
|
||||
const ctrl = await getSWController();
|
||||
ctrl.postMessage({
|
||||
type: 'START_UPLOAD',
|
||||
id,
|
||||
uploadUrl: UPLOAD_URL,
|
||||
space: SPACE,
|
||||
filename: file.name,
|
||||
action: action || null,
|
||||
});
|
||||
} catch (e) {
|
||||
// SW not available — fall back to direct upload from page
|
||||
directUpload(item, id, file, action);
|
||||
}
|
||||
}
|
||||
|
||||
async function directUpload(item, id, file, action) {
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file, file.name);
|
||||
fd.append('space', SPACE);
|
||||
if (action) fd.append('action', action);
|
||||
const resp = await fetch(UPLOAD_URL, { method: 'POST', body: fd });
|
||||
const result = await resp.json();
|
||||
await dbDelete('uploads', id);
|
||||
if (resp.status === 409 && result.duplicate) {
|
||||
showDuplicate(item, id, result, file.name);
|
||||
} else if (resp.ok && result.success) {
|
||||
showSuccess(item, result);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', UPLOAD_URL);
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const pct = Math.round((e.loaded / e.total) * 100);
|
||||
const bar = item.querySelector('.progress');
|
||||
if (bar) bar.style.width = pct + '%';
|
||||
const meta = item.querySelector('.meta');
|
||||
if (meta) meta.textContent = `${formatBytes(file.size)} - Uploading... ${pct}%`;
|
||||
}
|
||||
});
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
xhr.onload = () => {
|
||||
try {
|
||||
resolve({ status: xhr.status, data: JSON.parse(xhr.responseText) });
|
||||
} catch (e) {
|
||||
reject(new Error('Invalid response'));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error('Network error'));
|
||||
xhr.send(fd);
|
||||
});
|
||||
|
||||
if (result.status === 409 && result.data.duplicate) {
|
||||
showDuplicate(item, file, result.data);
|
||||
} else if (result.status >= 200 && result.status < 300 && result.data.success) {
|
||||
showSuccess(item, result.data);
|
||||
} else {
|
||||
showError(item, file.name, result.error || 'Upload failed');
|
||||
showError(item, file.name, result.data.error || 'Upload failed');
|
||||
}
|
||||
} catch (err) {
|
||||
showError(item, file.name, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function getSWController() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
return resolve(navigator.serviceWorker.controller);
|
||||
}
|
||||
// Wait for the SW to claim this page (happens after first install)
|
||||
const timeout = setTimeout(() => reject(new Error('SW timeout')), 10000);
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(navigator.serviceWorker.controller);
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
// --- Listen for messages from service worker ---
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
const { type, id, data, error, filename } = event.data;
|
||||
|
||||
if (type === 'UPLOAD_COMPLETE') {
|
||||
const item = uploadItems[id] || document.getElementById('upload-' + id);
|
||||
if (item) showSuccess(item, data);
|
||||
}
|
||||
|
||||
if (type === 'UPLOAD_DUPLICATE') {
|
||||
const item = uploadItems[id] || document.getElementById('upload-' + id);
|
||||
if (item) showDuplicate(item, id, data, filename);
|
||||
}
|
||||
|
||||
if (type === 'UPLOAD_ERROR') {
|
||||
const item = uploadItems[id] || document.getElementById('upload-' + id);
|
||||
if (item) showError(item, item.querySelector('h3')?.textContent || 'File', error);
|
||||
}
|
||||
|
||||
// Handle results loaded on page init
|
||||
if (type === 'PENDING_RESULTS') {
|
||||
(event.data.results || []).forEach(r => {
|
||||
// Only show results for this space
|
||||
if (r.space && r.space !== SPACE) return;
|
||||
const existing = document.getElementById('upload-' + r.id);
|
||||
if (existing) return; // Already shown
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'result-item';
|
||||
item.id = 'upload-' + r.id;
|
||||
results.insertBefore(item, results.firstChild);
|
||||
|
||||
if (r.status === 'success') {
|
||||
showSuccess(item, r.data);
|
||||
// Auto-clear after showing
|
||||
navigator.serviceWorker.controller?.postMessage({ type: 'CLEAR_RESULT', id: r.id });
|
||||
} else if (r.status === 'duplicate') {
|
||||
showDuplicate(item, r.id, r.data, r.filename);
|
||||
} else if (r.status === 'error') {
|
||||
showError(item, r.filename || 'File', r.error);
|
||||
navigator.serviceWorker.controller?.postMessage({ type: 'CLEAR_RESULT', id: r.id });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// On page load, ask SW for any completed results from background uploads
|
||||
getSWController().then(ctrl => {
|
||||
ctrl.postMessage({ type: 'GET_RESULTS' });
|
||||
}).catch(() => {});
|
||||
|
||||
function copyLink(btn, url) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
const orig = btn.textContent;
|
||||
|
|
|
|||
Loading…
Reference in New Issue