// WebSocket and application logic for BDFR Web Interface
class BDFRApp {
constructor() {
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.downloads = new Map();
this.authState = null;
this.authenticated = false;
this.initializeElements();
this.bindEvents();
this.connectWebSocket();
this.updateStatus();
this.startStatusPolling();
// Check for stored auth state first
this.authState = this.getStoredAuthState();
this.checkAuthentication();
}
initializeElements() {
// Forms
this.unifiedForm = document.getElementById('unifiedForm');
this.subredditForm = document.getElementById('subredditForm');
this.userForm = document.getElementById('userForm');
// Progress containers
this.progressContainer = document.getElementById('progressContainer');
this.downloadsList = document.getElementById('downloadsList');
this.downloadsItems = document.getElementById('downloadsItems');
// Status elements
this.wsStatus = document.getElementById('wsStatus');
this.bdfrStatus = document.getElementById('bdfrStatus');
// Authentication elements
this.authSection = document.getElementById('authSection');
this.authUser = document.getElementById('authUser');
this.authStatus = document.getElementById('authStatus');
this.loginBtn = document.getElementById('loginBtn');
this.logoutBtn = document.getElementById('logoutBtn');
this.authStateInput = document.getElementById('authState');
this.userAuthStateInput = document.getElementById('userAuthState');
}
bindEvents() {
// Form submissions
if (this.unifiedForm) {
this.unifiedForm.addEventListener('submit', (e) => this.handleUnifiedSubmit(e));
// Source type toggle
const sourceTypeRadios = this.unifiedForm.querySelectorAll('input[name="source_type"]');
sourceTypeRadios.forEach(radio => {
radio.addEventListener('change', (e) => this.updateSourceTypeUI(e.target.value));
});
}
if (this.subredditForm) {
this.subredditForm.addEventListener('submit', (e) => this.handleSubredditSubmit(e));
}
if (this.userForm) {
this.userForm.addEventListener('submit', (e) => this.handleUserSubmit(e));
}
// Authentication events
if (this.loginBtn) {
this.loginBtn.addEventListener('click', () => this.handleLogin());
}
if (this.logoutBtn) {
this.logoutBtn.addEventListener('click', () => this.handleLogout());
}
// Real-time input validation
['subreddit', 'username', 'sourceName'].forEach(id => {
const input = document.getElementById(id);
if (input) {
input.addEventListener('input', (e) => this.validateInput(e.target));
}
});
}
async connectWebSocket() {
try {
// Force HTTP for development - change this for production
const protocol = 'ws:';
const host = window.location.hostname || 'localhost';
const port = window.location.port || '8000';
const wsUrl = `${protocol}//${host}:${port}/ws/progress`;
console.log('[FRONTEND-WS] Attempting to connect to WebSocket:', wsUrl);
console.log('[FRONTEND-WS] Window location:', window.location.href);
console.log('[FRONTEND-WS] Host:', window.location.host);
console.log('[FRONTEND-WS] Hostname:', window.location.hostname);
console.log('[FRONTEND-WS] Port:', window.location.port);
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('[FRONTEND-WS] WebSocket connected successfully');
this.reconnectAttempts = 0;
this.updateWSStatus('Connected');
};
this.ws.onmessage = (event) => {
console.log('[FRONTEND-WS] Received message:', event.data);
const data = JSON.parse(event.data);
this.handleWebSocketMessage(data);
};
this.ws.onclose = (event) => {
console.log('[FRONTEND-WS] WebSocket disconnected:', event.code, event.reason);
this.updateWSStatus('Disconnected');
this.scheduleReconnect();
};
this.ws.onerror = (error) => {
console.error('[FRONTEND-WS] WebSocket error:', error);
this.updateWSStatus('Error');
};
} catch (error) {
console.error('Failed to connect WebSocket:', error);
this.scheduleReconnect();
}
}
scheduleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`[FRONTEND-WS] Scheduling reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
setTimeout(() => {
console.log(`[FRONTEND-WS] Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.connectWebSocket();
}, delay);
} else {
console.error('[FRONTEND-WS] Failed to connect after maximum attempts');
this.updateWSStatus('Failed to connect');
}
}
handleWebSocketMessage(data) {
console.log('WebSocket message received:', data);
// Ensure data has the required fields
if (!data.download_id && data.id) {
data.download_id = data.id;
}
switch (data.type) {
case 'progress':
case 'status_update':
this.updateProgress(data);
break;
case 'status':
this.showNotification(`Download ${data.download_id}: ${data.message}`, 'info');
break;
case 'completed':
this.showSuccess(`Download ${data.download_id} completed!`);
this.updateProgress(data); // Also update progress for completion
break;
case 'error':
this.showError(`Download ${data.download_id} failed: ${data.message}`);
this.updateProgress(data); // Also update progress for errors
break;
default:
console.log('Unknown message type:', data.type, data);
}
}
async handleSubredditSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const subreddit = formData.get('subreddit').trim();
if (!this.validateSubreddit(subreddit)) {
this.showError('Please enter a valid subreddit name');
return;
}
// Get no_dupes checkbox value and add it to formData
const noDupes = document.getElementById('noDupes').checked;
formData.set('no_dupes', noDupes ? 'true' : 'false');
// Show additional options for confirmation
const limit = formData.get('limit');
const sort = formData.get('sort');
if (confirm(`Start download for r/${subreddit}?\n\nOptions:\n- Limit: ${limit}\n- Sort: ${sort}\n- No duplicates: ${noDupes ? 'Yes' : 'No'}`)) {
try {
this.showLoading(e.target.querySelector('button'));
const response = await fetch('/api/download/subreddit', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
this.showSuccess(`Subreddit download started! ID: ${result.download_id}`);
e.target.reset();
} else {
this.showError(result.detail || 'Failed to start download');
}
} catch (error) {
console.error('Error:', error);
this.showError('Network error occurred');
} finally {
this.hideLoading(e.target.querySelector('button'));
}
}
}
async handleUserSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const username = formData.get('username').trim();
if (!this.validateUsername(username)) {
this.showError('Please enter a valid username');
return;
}
// Get no_dupes checkbox value and add it to formData
const noDupes = document.getElementById('userNoDupes').checked;
formData.set('no_dupes', noDupes ? 'true' : 'false');
// Get content type for confirmation
const contentType = formData.get('content_type');
const contentTypeLabel = contentType === 'submitted' ? 'submitted posts' :
contentType === 'upvoted' ? 'upvoted posts' : 'saved posts';
if (confirm(`Start download for u/${username}?\n\nContent: ${contentTypeLabel}\nLimit: ${formData.get('limit')}`)) {
try {
this.showLoading(e.target.querySelector('button'));
const response = await fetch('/api/download/user', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
this.showSuccess(`User download started! ID: ${result.download_id}`);
e.target.reset();
} else {
this.showError(result.detail || 'Failed to start download');
}
} catch (error) {
console.error('Error:', error);
this.showError('Network error occurred');
} finally {
this.hideLoading(e.target.querySelector('button'));
}
}
}
updateSourceTypeUI(sourceType) {
const sourceNameLabel = document.getElementById('sourceNameLabel');
const sourceNameInput = document.getElementById('sourceName');
const sourceNameHelp = document.getElementById('sourceNameHelp');
if (sourceType === 'subreddit') {
sourceNameLabel.textContent = 'Subreddit Name:';
sourceNameInput.placeholder = 'e.g., python, machinelearning';
sourceNameHelp.textContent = "Enter subreddit name without 'r/'";
} else {
sourceNameLabel.textContent = 'Username:';
sourceNameInput.placeholder = 'e.g., spez, your_username';
sourceNameHelp.textContent = "Enter Reddit username without 'u/'";
}
}
async handleUnifiedSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const downloadMode = formData.get('download_mode');
const sourceType = formData.get('source_type');
const sourceName = formData.get('source_name').trim();
const limit = formData.get('limit');
const sort = formData.get('sort');
const noDupes = document.getElementById('noDupes').checked;
const simpleCheck = document.getElementById('simpleCheck').checked;
// Validate source name
if (sourceType === 'subreddit' && !this.validateSubreddit(sourceName)) {
this.showError('Please enter a valid subreddit name');
return;
}
if (sourceType === 'user' && !this.validateUsername(sourceName)) {
this.showError('Please enter a valid username');
return;
}
// Build confirmation message
const modeLabels = {
'download': 'Download (media files)',
'archive': 'Archive (metadata only)',
'clone': 'Clone (media + metadata)'
};
const sourceLabel = sourceType === 'subreddit' ? `r/${sourceName}` : `u/${sourceName}`;
if (confirm(`Start ${modeLabels[downloadMode]} for ${sourceLabel}?\n\nOptions:\n- Limit: ${limit}\n- Sort: ${sort}\n- No duplicates: ${noDupes ? 'Yes' : 'No'}\n- Simple check: ${simpleCheck ? 'Yes' : 'No'}`)) {
try {
this.showLoading(e.target.querySelector('button'));
// Prepare form data for backend
const backendFormData = new FormData();
if (sourceType === 'subreddit') {
backendFormData.append('subreddit', sourceName);
} else {
backendFormData.append('username', sourceName);
}
backendFormData.append('limit', limit);
backendFormData.append('sort', sort);
backendFormData.append('time_filter', formData.get('time_filter') || '');
backendFormData.append('min_score', formData.get('min_score') || '');
backendFormData.append('no_dupes', noDupes ? 'true' : 'false');
backendFormData.append('simple_check', simpleCheck ? 'true' : 'false');
backendFormData.append('make_hard_links', formData.get('make_hard_links') ? 'true' : 'false');
backendFormData.append('download_mode', downloadMode);
if (this.authState) {
backendFormData.append('auth_state', this.authState);
}
// Determine endpoint based on source type
const endpoint = sourceType === 'subreddit'
? '/api/download/subreddit'
: '/api/download/user';
// For user downloads, always set submitted=true (default)
if (sourceType === 'user') {
backendFormData.append('submitted', 'true');
}
const response = await fetch(endpoint, {
method: 'POST',
body: backendFormData
});
const result = await response.json();
if (response.ok) {
this.showSuccess(`${modeLabels[downloadMode]} started! ID: ${result.download_id}`);
e.target.reset();
} else {
this.showError(result.detail || 'Failed to start download');
}
} catch (error) {
console.error('Error:', error);
this.showError('Network error occurred');
} finally {
this.hideLoading(e.target.querySelector('button'));
}
}
}
updateProgress(data) {
const download_id = data.download_id || data.id;
console.log('Updating progress for:', download_id, data);
if (!this.downloads.has(download_id)) {
console.log('Creating new progress card for:', download_id);
this.createProgressCard(download_id, data);
}
const card = this.downloads.get(download_id);
if (card) {
console.log('Updating existing progress card for:', download_id);
this.updateProgressCard(card, data);
}
this.showDownloadsList();
}
createProgressCard(downloadId, data) {
const card = document.createElement('div');
card.className = 'progress-card';
card.id = `progress-${downloadId}`;
const typeLabel = data.type === 'subreddit' ? 'Subreddit' : 'User';
const targetName = data.subreddit || data.username || 'Unknown';
const itemsProcessedInit = Number.isFinite(data.items_processed) ? data.items_processed : (data.data && Number.isFinite(data.data.items_processed) ? data.data.items_processed : 0);
const limitInit = Number.isFinite(data.limit) ? data.limit : (data.data && Number.isFinite(data.data.limit) ? data.data.limit : undefined);
const itemsFoundInit = Number.isFinite(data.items_found) ? data.items_found : (data.data && Number.isFinite(data.data.items_found) ? data.data.items_found : undefined);
const totalInit = (limitInit && limitInit > 0) ? limitInit : itemsFoundInit;
card.innerHTML = `
${data.progress || 0}% complete
${Number.isFinite(totalInit) ? `(${itemsProcessedInit}/${totalInit} items)` : ''}
- ${data.message || 'Starting...'}
${this.getProgressDetails(data)}
`;
// Persist basics for retry
card.dataset.subreddit = data.subreddit || '';
card.dataset.username = data.username || '';
if (Number.isFinite(totalInit)) {
card.dataset.limit = String(totalInit);
}
// Attach retry button immediately if already failed due to rate limit
this.maybeAttachRetry(card, data, downloadId);
this.downloads.set(downloadId, card);
this.downloadsItems.appendChild(card);
}
updateProgressCard(card, data) {
const statusElement = card.querySelector('.progress-status');
const progressFill = card.querySelector('.progress-fill');
const progressText = card.querySelector('.progress-text');
const phaseElement = card.querySelector('.progress-phase');
const downloadId = data.download_id || data.id;
const currentItemElement = card.querySelector(`#current-item-${downloadId}`);
const progressDetailsElement = card.querySelector(`#progress-details-${downloadId}`);
// Update status
statusElement.textContent = data.status;
statusElement.className = `progress-status status-${data.status}`;
// Update phase
if (phaseElement) {
phaseElement.textContent = `Phase: ${this.getPhaseLabel(data.phase || 'queued')}`;
}
// Update current item
if (currentItemElement) {
if (data.current_item) {
const itemTypeLabel = data.current_item_type === 'submission' ? 'Post' : 'Entry';
currentItemElement.textContent = `Current: ${itemTypeLabel} ${data.current_item}`;
currentItemElement.style.display = 'block';
} else {
currentItemElement.style.display = 'none';
}
}
// Update progress bar
if (progressFill) {
progressFill.style.width = `${data.progress || 0}%`;
}
// Update progress text
if (progressText) {
const itemsProcessed = Number.isFinite(data.items_processed) ? data.items_processed : (data.data && Number.isFinite(data.data.items_processed) ? data.data.items_processed : 0);
const limit = Number.isFinite(data.limit) ? data.limit : (data.data && Number.isFinite(data.data.limit) ? data.data.limit : undefined);
const itemsFound = Number.isFinite(data.items_found) ? data.items_found : (data.data && Number.isFinite(data.data.items_found) ? data.data.items_found : undefined);
const totalItems = (limit && limit > 0) ? limit : itemsFound;
let progressTextContent = `${data.progress || 0}% complete`;
if (Number.isFinite(totalItems)) {
progressTextContent += ` (${itemsProcessed}/${totalItems} items)`;
}
progressTextContent += ` - ${data.message || 'Processing...'}`;
progressText.textContent = progressTextContent;
}
// Update progress details
if (progressDetailsElement) {
progressDetailsElement.innerHTML = this.getProgressDetails(data);
}
// Update card class for animations
card.className = `progress-card ${data.status}`;
// Persist latest basics for retry
if (data.subreddit) card.dataset.subreddit = data.subreddit;
if (data.username) card.dataset.username = data.username;
if (Number.isFinite(data.limit)) {
card.dataset.limit = String(data.limit);
} else if (data.data && Number.isFinite(data.data.limit)) {
card.dataset.limit = String(data.data.limit);
}
// Attach retry button if applicable
this.maybeAttachRetry(card, data, downloadId);
}
getPhaseLabel(phase) {
const phaseLabels = {
'queued': 'Queued',
'fetching_submissions': 'Fetching Posts',
'preparing_download': 'Preparing Download',
'downloading_submission': 'Downloading Post',
'writing_file': 'Writing Files',
'writing_entry': 'Writing Entry',
'calculating_hashes': 'Calculating Hashes',
'completed': 'Completed',
'failed': 'Failed',
'cancelled': 'Cancelled'
};
return phaseLabels[phase] || phase.charAt(0).toUpperCase() + phase.slice(1);
}
getProgressDetails(data) {
let details = '';
if (data.current_subreddit) {
details += `Subreddit: ${data.current_subreddit}
`;
}
if (data.file_count) {
details += `Files to Hash: ${data.file_count}
`;
}
if (data.current_file) {
const shortPath = data.current_file.length > 50
? '...' + data.current_file.slice(-47)
: data.current_file;
details += `Writing: ${shortPath}
`;
}
return details;
}
showDownloadsList() {
if (this.downloads.size > 0) {
this.progressContainer.style.display = 'none';
this.downloadsList.style.display = 'block';
} else {
this.progressContainer.style.display = 'flex';
this.downloadsList.style.display = 'none';
}
}
validateInput(input) {
const isValid = input.value.trim().length > 0;
input.style.borderColor = isValid ? '#28a745' : '#dc3545';
return isValid;
}
validateSubreddit(subreddit) {
return subreddit.length > 0 && subreddit.match(/^[a-zA-Z0-9_]+$/);
}
validateUsername(username) {
return username.length > 0 && username.match(/^[a-zA-Z0-9_-]+$/);
}
showLoading(button) {
if (button) {
button.disabled = true;
button.textContent = 'Processing...';
}
}
hideLoading(button) {
if (button) {
button.disabled = false;
button.textContent = button.classList.contains('btn-primary') ? 'Start Download' : 'Start Download';
}
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showError(message) {
this.showNotification(message, 'error');
}
showNotification(message, type) {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
// Style the notification
Object.assign(notification.style, {
position: 'fixed',
top: '20px',
right: '20px',
padding: '15px 20px',
borderRadius: '8px',
color: 'white',
fontWeight: '600',
zIndex: '1000',
opacity: '0',
transform: 'translateY(-20px)',
transition: 'all 0.3s ease',
backgroundColor: type === 'success' ? '#28a745' : '#dc3545'
});
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateY(0)';
}, 100);
// Remove after 5 seconds
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 5000);
}
updateWSStatus(status) {
if (this.wsStatus) {
this.wsStatus.textContent = status;
this.wsStatus.className = `status-value status-${status.toLowerCase()}`;
}
}
async updateStatus() {
// try {
// // Update server time
// this.serverTime.textContent = new Date().toLocaleTimeString();
// // Check BDFR status
// const response = await fetch('/api/bdfr/status');
// const status = await response.json();
// if (this.bdfrStatus) {
// this.bdfrStatus.textContent = status.bdfr_available ? 'Online' : 'Offline';
// this.bdfrStatus.className = `status-value status-${status.bdfr_available ? 'online' : 'offline'}`;
// }
// } catch (error) {
// console.error('Failed to update status:', error);
// }
}
startStatusPolling() {
// Update status every 30 seconds
setInterval(() => {
this.updateStatus();
}, 30000);
// Initial update
this.updateStatus();
}
async clearCompletedDownloads() {
const completedCards = Array.from(this.downloadsItems.querySelectorAll('.progress-card.completed'));
if (completedCards.length === 0) {
this.showNotification('No completed downloads to clear', 'info');
return;
}
if (confirm(`Clear ${completedCards.length} completed download(s)?`)) {
completedCards.forEach(card => {
const downloadId = card.id.replace('progress-', '');
this.downloads.delete(downloadId);
card.remove();
});
this.showDownloadsList();
this.showSuccess('Completed downloads cleared');
}
}
// Retry helpers
isRateLimited(data) {
const msg = (data && data.message) ? String(data.message) : '';
const ex = (data && data.data && data.data.exception) ? String(data.data.exception) : '';
const phase = data && data.data && data.data.phase;
return phase === 'rate_limited' || msg.includes('429') || ex.includes('429');
}
maybeAttachRetry(card, data, downloadId) {
try {
const controls = card.querySelector('.progress-controls');
if (!controls) return;
const existing = controls.querySelector('.btn-retry');
// Detect current event as rate-limited and persist this state on the card
const detected = this.isRateLimited(data);
if (detected) {
card.dataset.rateLimited = 'true';
}
const persisted = card.dataset.rateLimited === 'true';
const shouldShow = data.status === 'failed' && (detected || persisted);
// Remove if no longer applicable; otherwise ensure present
if (!shouldShow) {
if (existing) existing.remove();
return;
}
if (!existing) {
const btn = document.createElement('button');
btn.className = 'btn-retry';
btn.textContent = 'Retry';
btn.title = 'Rate limited (429). Retry now.';
btn.onclick = () => this.retryDownload(downloadId);
controls.appendChild(btn);
}
} catch (e) {
console.warn('Failed to attach retry button', e);
}
}
async retryDownload(downloadId) {
try {
const card = this.downloads.get(downloadId) || document.getElementById(`progress-${downloadId}`);
if (!card) {
this.showError(`Cannot retry; card not found for ${downloadId}`);
return;
}
const subreddit = card.dataset.subreddit || '';
const username = card.dataset.username || '';
const limitStr = card.dataset.limit || '';
const limit = parseInt(limitStr, 10);
const hasLimit = Number.isFinite(limit);
let endpoint = '';
const formData = new FormData();
if (subreddit) {
endpoint = '/api/download/subreddit';
formData.append('subreddit', subreddit);
formData.append('limit', hasLimit ? String(limit) : '10');
formData.append('sort', 'hot');
// Enable no_dupes for retry to avoid re-downloading same files
formData.append('no_dupes', 'true');
if (this.authState) formData.append('auth_state', this.authState);
} else if (username) {
endpoint = '/api/download/user';
formData.append('username', username);
formData.append('limit', hasLimit ? String(limit) : '10');
formData.append('submitted', 'true');
// Enable no_dupes for retry to avoid re-downloading same files
formData.append('no_dupes', 'true');
if (this.authState) formData.append('auth_state', this.authState);
} else {
this.showError('Cannot determine original request (subreddit/user) for retry');
return;
}
// Remove old failed card before retrying
if (card.remove) card.remove();
this.downloads.delete(downloadId);
this.showNotification(`Retrying ${subreddit ? `r/${subreddit}` : `u/${username}`}...`, 'info');
const response = await fetch(endpoint, { method: 'POST', body: formData });
const result = await response.json();
if (response.ok) {
this.showSuccess(`Retry started: ${result.download_id}`);
// New download card will appear via websocket updates
} else {
this.showError(result.detail || 'Failed to start retry');
}
} catch (error) {
console.error('Retry error:', error);
this.showError('Retry failed');
}
}
// Authentication methods
async checkAuthentication() {
try {
// Use stored auth state if available
const stateToCheck = this.authState || this.getStoredAuthState();
const response = await fetch(`/api/auth/status${stateToCheck ? `?state=${stateToCheck}` : ''}`);
const authData = await response.json();
console.log('Auth check:', {
stateUsed: stateToCheck,
authenticated: authData.authenticated,
message: authData.message
});
this.authenticated = authData.authenticated;
// If authenticated, store the state for future use
if (authData.authenticated && stateToCheck) {
this.authState = stateToCheck;
this.storeAuthState(stateToCheck);
}
this.updateAuthDisplay(authData);
} catch (error) {
console.error('Failed to check authentication:', error);
}
}
getStoredAuthState() {
// Try to get stored state from sessionStorage
return sessionStorage.getItem('bdfr_auth_state');
}
storeAuthState(state) {
// Store state in sessionStorage for persistence
sessionStorage.setItem('bdfr_auth_state', state);
}
updateAuthDisplay(authData) {
if (authData.authenticated) {
this.authSection.style.display = 'block';
this.authStatus.textContent = '🟢 Connected';
this.authStatus.className = 'auth-status-indicator connected';
this.loginBtn.style.display = 'none';
this.logoutBtn.style.display = 'inline-block';
// Show Reddit username when authenticated
if (this.authUser) this.authUser.textContent = (authData.username || '').toString() || '-';
// Update forms with auth state
if (this.authStateInput) this.authStateInput.value = this.authState || '';
if (this.userAuthStateInput) this.userAuthStateInput.value = this.authState || '';
} else {
this.authSection.style.display = 'block';
this.authStatus.textContent = '🔴 Not Connected';
this.authStatus.className = 'auth-status-indicator disconnected';
this.loginBtn.style.display = 'inline-block';
this.logoutBtn.style.display = 'none';
// Reset Reddit username display
if (this.authUser) this.authUser.textContent = '-';
// Clear auth state from forms
if (this.authStateInput) this.authStateInput.value = '';
if (this.userAuthStateInput) this.userAuthStateInput.value = '';
}
}
async handleLogin() {
try {
// Get OAuth2 authorization URL
const response = await fetch('/auth/login');
const authData = await response.json();
if (response.ok) {
// Store state for later use
this.authState = authData.state;
this.storeAuthState(this.authState);
// Redirect to Reddit OAuth2
window.location.href = authData.authorization_url;
} else {
this.showError(authData.detail || 'Failed to initiate login');
}
} catch (error) {
console.error('Login error:', error);
this.showError('Failed to connect to Reddit');
}
}
async handleLogout() {
if (!this.authState) {
this.showError('No active session to logout');
return;
}
try {
const formData = new FormData();
formData.append('state', this.authState);
const response = await fetch('/auth/logout', {
method: 'POST',
body: formData
});
if (response.ok) {
this.authState = null;
this.authenticated = false;
// Clear stored auth state
sessionStorage.removeItem('bdfr_auth_state');
this.updateAuthDisplay({ authenticated: false });
this.showSuccess('Successfully logged out');
} else {
const error = await response.json();
this.showError(error.detail || 'Logout failed');
}
} catch (error) {
console.error('Logout error:', error);
this.showError('Logout failed');
}
}
// Handle OAuth2 callback
handleOAuth2Callback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
if (error) {
this.showError(`OAuth2 error: ${error}`);
return;
}
if (code && state) {
this.completeOAuth2Flow(code, state);
}
}
async completeOAuth2Flow(code, state) {
try {
const formData = new FormData();
formData.append('code', code);
formData.append('state', state);
const response = await fetch('/auth/callback', {
method: 'POST',
body: formData
});
if (response.ok) {
// Check if response is HTML (success page) or JSON
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('text/html')) {
// It's the success page, just show success message
this.showSuccess('Successfully authenticated with Reddit!');
this.authenticated = true;
// Store auth state for persistence (use a default since we don't have it from HTML response)
// The actual state will be retrieved when checking auth status
this.storeAuthState('active_session');
this.updateAuthDisplay({ authenticated: true });
// Clean URL
window.history.replaceState({}, document.title, window.location.pathname);
} else {
// It's JSON response (fallback)
const result = await response.json();
this.authState = result.tokens ? result.tokens.state : null;
this.authenticated = true;
this.updateAuthDisplay({ authenticated: true });
// Update user display if available
if (result.user && this.authUser) {
this.authUser.textContent = result.user.name;
}
this.showSuccess('Successfully authenticated with Reddit!');
window.history.replaceState({}, document.title, window.location.pathname);
}
} else {
// Try to parse as JSON, but handle HTML error pages gracefully
try {
const result = await response.json();
this.showError(result.detail || 'Authentication failed');
} catch (parseError) {
// If it's not JSON, it might be an HTML error page
this.showError('Authentication failed - please check your OAuth configuration');
}
}
} catch (error) {
console.error('OAuth2 completion error:', error);
this.showError('Authentication failed');
}
}
}
// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.bdfrApp = new BDFRApp();
// Check for OAuth2 callback parameters
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('code') || urlParams.has('error')) {
window.bdfrApp.handleOAuth2Callback();
}
// Check for authentication success indicator
if (urlParams.has('authenticated')) {
window.bdfrApp.showSuccess('Successfully authenticated with Reddit!');
window.bdfrApp.authenticated = true;
window.bdfrApp.updateAuthDisplay({ authenticated: true });
window.history.replaceState({}, document.title, window.location.pathname);
}
// Check for authentication error indicator
if (urlParams.has('auth_error')) {
window.bdfrApp.showError('Authentication failed - please check your OAuth configuration');
window.history.replaceState({}, document.title, window.location.pathname);
}
});
// Handle page visibility changes
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && window.bdfrApp) {
window.bdfrApp.updateStatus();
window.bdfrApp.checkAuthentication();
}
});