This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
GallerySubscriber/extension/popup/popup.js
T

409 lines
12 KiB
JavaScript

/**
* 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 and Pixiv are clickable (Pixiv initiates OAuth if no token)
// Other token-based platforms are disabled
const isDisabled = platform.authType === 'token' && !['discord', 'pixiv'].includes(key);
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';
}
// Special handling for Pixiv OAuth
if (key === 'pixiv') {
if (status.hasToken) {
return 'Token captured - ready to export';
}
return 'Click to authenticate with Pixiv';
}
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';
}
// Special handling for Pixiv
if (key === 'pixiv') {
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');
}