feat(swimlane): add swimlane engine with first version on SharedWithMe section

added group by:

        - None (= ordered by folders/file name)
        - Type (Folder first, then Image, Vidao, Audio, Document, etc...)
        - Owner
        - Size (With logarithmic groups))
        - Shared date (with groups: today, last 7 days, last 30 days, then year)
This commit is contained in:
Edouard Vanbelle
2026-05-27 00:32:10 +02:00
parent 2020e4374e
commit 5afb30ebfd
40 changed files with 1765 additions and 279 deletions
+101 -3
View File
@@ -8,11 +8,11 @@ import { installFetchInterceptor } from '../core/fetchWrapper.js';
installFetchInterceptor();
import { Modal } from '../components/modal.js';
import { formatFileSize, formatQuotaSize } from '../core/formatters.js';
import { escapeHtml, formatFileSize, formatQuotaSize } from '../core/formatters.js';
import { i18n } from '../core/i18n.js';
import { oxiIconsInit } from '../core/icons.js';
import { fileOps } from '../features/files/fileOperations.js';
import { batchToolbar } from '../features/files/batchToolbar.js';
import { fileOps } from '../features/files/fileOperations.js';
import { favorites } from '../features/library/favorites.js';
import { recent } from '../features/library/recent.js';
import { fileSharing } from '../features/sharing/fileSharing.js';
@@ -83,6 +83,15 @@ const _batchToolbarButons = `
const _toggleButtons = `
<div class="view-toggle">
<div class="group-by-selector hidden" id="group-by-selector">
<button class="toggle-btn group-by-btn" id="group-by-btn"
title="Group by" data-i18n-title="groupby.title">
<i class="fas fa-layer-group"></i>
<span class="group-by-label"></span>
</button>
<div class="group-by-menu hidden" id="group-by-menu"></div>
</div>
<span class="view-toggle-separator hidden" id="group-by-separator"></span>
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
@@ -146,6 +155,7 @@ const ACTIONS_BAR_TEMPLATES = {
`,
sharedwithme: `
<div class="action-buttons" id="default-buttons"></div>
${_batchToolbarButons}
${_toggleButtons}
`
};
@@ -189,6 +199,75 @@ function setActionsBarMode(mode, force = false) {
}
}
/**
* @typedef {{ key: string, label: string, setGroupBy: (key: string) => void }} GroupByCapableView
*/
/**
* The view that currently owns the group-by selector, or `null` when no
* section supports grouping. Set by `setGroupByView()` from navigation.js.
* @type {{ setGroupBy: (key: string) => void } | null}
*/
let _groupByView = null;
/**
* Update the reference to the view that handles group-by changes.
* Called by navigation.js when the active section changes.
* @param {{ setGroupBy: (key: string) => void } | null} view
*/
function setGroupByView(view) {
_groupByView = view;
}
/** @type {((e: MouseEvent) => void) | null} */
let _groupByDocumentClickHandler = null;
/**
* Populate and show (or hide) the group-by dropdown based on the active
* section's `groupByDefs`. Pass an empty array (or omit) to hide the button.
*
* Must be called AFTER `setActionsBarMode()` so the selector elements exist
* in the DOM.
*
* @param {Array<{key: string, label: string}>} [defs]
*/
function syncGroupByMenu(defs = []) {
const selector = document.getElementById('group-by-selector');
const separator = document.getElementById('group-by-separator');
const menu = document.getElementById('group-by-menu');
if (!selector || !menu) return;
const hasDefs = defs.length > 0;
selector.classList.toggle('hidden', !hasDefs);
separator?.classList.toggle('hidden', !hasDefs);
if (!hasDefs) {
// Reset active indicator when the section has no group-by support
const btn = document.getElementById('group-by-btn');
btn?.classList.remove('active');
const lbl = btn?.querySelector('.group-by-label');
if (lbl) lbl.textContent = '';
return;
}
// Rebuild menu options — call i18n.t() directly so each label is resolved
// at call time (translations are loaded by the time any section switch runs).
menu.innerHTML = `<button class="group-by-option active" data-group-by="">${escapeHtml(i18n.t('groupby.none', 'None'))}</button>`;
for (const def of defs) {
menu.insertAdjacentHTML('beforeend', `<button class="group-by-option" data-group-by="${escapeHtml(def.key)}">${escapeHtml(def.label)}</button>`);
}
// One stable document-level handler to close the menu on outside clicks.
if (_groupByDocumentClickHandler) {
document.removeEventListener('click', _groupByDocumentClickHandler);
}
_groupByDocumentClickHandler = (e) => {
if (/** @type {HTMLElement} */ (e.target)?.closest('#group-by-selector')) return;
document.getElementById('group-by-menu')?.classList.add('hidden');
};
document.addEventListener('click', _groupByDocumentClickHandler);
}
function setupActionsBarDelegation() {
if (actionsBarDelegationBound || !elements.actionsBar) return;
actionsBarDelegationBound = true;
@@ -197,7 +276,26 @@ function setupActionsBarDelegation() {
const btn = /** @type {HTMLElement} */ (e.target)?.closest('button');
if (!btn) return;
// ── Group-by option selected ──────────────────────────────────────────
if (btn.classList.contains('group-by-option')) {
const key = btn.dataset.groupBy ?? '';
_groupByView?.setGroupBy(key);
document.querySelectorAll('.group-by-option').forEach((b) => {
b.classList.remove('active');
});
btn.classList.add('active');
document.getElementById('group-by-menu')?.classList.add('hidden');
const groupByBtn = document.getElementById('group-by-btn');
groupByBtn?.classList.toggle('active', key !== '');
const lbl = groupByBtn?.querySelector('.group-by-label');
if (lbl) lbl.textContent = key !== '' ? (btn.textContent ?? '') : '';
return;
}
switch (btn.id) {
case 'group-by-btn':
document.getElementById('group-by-menu')?.classList.toggle('hidden');
return;
case 'upload-files-btn': {
e.stopPropagation();
const menu = document.getElementById('upload-dropdown-menu');
@@ -798,4 +896,4 @@ function updateStorageUsageDisplay(userData) {
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
}
export { deserializeHash, initApp, setActionsBarMode, updateHistory, updateStorageUsageDisplay };
export { deserializeHash, initApp, setActionsBarMode, setGroupByView, syncGroupByMenu, updateHistory, updateStorageUsageDisplay };