mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Implement model move, import, and download functionalities with corresponding UI and API updates.
This commit is contained in:
@@ -328,11 +328,11 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tree-node.has-children > .tree-node-content .tree-expand-icon {
|
||||
.tree-node.has-children>.tree-node-content .tree-expand-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tree-node:not(.has-children) > .tree-node-content .tree-expand-icon {
|
||||
.tree-node:not(.has-children)>.tree-node-content .tree-expand-icon {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -470,11 +470,11 @@
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.inline-toggle-container .toggle-switch input:checked + .toggle-slider {
|
||||
.inline-toggle-container .toggle-switch input:checked+.toggle-slider {
|
||||
background-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.inline-toggle-container .toggle-switch input:checked + .toggle-slider:before {
|
||||
.inline-toggle-container .toggle-switch input:checked+.toggle-slider:before {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
import { getStorageItem, getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js';
|
||||
import {
|
||||
getCompleteApiConfig,
|
||||
getCurrentModelType,
|
||||
import {
|
||||
getCompleteApiConfig,
|
||||
getCurrentModelType,
|
||||
isValidModelType,
|
||||
DOWNLOAD_ENDPOINTS,
|
||||
WS_ENDPOINTS
|
||||
@@ -51,7 +51,7 @@ export class BaseModelApiClient {
|
||||
async fetchModelsPage(page = 1, pageSize = null) {
|
||||
const pageState = this.getPageState();
|
||||
const actualPageSize = pageSize || pageState.pageSize || this.apiConfig.config.defaultPageSize;
|
||||
|
||||
|
||||
try {
|
||||
const params = this._buildQueryParams({
|
||||
page,
|
||||
@@ -63,9 +63,9 @@ export class BaseModelApiClient {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${this.apiConfig.config.displayName}s: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
return {
|
||||
items: data.items,
|
||||
totalItems: data.total,
|
||||
@@ -74,7 +74,7 @@ export class BaseModelApiClient {
|
||||
hasMore: page < data.total_pages,
|
||||
folders: data.folders
|
||||
};
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${this.apiConfig.config.displayName}s:`, error);
|
||||
showToast('toast.api.fetchFailed', { type: this.apiConfig.config.displayName, message: error.message }, 'error');
|
||||
@@ -84,7 +84,7 @@ export class BaseModelApiClient {
|
||||
|
||||
async loadMoreWithVirtualScroll(resetPage = false, updateFolders = false) {
|
||||
const pageState = this.getPageState();
|
||||
|
||||
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading(`Loading more ${this.apiConfig.config.displayName}s...`);
|
||||
|
||||
@@ -92,22 +92,22 @@ export class BaseModelApiClient {
|
||||
if (resetPage) {
|
||||
pageState.currentPage = 1; // Reset to first page
|
||||
}
|
||||
|
||||
|
||||
const result = await this.fetchModelsPage(pageState.currentPage, pageState.pageSize);
|
||||
|
||||
|
||||
state.virtualScroller.refreshWithData(
|
||||
result.items,
|
||||
result.totalItems,
|
||||
result.hasMore
|
||||
);
|
||||
|
||||
|
||||
pageState.hasMore = result.hasMore;
|
||||
pageState.currentPage = pageState.currentPage + 1;
|
||||
|
||||
|
||||
if (updateFolders) {
|
||||
sidebarManager.refresh();
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error reloading ${this.apiConfig.config.displayName}s:`, error);
|
||||
@@ -128,13 +128,13 @@ export class BaseModelApiClient {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_path: filePath })
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete ${this.apiConfig.config.singularName}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
if (state.virtualScroller) {
|
||||
state.virtualScroller.removeItemByFilePath(filePath);
|
||||
@@ -162,13 +162,13 @@ export class BaseModelApiClient {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_path: filePath })
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to exclude ${this.apiConfig.config.singularName}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
if (state.virtualScroller) {
|
||||
state.virtualScroller.removeItemByFilePath(filePath);
|
||||
@@ -190,7 +190,7 @@ export class BaseModelApiClient {
|
||||
async renameModelFile(filePath, newFileName) {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading(`Renaming ${this.apiConfig.config.singularName} file...`);
|
||||
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.rename, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -203,12 +203,12 @@ export class BaseModelApiClient {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
state.virtualScroller.updateSingleItem(filePath, {
|
||||
file_name: newFileName,
|
||||
state.virtualScroller.updateSingleItem(filePath, {
|
||||
file_name: newFileName,
|
||||
file_path: result.new_file_path,
|
||||
preview_url: result.new_preview_path
|
||||
});
|
||||
|
||||
|
||||
showToast('toast.api.fileNameUpdated', {}, 'success');
|
||||
} else {
|
||||
showToast('toast.api.fileRenameFailed', { error: result.error || 'Unknown error' }, 'error');
|
||||
@@ -227,21 +227,21 @@ export class BaseModelApiClient {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*,video/mp4';
|
||||
|
||||
|
||||
input.onchange = async () => {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
|
||||
|
||||
const file = input.files[0];
|
||||
await this.uploadPreview(filePath, file);
|
||||
};
|
||||
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
async uploadPreview(filePath, file, nsfwLevel = 0) {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Uploading preview...');
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('preview_file', file);
|
||||
formData.append('model_path', filePath);
|
||||
@@ -251,18 +251,18 @@ export class BaseModelApiClient {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const pageState = this.getPageState();
|
||||
|
||||
|
||||
const timestamp = Date.now();
|
||||
if (pageState.previewVersions) {
|
||||
pageState.previewVersions.set(filePath, timestamp);
|
||||
|
||||
|
||||
const storageKey = `${this.modelType}_preview_versions`;
|
||||
saveMapToStorage(storageKey, pageState.previewVersions);
|
||||
}
|
||||
@@ -285,7 +285,7 @@ export class BaseModelApiClient {
|
||||
async saveModelMetadata(filePath, data) {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Saving metadata...');
|
||||
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.save, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -339,18 +339,18 @@ export class BaseModelApiClient {
|
||||
state.loadingManager.showSimpleLoading(
|
||||
`${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${this.apiConfig.config.displayName}s...`
|
||||
);
|
||||
|
||||
|
||||
const url = new URL(this.apiConfig.endpoints.scan, window.location.origin);
|
||||
url.searchParams.append('full_rebuild', fullRebuild);
|
||||
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to refresh ${this.apiConfig.config.displayName}s: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
resetAndReload(true);
|
||||
|
||||
|
||||
showToast('toast.api.refreshComplete', { action: fullRebuild ? 'Full rebuild' : 'Refresh' }, 'success');
|
||||
} catch (error) {
|
||||
console.error('Refresh failed:', error);
|
||||
@@ -364,7 +364,7 @@ export class BaseModelApiClient {
|
||||
async refreshSingleModelMetadata(filePath) {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Refreshing metadata...');
|
||||
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.fetchCivitai, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -376,7 +376,7 @@ export class BaseModelApiClient {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
if (data.metadata && state.virtualScroller) {
|
||||
state.virtualScroller.updateSingleItem(filePath, data.metadata);
|
||||
@@ -399,21 +399,21 @@ export class BaseModelApiClient {
|
||||
|
||||
async fetchCivitaiMetadata() {
|
||||
let ws = null;
|
||||
|
||||
|
||||
await state.loadingManager.showWithProgress(async (loading) => {
|
||||
try {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
||||
|
||||
|
||||
const operationComplete = new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch(data.status) {
|
||||
|
||||
switch (data.status) {
|
||||
case 'started':
|
||||
loading.setStatus('Starting metadata fetch...');
|
||||
break;
|
||||
|
||||
|
||||
case 'processing':
|
||||
const percent = ((data.processed / data.total) * 100).toFixed(1);
|
||||
loading.setProgress(percent);
|
||||
@@ -421,7 +421,7 @@ export class BaseModelApiClient {
|
||||
`Processing (${data.processed}/${data.total}) ${data.current_name}`
|
||||
);
|
||||
break;
|
||||
|
||||
|
||||
case 'completed':
|
||||
loading.setProgress(100);
|
||||
loading.setStatus(
|
||||
@@ -429,34 +429,34 @@ export class BaseModelApiClient {
|
||||
);
|
||||
resolve();
|
||||
break;
|
||||
|
||||
|
||||
case 'error':
|
||||
reject(new Error(data.error));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
ws.onerror = (error) => {
|
||||
reject(new Error('WebSocket error: ' + error.message));
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Wait for WebSocket connection to establish
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
ws.onerror = reject;
|
||||
});
|
||||
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.fetchAllCivitai, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch metadata');
|
||||
}
|
||||
|
||||
|
||||
// Wait for the operation to complete via WebSocket
|
||||
await operationComplete;
|
||||
|
||||
@@ -492,15 +492,15 @@ export class BaseModelApiClient {
|
||||
for (let i = 0; i < filePaths.length; i++) {
|
||||
const filePath = filePaths[i];
|
||||
const fileName = filePath.split('/').pop();
|
||||
|
||||
|
||||
try {
|
||||
const overallProgress = Math.floor((i / totalItems) * 100);
|
||||
progressController.updateProgress(
|
||||
overallProgress,
|
||||
fileName,
|
||||
overallProgress,
|
||||
fileName,
|
||||
`Processing ${i + 1}/${totalItems}: ${fileName}`
|
||||
);
|
||||
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.fetchCivitai, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -512,7 +512,7 @@ export class BaseModelApiClient {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
if (data.metadata && state.virtualScroller) {
|
||||
state.virtualScroller.updateSingleItem(filePath, data.metadata);
|
||||
@@ -521,12 +521,12 @@ export class BaseModelApiClient {
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to refresh metadata');
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error refreshing metadata for ${fileName}:`, error);
|
||||
failedItems.push({ filePath, fileName, error: error.message });
|
||||
}
|
||||
|
||||
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ export class BaseModelApiClient {
|
||||
} else if (successCount > 0) {
|
||||
completionMessage = translate('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, `Refreshed ${successCount} of ${totalItems} ${this.apiConfig.config.displayName}s`);
|
||||
showToast('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, 'warning');
|
||||
|
||||
|
||||
// if (failedItems.length > 0) {
|
||||
// const failureMessage = failedItems.length <= 3
|
||||
// ? failedItems.map(item => `${item.fileName}: ${item.error}`).join('\n')
|
||||
@@ -770,7 +770,7 @@ export class BaseModelApiClient {
|
||||
|
||||
_buildQueryParams(baseParams, pageState) {
|
||||
const params = new URLSearchParams(baseParams);
|
||||
|
||||
|
||||
if (pageState.activeFolder !== null) {
|
||||
params.append('folder', pageState.activeFolder);
|
||||
}
|
||||
@@ -790,7 +790,7 @@ export class BaseModelApiClient {
|
||||
if (pageState.filters?.search) {
|
||||
params.append('search', pageState.filters.search);
|
||||
params.append('fuzzy', 'true');
|
||||
|
||||
|
||||
if (pageState.searchOptions) {
|
||||
params.append('search_filename', pageState.searchOptions.filename.toString());
|
||||
params.append('search_modelname', pageState.searchOptions.modelname.toString());
|
||||
@@ -804,7 +804,7 @@ export class BaseModelApiClient {
|
||||
}
|
||||
|
||||
params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false');
|
||||
|
||||
|
||||
if (pageState.filters) {
|
||||
if (pageState.filters.tags && Object.keys(pageState.filters.tags).length > 0) {
|
||||
Object.entries(pageState.filters.tags).forEach(([tag, state]) => {
|
||||
@@ -815,17 +815,17 @@ export class BaseModelApiClient {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
||||
pageState.filters.baseModel.forEach(model => {
|
||||
params.append('base_model', model);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Add license filters
|
||||
if (pageState.filters.license) {
|
||||
const licenseFilters = pageState.filters.license;
|
||||
|
||||
|
||||
if (licenseFilters.noCredit) {
|
||||
// For noCredit filter:
|
||||
// - 'include' means credit_required=False (no credit required)
|
||||
@@ -836,7 +836,7 @@ export class BaseModelApiClient {
|
||||
params.append('credit_required', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (licenseFilters.allowSelling) {
|
||||
// For allowSelling filter:
|
||||
// - 'include' means allow_selling_generated_content=True
|
||||
@@ -848,7 +848,7 @@ export class BaseModelApiClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (pageState.filters.modelTypes && pageState.filters.modelTypes.length > 0) {
|
||||
pageState.filters.modelTypes.forEach((type) => {
|
||||
params.append('model_type', type);
|
||||
@@ -895,13 +895,13 @@ export class BaseModelApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async moveSingleModel(filePath, targetPath) {
|
||||
async moveSingleModel(filePath, targetPath, useDefaultPaths = false) {
|
||||
// Only allow move if supported
|
||||
if (!this.apiConfig.config.supportsMove) {
|
||||
showToast('toast.api.moveNotSupported', { type: this.apiConfig.config.displayName }, 'warning');
|
||||
return null;
|
||||
}
|
||||
if (filePath.substring(0, filePath.lastIndexOf('/')) === targetPath) {
|
||||
if (filePath.substring(0, filePath.lastIndexOf('/')) === targetPath && !useDefaultPaths) {
|
||||
showToast('toast.api.alreadyInFolder', { type: this.apiConfig.config.displayName }, 'info');
|
||||
return null;
|
||||
}
|
||||
@@ -913,7 +913,8 @@ export class BaseModelApiClient {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
target_path: targetPath
|
||||
target_path: targetPath,
|
||||
use_default_paths: useDefaultPaths
|
||||
})
|
||||
});
|
||||
|
||||
@@ -941,12 +942,12 @@ export class BaseModelApiClient {
|
||||
return null;
|
||||
}
|
||||
|
||||
async moveBulkModels(filePaths, targetPath) {
|
||||
async moveBulkModels(filePaths, targetPath, useDefaultPaths = false) {
|
||||
if (!this.apiConfig.config.supportsMove) {
|
||||
showToast('toast.api.bulkMoveNotSupported', { type: this.apiConfig.config.displayName }, 'warning');
|
||||
return [];
|
||||
}
|
||||
const movedPaths = filePaths.filter(path => {
|
||||
const movedPaths = useDefaultPaths ? filePaths : filePaths.filter(path => {
|
||||
return path.substring(0, path.lastIndexOf('/')) !== targetPath;
|
||||
});
|
||||
|
||||
@@ -962,7 +963,8 @@ export class BaseModelApiClient {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_paths: movedPaths,
|
||||
target_path: targetPath
|
||||
target_path: targetPath,
|
||||
use_default_paths: useDefaultPaths
|
||||
})
|
||||
});
|
||||
|
||||
@@ -974,10 +976,10 @@ export class BaseModelApiClient {
|
||||
|
||||
if (result.success) {
|
||||
if (result.failure_count > 0) {
|
||||
showToast('toast.api.bulkMovePartial', {
|
||||
successCount: result.success_count,
|
||||
type: this.apiConfig.config.displayName,
|
||||
failureCount: result.failure_count
|
||||
showToast('toast.api.bulkMovePartial', {
|
||||
successCount: result.success_count,
|
||||
type: this.apiConfig.config.displayName,
|
||||
failureCount: result.failure_count
|
||||
}, 'warning');
|
||||
console.log('Move operation results:', result.results);
|
||||
const failedFiles = result.results
|
||||
@@ -987,18 +989,18 @@ export class BaseModelApiClient {
|
||||
return `${fileName}: ${r.message}`;
|
||||
});
|
||||
if (failedFiles.length > 0) {
|
||||
const failureMessage = failedFiles.length <= 3
|
||||
const failureMessage = failedFiles.length <= 3
|
||||
? failedFiles.join('\n')
|
||||
: failedFiles.slice(0, 3).join('\n') + `\n(and ${failedFiles.length - 3} more)`;
|
||||
showToast('toast.api.bulkMoveFailures', { failures: failureMessage }, 'warning', 6000);
|
||||
}
|
||||
} else {
|
||||
showToast('toast.api.bulkMoveSuccess', {
|
||||
successCount: result.success_count,
|
||||
type: this.apiConfig.config.displayName
|
||||
showToast('toast.api.bulkMoveSuccess', {
|
||||
successCount: result.success_count,
|
||||
type: this.apiConfig.config.displayName
|
||||
}, 'success');
|
||||
}
|
||||
|
||||
|
||||
// Return the results array with original_file_path and new_file_path
|
||||
return result.results || [];
|
||||
} else {
|
||||
@@ -1013,7 +1015,7 @@ export class BaseModelApiClient {
|
||||
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading(`Deleting ${this.apiConfig.config.displayName.toLowerCase()}s...`);
|
||||
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.bulkDelete, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -1023,13 +1025,13 @@ export class BaseModelApiClient {
|
||||
file_paths: filePaths
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete ${this.apiConfig.config.displayName.toLowerCase()}s: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
@@ -1050,20 +1052,20 @@ export class BaseModelApiClient {
|
||||
|
||||
async downloadExampleImages(modelHashes, modelTypes = null) {
|
||||
let ws = null;
|
||||
|
||||
|
||||
await state.loadingManager.showWithProgress(async (loading) => {
|
||||
try {
|
||||
// Connect to WebSocket for progress updates
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
||||
|
||||
|
||||
const operationComplete = new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
|
||||
if (data.type !== 'example_images_progress') return;
|
||||
|
||||
switch(data.status) {
|
||||
|
||||
switch (data.status) {
|
||||
case 'running':
|
||||
const percent = ((data.processed / data.total) * 100).toFixed(1);
|
||||
loading.setProgress(percent);
|
||||
@@ -1071,7 +1073,7 @@ export class BaseModelApiClient {
|
||||
`Processing (${data.processed}/${data.total}) ${data.current_model || ''}`
|
||||
);
|
||||
break;
|
||||
|
||||
|
||||
case 'completed':
|
||||
loading.setProgress(100);
|
||||
loading.setStatus(
|
||||
@@ -1079,33 +1081,33 @@ export class BaseModelApiClient {
|
||||
);
|
||||
resolve();
|
||||
break;
|
||||
|
||||
|
||||
case 'error':
|
||||
reject(new Error(data.error));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
ws.onerror = (error) => {
|
||||
reject(new Error('WebSocket error: ' + error.message));
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Wait for WebSocket connection to establish
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
ws.onerror = reject;
|
||||
});
|
||||
|
||||
|
||||
// Get the output directory from state
|
||||
const outputDir = state.global?.settings?.example_images_path || '';
|
||||
if (!outputDir) {
|
||||
throw new Error('Please set the example images path in the settings first.');
|
||||
}
|
||||
|
||||
|
||||
// Determine optimize setting
|
||||
const optimize = state.global?.settings?.optimize_example_images ?? true;
|
||||
|
||||
|
||||
// Make the API request to start the download process
|
||||
const response = await fetch(DOWNLOAD_ENDPOINTS.exampleImages, {
|
||||
method: 'POST',
|
||||
@@ -1119,18 +1121,18 @@ export class BaseModelApiClient {
|
||||
model_types: modelTypes || [this.apiConfig.config.singularName]
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to download example images');
|
||||
}
|
||||
|
||||
|
||||
// Wait for the operation to complete via WebSocket
|
||||
await operationComplete;
|
||||
|
||||
|
||||
showToast('toast.api.exampleImagesDownloadSuccess', {}, 'success');
|
||||
return true;
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error downloading example images:', error);
|
||||
showToast('toast.api.exampleImagesDownloadFailed', { message: error.message }, 'error');
|
||||
@@ -1150,13 +1152,13 @@ export class BaseModelApiClient {
|
||||
try {
|
||||
const params = new URLSearchParams({ file_path: filePath });
|
||||
const response = await fetch(`${this.apiConfig.endpoints.metadata}?${params}`);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} metadata: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
return data.metadata;
|
||||
} else {
|
||||
@@ -1172,13 +1174,13 @@ export class BaseModelApiClient {
|
||||
try {
|
||||
const params = new URLSearchParams({ file_path: filePath });
|
||||
const response = await fetch(`${this.apiConfig.endpoints.modelDescription}?${params}`);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} description: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
return data.description;
|
||||
} else {
|
||||
@@ -1197,26 +1199,26 @@ export class BaseModelApiClient {
|
||||
*/
|
||||
async autoOrganizeModels(filePaths = null) {
|
||||
let ws = null;
|
||||
|
||||
|
||||
await state.loadingManager.showWithProgress(async (loading) => {
|
||||
try {
|
||||
// Connect to WebSocket for progress updates
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
||||
|
||||
|
||||
const operationComplete = new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
|
||||
if (data.type !== 'auto_organize_progress') return;
|
||||
|
||||
switch(data.status) {
|
||||
|
||||
switch (data.status) {
|
||||
case 'started':
|
||||
loading.setProgress(0);
|
||||
const operationType = data.operation_type === 'bulk' ? 'selected models' : 'all models';
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.starting', { type: operationType }, `Starting auto-organize for ${operationType}...`));
|
||||
break;
|
||||
|
||||
|
||||
case 'processing':
|
||||
const percent = data.total > 0 ? ((data.processed / data.total) * 90).toFixed(1) : 0;
|
||||
loading.setProgress(percent);
|
||||
@@ -1230,12 +1232,12 @@ export class BaseModelApiClient {
|
||||
}, `Processing (${data.processed}/${data.total}) - ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
|
||||
);
|
||||
break;
|
||||
|
||||
|
||||
case 'cleaning':
|
||||
loading.setProgress(95);
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.cleaning', {}, 'Cleaning up empty directories...'));
|
||||
break;
|
||||
|
||||
|
||||
case 'completed':
|
||||
loading.setProgress(100);
|
||||
loading.setStatus(
|
||||
@@ -1246,25 +1248,25 @@ export class BaseModelApiClient {
|
||||
total: data.total
|
||||
}, `Completed: ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
|
||||
);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(data);
|
||||
}, 1500);
|
||||
break;
|
||||
|
||||
|
||||
case 'error':
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.error', { error: data.error }, `Error: ${data.error}`));
|
||||
reject(new Error(data.error));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error during auto-organize:', error);
|
||||
reject(new Error('Connection error'));
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Start the auto-organize operation
|
||||
const endpoint = this.apiConfig.endpoints.autoOrganize;
|
||||
const exclusionPatterns = (state.global.settings.auto_organize_exclusions || [])
|
||||
@@ -1286,29 +1288,29 @@ export class BaseModelApiClient {
|
||||
};
|
||||
|
||||
const response = await fetch(endpoint, requestOptions);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to start auto-organize operation');
|
||||
}
|
||||
|
||||
|
||||
// Wait for the operation to complete via WebSocket
|
||||
const result = await operationComplete;
|
||||
|
||||
|
||||
// Show appropriate success message based on results
|
||||
if (result.failures === 0) {
|
||||
showToast('toast.loras.autoOrganizeSuccess', {
|
||||
showToast('toast.loras.autoOrganizeSuccess', {
|
||||
count: result.success,
|
||||
type: result.operation_type === 'bulk' ? 'selected models' : 'all models'
|
||||
}, 'success');
|
||||
} else {
|
||||
showToast('toast.loras.autoOrganizePartialSuccess', {
|
||||
showToast('toast.loras.autoOrganizePartialSuccess', {
|
||||
success: result.success,
|
||||
failures: result.failures,
|
||||
total: result.total
|
||||
}, 'warning');
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during auto-organize:', error);
|
||||
showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error');
|
||||
|
||||
@@ -15,17 +15,17 @@ export class DownloadManager {
|
||||
this.modelVersionId = null;
|
||||
this.modelId = null;
|
||||
this.source = null;
|
||||
|
||||
|
||||
this.initialized = false;
|
||||
this.selectedFolder = '';
|
||||
this.apiClient = null;
|
||||
this.useDefaultPath = false;
|
||||
|
||||
|
||||
this.loadingManager = new LoadingManager();
|
||||
this.folderTreeManager = new FolderTreeManager();
|
||||
this.folderClickHandler = null;
|
||||
this.updateTargetPath = this.updateTargetPath.bind(this);
|
||||
|
||||
|
||||
// Bound methods for event handling
|
||||
this.handleValidateAndFetchVersions = this.validateAndFetchVersions.bind(this);
|
||||
this.handleProceedToLocation = this.proceedToLocation.bind(this);
|
||||
@@ -38,11 +38,11 @@ export class DownloadManager {
|
||||
|
||||
showDownloadModal() {
|
||||
console.log('Showing unified download modal...');
|
||||
|
||||
|
||||
// Get API client for current page type
|
||||
this.apiClient = getModelApiClient();
|
||||
const config = this.apiClient.apiConfig.config;
|
||||
|
||||
|
||||
if (!this.initialized) {
|
||||
const modal = document.getElementById('downloadModal');
|
||||
if (!modal) {
|
||||
@@ -52,15 +52,15 @@ export class DownloadManager {
|
||||
this.initializeEventHandlers();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
|
||||
// Update modal title and labels based on model type
|
||||
this.updateModalLabels();
|
||||
|
||||
|
||||
modalManager.showModal('downloadModal', null, () => {
|
||||
this.cleanupFolderBrowser();
|
||||
});
|
||||
this.resetSteps();
|
||||
|
||||
|
||||
// Auto-focus on the URL input
|
||||
setTimeout(() => {
|
||||
const urlInput = document.getElementById('modelUrl');
|
||||
@@ -78,23 +78,23 @@ export class DownloadManager {
|
||||
document.getElementById('backToUrlBtn').addEventListener('click', this.handleBackToUrl);
|
||||
document.getElementById('backToVersionsBtn').addEventListener('click', this.handleBackToVersions);
|
||||
document.getElementById('closeDownloadModal').addEventListener('click', this.handleCloseModal);
|
||||
|
||||
|
||||
// Default path toggle handler
|
||||
document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath);
|
||||
}
|
||||
|
||||
updateModalLabels() {
|
||||
const config = this.apiClient.apiConfig.config;
|
||||
|
||||
|
||||
// Update modal title
|
||||
document.getElementById('downloadModalTitle').textContent = translate('modals.download.titleWithType', { type: config.displayName });
|
||||
|
||||
|
||||
// Update URL label
|
||||
document.getElementById('modelUrlLabel').textContent = translate('modals.download.civitaiUrl');
|
||||
|
||||
|
||||
// Update root selection label
|
||||
document.getElementById('modelRootLabel').textContent = translate('modals.download.selectTypeRoot', { type: config.displayName });
|
||||
|
||||
|
||||
// Update path preview labels
|
||||
const pathLabels = document.querySelectorAll('.path-preview label');
|
||||
pathLabels.forEach(label => {
|
||||
@@ -102,7 +102,7 @@ export class DownloadManager {
|
||||
label.textContent = translate('modals.download.locationPreview') + ':';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Update initial path text
|
||||
const pathText = document.querySelector('#targetPathDisplay .path-text');
|
||||
if (pathText) {
|
||||
@@ -115,27 +115,27 @@ export class DownloadManager {
|
||||
document.getElementById('urlStep').style.display = 'block';
|
||||
document.getElementById('modelUrl').value = '';
|
||||
document.getElementById('urlError').textContent = '';
|
||||
|
||||
|
||||
// Clear folder path input
|
||||
const folderPathInput = document.getElementById('folderPath');
|
||||
if (folderPathInput) {
|
||||
folderPathInput.value = '';
|
||||
}
|
||||
|
||||
|
||||
this.currentVersion = null;
|
||||
this.versions = [];
|
||||
this.modelInfo = null;
|
||||
this.modelId = null;
|
||||
this.modelVersionId = null;
|
||||
this.source = null;
|
||||
|
||||
|
||||
this.selectedFolder = '';
|
||||
|
||||
|
||||
// Clear folder tree selection
|
||||
if (this.folderTreeManager) {
|
||||
this.folderTreeManager.clearSelection();
|
||||
}
|
||||
|
||||
|
||||
// Reset default path toggle
|
||||
this.loadDefaultPathSetting();
|
||||
}
|
||||
@@ -151,10 +151,10 @@ export class DownloadManager {
|
||||
async validateAndFetchVersions() {
|
||||
const url = document.getElementById('modelUrl').value.trim();
|
||||
const errorElement = document.getElementById('urlError');
|
||||
|
||||
|
||||
try {
|
||||
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions'));
|
||||
|
||||
|
||||
this.modelId = this.extractModelId(url);
|
||||
if (!this.modelId) {
|
||||
throw new Error(translate('modals.download.errors.invalidUrl'));
|
||||
@@ -166,7 +166,7 @@ export class DownloadManager {
|
||||
if (this.modelVersionId) {
|
||||
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
|
||||
}
|
||||
|
||||
|
||||
this.showVersionStep();
|
||||
} catch (error) {
|
||||
errorElement.textContent = error.message;
|
||||
@@ -239,20 +239,20 @@ export class DownloadManager {
|
||||
showVersionStep() {
|
||||
document.getElementById('urlStep').style.display = 'none';
|
||||
document.getElementById('versionStep').style.display = 'block';
|
||||
|
||||
|
||||
const versionList = document.getElementById('versionList');
|
||||
versionList.innerHTML = this.versions.map(version => {
|
||||
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
|
||||
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
|
||||
|
||||
const fileSize = version.modelSizeKB ?
|
||||
(version.modelSizeKB / 1024).toFixed(2) :
|
||||
|
||||
const fileSize = version.modelSizeKB ?
|
||||
(version.modelSizeKB / 1024).toFixed(2) :
|
||||
(version.files[0]?.sizeKB / 1024).toFixed(2);
|
||||
|
||||
|
||||
const existsLocally = version.existsLocally;
|
||||
const localPath = version.localPath;
|
||||
const isEarlyAccess = version.availability === 'EarlyAccess';
|
||||
|
||||
|
||||
let earlyAccessBadge = '';
|
||||
if (isEarlyAccess) {
|
||||
earlyAccessBadge = `
|
||||
@@ -261,8 +261,8 @@ export class DownloadManager {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const localStatus = existsLocally ?
|
||||
|
||||
const localStatus = existsLocally ?
|
||||
`<div class="local-badge">
|
||||
<i class="fas fa-check"></i> ${translate('modals.download.inLibrary')}
|
||||
<div class="local-path">${localPath || ''}</div>
|
||||
@@ -293,7 +293,7 @@ export class DownloadManager {
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
|
||||
// Add click handlers for version selection
|
||||
versionList.addEventListener('click', (event) => {
|
||||
const versionItem = event.target.closest('.version-item');
|
||||
@@ -301,12 +301,12 @@ export class DownloadManager {
|
||||
this.selectVersion(versionItem.dataset.versionId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Auto-select the version if there's only one
|
||||
if (this.versions.length === 1 && !this.currentVersion) {
|
||||
this.selectVersion(this.versions[0].id.toString());
|
||||
}
|
||||
|
||||
|
||||
this.updateNextButtonState();
|
||||
}
|
||||
|
||||
@@ -317,16 +317,16 @@ export class DownloadManager {
|
||||
document.querySelectorAll('.version-item').forEach(item => {
|
||||
item.classList.toggle('selected', item.dataset.versionId === versionId);
|
||||
});
|
||||
|
||||
|
||||
this.updateNextButtonState();
|
||||
}
|
||||
|
||||
|
||||
updateNextButtonState() {
|
||||
const nextButton = document.getElementById('nextFromVersion');
|
||||
if (!nextButton) return;
|
||||
|
||||
|
||||
const existsLocally = this.currentVersion?.existsLocally;
|
||||
|
||||
|
||||
if (existsLocally) {
|
||||
nextButton.disabled = true;
|
||||
nextButton.classList.add('disabled');
|
||||
@@ -343,7 +343,7 @@ export class DownloadManager {
|
||||
showToast('toast.loras.pleaseSelectVersion', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const existsLocally = this.currentVersion.existsLocally;
|
||||
if (existsLocally) {
|
||||
showToast('toast.loras.versionExists', {}, 'info');
|
||||
@@ -352,12 +352,12 @@ export class DownloadManager {
|
||||
|
||||
document.getElementById('versionStep').style.display = 'none';
|
||||
document.getElementById('locationStep').style.display = 'block';
|
||||
|
||||
|
||||
try {
|
||||
// Fetch model roots
|
||||
const rootsData = await this.apiClient.fetchModelRoots();
|
||||
const modelRoot = document.getElementById('modelRoot');
|
||||
modelRoot.innerHTML = rootsData.roots.map(root =>
|
||||
modelRoot.innerHTML = rootsData.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
@@ -380,7 +380,7 @@ export class DownloadManager {
|
||||
|
||||
// Initialize folder tree
|
||||
await this.initializeFolderTree();
|
||||
|
||||
|
||||
// Setup folder tree manager
|
||||
this.folderTreeManager.init({
|
||||
onPathChange: (path) => {
|
||||
@@ -388,16 +388,16 @@ export class DownloadManager {
|
||||
this.updateTargetPath();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Setup model root change handler
|
||||
modelRoot.addEventListener('change', async () => {
|
||||
await this.initializeFolderTree();
|
||||
this.updateTargetPath();
|
||||
});
|
||||
|
||||
|
||||
// Load default path setting for current model type
|
||||
this.loadDefaultPathSetting();
|
||||
|
||||
|
||||
this.updateTargetPath();
|
||||
} catch (error) {
|
||||
showToast('toast.downloads.loadError', { message: error.message }, 'error');
|
||||
@@ -408,7 +408,7 @@ export class DownloadManager {
|
||||
const modelType = this.apiClient.modelType;
|
||||
const storageKey = `use_default_path_${modelType}`;
|
||||
this.useDefaultPath = getStorageItem(storageKey, false);
|
||||
|
||||
|
||||
const toggleInput = document.getElementById('useDefaultPath');
|
||||
if (toggleInput) {
|
||||
toggleInput.checked = this.useDefaultPath;
|
||||
@@ -418,12 +418,12 @@ export class DownloadManager {
|
||||
|
||||
toggleDefaultPath(event) {
|
||||
this.useDefaultPath = event.target.checked;
|
||||
|
||||
|
||||
// Save to localStorage per model type
|
||||
const modelType = this.apiClient.modelType;
|
||||
const storageKey = `use_default_path_${modelType}`;
|
||||
setStorageItem(storageKey, this.useDefaultPath);
|
||||
|
||||
|
||||
this.updatePathSelectionUI();
|
||||
this.updateTargetPath();
|
||||
}
|
||||
@@ -446,7 +446,7 @@ export class DownloadManager {
|
||||
|
||||
const displayName = versionName || `#${versionId}`;
|
||||
let ws = null;
|
||||
let updateProgress = () => {};
|
||||
let updateProgress = () => { };
|
||||
|
||||
try {
|
||||
this.loadingManager.restoreProgressBar();
|
||||
@@ -549,7 +549,7 @@ export class DownloadManager {
|
||||
|
||||
updatePathSelectionUI() {
|
||||
const manualSelection = document.getElementById('manualPathSelection');
|
||||
|
||||
|
||||
// Always show manual path selection, but disable/enable based on useDefaultPath
|
||||
manualSelection.style.display = 'block';
|
||||
if (this.useDefaultPath) {
|
||||
@@ -566,11 +566,11 @@ export class DownloadManager {
|
||||
el.tabIndex = 0;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Always update the main path display
|
||||
this.updateTargetPath();
|
||||
}
|
||||
|
||||
|
||||
backToUrl() {
|
||||
document.getElementById('versionStep').style.display = 'none';
|
||||
document.getElementById('urlStep').style.display = 'block';
|
||||
@@ -592,7 +592,7 @@ export class DownloadManager {
|
||||
async startDownload() {
|
||||
const modelRoot = document.getElementById('modelRoot').value;
|
||||
const config = this.apiClient.apiConfig.config;
|
||||
|
||||
|
||||
if (!modelRoot) {
|
||||
showToast('toast.models.pleaseSelectRoot', { type: config.displayName }, 'error');
|
||||
return;
|
||||
@@ -601,7 +601,7 @@ export class DownloadManager {
|
||||
// Determine target folder and use_default_paths parameter
|
||||
let targetFolder = '';
|
||||
let useDefaultPaths = false;
|
||||
|
||||
|
||||
if (this.useDefaultPath) {
|
||||
useDefaultPaths = true;
|
||||
targetFolder = ''; // Not needed when using default paths
|
||||
@@ -646,7 +646,7 @@ export class DownloadManager {
|
||||
try {
|
||||
// Fetch unified folder tree
|
||||
const treeData = await this.apiClient.fetchUnifiedFolderTree();
|
||||
|
||||
|
||||
if (treeData.success) {
|
||||
// Load tree data into folder tree manager
|
||||
await this.folderTreeManager.loadTree(treeData.tree);
|
||||
@@ -674,23 +674,23 @@ export class DownloadManager {
|
||||
folderItem.classList.remove('selected');
|
||||
this.selectedFolder = '';
|
||||
} else {
|
||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||
f.classList.remove('selected'));
|
||||
folderItem.classList.add('selected');
|
||||
this.selectedFolder = folderItem.dataset.folder;
|
||||
}
|
||||
|
||||
|
||||
this.updateTargetPath();
|
||||
};
|
||||
|
||||
folderBrowser.addEventListener('click', this.folderClickHandler);
|
||||
|
||||
|
||||
const modelRoot = document.getElementById('modelRoot');
|
||||
const newFolder = document.getElementById('newFolder');
|
||||
|
||||
|
||||
modelRoot.addEventListener('change', this.updateTargetPath);
|
||||
newFolder.addEventListener('input', this.updateTargetPath);
|
||||
|
||||
|
||||
this.updateTargetPath();
|
||||
}
|
||||
|
||||
@@ -702,21 +702,21 @@ export class DownloadManager {
|
||||
this.folderClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const modelRoot = document.getElementById('modelRoot');
|
||||
const newFolder = document.getElementById('newFolder');
|
||||
|
||||
|
||||
if (modelRoot) modelRoot.removeEventListener('change', this.updateTargetPath);
|
||||
if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
|
||||
}
|
||||
|
||||
|
||||
updateTargetPath() {
|
||||
const pathDisplay = document.getElementById('targetPathDisplay');
|
||||
const modelRoot = document.getElementById('modelRoot').value;
|
||||
const config = this.apiClient.apiConfig.config;
|
||||
|
||||
|
||||
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: config.displayName });
|
||||
|
||||
|
||||
if (modelRoot) {
|
||||
if (this.useDefaultPath) {
|
||||
// Show actual template path
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ImportManager {
|
||||
this.importMode = 'url'; // Default mode: 'url' or 'upload'
|
||||
this.useDefaultPath = false;
|
||||
this.apiClient = null;
|
||||
|
||||
|
||||
// Initialize sub-managers
|
||||
this.loadingManager = new LoadingManager();
|
||||
this.stepManager = new ImportStepManager();
|
||||
@@ -36,7 +36,7 @@ export class ImportManager {
|
||||
this.recipeDataManager = new RecipeDataManager(this);
|
||||
this.downloadManager = new DownloadManager(this);
|
||||
this.folderTreeManager = new FolderTreeManager();
|
||||
|
||||
|
||||
// Bind methods
|
||||
this.formatFileSize = formatFileSize;
|
||||
this.updateTargetPath = this.updateTargetPath.bind(this);
|
||||
@@ -53,17 +53,17 @@ export class ImportManager {
|
||||
this.initializeEventHandlers();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
|
||||
// Get API client for LoRAs
|
||||
this.apiClient = getModelApiClient(MODEL_TYPES.LORA);
|
||||
|
||||
|
||||
// Reset state
|
||||
this.resetSteps();
|
||||
if (recipeData) {
|
||||
this.downloadableLoRAs = recipeData.loras;
|
||||
this.recipeId = recipeId;
|
||||
}
|
||||
|
||||
|
||||
// Show modal
|
||||
modalManager.showModal('importModal', null, () => {
|
||||
this.cleanupFolderBrowser();
|
||||
@@ -71,7 +71,7 @@ export class ImportManager {
|
||||
});
|
||||
|
||||
// Verify visibility and focus on URL input
|
||||
setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
// Ensure URL option is selected and focus on the input
|
||||
this.toggleImportMode('url');
|
||||
const urlInput = document.getElementById('imageUrlInput');
|
||||
@@ -93,32 +93,32 @@ export class ImportManager {
|
||||
// Clear UI state
|
||||
this.stepManager.removeInjectedStyles();
|
||||
this.stepManager.showStep('uploadStep');
|
||||
|
||||
|
||||
// Reset form inputs
|
||||
const fileInput = document.getElementById('recipeImageUpload');
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
|
||||
const urlInput = document.getElementById('imageUrlInput');
|
||||
if (urlInput) urlInput.value = '';
|
||||
|
||||
|
||||
const uploadError = document.getElementById('uploadError');
|
||||
if (uploadError) uploadError.textContent = '';
|
||||
|
||||
|
||||
const importUrlError = document.getElementById('importUrlError');
|
||||
if (importUrlError) importUrlError.textContent = '';
|
||||
|
||||
|
||||
const recipeName = document.getElementById('recipeName');
|
||||
if (recipeName) recipeName.value = '';
|
||||
|
||||
|
||||
const tagsContainer = document.getElementById('tagsContainer');
|
||||
if (tagsContainer) tagsContainer.innerHTML = `<div class="empty-tags">${translate('recipes.controls.import.noTagsAdded', {}, 'No tags added')}</div>`;
|
||||
|
||||
|
||||
// Clear folder path input
|
||||
const folderPathInput = document.getElementById('importFolderPath');
|
||||
if (folderPathInput) {
|
||||
folderPathInput.value = '';
|
||||
}
|
||||
|
||||
|
||||
// Reset state variables
|
||||
this.recipeImage = null;
|
||||
this.recipeData = null;
|
||||
@@ -127,30 +127,30 @@ export class ImportManager {
|
||||
this.missingLoras = [];
|
||||
this.downloadableLoRAs = [];
|
||||
this.selectedFolder = '';
|
||||
|
||||
|
||||
// Reset import mode
|
||||
this.importMode = 'url';
|
||||
this.toggleImportMode('url');
|
||||
|
||||
|
||||
// Clear folder tree selection
|
||||
if (this.folderTreeManager) {
|
||||
this.folderTreeManager.clearSelection();
|
||||
}
|
||||
|
||||
|
||||
// Reset default path toggle
|
||||
this.loadDefaultPathSetting();
|
||||
|
||||
|
||||
// Reset duplicate related properties
|
||||
this.duplicateRecipes = [];
|
||||
}
|
||||
|
||||
toggleImportMode(mode) {
|
||||
this.importMode = mode;
|
||||
|
||||
|
||||
// Update toggle buttons
|
||||
const uploadBtn = document.querySelector('.toggle-btn[data-mode="upload"]');
|
||||
const urlBtn = document.querySelector('.toggle-btn[data-mode="url"]');
|
||||
|
||||
|
||||
if (uploadBtn && urlBtn) {
|
||||
if (mode === 'upload') {
|
||||
uploadBtn.classList.add('active');
|
||||
@@ -160,11 +160,11 @@ export class ImportManager {
|
||||
urlBtn.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show/hide appropriate sections
|
||||
const uploadSection = document.getElementById('uploadSection');
|
||||
const urlSection = document.getElementById('urlSection');
|
||||
|
||||
|
||||
if (uploadSection && urlSection) {
|
||||
if (mode === 'upload') {
|
||||
uploadSection.style.display = 'block';
|
||||
@@ -174,11 +174,11 @@ export class ImportManager {
|
||||
urlSection.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clear error messages
|
||||
const uploadError = document.getElementById('uploadError');
|
||||
const importUrlError = document.getElementById('importUrlError');
|
||||
|
||||
|
||||
if (uploadError) uploadError.textContent = '';
|
||||
if (importUrlError) importUrlError.textContent = '';
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export class ImportManager {
|
||||
addTag() {
|
||||
this.recipeDataManager.addTag();
|
||||
}
|
||||
|
||||
|
||||
removeTag(tag) {
|
||||
this.recipeDataManager.removeTag(tag);
|
||||
}
|
||||
@@ -217,12 +217,12 @@ export class ImportManager {
|
||||
|
||||
async proceedToLocation() {
|
||||
this.stepManager.showStep('locationStep');
|
||||
|
||||
|
||||
try {
|
||||
// Fetch LoRA roots
|
||||
const rootsData = await this.apiClient.fetchModelRoots();
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
loraRoot.innerHTML = rootsData.roots.map(root =>
|
||||
loraRoot.innerHTML = rootsData.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
@@ -247,19 +247,19 @@ export class ImportManager {
|
||||
this.updateTargetPath();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Initialize folder tree
|
||||
await this.initializeFolderTree();
|
||||
|
||||
|
||||
// Setup lora root change handler
|
||||
loraRoot.addEventListener('change', async () => {
|
||||
await this.initializeFolderTree();
|
||||
this.updateTargetPath();
|
||||
});
|
||||
|
||||
|
||||
// Load default path setting for LoRAs
|
||||
this.loadDefaultPathSetting();
|
||||
|
||||
|
||||
this.updateTargetPath();
|
||||
} catch (error) {
|
||||
showToast('toast.recipes.importFailed', { message: error.message }, 'error');
|
||||
@@ -268,19 +268,19 @@ export class ImportManager {
|
||||
|
||||
backToUpload() {
|
||||
this.stepManager.showStep('uploadStep');
|
||||
|
||||
|
||||
// Reset file input
|
||||
const fileInput = document.getElementById('recipeImageUpload');
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
|
||||
// Reset URL input
|
||||
const urlInput = document.getElementById('imageUrlInput');
|
||||
if (urlInput) urlInput.value = '';
|
||||
|
||||
|
||||
// Clear error messages
|
||||
const uploadError = document.getElementById('uploadError');
|
||||
if (uploadError) uploadError.textContent = '';
|
||||
|
||||
|
||||
const importUrlError = document.getElementById('importUrlError');
|
||||
if (importUrlError) importUrlError.textContent = '';
|
||||
}
|
||||
@@ -296,7 +296,7 @@ export class ImportManager {
|
||||
loadDefaultPathSetting() {
|
||||
const storageKey = 'use_default_path_loras';
|
||||
this.useDefaultPath = getStorageItem(storageKey, false);
|
||||
|
||||
|
||||
const toggleInput = document.getElementById('importUseDefaultPath');
|
||||
if (toggleInput) {
|
||||
toggleInput.checked = this.useDefaultPath;
|
||||
@@ -306,18 +306,18 @@ export class ImportManager {
|
||||
|
||||
toggleDefaultPath(event) {
|
||||
this.useDefaultPath = event.target.checked;
|
||||
|
||||
|
||||
// Save to localStorage for LoRAs
|
||||
const storageKey = 'use_default_path_loras';
|
||||
setStorageItem(storageKey, this.useDefaultPath);
|
||||
|
||||
|
||||
this.updatePathSelectionUI();
|
||||
this.updateTargetPath();
|
||||
}
|
||||
|
||||
updatePathSelectionUI() {
|
||||
const manualSelection = document.getElementById('importManualPathSelection');
|
||||
|
||||
|
||||
// Always show manual path selection, but disable/enable based on useDefaultPath
|
||||
if (manualSelection) {
|
||||
manualSelection.style.display = 'block';
|
||||
@@ -336,7 +336,7 @@ export class ImportManager {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Always update the main path display
|
||||
this.updateTargetPath();
|
||||
}
|
||||
@@ -345,7 +345,7 @@ export class ImportManager {
|
||||
try {
|
||||
// Fetch unified folder tree
|
||||
const treeData = await this.apiClient.fetchUnifiedFolderTree();
|
||||
|
||||
|
||||
if (treeData.success) {
|
||||
// Load tree data into folder tree manager
|
||||
await this.folderTreeManager.loadTree(treeData.tree);
|
||||
@@ -368,8 +368,8 @@ export class ImportManager {
|
||||
updateTargetPath() {
|
||||
const pathDisplay = document.getElementById('importTargetPathDisplay');
|
||||
const loraRoot = document.getElementById('importLoraRoot').value;
|
||||
|
||||
let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory'); if (loraRoot) {
|
||||
|
||||
let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory'); if (loraRoot) {
|
||||
if (this.useDefaultPath) {
|
||||
// Show actual template path
|
||||
try {
|
||||
@@ -417,19 +417,19 @@ export class ImportManager {
|
||||
// Store the recipe data and ID
|
||||
this.recipeData = recipeData;
|
||||
this.recipeId = recipeId;
|
||||
|
||||
|
||||
// Show the modal and go to location step
|
||||
this.showImportModal(recipeData, recipeId);
|
||||
this.proceedToLocation();
|
||||
|
||||
|
||||
// Update the modal title
|
||||
const modalTitle = document.querySelector('#importModal h2');
|
||||
if (modalTitle) modalTitle.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
|
||||
|
||||
|
||||
// Update the save button text
|
||||
const saveButton = document.querySelector('#locationStep .primary-btn');
|
||||
if (saveButton) saveButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
|
||||
|
||||
|
||||
// Hide the back button
|
||||
const backButton = document.querySelector('#locationStep .secondary-btn');
|
||||
if (backButton) backButton.style.display = 'none';
|
||||
|
||||
@@ -6,6 +6,8 @@ import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||
import { RecipeSidebarApiClient } from '../api/recipeApi.js';
|
||||
import { FolderTreeManager } from '../components/FolderTreeManager.js';
|
||||
import { sidebarManager } from '../components/SidebarManager.js';
|
||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
|
||||
class MoveManager {
|
||||
constructor() {
|
||||
@@ -14,9 +16,11 @@ class MoveManager {
|
||||
this.folderTreeManager = new FolderTreeManager();
|
||||
this.initialized = false;
|
||||
this.recipeApiClient = null;
|
||||
|
||||
this.useDefaultPath = false;
|
||||
|
||||
// Bind methods
|
||||
this.updateTargetPath = this.updateTargetPath.bind(this);
|
||||
this.handleToggleDefaultPath = this.handleToggleDefaultPath.bind(this);
|
||||
}
|
||||
|
||||
_getApiClient(modelType = null) {
|
||||
@@ -31,15 +35,21 @@ class MoveManager {
|
||||
|
||||
initializeEventListeners() {
|
||||
if (this.initialized) return;
|
||||
|
||||
|
||||
const modelRootSelect = document.getElementById('moveModelRoot');
|
||||
|
||||
|
||||
// Initialize model root directory selector
|
||||
modelRootSelect.addEventListener('change', async () => {
|
||||
await this.initializeFolderTree();
|
||||
this.updateTargetPath();
|
||||
});
|
||||
|
||||
|
||||
// Default path toggle handler
|
||||
const toggleInput = document.getElementById('moveUseDefaultPath');
|
||||
if (toggleInput) {
|
||||
toggleInput.addEventListener('change', this.handleToggleDefaultPath);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
@@ -47,11 +57,11 @@ class MoveManager {
|
||||
// Reset state
|
||||
this.currentFilePath = null;
|
||||
this.bulkFilePaths = null;
|
||||
|
||||
|
||||
const apiClient = this._getApiClient(modelType);
|
||||
const currentPageType = state.currentPageType;
|
||||
const modelConfig = apiClient.apiConfig.config;
|
||||
|
||||
|
||||
// Handle bulk mode
|
||||
if (filePath === 'bulk') {
|
||||
const selectedPaths = Array.from(state.selectedModels);
|
||||
@@ -66,11 +76,11 @@ class MoveManager {
|
||||
this.currentFilePath = filePath;
|
||||
document.getElementById('moveModalTitle').textContent = `Move ${modelConfig.displayName}`;
|
||||
}
|
||||
|
||||
|
||||
// Update UI labels based on model type
|
||||
document.getElementById('moveRootLabel').textContent = `Select ${modelConfig.displayName} Root:`;
|
||||
document.getElementById('moveTargetPathDisplay').querySelector('.path-text').textContent = `Select a ${modelConfig.displayName.toLowerCase()} root directory`;
|
||||
|
||||
|
||||
// Clear folder path input
|
||||
const folderPathInput = document.getElementById('moveFolderPath');
|
||||
if (folderPathInput) {
|
||||
@@ -86,13 +96,13 @@ class MoveManager {
|
||||
} else {
|
||||
rootsData = await apiClient.fetchModelRoots();
|
||||
}
|
||||
|
||||
|
||||
if (!rootsData.roots || rootsData.roots.length === 0) {
|
||||
throw new Error(`No ${modelConfig.displayName.toLowerCase()} roots found`);
|
||||
}
|
||||
|
||||
// Populate model root selector
|
||||
modelRootSelect.innerHTML = rootsData.roots.map(root =>
|
||||
modelRootSelect.innerHTML = rootsData.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
@@ -105,7 +115,7 @@ class MoveManager {
|
||||
|
||||
// Initialize event listeners
|
||||
this.initializeEventListeners();
|
||||
|
||||
|
||||
// Setup folder tree manager
|
||||
this.folderTreeManager.init({
|
||||
onPathChange: (path) => {
|
||||
@@ -113,10 +123,13 @@ class MoveManager {
|
||||
},
|
||||
elementsPrefix: 'move'
|
||||
});
|
||||
|
||||
|
||||
// Initialize folder tree
|
||||
await this.initializeFolderTree();
|
||||
|
||||
// Load default path setting
|
||||
this.loadDefaultPathSetting(apiClient.modelType);
|
||||
|
||||
this.updateTargetPath();
|
||||
modalManager.showModal('moveModal', null, () => {
|
||||
// Cleanup on modal close
|
||||
@@ -124,19 +137,63 @@ class MoveManager {
|
||||
this.folderTreeManager.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${modelConfig.displayName.toLowerCase()} roots or folders:`, error);
|
||||
showToast('toast.models.moveFailed', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadDefaultPathSetting(modelType) {
|
||||
const storageKey = `use_default_path_${modelType}`;
|
||||
this.useDefaultPath = getStorageItem(storageKey, false);
|
||||
|
||||
const toggleInput = document.getElementById('moveUseDefaultPath');
|
||||
if (toggleInput) {
|
||||
toggleInput.checked = this.useDefaultPath;
|
||||
this.updatePathSelectionUI();
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleDefaultPath(event) {
|
||||
this.useDefaultPath = event.target.checked;
|
||||
|
||||
// Save to localStorage per model type
|
||||
const apiClient = this._getApiClient();
|
||||
const modelType = apiClient.modelType;
|
||||
const storageKey = `use_default_path_${modelType}`;
|
||||
setStorageItem(storageKey, this.useDefaultPath);
|
||||
|
||||
this.updatePathSelectionUI();
|
||||
this.updateTargetPath();
|
||||
}
|
||||
|
||||
updatePathSelectionUI() {
|
||||
const manualSelection = document.getElementById('moveManualPathSelection');
|
||||
if (!manualSelection) return;
|
||||
|
||||
if (this.useDefaultPath) {
|
||||
manualSelection.classList.add('disabled');
|
||||
// Disable all inputs and buttons inside manualSelection
|
||||
manualSelection.querySelectorAll('input, select, button').forEach(el => {
|
||||
el.disabled = true;
|
||||
el.tabIndex = -1;
|
||||
});
|
||||
} else {
|
||||
manualSelection.classList.remove('disabled');
|
||||
manualSelection.querySelectorAll('input, select, button').forEach(el => {
|
||||
el.disabled = false;
|
||||
el.tabIndex = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async initializeFolderTree() {
|
||||
try {
|
||||
const apiClient = this._getApiClient();
|
||||
// Fetch unified folder tree
|
||||
const treeData = await apiClient.fetchUnifiedFolderTree();
|
||||
|
||||
|
||||
if (treeData.success) {
|
||||
// Load tree data into folder tree manager
|
||||
await this.folderTreeManager.loadTree(treeData.tree);
|
||||
@@ -155,13 +212,27 @@ class MoveManager {
|
||||
const modelRoot = document.getElementById('moveModelRoot').value;
|
||||
const apiClient = this._getApiClient();
|
||||
const config = apiClient.apiConfig.config;
|
||||
|
||||
let fullPath = modelRoot || `Select a ${config.displayName.toLowerCase()} root directory`;
|
||||
|
||||
|
||||
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: config.displayName });
|
||||
|
||||
if (modelRoot) {
|
||||
const selectedPath = this.folderTreeManager ? this.folderTreeManager.getSelectedPath() : '';
|
||||
if (selectedPath) {
|
||||
fullPath += '/' + selectedPath;
|
||||
if (this.useDefaultPath) {
|
||||
// Show actual template path
|
||||
try {
|
||||
const singularType = apiClient.modelType.replace(/s$/, '');
|
||||
const templates = state.global.settings.download_path_templates;
|
||||
const template = templates[singularType];
|
||||
fullPath += `/${template}`;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch template:', error);
|
||||
fullPath += '/' + translate('modals.download.autoOrganizedPath');
|
||||
}
|
||||
} else {
|
||||
// Show manual path selection
|
||||
const selectedPath = this.folderTreeManager ? this.folderTreeManager.getSelectedPath() : '';
|
||||
if (selectedPath) {
|
||||
fullPath += '/' + selectedPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +243,7 @@ class MoveManager {
|
||||
const selectedRoot = document.getElementById('moveModelRoot').value;
|
||||
const apiClient = this._getApiClient();
|
||||
const config = apiClient.apiConfig.config;
|
||||
|
||||
|
||||
if (!selectedRoot) {
|
||||
showToast('toast.models.pleaseSelectRoot', { type: config.displayName.toLowerCase() }, 'error');
|
||||
return;
|
||||
@@ -180,7 +251,7 @@ class MoveManager {
|
||||
|
||||
// Get selected folder path from folder tree manager
|
||||
const targetFolder = this.folderTreeManager.getSelectedPath();
|
||||
|
||||
|
||||
let targetPath = selectedRoot;
|
||||
if (targetFolder) {
|
||||
targetPath = `${targetPath}/${targetFolder}`;
|
||||
@@ -189,7 +260,7 @@ class MoveManager {
|
||||
try {
|
||||
if (this.bulkFilePaths) {
|
||||
// Bulk move mode
|
||||
const results = await apiClient.moveBulkModels(this.bulkFilePaths, targetPath);
|
||||
const results = await apiClient.moveBulkModels(this.bulkFilePaths, targetPath, this.useDefaultPath);
|
||||
|
||||
// Update virtual scroller if in active folder view
|
||||
const pageState = getCurrentPageState();
|
||||
@@ -206,7 +277,7 @@ class MoveManager {
|
||||
if (result.success && result.new_file_path !== result.original_file_path) {
|
||||
const newFileName = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1);
|
||||
const baseFileName = newFileName.substring(0, newFileName.lastIndexOf('.'));
|
||||
|
||||
|
||||
state.virtualScroller.updateSingleItem(result.original_file_path, {
|
||||
file_path: result.new_file_path,
|
||||
file_name: baseFileName
|
||||
@@ -216,7 +287,7 @@ class MoveManager {
|
||||
}
|
||||
} else {
|
||||
// Single move mode
|
||||
const result = await apiClient.moveSingleModel(this.currentFilePath, targetPath);
|
||||
const result = await apiClient.moveSingleModel(this.currentFilePath, targetPath, this.useDefaultPath);
|
||||
|
||||
const pageState = getCurrentPageState();
|
||||
if (result && result.new_file_path) {
|
||||
@@ -226,7 +297,7 @@ class MoveManager {
|
||||
// Update both file_path and file_name if they changed
|
||||
const newFileName = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1);
|
||||
const baseFileName = newFileName.substring(0, newFileName.lastIndexOf('.'));
|
||||
|
||||
|
||||
state.virtualScroller.updateSingleItem(this.currentFilePath, {
|
||||
file_path: result.new_file_path,
|
||||
file_name: baseFileName
|
||||
@@ -239,7 +310,7 @@ class MoveManager {
|
||||
sidebarManager.refresh();
|
||||
|
||||
modalManager.closeModal('moveModal');
|
||||
|
||||
|
||||
// If we were in bulk mode, exit it after successful move
|
||||
if (this.bulkFilePaths && state.bulkMode) {
|
||||
bulkManager.toggleBulkMode();
|
||||
|
||||
Reference in New Issue
Block a user