feat: add local model hash calculation with manual-progress notifications
This commit is contained in:
@@ -2189,8 +2189,12 @@ export const missingBrowserMethods = {
|
||||
* Show a notification banner (similar to ComfyUI's "Reconnecting" banner)
|
||||
*/
|
||||
showNotification(message, type = 'success', options = {}) {
|
||||
const duration = type === 'success' ? 4000 : (type === 'error' ? 6000 : 3000);
|
||||
showNotificationUtils(message, type, duration, false, options);
|
||||
const duration = options?.duration || (type === 'success' ? 4000 : (type === 'error' ? 6000 : 3000));
|
||||
return showNotificationUtils(message, type, {
|
||||
...options,
|
||||
duration,
|
||||
deduplicate: options?.deduplicate || false
|
||||
});
|
||||
},
|
||||
|
||||
refreshMissingListRow(missing, options = {}) {
|
||||
|
||||
@@ -265,6 +265,124 @@ export const modelInfoMethods = {
|
||||
};
|
||||
},
|
||||
|
||||
getHashCompareFilename(model = {}) {
|
||||
return model.filename
|
||||
|| model.name
|
||||
|| String(this.getLocalHashComparePath(model) || '').split(/[\/\\]/).pop()
|
||||
|| 'Selected local model';
|
||||
},
|
||||
|
||||
formatHashCompareProgressMessage(filename = '', percent = 0, message = '') {
|
||||
const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
|
||||
const status = message || 'Calculating SHA256...';
|
||||
return `Calculating hash for ${filename}...\n${Math.round(safePercent)}% - ${status}`;
|
||||
},
|
||||
|
||||
updateHashCompareModelMetadata(model = {}, result = {}) {
|
||||
const sha256 = this.normalizeSha256ForCompare(result?.sha256 || result?.hash || '');
|
||||
if (!sha256 || !model || typeof model !== 'object') return;
|
||||
|
||||
model.sha256 = sha256;
|
||||
model.hash = sha256;
|
||||
if (!model.hashes || typeof model.hashes !== 'object' || Array.isArray(model.hashes)) {
|
||||
model.hashes = {};
|
||||
}
|
||||
model.hashes.SHA256 = sha256;
|
||||
if (result.metadata_path) {
|
||||
model.metadata_path = result.metadata_path;
|
||||
}
|
||||
},
|
||||
|
||||
async calculateLocalHashCandidatesForCompare(model = {}) {
|
||||
const path = this.getLocalHashComparePath(model);
|
||||
if (!path) {
|
||||
throw new Error('No local file path available');
|
||||
}
|
||||
|
||||
const filename = this.getHashCompareFilename(model);
|
||||
const notification = this.showNotification?.(
|
||||
this.formatHashCompareProgressMessage(filename, 0, 'Preparing hash calculation...'),
|
||||
'info',
|
||||
{ manualProgress: true }
|
||||
);
|
||||
|
||||
try {
|
||||
const start = await this.fetchJson('/model_resolver/calculate-file-hash/start', {
|
||||
method: 'POST',
|
||||
silent: true,
|
||||
body: JSON.stringify({
|
||||
file_path: path,
|
||||
metadata_path: model.metadata_path || model.metadataPath || ''
|
||||
})
|
||||
}, 'Start model hash calculation');
|
||||
|
||||
const progressId = start?.progress_id;
|
||||
if (!progressId) {
|
||||
throw new Error('No progress id returned');
|
||||
}
|
||||
|
||||
let result = null;
|
||||
for (;;) {
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
const progress = await this.fetchJson(
|
||||
`/model_resolver/calculate-file-hash/progress/${encodeURIComponent(progressId)}`,
|
||||
{ silent: true },
|
||||
'Get model hash progress'
|
||||
);
|
||||
notification?.updateProgress?.(
|
||||
progress?.percent || 0,
|
||||
this.formatHashCompareProgressMessage(
|
||||
filename,
|
||||
progress?.percent || 0,
|
||||
progress?.message || ''
|
||||
)
|
||||
);
|
||||
|
||||
if (progress?.status === 'done') {
|
||||
result = progress;
|
||||
break;
|
||||
}
|
||||
if (progress?.status === 'cancelled') {
|
||||
throw new Error('Hash calculation cancelled');
|
||||
}
|
||||
if (progress?.status === 'error') {
|
||||
throw new Error(progress.error || progress.message || 'Hash calculation failed');
|
||||
}
|
||||
}
|
||||
|
||||
const sha256 = this.normalizeSha256ForCompare(result?.sha256 || result?.hash || '');
|
||||
if (!sha256) {
|
||||
throw new Error('No hash returned');
|
||||
}
|
||||
|
||||
this.updateHashCompareModelMetadata(model, result);
|
||||
notification?.updateProgress?.(
|
||||
100,
|
||||
result?.metadata_updated
|
||||
? `Hash calculated and saved for ${filename}.`
|
||||
: `Hash calculated for ${filename}.`
|
||||
);
|
||||
window.setTimeout(() => notification?.close?.(), 900);
|
||||
|
||||
return {
|
||||
data: result,
|
||||
candidates: this.collectHashCandidatesForCompare(
|
||||
{
|
||||
...result,
|
||||
sha256,
|
||||
hash: sha256,
|
||||
hashes: { SHA256: sha256 }
|
||||
},
|
||||
'calculated hash',
|
||||
new Set()
|
||||
)
|
||||
};
|
||||
} catch (error) {
|
||||
notification?.close?.();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
markSearchResultHashBadgesForMissing(missing = {}, sha256 = '') {
|
||||
const hash = this.normalizeSha256ForCompare(sha256);
|
||||
if (!missing || !hash) return false;
|
||||
@@ -331,16 +449,24 @@ export const modelInfoMethods = {
|
||||
}
|
||||
}
|
||||
|
||||
const filename = model.filename
|
||||
|| model.name
|
||||
|| String(this.getLocalHashComparePath(model) || '').split(/[\/\\]/).pop()
|
||||
|| 'Selected local model';
|
||||
if (!localHashes.length) {
|
||||
try {
|
||||
const calculated = await this.calculateLocalHashCandidatesForCompare(model);
|
||||
metadataResult = calculated.data || metadataResult;
|
||||
localHashes = calculated.candidates;
|
||||
} catch (error) {
|
||||
this.showNotification?.(`Hash calculation failed: ${error?.message || error}`, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const filename = this.getHashCompareFilename(model);
|
||||
if (!localHashes.length) {
|
||||
const metadataPath = metadataResult?.metadata_path || model.metadata_path || '';
|
||||
this.showNotification?.(
|
||||
metadataPath
|
||||
? 'Local metadata exists, but no completed SHA256 hash was found.'
|
||||
: 'No local SHA256 metadata found for this model.',
|
||||
? 'Hash calculation finished, but no completed SHA256 hash was found.'
|
||||
: 'No local SHA256 hash found for this model.',
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
author: Azornes
|
||||
title: AzToast
|
||||
version: 1.0.0
|
||||
version: 1.0.1
|
||||
description: Notification Utilities (Toast System)
|
||||
*/
|
||||
|
||||
@@ -66,6 +66,7 @@ const activeNotifications = new Map();
|
||||
* 2. showNotification(message, backgroundColor = "#4a6cd4", duration = 3000, type = "info", deduplicate = false)
|
||||
*
|
||||
* @param message - The message to show
|
||||
* @returns Notification controller with updateProgress/updateMessage/close helpers.
|
||||
*/
|
||||
export function showNotification(message, typeOrBgColor = "info", durationOrOptions = 3000, typeArg = "info", deduplicateArg = false) {
|
||||
// Clean any prefix matching the project name (e.g. "[Model Resolver]")
|
||||
@@ -104,6 +105,7 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
|
||||
}
|
||||
deduplicate = deduplicateArg;
|
||||
}
|
||||
const manualProgress = Boolean(options?.manualProgress);
|
||||
|
||||
// If deduplication is enabled, check if this message already exists
|
||||
if (deduplicate) {
|
||||
@@ -154,7 +156,7 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
|
||||
});
|
||||
}, duration);
|
||||
existingNotification.timeout = newTimeout;
|
||||
return; // Don't create a new notification
|
||||
return existingNotification.controller || null; // Don't create a new notification
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +293,10 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
|
||||
// --- Progress Bar ---
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.style.cssText = `height: 4px; width: 100%; background: ${config.bg}; box-shadow: 0 0 12px ${config.bg}; transform-origin: left; transform: scaleX(1); transition: none; flex-shrink: 0;`;
|
||||
if (manualProgress) {
|
||||
progressBar.style.transform = 'scaleX(0)';
|
||||
progressBar.style.transition = 'transform 0.18s ease-out';
|
||||
}
|
||||
|
||||
// --- Assemble Notification ---
|
||||
notification.appendChild(leftBar);
|
||||
@@ -339,8 +345,11 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
|
||||
|
||||
let dismissTimeout = null;
|
||||
let progressAnimationFrame = null;
|
||||
let closed = false;
|
||||
|
||||
const closeNotification = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
if (dismissTimeout !== null) {
|
||||
clearTimeout(dismissTimeout);
|
||||
dismissTimeout = null;
|
||||
@@ -364,6 +373,25 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
|
||||
});
|
||||
};
|
||||
|
||||
const controller = {
|
||||
element: notification,
|
||||
progressBar,
|
||||
messageElement: msgSpan,
|
||||
updateMessage(nextMessage = '') {
|
||||
msgSpan.textContent = String(nextMessage || '');
|
||||
},
|
||||
updateProgress(percent = 0, nextMessage = undefined) {
|
||||
const numericPercent = Number(percent);
|
||||
const safePercent = Math.max(0, Math.min(100, Number.isFinite(numericPercent) ? numericPercent : 0));
|
||||
progressBar.style.transition = 'transform 0.18s ease-out';
|
||||
progressBar.style.transform = `scaleX(${safePercent / 100})`;
|
||||
if (nextMessage !== undefined) {
|
||||
msgSpan.textContent = String(nextMessage || '');
|
||||
}
|
||||
},
|
||||
close: closeNotification
|
||||
};
|
||||
|
||||
closeBtn.onclick = closeNotification;
|
||||
|
||||
const startDismissTimer = () => {
|
||||
@@ -389,6 +417,7 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
|
||||
};
|
||||
|
||||
const pauseAndRewindTimer = () => {
|
||||
if (manualProgress) return;
|
||||
if (dismissTimeout !== null)
|
||||
clearTimeout(dismissTimeout);
|
||||
dismissTimeout = null;
|
||||
@@ -417,6 +446,7 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
|
||||
});
|
||||
|
||||
notification.addEventListener('mouseleave', () => {
|
||||
if (manualProgress) return;
|
||||
startDismissTimer();
|
||||
// Update stored timeout if deduplicate is enabled
|
||||
if (deduplicate) {
|
||||
@@ -428,14 +458,17 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
|
||||
}
|
||||
});
|
||||
|
||||
startDismissTimer();
|
||||
if (!manualProgress) {
|
||||
startDismissTimer();
|
||||
}
|
||||
|
||||
// Store notification if deduplicate is enabled
|
||||
if (deduplicate) {
|
||||
activeNotifications.set(message, { element: notification, timeout: dismissTimeout, animationFrame: progressAnimationFrame });
|
||||
activeNotifications.set(message, { element: notification, timeout: dismissTimeout, animationFrame: progressAnimationFrame, controller });
|
||||
}
|
||||
|
||||
log.debug(`Notification shown: [${CONFIG.PROJECT_NAME}] ${message}`);
|
||||
return controller;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user