188 lines
6.2 KiB
JavaScript
188 lines
6.2 KiB
JavaScript
/**
|
|
* OxiCloud - Favorites Module (server-authoritative)
|
|
*
|
|
* Source of truth: GET /api/favorites/resources (cursor-paginated).
|
|
* The in-memory cache (`_cache`) is a Set of "type:id" keys that keeps
|
|
* `isFavorite()` synchronous so star icons are painted without a round-trip.
|
|
*
|
|
* Display is handled by `views/favorites/favoritesView.js`.
|
|
*/
|
|
|
|
import { ui } from '../../app/ui.js';
|
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
|
import { i18n } from '../../core/i18n.js';
|
|
import { fetchFavoritesPage } from '../../model/favoritesModel.js';
|
|
|
|
const favorites = {
|
|
/**
|
|
* Set of "type:id" cache keys. A Set is enough — we only need O(1) lookups.
|
|
* @type {Set<string>}
|
|
*/
|
|
_cache: new Set(),
|
|
|
|
/** Whether the initial fetch from the server has completed. */
|
|
_ready: false,
|
|
|
|
// ───────────────────── helpers ─────────────────────
|
|
|
|
_authHeaders() {
|
|
return { ...getCsrfHeaders() };
|
|
},
|
|
|
|
/**
|
|
* @param {string} id
|
|
* @param {string} type
|
|
* @returns {string}
|
|
*/
|
|
_cacheKey(id, type) {
|
|
return `${type}:${id}`;
|
|
},
|
|
|
|
// ───────────────────── lifecycle ─────────────────────
|
|
|
|
/**
|
|
* Initialise the module: fetch the full favorites list from the server and
|
|
* populate the in-memory cache. Called from navigation.js every time the
|
|
* Favorites section is entered (non-blocking — the view loads in parallel).
|
|
*/
|
|
async init() {
|
|
await this._fetchFromServer();
|
|
},
|
|
|
|
/**
|
|
* Fetch all favorited resource IDs from the server and rebuild the cache.
|
|
* Paginates through `GET /api/favorites/resources` until exhausted.
|
|
*/
|
|
async _fetchFromServer() {
|
|
try {
|
|
this._cache.clear();
|
|
|
|
let cursor = /** @type {string|undefined} */ (undefined);
|
|
|
|
// Paginate with the max page size so most users need only one request.
|
|
while (true) {
|
|
const data = await fetchFavoritesPage({ limit: 200, cursor, orderBy: 'name' });
|
|
for (const item of data.items) {
|
|
// `item.resource.id` works for both FileItem (id) and FolderItem (id).
|
|
const r = /** @type {Record<string, string>} */ (/** @type {unknown} */ (item.resource));
|
|
this._cache.add(this._cacheKey(r.id, item.resource_type));
|
|
}
|
|
if (!data.next_cursor) break;
|
|
cursor = data.next_cursor;
|
|
}
|
|
|
|
this._ready = true;
|
|
console.log(`Favorites cache loaded: ${this._cache.size} items`);
|
|
} catch (err) {
|
|
console.error('Error fetching favorites:', err);
|
|
}
|
|
},
|
|
|
|
// ───────────────────── public API ─────────────────────
|
|
|
|
/**
|
|
* Synchronous check used by the rendering layer to paint star icons.
|
|
* @param {string} id
|
|
* @param {string} type
|
|
* @returns {boolean}
|
|
*/
|
|
isFavorite(id, type) {
|
|
return this._cache.has(this._cacheKey(id, type));
|
|
},
|
|
|
|
/**
|
|
* Add an item to favourites (server-first, then update local cache).
|
|
* @param {string} id
|
|
* @param {string} name
|
|
* @param {string} type
|
|
* @param {string | null} _parentId - unused, kept for call-site compatibility
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async addToFavorites(id, name, type, _parentId) {
|
|
try {
|
|
const response = await fetch(`/api/favorites/${type}/${id}`, {
|
|
method: 'POST',
|
|
headers: this._authHeaders()
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server returned ${response.status}`);
|
|
}
|
|
|
|
// Optimistically update local cache without a full re-fetch.
|
|
this._cache.add(this._cacheKey(id, type));
|
|
|
|
if (ui?.showNotification) {
|
|
ui.showNotification(i18n.t('favorites.added_title'), `"${name}" ${i18n.t('favorites.added_msg')}`);
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error adding to favorites:', error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove an item from favourites (server-first, then update local cache).
|
|
* @param {string} id
|
|
* @param {string} type
|
|
* @param {string} [name] - Display name for the notification; falls back to `id`.
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async removeFromFavorites(id, type, name = id) {
|
|
try {
|
|
const response = await fetch(`/api/favorites/${type}/${id}`, {
|
|
method: 'DELETE',
|
|
headers: this._authHeaders()
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server returned ${response.status}`);
|
|
}
|
|
|
|
this._cache.delete(this._cacheKey(id, type));
|
|
|
|
if (ui?.showNotification) {
|
|
ui.showNotification(i18n.t('favorites.removed_title'), `"${name}" ${i18n.t('favorites.removed_msg')}`);
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error removing from favorites:', error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Batch-add multiple items to favourites in a single server call.
|
|
* Re-fetches the cache after success to stay consistent.
|
|
*
|
|
* @param {Array<{item_id: string, item_type: string}>} items
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async batchAdd(items) {
|
|
try {
|
|
const response = await fetch('/api/favorites/batch', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...this._authHeaders() },
|
|
body: JSON.stringify({ items })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server returned ${response.status}`);
|
|
}
|
|
|
|
// Re-fetch the full cache so the Set reflects the latest server state.
|
|
await this._fetchFromServer();
|
|
|
|
return true;
|
|
} catch (err) {
|
|
console.error('Error in batchAdd:', err);
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
export { favorites };
|