From 06b3c258ff44d74d81a7a2dd0da89f98e1da7837 Mon Sep 17 00:00:00 2001 From: Metal079 Date: Thu, 8 Jan 2026 20:03:00 -0600 Subject: [PATCH] Redesign lora filters with favorites and sorting --- src/app/home/options/options.component.css | 75 +++++++++--- src/app/home/options/options.component.html | 34 +++++- src/app/home/options/options.component.ts | 120 +++++++++++++++++++- 3 files changed, 206 insertions(+), 23 deletions(-) diff --git a/src/app/home/options/options.component.css b/src/app/home/options/options.component.css index 626ff2e..82f6af6 100644 --- a/src/app/home/options/options.component.css +++ b/src/app/home/options/options.component.css @@ -179,25 +179,49 @@ flex-wrap: wrap; } -.loras-grid { +.lora-toolbar { + display: grid; + gap: 12px; + align-items: end; +} + +.lora-toolbar__controls { + display: grid; + gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); +} + +.lora-toolbar__control .form-label { + margin-bottom: 0.25rem; +} + +.lora-toolbar__toggle .form-check { display: flex; - flex-wrap: wrap; - gap: 15px; - /* Space between items */ + align-items: center; + gap: 0.5rem; +} + +.lora-toolbar__toggle .form-check-label { + margin: 0; +} + +.loras-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 16px; overflow-y: auto; - /* Enable scrolling if the content exceeds the height */ overflow-x: hidden; - /* Hide horizontal scrollbar */ } .lora-card { border: 1px solid #ddd; border-radius: 8px; - overflow: visible; - min-height: 190px; - /* Set a minimum height to prevent layout shifts */ - max-width: 600px; - /* Enforce a consistent card width */ + overflow: hidden; + min-height: 230px; + background: #fff; + position: relative; + display: flex; + flex-direction: column; } .lora-image { @@ -208,6 +232,21 @@ /* Align the image towards the top */ } +.lora-favorite-btn { + position: absolute; + top: 8px; + right: 8px; + border-radius: 999px; + padding: 0.25rem 0.5rem; + border: none; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + z-index: 2; +} + +.lora-favorite-btn i { + color: #f5b700; +} + .lora-info { padding: 0.5rem; padding-bottom: 0; @@ -240,7 +279,7 @@ align-items: center; margin-bottom: 10px; /* flex-wrap: nowrap; Prevent wrapping */ - max-width: 30%; + max-width: 100%; /* Ensure it doesn't exceed the container width */ } @@ -346,16 +385,20 @@ } .lora-card { - width: calc(33% - 15px); - border: 1px solid #ddd; + width: 100%; box-sizing: border-box; } .btn.btn-primary.lora-request-btn { width: 100%; + grid-column: 1 / -1; } @media (max-width: 768px) { + .lora-toolbar__controls { + grid-template-columns: 1fr; + } + .button-group { flex-direction: row; width: 100%; @@ -375,6 +418,10 @@ } @media (max-width: 480px) { + .loras-grid { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + } + .input-button-container { flex-direction: column; align-items: flex-start; diff --git a/src/app/home/options/options.component.html b/src/app/home/options/options.component.html index 9bbf0e0..34c9ca4 100644 --- a/src/app/home/options/options.component.html +++ b/src/app/home/options/options.component.html @@ -577,10 +577,33 @@
Load LoRAs?
[style]="{ width: '100%', marginBottom: '10px' }" (onChange)="filterLoras()"> - -
- + +
+ +
+
+ + +
+
+ +
+ + Show +
+
+
@@ -599,6 +622,9 @@
Full Lora Preview
Request LoRAs!
+
diff --git a/src/app/home/options/options.component.ts b/src/app/home/options/options.component.ts index d7812b5..9c692dd 100644 --- a/src/app/home/options/options.component.ts +++ b/src/app/home/options/options.component.ts @@ -213,6 +213,12 @@ export class OptionsComponent implements OnInit { filteredLoras: any[] = []; selectedLoras: any[] = []; maxLoras: number = 3; + loraSortOrder: 'most_used' | 'last_used' | 'alpha_asc' | 'alpha_desc' | 'favorites' = 'most_used'; + loraFavoritesOnly: boolean = false; + private loraFavorites = new Set(); + private loraLastUsed: Record = {}; + private readonly loraFavoritesKey = 'loraFavorites'; + private readonly loraLastUsedKey = 'loraLastUsed'; // to show full sized lora image displayModal: boolean = false; @@ -586,6 +592,8 @@ export class OptionsComponent implements OnInit { this.generationRequest.prompt = value; }); + this.loadLoraPreferences(); + // Subscribe to sync status changes this.imageSyncService.syncStatus$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(status => { this.syncStatus = status; @@ -1923,6 +1931,7 @@ export class OptionsComponent implements OnInit { return; } this.selectedLoras = this.resolveHistoryLoras(image.loras); + this.selectedLoras.forEach((lora) => this.markLoraLastUsed(lora)); this.generationRequest.loras = this.selectedLoras; this.updateCreditCost(); } @@ -2503,6 +2512,86 @@ export class OptionsComponent implements OnInit { }); } + private loadLoraPreferences() { + try { + const rawFavorites = localStorage.getItem(this.loraFavoritesKey); + if (rawFavorites) { + const parsed = JSON.parse(rawFavorites); + if (Array.isArray(parsed)) { + this.loraFavorites = new Set(parsed.filter((id) => typeof id === 'string')); + } + } + } catch (error) { + console.warn('Failed to load LoRA favorites:', error); + this.loraFavorites = new Set(); + } + + try { + const rawLastUsed = localStorage.getItem(this.loraLastUsedKey); + if (rawLastUsed) { + const parsed = JSON.parse(rawLastUsed); + if (parsed && typeof parsed === 'object') { + this.loraLastUsed = parsed; + } + } + } catch (error) { + console.warn('Failed to load LoRA last used history:', error); + this.loraLastUsed = {}; + } + } + + private persistLoraFavorites() { + try { + localStorage.setItem(this.loraFavoritesKey, JSON.stringify([...this.loraFavorites])); + } catch (error) { + console.warn('Failed to save LoRA favorites:', error); + } + } + + private persistLoraLastUsed() { + try { + localStorage.setItem(this.loraLastUsedKey, JSON.stringify(this.loraLastUsed)); + } catch (error) { + console.warn('Failed to save LoRA last used history:', error); + } + } + + private getLoraKey(lora: any): string { + const versionId = lora?.version_id ?? lora?.versionId ?? lora?.id; + if (versionId != null) return String(versionId); + const name = lora?.name ?? ''; + const version = lora?.version ?? ''; + return `${name}::${version}`.toLowerCase(); + } + + isLoraFavorite(lora: any): boolean { + return this.loraFavorites.has(this.getLoraKey(lora)); + } + + toggleLoraFavorite(lora: any, event?: Event) { + if (event) { + event.stopPropagation(); + } + const key = this.getLoraKey(lora); + if (this.loraFavorites.has(key)) { + this.loraFavorites.delete(key); + } else { + this.loraFavorites.add(key); + } + this.persistLoraFavorites(); + this.filterLoras(); + } + + private markLoraLastUsed(lora: any) { + const key = this.getLoraKey(lora); + this.loraLastUsed[key] = Date.now(); + this.persistLoraLastUsed(); + } + + private getLoraLastUsed(lora: any): number { + return this.loraLastUsed[this.getLoraKey(lora)] ?? 0; + } + loadLoras() { this.stableDiffusionService.getLoras().pipe( @@ -2529,17 +2618,37 @@ export class OptionsComponent implements OnInit { } // Then filter by search query - this.filteredLoras = this.filteredLoras.filter(lora => - (lora.name.toLowerCase().includes(this.loraSearchQuery.toLowerCase()) || lora.version.toLowerCase().includes(this.loraSearchQuery.toLowerCase())) - ); + const query = this.loraSearchQuery.toLowerCase(); + this.filteredLoras = this.filteredLoras.filter(lora => { + const name = (lora?.name || '').toLowerCase(); + const version = (lora?.version || '').toLowerCase(); + return name.includes(query) || version.includes(query); + }); - // Sort by most uses - this.filteredLoras = this.filteredLoras.sort((a, b) => b.uses - a.uses); + if (this.loraFavoritesOnly) { + this.filteredLoras = this.filteredLoras.filter(lora => this.isLoraFavorite(lora)); + } // Filter by selected filters if (this.selectedTags.length > 0) { this.filteredLoras = this.filteredLoras.filter(lora => lora.tags.some((tag: string) => this.selectedTags.includes(tag))); } + + this.filteredLoras = [...this.filteredLoras].sort((a, b) => { + switch (this.loraSortOrder) { + case 'last_used': + return this.getLoraLastUsed(b) - this.getLoraLastUsed(a) || (a?.name || '').localeCompare(b?.name || ''); + case 'alpha_asc': + return (a?.name || '').localeCompare(b?.name || ''); + case 'alpha_desc': + return (b?.name || '').localeCompare(a?.name || ''); + case 'favorites': + return Number(this.isLoraFavorite(b)) - Number(this.isLoraFavorite(a)) || (a?.name || '').localeCompare(b?.name || ''); + case 'most_used': + default: + return (b?.uses || 0) - (a?.uses || 0) || (a?.name || '').localeCompare(b?.name || ''); + } + }); } // Function to select a LoRA @@ -2556,6 +2665,7 @@ export class OptionsComponent implements OnInit { lora.strength = 1.0; // Set the default strength to 1.0 this.selectedLoras.push(lora); this.generationRequest.loras = this.selectedLoras; + this.markLoraLastUsed(lora); // Add the trigger prompt, if it exists to the prompt input (only if it's not already there) if (lora.trigger_words) {