-
📄
-
+
+
+
-
+
+
+
-
+
diff --git a/src/popup/popup.ts b/src/popup/popup.ts
index b554863..45630a0 100644
--- a/src/popup/popup.ts
+++ b/src/popup/popup.ts
@@ -7,12 +7,16 @@ import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } fr
import { uploadPDF, testConnection, BinectAPIError } from '../utils/binect-api';
import { fetchPDFBytes, DetectedPDF } from '../utils/pdf-detector';
import { addTrackingEntry } from '../tracking/tracker';
+import { PDFQueueEntry, PDFStatus } from '../utils/pdf-queue';
// DOM Elements
const authView = document.getElementById('authView')!;
const mainView = document.getElementById('mainView')!;
const noPdfView = document.getElementById('noPdfView')!;
-const pdfView = document.getElementById('pdfView')!;
+const pdfListView = document.getElementById('pdfListView')!;
+const pdfList = document.getElementById('pdfList')!;
+const pdfCount = document.getElementById('pdfCount')!;
+const statusMessage = document.getElementById('statusMessage')!;
const loginForm = document.getElementById('loginForm') as HTMLFormElement;
const usernameInput = document.getElementById('username') as HTMLInputElement;
@@ -20,14 +24,6 @@ const passwordInput = document.getElementById('password') as HTMLInputElement;
const loginBtn = document.getElementById('loginBtn') as HTMLButtonElement;
const authError = document.getElementById('authError')!;
-const pdfFilename = document.getElementById('pdfFilename')!;
-const pdfSize = document.getElementById('pdfSize')!;
-const pdfDomain = document.getElementById('pdfDomain')!;
-const pdfTimestamp = document.getElementById('pdfTimestamp')!;
-
-const sendBtn = document.getElementById('sendBtn') as HTMLButtonElement;
-const statusMessage = document.getElementById('statusMessage')!;
-
const logoutBtn = document.getElementById('logoutBtn')!;
const helpBtn = document.getElementById('helpBtn')!;
const togglePasswordBtn = document.getElementById('togglePassword') as HTMLButtonElement;
@@ -35,7 +31,7 @@ const eyeIcon = document.getElementById('eyeIcon')!;
const eyeOffIcon = document.getElementById('eyeOffIcon')!;
// State
-let currentPDF: DetectedPDF | null = null;
+let pdfQueue: PDFQueueEntry[] = [];
let currentCredentials: { username: string; password: string } | null = null;
/**
@@ -55,7 +51,7 @@ async function init() {
await updateLastUse();
showMainView();
- await loadLastPDF();
+ await loadPDFQueue();
} else {
// Authentication failed, credentials may be invalid
showAuthView();
@@ -78,7 +74,6 @@ async function init() {
*/
function setupEventListeners() {
loginForm.addEventListener('submit', handleLogin);
- sendBtn.addEventListener('click', handleSendPDF);
logoutBtn.addEventListener('click', handleLogout);
helpBtn.addEventListener('click', handleHelp);
togglePasswordBtn.addEventListener('click', handleTogglePassword);
@@ -138,7 +133,7 @@ async function handleLogin(e: Event) {
await saveCredentials({ username, password });
showMainView();
- await loadLastPDF();
+ await loadPDFQueue();
} catch (error) {
if (error instanceof BinectAPIError) {
showError(error.message);
@@ -152,24 +147,132 @@ async function handleLogin(e: Event) {
}
/**
- * Handle send PDF
+ * Load PDF queue from background and current tab
*/
-async function handleSendPDF() {
- if (!currentPDF || !currentCredentials) {
+async function loadPDFQueue() {
+ console.log('[Popup] Loading PDF queue...');
+
+ // Get queue from background script
+ const response = await chrome.runtime.sendMessage({ action: 'getPDFQueue' });
+ pdfQueue = response?.entries || [];
+ console.log('[Popup] Got', pdfQueue.length, 'entries from background');
+
+ // Also check current tab for PDF
+ const currentTabPDF = await checkCurrentTabForPDF();
+ if (currentTabPDF) {
+ // Check if already in queue
+ const exists = pdfQueue.some(p => p.url === currentTabPDF.url);
+ if (!exists) {
+ console.log('[Popup] Adding current tab PDF to queue:', currentTabPDF.filename);
+ // Add to queue (will be added to persistent queue when user interacts)
+ const entry: PDFQueueEntry = {
+ ...currentTabPDF,
+ status: 'pending'
+ };
+ pdfQueue.unshift(entry);
+ }
+ }
+
+ // Render the list
+ renderPDFList();
+}
+
+/**
+ * Render the PDF list
+ */
+function renderPDFList() {
+ // Filter to only pending and failed PDFs
+ const pending = pdfQueue.filter(p => p.status === 'pending' || p.status === 'failed');
+
+ if (pending.length === 0) {
+ showNoPDF();
return;
}
- sendBtn.disabled = true;
- showStatus('Uploading...', 'uploading');
+ noPdfView.style.display = 'none';
+ pdfListView.style.display = 'block';
+
+ // Update count
+ pdfCount.textContent = `${pending.length} PDF${pending.length > 1 ? 's' : ''} ready`;
+
+ // Render list items
+ pdfList.innerHTML = pending.map(pdf => `
+
+
📄
+
+
${escapeHtml(pdf.filename)}
+
${formatFileSize(pdf.size)} · ${escapeHtml(pdf.sourceDomain)}
+
${getStatusText(pdf)}
+
+
+
+
+
+
+ `).join('');
+
+ // Add event listeners to buttons
+ pdfList.querySelectorAll('.btn-send-item').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ const id = (e.target as HTMLElement).dataset.id;
+ if (id) handleSendPDF(id);
+ });
+ });
+
+ pdfList.querySelectorAll('.btn-dismiss').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ const id = (e.target as HTMLElement).dataset.id;
+ if (id) handleDismissPDF(id);
+ });
+ });
+
+ hideStatus();
+}
+
+/**
+ * Get status text for a PDF
+ */
+function getStatusText(pdf: PDFQueueEntry): string {
+ switch (pdf.status) {
+ case 'uploading':
+ return 'Uploading...';
+ case 'failed':
+ return pdf.errorMessage || 'Upload failed';
+ default:
+ return formatTimestamp(pdf.timestamp);
+ }
+}
+
+/**
+ * Handle send PDF
+ */
+async function handleSendPDF(id: string) {
+ const pdf = pdfQueue.find(p => p.id === id);
+ if (!pdf || !currentCredentials) {
+ return;
+ }
+
+ // Update local state
+ pdf.status = 'uploading';
+ renderPDFList();
+
+ // Notify background
+ await chrome.runtime.sendMessage({
+ action: 'updatePDFStatus',
+ id,
+ status: 'uploading'
+ });
try {
// Fetch PDF bytes
- const pdfBytes = await fetchPDFBytes(currentPDF.url);
+ const pdfBytes = await fetchPDFBytes(pdf.url);
// Upload to Binect with credentials
const document = await uploadPDF(
pdfBytes,
- currentPDF.filename,
+ pdf.filename,
currentCredentials.username,
currentCredentials.password
);
@@ -177,26 +280,34 @@ async function handleSendPDF() {
// Track successful transfer
await addTrackingEntry({
timestamp: Date.now(),
- sourceDomain: currentPDF.sourceDomain,
+ sourceDomain: pdf.sourceDomain,
destinationUrl: 'https://api.binect.de/binectapi/v1/documents',
- pdfSize: pdfBytes.byteLength, // Use actual size from fetched data
+ pdfSize: pdfBytes.byteLength,
result: 'success'
});
// Update last use timestamp
await updateLastUse();
- // Notify background script
- chrome.runtime.sendMessage({ action: 'pdfSent' });
+ // Update status to uploaded
+ await chrome.runtime.sendMessage({
+ action: 'updatePDFStatus',
+ id,
+ status: 'uploaded',
+ meta: { binectDocumentId: document.id }
+ });
- showStatus(`Success! Document ID: ${document.id} (Status: ${document.status.text})`, 'success');
+ // Remove from local queue
+ pdfQueue = pdfQueue.filter(p => p.id !== id);
+ renderPDFList();
- // Clear PDF after 3 seconds
+ showStatus(`Sent! Document ID: ${document.id}`, 'success');
+
+ // Hide status after 3 seconds
setTimeout(() => {
- currentPDF = null;
- showNoPDF();
hideStatus();
}, 3000);
+
} catch (error) {
let errorMessage = 'Upload failed';
@@ -217,26 +328,46 @@ async function handleSendPDF() {
// Track failed transfer
await addTrackingEntry({
timestamp: Date.now(),
- sourceDomain: currentPDF.sourceDomain,
+ sourceDomain: pdf.sourceDomain,
destinationUrl: 'https://api.binect.de/binectapi/v1/documents',
- pdfSize: currentPDF.size || 0,
+ pdfSize: pdf.size || 0,
result: 'failure',
errorMessage
});
+ // Update status to failed
+ await chrome.runtime.sendMessage({
+ action: 'updatePDFStatus',
+ id,
+ status: 'failed',
+ meta: { errorMessage }
+ });
+
+ // Update local state
+ pdf.status = 'failed';
+ pdf.errorMessage = errorMessage;
+ renderPDFList();
+
showStatus(errorMessage, 'error');
- } finally {
- sendBtn.disabled = false;
}
}
+/**
+ * Handle dismiss PDF
+ */
+async function handleDismissPDF(id: string) {
+ await chrome.runtime.sendMessage({ action: 'removePDF', id });
+ pdfQueue = pdfQueue.filter(p => p.id !== id);
+ renderPDFList();
+}
+
/**
* Handle logout
*/
async function handleLogout() {
await deleteCredentials();
currentCredentials = null;
- currentPDF = null;
+ pdfQueue = [];
// Clear form
loginForm.reset();
@@ -252,96 +383,6 @@ function handleHelp() {
chrome.tabs.create({ url: chrome.runtime.getURL('tracking.html') });
}
-/**
- * Load last detected PDF
- */
-async function loadLastPDF() {
- console.log('[Popup] Loading last PDF...');
-
- // First, check if current tab is viewing a PDF
- const currentTabPDF = await checkCurrentTabForPDF();
-
- if (currentTabPDF) {
- console.log('[Popup] Found PDF in current tab:', currentTabPDF.filename);
- currentPDF = currentTabPDF;
- showPDF(currentPDF);
- return;
- }
-
- console.log('[Popup] No PDF in current tab, checking background script...');
-
- // If no PDF in current tab, ask background script for last detected download
- chrome.runtime.sendMessage({ action: 'getLastPDF' }, async (response) => {
- if (response && response.pdf && response.pdf !== null) {
- console.log('[Popup] Background returned PDF:', response.pdf.filename);
- currentPDF = response.pdf;
- showPDF(response.pdf);
- } else {
- console.log('[Popup] Background has no PDF, checking recent downloads as fallback...');
-
- // Fallback: Check recent downloads directly
- const recentPDF = await checkRecentDownloads();
- if (recentPDF !== null) {
- console.log('[Popup] Found recent PDF download:', recentPDF.filename);
- currentPDF = recentPDF;
- showPDF(recentPDF);
- } else {
- console.log('[Popup] No PDF found anywhere');
- showNoPDF();
- }
- }
- });
-}
-
-/**
- * Check recent downloads for PDFs (fallback mechanism)
- */
-async function checkRecentDownloads(): Promise
{
- return new Promise((resolve) => {
- chrome.downloads.search(
- {
- limit: 20, // Check last 20 downloads
- orderBy: ['-startTime']
- },
- (items) => {
- console.log('[Popup] Checked recent downloads:', items.length, 'items');
-
- // Find most recent completed PDF
- const pdfItem = items.find(
- (item) =>
- item.state === 'complete' &&
- (item.filename.toLowerCase().endsWith('.pdf') || item.mime === 'application/pdf')
- );
-
- if (pdfItem) {
- console.log('[Popup] Found recent PDF:', pdfItem.filename);
-
- // Extract domain
- let domain = 'unknown';
- try {
- const urlObj = new URL(pdfItem.url);
- domain = urlObj.hostname;
- } catch (e) {
- // Keep default
- }
-
- resolve({
- id: `download-${pdfItem.id}`,
- filename: pdfItem.filename.split('/').pop() || pdfItem.filename,
- url: pdfItem.url,
- size: pdfItem.fileSize,
- timestamp: Date.now(), // Use current time as approximation
- sourceDomain: domain
- });
- } else {
- console.log('[Popup] No recent PDF downloads found');
- resolve(null);
- }
- }
- );
- });
-}
-
/**
* Check if the current active tab is viewing a PDF
*/
@@ -427,22 +468,7 @@ function showMainView() {
*/
function showNoPDF() {
noPdfView.style.display = 'block';
- pdfView.style.display = 'none';
-}
-
-/**
- * Show PDF view
- */
-function showPDF(pdf: DetectedPDF) {
- noPdfView.style.display = 'none';
- pdfView.style.display = 'block';
-
- pdfFilename.textContent = pdf.filename;
- pdfSize.textContent = formatFileSize(pdf.size);
- pdfDomain.textContent = pdf.sourceDomain;
- pdfTimestamp.textContent = formatTimestamp(pdf.timestamp);
-
- sendBtn.disabled = false;
+ pdfListView.style.display = 'none';
hideStatus();
}
@@ -477,6 +503,15 @@ function hideStatus() {
statusMessage.style.display = 'none';
}
+/**
+ * Escape HTML to prevent XSS
+ */
+function escapeHtml(text: string): string {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
/**
* Format file size
*/
diff --git a/src/utils/pdf-queue.ts b/src/utils/pdf-queue.ts
new file mode 100644
index 0000000..0e2aa08
--- /dev/null
+++ b/src/utils/pdf-queue.ts
@@ -0,0 +1,226 @@
+/**
+ * PDF Queue storage utilities
+ *
+ * Manages a persistent list of detected PDFs with upload status tracking.
+ * Uses chrome.storage.local for persistence across service worker restarts.
+ */
+
+import { DetectedPDF } from './pdf-detector';
+
+const STORAGE_KEY = 'pdfQueue';
+const MAX_ENTRIES = 50;
+const UPLOADED_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
+const FAILED_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
+
+export type PDFStatus = 'pending' | 'uploading' | 'uploaded' | 'failed';
+
+export interface PDFQueueEntry extends DetectedPDF {
+ status: PDFStatus;
+ uploadedAt?: number;
+ binectDocumentId?: number;
+ errorMessage?: string;
+}
+
+interface PDFQueueState {
+ entries: PDFQueueEntry[];
+ lastUpdated: number;
+}
+
+/**
+ * Load queue from storage
+ */
+export async function loadQueue(): Promise {
+ const result = await chrome.storage.local.get(STORAGE_KEY);
+ if (result[STORAGE_KEY]) {
+ return result[STORAGE_KEY] as PDFQueueState;
+ }
+ return { entries: [], lastUpdated: Date.now() };
+}
+
+/**
+ * Save queue to storage
+ */
+export async function saveQueue(state: PDFQueueState): Promise {
+ state.lastUpdated = Date.now();
+ await chrome.storage.local.set({ [STORAGE_KEY]: state });
+}
+
+/**
+ * Add a PDF to the queue
+ *
+ * Returns the created entry, or null if the PDF was already uploaded.
+ * If the PDF already exists as pending/failed, returns the existing entry.
+ */
+export async function addPDF(pdf: DetectedPDF): Promise {
+ const state = await loadQueue();
+
+ // Check for duplicate by URL
+ const existing = state.entries.find(e => e.url === pdf.url);
+ if (existing) {
+ if (existing.status === 'uploaded') {
+ console.log('[PDF Queue] PDF already uploaded, skipping:', pdf.filename);
+ return null;
+ }
+ console.log('[PDF Queue] PDF already in queue:', pdf.filename);
+ return existing;
+ }
+
+ // Create new entry
+ const entry: PDFQueueEntry = {
+ ...pdf,
+ status: 'pending'
+ };
+
+ // Add to beginning (most recent first)
+ state.entries.unshift(entry);
+
+ // Enforce max entries
+ await enforceMaxEntries(state);
+
+ await saveQueue(state);
+ console.log('[PDF Queue] Added PDF:', pdf.filename);
+ return entry;
+}
+
+/**
+ * Update the status of a PDF in the queue
+ */
+export async function updatePDFStatus(
+ id: string,
+ status: PDFStatus,
+ meta?: { binectDocumentId?: number; errorMessage?: string }
+): Promise {
+ const state = await loadQueue();
+ const entry = state.entries.find(e => e.id === id);
+
+ if (!entry) {
+ console.warn('[PDF Queue] PDF not found for status update:', id);
+ return;
+ }
+
+ entry.status = status;
+
+ if (status === 'uploaded') {
+ entry.uploadedAt = Date.now();
+ if (meta?.binectDocumentId) {
+ entry.binectDocumentId = meta.binectDocumentId;
+ }
+ }
+
+ if (status === 'failed' && meta?.errorMessage) {
+ entry.errorMessage = meta.errorMessage;
+ }
+
+ await saveQueue(state);
+ console.log('[PDF Queue] Updated status:', id, status);
+}
+
+/**
+ * Remove a PDF from the queue
+ */
+export async function removePDF(id: string): Promise {
+ const state = await loadQueue();
+ const index = state.entries.findIndex(e => e.id === id);
+
+ if (index === -1) {
+ console.warn('[PDF Queue] PDF not found for removal:', id);
+ return;
+ }
+
+ state.entries.splice(index, 1);
+ await saveQueue(state);
+ console.log('[PDF Queue] Removed PDF:', id);
+}
+
+/**
+ * Get all pending and failed PDFs (for display in popup)
+ */
+export async function getPendingPDFs(): Promise {
+ const state = await loadQueue();
+ return state.entries.filter(e => e.status === 'pending' || e.status === 'failed');
+}
+
+/**
+ * Get count of pending PDFs (for badge)
+ */
+export async function getPendingCount(): Promise {
+ const pending = await getPendingPDFs();
+ return pending.length;
+}
+
+/**
+ * Clean up old entries to prevent unbounded growth
+ */
+export async function cleanupOldEntries(): Promise {
+ const state = await loadQueue();
+ const now = Date.now();
+ const initialCount = state.entries.length;
+
+ state.entries = state.entries.filter(entry => {
+ // Always keep pending entries
+ if (entry.status === 'pending' || entry.status === 'uploading') {
+ return true;
+ }
+
+ // Remove uploaded entries older than 7 days
+ if (entry.status === 'uploaded' && entry.uploadedAt) {
+ const age = now - entry.uploadedAt;
+ if (age > UPLOADED_MAX_AGE_MS) {
+ return false;
+ }
+ }
+
+ // Remove failed entries older than 24 hours
+ if (entry.status === 'failed') {
+ const age = now - entry.timestamp;
+ if (age > FAILED_MAX_AGE_MS) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ if (state.entries.length < initialCount) {
+ await saveQueue(state);
+ console.log('[PDF Queue] Cleaned up', initialCount - state.entries.length, 'old entries');
+ }
+}
+
+/**
+ * Enforce maximum entries by removing oldest uploaded/failed entries
+ */
+async function enforceMaxEntries(state: PDFQueueState): Promise {
+ while (state.entries.length > MAX_ENTRIES) {
+ // Find oldest uploaded entry
+ let removeIndex = -1;
+ let oldestTime = Infinity;
+
+ for (let i = state.entries.length - 1; i >= 0; i--) {
+ const entry = state.entries[i];
+ if (entry.status === 'uploaded' && entry.uploadedAt && entry.uploadedAt < oldestTime) {
+ oldestTime = entry.uploadedAt;
+ removeIndex = i;
+ }
+ }
+
+ // If no uploaded entries, find oldest failed
+ if (removeIndex === -1) {
+ for (let i = state.entries.length - 1; i >= 0; i--) {
+ const entry = state.entries[i];
+ if (entry.status === 'failed' && entry.timestamp < oldestTime) {
+ oldestTime = entry.timestamp;
+ removeIndex = i;
+ }
+ }
+ }
+
+ // If still nothing, we can't remove any more (all pending)
+ if (removeIndex === -1) {
+ console.warn('[PDF Queue] Max entries reached, but all are pending');
+ break;
+ }
+
+ state.entries.splice(removeIndex, 1);
+ }
+}