diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index df4af58..53525d6 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -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 = {}) { diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js index b5d4258..743d2f0 100644 --- a/web/resolver/views/model_info_methods.js +++ b/web/resolver/views/model_info_methods.js @@ -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; diff --git a/web/utils/notification_utils.js b/web/utils/notification_utils.js index 89d9ee9..b373a75 100644 --- a/web/utils/notification_utils.js +++ b/web/utils/notification_utils.js @@ -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; } /**