diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py
index 06aff4a5..b235459f 100644
--- a/py/routes/api_routes.py
+++ b/py/routes/api_routes.py
@@ -125,6 +125,7 @@ class ApiRoutes:
# Get filter parameters
base_models = request.query.get('base_models', None)
tags = request.query.get('tags', None)
+ favorites_only = request.query.get('favorites_only', 'false').lower() == 'true' # New parameter
# New parameters for recipe filtering
lora_hash = request.query.get('lora_hash', None)
@@ -155,7 +156,8 @@ class ApiRoutes:
base_models=filters.get('base_model', None),
tags=filters.get('tags', None),
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
@@ -195,6 +197,7 @@ class ApiRoutes:
"from_civitai": lora.get("from_civitai", True),
"usage_tips": lora.get("usage_tips", ""),
"notes": lora.get("notes", ""),
+ "favorite": lora.get("favorite", False), # Include favorite status in response
"civitai": ModelRouteUtils.filter_civitai_data(lora.get("civitai", {}))
}
diff --git a/py/routes/checkpoints_routes.py b/py/routes/checkpoints_routes.py
index fa4e20ee..46752f28 100644
--- a/py/routes/checkpoints_routes.py
+++ b/py/routes/checkpoints_routes.py
@@ -69,6 +69,7 @@ class CheckpointsRoutes:
fuzzy_search = request.query.get('fuzzy_search', 'false').lower() == 'true'
base_models = request.query.getall('base_model', [])
tags = request.query.getall('tag', [])
+ favorites_only = request.query.get('favorites_only', 'false').lower() == 'true' # Add favorites_only parameter
# Process search options
search_options = {
@@ -101,7 +102,8 @@ class CheckpointsRoutes:
base_models=base_models,
tags=tags,
search_options=search_options,
- hash_filters=hash_filters
+ hash_filters=hash_filters,
+ favorites_only=favorites_only # Pass favorites_only parameter
)
# Format response items
@@ -123,7 +125,8 @@ class CheckpointsRoutes:
async def get_paginated_data(self, page, page_size, sort_by='name',
folder=None, search=None, fuzzy_search=False,
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"""
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']
]
+ # 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
if folder is not None:
if search_options.get('recursive', False):
@@ -276,6 +286,7 @@ class CheckpointsRoutes:
"from_civitai": checkpoint.get("from_civitai", True),
"notes": checkpoint.get("notes", ""),
"model_type": checkpoint.get("model_type", "checkpoint"),
+ "favorite": checkpoint.get("favorite", False),
"civitai": ModelRouteUtils.filter_civitai_data(checkpoint.get("civitai", {}))
}
diff --git a/py/services/lora_scanner.py b/py/services/lora_scanner.py
index 9756e8f1..a333b2c3 100644
--- a/py/services/lora_scanner.py
+++ b/py/services/lora_scanner.py
@@ -122,7 +122,8 @@ class LoraScanner(ModelScanner):
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,
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
Args:
@@ -136,6 +137,7 @@ class LoraScanner(ModelScanner):
tags: List of tags to filter by
search_options: Dictionary with search options (filename, modelname, tags, recursive)
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()
@@ -194,6 +196,13 @@ class LoraScanner(ModelScanner):
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
if folder is not None:
if search_options.get('recursive', False):
diff --git a/py/utils/models.py b/py/utils/models.py
index 07e392ca..f0f7fc94 100644
--- a/py/utils/models.py
+++ b/py/utils/models.py
@@ -22,6 +22,7 @@ class BaseModelMetadata:
tags: List[str] = None # Model tags
modelDescription: str = "" # Full model description
civitai_deleted: bool = False # Whether deleted from Civitai
+ favorite: bool = False # Whether the model is a favorite
def __post_init__(self):
# Initialize empty lists to avoid mutable default parameter issue
diff --git a/static/css/components/card.css b/static/css/components/card.css
index ed424827..8c172872 100644
--- a/static/css/components/card.css
+++ b/static/css/components/card.css
@@ -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);
}
/* 响应式设计 */
diff --git a/static/css/layout.css b/static/css/layout.css
index d1aa0e1d..72d8120f 100644
--- a/static/css/layout.css
+++ b/static/css/layout.css
@@ -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);
diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js
index 8471f1f8..45898601 100644
--- a/static/js/api/baseModelApi.js
+++ b/static/js/api/baseModelApi.js
@@ -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);
diff --git a/static/js/api/checkpointApi.js b/static/js/api/checkpointApi.js
index cbf6a66a..a1a59c96 100644
--- a/static/js/api/checkpointApi.js
+++ b/static/js/api/checkpointApi.js
@@ -62,8 +62,13 @@ export async function refreshSingleCheckpointMetadata(filePath) {
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', {
method: 'POST',
headers: {
@@ -79,5 +84,5 @@ export async function saveCheckpointMetadata(filePath, data) {
throw new Error('Failed to save metadata');
}
- return await response.json();
+ return response.json();
}
\ No newline at end of file
diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js
index 5e433799..8c4cb9a5 100644
--- a/static/js/api/loraApi.js
+++ b/static/js/api/loraApi.js
@@ -9,6 +9,31 @@ import {
refreshSingleModelMetadata
} 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) {
return loadMoreModels({
resetPage,
diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js
index 96df010a..a07aee43 100644
--- a/static/js/components/CheckpointCard.js
+++ b/static/js/components/CheckpointCard.js
@@ -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 = `
${isVideo ?
@@ -82,6 +86,9 @@ export function createCheckpointCard(checkpoint) {
${checkpoint.base_model}
+
+
@@ -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();
diff --git a/static/js/components/ContextMenu/CheckpointContextMenu.js b/static/js/components/ContextMenu/CheckpointContextMenu.js
index 975ac986..c0975bbe 100644
--- a/static/js/components/ContextMenu/CheckpointContextMenu.js
+++ b/static/js/components/ContextMenu/CheckpointContextMenu.js
@@ -1,5 +1,5 @@
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 { NSFW_LEVELS } from '../../utils/constants.js';
import { getStorageItem } from '../../utils/storageHelpers.js';
@@ -82,7 +82,7 @@ export class CheckpointContextMenu extends BaseContextMenu {
if (!filePath) return;
try {
- await saveCheckpointMetadata(filePath, { preview_nsfw_level: level });
+ await saveModelMetadata(filePath, { preview_nsfw_level: level });
// Update card data
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
diff --git a/static/js/components/ContextMenu/LoraContextMenu.js b/static/js/components/ContextMenu/LoraContextMenu.js
index 64382d0e..146c3d94 100644
--- a/static/js/components/ContextMenu/LoraContextMenu.js
+++ b/static/js/components/ContextMenu/LoraContextMenu.js
@@ -1,5 +1,5 @@
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 { NSFW_LEVELS } from '../../utils/constants.js';
import { getStorageItem } from '../../utils/storageHelpers.js';
@@ -111,22 +111,7 @@ export class LoraContextMenu extends BaseContextMenu {
}
async 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 await response.json();
+ return saveModelMetadata(filePath, data);
}
updateCardBlurEffect(card, level) {
diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js
index fb97aadb..b5add51d 100644
--- a/static/js/components/LoraCard.js
+++ b/static/js/components/LoraCard.js
@@ -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 = `
${isVideo ?
@@ -82,6 +86,9 @@ export function createLoraCard(lora) {
${lora.base_model}
+
+
@@ -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();
diff --git a/static/js/components/checkpointModal/ModelMetadata.js b/static/js/components/checkpointModal/ModelMetadata.js
index 4bf7b2b3..18167f3b 100644
--- a/static/js/components/checkpointModal/ModelMetadata.js
+++ b/static/js/components/checkpointModal/ModelMetadata.js
@@ -5,31 +5,7 @@
import { showToast } from '../../utils/uiHelpers.js';
import { BASE_MODELS } from '../../utils/constants.js';
import { updateCheckpointCard } from '../../utils/cardUpdater.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();
-}
+import { saveModelMetadata } from '../../api/checkpointApi.js';
/**
* Set up model name editing functionality
diff --git a/static/js/components/checkpointModal/index.js b/static/js/components/checkpointModal/index.js
index 879f9218..9212d57b 100644
--- a/static/js/components/checkpointModal/index.js
+++ b/static/js/components/checkpointModal/index.js
@@ -11,9 +11,9 @@ import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
import {
setupModelNameEditing,
setupBaseModelEditing,
- setupFileNameEditing,
- saveModelMetadata
+ setupFileNameEditing
} from './ModelMetadata.js';
+import { saveModelMetadata } from '../../api/checkpointApi.js';
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
import { updateCheckpointCard } from '../../utils/cardUpdater.js';
diff --git a/static/js/components/controls/LorasControls.js b/static/js/components/controls/LorasControls.js
index 0a45a9e6..521a1cee 100644
--- a/static/js/components/controls/LorasControls.js
+++ b/static/js/components/controls/LorasControls.js
@@ -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
diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js
index b30151c1..caf101d5 100644
--- a/static/js/components/controls/PageControls.js
+++ b/static/js/components/controls/PageControls.js
@@ -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);
+ }
}
\ No newline at end of file
diff --git a/static/js/components/loraModal/ModelMetadata.js b/static/js/components/loraModal/ModelMetadata.js
index eb0a3d18..81300150 100644
--- a/static/js/components/loraModal/ModelMetadata.js
+++ b/static/js/components/loraModal/ModelMetadata.js
@@ -5,31 +5,7 @@
import { showToast } from '../../utils/uiHelpers.js';
import { BASE_MODELS } from '../../utils/constants.js';
import { updateLoraCard } from '../../utils/cardUpdater.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();
-}
+import { saveModelMetadata } from '../../api/loraApi.js';
/**
* 设置模型名称编辑功能
diff --git a/static/js/components/loraModal/PresetTags.js b/static/js/components/loraModal/PresetTags.js
index a6abab2b..4e331159 100644
--- a/static/js/components/loraModal/PresetTags.js
+++ b/static/js/components/loraModal/PresetTags.js
@@ -2,8 +2,7 @@
* PresetTags.js
* 处理LoRA模型预设参数标签相关的功能模块
*/
-import { saveModelMetadata } from './ModelMetadata.js';
-import { showToast } from '../../utils/uiHelpers.js';
+import { saveModelMetadata } from '../../api/loraApi.js';
/**
* 解析预设参数
diff --git a/static/js/components/loraModal/TriggerWords.js b/static/js/components/loraModal/TriggerWords.js
index e80c9e39..5c9004f3 100644
--- a/static/js/components/loraModal/TriggerWords.js
+++ b/static/js/components/loraModal/TriggerWords.js
@@ -3,7 +3,7 @@
* 处理LoRA模型触发词相关的功能模块
*/
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
-import { saveModelMetadata } from './ModelMetadata.js';
+import { saveModelMetadata } from '../../api/loraApi.js';
/**
* 渲染触发词
diff --git a/static/js/components/loraModal/index.js b/static/js/components/loraModal/index.js
index 69e71675..db9b41e5 100644
--- a/static/js/components/loraModal/index.js
+++ b/static/js/components/loraModal/index.js
@@ -13,9 +13,9 @@ import { loadRecipesForLora } from './RecipeTab.js'; // Add import for recipe ta
import {
setupModelNameEditing,
setupBaseModelEditing,
- setupFileNameEditing,
- saveModelMetadata
+ setupFileNameEditing
} from './ModelMetadata.js';
+import { saveModelMetadata } from '../../api/loraApi.js';
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
import { updateLoraCard } from '../../utils/cardUpdater.js';
diff --git a/static/js/state/index.js b/static/js/state/index.js
index a498e57b..23b41226 100644
--- a/static/js/state/index.js
+++ b/static/js/state/index.js
@@ -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,
}
},
diff --git a/static/vendor/font-awesome/webfonts/fa-regular-400.ttf b/static/vendor/font-awesome/webfonts/fa-regular-400.ttf
new file mode 100644
index 00000000..549d68dc
Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-regular-400.ttf differ
diff --git a/static/vendor/font-awesome/webfonts/fa-regular-400.woff2 b/static/vendor/font-awesome/webfonts/fa-regular-400.woff2
new file mode 100644
index 00000000..18400d7f
Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-regular-400.woff2 differ
diff --git a/templates/components/controls.html b/templates/components/controls.html
index 56921cd5..2236b997 100644
--- a/templates/components/controls.html
+++ b/templates/components/controls.html
@@ -35,6 +35,11 @@
{% endif %}
+
+
+