adding search engine

This commit is contained in:
DioCrafts
2025-03-27 01:13:34 +01:00
parent dacc3ecc4c
commit 7affff379d
24 changed files with 1531 additions and 66 deletions
+49
View File
@@ -144,6 +144,8 @@ body {
max-width: 500px;
position: relative;
margin-right: 20px;
display: flex;
align-items: center;
}
.search-container input {
@@ -165,6 +167,53 @@ body {
font-size: 16px;
}
.search-button {
background-color: #ff5e3a;
color: white;
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
margin-left: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.search-button:hover {
background-color: #e64a29;
}
/* Estilos para resultados de búsqueda */
.search-results-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
margin-bottom: 15px;
border-bottom: 1px solid #eee;
width: 100%;
}
.search-results-header h3 {
margin: 0;
font-size: 16px;
color: #555;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
color: #888;
width: 100%;
}
.user-controls {
display: flex;
align-items: center;
+2
View File
@@ -16,6 +16,7 @@
<script src="/js/fileRenderer.js"></script>
<script src="/js/contextMenus.js"></script>
<script src="/js/fileOperations.js"></script>
<script src="/js/search.js"></script>
<script src="/js/ui.js"></script>
<script src="/js/app.js"></script>
@@ -82,6 +83,7 @@
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<input type="text" data-i18n-placeholder="actions.search" placeholder="Buscar archivos, carpetas...">
<button id="search-button" class="search-button"><i class="fas fa-search"></i></button>
</div>
<div class="user-controls">
+200 -15
View File
@@ -14,6 +14,7 @@ const app = {
moveDialogMode: 'file', // Move dialog mode: 'file' or 'folder'
isTrashView: false, // Whether we're in trash view
currentSection: 'files', // Current section: 'files' or 'trash'
isSearchMode: false, // Whether we're in search mode
};
// DOM elements
@@ -34,11 +35,6 @@ function initApp() {
// Setup event listeners
setupEventListeners();
// Load initial view
app.currentPath = '';
ui.updateBreadcrumb('');
loadFiles();
// Initialize file renderer if available
if (window.fileRenderer) {
console.log('Using optimized file renderer');
@@ -46,8 +42,26 @@ function initApp() {
console.log('Using standard file rendering');
}
// Check authentication
checkAuthentication();
// Wait for translations to load before checking authentication
if (window.i18n && window.i18n.isLoaded && window.i18n.isLoaded()) {
// Translations already loaded, proceed with authentication
checkAuthentication();
} else {
// Wait for translations to be loaded before proceeding
console.log('Waiting for translations to load...');
window.addEventListener('translationsLoaded', () => {
console.log('Translations loaded, proceeding with authentication');
checkAuthentication();
});
// Set a timeout as a fallback in case translations take too long
setTimeout(() => {
if (!window.i18n || !window.i18n.isLoaded || !window.i18n.isLoaded()) {
console.warn('Translations loading timeout, proceeding with authentication anyway');
checkAuthentication();
}
}, 3000); // 3 second timeout
}
}
/**
@@ -68,6 +82,7 @@ function cacheElements() {
elements.actionsBar = document.querySelector('.actions-bar');
elements.navItems = document.querySelectorAll('.nav-item');
elements.trashBtn = document.querySelector('.nav-item:nth-child(5)'); // The trash nav item
elements.searchInput = document.querySelector('.search-container input');
}
/**
@@ -77,6 +92,30 @@ function setupEventListeners() {
// Set up drag and drop
ui.setupDragAndDrop();
// Search input
elements.searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const query = elements.searchInput.value.trim();
if (query) {
performSearch(query);
} else if (app.isSearchMode) {
// If search is empty and we're in search mode, return to normal view
app.isSearchMode = false;
app.currentPath = '';
ui.updateBreadcrumb('');
loadFiles();
}
}
});
// Search button
document.getElementById('search-button').addEventListener('click', () => {
const query = elements.searchInput.value.trim();
if (query) {
performSearch(query);
}
});
// Upload button
elements.uploadBtn.addEventListener('click', () => {
elements.dropzone.style.display = elements.dropzone.style.display === 'none' ? 'block' : 'none';
@@ -328,12 +367,17 @@ async function loadTrashItems() {
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="files.original_location">Ubicación original</div>
<div data-i18n="files.deleted_date">Fecha eliminación</div>
<div data-i18n="files.actions">Acciones</div>
<div data-i18n="trash.original_location">Ubicación original</div>
<div data-i18n="trash.deleted_date">Fecha eliminación</div>
<div data-i18n="trash.actions">Acciones</div>
</div>
`;
// Translate the header if i18n is available
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
}
// Update breadcrumb for trash
ui.updateBreadcrumb(window.i18n ? window.i18n.t('nav.trash') : 'Papelera');
@@ -394,10 +438,10 @@ function addTrashItemToView(item) {
<div class="file-name">${item.name}</div>
<div class="file-info">${typeLabel} - ${formattedDate}</div>
<div class="trash-actions">
<button class="btn-restore" title="Restaurar">
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restaurar'}">
<i class="fas fa-undo"></i>
</button>
<button class="btn-delete" title="Eliminar permanentemente">
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Eliminar permanentemente'}">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -438,10 +482,10 @@ function addTrashItemToView(item) {
<div class="path-cell">${item.original_path || '--'}</div>
<div class="date-cell">${formattedDate}</div>
<div class="actions-cell">
<button class="btn-restore" title="Restaurar">
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restaurar'}">
<i class="fas fa-undo"></i>
</button>
<button class="btn-delete" title="Eliminar permanentemente">
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Eliminar permanentemente'}">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -465,11 +509,70 @@ function addTrashItemToView(item) {
elements.filesListView.appendChild(listElement);
}
/**
* Perform search with the given query
* @param {string} query - Search query
*/
async function performSearch(query) {
console.log(`Performing search for: "${query}"`);
try {
// Update UI to indicate search mode
app.isSearchMode = true;
// Set breadcrumb for search
ui.updateBreadcrumb(`Búsqueda: "${query}"`);
// Prepare search options
const options = {
recursive: true, // Search in all subfolders
limit: 100 // Limit results for performance
};
// Always restrict search to the user's current folder context
// This ensures users can't search outside their personal folder
if (!app.isTrashView) {
// If we're in a subfolder, search from there, otherwise use the user's home folder
options.folder_id = app.currentPath;
// Always include folder_id even if it's the root of user's home folder
// so user cannot search outside their allowed scope
if (!options.folder_id || options.folder_id === '') {
// Fall back to user's home folder - we should never be here
// because findUserHomeFolder should have set app.currentPath
console.warn("Search without folder_id - this shouldn't happen with proper user context");
// Try to get folder from localStorage if available
const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
if (userData.username) {
console.log("Retrieving home folder for user before search");
await findUserHomeFolder(userData.username);
options.folder_id = app.currentPath;
}
}
}
console.log(`Searching with options:`, options);
// Perform the search
const searchResults = await window.search.searchFiles(query, options);
// Display search results
window.search.displaySearchResults(searchResults);
} catch (error) {
console.error('Search error:', error);
window.ui.showNotification('Error', 'Error al realizar la búsqueda');
}
}
// Expose needed functions to global scope
window.app = app;
window.loadFiles = loadFiles;
window.loadTrashItems = loadTrashItems;
window.formatFileSize = formatFileSize;
window.performSearch = performSearch;
// Set up global selectFolder function for navigation
window.selectFolder = (id, name) => {
@@ -479,7 +582,7 @@ window.selectFolder = (id, name) => {
};
/**
* Check if user is authenticated
* Check if user is authenticated and load user's home folder
*/
function checkAuthentication() {
// Nombres de variables según auth.js
@@ -505,6 +608,88 @@ function checkAuthentication() {
if (userAvatar) {
userAvatar.textContent = userInitials;
}
// Find and load the user's home folder
findUserHomeFolder(userData.username);
} else {
// If no user data, fallback to standard load
app.currentPath = '';
ui.updateBreadcrumb('');
loadFiles();
}
}
/**
* Find the user's home folder and load it
* @param {string} username - The current user's username
*/
async function findUserHomeFolder(username) {
try {
console.log("Finding home folder for user:", username);
// First, load all folders at the root
const response = await fetch('/api/folders');
if (!response.ok) {
throw new Error(`Error loading folders: ${response.status}`);
}
const folders = await response.json();
const folderList = Array.isArray(folders) ? folders : [];
// Look for a folder with a name pattern that matches the user's home folder
// Typically named "Mi Carpeta - username"
const homeFolderPattern = `Mi Carpeta - ${username}`;
let homeFolder = folderList.find(folder => folder.name === homeFolderPattern);
// If exact match not found, try a more flexible match
if (!homeFolder) {
homeFolder = folderList.find(folder =>
folder.name.toLowerCase().includes(username.toLowerCase()) ||
folder.name.startsWith('Mi Carpeta -')
);
}
if (homeFolder) {
console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`);
// Store the home folder ID and name in the app state
// This is used for breadcrumb navigation and restricting user access
app.userHomeFolderId = homeFolder.id;
app.userHomeFolderName = homeFolder.name;
// Set this as the current path and load its contents
app.currentPath = homeFolder.id;
ui.updateBreadcrumb(homeFolder.name);
loadFiles();
} else {
console.warn("Could not find user's home folder, fallback to first folder or root");
// If we can't find a specific home folder but there are folders,
// use the first folder as the user's home
if (folderList.length > 0) {
const fallbackFolder = folderList[0];
console.log(`Using first folder as fallback: ${fallbackFolder.name} (${fallbackFolder.id})`);
app.userHomeFolderId = fallbackFolder.id;
app.userHomeFolderName = fallbackFolder.name;
app.currentPath = fallbackFolder.id;
ui.updateBreadcrumb(fallbackFolder.name);
loadFiles();
} else {
// No folders at all - this is an edge case
console.warn("No folders found, using root");
app.currentPath = '';
ui.updateBreadcrumb('');
loadFiles();
}
}
} catch (error) {
console.error('Error finding user home folder:', error);
// Fall back to loading root in case of error
app.currentPath = '';
ui.updateBreadcrumb('');
loadFiles();
}
}
+2 -1
View File
@@ -419,7 +419,8 @@ const fileOps = {
* @returns {Promise<boolean>} - Éxito de la operación
*/
async emptyTrash() {
if (!confirm('¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos y no se puede deshacer.')) {
const confirmMsg = window.i18n ? window.i18n.t('trash.empty_confirm') : '¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos.';
if (!confirm(confirmMsg)) {
return false;
}
+22 -3
View File
@@ -227,14 +227,33 @@ function getSupportedLocales() {
return [...supportedLocales];
}
// Flag to track if translations are loaded
let translationsLoaded = false;
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initI18n);
document.addEventListener('DOMContentLoaded', async () => {
await initI18n();
translationsLoaded = true;
// Dispatch an event when translations are fully loaded
window.dispatchEvent(new Event('translationsLoaded'));
});
// Improved t function with fallback for early calls
function safeT(key, params = {}) {
if (!translationsLoaded) {
console.warn(`Translations for ${currentLocale} not loaded yet`);
// Return a default value or the key depending on context
return key.split('.').pop() || key;
}
return t(key, params);
}
// Export functions for use in other modules
window.i18n = {
t,
t: safeT,
setLocale,
getCurrentLocale,
getSupportedLocales,
translatePage
translatePage,
isLoaded: () => translationsLoaded
};
+196
View File
@@ -0,0 +1,196 @@
/**
* OxiCloud - Search Module
* This file handles search functionality for files and folders
*/
const search = {
/**
* Perform a basic search using query string
* @param {string} query - Search query
* @param {Object} options - Additional search options
* @returns {Promise<Object>} - Search results
*/
async searchFiles(query, options = {}) {
try {
// Prepare search parameters
const params = new URLSearchParams();
params.append('query', query);
// Add optional parameters
if (options.folder_id) params.append('folder_id', options.folder_id);
if (options.recursive !== undefined) params.append('recursive', options.recursive);
if (options.file_types) params.append('type', options.file_types);
if (options.min_size) params.append('min_size', options.min_size);
if (options.max_size) params.append('max_size', options.max_size);
if (options.created_after) params.append('created_after', options.created_after);
if (options.created_before) params.append('created_before', options.created_before);
if (options.modified_after) params.append('modified_after', options.modified_after);
if (options.modified_before) params.append('modified_before', options.modified_before);
if (options.limit) params.append('limit', options.limit);
if (options.offset) params.append('offset', options.offset);
// Create search URL
const url = `/api/search?${params.toString()}`;
console.log(`Performing search with URL: ${url}`);
// Perform the search request
const response = await fetch(url);
if (response.ok) {
return await response.json();
} else {
let errorText = '';
try {
const errorJson = await response.json();
errorText = errorJson.error || response.statusText;
} catch (e) {
errorText = response.statusText;
}
console.error(`Search error: ${errorText}`);
throw new Error(`Search failed: ${errorText}`);
}
} catch (error) {
console.error('Error performing search:', error);
window.ui.showNotification('Error', 'Error al realizar la búsqueda');
return { files: [], folders: [], total_count: 0 };
}
},
/**
* Perform advanced search with multiple criteria
* @param {Object} criteria - Search criteria
* @returns {Promise<Object>} - Search results
*/
async advancedSearch(criteria) {
try {
console.log('Performing advanced search with criteria:', criteria);
// Use POST endpoint for advanced search
const response = await fetch('/api/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(criteria)
});
if (response.ok) {
return await response.json();
} else {
let errorText = '';
try {
const errorJson = await response.json();
errorText = errorJson.error || response.statusText;
} catch (e) {
errorText = response.statusText;
}
console.error(`Advanced search error: ${errorText}`);
throw new Error(`Advanced search failed: ${errorText}`);
}
} catch (error) {
console.error('Error performing advanced search:', error);
window.ui.showNotification('Error', 'Error al realizar la búsqueda avanzada');
return { files: [], folders: [], total_count: 0 };
}
},
/**
* Display search results in the UI
* @param {Object} results - Search results object with files and folders arrays
*/
displaySearchResults(results) {
// Get the files grid and list view elements
const filesGrid = document.getElementById('files-grid');
const filesListView = document.getElementById('files-list-view');
// Clear existing content
filesGrid.innerHTML = '';
filesListView.innerHTML = `
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="files.size">Tamaño</div>
<div data-i18n="files.modified">Modificado</div>
</div>
`;
// Add search results header
const searchHeader = document.createElement('div');
searchHeader.className = 'search-results-header';
searchHeader.innerHTML = `
<h3>Resultados de búsqueda (${results.total_count || (results.files.length + results.folders.length)})</h3>
<button class="btn btn-secondary" id="clear-search-btn">
<i class="fas fa-times"></i> Limpiar búsqueda
</button>
`;
filesGrid.appendChild(searchHeader);
// Add event listener to clear search button
const clearSearchBtn = document.getElementById('clear-search-btn');
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', () => {
// Clear search input
document.querySelector('.search-container input').value = '';
// Load regular files view
window.app.currentPath = '';
window.ui.updateBreadcrumb('');
window.loadFiles();
});
}
// If no results, show empty state
if (results.files.length === 0 && results.folders.length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<i class="fas fa-search" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
<p>No se encontraron resultados para esta búsqueda</p>
`;
filesGrid.appendChild(emptyState);
return;
}
// Process folders
results.folders.forEach(folder => {
window.ui.addFolderToView(folder);
});
// Process files
results.files.forEach(file => {
window.ui.addFileToView(file);
});
// Update file icons
window.ui.updateFileIcons();
},
/**
* Clear the search cache on the server
* @returns {Promise<boolean>} - Success status
*/
async clearSearchCache() {
try {
const response = await fetch('/api/search/cache', {
method: 'DELETE'
});
if (response.ok) {
window.ui.showNotification('Caché limpiada', 'Caché de búsqueda limpiada correctamente');
return true;
} else {
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda');
return false;
}
} catch (error) {
console.error('Error clearing search cache:', error);
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda');
return false;
}
}
};
// Expose the search module globally
window.search = search;
+44 -9
View File
@@ -190,19 +190,54 @@ const ui = {
updateBreadcrumb(folderName) {
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb.innerHTML = '';
// Get user info to help determine home folder
const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
const username = userData.username || '';
// Create the home item - for users, this is their personal folder
const homeItem = document.createElement('span');
homeItem.className = 'breadcrumb-item';
homeItem.textContent = window.i18n ? window.i18n.t('breadcrumb.home') : 'Home';
homeItem.addEventListener('click', () => {
window.app.currentPath = '';
this.updateBreadcrumb('');
window.loadFiles();
});
// Helper function to safely get translation text
const getTranslatedText = (key, defaultValue) => {
if (!window.i18n || !window.i18n.t) return defaultValue;
return window.i18n.t(key);
};
// Set appropriate text for home item
if (username && folderName && folderName.includes(username)) {
// If the current folder is the user's home folder, label it as "Home"
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
} else if (folderName && folderName.startsWith('Mi Carpeta')) {
// If the current folder is another user's home folder or a special folder, use its name
homeItem.textContent = folderName;
} else {
// Default - use "Home" label
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
// For searching, we might have a custom breadcrumb text
if (folderName && folderName.startsWith('Búsqueda:')) {
// We're in search mode - don't add click handler
breadcrumb.appendChild(homeItem);
return;
}
}
// Add click handler - but only if we have a user home folder to return to
if (window.app.userHomeFolderId) {
homeItem.addEventListener('click', () => {
window.app.currentPath = window.app.userHomeFolderId;
this.updateBreadcrumb(window.app.userHomeFolderName || 'Home');
window.loadFiles();
});
}
breadcrumb.appendChild(homeItem);
if (folderName) {
// If we have a subfolder, add it to the breadcrumb
if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Búsqueda:')) {
const separator = document.createElement('span');
separator.className = 'breadcrumb-separator';
separator.textContent = '>';
+10
View File
@@ -67,6 +67,16 @@
"breadcrumb": {
"home": "Home"
},
"trash": {
"empty_trash": "Empty Trash",
"empty_state": "Trash is empty",
"original_location": "Original location",
"deleted_date": "Deletion date",
"actions": "Actions",
"restore": "Restore",
"delete_permanently": "Delete permanently",
"empty_confirm": "Are you sure you want to empty the trash? This will permanently delete all items."
},
"auth": {
"login_title": "Sign in",
"username": "Username",
+10
View File
@@ -67,6 +67,16 @@
"breadcrumb": {
"home": "Inicio"
},
"trash": {
"empty_trash": "Vaciar papelera",
"empty_state": "La papelera está vacía",
"original_location": "Ubicación original",
"deleted_date": "Fecha de eliminación",
"actions": "Acciones",
"restore": "Restaurar",
"delete_permanently": "Eliminar permanentemente",
"empty_confirm": "¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos."
},
"auth": {
"login_title": "Iniciar sesión",
"username": "Usuario",