536 lines
17 KiB
JavaScript
536 lines
17 KiB
JavaScript
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.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 concise information
|
||
if (this.syncStatusElement) {
|
||
if (status.isRunning) {
|
||
let statusText = 'Running';
|
||
|
||
// Add current phase information (shortened)
|
||
if (status.currentPhase) {
|
||
const phase = status.currentPhase === 'watching' ? 'Watching' :
|
||
status.currentPhase === 'downloading' ? 'Downloading' :
|
||
status.currentPhase === 'uploading' ? 'Uploading' :
|
||
status.currentPhase === 'completed' ? 'Complete' :
|
||
status.currentPhase;
|
||
statusText = phase;
|
||
}
|
||
|
||
this.syncStatusElement.textContent = statusText;
|
||
this.syncStatusElement.className = 'status-value running';
|
||
} else {
|
||
this.syncStatusElement.textContent = 'Stopped';
|
||
this.syncStatusElement.className = 'status-value stopped';
|
||
}
|
||
}
|
||
|
||
// 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 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
|
||
}
|
||
|
||
/**
|
||
* Open settings modal
|
||
*/
|
||
openSettings() {
|
||
const modal = document.getElementById('settingsModal');
|
||
if (modal) {
|
||
modal.classList.add('show');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Minimize window
|
||
*/
|
||
minimizeWindow() {
|
||
if (window.electronAPI && window.electronAPI.invoke) {
|
||
window.electronAPI.invoke('window:minimize');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Close window
|
||
*/
|
||
closeWindow() {
|
||
if (window.electronAPI && window.electronAPI.invoke) {
|
||
window.electronAPI.invoke('window:close');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
}
|