From 3e86bb126b9e75a9d96e85fad6edd365fc3723ea Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 15 Jan 2026 14:55:32 +0100 Subject: [PATCH] Add PDF list view with upload status tracking - Show all pending PDFs in a scrollable list instead of single PDF - Track upload status (pending/uploading/uploaded/failed) per PDF - Store queue in chrome.storage.local for persistence - Prevent duplicate uploads by checking URL against uploaded PDFs - Add Dismiss button to remove PDFs from queue - Show badge with count of pending PDFs - Auto-cleanup old entries (uploaded >7 days, failed >24h) Co-Authored-By: Claude Opus 4.5 --- src/background/service-worker.ts | 126 +++++++++---- src/popup/popup.css | 133 +++++++++++++ src/popup/popup.html | 23 +-- src/popup/popup.ts | 313 +++++++++++++++++-------------- src/utils/pdf-queue.ts | 226 ++++++++++++++++++++++ 5 files changed, 626 insertions(+), 195 deletions(-) create mode 100644 src/utils/pdf-queue.ts diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts index 4e3c888..6a03e8b 100644 --- a/src/background/service-worker.ts +++ b/src/background/service-worker.ts @@ -1,13 +1,19 @@ /** * Service Worker (Background Script) - * Handles PDF detection and credential expiry checks + * Handles PDF detection, queue management, and credential expiry checks */ import { startPDFDetection, DetectedPDF } from '../utils/pdf-detector'; import { loadCredentials } from '../utils/storage'; - -// Store last detected PDF in memory (ephemeral) -let lastDetectedPDF: DetectedPDF | null = null; +import { + addPDF, + getPendingCount, + getPendingPDFs, + updatePDFStatus, + removePDF, + cleanupOldEntries, + PDFStatus +} from '../utils/pdf-queue'; /** * Initialize extension on install @@ -16,9 +22,10 @@ chrome.runtime.onInstalled.addListener((details) => { console.log('[Service Worker] onInstalled event:', details.reason); if (details.reason === 'install') { console.log('[Service Worker] BinectChrome installed'); - setupCredentialExpiryAlarm(); + setupAlarms(); } else if (details.reason === 'update') { console.log('[Service Worker] BinectChrome updated'); + setupAlarms(); } }); @@ -27,16 +34,24 @@ chrome.runtime.onInstalled.addListener((details) => { */ chrome.runtime.onStartup.addListener(() => { console.log('[Service Worker] onStartup event - BinectChrome started'); - setupCredentialExpiryAlarm(); + setupAlarms(); + updateBadge(); }); /** - * Set up alarm to check credential expiry daily + * Set up alarms for periodic tasks */ -function setupCredentialExpiryAlarm() { +function setupAlarms() { + // Credential expiry check chrome.alarms.create('checkCredentialExpiry', { - delayInMinutes: 1, // First check in 1 minute - periodInMinutes: 24 * 60 // Then every 24 hours + delayInMinutes: 1, + periodInMinutes: 24 * 60 // Every 24 hours + }); + + // PDF queue cleanup + chrome.alarms.create('cleanupPDFQueue', { + delayInMinutes: 60, // First cleanup in 1 hour + periodInMinutes: 6 * 60 // Every 6 hours }); } @@ -47,6 +62,9 @@ chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'checkCredentialExpiry') { checkAndDeleteExpiredCredentials(); } + if (alarm.name === 'cleanupPDFQueue') { + cleanupOldEntries(); + } }); /** @@ -54,39 +72,43 @@ chrome.alarms.onAlarm.addListener((alarm) => { */ async function checkAndDeleteExpiredCredentials() { const credentials = await loadCredentials(); - // loadCredentials already handles expiry check and deletion - // If credentials are expired, it returns null and deletes them if (credentials === null) { - console.log('Credentials expired and deleted'); + console.log('[Service Worker] Credentials expired and deleted'); } } /** - * Initialize badge with default icon + * Update badge with pending PDF count */ -function initializeBadge() { - // Set a default badge to make extension visible - chrome.action.setBadgeText({ text: '•' }); +async function updateBadge() { + const count = await getPendingCount(); + const text = count > 0 ? count.toString() : '•'; + chrome.action.setBadgeText({ text }); chrome.action.setBadgeBackgroundColor({ color: '#4A90E2' }); // Binect Blue - console.log('[Service Worker] Default badge set'); + console.log('[Service Worker] Badge updated:', text); } // Initialize badge on load -initializeBadge(); +updateBadge(); /** * Start PDF detection */ console.log('[Service Worker] Initializing PDF detection...'); -startPDFDetection((pdf: DetectedPDF) => { - console.log('[Service Worker] PDF DETECTED CALLBACK:', pdf.filename); - lastDetectedPDF = pdf; +startPDFDetection(async (pdf: DetectedPDF) => { + console.log('[Service Worker] PDF DETECTED:', pdf.filename); - // Update badge to indicate PDF detected - chrome.action.setBadgeText({ text: '1' }); - chrome.action.setBadgeBackgroundColor({ color: '#4A90E2' }); // Binect Blue + // Add to persistent queue + const entry = await addPDF(pdf); - console.log('[Service Worker] Badge updated, PDF stored in memory'); + if (entry) { + console.log('[Service Worker] PDF added to queue:', entry.filename); + } else { + console.log('[Service Worker] PDF skipped (already uploaded):', pdf.filename); + } + + // Update badge + await updateBadge(); }); /** @@ -95,25 +117,47 @@ startPDFDetection((pdf: DetectedPDF) => { chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.log('[Service Worker] Message received:', request.action); + if (request.action === 'getPDFQueue') { + getPendingPDFs().then(entries => { + console.log('[Service Worker] Returning PDF queue:', entries.length, 'entries'); + sendResponse({ entries }); + }); + return true; + } + + if (request.action === 'updatePDFStatus') { + const { id, status, meta } = request as { id: string; status: PDFStatus; meta?: object }; + updatePDFStatus(id, status, meta).then(() => { + return updateBadge(); + }).then(() => { + sendResponse({ success: true }); + }); + return true; + } + + if (request.action === 'removePDF') { + removePDF(request.id).then(() => { + return updateBadge(); + }).then(() => { + sendResponse({ success: true }); + }); + return true; + } + + // Legacy handlers for backward compatibility if (request.action === 'getLastPDF') { - console.log('[Service Worker] Returning last PDF:', lastDetectedPDF ? lastDetectedPDF.filename : 'none'); - sendResponse({ pdf: lastDetectedPDF }); + // Return the first pending PDF for backward compatibility + getPendingPDFs().then(entries => { + const pdf = entries.length > 0 ? entries[0] : null; + sendResponse({ pdf }); + }); return true; } - if (request.action === 'clearLastPDF') { - console.log('[Service Worker] Clearing last PDF'); - lastDetectedPDF = null; - chrome.action.setBadgeText({ text: '•' }); // Reset to default badge - sendResponse({ success: true }); - return true; - } - - if (request.action === 'pdfSent') { - console.log('[Service Worker] PDF sent, resetting badge'); - // Reset badge after successful send - chrome.action.setBadgeText({ text: '•' }); // Reset to default badge - sendResponse({ success: true }); + if (request.action === 'clearLastPDF' || request.action === 'pdfSent') { + updateBadge().then(() => { + sendResponse({ success: true }); + }); return true; } diff --git a/src/popup/popup.css b/src/popup/popup.css index 9a0a592..47bb47a 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -272,6 +272,139 @@ body { margin-top: var(--spacing-xs); } +/* PDF List */ +.pdf-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-sm); + padding-bottom: var(--spacing-sm); + border-bottom: 1px solid var(--border-color); +} + +.pdf-count { + font-size: 12px; + color: var(--text-secondary); + font-weight: 500; +} + +.pdf-list { + max-height: 280px; + overflow-y: auto; +} + +.pdf-list-item { + display: flex; + gap: var(--spacing-sm); + padding: var(--spacing-sm); + background: var(--light-bg); + border-radius: var(--border-radius); + margin-bottom: var(--spacing-sm); + transition: background 0.2s; +} + +.pdf-list-item:last-child { + margin-bottom: 0; +} + +.pdf-list-item:hover { + background: var(--border-color); +} + +.pdf-list-item.uploading { + opacity: 0.7; +} + +.pdf-list-item.uploaded { + background: rgba(76, 175, 80, 0.1); +} + +.pdf-list-item.failed { + background: rgba(229, 57, 53, 0.1); +} + +.pdf-item-icon { + font-size: 24px; + flex-shrink: 0; +} + +.pdf-item-details { + flex: 1; + min-width: 0; +} + +.pdf-item-filename { + font-weight: 500; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pdf-item-meta { + font-size: 11px; + color: var(--text-secondary); + margin-top: 2px; +} + +.pdf-item-status { + font-size: 10px; + color: var(--text-light); + margin-top: 2px; +} + +.pdf-item-status.success { + color: var(--signal-green); +} + +.pdf-item-status.error { + color: var(--red); +} + +.pdf-item-actions { + display: flex; + flex-direction: column; + gap: 4px; + flex-shrink: 0; +} + +.btn-send-item { + padding: 6px 12px; + font-size: 11px; + min-height: auto; + background: var(--binect-blue); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s; +} + +.btn-send-item:hover { + background: var(--binect-blue-deep); +} + +.btn-send-item:disabled { + background: var(--border-color); + cursor: not-allowed; +} + +.btn-dismiss { + padding: 4px 8px; + font-size: 10px; + background: transparent; + color: var(--text-light); + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.btn-dismiss:hover { + background: var(--light-bg); + color: var(--text-secondary); +} + /* Status Messages */ .status-message { padding: var(--spacing-md); diff --git a/src/popup/popup.html b/src/popup/popup.html index b0cef30..84ffd17 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -53,24 +53,17 @@

No PDF detected. Open or download a PDF to get started.

- -