Files
Oxicloud/static/js/features/library/favorites.js
T

297 lines
11 KiB
JavaScript
Raw Normal View History

2025-04-02 03:43:44 +02:00
/**
* OxiCloud - Favorites Module (server-authoritative)
*
* Source of truth: GET /api/favorites (enriched with name/size/mime via SQL JOIN).
* Local in-memory cache (`_cache`) keeps `isFavorite()` synchronous for the
* rendering path so star icons can be painted without a round-trip.
2025-04-02 03:43:44 +02:00
*/
import { ui } from '../../app/ui.js';
import { ResourceListComponent } from '../../components/resourceList.js';
import { getCsrfHeaders } from '../../core/csrf.js';
import { i18n } from '../../core/i18n.js';
import { batchToolbar } from '../files/batchToolbar.js';
import * as pathTooltip from '../pathTooltip.js';
2026-05-07 23:40:02 +02:00
/** @import {FavoriteItem, FileItem, FolderItem} from '../../core/types.js' */
2025-04-02 03:43:44 +02:00
const favorites = {
2026-05-07 23:40:02 +02:00
/** @type {Map<string, FavoriteItem>} key = "file:<id>" | "folder:<id>" */
_cache: new Map(),
/** Whether the initial fetch from the server has completed */
_ready: false,
2026-05-26 23:21:58 +02:00
/** @type {ResourceListComponent|null} */
_component: null,
// ───────────────────── helpers ─────────────────────
_authHeaders() {
return { ...getCsrfHeaders() };
2025-04-02 03:43:44 +02:00
},
2026-05-07 23:40:02 +02:00
/**
* @param {string} id
* @param {string} type
*/
_cacheKey(id, type) {
return `${type}:${id}`;
},
/**
* Replace the entire in-memory cache from an array of FavoriteItemDto
* objects (as returned by the batch endpoint). Avoids an extra
* GET /api/favorites round-trip.
2026-05-07 23:40:02 +02:00
* @param {any[]} items
*/
_replaceCacheFromResponse(items) {
this._cache.clear();
for (const item of items) {
this._cache.set(this._cacheKey(item.item_id, item.item_type), item);
}
this._ready = true;
console.log(`Favorites cache replaced from response: ${this._cache.size} items`);
},
// ───────────────────── lifecycle ─────────────────────
2025-04-02 03:43:44 +02:00
/**
* Initialise the module: fetch the full list from the server and populate
* the in-memory cache. Called once from app.js on startup.
2025-04-02 03:43:44 +02:00
*/
async init() {
console.log('Initializing favorites module (server-authoritative)');
await this._fetchFromServer();
2025-04-02 03:43:44 +02:00
},
2025-04-02 03:43:44 +02:00
/**
* Fetch favourites from the backend and rebuild the cache.
2025-04-02 03:43:44 +02:00
*/
async _fetchFromServer() {
2025-04-02 03:43:44 +02:00
try {
const response = await fetch('/api/favorites', {
headers: this._authHeaders()
2025-04-02 03:43:44 +02:00
});
2025-04-02 03:43:44 +02:00
if (!response.ok) {
console.warn(`Favorites API returned ${response.status}`);
return;
2025-04-02 03:43:44 +02:00
}
2026-05-07 23:40:02 +02:00
/** @type {FavoriteItem[]} */
const items = await response.json();
this._cache.clear();
for (const item of items) {
this._cache.set(this._cacheKey(item.item_id, item.item_type), item);
2025-04-02 03:43:44 +02:00
}
this._ready = true;
console.log(`Favorites cache loaded: ${this._cache.size} items`);
} catch (err) {
console.error('Error fetching favorites:', err);
2025-04-02 03:43:44 +02:00
}
},
// ───────────────────── public API ─────────────────────
2025-04-02 03:43:44 +02:00
/**
* Synchronous check used by ui.js to paint star icons.
2026-05-07 23:40:02 +02:00
* @param {string} id
* @param {string} type
2025-04-02 03:43:44 +02:00
*/
isFavorite(id, type) {
return this._cache.has(this._cacheKey(id, type));
2025-04-02 03:43:44 +02:00
},
2025-04-02 03:43:44 +02:00
/**
* Add an item to favourites (server-first).
2026-05-07 23:40:02 +02:00
* @param {string} id
* @param {string} name
* @param {string} type
2026-05-26 23:21:58 +02:00
* @param {string | null} _parentId
2025-04-02 03:43:44 +02:00
*/
async addToFavorites(id, name, type, _parentId) {
2025-04-02 03:43:44 +02:00
try {
const response = await fetch(`/api/favorites/${type}/${id}`, {
method: 'POST',
headers: this._authHeaders()
2025-04-02 03:43:44 +02:00
});
2025-04-02 03:43:44 +02:00
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
// Refresh cache from server to get enriched data
await this._fetchFromServer();
// Notify user
if (ui?.showNotification) {
2026-04-26 00:22:49 +02:00
ui.showNotification(i18n.t('favorites.added_title'), `"${name}" ${i18n.t('favorites.added_msg')}`);
}
2025-04-02 03:43:44 +02:00
return true;
} catch (error) {
console.error('Error adding to favorites:', error);
2025-04-02 03:43:44 +02:00
return false;
}
},
2025-04-02 03:43:44 +02:00
/**
* Remove an item from favourites (server-first).
2026-05-07 23:40:02 +02:00
* @param {string} id
* @param {string} type
2025-04-02 03:43:44 +02:00
*/
async removeFromFavorites(id, type) {
2025-04-02 03:43:44 +02:00
try {
// Remember name for notification before removing from cache
const cached = this._cache.get(this._cacheKey(id, type));
const itemName = cached?.item_name || id;
2025-04-02 03:43:44 +02:00
const response = await fetch(`/api/favorites/${type}/${id}`, {
method: 'DELETE',
headers: this._authHeaders()
2025-04-02 03:43:44 +02:00
});
2025-04-02 03:43:44 +02:00
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
// Remove from local cache
this._cache.delete(this._cacheKey(id, type));
if (ui?.showNotification) {
2026-04-26 00:22:49 +02:00
ui.showNotification(i18n.t('favorites.removed_title'), `"${itemName}" ${i18n.t('favorites.removed_msg')}`);
2025-04-02 03:43:44 +02:00
}
2025-04-02 03:43:44 +02:00
return true;
} catch (error) {
console.error('Error removing from favorites:', error);
return false;
}
},
// ───────────────────── display ─────────────────────
2025-04-02 03:43:44 +02:00
/**
* Render the favourites view. All data comes from the in-memory cache
* (which was populated from the enriched backend response — zero extra
* fetches).
2025-04-02 03:43:44 +02:00
*/
async displayFavorites() {
try {
await this._fetchFromServer();
2026-05-26 23:21:58 +02:00
ui.resetFilesList();
batchToolbar.init();
2026-04-30 02:02:27 +02:00
ui.updateBreadcrumb();
if (this._cache.size === 0) {
ui.showError(`
<i class="fas fa-star empty-state-icon"></i>
<p>${i18n.t('favorites.empty_state')}</p>
<p>${i18n.t('favorites.empty_hint')}</p>
`);
2025-04-02 03:43:44 +02:00
return;
}
/** @type {Array<FileItem|FolderItem>} */
const items = [];
2026-05-07 23:40:02 +02:00
for (const item of this._cache.values()) {
2026-05-26 23:21:58 +02:00
// owner_id comes from the backend JOIN (actual file/folder owner)
if (item.item_type === 'folder') {
items.push(
2026-05-26 23:21:58 +02:00
/** @type {FolderItem} */ ({
2026-05-07 23:40:02 +02:00
id: item.item_id,
name: item.item_name || item.item_id,
parent_id: item.parent_id || '',
modified_at: item.modified_at || item.created_at,
path: item.item_path || '',
category: 'folder',
created_at: item.created_at,
icon_class: item.icon_class,
icon_special_class: item.icon_special_class,
owner_id: item.owner_id ?? '',
2026-05-07 23:40:02 +02:00
is_root: false
2026-05-26 23:21:58 +02:00
})
2026-05-07 23:40:02 +02:00
);
} else {
items.push(
2026-05-26 23:21:58 +02:00
/** @type {FileItem} */ ({
2026-05-07 23:40:02 +02:00
id: item.item_id,
name: item.item_name || item.item_id,
folder_id: item.parent_id || '',
mime_type: item.item_mime_type,
icon_class: item.icon_class,
icon_special_class: item.icon_special_class,
category: item.category,
size: item.item_size || 0,
size_formatted: item.size_formatted,
modified_at: item.modified_at || item.created_at,
path: item.item_path || '',
owner_id: item.owner_id ?? '',
2026-05-07 23:40:02 +02:00
created_at: item.created_at,
sort_date: item.created_at
2026-05-26 23:21:58 +02:00
})
2026-05-07 23:40:02 +02:00
);
2025-04-02 03:43:44 +02:00
}
}
const filesList = document.getElementById('files-list');
2026-05-26 23:21:58 +02:00
if (filesList) {
if (!this._component) {
this._component = new ResourceListComponent(/** @type {HTMLElement} */ (filesList), {
selectable: true,
showFavorite: true,
showOwner: true,
showShareBadge: true,
draggable: false,
showContextMenu: true,
itemModifierClass: 'favorite-item',
isFavorite: (id, type) => this.isFavorite(id, type),
onOpen: (item) => ui.openItem(item),
onFavoriteToggle: async (item) => {
const isFile = 'mime_type' in item;
const type = isFile ? 'file' : 'folder';
if (this.isFavorite(item.id, type)) {
await this.removeFromFavorites(item.id, type);
this._component?.setFavoriteVisualState(item.id, type, false);
} else {
await this.addToFavorites(item.id, item.name, type, null);
this._component?.setFavoriteVisualState(item.id, type, true);
2026-05-26 23:21:58 +02:00
}
},
onContextMenu: (item, e) => ui.showContextMenuForItem(item, e),
onSelectionChange: (selectedItems) => {
batchToolbar._selected.clear();
for (const sel of selectedItems) {
const isFile = 'mime_type' in sel;
batchToolbar._selected.set(sel.id, {
id: sel.id,
name: sel.name,
type: isFile ? 'file' : 'folder',
parentId: isFile ? /** @type {FileItem} */ (sel).folder_id || '' : /** @type {FolderItem} */ (sel).parent_id || ''
});
}
batchToolbar._syncUI();
2026-05-26 23:21:58 +02:00
}
});
2026-05-26 23:21:58 +02:00
}
batchToolbar.setActiveComponent(this._component);
this._component.render(items);
2026-05-26 23:21:58 +02:00
pathTooltip.init(filesList);
}
await this._component?.resolveOwnerCells();
2025-04-02 03:43:44 +02:00
} catch (error) {
console.error('Error displaying favorites:', error);
if (ui?.showNotification) {
ui.showNotification('Error', 'Error loading favorite items');
2025-04-02 03:43:44 +02:00
}
}
}
};
export { favorites };