feat(modal): open model metadata sidecar location from the model modal

Centralized sidecar storage moves .metadata.json into a mirror tree, so
the Location link in the model modal could no longer reveal a model's
sidecar. The Location row now shows an inline metadata icon button
(centralized mode only) that calls a new
POST /api/lm/models/open-sidecar-location endpoint: the backend resolves
the sidecar through py/utils/sidecar_paths, reveals the .metadata.json
selected in the file manager (falling back to the sidecar directory when
it has not been written yet), and returns the path for clipboard mode on
headless/Docker sessions. The platform reveal logic is factored into a
shared FileSystemHandler._reveal_file helper. All 9 locales translated.
This commit is contained in:
Will Miao
2026-10-10 07:41:48 +08:00
parent 7937aeaa04
commit af90fe873d
17 changed files with 622 additions and 58 deletions
@@ -349,6 +349,34 @@
font-size: 0.9em;
}
.file-path-wrapper {
display: flex;
align-items: baseline;
gap: 6px;
}
.file-path-wrapper .file-path {
flex: 1;
min-width: 0;
word-break: break-all;
}
.location-action-btn {
background: transparent;
border: none;
color: var(--lora-accent);
cursor: pointer;
padding: 2px 5px;
border-radius: var(--border-radius-xs);
flex-shrink: 0;
opacity: 0.7;
}
.location-action-btn:hover {
opacity: 1;
background: rgba(66, 153, 225, 0.1);
}
.file-path[data-action="open-file-location"] {
cursor: pointer;
text-decoration: underline;
+55 -5
View File
@@ -365,6 +365,16 @@ export async function showModelModal(model, modelType) {
};
const escapedFilePathAttr = escapeAttribute(modelWithFullData.file_path || '');
const escapedFolderPath = escapeHtml((modelWithFullData.file_path || '').replace(/[^/]+$/, '') || 'N/A');
// Centralized sidecar storage moves .metadata.json into a mirror tree, so
// the model path link can no longer reveal it — offer a dedicated entry
// only in that mode (alongside mode keeps sidecars next to the model).
const sidecarLocationButton = state.global.settings.sidecar_storage_mode === 'centralized'
? `<button class="location-action-btn" title="${translate('modals.model.actions.openSidecarLocation', {}, 'Open metadata location')}"
data-action="open-sidecar-location"
data-filepath="${escapedFilePathAttr}">
<i class="fas fa-file-alt"></i>
</button>`
: '';
// 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. Civitai model /
@@ -697,11 +707,14 @@ export async function showModelModal(model, modelType) {
<div class="info-item">
<div class="location-wrapper">
<label>${translate('modals.model.metadata.location', {}, 'Location')}</label>
<span class="file-path" title="${translate('modals.model.actions.openFileLocation', {}, 'Open file location')}"
data-action="open-file-location"
data-filepath="${escapedFilePathAttr}">
${escapedFolderPath}
</span>
<div class="file-path-wrapper">
<span class="file-path" title="${translate('modals.model.actions.openFileLocation', {}, 'Open file location')}"
data-action="open-file-location"
data-filepath="${escapedFilePathAttr}">
${escapedFolderPath}
</span>
${sidecarLocationButton}
</div>
</div>
</div>
<div class="info-item base-size">
@@ -1014,6 +1027,12 @@ function setupEventHandlers(filePath, modelType) {
openFileLocation(filePath);
}
break;
case 'open-sidecar-location':
const sidecarModelPath = target.dataset.filepath || getModalFilePath();
if (sidecarModelPath) {
openSidecarLocation(sidecarModelPath);
}
break;
case 'nav-prev':
handleDirectionalNavigation('prev', modelType);
break;
@@ -1412,6 +1431,37 @@ async function openFileLocation(filePath) {
}
}
/**
* Call backend to open the model's .metadata.json sidecar location
* @param {string} filePath model file path (backend derives the sidecar path)
*/
async function openSidecarLocation(filePath) {
try {
const resp = await fetch('/api/lm/models/open-sidecar-location', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ 'file_path': filePath })
});
if (!resp.ok) throw new Error('Failed to open sidecar location');
const data = await resp.json();
if (data.mode === 'clipboard' && data.path) {
try {
await navigator.clipboard.writeText(data.path);
showToast('modals.model.openSidecarLocation.copied', { path: data.path }, 'success');
} catch (clipboardErr) {
console.warn('Clipboard API not available:', clipboardErr);
showToast('modals.model.openSidecarLocation.clipboardFallback', { path: data.path }, 'info');
}
} else {
showToast('modals.model.openSidecarLocation.success', {}, 'success');
}
} catch (err) {
showToast('modals.model.openSidecarLocation.failed', {}, 'error');
}
}
async function handleSendToWorkflow(target, modelType) {
const filePath = getModalFilePath();
if (!filePath) {