refactor(xtream): generalize xtream api related components

This commit is contained in:
4gray committed 2025-04-21 23:55:11 +02:00
1 parent e23c8f7eef
commit a302054af3
11 files changed
+123 -99

No files matched your search

@@ -19,7 +19,7 @@ import { XtreamStore } from '../xtream.store';
MatTooltip,
NgOptimizedImage,
TranslateModule,
]
],
})
export class CategoryContentViewComponent {
private readonly activatedRoute = inject(ActivatedRoute);
@@ -39,7 +39,6 @@ export class CategoryContentViewComponent {
}
onItemClick(item: any) {
console.log(item);
this.xtreamStore.setSelectedItem(item);
this.router.navigate([item.xtream_id], {
relativeTo: this.activatedRoute,
@@ -1,10 +1,6 @@
@let items = xtreamStore.getCategoriesBySelectedType();
@let contentType = xtreamStore.selectedContentType();
@let selectedCategory = xtreamStore.selectedCategoryId();
@if (items?.length > 0) {
@if (items()?.length > 0) {
<mat-nav-list>
@for (item of items; track $index) {
@for (item of items(); track $index) {
<mat-list-item
class="category-item"
[class.selected]="isSelected(item)"
@@ -14,8 +10,7 @@
item.category_name || $any(item).name || 'No category name'
}}
</mat-list-item>
}
@if (!items?.length) {
} @empty {
<app-playlist-error-view
title="No results"
[description]="
@@ -1,47 +1,30 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Output,
inject,
input,
output,
} from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { TranslatePipe } from '@ngx-translate/core';
import { XtreamCategory } from '../../../../shared/xtream-category.interface';
import { PlaylistErrorViewComponent } from '../playlist-error-view/playlist-error-view.component';
import { XtreamStore } from '../xtream.store';
@Component({
selector: 'app-category-view',
imports: [
MatCardModule,
MatListModule,
MatTooltipModule,
PlaylistErrorViewComponent,
TranslateModule,
],
imports: [MatListModule, PlaylistErrorViewComponent, TranslatePipe],
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './category-view.component.html',
styleUrls: ['./category-view.component.scss']
styleUrls: ['./category-view.component.scss'],
})
export class CategoryViewComponent {
@Output() categoryClicked = new EventEmitter<XtreamCategory>();
readonly items = input([]);
readonly selectedCategoryId = input<number>();
readonly xtreamStore = inject(XtreamStore);
private readonly route = inject(ActivatedRoute);
ngOnInit(): void {
const { categoryId } = this.route.snapshot.params;
if (categoryId)
this.xtreamStore.setSelectedCategory(Number(categoryId));
}
readonly categoryClicked = output<XtreamCategory>();
isSelected(item: XtreamCategory): boolean {
const selectedCategory = this.xtreamStore.selectedCategoryId();
const selectedCategory = this.selectedCategoryId();
const itemId = Number((item as any).category_id || item.id);
return selectedCategory !== null && selectedCategory === itemId;
return selectedCategory !== null && Number(selectedCategory) === itemId;
}
}
@@ -6,15 +6,18 @@
<button mat-icon-button (click)="backToCategories()">
<mat-icon>chevron_left</mat-icon>
</button>
<h2>Channels</h2>
<h2>{{ 'HOME.PLAYLISTS.CHANNELS' | translate }}</h2>
} @else {
<h2>Categories</h2>
<h2>{{ 'PORTALS.ALL_CATEGORIES' | translate }}</h2>
}
</div>
@if (selectedCategoryId()) {
<app-portal-channels-list (playClicked)="playLive($event)" />
} @else {
<app-category-view (categoryClicked)="selectCategory($event)" />
<app-category-view
[items]="categories()"
(categoryClicked)="selectCategory($event)"
/>
}
</div>
<div class="content-container">
@@ -32,6 +35,8 @@
</div>
</div>
} @else {
<div class="no-channel-selected">Select a category and channel</div>
<div class="no-channel-selected">
{{ 'PORTALS.SELECT_CATEGORY' | translate }}
</div>
}
</div>
@@ -11,7 +11,7 @@ import { MatIcon } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { TranslatePipe } from '@ngx-translate/core';
import { XtreamCategory } from '../../../../shared/xtream-category.interface';
import { EpgViewComponent } from '../../portals/epg-view/epg-view.component';
import { WebPlayerViewComponent } from '../../portals/web-player-view/web-player-view.component';
@@ -36,16 +36,18 @@ import { XtreamStore } from '../xtream.store';
MatInputModule,
MatListModule,
PortalChannelsListComponent,
TranslateModule,
TranslatePipe,
WebPlayerViewComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LiveStreamLayoutComponent implements OnInit {
private favoritesService = inject(FavoritesService);
private playerService = inject(PlayerService);
private readonly xtreamStore = inject(XtreamStore);
private readonly settingsStore = inject(SettingsStore);
readonly categories = this.xtreamStore.getCategoriesBySelectedType;
readonly epgItems = this.xtreamStore.epgItems;
readonly selectedCategoryId = this.xtreamStore.selectedCategoryId;
private readonly hideExternalInfoDialog =
@@ -80,7 +82,6 @@ export class LiveStreamLayoutComponent implements OnInit {
this.xtreamStore.currentPlaylist();
const streamFormat = this.settingsStore.streamFormat();
const streamUrl = `${serverUrl}/live/${username}/${password}/${item.xtream_id}.${streamFormat}`;
// TODO: offer option to select TS or m3u8
this.openPlayer(streamUrl, item.title, item.poster_url);
this.xtreamStore.setSelectedItem(item);
this.xtreamStore.loadEpg();
@@ -99,7 +100,6 @@ export class LiveStreamLayoutComponent implements OnInit {
selectCategory(category: XtreamCategory) {
const categoryId = (category as any).category_id ?? category.id;
console.log('Selected category:', categoryId);
this.xtreamStore.setSelectedCategory(categoryId);
}
@@ -18,7 +18,7 @@
routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
matTooltip="Home"
[matTooltip]="'PORTALS.SIDEBAR.HOME' | translate"
>
<mat-icon>home</mat-icon>
</button>
@@ -26,7 +26,7 @@
mat-icon-button
class="action-button"
(click)="openAccountInfo()"
matTooltip="Account Info"
[matTooltip]="'PORTALS.SIDEBAR.ACCOUNT_INFO' | translate"
>
<mat-icon>person</mat-icon>
</button>
@@ -34,7 +34,7 @@
mat-icon-button
class="action-button"
(click)="openPlaylistInfo()"
matTooltip="Playlist Info"
[matTooltip]="'PORTALS.SIDEBAR.PLAYLIST_INFO' | translate"
>
<mat-icon>playlist_play</mat-icon>
</button>
@@ -43,67 +43,75 @@
</div>
<div class="nav-section">
<div class="nav-group-label">Main</div>
<div class="nav-group-label">{{ 'PORTALS.SIDEBAR.MAIN' | translate }}</div>
<mat-nav-list>
<a
mat-list-item
(click)="xtreamStore.setSelectedContentType('vod')"
routerLink="./vod"
(click)="categoryClick.emit('vod')"
[class.active]="isContentTypeActive('vod')"
>
<mat-icon matListItemIcon>movie</mat-icon>
<span matListItemTitle>Movies</span>
<span matListItemTitle>{{
'PORTALS.SIDEBAR.MOVIES' | translate
}}</span>
</a>
<a
mat-list-item
(click)="xtreamStore.setSelectedContentType('live')"
routerLink="./live"
(click)="categoryClick.emit('live')"
[class.active]="isContentTypeActive('live')"
>
<mat-icon matListItemIcon>live_tv</mat-icon>
<span matListItemTitle>Live TV</span>
<span matListItemTitle>{{
'PORTALS.SIDEBAR.LIVE_TV' | translate
}}</span>
</a>
<a
mat-list-item
routerLink="./series"
(click)="xtreamStore.setSelectedContentType('series')"
(click)="categoryClick.emit('series')"
[class.active]="isContentTypeActive('series')"
>
<mat-icon matListItemIcon>tv</mat-icon>
<span matListItemTitle>TV Series</span>
<span matListItemTitle>{{
'PORTALS.SIDEBAR.SERIES' | translate
}}</span>
</a>
</mat-nav-list>
</div>
<div class="nav-section">
<div class="nav-group-label">Library</div>
<div class="nav-group-label">
{{ 'PORTALS.SIDEBAR.LIBRARY' | translate }}
</div>
<mat-nav-list>
<a
mat-list-item
routerLink="./search"
routerLinkActive="active"
(click)="xtreamStore.setSelectedContentType(undefined)"
(click)="pageClicked.emit('search')"
>
<mat-icon matListItemIcon>search</mat-icon>
<span matListItemTitle>Search</span>
<span matListItemTitle>{{
'PORTALS.SIDEBAR.SEARCH' | translate
}}</span>
</a>
<a
mat-list-item
routerLink="./recent"
routerLinkActive="active"
(click)="xtreamStore.setSelectedContentType(undefined)"
(click)="pageClicked.emit('recent')"
>
<mat-icon matListItemIcon>history</mat-icon>
<span matListItemTitle>Recent</span>
<span matListItemTitle>{{
'PORTALS.SIDEBAR.RECENT' | translate
}}</span>
</a>
<a
mat-list-item
routerLink="./favorites"
routerLinkActive="active"
(click)="xtreamStore.setSelectedContentType(undefined)"
(click)="pageClicked.emit('favorites')"
>
<mat-icon matListItemIcon>favorite</mat-icon>
<span matListItemTitle>Favorites</span>
<span matListItemTitle>{{
'PORTALS.SIDEBAR.FAVORITES' | translate
}}</span>
</a>
</mat-nav-list>
</div>
@@ -1,47 +1,49 @@
import { Component, inject } from '@angular/core';
import { Component, inject, input, output } from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { MatTooltip } from '@angular/material/tooltip';
import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { TranslatePipe } from '@ngx-translate/core';
import { PlaylistInfoComponent } from '../../home/recent-playlists/playlist-info/playlist-info.component';
import { SettingsComponent } from '../../settings/settings.component';
import { selectActivePlaylist } from '../../state/selectors';
import { selectPlaylistById } from '../../state/selectors';
import { AccountInfoComponent } from '../account-info/account-info.component';
import { XtreamStore } from '../xtream.store';
@Component({
selector: 'app-navigation',
imports: [
MatListModule,
MatIcon,
MatIconButton,
MatListModule,
MatTooltip,
RouterLink,
RouterLinkActive,
TranslateModule,
MatTooltipModule,
TranslatePipe,
],
templateUrl: './navigation.component.html',
styleUrl: './navigation.component.scss'
styleUrl: './navigation.component.scss',
})
export class NavigationComponent {
private readonly activatedRoute = inject(ActivatedRoute);
private readonly dialog = inject(MatDialog);
private readonly store = inject(Store);
readonly xtreamStore = inject(XtreamStore);
// Make currentPlaylist public so template can access it
readonly currentPlaylist = this.store.selectSignal(selectActivePlaylist);
readonly portalStatus = input<'active' | 'inactive' | 'expired'>();
ngOnInit() {
this.xtreamStore.checkPortalStatus();
}
readonly categoryClick = output<'vod' | 'live' | 'series'>();
readonly pageClicked = output<'search' | 'recent' | 'favorites'>();
readonly currentPlaylist = this.store.selectSignal(
selectPlaylistById(this.activatedRoute.snapshot.params.id)
);
getStatusColor(): string {
const status = this.xtreamStore.portalStatus();
switch (status) {
switch (this.portalStatus()) {
case 'active':
return 'status-active';
case 'inactive':
@@ -54,8 +56,7 @@ export class NavigationComponent {
}
getStatusIcon(): string {
const status = this.xtreamStore.portalStatus();
switch (status) {
switch (this.portalStatus()) {
case 'active':
return 'check_circle';
case 'inactive':
@@ -1,8 +1,12 @@
<div class="sidebar">
<div class="sidebar-header">
<h2>Categories</h2>
<h2>{{ 'PORTALS.ALL_CATEGORIES' | translate }}</h2>
</div>
<app-category-view (categoryClicked)="categoryClicked($event)" />
<app-category-view
[items]="categories()"
[selectedCategoryId]="selectedCategoryId()"
(categoryClicked)="categoryClicked($event)"
/>
</div>
<div class="main-content">
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { CategoryViewComponent } from './category-view/category-view.component';
import { MatIconButton } from '@angular/material/button';
@@ -20,15 +20,22 @@ import { XtreamStore } from './xtream.store';
MpvPlayerBarComponent,
MatIcon,
MatIconButton,
]
],
})
export class XtreamMainContainerComponent {
export class XtreamMainContainerComponent implements OnInit {
readonly router = inject(Router);
readonly route = inject(ActivatedRoute);
readonly xtreamStore = inject(XtreamStore);
constructor(
private router: Router,
private route: ActivatedRoute
) {}
readonly categories = this.xtreamStore.getCategoriesBySelectedType;
readonly selectedCategoryId = this.xtreamStore.selectedCategoryId;
ngOnInit(): void {
const { categoryId } = this.route.snapshot.params;
if (categoryId)
this.xtreamStore.setSelectedCategory(Number(categoryId));
}
categoryClicked(category: XtreamCategory) {
const categoryId = (category as any).category_id ?? category.id;
@@ -6,6 +6,11 @@
}
<div class="xtream-container">
<app-navigation class="navigation-sidebar" />
<app-navigation
class="navigation-sidebar"
[portalStatus]="portalStatus()"
(categoryClick)="handleCategoryClick($event)"
(pageClicked)="handlePageClick($event)"
/>
<router-outlet />
</div>
@@ -1,5 +1,5 @@
import { Component, effect, inject } from '@angular/core';
import { ActivatedRoute, RouterOutlet } from '@angular/router';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import * as PlaylistActions from '../../state/actions';
@@ -16,16 +16,18 @@ import { XtreamStore } from '../xtream.store';
RouterOutlet,
TranslateModule,
],
providers: [XtreamStore]
providers: [XtreamStore],
})
export class XtreamShellComponent {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly store = inject(Store);
private readonly xtreamStore = inject(XtreamStore);
readonly getImportCount = this.xtreamStore.getImportCount;
readonly isImporting = this.xtreamStore.isImporting;
readonly itemsToImport = this.xtreamStore.itemsToImport;
readonly portalStatus = this.xtreamStore.portalStatus;
constructor() {
effect(
@@ -39,10 +41,25 @@ export class XtreamShellComponent {
}
ngOnInit() {
this.xtreamStore.checkPortalStatus();
this.store.dispatch(
PlaylistActions.setActivePlaylist({
playlistId: this.route.snapshot.params.id,
})
);
}
handleCategoryClick(category: 'vod' | 'live' | 'series') {
this.xtreamStore.setSelectedContentType(category);
this.router.navigate([category], {
relativeTo: this.route,
});
}
handlePageClick(page: 'search' | 'recent' | 'favorites') {
this.xtreamStore.setSelectedContentType(undefined);
this.router.navigate([page], {
relativeTo: this.route,
});
}
}