From 0b0caa1142a01c03bd3201048cb61ea2366ced97 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 13 Mar 2025 20:37:23 +0800 Subject: [PATCH] Fix layout --- pages/checkpoints.html | 11 ---- pages/loras.html | 10 ---- pages/recipes.html | 10 ---- static/css/components/modal.css | 6 +-- static/css/components/search-filter.css | 25 ++++++--- static/css/layout.css | 1 + static/js/main.js | 10 +++- static/js/managers/FilterManager.js | 34 ++++++++---- static/js/utils/infiniteScroll.js | 5 +- static/js/utils/search.js | 70 +++++++++++++++++++++---- static/js/utils/uiHelpers.js | 68 +++++++++++++++++++----- 11 files changed, 175 insertions(+), 75 deletions(-) delete mode 100644 pages/checkpoints.html delete mode 100644 pages/loras.html delete mode 100644 pages/recipes.html diff --git a/pages/checkpoints.html b/pages/checkpoints.html deleted file mode 100644 index 4a8ec53e..00000000 --- a/pages/checkpoints.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - {% include 'components/header.html' %} - - - \ No newline at end of file diff --git a/pages/loras.html b/pages/loras.html deleted file mode 100644 index 55da26e9..00000000 --- a/pages/loras.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - {% include 'components/header.html' %} - - - diff --git a/pages/recipes.html b/pages/recipes.html deleted file mode 100644 index 55da26e9..00000000 --- a/pages/recipes.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - {% include 'components/header.html' %} - - - diff --git a/static/css/components/modal.css b/static/css/components/modal.css index a8684865..0715f52b 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -2,10 +2,10 @@ .modal { display: none; position: fixed; - top: 0; + top: 48px; /* Start below the header */ left: 0; width: 100%; - height: 100%; + height: calc(100% - 48px); /* Adjust height to exclude header */ background: rgba(0, 0, 0, 0.2); /* 调整为更淡的半透明黑色 */ z-index: var(--z-modal); overflow: hidden; /* 改为 hidden,防止双滚动条 */ @@ -24,7 +24,7 @@ body.modal-open { max-width: 800px; height: auto; max-height: 90vh; - margin: 2rem auto; + margin: 1rem auto; /* Reduce top margin from 5rem to 1rem */ background: var(--lora-surface); border-radius: var(--border-radius-base); padding: var(--space-3); diff --git a/static/css/components/search-filter.css b/static/css/components/search-filter.css index f18abfeb..22dab426 100644 --- a/static/css/components/search-filter.css +++ b/static/css/components/search-filter.css @@ -144,10 +144,9 @@ /* Filter Panel Styles */ .filter-panel { - position: absolute; - top: 140px; /* Adjust to be closer to the filter button */ + position: fixed; right: 20px; - width: 300px; + width: 320px; background-color: var(--card-bg); border: 1px solid var(--border-color); border-radius: var(--border-radius-base); @@ -312,7 +311,7 @@ width: calc(100% - 40px); left: 20px; right: 20px; - top: 140px; + top: 160px; /* Adjusted for mobile layout */ } } @@ -351,10 +350,9 @@ /* Search Options Panel */ .search-options-panel { - position: absolute; - top: 140px; - right: 65px; /* Position it closer to the search options button */ - width: 280px; /* Slightly wider to accommodate tags better */ + position: fixed; + right: 20px; + width: 280px; background-color: var(--card-bg); border: 1px solid var(--border-color); border-radius: var(--border-radius-base); @@ -507,4 +505,15 @@ input:checked + .slider:before { .slider.round:before { border-radius: 50%; +} + +/* Mobile adjustments */ +@media (max-width: 768px) { + .search-options-panel, + .filter-panel { + width: calc(100% - 40px); + left: 20px; + right: 20px; + top: 160px; /* Adjusted for mobile layout */ + } } \ No newline at end of file diff --git a/static/css/layout.css b/static/css/layout.css index db14461a..3b980cf7 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -4,6 +4,7 @@ overflow-y: auto; /* Enable scrolling here */ width: 100%; position: relative; + overflow-y: scroll; } .container { diff --git a/static/js/main.js b/static/js/main.js index d1974db6..797e5c3c 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -17,7 +17,8 @@ import { openCivitai, toggleFolderTags, initFolderTagsVisibility, - initBackToTop + initBackToTop, + updatePanelPositions } from './utils/uiHelpers.js'; import { initializeInfiniteScroll } from './utils/infiniteScroll.js'; import { showDeleteModal, confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; @@ -56,6 +57,7 @@ window.toggleApiKeyVisibility = toggleApiKeyVisibility; window.moveManager = moveManager; window.toggleShowcase = toggleShowcase; window.scrollToTop = scrollToTop; +window.updatePanelPositions = updatePanelPositions; // Export bulk manager methods to window window.toggleBulkMode = () => bulkManager.toggleBulkMode(); @@ -96,6 +98,12 @@ document.addEventListener('DOMContentLoaded', async () => { // Initialize the bulk manager bulkManager.initialize(); + + // Initial positioning + updatePanelPositions(); + + // Update positions on window resize + window.addEventListener('resize', updatePanelPositions); }); // Initialize event listeners diff --git a/static/js/managers/FilterManager.js b/static/js/managers/FilterManager.js index 3f05c73e..5f5ef8ef 100644 --- a/static/js/managers/FilterManager.js +++ b/static/js/managers/FilterManager.js @@ -13,6 +13,7 @@ export class FilterManager { this.filterPanel = document.getElementById('filterPanel'); this.filterButton = document.getElementById('filterButton'); this.activeFiltersCount = document.getElementById('activeFiltersCount'); + this.tagsLoaded = false; this.initialize(); } @@ -140,20 +141,35 @@ export class FilterManager { }); } - toggleFilterPanel() { - const wasHidden = this.filterPanel.classList.contains('hidden'); - - this.filterPanel.classList.toggle('hidden'); - - // If the panel is being opened, load the top tags and update selections - if (wasHidden) { - this.loadTopTags(); - this.updateTagSelections(); + toggleFilterPanel() { + if (this.filterPanel) { + const isHidden = this.filterPanel.classList.contains('hidden'); + + if (isHidden) { + // Update panel positions before showing + if (window.searchManager && typeof window.searchManager.updatePanelPositions === 'function') { + window.searchManager.updatePanelPositions(); + } else if (typeof updatePanelPositions === 'function') { + updatePanelPositions(); + } + + this.filterPanel.classList.remove('hidden'); + this.filterButton.classList.add('active'); + + // Load tags if they haven't been loaded yet + if (!this.tagsLoaded) { + this.loadTopTags(); + this.tagsLoaded = true; + } + } else { + this.closeFilterPanel(); + } } } closeFilterPanel() { this.filterPanel.classList.add('hidden'); + this.filterButton.classList.remove('active'); } updateTagSelections() { diff --git a/static/js/utils/infiniteScroll.js b/static/js/utils/infiniteScroll.js index 448ede8d..4723bb7a 100644 --- a/static/js/utils/infiniteScroll.js +++ b/static/js/utils/infiniteScroll.js @@ -1,16 +1,19 @@ import { state } from '../state/index.js'; import { loadMoreLoras } from '../api/loraApi.js'; +import { debounce } from './debounce.js'; export function initializeInfiniteScroll() { if (state.observer) { state.observer.disconnect(); } + const debouncedLoadMore = debounce(loadMoreLoras, 200); + state.observer = new IntersectionObserver( (entries) => { const target = entries[0]; if (target.isIntersecting && !state.isLoading && state.hasMore) { - loadMoreLoras(); + debouncedLoadMore(); } }, { threshold: 0.1 } diff --git a/static/js/utils/search.js b/static/js/utils/search.js index f956acbf..1c831c30 100644 --- a/static/js/utils/search.js +++ b/static/js/utils/search.js @@ -30,6 +30,34 @@ export class SearchManager { // Initialize search options this.initSearchOptions(); + + // Add global click handler to close panels when clicking outside + document.addEventListener('click', (e) => { + // Close search options panel when clicking outside + if (this.searchOptionsPanel && + !this.searchOptionsPanel.contains(e.target) && + e.target !== this.searchOptionsToggle && + !this.searchOptionsToggle.contains(e.target)) { + this.closeSearchOptionsPanel(); + } + + // Close filter panel when clicking outside (if filterManager exists) + const filterPanel = document.getElementById('filterPanel'); + const filterButton = document.getElementById('filterButton'); + if (filterPanel && + !filterPanel.contains(e.target) && + e.target !== filterButton && + !filterButton.contains(e.target) && + window.filterManager) { + window.filterManager.closeFilterPanel(); + } + }); + + // Initialize panel positions + this.updatePanelPositions(); + + // Add resize listener + window.addEventListener('resize', this.updatePanelPositions.bind(this)); } initSearchOptions() { @@ -103,16 +131,6 @@ export class SearchManager { // Ensure at least one search option is selected this.validateSearchOptions(); - - // Close panel when clicking outside - document.addEventListener('click', (e) => { - if (this.searchOptionsPanel && - !this.searchOptionsPanel.contains(e.target) && - e.target !== this.searchOptionsToggle && - !this.searchOptionsToggle.contains(e.target)) { - this.closeSearchOptionsPanel(); - } - }); } // Add method to validate search options @@ -137,6 +155,8 @@ export class SearchManager { if (this.searchOptionsPanel) { const isHidden = this.searchOptionsPanel.classList.contains('hidden'); if (isHidden) { + // Update position before showing + this.updatePanelPositions(); this.searchOptionsPanel.classList.remove('hidden'); this.searchOptionsToggle.classList.add('active'); } else { @@ -209,6 +229,9 @@ export class SearchManager { this.isSearching = true; state.loadingManager.showSimpleLoading('Searching...'); + // Store current scroll position + const scrollPosition = window.pageYOffset || document.documentElement.scrollTop; + state.currentPage = 1; state.hasMore = true; @@ -250,6 +273,14 @@ export class SearchManager { state.hasMore = state.currentPage < data.total_pages; state.currentPage++; } + + // Restore scroll position after content is loaded + setTimeout(() => { + window.scrollTo({ + top: scrollPosition, + behavior: 'instant' // Use 'instant' to prevent animation + }); + }, 10); } } catch (error) { console.error('Search error:', error); @@ -259,4 +290,23 @@ export class SearchManager { state.loadingManager.hide(); } } + + updatePanelPositions() { + const searchOptionsPanel = document.getElementById('searchOptionsPanel'); + const filterPanel = document.getElementById('filterPanel'); + + if (!searchOptionsPanel || !filterPanel) return; + + // Get the controls container + const controls = document.querySelector('.controls'); + if (!controls) return; + + // Calculate the position based on the bottom of the controls container + const controlsRect = controls.getBoundingClientRect(); + const topPosition = controlsRect.bottom + 10; // Add 10px padding + + // Set the positions + searchOptionsPanel.style.top = `${topPosition}px`; + filterPanel.style.top = `${topPosition}px`; + } } \ No newline at end of file diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 0597488c..50af65d5 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -98,16 +98,57 @@ export function openCivitai(modelName) { } } +/** + * Dynamically positions the search options panel and filter panel + * based on the current layout and folder tags container height + */ +export function updatePanelPositions() { + const searchOptionsPanel = document.getElementById('searchOptionsPanel'); + const filterPanel = document.getElementById('filterPanel'); + + if (!searchOptionsPanel || !filterPanel) return; + + // Get the controls container + const controls = document.querySelector('.controls'); + if (!controls) return; + + // Calculate the position based on the bottom of the controls container + const controlsRect = controls.getBoundingClientRect(); + const topPosition = controlsRect.bottom + 10; // Add 10px padding + + // Set the positions + searchOptionsPanel.style.top = `${topPosition}px`; + filterPanel.style.top = `${topPosition}px`; +} + +// Update the toggleFolderTags function export function toggleFolderTags() { const folderTags = document.querySelector('.folder-tags'); - const btn = document.querySelector('.toggle-folders-btn'); - const isCollapsed = folderTags.classList.toggle('collapsed'); + const toggleBtn = document.querySelector('.toggle-folders-btn i'); - // 更新按钮提示文本 - btn.title = isCollapsed ? 'Expand folder tags' : 'Collapse folder tags'; - - // 保存状态到 localStorage - localStorage.setItem('folderTagsCollapsed', isCollapsed); + if (folderTags) { + folderTags.classList.toggle('collapsed'); + + if (folderTags.classList.contains('collapsed')) { + toggleBtn.className = 'fas fa-chevron-down'; + toggleBtn.parentElement.title = 'Expand folder tags'; + localStorage.setItem('folderTagsCollapsed', 'true'); + } else { + toggleBtn.className = 'fas fa-chevron-up'; + toggleBtn.parentElement.title = 'Collapse folder tags'; + localStorage.setItem('folderTagsCollapsed', 'false'); + } + + // Update panel positions after toggling + // Use a small delay to ensure the DOM has updated + setTimeout(() => { + if (window.searchManager && typeof window.searchManager.updatePanelPositions === 'function') { + window.searchManager.updatePanelPositions(); + } else if (typeof updatePanelPositions === 'function') { + updatePanelPositions(); + } + }, 50); + } } // Add this to your existing initialization code @@ -128,10 +169,13 @@ export function initBackToTop() { button.title = 'Back to top'; document.body.appendChild(button); + // Get the scrollable container + const scrollContainer = document.querySelector('.page-content'); + // Show/hide button based on scroll position const toggleBackToTop = () => { - const scrollThreshold = window.innerHeight * 0.75; - if (window.scrollY > scrollThreshold) { + const scrollThreshold = window.innerHeight * 0.3; + if (scrollContainer.scrollTop > scrollThreshold) { button.classList.add('visible'); } else { button.classList.remove('visible'); @@ -140,14 +184,14 @@ export function initBackToTop() { // Smooth scroll to top button.addEventListener('click', () => { - window.scrollTo({ + scrollContainer.scrollTo({ top: 0, behavior: 'smooth' }); }); - // Listen for scroll events - window.addEventListener('scroll', toggleBackToTop); + // Listen for scroll events on the scrollable container + scrollContainer.addEventListener('scroll', toggleBackToTop); // Initial check toggleBackToTop();