mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(model-modal): add dynamic update availability indicators, see #715
- Add update badge to versions tab button when model has updates - Sync update status between modal and model cards in gallery - Pass `onUpdateStatusChange` callback to versions tab for real-time updates - Introduce `updateAvailabilityState` to track update status changes - Improve user awareness of available model updates across UI components
This commit is contained in:
@@ -312,6 +312,8 @@ export async function showModelModal(model, modelType) {
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
: '';
|
: '';
|
||||||
const hasUpdateAvailable = Boolean(modelWithFullData.update_available);
|
const hasUpdateAvailable = Boolean(modelWithFullData.update_available);
|
||||||
|
const updateAvailabilityState = { hasUpdateAvailable };
|
||||||
|
const updateBadgeTooltip = translate('modelCard.badges.updateAvailable', {}, 'Update available');
|
||||||
|
|
||||||
// Prepare LoRA specific data with complete civitai data
|
// Prepare LoRA specific data with complete civitai data
|
||||||
const escapedWords = (modelType === 'loras' || modelType === 'embeddings') && modelWithFullData.civitai?.trainedWords?.length ?
|
const escapedWords = (modelType === 'loras' || modelType === 'embeddings') && modelWithFullData.civitai?.trainedWords?.length ?
|
||||||
@@ -521,6 +523,82 @@ export async function showModelModal(model, modelType) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
function updateVersionsTabBadge(hasUpdate) {
|
||||||
|
const modalElement = document.getElementById(modalId);
|
||||||
|
if (!modalElement) return;
|
||||||
|
|
||||||
|
const tabButton = modalElement.querySelector('.tab-btn[data-tab="versions"]');
|
||||||
|
if (!tabButton) return;
|
||||||
|
|
||||||
|
tabButton.classList.toggle('tab-btn--has-update', hasUpdate);
|
||||||
|
|
||||||
|
let badge = tabButton.querySelector('.tab-badge--update');
|
||||||
|
if (hasUpdate) {
|
||||||
|
if (!badge) {
|
||||||
|
badge = document.createElement('span');
|
||||||
|
badge.className = 'tab-badge tab-badge--update';
|
||||||
|
badge.textContent = versionsBadgeLabel;
|
||||||
|
badge.title = updateBadgeTooltip;
|
||||||
|
tabButton.appendChild(badge);
|
||||||
|
} else {
|
||||||
|
badge.textContent = versionsBadgeLabel;
|
||||||
|
badge.title = updateBadgeTooltip;
|
||||||
|
}
|
||||||
|
} else if (badge) {
|
||||||
|
badge.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCardUpdateAvailability(hasUpdate) {
|
||||||
|
const filePath = modelWithFullData.file_path;
|
||||||
|
if (!filePath) return;
|
||||||
|
|
||||||
|
let updatedViaScroller = false;
|
||||||
|
if (state.virtualScroller?.updateSingleItem) {
|
||||||
|
updatedViaScroller = state.virtualScroller.updateSingleItem(filePath, {
|
||||||
|
update_available: hasUpdate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedViaScroller) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
|
? window.CSS.escape(filePath)
|
||||||
|
: filePath.replace(/["\\]/g, '\\$&');
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
card.dataset.update_available = hasUpdate ? 'true' : 'false';
|
||||||
|
card.classList.toggle('has-update', hasUpdate);
|
||||||
|
|
||||||
|
const headerInfo = card.querySelector('.card-header-info');
|
||||||
|
if (!headerInfo) return;
|
||||||
|
|
||||||
|
let badge = headerInfo.querySelector('.model-update-badge');
|
||||||
|
if (hasUpdate) {
|
||||||
|
if (!badge) {
|
||||||
|
badge = document.createElement('span');
|
||||||
|
badge.className = 'model-update-badge';
|
||||||
|
badge.title = updateBadgeTooltip;
|
||||||
|
badge.textContent = versionsBadgeLabel;
|
||||||
|
headerInfo.appendChild(badge);
|
||||||
|
}
|
||||||
|
} else if (badge) {
|
||||||
|
badge.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateStatusChange(hasUpdate) {
|
||||||
|
if (updateAvailabilityState.hasUpdateAvailable === hasUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateAvailabilityState.hasUpdateAvailable = hasUpdate;
|
||||||
|
updateVersionsTabBadge(hasUpdate);
|
||||||
|
updateCardUpdateAvailability(hasUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
let showcaseCleanup;
|
let showcaseCleanup;
|
||||||
|
|
||||||
const onCloseCallback = function() {
|
const onCloseCallback = function() {
|
||||||
@@ -542,11 +620,14 @@ export async function showModelModal(model, modelType) {
|
|||||||
if (activeModalElement) {
|
if (activeModalElement) {
|
||||||
activeModalElement.dataset.filePath = modelWithFullData.file_path || '';
|
activeModalElement.dataset.filePath = modelWithFullData.file_path || '';
|
||||||
}
|
}
|
||||||
|
updateVersionsTabBadge(updateAvailabilityState.hasUpdateAvailable);
|
||||||
const versionsTabController = initVersionsTab({
|
const versionsTabController = initVersionsTab({
|
||||||
modalId,
|
modalId,
|
||||||
modelType,
|
modelType,
|
||||||
modelId: civitaiModelId,
|
modelId: civitaiModelId,
|
||||||
currentVersionId: civitaiVersionId,
|
currentVersionId: civitaiVersionId,
|
||||||
|
currentBaseModel: modelWithFullData.base_model,
|
||||||
|
onUpdateStatusChange: handleUpdateStatusChange,
|
||||||
});
|
});
|
||||||
setupEditableFields(modelWithFullData.file_path, modelType);
|
setupEditableFields(modelWithFullData.file_path, modelType);
|
||||||
showcaseCleanup = setupShowcaseScroll(modalId);
|
showcaseCleanup = setupShowcaseScroll(modalId);
|
||||||
|
|||||||
@@ -228,6 +228,64 @@ function getCurrentVersionBaseModel(record, versionId) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveUpdateAvailability(record, baseModel, currentVersionId) {
|
||||||
|
if (!record) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategy = state?.global?.settings?.update_flag_strategy;
|
||||||
|
const sameBaseMode = strategy === DISPLAY_FILTER_MODES.SAME_BASE;
|
||||||
|
|
||||||
|
if (!sameBaseMode) {
|
||||||
|
return Boolean(record?.hasUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedBase = normalizeBaseModelName(baseModel);
|
||||||
|
if (!normalizedBase || !Array.isArray(record.versions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedCurrentVersionId =
|
||||||
|
typeof currentVersionId === 'number'
|
||||||
|
? currentVersionId
|
||||||
|
: currentVersionId
|
||||||
|
? Number(currentVersionId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let threshold = null;
|
||||||
|
for (const version of record.versions) {
|
||||||
|
if (!version.isInLibrary) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const versionBase = normalizeBaseModelName(version.baseModel);
|
||||||
|
if (versionBase !== normalizedBase) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (threshold === null || version.versionId > threshold) {
|
||||||
|
threshold = version.versionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threshold === null) {
|
||||||
|
threshold = normalizedCurrentVersionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threshold === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.versions.some(version => {
|
||||||
|
if (version.isInLibrary || version.shouldIgnore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const versionBase = normalizeBaseModelName(version.baseModel);
|
||||||
|
if (versionBase !== normalizedBase) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return typeof version.versionId === 'number' && version.versionId > threshold;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getAutoplaySetting() {
|
function getAutoplaySetting() {
|
||||||
try {
|
try {
|
||||||
return Boolean(state?.global?.settings?.autoplay_on_hover);
|
return Boolean(state?.global?.settings?.autoplay_on_hover);
|
||||||
@@ -490,6 +548,8 @@ export function initVersionsTab({
|
|||||||
modelType,
|
modelType,
|
||||||
modelId,
|
modelId,
|
||||||
currentVersionId,
|
currentVersionId,
|
||||||
|
currentBaseModel,
|
||||||
|
onUpdateStatusChange,
|
||||||
}) {
|
}) {
|
||||||
const pane = document.querySelector(`#${modalId} #versions-tab`);
|
const pane = document.querySelector(`#${modalId} #versions-tab`);
|
||||||
const container = pane ? pane.querySelector('.model-versions-tab') : null;
|
const container = pane ? pane.querySelector('.model-versions-tab') : null;
|
||||||
@@ -513,6 +573,10 @@ export function initVersionsTab({
|
|||||||
record: null,
|
record: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateStatusChangeHandler =
|
||||||
|
typeof onUpdateStatusChange === 'function' ? onUpdateStatusChange : null;
|
||||||
|
let lastNotifiedUpdateState = null;
|
||||||
|
|
||||||
let displayMode = getDefaultDisplayMode();
|
let displayMode = getDefaultDisplayMode();
|
||||||
|
|
||||||
let apiClient;
|
let apiClient;
|
||||||
@@ -538,77 +602,77 @@ export function initVersionsTab({
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(record) {
|
function render(record) {
|
||||||
controller.record = record;
|
controller.record = record;
|
||||||
controller.hasLoaded = true;
|
controller.hasLoaded = true;
|
||||||
|
|
||||||
if (!record || !Array.isArray(record.versions) || record.versions.length === 0) {
|
if (!record || !Array.isArray(record.versions) || record.versions.length === 0) {
|
||||||
renderEmptyState(container);
|
renderEmptyState(container);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const latestLibraryVersionId = getLatestLibraryVersionId(record);
|
|
||||||
const { normalized: currentBaseModelNormalized, raw: currentBaseModelLabel } =
|
|
||||||
getCurrentVersionBaseModel(record, normalizedCurrentVersionId);
|
|
||||||
const isFilteringActive =
|
|
||||||
displayMode === DISPLAY_FILTER_MODES.SAME_BASE &&
|
|
||||||
Boolean(currentBaseModelNormalized);
|
|
||||||
|
|
||||||
const sortedVersions = [...record.versions].sort(
|
|
||||||
(a, b) => Number(b.versionId) - Number(a.versionId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredVersions = sortedVersions.filter(version => {
|
|
||||||
if (!isFilteringActive) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized;
|
|
||||||
});
|
|
||||||
|
|
||||||
const dividerThresholdVersionId = (() => {
|
const latestLibraryVersionId = getLatestLibraryVersionId(record);
|
||||||
if (!isFilteringActive) {
|
const { normalized: currentBaseModelNormalized, raw: currentBaseModelLabel } =
|
||||||
return latestLibraryVersionId;
|
getCurrentVersionBaseModel(record, normalizedCurrentVersionId);
|
||||||
}
|
const isFilteringActive =
|
||||||
const baseLocalVersionIds = record.versions
|
displayMode === DISPLAY_FILTER_MODES.SAME_BASE &&
|
||||||
.filter(
|
Boolean(currentBaseModelNormalized);
|
||||||
version =>
|
|
||||||
version.isInLibrary &&
|
|
||||||
normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized &&
|
|
||||||
typeof version.versionId === 'number'
|
|
||||||
)
|
|
||||||
.map(version => version.versionId);
|
|
||||||
if (!baseLocalVersionIds.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Math.max(...baseLocalVersionIds);
|
|
||||||
})();
|
|
||||||
|
|
||||||
let dividerInserted = false;
|
const sortedVersions = [...record.versions].sort(
|
||||||
|
(a, b) => Number(b.versionId) - Number(a.versionId)
|
||||||
|
);
|
||||||
|
|
||||||
const rowsMarkup = filteredVersions
|
const filteredVersions = sortedVersions.filter(version => {
|
||||||
.map(version => {
|
if (!isFilteringActive) {
|
||||||
let markup = '';
|
return true;
|
||||||
if (
|
|
||||||
!dividerInserted &&
|
|
||||||
typeof dividerThresholdVersionId === 'number' &&
|
|
||||||
!(version.versionId > dividerThresholdVersionId)
|
|
||||||
) {
|
|
||||||
dividerInserted = true;
|
|
||||||
markup += '<div class="version-divider" role="presentation"></div>';
|
|
||||||
}
|
}
|
||||||
markup += renderRow(version, {
|
return normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized;
|
||||||
latestLibraryVersionId: dividerThresholdVersionId,
|
});
|
||||||
currentVersionId: normalizedCurrentVersionId,
|
|
||||||
modelId: record?.modelId ?? modelId,
|
|
||||||
});
|
|
||||||
return markup;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
const listContent =
|
const dividerThresholdVersionId = (() => {
|
||||||
rowsMarkup || renderFilteredEmptyState(currentBaseModelLabel);
|
if (!isFilteringActive) {
|
||||||
|
return latestLibraryVersionId;
|
||||||
|
}
|
||||||
|
const baseLocalVersionIds = record.versions
|
||||||
|
.filter(
|
||||||
|
version =>
|
||||||
|
version.isInLibrary &&
|
||||||
|
normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized &&
|
||||||
|
typeof version.versionId === 'number'
|
||||||
|
)
|
||||||
|
.map(version => version.versionId);
|
||||||
|
if (!baseLocalVersionIds.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.max(...baseLocalVersionIds);
|
||||||
|
})();
|
||||||
|
|
||||||
container.innerHTML = `
|
let dividerInserted = false;
|
||||||
|
|
||||||
|
const rowsMarkup = filteredVersions
|
||||||
|
.map(version => {
|
||||||
|
let markup = '';
|
||||||
|
if (
|
||||||
|
!dividerInserted &&
|
||||||
|
typeof dividerThresholdVersionId === 'number' &&
|
||||||
|
!(version.versionId > dividerThresholdVersionId)
|
||||||
|
) {
|
||||||
|
dividerInserted = true;
|
||||||
|
markup += '<div class="version-divider" role="presentation"></div>';
|
||||||
|
}
|
||||||
|
markup += renderRow(version, {
|
||||||
|
latestLibraryVersionId: dividerThresholdVersionId,
|
||||||
|
currentVersionId: normalizedCurrentVersionId,
|
||||||
|
modelId: record?.modelId ?? modelId,
|
||||||
|
});
|
||||||
|
return markup;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const listContent =
|
||||||
|
rowsMarkup || renderFilteredEmptyState(currentBaseModelLabel);
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
${renderToolbar(record, {
|
${renderToolbar(record, {
|
||||||
displayMode,
|
displayMode,
|
||||||
isFilteringActive,
|
isFilteringActive,
|
||||||
@@ -618,8 +682,20 @@ function render(record) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
setupMediaHoverInteractions(container);
|
setupMediaHoverInteractions(container);
|
||||||
}
|
|
||||||
|
if (updateStatusChangeHandler) {
|
||||||
|
const resolvedFlag = resolveUpdateAvailability(
|
||||||
|
record,
|
||||||
|
currentBaseModel,
|
||||||
|
normalizedCurrentVersionId
|
||||||
|
);
|
||||||
|
if (resolvedFlag !== lastNotifiedUpdateState) {
|
||||||
|
lastNotifiedUpdateState = resolvedFlag;
|
||||||
|
updateStatusChangeHandler(resolvedFlag, record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadVersions({ forceRefresh = false, eager = false } = {}) {
|
async function loadVersions({ forceRefresh = false, eager = false } = {}) {
|
||||||
if (controller.isLoading) {
|
if (controller.isLoading) {
|
||||||
|
|||||||
Reference in New Issue
Block a user