526 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class RekordboxSyncRenderer {
constructor() {
this.initializeElements();
this.setupEventListeners();
this.setupElectronListeners();
this.loadInitialState();
}
/**
* Initialize DOM elements
*/
initializeElements() {
// Buttons
this.exportEnvBtn = document.getElementById('exportEnvBtn');
// Status elements
this.syncStatusElement = document.getElementById('syncStatus');
this.filesSyncedElement = document.getElementById('filesSynced');
this.lastSyncElement = document.getElementById('lastSync');
this.syncDetailsElement = document.getElementById('syncDetails');
this.syncModeElement = document.getElementById('syncMode');
// Activity log
this.activityLogElement = document.getElementById('activityLog');
// Configuration elements
this.s3EndpointInput = document.getElementById('s3Endpoint');
this.s3AccessKeyInput = document.getElementById('s3AccessKey');
this.s3SecretKeyInput = document.getElementById('s3SecretKey');
this.s3BucketInput = document.getElementById('s3Bucket');
this.s3RegionInput = document.getElementById('s3Region');
this.localPathInput = document.getElementById('localPath');
this.syncIntervalInput = document.getElementById('syncInterval');
// Save button
this.saveConfigBtn = document.getElementById('saveConfigBtn');
}
/**
* Setup event listeners for UI elements
*/
setupEventListeners() {
// Sync control buttons
if (this.startBtn) {
this.startBtn.addEventListener('click', () => this.startSync());
}
if (this.stopBtn) {
this.stopBtn.addEventListener('click', () => this.stopSync());
}
if (this.exportEnvBtn) {
this.exportEnvBtn.addEventListener('click', () => this.exportToEnv());
}
// Configuration save button
if (this.saveConfigBtn) {
this.saveConfigBtn.addEventListener('click', () => this.saveConfiguration());
}
// Force sync button
const forceSyncBtn = document.getElementById('forceSyncBtn');
if (forceSyncBtn) {
forceSyncBtn.addEventListener('click', () => this.forceFullSync());
}
// Immediate sync button
const immediateSyncBtn = document.getElementById('immediateSyncBtn');
if (immediateSyncBtn) {
immediateSyncBtn.addEventListener('click', () => this.triggerImmediateSync());
}
}
/**
* Setup Electron IPC listeners
*/
setupElectronListeners() {
if (!window.electronAPI) {
console.error('❌ Electron API not available');
return;
}
// Sync status updates
window.electronAPI.on('sync-status-changed', (status) => {
this.updateSyncStatus(status);
});
// File change events
window.electronAPI.on('file-changed', (event) => {
console.log('📁 File changed:', event);
this.addActivityLog('info', `File changed: ${event.path}`);
});
window.electronAPI.on('file-added', (event) => {
console.log(' File added:', event);
this.addActivityLog('success', `File added: ${event.path}`);
});
window.electronAPI.on('file-removed', (event) => {
console.log(' File removed:', event);
this.addActivityLog('info', `File removed: ${event.path}`);
});
// Sync operation updates
window.electronAPI.on('sync-operation-started', (operation) => {
console.log('🔄 Operation started:', operation);
this.addActivityLog('info', `Started ${operation.type}: ${operation.s3Key || operation.localPath}`);
});
window.electronAPI.on('sync-operation-completed', (operation) => {
console.log('✅ Operation completed:', operation);
this.addActivityLog('success', `Completed ${operation.type}: ${operation.s3Key || operation.localPath}`);
});
window.electronAPI.on('sync-operation-failed', (operation) => {
console.log('❌ Operation failed:', operation);
this.addActivityLog('error', `Failed ${operation.type}: ${operation.s3Key || operation.localPath} - ${operation.error || 'Unknown error'}`);
});
// Sync lifecycle events
window.electronAPI.on('sync-started', (type) => {
console.log('🚀 Sync started:', type);
this.addActivityLog('info', `Sync started: ${type}`);
});
window.electronAPI.on('sync-completed', (type) => {
console.log('🎉 Sync completed:', type);
this.addActivityLog('success', `Sync completed: ${type}`);
});
window.electronAPI.on('sync-error', (error) => {
console.log('💥 Sync error:', error);
this.addActivityLog('error', `Sync error: ${error.message || 'Unknown error'}`);
// Update UI to show error state
if (this.syncStatusElement) {
this.syncStatusElement.textContent = 'Error - Sync failed';
this.syncStatusElement.className = 'status-value error';
}
// Re-enable start button on error
if (this.startBtn) this.startBtn.disabled = false;
if (this.stopBtn) this.stopBtn.disabled = true;
});
// Engine events
window.electronAPI.on('sync-engine-started', () => {
console.log('✅ Sync engine started');
this.addActivityLog('success', 'Sync engine started');
});
window.electronAPI.on('sync-engine-stopped', () => {
console.log('⏹️ Sync engine stopped');
this.addActivityLog('info', 'Sync engine stopped');
});
// MinIO output events
window.electronAPI.on('aws-output', (output) => {
console.log('🔍 AWS S3 output received:', output);
this.addActivityLog('info', `AWS S3 ${output.direction}: ${output.output}`);
});
// File change events
window.electronAPI.on('file-changed', (event) => {
console.log('📁 File changed:', event);
this.addActivityLog('info', `File changed: ${event.path}`);
});
}
/**
* Load initial application state
*/
async loadInitialState() {
try {
// Load configuration
const config = await window.electronAPI.invoke('config:get');
this.populateConfigurationForm(config);
// Load current sync status
const status = await window.electronAPI.invoke('sync:get-status');
if (status) {
this.updateSyncStatus(status);
}
} catch (error) {
console.error('❌ Failed to load initial state:', error);
this.addActivityLog('error', `Failed to load initial state: ${error.message || 'Unknown error'}`);
}
}
/**
* Force full sync
*/
async forceFullSync() {
try {
this.addActivityLog('info', 'Forcing full sync...');
await window.electronAPI.invoke('sync:force-full');
this.addActivityLog('success', 'Full sync initiated');
} catch (error) {
console.error('❌ Failed to force full sync:', error);
this.addActivityLog('error', `Failed to force full sync: ${error.message || 'Unknown error'}`);
}
}
/**
* Update sync status display
*/
updateSyncStatus(status) {
if (!status) return;
// Update main status with more detailed information
if (this.syncStatusElement) {
if (status.isRunning) {
let statusText = 'Running';
// Add current phase information
if (status.currentPhase) {
statusText += ` - ${status.currentPhase}`;
}
// Add file counts (cleaner display)
if (status.stats && status.stats.totalFilesSynced > 0) {
statusText += ` - ${status.stats.totalFilesSynced} local files`;
}
this.syncStatusElement.textContent = statusText;
this.syncStatusElement.className = 'status-value running';
} else {
if (status.completedCount > 0 && status.pendingCount === 0 && status.inProgressCount === 0) {
this.syncStatusElement.textContent = 'Completed';
this.syncStatusElement.className = 'status-value completed';
} else {
this.syncStatusElement.textContent = 'Stopped';
this.syncStatusElement.className = 'status-value stopped';
}
if (this.startBtn) this.startBtn.disabled = false;
if (this.stopBtn) this.stopBtn.disabled = true;
}
// Also update the force sync button state
const forceSyncBtn = document.getElementById('forceSyncBtn');
if (forceSyncBtn) {
forceSyncBtn.disabled = status.isRunning;
}
}
// Update files synced count with more detail
if (this.filesSyncedElement) {
console.log('🔍 Updating files synced element with:', {
actualFileCount: status.actualFileCount,
statsTotalFiles: status.stats?.totalFilesSynced
});
// Use actual file count from main process if available
if (status.actualFileCount !== undefined) {
const text = `${status.actualFileCount} files in local folder`;
console.log('📝 Setting files synced text to:', text);
this.filesSyncedElement.textContent = text;
} else if (status.stats && status.stats.totalFilesSynced > 0) {
const text = `${status.stats.totalFilesSynced} files in local folder`;
console.log('📝 Setting files synced text to:', text);
this.filesSyncedElement.textContent = text;
} else {
console.log('📝 Setting files synced text to: 0 files');
this.filesSyncedElement.textContent = '0 files';
}
} else {
console.warn('⚠️ filesSyncedElement not found!');
}
// Update last sync time
if (this.lastSyncElement) {
if (status.lastSync) {
const date = new Date(status.lastSync);
this.lastSyncElement.textContent = date.toLocaleString();
} else {
this.lastSyncElement.textContent = 'Never';
}
}
// Update detailed status
this.updateDetailedStatus(status);
// Update phase and progress
if (status.currentPhase && status.progressMessage) {
this.addActivityLog('info', `${status.currentPhase}: ${status.progressMessage}`);
}
// Log progress changes to reduce spam
if (status.progress && status.progress.percent > 0) {
const currentProgress = status.progress.percent;
if (!this.lastLoggedProgress || Math.abs(currentProgress - this.lastLoggedProgress) >= 10) {
const progressText = `Progress: ${currentProgress}% - ${status.progress.message || 'Syncing files...'}`;
this.addActivityLog('info', progressText);
this.lastLoggedProgress = currentProgress;
}
}
// Add file count updates (but only when they change significantly)
if (status.stats && status.stats.totalFilesSynced > 0) {
const currentFileCount = status.stats.totalFilesSynced;
if (!this.lastLoggedFileCount || Math.abs(currentFileCount - this.lastLoggedFileCount) >= 50) {
const fileText = `Local files: ${currentFileCount} (${status.stats.filesDownloaded} downloaded, ${status.stats.filesUploaded} uploaded)`;
this.addActivityLog('success', fileText);
this.lastLoggedFileCount = currentFileCount;
}
}
}
/**
* Update detailed status display
*/
updateDetailedStatus(status) {
if (!this.syncDetailsElement) return;
let detailsHTML = '';
if (status.pendingCount > 0) {
detailsHTML += `<div class="status-detail pending">📋 ${status.pendingCount} pending</div>`;
}
if (status.inProgressCount > 0) {
detailsHTML += `<div class="status-detail in-progress">🔄 ${status.inProgressCount} in progress</div>`;
}
if (status.completedCount > 0) {
detailsHTML += `<div class="status-detail completed">✅ ${status.completedCount} completed</div>`;
}
if (status.failedCount > 0) {
detailsHTML += `<div class="status-detail failed">❌ ${status.failedCount} failed</div>`;
}
if (status.errors && status.errors.length > 0) {
detailsHTML += `<div class="status-detail errors">⚠️ ${status.errors.length} errors</div>`;
}
this.syncDetailsElement.innerHTML = detailsHTML;
}
/**
* Populate configuration form
*/
populateConfigurationForm(config) {
if (!config) return;
if (this.s3EndpointInput) this.s3EndpointInput.value = config.s3?.endpoint || '';
if (this.s3AccessKeyInput) this.s3AccessKeyInput.value = config.s3?.accessKeyId || '';
if (this.s3SecretKeyInput) this.s3SecretKeyInput.value = config.s3?.secretAccessKey || '';
if (this.s3BucketInput) this.s3BucketInput.value = config.s3?.bucketName || '';
if (this.s3RegionInput) this.s3RegionInput.value = config.s3?.region || '';
if (this.localPathInput) this.localPathInput.value = config.sync?.localPath || '';
if (this.syncIntervalInput) this.syncIntervalInput.value = config.sync?.syncInterval || 30000;
}
/**
* Save configuration
*/
async saveConfiguration() {
try {
const config = {
s3: {
endpoint: this.s3EndpointInput?.value || '',
accessKeyId: this.s3AccessKeyInput?.value || '',
secretAccessKey: this.s3SecretKeyInput?.value || '',
bucketName: this.s3BucketInput?.value || '',
region: this.s3RegionInput?.value || '',
},
sync: {
localPath: this.localPathInput?.value || '',
syncInterval: parseInt(this.syncIntervalInput?.value || '30000'),
}
};
// Update S3 config
await window.electronAPI.invoke('config:update-s3', config.s3);
// Update sync config
await window.electronAPI.invoke('config:update-sync', config.sync);
this.addActivityLog('success', 'Configuration saved successfully');
// Test S3 connection
try {
const testResult = await window.electronAPI.invoke('config:test-s3');
if (testResult.success) {
this.addActivityLog('success', 'S3 connection test successful');
} else {
this.addActivityLog('error', `S3 connection test failed: ${testResult.error}`);
}
} catch (error) {
this.addActivityLog('error', `S3 connection test failed: ${error.message || 'Unknown error'}`);
}
} catch (error) {
console.error('❌ Failed to save configuration:', error);
this.addActivityLog('error', `Failed to save configuration: ${error.message || 'Unknown error'}`);
}
}
/**
* Force full sync
*/
async forceFullSync() {
try {
this.addActivityLog('info', '🔄 Starting force full sync...');
await window.electronAPI.invoke('sync:force-full');
this.addActivityLog('success', '✅ Force full sync completed');
} catch (error) {
console.error('❌ Force full sync failed:', error);
this.addActivityLog('error', `❌ Force full sync failed: ${error.message || 'Unknown error'}`);
}
}
/**
* Trigger immediate sync to propagate local changes
*/
async triggerImmediateSync() {
try {
this.addActivityLog('info', '🚀 Triggering immediate sync to propagate local changes...');
await window.electronAPI.invoke('sync:trigger-immediate');
this.addActivityLog('success', '✅ Immediate sync completed');
} catch (error) {
console.error('❌ Immediate sync failed:', error);
this.addActivityLog('error', `❌ Immediate sync failed: ${error.message || 'Unknown error'}`);
}
}
/**
* Export configuration to .env file
*/
async exportToEnv() {
try {
const result = await window.electronAPI.invoke('config:export-env');
if (result.success) {
this.addActivityLog('success', 'Configuration exported to .env file successfully');
} else {
this.addActivityLog('error', `Failed to export configuration: ${result.error}`);
}
} catch (error) {
console.error('❌ Failed to export configuration:', error);
this.addActivityLog('error', `Failed to export configuration: ${error.message || 'Unknown error'}`);
}
}
/**
* Add entry to activity log
*/
addActivityLog(type, message) {
if (!this.activityLogElement) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.innerHTML = `
<span class="log-timestamp">[${timestamp}]</span>
<span class="log-message">${message}</span>
`;
this.activityLogElement.appendChild(logEntry);
// Scroll to bottom
this.activityLogElement.scrollTop = this.activityLogElement.scrollHeight;
// Limit log entries to prevent memory issues
const entries = this.activityLogElement.querySelectorAll('.log-entry');
if (entries.length > 100) {
entries[0].remove();
}
// Update the activity count in the header if it exists
this.updateActivityCount();
}
/**
* Update activity count in the header
*/
updateActivityCount() {
const entries = this.activityLogElement?.querySelectorAll('.log-entry') || [];
const count = entries.length;
// Find and update any activity count display
const activityCountElement = document.querySelector('.activity-count');
if (activityCountElement) {
activityCountElement.textContent = count;
}
}
/**
* Show notification
*/
showNotification(type, title, message) {
// You could implement desktop notifications here
}
}
// Initialize the renderer when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
try {
window.rekordboxSyncRenderer = new RekordboxSyncRenderer();
} catch (error) {
console.error('❌ Failed to initialize renderer:', error);
}
});
// Fallback initialization if DOMContentLoaded doesn't fire
if (document.readyState === 'loading') {
// Wait for DOMContentLoaded
} else {
try {
window.rekordboxSyncRenderer = new RekordboxSyncRenderer();
} catch (error) {
console.error('❌ Failed to initialize renderer (fallback):', error);
}
}