mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
checkpoint
This commit is contained in:
@@ -40,6 +40,7 @@ class ApiRoutes:
|
||||
app.router.add_post('/api/fetch-all-civitai', routes.fetch_all_civitai)
|
||||
app.router.add_get('/ws/fetch-progress', ws_manager.handle_connection)
|
||||
app.router.add_get('/api/lora-roots', routes.get_lora_roots)
|
||||
app.router.add_get('/api/folders', routes.get_folders)
|
||||
app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions)
|
||||
app.router.add_post('/api/download-lora', routes.download_lora)
|
||||
app.router.add_post('/api/settings', routes.update_settings)
|
||||
@@ -520,6 +521,13 @@ class ApiRoutes:
|
||||
return web.json_response({
|
||||
'roots': config.loras_roots
|
||||
})
|
||||
|
||||
async def get_folders(self, request: web.Request) -> web.Response:
|
||||
"""Get all folders in the cache"""
|
||||
cache = await self.scanner.get_cached_data()
|
||||
return web.json_response({
|
||||
'folders': cache.folders
|
||||
})
|
||||
|
||||
async def get_civitai_versions(self, request: web.Request) -> web.Response:
|
||||
"""Get available versions for a Civitai model with local availability info"""
|
||||
|
||||
@@ -180,7 +180,6 @@ class RecipeRoutes:
|
||||
|
||||
# Extract metadata from the image using ExifUtils
|
||||
user_comment = ExifUtils.extract_user_comment(temp_path)
|
||||
print(f"User comment: {user_comment}", file=sys.stderr)
|
||||
|
||||
# If no metadata found, return a more specific error
|
||||
if not user_comment:
|
||||
@@ -191,7 +190,6 @@ class RecipeRoutes:
|
||||
|
||||
# Parse the recipe metadata
|
||||
metadata = ExifUtils.parse_recipe_metadata(user_comment)
|
||||
print(f"Metadata: {metadata}", file=sys.stderr)
|
||||
|
||||
# Look for Civitai resources in the metadata
|
||||
civitai_resources = metadata.get('loras', [])
|
||||
@@ -220,7 +218,6 @@ class RecipeRoutes:
|
||||
|
||||
# Get additional info from Civitai
|
||||
civitai_info = await self.civitai_client.get_model_version_info(model_version_id)
|
||||
print(f"Civitai info: {civitai_info}", file=sys.stderr)
|
||||
|
||||
# Check if this LoRA exists locally by SHA256 hash
|
||||
exists_locally = False
|
||||
|
||||
@@ -3,6 +3,341 @@
|
||||
margin: var(--space-2) 0;
|
||||
}
|
||||
|
||||
/* File Input Styles */
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.file-input-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.file-input-button:hover {
|
||||
background: oklch(from var(--lora-accent) l c h / 0.9);
|
||||
}
|
||||
|
||||
.file-input-wrapper:hover .file-input-button {
|
||||
background: oklch(from var(--lora-accent) l c h / 0.9);
|
||||
}
|
||||
|
||||
/* Recipe Details Layout */
|
||||
.recipe-details-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.recipe-image-container {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
overflow: hidden;
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.recipe-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.recipe-image img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.recipe-form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
/* Tags Input Styles */
|
||||
.tag-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.tag-input-container input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: var(--space-1);
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.recipe-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.recipe-tag i {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.recipe-tag i:hover {
|
||||
opacity: 1;
|
||||
color: var(--lora-error);
|
||||
}
|
||||
|
||||
.empty-tags {
|
||||
color: var(--text-color);
|
||||
opacity: 0.6;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* LoRAs List Styles */
|
||||
.loras-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: var(--space-2) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.lora-item {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--bg-color);
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.lora-item.exists-locally {
|
||||
background: oklch(var(--lora-accent) / 0.05);
|
||||
border-left: 4px solid var(--lora-accent);
|
||||
}
|
||||
|
||||
.lora-item.missing-locally {
|
||||
border-left: 4px solid var(--lora-error);
|
||||
}
|
||||
|
||||
.lora-thumbnail {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--border-radius-xs);
|
||||
overflow: hidden;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.lora-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.lora-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lora-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.lora-content h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
color: var(--text-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.lora-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.lora-info .base-model {
|
||||
background: oklch(var(--lora-accent) / 0.1);
|
||||
color: var(--lora-accent);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.lora-version {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.weight-badge {
|
||||
background: var(--lora-surface);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Missing LoRAs List */
|
||||
.missing-loras-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin: var(--space-2) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: var(--space-1);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--lora-surface);
|
||||
}
|
||||
|
||||
.missing-lora-item {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-1);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.missing-lora-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.missing-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--lora-error);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.missing-badge i {
|
||||
margin-right: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.lora-count-info {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.8;
|
||||
font-weight: normal;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Location Selection Styles */
|
||||
.location-selection {
|
||||
margin: var(--space-2) 0;
|
||||
padding: var(--space-2);
|
||||
background: var(--lora-surface);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
/* Reuse folder browser and path preview styles from download-modal.css */
|
||||
.folder-browser {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: var(--space-1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-xs);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.folder-item:hover {
|
||||
background: var(--lora-surface);
|
||||
}
|
||||
|
||||
.folder-item.selected {
|
||||
background: oklch(var(--lora-accent) / 0.1);
|
||||
border: 1px solid var(--lora-accent);
|
||||
}
|
||||
|
||||
.path-preview {
|
||||
margin-bottom: var(--space-3);
|
||||
padding: var(--space-2);
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
border: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.path-preview label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.path-display {
|
||||
padding: var(--space-1);
|
||||
color: var(--text-color);
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
opacity: 0.85;
|
||||
background: var(--lora-surface);
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
/* Input Group Styles */
|
||||
.input-group {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
@@ -29,447 +364,30 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Image Upload Styles */
|
||||
.image-upload-container {
|
||||
/* Modal Actions */
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-preview .placeholder {
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input-wrapper .file-input-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
/* Dark theme adjustments */
|
||||
[data-theme="dark"] .lora-item {
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.file-input-wrapper:hover .file-input-button {
|
||||
background: var(--lora-surface-hover);
|
||||
}
|
||||
|
||||
/* Recipe Details Styles */
|
||||
.recipe-details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.recipe-name-container {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.recipe-name-container label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recipe-name-container input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tags-section {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.tags-section label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tag-input-container input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.recipe-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.recipe-tag i {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.recipe-tag i:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty-tags {
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* LoRAs List Styles */
|
||||
.loras-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: var(--space-2) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.lora-item {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--bg-color);
|
||||
margin: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lora-item.exists-locally {
|
||||
background: oklch(var(--lora-accent) / 0.05);
|
||||
border-left: 4px solid var(--lora-accent);
|
||||
}
|
||||
|
||||
.lora-item.missing-locally {
|
||||
background: oklch(var(--lora-error) / 0.05);
|
||||
border-left: 4px solid var(--lora-error);
|
||||
}
|
||||
|
||||
.lora-thumbnail {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--border-radius-xs);
|
||||
overflow: hidden;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.lora-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.lora-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lora-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.lora-content h3 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
color: var(--text-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.lora-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.lora-info .base-model {
|
||||
background: oklch(var(--lora-accent) / 0.1);
|
||||
color: var(--lora-accent);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.weight-badge {
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.lora-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 0.85em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.lora-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.local-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.local-badge i {
|
||||
margin-right: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.missing-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--lora-error);
|
||||
color: var(--lora-text);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.missing-badge i {
|
||||
margin-right: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.local-path {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
[data-theme="dark"] .recipe-tag {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: var(--space-1);
|
||||
margin-top: 4px;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color);
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.local-badge:hover .local-path {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Missing LoRAs List */
|
||||
.missing-loras-list {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
margin: var(--space-2) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.missing-lora-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: oklch(var(--lora-error) / 0.05);
|
||||
border-left: 4px solid var(--lora-error);
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.lora-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.lora-type {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Folder Browser Styles */
|
||||
.folder-browser {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: var(--space-1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Modal Header Styles - Updated to match download-modal */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* align-items: center; */
|
||||
margin-bottom: var(--space-3);
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 0 5px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Recipe Details Layout */
|
||||
.recipe-details-layout {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.recipe-image-container {
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
.recipe-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.recipe-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.recipe-form-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* Simplify file input for step 1 */
|
||||
.file-input-wrapper {
|
||||
margin: var(--space-3) auto;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* Update LoRA item styles to include version */
|
||||
.lora-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lora-version {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.lora-count-info {
|
||||
font-size: 0.85em;
|
||||
font-weight: normal;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
margin-left: 8px;
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.recipe-details-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recipe-image-container {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@ export class ImportManager {
|
||||
this.loadingManager = new LoadingManager();
|
||||
this.folderClickHandler = null;
|
||||
this.updateTargetPath = this.updateTargetPath.bind(this);
|
||||
|
||||
// 添加对注入样式的引用
|
||||
this.injectedStyles = null;
|
||||
}
|
||||
|
||||
showImportModal() {
|
||||
console.log('Showing import modal...');
|
||||
if (!this.initialized) {
|
||||
// Check if modal exists
|
||||
const modal = document.getElementById('importModal');
|
||||
@@ -37,13 +39,26 @@ export class ImportManager {
|
||||
modalManager.showModal('importModal', null, () => {
|
||||
// Cleanup handler when modal closes
|
||||
this.cleanupFolderBrowser();
|
||||
|
||||
// 移除任何强制样式
|
||||
this.removeInjectedStyles();
|
||||
});
|
||||
this.resetSteps();
|
||||
}
|
||||
|
||||
// 添加移除注入样式的方法
|
||||
removeInjectedStyles() {
|
||||
if (this.injectedStyles && this.injectedStyles.parentNode) {
|
||||
this.injectedStyles.parentNode.removeChild(this.injectedStyles);
|
||||
this.injectedStyles = null;
|
||||
}
|
||||
}
|
||||
|
||||
resetSteps() {
|
||||
document.querySelectorAll('.import-step').forEach(step => step.style.display = 'none');
|
||||
document.getElementById('uploadStep').style.display = 'block';
|
||||
// 移除可能存在的强制样式
|
||||
this.removeInjectedStyles();
|
||||
|
||||
this.showStep('uploadStep');
|
||||
|
||||
// Reset file input
|
||||
const fileInput = document.getElementById('recipeImageUpload');
|
||||
@@ -147,8 +162,7 @@ export class ImportManager {
|
||||
}
|
||||
|
||||
showRecipeDetailsStep() {
|
||||
document.getElementById('uploadStep').style.display = 'none';
|
||||
document.getElementById('detailsStep').style.display = 'block';
|
||||
this.showStep('detailsStep');
|
||||
|
||||
// Set default recipe name from image filename
|
||||
const recipeName = document.getElementById('recipeName');
|
||||
@@ -277,6 +291,8 @@ export class ImportManager {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Proceeding from details, missing LoRAs:', this.missingLoras.length);
|
||||
|
||||
// If we have missing LoRAs, go to location step
|
||||
if (this.missingLoras.length > 0) {
|
||||
this.proceedToLocation();
|
||||
@@ -287,239 +303,242 @@ export class ImportManager {
|
||||
}
|
||||
|
||||
async proceedToLocation() {
|
||||
document.getElementById('detailsStep').style.display = 'none';
|
||||
document.getElementById('locationStep').style.display = 'block';
|
||||
// 先移除可能已有的样式
|
||||
this.removeInjectedStyles();
|
||||
|
||||
// 添加强制CSS覆盖
|
||||
this.injectedStyles = document.createElement('style');
|
||||
this.injectedStyles.innerHTML = `
|
||||
#locationStep {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
position: static !important;
|
||||
z-index: 10000 !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
transform: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(this.injectedStyles);
|
||||
console.log('Added override CSS to force visibility');
|
||||
|
||||
this.showStep('locationStep');
|
||||
|
||||
try {
|
||||
this.loadingManager.showSimpleLoading('Loading download options...');
|
||||
|
||||
const response = await fetch('/api/lora-roots');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch LoRA roots');
|
||||
// Fetch LoRA roots
|
||||
const rootsResponse = await fetch('/api/lora-roots');
|
||||
if (!rootsResponse.ok) {
|
||||
throw new Error(`Failed to fetch LoRA roots: ${rootsResponse.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const rootsData = await rootsResponse.json();
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
|
||||
// Check if we have roots
|
||||
if (!data.roots || data.roots.length === 0) {
|
||||
throw new Error('No LoRA root directories configured');
|
||||
if (loraRoot) {
|
||||
loraRoot.innerHTML = rootsData.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
// Populate roots dropdown
|
||||
loraRoot.innerHTML = data.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
// Fetch folders
|
||||
const foldersResponse = await fetch('/api/folders');
|
||||
if (!foldersResponse.ok) {
|
||||
throw new Error(`Failed to fetch folders: ${foldersResponse.status}`);
|
||||
}
|
||||
|
||||
const foldersData = await foldersResponse.json();
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (folderBrowser) {
|
||||
folderBrowser.innerHTML = foldersData.folders.map(folder =>
|
||||
folder ? `<div class="folder-item" data-folder="${folder}">${folder}</div>` : ''
|
||||
).join('');
|
||||
}
|
||||
|
||||
// Initialize folder browser after loading roots
|
||||
await this.initializeFolderBrowser();
|
||||
|
||||
// Display missing LoRAs
|
||||
const missingLorasList = document.getElementById('missingLorasList');
|
||||
if (missingLorasList) {
|
||||
missingLorasList.innerHTML = this.missingLoras.map(lora => `
|
||||
<div class="missing-lora-item">
|
||||
<div class="lora-name">${lora.name}</div>
|
||||
<div class="lora-type">${lora.type || 'lora'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Update target path display
|
||||
this.updateTargetPath();
|
||||
|
||||
// Initialize folder browser after loading data
|
||||
this.initializeFolderBrowser();
|
||||
} catch (error) {
|
||||
console.error('Error in proceedToLocation:', error);
|
||||
console.error('Error in API calls:', error);
|
||||
showToast(error.message, 'error');
|
||||
// Go back to details step on error
|
||||
this.backToDetails();
|
||||
} finally {
|
||||
this.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
backToUpload() {
|
||||
document.getElementById('detailsStep').style.display = 'none';
|
||||
document.getElementById('uploadStep').style.display = 'block';
|
||||
this.showStep('uploadStep');
|
||||
}
|
||||
|
||||
backToDetails() {
|
||||
document.getElementById('locationStep').style.display = 'none';
|
||||
document.getElementById('detailsStep').style.display = 'block';
|
||||
this.showStep('detailsStep');
|
||||
}
|
||||
|
||||
async saveRecipe() {
|
||||
if (!this.recipeName) {
|
||||
showToast('Please enter a recipe name', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// If we're in the location step, we need to download missing LoRAs first
|
||||
if (document.getElementById('locationStep').style.display !== 'none') {
|
||||
const loraRoot = document.getElementById('importLoraRoot').value;
|
||||
const newFolder = document.getElementById('importNewFolder').value.trim();
|
||||
|
||||
if (!loraRoot) {
|
||||
showToast('Please select a LoRA root directory', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct relative path
|
||||
let targetFolder = '';
|
||||
if (this.selectedFolder) {
|
||||
targetFolder = this.selectedFolder;
|
||||
}
|
||||
if (newFolder) {
|
||||
targetFolder = targetFolder ?
|
||||
`${targetFolder}/${newFolder}` : newFolder;
|
||||
}
|
||||
|
||||
// Show loading with progress bar for download
|
||||
this.loadingManager.show('Downloading missing LoRAs...', 0);
|
||||
|
||||
// Setup WebSocket for progress updates
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.status === 'progress') {
|
||||
this.loadingManager.setProgress(data.progress);
|
||||
this.loadingManager.setStatus(`Downloading: ${data.progress}%`);
|
||||
}
|
||||
};
|
||||
|
||||
// Download missing LoRAs
|
||||
const downloadResponse = await fetch('/api/recipes/download-missing-loras', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
loras: this.missingLoras,
|
||||
lora_root: loraRoot,
|
||||
relative_path: targetFolder
|
||||
})
|
||||
});
|
||||
|
||||
if (!downloadResponse.ok) {
|
||||
throw new Error(await downloadResponse.text());
|
||||
}
|
||||
|
||||
// Update missing LoRAs with downloaded paths
|
||||
const downloadResult = await downloadResponse.json();
|
||||
this.recipeData.loras = this.recipeData.loras.map(lora => {
|
||||
const downloaded = downloadResult.downloaded.find(d => d.id === lora.id);
|
||||
if (downloaded) {
|
||||
return {
|
||||
...lora,
|
||||
existsLocally: true,
|
||||
localPath: downloaded.localPath
|
||||
};
|
||||
}
|
||||
return lora;
|
||||
});
|
||||
}
|
||||
|
||||
// Now save the recipe
|
||||
// First save the recipe
|
||||
this.loadingManager.showSimpleLoading('Saving recipe...');
|
||||
|
||||
// Create form data for recipe save
|
||||
// Create form data for save request
|
||||
const formData = new FormData();
|
||||
formData.append('image', this.recipeImage);
|
||||
formData.append('name', this.recipeName);
|
||||
formData.append('tags', JSON.stringify(this.recipeTags));
|
||||
formData.append('recipe_data', JSON.stringify(this.recipeData));
|
||||
formData.append('metadata', JSON.stringify(this.recipeData));
|
||||
|
||||
// Save recipe
|
||||
const saveResponse = await fetch('/api/recipes/save', {
|
||||
// Send save request
|
||||
const response = await fetch('/api/recipes/save', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error(await saveResponse.text());
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save recipe');
|
||||
}
|
||||
|
||||
showToast('Recipe saved successfully', 'success');
|
||||
// Show success message for recipe save
|
||||
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
|
||||
|
||||
// Check if we need to download LoRAs
|
||||
if (this.missingLoras.length > 0) {
|
||||
// For download, we need to validate the target path
|
||||
const loraRoot = document.getElementById('importLoraRoot')?.value;
|
||||
if (!loraRoot) {
|
||||
throw new Error('Please select a LoRA root directory');
|
||||
}
|
||||
|
||||
// Build target path
|
||||
let targetPath = loraRoot;
|
||||
if (this.selectedFolder) {
|
||||
targetPath += '/' + this.selectedFolder;
|
||||
}
|
||||
|
||||
const newFolder = document.getElementById('importNewFolder')?.value?.trim();
|
||||
if (newFolder) {
|
||||
targetPath += '/' + newFolder;
|
||||
}
|
||||
|
||||
// Set up WebSocket for progress updates
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
||||
|
||||
// Download missing LoRAs sequentially
|
||||
this.loadingManager.show('Downloading LoRAs...', 0);
|
||||
|
||||
let completedDownloads = 0;
|
||||
for (let i = 0; i < this.missingLoras.length; i++) {
|
||||
const lora = this.missingLoras[i];
|
||||
|
||||
// Update overall progress
|
||||
this.loadingManager.setStatus(`Downloading LoRA ${i+1}/${this.missingLoras.length}: ${lora.name}`);
|
||||
|
||||
// Set up progress tracking for current download
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.status === 'progress') {
|
||||
// Calculate overall progress: completed files + current file progress
|
||||
const overallProgress = Math.floor(
|
||||
(completedDownloads + data.progress/100) / this.missingLoras.length * 100
|
||||
);
|
||||
this.loadingManager.setProgress(overallProgress);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Download the LoRA
|
||||
const response = await fetch('/api/download-lora', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
download_url: lora.downloadUrl,
|
||||
lora_root: loraRoot,
|
||||
relative_path: targetPath.replace(loraRoot + '/', '')
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`Failed to download LoRA ${lora.name}: ${errorText}`);
|
||||
// Continue with next download
|
||||
} else {
|
||||
completedDownloads++;
|
||||
}
|
||||
} catch (downloadError) {
|
||||
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
|
||||
// Continue with next download
|
||||
}
|
||||
}
|
||||
|
||||
// Close WebSocket
|
||||
ws.close();
|
||||
|
||||
// Show final completion message
|
||||
if (completedDownloads === this.missingLoras.length) {
|
||||
showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success');
|
||||
} else {
|
||||
showToast(`Downloaded ${completedDownloads} of ${this.missingLoras.length} LoRAs`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal and reload recipes
|
||||
modalManager.closeModal('importModal');
|
||||
|
||||
// Reload recipes
|
||||
window.location.reload();
|
||||
// Refresh the recipe list if needed
|
||||
if (typeof refreshRecipes === 'function') {
|
||||
refreshRecipes();
|
||||
} else {
|
||||
// Fallback to reloading the page
|
||||
resetAndReload();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving recipe:', error);
|
||||
showToast(error.message, 'error');
|
||||
} finally {
|
||||
this.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Add new method to handle folder selection
|
||||
async initializeFolderBrowser() {
|
||||
initializeFolderBrowser() {
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (!folderBrowser) return;
|
||||
|
||||
// Cleanup existing handler if any
|
||||
this.cleanupFolderBrowser();
|
||||
|
||||
try {
|
||||
// Get the selected root
|
||||
const loraRoot = document.getElementById('importLoraRoot').value;
|
||||
if (!loraRoot) {
|
||||
folderBrowser.innerHTML = '<div class="empty-folder">Please select a LoRA root directory</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch folders for the selected root
|
||||
const response = await fetch(`/api/folders?root=${encodeURIComponent(loraRoot)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch folders');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Display folders
|
||||
if (data.folders && data.folders.length > 0) {
|
||||
folderBrowser.innerHTML = data.folders.map(folder => `
|
||||
<div class="folder-item" data-folder="${folder}">
|
||||
<i class="fas fa-folder"></i> ${folder}
|
||||
</div>
|
||||
`).join('');
|
||||
// Create new handler
|
||||
this.folderClickHandler = (event) => {
|
||||
const folderItem = event.target.closest('.folder-item');
|
||||
if (!folderItem) return;
|
||||
|
||||
if (folderItem.classList.contains('selected')) {
|
||||
folderItem.classList.remove('selected');
|
||||
this.selectedFolder = '';
|
||||
} else {
|
||||
folderBrowser.innerHTML = '<div class="empty-folder">No folders found</div>';
|
||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||
f.classList.remove('selected'));
|
||||
folderItem.classList.add('selected');
|
||||
this.selectedFolder = folderItem.dataset.folder;
|
||||
}
|
||||
|
||||
// Create new handler
|
||||
this.folderClickHandler = (event) => {
|
||||
const folderItem = event.target.closest('.folder-item');
|
||||
if (!folderItem) return;
|
||||
// Update path display after folder selection
|
||||
this.updateTargetPath();
|
||||
};
|
||||
|
||||
if (folderItem.classList.contains('selected')) {
|
||||
folderItem.classList.remove('selected');
|
||||
this.selectedFolder = '';
|
||||
} else {
|
||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||
f.classList.remove('selected'));
|
||||
folderItem.classList.add('selected');
|
||||
this.selectedFolder = folderItem.dataset.folder;
|
||||
}
|
||||
|
||||
// Update path display after folder selection
|
||||
this.updateTargetPath();
|
||||
};
|
||||
|
||||
// Add the new handler
|
||||
folderBrowser.addEventListener('click', this.folderClickHandler);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing folder browser:', error);
|
||||
folderBrowser.innerHTML = `<div class="error-message">Error: ${error.message}</div>`;
|
||||
}
|
||||
// Add the new handler
|
||||
folderBrowser.addEventListener('click', this.folderClickHandler);
|
||||
|
||||
// Add event listeners for path updates
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
const newFolder = document.getElementById('importNewFolder');
|
||||
|
||||
loraRoot.addEventListener('change', async () => {
|
||||
await this.initializeFolderBrowser();
|
||||
this.updateTargetPath();
|
||||
});
|
||||
|
||||
newFolder.addEventListener('input', this.updateTargetPath);
|
||||
if (loraRoot) loraRoot.addEventListener('change', this.updateTargetPath);
|
||||
if (newFolder) newFolder.addEventListener('input', this.updateTargetPath);
|
||||
|
||||
console.log('Initializing folder browser...');
|
||||
// Update initial path
|
||||
this.updateTargetPath();
|
||||
}
|
||||
@@ -541,15 +560,14 @@ export class ImportManager {
|
||||
if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
|
||||
}
|
||||
|
||||
// Add new method to update target path
|
||||
updateTargetPath() {
|
||||
const pathDisplay = document.getElementById('importTargetPathDisplay');
|
||||
if (!pathDisplay) return;
|
||||
|
||||
const loraRoot = document.getElementById('importLoraRoot')?.value || '';
|
||||
const newFolder = document.getElementById('importNewFolder')?.value.trim() || '';
|
||||
const newFolder = document.getElementById('importNewFolder')?.value?.trim() || '';
|
||||
|
||||
let fullPath = loraRoot || 'Select a LoRA root directory';
|
||||
let fullPath = loraRoot || 'Select a LoRA root directory';
|
||||
|
||||
if (loraRoot) {
|
||||
if (this.selectedFolder) {
|
||||
@@ -559,7 +577,41 @@ export class ImportManager {
|
||||
fullPath += '/' + newFolder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pathDisplay.innerHTML = `<span class="path-text">${fullPath}</span>`;
|
||||
}
|
||||
|
||||
showStep(stepId) {
|
||||
// 隐藏所有步骤
|
||||
document.querySelectorAll('.import-step').forEach(step => {
|
||||
step.style.display = 'none';
|
||||
});
|
||||
|
||||
// 显示目标步骤
|
||||
const targetStep = document.getElementById(stepId);
|
||||
if (targetStep) {
|
||||
// 强制显示目标步骤 - 使用 !important 覆盖任何其他CSS规则
|
||||
targetStep.setAttribute('style', 'display: block !important');
|
||||
|
||||
// 调试信息
|
||||
console.log(`Showing step: ${stepId}`);
|
||||
const rect = targetStep.getBoundingClientRect();
|
||||
console.log('Step dimensions:', {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
visible: rect.width > 0 && rect.height > 0
|
||||
});
|
||||
|
||||
// 强制重新计算布局
|
||||
targetStep.offsetHeight;
|
||||
|
||||
// 滚动模态内容到顶部
|
||||
const modalContent = document.querySelector('#importModal .modal-content');
|
||||
if (modalContent) {
|
||||
modalContent.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +1,102 @@
|
||||
<div id="importModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Import Recipe</h2>
|
||||
<span class="close-modal" onclick="modalManager.closeModal('importModal')">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Step 1: Upload Image -->
|
||||
<div id="uploadStep" class="import-step">
|
||||
<p>Upload an image with LoRA metadata to import as a recipe.</p>
|
||||
|
||||
<button class="close" onclick="modalManager.closeModal('importModal')">×</button>
|
||||
<h2>Import Recipe</h2>
|
||||
|
||||
<!-- Step 1: Upload Image -->
|
||||
<div class="import-step" id="uploadStep">
|
||||
<p>Upload an image with LoRA metadata to import as a recipe.</p>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="recipeImageUpload">Select Image:</label>
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="recipeImageUpload" accept="image/*" onchange="importManager.handleImageUpload(event)">
|
||||
<div class="file-input-button">
|
||||
<i class="fas fa-upload"></i> Select Image
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="uploadError" class="error-message"></div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="secondary-btn" onclick="modalManager.closeModal('importModal')">Cancel</button>
|
||||
</div>
|
||||
<div class="error-message" id="uploadError"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Recipe Details -->
|
||||
<div id="detailsStep" class="import-step" style="display: none;">
|
||||
<div class="recipe-details-layout">
|
||||
<div class="recipe-image-container">
|
||||
<div id="recipeImagePreview" class="recipe-image"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="secondary-btn" onclick="modalManager.closeModal('importModal')">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Recipe Details -->
|
||||
<div class="import-step" id="detailsStep" style="display: none;">
|
||||
<div class="recipe-details-layout">
|
||||
<div class="recipe-image-container">
|
||||
<div id="recipeImagePreview" class="recipe-image"></div>
|
||||
</div>
|
||||
|
||||
<div class="recipe-form-container">
|
||||
<div class="input-group">
|
||||
<label for="recipeName">Recipe Name</label>
|
||||
<input type="text" id="recipeName" placeholder="Enter recipe name"
|
||||
onchange="importManager.handleRecipeNameChange(event)">
|
||||
</div>
|
||||
|
||||
<div class="recipe-form-container">
|
||||
<div class="recipe-name-container">
|
||||
<label for="recipeName">Recipe Name</label>
|
||||
<input type="text" id="recipeName" placeholder="Enter recipe name"
|
||||
onchange="importManager.handleRecipeNameChange(event)">
|
||||
<div class="input-group">
|
||||
<label>Tags (optional)</label>
|
||||
<div class="tag-input-container">
|
||||
<input type="text" id="tagInput" placeholder="Add a tag">
|
||||
<button class="secondary-btn" onclick="importManager.addTag()">
|
||||
<i class="fas fa-plus"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tags-section">
|
||||
<label>Tags (optional)</label>
|
||||
<div class="tag-input-container">
|
||||
<input type="text" id="tagInput" placeholder="Add a tag">
|
||||
<button class="secondary-btn" onclick="importManager.addTag()">
|
||||
<i class="fas fa-plus"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
<div id="tagsContainer" class="tags-container">
|
||||
<div class="empty-tags">No tags added</div>
|
||||
</div>
|
||||
<div id="tagsContainer" class="tags-container">
|
||||
<div class="empty-tags">No tags added</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loras-section">
|
||||
<label>LoRAs in this Recipe <span id="loraCountInfo" class="lora-count-info">(0/0 in library)</span></label>
|
||||
<div id="lorasList" class="loras-list">
|
||||
<!-- LoRAs will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="secondary-btn" onclick="importManager.backToUpload()">Back</button>
|
||||
<button class="primary-btn" onclick="importManager.proceedFromDetails()">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Download Location (if needed) -->
|
||||
<div id="locationStep" class="import-step" style="display: none;">
|
||||
<p>The following LoRAs are not in your library and need to be downloaded:</p>
|
||||
|
||||
<div id="missingLorasList" class="missing-loras-list">
|
||||
<!-- Missing LoRAs will be populated here -->
|
||||
<div class="input-group">
|
||||
<label>LoRAs in this Recipe <span id="loraCountInfo" class="lora-count-info">(0/0 in library)</span></label>
|
||||
<div id="lorasList" class="loras-list">
|
||||
<!-- LoRAs will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="secondary-btn" onclick="importManager.backToUpload()">Back</button>
|
||||
<button class="primary-btn" onclick="importManager.proceedFromDetails()">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Download Location (if needed) -->
|
||||
<div class="import-step" id="locationStep" style="display: none;">
|
||||
<div class="location-selection">
|
||||
<!-- Move path preview to top -->
|
||||
<div class="path-preview">
|
||||
<label>Download Location Preview:</label>
|
||||
<div class="path-display" id="importTargetPathDisplay">
|
||||
<span class="path-text">Select a LoRA root directory</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Select LoRA Root:</label>
|
||||
<select id="importLoraRoot"></select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="importLoraRoot">Select LoRA Root Directory</label>
|
||||
<select id="importLoraRoot">
|
||||
<!-- LoRA roots will be populated here -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Select Folder (optional)</label>
|
||||
<div id="importFolderBrowser" class="folder-browser">
|
||||
<label>Target Folder:</label>
|
||||
<div class="folder-browser" id="importFolderBrowser">
|
||||
<!-- Folders will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="importNewFolder">Create New Folder (optional)</label>
|
||||
<label for="importNewFolder">New Folder (optional):</label>
|
||||
<input type="text" id="importNewFolder" placeholder="Enter folder name">
|
||||
</div>
|
||||
|
||||
<div class="path-preview">
|
||||
<label>Download Location:</label>
|
||||
<div id="importTargetPathDisplay" class="path-display"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="secondary-btn" onclick="importManager.backToDetails()">Back</button>
|
||||
<button class="primary-btn" onclick="importManager.saveRecipe()">Download & Save Recipe</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="secondary-btn" onclick="importManager.backToDetails()">Back</button>
|
||||
<button class="primary-btn" onclick="importManager.saveRecipe()">Download & Save Recipe</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user