mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 22:52:12 -03:00
feat: Implement model move, import, and download functionalities with corresponding UI and API updates.
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ export class BaseModelApiClient {
|
|||||||
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;
|
||||||
@@ -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
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1063,7 +1065,7 @@ export class BaseModelApiClient {
|
|||||||
|
|
||||||
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);
|
||||||
@@ -1210,7 +1212,7 @@ export class BaseModelApiClient {
|
|||||||
|
|
||||||
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';
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ export class ImportManager {
|
|||||||
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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -40,6 +44,12 @@ class MoveManager {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +127,9 @@ class MoveManager {
|
|||||||
// 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
|
||||||
@@ -131,6 +144,50 @@ class MoveManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -156,12 +213,26 @@ class MoveManager {
|
|||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -6,50 +6,65 @@
|
|||||||
<span class="close" onclick="modalManager.closeModal('moveModal')">×</span>
|
<span class="close" onclick="modalManager.closeModal('moveModal')">×</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>
|
|
||||||
|
|
||||||
<!-- Breadcrumb navigation -->
|
<!-- Path input with autocomplete -->
|
||||||
<div class="breadcrumb-nav" id="moveBreadcrumbNav">
|
<div class="input-group">
|
||||||
<span class="breadcrumb-item root" data-path="">
|
<label for="moveFolderPath">{{ t('modals.moveModel.targetFolderPath') }}</label>
|
||||||
<i class="fas fa-home"></i> {{ t('modals.moveModel.root') }}
|
<div class="path-input-container">
|
||||||
</span>
|
<input type="text" id="moveFolderPath" placeholder="{{ t('modals.moveModel.pathPlaceholder') }}"
|
||||||
</div>
|
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 class="path-suggestions" id="movePathSuggestions" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Hierarchical folder tree -->
|
<!-- Breadcrumb navigation -->
|
||||||
<div class="input-group">
|
<div class="breadcrumb-nav" id="moveBreadcrumbNav">
|
||||||
<label>{{ t('modals.moveModel.browseFolders') }}</label>
|
<span class="breadcrumb-item root" data-path="">
|
||||||
<div class="folder-tree-container">
|
<i class="fas fa-home"></i> {{ t('modals.moveModel.root') }}
|
||||||
<div class="folder-tree" id="moveFolderTree">
|
</span>
|
||||||
<!-- Tree will be loaded dynamically -->
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user