| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| .channel-logo { | ||
| max-height: 48px; | ||
| overflow: hidden; | ||
| } | ||
|
|
||
| .channel-name { | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| display: -webkit-box; | ||
| -webkit-line-clamp: 1; | ||
| -webkit-box-orient: vertical; | ||
| font-size: 0.9em; | ||
| max-width: 290px; | ||
| } | ||
|
|
||
| .active { | ||
| background: #ddd; | ||
| } | ||
|
|
||
| ::ng-deep { | ||
| .dark-theme { | ||
| .active { | ||
| background: #000; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| .drag-icon { | ||
| cursor: move; | ||
| margin-left: -20px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { Component, EventEmitter, Input, Output } from '@angular/core'; | ||
|
|
||
| @Component({ | ||
| selector: 'app-channel-list-item', | ||
| styleUrls: ['./channel-list-item.component.scss'], | ||
| template: `<mat-list-item | ||
| cdkDrag | ||
| [cdkDragDisabled]="!isDraggable" | ||
| cdkDragPreviewContainer="parent" | ||
| [class.active]="selected" | ||
| (click)="clicked.emit()" | ||
| data-test-id="channel-item" | ||
| > | ||
| <mat-icon | ||
| *ngIf="isDraggable" | ||
| mat-list-icon | ||
| cdkDragHandle | ||
| class="drag-icon" | ||
| >drag_indicator</mat-icon | ||
| > | ||
| <div class="channel-logo" *ngIf="logo"> | ||
| <img [src]="logo" width="48" onerror="this.style.display='none'" /> | ||
| </div> | ||
| <p matLine class="channel-name"> | ||
| {{ name }} | ||
| </p> | ||
| <button | ||
| *ngIf="showFavoriteButton" | ||
| mat-icon-button | ||
| color="primary" | ||
| [matTooltip]="'CHANNELS.REMOVE_FAVORITE' | translate" | ||
| (click)="favoriteToggled.emit($event)" | ||
| > | ||
| <mat-icon color="accent">star</mat-icon> | ||
| </button> | ||
| <mat-divider></mat-divider | ||
| ></mat-list-item>`, | ||
| }) | ||
| export class ChannelListItemComponent { | ||
| @Input() isDraggable = false; | ||
| @Input() logo!: string; | ||
| @Input() name = ''; | ||
| @Input() showFavoriteButton = false; | ||
| @Input() selected = false; | ||
|
|
||
| @Output() clicked = new EventEmitter<void>(); | ||
| @Output() favoriteToggled = new EventEmitter(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| <div id="playlists-navigation"> | ||
| <div class="current-playlist"> | ||
| <button mat-icon-button matTooltip="Back" (click)="goBack()"> | ||
| <mat-icon>arrow_back_ios</mat-icon> | ||
| </button> | ||
| <div class="playlist-info"> | ||
| <div class="name"> | ||
| <ng-container | ||
| *ngIf="sidebarView === 'CHANNELS'; else allPlaylists" | ||
| > | ||
| {{ playlistTitle$ | async }} | ||
| </ng-container> | ||
| <ng-template #allPlaylists> | ||
| {{ 'HOME.PLAYLISTS.MY_PLAYLISTS' | translate }} | ||
| </ng-template> | ||
| </div> | ||
| <div class="channels-count"> | ||
| {{ | ||
| sidebarView === 'CHANNELS' | ||
| ? channels.length + | ||
| ' ' + | ||
| ('HOME.PLAYLISTS.CHANNELS' | translate) | ||
| : ('HOME.PLAYLISTS.MY_PLAYLISTS_SUBTITLE' | translate) | ||
| }} | ||
| </div> | ||
| </div> | ||
|
|
||
| <button | ||
| mat-icon-button | ||
| *ngIf="sidebarView === 'CHANNELS'" | ||
| routerLink="/" | ||
| [matTooltip]=" | ||
| 'CHANNELS.UPLOAD_OR_SELECT_OTHER_PLAYLIST' | translate | ||
| " | ||
| > | ||
| <mat-icon>playlist_add</mat-icon> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <mat-divider></mat-divider> | ||
| <app-channel-list-container | ||
| *ngIf="sidebarView === 'CHANNELS'; else playlistsTemplate" | ||
| [channelList]="channels" | ||
| > | ||
| </app-channel-list-container> | ||
| <ng-template #playlistsTemplate> | ||
| <app-recent-playlists | ||
| class="recent-playlists" | ||
| [class.electron]="dataService.isElectron" | ||
| (playlistClicked)="selectPlaylist()" | ||
| ></app-recent-playlists> | ||
| </ng-template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #playlists-navigation { | ||
| background-color: #000; | ||
| color: #fff; | ||
|
|
||
| .current-playlist { | ||
| display: flex; | ||
| align-items: center; | ||
| padding: 8px; | ||
| } | ||
|
|
||
| .playlist-info { | ||
| flex: 1; | ||
| max-width: 300px; | ||
|
|
||
| .name { | ||
| font-size: 1.1em; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .channels-count { | ||
| font-size: 0.8em; | ||
| color: #ccc; | ||
| text-transform: lowercase; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| .recent-playlists { | ||
| height: calc(100vh - 60px); | ||
| overflow: auto; | ||
| width: 100%; | ||
| display: block; | ||
| } | ||
|
|
||
| .electron { | ||
| height: calc(100vh - 85px) !important; | ||
| } | ||
|
|
||
| @media only screen and (max-width: 480px) { | ||
| .playlist-info { | ||
| .name { | ||
| max-width: 105px; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { Component, Input } from '@angular/core'; | ||
| import { Router } from '@angular/router'; | ||
| import { Store } from '@ngrx/store'; | ||
| import { Channel } from '../../../../../../shared/channel.interface'; | ||
| import { DataService } from '../../../../services/data.service'; | ||
| import * as PlaylistActions from '../../../../state/actions'; | ||
| import { selectPlaylistTitle } from '../../../../state/selectors'; | ||
| import { SidebarView } from '../video-player.component'; | ||
|
|
||
| @Component({ | ||
| selector: 'app-sidebar', | ||
| templateUrl: './sidebar.component.html', | ||
| styleUrls: ['./sidebar.component.scss'], | ||
| }) | ||
| export class SidebarComponent { | ||
| @Input() channels: Channel[] = []; | ||
|
|
||
| isElectron = this.dataService.isElectron; | ||
|
|
||
| playlistTitle$ = this.store.select(selectPlaylistTitle); | ||
|
|
||
| sidebarView: SidebarView = 'CHANNELS'; | ||
|
|
||
| constructor( | ||
| public dataService: DataService, | ||
| private router: Router, | ||
| private store: Store | ||
| ) {} | ||
|
|
||
| goBack(): void { | ||
| if (this.sidebarView === 'PLAYLISTS') { | ||
| this.store.dispatch(PlaylistActions.resetActiveChannel()); | ||
| this.router.navigate(['/']); | ||
| } else { | ||
| this.sidebarView = 'PLAYLISTS'; | ||
| } | ||
| } | ||
|
|
||
| selectPlaylist() { | ||
| this.sidebarView = 'CHANNELS'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| <mat-toolbar color="primary"> | ||
| <button | ||
| mat-icon-button | ||
| (click)="toggleLeftDrawerClicked.emit()" | ||
| [matTooltip]="'TOP_MENU.OPEN_CHANNELS_LIST' | translate" | ||
| > | ||
| <mat-icon>menu</mat-icon> | ||
| </button> | ||
| <button | ||
| *ngIf="(playlistId$ | async) !== 'GLOBAL_FAVORITES'" | ||
| mat-icon-button | ||
| (click)="addToFavorites(activeChannel)" | ||
| [matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate" | ||
| > | ||
| <mat-icon> | ||
| {{ | ||
| (favorites$ | async).includes(activeChannel?.id) | ||
| ? 'star' | ||
| : 'star_outline' | ||
| }}</mat-icon | ||
| > | ||
| </button> | ||
| {{ activeChannel?.name }} | ||
| <div class="spacer"></div> | ||
| <ng-container *ngIf="isEpgAvailable$ | async"> | ||
| <button | ||
| mat-icon-button | ||
| (click)="multiEpgClicked.emit()" | ||
| [matTooltip]="'TOP_MENU.OPEN_MULTI_EPG' | translate" | ||
| > | ||
| <mat-icon>view_list</mat-icon> | ||
| </button> | ||
| <button | ||
| mat-button | ||
| (click)="toggleRightDrawerClicked.emit()" | ||
| [matTooltip]="'TOP_MENU.OPEN_EPG_LIST' | translate" | ||
| > | ||
| EPG | ||
| </button> | ||
| </ng-container> | ||
| </mat-toolbar> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .spacer { | ||
| flex: 1 1 auto; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { Component, EventEmitter, Input, Output } from '@angular/core'; | ||
| import { MatSnackBar } from '@angular/material/snack-bar'; | ||
| import { Store } from '@ngrx/store'; | ||
| import { TranslateService } from '@ngx-translate/core'; | ||
| import { Channel } from '../../../../../../shared/channel.interface'; | ||
| import { updateFavorites } from '../../../../state/actions'; | ||
| import { | ||
| selectActivePlaylistId, | ||
| selectFavorites, | ||
| selectIsEpgAvailable, | ||
| } from '../../../../state/selectors'; | ||
|
|
||
| @Component({ | ||
| selector: 'app-toolbar', | ||
| templateUrl: './toolbar.component.html', | ||
| styleUrls: ['./toolbar.component.scss'], | ||
| }) | ||
| export class ToolbarComponent { | ||
| @Input() activeChannel!: Channel; | ||
| @Output() multiEpgClicked = new EventEmitter<void>(); | ||
| @Output() toggleLeftDrawerClicked = new EventEmitter<void>(); | ||
| @Output() toggleRightDrawerClicked = new EventEmitter<void>(); | ||
|
|
||
| favorites$ = this.store.select(selectFavorites); | ||
| isEpgAvailable$ = this.store.select(selectIsEpgAvailable); | ||
| playlistId$ = this.store.select(selectActivePlaylistId); | ||
|
|
||
| constructor( | ||
| private snackBar: MatSnackBar, | ||
| private store: Store, | ||
| private translateService: TranslateService | ||
| ) {} | ||
|
|
||
| /** | ||
| * Adds/removes a given channel to the favorites list | ||
| * @param channel channel to add | ||
| */ | ||
| addToFavorites(channel: Channel): void { | ||
| this.snackBar.open( | ||
| this.translateService.instant('CHANNELS.FAVORITES_UPDATED'), | ||
| null, | ||
| { duration: 2000 } | ||
| ); | ||
| this.store.dispatch(updateFavorites({ channel })); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| /* eslint-disable @typescript-eslint/no-unused-vars */ | ||
| import { Injectable } from '@angular/core'; | ||
| import { parse } from 'iptv-playlist-parser'; | ||
| import { NgxIndexedDBService } from 'ngx-indexed-db'; | ||
| import { combineLatest, map, Observable, switchMap } from 'rxjs'; | ||
| import { Channel } from '../../../shared/channel.interface'; | ||
| import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../../shared/constants'; | ||
| import { | ||
| Playlist, | ||
| PlaylistUpdateState, | ||
| } from '../../../shared/playlist.interface'; | ||
| import { | ||
| aggregateFavoriteChannels, | ||
| createFavoritesPlaylist, | ||
| createPlaylistObject, | ||
| } from '../../../shared/playlist.utils'; | ||
| import { DbStores } from '../indexed-db.config'; | ||
| import { PlaylistMeta } from '../shared/playlist-meta.type'; | ||
|
|
||
| @Injectable({ | ||
| providedIn: 'root', | ||
| }) | ||
| export class PlaylistsService { | ||
| constructor(private dbService: NgxIndexedDBService) {} | ||
|
|
||
| getAllPlaylists() { | ||
| return this.dbService.getAll<Playlist>(DbStores.Playlists).pipe( | ||
| map((data) => | ||
| data.map(({ playlist, items, header, ...rest }) => ({ | ||
| ...rest, | ||
| })) | ||
| ), | ||
| map((playlists) => | ||
| playlists.sort((a, b) => a.position - b.position) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| addPlaylist(playlist) { | ||
| return this.dbService.add(DbStores.Playlists, playlist); | ||
| } | ||
|
|
||
| getPlaylistChannels(id: string) { | ||
| let playlist$: Observable<Partial<Playlist>>; | ||
| if (id === GLOBAL_FAVORITES_PLAYLIST_ID) { | ||
| playlist$ = this.getPlaylistWithGlobalFavorites(); | ||
| } else { | ||
| playlist$ = this.dbService.getByID<Playlist>( | ||
| DbStores.Playlists, | ||
| id | ||
| ); | ||
| } | ||
| return playlist$.pipe( | ||
| map((data: Playlist) => data.playlist.items as Channel[]) | ||
| ); | ||
| } | ||
|
|
||
| deletePlaylist(playlistId: string) { | ||
| return this.dbService.delete(DbStores.Playlists, playlistId); | ||
| } | ||
|
|
||
| updatePlaylist(playlistId: string, updatedPlaylist: Playlist) { | ||
| return this.getPlaylistById(playlistId).pipe( | ||
| switchMap((currentPlaylist: Playlist) => | ||
| this.dbService.update(DbStores.Playlists, { | ||
| ...currentPlaylist, | ||
| ...updatedPlaylist, | ||
| count: updatedPlaylist.playlist.items.length, | ||
| updateDate: Date.now(), | ||
| updateState: PlaylistUpdateState.UPDATED, | ||
| }) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| getPlaylistById(id: string) { | ||
| return this.dbService.getByID<Playlist>(DbStores.Playlists, id); | ||
| } | ||
|
|
||
| updatePlaylistMeta(updatedPlaylist: PlaylistMeta) { | ||
| return this.getPlaylistById(updatedPlaylist._id).pipe( | ||
| switchMap((playlist) => | ||
| this.dbService.update(DbStores.Playlists, { | ||
| ...playlist, | ||
| title: updatedPlaylist.title, | ||
| autoRefresh: updatedPlaylist.autoRefresh, | ||
| }) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| updateFavorites(id: string, favorites: string[]) { | ||
| return this.getPlaylistById(id).pipe( | ||
| switchMap((playlist) => | ||
| this.dbService.update(DbStores.Playlists, { | ||
| ...playlist, | ||
| favorites, | ||
| }) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| updateManyPlaylists(playlists: Playlist[]) { | ||
| return combineLatest( | ||
| playlists.map((playlist) => { | ||
| return this.dbService.update(DbStores.Playlists, { | ||
| ...playlist, | ||
| updateDate: Date.now(), | ||
| autoRefresh: true, | ||
| }); | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| getFavoriteChannels(playlistId: string) { | ||
| return this.dbService | ||
| .getByID<Playlist>(DbStores.Playlists, playlistId) | ||
| .pipe( | ||
| map((data) => | ||
| data.playlist.items.filter((channel) => | ||
| data.favorites.includes((channel as Channel).id) | ||
| ) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| updatePlaylistPositions( | ||
| positionUpdates: { | ||
| id: string; | ||
| changes: { position: number }; | ||
| }[] | ||
| ) { | ||
| return combineLatest( | ||
| positionUpdates.map((item, index) => { | ||
| return this.dbService.getByID(DbStores.Playlists, item.id).pipe( | ||
| switchMap((playlist: Playlist) => | ||
| this.dbService.update(DbStores.Playlists, { | ||
| ...playlist, | ||
| position: item.changes.position, | ||
| }) | ||
| ) | ||
| ); | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| handlePlaylistParsing( | ||
| uploadType: 'FILE' | 'URL' | 'TEXT', | ||
| playlist: string, | ||
| title: string, | ||
| path?: string | ||
| ) { | ||
| const parsedPlaylist = parse(playlist); | ||
| return createPlaylistObject(title, parsedPlaylist, path, uploadType); | ||
| } | ||
|
|
||
| getPlaylistWithGlobalFavorites() { | ||
| return this.dbService.getAll(DbStores.Playlists).pipe( | ||
| map((playlists: Playlist[]) => { | ||
| const favoriteChannels = aggregateFavoriteChannels(playlists); | ||
| const favPlaylist = createFavoritesPlaylist(favoriteChannels); | ||
| return favPlaylist; | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| addManyPlaylists(playlists: Playlist[]) { | ||
| return this.dbService.bulkAdd(DbStores.Playlists, playlists as any); // TODO: update ngx-indexed-db | ||
| } | ||
|
|
||
| getPlaylistsForAutoUpdate() { | ||
| return this.dbService.getAll(DbStores.Playlists).pipe( | ||
| map((playlists: Playlist[]) => { | ||
| return playlists | ||
| .filter((item) => item.autoRefresh) | ||
| .map( | ||
| ({ playlist, header, items, favorites, ...rest }) => | ||
| rest | ||
| ); | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| setFavorites(playlistId: string, favorites: string[]) { | ||
| return this.getPlaylistById(playlistId).pipe( | ||
| switchMap((playlist) => | ||
| this.dbService.update(DbStores.Playlists, { | ||
| ...playlist, | ||
| favorites, | ||
| }) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| getRawPlaylistById(id: string) { | ||
| return this.dbService.getByID<Playlist>(DbStores.Playlists, id).pipe( | ||
| map((playlist) => { | ||
| return ( | ||
| // eslint-disable-next-line @typescript-eslint/restrict-plus-operands | ||
| playlist.playlist.header.raw + | ||
| '\n' + | ||
| playlist.playlist.items.map((item) => item.raw).join('\n') | ||
| ); | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| getAllData() { | ||
| return this.dbService.getAll<Playlist>(DbStores.Playlists); | ||
| } | ||
|
|
||
| removeAll() { | ||
| return this.dbService.clear(DbStores.Playlists); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,4 +9,5 @@ export enum Language { | |
| SPANISH = 'es', | ||
| CHINESE = 'zh', | ||
| FRENCH = 'fr', | ||
| ITALIAN = 'it', | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,41 @@ | ||
| button { | ||
| cursor: pointer !important; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| .settings-container { | ||
| overflow: auto; | ||
| height: calc(100vh - 140px); | ||
| } | ||
|
|
||
| .row { | ||
| display: flex; | ||
| padding: 10px; | ||
|
|
||
| :nth-child(2) { | ||
| text-align: right; | ||
|
|
||
| mat-form-field { | ||
| font-size: 14px; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| .column { | ||
| flex: 1; | ||
|
|
||
| p { | ||
| font-size: 0.9em; | ||
| opacity: 0.6; | ||
| padding: 2px 0; | ||
| margin: 0; | ||
| } | ||
| } | ||
|
|
||
| .action-buttons { | ||
| margin-top: 10px; | ||
|
|
||
| button { | ||
| margin: 0 10px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,65 @@ | ||
| import { Component, Inject } from '@angular/core'; | ||
| import { MAT_DIALOG_DATA } from '@angular/material/dialog'; | ||
| import { MatIconModule } from '@angular/material/icon'; | ||
| import { TranslateModule } from '@ngx-translate/core'; | ||
|
|
||
| @Component({ | ||
| standalone: true, | ||
| imports: [MatIconModule, TranslateModule], | ||
| template: ` | ||
| <div mat-dialog-content> | ||
| <img src="./assets/icons/icon-tv-256.png" width="128" /><br /> | ||
| <h2 mat-dialog-title>{{ 'ABOUT.TITLE' | translate }}</h2> | ||
| <p>{{ 'ABOUT.DESCRIPTION' | translate }}</p> | ||
| <p>{{ 'ABOUT.VERSION' | translate }}: {{ appVersion }}</p> | ||
| <p> | ||
| <a | ||
| href="https://github.com/4gray/iptvnator" | ||
| target="_blank" | ||
| [title]="'ABOUT.GITHUB_TOOLTIP' | translate" | ||
| [attr.aria-label]="'ABOUT.GITHUB_TOOLTIP' | translate" | ||
| ><img | ||
| src="./assets/icons/github-light.png" | ||
| [title]="'ABOUT.GITHUB_TOOLTIP' | translate" /></a | ||
| > | ||
| <a | ||
| href="http://twitter.com/share?text=IPTVnator — free cross-platform IPTV player. Available as PWA and as native application.&url=https://github.com/4gray/iptvnator&hashtags=iptv,m3u,video-player" | ||
| [title]="'ABOUT.TWITTER_TOOLTIP' | translate" | ||
| > | ||
| <img | ||
| height="32" | ||
| src="./assets/icons/twitter-light.png" | ||
| [title]="'ABOUT.TWITTER_TOOLTIP' | translate" | ||
| /> | ||
| </a> | ||
| <a | ||
| href="https://www.buymeacoffee.com/4gray" | ||
| target="_blank" | ||
| [title]="'ABOUT.BUY_ME_A_COFFEE_TOOLTIP' | translate" | ||
| [attr.aria-label]=" | ||
| 'ABOUT.BUY_ME_A_COFFEE_TOOLTIP' | translate | ||
| " | ||
| ><mat-icon>local_cafe</mat-icon></a | ||
| > | ||
| </p> | ||
| </div> | ||
| `, | ||
| styles: [ | ||
| ` | ||
| button { | ||
| text-transform: uppercase; | ||
| } | ||
| a { | ||
| color: #fff; | ||
| } | ||
| .mat-icon { | ||
| font-size: 32px; | ||
| } | ||
| `, | ||
| ], | ||
| }) | ||
| export class AboutDialogComponent { | ||
| constructor(@Inject(MAT_DIALOG_DATA) public appVersion: string) {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export interface ConfirmDialogData { | ||
| title: string; | ||
| message: string; | ||
| confirmLabel?: string; | ||
| cancelLabel?: string; | ||
| onConfirm: () => void; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { Injectable, Pipe, PipeTransform } from '@angular/core'; | ||
|
|
||
| @Pipe({ | ||
| name: 'filterBy', | ||
| }) | ||
| @Injectable() | ||
| export class FilterPipe implements PipeTransform { | ||
| transform(array: any[], filter: string, property: string): any { | ||
| if (!array || !filter) { | ||
| return array; | ||
| } | ||
| return array.filter((item) => | ||
| item[property].toLowerCase().includes(filter.toLowerCase()) | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,4 +13,6 @@ export type PlaylistMeta = Pick< | |
| | 'updateDate' | ||
| | 'updateState' | ||
| | 'position' | ||
| | 'autoRefresh' | ||
| | 'favorites' | ||
| >; | ||