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.
162 lines
11 KiB
HTML
162 lines
11 KiB
HTML
<div class="controls">
|
|
{% if page_id != 'recipes' %}
|
|
<div id="excludedViewBanner" class="excluded-view-banner hidden">
|
|
<div class="excluded-view-banner__content">
|
|
<div class="excluded-view-banner__title">
|
|
<i class="fas fa-eye-slash"></i>
|
|
<span>{{ t('globalContextMenu.manageExcludedModels.label', default='Excluded Models') }}</span>
|
|
</div>
|
|
<button id="excludedViewBackBtn" class="excluded-view-banner__back">
|
|
<i class="fas fa-arrow-left"></i>
|
|
<span>{{ t('common.actions.back', default='Back') }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<div class="actions">
|
|
<div class="action-buttons">
|
|
<div title="{% if page_id == 'recipes' %}{{ t('recipes.controls.sort.title') }}{% else %}{{ t('loras.controls.sort.title') }}{% endif %}" class="control-group sort-dropdown-group dropdown-group" data-sort-dropdown>
|
|
<button type="button" class="sort-trigger" aria-haspopup="listbox" aria-expanded="false">
|
|
<span class="sort-trigger__label"></span>
|
|
<i class="fas fa-caret-down sort-trigger__caret" aria-hidden="true"></i>
|
|
</button>
|
|
<div class="dropdown-menu sort-dropdown-menu" role="listbox"></div>
|
|
<select id="sortSelect" class="sort-select-native" tabindex="-1" aria-hidden="true">
|
|
<optgroup label="{{ t('loras.controls.sort.name') }}">
|
|
<option value="name:asc">{{ t('loras.controls.sort.nameAsc') }}</option>
|
|
<option value="name:desc">{{ t('loras.controls.sort.nameDesc') }}</option>
|
|
</optgroup>
|
|
<optgroup label="{% if page_id == 'recipes' %}{{ t('recipes.controls.sort.date') }}{% else %}{{ t('loras.controls.sort.date') }}{% endif %}">
|
|
<option value="date:desc">{{ t('loras.controls.sort.dateDesc') }}</option>
|
|
<option value="date:asc">{{ t('loras.controls.sort.dateAsc') }}</option>
|
|
</optgroup>
|
|
{% if page_id != 'recipes' %}
|
|
<optgroup label="{{ t('loras.controls.sort.size') }}">
|
|
<option value="size:desc">{{ t('loras.controls.sort.sizeDesc') }}</option>
|
|
<option value="size:asc">{{ t('loras.controls.sort.sizeAsc') }}</option>
|
|
</optgroup>
|
|
{% endif %}
|
|
{% if page_id != 'embeddings' and page_id != 'recipes' and page_id != 'other' %}
|
|
<optgroup label="{{ t('loras.controls.sort.usage', default='Usage') }}">
|
|
<option value="usage:desc">{{ t('loras.controls.sort.usageDesc', default='Times used (high to low)') }}</option>
|
|
<option value="usage:asc">{{ t('loras.controls.sort.usageAsc', default='Times used (low to high)') }}</option>
|
|
</optgroup>
|
|
{% endif %}
|
|
{% if page_id != 'recipes' %}
|
|
<optgroup class="sort-option-versions-count" label="{{ t('loras.controls.sort.versionsCount', default='Local Versions') }}">
|
|
<option value="versions_count:desc">{{ t('loras.controls.sort.versionsCountDesc', default='Most versions first') }}</option>
|
|
<option value="versions_count:asc">{{ t('loras.controls.sort.versionsCountAsc', default='Fewest versions first') }}</option>
|
|
</optgroup>
|
|
{% endif %}
|
|
{% if page_id == 'recipes' %}
|
|
<optgroup label="{{ t('recipes.controls.sort.lorasCount') }}">
|
|
<option value="loras_count:desc">{{ t('recipes.controls.sort.lorasCountDesc') }}</option>
|
|
<option value="loras_count:asc">{{ t('recipes.controls.sort.lorasCountAsc') }}</option>
|
|
</optgroup>
|
|
{% endif %}
|
|
{% if page_id == 'recipes' %}
|
|
<optgroup label="{{ t('recipes.controls.sort.opened', default='Recently Opened') }}">
|
|
<option value="opened:desc">{{ t('recipes.controls.sort.openedDesc', default='Recently opened') }}</option>
|
|
</optgroup>
|
|
{% endif %}
|
|
<optgroup label="{{ t('loras.controls.sort.random', default='Random') }}">
|
|
<option value="random">{{ t('loras.controls.sort.randomAction', default='Randomize (shuffle)') }}</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
<div title="{% if page_id == 'recipes' %}{{ t('recipes.controls.refresh.title') }}{% else %}{{ t('loras.controls.refresh.title') }}{% endif %}" class="control-group dropdown-group">
|
|
<button data-action="refresh" class="dropdown-main"><i class="fas fa-sync"></i> <span><span>{{ t('common.actions.refresh') }}</span> <kbd class="shortcut-key">R</kbd></span></button>
|
|
<button class="dropdown-toggle" aria-label="Show refresh options">
|
|
<i class="fas fa-caret-down"></i>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<div class="dropdown-item" data-action="full-rebuild" title="{% if page_id == 'recipes' %}{{ t('recipes.controls.refresh.fullTooltip', default='Rebuild cache - full rescan of all recipe files') }}{% else %}{{ t('loras.controls.refresh.fullTooltip') }}{% endif %}">
|
|
<i class="fas fa-tools"></i> <span>{% if page_id == 'recipes' %}{{ t('loras.controls.refresh.full', default='Rebuild Cache') }}{% else %}{{ t('loras.controls.refresh.full') }}{% endif %}</span>
|
|
</div>
|
|
{% if page_id != 'recipes' %}
|
|
<!-- Per-root scan scope, filled by PageControls.loadScanScopeMenu() -->
|
|
<div class="dropdown-separator"></div>
|
|
<div class="dropdown-section-title">{{ t('loras.controls.refresh.scopeSection') }}</div>
|
|
<div id="refreshScopeMenu" class="dropdown-scope-list"></div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if page_id != 'recipes' %}
|
|
<div class="control-group">
|
|
<button data-action="fetch" title="{{ t('loras.controls.fetch.title') }}"><i class="fas fa-download"></i> <span><span>{{ t('loras.controls.fetch.action') }}</span> <kbd class="shortcut-key">F</kbd></span></button>
|
|
</div>
|
|
<div class="control-group">
|
|
<button data-action="download" title="{{ t('loras.controls.download.title') }}">
|
|
<i class="fas fa-cloud-download-alt"></i> <span><span>{{ t('loras.controls.download.action') }}</span> <kbd class="shortcut-key">D</kbd></span>
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
{% if page_id == 'recipes' %}
|
|
<div title="{{ t('recipes.controls.import.title') }}" class="control-group">
|
|
<button onclick="importManager.showImportModal()"><i class="fas fa-file-import"></i> {{ t('recipes.controls.import.action') }}</button>
|
|
</div>
|
|
<div title="{{ t('recipes.batchImport.title') }}" class="control-group">
|
|
<button onclick="batchImportManager.showModal()"><i class="fas fa-layer-group"></i> {{ t('recipes.batchImport.action') }}</button>
|
|
</div>
|
|
{% endif %}
|
|
<div class="control-group">
|
|
<button id="bulkOperationsBtn" data-action="bulk" title="{{ t('loras.controls.bulk.title') }}">
|
|
<i class="fas fa-th-large"></i> <span><span>{{ t('loras.controls.bulk.action') }}</span> <kbd class="shortcut-key">B</kbd></span>
|
|
</button>
|
|
</div>
|
|
<div class="control-group">
|
|
<button id="findDuplicatesBtn" data-action="find-duplicates" title="{{ t('loras.controls.duplicates.title') }}">
|
|
<i class="fas fa-clone"></i> <span>{{ t('loras.controls.duplicates.action') }}</span>
|
|
<span id="duplicatesBadge" class="badge"></span>
|
|
</button>
|
|
</div>
|
|
<div class="control-group">
|
|
<button id="favoriteFilterBtn" data-action="toggle-favorites" class="favorite-filter" title="{{ t('loras.controls.favorites.title') }}">
|
|
<i class="fas fa-star"></i> <span>{{ t('loras.controls.favorites.action') }}</span>
|
|
</button>
|
|
</div>
|
|
{% if page_id != 'recipes' %}
|
|
<div class="control-group dropdown-group update-filter-group">
|
|
<button id="updateFilterBtn" data-action="toggle-updates" class="dropdown-main update-filter" title="{{ t('loras.controls.updates.title') }}">
|
|
<i class="fas fa-exclamation-circle"></i> <span>{{ t('loras.controls.updates.action') }}</span>
|
|
</button>
|
|
<button id="updateFilterMenuToggle" class="dropdown-toggle" aria-label="{{ t('loras.controls.updates.menuLabel') }}">
|
|
<i class="fas fa-caret-down"></i>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<div id="checkUpdatesMenuItem" class="dropdown-item" data-action="check-updates" title="{{ t('loras.controls.updates.checkTooltip') }}">
|
|
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.controls.updates.check') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<div id="customFilterIndicator" class="control-group hidden">
|
|
<div class="filter-active">
|
|
<i class="fas fa-filter"></i> <span class="customFilterText" title=""></span>
|
|
<i class="fas fa-times-circle clear-filter"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls-right">
|
|
{% if page_id == 'recipes' %}
|
|
<div class="control-group layout-toggle-group" role="group" aria-label="{{ t('recipes.controls.layout.title') }}" title="{{ t('recipes.controls.layout.title') }}">
|
|
<button type="button" class="layout-toggle-btn" data-recipes-layout="grid" aria-pressed="false" title="{{ t('recipes.controls.layout.grid') }}" aria-label="{{ t('recipes.controls.layout.grid') }}">
|
|
<i class="fas fa-th-large" aria-hidden="true"></i>
|
|
</button>
|
|
<button type="button" class="layout-toggle-btn" data-recipes-layout="masonry" aria-pressed="false" title="{{ t('recipes.controls.layout.masonry') }}" aria-label="{{ t('recipes.controls.layout.masonry') }}">
|
|
<i class="fas fa-columns" aria-hidden="true"></i>
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
<div class="control-group doctor-control-group">
|
|
<button id="doctorTriggerBtn" class="doctor-trigger" title="{{ t('doctor.buttonTitle', default='Run diagnostics and common fixes') }}">
|
|
<i class="fas fa-stethoscope"></i>
|
|
<span>{{ t('doctor.title', default='Doctor') }}</span>
|
|
<span id="doctorStatusBadge" class="doctor-status-badge hidden" aria-hidden="true"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div> |