feat: fix favorites display and add star indicator on favorited items

- Add ?metadata=true support to GET /api/files/{id} to return JSON metadata
  instead of binary content (was the root cause of favorites not loading)
- Fix favorites loadFileDetails to use metadata endpoint with auth headers
- Add star icon on favorited files/folders in grid view (top-left corner)
- Add star icon on favorited files/folders in list view (next to name)
- Refresh file view when toggling favorites so star appears/disappears
- Add CSS styles for .favorite-star and .favorite-star-inline
This commit is contained in:
Dionisio
2026-02-13 09:32:16 +01:00
parent fb65d1976b
commit 5c53d94c0c
4 changed files with 83 additions and 23 deletions
@@ -270,6 +270,20 @@ impl FileHandler {
} }
}; };
// ── Metadata-only request ────────────────────────────────────
if params.get("metadata").map_or(false, |v| v == "true" || v == "1") {
return (StatusCode::OK, Json(serde_json::json!({
"id": file_dto.id,
"name": file_dto.name,
"path": file_dto.path,
"size": file_dto.size,
"mime_type": file_dto.mime_type,
"folder_id": file_dto.folder_id,
"created_at": file_dto.created_at,
"modified_at": file_dto.modified_at
}))).into_response();
}
let etag = format!("\"{}-{}\"", id, file_dto.modified_at); let etag = format!("\"{}-{}\"", id, file_dto.modified_at);
// ── ETag (304 Not Modified) ────────────────────────────────── // ── ETag (304 Not Modified) ──────────────────────────────────
+37
View File
@@ -92,4 +92,41 @@
.favorite-indicator.active { .favorite-indicator.active {
animation: favorite-pulse 0.3s ease; animation: favorite-pulse 0.3s ease;
}
/* ── Star badge on file/folder cards (normal view) ── */
/* Grid view: star in top-left corner */
.file-card .favorite-star {
position: absolute;
top: 8px;
left: 8px;
font-size: 14px;
color: #ffc107;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.15));
z-index: 5;
pointer-events: none;
[dir='rtl'] & {
left: unset;
right: 8px;
}
}
.file-card .favorite-star.active i {
animation: favorite-pulse 0.3s ease;
}
/* List view: small star next to the name */
.favorite-star-inline {
color: #ffc107;
font-size: 11px;
margin-left: 6px;
vertical-align: middle;
filter: drop-shadow(0 0 1px rgba(255, 193, 7, 0.4));
[dir='rtl'] & {
margin-left: 0;
margin-right: 6px;
}
} }
+20 -23
View File
@@ -234,6 +234,11 @@ const favorites = {
`"${name}" 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; return true;
} catch (error) { } catch (error) {
console.error('Error adding to favorites:', error); console.error('Error adding to favorites:', error);
@@ -275,6 +280,12 @@ const favorites = {
`"${item.name}" 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 true;
} }
@@ -371,7 +382,9 @@ const favorites = {
*/ */
async loadFolderDetails(favorite, filesGrid, filesListView) { async loadFolderDetails(favorite, filesGrid, filesListView) {
try { try {
const response = await fetch(`/api/folders/${favorite.id}`); const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const response = await fetch(`/api/folders/${favorite.id}`, { headers });
if (response.ok) { if (response.ok) {
const folder = await response.json(); const folder = await response.json();
@@ -398,30 +411,14 @@ const favorites = {
*/ */
async loadFileDetails(favorite, filesGrid, filesListView) { async loadFileDetails(favorite, filesGrid, filesListView) {
try { try {
const response = await fetch(`/api/files/${favorite.id}`); 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) { if (response.ok) {
// Check if the response is JSON or binary data like a PDF const file = await response.json();
const contentType = response.headers.get('content-type'); // Create UI element with favorite indicator
this.createFavoriteFileElement(file, filesGrid, filesListView);
if (contentType && contentType.includes('application/json')) {
const file = await response.json();
// Create UI element with favorite indicator
this.createFavoriteFileElement(file, filesGrid, filesListView);
} else {
// For non-JSON responses (like PDFs or other binary files)
// Create a simplified file element with minimal information
const file = {
id: favorite.id,
name: favorite.name || `File ${favorite.id}`,
mime_type: contentType || 'application/octet-stream',
size: 0, // We don't know the size from this response
modified_at: Math.floor(Date.now() / 1000) // Current time in seconds
};
this.createFavoriteFileElement(file, filesGrid, filesListView);
}
} else if (response.status === 404) { } else if (response.status === 404) {
// File not found, might be deleted // File not found, might be deleted
console.log(`Favorite file ${favorite.id} not found, removing from favorites`); console.log(`Favorite file ${favorite.id} not found, removing from favorites`);
+12
View File
@@ -666,9 +666,14 @@ const ui = {
folderGridElement.dataset.folderId = folder.id; folderGridElement.dataset.folderId = folder.id;
folderGridElement.dataset.folderName = folder.name; folderGridElement.dataset.folderName = folder.name;
folderGridElement.dataset.parentId = folder.parent_id || ""; folderGridElement.dataset.parentId = folder.parent_id || "";
// Check if folder is a favorite
const isFolderFav = window.favorites && window.favorites.isFavorite(folder.id, 'folder');
folderGridElement.innerHTML = ` folderGridElement.innerHTML = `
<div class="file-card-checkbox"><i class="fas fa-check"></i></div> <div class="file-card-checkbox"><i class="fas fa-check"></i></div>
<button class="file-card-more"><i class="fas fa-ellipsis-v"></i></button> <button class="file-card-more"><i class="fas fa-ellipsis-v"></i></button>
${isFolderFav ? '<div class="favorite-star active"><i class="fas fa-star"></i></div>' : ''}
<div class="file-icon folder-icon"> <div class="file-icon folder-icon">
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
</div> </div>
@@ -813,6 +818,7 @@ const ui = {
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
</div> </div>
<span>${folder.name}</span> <span>${folder.name}</span>
${isFolderFav ? '<i class="fas fa-star favorite-star-inline"></i>' : ''}
</div> </div>
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div> <div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div>
<div class="size-cell">--</div> <div class="size-cell">--</div>
@@ -927,9 +933,14 @@ const ui = {
// Grid view element // Grid view element
const fileGridElement = document.createElement('div'); const fileGridElement = document.createElement('div');
fileGridElement.className = 'file-card'; fileGridElement.className = 'file-card';
// Check if file is a favorite
const isFileFav = window.favorites && window.favorites.isFavorite(file.id, 'file');
fileGridElement.innerHTML = ` fileGridElement.innerHTML = `
<div class="file-card-checkbox"><i class="fas fa-check"></i></div> <div class="file-card-checkbox"><i class="fas fa-check"></i></div>
<button class="file-card-more"><i class="fas fa-ellipsis-v"></i></button> <button class="file-card-more"><i class="fas fa-ellipsis-v"></i></button>
${isFileFav ? '<div class="favorite-star active"><i class="fas fa-star"></i></div>' : ''}
<div class="file-icon"> <div class="file-icon">
<i class="${iconClass}"></i> <i class="${iconClass}"></i>
</div> </div>
@@ -1037,6 +1048,7 @@ const ui = {
<i class="${iconClass}"></i> <i class="${iconClass}"></i>
</div> </div>
<span>${file.name}</span> <span>${file.name}</span>
${isFileFav ? '<i class="fas fa-star favorite-star-inline"></i>' : ''}
</div> </div>
<div class="type-cell">${typeLabel}</div> <div class="type-cell">${typeLabel}</div>
<div class="size-cell">${fileSize}</div> <div class="size-cell">${fileSize}</div>