feat: Implement model move, import, and download functionalities with corresponding UI and API updates.

This commit is contained in:
Will Miao
2025-12-28 21:18:27 +08:00
parent e5b557504e
commit 5d5a2a998a
9 changed files with 420 additions and 308 deletions

View File

@@ -120,7 +120,7 @@ class BaseModelRoutes(ABC):
self.service = service self.service = service
self.model_type = service.model_type self.model_type = service.model_type
self.model_file_service = ModelFileService(service.scanner, service.model_type) self.model_file_service = ModelFileService(service.scanner, service.model_type)
self.model_move_service = ModelMoveService(service.scanner) self.model_move_service = ModelMoveService(service.scanner, service.model_type)
self.model_lifecycle_service = ModelLifecycleService( self.model_lifecycle_service = ModelLifecycleService(
scanner=service.scanner, scanner=service.scanner,
metadata_manager=MetadataManager, metadata_manager=MetadataManager,
@@ -270,7 +270,7 @@ class BaseModelRoutes(ABC):
def _ensure_move_service(self) -> ModelMoveService: def _ensure_move_service(self) -> ModelMoveService:
if self.model_move_service is None: if self.model_move_service is None:
service = self._ensure_service() service = self._ensure_service()
self.model_move_service = ModelMoveService(service.scanner) self.model_move_service = ModelMoveService(service.scanner, service.model_type)
return self.model_move_service return self.model_move_service
def _ensure_lifecycle_service(self) -> ModelLifecycleService: def _ensure_lifecycle_service(self) -> ModelLifecycleService:

View File

@@ -1052,9 +1052,10 @@ class ModelMoveHandler:
data = await request.json() data = await request.json()
file_path = data.get("file_path") file_path = data.get("file_path")
target_path = data.get("target_path") target_path = data.get("target_path")
use_default_paths = data.get("use_default_paths", False)
if not file_path or not target_path: if not file_path or not target_path:
return web.Response(text="File path and target path are required", status=400) return web.Response(text="File path and target path are required", status=400)
result = await self._move_service.move_model(file_path, target_path) result = await self._move_service.move_model(file_path, target_path, use_default_paths=use_default_paths)
status = 200 if result.get("success") else 500 status = 200 if result.get("success") else 500
return web.json_response(result, status=status) return web.json_response(result, status=status)
except Exception as exc: except Exception as exc:
@@ -1066,9 +1067,10 @@ class ModelMoveHandler:
data = await request.json() data = await request.json()
file_paths = data.get("file_paths", []) file_paths = data.get("file_paths", [])
target_path = data.get("target_path") target_path = data.get("target_path")
use_default_paths = data.get("use_default_paths", False)
if not file_paths or not target_path: if not file_paths or not target_path:
return web.Response(text="File paths and target path are required", status=400) return web.Response(text="File paths and target path are required", status=400)
result = await self._move_service.move_models_bulk(file_paths, target_path) result = await self._move_service.move_models_bulk(file_paths, target_path, use_default_paths=use_default_paths)
return web.json_response(result) return web.json_response(result)
except Exception as exc: except Exception as exc:
self._logger.error("Error moving models in bulk: %s", exc, exc_info=True) self._logger.error("Error moving models in bulk: %s", exc, exc_info=True)

View File

@@ -446,25 +446,46 @@ class ModelFileService:
class ModelMoveService: class ModelMoveService:
"""Service for handling individual model moves""" """Service for handling individual model moves"""
def __init__(self, scanner): def __init__(self, scanner, model_type: str):
"""Initialize the service """Initialize the service
Args: Args:
scanner: Model scanner instance scanner: Model scanner instance
model_type: Type of model (e.g., 'lora', 'checkpoint')
""" """
self.scanner = scanner self.scanner = scanner
self.model_type = model_type
async def move_model(self, file_path: str, target_path: str) -> Dict[str, Any]: async def move_model(self, file_path: str, target_path: str, use_default_paths: bool = False) -> Dict[str, Any]:
"""Move a single model file """Move a single model file
Args: Args:
file_path: Source file path file_path: Source file path
target_path: Target directory path target_path: Target directory path (used as root if use_default_paths is True)
use_default_paths: Whether to use default path template for organization
Returns: Returns:
Dictionary with move result Dictionary with move result
""" """
try: try:
if use_default_paths:
# Find the model in cache to get metadata
cache = await self.scanner.get_cached_data()
model_data = next((m for m in cache.raw_data if m.get('file_path') == file_path), None)
if model_data:
from ..utils.utils import calculate_relative_path_for_model
relative_path = calculate_relative_path_for_model(model_data, self.model_type)
if relative_path:
target_path = os.path.join(target_path, relative_path).replace(os.sep, '/')
elif not get_settings_manager().get_download_path_template(self.model_type):
# Flat structure, target_path remains the root
pass
else:
# Could not calculate relative path (e.g. missing metadata)
# Fallback to manual target_path or skip?
pass
source_dir = os.path.dirname(file_path) source_dir = os.path.dirname(file_path)
if os.path.normpath(source_dir) == os.path.normpath(target_path): if os.path.normpath(source_dir) == os.path.normpath(target_path):
logger.info(f"Source and target directories are the same: {source_dir}") logger.info(f"Source and target directories are the same: {source_dir}")
@@ -498,12 +519,13 @@ class ModelMoveService:
'new_file_path': None 'new_file_path': None
} }
async def move_models_bulk(self, file_paths: List[str], target_path: str) -> Dict[str, Any]: async def move_models_bulk(self, file_paths: List[str], target_path: str, use_default_paths: bool = False) -> Dict[str, Any]:
"""Move multiple model files """Move multiple model files
Args: Args:
file_paths: List of source file paths file_paths: List of source file paths
target_path: Target directory path target_path: Target directory path (used as root if use_default_paths is True)
use_default_paths: Whether to use default path template for organization
Returns: Returns:
Dictionary with bulk move results Dictionary with bulk move results
@@ -512,7 +534,7 @@ class ModelMoveService:
results = [] results = []
for file_path in file_paths: for file_path in file_paths:
result = await self.move_model(file_path, target_path) result = await self.move_model(file_path, target_path, use_default_paths=use_default_paths)
results.append({ results.append({
"original_file_path": file_path, "original_file_path": file_path,
"new_file_path": result.get('new_file_path'), "new_file_path": result.get('new_file_path'),

View File

@@ -328,11 +328,11 @@
display: block; display: block;
} }
.tree-node.has-children > .tree-node-content .tree-expand-icon { .tree-node.has-children>.tree-node-content .tree-expand-icon {
opacity: 1; 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; opacity: 0;
pointer-events: none; pointer-events: none;
} }
@@ -470,11 +470,11 @@
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 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); 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); transform: translateX(18px);
} }

View File

