mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-08 10:32:13 -03:00
Refreshing had no way to say "scan only this drive": a user with three external
drives had to spin all of them up for every refresh, and switching a drive off
made the next refresh treat its whole library as deleted (rows pruned from the
memory cache and the SQLite cache, preview_url stripped on the next scroll).
Backend (py/services/model_scanner.py, py/config.py):
* ReconcileScope(roots, folder) + _reconcile_cache(scope=...): files inside the
scope reconcile normally, everything outside is neither re-read nor removed.
The folder half is plumbing for the sidebar entry in the next change.
* Path-level pruning guard: cached entries under a path this walk could not
read are kept and reported instead of removed. Sources: a configured root that
is not reachable (drive switched off while LM runs), a directory os.walk
failed to enter (permissions / I/O error / Windows junction to an offline
drive), and a known first-level symlink whose target is gone
(Config.iter_path_mappings()).
* The recorded folder list is unioned instead of replaced whenever the scan did
not verify every root, so a scoped scan cannot empty the sidebar.
* _reconcile_cache returns a summary (added / removed / repaired /
scanned_roots / skipped_roots / unavailable_paths / kept_unreachable),
exposed as ModelScanner.last_reconcile_summary, returned by
BaseModelService.scan_models() and broadcast in the completed WS payload.
* _root_display_labels(): set-aware labels ("G: loras", "usb/loras") grown
leftwards with real parent segments until unique, shared by the walk-progress
line and the roots API.
* GET /scan accepts repeated `roots` (400 for unknown roots, 400 combined with
full_rebuild=true); GET /roots gains root_details (label / reachable / cached
count) while `roots` stays a plain path list for existing callers.
* serve_preview: a 404 no longer clears the cached preview_url when the file's
own directory is unreachable - browsing the grid with a drive off used to
strip preview references from the persistent cache.
Frontend:
* Refresh ▾ gains a "Scan one folder" section listing the page's roots with
their cached counts; offline roots stay clickable and explain themselves; rows
are wired by delegation (new static/js/components/controls/ScanScopeMenu.js).
* A scoped scan reports "Scanned <root>: N new, M removed"; a scan that kept
entries reports "<N> models kept: <paths> not reachable".
* registerAPI() now injects the two cross-page passthroughs (fetchModelRoots and
an argument-forwarding refreshModels) so a page facade cannot drop them: the
first version rendered an empty menu and would have run a full refresh.
* createToastElement whitelists toast types, so a wrong `type` argument degrades
to the info style instead of rendering an unstyled box.
Verified in a sandbox instance with three roots: a scoped scan walks only the
requested root (progress roots=0/1, 240 files); a full refresh with one root
offline reports kept_unreachable=60 and leaves all 420 models cached; /roots
reports the offline root with its cached count. 3699 passed, 7 skipped;
frontend 1488 passed (148 files); vue widgets 96 passed.
160 lines
5.8 KiB
JavaScript
160 lines
5.8 KiB
JavaScript
// LorasControls.js - Specific implementation for the LoRAs page
|
|
import { PageControls } from './PageControls.js';
|
|
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
|
import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
|
import { createAlphabetBar } from '../alphabet/index.js';
|
|
import { downloadManager } from '../../managers/DownloadManager.js';
|
|
|
|
/**
|
|
* LorasControls class - Extends PageControls for LoRA-specific functionality
|
|
*/
|
|
export class LorasControls extends PageControls {
|
|
constructor() {
|
|
// Initialize with 'loras' page type
|
|
super('loras');
|
|
|
|
// Register API methods specific to the LoRAs page
|
|
this.registerLorasAPI();
|
|
|
|
// Check for custom filters (e.g., from recipe navigation)
|
|
this.checkCustomFilters();
|
|
|
|
// Initialize alphabet bar component
|
|
this.initAlphabetBar();
|
|
}
|
|
|
|
/**
|
|
* Register LoRA-specific API methods
|
|
*/
|
|
registerLorasAPI() {
|
|
const lorasAPI = {
|
|
// Core API functions
|
|
loadMoreModels: async (resetPage = false, updateFolders = false) => {
|
|
return await getModelApiClient().loadMoreWithVirtualScroll(resetPage, updateFolders);
|
|
},
|
|
|
|
resetAndReload: async (updateFolders = false) => {
|
|
return await resetAndReload(updateFolders);
|
|
},
|
|
|
|
|
|
// LoRA-specific API functions
|
|
fetchFromCivitai: async () => {
|
|
return await getModelApiClient().fetchCivitaiMetadata();
|
|
},
|
|
|
|
showDownloadModal: () => {
|
|
downloadManager.showDownloadModal();
|
|
},
|
|
|
|
toggleBulkMode: () => {
|
|
if (window.bulkManager) {
|
|
window.bulkManager.toggleBulkMode();
|
|
} else {
|
|
console.error('Bulk manager not available');
|
|
}
|
|
},
|
|
|
|
clearCustomFilter: async () => {
|
|
await this.clearCustomFilter();
|
|
}
|
|
};
|
|
|
|
// Register the API
|
|
this.registerAPI(lorasAPI);
|
|
}
|
|
|
|
/**
|
|
* Check for custom filter parameters in session storage (e.g., from recipe page navigation)
|
|
*/
|
|
checkCustomFilters() {
|
|
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
|
|
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
|
|
const filterRecipeName = getSessionItem('filterRecipeName');
|
|
const viewLoraDetail = getSessionItem('viewLoraDetail');
|
|
|
|
if ((filterLoraHash || filterLoraHashes) && filterRecipeName) {
|
|
// Found custom filter parameters, set up the custom filter
|
|
|
|
// Show the filter indicator
|
|
const indicator = document.getElementById('customFilterIndicator');
|
|
const filterText = indicator?.querySelector('.customFilterText');
|
|
|
|
if (indicator && filterText) {
|
|
indicator.classList.remove('hidden');
|
|
|
|
// Set text content with recipe name
|
|
const filterType = filterLoraHash && viewLoraDetail ? "Viewing LoRA from" : "Viewing LoRAs from";
|
|
const displayText = `${filterType}: ${filterRecipeName}`;
|
|
|
|
filterText.textContent = this._truncateText(displayText, 30);
|
|
filterText.setAttribute('title', displayText);
|
|
|
|
// Add pulse animation
|
|
const filterElement = indicator.querySelector('.filter-active');
|
|
if (filterElement) {
|
|
filterElement.classList.add('animate');
|
|
setTimeout(() => filterElement.classList.remove('animate'), 600);
|
|
}
|
|
}
|
|
|
|
// If we're viewing a specific LoRA detail, set up to open the modal
|
|
if (filterLoraHash && viewLoraDetail) {
|
|
this.pageState.pendingLoraHash = filterLoraHash;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the custom filter and reload the page
|
|
*/
|
|
async clearCustomFilter() {
|
|
// Check for View Local Versions filter first (handles VLM and reloads)
|
|
const vlmModelId = getSessionItem('vlm_model_id');
|
|
if (vlmModelId) {
|
|
removeSessionItem('vlm_model_id');
|
|
removeSessionItem('vlm_model_name');
|
|
removeSessionItem('vlm_base_model');
|
|
removeSessionItem('vlm_page_type');
|
|
this._restoreSortAfterVlm();
|
|
const indicator = document.getElementById('customFilterIndicator');
|
|
if (indicator) {
|
|
indicator.classList.add('hidden');
|
|
}
|
|
await resetAndReload();
|
|
return;
|
|
}
|
|
|
|
console.log("Clearing custom filter...");
|
|
// Remove filter parameters from session storage
|
|
removeSessionItem('recipe_to_lora_filterLoraHash');
|
|
removeSessionItem('recipe_to_lora_filterLoraHashes');
|
|
removeSessionItem('filterRecipeName');
|
|
removeSessionItem('viewLoraDetail');
|
|
|
|
// Hide the filter indicator
|
|
const indicator = document.getElementById('customFilterIndicator');
|
|
if (indicator) {
|
|
indicator.classList.add('hidden');
|
|
}
|
|
|
|
// Reset state
|
|
if (this.pageState.pendingLoraHash) {
|
|
delete this.pageState.pendingLoraHash;
|
|
}
|
|
|
|
// Reload the loras
|
|
await resetAndReload();
|
|
}
|
|
|
|
/**
|
|
* Initialize the alphabet bar component
|
|
*/
|
|
initAlphabetBar() {
|
|
// Create the alphabet bar component
|
|
this.alphabetBar = createAlphabetBar('loras');
|
|
|
|
// Expose the alphabet bar to the global scope for debugging
|
|
window.alphabetBar = this.alphabetBar;
|
|
}
|
|
} |