refactor(xtream): refine search result card layout

This commit is contained in:
4gray committed 2026-01-12 00:55:18 +01:00
1 parent 54f4abb48b
commit 76191b415f
6 files changed
+250 -125

No files matched your search

@@ -25,9 +25,6 @@ import { MatIconModule } from '@angular/material/icon';
@if (description) {
<p>{{ description }}</p>
}
@if (type) {
<div class="type-badge" [class]="type">{{ type }}</div>
}
@if (showPlaylistInfo && playlistName) {
<div class="playlist-badge">
<mat-icon>playlist_play</mat-icon>
@@ -40,46 +37,92 @@ import { MatIconModule } from '@angular/material/icon';
`
:host {
display: block;
height: 100%;
}
mat-card {
cursor: pointer;
}
h4 {
margin: 0;
padding: 8px;
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%;
height: 200px;
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;
}
}
.type-badge {
position: absolute;
top: 8px;
right: 8px;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
text-transform: uppercase;
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: 8px;
font-size: 12px;
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;
}
}
`,
],
@@ -93,4 +136,4 @@ export class SearchResultItemComponent {
@Input() showPlaylistInfo = false;
@Output() itemClick = new EventEmitter<void>();
}
}
@@ -89,14 +89,11 @@ export class PortalChannelsListComponent implements AfterViewInit {
this.favoritesService
.getFavorites(playlist.id)
.subscribe((favorites) => {
// Map using content.id instead of xtream_id
favorites.forEach((fav: any) => {
this.favorites.set(fav.xtream_id, true);
});
console.log(this.favorites);
});
}
// Removed loadCurrentEpgData() call since we're using virtual scroll
}
ngAfterViewInit() {
@@ -1,24 +1,43 @@
<div class="search-page">
<div class="header">
<h2>{{ 'PORTALS.SIDEBAR.SEARCH' | translate }}</h2>
@if (isGlobalSearch) {
<button mat-icon-button (click)="dialogRef.close()">
<mat-icon>close</mat-icon>
</button>
}
</div>
<div class="header sticky-header">
<div class="header-top">
<div class="header-left">
<h2>{{ 'PORTALS.SIDEBAR.SEARCH' | translate }}</h2>
<div class="type-filters">
@for (filter of filterConfig; track filter.key) {
<mat-checkbox
[(ngModel)]="filters[filter.key]"
(ngModelChange)="executeSearch()"
>
{{ filter.translationKey | translate }}
</mat-checkbox>
}
</div>
</div>
<app-search-form
[(searchTerm)]="searchTerm"
[filters]="filters"
[filterConfig]="filterConfig"
/>
<div class="header-right">
@if (!isGlobalSearch && xtreamStore.searchResults().length > 0) {
<div class="results-count">
Found {{ xtreamStore.searchResults().length }} results
</div>
}
@if (isGlobalSearch) {
<button mat-icon-button (click)="dialogRef.close()">
<mat-icon>close</mat-icon>
</button>
}
</div>
</div>
@if (!isGlobalSearch && xtreamStore.searchResults().length > 0) {
<div class="results-count">
Found {{ xtreamStore.searchResults().length }} results
<div class="search-input-container">
<app-search-form
[(searchTerm)]="searchTerm"
[filters]="filters"
[filterConfig]="[]"
/>
</div>
</div>
}
<div class="results-container">
@if (xtreamStore.searchResults().length > 0) { @if (isGlobalSearch) {
@@ -93,10 +112,22 @@
</mat-card>
}
</div>
} } @else if (searchTerm && searchTerm.length >= 3) {
} } @else if (searchTerm() && searchTerm().length >= 3) {
<div class="no-items">
<mat-icon>search_off</mat-icon>
<p>No results found for "{{ searchTerm() }}"</p>
<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>
</div>
</div>
} @else {
<div class="no-items initial-state">
<mat-icon>search</mat-icon>
<div class="no-items-content">
<h3>{{ 'PORTALS.SEARCH_VIEW.INITIAL_TITLE' | translate }}</h3>
<p>{{ 'PORTALS.SEARCH_VIEW.INITIAL_DESCRIPTION' | translate }}</p>
</div>
</div>
}
</div>
@@ -7,31 +7,75 @@
}
.search-page {
padding: 20px;
padding: 0 20px 20px 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
// Use theme background variable or fallback
background: var(--background, var(--mat-sidenav-content-background-color, #fff));
padding: 20px 0 10px 0;
margin-bottom: 20px;
}
// Subtle shadow or border for separation
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
display: flex;
flex-direction: column;
align-items: stretch;
gap: 16px;
.search-container {
flex-shrink: 0;
.search-field {
width: 100%;
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.type-filters {
mat-checkbox {
margin-right: 1rem;
.header-left {
display: flex;
align-items: center;
gap: 32px;
flex: 1;
flex-wrap: wrap;
h2 {
margin: 0;
white-space: nowrap;
}
.type-filters {
display: flex;
gap: 20px;
align-items: center;
mat-checkbox {
margin: 0;
}
}
}
.results-count {
font-size: 0.9rem;
.header-right {
display: flex;
align-items: center;
gap: 16px;
.results-count {
font-size: 0.85rem;
color: var(--text-color-secondary, rgba(0,0,0,0.7));
background: rgba(0, 0, 0, 0.06);
padding: 6px 12px;
border-radius: 16px;
white-space: nowrap;
font-weight: 500;
display: flex;
align-items: center;
}
}
.search-input-container {
margin-bottom: 0;
}
}
@@ -50,54 +94,72 @@
mat-card {
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.poster,
.poster-placeholder {
border-radius: 13px;
border-radius: 8px;
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
margin-bottom: 0.5rem;
margin-bottom: 0.75rem;
}
.poster-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.05);
mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
opacity: 0.3;
}
}
h3 {
margin: 0.5rem 0;
font-size: 1rem;
font-weight: normal;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
p {
font-size: 0.8rem;
color: rgba(0, 0, 0, 0.6);
margin: 0.5rem 0;
font-size: 0.85rem;
color: var(--text-color-secondary, rgba(0, 0, 0, 0.6));
margin: 0 0 0.75rem 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.type-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
&.live {
background: #e53935;
color: white;
background: rgba(229, 57, 53, 0.1);
color: #e53935;
}
&.movie {
background: #1e88e5;
color: white;
background: rgba(30, 136, 229, 0.1);
color: #1e88e5;
}
&.series {
background: #43a047;
color: white;
background: rgba(67, 160, 71, 0.1);
color: #43a047;
}
}
}
@@ -109,18 +171,45 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
padding: 4rem 2rem;
text-align: center;
mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
margin-bottom: 1rem;
font-size: 64px;
width: 64px;
height: 64px;
margin-bottom: 1.5rem;
opacity: 0.5;
}
p {
font-size: 1.1rem;
margin: 0;
.no-items-content {
h3 {
margin: 0 0 0.5rem;
font-size: 1.5rem;
font-weight: 500;
color: var(--text-color);
}
p {
font-size: 1.1rem;
margin: 0;
opacity: 0.8;
}
.search-term-note {
margin-top: 1rem;
font-size: 0.9rem;
font-style: italic;
opacity: 0.6;
}
}
&.initial-state {
opacity: 0.8;
mat-icon {
color: var(--primary-color);
opacity: 0.6;
}
}
}
@@ -130,6 +219,11 @@
height: 100vh;
}
/* Ensure sticky header background matches overlay background */
.header {
background: var(--background);
}
.results-grid {
mat-card {
position: relative;
@@ -200,49 +294,6 @@
}
:host-context(.global-search-overlay) {
.search-page {
background: var(--background);
height: 100vh;
}
.results-grid {
mat-card {
position: relative;
.poster,
.poster-placeholder {
margin-top: 24px;
}
.playlist-badge {
position: absolute;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
padding: 6px 8px;
font-size: 12px;
color: white;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
backdrop-filter: blur(5px);
display: flex;
align-items: center;
gap: 4px;
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
flex-shrink: 0;
}
}
}
}
.playlist-group {
.playlist-title {
background: var(--surface);
@@ -51,9 +51,7 @@ interface SearchResultsData {
SearchResultItemComponent,
TranslatePipe,
],
providers: [
XtreamStore
],
providers: [],
templateUrl: './search-results.component.html',
styleUrls: ['./search-results.component.scss'],
})
@@ -112,7 +110,7 @@ export class SearchResultsComponent implements AfterViewInit {
});
}
private executeSearch() {
executeSearch() {
const types = Object.entries(this.filters)
.filter(([_, enabled]) => enabled)
.map(([type]) => type);
+5
View File
@@ -384,6 +384,7 @@
"LIBRARY": "Library",
"SEARCH": "Search",
"RECENT": "Recent",
"RECENTLY_ADDED": "Recently added",
"FAVORITES": "Favorites",
"HOME": "Home",
"PLAYLIST_INFO": "Playlist info",
@@ -422,6 +423,10 @@
"TITLE": "No results found",
"NO_SEARCH_RESULTS": "Nothing found, try to change you search request"
},
"SEARCH_VIEW": {
"INITIAL_TITLE": "Search for content",
"INITIAL_DESCRIPTION": "Enter at least 3 characters to search across live TV, movies and series."
},
"STREAM_URL_COPIED": "Stream URL copied to clipboard",
"COPY_STREAM_URL": "Copy Stream URL",
"SELECT_CATEGORY": "Select a category",