// Desktop File Manager for Task Challenge Game class DesktopFileManager { constructor(dataManager) { this.dataManager = dataManager; this.appPath = null; this.imageDirectories = { tasks: null, consequences: null }; this.audioDirectories = { background: null, ambient: null }; this.videoDirectories = { background: null, tasks: null, rewards: null, punishments: null }; // External video directories (linked, not copied) this.externalVideoDirectories = []; // Array of linked directory objects this.allLinkedVideos = []; // Cached array of all videos from all directories this.init(); } async init() { // Check if we're running in Electron this.isElectron = window.electronAPI !== undefined; if (this.isElectron) { try { this.appPath = await window.electronAPI.getAppPath(); if (this.appPath) { this.imageDirectories.tasks = await window.electronAPI.pathJoin(this.appPath, 'images', 'tasks'); this.imageDirectories.consequences = await window.electronAPI.pathJoin(this.appPath, 'images', 'consequences'); this.audioDirectories.background = await window.electronAPI.pathJoin(this.appPath, 'audio', 'background'); this.audioDirectories.ambient = await window.electronAPI.pathJoin(this.appPath, 'audio', 'ambient'); this.videoDirectories.background = await window.electronAPI.pathJoin(this.appPath, 'videos', 'background'); this.videoDirectories.tasks = await window.electronAPI.pathJoin(this.appPath, 'videos', 'tasks'); this.videoDirectories.rewards = await window.electronAPI.pathJoin(this.appPath, 'videos', 'rewards'); this.videoDirectories.punishments = await window.electronAPI.pathJoin(this.appPath, 'videos', 'punishments'); // Ensure directories exist await window.electronAPI.createDirectory(this.imageDirectories.tasks); await window.electronAPI.createDirectory(this.imageDirectories.consequences); await window.electronAPI.createDirectory(this.audioDirectories.background); await window.electronAPI.createDirectory(this.audioDirectories.ambient); // Note: No longer creating/using local video directories // All videos come from external linked directories only console.log('Desktop file manager initialized'); console.log('App path:', this.appPath); console.log('Image directories:', this.imageDirectories); console.log('Audio directories:', this.audioDirectories); // Load any previously linked external directories await this.loadLinkedDirectories(); } else { console.error('Failed to get app path'); } } catch (error) { console.error('Error initializing desktop file manager:', error); this.isElectron = false; } } else { console.log('Running in browser mode - file manager disabled'); } } async selectAndImportImages(category = 'task') { if (!this.isElectron) { this.showNotification('File import only available in desktop version', 'warning'); return []; } try { // Open file dialog const filePaths = await window.electronAPI.selectImages(); if (filePaths.length === 0) { return []; } const importedImages = []; const targetDir = category === 'task' ? this.imageDirectories.tasks : this.imageDirectories.consequences; if (!targetDir) { console.error('Target directory not initialized'); this.showNotification('Directory not initialized', 'error'); return []; } for (const filePath of filePaths) { const fileName = filePath.split(/[\\/]/).pop(); const targetPath = await window.electronAPI.pathJoin(targetDir, fileName); // Copy file to app directory const success = await window.electronAPI.copyImage(filePath, targetPath); if (success) { importedImages.push({ name: fileName, path: targetPath, category: category }); console.log(`Imported: ${fileName} to ${category}`); } else { console.error(`Failed to import: ${fileName}`); } } if (importedImages.length > 0) { // Update the game's image storage await this.updateImageStorage(importedImages); this.showNotification(`Imported ${importedImages.length} image(s) to ${category}!`, 'success'); } return importedImages; } catch (error) { console.error('Error importing images:', error); this.showNotification('Failed to import images', 'error'); return []; } } async selectAndImportAudio(category = 'background') { if (!this.isElectron) { this.showNotification('Audio import only available in desktop version', 'warning'); return []; } try { // Open file dialog for audio files const filePaths = await window.electronAPI.selectAudio(); if (filePaths.length === 0) { return []; } const importedAudio = []; let targetDir; switch(category) { case 'background': targetDir = this.audioDirectories.background; break; case 'ambient': targetDir = this.audioDirectories.ambient; break; case 'effects': targetDir = this.audioDirectories.effects; break; default: targetDir = this.audioDirectories.background; } if (!targetDir) { console.error('Target audio directory not initialized'); this.showNotification('Audio directory not initialized', 'error'); return []; } for (const filePath of filePaths) { const fileName = filePath.split(/[\\/]/).pop(); const targetPath = await window.electronAPI.pathJoin(targetDir, fileName); // Copy file to app directory const success = await window.electronAPI.copyAudio(filePath, targetPath); if (success) { importedAudio.push({ name: fileName, path: targetPath, category: category, title: this.getAudioTitle(fileName) }); console.log(`Imported audio: ${fileName} to ${category}`); } else { console.error(`Failed to import audio: ${fileName}`); } } if (importedAudio.length > 0) { // Update the game's audio storage await this.updateAudioStorage(importedAudio); this.showNotification(`Imported ${importedAudio.length} audio file(s) to ${category}!`, 'success'); } return importedAudio; } catch (error) { console.error('Error importing audio:', error); this.showNotification('Failed to import audio files', 'error'); return []; } } async selectAndImportVideos(category = 'background') { if (!this.isElectron) { this.showNotification('Video import only available in desktop version', 'warning'); return []; } try { // Open file dialog for video files const filePaths = await window.electronAPI.selectVideos(); if (filePaths.length === 0) { return []; } const importedVideos = []; let targetDir; switch(category) { case 'background': targetDir = this.videoDirectories.background; break; case 'task': case 'tasks': targetDir = this.videoDirectories.tasks; break; case 'reward': case 'rewards': targetDir = this.videoDirectories.rewards; break; case 'punishment': case 'punishments': targetDir = this.videoDirectories.punishments; break; default: targetDir = this.videoDirectories.background; } if (!targetDir) { console.error('Target video directory not initialized'); this.showNotification('Video directory not initialized', 'error'); return []; } for (const filePath of filePaths) { const fileName = filePath.split(/[\\/]/).pop(); const targetPath = await window.electronAPI.pathJoin(targetDir, fileName); // Copy file to app directory const success = await window.electronAPI.copyVideo(filePath, targetPath); if (success) { importedVideos.push({ name: fileName, path: targetPath, category: category, title: this.getVideoTitle(fileName), url: `file:///${targetPath.replace(/\\/g, '/')}` }); console.log(`Imported video: ${fileName} to ${category}`); } else { console.error(`Failed to import video: ${fileName}`); } } if (importedVideos.length > 0) { // Update the game's video storage await this.updateVideoStorage(importedVideos); this.showNotification(`Imported ${importedVideos.length} video file(s) to ${category}!`, 'success'); } return importedVideos; } catch (error) { console.error('Error importing videos:', error); this.showNotification('Failed to import video files', 'error'); return []; } } async addVideoDirectory(customName = null) { if (!this.isElectron) { this.showNotification('Directory linking only available in desktop version', 'warning'); return null; } try { // Open directory dialog const directoryPath = await window.electronAPI.selectDirectory(); if (!directoryPath) { return null; } // Check if directory is already linked const existingDir = this.externalVideoDirectories.find(dir => dir.path === directoryPath); if (existingDir) { this.showNotification('Directory is already linked!', 'warning'); return existingDir; } // Show scanning notification this.showNotification('🔍 Scanning directory for videos... This may take a moment for large collections.', 'info'); // Scan the directory recursively for video files console.log(`🔍 Scanning directory recursively: ${directoryPath}`); const videoFiles = await window.electronAPI.readVideoDirectoryRecursive(directoryPath); console.log(`Found ${videoFiles.length} video files in directory:`, directoryPath); if (videoFiles.length === 0) { this.showNotification('No video files found in selected directory', 'warning'); return null; } // Create directory object const directoryName = customName || this.getDirectoryName(directoryPath); const directoryObj = { id: Date.now(), // Unique ID name: directoryName, path: directoryPath, videoCount: videoFiles.length, dateAdded: new Date().toISOString(), isRecursive: true }; // Process videos in chunks to avoid blocking UI const linkedVideos = await this.processVideosInChunks(videoFiles, directoryObj); // Add to linked directories this.externalVideoDirectories.push(directoryObj); // Update cached video list this.allLinkedVideos.push(...linkedVideos); // Save to persistent storage await this.saveLinkedDirectories(); // Update video storage await this.updateUnifiedVideoStorage(); this.showNotification( `✅ Linked directory "${directoryName}"!\nFound ${videoFiles.length} videos`, 'success' ); console.log(`🔗 Linked directory: ${directoryName} (${videoFiles.length} videos)`); return { directory: directoryObj, videoCount: videoFiles.length, videos: linkedVideos }; } catch (error) { console.error('Error linking video directory:', error); this.showNotification('Failed to link video directory', 'error'); return null; } } async processVideosInChunks(videoFiles, directoryObj) { const chunkSize = 100; // Process 100 videos at a time const linkedVideos = []; for (let i = 0; i < videoFiles.length; i += chunkSize) { const chunk = videoFiles.slice(i, i + chunkSize); const progress = Math.round(((i + chunk.length) / videoFiles.length) * 100); console.log(`Processing videos ${i + 1}-${i + chunk.length} of ${videoFiles.length} (${progress}%)`); const chunkProcessed = chunk.map(video => ({ id: `${directoryObj.id}_${video.name}`, name: video.name, path: video.path, url: `file:///${video.path.replace(/\\/g, '/')}`, title: this.getVideoTitle(video.name), size: video.size || 0, directoryId: directoryObj.id, directoryName: directoryObj.name, isExternal: true, relativePath: video.path.replace(directoryObj.path, '').replace(/^[\\/]/, '') })); linkedVideos.push(...chunkProcessed); // Small delay to keep UI responsive if (i + chunkSize < videoFiles.length) { await new Promise(resolve => setTimeout(resolve, 10)); } } return linkedVideos; } async removeVideoDirectory(directoryId) { if (!this.isElectron) { return false; } try { // Find directory const dirIndex = this.externalVideoDirectories.findIndex(dir => dir.id === directoryId); if (dirIndex === -1) { this.showNotification('Directory not found', 'error'); return false; } const directory = this.externalVideoDirectories[dirIndex]; // Remove from arrays this.externalVideoDirectories.splice(dirIndex, 1); this.allLinkedVideos = this.allLinkedVideos.filter(video => video.directoryId !== directoryId); // Save to persistent storage await this.saveLinkedDirectories(); // Update video storage await this.updateUnifiedVideoStorage(); this.showNotification(`Unlinked directory: ${directory.name}`, 'success'); return true; } catch (error) { console.error('Error unlinking directory:', error); this.showNotification('Failed to unlink directory', 'error'); return false; } } async refreshAllDirectories() { if (!this.isElectron) { return; } console.log('🔄 Refreshing all linked directories...'); this.allLinkedVideos = []; for (const directory of this.externalVideoDirectories) { try { console.log(`🔍 Rescanning: ${directory.name}`); const videoFiles = await window.electronAPI.readVideoDirectoryRecursive(directory.path); const linkedVideos = videoFiles.map(video => ({ id: `${directory.id}_${video.name}`, name: video.name, path: video.path, url: `file:///${video.path.replace(/\\/g, '/')}`, title: this.getVideoTitle(video.name), size: video.size || 0, directoryId: directory.id, directoryName: directory.name, isExternal: true, relativePath: video.path.replace(directory.path, '').replace(/^[\\/]/, '') })); this.allLinkedVideos.push(...linkedVideos); // Update directory video count directory.videoCount = videoFiles.length; console.log(`✅ Found ${videoFiles.length} videos in ${directory.name}`); } catch (error) { console.warn(`Could not access directory ${directory.name}:`, error); // Directory might be unavailable (external drive, network, etc.) } } await this.saveLinkedDirectories(); await this.updateUnifiedVideoStorage(); const totalVideos = this.allLinkedVideos.length; this.showNotification(`🔄 Refreshed ${this.externalVideoDirectories.length} directories, found ${totalVideos} videos`, 'success'); } async saveLinkedDirectories() { const data = { directories: this.externalVideoDirectories, lastUpdated: new Date().toISOString() }; this.dataManager.set('linkedVideoDirectories', data); } async loadLinkedDirectories() { if (!this.isElectron) { return; } try { const data = this.dataManager.get('linkedVideoDirectories'); if (data && data.directories) { this.externalVideoDirectories = data.directories; console.log(`📁 Loaded ${this.externalVideoDirectories.length} linked directories`); // Refresh all directories to get current video lists await this.refreshAllDirectories(); } } catch (error) { console.error('Error loading linked directories:', error); } } async updateUnifiedVideoStorage() { // Store all videos in a single unified list (no categories) // If we have no linked directories but there are existing videos in storage, // don't overwrite the existing data if (this.externalVideoDirectories.length === 0 && this.allLinkedVideos.length === 0) { // Check if there are existing videos in storage try { const existingData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}'); if (existingData.allVideos && existingData.allVideos.length > 0) { console.log(`📹 Preserving existing unified video library: ${existingData.allVideos.length} videos`); // Don't overwrite - preserve existing data return; } } catch (error) { console.warn('Error checking existing unified video library:', error); } } const videoData = { allVideos: this.allLinkedVideos, directories: this.externalVideoDirectories, lastUpdated: new Date().toISOString() }; localStorage.setItem('unifiedVideoLibrary', JSON.stringify(videoData)); console.log(`📹 Updated unified video library: ${this.allLinkedVideos.length} videos from ${this.externalVideoDirectories.length} directories`); // Trigger video manager reload if it exists if (window.videoPlayerManager) { window.videoPlayerManager.loadVideoFiles(); } } getDirectoryName(directoryPath) { // Extract just the folder name from the full path return directoryPath.split(/[\\/]/).pop() || 'Unknown Directory'; } getAllVideos() { return this.allLinkedVideos; } getDirectoriesInfo() { return this.externalVideoDirectories.map(dir => ({ id: dir.id, name: dir.name, path: dir.path, videoCount: dir.videoCount, dateAdded: dir.dateAdded })); } async scanDirectoryForImages(category = 'task') { if (!this.isElectron) { return []; } try { const targetDir = category === 'task' ? this.imageDirectories.tasks : this.imageDirectories.consequences; if (!targetDir) { console.error(`Target directory not initialized for ${category}`); return []; } const images = await window.electronAPI.readDirectory(targetDir); console.log(`Found ${images.length} images in ${category} directory`); return images.map(img => ({ ...img, category: category })); } catch (error) { console.error(`Error scanning ${category} directory:`, error); return []; } } async updateImageStorage(images) { // Get existing images let customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; // Convert old format if necessary if (Array.isArray(customImages)) { customImages = { task: customImages, consequence: [] }; } // Add new images (avoid duplicates) for (const image of images) { const category = image.category === 'tasks' ? 'task' : image.category === 'consequences' ? 'consequence' : image.category; if (!customImages[category]) { customImages[category] = []; } // Check for duplicates by path const exists = customImages[category].some(existing => { if (typeof existing === 'string') { return existing === image.path; } else if (typeof existing === 'object') { return existing.path === image.path; } return false; }); if (!exists) { customImages[category].push(image.path); } } // Save updated images this.dataManager.set('customImages', customImages); return customImages; } async deleteImage(imagePath, category) { if (!this.isElectron) { return false; } try { const success = await window.electronAPI.deleteFile(imagePath); if (success) { // Remove from storage let customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; if (Array.isArray(customImages)) { customImages = { task: customImages, consequence: [] }; } if (customImages[category]) { customImages[category] = customImages[category].filter(img => { if (typeof img === 'string') { return img !== imagePath; } else if (typeof img === 'object') { return img.path !== imagePath; } return true; }); } this.dataManager.set('customImages', customImages); this.showNotification('Image deleted successfully', 'success'); return true; } else { this.showNotification('Failed to delete image', 'error'); return false; } } catch (error) { console.error('Error deleting image:', error); this.showNotification('Error deleting image', 'error'); return false; } } async openImageDirectory(category = 'task') { if (!this.isElectron) { this.showNotification('This feature is only available in desktop version', 'info'); return; } // Note: We would need to add shell integration to open the folder // For now, just show the path const dir = this.imageDirectories[category === 'task' ? 'tasks' : 'consequences']; this.showNotification(`Images stored in: ${dir}`, 'info'); console.log(`${category} images directory:`, dir); } async scanDirectoryForAudio(category = 'background') { if (!this.isElectron) { return []; } try { let targetDir; switch(category) { case 'background': targetDir = this.audioDirectories.background; break; case 'ambient': targetDir = this.audioDirectories.ambient; break; case 'effects': targetDir = this.audioDirectories.effects; break; default: targetDir = this.audioDirectories.background; } if (!targetDir) { console.error(`Target audio directory not initialized for ${category}`); return []; } const audioFiles = await window.electronAPI.readAudioDirectory(targetDir); console.log(`Found ${audioFiles.length} audio files in ${category} directory`); return audioFiles.map(audio => ({ ...audio, category: category, title: this.getAudioTitle(audio.name) })); } catch (error) { console.error(`Error scanning ${category} audio directory:`, error); return []; } } async scanAllAudioDirectories() { if (!this.isElectron) { this.showNotification('Audio directory scanning only available in desktop version', 'warning'); return { background: [], ambient: [] }; } const backgroundAudio = await this.scanDirectoryForAudio('background'); const ambientAudio = await this.scanDirectoryForAudio('ambient'); const results = { background: backgroundAudio, ambient: ambientAudio }; // Update game audio storage if (backgroundAudio.length > 0 || ambientAudio.length > 0) { await this.updateAudioStorage([...backgroundAudio, ...ambientAudio]); } return results; } async updateAudioStorage(audioFiles) { if (!Array.isArray(audioFiles) || audioFiles.length === 0) { return; } // Get existing audio let customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [] }; // Add new audio files (avoid duplicates) for (const audio of audioFiles) { const category = audio.category || 'background'; if (!customAudio[category]) { customAudio[category] = []; } // Check for duplicates by path const exists = customAudio[category].some(existing => { if (typeof existing === 'string') { return existing === audio.path; } else if (typeof existing === 'object') { return existing.path === audio.path; } return false; }); if (!exists) { customAudio[category].push({ name: audio.name, path: audio.path, title: audio.title, category: audio.category }); } } // Save back to storage this.dataManager.set('customAudio', customAudio); console.log('Audio storage updated:', customAudio); } async deleteAudio(audioPath, category = 'background') { if (!this.isElectron) { this.showNotification('Audio deletion only available in desktop version', 'warning'); return false; } try { const success = await window.electronAPI.deleteFile(audioPath); if (success) { // Remove from storage let customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [] }; if (customAudio[category]) { customAudio[category] = customAudio[category].filter(audio => { if (typeof audio === 'string') { return audio !== audioPath; } return audio.path !== audioPath; }); } this.dataManager.set('customAudio', customAudio); console.log(`Deleted audio file: ${audioPath}`); return true; } return false; } catch (error) { console.error('Error deleting audio:', error); return false; } } getAudioTitle(fileName) { // Remove file extension and clean up the name for display return fileName .replace(/\.[^/.]+$/, '') // Remove extension .replace(/[-_]/g, ' ') // Replace dashes and underscores with spaces .replace(/\b\w/g, l => l.toUpperCase()); // Capitalize first letters } getAudioPath(audioName, category = 'background') { if (!this.isElectron) { return `audio/${audioName}`; } let dir; switch(category) { case 'background': dir = this.audioDirectories.background; break; case 'ambient': dir = this.audioDirectories.ambient; break; case 'effects': dir = this.audioDirectories.effects; break; default: dir = this.audioDirectories.background; } return `${dir}/${audioName}`; } showNotification(message, type = 'info') { // Use the game's existing notification system if (window.game && window.game.showNotification) { window.game.showNotification(message, type); } else { console.log(`[${type.toUpperCase()}] ${message}`); } } getImagePath(imageName, category = 'task') { if (!this.isElectron) { return `images/${category}s/${imageName}`; } const dir = this.imageDirectories[category === 'task' ? 'tasks' : 'consequences']; return `${dir}/${imageName}`; } getVideoTitle(fileName) { // Remove file extension and clean up the name for display return fileName .replace(/\.[^/.]+$/, '') // Remove extension .replace(/[-_]/g, ' ') // Replace dashes and underscores with spaces .replace(/\b\w/g, l => l.toUpperCase()); // Capitalize first letters } getVideoPath(videoName, category = 'background') { if (!this.isElectron) { return `videos/${videoName}`; } let dir; switch(category) { case 'background': dir = this.videoDirectories.background; break; case 'tasks': dir = this.videoDirectories.tasks; break; case 'rewards': dir = this.videoDirectories.rewards; break; case 'punishments': dir = this.videoDirectories.punishments; break; default: dir = this.videoDirectories.background; } return `${dir}/${videoName}`; } async updateVideoStorage(videoFiles) { // Get existing videos from localStorage const existingVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}'); console.log('Updating video storage with', videoFiles.length, 'videos'); // Add new videos videoFiles.forEach(video => { console.log(`Adding video to category: ${video.category}, name: ${video.name}`); if (!existingVideos[video.category]) { existingVideos[video.category] = []; console.log(`Created new category: ${video.category}`); } // Check if video already exists (prevent duplicates) const exists = existingVideos[video.category].some(existing => existing.name === video.name); if (!exists) { existingVideos[video.category].push(video); console.log(`Added video: ${video.name} to ${video.category}`); } else { console.log(`Video already exists: ${video.name} in ${video.category}`); } }); console.log('Final video storage:', existingVideos); // Save back to localStorage localStorage.setItem('videoFiles', JSON.stringify(existingVideos)); // Trigger video manager reload if it exists if (window.videoPlayerManager) { window.videoPlayerManager.loadVideoFiles(); } } async scanDirectoryForVideos(category = 'background') { if (!this.isElectron) { return []; } try { let targetDir; switch(category) { case 'background': targetDir = this.videoDirectories.background; break; case 'task': case 'tasks': targetDir = this.videoDirectories.tasks; break; case 'reward': case 'rewards': targetDir = this.videoDirectories.rewards; break; case 'punishment': case 'punishments': targetDir = this.videoDirectories.punishments; break; default: targetDir = this.videoDirectories.background; } if (!targetDir) { console.log(`Video directory for ${category} not initialized`); return []; } const videos = await window.electronAPI.readVideoDirectory(targetDir); // Add category information return videos.map(video => ({ ...video, category: category })); } catch (error) { console.error(`Error scanning video directory for ${category}:`, error); return []; } } async loadAllVideos() { if (!this.isElectron) { return; } try { // Scan all video directories const backgroundVideos = await this.scanDirectoryForVideos('background'); const taskVideos = await this.scanDirectoryForVideos('tasks'); const rewardVideos = await this.scanDirectoryForVideos('rewards'); const punishmentVideos = await this.scanDirectoryForVideos('punishments'); // Update storage with all found videos await this.updateVideoStorage([...backgroundVideos, ...taskVideos, ...rewardVideos, ...punishmentVideos]); } catch (error) { console.error('Error loading videos from directories:', error); } } } // Global file manager instance let desktopFileManager = null;