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:
@@ -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) ──────────────────────────────────
|
||||||
|
|||||||
@@ -93,3 +93,40 @@
|
|||||||
.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
@@ -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`);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user