@@ -2,9 +2,9 @@ import { state, getCurrentPageState } from '../state/index.js';
import { showToast } from '../utils/uiHelpers.js'; import { showToast } from '../utils/uiHelpers.js';
import { translate } from '../utils/i18nHelpers.js'; import { translate } from '../utils/i18nHelpers.js';
import { getStorageItem, getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js'; import { getStorageItem, getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js';
import { import {
getCompleteApiConfig, getCompleteApiConfig,
getCurrentModelType, getCurrentModelType,
isValidModelType, isValidModelType,
DOWNLOAD_ENDPOINTS, DOWNLOAD_ENDPOINTS,
WS_ENDPOINTS WS_ENDPOINTS
@@ -51,7 +51,7 @@ export class BaseModelApiClient {
async fetchModelsPage(page = 1, pageSize = null) { async fetchModelsPage(page = 1, pageSize = null) {
const pageState = this.getPageState(); const pageState = this.getPageState();
const actualPageSize = pageSize || pageState.pageSize || this.apiConfig.config.defaultPageSize; const actualPageSize = pageSize || pageState.pageSize || this.apiConfig.config.defaultPageSize;
try { try {
const params = this._buildQueryParams({ const params = this._buildQueryParams({
page, page,
@@ -63,9 +63,9 @@ export class BaseModelApiClient {
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch ${this.apiConfig.config.displayName}s: ${response.statusText}`); throw new Error(`Failed to fetch ${this.apiConfig.config.displayName}s: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
return { return {
items: data.items, items: data.items,
totalItems: data.total, totalItems: data.total,
@@ -74,7 +74,7 @@ export class BaseModelApiClient {
hasMore: page < data.total_pages, hasMore: page < data.total_pages,
folders: data.folders folders: data.folders
}; };
} catch (error) { } catch (error) {
console.error(`Error fetching ${this.apiConfig.config.displayName}s:`, error); console.error(`Error fetching ${this.apiConfig.config.displayName}s:`, error);
showToast('toast.api.fetchFailed', { type: this.apiConfig.config.displayName, message: error.message }, '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) { async loadMoreWithVirtualScroll(resetPage = false, updateFolders = false) {
const pageState = this.getPageState(); const pageState = this.getPageState();
try { try {
state.loadingManager.showSimpleLoading(`Loading more ${this.apiConfig.config.displayName}s...`); state.loadingManager.showSimpleLoading(`Loading more ${this.apiConfig.config.displayName}s...`);
@@ -92,22 +92,22 @@ export class BaseModelApiClient {
if (resetPage) { if (resetPage) {
pageState.currentPage = 1; // Reset to first page pageState.currentPage = 1; // Reset to first page
} }
const result = await this.fetchModelsPage(pageState.currentPage, pageState.pageSize); const result = await this.fetchModelsPage(pageState.currentPage, pageState.pageSize);
state.virtualScroller.refreshWithData( state.virtualScroller.refreshWithData(
result.items, result.items,
result.totalItems, result.totalItems,
result.hasMore result.hasMore
); );
pageState.hasMore = result.hasMore; pageState.hasMore = result.hasMore;
pageState.currentPage = pageState.currentPage + 1; pageState.currentPage = pageState.currentPage + 1;
if (updateFolders) { if (updateFolders) {
sidebarManager.refresh(); sidebarManager.refresh();
} }
return result; return result;
} catch (error) { } catch (error) {
console.error(`Error reloading ${this.apiConfig.config.displayName}s:`, error); console.error(`Error reloading ${this.apiConfig.config.displayName}s:`, error);
@@ -128,13 +128,13 @@ export class BaseModelApiClient {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_path: filePath }) body: JSON.stringify({ file_path: filePath })
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to delete ${this.apiConfig.config.singularName}: ${response.statusText}`); throw new Error(`Failed to delete ${this.apiConfig.config.singularName}: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
if (state.virtualScroller) { if (state.virtualScroller) {
state.virtualScroller.removeItemByFilePath(filePath); state.virtualScroller.removeItemByFilePath(filePath);
@@ -162,13 +162,13 @@ export class BaseModelApiClient {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_path: filePath }) body: JSON.stringify({ file_path: filePath })
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to exclude ${this.apiConfig.config.singularName}: ${response.statusText}`); throw new Error(`Failed to exclude ${this.apiConfig.config.singularName}: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
if (state.virtualScroller) { if (state.virtualScroller) {
state.virtualScroller.removeItemByFilePath(filePath); state.virtualScroller.removeItemByFilePath(filePath);
@@ -190,7 +190,7 @@ export class BaseModelApiClient {
async renameModelFile(filePath, newFileName) { async renameModelFile(filePath, newFileName) {
try { try {
state.loadingManager.showSimpleLoading(`Renaming ${this.apiConfig.config.singularName} file...`); state.loadingManager.showSimpleLoading(`Renaming ${this.apiConfig.config.singularName} file...`);
const response = await fetch(this.apiConfig.endpoints.rename, { const response = await fetch(this.apiConfig.endpoints.rename, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -203,12 +203,12 @@ export class BaseModelApiClient {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
state.virtualScroller.updateSingleItem(filePath, { state.virtualScroller.updateSingleItem(filePath, {
file_name: newFileName, file_name: newFileName,
file_path: result.new_file_path, file_path: result.new_file_path,
preview_url: result.new_preview_path preview_url: result.new_preview_path
}); });
showToast('toast.api.fileNameUpdated', {}, 'success'); showToast('toast.api.fileNameUpdated', {}, 'success');
} else { } else {
showToast('toast.api.fileRenameFailed', { error: result.error || 'Unknown error' }, 'error'); showToast('toast.api.fileRenameFailed', { error: result.error || 'Unknown error' }, 'error');
@@ -227,21 +227,21 @@ export class BaseModelApiClient {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.accept = 'image/*,video/mp4'; input.accept = 'image/*,video/mp4';
input.onchange = async () => { input.onchange = async () => {
if (!input.files || !input.files[0]) return; if (!input.files || !input.files[0]) return;
const file = input.files[0]; const file = input.files[0];
await this.uploadPreview(filePath, file); await this.uploadPreview(filePath, file);
}; };
input.click(); input.click();
} }
async uploadPreview(filePath, file, nsfwLevel = 0) { async uploadPreview(filePath, file, nsfwLevel = 0) {
try { try {
state.loadingManager.showSimpleLoading('Uploading preview...'); state.loadingManager.showSimpleLoading('Uploading preview...');
const formData = new FormData(); const formData = new FormData();
formData.append('preview_file', file); formData.append('preview_file', file);
formData.append('model_path', filePath); formData.append('model_path', filePath);
@@ -251,18 +251,18 @@ export class BaseModelApiClient {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Upload failed'); throw new Error('Upload failed');
} }
const data = await response.json(); const data = await response.json();
const pageState = this.getPageState(); const pageState = this.getPageState();
const timestamp = Date.now(); const timestamp = Date.now();
if (pageState.previewVersions) { if (pageState.previewVersions) {
pageState.previewVersions.set(filePath, timestamp); pageState.previewVersions.set(filePath, timestamp);
const storageKey = `${this.modelType}_preview_versions`; const storageKey = `${this.modelType}_preview_versions`;
saveMapToStorage(storageKey, pageState.previewVersions); saveMapToStorage(storageKey, pageState.previewVersions);
} }
@@ -285,7 +285,7 @@ export class BaseModelApiClient {
async saveModelMetadata(filePath, data) { async saveModelMetadata(filePath, data) {
try { try {
state.loadingManager.showSimpleLoading('Saving metadata...'); state.loadingManager.showSimpleLoading('Saving metadata...');
const response = await fetch(this.apiConfig.endpoints.save, { const response = await fetch(this.apiConfig.endpoints.save, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -339,18 +339,18 @@ export class BaseModelApiClient {
state.loadingManager.showSimpleLoading( state.loadingManager.showSimpleLoading(
`${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${this.apiConfig.config.displayName}s...` `${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${this.apiConfig.config.displayName}s...`
); );
const url = new URL(this.apiConfig.endpoints.scan, window.location.origin); const url = new URL(this.apiConfig.endpoints.scan, window.location.origin);
url.searchParams.append('full_rebuild', fullRebuild); url.searchParams.append('full_rebuild', fullRebuild);
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to refresh ${this.apiConfig.config.displayName}s: ${response.status} ${response.statusText}`); throw new Error(`Failed to refresh ${this.apiConfig.config.displayName}s: ${response.status} ${response.statusText}`);
} }
resetAndReload(true); resetAndReload(true);
showToast('toast.api.refreshComplete', { action: fullRebuild ? 'Full rebuild' : 'Refresh' }, 'success'); showToast('toast.api.refreshComplete', { action: fullRebuild ? 'Full rebuild' : 'Refresh' }, 'success');
} catch (error) { } catch (error) {
console.error('Refresh failed:', error); console.error('Refresh failed:', error);
@@ -364,7 +364,7 @@ export class BaseModelApiClient {
async refreshSingleModelMetadata(filePath) { async refreshSingleModelMetadata(filePath) {
try { try {
state.loadingManager.showSimpleLoading('Refreshing metadata...'); state.loadingManager.showSimpleLoading('Refreshing metadata...');
const response = await fetch(this.apiConfig.endpoints.fetchCivitai, { const response = await fetch(this.apiConfig.endpoints.fetchCivitai, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -376,7 +376,7 @@ export class BaseModelApiClient {
} }
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
if (data.metadata && state.virtualScroller) { if (data.metadata && state.virtualScroller) {
state.virtualScroller.updateSingleItem(filePath, data.metadata); state.virtualScroller.updateSingleItem(filePath, data.metadata);
@@ -399,21 +399,21 @@ export class BaseModelApiClient {
async fetchCivitaiMetadata() { async fetchCivitaiMetadata() {
let ws = null; let ws = null;
await state.loadingManager.showWithProgress(async (loading) => { await state.loadingManager.showWithProgress(async (loading) => {
try { try {
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`); ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
const operationComplete = new Promise((resolve, reject) => { const operationComplete = new Promise((resolve, reject) => {
ws.onmessage = (event) => { ws.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
switch(data.status) { switch (data.status) {
case 'started': case 'started':
loading.setStatus('Starting metadata fetch...'); loading.setStatus('Starting metadata fetch...');
break; break;
case 'processing': case 'processing':
const percent = ((data.processed / data.total) * 100).toFixed(1); const percent = ((data.processed / data.total) * 100).toFixed(1);
loading.setProgress(percent); loading.setProgress(percent);
@@ -421,7 +421,7 @@ export class BaseModelApiClient {
`Processing (${data.processed}/${data.total}) ${data.current_name}` `Processing (${data.processed}/${data.total}) ${data.current_name}`
); );
break; break;
case 'completed': case 'completed':
loading.setProgress(100); loading.setProgress(100);
loading.setStatus( loading.setStatus(
@@ -429,34 +429,34 @@ export class BaseModelApiClient {
); );
resolve(); resolve();
break; break;
case 'error': case 'error':
reject(new Error(data.error)); reject(new Error(data.error));
break; break;
} }
}; };
ws.onerror = (error) => { ws.onerror = (error) => {
reject(new Error('WebSocket error: ' + error.message)); reject(new Error('WebSocket error: ' + error.message));
}; };
}); });
// Wait for WebSocket connection to establish // Wait for WebSocket connection to establish
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
ws.onopen = resolve; ws.onopen = resolve;
ws.onerror = reject; ws.onerror = reject;
}); });
const response = await fetch(this.apiConfig.endpoints.fetchAllCivitai, { const response = await fetch(this.apiConfig.endpoints.fetchAllCivitai, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}) body: JSON.stringify({})
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch metadata'); throw new Error('Failed to fetch metadata');
} }
// Wait for the operation to complete via WebSocket // Wait for the operation to complete via WebSocket
await operationComplete; await operationComplete;
@@ -492,15 +492,15 @@ export class BaseModelApiClient {
for (let i = 0; i < filePaths.length; i++) { for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i]; const filePath = filePaths[i];
const fileName = filePath.split('/').pop(); const fileName = filePath.split('/').pop();
try { try {
const overallProgress = Math.floor((i / totalItems) * 100); const overallProgress = Math.floor((i / totalItems) * 100);
progressController.updateProgress( progressController.updateProgress(
overallProgress, overallProgress,
fileName, fileName,
`Processing ${i + 1}/${totalItems}: ${fileName}` `Processing ${i + 1}/${totalItems}: ${fileName}`
); );
const response = await fetch(this.apiConfig.endpoints.fetchCivitai, { const response = await fetch(this.apiConfig.endpoints.fetchCivitai, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -512,7 +512,7 @@ export class BaseModelApiClient {
} }
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
if (data.metadata && state.virtualScroller) { if (data.metadata && state.virtualScroller) {
state.virtualScroller.updateSingleItem(filePath, data.metadata); state.virtualScroller.updateSingleItem(filePath, data.metadata);
@@ -521,12 +521,12 @@ export class BaseModelApiClient {
} else { } else {
throw new Error(data.error || 'Failed to refresh metadata'); throw new Error(data.error || 'Failed to refresh metadata');
} }
} catch (error) { } catch (error) {
console.error(`Error refreshing metadata for ${fileName}:`, error); console.error(`Error refreshing metadata for ${fileName}:`, error);
failedItems.push({ filePath, fileName, error: error.message }); failedItems.push({ filePath, fileName, error: error.message });
} }
processedCount++; processedCount++;
} }
@@ -537,7 +537,7 @@ export class BaseModelApiClient {
} else if (successCount > 0) { } 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`); 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'); showToast('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, 'warning');
// if (failedItems.length > 0) { // if (failedItems.length > 0) {
// const failureMessage = failedItems.length <= 3 // const failureMessage = failedItems.length <= 3
// ? failedItems.map(item => `${item.fileName}: ${item.error}`).join('\n') // ? failedItems.map(item => `${item.fileName}: ${item.error}`).join('\n')
@@ -770,7 +770,7 @@ export class BaseModelApiClient {
_buildQueryParams(baseParams, pageState) { _buildQueryParams(baseParams, pageState) {
const params = new URLSearchParams(baseParams); const params = new URLSearchParams(baseParams);
if (pageState.activeFolder !== null) { if (pageState.activeFolder !== null) {
params.append('folder', pageState.activeFolder); params.append('folder', pageState.activeFolder);
} }
@@ -790,7 +790,7 @@ export class BaseModelApiClient {
if (pageState.filters?.search) { if (pageState.filters?.search) {
params.append('search', pageState.filters.search); params.append('search', pageState.filters.search);
params.append('fuzzy', 'true'); params.append('fuzzy', 'true');
if (pageState.searchOptions) { if (pageState.searchOptions) {
params.append('search_filename', pageState.searchOptions.filename.toString()); params.append('search_filename', pageState.searchOptions.filename.toString());
params.append('search_modelname', pageState.searchOptions.modelname.toString()); params.append('search_modelname', pageState.searchOptions.modelname.toString());
@@ -804,7 +804,7 @@ export class BaseModelApiClient {
} }
params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false'); params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false');
if (pageState.filters) { if (pageState.filters) {
if (pageState.filters.tags && Object.keys(pageState.filters.tags).length > 0) { if (pageState.filters.tags && Object.keys(pageState.filters.tags).length > 0) {
Object.entries(pageState.filters.tags).forEach(([tag, state]) => { Object.entries(pageState.filters.tags).forEach(([tag, state]) => {
@@ -815,17 +815,17 @@ export class BaseModelApiClient {
} }
}); });
} }
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) { if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
pageState.filters.baseModel.forEach(model => { pageState.filters.baseModel.forEach(model => {
params.append('base_model', model); params.append('base_model', model);
}); });
} }
// Add license filters // Add license filters
if (pageState.filters.license) { if (pageState.filters.license) {
const licenseFilters = pageState.filters.license; const licenseFilters = pageState.filters.license;
if (licenseFilters.noCredit) { if (licenseFilters.noCredit) {
// For noCredit filter: // For noCredit filter:
// - 'include' means credit_required=False (no credit required) // - 'include' means credit_required=False (no credit required)
@@ -836,7 +836,7 @@ export class BaseModelApiClient {
params.append('credit_required', 'true'); params.append('credit_required', 'true');
} }
} }
if (licenseFilters.allowSelling) { if (licenseFilters.allowSelling) {
// For allowSelling filter: // For allowSelling filter:
// - 'include' means allow_selling_generated_content=True // - 'include' means allow_selling_generated_content=True
@@ -848,7 +848,7 @@ export class BaseModelApiClient {
} }
} }
} }
if (pageState.filters.modelTypes && pageState.filters.modelTypes.length > 0) { if (pageState.filters.modelTypes && pageState.filters.modelTypes.length > 0) {
pageState.filters.modelTypes.forEach((type) => { pageState.filters.modelTypes.forEach((type) => {
params.append('model_type', 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 // Only allow move if supported
if (!this.apiConfig.config.supportsMove) { if (!this.apiConfig.config.supportsMove) {
showToast('toast.api.moveNotSupported', { type: this.apiConfig.config.displayName }, 'warning'); showToast('toast.api.moveNotSupported', { type: this.apiConfig.config.displayName }, 'warning');
return null; 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'); showToast('toast.api.alreadyInFolder', { type: this.apiConfig.config.displayName }, 'info');
return null; return null;
} }
@@ -913,7 +913,8 @@ export class BaseModelApiClient {
}, },
body: JSON.stringify({ body: JSON.stringify({
file_path: filePath, file_path: filePath,
target_path: targetPath target_path: targetPath,
use_default_paths: useDefaultPaths
}) })
}); });
@@ -941,12 +942,12 @@ export class BaseModelApiClient {
return null; return null;
} }
async moveBulkModels(filePaths, targetPath) { async moveBulkModels(filePaths, targetPath, useDefaultPaths = false) {
if (!this.apiConfig.config.supportsMove) { if (!this.apiConfig.config.supportsMove) {
showToast('toast.api.bulkMoveNotSupported', { type: this.apiConfig.config.displayName }, 'warning'); showToast('toast.api.bulkMoveNotSupported', { type: this.apiConfig.config.displayName }, 'warning');
return []; return [];
} }
const movedPaths = filePaths.filter(path => { const movedPaths = useDefaultPaths ? filePaths : filePaths.filter(path => {
return path.substring(0, path.lastIndexOf('/')) !== targetPath; return path.substring(0, path.lastIndexOf('/')) !== targetPath;
}); });
@@ -962,7 +963,8 @@ export class BaseModelApiClient {
}, },
body: JSON.stringify({ body: JSON.stringify({
file_paths: movedPaths, 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.success) {
if (result.failure_count > 0) { if (result.failure_count > 0) {
showToast('toast.api.bulkMovePartial', { showToast('toast.api.bulkMovePartial', {
successCount: result.success_count, successCount: result.success_count,
type: this.apiConfig.config.displayName, type: this.apiConfig.config.displayName,
failureCount: result.failure_count failureCount: result.failure_count
}, 'warning'); }, 'warning');
console.log('Move operation results:', result.results); console.log('Move operation results:', result.results);
const failedFiles = result.results const failedFiles = result.results
@@ -987,18 +989,18 @@ export class BaseModelApiClient {
return `${fileName}: ${r.message}`; return `${fileName}: ${r.message}`;
}); });
if (failedFiles.length > 0) { if (failedFiles.length > 0) {
const failureMessage = failedFiles.length <= 3 const failureMessage = failedFiles.length <= 3
? failedFiles.join('\n') ? failedFiles.join('\n')
: failedFiles.slice(0, 3).join('\n') + `\n(and ${failedFiles.length - 3} more)`; : failedFiles.slice(0, 3).join('\n') + `\n(and ${failedFiles.length - 3} more)`;
showToast('toast.api.bulkMoveFailures', { failures: failureMessage }, 'warning', 6000); showToast('toast.api.bulkMoveFailures', { failures: failureMessage }, 'warning', 6000);
} }
} else { } else {
showToast('toast.api.bulkMoveSuccess', { showToast('toast.api.bulkMoveSuccess', {
successCount: result.success_count, successCount: result.success_count,
type: this.apiConfig.config.displayName type: this.apiConfig.config.displayName
}, 'success'); }, 'success');
} }
// Return the results array with original_file_path and new_file_path // Return the results array with original_file_path and new_file_path
return result.results || []; return result.results || [];
} else { } else {
@@ -1013,7 +1015,7 @@ export class BaseModelApiClient {
try { try {
state.loadingManager.showSimpleLoading(`Deleting ${this.apiConfig.config.displayName.toLowerCase()}s...`); state.loadingManager.showSimpleLoading(`Deleting ${this.apiConfig.config.displayName.toLowerCase()}s...`);
const response = await fetch(this.apiConfig.endpoints.bulkDelete, { const response = await fetch(this.apiConfig.endpoints.bulkDelete, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -1023,13 +1025,13 @@ export class BaseModelApiClient {
file_paths: filePaths file_paths: filePaths
}) })
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to delete ${this.apiConfig.config.displayName.toLowerCase()}s: ${response.statusText}`); throw new Error(`Failed to delete ${this.apiConfig.config.displayName.toLowerCase()}s: ${response.statusText}`);
} }
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
return { return {
success: true, success: true,
@@ -1050,20 +1052,20 @@ export class BaseModelApiClient {
async downloadExampleImages(modelHashes, modelTypes = null) { async downloadExampleImages(modelHashes, modelTypes = null) {
let ws = null; let ws = null;
await state.loadingManager.showWithProgress(async (loading) => { await state.loadingManager.showWithProgress(async (loading) => {
try { try {
// Connect to WebSocket for progress updates // Connect to WebSocket for progress updates
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`); ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
const operationComplete = new Promise((resolve, reject) => { const operationComplete = new Promise((resolve, reject) => {
ws.onmessage = (event) => { ws.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.type !== 'example_images_progress') return; if (data.type !== 'example_images_progress') return;
switch(data.status) { switch (data.status) {
case 'running': case 'running':
const percent = ((data.processed / data.total) * 100).toFixed(1); const percent = ((data.processed / data.total) * 100).toFixed(1);
loading.setProgress(percent); loading.setProgress(percent);
@@ -1071,7 +1073,7 @@ export class BaseModelApiClient {
`Processing (${data.processed}/${data.total}) ${data.current_model || ''}` `Processing (${data.processed}/${data.total}) ${data.current_model || ''}`
); );
break; break;
case 'completed': case 'completed':
loading.setProgress(100); loading.setProgress(100);
loading.setStatus( loading.setStatus(
@@ -1079,33 +1081,33 @@ export class BaseModelApiClient {
); );
resolve(); resolve();
break; break;
case 'error': case 'error':
reject(new Error(data.error)); reject(new Error(data.error));
break; break;
} }
}; };
ws.onerror = (error) => { ws.onerror = (error) => {
reject(new Error('WebSocket error: ' + error.message)); reject(new Error('WebSocket error: ' + error.message));
}; };
}); });
// Wait for WebSocket connection to establish // Wait for WebSocket connection to establish
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
ws.onopen = resolve; ws.onopen = resolve;
ws.onerror = reject; ws.onerror = reject;
}); });
// Get the output directory from state // Get the output directory from state
const outputDir = state.global?.settings?.example_images_path || ''; const outputDir = state.global?.settings?.example_images_path || '';
if (!outputDir) { if (!outputDir) {
throw new Error('Please set the example images path in the settings first.'); throw new Error('Please set the example images path in the settings first.');
} }
// Determine optimize setting // Determine optimize setting
const optimize = state.global?.settings?.optimize_example_images ?? true; const optimize = state.global?.settings?.optimize_example_images ?? true;
// Make the API request to start the download process // Make the API request to start the download process
const response = await fetch(DOWNLOAD_ENDPOINTS.exampleImages, { const response = await fetch(DOWNLOAD_ENDPOINTS.exampleImages, {
method: 'POST', method: 'POST',
@@ -1119,18 +1121,18 @@ export class BaseModelApiClient {
model_types: modelTypes || [this.apiConfig.config.singularName] model_types: modelTypes || [this.apiConfig.config.singularName]
}) })
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to download example images'); throw new Error(errorData.error || 'Failed to download example images');
} }
// Wait for the operation to complete via WebSocket // Wait for the operation to complete via WebSocket
await operationComplete; await operationComplete;
showToast('toast.api.exampleImagesDownloadSuccess', {}, 'success'); showToast('toast.api.exampleImagesDownloadSuccess', {}, 'success');
return true; return true;
} catch (error) { } catch (error) {
console.error('Error downloading example images:', error); console.error('Error downloading example images:', error);
showToast('toast.api.exampleImagesDownloadFailed', { message: error.message }, 'error'); showToast('toast.api.exampleImagesDownloadFailed', { message: error.message }, 'error');
@@ -1150,13 +1152,13 @@ export class BaseModelApiClient {
try { try {
const params = new URLSearchParams({ file_path: filePath }); const params = new URLSearchParams({ file_path: filePath });
const response = await fetch(`${this.apiConfig.endpoints.metadata}?${params}`); const response = await fetch(`${this.apiConfig.endpoints.metadata}?${params}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} metadata: ${response.statusText}`); throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} metadata: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
return data.metadata; return data.metadata;
} else { } else {
@@ -1172,13 +1174,13 @@ export class BaseModelApiClient {
try { try {
const params = new URLSearchParams({ file_path: filePath }); const params = new URLSearchParams({ file_path: filePath });
const response = await fetch(`${this.apiConfig.endpoints.modelDescription}?${params}`); const response = await fetch(`${this.apiConfig.endpoints.modelDescription}?${params}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} description: ${response.statusText}`); throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} description: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
return data.description; return data.description;
} else { } else {
@@ -1197,26 +1199,26 @@ export class BaseModelApiClient {
*/ */
async autoOrganizeModels(filePaths = null) { async autoOrganizeModels(filePaths = null) {
let ws = null; let ws = null;
await state.loadingManager.showWithProgress(async (loading) => { await state.loadingManager.showWithProgress(async (loading) => {
try { try {
// Connect to WebSocket for progress updates // Connect to WebSocket for progress updates
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`); ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
const operationComplete = new Promise((resolve, reject) => { const operationComplete = new Promise((resolve, reject) => {
ws.onmessage = (event) => { ws.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.type !== 'auto_organize_progress') return; if (data.type !== 'auto_organize_progress') return;
switch(data.status) { switch (data.status) {
case 'started': case 'started':
loading.setProgress(0); loading.setProgress(0);
const operationType = data.operation_type === 'bulk' ? 'selected models' : 'all models'; const operationType = data.operation_type === 'bulk' ? 'selected models' : 'all models';
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.starting', { type: operationType }, `Starting auto-organize for ${operationType}...`)); loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.starting', { type: operationType }, `Starting auto-organize for ${operationType}...`));
break; break;
case 'processing': case 'processing':
const percent = data.total > 0 ? ((data.processed / data.total) * 90).toFixed(1) : 0; const percent = data.total > 0 ? ((data.processed / data.total) * 90).toFixed(1) : 0;
loading.setProgress(percent); loading.setProgress(percent);
@@ -1230,12 +1232,12 @@ export class BaseModelApiClient {
}, `Processing (${data.processed}/${data.total}) - ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`) }, `Processing (${data.processed}/${data.total}) - ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
); );
break; break;
case 'cleaning': case 'cleaning':
loading.setProgress(95); loading.setProgress(95);
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.cleaning', {}, 'Cleaning up empty directories...')); loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.cleaning', {}, 'Cleaning up empty directories...'));
break; break;
case 'completed': case 'completed':
loading.setProgress(100); loading.setProgress(100);
loading.setStatus( loading.setStatus(
@@ -1246,25 +1248,25 @@ export class BaseModelApiClient {
total: data.total total: data.total
}, `Completed: ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`) }, `Completed: ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
); );
setTimeout(() => { setTimeout(() => {
resolve(data); resolve(data);
}, 1500); }, 1500);
break; break;
case 'error': case 'error':
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.error', { error: data.error }, `Error: ${data.error}`)); loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.error', { error: data.error }, `Error: ${data.error}`));
reject(new Error(data.error)); reject(new Error(data.error));
break; break;
} }
}; };
ws.onerror = (error) => { ws.onerror = (error) => {
console.error('WebSocket error during auto-organize:', error); console.error('WebSocket error during auto-organize:', error);
reject(new Error('Connection error')); reject(new Error('Connection error'));
}; };
}); });
// Start the auto-organize operation // Start the auto-organize operation
const endpoint = this.apiConfig.endpoints.autoOrganize; const endpoint = this.apiConfig.endpoints.autoOrganize;
const exclusionPatterns = (state.global.settings.auto_organize_exclusions || []) const exclusionPatterns = (state.global.settings.auto_organize_exclusions || [])
@@ -1286,29 +1288,29 @@ export class BaseModelApiClient {
}; };
const response = await fetch(endpoint, requestOptions); const response = await fetch(endpoint, requestOptions);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to start auto-organize operation'); throw new Error(errorData.error || 'Failed to start auto-organize operation');
} }
// Wait for the operation to complete via WebSocket // Wait for the operation to complete via WebSocket
const result = await operationComplete; const result = await operationComplete;
// Show appropriate success message based on results // Show appropriate success message based on results
if (result.failures === 0) { if (result.failures === 0) {
showToast('toast.loras.autoOrganizeSuccess', { showToast('toast.loras.autoOrganizeSuccess', {
count: result.success, count: result.success,
type: result.operation_type === 'bulk' ? 'selected models' : 'all models' type: result.operation_type === 'bulk' ? 'selected models' : 'all models'
}, 'success'); }, 'success');
} else { } else {
showToast('toast.loras.autoOrganizePartialSuccess', { showToast('toast.loras.autoOrganizePartialSuccess', {
success: result.success, success: result.success,
failures: result.failures, failures: result.failures,
total: result.total total: result.total
}, 'warning'); }, 'warning');
} }
} catch (error) { } catch (error) {
console.error('Error during auto-organize:', error); console.error('Error during auto-organize:', error);
showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error'); showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error');

View File

@@ -15,17 +15,17 @@ export class DownloadManager {
this.modelVersionId = null; this.modelVersionId = null;
this.modelId = null; this.modelId = null;
this.source = null; this.source = null;
this.initialized = false; this.initialized = false;
this.selectedFolder = ''; this.selectedFolder = '';
this.apiClient = null; this.apiClient = null;
this.useDefaultPath = false; this.useDefaultPath = false;
this.loadingManager = new LoadingManager(); this.loadingManager = new LoadingManager();
this.folderTreeManager = new FolderTreeManager(); this.folderTreeManager = new FolderTreeManager();
this.folderClickHandler = null; this.folderClickHandler = null;
this.updateTargetPath = this.updateTargetPath.bind(this); this.updateTargetPath = this.updateTargetPath.bind(this);
// Bound methods for event handling // Bound methods for event handling
this.handleValidateAndFetchVersions = this.validateAndFetchVersions.bind(this); this.handleValidateAndFetchVersions = this.validateAndFetchVersions.bind(this);
this.handleProceedToLocation = this.proceedToLocation.bind(this); this.handleProceedToLocation = this.proceedToLocation.bind(this);
@@ -38,11 +38,11 @@ export class DownloadManager {
showDownloadModal() { showDownloadModal() {
console.log('Showing unified download modal...'); console.log('Showing unified download modal...');
// Get API client for current page type // Get API client for current page type
this.apiClient = getModelApiClient(); this.apiClient = getModelApiClient();
const config = this.apiClient.apiConfig.config; const config = this.apiClient.apiConfig.config;
if (!this.initialized) { if (!this.initialized) {
const modal = document.getElementById('downloadModal'); const modal = document.getElementById('downloadModal');
if (!modal) { if (!modal) {
@@ -52,15 +52,15 @@ export class DownloadManager {
this.initializeEventHandlers(); this.initializeEventHandlers();
this.initialized = true; this.initialized = true;
} }
// Update modal title and labels based on model type // Update modal title and labels based on model type
this.updateModalLabels(); this.updateModalLabels();
modalManager.showModal('downloadModal', null, () => { modalManager.showModal('downloadModal', null, () => {
this.cleanupFolderBrowser(); this.cleanupFolderBrowser();
}); });
this.resetSteps(); this.resetSteps();
// Auto-focus on the URL input // Auto-focus on the URL input
setTimeout(() => { setTimeout(() => {
const urlInput = document.getElementById('modelUrl'); const urlInput = document.getElementById('modelUrl');
@@ -78,23 +78,23 @@ export class DownloadManager {
document.getElementById('backToUrlBtn').addEventListener('click', this.handleBackToUrl); document.getElementById('backToUrlBtn').addEventListener('click', this.handleBackToUrl);
document.getElementById('backToVersionsBtn').addEventListener('click', this.handleBackToVersions); document.getElementById('backToVersionsBtn').addEventListener('click', this.handleBackToVersions);
document.getElementById('closeDownloadModal').addEventListener('click', this.handleCloseModal); document.getElementById('closeDownloadModal').addEventListener('click', this.handleCloseModal);
// Default path toggle handler // Default path toggle handler
document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath); document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath);
} }
updateModalLabels() { updateModalLabels() {
const config = this.apiClient.apiConfig.config; const config = this.apiClient.apiConfig.config;
// Update modal title // Update modal title
document.getElementById('downloadModalTitle').textContent = translate('modals.download.titleWithType', { type: config.displayName }); document.getElementById('downloadModalTitle').textContent = translate('modals.download.titleWithType', { type: config.displayName });
// Update URL label // Update URL label
document.getElementById('modelUrlLabel').textContent = translate('modals.download.civitaiUrl'); document.getElementById('modelUrlLabel').textContent = translate('modals.download.civitaiUrl');
// Update root selection label // Update root selection label
document.getElementById('modelRootLabel').textContent = translate('modals.download.selectTypeRoot', { type: config.displayName }); document.getElementById('modelRootLabel').textContent = translate('modals.download.selectTypeRoot', { type: config.displayName });
// Update path preview labels // Update path preview labels
const pathLabels = document.querySelectorAll('.path-preview label'); const pathLabels = document.querySelectorAll('.path-preview label');
pathLabels.forEach(label => { pathLabels.forEach(label => {
@@ -102,7 +102,7 @@ export class DownloadManager {
label.textContent = translate('modals.download.locationPreview') + ':'; label.textContent = translate('modals.download.locationPreview') + ':';
} }
}); });
// Update initial path text // Update initial path text
const pathText = document.querySelector('#targetPathDisplay .path-text'); const pathText = document.querySelector('#targetPathDisplay .path-text');
if (pathText) { if (pathText) {
@@ -115,27 +115,27 @@ export class DownloadManager {
document.getElementById('urlStep').style.display = 'block'; document.getElementById('urlStep').style.display = 'block';
document.getElementById('modelUrl').value = ''; document.getElementById('modelUrl').value = '';
document.getElementById('urlError').textContent = ''; document.getElementById('urlError').textContent = '';
// Clear folder path input // Clear folder path input
const folderPathInput = document.getElementById('folderPath'); const folderPathInput = document.getElementById('folderPath');
if (folderPathInput) { if (folderPathInput) {
folderPathInput.value = ''; folderPathInput.value = '';
} }
this.currentVersion = null; this.currentVersion = null;
this.versions = []; this.versions = [];
this.modelInfo = null; this.modelInfo = null;
this.modelId = null; this.modelId = null;
this.modelVersionId = null; this.modelVersionId = null;
this.source = null; this.source = null;
this.selectedFolder = ''; this.selectedFolder = '';
// Clear folder tree selection // Clear folder tree selection
if (this.folderTreeManager) { if (this.folderTreeManager) {
this.folderTreeManager.clearSelection(); this.folderTreeManager.clearSelection();
} }
// Reset default path toggle // Reset default path toggle
this.loadDefaultPathSetting(); this.loadDefaultPathSetting();
} }
@@ -151,10 +151,10 @@ export class DownloadManager {
async validateAndFetchVersions() { async validateAndFetchVersions() {
const url = document.getElementById('modelUrl').value.trim(); const url = document.getElementById('modelUrl').value.trim();
const errorElement = document.getElementById('urlError'); const errorElement = document.getElementById('urlError');
try { try {
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions')); this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions'));
this.modelId = this.extractModelId(url); this.modelId = this.extractModelId(url);
if (!this.modelId) { if (!this.modelId) {
throw new Error(translate('modals.download.errors.invalidUrl')); throw new Error(translate('modals.download.errors.invalidUrl'));
@@ -166,7 +166,7 @@ export class DownloadManager {
if (this.modelVersionId) { if (this.modelVersionId) {
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId); this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
} }
this.showVersionStep(); this.showVersionStep();
} catch (error) { } catch (error) {
errorElement.textContent = error.message; errorElement.textContent = error.message;
@@ -239,20 +239,20 @@ export class DownloadManager {
showVersionStep() { showVersionStep() {
document.getElementById('urlStep').style.display = 'none'; document.getElementById('urlStep').style.display = 'none';
document.getElementById('versionStep').style.display = 'block'; document.getElementById('versionStep').style.display = 'block';
const versionList = document.getElementById('versionList'); const versionList = document.getElementById('versionList');
versionList.innerHTML = this.versions.map(version => { versionList.innerHTML = this.versions.map(version => {
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4')); const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png'; const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
const fileSize = version.modelSizeKB ? const fileSize = version.modelSizeKB ?
(version.modelSizeKB / 1024).toFixed(2) : (version.modelSizeKB / 1024).toFixed(2) :
(version.files[0]?.sizeKB / 1024).toFixed(2); (version.files[0]?.sizeKB / 1024).toFixed(2);
const existsLocally = version.existsLocally; const existsLocally = version.existsLocally;
const localPath = version.localPath; const localPath = version.localPath;
const isEarlyAccess = version.availability === 'EarlyAccess'; const isEarlyAccess = version.availability === 'EarlyAccess';
let earlyAccessBadge = ''; let earlyAccessBadge = '';
if (isEarlyAccess) { if (isEarlyAccess) {
earlyAccessBadge = ` earlyAccessBadge = `
@@ -261,8 +261,8 @@ export class DownloadManager {
</div> </div>
`; `;
} }
const localStatus = existsLocally ? const localStatus = existsLocally ?
`<div class="local-badge"> `<div class="local-badge">
<i class="fas fa-check"></i> ${translate('modals.download.inLibrary')} <i class="fas fa-check"></i> ${translate('modals.download.inLibrary')}
<div class="local-path">${localPath || ''}</div> <div class="local-path">${localPath || ''}</div>
@@ -293,7 +293,7 @@ export class DownloadManager {
</div> </div>
`; `;
}).join(''); }).join('');
// Add click handlers for version selection // Add click handlers for version selection
versionList.addEventListener('click', (event) => { versionList.addEventListener('click', (event) => {
const versionItem = event.target.closest('.version-item'); const versionItem = event.target.closest('.version-item');
@@ -301,12 +301,12 @@ export class DownloadManager {
this.selectVersion(versionItem.dataset.versionId); this.selectVersion(versionItem.dataset.versionId);
} }
}); });
// Auto-select the version if there's only one // Auto-select the version if there's only one
if (this.versions.length === 1 && !this.currentVersion) { if (this.versions.length === 1 && !this.currentVersion) {
this.selectVersion(this.versions[0].id.toString()); this.selectVersion(this.versions[0].id.toString());
} }
this.updateNextButtonState(); this.updateNextButtonState();
} }
@@ -317,16 +317,16 @@ export class DownloadManager {
document.querySelectorAll('.version-item').forEach(item => { document.querySelectorAll('.version-item').forEach(item => {
item.classList.toggle('selected', item.dataset.versionId === versionId); item.classList.toggle('selected', item.dataset.versionId === versionId);
}); });
this.updateNextButtonState(); this.updateNextButtonState();
} }
updateNextButtonState() { updateNextButtonState() {
const nextButton = document.getElementById('nextFromVersion'); const nextButton = document.getElementById('nextFromVersion');
if (!nextButton) return; if (!nextButton) return;
const existsLocally = this.currentVersion?.existsLocally; const existsLocally = this.currentVersion?.existsLocally;
if (existsLocally) { if (existsLocally) {
nextButton.disabled = true; nextButton.disabled = true;
nextButton.classList.add('disabled'); nextButton.classList.add('disabled');
@@ -343,7 +343,7 @@ export class DownloadManager {
showToast('toast.loras.pleaseSelectVersion', {}, 'error'); showToast('toast.loras.pleaseSelectVersion', {}, 'error');
return; return;
} }
const existsLocally = this.currentVersion.existsLocally; const existsLocally = this.currentVersion.existsLocally;
if (existsLocally) { if (existsLocally) {
showToast('toast.loras.versionExists', {}, 'info'); showToast('toast.loras.versionExists', {}, 'info');
@@ -352,12 +352,12 @@ export class DownloadManager {
document.getElementById('versionStep').style.display = 'none'; document.getElementById('versionStep').style.display = 'none';
document.getElementById('locationStep').style.display = 'block'; document.getElementById('locationStep').style.display = 'block';
try { try {
// Fetch model roots // Fetch model roots
const rootsData = await this.apiClient.fetchModelRoots(); const rootsData = await this.apiClient.fetchModelRoots();
const modelRoot = document.getElementById('modelRoot'); const modelRoot = document.getElementById('modelRoot');
modelRoot.innerHTML = rootsData.roots.map(root => modelRoot.innerHTML = rootsData.roots.map(root =>
`<option value="${root}">${root}</option>` `<option value="${root}">${root}</option>`
).join(''); ).join('');
@@ -380,7 +380,7 @@ export class DownloadManager {
// Initialize folder tree // Initialize folder tree
await this.initializeFolderTree(); await this.initializeFolderTree();
// Setup folder tree manager // Setup folder tree manager
this.folderTreeManager.init({ this.folderTreeManager.init({
onPathChange: (path) => { onPathChange: (path) => {
@@ -388,16 +388,16 @@ export class DownloadManager {
this.updateTargetPath(); this.updateTargetPath();
} }
}); });
// Setup model root change handler // Setup model root change handler
modelRoot.addEventListener('change', async () => { modelRoot.addEventListener('change', async () => {
await this.initializeFolderTree(); await this.initializeFolderTree();
this.updateTargetPath(); this.updateTargetPath();
}); });
// Load default path setting for current model type // Load default path setting for current model type
this.loadDefaultPathSetting(); this.loadDefaultPathSetting();
this.updateTargetPath(); this.updateTargetPath();
} catch (error) { } catch (error) {
showToast('toast.downloads.loadError', { message: error.message }, 'error'); showToast('toast.downloads.loadError', { message: error.message }, 'error');
@@ -408,7 +408,7 @@ export class DownloadManager {
const modelType = this.apiClient.modelType; const modelType = this.apiClient.modelType;
const storageKey = `use_default_path_${modelType}`; const storageKey = `use_default_path_${modelType}`;
this.useDefaultPath = getStorageItem(storageKey, false); this.useDefaultPath = getStorageItem(storageKey, false);
const toggleInput = document.getElementById('useDefaultPath'); const toggleInput = document.getElementById('useDefaultPath');
if (toggleInput) { if (toggleInput) {
toggleInput.checked = this.useDefaultPath; toggleInput.checked = this.useDefaultPath;
@@ -418,12 +418,12 @@ export class DownloadManager {
toggleDefaultPath(event) { toggleDefaultPath(event) {
this.useDefaultPath = event.target.checked; this.useDefaultPath = event.target.checked;
// Save to localStorage per model type // Save to localStorage per model type
const modelType = this.apiClient.modelType; const modelType = this.apiClient.modelType;
const storageKey = `use_default_path_${modelType}`; const storageKey = `use_default_path_${modelType}`;
setStorageItem(storageKey, this.useDefaultPath); setStorageItem(storageKey, this.useDefaultPath);
this.updatePathSelectionUI(); this.updatePathSelectionUI();
this.updateTargetPath(); this.updateTargetPath();
} }
@@ -446,7 +446,7 @@ export class DownloadManager {
const displayName = versionName || `#${versionId}`; const displayName = versionName || `#${versionId}`;
let ws = null; let ws = null;
let updateProgress = () => {}; let updateProgress = () => { };
try { try {
this.loadingManager.restoreProgressBar(); this.loadingManager.restoreProgressBar();
@@ -549,7 +549,7 @@ export class DownloadManager {
updatePathSelectionUI() { updatePathSelectionUI() {
const manualSelection = document.getElementById('manualPathSelection'); const manualSelection = document.getElementById('manualPathSelection');
// Always show manual path selection, but disable/enable based on useDefaultPath // Always show manual path selection, but disable/enable based on useDefaultPath
manualSelection.style.display = 'block'; manualSelection.style.display = 'block';
if (this.useDefaultPath) { if (this.useDefaultPath) {
@@ -566,11 +566,11 @@ export class DownloadManager {
el.tabIndex = 0; el.tabIndex = 0;
}); });
} }
// Always update the main path display // Always update the main path display
this.updateTargetPath(); this.updateTargetPath();
} }
backToUrl() { backToUrl() {
document.getElementById('versionStep').style.display = 'none'; document.getElementById('versionStep').style.display = 'none';
document.getElementById('urlStep').style.display = 'block'; document.getElementById('urlStep').style.display = 'block';
@@ -592,7 +592,7 @@ export class DownloadManager {
async startDownload() { async startDownload() {
const modelRoot = document.getElementById('modelRoot').value; const modelRoot = document.getElementById('modelRoot').value;
const config = this.apiClient.apiConfig.config; const config = this.apiClient.apiConfig.config;
if (!modelRoot) { if (!modelRoot) {
showToast('toast.models.pleaseSelectRoot', { type: config.displayName }, 'error'); showToast('toast.models.pleaseSelectRoot', { type: config.displayName }, 'error');
return; return;
@@ -601,7 +601,7 @@ export class DownloadManager {
// Determine target folder and use_default_paths parameter // Determine target folder and use_default_paths parameter
let targetFolder = ''; let targetFolder = '';
let useDefaultPaths = false; let useDefaultPaths = false;
if (this.useDefaultPath) { if (this.useDefaultPath) {
useDefaultPaths = true; useDefaultPaths = true;
targetFolder = ''; // Not needed when using default paths targetFolder = ''; // Not needed when using default paths
@@ -646,7 +646,7 @@ export class DownloadManager {
try { try {
// Fetch unified folder tree // Fetch unified folder tree
const treeData = await this.apiClient.fetchUnifiedFolderTree(); const treeData = await this.apiClient.fetchUnifiedFolderTree();
if (treeData.success) { if (treeData.success) {
// Load tree data into folder tree manager // Load tree data into folder tree manager
await this.folderTreeManager.loadTree(treeData.tree); await this.folderTreeManager.loadTree(treeData.tree);
@@ -674,23 +674,23 @@ export class DownloadManager {
folderItem.classList.remove('selected'); folderItem.classList.remove('selected');
this.selectedFolder = ''; this.selectedFolder = '';
} else { } else {
folderBrowser.querySelectorAll('.folder-item').forEach(f => folderBrowser.querySelectorAll('.folder-item').forEach(f =>
f.classList.remove('selected')); f.classList.remove('selected'));
folderItem.classList.add('selected'); folderItem.classList.add('selected');
this.selectedFolder = folderItem.dataset.folder; this.selectedFolder = folderItem.dataset.folder;
} }
this.updateTargetPath(); this.updateTargetPath();
}; };
folderBrowser.addEventListener('click', this.folderClickHandler); folderBrowser.addEventListener('click', this.folderClickHandler);
const modelRoot = document.getElementById('modelRoot'); const modelRoot = document.getElementById('modelRoot');
const newFolder = document.getElementById('newFolder'); const newFolder = document.getElementById('newFolder');
modelRoot.addEventListener('change', this.updateTargetPath); modelRoot.addEventListener('change', this.updateTargetPath);
newFolder.addEventListener('input', this.updateTargetPath); newFolder.addEventListener('input', this.updateTargetPath);
this.updateTargetPath(); this.updateTargetPath();
} }
@@ -702,21 +702,21 @@ export class DownloadManager {
this.folderClickHandler = null; this.folderClickHandler = null;
} }
} }
const modelRoot = document.getElementById('modelRoot'); const modelRoot = document.getElementById('modelRoot');
const newFolder = document.getElementById('newFolder'); const newFolder = document.getElementById('newFolder');
if (modelRoot) modelRoot.removeEventListener('change', this.updateTargetPath); if (modelRoot) modelRoot.removeEventListener('change', this.updateTargetPath);
if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath); if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
} }
updateTargetPath() { updateTargetPath() {
const pathDisplay = document.getElementById('targetPathDisplay'); const pathDisplay = document.getElementById('targetPathDisplay');
const modelRoot = document.getElementById('modelRoot').value; const modelRoot = document.getElementById('modelRoot').value;
const config = this.apiClient.apiConfig.config; const config = this.apiClient.apiConfig.config;
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: config.displayName }); let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: config.displayName });
if (modelRoot) { if (modelRoot) {
if (this.useDefaultPath) { if (this.useDefaultPath) {
// Show actual template path // Show actual template path

View File

@@ -28,7 +28,7 @@ export class ImportManager {
this.importMode = 'url'; // Default mode: 'url' or 'upload' this.importMode = 'url'; // Default mode: 'url' or 'upload'
this.useDefaultPath = false; this.useDefaultPath = false;
this.apiClient = null; this.apiClient = null;
// Initialize sub-managers // Initialize sub-managers
this.loadingManager = new LoadingManager(); this.loadingManager = new LoadingManager();
this.stepManager = new ImportStepManager(); this.stepManager = new ImportStepManager();
@@ -36,7 +36,7 @@ export class ImportManager {
this.recipeDataManager = new RecipeDataManager(this); this.recipeDataManager = new RecipeDataManager(this);
this.downloadManager = new DownloadManager(this); this.downloadManager = new DownloadManager(this);
this.folderTreeManager = new FolderTreeManager(); this.folderTreeManager = new FolderTreeManager();
// Bind methods // Bind methods
this.formatFileSize = formatFileSize; this.formatFileSize = formatFileSize;
this.updateTargetPath = this.updateTargetPath.bind(this); this.updateTargetPath = this.updateTargetPath.bind(this);
@@ -53,17 +53,17 @@ export class ImportManager {
this.initializeEventHandlers(); this.initializeEventHandlers();
this.initialized = true; this.initialized = true;
} }
// Get API client for LoRAs // Get API client for LoRAs
this.apiClient = getModelApiClient(MODEL_TYPES.LORA); this.apiClient = getModelApiClient(MODEL_TYPES.LORA);
// Reset state // Reset state
this.resetSteps(); this.resetSteps();
if (recipeData) { if (recipeData) {
this.downloadableLoRAs = recipeData.loras; this.downloadableLoRAs = recipeData.loras;
this.recipeId = recipeId; this.recipeId = recipeId;
} }
// Show modal // Show modal
modalManager.showModal('importModal', null, () => { modalManager.showModal('importModal', null, () => {
this.cleanupFolderBrowser(); this.cleanupFolderBrowser();
@@ -71,7 +71,7 @@ export class ImportManager {
}); });
// Verify visibility and focus on URL input // Verify visibility and focus on URL input
setTimeout(() => { setTimeout(() => {
// Ensure URL option is selected and focus on the input // Ensure URL option is selected and focus on the input
this.toggleImportMode('url'); this.toggleImportMode('url');
const urlInput = document.getElementById('imageUrlInput'); const urlInput = document.getElementById('imageUrlInput');
@@ -93,32 +93,32 @@ export class ImportManager {
// Clear UI state // Clear UI state
this.stepManager.removeInjectedStyles(); this.stepManager.removeInjectedStyles();
this.stepManager.showStep('uploadStep'); this.stepManager.showStep('uploadStep');
// Reset form inputs // Reset form inputs
const fileInput = document.getElementById('recipeImageUpload'); const fileInput = document.getElementById('recipeImageUpload');
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
const urlInput = document.getElementById('imageUrlInput'); const urlInput = document.getElementById('imageUrlInput');
if (urlInput) urlInput.value = ''; if (urlInput) urlInput.value = '';
const uploadError = document.getElementById('uploadError'); const uploadError = document.getElementById('uploadError');
if (uploadError) uploadError.textContent = ''; if (uploadError) uploadError.textContent = '';
const importUrlError = document.getElementById('importUrlError'); const importUrlError = document.getElementById('importUrlError');
if (importUrlError) importUrlError.textContent = ''; if (importUrlError) importUrlError.textContent = '';
const recipeName = document.getElementById('recipeName'); const recipeName = document.getElementById('recipeName');
if (recipeName) recipeName.value = ''; if (recipeName) recipeName.value = '';
const tagsContainer = document.getElementById('tagsContainer'); const tagsContainer = document.getElementById('tagsContainer');
if (tagsContainer) tagsContainer.innerHTML = `<div class="empty-tags">${translate('recipes.controls.import.noTagsAdded', {}, 'No tags added')}</div>`; if (tagsContainer) tagsContainer.innerHTML = `<div class="empty-tags">${translate('recipes.controls.import.noTagsAdded', {}, 'No tags added')}</div>`;
// Clear folder path input // Clear folder path input
const folderPathInput = document.getElementById('importFolderPath'); const folderPathInput = document.getElementById('importFolderPath');
if (folderPathInput) { if (folderPathInput) {
folderPathInput.value = ''; folderPathInput.value = '';
} }
// Reset state variables // Reset state variables
this.recipeImage = null; this.recipeImage = null;
this.recipeData = null; this.recipeData = null;
@@ -127,30 +127,30 @@ export class ImportManager {
this.missingLoras = []; this.missingLoras = [];
this.downloadableLoRAs = []; this.downloadableLoRAs = [];
this.selectedFolder = ''; this.selectedFolder = '';
// Reset import mode // Reset import mode
this.importMode = 'url'; this.importMode = 'url';
this.toggleImportMode('url'); this.toggleImportMode('url');
// Clear folder tree selection // Clear folder tree selection
if (this.folderTreeManager) { if (this.folderTreeManager) {
this.folderTreeManager.clearSelection(); this.folderTreeManager.clearSelection();
} }
// Reset default path toggle // Reset default path toggle
this.loadDefaultPathSetting(); this.loadDefaultPathSetting();
// Reset duplicate related properties // Reset duplicate related properties
this.duplicateRecipes = []; this.duplicateRecipes = [];
} }
toggleImportMode(mode) { toggleImportMode(mode) {
this.importMode = mode; this.importMode = mode;
// Update toggle buttons // Update toggle buttons
const uploadBtn = document.querySelector('.toggle-btn[data-mode="upload"]'); const uploadBtn = document.querySelector('.toggle-btn[data-mode="upload"]');
const urlBtn = document.querySelector('.toggle-btn[data-mode="url"]'); const urlBtn = document.querySelector('.toggle-btn[data-mode="url"]');
if (uploadBtn && urlBtn) { if (uploadBtn && urlBtn) {
if (mode === 'upload') { if (mode === 'upload') {
uploadBtn.classList.add('active'); uploadBtn.classList.add('active');
@@ -160,11 +160,11 @@ export class ImportManager {
urlBtn.classList.add('active'); urlBtn.classList.add('active');
} }
} }
// Show/hide appropriate sections // Show/hide appropriate sections
const uploadSection = document.getElementById('uploadSection'); const uploadSection = document.getElementById('uploadSection');
const urlSection = document.getElementById('urlSection'); const urlSection = document.getElementById('urlSection');
if (uploadSection && urlSection) { if (uploadSection && urlSection) {
if (mode === 'upload') { if (mode === 'upload') {
uploadSection.style.display = 'block'; uploadSection.style.display = 'block';
@@ -174,11 +174,11 @@ export class ImportManager {
urlSection.style.display = 'block'; urlSection.style.display = 'block';
} }
} }
// Clear error messages // Clear error messages
const uploadError = document.getElementById('uploadError'); const uploadError = document.getElementById('uploadError');
const importUrlError = document.getElementById('importUrlError'); const importUrlError = document.getElementById('importUrlError');
if (uploadError) uploadError.textContent = ''; if (uploadError) uploadError.textContent = '';
if (importUrlError) importUrlError.textContent = ''; if (importUrlError) importUrlError.textContent = '';
} }
@@ -206,7 +206,7 @@ export class ImportManager {
addTag() { addTag() {
this.recipeDataManager.addTag(); this.recipeDataManager.addTag();
} }
removeTag(tag) { removeTag(tag) {
this.recipeDataManager.removeTag(tag); this.recipeDataManager.removeTag(tag);
} }
@@ -217,12 +217,12 @@ export class ImportManager {
async proceedToLocation() { async proceedToLocation() {
this.stepManager.showStep('locationStep'); this.stepManager.showStep('locationStep');
try { try {
// Fetch LoRA roots // Fetch LoRA roots
const rootsData = await this.apiClient.fetchModelRoots(); const rootsData = await this.apiClient.fetchModelRoots();
const loraRoot = document.getElementById('importLoraRoot'); const loraRoot = document.getElementById('importLoraRoot');
loraRoot.innerHTML = rootsData.roots.map(root => loraRoot.innerHTML = rootsData.roots.map(root =>
`<option value="${root}">${root}</option>` `<option value="${root}">${root}</option>`
).join(''); ).join('');
@@ -247,19 +247,19 @@ export class ImportManager {
this.updateTargetPath(); this.updateTargetPath();
} }
}); });
// Initialize folder tree // Initialize folder tree
await this.initializeFolderTree(); await this.initializeFolderTree();
// Setup lora root change handler // Setup lora root change handler
loraRoot.addEventListener('change', async () => { loraRoot.addEventListener('change', async () => {
await this.initializeFolderTree(); await this.initializeFolderTree();
this.updateTargetPath(); this.updateTargetPath();
}); });
// Load default path setting for LoRAs // Load default path setting for LoRAs
this.loadDefaultPathSetting(); this.loadDefaultPathSetting();
this.updateTargetPath(); this.updateTargetPath();
} catch (error) { } catch (error) {
showToast('toast.recipes.importFailed', { message: error.message }, 'error'); showToast('toast.recipes.importFailed', { message: error.message }, 'error');
@@ -268,19 +268,19 @@ export class ImportManager {
backToUpload() { backToUpload() {
this.stepManager.showStep('uploadStep'); this.stepManager.showStep('uploadStep');
// Reset file input // Reset file input
const fileInput = document.getElementById('recipeImageUpload'); const fileInput = document.getElementById('recipeImageUpload');
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
// Reset URL input // Reset URL input
const urlInput = document.getElementById('imageUrlInput'); const urlInput = document.getElementById('imageUrlInput');
if (urlInput) urlInput.value = ''; if (urlInput) urlInput.value = '';
// Clear error messages // Clear error messages
const uploadError = document.getElementById('uploadError'); const uploadError = document.getElementById('uploadError');
if (uploadError) uploadError.textContent = ''; if (uploadError) uploadError.textContent = '';
const importUrlError = document.getElementById('importUrlError'); const importUrlError = document.getElementById('importUrlError');
if (importUrlError) importUrlError.textContent = ''; if (importUrlError) importUrlError.textContent = '';
} }
@@ -296,7 +296,7 @@ export class ImportManager {
loadDefaultPathSetting() { loadDefaultPathSetting() {
const storageKey = 'use_default_path_loras'; const storageKey = 'use_default_path_loras';
this.useDefaultPath = getStorageItem(storageKey, false); this.useDefaultPath = getStorageItem(storageKey, false);
const toggleInput = document.getElementById('importUseDefaultPath'); const toggleInput = document.getElementById('importUseDefaultPath');
if (toggleInput) { if (toggleInput) {
toggleInput.checked = this.useDefaultPath; toggleInput.checked = this.useDefaultPath;
@@ -306,18 +306,18 @@ export class ImportManager {
toggleDefaultPath(event) { toggleDefaultPath(event) {
this.useDefaultPath = event.target.checked; this.useDefaultPath = event.target.checked;
// Save to localStorage for LoRAs // Save to localStorage for LoRAs
const storageKey = 'use_default_path_loras'; const storageKey = 'use_default_path_loras';
setStorageItem(storageKey, this.useDefaultPath); setStorageItem(storageKey, this.useDefaultPath);
this.updatePathSelectionUI(); this.updatePathSelectionUI();
this.updateTargetPath(); this.updateTargetPath();
} }
updatePathSelectionUI() { updatePathSelectionUI() {
const manualSelection = document.getElementById('importManualPathSelection'); const manualSelection = document.getElementById('importManualPathSelection');
// Always show manual path selection, but disable/enable based on useDefaultPath // Always show manual path selection, but disable/enable based on useDefaultPath
if (manualSelection) { if (manualSelection) {
manualSelection.style.display = 'block'; manualSelection.style.display = 'block';
@@ -336,7 +336,7 @@ export class ImportManager {
}); });
} }
} }
// Always update the main path display // Always update the main path display
this.updateTargetPath(); this.updateTargetPath();
} }
@@ -345,7 +345,7 @@ export class ImportManager {
try { try {
// Fetch unified folder tree // Fetch unified folder tree
const treeData = await this.apiClient.fetchUnifiedFolderTree(); const treeData = await this.apiClient.fetchUnifiedFolderTree();
if (treeData.success) { if (treeData.success) {
// Load tree data into folder tree manager // Load tree data into folder tree manager
await this.folderTreeManager.loadTree(treeData.tree); await this.folderTreeManager.loadTree(treeData.tree);
@@ -368,8 +368,8 @@ export class ImportManager {
updateTargetPath() { updateTargetPath() {
const pathDisplay = document.getElementById('importTargetPathDisplay'); const pathDisplay = document.getElementById('importTargetPathDisplay');
const loraRoot = document.getElementById('importLoraRoot').value; 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) { if (this.useDefaultPath) {
// Show actual template path // Show actual template path
try { try {
@@ -417,19 +417,19 @@ export class ImportManager {
// Store the recipe data and ID // Store the recipe data and ID
this.recipeData = recipeData; this.recipeData = recipeData;
this.recipeId = recipeId; this.recipeId = recipeId;
// Show the modal and go to location step // Show the modal and go to location step
this.showImportModal(recipeData, recipeId); this.showImportModal(recipeData, recipeId);
this.proceedToLocation(); this.proceedToLocation();
// Update the modal title // Update the modal title
const modalTitle = document.querySelector('#importModal h2'); const modalTitle = document.querySelector('#importModal h2');
if (modalTitle) modalTitle.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs'); if (modalTitle) modalTitle.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
// Update the save button text // Update the save button text
const saveButton = document.querySelector('#locationStep .primary-btn'); const saveButton = document.querySelector('#locationStep .primary-btn');
if (saveButton) saveButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs'); if (saveButton) saveButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
// Hide the back button // Hide the back button
const backButton = document.querySelector('#locationStep .secondary-btn'); const backButton = document.querySelector('#locationStep .secondary-btn');
if (backButton) backButton.style.display = 'none'; if (backButton) backButton.style.display = 'none';

View File

@@ -6,6 +6,8 @@ import { getModelApiClient } from '../api/modelApiFactory.js';
import { RecipeSidebarApiClient } from '../api/recipeApi.js'; import { RecipeSidebarApiClient } from '../api/recipeApi.js';
import { FolderTreeManager } from '../components/FolderTreeManager.js'; import { FolderTreeManager } from '../components/FolderTreeManager.js';
import { sidebarManager } from '../components/SidebarManager.js'; import { sidebarManager } from '../components/SidebarManager.js';
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
import { translate } from '../utils/i18nHelpers.js';
class MoveManager { class MoveManager {
constructor() { constructor() {
@@ -14,9 +16,11 @@ class MoveManager {
this.folderTreeManager = new FolderTreeManager(); this.folderTreeManager = new FolderTreeManager();
this.initialized = false; this.initialized = false;
this.recipeApiClient = null; this.recipeApiClient = null;
this.useDefaultPath = false;
// Bind methods // Bind methods
this.updateTargetPath = this.updateTargetPath.bind(this); this.updateTargetPath = this.updateTargetPath.bind(this);
this.handleToggleDefaultPath = this.handleToggleDefaultPath.bind(this);
} }
_getApiClient(modelType = null) { _getApiClient(modelType = null) {
@@ -31,15 +35,21 @@ class MoveManager {
initializeEventListeners() { initializeEventListeners() {
if (this.initialized) return; if (this.initialized) return;
const modelRootSelect = document.getElementById('moveModelRoot'); const modelRootSelect = document.getElementById('moveModelRoot');
// Initialize model root directory selector // Initialize model root directory selector
modelRootSelect.addEventListener('change', async () => { modelRootSelect.addEventListener('change', async () => {
await this.initializeFolderTree(); await this.initializeFolderTree();
this.updateTargetPath(); this.updateTargetPath();
}); });
// Default path toggle handler
const toggleInput = document.getElementById('moveUseDefaultPath');
if (toggleInput) {
toggleInput.addEventListener('change', this.handleToggleDefaultPath);
}
this.initialized = true; this.initialized = true;
} }
@@ -47,11 +57,11 @@ class MoveManager {
// Reset state // Reset state
this.currentFilePath = null; this.currentFilePath = null;
this.bulkFilePaths = null; this.bulkFilePaths = null;
const apiClient = this._getApiClient(modelType); const apiClient = this._getApiClient(modelType);
const currentPageType = state.currentPageType; const currentPageType = state.currentPageType;
const modelConfig = apiClient.apiConfig.config; const modelConfig = apiClient.apiConfig.config;
// Handle bulk mode // Handle bulk mode
if (filePath === 'bulk') { if (filePath === 'bulk') {
const selectedPaths = Array.from(state.selectedModels); const selectedPaths = Array.from(state.selectedModels);
@@ -66,11 +76,11 @@ class MoveManager {
this.currentFilePath = filePath; this.currentFilePath = filePath;
document.getElementById('moveModalTitle').textContent = `Move ${modelConfig.displayName}`; document.getElementById('moveModalTitle').textContent = `Move ${modelConfig.displayName}`;
} }
// Update UI labels based on model type // Update UI labels based on model type
document.getElementById('moveRootLabel').textContent = `Select ${modelConfig.displayName} Root:`; document.getElementById('moveRootLabel').textContent = `Select ${modelConfig.displayName} Root:`;
document.getElementById('moveTargetPathDisplay').querySelector('.path-text').textContent = `Select a ${modelConfig.displayName.toLowerCase()} root directory`; document.getElementById('moveTargetPathDisplay').querySelector('.path-text').textContent = `Select a ${modelConfig.displayName.toLowerCase()} root directory`;
// Clear folder path input // Clear folder path input
const folderPathInput = document.getElementById('moveFolderPath'); const folderPathInput = document.getElementById('moveFolderPath');
if (folderPathInput) { if (folderPathInput) {
@@ -86,13 +96,13 @@ class MoveManager {
} else { } else {
rootsData = await apiClient.fetchModelRoots(); rootsData = await apiClient.fetchModelRoots();
} }
if (!rootsData.roots || rootsData.roots.length === 0) { if (!rootsData.roots || rootsData.roots.length === 0) {
throw new Error(`No ${modelConfig.displayName.toLowerCase()} roots found`); throw new Error(`No ${modelConfig.displayName.toLowerCase()} roots found`);
} }
// Populate model root selector // Populate model root selector
modelRootSelect.innerHTML = rootsData.roots.map(root => modelRootSelect.innerHTML = rootsData.roots.map(root =>
`<option value="${root}">${root}</option>` `<option value="${root}">${root}</option>`
).join(''); ).join('');
@@ -105,7 +115,7 @@ class MoveManager {
// Initialize event listeners // Initialize event listeners
this.initializeEventListeners(); this.initializeEventListeners();
// Setup folder tree manager // Setup folder tree manager
this.folderTreeManager.init({ this.folderTreeManager.init({
onPathChange: (path) => { onPathChange: (path) => {
@@ -113,10 +123,13 @@ class MoveManager {
}, },
elementsPrefix: 'move' elementsPrefix: 'move'
}); });
// Initialize folder tree // Initialize folder tree
await this.initializeFolderTree(); await this.initializeFolderTree();
// Load default path setting
this.loadDefaultPathSetting(apiClient.modelType);
this.updateTargetPath(); this.updateTargetPath();
modalManager.showModal('moveModal', null, () => { modalManager.showModal('moveModal', null, () => {
// Cleanup on modal close // Cleanup on modal close
@@ -124,19 +137,63 @@ class MoveManager {
this.folderTreeManager.destroy(); this.folderTreeManager.destroy();
} }
}); });
} catch (error) { } catch (error) {
console.error(`Error fetching ${modelConfig.displayName.toLowerCase()} roots or folders:`, error); console.error(`Error fetching ${modelConfig.displayName.toLowerCase()} roots or folders:`, error);
showToast('toast.models.moveFailed', { message: error.message }, '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() { async initializeFolderTree() {
try { try {
const apiClient = this._getApiClient(); const apiClient = this._getApiClient();
// Fetch unified folder tree // Fetch unified folder tree
const treeData = await apiClient.fetchUnifiedFolderTree(); const treeData = await apiClient.fetchUnifiedFolderTree();
if (treeData.success) { if (treeData.success) {
// Load tree data into folder tree manager // Load tree data into folder tree manager
await this.folderTreeManager.loadTree(treeData.tree); await this.folderTreeManager.loadTree(treeData.tree);
@@ -155,13 +212,27 @@ class MoveManager {
const modelRoot = document.getElementById('moveModelRoot').value; const modelRoot = document.getElementById('moveModelRoot').value;
const apiClient = this._getApiClient(); const apiClient = this._getApiClient();
const config = apiClient.apiConfig.config; 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) { if (modelRoot) {
const selectedPath = this.folderTreeManager ? this.folderTreeManager.getSelectedPath() : ''; if (this.useDefaultPath) {
if (selectedPath) { // Show actual template path
fullPath += '/' + selectedPath; 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 selectedRoot = document.getElementById('moveModelRoot').value;
const apiClient = this._getApiClient(); const apiClient = this._getApiClient();
const config = apiClient.apiConfig.config; const config = apiClient.apiConfig.config;
if (!selectedRoot) { if (!selectedRoot) {
showToast('toast.models.pleaseSelectRoot', { type: config.displayName.toLowerCase() }, 'error'); showToast('toast.models.pleaseSelectRoot', { type: config.displayName.toLowerCase() }, 'error');
return; return;
@@ -180,7 +251,7 @@ class MoveManager {
// Get selected folder path from folder tree manager // Get selected folder path from folder tree manager
const targetFolder = this.folderTreeManager.getSelectedPath(); const targetFolder = this.folderTreeManager.getSelectedPath();
let targetPath = selectedRoot; let targetPath = selectedRoot;
if (targetFolder) { if (targetFolder) {
targetPath = `${targetPath}/${targetFolder}`; targetPath = `${targetPath}/${targetFolder}`;
@@ -189,7 +260,7 @@ class MoveManager {
try { try {
if (this.bulkFilePaths) { if (this.bulkFilePaths) {
// Bulk move mode // 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 // Update virtual scroller if in active folder view
const pageState = getCurrentPageState(); const pageState = getCurrentPageState();
@@ -206,7 +277,7 @@ class MoveManager {
if (result.success && result.new_file_path !== result.original_file_path) { 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 newFileName = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1);
const baseFileName = newFileName.substring(0, newFileName.lastIndexOf('.')); const baseFileName = newFileName.substring(0, newFileName.lastIndexOf('.'));
state.virtualScroller.updateSingleItem(result.original_file_path, { state.virtualScroller.updateSingleItem(result.original_file_path, {
file_path: result.new_file_path, file_path: result.new_file_path,
file_name: baseFileName file_name: baseFileName
@@ -216,7 +287,7 @@ class MoveManager {
} }
} else { } else {
// Single move mode // Single move mode
const result = await apiClient.moveSingleModel(this.currentFilePath, targetPath); const result = await apiClient.moveSingleModel(this.currentFilePath, targetPath, this.useDefaultPath);
const pageState = getCurrentPageState(); const pageState = getCurrentPageState();
if (result && result.new_file_path) { if (result && result.new_file_path) {
@@ -226,7 +297,7 @@ class MoveManager {
// Update both file_path and file_name if they changed // Update both file_path and file_name if they changed
const newFileName = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1); const newFileName = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1);
const baseFileName = newFileName.substring(0, newFileName.lastIndexOf('.')); const baseFileName = newFileName.substring(0, newFileName.lastIndexOf('.'));
state.virtualScroller.updateSingleItem(this.currentFilePath, { state.virtualScroller.updateSingleItem(this.currentFilePath, {
file_path: result.new_file_path, file_path: result.new_file_path,
file_name: baseFileName file_name: baseFileName
@@ -239,7 +310,7 @@ class MoveManager {
sidebarManager.refresh(); sidebarManager.refresh();
modalManager.closeModal('moveModal'); modalManager.closeModal('moveModal');
// If we were in bulk mode, exit it after successful move // If we were in bulk mode, exit it after successful move
if (this.bulkFilePaths && state.bulkMode) { if (this.bulkFilePaths && state.bulkMode) {
bulkManager.toggleBulkMode(); bulkManager.toggleBulkMode();

View File

@@ -6,50 +6,65 @@
<span class="close" onclick="modalManager.closeModal('moveModal')">&times;</span> <span class="close" onclick="modalManager.closeModal('moveModal')">&times;</span>
</div> </div>
<div class="location-selection"> <div class="location-selection">
<!-- Path preview --> <!-- Path preview with inline toggle -->
<div class="path-preview"> <div class="path-preview">
<label>{{ t('modals.moveModel.targetLocationPreview') }}</label> <div class="path-preview-header">
<label>{{ t('modals.moveModel.targetLocationPreview') }}:</label>
<div class="inline-toggle-container" title="{{ t('modals.download.useDefaultPathTooltip') }}">
<span class="inline-toggle-label">{{ t('modals.download.useDefaultPath') }}</span>
<div class="toggle-switch">
<input type="checkbox" id="moveUseDefaultPath">
<label for="moveUseDefaultPath" class="toggle-slider"></label>
</div>
</div>
</div>
<div class="path-display" id="moveTargetPathDisplay"> <div class="path-display" id="moveTargetPathDisplay">
<span class="path-text">{{ t('modals.download.selectRootDirectory') }}</span> <span class="path-text">{{ t('modals.download.selectRootDirectory') }}</span>
</div> </div>
</div> </div>
<div class="input-group"> <!-- Manual Location Selection (disabled when using default path) -->
<label for="moveModelRoot" id="moveRootLabel">{{ t('modals.moveModel.selectModelRoot') }}</label> <div id="moveManualPathSelection" class="manual-path-selection">
<select id="moveModelRoot"></select> <div class="input-group">
</div> <label for="moveModelRoot" id="moveRootLabel">{{ t('modals.moveModel.selectModelRoot') }}</label>
<select id="moveModelRoot"></select>
<!-- Path input with autocomplete -->
<div class="input-group">
<label for="moveFolderPath">{{ t('modals.moveModel.targetFolderPath') }}</label>
<div class="path-input-container">
<input type="text" id="moveFolderPath" placeholder="{{ t('modals.moveModel.pathPlaceholder') }}" autocomplete="off" />
<button type="button" id="moveCreateFolderBtn" class="create-folder-btn" title="{{ t('modals.moveModel.createNewFolder') }}">
<i class="fas fa-plus"></i>
</button>
</div> </div>
<div class="path-suggestions" id="movePathSuggestions" style="display: none;"></div>
</div> <!-- Path input with autocomplete -->
<div class="input-group">
<!-- Breadcrumb navigation --> <label for="moveFolderPath">{{ t('modals.moveModel.targetFolderPath') }}</label>
<div class="breadcrumb-nav" id="moveBreadcrumbNav"> <div class="path-input-container">
<span class="breadcrumb-item root" data-path=""> <input type="text" id="moveFolderPath" placeholder="{{ t('modals.moveModel.pathPlaceholder') }}"
<i class="fas fa-home"></i> {{ t('modals.moveModel.root') }} autocomplete="off" />
</span> <button type="button" id="moveCreateFolderBtn" class="create-folder-btn"
</div> title="{{ t('modals.moveModel.createNewFolder') }}">
<i class="fas fa-plus"></i>
<!-- Hierarchical folder tree --> </button>
<div class="input-group"> </div>
<label>{{ t('modals.moveModel.browseFolders') }}</label> <div class="path-suggestions" id="movePathSuggestions" style="display: none;"></div>
<div class="folder-tree-container"> </div>
<div class="folder-tree" id="moveFolderTree">
<!-- Tree will be loaded dynamically --> <!-- Breadcrumb navigation -->
<div class="breadcrumb-nav" id="moveBreadcrumbNav">
<span class="breadcrumb-item root" data-path="">
<i class="fas fa-home"></i> {{ t('modals.moveModel.root') }}
</span>
</div>
<!-- Hierarchical folder tree -->
<div class="input-group">
<label>{{ t('modals.moveModel.browseFolders') }}</label>
<div class="folder-tree-container">
<div class="folder-tree" id="moveFolderTree">
<!-- Tree will be loaded dynamically -->
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button class="cancel-btn" onclick="modalManager.closeModal('moveModal')">{{ t('common.actions.cancel') }}</button> <button class="cancel-btn" onclick="modalManager.closeModal('moveModal')">{{ t('common.actions.cancel')
}}</button>
<button class="primary-btn" onclick="moveManager.moveModel()">{{ t('common.actions.move') }}</button> <button class="primary-btn" onclick="moveManager.moveModel()">{{ t('common.actions.move') }}</button>
</div> </div>
</div> </div>