mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
@@ -125,6 +125,7 @@ class ApiRoutes:
|
|||||||
# Get filter parameters
|
# Get filter parameters
|
||||||
base_models = request.query.get('base_models', None)
|
base_models = request.query.get('base_models', None)
|
||||||
tags = request.query.get('tags', None)
|
tags = request.query.get('tags', None)
|
||||||
|
favorites_only = request.query.get('favorites_only', 'false').lower() == 'true' # New parameter
|
||||||
|
|
||||||
# New parameters for recipe filtering
|
# New parameters for recipe filtering
|
||||||
lora_hash = request.query.get('lora_hash', None)
|
lora_hash = request.query.get('lora_hash', None)
|
||||||
@@ -155,7 +156,8 @@ class ApiRoutes:
|
|||||||
base_models=filters.get('base_model', None),
|
base_models=filters.get('base_model', None),
|
||||||
tags=filters.get('tags', None),
|
tags=filters.get('tags', None),
|
||||||
search_options=search_options,
|
search_options=search_options,
|
||||||
hash_filters=hash_filters
|
hash_filters=hash_filters,
|
||||||
|
favorites_only=favorites_only # Pass favorites_only parameter
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all available folders from cache
|
# Get all available folders from cache
|
||||||
@@ -195,6 +197,7 @@ class ApiRoutes:
|
|||||||
"from_civitai": lora.get("from_civitai", True),
|
"from_civitai": lora.get("from_civitai", True),
|
||||||
"usage_tips": lora.get("usage_tips", ""),
|
"usage_tips": lora.get("usage_tips", ""),
|
||||||
"notes": lora.get("notes", ""),
|
"notes": lora.get("notes", ""),
|
||||||
|
"favorite": lora.get("favorite", False), # Include favorite status in response
|
||||||
"civitai": ModelRouteUtils.filter_civitai_data(lora.get("civitai", {}))
|
"civitai": ModelRouteUtils.filter_civitai_data(lora.get("civitai", {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class CheckpointsRoutes:
|
|||||||
fuzzy_search = request.query.get('fuzzy_search', 'false').lower() == 'true'
|
fuzzy_search = request.query.get('fuzzy_search', 'false').lower() == 'true'
|
||||||
base_models = request.query.getall('base_model', [])
|
base_models = request.query.getall('base_model', [])
|
||||||
tags = request.query.getall('tag', [])
|
tags = request.query.getall('tag', [])
|
||||||
|
favorites_only = request.query.get('favorites_only', 'false').lower() == 'true' # Add favorites_only parameter
|
||||||
|
|
||||||
# Process search options
|
# Process search options
|
||||||
search_options = {
|
search_options = {
|
||||||
@@ -101,7 +102,8 @@ class CheckpointsRoutes:
|
|||||||
base_models=base_models,
|
base_models=base_models,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
search_options=search_options,
|
search_options=search_options,
|
||||||
hash_filters=hash_filters
|
hash_filters=hash_filters,
|
||||||
|
favorites_only=favorites_only # Pass favorites_only parameter
|
||||||
)
|
)
|
||||||
|
|
||||||
# Format response items
|
# Format response items
|
||||||
@@ -123,7 +125,8 @@ class CheckpointsRoutes:
|
|||||||
async def get_paginated_data(self, page, page_size, sort_by='name',
|
async def get_paginated_data(self, page, page_size, sort_by='name',
|
||||||
folder=None, search=None, fuzzy_search=False,
|
folder=None, search=None, fuzzy_search=False,
|
||||||
base_models=None, tags=None,
|
base_models=None, tags=None,
|
||||||
search_options=None, hash_filters=None):
|
search_options=None, hash_filters=None,
|
||||||
|
favorites_only=False): # Add favorites_only parameter with default False
|
||||||
"""Get paginated and filtered checkpoint data"""
|
"""Get paginated and filtered checkpoint data"""
|
||||||
cache = await self.scanner.get_cached_data()
|
cache = await self.scanner.get_cached_data()
|
||||||
|
|
||||||
@@ -181,6 +184,13 @@ class CheckpointsRoutes:
|
|||||||
if not cp.get('preview_nsfw_level') or cp.get('preview_nsfw_level') < NSFW_LEVELS['R']
|
if not cp.get('preview_nsfw_level') or cp.get('preview_nsfw_level') < NSFW_LEVELS['R']
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Apply favorites filtering if enabled
|
||||||
|
if favorites_only:
|
||||||
|
filtered_data = [
|
||||||
|
cp for cp in filtered_data
|
||||||
|
if cp.get('favorite', False) is True
|
||||||
|
]
|
||||||
|
|
||||||
# Apply folder filtering
|
# Apply folder filtering
|
||||||
if folder is not None:
|
if folder is not None:
|
||||||
if search_options.get('recursive', False):
|
if search_options.get('recursive', False):
|
||||||
@@ -276,6 +286,7 @@ class CheckpointsRoutes:
|
|||||||
"from_civitai": checkpoint.get("from_civitai", True),
|
"from_civitai": checkpoint.get("from_civitai", True),
|
||||||
"notes": checkpoint.get("notes", ""),
|
"notes": checkpoint.get("notes", ""),
|
||||||
"model_type": checkpoint.get("model_type", "checkpoint"),
|
"model_type": checkpoint.get("model_type", "checkpoint"),
|
||||||
|
"favorite": checkpoint.get("favorite", False),
|
||||||
"civitai": ModelRouteUtils.filter_civitai_data(checkpoint.get("civitai", {}))
|
"civitai": ModelRouteUtils.filter_civitai_data(checkpoint.get("civitai", {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ class LoraScanner(ModelScanner):
|
|||||||
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'name',
|
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'name',
|
||||||
folder: str = None, search: str = None, fuzzy_search: bool = False,
|
folder: str = None, search: str = None, fuzzy_search: bool = False,
|
||||||
base_models: list = None, tags: list = None,
|
base_models: list = None, tags: list = None,
|
||||||
search_options: dict = None, hash_filters: dict = None) -> Dict:
|
search_options: dict = None, hash_filters: dict = None,
|
||||||
|
favorites_only: bool = False) -> Dict:
|
||||||
"""Get paginated and filtered lora data
|
"""Get paginated and filtered lora data
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -136,6 +137,7 @@ class LoraScanner(ModelScanner):
|
|||||||
tags: List of tags to filter by
|
tags: List of tags to filter by
|
||||||
search_options: Dictionary with search options (filename, modelname, tags, recursive)
|
search_options: Dictionary with search options (filename, modelname, tags, recursive)
|
||||||
hash_filters: Dictionary with hash filtering options (single_hash or multiple_hashes)
|
hash_filters: Dictionary with hash filtering options (single_hash or multiple_hashes)
|
||||||
|
favorites_only: Filter for favorite models only
|
||||||
"""
|
"""
|
||||||
cache = await self.get_cached_data()
|
cache = await self.get_cached_data()
|
||||||
|
|
||||||
@@ -194,6 +196,13 @@ class LoraScanner(ModelScanner):
|
|||||||
if not lora.get('preview_nsfw_level') or lora.get('preview_nsfw_level') < NSFW_LEVELS['R']
|
if not lora.get('preview_nsfw_level') or lora.get('preview_nsfw_level') < NSFW_LEVELS['R']
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Apply favorites filtering if enabled
|
||||||
|
if favorites_only:
|
||||||
|
filtered_data = [
|
||||||
|
lora for lora in filtered_data
|
||||||
|
if lora.get('favorite', False) is True
|
||||||
|
]
|
||||||
|
|
||||||
# Apply folder filtering
|
# Apply folder filtering
|
||||||
if folder is not None:
|
if folder is not None:
|
||||||
if search_options.get('recursive', False):
|
if search_options.get('recursive', False):
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class BaseModelMetadata:
|
|||||||
tags: List[str] = None # Model tags
|
tags: List[str] = None # Model tags
|
||||||
modelDescription: str = "" # Full model description
|
modelDescription: str = "" # Full model description
|
||||||
civitai_deleted: bool = False # Whether deleted from Civitai
|
civitai_deleted: bool = False # Whether deleted from Civitai
|
||||||
|
favorite: bool = False # Whether the model is a favorite
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
# Initialize empty lists to avoid mutable default parameter issue
|
# Initialize empty lists to avoid mutable default parameter issue
|
||||||
|
|||||||
@@ -192,12 +192,43 @@
|
|||||||
margin-left: var(--space-1);
|
margin-left: var(--space-1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: white;
|
color: white;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s, transform 0.15s ease;
|
||||||
font-size: 0.9em;
|
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 {
|
.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;
|
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 */
|
/* Active state for buttons that can be toggled */
|
||||||
.control-group button.active {
|
.control-group button.active {
|
||||||
background: var(--lora-accent);
|
background: var(--lora-accent);
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export async function loadMoreModels(options = {}) {
|
|||||||
params.append('folder', pageState.activeFolder);
|
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
|
// Add search parameters if there's a search term
|
||||||
if (pageState.filters?.search) {
|
if (pageState.filters?.search) {
|
||||||
params.append('search', pageState.filters.search);
|
params.append('search', pageState.filters.search);
|
||||||
|
|||||||
@@ -62,8 +62,13 @@ export async function refreshSingleCheckpointMetadata(filePath) {
|
|||||||
return refreshSingleModelMetadata(filePath, 'checkpoint');
|
return refreshSingleModelMetadata(filePath, 'checkpoint');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save checkpoint metadata (similar to the Lora version)
|
/**
|
||||||
export async function saveCheckpointMetadata(filePath, data) {
|
* Save model metadata to the server
|
||||||
|
* @param {string} filePath - Path to the model file
|
||||||
|
* @param {Object} data - Metadata to save
|
||||||
|
* @returns {Promise} - Promise that resolves with the server response
|
||||||
|
*/
|
||||||
|
export async function saveModelMetadata(filePath, data) {
|
||||||
const response = await fetch('/api/checkpoints/save-metadata', {
|
const response = await fetch('/api/checkpoints/save-metadata', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -79,5 +84,5 @@ export async function saveCheckpointMetadata(filePath, data) {
|
|||||||
throw new Error('Failed to save metadata');
|
throw new Error('Failed to save metadata');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,31 @@ import {
|
|||||||
refreshSingleModelMetadata
|
refreshSingleModelMetadata
|
||||||
} from './baseModelApi.js';
|
} from './baseModelApi.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save model metadata to the server
|
||||||
|
* @param {string} filePath - File path
|
||||||
|
* @param {Object} data - Data to save
|
||||||
|
* @returns {Promise} Promise of the save operation
|
||||||
|
*/
|
||||||
|
export async function saveModelMetadata(filePath, data) {
|
||||||
|
const response = await fetch('/api/loras/save-metadata', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath,
|
||||||
|
...data
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
||||||
return loadMoreModels({
|
return loadMoreModels({
|
||||||
resetPage,
|
resetPage,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { showToast, copyToClipboard } from '../utils/uiHelpers.js';
|
|||||||
import { state } from '../state/index.js';
|
import { state } from '../state/index.js';
|
||||||
import { showCheckpointModal } from './checkpointModal/index.js';
|
import { showCheckpointModal } from './checkpointModal/index.js';
|
||||||
import { NSFW_LEVELS } from '../utils/constants.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) {
|
export function createCheckpointCard(checkpoint) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@@ -17,6 +17,7 @@ export function createCheckpointCard(checkpoint) {
|
|||||||
card.dataset.from_civitai = checkpoint.from_civitai;
|
card.dataset.from_civitai = checkpoint.from_civitai;
|
||||||
card.dataset.notes = checkpoint.notes || '';
|
card.dataset.notes = checkpoint.notes || '';
|
||||||
card.dataset.base_model = checkpoint.base_model || 'Unknown';
|
card.dataset.base_model = checkpoint.base_model || 'Unknown';
|
||||||
|
card.dataset.favorite = checkpoint.favorite ? 'true' : 'false';
|
||||||
|
|
||||||
// Store metadata if available
|
// Store metadata if available
|
||||||
if (checkpoint.civitai) {
|
if (checkpoint.civitai) {
|
||||||
@@ -65,6 +66,9 @@ export function createCheckpointCard(checkpoint) {
|
|||||||
const isVideo = previewUrl.endsWith('.mp4');
|
const isVideo = previewUrl.endsWith('.mp4');
|
||||||
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
|
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
|
||||||
|
|
||||||
|
// Get favorite status from checkpoint data
|
||||||
|
const isFavorite = checkpoint.favorite === true;
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||||
${isVideo ?
|
${isVideo ?
|
||||||
@@ -82,6 +86,9 @@ export function createCheckpointCard(checkpoint) {
|
|||||||
${checkpoint.base_model}
|
${checkpoint.base_model}
|
||||||
</span>
|
</span>
|
||||||
<div class="card-actions">
|
<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"
|
<i class="fas fa-globe"
|
||||||
title="${checkpoint.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
|
title="${checkpoint.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
|
||||||
${!checkpoint.from_civitai ? 'style="opacity: 0.5; cursor: not-allowed"' : ''}>
|
${!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
|
// Copy button click event
|
||||||
card.querySelector('.fa-copy')?.addEventListener('click', async e => {
|
card.querySelector('.fa-copy')?.addEventListener('click', async e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseContextMenu } from './BaseContextMenu.js';
|
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||||
import { refreshSingleCheckpointMetadata, saveCheckpointMetadata } from '../../api/checkpointApi.js';
|
import { refreshSingleCheckpointMetadata, saveModelMetadata } from '../../api/checkpointApi.js';
|
||||||
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
|
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
|
||||||
import { NSFW_LEVELS } from '../../utils/constants.js';
|
import { NSFW_LEVELS } from '../../utils/constants.js';
|
||||||
import { getStorageItem } from '../../utils/storageHelpers.js';
|
import { getStorageItem } from '../../utils/storageHelpers.js';
|
||||||
@@ -82,7 +82,7 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
|||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await saveCheckpointMetadata(filePath, { preview_nsfw_level: level });
|
await saveModelMetadata(filePath, { preview_nsfw_level: level });
|
||||||
|
|
||||||
// Update card data
|
// Update card data
|
||||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseContextMenu } from './BaseContextMenu.js';
|
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||||
import { refreshSingleLoraMetadata } from '../../api/loraApi.js';
|
import { refreshSingleLoraMetadata, saveModelMetadata } from '../../api/loraApi.js';
|
||||||
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
|
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
|
||||||
import { NSFW_LEVELS } from '../../utils/constants.js';
|
import { NSFW_LEVELS } from '../../utils/constants.js';
|
||||||
import { getStorageItem } from '../../utils/storageHelpers.js';
|
import { getStorageItem } from '../../utils/storageHelpers.js';
|
||||||
@@ -111,22 +111,7 @@ export class LoraContextMenu extends BaseContextMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveModelMetadata(filePath, data) {
|
async saveModelMetadata(filePath, data) {
|
||||||
const response = await fetch('/api/loras/save-metadata', {
|
return saveModelMetadata(filePath, data);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath,
|
|
||||||
...data
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to save metadata');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCardBlurEffect(card, level) {
|
updateCardBlurEffect(card, level) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { state } from '../state/index.js';
|
|||||||
import { showLoraModal } from './loraModal/index.js';
|
import { showLoraModal } from './loraModal/index.js';
|
||||||
import { bulkManager } from '../managers/BulkManager.js';
|
import { bulkManager } from '../managers/BulkManager.js';
|
||||||
import { NSFW_LEVELS } from '../utils/constants.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) {
|
export function createLoraCard(lora) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@@ -20,6 +20,7 @@ export function createLoraCard(lora) {
|
|||||||
card.dataset.usage_tips = lora.usage_tips;
|
card.dataset.usage_tips = lora.usage_tips;
|
||||||
card.dataset.notes = lora.notes;
|
card.dataset.notes = lora.notes;
|
||||||
card.dataset.meta = JSON.stringify(lora.civitai || {});
|
card.dataset.meta = JSON.stringify(lora.civitai || {});
|
||||||
|
card.dataset.favorite = lora.favorite ? 'true' : 'false';
|
||||||
|
|
||||||
// Store tags and model description
|
// Store tags and model description
|
||||||
if (lora.tags && Array.isArray(lora.tags)) {
|
if (lora.tags && Array.isArray(lora.tags)) {
|
||||||
@@ -65,6 +66,9 @@ export function createLoraCard(lora) {
|
|||||||
const isVideo = previewUrl.endsWith('.mp4');
|
const isVideo = previewUrl.endsWith('.mp4');
|
||||||
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
|
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
|
||||||
|
|
||||||
|
// Get favorite status from the lora data
|
||||||
|
const isFavorite = lora.favorite === true;
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||||
${isVideo ?
|
${isVideo ?
|
||||||
@@ -82,6 +86,9 @@ export function createLoraCard(lora) {
|
|||||||
${lora.base_model}
|
${lora.base_model}
|
||||||
</span>
|
</span>
|
||||||
<div class="card-actions">
|
<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"
|
<i class="fas fa-globe"
|
||||||
title="${lora.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
|
title="${lora.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
|
||||||
${!lora.from_civitai ? 'style="opacity: 0.5; cursor: not-allowed"' : ''}>
|
${!lora.from_civitai ? 'style="opacity: 0.5; cursor: not-allowed"' : ''}>
|
||||||
@@ -135,6 +142,7 @@ export function createLoraCard(lora) {
|
|||||||
base_model: card.dataset.base_model,
|
base_model: card.dataset.base_model,
|
||||||
usage_tips: card.dataset.usage_tips,
|
usage_tips: card.dataset.usage_tips,
|
||||||
notes: card.dataset.notes,
|
notes: card.dataset.notes,
|
||||||
|
favorite: card.dataset.favorite === 'true',
|
||||||
// Parse civitai metadata from the card's dataset
|
// Parse civitai metadata from the card's dataset
|
||||||
civitai: (() => {
|
civitai: (() => {
|
||||||
try {
|
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
|
// Copy button click event
|
||||||
card.querySelector('.fa-copy')?.addEventListener('click', async e => {
|
card.querySelector('.fa-copy')?.addEventListener('click', async e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -5,31 +5,7 @@
|
|||||||
import { showToast } from '../../utils/uiHelpers.js';
|
import { showToast } from '../../utils/uiHelpers.js';
|
||||||
import { BASE_MODELS } from '../../utils/constants.js';
|
import { BASE_MODELS } from '../../utils/constants.js';
|
||||||
import { updateCheckpointCard } from '../../utils/cardUpdater.js';
|
import { updateCheckpointCard } from '../../utils/cardUpdater.js';
|
||||||
|
import { saveModelMetadata } from '../../api/checkpointApi.js';
|
||||||
/**
|
|
||||||
* Save model metadata to the server
|
|
||||||
* @param {string} filePath - Path to the model file
|
|
||||||
* @param {Object} data - Metadata to save
|
|
||||||
* @returns {Promise} - Promise that resolves with the server response
|
|
||||||
*/
|
|
||||||
export async function saveModelMetadata(filePath, data) {
|
|
||||||
const response = await fetch('/api/checkpoints/save-metadata', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath,
|
|
||||||
...data
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to save metadata');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up model name editing functionality
|
* Set up model name editing functionality
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
|
|||||||
import {
|
import {
|
||||||
setupModelNameEditing,
|
setupModelNameEditing,
|
||||||
setupBaseModelEditing,
|
setupBaseModelEditing,
|
||||||
setupFileNameEditing,
|
setupFileNameEditing
|
||||||
saveModelMetadata
|
|
||||||
} from './ModelMetadata.js';
|
} from './ModelMetadata.js';
|
||||||
|
import { saveModelMetadata } from '../../api/checkpointApi.js';
|
||||||
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
||||||
import { updateCheckpointCard } from '../../utils/cardUpdater.js';
|
import { updateCheckpointCard } from '../../utils/cardUpdater.js';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { PageControls } from './PageControls.js';
|
import { PageControls } from './PageControls.js';
|
||||||
import { loadMoreLoras, fetchCivitai, resetAndReload, refreshLoras } from '../../api/loraApi.js';
|
import { loadMoreLoras, fetchCivitai, resetAndReload, refreshLoras } from '../../api/loraApi.js';
|
||||||
import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
||||||
import { showToast } from '../../utils/uiHelpers.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LorasControls class - Extends PageControls for LoRA-specific functionality
|
* LorasControls class - Extends PageControls for LoRA-specific functionality
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages
|
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages
|
||||||
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js';
|
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';
|
import { showToast } from '../../utils/uiHelpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,6 +26,9 @@ export class PageControls {
|
|||||||
// Initialize event listeners
|
// Initialize event listeners
|
||||||
this.initEventListeners();
|
this.initEventListeners();
|
||||||
|
|
||||||
|
// Initialize favorites filter button state
|
||||||
|
this.initFavoritesFilter();
|
||||||
|
|
||||||
console.log(`PageControls initialized for ${pageType} page`);
|
console.log(`PageControls initialized for ${pageType} page`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +124,12 @@ export class PageControls {
|
|||||||
bulkButton.addEventListener('click', () => this.toggleBulkMode());
|
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');
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,31 +5,7 @@
|
|||||||
import { showToast } from '../../utils/uiHelpers.js';
|
import { showToast } from '../../utils/uiHelpers.js';
|
||||||
import { BASE_MODELS } from '../../utils/constants.js';
|
import { BASE_MODELS } from '../../utils/constants.js';
|
||||||
import { updateLoraCard } from '../../utils/cardUpdater.js';
|
import { updateLoraCard } from '../../utils/cardUpdater.js';
|
||||||
|
import { saveModelMetadata } from '../../api/loraApi.js';
|
||||||
/**
|
|
||||||
* 保存模型元数据到服务器
|
|
||||||
* @param {string} filePath - 文件路径
|
|
||||||
* @param {Object} data - 要保存的数据
|
|
||||||
* @returns {Promise} 保存操作的Promise
|
|
||||||
*/
|
|
||||||
export async function saveModelMetadata(filePath, data) {
|
|
||||||
const response = await fetch('/api/loras/save-metadata', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath,
|
|
||||||
...data
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to save metadata');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置模型名称编辑功能
|
* 设置模型名称编辑功能
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
* PresetTags.js
|
* PresetTags.js
|
||||||
* 处理LoRA模型预设参数标签相关的功能模块
|
* 处理LoRA模型预设参数标签相关的功能模块
|
||||||
*/
|
*/
|
||||||
import { saveModelMetadata } from './ModelMetadata.js';
|
import { saveModelMetadata } from '../../api/loraApi.js';
|
||||||
import { showToast } from '../../utils/uiHelpers.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析预设参数
|
* 解析预设参数
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 处理LoRA模型触发词相关的功能模块
|
* 处理LoRA模型触发词相关的功能模块
|
||||||
*/
|
*/
|
||||||
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
|
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
|
||||||
import { saveModelMetadata } from './ModelMetadata.js';
|
import { saveModelMetadata } from '../../api/loraApi.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染触发词
|
* 渲染触发词
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import { loadRecipesForLora } from './RecipeTab.js'; // Add import for recipe ta
|
|||||||
import {
|
import {
|
||||||
setupModelNameEditing,
|
setupModelNameEditing,
|
||||||
setupBaseModelEditing,
|
setupBaseModelEditing,
|
||||||
setupFileNameEditing,
|
setupFileNameEditing
|
||||||
saveModelMetadata
|
|
||||||
} from './ModelMetadata.js';
|
} from './ModelMetadata.js';
|
||||||
|
import { saveModelMetadata } from '../../api/loraApi.js';
|
||||||
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
||||||
import { updateLoraCard } from '../../utils/cardUpdater.js';
|
import { updateLoraCard } from '../../utils/cardUpdater.js';
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const state = {
|
|||||||
bulkMode: false,
|
bulkMode: false,
|
||||||
selectedLoras: new Set(),
|
selectedLoras: new Set(),
|
||||||
loraMetadataCache: new Map(),
|
loraMetadataCache: new Map(),
|
||||||
|
showFavoritesOnly: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
recipes: {
|
recipes: {
|
||||||
@@ -61,7 +62,8 @@ export const state = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
search: ''
|
search: ''
|
||||||
},
|
},
|
||||||
pageSize: 20
|
pageSize: 20,
|
||||||
|
showFavoritesOnly: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
checkpoints: {
|
checkpoints: {
|
||||||
@@ -80,7 +82,8 @@ export const state = {
|
|||||||
filters: {
|
filters: {
|
||||||
baseModel: [],
|
baseModel: [],
|
||||||
tags: []
|
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.
@@ -35,6 +35,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="control-group">
|
||||||
|
<button id="favoriteFilterBtn" data-action="toggle-favorites" class="favorite-filter" title="Show favorites only">
|
||||||
|
<i class="fas fa-star"></i> Favorites
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div id="customFilterIndicator" class="control-group hidden">
|
<div id="customFilterIndicator" class="control-group hidden">
|
||||||
<div class="filter-active">
|
<div class="filter-active">
|
||||||
<i class="fas fa-filter"></i> <span class="customFilterText" title=""></span>
|
<i class="fas fa-filter"></i> <span class="customFilterText" title=""></span>
|
||||||
|
|||||||
Reference in New Issue
Block a user