From bd474aa67e968d964d2db288110cd518b0d2ac64 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Thu, 28 May 2026 11:12:36 +0200 Subject: [PATCH] feat(viewPref): keep record of user view preferences per section (groupBy, order, list or grid) --- static/js/app/filesView.js | 10 +- static/js/app/navigation.js | 27 +++++ static/js/app/ui.js | 3 + static/js/core/groupBySync.js | 55 ++++++++++ static/js/core/viewPrefs.js | 100 ++++++++++++++++++ static/js/views/favorites/favoritesView.js | 8 +- .../js/views/sharedWithMe/sharedWithMeView.js | 8 +- 7 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 static/js/core/groupBySync.js create mode 100644 static/js/core/viewPrefs.js diff --git a/static/js/app/filesView.js b/static/js/app/filesView.js index 4ea1749a..f2af0004 100644 --- a/static/js/app/filesView.js +++ b/static/js/app/filesView.js @@ -17,6 +17,7 @@ import { ResourceListComponent } from '../components/resourceList.js'; import { normalizeDateBucket, sizeBucket } from '../core/formatters.js'; import { i18n } from '../core/i18n.js'; +import * as viewPrefs from '../core/viewPrefs.js'; import { batchToolbar } from '../features/files/batchToolbar.js'; import { inlineViewer } from '../features/files/inlineViewer.js'; import { favorites } from '../features/library/favorites.js'; @@ -163,6 +164,7 @@ const filesView = { setGroupBy(key) { if (_groupBy === key) return; _groupBy = key; + viewPrefs.save('files', _groupBy, _reversed, viewPrefs.load('files').view); _nextCursor = null; _component?.clear(); _loadPage({ isFirstPage: true }); @@ -176,6 +178,7 @@ const filesView = { setDirection(reversed) { if (_reversed === reversed) return; _reversed = reversed; + viewPrefs.save('files', _groupBy, _reversed, viewPrefs.load('files').view); _nextCursor = null; _component?.clear(); _loadPage({ isFirstPage: true }); @@ -359,10 +362,11 @@ async function loadFiles(options = { insertHistory: true }) { return; } - // Reset cursor, groupBy, and direction on navigation to a different folder. + // Reset cursor on navigation; restore saved group-by/direction preferences. _nextCursor = null; - _groupBy = ''; - _reversed = false; + const _savedPrefs = viewPrefs.load('files'); + _groupBy = _savedPrefs.groupBy; + _reversed = _savedPrefs.reversed; // Delay spinner so fast loads avoid the flash const spinnerTimeout = setTimeout(() => { diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index f26fd185..45540654 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -3,7 +3,9 @@ * Extracted from main.js to keep navigation concerns isolated. */ +import { applyGroupByMenuState } from '../core/groupBySync.js'; import { i18n } from '../core/i18n.js'; +import * as viewPrefs from '../core/viewPrefs.js'; import { batchToolbar } from '../features/files/batchToolbar.js'; import { favorites } from '../features/library/favorites.js'; import { musicView } from '../features/library/music.js'; @@ -22,6 +24,14 @@ import { ui } from './ui.js'; * Sync the hidden class and inline display for the grid/list containers * based on the current view preference. */ +/** + * Restore the grid/list view preference for a section before rendering. + * @param {string} section Matches `app.currentSection` values. + */ +function restoreView(section) { + app.currentView = viewPrefs.resolveView(section); +} + function syncViewContainers() { const filesList = document.getElementById('files-list'); const gridViewBtn = document.getElementById('grid-view-btn'); @@ -225,11 +235,16 @@ function switchToSharedWithMeSection() { setGroupByView(sharedWithMeView); syncGroupByMenu(sharedWithMeView.groupByDefs); + // Restore the saved group-by selection in the dropdown. + const swmPrefs = viewPrefs.load('sharedwithme'); + applyGroupByMenuState(swmPrefs.groupBy, swmPrefs.reversed); + // Show the Owner column — names are resolved async after render. ui.setOwnerColumnVisible(true); // Show the standard files container and respect grid/list preference toggleFileContainer(true); + restoreView('sharedwithme'); syncViewContainers(); if (batchToolbar) batchToolbar.clear(); @@ -246,6 +261,10 @@ function switchToFilesSection() { setGroupByView(filesView); syncGroupByMenu(filesView.groupByDefs); + // Restore the saved group-by selection in the dropdown. + const filesPrefs = viewPrefs.load('files'); + applyGroupByMenuState(filesPrefs.groupBy, filesPrefs.reversed); + // Show owner column in the Files section ui.setOwnerColumnVisible(true); @@ -257,6 +276,7 @@ function switchToFilesSection() { toggleFileContainer(true); // ensure correct view + restoreView('files'); syncViewContainers(); //reset files view + remove any error @@ -282,6 +302,10 @@ function switchToFavoritesSection() { setGroupByView(favoritesView); syncGroupByMenu(favoritesView.groupByDefs); + // Restore the saved group-by selection in the dropdown. + const favPrefs = viewPrefs.load('favorites'); + applyGroupByMenuState(favPrefs.groupBy, favPrefs.reversed); + // Show the Owner column — names are resolved async after render. ui.setOwnerColumnVisible(true); @@ -293,6 +317,7 @@ function switchToFavoritesSection() { toggleFileContainer(true); // ensure correct view + restoreView('favorites'); syncViewContainers(); if (batchToolbar) batchToolbar.clear(); @@ -320,6 +345,7 @@ function switchToRecentFilesSection() { toggleFileContainer(true); // ensure correct view + restoreView('recent'); syncViewContainers(); //reset files view + remove any error @@ -381,6 +407,7 @@ function switchToTrashSection() { ui.resetFilesList(); //ensure buttons match the current view + restoreView('trash'); syncViewContainers(); // Load trash items diff --git a/static/js/app/ui.js b/static/js/app/ui.js index d4c7af10..991280a3 100644 --- a/static/js/app/ui.js +++ b/static/js/app/ui.js @@ -7,6 +7,7 @@ import { i18n } from '../core/i18n.js'; import { OxiIcons } from '../core/icons.js'; +import * as viewPrefs from '../core/viewPrefs.js'; import { batchToolbar } from '../features/files/batchToolbar.js'; import { contextMenus } from '../features/files/contextMenus.js'; import { fileOps } from '../features/files/fileOperations.js'; @@ -419,6 +420,7 @@ const ui = { switchToGridView() { app.currentView = 'grid'; localStorage.setItem('oxicloud-view', 'grid'); + if (app.currentSection) viewPrefs.saveView(app.currentSection, 'grid'); syncViewContainers(); }, @@ -429,6 +431,7 @@ const ui = { switchToListView() { app.currentView = 'list'; localStorage.setItem('oxicloud-view', 'list'); + if (app.currentSection) viewPrefs.saveView(app.currentSection, 'list'); syncViewContainers(); }, diff --git a/static/js/core/groupBySync.js b/static/js/core/groupBySync.js new file mode 100644 index 00000000..a0526836 --- /dev/null +++ b/static/js/core/groupBySync.js @@ -0,0 +1,55 @@ +// @ts-check + +/** + * groupBySync — apply groupBy + sort-direction state to the group-by menu UI. + * + * Pure DOM helper with no module imports so it can be safely imported by any + * view without creating circular dependencies. + * + * Call after `syncGroupByMenu()` has built the option list, e.g. when + * restoring saved preferences on section entry. + * + * Usage: + * import { applyGroupByMenuState } from '../core/groupBySync.js'; + * applyGroupByMenuState('type', true); + */ + +/** + * Reflect `groupBy` and `reversed` in the group-by menu DOM: + * - marks the matching `.group-by-option` as active + * - updates the group-by button label and active class + * - toggles the sort-direction button active class + * + * No-op when the menu elements are not in the DOM (e.g. before initApp). + * + * @param {string} groupBy Active group-by key, or `''` for "None". + * @param {boolean} reversed Whether sort direction is reversed. + */ +function applyGroupByMenuState(groupBy, reversed) { + // Mark the matching option as active; clear all others. + for (const b of document.querySelectorAll('.group-by-option')) { + const btn = /** @type {HTMLElement} */ (b); + btn.classList.toggle('active', (btn.dataset.groupBy ?? '') === groupBy); + } + + // Group-by button: active class + label text. + const groupByBtn = document.getElementById('group-by-btn'); + groupByBtn?.classList.toggle('active', groupBy !== ''); + + const lbl = groupByBtn?.querySelector('.group-by-label'); + if (lbl) { + if (groupBy === '') { + lbl.textContent = ''; + } else { + const activeOpt = /** @type {HTMLElement|null} */ ( + document.querySelector(`.group-by-option[data-group-by="${CSS.escape(groupBy)}"]`) + ); + lbl.textContent = activeOpt?.textContent ?? ''; + } + } + + // Sort-direction button: active = reversed. + document.getElementById('sort-dir-btn')?.classList.toggle('active', reversed); +} + +export { applyGroupByMenuState }; diff --git a/static/js/core/viewPrefs.js b/static/js/core/viewPrefs.js new file mode 100644 index 00000000..63cbd7fa --- /dev/null +++ b/static/js/core/viewPrefs.js @@ -0,0 +1,100 @@ +// @ts-check + +/** + * View preferences — persist groupBy, sort-direction, and grid/list view + * per section in localStorage. + * + * Each section has its own key (`oxicloud.view.
`) so that, for + * example, Favorites can be in list view with "Type" grouping while Files + * is in grid view with no grouping. + * + * Section keys match `app.currentSection` values: + * 'files' | 'favorites' | 'sharedwithme' | 'recent' | 'trash' | + * 'photos' | 'music' | 'shared' + * + * All errors (quota, private browsing, JSON parse) are silently swallowed. + * + * Usage: + * import * as viewPrefs from '../core/viewPrefs.js'; + * + * // Read + * const { groupBy, reversed, view } = viewPrefs.load('files'); + * + * // Write all fields at once + * viewPrefs.save('files', 'type', true, 'list'); + * + * // Write only the view (grid/list) toggle, keeping stored groupBy/reversed + * viewPrefs.saveView('favorites', 'grid'); + * + * // Resolve which view (grid/list) to apply on section entry + * const v = viewPrefs.resolveView('sharedwithme'); // 'grid' | 'list' + */ + +const _PREFIX = 'oxicloud.view.'; + +/** + * @typedef {'grid'|'list'|''} ViewMode + * @typedef {{ groupBy: string, reversed: boolean, view: ViewMode }} ViewPrefs + */ + +/** + * Load saved preferences for a section. + * Returns safe defaults when nothing is stored or storage is unavailable. + * @param {string} section + * @returns {ViewPrefs} + */ +function load(section) { + try { + const raw = localStorage.getItem(_PREFIX + section); + if (!raw) return { groupBy: '', reversed: false, view: '' }; + const p = JSON.parse(raw); + return { + groupBy: typeof p.groupBy === 'string' ? p.groupBy : '', + reversed: Boolean(p.reversed), + view: p.view === 'grid' || p.view === 'list' ? p.view : '' + }; + } catch { + return { groupBy: '', reversed: false, view: '' }; + } +} + +/** + * Persist all preferences for a section. + * @param {string} section + * @param {string} groupBy + * @param {boolean} reversed + * @param {ViewMode} view + */ +function save(section, groupBy, reversed, view) { + try { + localStorage.setItem(_PREFIX + section, JSON.stringify({ groupBy, reversed, view })); + } catch { + // Silently ignore quota errors or restricted environments. + } +} + +/** + * Update only the grid/list view for a section, preserving groupBy and reversed. + * @param {string} section + * @param {ViewMode} view + */ +function saveView(section, view) { + const current = load(section); + save(section, current.groupBy, current.reversed, view); +} + +/** + * Resolve the view mode to apply when entering a section. + * Priority: section-specific pref → legacy global `oxicloud-view` key → `'grid'`. + * @param {string} section + * @returns {'grid'|'list'} + */ +function resolveView(section) { + const prefs = load(section); + if (prefs.view) return prefs.view; + // Fall back to the pre-existing global key (backward compatibility). + const global = localStorage.getItem('oxicloud-view'); + return global === 'list' ? 'list' : 'grid'; +} + +export { load, resolveView, save, saveView }; diff --git a/static/js/views/favorites/favoritesView.js b/static/js/views/favorites/favoritesView.js index b7dc9de0..86fffd07 100644 --- a/static/js/views/favorites/favoritesView.js +++ b/static/js/views/favorites/favoritesView.js @@ -20,6 +20,7 @@ import { ui } from '../../app/ui.js'; import { ResourceListComponent } from '../../components/resourceList.js'; import { normalizeDateBucket, sizeBucket } from '../../core/formatters.js'; import { i18n } from '../../core/i18n.js'; +import * as viewPrefs from '../../core/viewPrefs.js'; import { batchToolbar } from '../../features/files/batchToolbar.js'; import * as itemTooltip from '../../features/itemTooltip.js'; import { favorites } from '../../features/library/favorites.js'; @@ -164,6 +165,7 @@ const favoritesView = { setGroupBy(key) { if (this._groupBy === key) return; this._groupBy = key; + viewPrefs.save('favorites', this._groupBy, this._reversed, viewPrefs.load('favorites').view); this._nextCursor = null; this._component?.clear(); this._loadPage(); @@ -177,6 +179,7 @@ const favoritesView = { setDirection(reversed) { if (this._reversed === reversed) return; this._reversed = reversed; + viewPrefs.save('favorites', this._groupBy, this._reversed, viewPrefs.load('favorites').view); this._nextCursor = null; this._component?.clear(); this._loadPage(); @@ -189,8 +192,9 @@ const favoritesView = { async init() { this._nextCursor = null; this._loading = false; - this._groupBy = ''; - this._reversed = false; + const _savedPrefs = viewPrefs.load('favorites'); + this._groupBy = _savedPrefs.groupBy; + this._reversed = _savedPrefs.reversed; this._ensureLoadMoreButton(); diff --git a/static/js/views/sharedWithMe/sharedWithMeView.js b/static/js/views/sharedWithMe/sharedWithMeView.js index 779e78df..ba4cb839 100644 --- a/static/js/views/sharedWithMe/sharedWithMeView.js +++ b/static/js/views/sharedWithMe/sharedWithMeView.js @@ -14,6 +14,7 @@ import { ui } from '../../app/ui.js'; import { ResourceListComponent } from '../../components/resourceList.js'; import { normalizeDateBucket, sizeBucket } from '../../core/formatters.js'; import { i18n } from '../../core/i18n.js'; +import * as viewPrefs from '../../core/viewPrefs.js'; import { batchToolbar } from '../../features/files/batchToolbar.js'; import * as itemTooltip from '../../features/itemTooltip.js'; import { favorites } from '../../features/library/favorites.js'; @@ -165,6 +166,7 @@ const sharedWithMeView = { setGroupBy(key) { if (this._groupBy === key) return; this._groupBy = key; + viewPrefs.save('sharedwithme', this._groupBy, this._reversed, viewPrefs.load('sharedwithme').view); this._nextCursor = null; // restart from first page this._component?.clear(); this._loadPage(); @@ -178,6 +180,7 @@ const sharedWithMeView = { setDirection(reversed) { if (this._reversed === reversed) return; this._reversed = reversed; + viewPrefs.save('sharedwithme', this._groupBy, this._reversed, viewPrefs.load('sharedwithme').view); this._nextCursor = null; this._component?.clear(); this._loadPage(); @@ -190,8 +193,9 @@ const sharedWithMeView = { async init() { this._nextCursor = null; this._loading = false; - this._groupBy = ''; - this._reversed = false; + const _savedPrefs = viewPrefs.load('sharedwithme'); + this._groupBy = _savedPrefs.groupBy; + this._reversed = _savedPrefs.reversed; this._ensureLoadMoreButton();