mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Add name pattern filtering to LoRA Pool node allowing users to filter LoRAs by filename or model name using either plain text or regex patterns. Features: - Include patterns: only show LoRAs matching at least one pattern - Exclude patterns: exclude LoRAs matching any pattern - Regex toggle: switch between substring and regex matching - Case-insensitive matching for both modes - Invalid regex automatically falls back to substring matching - Filters apply to both file_name and model_name fields Backend: - Update LoraPoolLM._default_config() with namePatterns structure - Add name pattern filtering to _apply_pool_filters() and _apply_specific_filters() - Add API parameter parsing for name_pattern_include/exclude/use_regex - Update LoraPoolConfig type with namePatterns field Frontend: - Add NamePatternsSection.vue component with pattern input UI - Update useLoraPoolState to manage pattern state and API integration - Update LoraPoolSummaryView to display NamePatternsSection - Increase LORA_POOL_WIDGET_MIN_HEIGHT to accommodate new UI Tests: - Add 7 test cases covering text/regex include, exclude, combined filtering, model name fallback, and invalid regex handling Closes #839
205 lines
6.0 KiB
TypeScript
205 lines
6.0 KiB
TypeScript
import { ref, computed, watch } from 'vue'
|
|
import type {
|
|
LoraPoolConfig,
|
|
BaseModelOption,
|
|
TagOption,
|
|
FolderTreeNode,
|
|
LoraItem,
|
|
ComponentWidget
|
|
} from './types'
|
|
import { useLoraPoolApi } from './useLoraPoolApi'
|
|
|
|
export function useLoraPoolState(widget: ComponentWidget<LoraPoolConfig>) {
|
|
const api = useLoraPoolApi()
|
|
|
|
// Flag to prevent infinite loops during config restoration
|
|
// callback → restoreFromConfig → watch → refreshPreview → buildConfig → widget.value = config → callback → ...
|
|
let isRestoring = false
|
|
|
|
// Filter state
|
|
const selectedBaseModels = ref<string[]>([])
|
|
const includeTags = ref<string[]>([])
|
|
const excludeTags = ref<string[]>([])
|
|
const includeFolders = ref<string[]>([])
|
|
const excludeFolders = ref<string[]>([])
|
|
const noCreditRequired = ref(false)
|
|
const allowSelling = ref(false)
|
|
const includePatterns = ref<string[]>([])
|
|
const excludePatterns = ref<string[]>([])
|
|
const useRegex = ref(false)
|
|
|
|
// Available options from API
|
|
const availableBaseModels = ref<BaseModelOption[]>([])
|
|
const availableTags = ref<TagOption[]>([])
|
|
const folderTree = ref<FolderTreeNode[]>([])
|
|
|
|
// Preview state
|
|
const previewItems = ref<LoraItem[]>([])
|
|
const matchCount = ref(0)
|
|
const isLoading = computed(() => api.isLoading.value)
|
|
|
|
// Build config from current state
|
|
const buildConfig = (): LoraPoolConfig => {
|
|
const config: LoraPoolConfig = {
|
|
version: 2,
|
|
filters: {
|
|
baseModels: selectedBaseModels.value,
|
|
tags: {
|
|
include: includeTags.value,
|
|
exclude: excludeTags.value
|
|
},
|
|
folders: {
|
|
include: includeFolders.value,
|
|
exclude: excludeFolders.value
|
|
},
|
|
license: {
|
|
noCreditRequired: noCreditRequired.value,
|
|
allowSelling: allowSelling.value
|
|
},
|
|
namePatterns: {
|
|
include: includePatterns.value,
|
|
exclude: excludePatterns.value,
|
|
useRegex: useRegex.value
|
|
}
|
|
},
|
|
preview: {
|
|
matchCount: matchCount.value,
|
|
lastUpdated: Date.now()
|
|
}
|
|
}
|
|
|
|
// Update widget value (this triggers callback for UI sync)
|
|
// Skip during restoration to prevent infinite loops:
|
|
// callback → restoreFromConfig → watch → refreshPreview → buildConfig → widget.value = config → callback → ...
|
|
if (!isRestoring) {
|
|
widget.value = config
|
|
}
|
|
return config
|
|
}
|
|
|
|
// Restore state from config
|
|
const restoreFromConfig = (config: LoraPoolConfig) => {
|
|
// Set flag to prevent buildConfig from triggering widget.value updates during restoration
|
|
// This breaks the infinite loop: callback → restoreFromConfig → watch → refreshPreview → buildConfig → widget.value = config → callback
|
|
isRestoring = true
|
|
|
|
try {
|
|
if (!config?.filters) return
|
|
|
|
const { filters, preview } = config
|
|
|
|
// Helper to update ref only if value changed
|
|
const updateIfChanged = <T>(refValue: { value: T }, newValue: T) => {
|
|
if (JSON.stringify(refValue.value) !== JSON.stringify(newValue)) {
|
|
refValue.value = newValue
|
|
}
|
|
}
|
|
|
|
updateIfChanged(selectedBaseModels, filters.baseModels || [])
|
|
updateIfChanged(includeTags, filters.tags?.include || [])
|
|
updateIfChanged(excludeTags, filters.tags?.exclude || [])
|
|
updateIfChanged(includeFolders, filters.folders?.include || [])
|
|
updateIfChanged(excludeFolders, filters.folders?.exclude || [])
|
|
updateIfChanged(noCreditRequired, filters.license?.noCreditRequired ?? false)
|
|
updateIfChanged(allowSelling, filters.license?.allowSelling ?? false)
|
|
updateIfChanged(includePatterns, filters.namePatterns?.include || [])
|
|
updateIfChanged(excludePatterns, filters.namePatterns?.exclude || [])
|
|
updateIfChanged(useRegex, filters.namePatterns?.useRegex ?? false)
|
|
|
|
// matchCount doesn't trigger watchers, so direct assignment is fine
|
|
matchCount.value = preview?.matchCount || 0
|
|
} finally {
|
|
isRestoring = false
|
|
}
|
|
}
|
|
|
|
// Fetch filter options from API
|
|
const fetchFilterOptions = async () => {
|
|
const [baseModels, tags, folders] = await Promise.all([
|
|
api.fetchBaseModels(),
|
|
api.fetchTags(),
|
|
api.fetchFolderTree()
|
|
])
|
|
|
|
availableBaseModels.value = baseModels
|
|
availableTags.value = tags
|
|
folderTree.value = folders
|
|
}
|
|
|
|
// Refresh preview with current filters
|
|
const refreshPreview = async () => {
|
|
const result = await api.fetchLoras({
|
|
baseModels: selectedBaseModels.value,
|
|
tagsInclude: includeTags.value,
|
|
tagsExclude: excludeTags.value,
|
|
foldersInclude: includeFolders.value,
|
|
foldersExclude: excludeFolders.value,
|
|
noCreditRequired: noCreditRequired.value || undefined,
|
|
allowSelling: allowSelling.value || undefined,
|
|
namePatternsInclude: includePatterns.value,
|
|
namePatternsExclude: excludePatterns.value,
|
|
namePatternsUseRegex: useRegex.value,
|
|
pageSize: 6
|
|
})
|
|
|
|
previewItems.value = result.items
|
|
matchCount.value = result.total
|
|
buildConfig()
|
|
}
|
|
|
|
// Debounced filter change handler
|
|
let filterTimeout: ReturnType<typeof setTimeout> | null = null
|
|
const onFilterChange = () => {
|
|
if (filterTimeout) clearTimeout(filterTimeout)
|
|
filterTimeout = setTimeout(() => {
|
|
refreshPreview()
|
|
}, 300)
|
|
}
|
|
|
|
// Watch all filter changes
|
|
watch([
|
|
selectedBaseModels,
|
|
includeTags,
|
|
excludeTags,
|
|
includeFolders,
|
|
excludeFolders,
|
|
noCreditRequired,
|
|
allowSelling,
|
|
includePatterns,
|
|
excludePatterns,
|
|
useRegex
|
|
], onFilterChange, { deep: true })
|
|
|
|
return {
|
|
// Filter state
|
|
selectedBaseModels,
|
|
includeTags,
|
|
excludeTags,
|
|
includeFolders,
|
|
excludeFolders,
|
|
noCreditRequired,
|
|
allowSelling,
|
|
includePatterns,
|
|
excludePatterns,
|
|
useRegex,
|
|
|
|
// Available options
|
|
availableBaseModels,
|
|
availableTags,
|
|
folderTree,
|
|
|
|
// Preview state
|
|
previewItems,
|
|
matchCount,
|
|
isLoading,
|
|
|
|
// Actions
|
|
buildConfig,
|
|
restoreFromConfig,
|
|
fetchFilterOptions,
|
|
refreshPreview
|
|
}
|
|
}
|
|
|
|
export type LoraPoolStateReturn = ReturnType<typeof useLoraPoolState>
|