style: enhance favorites and recently viewed UI with improved layout and functionality

This commit is contained in:
4gray committed 2026-02-04 00:41:03 +01:00
1 parent 139944281c
commit d64283e33d
5 files changed
+243 -184

No files matched your search

@@ -38,46 +38,40 @@
@if (favorites().length) {
<div class="items-grid">
@for (item of favorites(); track $index) {
<mat-card class="item-card" (click)="openFavorite(item)">
<img
[src]="
item.poster_url ||
item.cover ||
'assets/default-' +
selectedCategoryId() +
'.png'
"
(error)="
$event.target.src =
<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'
"
[alt]="item.title"
class="item-image"
/>
<mat-card-content>
<h3>
{{ item?.title || item?.details?.info?.name }}
</h3>
"
(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">
Added:
{{ item?.added_at | date: 'd MMMM, y, HH:MM' }}
{{ item?.added_at | date: 'd MMM, HH:mm' }}
</p>
</mat-card-content>
<button
mat-icon-button
color="warn"
class="favorite-button"
[matTooltip]="
'CHANNELS.REMOVE_FAVORITE' | translate
"
(click)="
$event.stopPropagation();
removeFromFavorites(item)
"
>
<mat-icon>delete</mat-icon>
</button>
</mat-card>
</div>
</div>
}
</div>
} @else {
@@ -48,8 +48,8 @@
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 20px;
padding: 16px;
width: 100%;
box-sizing: border-box;
@@ -57,54 +57,95 @@
}
.item-card {
position: relative;
cursor: pointer;
transition: transform 0.2s;
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: scale(1.02);
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
}
.item-image {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
}
.thumbnail-wrapper {
position: relative;
.favorite-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
}
.poster {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
display: block;
}
mat-card-content {
padding: 12px;
flex-grow: 1;
.remove-button {
position: absolute;
top: 4px;
right: 4px;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
width: 28px;
height: 28px;
padding: 0;
h3 {
margin: 0;
font-weight: normal;
font-size: 14px;
line-height: 1.2;
max-height: 2.4em;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: normal;
&: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;
}
}
}
}
.added-date {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
margin: 4px 0 0 0;
.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 {
@@ -32,36 +32,39 @@
</h3>
<div class="items-grid">
@for (item of $any(group).value; track item.id) {
<mat-card class="item-card" (click)="openItem(item)">
<button
mat-icon-button
class="remove-button"
(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 =
<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'
"
[alt]="item.title"
class="poster"
/>
"
(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]="item.type" class="type-badge">
{{ item.type | titlecase }}
</p>
<p class="viewed-at">
{{ item.viewed_at | date : 'd MMMM, y, HH:mm' }}
{{ item.viewed_at | date : 'd MMM, HH:mm' }}
</p>
</div>
</mat-card>
</div>
}
</div>
</div>
@@ -70,32 +73,35 @@
} @else {
<div class="items-grid">
@for (item of recentItems(); track item.id) {
<mat-card class="item-card" (click)="openItem(item)">
<button
mat-icon-button
class="remove-button"
(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"
/>
<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]="item.type" class="type-badge">
{{ item.type | titlecase }}
</p>
<p class="viewed-at">
{{ item.viewed_at | date : 'd MMMM, y, hh:mm' }}
{{ item.viewed_at | date : 'd MMM, HH:mm' }}
</p>
</div>
</mat-card>
</div>
}
</div>
} } @else {
@@ -25,59 +25,99 @@
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 20px;
}
.item-card {
cursor: pointer;
transition: transform 0.2s;
position: relative;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: scale(1.02);
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.remove-button {
position: absolute;
top: 5px;
right: 5px;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
&:hover {
background: rgba(0, 0, 0, 0.7);
.thumbnail-wrapper {
position: relative;
.poster {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
display: block;
}
mat-icon {
color: white;
.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;
}
}
}
}
.poster {
width: 100%;
height: 280px;
object-fit: cover;
}
.item-info {
padding: 8px 10px;
.item-info {
padding: 16px;
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;
}
h3 {
margin: 0;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
p {
margin: 8px 0;
}
.viewed-at {
font-size: 12px;
.viewed-at {
margin: 4px 0 0;
font-size: 0.7rem;
opacity: 0.6;
}
}
}
@@ -93,26 +133,6 @@
}
}
.type-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
text-transform: uppercase;
&.live {
background: #e53935;
color: white;
}
&.movie {
background: #1e88e5;
color: white;
}
&.series {
background: #43a047;
color: white;
}
}
.playlist-section {
.section-title {
margin: 0 0 1.5rem;
@@ -1,9 +1,9 @@
import { DatePipe, KeyValuePipe, TitleCasePipe } from '@angular/common';
import { DatePipe, KeyValuePipe } from '@angular/common';
import { Component, computed, inject, Optional } from '@angular/core';
import { MatButton, MatIconButton } from '@angular/material/button';
import { MatCard } from '@angular/material/card';
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 { XtreamStore } from '../stores/xtream.store';
@@ -14,11 +14,9 @@ import { XtreamStore } from '../stores/xtream.store';
DatePipe,
KeyValuePipe,
MatButton,
MatCard,
MatIcon,
MatIconButton,
MatIconButton,
TitleCasePipe,
MatTooltip,
],
providers: [],
templateUrl: './recently-viewed.component.html',