mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
refactor: refactor favorites and recently viewed components to use content card
This commit is contained in:
1 parent
e6923d3fff
commit
2f1e455330
10 files changed
+55
-410
No files matched your search
+10
-34
@@ -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 {
|
||||
|
||||
+2
-95
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user