2026-03-22 22:55:42 +01:00
|
|
|
|
// @ts-check
|
|
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* OxiCloud – Files section view.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Orchestrates the main Files section:
|
|
|
|
|
|
* - Data fetching via `filesModel`
|
|
|
|
|
|
* - Rendering via a `ResourceListComponent` instance
|
|
|
|
|
|
* - Drag-and-drop initialisation (delegated to `ui.initDragDrop`)
|
|
|
|
|
|
*
|
|
|
|
|
|
* Exports `loadFiles` (navigation & deep-link entry-point) and `addItem`
|
|
|
|
|
|
* (post-upload / post-create optimistic UI updates used by fileOperations
|
|
|
|
|
|
* and search).
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { ResourceListComponent } from '../components/resourceList.js';
|
2026-04-13 15:09:10 +02:00
|
|
|
|
import { i18n } from '../core/i18n.js';
|
2026-05-26 22:59:43 +02:00
|
|
|
|
import { batchToolbar } from '../features/files/batchToolbar.js';
|
2026-05-27 00:32:10 +02:00
|
|
|
|
import { inlineViewer } from '../features/files/inlineViewer.js';
|
2026-05-27 22:51:15 +02:00
|
|
|
|
import { favorites } from '../features/library/favorites.js';
|
|
|
|
|
|
import { fetchListing, rebuildBreadCrumb } from '../model/filesModel.js';
|
|
|
|
|
|
import { grants } from '../model/grants.js';
|
2026-04-13 15:09:10 +02:00
|
|
|
|
import { resolveHomeFolder } from './authSession.js';
|
|
|
|
|
|
import { updateHistory } from './main.js';
|
|
|
|
|
|
import { app } from './state.js';
|
|
|
|
|
|
import { ui } from './ui.js';
|
|
|
|
|
|
import { uiNotifications } from './uiNotifications.js';
|
|
|
|
|
|
|
2026-05-07 23:40:02 +02:00
|
|
|
|
/** @import {FileItem, FolderItem} from '../core/types.js' */
|
2026-04-13 15:09:10 +02:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
/** @type {ResourceListComponent|null} */
|
|
|
|
|
|
let _component = null;
|
|
|
|
|
|
|
|
|
|
|
|
/** Guard against concurrent `loadFiles` calls. */
|
|
|
|
|
|
let _loading = false;
|
2026-03-22 22:55:42 +01:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-27 22:51:15 +02:00
|
|
|
|
* Return (creating on first call) the `ResourceListComponent` bound to
|
|
|
|
|
|
* `#files-list`. The element must already be in the DOM.
|
|
|
|
|
|
* @returns {ResourceListComponent|null}
|
2026-02-20 12:27:52 +01:00
|
|
|
|
*/
|
2026-05-27 22:51:15 +02:00
|
|
|
|
function _ensureComponent() {
|
|
|
|
|
|
const filesList = document.getElementById('files-list');
|
|
|
|
|
|
if (!filesList) return null;
|
|
|
|
|
|
|
|
|
|
|
|
if (!_component) {
|
|
|
|
|
|
_component = new ResourceListComponent(/** @type {HTMLElement} */ (filesList), {
|
|
|
|
|
|
selectable: true,
|
|
|
|
|
|
showFavorite: true,
|
|
|
|
|
|
showOwner: true,
|
|
|
|
|
|
showShareBadge: true,
|
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
showContextMenu: true,
|
|
|
|
|
|
isFavorite: (id, type) => favorites.isFavorite(id, type),
|
|
|
|
|
|
isShared: (id, type) => grants.getOutgoingGrantsFor(type, id).length > 0,
|
|
|
|
|
|
onOpen: (item) => ui.openItem(item),
|
|
|
|
|
|
onFavoriteToggle: async (item) => {
|
|
|
|
|
|
const isFile = 'mime_type' in item;
|
|
|
|
|
|
const type = isFile ? 'file' : 'folder';
|
|
|
|
|
|
if (favorites.isFavorite(item.id, type)) {
|
|
|
|
|
|
await favorites.removeFromFavorites(item.id, type);
|
|
|
|
|
|
_component?.setFavoriteVisualState(item.id, type, false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await favorites.addToFavorites(item.id, item.name, type, null);
|
|
|
|
|
|
_component?.setFavoriteVisualState(item.id, type, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
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-04-07 22:48:59 +02:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
// Wire drag-and-drop on the container once the component is created.
|
|
|
|
|
|
ui.initDragDrop(/** @type {HTMLElement} */ (filesList));
|
2026-03-22 22:55:42 +01:00
|
|
|
|
}
|
2026-05-27 22:51:15 +02:00
|
|
|
|
|
|
|
|
|
|
return _component;
|
2026-03-22 22:55:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-27 22:51:15 +02:00
|
|
|
|
* Append a single item to the current view (post-upload / post-create
|
|
|
|
|
|
* optimistic update). No-op when the Files section is not active or the
|
|
|
|
|
|
* item is already in the list.
|
2026-05-22 10:15:25 +02:00
|
|
|
|
*
|
2026-05-27 22:51:15 +02:00
|
|
|
|
* Called by `fileOperations.js` and `search.js`.
|
2026-05-22 10:15:25 +02:00
|
|
|
|
*
|
2026-05-27 22:51:15 +02:00
|
|
|
|
* @param {FileItem|FolderItem} item
|
2026-03-22 22:55:42 +01:00
|
|
|
|
*/
|
2026-05-27 22:51:15 +02:00
|
|
|
|
function addItem(item) {
|
|
|
|
|
|
const component = _ensureComponent();
|
|
|
|
|
|
if (!component) return;
|
|
|
|
|
|
// Reveal the list if the empty-state is showing
|
|
|
|
|
|
ui.resetFilesList();
|
|
|
|
|
|
component.addItem(item);
|
2026-03-22 22:55:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-27 22:51:15 +02:00
|
|
|
|
* Load and render the contents of `app.currentPath`, rebuilding the
|
|
|
|
|
|
* breadcrumb and updating browser history.
|
2026-04-30 02:02:27 +02:00
|
|
|
|
*
|
2026-05-27 22:51:15 +02:00
|
|
|
|
* @param {Object} [options]
|
|
|
|
|
|
* @param {boolean} [options.insertHistory=true]
|
|
|
|
|
|
* @param {boolean} [options.forceRefresh=false]
|
2026-03-22 22:55:42 +01:00
|
|
|
|
*/
|
2026-04-07 22:48:59 +02:00
|
|
|
|
async function loadFiles(options = { insertHistory: true }) {
|
2026-05-27 22:51:15 +02:00
|
|
|
|
if (_loading) {
|
|
|
|
|
|
console.log('A file load is already in progress, ignoring request');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
_loading = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Delay spinner so fast loads avoid the flash
|
|
|
|
|
|
const spinnerTimeout = setTimeout(() => {
|
|
|
|
|
|
ui.showError(`
|
|
|
|
|
|
<div class="files-loading-spinner">
|
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
|
<span>${i18n.t('files.loading')}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`);
|
|
|
|
|
|
}, 100);
|
2026-03-22 22:55:42 +01:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
try {
|
|
|
|
|
|
if (!app.userHomeFolderId) await resolveHomeFolder();
|
2026-02-20 12:27:52 +01:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
// Resolve path to home folder when none is set
|
2026-02-20 12:27:52 +01:00
|
|
|
|
if (!app.currentPath || app.currentPath === '') {
|
|
|
|
|
|
if (app.userHomeFolderId) {
|
|
|
|
|
|
app.currentPath = app.userHomeFolderId;
|
2026-02-21 00:19:04 +01:00
|
|
|
|
app.breadcrumbPath = [];
|
2026-02-20 12:27:52 +01:00
|
|
|
|
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
|
|
|
|
|
} else {
|
2026-05-27 22:51:15 +02:00
|
|
|
|
console.warn('No home folder id — this should not normally happen');
|
2026-05-07 23:40:02 +02:00
|
|
|
|
}
|
2026-02-20 12:27:52 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
await rebuildBreadCrumb();
|
|
|
|
|
|
ui.updateBreadcrumb();
|
|
|
|
|
|
updateHistory(options.insertHistory ?? true);
|
2026-02-20 12:27:52 +01:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
const { folders, files } = await fetchListing(app.currentPath, {
|
|
|
|
|
|
forceRefresh: options.forceRefresh ?? false
|
|
|
|
|
|
});
|
2026-02-20 12:27:52 +01:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
clearTimeout(spinnerTimeout);
|
2026-02-20 12:27:52 +01:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
// Prepare the container (shows #files-list, hides error panel)
|
2026-04-13 15:09:10 +02:00
|
|
|
|
ui.resetFilesList();
|
2026-02-20 12:27:52 +01:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
const component = _ensureComponent();
|
|
|
|
|
|
if (!component) return;
|
2026-04-30 02:02:27 +02:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
batchToolbar.clear();
|
|
|
|
|
|
batchToolbar.init();
|
|
|
|
|
|
batchToolbar.setActiveComponent(component);
|
2026-02-20 12:27:52 +01:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
if (folders.length === 0 && files.length === 0) {
|
2026-04-13 15:09:10 +02:00
|
|
|
|
ui.showEmptyList();
|
2026-03-09 00:08:34 +01:00
|
|
|
|
} else {
|
2026-05-27 22:51:15 +02:00
|
|
|
|
component.render([...folders, ...files]);
|
|
|
|
|
|
await component.resolveOwnerCells();
|
|
|
|
|
|
}
|
2026-04-06 10:59:46 +02:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
console.log(`Loaded ${folders.length} folders and ${files.length} files`);
|
2026-04-06 10:59:46 +02:00
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
// Deep-link: open a specific file if requested via app.viewFile
|
|
|
|
|
|
if (app.viewFile) {
|
|
|
|
|
|
const fileFound = files.find((f) => f.id === app.viewFile) ?? null;
|
|
|
|
|
|
if (fileFound) {
|
|
|
|
|
|
console.log(`file ${app.viewFile} found, calling viewer`);
|
|
|
|
|
|
await inlineViewer.openFile(fileFound);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`file ${app.viewFile} not found`);
|
|
|
|
|
|
app.viewFile = null;
|
|
|
|
|
|
updateHistory(false);
|
2026-04-06 10:59:46 +02:00
|
|
|
|
}
|
2026-03-09 00:08:34 +01:00
|
|
|
|
}
|
2026-05-27 22:51:15 +02:00
|
|
|
|
} catch (/** @type {any} */ err) {
|
|
|
|
|
|
clearTimeout(spinnerTimeout);
|
|
|
|
|
|
if (err?.status === 403) {
|
|
|
|
|
|
ui.showError(`<p>${i18n.t('errors.forbidden', 'Could not load files')}</p>`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('Error loading folders:', err);
|
|
|
|
|
|
uiNotifications.show('Error', 'Could not load files and folders');
|
|
|
|
|
|
}
|
2026-02-20 12:27:52 +01:00
|
|
|
|
} finally {
|
2026-05-27 22:51:15 +02:00
|
|
|
|
_loading = false;
|
2026-02-20 12:27:52 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 22:51:15 +02:00
|
|
|
|
export { addItem, loadFiles };
|