mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: add favorites filtering functionality across models and UI components
This commit is contained in:
@@ -192,12 +192,43 @@
|
||||
margin-left: var(--space-1);
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
transition: opacity 0.2s;
|
||||
font-size: 0.9em;
|
||||
transition: opacity 0.2s, transform 0.15s ease;
|
||||
font-size: 1.0em; /* Increased from 0.9em for better visibility */
|
||||
width: 16px; /* Fixed width for consistent spacing */
|
||||
height: 16px; /* Fixed height for larger touch target */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
padding: 4px; /* Add padding to increase clickable area */
|
||||
box-sizing: content-box; /* Ensure padding adds to dimensions */
|
||||
position: relative; /* For proper positioning */
|
||||
margin: 0; /* Reset margin */
|
||||
}
|
||||
|
||||
.card-actions i::before {
|
||||
position: absolute; /* Position the icon glyph */
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%); /* Center the icon */
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-1); /* Use gap instead of margin for spacing between icons */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-actions i:hover {
|
||||
opacity: 0.8;
|
||||
opacity: 0.9;
|
||||
transform: scale(1.1);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Style for active favorites */
|
||||
.favorite-active {
|
||||
color: #ffc107 !important; /* Gold color for favorites */
|
||||
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
||||
@@ -81,6 +81,22 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.control-group button.favorite-filter {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.control-group button.favorite-filter.active {
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.control-group button.favorite-filter i {
|
||||
margin-right: 4px;
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
/* Active state for buttons that can be toggled */
|
||||
.control-group button.active {
|
||||
background: var(--lora-accent);
|
||||
|
||||
@@ -45,6 +45,11 @@ export async function loadMoreModels(options = {}) {
|
||||
params.append('folder', pageState.activeFolder);
|
||||
}
|
||||
|
||||
// Add favorites filter parameter if enabled
|
||||
if (pageState.showFavoritesOnly) {
|
||||
params.append('favorites_only', 'true');
|
||||
}
|
||||
|
||||
// Add search parameters if there's a search term
|
||||
if (pageState.filters?.search) {
|
||||
params.append('search', pageState.filters.search);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { showToast, copyToClipboard } from '../utils/uiHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { showCheckpointModal } from './checkpointModal/index.js';
|
||||
import { NSFW_LEVELS } from '../utils/constants.js';
|
||||
import { replaceCheckpointPreview as apiReplaceCheckpointPreview } from '../api/checkpointApi.js';
|
||||
import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata } from '../api/checkpointApi.js';
|
||||
|
||||
export function createCheckpointCard(checkpoint) {
|
||||
const card = document.createElement('div');
|
||||
@@ -17,6 +17,7 @@ export function createCheckpointCard(checkpoint) {
|
||||
card.dataset.from_civitai = checkpoint.from_civitai;
|
||||
card.dataset.notes = checkpoint.notes || '';
|
||||
card.dataset.base_model = checkpoint.base_model || 'Unknown';
|
||||
card.dataset.favorite = checkpoint.favorite ? 'true' : 'false';
|
||||
|
||||
// Store metadata if available
|
||||
if (checkpoint.civitai) {
|
||||
@@ -65,6 +66,9 @@ export function createCheckpointCard(checkpoint) {
|
||||
const isVideo = previewUrl.endsWith('.mp4');
|
||||
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
|
||||
|
||||
// Get favorite status from checkpoint data
|
||||
const isFavorite = checkpoint.favorite === true;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||
${isVideo ?
|
||||
@@ -82,6 +86,9 @@ export function createCheckpointCard(checkpoint) {
|
||||
${checkpoint.base_model}
|
||||
</span>
|
||||
<div class="card-actions">
|
||||
<i class="${isFavorite ? 'fas fa-star favorite-active' : 'far fa-star'}"
|
||||
title="${isFavorite ? 'Remove from favorites' : 'Add to favorites'}">
|
||||
</i>
|
||||
<i class="fas fa-globe"
|
||||
title="${checkpoint.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
|
||||
${!checkpoint.from_civitai ? 'style="opacity: 0.5; cursor: not-allowed"' : ''}>
|
||||
@@ -198,6 +205,39 @@ export function createCheckpointCard(checkpoint) {
|
||||
});
|
||||
}
|
||||
|
||||
// Favorite button click event
|
||||
card.querySelector('.fa-star')?.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
const starIcon = e.currentTarget;
|
||||
const isFavorite = starIcon.classList.contains('fas');
|
||||
const newFavoriteState = !isFavorite;
|
||||
|
||||
try {
|
||||
// Save the new favorite state to the server
|
||||
await saveModelMetadata(card.dataset.filepath, {
|
||||
favorite: newFavoriteState
|
||||
});
|
||||
|
||||
// Update the UI
|
||||
if (newFavoriteState) {
|
||||
starIcon.classList.remove('far');
|
||||
starIcon.classList.add('fas', 'favorite-active');
|
||||
starIcon.title = 'Remove from favorites';
|
||||
card.dataset.favorite = 'true';
|
||||
showToast('Added to favorites', 'success');
|
||||
} else {
|
||||
starIcon.classList.remove('fas', 'favorite-active');
|
||||
starIcon.classList.add('far');
|
||||
starIcon.title = 'Add to favorites';
|
||||
card.dataset.favorite = 'false';
|
||||
showToast('Removed from favorites', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update favorite status:', error);
|
||||
showToast('Failed to update favorite status', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Copy button click event
|
||||
card.querySelector('.fa-copy')?.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { state } from '../state/index.js';
|
||||
import { showLoraModal } from './loraModal/index.js';
|
||||
import { bulkManager } from '../managers/BulkManager.js';
|
||||
import { NSFW_LEVELS } from '../utils/constants.js';
|
||||
import { replacePreview, deleteModel } from '../api/loraApi.js'
|
||||
import { replacePreview, deleteModel, saveModelMetadata } from '../api/loraApi.js'
|
||||
|
||||
export function createLoraCard(lora) {
|
||||
const card = document.createElement('div');
|
||||
@@ -20,6 +20,7 @@ export function createLoraCard(lora) {
|
||||
card.dataset.usage_tips = lora.usage_tips;
|
||||
card.dataset.notes = lora.notes;
|
||||
card.dataset.meta = JSON.stringify(lora.civitai || {});
|
||||
card.dataset.favorite = lora.favorite ? 'true' : 'false';
|
||||
|
||||
// Store tags and model description
|
||||
if (lora.tags && Array.isArray(lora.tags)) {
|
||||
@@ -65,6 +66,9 @@ export function createLoraCard(lora) {
|
||||
const isVideo = previewUrl.endsWith('.mp4');
|
||||
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
|
||||
|
||||
// Get favorite status from the lora data
|
||||
const isFavorite = lora.favorite === true;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||
${isVideo ?
|
||||
@@ -82,6 +86,9 @@ export function createLoraCard(lora) {
|
||||
${lora.base_model}
|
||||
</span>
|
||||
<div class="card-actions">
|
||||
<i class="${isFavorite ? 'fas fa-star favorite-active' : 'far fa-star'}"
|
||||
title="${isFavorite ? 'Remove from favorites' : 'Add to favorites'}">
|
||||
</i>
|
||||
<i class="fas fa-globe"
|
||||
title="${lora.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
|
||||
${!lora.from_civitai ? 'style="opacity: 0.5; cursor: not-allowed"' : ''}>
|
||||
@@ -135,6 +142,7 @@ export function createLoraCard(lora) {
|
||||
base_model: card.dataset.base_model,
|
||||
usage_tips: card.dataset.usage_tips,
|
||||
notes: card.dataset.notes,
|
||||
favorite: card.dataset.favorite === 'true',
|
||||
// Parse civitai metadata from the card's dataset
|
||||
civitai: (() => {
|
||||
try {
|
||||
@@ -198,6 +206,39 @@ export function createLoraCard(lora) {
|
||||
});
|
||||
}
|
||||
|
||||
// Favorite button click event
|
||||
card.querySelector('.fa-star')?.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
const starIcon = e.currentTarget;
|
||||
const isFavorite = starIcon.classList.contains('fas');
|
||||
const newFavoriteState = !isFavorite;
|
||||
|
||||
try {
|
||||
// Save the new favorite state to the server
|
||||
await saveModelMetadata(card.dataset.filepath, {
|
||||
favorite: newFavoriteState
|
||||
});
|
||||
|
||||
// Update the UI
|
||||
if (newFavoriteState) {
|
||||
starIcon.classList.remove('far');
|
||||
starIcon.classList.add('fas', 'favorite-active');
|
||||
starIcon.title = 'Remove from favorites';
|
||||
card.dataset.favorite = 'true';
|
||||
showToast('Added to favorites', 'success');
|
||||
} else {
|
||||
starIcon.classList.remove('fas', 'favorite-active');
|
||||
starIcon.classList.add('far');
|
||||
starIcon.title = 'Add to favorites';
|
||||
card.dataset.favorite = 'false';
|
||||
showToast('Removed from favorites', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update favorite status:', error);
|
||||
showToast('Failed to update favorite status', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Copy button click event
|
||||
card.querySelector('.fa-copy')?.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { PageControls } from './PageControls.js';
|
||||
import { loadMoreLoras, fetchCivitai, resetAndReload, refreshLoras } from '../../api/loraApi.js';
|
||||
import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
|
||||
/**
|
||||
* LorasControls class - Extends PageControls for LoRA-specific functionality
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages
|
||||
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js';
|
||||
import { getStorageItem, setStorageItem } from '../../utils/storageHelpers.js';
|
||||
import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js';
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
|
||||
/**
|
||||
@@ -26,6 +26,9 @@ export class PageControls {
|
||||
// Initialize event listeners
|
||||
this.initEventListeners();
|
||||
|
||||
// Initialize favorites filter button state
|
||||
this.initFavoritesFilter();
|
||||
|
||||
console.log(`PageControls initialized for ${pageType} page`);
|
||||
}
|
||||
|
||||
@@ -121,6 +124,12 @@ export class PageControls {
|
||||
bulkButton.addEventListener('click', () => this.toggleBulkMode());
|
||||
}
|
||||
}
|
||||
|
||||
// Favorites filter button handler
|
||||
const favoriteFilterBtn = document.getElementById('favoriteFilterBtn');
|
||||
if (favoriteFilterBtn) {
|
||||
favoriteFilterBtn.addEventListener('click', () => this.toggleFavoritesOnly());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,4 +394,50 @@ export class PageControls {
|
||||
showToast('Failed to clear custom filter: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the favorites filter button state
|
||||
*/
|
||||
initFavoritesFilter() {
|
||||
const favoriteFilterBtn = document.getElementById('favoriteFilterBtn');
|
||||
if (favoriteFilterBtn) {
|
||||
// Get current state from session storage with page-specific key
|
||||
const storageKey = `show_favorites_only_${this.pageType}`;
|
||||
const showFavoritesOnly = getSessionItem(storageKey, false);
|
||||
|
||||
// Update button state
|
||||
if (showFavoritesOnly) {
|
||||
favoriteFilterBtn.classList.add('active');
|
||||
}
|
||||
|
||||
// Update app state
|
||||
this.pageState.showFavoritesOnly = showFavoritesOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle favorites-only filter and reload models
|
||||
*/
|
||||
async toggleFavoritesOnly() {
|
||||
const favoriteFilterBtn = document.getElementById('favoriteFilterBtn');
|
||||
|
||||
// Toggle the filter state in storage
|
||||
const storageKey = `show_favorites_only_${this.pageType}`;
|
||||
const currentState = this.pageState.showFavoritesOnly;
|
||||
const newState = !currentState;
|
||||
|
||||
// Update session storage
|
||||
setSessionItem(storageKey, newState);
|
||||
|
||||
// Update state
|
||||
this.pageState.showFavoritesOnly = newState;
|
||||
|
||||
// Update button appearance
|
||||
if (favoriteFilterBtn) {
|
||||
favoriteFilterBtn.classList.toggle('active', newState);
|
||||
}
|
||||
|
||||
// Reload models with new filter
|
||||
await this.resetAndReload(true);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ export const state = {
|
||||
bulkMode: false,
|
||||
selectedLoras: new Set(),
|
||||
loraMetadataCache: new Map(),
|
||||
showFavoritesOnly: false,
|
||||
},
|
||||
|
||||
recipes: {
|
||||
@@ -61,7 +62,8 @@ export const state = {
|
||||
tags: [],
|
||||
search: ''
|
||||
},
|
||||
pageSize: 20
|
||||
pageSize: 20,
|
||||
showFavoritesOnly: false,
|
||||
},
|
||||
|
||||
checkpoints: {
|
||||
@@ -80,7 +82,8 @@ export const state = {
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: []
|
||||
}
|
||||
},
|
||||
showFavoritesOnly: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
BIN
static/vendor/font-awesome/webfonts/fa-regular-400.ttf
vendored
Normal file
BIN
static/vendor/font-awesome/webfonts/fa-regular-400.ttf
vendored
Normal file
Binary file not shown.
BIN
static/vendor/font-awesome/webfonts/fa-regular-400.woff2
vendored
Normal file
BIN
static/vendor/font-awesome/webfonts/fa-regular-400.woff2
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user