feat: enhance search functionality with improved filtering and UI updates

This commit is contained in:
4gray committed 2026-02-08 19:18:05 +01:00
1 parent f5edba7d4c
commit c253db4cef
10 files changed
+192 -203

No files matched your search

@@ -1,5 +1,9 @@
<div class="search-container">
<mat-form-field appearance="outline" class="search-field">
<mat-form-field
appearance="outline"
class="search-field"
subscriptSizing="dynamic"
>
<mat-label>{{ placeholder }}</mat-label>
<input
#searchInput
@@ -8,7 +12,13 @@
[ngModel]="searchTerm()"
(ngModelChange)="onSearchTermChange()"
/>
<mat-icon matSuffix>search</mat-icon>
@if (searchTerm()) {
<button mat-icon-button matSuffix (click)="clearSearch()">
<mat-icon>close</mat-icon>
</button>
} @else {
<mat-icon matSuffix>search</mat-icon>
}
</mat-form-field>
@if (filterConfig.length > 0) {
@@ -9,6 +9,7 @@ import {
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
@@ -33,6 +34,7 @@ export interface SearchFilter {
MatInputModule,
MatIconModule,
MatCheckboxModule,
MatButtonModule,
TranslatePipe,
],
templateUrl: './search-form.component.html',
@@ -76,6 +78,11 @@ export class SearchFormComponent {
}, 500);
}
clearSearch() {
this.searchInput.nativeElement.value = '';
this.searchTerm.set('');
}
focusSearchInput() {
this.searchInput.nativeElement.focus();
}
@@ -17,7 +17,10 @@
<div class="header-right">
@if (showResultsCount() && resultsCount() > 0) {
<div class="results-count">
Found {{ resultsCount() }} results
{{
'PORTALS.SEARCH_VIEW.RESULTS_FOUND'
| translate: { count: resultsCount() }
}}
</div>
}
@if (showCloseButton()) {
@@ -36,6 +39,9 @@
(searchTermChange)="onSearchTermChange($event)"
/>
</div>
<!-- Subheader slot (info notes, secondary controls) -->
<ng-content select="[subheader]" />
</div>
<div class="results-container">
@@ -52,7 +58,12 @@
<div class="no-items-content">
<h3>{{ 'PORTALS.EMPTY_LIST_VIEW.TITLE' | translate }}</h3>
<p>{{ 'PORTALS.EMPTY_LIST_VIEW.NO_SEARCH_RESULTS' | translate }}</p>
<p class="search-term-note">No results found for "{{ searchTerm() }}"</p>
<p class="search-term-note">
{{
'PORTALS.SEARCH_VIEW.NO_RESULTS_FOR'
| translate: { term: searchTerm() }
}}
</p>
</div>
</div>
} @else if (showInitialState) {
@@ -20,7 +20,7 @@
flex-direction: column;
flex: 1;
overflow-y: auto;
padding: 0 20px 20px 20px;
padding: 0 20px 0 20px;
}
.header {
@@ -29,7 +29,7 @@
z-index: 100;
background: var(--background, var(--mat-sidenav-content-background-color, #fff));
padding: 20px 0 10px 0;
margin-bottom: 20px;
margin-bottom: 2px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
display: flex;
@@ -154,6 +154,9 @@
// Global search overlay context
:host-context(.global-search-overlay) {
// Prevent Electron's app-region: drag from bleeding through the dialog overlay
app-region: no-drag;
.search-page {
background: var(--background);
height: 100vh;
@@ -1,139 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
@Component({
selector: 'app-search-result-item',
imports: [MatCardModule, MatIconModule],
template: `
<mat-card (click)="itemClick.emit()">
@if (posterUrl) {
<img
[src]="posterUrl"
[alt]="title"
(error)="
$event.target.src = './assets/images/default-poster.png'
"
class="poster"
/>
} @else {
<div class="poster-placeholder">
<mat-icon>movie</mat-icon>
</div>
}
<h4>{{ title }}</h4>
@if (description) {
<p>{{ description }}</p>
}
@if (showPlaylistInfo && playlistName) {
<div class="playlist-badge">
<mat-icon>playlist_play</mat-icon>
<span>{{ playlistName }}</span>
</div>
}
</mat-card>
`,
styles: [
`
:host {
display: block;
height: 100%;
}
mat-card {
cursor: pointer;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
background: transparent !important;
box-shadow: none !important;
border: none !important;
padding: 0 !important;
}
.poster {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
display: block;
border-radius: 8px;
}
.poster-placeholder {
width: 100%;
aspect-ratio: 2/3;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.05);
border-radius: 8px;
mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
opacity: 0.3;
}
}
h4 {
margin: 0;
padding: 10px 0 2px 0;
font-size: 0.9rem;
font-weight: 500;
line-height: 1.3em;
height: 2.6em; /* Fixed height for 2 lines */
color: var(--text-color, #fff);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
p {
margin: 0;
padding: 0 0 10px 0;
font-size: 0.75rem;
line-height: 1.2em;
height: 1.2em; /* Fixed height for 1 line */
color: var(--text-color-secondary, rgba(255, 255, 255, 0.7));
opacity: 0.9; /* Improved readability */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.playlist-badge {
display: flex;
align-items: center;
gap: 4px;
margin-top: auto;
padding: 0 0 10px 0;
font-size: 11px;
opacity: 0.8;
color: var(--text-color-secondary, inherit);
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
}
}
`,
],
})
export class SearchResultItemComponent {
@Input() title = '';
@Input() description = '';
@Input() posterUrl = '';
@Input() type = '';
@Input() playlistName = '';
@Input() showPlaylistInfo = false;
@Output() itemClick = new EventEmitter<void>();
}