Add CivitAI re-linking functionality and related UI components. Fixes #216

- Implemented new API endpoints for re-linking models to CivitAI.
- Added context menu options for re-linking in both Lora and Checkpoint context menus.
- Created a modal for user confirmation and input for CivitAI model URL.
- Updated styles for the new modal and context menu items.
- Enhanced error handling and user feedback during the re-linking process.
This commit is contained in:
Will Miao
2025-06-09 17:23:03 +08:00
parent 4e5c8b2dd0
commit 787e37b7c6
13 changed files with 438 additions and 454 deletions

View File

@@ -4,6 +4,9 @@ import { showToast, getNSFWLevelName, openExampleImagesFolder } from '../../util
import { NSFW_LEVELS } from '../../utils/constants.js';
import { getStorageItem } from '../../utils/storageHelpers.js';
import { showExcludeModal } from '../../utils/modalUtils.js';
import { modalManager } from '../../managers/ModalManager.js';
import { state } from '../../state/index.js';
import { resetAndReload } from '../../api/checkpointApi.js';
export class CheckpointContextMenu extends BaseContextMenu {
constructor() {
@@ -56,6 +59,10 @@ export class CheckpointContextMenu extends BaseContextMenu {
// Refresh metadata from CivitAI
refreshSingleCheckpointMetadata(this.currentCard.dataset.filepath);
break;
case 'relink-civitai':
// Handle re-link to Civitai
this.showRelinkCivitaiModal();
break;
case 'set-nsfw':
// Set NSFW level
this.showNSFWLevelSelector(null, null, this.currentCard);
@@ -319,4 +326,87 @@ export class CheckpointContextMenu extends BaseContextMenu {
// Show selector
selector.style.display = 'block';
}
showRelinkCivitaiModal() {
const filePath = this.currentCard.dataset.filepath;
if (!filePath) return;
// Set up confirm button handler
const confirmBtn = document.getElementById('confirmRelinkBtn');
const urlInput = document.getElementById('civitaiModelUrl');
const errorDiv = document.getElementById('civitaiModelUrlError');
// Remove previous event listener if exists
if (this._boundRelinkHandler) {
confirmBtn.removeEventListener('click', this._boundRelinkHandler);
}
// Create new bound handler
this._boundRelinkHandler = async () => {
const url = urlInput.value.trim();
const modelVersionId = this.extractModelVersionId(url);
if (!modelVersionId) {
errorDiv.textContent = 'Invalid URL format. Must include modelVersionId parameter.';
return;
}
errorDiv.textContent = '';
modalManager.closeModal('relinkCivitaiModal');
try {
state.loadingManager.showSimpleLoading('Re-linking to Civitai...');
const response = await fetch('/api/checkpoints/relink-civitai', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
file_path: filePath,
model_version_id: modelVersionId
})
});
if (!response.ok) {
throw new Error(`Failed to re-link model: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
showToast('Model successfully re-linked to Civitai', 'success');
// Reload the current view to show updated data
await resetAndReload();
} else {
throw new Error(data.error || 'Failed to re-link model');
}
} catch (error) {
console.error('Error re-linking model:', error);
showToast(`Error: ${error.message}`, 'error');
} finally {
state.loadingManager.hide();
}
};
// Set new event listener
confirmBtn.addEventListener('click', this._boundRelinkHandler);
// Clear previous input
urlInput.value = '';
errorDiv.textContent = '';
// Show modal
modalManager.showModal('relinkCivitaiModal');
}
extractModelVersionId(url) {
try {
const parsedUrl = new URL(url);
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
return modelVersionId;
} catch (e) {
return null;
}
}
}

View File

@@ -1,9 +1,11 @@
import { BaseContextMenu } from './BaseContextMenu.js';
import { refreshSingleLoraMetadata, saveModelMetadata, replacePreview } from '../../api/loraApi.js';
import { refreshSingleLoraMetadata, saveModelMetadata, replacePreview, resetAndReload } from '../../api/loraApi.js';
import { showToast, getNSFWLevelName, copyToClipboard, sendLoraToWorkflow, openExampleImagesFolder } from '../../utils/uiHelpers.js';
import { NSFW_LEVELS } from '../../utils/constants.js';
import { getStorageItem } from '../../utils/storageHelpers.js';
import { showExcludeModal, showDeleteModal } from '../../utils/modalUtils.js';
import { modalManager } from '../../managers/ModalManager.js';
import { state } from '../../state/index.js';
export class LoraContextMenu extends BaseContextMenu {
constructor() {
@@ -64,6 +66,9 @@ export class LoraContextMenu extends BaseContextMenu {
case 'refresh-metadata':
refreshSingleLoraMetadata(this.currentCard.dataset.filepath);
break;
case 'relink-civitai':
this.showRelinkCivitaiModal();
break;
case 'set-nsfw':
this.showNSFWLevelSelector(null, null, this.currentCard);
break;
@@ -93,6 +98,90 @@ export class LoraContextMenu extends BaseContextMenu {
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
}
// New method to handle re-link to Civitai
showRelinkCivitaiModal() {
const filePath = this.currentCard.dataset.filepath;
if (!filePath) return;
// Set up confirm button handler
const confirmBtn = document.getElementById('confirmRelinkBtn');
const urlInput = document.getElementById('civitaiModelUrl');
const errorDiv = document.getElementById('civitaiModelUrlError');
// Remove previous event listener if exists
if (this._boundRelinkHandler) {
confirmBtn.removeEventListener('click', this._boundRelinkHandler);
}
// Create new bound handler
this._boundRelinkHandler = async () => {
const url = urlInput.value.trim();
const modelVersionId = this.extractModelVersionId(url);
if (!modelVersionId) {
errorDiv.textContent = 'Invalid URL format. Must include modelVersionId parameter.';
return;
}
errorDiv.textContent = '';
modalManager.closeModal('relinkCivitaiModal');
try {
state.loadingManager.showSimpleLoading('Re-linking to Civitai...');
const response = await fetch('/api/relink-civitai', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
file_path: filePath,
model_version_id: modelVersionId
})
});
if (!response.ok) {
throw new Error(`Failed to re-link model: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
showToast('Model successfully re-linked to Civitai', 'success');
// Reload the current view to show updated data
await resetAndReload();
} else {
throw new Error(data.error || 'Failed to re-link model');
}
} catch (error) {
console.error('Error re-linking model:', error);
showToast(`Error: ${error.message}`, 'error');
} finally {
state.loadingManager.hide();
}
};
// Set new event listener
confirmBtn.addEventListener('click', this._boundRelinkHandler);
// Clear previous input
urlInput.value = '';
errorDiv.textContent = '';
// Show modal
modalManager.showModal('relinkCivitaiModal');
}
extractModelVersionId(url) {
try {
const parsedUrl = new URL(url);
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
return modelVersionId;
} catch (e) {
return null;
}
}
// NSFW Selector methods from the original context menu
initNSFWSelector() {
// Close button