Files
ComfyUI-Lora-Manager/static/js/components/Header.js
Will Miao be75ad930e feat(layout): implement responsive edge-to-edge card grid with density-aware column calculation
- Add dynamic column calculation based on container width and min card width
- Prevent tiny cards on narrow windows by respecting density-based minimums:
  - Default: 240px, Medium: 200px, Compact: 170px
- Fix edge-to-edge layout with proper CSS selector (.virtual-scroll-item.model-card)
- Add hamburger menu for mobile/small screens with proper translations
- Update all locale files with 'common.actions.menu' key

Fixes: Cards becoming too small/overlapping on narrow window widths (e.g., 1156px)
Changes: 15 files, +569/-114 lines
2026-05-01 21:34:31 +08:00

293 lines
11 KiB
JavaScript

import { updateService } from '../managers/UpdateService.js';
import { toggleTheme } from '../utils/uiHelpers.js';
import { SearchManager } from '../managers/SearchManager.js';
import { FilterManager } from '../managers/FilterManager.js';
import { initPageState } from '../state/index.js';
import { getStorageItem } from '../utils/storageHelpers.js';
import { updateElementAttribute } from '../utils/i18nHelpers.js';
import { renderSupporters } from '../services/supportersService.js';
/**
* Header.js - Manages the application header behavior across different pages
* Handles initialization of appropriate search and filter managers based on current page
*/
export class HeaderManager {
constructor() {
this.currentPage = this.detectCurrentPage();
initPageState(this.currentPage);
this.searchManager = null;
this.filterManager = null;
// Initialize appropriate managers based on current page
if (this.currentPage !== 'statistics') {
this.initializeManagers();
}
// Set up common header functionality
this.initializeCommonElements();
}
detectCurrentPage() {
const path = window.location.pathname;
if (path.includes('/loras/recipes')) return 'recipes';
if (path.includes('/checkpoints')) return 'checkpoints';
if (path.includes('/embeddings')) return 'embeddings';
if (path.includes('/statistics')) return 'statistics';
if (path.includes('/loras')) return 'loras';
return 'unknown';
}
initializeManagers() {
// Initialize SearchManager for all page types
this.searchManager = new SearchManager({ page: this.currentPage });
window.searchManager = this.searchManager;
this.filterManager = new FilterManager({ page: this.currentPage });
window.filterManager = this.filterManager;
}
initializeCommonElements() {
// Handle theme toggle
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
const currentTheme = getStorageItem('theme') || 'auto';
themeToggle.classList.add(`theme-${currentTheme}`);
// Use i18nHelpers to update themeToggle's title
this.updateThemeTooltip(themeToggle, currentTheme);
themeToggle.addEventListener('click', async () => {
if (typeof toggleTheme === 'function') {
const newTheme = toggleTheme();
// Use i18nHelpers to update themeToggle's title
this.updateThemeTooltip(themeToggle, newTheme);
}
});
}
// Handle settings toggle
const settingsToggle = document.querySelector('.settings-toggle');
if (settingsToggle) {
settingsToggle.addEventListener('click', () => {
if (window.settingsManager) {
window.settingsManager.toggleSettings();
}
});
}
// Handle update toggle
const updateToggle = document.getElementById('updateToggleBtn');
if (updateToggle) {
updateToggle.addEventListener('click', () => {
updateService.toggleUpdateModal();
});
}
// Handle support toggle
const supportToggle = document.getElementById('supportToggleBtn');
if (supportToggle) {
supportToggle.addEventListener('click', async () => {
if (window.modalManager) {
window.modalManager.toggleModal('supportModal');
// Load supporters data when modal opens
try {
await renderSupporters();
} catch (error) {
console.error('Error loading supporters:', error);
}
}
});
}
// Handle QR code toggle
const qrToggle = document.getElementById('toggleQRCode');
const qrContainer = document.getElementById('qrCodeContainer');
if (qrToggle && qrContainer) {
qrToggle.addEventListener('click', function() {
qrContainer.classList.toggle('show');
qrToggle.classList.toggle('active');
const toggleText = qrToggle.querySelector('.toggle-text');
if (qrContainer.classList.contains('show')) {
toggleText.textContent = 'Hide WeChat QR Code';
// Add small delay to ensure DOM is updated before scrolling
setTimeout(() => {
const supportModal = document.querySelector('.support-modal');
if (supportModal) {
supportModal.scrollTo({
top: supportModal.scrollHeight,
behavior: 'smooth'
});
}
}, 250);
} else {
toggleText.textContent = 'Show WeChat QR Code';
}
});
}
// Hide search functionality on Statistics page
this.updateHeaderForPage();
// Initialize hamburger menu for mobile
this.initializeHamburgerMenu();
}
initializeHamburgerMenu() {
const hamburgerBtn = document.getElementById('hamburgerMenuBtn');
const hamburgerDropdown = document.getElementById('hamburgerDropdown');
if (!hamburgerBtn || !hamburgerDropdown) return;
// Toggle dropdown on hamburger button click
hamburgerBtn.addEventListener('click', (e) => {
e.stopPropagation();
hamburgerDropdown.classList.toggle('active');
const icon = hamburgerBtn.querySelector('i');
if (hamburgerDropdown.classList.contains('active')) {
icon.classList.remove('fa-bars');
icon.classList.add('fa-times');
} else {
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
}
});
// Handle dropdown item clicks
const dropdownItems = hamburgerDropdown.querySelectorAll('.dropdown-item');
dropdownItems.forEach(item => {
item.addEventListener('click', (e) => {
const action = item.dataset.action;
this.handleHamburgerAction(action);
hamburgerDropdown.classList.remove('active');
const icon = hamburgerBtn.querySelector('i');
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!hamburgerDropdown.contains(e.target) && !hamburgerBtn.contains(e.target)) {
hamburgerDropdown.classList.remove('active');
const icon = hamburgerBtn.querySelector('i');
if (icon) {
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
}
}
});
// Update theme icon in hamburger menu based on current theme
this.updateHamburgerThemeIcon();
}
handleHamburgerAction(action) {
switch (action) {
case 'theme':
if (typeof toggleTheme === 'function') {
const newTheme = toggleTheme();
// Update theme toggle in header if it exists
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.classList.remove('theme-light', 'theme-dark', 'theme-auto');
themeToggle.classList.add(`theme-${newTheme}`);
this.updateThemeTooltip(themeToggle, newTheme);
}
this.updateHamburgerThemeIcon();
}
break;
case 'settings':
if (window.settingsManager) {
window.settingsManager.toggleSettings();
}
break;
case 'help':
const helpToggle = document.getElementById('helpToggleBtn');
if (helpToggle) {
helpToggle.click();
}
break;
case 'notifications':
updateService.toggleUpdateModal();
break;
case 'support':
if (window.modalManager) {
window.modalManager.toggleModal('supportModal');
renderSupporters().catch(error => {
console.error('Error loading supporters:', error);
});
}
break;
}
}
updateHamburgerThemeIcon() {
const themeItem = document.querySelector('.dropdown-item[data-action="theme"]');
if (!themeItem) return;
const currentTheme = getStorageItem('theme') || 'auto';
const icon = themeItem.querySelector('i');
const text = themeItem.querySelector('span');
if (icon) {
icon.classList.remove('fa-moon', 'fa-sun', 'fa-adjust');
if (currentTheme === 'light') {
icon.classList.add('fa-sun');
} else if (currentTheme === 'dark') {
icon.classList.add('fa-moon');
} else {
icon.classList.add('fa-adjust');
}
}
// Update text based on current theme
if (text) {
const key = currentTheme === 'light' ? 'header.theme.switchToDark' :
currentTheme === 'dark' ? 'header.theme.switchToLight' :
'header.theme.toggle';
updateElementAttribute(themeItem, 'aria-label', key, {}, '');
}
}
updateHeaderForPage() {
const headerSearch = document.getElementById('headerSearch');
const searchInput = headerSearch?.querySelector('#searchInput');
const searchButtons = headerSearch?.querySelectorAll('button');
const placeholderKey = 'header.search.placeholders.' + this.currentPage;
if (this.currentPage === 'statistics' && headerSearch) {
headerSearch.classList.add('disabled');
if (searchInput) {
searchInput.disabled = true;
// Use i18nHelpers to update placeholder
updateElementAttribute(searchInput, 'placeholder', 'header.search.notAvailable', {}, 'Search not available on statistics page');
}
searchButtons?.forEach(btn => btn.disabled = true);
} else if (headerSearch) {
headerSearch.classList.remove('disabled');
if (searchInput) {
searchInput.disabled = false;
// Use i18nHelpers to update placeholder
updateElementAttribute(searchInput, 'placeholder', placeholderKey, {}, '');
}
searchButtons?.forEach(btn => btn.disabled = false);
}
}
updateThemeTooltip(themeToggle, currentTheme) {
if (!themeToggle) return;
let key;
if (currentTheme === 'light') {
key = 'header.theme.switchToDark';
} else if (currentTheme === 'dark') {
key = 'header.theme.switchToLight';
} else {
key = 'header.theme.toggle';
}
// Use i18nHelpers to update title
updateElementAttribute(themeToggle, 'title', key, {}, '');
}
}