refactor: streamline example images download functionality and UI updates

This commit is contained in:
Will Miao
2025-04-30 13:20:44 +08:00
parent cb876cf77e
commit 26d9a9caa6
7 changed files with 235 additions and 198 deletions

View File

@@ -48,9 +48,6 @@ class MiscRoutes:
app.router.add_post('/api/pause-example-images', MiscRoutes.pause_example_images)
app.router.add_post('/api/resume-example-images', MiscRoutes.resume_example_images)
# Folder browser route
app.router.add_post('/api/browse-folder', MiscRoutes.browse_folder)
@staticmethod
async def update_settings(request):
"""Update application settings"""
@@ -67,17 +64,10 @@ class MiscRoutes:
'error': f"Path does not exist: {value}"
})
# Add static route for example images path if it changed
# Path changed - server restart required for new path to take effect
old_path = settings.get('example_images_path')
if old_path != value:
# We need to add the new static route
# Note: we can't remove old routes in aiohttp, but we can create a new one
app = request.app
try:
app.router.add_static('/example_images_static', value)
logger.info(f"Added static route for example images: /example_images_static -> {value}")
except Exception as e:
logger.warning(f"Could not add static route for example images: {str(e)}")
logger.info(f"Example images path changed to {value} - server restart required")
# Save to settings
settings.set(key, value)
@@ -174,7 +164,7 @@ class MiscRoutes:
output_dir = data.get('output_dir')
optimize = data.get('optimize', True)
model_types = data.get('model_types', ['lora', 'checkpoint'])
delay = float(data.get('delay', 1.0))
delay = float(data.get('delay', 0.2))
if not output_dir:
return web.json_response({
@@ -494,55 +484,3 @@ class MiscRoutes:
# Set download status to not downloading
is_downloading = False
@staticmethod
async def browse_folder(request):
"""Open a folder browser dialog and return the selected path
Expects a JSON body with:
{
"initial_dir": "string" // Optional initial directory
}
"""
try:
# Parse the request body
data = await request.json()
initial_dir = data.get('initial_dir', '')
# If initial_dir doesn't exist, use a default path
if initial_dir and not os.path.isdir(initial_dir):
initial_dir = ''
# Create a hidden root window for the dialog
root = tk.Tk()
root.withdraw() # Hide the root window
# Show the folder selection dialog
folder_path = filedialog.askdirectory(
initialdir=initial_dir,
title="Select folder for example images"
)
# Destroy the root window
root.destroy()
if folder_path:
# Convert to proper path format for the OS
folder_path = os.path.normpath(folder_path)
return web.json_response({
'success': True,
'folder': folder_path
})
else:
return web.json_response({
'success': False,
'error': 'No folder selected'
})
except Exception as e:
logger.error(f"Failed to browse folder: {e}", exc_info=True)
return web.json_response({
'success': False,
'error': str(e)
}, status=500)

View File

@@ -190,14 +190,6 @@
border-color: var(--lora-border);
}
/* Add disabled button styles */
.primary-btn.disabled {
background-color: var(--border-color);
color: var(--text-color);
opacity: 0.7;
cursor: not-allowed;
}
/* Enhance the local badge to make it more noticeable */
.version-item.exists-locally {
background: oklch(var(--lora-accent) / 0.05);

View File

@@ -502,7 +502,7 @@ input:checked + .toggle-slider:before {
gap: var(--space-2);
}
.download-buttons .primary-btn {
.primary-btn {
display: flex;
align-items: center;
gap: 8px;
@@ -516,30 +516,80 @@ input:checked + .toggle-slider:before {
font-size: 0.95em;
}
.download-buttons .primary-btn:hover {
background-color: oklch(var(--lora-accent) / 0.9);
.primary-btn:hover {
background-color: oklch(from var(--lora-accent) l c h / 85%);
color: var(--lora-text);
}
.path-browser-control {
/* Secondary button styles */
.secondary-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: var(--card-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
cursor: pointer;
transition: all 0.2s;
font-size: 0.95em;
}
.secondary-btn:hover {
background-color: var(--border-color);
color: var(--text-color);
}
/* Disabled button styles */
.primary-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: var(--lora-accent);
color: var(--lora-text);
pointer-events: none;
}
.secondary-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Dark theme specific button adjustments */
[data-theme="dark"] .primary-btn:hover {
background-color: oklch(from var(--lora-accent) l c h / 75%);
}
[data-theme="dark"] .secondary-btn {
background-color: var(--lora-surface);
}
[data-theme="dark"] .secondary-btn:hover {
background-color: oklch(35% 0.02 256 / 0.98);
}
.primary-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.path-control {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
}
.path-browser-control input[type="text"] {
.path-control input[type="text"] {
flex: 1;
}
.browse-btn {
white-space: nowrap;
padding: 6px 12px;
background-color: var(--button-bg);
padding: 6px 10px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
}
.browse-btn:hover {
background-color: var(--button-hover-bg);
background-color: var(--lora-surface);
color: var(--text-color);
font-size: 0.95em;
height: 32px;
}
.primary-btn.disabled {

View File

@@ -88,7 +88,8 @@
color: var(--text-color);
}
.progress-container {
/* Use specific selectors to avoid conflicts with loading.css */
.progress-panel .progress-container {
width: 100%;
background-color: var(--lora-border);
border-radius: 4px;
@@ -96,7 +97,7 @@
height: var(--space-1);
}
.progress-bar {
.progress-panel .progress-bar {
width: 0%;
height: 100%;
background-color: var(--lora-accent);

View File

@@ -8,10 +8,14 @@ class ExampleImagesManager {
this.isPaused = false;
this.progressUpdateInterval = null;
this.startTime = null;
this.progressPanel = document.getElementById('exampleImagesProgress');
this.progressPanel = null;
// Wait for DOM before initializing event listeners
document.addEventListener('DOMContentLoaded', () => this.initEventListeners());
document.addEventListener('DOMContentLoaded', () => {
this.initEventListeners();
// Initialize progress panel reference
this.progressPanel = document.getElementById('exampleImagesProgress');
});
// Initialize download path field
this.initializePathOptions();
@@ -22,14 +26,9 @@ class ExampleImagesManager {
// Initialize event listeners for buttons
initEventListeners() {
const startBtn = document.getElementById('startExampleDownloadBtn');
if (startBtn) {
startBtn.onclick = () => this.startDownload();
}
const resumeBtn = document.getElementById('resumeExampleDownloadBtn');
if (resumeBtn) {
resumeBtn.onclick = () => this.resumeDownload();
const downloadBtn = document.getElementById('exampleImagesDownloadBtn');
if (downloadBtn) {
downloadBtn.onclick = () => this.handleDownloadButton();
}
}
@@ -49,12 +48,6 @@ class ExampleImagesManager {
this.updateDownloadButtonState(false);
}
// Add event listener to the browse button
const browseButton = document.getElementById('browseExampleImagesPath');
if (browseButton) {
browseButton.addEventListener('click', () => this.browseFolderDialog());
}
// Add event listener to validate path input
pathInput.addEventListener('input', async () => {
const hasPath = pathInput.value.trim() !== '';
@@ -98,47 +91,29 @@ class ExampleImagesManager {
// Method to update download button state
updateDownloadButtonState(enabled) {
const startBtn = document.getElementById('startExampleDownloadBtn');
if (startBtn) {
const downloadBtn = document.getElementById('exampleImagesDownloadBtn');
if (downloadBtn) {
if (enabled) {
startBtn.classList.remove('disabled');
startBtn.disabled = false;
downloadBtn.classList.remove('disabled');
downloadBtn.disabled = false;
} else {
startBtn.classList.add('disabled');
startBtn.disabled = true;
downloadBtn.classList.add('disabled');
downloadBtn.disabled = true;
}
}
}
// Method to open folder browser dialog
async browseFolderDialog() {
try {
const response = await fetch('/api/browse-folder', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
initial_dir: getStorageItem('example_images_path', '')
})
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (data.success && data.folder) {
const pathInput = document.getElementById('exampleImagesPath');
pathInput.value = data.folder;
setStorageItem('example_images_path', data.folder);
this.updateDownloadButtonState(true);
showToast('Example images path updated successfully', 'success');
}
} catch (error) {
console.error('Failed to browse folder:', error);
showToast('Failed to browse folder. Please ensure the server supports this feature.', 'error');
// Method to handle download button click based on current state
async handleDownloadButton() {
if (this.isDownloading && this.isPaused) {
// If download is paused, resume it
this.resumeDownload();
} else if (!this.isDownloading) {
// If no download in progress, start a new one
this.startDownload();
} else {
// If download is in progress, show info toast
showToast('Download already in progress', 'info');
}
}
@@ -151,13 +126,27 @@ class ExampleImagesManager {
this.isDownloading = data.is_downloading;
this.isPaused = data.status.status === 'paused';
// Update download button text based on status
this.updateDownloadButtonText();
if (this.isDownloading) {
this.updateUI(data.status);
this.showProgressPanel();
// Ensure progress panel exists before updating UI
if (!this.progressPanel) {
this.progressPanel = document.getElementById('exampleImagesProgress');
}
// Start the progress update interval if downloading
if (!this.progressUpdateInterval) {
this.startProgressUpdates();
if (this.progressPanel) {
this.updateUI(data.status);
this.showProgressPanel();
// Start the progress update interval if downloading
if (!this.progressUpdateInterval) {
this.startProgressUpdates();
}
} else {
console.warn('Progress panel not found, will retry on next update');
// Set a shorter timeout to try again
setTimeout(() => this.checkDownloadStatus(), 500);
}
}
}
@@ -166,6 +155,18 @@ class ExampleImagesManager {
}
}
// Update download button text based on current state
updateDownloadButtonText() {
const btnTextElement = document.getElementById('exampleDownloadBtnText');
if (btnTextElement) {
if (this.isDownloading && this.isPaused) {
btnTextElement.textContent = "Resume";
} else if (!this.isDownloading) {
btnTextElement.textContent = "Download";
}
}
}
async startDownload() {
if (this.isDownloading) {
showToast('Download already in progress', 'warning');
@@ -176,7 +177,7 @@ class ExampleImagesManager {
const outputDir = document.getElementById('exampleImagesPath').value || '';
if (!outputDir) {
showToast('Please select a download location first', 'warning');
showToast('Please enter a download location first', 'warning');
return;
}
@@ -203,11 +204,9 @@ class ExampleImagesManager {
this.updateUI(data.status);
this.showProgressPanel();
this.startProgressUpdates();
this.updateDownloadButtonText();
showToast('Example images download started', 'success');
// Hide the start button, show resume button
document.getElementById('startExampleDownloadBtn').style.display = 'none';
// Close settings modal
modalManager.closeModal('settingsModal');
} else {
@@ -236,10 +235,8 @@ class ExampleImagesManager {
document.getElementById('downloadStatusText').textContent = 'Paused';
document.getElementById('pauseExampleDownloadBtn').innerHTML = '<i class="fas fa-play"></i>';
document.getElementById('pauseExampleDownloadBtn').onclick = () => this.resumeDownload();
this.updateDownloadButtonText();
showToast('Download paused', 'info');
// Show resume button in settings too
document.getElementById('resumeExampleDownloadBtn').style.display = 'block';
} else {
showToast(data.error || 'Failed to pause download', 'error');
}
@@ -266,10 +263,8 @@ class ExampleImagesManager {
document.getElementById('downloadStatusText').textContent = 'Downloading';
document.getElementById('pauseExampleDownloadBtn').innerHTML = '<i class="fas fa-pause"></i>';
document.getElementById('pauseExampleDownloadBtn').onclick = () => this.pauseDownload();
this.updateDownloadButtonText();
showToast('Download resumed', 'success');
// Hide resume button in settings
document.getElementById('resumeExampleDownloadBtn').style.display = 'none';
} else {
showToast(data.error || 'Failed to resume download', 'error');
}
@@ -298,6 +293,10 @@ class ExampleImagesManager {
if (data.success) {
this.isDownloading = data.is_downloading;
this.isPaused = data.status.status === 'paused';
// Update download button text
this.updateDownloadButtonText();
if (this.isDownloading) {
this.updateUI(data.status);
@@ -313,10 +312,6 @@ class ExampleImagesManager {
} else if (data.status.status === 'error') {
showToast('Example images download failed', 'error');
}
// Reset UI
document.getElementById('startExampleDownloadBtn').style.display = 'block';
document.getElementById('resumeExampleDownloadBtn').style.display = 'none';
}
}
} catch (error) {
@@ -325,21 +320,38 @@ class ExampleImagesManager {
}
updateUI(status) {
// Ensure progress panel exists
if (!this.progressPanel) {
this.progressPanel = document.getElementById('exampleImagesProgress');
if (!this.progressPanel) {
console.error('Progress panel element not found in DOM');
return;
}
}
// Update status text
const statusText = document.getElementById('downloadStatusText');
statusText.textContent = this.getStatusText(status.status);
if (statusText) {
statusText.textContent = this.getStatusText(status.status);
}
// Update progress counts and bar
const progressCounts = document.getElementById('downloadProgressCounts');
progressCounts.textContent = `${status.completed}/${status.total}`;
if (progressCounts) {
progressCounts.textContent = `${status.completed}/${status.total}`;
}
const progressBar = document.getElementById('downloadProgressBar');
const progressPercent = status.total > 0 ? (status.completed / status.total) * 100 : 0;
progressBar.style.width = `${progressPercent}%`;
if (progressBar) {
const progressPercent = status.total > 0 ? (status.completed / status.total) * 100 : 0;
progressBar.style.width = `${progressPercent}%`;
}
// Update current model
const currentModel = document.getElementById('currentModelName');
currentModel.textContent = status.current_model || '-';
if (currentModel) {
currentModel.textContent = status.current_model || '-';
}
// Update time stats
this.updateTimeStats(status);
@@ -348,14 +360,21 @@ class ExampleImagesManager {
this.updateErrors(status);
// Update pause/resume button
if (status.status === 'paused') {
document.getElementById('pauseExampleDownloadBtn').innerHTML = '<i class="fas fa-play"></i>';
document.getElementById('pauseExampleDownloadBtn').onclick = () => this.resumeDownload();
document.getElementById('resumeExampleDownloadBtn').style.display = 'block';
} else {
document.getElementById('pauseExampleDownloadBtn').innerHTML = '<i class="fas fa-pause"></i>';
document.getElementById('pauseExampleDownloadBtn').onclick = () => this.pauseDownload();
document.getElementById('resumeExampleDownloadBtn').style.display = 'none';
const pauseBtn = document.getElementById('pauseExampleDownloadBtn');
const resumeBtn = document.getElementById('resumeExampleDownloadBtn');
if (pauseBtn) {
if (status.status === 'paused') {
pauseBtn.innerHTML = '<i class="fas fa-play"></i>';
pauseBtn.onclick = () => this.resumeDownload();
} else {
pauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
pauseBtn.onclick = () => this.pauseDownload();
}
}
if (resumeBtn) {
resumeBtn.style.display = status.status === 'paused' ? 'block' : 'none';
}
}
@@ -363,6 +382,8 @@ class ExampleImagesManager {
const elapsedTime = document.getElementById('elapsedTime');
const remainingTime = document.getElementById('remainingTime');
if (!elapsedTime || !remainingTime) return;
// Calculate elapsed time
let elapsed;
if (status.start_time) {
@@ -389,6 +410,8 @@ class ExampleImagesManager {
const errorContainer = document.getElementById('downloadErrorContainer');
const errorList = document.getElementById('downloadErrors');
if (!errorContainer || !errorList) return;
if (status.errors && status.errors.length > 0) {
// Show only the last 3 errors
const recentErrors = status.errors.slice(-3);
@@ -425,22 +448,41 @@ class ExampleImagesManager {
}
showProgressPanel() {
// Ensure progress panel exists
if (!this.progressPanel) {
this.progressPanel = document.getElementById('exampleImagesProgress');
if (!this.progressPanel) {
console.error('Progress panel element not found in DOM');
return;
}
}
this.progressPanel.classList.add('visible');
}
hideProgressPanel() {
if (!this.progressPanel) {
this.progressPanel = document.getElementById('exampleImagesProgress');
if (!this.progressPanel) return;
}
this.progressPanel.classList.remove('visible');
}
toggleProgressPanel() {
if (!this.progressPanel) {
this.progressPanel = document.getElementById('exampleImagesProgress');
if (!this.progressPanel) return;
}
this.progressPanel.classList.toggle('collapsed');
// Update icon
const icon = document.querySelector('#collapseProgressBtn i');
if (this.progressPanel.classList.contains('collapsed')) {
icon.className = 'fas fa-chevron-up';
} else {
icon.className = 'fas fa-chevron-down';
if (icon) {
if (this.progressPanel.classList.contains('collapsed')) {
icon.className = 'fas fa-chevron-up';
} else {
icon.className = 'fas fa-chevron-down';
}
}
}
}

View File

@@ -135,15 +135,15 @@
<div class="setting-info">
<label for="exampleImagesPath">Download Location</label>
</div>
<div class="setting-control path-browser-control">
<input type="text" id="exampleImagesPath" placeholder="Select a folder for example images" />
<button id="browseExampleImagesPath" class="browse-btn">
<i class="fas fa-folder-open"></i> Browse
<div class="setting-control path-control">
<input type="text" id="exampleImagesPath" placeholder="Enter folder path for example images" />
<button id="exampleImagesDownloadBtn" class="primary-btn">
<i class="fas fa-download"></i> <span id="exampleDownloadBtnText">Download</span>
</button>
</div>
</div>
<div class="input-help">
Choose where to save example images downloaded from Civitai
Enter the folder path where example images from Civitai will be saved
</div>
</div>
@@ -164,17 +164,6 @@
Optimize example images to reduce file size and improve loading speed
</div>
</div>
<div class="setting-item">
<div class="setting-row download-buttons">
<button id="startExampleDownloadBtn" class="primary-btn disabled">
<i class="fas fa-download"></i> Download Example Images
</button>
<button id="resumeExampleDownloadBtn" class="primary-btn" style="display: none;">
<i class="fas fa-play"></i> Resume Download
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -5,10 +5,10 @@
<i class="fas fa-images"></i> Example Images Download
</div>
<div class="progress-panel-actions">
<button id="pauseExampleDownloadBtn" class="icon-button" onclick="exampleImagesManager.pauseDownload()">
<button id="pauseExampleDownloadBtn" class="icon-button">
<i class="fas fa-pause"></i>
</button>
<button id="collapseProgressBtn" class="icon-button" onclick="exampleImagesManager.toggleProgressPanel()">
<button id="collapseProgressBtn" class="icon-button">
<i class="fas fa-chevron-down"></i>
</button>
</div>
@@ -45,4 +45,29 @@
<div id="downloadErrors" class="error-list"></div>
</div>
</div>
</div>
</div>
<script>
// Initialize button onclick handlers when DOM is fully loaded
document.addEventListener('DOMContentLoaded', function() {
const pauseBtn = document.getElementById('pauseExampleDownloadBtn');
const collapseBtn = document.getElementById('collapseProgressBtn');
// Make window.exampleImagesManager globally available
if (window.exampleImagesManager === undefined && typeof exampleImagesManager !== 'undefined') {
window.exampleImagesManager = exampleImagesManager;
}
if (pauseBtn && window.exampleImagesManager) {
pauseBtn.onclick = () => window.exampleImagesManager.pauseDownload();
}
if (collapseBtn && window.exampleImagesManager) {
collapseBtn.onclick = () => {
if (window.exampleImagesManager.toggleProgressPanel) {
window.exampleImagesManager.toggleProgressPanel();
}
};
}
});
</script>