/** * Popup UI Logic */ import './popup.css'; import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } from '../utils/storage'; 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 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; const passwordInput = document.getElementById('password') as HTMLInputElement; const loginBtn = document.getElementById('loginBtn') as HTMLButtonElement; const authError = document.getElementById('authError')!; const logoutBtn = document.getElementById('logoutBtn')!; const helpBtn = document.getElementById('helpBtn')!; const togglePasswordBtn = document.getElementById('togglePassword') as HTMLButtonElement; const eyeIcon = document.getElementById('eyeIcon')!; const eyeOffIcon = document.getElementById('eyeOffIcon')!; // State let pdfQueue: PDFQueueEntry[] = []; let currentCredentials: { username: string; password: string } | null = null; /** * Initialize popup */ async function init() { // Check if user has credentials const credentials = await loadCredentials(); if (credentials) { // Try to test connection try { const isConnected = await testConnection(credentials.username, credentials.password); if (isConnected) { currentCredentials = credentials; await updateLastUse(); showMainView(); await loadPDFQueue(); } else { // Authentication failed, credentials may be invalid showAuthView(); } } catch (error) { // Connection test failed console.error('[Popup] Connection test failed:', error); showAuthView(); } } else { showAuthView(); } // Setup event listeners setupEventListeners(); } /** * Setup event listeners */ function setupEventListeners() { loginForm.addEventListener('submit', handleLogin); logoutBtn.addEventListener('click', handleLogout); helpBtn.addEventListener('click', handleHelp); togglePasswordBtn.addEventListener('click', handleTogglePassword); } /** * Handle password visibility toggle */ function handleTogglePassword() { const isPassword = passwordInput.type === 'password'; if (isPassword) { // Show password passwordInput.type = 'text'; eyeIcon.style.display = 'none'; eyeOffIcon.style.display = 'block'; togglePasswordBtn.setAttribute('aria-label', 'Hide password'); togglePasswordBtn.setAttribute('title', 'Hide password'); } else { // Hide password passwordInput.type = 'password'; eyeIcon.style.display = 'block'; eyeOffIcon.style.display = 'none'; togglePasswordBtn.setAttribute('aria-label', 'Show password'); togglePasswordBtn.setAttribute('title', 'Show password'); } } /** * Handle login */ async function handleLogin(e: Event) { e.preventDefault(); const username = usernameInput.value.trim(); const password = passwordInput.value; if (!username || !password) { showError('Please enter username and password'); return; } loginBtn.disabled = true; loginBtn.textContent = 'Signing in...'; hideError(); try { const isConnected = await testConnection(username, password); if (!isConnected) { showError('Invalid credentials. Please check your username and password.'); return; } // Save credentials currentCredentials = { username, password }; await saveCredentials({ username, password }); showMainView(); await loadPDFQueue(); } catch (error) { if (error instanceof BinectAPIError) { showError(error.message); } else { showError('Authentication failed. Please try again.'); } } finally { loginBtn.disabled = false; loginBtn.textContent = 'Sign In'; } } /** * Load PDF queue from background and current tab */ 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; } 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(pdf.url); // Upload to Binect with credentials const document = await uploadPDF( pdfBytes, pdf.filename, currentCredentials.username, currentCredentials.password ); // Track successful transfer await addTrackingEntry({ timestamp: Date.now(), sourceDomain: pdf.sourceDomain, destinationUrl: 'https://api.binect.de/binectapi/v1/documents', pdfSize: pdfBytes.byteLength, result: 'success' }); // Update last use timestamp await updateLastUse(); // Update status to uploaded await chrome.runtime.sendMessage({ action: 'updatePDFStatus', id, status: 'uploaded', meta: { binectDocumentId: document.id } }); // Remove from local queue pdfQueue = pdfQueue.filter(p => p.id !== id); renderPDFList(); showStatus(`Sent! Document ID: ${document.id}`, 'success'); // Hide status after 3 seconds setTimeout(() => { hideStatus(); }, 3000); } catch (error) { let errorMessage = 'Upload failed'; if (error instanceof BinectAPIError) { errorMessage = error.message; // If auth error, might need to re-login if (error.statusCode === 401 || error.statusCode === 403) { errorMessage = 'Invalid credentials. Please sign in again.'; setTimeout(() => { handleLogout(); }, 2000); } } else if (error instanceof Error) { errorMessage = error.message; } // Track failed transfer await addTrackingEntry({ timestamp: Date.now(), sourceDomain: pdf.sourceDomain, destinationUrl: 'https://api.binect.de/binectapi/v1/documents', 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'); } } /** * 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; pdfQueue = []; // Clear form loginForm.reset(); showAuthView(); } /** * Handle help button */ function handleHelp() { // Open tracking page chrome.tabs.create({ url: chrome.runtime.getURL('tracking.html') }); } /** * Check if the current active tab is viewing a PDF */ async function checkCurrentTabForPDF(): Promise { try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab || !tab.url) { return null; } // Check if the URL is a PDF const url = tab.url; const isPDF = url.toLowerCase().endsWith('.pdf') || url.includes('type=application/pdf') || url.includes('mime=application/pdf'); if (isPDF) { // Extract filename from URL let filename = 'document.pdf'; try { const urlObj = new URL(url); const pathname = urlObj.pathname; const pathParts = pathname.split('/'); const lastPart = pathParts[pathParts.length - 1]; if (lastPart && lastPart.toLowerCase().endsWith('.pdf')) { filename = decodeURIComponent(lastPart); } else if (tab.title && tab.title !== 'about:blank' && !tab.title.startsWith('chrome://')) { // Use tab title if available filename = tab.title.endsWith('.pdf') ? tab.title : `${tab.title}.pdf`; } } catch (e) { // Use tab title as fallback if (tab.title && tab.title !== 'about:blank') { filename = tab.title.endsWith('.pdf') ? tab.title : `${tab.title}.pdf`; } } // Extract domain let domain = 'unknown'; try { const urlObj = new URL(url); domain = urlObj.hostname; } catch (e) { // Keep default } return { id: `tab-${tab.id}`, filename, url, size: 0, // Unknown size for viewed PDFs timestamp: Date.now(), sourceDomain: domain }; } return null; } catch (error) { console.error('Error checking current tab for PDF:', error); return null; } } /** * Show auth view */ function showAuthView() { authView.style.display = 'block'; mainView.style.display = 'none'; } /** * Show main view */ function showMainView() { authView.style.display = 'none'; mainView.style.display = 'block'; } /** * Show no PDF view */ function showNoPDF() { noPdfView.style.display = 'block'; pdfListView.style.display = 'none'; hideStatus(); } /** * Show error message */ function showError(message: string) { authError.textContent = message; authError.style.display = 'block'; } /** * Hide error message */ function hideError() { authError.style.display = 'none'; } /** * Show status message */ function showStatus(message: string, type: 'uploading' | 'success' | 'error') { statusMessage.textContent = message; statusMessage.className = `status-message ${type}`; statusMessage.style.display = 'block'; } /** * Hide status message */ 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 */ function formatFileSize(bytes: number): string { if (bytes === 0) { return 'Size unknown'; } else if (bytes < 1024) { return `${bytes} B`; } else if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)} KB`; } else { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } } /** * Format timestamp */ function formatTimestamp(timestamp: number): string { const now = Date.now(); const diff = now - timestamp; if (diff < 60 * 1000) { return 'Just now'; } else if (diff < 60 * 60 * 1000) { const minutes = Math.floor(diff / (60 * 1000)); return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; } else if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); return `${hours} hour${hours > 1 ? 's' : ''} ago`; } else { return new Date(timestamp).toLocaleDateString(); } } // Initialize on load init();