feat(viewPref): keep record of user view preferences per section (groupBy, order, list or grid)
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
@@ -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.<section>`) 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 };
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user