mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-24 22:41:26 -03:00
feat(ui): add hash search option and de-emphasized hash display in model modal
This commit is contained in:
@@ -216,6 +216,62 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Hashes footnote — borderless full-width muted line; reads as a footnote
|
||||
to the file info grid rather than a peer field */
|
||||
.hash-footnote {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
padding: 0 var(--space-1);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.hash-footnote .hash-entry {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hash-footnote .hash-kind {
|
||||
font-size: 0.7em;
|
||||
opacity: 0.5;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hash-footnote .model-hash-value {
|
||||
font-family: monospace;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hash-footnote .hash-sep {
|
||||
opacity: 0.3;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.hash-footnote .hash-copy-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 2px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-color);
|
||||
opacity: 0.35;
|
||||
font-size: 0.7em;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hash-footnote .hash-copy-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Toggle button — icon only, inline with the label */
|
||||
.notes-toggle-btn {
|
||||
display: none; /* shown by JS when content exceeds threshold */
|
||||
|
||||
@@ -1337,6 +1337,9 @@ export class BaseModelApiClient {
|
||||
if (pageState.searchOptions.creator !== undefined) {
|
||||
params.append('search_creator', pageState.searchOptions.creator.toString());
|
||||
}
|
||||
if (pageState.searchOptions.hash !== undefined) {
|
||||
params.append('search_hash', pageState.searchOptions.hash.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,6 +316,7 @@ async function showModelModalFromCard(card, modelType) {
|
||||
// Create model metadata object
|
||||
const modelMeta = {
|
||||
sha256: card.dataset.sha256,
|
||||
autov3: card.dataset.autov3 || '',
|
||||
preview_url: getCardPreviewUrl(card),
|
||||
file_path: card.dataset.filepath,
|
||||
model_name: card.dataset.name,
|
||||
@@ -406,6 +407,7 @@ function showExampleAccessModal(card, modelType) {
|
||||
// Get the model data from card dataset (works for both lora and checkpoint)
|
||||
const modelMeta = {
|
||||
sha256: card.dataset.sha256,
|
||||
autov3: card.dataset.autov3 || '',
|
||||
preview_url: getCardPreviewUrl(card),
|
||||
file_path: card.dataset.filepath,
|
||||
model_name: card.dataset.name,
|
||||
@@ -460,6 +462,7 @@ export function createModelCard(model, modelType) {
|
||||
// below, which ignore internal card drags via MODEL_CARD_DRAG_MIME_TYPE.
|
||||
card.draggable = true;
|
||||
card.dataset.sha256 = model.sha256;
|
||||
card.dataset.autov3 = model.autov3 || '';
|
||||
card.dataset.filepath = model.file_path;
|
||||
card.dataset.name = model.model_name;
|
||||
card.dataset.file_name = model.file_name;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { showToast, openCivitai, sendLoraToWorkflow, sendEmbeddingToWorkflow, sendModelPathToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js';
|
||||
import { showToast, openCivitai, sendLoraToWorkflow, sendEmbeddingToWorkflow, sendModelPathToWorkflow, buildLoraSyntax, copyToClipboard } from '../../utils/uiHelpers.js';
|
||||
import { modalManager } from '../../managers/ModalManager.js';
|
||||
import { MODEL_TYPES } from '../../api/apiConfig.js';
|
||||
import {
|
||||
@@ -351,6 +351,39 @@ export async function showModelModal(model, modelType) {
|
||||
};
|
||||
const escapedFilePathAttr = escapeAttribute(modelWithFullData.file_path || '');
|
||||
const escapedFolderPath = escapeHtml((modelWithFullData.file_path || '').replace(/[^/]+$/, '') || 'N/A');
|
||||
// De-emphasized hash display: a borderless full-width footnote line below
|
||||
// the info grid — sha256 middle-truncated (first 10 + last 6), autov3 in
|
||||
// full (12 chars); the full value is copied via data-hash.
|
||||
const modelSha256 = modelWithFullData.sha256 || '';
|
||||
const modelAutov3 = modelWithFullData.autov3 || '';
|
||||
const truncatedSha256 = modelSha256.length > 16
|
||||
? `${modelSha256.slice(0, 10)}\u2026${modelSha256.slice(-6)}`
|
||||
: modelSha256;
|
||||
const copyHashTitle = translate('modals.model.actions.copyHash', {}, 'Copy hash');
|
||||
const hashEntries = [];
|
||||
if (modelSha256) {
|
||||
hashEntries.push(`
|
||||
<span class="hash-entry">
|
||||
<span class="hash-kind">SHA256</span>
|
||||
<span class="model-hash-value" title="${escapeAttribute(modelSha256)}">${escapeHtml(truncatedSha256)}</span>
|
||||
<button class="hash-copy-btn" data-action="copy-hash" data-hash="${escapeAttribute(modelSha256)}" title="${copyHashTitle}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</span>`);
|
||||
}
|
||||
if (modelAutov3) {
|
||||
hashEntries.push(`
|
||||
<span class="hash-entry">
|
||||
<span class="hash-kind">AutoV3</span>
|
||||
<span class="model-hash-value" title="${escapeAttribute(modelAutov3)}">${escapeHtml(modelAutov3)}</span>
|
||||
<button class="hash-copy-btn" data-action="copy-hash" data-hash="${escapeAttribute(modelAutov3)}" title="${copyHashTitle}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</span>`);
|
||||
}
|
||||
const hashesMarkup = modelSha256 && hashEntries.length ? `
|
||||
<div class="hash-footnote" aria-label="${translate('modals.model.metadata.hashes', {}, 'Hashes')}">${hashEntries.join('<span class="hash-sep">·</span>')}
|
||||
</div>` : '';
|
||||
const useNewIcons = state.global.settings.use_new_license_icons !== false;
|
||||
const licenseIcons = useNewIcons
|
||||
? renderNewLicenseIcons(modelWithFullData)
|
||||
@@ -613,6 +646,7 @@ export async function showModelModal(model, modelType) {
|
||||
<span>${formatFileSize(modelWithFullData.file_size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
${hashesMarkup}
|
||||
${typeSpecificContent}
|
||||
<div class="info-item notes">
|
||||
<div class="notes-header">
|
||||
@@ -910,6 +944,11 @@ function setupEventHandlers(filePath, modelType) {
|
||||
case 'send-to-workflow':
|
||||
handleSendToWorkflow(target, modelType);
|
||||
break;
|
||||
case 'copy-hash':
|
||||
if (target.dataset.hash) {
|
||||
copyToClipboard(target.dataset.hash, 'Hash copied to clipboard');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,10 @@ class I18nManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/locales/${normalizedLocale}.json`);
|
||||
// 'no-cache' forces revalidation (cheap 304 via ETag) so locale
|
||||
// edits are picked up on a plain reload instead of serving a
|
||||
// stale cached copy.
|
||||
const response = await fetch(`/locales/${normalizedLocale}.json`, { cache: 'no-cache' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@ export class SearchManager {
|
||||
pageState.searchOptions.modelname = options.modelname || false;
|
||||
pageState.searchOptions.tags = options.tags || false;
|
||||
pageState.searchOptions.creator = options.creator || false;
|
||||
pageState.searchOptions.hash = options.hash || false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ export const state = {
|
||||
modelname: true,
|
||||
tags: false,
|
||||
creator: false,
|
||||
hash: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.LORA}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
@@ -168,6 +169,7 @@ export const state = {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
creator: false,
|
||||
hash: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
@@ -207,6 +209,7 @@ export const state = {
|
||||
modelname: true,
|
||||
tags: false,
|
||||
creator: false,
|
||||
hash: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.EMBEDDING}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
|
||||
Reference in New Issue
Block a user