-
Notifications
You must be signed in to change notification settings - Fork 0
LoRA tab: add favorites, sorting, and responsive toolbar #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -577,10 +577,33 @@ <h5>Load LoRAs?</h5> | |||||||||||||||||||||||
| [style]="{ width: '100%', marginBottom: '10px' }" (onChange)="filterLoras()"> | ||||||||||||||||||||||||
| </p-multiSelect> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <!-- Search and Add Custom LoRA --> | ||||||||||||||||||||||||
| <div class="d-flex justify-content-between mb-3"> | ||||||||||||||||||||||||
| <input type="text" class="form-control w-80" placeholder="Search LoRAs..." [(ngModel)]="loraSearchQuery" | ||||||||||||||||||||||||
| (ngModelChange)="filterLoras()"> | ||||||||||||||||||||||||
| <!-- Search + Sort + Favorites --> | ||||||||||||||||||||||||
| <div class="lora-toolbar mb-3"> | ||||||||||||||||||||||||
| <div class="lora-toolbar__search"> | ||||||||||||||||||||||||
| <label class="form-label">Search LoRAs</label> | ||||||||||||||||||||||||
| <input type="text" class="form-control" placeholder="Search by name or version..." [(ngModel)]="loraSearchQuery" | ||||||||||||||||||||||||
| (ngModelChange)="filterLoras()"> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| <div class="lora-toolbar__controls"> | ||||||||||||||||||||||||
| <div class="lora-toolbar__control"> | ||||||||||||||||||||||||
| <label class="form-label">Sort by</label> | ||||||||||||||||||||||||
| <select class="form-select" [(ngModel)]="loraSortOrder" (ngModelChange)="filterLoras()"> | ||||||||||||||||||||||||
| <option value="most_used">Most used</option> | ||||||||||||||||||||||||
| <option value="last_used">Last used</option> | ||||||||||||||||||||||||
| <option value="favorites">Favorites first</option> | ||||||||||||||||||||||||
| <option value="alpha_asc">Alphabetical A → Z</option> | ||||||||||||||||||||||||
| <option value="alpha_desc">Alphabetical Z → A</option> | ||||||||||||||||||||||||
| </select> | ||||||||||||||||||||||||
|
Comment on lines
+583
to
+596
|
||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| <div class="lora-toolbar__control lora-toolbar__toggle"> | ||||||||||||||||||||||||
| <label class="form-label">Favorites only</label> | ||||||||||||||||||||||||
| <div class="form-check form-switch"> | ||||||||||||||||||||||||
| <input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="loraFavoritesOnly" | ||||||||||||||||||||||||
| (ngModelChange)="filterLoras()"> | ||||||||||||||||||||||||
| <span class="form-check-label">Show</span> | ||||||||||||||||||||||||
|
Comment on lines
+601
to
+603
|
||||||||||||||||||||||||
| <input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="loraFavoritesOnly" | |
| (ngModelChange)="filterLoras()"> | |
| <span class="form-check-label">Show</span> | |
| <input | |
| id="loraFavoritesOnlySwitch" | |
| class="form-check-input" | |
| type="checkbox" | |
| role="switch" | |
| [(ngModel)]="loraFavoritesOnly" | |
| (ngModelChange)="filterLoras()"> | |
| <label class="form-check-label" for="loraFavoritesOnlySwitch">Show</label> |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The favorite button uses an icon without any accessible text label. Screen reader users won't know what this button does. Add an aria-label attribute (e.g., aria-label="Toggle favorite") or use a visually-hidden text element to provide context for assistive technologies.
| <button class="btn btn-light lora-favorite-btn" type="button" (click)="toggleLoraFavorite(lora, $event)"> | |
| <button | |
| class="btn btn-light lora-favorite-btn" | |
| type="button" | |
| (click)="toggleLoraFavorite(lora, $event)" | |
| [attr.aria-label]="isLoraFavorite(lora) ? 'Remove from favorites' : 'Add to favorites'"> |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<string>(); | ||||||||||
| private loraLastUsed: Record<string, number> = {}; | ||||||||||
| 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 = {}; | ||||||||||
| } | ||||||||||
|
Comment on lines
+2529
to
+2540
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| 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); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
Comment on lines
+2543
to
+2557
|
||||||||||
|
|
||||||||||
| 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(); | ||||||||||
|
||||||||||
| return `${name}::${version}`.toLowerCase(); | |
| return JSON.stringify({ name, version }).toLowerCase(); |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getLoraKey method lowercases the name::version fallback key, but the resolveHistoryLoras method uses name::version keys without lowercasing when building the byNameVersion Map. This inconsistency could prevent proper resolution of LoRAs that don't have version_id when restoring from history, potentially causing favorites and last-used data to not match correctly. Ensure consistent casing is used in both methods.
| return `${name}::${version}`.toLowerCase(); | |
| return `${name}::${version}`; |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feature adds significant new functionality (favorites, last-used tracking, multiple sort modes) but no test coverage is provided. Since the component already has a spec file (options.component.spec.ts), consider adding tests for the new methods: loadLoraPreferences, persistLoraFavorites, persistLoraLastUsed, getLoraKey, isLoraFavorite, toggleLoraFavorite, markLoraLastUsed, and the updated filterLoras sorting logic.
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sorting logic is applied after the favorites-only filter. This means when loraFavoritesOnly is true and loraSortOrder is 'favorites', the sort will still execute but won't be meaningful since all items are already favorites. Consider optimizing by skipping the 'favorites' sort when loraFavoritesOnly is active, or applying the favorites filter after sorting to maintain better separation of concerns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lora-card overflow property changed from 'visible' to 'hidden'. If any child elements (like tooltips or dropdowns) need to overflow the card boundaries, they will now be clipped. Verify that all interactive elements within lora-card work correctly with overflow: hidden, or consider using overflow: visible for specific scenarios if needed.