/** * Popup script - handles UI interactions */ document.addEventListener('DOMContentLoaded', async () => { try { await init(); } catch (error) { console.error('Popup init error:', error); // Show error in UI document.getElementById('setup-required').classList.remove('hidden'); const alertDiv = document.querySelector('#setup-required .alert'); if (alertDiv) { alertDiv.textContent = ''; const strong = document.createElement('strong'); strong.textContent = 'Error'; const p = document.createElement('p'); p.textContent = error.message; alertDiv.appendChild(strong); alertDiv.appendChild(p); alertDiv.className = 'alert alert-error'; } } }); // Rate limit connection tests (max once per 2 minutes) const CONNECTION_TEST_INTERVAL = 2 * 60 * 1000; // 2 minutes /** * Initialize the popup */ async function init() { console.log('Popup initializing...'); // Check configuration (this is fast - just reads from storage) let config; try { config = await browser.runtime.sendMessage({ type: 'GET_CONFIG' }); console.log('Got config:', config); } catch (error) { console.error('Failed to get config:', error); throw new Error(`Failed to communicate with background script: ${error.message}`); } if (!config || !config.apiUrl || !config.apiKey) { console.log('Config missing, showing setup'); showSetupRequired(); return; } // Show main content IMMEDIATELY - don't wait for connection test or platform status document.getElementById('setup-required').classList.add('hidden'); document.getElementById('main-content').classList.remove('hidden'); // Setup event listeners right away setupEventListeners(); // Show loading state for platforms const platformsContainer = document.getElementById('platforms-list'); platformsContainer.textContent = ''; const loadingDiv = document.createElement('div'); loadingDiv.style.cssText = 'text-align: center; padding: 20px; color: var(--text-secondary);'; loadingDiv.textContent = 'Loading platforms...'; platformsContainer.appendChild(loadingDiv); // Now do background tasks in parallel (non-blocking) // Check if we should test connection (rate limited) testConnectionIfNeeded(); // Load platform status (this can take time) loadPlatformStatus().catch(error => { console.error('Failed to load platform status:', error); showError(`Failed to load platforms: ${error.message}`); }); } /** * Test connection only if enough time has passed since last test */ async function testConnectionIfNeeded() { try { // Check last test time from storage const stored = await browser.storage.local.get(['lastConnectionTest', 'lastConnectionStatus']); const now = Date.now(); const lastTest = stored.lastConnectionTest || 0; // Use cached status if recent if (now - lastTest < CONNECTION_TEST_INTERVAL && stored.lastConnectionStatus !== undefined) { console.log('Using cached connection status'); updateConnectionIndicator(stored.lastConnectionStatus); if (!stored.lastConnectionStatus) { showError('Cannot connect to backend (cached status)'); } return; } // Test connection in background console.log('Testing connection...'); const connectionStatus = await browser.runtime.sendMessage({ type: 'TEST_CONNECTION' }); console.log('Connection status:', connectionStatus); // Cache the result await browser.storage.local.set({ lastConnectionTest: now, lastConnectionStatus: connectionStatus.connected }); updateConnectionIndicator(connectionStatus.connected); if (!connectionStatus.connected) { showError(`Cannot connect to backend: ${connectionStatus.error}`); } } catch (error) { console.error('Failed to test connection:', error); updateConnectionIndicator(false); } } /** * Show the setup required message */ function showSetupRequired() { document.getElementById('setup-required').classList.remove('hidden'); document.getElementById('main-content').classList.add('hidden'); document.getElementById('open-settings-btn').addEventListener('click', () => { browser.runtime.openOptionsPage(); }); } /** * Update the connection status indicator * @param {boolean} connected - Whether connected to backend */ function updateConnectionIndicator(connected) { const indicator = document.getElementById('connection-status'); indicator.classList.toggle('connected', connected); indicator.title = connected ? 'Connected to GallerySubscriber' : 'Disconnected'; } /** * Load and display platform status */ async function loadPlatformStatus() { const status = await browser.runtime.sendMessage({ type: 'GET_PLATFORM_STATUS' }); const container = document.getElementById('platforms-list'); container.textContent = ''; for (const [key, platform] of Object.entries(PLATFORMS)) { const platformStatus = status[key] || {}; const card = createPlatformCard(key, platform, platformStatus); container.appendChild(card); } } /** * Create a platform card element * @param {string} key - Platform key * @param {object} platform - Platform definition * @param {object} status - Platform status * @returns {HTMLElement} */ function createPlatformCard(key, platform, status) { const card = document.createElement('div'); card.className = 'platform-card'; card.dataset.platform = key; // Discord is clickable if we have a token captured // Other token-based platforms are disabled const isDisabled = platform.authType === 'token' && key !== 'discord'; const isDiscordWithoutToken = key === 'discord' && !status.hasToken; if (isDisabled || isDiscordWithoutToken) { card.classList.add('disabled'); } const statusText = getStatusText(status, platform, key); const statusClass = getStatusClass(status, platform, key); // Build card content using DOM methods (avoids innerHTML security warnings) const iconDiv = document.createElement('div'); iconDiv.className = 'platform-icon'; iconDiv.style.backgroundColor = platform.color; iconDiv.textContent = platform.name.charAt(0); const infoDiv = document.createElement('div'); infoDiv.className = 'info'; const nameDiv = document.createElement('div'); nameDiv.className = 'name'; nameDiv.textContent = platform.name; const statusDiv = document.createElement('div'); statusDiv.className = `status ${statusClass}`; statusDiv.textContent = statusText; infoDiv.appendChild(nameDiv); infoDiv.appendChild(statusDiv); const actionSpan = document.createElement('span'); actionSpan.className = 'action-icon'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '20'); svg.setAttribute('height', '20'); svg.setAttribute('viewBox', '0 0 24 24'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('fill', 'currentColor'); path.setAttribute('d', 'M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z'); svg.appendChild(path); actionSpan.appendChild(svg); card.appendChild(iconDiv); card.appendChild(infoDiv); card.appendChild(actionSpan); if (!isDisabled && !isDiscordWithoutToken) { card.addEventListener('click', () => exportPlatformCookies(key, card)); } return card; } /** * Get status text for display * @param {object} status - Platform status * @param {object} platform - Platform definition * @param {string} key - Platform key * @returns {string} */ function getStatusText(status, platform, key) { // Special handling for Discord token-based auth if (key === 'discord') { if (status.hasToken) { return 'Token captured - ready to export'; } return 'Open Discord in browser to capture token'; } if (platform.authType === 'token') { return 'Manual token entry required'; } if (status.error) { return 'Error checking cookies'; } if (!status.hasCookies || status.cookieCount === 0) { return 'No cookies found - log in first'; } return `${status.cookieCount} cookies ready`; } /** * Get status CSS class * @param {object} status - Platform status * @param {object} platform - Platform definition * @param {string} key - Platform key * @returns {string} */ function getStatusClass(status, platform, key) { // Special handling for Discord if (key === 'discord') { return status.hasToken ? 'ready' : 'no-cookies'; } if (platform.authType === 'token') { return 'no-cookies'; } if (status.error) { return 'error'; } if (!status.hasCookies || status.cookieCount === 0) { return 'no-cookies'; } return 'ready'; } /** * Export cookies for a single platform * @param {string} platformKey - Platform key * @param {HTMLElement} cardElement - The card element */ async function exportPlatformCookies(platformKey, cardElement) { cardElement.classList.add('loading'); hideStatusMessage(); try { const result = await browser.runtime.sendMessage({ type: 'EXPORT_COOKIES', platform: platformKey }); if (result.error) { showError(result.error); } else { // Handle different response types (cookies vs token) const successMsg = result.cookieCount !== undefined ? `${PLATFORMS[platformKey].name}: ${result.cookieCount} cookies exported!` : `${PLATFORMS[platformKey].name}: Token exported successfully!`; showSuccess(successMsg); // Refresh status await loadPlatformStatus(); } } catch (error) { showError(error.message); } finally { cardElement.classList.remove('loading'); } } /** * Export cookies for all platforms */ async function exportAllCookies() { const btn = document.getElementById('export-all-btn'); btn.disabled = true; btn.textContent = 'Exporting...'; hideStatusMessage(); try { const results = await browser.runtime.sendMessage({ type: 'EXPORT_ALL_COOKIES' }); const successes = Object.values(results).filter(r => r.success).length; const skipped = Object.values(results).filter(r => r.skipped).length; const failures = Object.values(results).filter(r => !r.success && !r.skipped).length; if (failures === 0 && successes > 0) { showSuccess(`Exported ${successes} platform(s) successfully!`); } else if (successes > 0) { showWarning(`${successes} succeeded, ${failures} failed`); } else if (failures > 0) { showError('All exports failed. Are you logged in?'); } else { showWarning('No platforms to export'); } await loadPlatformStatus(); } catch (error) { showError(error.message); } finally { btn.disabled = false; btn.textContent = 'Export All Platforms'; } } /** * Setup event listeners */ function setupEventListeners() { document.getElementById('export-all-btn').addEventListener('click', exportAllCookies); document.getElementById('settings-btn').addEventListener('click', () => { browser.runtime.openOptionsPage(); }); } /** * Show success message * @param {string} message - Message to display */ function showSuccess(message) { showStatusMessage(message, 'success'); } /** * Show error message * @param {string} message - Message to display */ function showError(message) { showStatusMessage(message, 'error'); } /** * Show warning message * @param {string} message - Message to display */ function showWarning(message) { showStatusMessage(message, 'warning'); } /** * Show a status message * @param {string} message - Message text * @param {string} type - Message type (success, error, warning) */ function showStatusMessage(message, type) { const el = document.getElementById('status-message'); el.textContent = message; el.className = `status-message ${type}`; el.classList.remove('hidden'); } /** * Hide the status message */ function hideStatusMessage() { document.getElementById('status-message').classList.add('hidden'); }