/** * OxiCloud - Favorites Module * This file handles favoriting files and folders, persisting favorites, and displaying favorite items */ // Favorites Module const favorites = { // Base key for storing favorites in localStorage (username is appended) STORAGE_KEY_PREFIX: 'oxicloud_favorites', // Legacy key (pre-fix, shared across all users) LEGACY_STORAGE_KEY: 'oxicloud_favorites', // Flag to indicate if backend API is available backendApiAvailable: false, /** * Get the user-specific storage key for favorites. * Falls back to legacy global key if username is unavailable. * @returns {string} localStorage key scoped to the current user */ getStorageKey() { try { const userData = JSON.parse(localStorage.getItem('oxicloud_user') || '{}'); if (userData.username) { return `${this.STORAGE_KEY_PREFIX}_${userData.username}`; } } catch (e) { console.warn('Could not determine current user for favorites key'); } return this.LEGACY_STORAGE_KEY; }, /** * Initialize favorites module */ init() { console.log('Initializing favorites module'); this.migrateFromLegacyKey(); this.loadFavorites(); // Check if backend favorites API is available this.checkBackendAvailability(); }, /** * Migrate data from the old global key to the user-specific key. */ migrateFromLegacyKey() { const userKey = this.getStorageKey(); if (userKey === this.LEGACY_STORAGE_KEY) return; const legacyData = localStorage.getItem(this.LEGACY_STORAGE_KEY); if (legacyData && !localStorage.getItem(userKey)) { console.log('Migrating favorites from legacy global key to user-specific key'); localStorage.setItem(userKey, legacyData); } localStorage.removeItem(this.LEGACY_STORAGE_KEY); }, /** * Check if backend favorites API is available */ async checkBackendAvailability() { try { // Add error handling to prevent console errors by catching 500 errors const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); // 3s timeout const favToken = localStorage.getItem('oxicloud_token'); const favHeaders = favToken ? { 'Authorization': `Bearer ${favToken}` } : {}; const response = await fetch('/api/favorites', { method: 'GET', headers: favHeaders, signal: controller.signal }).catch(err => { console.warn('Network error checking favorites API:', err); return { ok: false, status: 0 }; }); clearTimeout(timeoutId); // Check if the response indicates the API is properly implemented this.backendApiAvailable = response.ok; if (!response.ok) { console.log(`Backend favorites API returned status ${response.status} - using local storage fallback`); this.backendApiAvailable = false; } else { console.log('Backend favorites API is available'); // If backend API is available, sync local favorites with server this.syncWithServer(); } } catch (error) { console.warn('Error checking backend favorites API availability:', error); this.backendApiAvailable = false; } }, /** * Sync local favorites with server */ async syncWithServer() { try { // Get server favorites const syncToken = localStorage.getItem('oxicloud_token'); const syncHeaders = syncToken ? { 'Authorization': `Bearer ${syncToken}` } : {}; const response = await fetch('/api/favorites', { headers: syncHeaders }); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } const serverFavorites = await response.json(); const localFavorites = this.loadFavorites(); console.log('Syncing favorites with server', { serverCount: serverFavorites.length, localCount: localFavorites.length }); // Create a map of server favorites for quick lookup const serverFavoritesMap = new Map(); serverFavorites.forEach(item => { serverFavoritesMap.set(`${item.item_type}:${item.item_id}`, item); }); // Add local favorites that aren't on server for (const localItem of localFavorites) { const key = `${localItem.type}:${localItem.id}`; if (!serverFavoritesMap.has(key)) { console.log(`Adding local favorite to server: ${key}`); await this.addToServerFavorites(localItem.id, localItem.type); } } // Store server favorites locally (complete sync) const mergedFavorites = serverFavorites.map(item => ({ id: item.item_id, name: '', // Name will be populated when viewing favorites type: item.item_type, parentId: null, // Will be determined when viewing dateAdded: item.created_at })); this.saveFavorites(mergedFavorites); console.log('Favorites sync completed'); } catch (error) { console.error('Error syncing favorites with server:', error); } }, /** * Load favorites from localStorage * @returns {Array} Array of favorite items */ loadFavorites() { try { const stored = localStorage.getItem(this.getStorageKey()); return stored ? JSON.parse(stored) : []; } catch (error) { console.error('Error loading favorites:', error); return []; } }, /** * Save favorites to localStorage * @param {Array} favorites - Array of favorite items to save */ saveFavorites(favorites) { try { localStorage.setItem(this.getStorageKey(), JSON.stringify(favorites)); } catch (error) { console.error('Error saving favorites:', error); } }, /** * Add a favorite to the server * @param {string} id - Item ID * @param {string} type - 'file' or 'folder' */ async addToServerFavorites(id, type) { try { const addToken = localStorage.getItem('oxicloud_token'); const addHeaders = { 'Content-Type': 'application/json' }; if (addToken) addHeaders['Authorization'] = `Bearer ${addToken}`; const response = await fetch(`/api/favorites/${type}/${id}`, { method: 'POST', headers: addHeaders }); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } return true; } catch (error) { console.error('Error adding favorite to server:', error); return false; } }, /** * Remove a favorite from the server * @param {string} id - Item ID * @param {string} type - 'file' or 'folder' */ async removeFromServerFavorites(id, type) { try { const rmToken = localStorage.getItem('oxicloud_token'); const rmHeaders = rmToken ? { 'Authorization': `Bearer ${rmToken}` } : {}; const response = await fetch(`/api/favorites/${type}/${id}`, { method: 'DELETE', headers: rmHeaders }); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } return true; } catch (error) { console.error('Error removing favorite from server:', error); return false; } }, /** * Add an item to favorites * @param {string} id - Item ID * @param {string} name - Item name * @param {string} type - 'file' or 'folder' * @param {string} parentId - Parent folder ID (or null for root items) * @returns {boolean} Success status */ async addToFavorites(id, name, type, parentId) { try { const favorites = this.loadFavorites(); // Check if already in favorites if (favorites.some(item => item.id === id && item.type === type)) { console.log(`Item ${id} already in favorites`); return false; } // Add to favorites favorites.push({ id, name, type, parentId: parentId || null, dateAdded: new Date().toISOString() }); // Save updated favorites locally this.saveFavorites(favorites); // If backend API is available, sync with server if (this.backendApiAvailable) { await this.addToServerFavorites(id, type); } // Show success notification window.ui.showNotification( 'Added to favorites', `"${name}" added to favorites` ); // Refresh file view to show star icon if (window.app.currentSection === 'files' && typeof window.loadFiles === 'function') { window.loadFiles(); } return true; } catch (error) { console.error('Error adding to favorites:', error); return false; } }, /** * Remove an item from favorites * @param {string} id - Item ID * @param {string} type - 'file' or 'folder' * @returns {boolean} Success status */ async removeFromFavorites(id, type) { try { let favorites = this.loadFavorites(); const initialLength = favorites.length; // Find the item to get its name for notification const item = favorites.find(item => item.id === id && item.type === type); // Filter out the item favorites = favorites.filter(item => !(item.id === id && item.type === type)); // Save updated favorites locally this.saveFavorites(favorites); // If backend API is available, sync with server if (this.backendApiAvailable) { await this.removeFromServerFavorites(id, type); } // Check if anything was removed if (favorites.length < initialLength) { // Show success notification if item was found if (item) { window.ui.showNotification( 'Removed from favorites', `"${item.name}" removed from favorites` ); } // Refresh file view to remove star icon if (window.app.currentSection === 'files' && typeof window.loadFiles === 'function') { window.loadFiles(); } return true; } return false; } catch (error) { console.error('Error removing from favorites:', error); return false; } }, /** * Check if an item is in favorites * @param {string} id - Item ID * @param {string} type - 'file' or 'folder' * @returns {boolean} True if item is in favorites */ isFavorite(id, type) { const favorites = this.loadFavorites(); return favorites.some(item => item.id === id && item.type === type); }, /** * Load and display favorite items in the UI */ async displayFavorites() { try { const favorites = this.loadFavorites(); // Clear existing content const filesGrid = document.getElementById('files-grid'); const filesListView = document.getElementById('files-list-view'); filesGrid.innerHTML = ''; filesListView.innerHTML = `
${window.i18n ? window.i18n.t('favorites.empty_state') : 'No favorite items'}
${window.i18n ? window.i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}
`; filesGrid.appendChild(emptyState); return; } // Load details for each favorite item let loadedItems = 0; const totalItems = favorites.length; // Process each favorite item for (const favorite of favorites) { try { if (favorite.type === 'folder') { await this.loadFolderDetails(favorite, filesGrid, filesListView); } else { await this.loadFileDetails(favorite, filesGrid, filesListView); } } catch (error) { console.error(`Error loading favorite ${favorite.type} ${favorite.id}:`, error); } // Update progress - could be used for loading indicator loadedItems++; console.log(`Loaded ${loadedItems}/${totalItems} favorite items`); } // Update file icons window.ui.updateFileIcons(); } catch (error) { console.error('Error displaying favorites:', error); window.ui.showNotification('Error', 'Error loading favorite items'); } }, /** * Load folder details and add to view * @param {Object} favorite - Favorite folder item * @param {HTMLElement} filesGrid - Grid view container * @param {HTMLElement} filesListView - List view container */ async loadFolderDetails(favorite, filesGrid, filesListView) { try { const token = localStorage.getItem('oxicloud_token'); const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; const response = await fetch(`/api/folders/${favorite.id}`, { headers }); if (response.ok) { const folder = await response.json(); // Create UI element with favorite indicator this.createFavoriteFolderElement(folder, filesGrid, filesListView); } else if (response.status === 404) { // Folder not found, might be deleted console.log(`Favorite folder ${favorite.id} not found, removing from favorites`); this.removeFromFavorites(favorite.id, 'folder'); } else { console.error(`Error loading folder ${favorite.id}:`, response.statusText); } } catch (error) { console.error(`Error loading folder details for ${favorite.id}:`, error); } }, /** * Load file details and add to view * @param {Object} favorite - Favorite file item * @param {HTMLElement} filesGrid - Grid view container * @param {HTMLElement} filesListView - List view container */ async loadFileDetails(favorite, filesGrid, filesListView) { try { const token = localStorage.getItem('oxicloud_token'); const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; const response = await fetch(`/api/files/${favorite.id}?metadata=true`, { headers }); if (response.ok) { const file = await response.json(); // Create UI element with favorite indicator this.createFavoriteFileElement(file, filesGrid, filesListView); } else if (response.status === 404) { // File not found, might be deleted console.log(`Favorite file ${favorite.id} not found, removing from favorites`); this.removeFromFavorites(favorite.id, 'file'); } else { console.error(`Error loading file ${favorite.id}:`, response.statusText); } } catch (error) { console.error(`Error loading file details for ${favorite.id}:`, error); // Create a fallback file element to prevent the favorite from disappearing const fallbackFile = { id: favorite.id, name: favorite.name || `File ${favorite.id}`, mime_type: 'application/octet-stream', size: 0, modified_at: Math.floor(Date.now() / 1000) }; this.createFavoriteFileElement(fallbackFile, filesGrid, filesListView); } }, /** * Create a folder element with favorite indicator * @param {Object} folder - Folder object * @param {HTMLElement} filesGrid - Grid view container * @param {HTMLElement} filesListView - List view container */ createFavoriteFolderElement(folder, filesGrid, filesListView) { // Create standard folder element const folderGridElement = document.createElement('div'); folderGridElement.className = 'file-card favorite-item'; folderGridElement.dataset.folderId = folder.id; folderGridElement.dataset.folderName = folder.name; folderGridElement.dataset.parentId = folder.parent_id || ""; // Add favorite star folderGridElement.innerHTML = `