diff --git a/static/css/layout.css b/static/css/layout.css index cea96f6d..72e364bf 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -32,7 +32,7 @@ /* 调整搜索框样式以匹配其他控件 */ .search-container input { width: 100%; - padding: 6px 40px 6px 12px; /* 减小右侧padding */ + padding: 6px 75px 6px 12px; /* Increased right padding to accommodate both buttons */ border: 1px solid oklch(65% 0.02 256); /* 更深的边框颜色,提高对比度 */ border-radius: var(--border-radius-sm); background: var(--lora-surface); @@ -49,7 +49,7 @@ .search-icon { position: absolute; - right: 40px; /* 调整到toggle按钮左侧 */ + right: 80px; /* Adjusted to make space for both toggle buttons */ top: 50%; transform: translateY(-50%); color: oklch(var(--text-color) / 0.5); @@ -60,7 +60,7 @@ /* 修改清空按钮样式 */ .search-clear { position: absolute; - right: 65px; /* 放到search-icon左侧 */ + right: 105px; /* Adjusted further left to avoid overlapping */ top: 50%; transform: translateY(-50%); color: oklch(var(--text-color) / 0.5); @@ -98,19 +98,70 @@ } .search-mode-toggle:hover { - background: var(--lora-accent); - color: white; + background-color: var(--lora-surface-hover, oklch(95% 0.02 256)); + color: var(--lora-accent); + border-color: var(--lora-accent); } .search-mode-toggle.active { - background: var(--lora-accent); - color: white; + background-color: oklch(95% 0.05 256); /* Lighter background that's more consistent */ + color: var(--lora-accent); + border-color: var(--lora-accent); } .search-mode-toggle i { font-size: 0.9em; } +.search-filter-toggle { + background: var(--lora-surface); + border: 1px solid oklch(65% 0.02 256); + border-radius: var(--border-radius-sm); + color: var(--text-color); + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + position: relative; +} + +.search-filter-toggle:hover { + background-color: var(--lora-surface-hover, oklch(95% 0.02 256)); + color: var(--lora-accent); + border-color: var(--lora-accent); +} + +.search-filter-toggle.active { + background-color: oklch(95% 0.05 256); /* Lighter background that's more consistent */ + color: var(--lora-accent); + border-color: var(--lora-accent); +} + +.search-filter-toggle i { + font-size: 0.9em; +} + +.filter-badge { + position: absolute; + top: -6px; + right: -6px; + background-color: var(--lora-accent); + color: white; + width: 16px; + height: 16px; + border-radius: 50%; + font-size: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: bold; +} + .corner-controls { position: fixed; top: 20px; @@ -308,6 +359,151 @@ opacity: 0; } +/* Filter Panel Styles */ +.filter-panel { + position: absolute; + top: 140px; /* Adjust to be closer to the filter button */ + right: 20px; + width: 300px; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-base); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + z-index: var(--z-overlay); /* Increase z-index to be above cards */ + padding: 16px; + transition: transform 0.3s ease, opacity 0.3s ease; + transform-origin: top right; + max-height: calc(100vh - 160px); + overflow-y: auto; +} + +.filter-panel.hidden { + opacity: 0; + transform: scale(0.95); + pointer-events: none; +} + +.filter-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.filter-header h3 { + margin: 0; + font-size: 18px; + color: var(--text-color); +} + +.close-filter-btn { + background: none; + border: none; + color: var(--text-color); + cursor: pointer; + font-size: 16px; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.close-filter-btn:hover { + color: var(--lora-accent); +} + +.filter-section { + margin-bottom: 16px; +} + +.filter-section h4 { + margin: 0 0 8px 0; + font-size: 14px; + color: var(--text-color); + opacity: 0.8; +} + +.filter-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.filter-tag { + padding: 4px 10px; + border-radius: var(--border-radius-sm); + background-color: var(--lora-surface); + border: 1px solid var(--border-color); + color: var(--text-color); + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-tag:hover { + background-color: var(--lora-surface-hover); +} + +.filter-tag.active { + background-color: var(--lora-accent); + color: white; + border-color: var(--lora-accent); +} + +/* Base model tag specific styles - removing accent colors */ +.filter-tag.base-model-tag.sd-1-5, +.filter-tag.base-model-tag.sd-2-0, +.filter-tag.base-model-tag.sd-2-1, +.filter-tag.base-model-tag.sdxl, +.filter-tag.base-model-tag.flux, +.filter-tag.base-model-tag.il, +.filter-tag.base-model-tag.pony, +.filter-tag.base-model-tag.hunyuan, +.filter-tag.base-model-tag.unknown { + /* Removing the specific colored borders */ + border-left: none; +} + +/* Filter actions */ +.filter-actions { + display: flex; + justify-content: space-between; + margin-top: 16px; + gap: 8px; +} + +.clear-filters-btn, +.apply-filters-btn { + padding: 6px 12px; + border-radius: var(--border-radius-sm); + font-size: 14px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.clear-filters-btn { + background-color: transparent; + color: var(--text-color); + border: 1px solid var(--border-color); + flex: 1; +} + +.clear-filters-btn:hover { + background-color: var(--lora-surface-hover); +} + +.apply-filters-btn { + background-color: var(--lora-accent); + color: white; + border: 1px solid var(--lora-accent); + flex: 1; +} + +.apply-filters-btn:hover { + background-color: var(--lora-accent-hover, var(--lora-accent)); + filter: brightness(1.1); +} + @media (max-width: 768px) { .actions { flex-wrap: wrap; @@ -323,6 +519,7 @@ width: 100%; order: -1; margin-left: 0; + margin-right: 0; } .folder-tags-container { @@ -355,4 +552,11 @@ .back-to-top { bottom: 60px; /* Give some extra space from bottom on mobile */ } + + .filter-panel { + width: calc(100% - 40px); + left: 20px; + right: 20px; + top: 140px; + } } \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 5475890b..d868e449 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -24,6 +24,7 @@ import { DownloadManager } from './managers/DownloadManager.js'; import { SettingsManager, toggleApiKeyVisibility } from './managers/SettingsManager.js'; import { LoraContextMenu } from './components/ContextMenu.js'; import { moveManager } from './managers/MoveManager.js'; +import { FilterManager } from './managers/FilterManager.js'; // Export all functions that need global access window.loadMoreLoras = loadMoreLoras; @@ -53,6 +54,7 @@ document.addEventListener('DOMContentLoaded', () => { state.loadingManager = new LoadingManager(); modalManager.initialize(); // Initialize modalManager after DOM is loaded window.downloadManager = new DownloadManager(); // Move this after modalManager initialization + window.filterManager = new FilterManager(); // Initialize filter manager initializeInfiniteScroll(); initializeEventListeners(); lazyLoadImages(); diff --git a/static/js/managers/FilterManager.js b/static/js/managers/FilterManager.js new file mode 100644 index 00000000..30620937 --- /dev/null +++ b/static/js/managers/FilterManager.js @@ -0,0 +1,159 @@ +import { BASE_MODELS, BASE_MODEL_CLASSES } from '../utils/constants.js'; +import { state } from '../state/index.js'; +import { showToast } from '../utils/uiHelpers.js'; + +export class FilterManager { + constructor() { + this.filters = { + baseModel: [] + }; + + this.filterPanel = document.getElementById('filterPanel'); + this.filterButton = document.getElementById('filterButton'); + this.activeFiltersCount = document.getElementById('activeFiltersCount'); + + this.initialize(); + } + + initialize() { + // Create base model filter tags + this.createBaseModelTags(); + + // Close filter panel when clicking outside + document.addEventListener('click', (e) => { + if (!this.filterPanel.contains(e.target) && + e.target !== this.filterButton && + !this.filterButton.contains(e.target) && + !this.filterPanel.classList.contains('hidden')) { + this.closeFilterPanel(); + } + }); + + // Initialize active filters from localStorage if available + this.loadFiltersFromStorage(); + } + + createBaseModelTags() { + const baseModelTagsContainer = document.getElementById('baseModelTags'); + if (!baseModelTagsContainer) return; + + baseModelTagsContainer.innerHTML = ''; + + Object.entries(BASE_MODELS).forEach(([key, value]) => { + const tag = document.createElement('div'); + tag.className = `filter-tag base-model-tag ${BASE_MODEL_CLASSES[value]}`; + tag.dataset.baseModel = value; + tag.innerHTML = value; + + // Add click handler to toggle selection + tag.addEventListener('click', () => { + tag.classList.toggle('active'); + + if (tag.classList.contains('active')) { + if (!this.filters.baseModel.includes(value)) { + this.filters.baseModel.push(value); + } + } else { + this.filters.baseModel = this.filters.baseModel.filter(model => model !== value); + } + + this.updateActiveFiltersCount(); + }); + + baseModelTagsContainer.appendChild(tag); + }); + } + + toggleFilterPanel() { + this.filterPanel.classList.toggle('hidden'); + + // Mark selected filters + if (!this.filterPanel.classList.contains('hidden')) { + this.updateTagSelections(); + } + } + + closeFilterPanel() { + this.filterPanel.classList.add('hidden'); + } + + updateTagSelections() { + // Update base model tags + const baseModelTags = document.querySelectorAll('.base-model-tag'); + baseModelTags.forEach(tag => { + const baseModel = tag.dataset.baseModel; + if (this.filters.baseModel.includes(baseModel)) { + tag.classList.add('active'); + } else { + tag.classList.remove('active'); + } + }); + } + + updateActiveFiltersCount() { + const totalActiveFilters = this.filters.baseModel.length; + + if (totalActiveFilters > 0) { + this.activeFiltersCount.textContent = totalActiveFilters; + this.activeFiltersCount.style.display = 'inline-flex'; + } else { + this.activeFiltersCount.style.display = 'none'; + } + } + + applyFilters() { + // Save filters to localStorage + localStorage.setItem('loraFilters', JSON.stringify(this.filters)); + + // Apply filters to cards (will be implemented later) + showToast('Filters applied', 'success'); + + // Close the filter panel + this.closeFilterPanel(); + + // Update filter button to show active state + if (this.hasActiveFilters()) { + this.filterButton.classList.add('active'); + } else { + this.filterButton.classList.remove('active'); + } + } + + clearFilters() { + // Clear all filters + this.filters = { + baseModel: [] + }; + + // Update UI + this.updateTagSelections(); + this.updateActiveFiltersCount(); + + // Remove from localStorage + localStorage.removeItem('loraFilters'); + + this.filterButton.classList.remove('active'); + showToast('All filters cleared', 'info'); + } + + loadFiltersFromStorage() { + const savedFilters = localStorage.getItem('loraFilters'); + if (savedFilters) { + try { + this.filters = JSON.parse(savedFilters); + this.updateTagSelections(); + this.updateActiveFiltersCount(); + + if (this.hasActiveFilters()) { + this.filterButton.classList.add('active'); + } + } catch (error) { + console.error('Error loading filters from storage:', error); + } + } + } + + hasActiveFilters() { + return this.filters.baseModel.length > 0; + } +} diff --git a/static/js/state/index.js b/static/js/state/index.js index 0506fd97..746e2909 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -7,5 +7,8 @@ export const state = { loadingManager: null, observer: null, previewVersions: new Map(), - searchManager: null // 添加 searchManager + searchManager: null, + filters: { + baseModel: [] + } }; \ No newline at end of file diff --git a/static/js/utils/constants.js b/static/js/utils/constants.js new file mode 100644 index 00000000..f49bfefe --- /dev/null +++ b/static/js/utils/constants.js @@ -0,0 +1,24 @@ +export const BASE_MODELS = { + SD_1_5: "SD1.5", + SD_2_0: "SD2.0", + SD_2_1: "SD2.1", + SDXL: "SDXL", + FLUX_1_D: "Flux.1 D", + IL: "IL", + PONY: "Pony", + HUNYUAN_VIDEO: "Hunyuan Video", + UNKNOWN: "Unknown" +}; + +// Base model display names and their corresponding class names (for styling) +export const BASE_MODEL_CLASSES = { + [BASE_MODELS.SD_1_5]: "sd-1-5", + [BASE_MODELS.SD_2_0]: "sd-2-0", + [BASE_MODELS.SD_2_1]: "sd-2-1", + [BASE_MODELS.SDXL]: "sdxl", + [BASE_MODELS.FLUX_1_D]: "flux", + [BASE_MODELS.IL]: "il", + [BASE_MODELS.PONY]: "pony", + [BASE_MODELS.HUNYUAN_VIDEO]: "hunyuan", + [BASE_MODELS.UNKNOWN]: "unknown" +}; \ No newline at end of file diff --git a/templates/components/controls.html b/templates/components/controls.html index 455ac226..ef1d0332 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -35,6 +35,34 @@ + + + + + \ No newline at end of file