f25987e553
Root cause: localStorage keys 'oxicloud_recent_files' and
'oxicloud_favorites' were global — shared across all users on the same
browser. When user A logged out and user B logged in, user B could see
(and access) user A's recent files and favorites.
Fixes applied:
recent.js:
- Storage key now user-specific: 'oxicloud_recent_files_{username}'
- getStorageKey() derives key from current user in localStorage
- migrateFromLegacyKey() moves data from old global key on init
- Legacy global key is always removed after migration
favorites.js:
- Same pattern: 'oxicloud_favorites_{username}'
- getStorageKey() + migrateFromLegacyKey() added
auth.js (logout):
- Clears user-specific recent and favorites keys before removing
user data, plus removes any legacy global keys
Bumps service worker cache to v13.
342 lines
13 KiB
JavaScript
342 lines
13 KiB
JavaScript
/**
|
|
* OxiCloud - Recent Files Module
|
|
* This file handles tracking and displaying recently accessed files
|
|
*/
|
|
|
|
// Recent Files Module
|
|
const recent = {
|
|
// Base key for storing recent files in localStorage (username is appended)
|
|
STORAGE_KEY_PREFIX: 'oxicloud_recent_files',
|
|
|
|
// Legacy key (pre-fix, shared across all users)
|
|
LEGACY_STORAGE_KEY: 'oxicloud_recent_files',
|
|
|
|
// Maximum number of recent files to store
|
|
MAX_RECENT_FILES: 20,
|
|
|
|
/**
|
|
* Get the user-specific storage key for recent files.
|
|
* 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 recent files key');
|
|
}
|
|
// Should not happen in normal flow — user must be logged in
|
|
return this.LEGACY_STORAGE_KEY;
|
|
},
|
|
|
|
/**
|
|
* Initialize recent files module
|
|
*/
|
|
init() {
|
|
console.log('Initializing recent files module');
|
|
this.migrateFromLegacyKey();
|
|
this.ensureRecentFilesStorage();
|
|
this.setupEventListeners();
|
|
},
|
|
|
|
/**
|
|
* Migrate data from the old global key to the user-specific key.
|
|
* This runs once: if the legacy key has data and the user-specific key
|
|
* does not yet exist, the data is moved.
|
|
*/
|
|
migrateFromLegacyKey() {
|
|
const userKey = this.getStorageKey();
|
|
// Only migrate if the key is actually user-specific
|
|
if (userKey === this.LEGACY_STORAGE_KEY) return;
|
|
|
|
const legacyData = localStorage.getItem(this.LEGACY_STORAGE_KEY);
|
|
if (legacyData && !localStorage.getItem(userKey)) {
|
|
console.log('Migrating recent files from legacy global key to user-specific key');
|
|
localStorage.setItem(userKey, legacyData);
|
|
}
|
|
// Always remove the legacy key so other users don't see stale data
|
|
localStorage.removeItem(this.LEGACY_STORAGE_KEY);
|
|
},
|
|
|
|
/**
|
|
* Make sure the recent files storage is initialized
|
|
*/
|
|
ensureRecentFilesStorage() {
|
|
const key = this.getStorageKey();
|
|
if (!localStorage.getItem(key)) {
|
|
localStorage.setItem(key, JSON.stringify([]));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set up event listeners to track file access
|
|
*/
|
|
setupEventListeners() {
|
|
// Listen for custom event when a file is accessed
|
|
document.addEventListener('file-accessed', (event) => {
|
|
if (event.detail && event.detail.file) {
|
|
this.addRecentFile(event.detail.file);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Add a file to recent files
|
|
* @param {Object} file - File object containing id, name, folder_id, etc.
|
|
*/
|
|
addRecentFile(file) {
|
|
// Don't add if no file or no ID
|
|
if (!file || !file.id) {
|
|
return;
|
|
}
|
|
|
|
// Get current recent files
|
|
const recentFiles = this.getRecentFiles();
|
|
|
|
// Remove if file already exists in recent files
|
|
const existingIndex = recentFiles.findIndex(item => item.id === file.id);
|
|
if (existingIndex !== -1) {
|
|
recentFiles.splice(existingIndex, 1);
|
|
}
|
|
|
|
// Add file with timestamp to the beginning of the array
|
|
const fileWithTimestamp = {
|
|
...file,
|
|
accessedAt: Date.now()
|
|
};
|
|
|
|
recentFiles.unshift(fileWithTimestamp);
|
|
|
|
// Keep only the most recent files (limit to MAX_RECENT_FILES)
|
|
const trimmedFiles = recentFiles.slice(0, this.MAX_RECENT_FILES);
|
|
|
|
// Save back to localStorage (user-scoped key)
|
|
localStorage.setItem(this.getStorageKey(), JSON.stringify(trimmedFiles));
|
|
},
|
|
|
|
/**
|
|
* Get recent files from localStorage
|
|
* @returns {Array} Array of recent file objects with timestamps
|
|
*/
|
|
getRecentFiles() {
|
|
try {
|
|
const recentFilesJson = localStorage.getItem(this.getStorageKey());
|
|
return recentFilesJson ? JSON.parse(recentFilesJson) : [];
|
|
} catch (error) {
|
|
console.error('Error loading recent files:', error);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear all recent files
|
|
*/
|
|
clearRecentFiles() {
|
|
localStorage.setItem(this.getStorageKey(), JSON.stringify([]));
|
|
},
|
|
|
|
/**
|
|
* Display recent files in the UI
|
|
*/
|
|
async displayRecentFiles() {
|
|
try {
|
|
const recentFiles = this.getRecentFiles();
|
|
|
|
// Clear existing content
|
|
const filesGrid = document.getElementById('files-grid');
|
|
const filesListView = document.getElementById('files-list-view');
|
|
|
|
filesGrid.innerHTML = '';
|
|
filesListView.innerHTML = `
|
|
<div class="list-header recent-header">
|
|
<div></div>
|
|
<div data-i18n="files.name">Name</div>
|
|
<div data-i18n="files.type">Type</div>
|
|
<div data-i18n="files.size">Size</div>
|
|
<div data-i18n="recent.accessed">Accessed</div>
|
|
</div>
|
|
`;
|
|
|
|
// Update breadcrumb - just show Home
|
|
window.ui.updateBreadcrumb('');
|
|
|
|
// Show empty state if no recent files
|
|
if (recentFiles.length === 0) {
|
|
const emptyState = document.createElement('div');
|
|
emptyState.className = 'empty-state';
|
|
emptyState.innerHTML = `
|
|
<i class="fas fa-clock" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
|
<p>${window.i18n ? window.i18n.t('recent.empty_state') : 'No recent files'}</p>
|
|
<p>${window.i18n ? window.i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
|
|
`;
|
|
filesGrid.appendChild(emptyState);
|
|
return;
|
|
}
|
|
|
|
// Process each recent file
|
|
for (const recentFile of recentFiles) {
|
|
this.createRecentFileElement(recentFile, filesGrid, filesListView);
|
|
}
|
|
|
|
// Update file icons
|
|
window.ui.updateFileIcons();
|
|
|
|
} catch (error) {
|
|
console.error('Error displaying recent files:', error);
|
|
window.ui.showNotification('Error', 'Error loading recent files');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create a file element for a recent file
|
|
* @param {Object} file - Recent file object
|
|
* @param {HTMLElement} filesGrid - Grid view container
|
|
* @param {HTMLElement} filesListView - List view container
|
|
*/
|
|
createRecentFileElement(file, filesGrid, filesListView) {
|
|
// Determine icon and type
|
|
let iconClass = 'fas fa-file';
|
|
let iconSpecialClass = '';
|
|
let typeLabel = window.i18n ? window.i18n.t('files.file_types.document') : 'Document';
|
|
|
|
if (file.mime_type) {
|
|
if (file.mime_type.startsWith('image/')) {
|
|
iconClass = 'fas fa-file-image';
|
|
iconSpecialClass = 'image-icon';
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image';
|
|
} else if (file.mime_type.startsWith('text/')) {
|
|
iconClass = 'fas fa-file-alt';
|
|
iconSpecialClass = 'text-icon';
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text';
|
|
} else if (file.mime_type.startsWith('video/')) {
|
|
iconClass = 'fas fa-file-video';
|
|
iconSpecialClass = 'video-icon';
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.video') : 'Video';
|
|
} else if (file.mime_type.startsWith('audio/')) {
|
|
iconClass = 'fas fa-file-audio';
|
|
iconSpecialClass = 'audio-icon';
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.audio') : 'Audio';
|
|
} else if (file.mime_type === 'application/pdf') {
|
|
iconClass = 'fas fa-file-pdf';
|
|
iconSpecialClass = 'pdf-icon';
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.pdf') : 'PDF';
|
|
}
|
|
}
|
|
|
|
// Format size and date
|
|
const fileSize = window.formatFileSize ? window.formatFileSize(file.size || 0) : '0 B';
|
|
const accessedDate = new Date(file.accessedAt);
|
|
const formattedDate = accessedDate.toLocaleDateString() + ' ' +
|
|
accessedDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
|
|
// Grid view element
|
|
const fileGridElement = document.createElement('div');
|
|
fileGridElement.className = 'file-card recent-item';
|
|
fileGridElement.dataset.fileId = file.id;
|
|
fileGridElement.dataset.fileName = file.name;
|
|
fileGridElement.dataset.folderId = file.folder_id || "";
|
|
|
|
fileGridElement.innerHTML = `
|
|
<div class="recent-indicator">
|
|
<i class="fas fa-clock"></i>
|
|
</div>
|
|
<div class="file-icon ${iconSpecialClass}">
|
|
<i class="${iconClass}"></i>
|
|
</div>
|
|
<div class="file-name">${file.name}</div>
|
|
<div class="file-info">Accessed ${formattedDate.split(' ')[0]}</div>
|
|
`;
|
|
|
|
// View or download on click
|
|
fileGridElement.addEventListener('click', () => {
|
|
if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
|
|
window.inlineViewer.openFile(file);
|
|
} else if (window.fileOps) {
|
|
window.fileOps.downloadFile(file.id, file.name);
|
|
}
|
|
|
|
// Dispatch custom event to update recent files
|
|
document.dispatchEvent(new CustomEvent('file-accessed', {
|
|
detail: { file }
|
|
}));
|
|
});
|
|
|
|
// Context menu
|
|
fileGridElement.addEventListener('contextmenu', (e) => {
|
|
e.preventDefault();
|
|
|
|
window.app.contextMenuTargetFile = {
|
|
id: file.id,
|
|
name: file.name,
|
|
folder_id: file.folder_id || ""
|
|
};
|
|
|
|
let fileContextMenu = document.getElementById('file-context-menu');
|
|
fileContextMenu.style.left = `${e.pageX}px`;
|
|
fileContextMenu.style.top = `${e.pageY}px`;
|
|
fileContextMenu.style.display = 'block';
|
|
});
|
|
|
|
filesGrid.appendChild(fileGridElement);
|
|
|
|
// List view element
|
|
const fileListElement = document.createElement('div');
|
|
fileListElement.className = 'file-item recent-item';
|
|
fileListElement.dataset.fileId = file.id;
|
|
fileListElement.dataset.fileName = file.name;
|
|
fileListElement.dataset.folderId = file.folder_id || "";
|
|
|
|
fileListElement.innerHTML = `
|
|
<div class="recent-indicator">
|
|
<i class="fas fa-clock"></i>
|
|
</div>
|
|
<div class="name-cell">
|
|
<div class="file-icon ${iconSpecialClass}">
|
|
<i class="${iconClass}"></i>
|
|
</div>
|
|
<span>${file.name}</span>
|
|
</div>
|
|
<div class="type-cell">${typeLabel}</div>
|
|
<div class="size-cell">${fileSize}</div>
|
|
<div class="date-cell">${formattedDate}</div>
|
|
`;
|
|
|
|
// View or download on click
|
|
fileListElement.addEventListener('click', () => {
|
|
if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
|
|
window.inlineViewer.openFile(file);
|
|
} else if (window.fileOps) {
|
|
window.fileOps.downloadFile(file.id, file.name);
|
|
}
|
|
|
|
// Dispatch custom event to update recent files
|
|
document.dispatchEvent(new CustomEvent('file-accessed', {
|
|
detail: { file }
|
|
}));
|
|
});
|
|
|
|
// Context menu
|
|
fileListElement.addEventListener('contextmenu', (e) => {
|
|
e.preventDefault();
|
|
|
|
window.app.contextMenuTargetFile = {
|
|
id: file.id,
|
|
name: file.name,
|
|
folder_id: file.folder_id || ""
|
|
};
|
|
|
|
let fileContextMenu = document.getElementById('file-context-menu');
|
|
fileContextMenu.style.left = `${e.pageX}px`;
|
|
fileContextMenu.style.top = `${e.pageY}px`;
|
|
fileContextMenu.style.display = 'block';
|
|
});
|
|
|
|
filesListView.appendChild(fileListElement);
|
|
}
|
|
};
|
|
|
|
// Expose recent module globally
|
|
window.recent = recent; |