Files
ComfyUI-Lora-Manager/static/js/components/controls/PageControls.js
T
Will Miao e42d649df0 refactor(update)!: make obtainability a property of updates, not a surface
The owner could not tell from the UI what "Buzz Price Tracking" enabled, what the
"Price alert threshold" number meant, or what "Price Alerts" was alerting about.
That was not a copy problem: the implementation exposed our mechanism (a page
scrape) and our SQL predicates as the user's concepts. Two concrete defects came
from the same root:

- the alert population included versions the user already owns (neither the event
  generator nor the query filtered on is_in_library; in the owner's library 28 of
  52 gated versions were already downloaded, so most "alerts" were about files
  already on disk, which cannot become cheaper *for them*);
- a threshold-filtered state list lived in a notification surface, so an empty
  panel had three indistinguishable causes and read as a broken feature.

The information model is now the version plus ownership: cost is shown only where
a decision exists. Owned -> nothing. Not owned and free -> nothing. Not owned and
gated -> the price when it is known, `Paid` without a number when it is not, and
early access keeps its countdown because "free on <date>" decides between waiting
and paying. The numeric threshold has no place in that model: every decision is
categorical (wait / pay / skip), so the setting, the comparison and the whole
alert-state machine are gone.

- both alert-state columns are removed from the schema rather than left dead; a
  database created by an unreleased build has them dropped on open (native
  ALTER TABLE ... DROP COLUMN, guarded), which is a no-op for everyone else
- gate events are emitted only for versions the user does not have, and the
  price-drop event goes with the threshold it belonged to
- both alert endpoints, PriceAlertsHandler and the service-registry adapter field
  it needed are removed: events already reach the UI through the refresh response
- the bell tab, panel, CSS, both entry points, the unread watermark and their
  locales are removed; the setting keeps only the enable flag and the refresh
  interval and is framed as plumbing
- "Price unavailable" is replaced by `Paid`: the gate is certain from the public
  API, only the number is best-effort, and that is our plumbing, not the user's
  problem

Verified against a copy of the owner's real database: 52 gated versions ->
28 owned (now silent) + 24 the feature is actually about; the drop migration ran
and both removed endpoints 404.
2026-10-05 08:54:08 +08:00

1089 lines
38 KiB
JavaScript

