refactor: refactor favorites and recently viewed components to use content card

This commit is contained in:
4gray committed 2026-02-04 22:13:23 +01:00
1 parent e6923d3fff
commit 2f1e455330
10 files changed
+55 -410

No files matched your search

@@ -38,40 +38,16 @@
@if (favorites().length) {
<div class="items-grid">
@for (item of favorites(); track $index) {
<div class="item-card" (click)="openFavorite(item)">
<div class="thumbnail-wrapper">
<button
mat-icon-button
class="remove-button"
[matTooltip]="'CHANNELS.REMOVE_FAVORITE' | translate"
(click)="$event.stopPropagation(); removeFromFavorites(item)"
>
<mat-icon>close</mat-icon>
</button>
<img
[src]="
item.poster_url ||
item.cover ||
'./assets/images/default-poster.png'
"
(error)="
$event.target.src =
'./assets/images/default-poster.png'
"
[alt]="item.title"
class="poster"
/>
<span class="type-badge" [class]="item.type">
{{ item.type }}
</span>
</div>
<div class="item-info">
<h3>{{ item?.title || item?.details?.info?.name }}</h3>
<p class="added-date">
{{ item?.added_at | date: 'd MMM, HH:mm' }}
</p>
</div>
</div>
<app-content-card
[posterUrl]="item.poster_url || item.cover"
[title]="item?.title || item?.details?.info?.name || 'Unknown'"
[type]="item.type"
[date]="item.added_at"
[showRemoveButton]="true"
[removeTooltip]="'CHANNELS.REMOVE_FAVORITE' | translate"
(cardClick)="openFavorite(item)"
(remove)="removeFromFavorites(item)"
/>
}
</div>
} @else {
@@ -1,4 +1,5 @@
@use '@angular/material' as mat;
@use '../../../shared/styles/content-grid' as grid;
:host {
display: flex;
@@ -47,107 +48,13 @@
}
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 20px;
@include grid.content-grid(140px, 20px);
padding: 16px;
width: 100%;
box-sizing: border-box;
margin: 0;
}
.item-card {
cursor: pointer;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.thumbnail-wrapper {
position: relative;
.poster {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
display: block;
}
.remove-button {
position: absolute;
top: 4px;
right: 4px;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
width: 28px;
height: 28px;
padding: 0;
&:hover {
background: rgba(0, 0, 0, 0.8);
}
mat-icon {
color: white;
font-size: 18px;
width: 18px;
height: 18px;
}
}
.type-badge {
position: absolute;
top: 6px;
left: 6px;
padding: 3px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.6);
color: #fff;
&.live {
color: #ef5350;
}
&.movie {
color: #42a5f5;
}
&.series {
color: #66bb6a;
}
}
}
.item-info {
padding: 8px 10px;
h3 {
margin: 0;
font-size: 0.8rem;
font-weight: 500;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.added-date {
margin: 4px 0 0;
font-size: 0.7rem;
opacity: 0.6;
}
}
}
.no-items {
display: flex;
flex-direction: column;
@@ -1,12 +1,9 @@
import { DatePipe } from '@angular/common';
import { Component, input, output } from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
import { PlaylistSwitcherComponent, ResizableDirective } from 'components';
import { CategoryViewComponent } from '../../../xtream-tauri/category-view/category-view.component';
import { ContentCardComponent } from '../content-card/content-card.component';
@Component({
selector: 'app-favorites-layout',
@@ -17,11 +14,8 @@ import { CategoryViewComponent } from '../../../xtream-tauri/category-view/categ
],
imports: [
CategoryViewComponent,
DatePipe,
MatCardModule,
ContentCardComponent,
MatIcon,
MatIconButton,
MatTooltip,
PlaylistSwitcherComponent,
ResizableDirective,
TranslatePipe,
@@ -1 +1,2 @@
export * from './content-card/content-card.component';
export * from './header/header.component';
@@ -32,39 +32,16 @@
</h3>
<div class="items-grid">
@for (item of $any(group).value; track item.id) {
<div class="item-card" (click)="openItem(item)">
<div class="thumbnail-wrapper">
<button
mat-icon-button
class="remove-button"
matTooltip="Remove from history"
(click)="removeItem($event, item.id)"
>
<mat-icon>close</mat-icon>
</button>
<img
[src]="
item.poster_url ||
'./assets/images/default-poster.png'
"
(error)="
$event.target.src =
'./assets/images/default-poster.png'
"
[alt]="item.title"
class="poster"
/>
<span class="type-badge" [class]="item.type">
{{ item.type }}
</span>
</div>
<div class="item-info">
<h3>{{ item.title }}</h3>
<p class="viewed-at">
{{ item.viewed_at | date : 'd MMM, HH:mm' }}
</p>
</div>
</div>
<app-content-card
[posterUrl]="item.poster_url"
[title]="item.title"
[type]="item.type"
[date]="item.viewed_at"
[showRemoveButton]="true"
[removeTooltip]="'Remove from history'"
(cardClick)="openItem(item)"
(remove)="onRemoveItem(item.id)"
/>
}
</div>
</div>
@@ -73,35 +50,16 @@
} @else {
<div class="items-grid">
@for (item of recentItems(); track item.id) {
<div class="item-card" (click)="openItem(item)">
<div class="thumbnail-wrapper">
<button
mat-icon-button
class="remove-button"
matTooltip="Remove from history"
(click)="removeItem($event, item.id)"
>
<mat-icon>close</mat-icon>
</button>
<img
[src]="item.poster_url || './assets/images/default-poster.png'"
(error)="
$event.target.src = './assets/images/default-poster.png'
"
[alt]="item.title"
class="poster"
/>
<span class="type-badge" [class]="item.type">
{{ item.type }}
</span>
</div>
<div class="item-info">
<h3>{{ item.title }}</h3>
<p class="viewed-at">
{{ item.viewed_at | date : 'd MMM, HH:mm' }}
</p>
</div>
</div>
<app-content-card
[posterUrl]="item.poster_url"
[title]="item.title"
[type]="item.type"
[date]="item.viewed_at"
[showRemoveButton]="true"
[removeTooltip]="'Remove from history'"
(cardClick)="openItem(item)"
(remove)="onRemoveItem(item.id)"
/>
}
</div>
} } @else {
@@ -1,3 +1,5 @@
@use '../../shared/styles/content-grid' as grid;
:host {
display: flex;
flex-direction: column;
@@ -24,101 +26,7 @@
}
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 20px;
}
.item-card {
cursor: pointer;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.thumbnail-wrapper {
position: relative;
.poster {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
display: block;
}
.remove-button {
position: absolute;
top: 4px;
right: 4px;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
width: 28px;
height: 28px;
padding: 0;
&:hover {
background: rgba(0, 0, 0, 0.8);
}
mat-icon {
color: white;
font-size: 18px;
width: 18px;
height: 18px;
}
}
.type-badge {
position: absolute;
top: 6px;
left: 6px;
padding: 3px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.6);
color: #fff;
&.live {
color: #ef5350;
}
&.movie {
color: #42a5f5;
}
&.series {
color: #66bb6a;
}
}
}
.item-info {
padding: 8px 10px;
h3 {
margin: 0;
font-size: 0.8rem;
font-weight: 500;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.viewed-at {
margin: 4px 0 0;
font-size: 0.7rem;
opacity: 0.6;
}
}
@include grid.content-grid(140px, 20px);
}
.no-items {
@@ -180,4 +88,4 @@
background: var(--surface);
}
}
}
}
@@ -1,22 +1,21 @@
import { DatePipe, KeyValuePipe } from '@angular/common';
import { KeyValuePipe } from '@angular/common';
import { Component, computed, inject, Optional } from '@angular/core';
import { MatButton, MatIconButton } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { ActivatedRoute, Router } from '@angular/router';
import groupBy from 'lodash/groupBy';
import { ContentCardComponent } from '../../shared/components/content-card/content-card.component';
import { XtreamStore } from '../stores/xtream.store';
@Component({
selector: 'app-recently-viewed',
imports: [
DatePipe,
ContentCardComponent,
KeyValuePipe,
MatButton,
MatIcon,
MatIconButton,
MatTooltip,
],
providers: [],
templateUrl: './recently-viewed.component.html',
@@ -88,8 +87,7 @@ export class RecentlyViewedComponent {
}
}
removeItem(event: Event, itemId: number) {
event.stopPropagation();
onRemoveItem(itemId: number) {
this.xtreamStore.removeRecentItem({
itemId,
playlistId: this.currentPlaylist().id,
@@ -75,31 +75,13 @@
<!-- Playlist-level Search Results -->
<div class="results-grid">
@for (item of xtreamStore.searchResults(); track item.id) {
<div class="result-card" (click)="selectItem(item)">
<div class="thumbnail-wrapper">
@if (item.poster_url) {
<img
[src]="item.poster_url"
[alt]="item.title"
(error)="
$event.target.src =
'./assets/images/default-poster.png'
"
class="poster"
/>
} @else {
<div class="poster-placeholder">
<mat-icon>{{ item.type === 'live' ? 'live_tv' : item.type === 'series' ? 'tv' : 'movie' }}</mat-icon>
</div>
}
<span class="type-badge" [class]="item.type">
{{ item.type }}
</span>
</div>
<div class="card-info">
<h3>{{ item.title }}</h3>
</div>
</div>
<app-content-card
[posterUrl]="item.poster_url"
[title]="item.title"
[type]="item.type"
[showPlaceholder]="true"
(cardClick)="selectItem(item)"
/>
}
</div>
} } @else if (searchTerm() && searchTerm().length >= 3) {
@@ -1,3 +1,5 @@
@use '../../shared/styles/content-grid' as grid;
:host {
display: flex;
flex-direction: column;
@@ -80,89 +82,8 @@
overflow-y: auto;
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 20px;
@include grid.content-grid(140px, 20px);
padding-bottom: 1rem;
.result-card {
cursor: pointer;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.thumbnail-wrapper {
position: relative;
.poster,
.poster-placeholder {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
display: block;
}
.poster-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: rgba(128, 128, 128, 0.1);
mat-icon {
font-size: 36px;
width: 36px;
height: 36px;
opacity: 0.25;
}
}
.type-badge {
position: absolute;
top: 6px;
left: 6px;
padding: 3px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.6);
color: #fff;
&.live {
color: #ef5350;
}
&.movie {
color: #42a5f5;
}
&.series {
color: #66bb6a;
}
}
}
.card-info {
padding: 8px 10px;
h3 {
margin: 0;
font-size: 0.8rem;
font-weight: 500;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
}
}
}
@@ -10,7 +10,6 @@ import {
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatIconButton } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import {
MAT_DIALOG_DATA,
@@ -26,6 +25,7 @@ import groupBy from 'lodash/groupBy';
import { DatabaseService } from 'services';
import { XtreamContentItem } from '../data-sources/xtream-data-source.interface';
import { ContentType } from '../xtream-state';
import { ContentCardComponent } from '../../shared/components/content-card/content-card.component';
import { SearchFormComponent } from '../../shared/components/search-form/search-form.component';
import { SearchResultItemComponent } from '../../shared/components/search-result-item/search-result-item.component';
import { XtreamStore } from '../stores/xtream.store';
@@ -38,9 +38,9 @@ interface SearchResultsData {
@Component({
selector: 'app-search-results',
imports: [
ContentCardComponent,
FormsModule,
KeyValuePipe,
MatCardModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,