feat(recipes): add toolbar toggle and settings preview for masonry layout

This commit is contained in:
Will Miao
2026-08-16 15:23:30 +08:00
parent f53352efb2
commit 658f88ca48
18 changed files with 338 additions and 18 deletions
+49 -12
View File
@@ -1017,11 +1017,8 @@ export class SettingsManager {
displayDensitySelect.value = state.global.settings.display_density || 'default';
}
// Set recipes layout setting
const recipesLayoutSelect = document.getElementById('recipesLayout');
if (recipesLayoutSelect) {
recipesLayoutSelect.value = state.global.settings.recipes_layout || 'grid';
}
// Set recipes layout setting (segmented control active state)
this.updateRecipesLayoutControls(state.global.settings.recipes_layout || 'grid');
// Set card info display setting
const cardInfoDisplaySelect = document.getElementById('cardInfoDisplay');
@@ -2294,19 +2291,18 @@ export class SettingsManager {
: element.value;
try {
// Recipes layout has its own shared entry point used by both the
// settings modal segmented control and the recipes page toolbar toggle
if (settingKey === 'recipes_layout') {
return this.saveRecipesLayout(element.value);
}
// Update frontend state with mapped keys
await this.saveSetting(settingKey, value);
// Apply frontend settings immediately
this.applyFrontendSettings();
// Dispatch layout change event; the scroller instance is about to be rebuilt,
// so calculateLayout() must NOT run on the old instance here
if (settingKey === 'recipes_layout') {
window.dispatchEvent(new CustomEvent('lm:recipes-layout-changed'));
return;
}
// Recalculate layout when display density changes
if (settingKey === 'display_density' && state.virtualScroller) {
state.virtualScroller.calculateLayout();
@@ -2334,6 +2330,47 @@ export class SettingsManager {
}
}
/**
* Save the recipes page layout (grid | masonry) and rebuild the scroller.
* Shared entry point for the settings modal segmented control and the
* recipes page toolbar toggle; both stay in sync via
* updateRecipesLayoutControls().
*/
async saveRecipesLayout(value) {
if (value !== 'grid' && value !== 'masonry') {
return;
}
// Update frontend state with mapped keys
await this.saveSetting('recipes_layout', value);
// Apply frontend settings immediately
this.applyFrontendSettings();
// Dispatch layout change event; the scroller instance is about to be rebuilt,
// so calculateLayout() must NOT run on the old instance here
window.dispatchEvent(new CustomEvent('lm:recipes-layout-changed'));
this.updateRecipesLayoutControls(value);
}
/**
* Sync the active state of every recipes layout control
* (settings modal segmented control and recipes page toolbar toggle).
*/
updateRecipesLayoutControls(value) {
document.querySelectorAll('[data-recipes-layout]').forEach((control) => {
const active = control.dataset.recipesLayout === value;
control.classList.toggle('active', active);
if (control.hasAttribute('aria-pressed')) {
control.setAttribute('aria-pressed', String(active));
}
if (control.hasAttribute('aria-checked')) {
control.setAttribute('aria-checked', String(active));
}
});
}
async saveRangeSetting(elementId, displayId, settingKey) {
const element = document.getElementById(elementId);
if (!element) return;
+24
View File
@@ -282,6 +282,30 @@ class RecipeManager {
});
}
// Layout toggle (grid / masonry) — shares the recipes_layout setting with
// the settings modal segmented control; active states stay in sync via
// settingsManager.updateRecipesLayoutControls() after each save
const layoutToggleBtns = document.querySelectorAll('.layout-toggle-btn');
if (layoutToggleBtns.length) {
const currentLayout = state.global.settings?.recipes_layout || 'grid';
layoutToggleBtns.forEach((btn) => {
const isActive = btn.dataset.recipesLayout === currentLayout;
btn.classList.toggle('active', isActive);
btn.setAttribute('aria-pressed', String(isActive));
btn.addEventListener('click', async () => {
const layout = btn.dataset.recipesLayout;
if ((state.global.settings?.recipes_layout || 'grid') === layout) {
return;
}
try {
await window.settingsManager?.saveRecipesLayout(layout);
} catch (error) {
console.error('Failed to switch recipes layout:', error);
}
});
});
}
// Rebuild the scroller on layout switch; in duplicates mode defer until
// exitDuplicateMode re-enables the scroller (direct recreation would dispose
// the old instance while initializeVirtualScroll skips duplicates mode)