// PageControls.js - Manages controls for both LoRAs and Checkpoints pages
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js';
import { getStorageItem, setStorageItem, removeStorageItem, getSessionItem, setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
import { showToast, openCivitaiByMetadata, isTypingContext } from '../../utils/uiHelpers.js';
import { eventManager } from '../../utils/EventManager.js';
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
import { sidebarManager } from '../SidebarManager.js';
import { initSortDropdown, applySortToSelect, randomizeSortValue } from './SortDropdown.js';
/**
* PageControls class - Unified control management for model pages
*/
export class PageControls {
constructor(pageType) {
// Set the current page type in state
setCurrentPageType(pageType);
// Store the page type
this.pageType = pageType;
// Get the current page state
this.pageState = getCurrentPageState();
// Initialize state based on page type
this.initializeState();
// Store API methods
this.api = null;
// Use global sidebar manager
this.sidebarManager = sidebarManager;
this._updateCheckInProgress = false;
// Initialize event listeners
this.initEventListeners();
// Initialize update availability filter button state
this.initUpdateAvailableFilter();
// Initialize favorites filter button state
this.initFavoritesFilter();
this.initExcludedViewControls();
this.syncExcludedViewState();
console.log(`PageControls initialized for ${pageType} page`);
window.pageControls = this;
}
/**
* Initialize state based on page type
*/
initializeState() {
// Set default values
this.pageState.pageSize = 100;
this.pageState.isLoading = false;
this.pageState.hasMore = true;
// Set default sort based on page type
this.pageState.sortBy = this.pageType === 'loras' ? 'name:asc' : 'name:asc';
// Load sort preference
this.loadSortPreference();
if (!this.pageState.viewMode) {
this.pageState.viewMode = 'active';
}
if (!this.pageState.excludedViewState) {
this.pageState.excludedViewState = {
sortBy: 'name:asc',
search: '',
};
}
if (!this.pageState.filters?.search) {
this.pageState.filters.search = '';
}
}
/**
* Register API methods for the page
* @param {Object} api - API methods for the page
*/
registerAPI(api) {
this.api = api;
console.log(`API methods registered for ${this.pageType} page`);
// Initialize sidebar manager after API is registered
this.initSidebarManager();
}
/**
* Initialize sidebar manager
*/
async initSidebarManager() {
try {
this.sidebarManager.setHostPageControls(this);
await this.sidebarManager.initialize(this);
} catch (error) {
console.error('Failed to initialize SidebarManager:', error);
}
}
/**
* Initialize event listeners for controls
*/
initEventListeners() {
// Sort select handler
const sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
initSortDropdown(sortSelect);
applySortToSelect(this.pageState.sortBy);
sortSelect.addEventListener('change', async (e) => {
let value = e.target.value;
if (value.startsWith('random')) {
// Every pick of Random reshuffles the list: generate a
// fresh seed so the backend keeps a stable order across
// paginated requests.
value = randomizeSortValue();
}
this.pageState.sortBy = value;
this.saveSortPreference(value);
// Reset the seeded Random option when switching away from
// Random, or re-apply the fresh seed when picking it again.
applySortToSelect(value);
await this.resetAndReload();
});
}
// Refresh button handler
const refreshBtn = document.querySelector('[data-action="refresh"]');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.refreshModels(false)); // Regular refresh (incremental)
}
// Initialize dropdown functionality
this.initDropdowns();
// Clear custom filter handler
const clearFilterBtn = document.querySelector('.clear-filter');
if (clearFilterBtn) {
clearFilterBtn.addEventListener('click', () => this.clearCustomFilter());
}
// Check for View Local Versions filter
this.checkVlmFilter();
// Page-specific event listeners
this.initPageSpecificListeners();
// Keyboard shortcuts for the actions toolbar (R / F / D)
this.registerKeyboardShortcuts();
}
/**
* Register keyboard shortcuts for the actions toolbar buttons
* (R = refresh, F = fetch metadata, D = download)
*/
registerKeyboardShortcuts() {
eventManager.addHandler('keydown', 'pageControls-actions', (e) => {
return this.handleActionShortcut(e);
}, {
priority: 90,
skipWhenModalOpen: true
});
}
/**
* Handle a keydown event for the actions toolbar shortcuts
* @param {KeyboardEvent} e
* @returns {boolean} True when the event was handled and propagation should stop
*/
handleActionShortcut(e) {
// Plain letters only — leave modified combos (Ctrl/Cmd/Alt) alone
if (e.ctrlKey || e.metaKey || e.altKey) {
return false;
}
// Don't hijack keys while typing in a text entry context
if (isTypingContext(e.target)) {
return false;
}
const actionByKey = {
r: 'refresh',
f: 'fetch',
d: 'download'
};
const action = actionByKey[e.key.toLowerCase()];
if (!action) {
return false;
}
// The button may not exist on this page (e.g. recipes has no
// fetch/download) — let other handlers run in that case
const button = document.querySelector(`[data-action="${action}"]`);
if (!button) {
return false;
}
e.preventDefault();
// Native disabled buttons ignore .click(), so an in-progress
// refresh is safe
button.click();
return true;
}
initExcludedViewControls() {
const backButton = document.getElementById('excludedViewBackBtn');
if (backButton) {
backButton.addEventListener('click', async () => {
await this.exitExcludedView();
});
}
}
/**
* Initialize dropdown functionality
*/
initDropdowns() {
// Handle dropdown toggles
const dropdownToggles = document.querySelectorAll('.dropdown-toggle');
dropdownToggles.forEach(toggle => {
toggle.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent triggering parent button
const dropdownGroup = toggle.closest('.dropdown-group');
// Close all other open dropdowns first
document.querySelectorAll('.dropdown-group.active').forEach(group => {
if (group !== dropdownGroup) {
group.classList.remove('active');
}
});
// Toggle current dropdown
dropdownGroup.classList.toggle('active');
});
});
// Handle full rebuild option
const fullRebuildOption = document.querySelector('[data-action="full-rebuild"]');
if (fullRebuildOption) {
fullRebuildOption.addEventListener('click', (e) => {
e.stopPropagation();
this.refreshModels(true);
// Close the dropdown
document.querySelector('.dropdown-group.active')?.classList.remove('active');
});
}
const checkUpdatesOption = document.getElementById('checkUpdatesMenuItem');
if (checkUpdatesOption) {
checkUpdatesOption.addEventListener('click', async (e) => {
e.stopPropagation();
await this.handleCheckModelUpdates(e.currentTarget);
});
}
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.dropdown-group')) {
document.querySelectorAll('.dropdown-group.active').forEach(group => {
group.classList.remove('active');
});
}
});
}
async handleCheckModelUpdates(menuItem) {
if (this._updateCheckInProgress) {
return;
}
const updateFilterBtn = document.getElementById('updateFilterBtn');
const dropdownToggle = document.getElementById('updateFilterMenuToggle');
const dropdownGroup = menuItem?.closest('.dropdown-group');
const iconElement = updateFilterBtn?.querySelector('i');
const setLoadingState = (isLoading) => {
if (updateFilterBtn) {
updateFilterBtn.disabled = isLoading;
updateFilterBtn.classList.toggle('loading', isLoading);
updateFilterBtn.setAttribute('aria-busy', isLoading ? 'true' : 'false');
if (iconElement) {
if (isLoading) {
if (!iconElement.dataset.originalClass) {
iconElement.dataset.originalClass = iconElement.className;
}
iconElement.className = 'fas fa-spinner fa-spin';
} else {
const originalClass = iconElement.dataset.originalClass;
if (originalClass) {
iconElement.className = originalClass;
delete iconElement.dataset.originalClass;
} else {
iconElement.classList.remove('fa-spinner', 'fa-spin');
if (!iconElement.classList.contains('fa-exclamation-circle')) {
iconElement.classList.add('fa-exclamation-circle');
}
}
}
}
}
if (dropdownToggle) {
dropdownToggle.disabled = isLoading;
dropdownToggle.classList.toggle('loading', isLoading);
}
if (menuItem) {
menuItem.classList.toggle('disabled', isLoading);
if (isLoading) {
menuItem.setAttribute('aria-disabled', 'true');
} else {
menuItem.removeAttribute('aria-disabled');
}
}
};
this._updateCheckInProgress = true;
setLoadingState(true);
const handleComplete = () => {
this._updateCheckInProgress = false;
setLoadingState(false);
};
try {
await performModelUpdateCheck({
onComplete: handleComplete,
});
} catch (error) {
console.error('Failed to check model updates:', error);
} finally {
if (this._updateCheckInProgress) {
this._updateCheckInProgress = false;
setLoadingState(false);
}
dropdownGroup?.classList.remove('active');
}
}
/**
* Initialize page-specific event listeners
*/
initPageSpecificListeners() {
// Fetch from Civitai button - available for both loras and checkpoints
const fetchButton = document.querySelector('[data-action="fetch"]');
if (fetchButton) {
fetchButton.addEventListener('click', () => this.fetchFromCivitai());
}
const downloadButton = document.querySelector('[data-action="download"]');
if (downloadButton) {
downloadButton.addEventListener('click', () => this.showDownloadModal());
}
// Find duplicates button - available for both loras and checkpoints
const duplicatesButton = document.querySelector('[data-action="find-duplicates"]');
if (duplicatesButton) {
duplicatesButton.addEventListener('click', () => this.findDuplicates());
}
const bulkButton = document.querySelector('[data-action="bulk"]');
if (bulkButton) {
bulkButton.addEventListener('click', () => this.toggleBulkMode());
}
// Favorites filter button handler
const favoriteFilterBtn = document.getElementById('favoriteFilterBtn');
if (favoriteFilterBtn) {
favoriteFilterBtn.addEventListener('click', () => this.toggleFavoritesOnly());
}
const updateFilterBtn = document.getElementById('updateFilterBtn');
if (updateFilterBtn) {
updateFilterBtn.addEventListener('click', () => this.toggleUpdateAvailableOnly());
}
}
/**
* Load sort preference from storage
*/
loadSortPreference() {
// Use separate keys for grouped vs non-grouped sort so each mode
// remembers its own preference independently
const key = state.global.settings.group_by_model
? `${this.pageType}_sort_grouped`
: `${this.pageType}_sort`;
const savedSort = getStorageItem(key);
if (savedSort) {
// Handle legacy format conversion
const convertedSort = this.convertLegacySortFormat(savedSort);
this.pageState.sortBy = convertedSort;
applySortToSelect(convertedSort);
}
}
/**
* Convert legacy sort format to new format
* @param {string} sortValue - The sort value to convert
* @returns {string} - Converted sort value
*/
convertLegacySortFormat(sortValue) {
// Convert old format to new format with direction
switch (sortValue) {
case 'name':
return 'name:asc';
case 'date':
return 'date:desc'; // Newest first is more intuitive default
case 'size':
return 'size:desc'; // Largest first is more intuitive default
default:
// If it's already in new format or unknown, return as is
return sortValue.includes(':') ? sortValue : 'name:asc';
}
}
/**
* Save sort preference to storage
* @param {string} sortValue - The sort value to save
*/
saveSortPreference(sortValue) {
if (this.pageState.viewMode === 'excluded') {
this.pageState.excludedViewState = {
...(this.pageState.excludedViewState || {}),
sortBy: sortValue,
};
return;
}
// Separate storage for grouped vs non-grouped sort
const key = state.global.settings.group_by_model
? `${this.pageType}_sort_grouped`
: `${this.pageType}_sort`;
setStorageItem(key, sortValue);
}
/**
* Open model page on Civitai
* @param {string} modelName - Name of the model
*/
openCivitai(modelName) {
// Get card selector based on page type
const cardSelector = this.pageType === 'loras'
? `.model-card[data-name="${modelName}"]`
: `.checkpoint-card[data-name="${modelName}"]`;
const card = document.querySelector(cardSelector);
if (!card) return;
const metaData = JSON.parse(card.dataset.meta);
const civitaiId = metaData.modelId;
const versionId = metaData.id;
openCivitaiByMetadata(civitaiId, versionId, modelName);
}
/**
* Reset and reload the models list
*/
async resetAndReload(updateFolders = false) {
if (!this.api) {
console.error('API methods not registered');
return;
}
try {
await this.api.resetAndReload(updateFolders);
// Refresh sidebar after reload if folders were updated
if (updateFolders && this.sidebarManager) {
await this.sidebarManager.refresh();
}
} catch (error) {
console.error(`Error reloading ${this.pageType}:`, error);
showToast('toast.controls.reloadFailed', { pageType: this.pageType, message: error.message }, 'error');
}
}
/**
* Refresh models list
* @param {boolean} fullRebuild - Whether to perform a full rebuild
*/
async refreshModels(fullRebuild = false) {
if (!this.api) {
console.error('API methods not registered');
return;
}
try {
await this.api.refreshModels(fullRebuild);
// Refresh sidebar after rebuild
if (this.sidebarManager) {
await this.sidebarManager.refresh();
}
} catch (error) {
console.error(`Error ${fullRebuild ? 'rebuilding' : 'refreshing'} ${this.pageType}:`, error);
showToast('toast.controls.refreshFailed', { action: fullRebuild ? 'rebuild' : 'refresh', pageType: this.pageType, message: error.message }, 'error');
}
if (window.modelDuplicatesManager) {
// Update duplicates badge after refresh
window.modelDuplicatesManager.updateDuplicatesBadgeAfterRefresh();
}
}
/**
* Fetch metadata from Civitai (available for both LoRAs and Checkpoints)
*/
async fetchFromCivitai() {
if (!this.api) {
console.error('API methods not registered');
return;
}
try {
await this.api.fetchFromCivitai();
} catch (error) {
console.error('Error fetching metadata:', error);
showToast('toast.controls.fetchMetadataFailed', { message: error.message }, 'error');
}
}
/**
* Show download modal
*/
showDownloadModal() {
this.api.showDownloadModal();
}
/**
* Toggle bulk mode
*/
toggleBulkMode() {
this.api.toggleBulkMode();
}
/**
* Clear custom filter
*/
/**
* Dynamically add the VLM sort option (version_id:desc) to the sort dropdown.
* It is not a permanent option — only present while VLM is active.
*/
_addVlmSortOption() {
const sortSelect = document.getElementById('sortSelect');
if (!sortSelect) return;
// Only add if not already present
if (sortSelect.querySelector('option[value="version_id:desc"]')) return;
const opt = document.createElement('option');
opt.value = 'version_id:desc';
opt.textContent = this._t('loras.controls.sort.versionIdDesc', 'Newest version first');
sortSelect.appendChild(opt);
}
/**
* Remove the VLM sort option from the sort dropdown.
*/
_removeVlmSortOption() {
const sortSelect = document.getElementById('sortSelect');
if (!sortSelect) return;
const opt = sortSelect.querySelector('option[value="version_id:desc"]');
if (opt) opt.remove();
}
/**
* Look up a translation key via the global i18n helper, falling back to
* a plain-text default when the key is missing or i18n is unavailable.
*/
_t(key, fallback) {
if (typeof window.i18n?.t === 'function') {
return window.i18n.t(key, { defaultValue: fallback });
}
return fallback;
}
/**
* Restore the sort dropdown state after VLM is cleared.
* Shared by PageControls.clearCustomFilter() and subclass overrides.
*/
_restoreSortAfterVlm() {
const prevSort = getSessionItem('vlm_prev_sort');
removeSessionItem('vlm_prev_sort');
const restoredSort = prevSort || 'name:asc';
this.pageState.sortBy = restoredSort;
this.saveSortPreference(restoredSort);
this._removeVlmSortOption();
applySortToSelect(restoredSort);
const sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
sortSelect.disabled = false;
}
}
/**
* Trigger View Local Versions without page reload
* Sets sessionStorage and reloads data via the API.
*/
triggerVlmView(modelId, modelName, baseModel, pageType) {
const targetPageType = pageType || this.pageType;
setSessionItem('vlm_model_id', String(modelId));
setSessionItem('vlm_model_name', modelName || String(modelId));
setSessionItem('vlm_page_type', targetPageType);
if (baseModel) {
setSessionItem('vlm_base_model', baseModel);
} else {
removeSessionItem('vlm_base_model');
}
// Save current sort preference so it can be restored when VLM is cleared
setSessionItem('vlm_prev_sort', this.pageState.sortBy);
// Inject the temporary sort option and force version_id:desc
this._addVlmSortOption();
this.pageState.sortBy = 'version_id:desc';
this.saveSortPreference('version_id:desc');
const sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
sortSelect.value = 'version_id:desc';
sortSelect.disabled = true;
}
// Reload data via API (no page reload)
this.resetAndReload(true).then(() => {
// Show the VLM indicator after data loads
this.checkVlmFilter();
});
}
/**
* Called when group_by_model is toggled.
* Swaps between {pageType}_sort (non-group) and {pageType}_sort_grouped,
* so each mode remembers its own sort preference independently.
*/
onGroupByModelToggled(isEnabled) {
const groupedKey = `${this.pageType}_sort_grouped`;
if (isEnabled) {
// Entering group mode: restore last-used grouped sort, if any
const savedGroupedSort = getStorageItem(groupedKey);
if (savedGroupedSort) {
this.pageState.sortBy = savedGroupedSort;
applySortToSelect(savedGroupedSort);
}
} else {
// Leaving group mode: persist current sort for next time, restore non-group sort
setStorageItem(groupedKey, this.pageState.sortBy);
const savedNormalSort = getStorageItem(`${this.pageType}_sort`);
if (savedNormalSort) {
this.pageState.sortBy = savedNormalSort;
applySortToSelect(savedNormalSort);
}
}
}
/**
* Check for View Local Versions filter in sessionStorage (page-type-scoped)
*/
checkVlmFilter() {
const vlmModelId = getSessionItem('vlm_model_id');
const vlmPageType = getSessionItem('vlm_page_type');
const sortSelect = document.getElementById('sortSelect');
// Only show VLM indicator when it belongs to the current page type
if (vlmModelId && vlmPageType !== this.pageType) {
// Stale VLM data from a different page — clean up
removeSessionItem('vlm_model_id');
removeSessionItem('vlm_model_name');
removeSessionItem('vlm_base_model');
removeSessionItem('vlm_page_type');
removeSessionItem('vlm_prev_sort');
this._removeVlmSortOption();
if (sortSelect) sortSelect.disabled = false;
return;
}
const vlmModelName = getSessionItem('vlm_model_name');
const vlmBaseModel = getSessionItem('vlm_base_model');
if (vlmModelId && vlmModelName) {
// VLM is active — inject sort option, disable dropdown, show indicator
this._addVlmSortOption();
if (sortSelect) {
sortSelect.value = 'version_id:desc';
sortSelect.disabled = true;
}
const indicator = document.getElementById('customFilterIndicator');
const filterText = indicator?.querySelector('.customFilterText');
if (indicator && filterText) {
indicator.classList.remove('hidden');
const prefix = vlmBaseModel
? 'Showing same-base versions from'
: 'Showing all versions from';
const displayText = `${prefix}: ${vlmModelName}`;
filterText.textContent = this._truncateText(displayText, 40);
filterText.setAttribute('title', displayText);
}
} else {
// No VLM — ensure sort option is removed and dropdown is enabled
this._removeVlmSortOption();
if (sortSelect) sortSelect.disabled = false;
}
}
/**
* Clear custom filter
*/
async clearCustomFilter() {
// Check for View Local Versions filter first
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();
// Hide the indicator
const indicator = document.getElementById('customFilterIndicator');
if (indicator) {
indicator.classList.add('hidden');
}
// Reload data via API (no page reload)
await this.resetAndReload(true);
return;
}
// Otherwise delegate to subclass for recipe filters
if (!this.api) {
console.error('API methods not registered');
return;
}
try {
await this.api.clearCustomFilter();
} catch (error) {
console.error('Error clearing custom filter:', error);
showToast('toast.controls.clearFilterFailed', { message: error.message }, 'error');
}
}
/**
* Truncate text with ellipsis
*/
_truncateText(text, maxLength) {
if (!text) return '';
return text.length > maxLength ? `${text.substring(0, maxLength - 3)}...` : text;
}
/**
* Initialize the favorites filter button state
*/
initFavoritesFilter() {
const favoriteFilterBtn = document.getElementById('favoriteFilterBtn');
if (favoriteFilterBtn) {
// Get current state from session storage with page-specific key
const storageKey = `show_favorites_only_${this.pageType}`;
const showFavoritesOnly = getSessionItem(storageKey, false);
// Update button state
if (showFavoritesOnly) {
favoriteFilterBtn.classList.add('active');
}
// Update app state
this.pageState.showFavoritesOnly = showFavoritesOnly;
}
this.updateActionButtonStates();
}
/**
* Initialize update availability filter button state
*/
initUpdateAvailableFilter() {
const storageKey = `show_update_available_only_${this.pageType}`;
const storedValue = getSessionItem(storageKey, false);
const showUpdatesOnly = storedValue === true || storedValue === 'true';
this.pageState.showUpdateAvailableOnly = showUpdatesOnly;
const updateFilterBtn = document.getElementById('updateFilterBtn');
if (updateFilterBtn) {
updateFilterBtn.classList.toggle('active', showUpdatesOnly);
}
this.updateActionButtonStates();
}
/**
* Toggle favorites-only filter and reload models
*/
async toggleFavoritesOnly() {
if (this.pageState.viewMode === 'excluded') {
return;
}
const favoriteFilterBtn = document.getElementById('favoriteFilterBtn');
// Toggle the filter state in storage
const storageKey = `show_favorites_only_${this.pageType}`;
const currentState = this.pageState.showFavoritesOnly;
const newState = !currentState;
// Update session storage
setSessionItem(storageKey, newState);
// Update state
this.pageState.showFavoritesOnly = newState;
// Update button appearance
if (favoriteFilterBtn) {
favoriteFilterBtn.classList.toggle('active', newState);
}
// Reload models with new filter
await this.resetAndReload(true);
}
/**
* Toggle update-available-only filter and reload models
*/
async toggleUpdateAvailableOnly() {
if (this.pageState.viewMode === 'excluded') {
return;
}
const updateFilterBtn = document.getElementById('updateFilterBtn');
const storageKey = `show_update_available_only_${this.pageType}`;
const newState = !this.pageState.showUpdateAvailableOnly;
setSessionItem(storageKey, newState);
this.pageState.showUpdateAvailableOnly = newState;
if (updateFilterBtn) {
updateFilterBtn.classList.toggle('active', newState);
}
await this.resetAndReload(true);
}
cloneFilters(filters = this.pageState.filters) {
return JSON.parse(JSON.stringify(filters || {}));
}
buildExcludedFilters(search = '') {
return {
baseModel: [],
tags: {},
license: {},
modelTypes: [],
search,
tagLogic: 'any',
};
}
applyFilterState(filters) {
this.pageState.filters = filters;
if (window.filterManager) {
window.filterManager.filters = window.filterManager.initializeFilters(filters);
window.filterManager.updateActiveFiltersCount();
if (typeof window.filterManager.updateSelections === 'function') {
window.filterManager.updateSelections();
}
window.filterManager.closeFilterPanel();
}
}
updateActionButtonStates() {
const favoriteFilterBtn = document.getElementById('favoriteFilterBtn');
if (favoriteFilterBtn) {
favoriteFilterBtn.classList.toggle('active', Boolean(this.pageState.showFavoritesOnly));
}
const updateFilterBtn = document.getElementById('updateFilterBtn');
if (updateFilterBtn) {
updateFilterBtn.classList.toggle('active', Boolean(this.pageState.showUpdateAvailableOnly));
}
}
syncExcludedViewState() {
const isExcludedView = this.pageState.viewMode === 'excluded';
const sortSelect = document.getElementById('sortSelect');
const searchInput = document.getElementById('searchInput');
const excludedBanner = document.getElementById('excludedViewBanner');
const filterButton = document.getElementById('filterButton');
const breadcrumbContainer = document.getElementById('breadcrumbContainer');
const duplicatesBanner = document.getElementById('duplicatesBanner');
const alphabetBarContainer = document.querySelector('.alphabet-bar-container');
const hiddenSelectors = [
'[data-action="fetch"]',
'[data-action="download"]',
'[data-action="bulk"]',
'[data-action="find-duplicates"]',
'#favoriteFilterBtn',
'.update-filter-group',
];
const customFilterIndicator = document.getElementById('customFilterIndicator');
document.body.classList.toggle('excluded-view-active', isExcludedView);
excludedBanner?.classList.toggle('hidden', !isExcludedView);
breadcrumbContainer?.classList.toggle('hidden', isExcludedView);
alphabetBarContainer?.classList.toggle('hidden', isExcludedView);
if (duplicatesBanner && isExcludedView) {
duplicatesBanner.style.display = 'none';
}
hiddenSelectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((element) => {
element.classList.toggle('hidden', isExcludedView);
});
});
if (customFilterIndicator && isExcludedView) {
customFilterIndicator.classList.add('hidden');
}
if (filterButton) {
filterButton.disabled = isExcludedView;
filterButton.classList.toggle('hidden', isExcludedView);
}
const activeFiltersCount = document.getElementById('activeFiltersCount');
if (activeFiltersCount && isExcludedView) {
activeFiltersCount.style.display = 'none';
}
if (sortSelect) {
applySortToSelect(this.pageState.sortBy);
}
if (searchInput) {
searchInput.value = this.pageState.filters?.search || '';
}
this.updateActionButtonStates();
}
suspendInteractiveModes() {
const snapshot = {
bulkMode: Boolean(state.bulkMode),
duplicatesMode: Boolean(this.pageState.duplicatesMode),
};
if (snapshot.bulkMode && window.bulkManager?.toggleBulkMode) {
window.bulkManager.toggleBulkMode();
}
if (snapshot.duplicatesMode && window.modelDuplicatesManager?.exitDuplicateMode) {
window.modelDuplicatesManager.exitDuplicateMode();
}
return snapshot;
}
async restoreInteractiveModes(snapshot = {}) {
if (snapshot.bulkMode && !state.bulkMode && window.bulkManager?.toggleBulkMode) {
window.bulkManager.toggleBulkMode();
}
if (!snapshot.duplicatesMode || this.pageState.duplicatesMode) {
return;
}
const duplicatesManager = window.modelDuplicatesManager;
if (!duplicatesManager) {
return;
}
if (typeof duplicatesManager.enterDuplicateMode === 'function' &&
Array.isArray(duplicatesManager.duplicateGroups) &&
duplicatesManager.duplicateGroups.length > 0) {
duplicatesManager.enterDuplicateMode();
return;
}
if (typeof duplicatesManager.findDuplicates === 'function') {
await duplicatesManager.findDuplicates();
}
}
syncCustomFilterIndicator() {
const indicator = document.getElementById('customFilterIndicator');
if (!indicator) {
return;
}
if (this.pageState.viewMode === 'excluded') {
indicator.classList.add('hidden');
return;
}
if (typeof this.checkCustomFilters === 'function') {
this.checkCustomFilters();
}
}
async enterExcludedView() {
if (this.pageState.viewMode === 'excluded') {
return;
}
const interactionSnapshot = this.suspendInteractiveModes();
this.pageState.activeViewSnapshot = {
sortBy: this.pageState.sortBy,
activeFolder: this.pageState.activeFolder,
activeLetterFilter: this.pageState.activeLetterFilter ?? null,
showFavoritesOnly: this.pageState.showFavoritesOnly,
showUpdateAvailableOnly: this.pageState.showUpdateAvailableOnly,
bulkMode: interactionSnapshot.bulkMode,
duplicatesMode: interactionSnapshot.duplicatesMode,
filters: this.cloneFilters(),
};
const excludedState = this.pageState.excludedViewState || {
sortBy: 'name:asc',
search: '',
};
this.pageState.viewMode = 'excluded';
this.pageState.sortBy = excludedState.sortBy || 'name:asc';
this.pageState.currentPage = 1;
this.pageState.activeFolder = null;
this.pageState.activeLetterFilter = null;
this.pageState.showFavoritesOnly = false;
this.pageState.showUpdateAvailableOnly = false;
this.applyFilterState(this.buildExcludedFilters(excludedState.search || ''));
this.syncExcludedViewState();
await this.resetAndReload(false);
}
async exitExcludedView() {
if (this.pageState.viewMode !== 'excluded') {
return;
}
this.pageState.excludedViewState = {
...(this.pageState.excludedViewState || {}),
sortBy: this.pageState.sortBy,
search: this.pageState.filters?.search || '',
};
const snapshot = this.pageState.activeViewSnapshot || {};
this.pageState.viewMode = 'active';
this.pageState.sortBy = snapshot.sortBy || this.convertLegacySortFormat(getStorageItem(`${this.pageType}_sort`) || 'name:asc');
this.pageState.currentPage = 1;
this.pageState.activeFolder = snapshot.activeFolder ?? getStorageItem(`${this.pageType}_activeFolder`);
this.pageState.activeLetterFilter = snapshot.activeLetterFilter ?? null;
this.pageState.showFavoritesOnly = Boolean(snapshot.showFavoritesOnly);
this.pageState.showUpdateAvailableOnly = Boolean(snapshot.showUpdateAvailableOnly);
this.applyFilterState(snapshot.filters || this.buildExcludedFilters(''));
this.pageState.activeViewSnapshot = null;
this.syncExcludedViewState();
await this.resetAndReload(true);
this.syncCustomFilterIndicator();
await this.restoreInteractiveModes(snapshot);
}
/**
* Find duplicate models
*/
findDuplicates() {
if (window.modelDuplicatesManager) {
// Change to toggle functionality
window.modelDuplicatesManager.toggleDuplicateMode();
} else {
console.error('Model duplicates manager not available');
}
}
/**
* Clean up resources
*/
destroy() {
// Note: We don't destroy the global sidebar manager, just clean it up
// The global instance will be reused for other page controls
if (this.sidebarManager && this.sidebarManager.isInitialized) {
this.sidebarManager.cleanup();
}
}
}