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 @@
-
+
+
@@ -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) {