feat(scan): a scan page replaces the scan wizard; unmatched models can be looked up again
The rail's scan button and Home's "Scan model folders" open a tool page instead of the seven-switch dialog: counts of models matched on Civitai, not matched and not scanned yet (GET /anomalous/scan_summary), one button to scan, progress inside the page (scan_progress.js hosts its panel there; it floats again when the page or the browser is closed). Offline, renaming files, fetching everything again, picking models and the API key (with its set / not set state) are under Advanced; the two risky switches reset after each scan. The provenance switch moved to Settings -> Model settings. Scanner: - --retry-unmatched: only models whose info was written from the file alone (Civitai had no match or could not be reached) are looked up again, with the saved hash when the file is unchanged and without replacing covers. Before, such models stayed local-only unless every model was refetched, which also deleted custom covers. - A display name set in the model editor is no longer replaced by Civitai's name on each scan or when the info is fetched again. - Both scan routes map request fields through _scraper_flags; folder cache clearing is one helper (scanner.py 602 -> 579 lines). - sidecar_info / is_unmatched live in model_identity.py for scraper and API. ui_scan_wizard.js is gone: scan_runner.js starts and follows scans, ui_scan_page.js renders the page; 36 wizard-only locale keys removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f13b725d97
commit
5af9b6d260
@@ -1,918 +0,0 @@
|
||||
/**
|
||||
* Scan wizard UI and scan launch/polling coordination.
|
||||
*/
|
||||
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { translate } from './locales.js';
|
||||
import { updateScanProgress, finishScanProgress, failScanProgress } from './scan_progress.js';
|
||||
import { showWorkbenchToast } from './ui_prompt_toast.js';
|
||||
|
||||
const t = (key, params) => translate(key, params);
|
||||
|
||||
const SCAN_RADAR_ICON_SVG = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><circle cx="12" cy="12" r="9"/><line x1="12" y1="3" x2="12" y2="21" stroke-opacity="0.35"/><line x1="3" y1="12" x2="21" y2="12" stroke-opacity="0.35"/><line x1="12" y1="12" x2="18.5" y2="5.5" stroke-width="2"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>`;
|
||||
|
||||
export function setScanButtonState(btn, isScanning) {
|
||||
if (!btn) return;
|
||||
btn.innerHTML = SCAN_RADAR_ICON_SVG;
|
||||
btn.classList.toggle('anomalous-radar-spinning', Boolean(isScanning));
|
||||
btn.style.opacity = isScanning ? '0.85' : '1';
|
||||
btn.style.animation = '';
|
||||
}
|
||||
|
||||
function setActiveScanButtonState(isScanning) {
|
||||
setScanButtonState(document.getElementById('anomalous-scan-btn'), isScanning);
|
||||
}
|
||||
|
||||
export function openScanWizard({ isGlobal = false, targetFiles = null } = {}) {
|
||||
let wizard = document.getElementById('anomalous-wizard-modal');
|
||||
if (wizard) {
|
||||
if (typeof wizard.cleanupModal === 'function') wizard.cleanupModal();
|
||||
else if (wizard.parentNode) wizard.parentNode.removeChild(wizard);
|
||||
}
|
||||
|
||||
wizard = document.createElement('div');
|
||||
wizard.id = 'anomalous-wizard-modal';
|
||||
wizard.style.position = 'fixed';
|
||||
wizard.style.top = '0';
|
||||
wizard.style.left = '0';
|
||||
wizard.style.width = '100vw';
|
||||
wizard.style.height = '100vh';
|
||||
wizard.style.backgroundColor = 'rgba(0,0,0,0.6)';
|
||||
wizard.style.zIndex = '999999';
|
||||
wizard.style.display = 'flex';
|
||||
wizard.style.justifyContent = 'center';
|
||||
wizard.style.alignItems = 'center';
|
||||
wizard.style.fontFamily = 'Roboto, "Segoe UI", sans-serif';
|
||||
|
||||
const closeWizard = () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
if (wizard && wizard.parentNode) {
|
||||
wizard.parentNode.removeChild(wizard);
|
||||
}
|
||||
};
|
||||
wizard.cleanupModal = closeWizard;
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Escape') closeWizard();
|
||||
};
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
wizard.onclick = (e) => {
|
||||
if (e.target === wizard) closeWizard();
|
||||
};
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.background = '#1E1E1E';
|
||||
content.style.borderRadius = '12px';
|
||||
content.style.padding = '28px 32px 20px 32px';
|
||||
content.style.width = '760px';
|
||||
content.style.maxWidth = '95%';
|
||||
content.style.maxHeight = '85vh';
|
||||
content.style.display = 'flex';
|
||||
content.style.flexDirection = 'column';
|
||||
content.style.boxShadow = '0 11px 15px -7px rgba(0,0,0,0.2), 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12)';
|
||||
content.style.color = '#fff';
|
||||
content.style.position = 'relative';
|
||||
|
||||
// Top Header Area
|
||||
const headerArea = document.createElement('div');
|
||||
headerArea.style.display = 'flex';
|
||||
headerArea.style.justifyContent = 'space-between';
|
||||
headerArea.style.alignItems = 'center';
|
||||
headerArea.style.marginBottom = '24px';
|
||||
|
||||
const title = document.createElement('h2');
|
||||
if (targetFiles) {
|
||||
title.textContent = t('sidebarSingleModelScan', { files: targetFiles });
|
||||
} else {
|
||||
title.textContent = t(isGlobal ? 'sidebarGlobalScanWizard' : 'sidebarScanWizardTitle');
|
||||
}
|
||||
title.style.margin = '0';
|
||||
title.style.fontSize = '1.6em';
|
||||
title.style.fontWeight = '500';
|
||||
|
||||
// Toolbar right
|
||||
const topToolbar = document.createElement('div');
|
||||
topToolbar.style.display = 'flex';
|
||||
topToolbar.style.gap = '8px';
|
||||
|
||||
const createGhostBtn = (icon, textKey, onClick) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = `${icon} ${t(textKey)}`;
|
||||
btn.style.padding = '6px 12px';
|
||||
btn.style.background = 'transparent';
|
||||
btn.style.color = '#ccc';
|
||||
btn.style.border = '1px solid rgba(255,255,255,0.1)';
|
||||
btn.style.borderRadius = '6px';
|
||||
btn.style.cursor = 'pointer';
|
||||
btn.style.fontSize = '0.85em';
|
||||
btn.style.transition = 'all 0.2s';
|
||||
btn.onmouseover = () => { btn.style.background = 'rgba(255,255,255,0.08)'; btn.style.color = '#fff'; };
|
||||
btn.onmouseout = () => { btn.style.background = 'transparent'; btn.style.color = '#ccc'; };
|
||||
btn.onclick = onClick;
|
||||
return btn;
|
||||
};
|
||||
|
||||
const apiKeyBtn = createGhostBtn('🔑', 'sidebarApiKey', () => {
|
||||
const newKey = prompt(t('sidebarApiKeyPrompt'), '');
|
||||
if (newKey !== null) {
|
||||
fetch('/anomalous/save_config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ api_key: newKey.trim() })
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.status !== 'ok') throw new Error(data.message || 'Save failed');
|
||||
alert(t('sidebarKeySaved'));
|
||||
}).catch(error => alert(t('sidebarSaveFailed') + error.message));
|
||||
}
|
||||
});
|
||||
|
||||
topToolbar.appendChild(apiKeyBtn);
|
||||
|
||||
headerArea.appendChild(title);
|
||||
headerArea.appendChild(topToolbar);
|
||||
content.appendChild(headerArea);
|
||||
|
||||
let scanMode = 'civitai';
|
||||
let enableRename = true;
|
||||
let enableAutoCheck = true;
|
||||
let enableForceOverwrite = false;
|
||||
let updateSections = () => { };
|
||||
|
||||
const formGroup = document.createElement('div');
|
||||
formGroup.style.display = 'flex';
|
||||
formGroup.style.flexDirection = 'column';
|
||||
formGroup.style.gap = '24px';
|
||||
formGroup.style.overflowY = 'auto';
|
||||
formGroup.style.flex = '1';
|
||||
formGroup.style.minHeight = '0';
|
||||
formGroup.style.paddingRight = '6px';
|
||||
|
||||
const createChoiceCard = (id, icon, titleKey, descKey, isSelected) => {
|
||||
const card = document.createElement('div');
|
||||
card.style.flex = '1';
|
||||
card.style.background = isSelected ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.04)';
|
||||
card.style.border = `2px solid ${isSelected ? '#e5e7eb' : 'transparent'}`;
|
||||
card.style.borderRadius = '8px';
|
||||
card.style.padding = '16px';
|
||||
card.style.cursor = 'pointer';
|
||||
card.style.transition = 'all 0.2s';
|
||||
card.style.display = 'flex';
|
||||
card.style.flexDirection = 'column';
|
||||
card.style.gap = '12px';
|
||||
card.style.alignItems = 'flex-start';
|
||||
|
||||
card.onmouseover = () => {
|
||||
if (!isSelected) card.style.background = 'rgba(255, 255, 255, 0.08)';
|
||||
};
|
||||
card.onmouseout = () => {
|
||||
if (!isSelected) card.style.background = 'rgba(255, 255, 255, 0.04)';
|
||||
};
|
||||
|
||||
const topRow = document.createElement('div');
|
||||
topRow.style.display = 'flex';
|
||||
topRow.style.alignItems = 'center';
|
||||
topRow.style.gap = '12px';
|
||||
|
||||
const iconDiv = document.createElement('div');
|
||||
iconDiv.innerText = icon;
|
||||
iconDiv.style.fontSize = '1.6em';
|
||||
|
||||
const tTitle = document.createElement('div');
|
||||
tTitle.textContent = t(titleKey);
|
||||
tTitle.style.fontWeight = '500';
|
||||
tTitle.style.fontSize = '1.05em';
|
||||
tTitle.style.color = isSelected ? '#ffffff' : '#e5e7eb';
|
||||
|
||||
topRow.appendChild(iconDiv);
|
||||
topRow.appendChild(tTitle);
|
||||
|
||||
const tDesc = document.createElement('div');
|
||||
tDesc.textContent = t(descKey);
|
||||
tDesc.style.fontSize = '0.85em';
|
||||
tDesc.style.color = '#9ca3af';
|
||||
tDesc.style.lineHeight = '1.5';
|
||||
|
||||
card.appendChild(topRow);
|
||||
card.appendChild(tDesc);
|
||||
return card;
|
||||
};
|
||||
|
||||
let targetMode = 'all';
|
||||
let selectedForScan = new Map();
|
||||
|
||||
if (!targetFiles) {
|
||||
const section0 = document.createElement('div');
|
||||
const sec0Title = document.createElement('div');
|
||||
sec0Title.textContent = t('sidebarStep0');
|
||||
sec0Title.style.fontWeight = '500';
|
||||
sec0Title.style.color = '#e5e7eb';
|
||||
sec0Title.style.marginBottom = '12px';
|
||||
sec0Title.style.fontSize = '0.95em';
|
||||
section0.appendChild(sec0Title);
|
||||
|
||||
const tContainer = document.createElement('div');
|
||||
tContainer.style.display = 'flex';
|
||||
tContainer.style.flexDirection = 'row';
|
||||
tContainer.style.gap = '16px';
|
||||
tContainer.style.marginBottom = '16px';
|
||||
|
||||
const customActionDiv = document.createElement('div');
|
||||
customActionDiv.style.display = 'none';
|
||||
customActionDiv.style.marginTop = '16px';
|
||||
|
||||
const openSelectorBtn = document.createElement('button');
|
||||
openSelectorBtn.textContent = t('sidebarOpenSelector');
|
||||
openSelectorBtn.style.cssText = 'width:100%;padding:12px;background:#e5e7eb;color:#111;border:none;border-radius:6px;cursor:pointer;font-weight:600;font-size:1.05em;box-shadow:0 2px 4px rgba(0,0,0,0.2);';
|
||||
|
||||
const selectedCountSpan = document.createElement('div');
|
||||
selectedCountSpan.style.cssText = 'text-align:center;color:#e5e7eb;margin-top:8px;font-size:0.9em;';
|
||||
selectedCountSpan.textContent = t('sidebarSelectedZero');
|
||||
|
||||
const updateSelectedCount = () => {
|
||||
let total = 0;
|
||||
for (const set of selectedForScan.values()) total += set.size;
|
||||
selectedCountSpan.textContent = t('sidebarSelectedCount', { count: total });
|
||||
};
|
||||
|
||||
openSelectorBtn.onclick = () => {
|
||||
this._openAdvancedModelSelector(selectedForScan, (newSelection) => {
|
||||
selectedForScan = newSelection;
|
||||
updateSelectedCount();
|
||||
});
|
||||
};
|
||||
|
||||
customActionDiv.appendChild(openSelectorBtn);
|
||||
customActionDiv.appendChild(selectedCountSpan);
|
||||
|
||||
let tCard1, tCard2;
|
||||
const updateTargetCards = () => {
|
||||
if (tCard1 && tCard2) {
|
||||
tContainer.removeChild(tCard1);
|
||||
tContainer.removeChild(tCard2);
|
||||
}
|
||||
tCard1 = createChoiceCard('all', '📂', 'sidebarTargetGlobal', 'sidebarTargetGlobalDesc', targetMode === 'all');
|
||||
tCard2 = createChoiceCard('custom', '☑️', 'sidebarTargetCustom', 'sidebarTargetCustomDesc', targetMode === 'custom');
|
||||
|
||||
tCard1.onclick = () => {
|
||||
targetMode = 'all';
|
||||
updateTargetCards();
|
||||
customActionDiv.style.display = 'none';
|
||||
};
|
||||
tCard2.onclick = () => {
|
||||
targetMode = 'custom';
|
||||
updateTargetCards();
|
||||
customActionDiv.style.display = 'block';
|
||||
};
|
||||
|
||||
tContainer.appendChild(tCard1);
|
||||
tContainer.appendChild(tCard2);
|
||||
};
|
||||
updateTargetCards();
|
||||
section0.appendChild(tContainer);
|
||||
section0.appendChild(customActionDiv);
|
||||
formGroup.appendChild(section0);
|
||||
}
|
||||
|
||||
// === Step 1: Fetch Data ===
|
||||
const section1 = document.createElement('div');
|
||||
const sec1Title = document.createElement('div');
|
||||
sec1Title.textContent = t('sidebarStep1');
|
||||
sec1Title.style.fontWeight = '500';
|
||||
sec1Title.style.color = '#e5e7eb';
|
||||
sec1Title.style.marginBottom = '12px';
|
||||
sec1Title.style.fontSize = '0.95em';
|
||||
section1.appendChild(sec1Title);
|
||||
|
||||
const cardsContainer = document.createElement('div');
|
||||
cardsContainer.style.display = 'flex';
|
||||
cardsContainer.style.flexDirection = 'row';
|
||||
cardsContainer.style.gap = '16px';
|
||||
|
||||
|
||||
|
||||
let card1, card2;
|
||||
const updateCards = () => {
|
||||
if (card1 && card2) {
|
||||
cardsContainer.removeChild(card1);
|
||||
cardsContainer.removeChild(card2);
|
||||
}
|
||||
card1 = createChoiceCard('civitai', '🌍', 'sidebarOnline', 'sidebarOnlineDesc', scanMode === 'civitai');
|
||||
card2 = createChoiceCard('offline', '🔌', 'sidebarOffline', 'sidebarOfflineDesc', scanMode === 'offline');
|
||||
|
||||
card1.onclick = () => { scanMode = 'civitai'; updateCards(); updateSections(); };
|
||||
card2.onclick = () => { scanMode = 'offline'; updateCards(); updateSections(); };
|
||||
|
||||
cardsContainer.appendChild(card1);
|
||||
cardsContainer.appendChild(card2);
|
||||
};
|
||||
updateCards();
|
||||
section1.appendChild(cardsContainer);
|
||||
formGroup.appendChild(section1);
|
||||
|
||||
// Material Switch Builder
|
||||
const createMaterialSwitch = (initialState, onChange) => {
|
||||
const track = document.createElement('div');
|
||||
track.style.width = '36px';
|
||||
track.style.height = '14px';
|
||||
track.style.borderRadius = '7px';
|
||||
track.style.background = initialState ? 'rgba(255, 255, 255, 0.45)' : 'rgba(255,255,255,0.15)';
|
||||
track.style.position = 'relative';
|
||||
track.style.cursor = 'pointer';
|
||||
track.style.transition = 'background 0.3s';
|
||||
track.style.display = 'flex';
|
||||
track.style.alignItems = 'center';
|
||||
|
||||
const thumb = document.createElement('div');
|
||||
thumb.style.width = '20px';
|
||||
thumb.style.height = '20px';
|
||||
thumb.style.borderRadius = '50%';
|
||||
thumb.style.background = initialState ? '#ffffff' : '#888888';
|
||||
thumb.style.position = 'absolute';
|
||||
thumb.style.left = initialState ? '16px' : '0px';
|
||||
thumb.style.transition = 'left 0.3s, background 0.3s';
|
||||
thumb.style.boxShadow = '0 1px 3px rgba(0,0,0,0.4)';
|
||||
|
||||
let state = initialState;
|
||||
track.onclick = () => {
|
||||
state = !state;
|
||||
track.style.background = state ? 'rgba(255, 255, 255, 0.45)' : 'rgba(255,255,255,0.15)';
|
||||
thumb.style.background = state ? '#ffffff' : '#888888';
|
||||
thumb.style.left = state ? '16px' : '0px';
|
||||
onChange(state);
|
||||
};
|
||||
track.appendChild(thumb);
|
||||
return track;
|
||||
};
|
||||
|
||||
const createListRow = (icon, titleKey, descKey, actionEl) => {
|
||||
const row = document.createElement('div');
|
||||
row.style.display = 'flex';
|
||||
row.style.alignItems = 'center';
|
||||
row.style.justifyContent = 'space-between';
|
||||
row.style.padding = '12px 0';
|
||||
row.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.style.display = 'flex';
|
||||
left.style.alignItems = 'flex-start';
|
||||
left.style.gap = '16px';
|
||||
|
||||
const iconEl = document.createElement('div');
|
||||
if (typeof icon === 'string' && icon.startsWith('<')) {
|
||||
iconEl.innerHTML = icon;
|
||||
} else {
|
||||
iconEl.innerText = icon;
|
||||
}
|
||||
iconEl.style.fontSize = '1.4em';
|
||||
iconEl.style.lineHeight = '1.2';
|
||||
iconEl.style.width = '24px';
|
||||
iconEl.style.textAlign = 'center';
|
||||
|
||||
const textDiv = document.createElement('div');
|
||||
const titleEl = document.createElement('div');
|
||||
titleEl.textContent = t(titleKey);
|
||||
titleEl.style.fontWeight = '500';
|
||||
titleEl.style.fontSize = '1.0em';
|
||||
titleEl.style.color = '#fff';
|
||||
|
||||
const d = document.createElement('div');
|
||||
d.innerHTML = t(descKey);
|
||||
d.style.fontSize = '0.85em';
|
||||
d.style.color = '#aaa';
|
||||
d.style.marginTop = '4px';
|
||||
|
||||
textDiv.appendChild(titleEl);
|
||||
textDiv.appendChild(d);
|
||||
left.appendChild(iconEl);
|
||||
left.appendChild(textDiv);
|
||||
row.appendChild(left);
|
||||
if (actionEl) row.appendChild(actionEl);
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
// === Step 2: Normalize Naming ===
|
||||
const section2 = document.createElement('div');
|
||||
const sec2Title = document.createElement('div');
|
||||
sec2Title.textContent = t('sidebarStep2');
|
||||
sec2Title.style.fontWeight = '500';
|
||||
sec2Title.style.color = '#e5e7eb';
|
||||
sec2Title.style.marginBottom = '8px';
|
||||
sec2Title.style.fontSize = '0.95em';
|
||||
section2.appendChild(sec2Title);
|
||||
|
||||
const s2List = document.createElement('div');
|
||||
let enableVirtualRename = true;
|
||||
let enablePhysicalRename = false;
|
||||
|
||||
const dualChannelRow = document.createElement('div');
|
||||
dualChannelRow.style.display = 'flex';
|
||||
dualChannelRow.style.flexDirection = 'column';
|
||||
dualChannelRow.style.gap = '14px';
|
||||
dualChannelRow.style.padding = '0 16px 12px 16px';
|
||||
dualChannelRow.style.marginLeft = '40px';
|
||||
|
||||
const updateDualChannelUI = () => {
|
||||
dualChannelRow.style.opacity = enableRename ? '1' : '0.4';
|
||||
dualChannelRow.style.pointerEvents = enableRename ? 'auto' : 'none';
|
||||
};
|
||||
|
||||
const virtualSwitch = createMaterialSwitch(enableVirtualRename, (s) => enableVirtualRename = s);
|
||||
let physicalProtectionNotice = null;
|
||||
const physicalSwitch = createMaterialSwitch(enablePhysicalRename, (s) => {
|
||||
enablePhysicalRename = s;
|
||||
if (physicalProtectionNotice) {
|
||||
physicalProtectionNotice.style.display = s ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
const vContainer = document.createElement('div');
|
||||
vContainer.style.display = 'flex';
|
||||
vContainer.style.flexDirection = 'column';
|
||||
|
||||
const vRow = document.createElement('div');
|
||||
vRow.style.display = 'flex';
|
||||
vRow.style.alignItems = 'center';
|
||||
vRow.style.gap = '8px';
|
||||
vRow.innerHTML = `<span style="font-size:0.9em; color:#ddd;">✨ ${t('sidebarVirtualRename')}</span>`;
|
||||
vRow.appendChild(virtualSwitch);
|
||||
|
||||
const vDesc = document.createElement('div');
|
||||
vDesc.style.fontSize = '0.8em';
|
||||
vDesc.style.color = '#888';
|
||||
vDesc.style.marginTop = '4px';
|
||||
vDesc.textContent = t('sidebarVirtualRenameDesc');
|
||||
vContainer.appendChild(vRow);
|
||||
vContainer.appendChild(vDesc);
|
||||
|
||||
const pContainer = document.createElement('div');
|
||||
pContainer.style.display = 'flex';
|
||||
pContainer.style.flexDirection = 'column';
|
||||
|
||||
const pRow = document.createElement('div');
|
||||
pRow.style.display = 'flex';
|
||||
pRow.style.alignItems = 'center';
|
||||
pRow.style.gap = '8px';
|
||||
pRow.innerHTML = `<span style="font-size:0.9em; color:#ddd; display:inline-flex; align-items:center; gap:6px;"><svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>${t('sidebarPhysicalRename')}</span>`;
|
||||
pRow.appendChild(physicalSwitch);
|
||||
|
||||
const pDesc = document.createElement('div');
|
||||
pDesc.style.fontSize = '0.8em';
|
||||
pDesc.style.color = '#888';
|
||||
pDesc.style.marginTop = '4px';
|
||||
pDesc.innerHTML = t('sidebarPhysicalRenameDesc');
|
||||
pContainer.appendChild(pRow);
|
||||
pContainer.appendChild(pDesc);
|
||||
|
||||
physicalProtectionNotice = document.createElement('div');
|
||||
physicalProtectionNotice.style.display = enablePhysicalRename ? 'block' : 'none';
|
||||
physicalProtectionNotice.style.marginTop = '8px';
|
||||
physicalProtectionNotice.style.padding = '9px 11px';
|
||||
physicalProtectionNotice.style.border = '1px solid rgba(251, 188, 4, 0.45)';
|
||||
physicalProtectionNotice.style.borderRadius = '6px';
|
||||
physicalProtectionNotice.style.background = 'rgba(251, 188, 4, 0.08)';
|
||||
physicalProtectionNotice.style.color = '#fdd663';
|
||||
physicalProtectionNotice.style.fontSize = '0.8em';
|
||||
physicalProtectionNotice.style.lineHeight = '1.45';
|
||||
physicalProtectionNotice.textContent = t('sidebarPhysicalRenameProtectedNotice');
|
||||
pContainer.appendChild(physicalProtectionNotice);
|
||||
|
||||
dualChannelRow.appendChild(vContainer);
|
||||
dualChannelRow.appendChild(pContainer);
|
||||
|
||||
const editSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#dc143c;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>';
|
||||
s2List.appendChild(createListRow(editSvg, 'sidebarNormalize', 'sidebarNormalizeDesc', createMaterialSwitch(enableRename, (s) => { enableRename = s; updateDualChannelUI(); })));
|
||||
s2List.lastChild.style.borderBottom = 'none';
|
||||
s2List.appendChild(dualChannelRow);
|
||||
updateDualChannelUI();
|
||||
section2.appendChild(s2List);
|
||||
formGroup.appendChild(section2);
|
||||
|
||||
// === Step 3: Workflow Protection & Fix ===
|
||||
const section3 = document.createElement('div');
|
||||
const sec3Title = document.createElement('div');
|
||||
sec3Title.textContent = t('sidebarStep3');
|
||||
sec3Title.style.fontWeight = '500';
|
||||
sec3Title.style.color = '#e5e7eb';
|
||||
sec3Title.style.marginBottom = '8px';
|
||||
sec3Title.style.fontSize = '0.95em';
|
||||
section3.appendChild(sec3Title);
|
||||
|
||||
const s3List = document.createElement('div');
|
||||
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
|
||||
const cubeSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#dc143c;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
|
||||
const warnSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#f59e0b;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
|
||||
const wandSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#8b5cf6;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';
|
||||
s3List.appendChild(createListRow(cubeSvg, 'sidebarProvenance', 'sidebarProvenanceDesc', createMaterialSwitch(isInject, (s) => localStorage.setItem('anomalous_inject_hash', s ? 'true' : 'false'))));
|
||||
s3List.appendChild(createListRow(warnSvg, 'sidebarOverwrite', 'sidebarOverwriteDesc', createMaterialSwitch(enableForceOverwrite, (s) => enableForceOverwrite = s)));
|
||||
s3List.appendChild(createListRow(wandSvg, 'sidebarSmartFix', 'sidebarSmartFixDesc', createMaterialSwitch(enableAutoCheck, (s) => enableAutoCheck = s)));
|
||||
s3List.lastChild.style.borderBottom = 'none';
|
||||
|
||||
section3.appendChild(s3List);
|
||||
formGroup.appendChild(section3);
|
||||
content.appendChild(formGroup);
|
||||
|
||||
updateSections = () => {
|
||||
if (scanMode === 'offline') {
|
||||
section2.style.opacity = '0.3';
|
||||
section2.style.pointerEvents = 'none';
|
||||
section3.style.opacity = '0.3';
|
||||
section3.style.pointerEvents = 'none';
|
||||
} else {
|
||||
section2.style.opacity = '1';
|
||||
section2.style.pointerEvents = 'auto';
|
||||
section3.style.opacity = '1';
|
||||
section3.style.pointerEvents = 'auto';
|
||||
}
|
||||
};
|
||||
updateSections();
|
||||
|
||||
// Execute Scan Logic
|
||||
const doScan = async () => {
|
||||
try {
|
||||
// Offline greys out renaming and overwriting, so none of them may be sent.
|
||||
const offline = scanMode === 'offline';
|
||||
const reqBody = {
|
||||
offline_only: offline,
|
||||
skip_rename: offline || !enableRename,
|
||||
virtual_rename: !offline && enableRename && enableVirtualRename,
|
||||
physical_rename: !offline && enableRename && enablePhysicalRename,
|
||||
force_overwrite: !offline && enableForceOverwrite
|
||||
};
|
||||
|
||||
if (targetMode === 'custom') {
|
||||
let hasAny = false;
|
||||
for (const set of selectedForScan.values()) {
|
||||
if (set.size > 0) hasAny = true;
|
||||
}
|
||||
if (!hasAny) {
|
||||
alert(t('sidebarNoModelsSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
closeWizard();
|
||||
const activeScanBtn = document.getElementById('anomalous-scan-btn');
|
||||
if (activeScanBtn) {
|
||||
setScanButtonState(activeScanBtn, true);
|
||||
}
|
||||
|
||||
const customFolders = Array.from(selectedForScan.entries()).filter(([, files]) => files.size > 0);
|
||||
let customFolderCurrent = 0;
|
||||
updateScanProgress({
|
||||
scanning: true,
|
||||
phase: 'preparing',
|
||||
folder_total: customFolders.length,
|
||||
folder_current: 0,
|
||||
});
|
||||
|
||||
// Sequential scan for multiple folders
|
||||
for (const [folderKey, fileSet] of customFolders) {
|
||||
customFolderCurrent += 1;
|
||||
const parts = folderKey.split('|');
|
||||
if (parts.length < 3) continue;
|
||||
const type = parts[0], path_idx = parts[1], subfolder = parts.slice(2).join('|');
|
||||
|
||||
const params = new URLSearchParams({ type, path_idx, subfolder });
|
||||
const targetUrl = '/anomalous/scan?' + params.toString();
|
||||
|
||||
const currentReqBody = { ...reqBody, target_files: Array.from(fileSet) };
|
||||
|
||||
try {
|
||||
const res = await fetch(targetUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(currentReqBody)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'ok') {
|
||||
// wait for this folder to finish scanning before starting next
|
||||
await new Promise(resolve => {
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
const statusUrl = '/anomalous/scan_status?' + params.toString();
|
||||
const statusRes = await fetch(statusUrl);
|
||||
const statusData = await statusRes.json();
|
||||
updateScanProgress({
|
||||
...statusData,
|
||||
folder_total: customFolders.length,
|
||||
folder_current: customFolderCurrent,
|
||||
folder: subfolder,
|
||||
});
|
||||
if (!statusData.scanning) {
|
||||
clearInterval(poll);
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
clearInterval(poll);
|
||||
resolve();
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
} else {
|
||||
console.error("Scan failed for folder: " + folderKey, data.message);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Request failed for folder: " + folderKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
setActiveScanButtonState(false);
|
||||
finishScanProgress();
|
||||
try {
|
||||
if (app?.refreshComboInNodes) await app.refreshComboInNodes();
|
||||
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
|
||||
} catch (e) {
|
||||
console.warn('[AMB] Error reloading hashes or combo nodes:', e);
|
||||
}
|
||||
if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) {
|
||||
window.anomalous_resolve_all_missing_nodes(true);
|
||||
}
|
||||
this.loadModels();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Existing logic for 'all' mode or single file
|
||||
let finalTargetFiles = targetFiles;
|
||||
let currentReqBody = { ...reqBody };
|
||||
|
||||
let targetUrl = '';
|
||||
if (isGlobal && targetMode !== 'custom') {
|
||||
targetUrl = '/anomalous/scan_all';
|
||||
} else {
|
||||
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder });
|
||||
if (finalTargetFiles) {
|
||||
currentReqBody.target_files = finalTargetFiles.split(',');
|
||||
}
|
||||
targetUrl = '/anomalous/scan?' + params.toString();
|
||||
}
|
||||
|
||||
const res = await fetch(targetUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(currentReqBody)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.status === 'ok') {
|
||||
updateScanProgress({ scanning: true, phase: 'preparing', recovered: data.recovered });
|
||||
setActiveScanButtonState(true);
|
||||
|
||||
// Start polling
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
let statusUrl = '/anomalous/global_scan_status';
|
||||
if (!isGlobal) {
|
||||
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder });
|
||||
statusUrl = '/anomalous/scan_status?' + params.toString();
|
||||
}
|
||||
const statusRes = await fetch(statusUrl);
|
||||
const statusData = await statusRes.json();
|
||||
updateScanProgress(statusData);
|
||||
if (!statusData.scanning) {
|
||||
clearInterval(poll);
|
||||
if (statusData.interrupted) failScanProgress(t('scanProgressInterrupted'));
|
||||
else finishScanProgress();
|
||||
setActiveScanButtonState(false);
|
||||
|
||||
try {
|
||||
if (app?.refreshComboInNodes) await app.refreshComboInNodes();
|
||||
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
|
||||
} catch (e) {
|
||||
console.warn('[AMB] Error reloading hashes or combo nodes:', e);
|
||||
}
|
||||
if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) {
|
||||
window.anomalous_resolve_all_missing_nodes(true);
|
||||
}
|
||||
this.loadModels();
|
||||
}
|
||||
} catch (err) {
|
||||
clearInterval(poll);
|
||||
setActiveScanButtonState(false);
|
||||
}
|
||||
}, 2000);
|
||||
closeWizard();
|
||||
return; // return early so we don't remove wizard again below
|
||||
} else {
|
||||
failScanProgress(t('sidebarScanFailed') + data.message);
|
||||
alert(t('sidebarScanFailed') + data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
failScanProgress(String(e));
|
||||
alert("Error: " + e);
|
||||
}
|
||||
|
||||
closeWizard();
|
||||
};
|
||||
|
||||
const footer = document.createElement('div');
|
||||
footer.style.marginTop = '16px';
|
||||
footer.style.paddingTop = '16px';
|
||||
footer.style.borderTop = '1px solid rgba(255,255,255,0.08)';
|
||||
footer.style.display = 'flex';
|
||||
footer.style.justifyContent = 'flex-end';
|
||||
footer.style.gap = '8px';
|
||||
footer.style.flexShrink = '0';
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.textContent = t('sidebarCancel');
|
||||
closeBtn.style.padding = '8px 16px';
|
||||
closeBtn.style.background = 'transparent';
|
||||
closeBtn.style.color = '#9ca3af';
|
||||
closeBtn.style.border = 'none';
|
||||
closeBtn.style.borderRadius = '4px';
|
||||
closeBtn.style.cursor = 'pointer';
|
||||
closeBtn.style.fontSize = '0.95em';
|
||||
closeBtn.style.fontWeight = '500';
|
||||
closeBtn.style.textTransform = 'uppercase';
|
||||
closeBtn.style.transition = 'background 0.2s';
|
||||
closeBtn.onmouseover = () => closeBtn.style.background = 'rgba(255, 255, 255, 0.08)';
|
||||
closeBtn.onmouseout = () => closeBtn.style.background = 'transparent';
|
||||
closeBtn.onclick = closeWizard;
|
||||
|
||||
const startBtn = document.createElement('button');
|
||||
startBtn.textContent = t('sidebarExecute');
|
||||
startBtn.style.padding = '8px 24px';
|
||||
startBtn.style.background = '#e5e7eb';
|
||||
startBtn.style.color = '#111827';
|
||||
startBtn.style.border = 'none';
|
||||
startBtn.style.borderRadius = '4px';
|
||||
startBtn.style.cursor = 'pointer';
|
||||
startBtn.style.fontSize = '0.95em';
|
||||
startBtn.style.fontWeight = '600';
|
||||
startBtn.style.textTransform = 'uppercase';
|
||||
startBtn.style.transition = 'background 0.2s, box-shadow 0.2s';
|
||||
startBtn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
|
||||
startBtn.onmouseover = () => {
|
||||
startBtn.style.background = '#ffffff';
|
||||
startBtn.style.boxShadow = '0 3px 8px rgba(0,0,0,0.4)';
|
||||
};
|
||||
startBtn.onmouseout = () => {
|
||||
startBtn.style.background = '#e5e7eb';
|
||||
startBtn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
|
||||
};
|
||||
startBtn.onclick = doScan;
|
||||
|
||||
footer.appendChild(closeBtn);
|
||||
footer.appendChild(startBtn);
|
||||
content.appendChild(footer);
|
||||
wizard.appendChild(content);
|
||||
document.body.appendChild(wizard);
|
||||
}
|
||||
|
||||
export async function formatScanCompletionToast(model) {
|
||||
const isZh = window.anomalous_browser_lang === 'zh';
|
||||
const fallbackName = model.name || model.filename;
|
||||
try {
|
||||
const findRes = await fetch('/anomalous/find_model?search=' + encodeURIComponent(model.filename));
|
||||
if (findRes.ok) {
|
||||
const updated = await findRes.json();
|
||||
const target = updated?.model || updated || model;
|
||||
if (target && target.metadata) {
|
||||
const meta = target.metadata;
|
||||
const isCivitai = Boolean(
|
||||
(meta.id && meta.id !== -1) ||
|
||||
(meta.modelId && meta.modelId !== -1) ||
|
||||
(meta.model_id && meta.model_id !== -1) ||
|
||||
(meta.version_id && meta.version_id !== -1) ||
|
||||
meta.civitai_url
|
||||
);
|
||||
const hasPreview = Boolean(target.preview_url);
|
||||
const baseModel = meta.baseModel;
|
||||
|
||||
if (isCivitai && hasPreview) {
|
||||
return isZh ? `✓ 已从 Civitai 获取封面与模型信息!` : `✓ Civitai cover & metadata fetched!`;
|
||||
}
|
||||
if (isCivitai && !hasPreview) {
|
||||
return isZh ? `✓ 已匹配到 Civitai 信息(线上未提供封面)` : `✓ Civitai metadata matched (no cover online)`;
|
||||
}
|
||||
if (!isCivitai) {
|
||||
if (baseModel) {
|
||||
return isZh
|
||||
? `ℹ️ 非 Civitai 模型:已识别底模为 [${baseModel}]`
|
||||
: `ℹ️ Non-Civitai model: inferred base model [${baseModel}]`;
|
||||
}
|
||||
return isZh
|
||||
? `ℹ️ 未在 Civitai 匹配到此模型`
|
||||
: `ℹ️ No Civitai match found for this model`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fallback to standard text
|
||||
}
|
||||
return isZh ? `✓ 模型 [${fallbackName}] 扫描完成!` : `✓ Model [${fallbackName}] scanned!`;
|
||||
}
|
||||
|
||||
function pollDirectScanStatus(params, titleText, model, onComplete) {
|
||||
const statusUrl = '/anomalous/scan_status?' + params.toString();
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
const statusRes = await fetch(statusUrl);
|
||||
const statusData = await statusRes.json();
|
||||
updateScanProgress(statusData, titleText);
|
||||
|
||||
if (!statusData.scanning) {
|
||||
clearInterval(poll);
|
||||
if (statusData.interrupted) {
|
||||
failScanProgress(t('scanProgressInterrupted'));
|
||||
showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '扫描被中断' : 'Scan interrupted');
|
||||
} else {
|
||||
const toastMsg = await formatScanCompletionToast(model);
|
||||
finishScanProgress(toastMsg);
|
||||
showWorkbenchToast(toastMsg);
|
||||
}
|
||||
onComplete(true);
|
||||
}
|
||||
} catch (err) {
|
||||
clearInterval(poll);
|
||||
failScanProgress(String(err));
|
||||
onComplete(false);
|
||||
}
|
||||
}, 1200);
|
||||
return poll;
|
||||
}
|
||||
|
||||
export async function triggerDirectModelScan(model, triggerBtn = null, browserInstance = null) {
|
||||
const browser = browserInstance || this || {};
|
||||
if (!model || !model.filename) return;
|
||||
|
||||
const modelLabel = model.name || model.filename;
|
||||
const isZh = window.anomalous_browser_lang === 'zh';
|
||||
const titleText = isZh ? `精准扫描: ${modelLabel}` : `Scanning: ${modelLabel}`;
|
||||
|
||||
if (triggerBtn) {
|
||||
triggerBtn.classList.add('anomalous-radar-spinning');
|
||||
if (triggerBtn.firstElementChild) {
|
||||
triggerBtn.firstElementChild.style.animation = 'anomalous-radar-spin 1.2s linear infinite';
|
||||
triggerBtn.firstElementChild.style.stroke = '#10b981';
|
||||
}
|
||||
triggerBtn.style.pointerEvents = 'none';
|
||||
triggerBtn.style.opacity = '0.7';
|
||||
}
|
||||
setActiveScanButtonState(true);
|
||||
updateScanProgress({ scanning: true, phase: 'preparing', total: 1, current: 0, filename: model.filename }, titleText);
|
||||
|
||||
const resetBtn = () => {
|
||||
setActiveScanButtonState(false);
|
||||
if (triggerBtn) {
|
||||
triggerBtn.classList.remove('anomalous-radar-spinning');
|
||||
if (triggerBtn.firstElementChild) {
|
||||
triggerBtn.firstElementChild.style.animation = '';
|
||||
triggerBtn.firstElementChild.style.stroke = '';
|
||||
}
|
||||
triggerBtn.style.pointerEvents = '';
|
||||
triggerBtn.style.opacity = '';
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
type: browser.currentType || 'checkpoints',
|
||||
path_idx: browser.currentPathIdx || 0,
|
||||
subfolder: browser.currentSubfolder || '/',
|
||||
});
|
||||
const reqBody = {
|
||||
target_files: [model.filename],
|
||||
offline_only: false,
|
||||
skip_rename: true,
|
||||
virtual_rename: false,
|
||||
physical_rename: false,
|
||||
force_overwrite: false
|
||||
};
|
||||
|
||||
const res = await fetch('/anomalous/scan?' + params.toString(), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqBody)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.status === 'ok') {
|
||||
showWorkbenchToast(isZh ? `开始精准扫描: ${modelLabel}` : `Scanning model: ${modelLabel}`);
|
||||
pollDirectScanStatus(params, titleText, model, async () => {
|
||||
resetBtn();
|
||||
if (typeof browser.loadModels === 'function') {
|
||||
browser.loadModels();
|
||||
}
|
||||
try {
|
||||
if (app?.refreshComboInNodes) await app.refreshComboInNodes();
|
||||
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
|
||||
} catch (e) {
|
||||
console.warn('[AMB] Error reloading hashes or combo nodes:', e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resetBtn();
|
||||
const errMsg = data.message || (isZh ? '扫描启动失败' : 'Failed to start scan');
|
||||
failScanProgress(errMsg);
|
||||
showWorkbenchToast(errMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
resetBtn();
|
||||
failScanProgress(String(e));
|
||||
showWorkbenchToast(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user