Enhance settings modal with video autoplay on hover option and improve layout. Fixes https://github.com/willmiao/ComfyUI-Lora-Manager/issues/92

This commit is contained in:
Will Miao
2025-04-09 11:18:30 +08:00
parent 785b2e7287
commit ae0629628e
5 changed files with 341 additions and 73 deletions

View File

@@ -196,7 +196,7 @@ body.modal-open {
}
.settings-modal {
max-width: 500px;
max-width: 650px; /* Further increased from 600px for more space */
}
/* Settings Links */
@@ -266,14 +266,22 @@ body.modal-open {
}
}
/* API key input specific styles */
.api-key-input {
width: 100%; /* Take full width of parent */
position: relative;
display: flex;
align-items: center;
}
.api-key-input input {
padding-right: 40px;
width: 100%;
padding: 6px 40px 6px 10px; /* Add left padding */
height: 32px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
background-color: var(--lora-surface);
color: var(--text-color);
}
.api-key-input .toggle-visibility {
@@ -294,8 +302,10 @@ body.modal-open {
.input-help {
font-size: 0.85em;
color: var(--text-color);
opacity: 0.8;
margin-top: 4px;
opacity: 0.7;
margin-top: 8px; /* Space between control and help */
line-height: 1.4;
width: 100%; /* Full width */
}
/* 统一各个 section 的样式 */
@@ -341,8 +351,8 @@ body.modal-open {
.setting-item {
display: flex;
flex-direction: column;
margin-bottom: var(--space-2);
flex-direction: column; /* Changed to column for help text placement */
margin-bottom: var(--space-3); /* Increased to provide more spacing between items */
padding: var(--space-1);
border-radius: var(--border-radius-xs);
}
@@ -355,35 +365,52 @@ body.modal-open {
background: rgba(255, 255, 255, 0.05);
}
.setting-info {
margin-bottom: var(--space-1);
/* Control row with label and input together */
.setting-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
}
.setting-info {
margin-bottom: 0;
width: 35%; /* Increased from 30% to prevent wrapping */
flex-shrink: 0; /* Prevent shrinking */
}
.setting-info label {
display: block;
margin-bottom: 4px;
font-weight: 500;
margin-bottom: 0;
white-space: nowrap; /* Prevent label wrapping */
}
.setting-control {
width: 100%;
margin-bottom: var(--space-1);
width: 60%; /* Decreased slightly from 65% */
margin-bottom: 0;
display: flex;
justify-content: flex-end; /* Right-align all controls */
}
/* Select Control Styles */
.select-control {
width: 100%;
display: flex;
justify-content: flex-end;
}
.select-control select {
width: 100%;
max-width: 100%; /* Increased from 200px */
padding: 6px 10px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
background-color: var(--lora-surface);
color: var(--text-color);
font-size: 0.95em;
height: 32px;
}
/* Fix dark theme select dropdown text color */
@@ -409,6 +436,7 @@ body.modal-open {
width: 50px;
height: 24px;
cursor: pointer;
margin-left: auto; /* Push to right side */
}
.toggle-switch input {
@@ -458,15 +486,6 @@ input:checked + .toggle-slider:before {
width: 22px;
}
/* Update input help styles */
.input-help {
font-size: 0.85em;
color: var(--text-color);
opacity: 0.7;
margin-top: 4px;
line-height: 1.4;
}
/* Blur effect for NSFW content */
.nsfw-blur {
filter: blur(12px);

View File

@@ -57,10 +57,15 @@ export function createLoraCard(lora) {
nsfwText = "R-rated Content";
}
// Check if autoplayOnHover is enabled for video previews
const autoplayOnHover = state.global.settings.autoplayOnHover || false;
const isVideo = previewUrl.endsWith('.mp4');
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
card.innerHTML = `
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
${previewUrl.endsWith('.mp4') ?
`<video controls autoplay muted loop>
${isVideo ?
`<video ${videoAttrs}>
<source src="${versionedPreviewUrl}" type="video/mp4">
</video>` :
`<img src="${versionedPreviewUrl}" alt="${lora.model_name}">`
@@ -246,6 +251,26 @@ export function createLoraCard(lora) {
actionGroup.style.display = 'none';
});
}
// Add autoplayOnHover handlers for video elements if needed
const videoElement = card.querySelector('video');
if (videoElement && autoplayOnHover) {
const cardPreview = card.querySelector('.card-preview');
// Remove autoplay attribute and pause initially
videoElement.removeAttribute('autoplay');
videoElement.pause();
// Add mouse events to trigger play/pause
cardPreview.addEventListener('mouseenter', () => {
videoElement.play();
});
cardPreview.addEventListener('mouseleave', () => {
videoElement.pause();
videoElement.currentTime = 0;
});
}
return card;
}

View File

@@ -4,7 +4,7 @@ import { LoadingManager } from './managers/LoadingManager.js';
import { modalManager } from './managers/ModalManager.js';
import { updateService } from './managers/UpdateService.js';
import { HeaderManager } from './components/Header.js';
import { SettingsManager } from './managers/SettingsManager.js';
import { settingsManager } from './managers/SettingsManager.js';
import { showToast, initTheme, initBackToTop, lazyLoadImages } from './utils/uiHelpers.js';
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
import { migrateStorageItems } from './utils/storageHelpers.js';
@@ -26,7 +26,7 @@ export class AppCore {
modalManager.initialize();
updateService.initialize();
window.modalManager = modalManager;
window.settingsManager = new SettingsManager();
window.settingsManager = settingsManager;
// Initialize UI components
window.headerManager = new HeaderManager();
@@ -76,4 +76,4 @@ document.addEventListener('DOMContentLoaded', () => {
export const appCore = new AppCore();
// Export common utilities for global use
export { showToast, lazyLoadImages, initializeInfiniteScroll };
export { showToast, lazyLoadImages, initializeInfiniteScroll };

View File

@@ -65,6 +65,12 @@ export class SettingsManager {
// Sync with state (backend will set this via template)
state.global.settings.show_only_sfw = showOnlySFWCheckbox.checked;
}
// Set video autoplay on hover setting
const autoplayOnHoverCheckbox = document.getElementById('autoplayOnHover');
if (autoplayOnHoverCheckbox) {
autoplayOnHoverCheckbox.checked = state.global.settings.autoplayOnHover || false;
}
// Load default lora root
await this.loadLoraRoots();
@@ -120,11 +126,170 @@ export class SettingsManager {
this.isOpen = !this.isOpen;
}
// Auto-save methods for different control types
// For toggle switches
async saveToggleSetting(elementId, settingKey) {
const element = document.getElementById(elementId);
if (!element) return;
const value = element.checked;
// Update frontend state
if (settingKey === 'blur_mature_content') {
state.global.settings.blurMatureContent = value;
} else if (settingKey === 'show_only_sfw') {
state.global.settings.show_only_sfw = value;
} else if (settingKey === 'autoplay_on_hover') {
state.global.settings.autoplayOnHover = value;
} else {
// For any other settings that might be added in the future
state.global.settings[settingKey] = value;
}
// Save to localStorage
setStorageItem('settings', state.global.settings);
try {
// For backend settings, make API call
if (['show_only_sfw', 'blur_mature_content', 'autoplay_on_hover'].includes(settingKey)) {
const payload = {};
payload[settingKey] = value;
const response = await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to save setting');
}
showToast(`${settingKey.replace(/_/g, ' ')} updated`, 'success');
}
// Apply frontend settings immediately
this.applyFrontendSettings();
if (settingKey === 'show_only_sfw') {
this.reloadContent();
}
} catch (error) {
showToast('Failed to save setting: ' + error.message, 'error');
}
}
// For select dropdowns
async saveSelectSetting(elementId, settingKey) {
const element = document.getElementById(elementId);
if (!element) return;
const value = element.value;
// Update frontend state
if (settingKey === 'default_lora_root') {
state.global.settings.default_loras_root = value;
} else {
// For any other settings that might be added in the future
state.global.settings[settingKey] = value;
}
// Save to localStorage
setStorageItem('settings', state.global.settings);
try {
// For backend settings, make API call
const payload = {};
payload[settingKey] = value;
const response = await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to save setting');
}
showToast(`${settingKey.replace(/_/g, ' ')} updated`, 'success');
} catch (error) {
showToast('Failed to save setting: ' + error.message, 'error');
}
}
// For input fields
async saveInputSetting(elementId, settingKey) {
const element = document.getElementById(elementId);
if (!element) return;
const value = element.value;
// For API key or other inputs that need to be saved on backend
try {
// Check if value has changed from existing value
const currentValue = state.global.settings[settingKey] || '';
if (value === currentValue) {
return; // No change, exit early
}
// Update state
state.global.settings[settingKey] = value;
// Save to localStorage if appropriate
if (!settingKey.includes('api_key')) { // Don't store API keys in localStorage for security
setStorageItem('settings', state.global.settings);
}
// For backend settings, make API call
const payload = {};
payload[settingKey] = value;
const response = await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to save setting');
}
showToast(`${settingKey.replace(/_/g, ' ')} updated`, 'success');
} catch (error) {
showToast('Failed to save setting: ' + error.message, 'error');
}
}
async reloadContent() {
if (this.currentPage === 'loras') {
// Reload the loras without updating folders
await resetAndReload(false);
} else if (this.currentPage === 'recipes') {
// Reload the recipes without updating folders
await window.recipeManager.loadRecipes();
} else if (this.currentPage === 'checkpoints') {
// Reload the checkpoints without updating folders
await window.checkpointsManager.loadCheckpoints();
}
}
async saveSettings() {
// Get frontend settings from UI
const blurMatureContent = document.getElementById('blurMatureContent').checked;
const showOnlySFW = document.getElementById('showOnlySFW').checked;
const defaultLoraRoot = document.getElementById('defaultLoraRoot').value;
const autoplayOnHover = document.getElementById('autoplayOnHover').checked;
// Get backend settings
const apiKey = document.getElementById('civitaiApiKey').value;
@@ -133,6 +298,7 @@ export class SettingsManager {
state.global.settings.blurMatureContent = blurMatureContent;
state.global.settings.show_only_sfw = showOnlySFW;
state.global.settings.default_loras_root = defaultLoraRoot;
state.global.settings.autoplayOnHover = autoplayOnHover;
// Save settings to localStorage
setStorageItem('settings', state.global.settings);
@@ -186,11 +352,42 @@ export class SettingsManager {
}
});
// Apply autoplay setting to existing videos in card previews
const autoplayOnHover = state.global.settings.autoplayOnHover;
document.querySelectorAll('.card-preview video').forEach(video => {
// Remove previous event listeners by cloning and replacing the element
const videoParent = video.parentElement;
const videoClone = video.cloneNode(true);
if (autoplayOnHover) {
// Pause video initially and set up mouse events for hover playback
videoClone.removeAttribute('autoplay');
videoClone.pause();
// Add mouse events to the parent element
videoParent.onmouseenter = () => videoClone.play();
videoParent.onmouseleave = () => {
videoClone.pause();
videoClone.currentTime = 0;
};
} else {
// Use default autoplay behavior
videoClone.setAttribute('autoplay', '');
videoParent.onmouseenter = null;
videoParent.onmouseleave = null;
}
videoParent.replaceChild(videoClone, video);
});
// For show_only_sfw, there's no immediate action needed as it affects content loading
// The setting will take effect on next reload
}
}
// Create singleton instance
export const settingsManager = new SettingsManager();
// Helper function for toggling API key visibility
export function toggleApiKeyVisibility(button) {
const input = button.parentElement.querySelector('input');