mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(xtream): implement portal channels list and enhance layout for improved navigation
This commit is contained in:
1 parent
e4569556a1
commit
89ec644d9f
6 files changed
+317
-253
No files matched your search
@@ -1,74 +1,24 @@
|
||||
<div
|
||||
[class]="
|
||||
player === 'videojs' || player === 'html5'
|
||||
? 'itv-container'
|
||||
: 'itv-container-epg'
|
||||
"
|
||||
>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-content">
|
||||
<div class="search-panel">
|
||||
<mat-form-field class="search-bar">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
spellcheck="false"
|
||||
type="search"
|
||||
[placeholder]="'CHANNELS.SEARCH_CHANNEL' | translate"
|
||||
[(ngModel)]="searchString"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-nav-list>
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="48"
|
||||
class="scroll-viewport-portals"
|
||||
>
|
||||
<mat-list-item
|
||||
*cdkVirtualFor="
|
||||
let item of channels()
|
||||
| filterBy: searchString() : 'title';
|
||||
trackBy: trackBy;
|
||||
templateCacheSize: 0
|
||||
"
|
||||
(click)="playLive(item)"
|
||||
[activated]="
|
||||
xtreamStore.selectedItem()?.xtream_id ===
|
||||
item.xtream_id
|
||||
"
|
||||
>
|
||||
<img
|
||||
matListItemAvatar
|
||||
class="stream-icon"
|
||||
*ngIf="item.poster_url"
|
||||
[src]="item.poster_url"
|
||||
(error)="
|
||||
$event.target.src =
|
||||
'./assets/images/default-poster.png'
|
||||
"
|
||||
/>
|
||||
<div class="title" matListItemTitle>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<button
|
||||
mat-icon-button
|
||||
matListItemMeta
|
||||
(click)="toggleFavorite($event, item)"
|
||||
>
|
||||
<mat-icon>
|
||||
{{
|
||||
favorites.get(item.xtream_id)
|
||||
? 'star'
|
||||
: 'star_outline'
|
||||
}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</mat-nav-list>
|
||||
</div>
|
||||
@let isEmbeddedPlayer = player() === 'videojs' || player() === 'html5';
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
@if (selectedCategoryId()) {
|
||||
<button mat-icon-button (click)="backToCategories()">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<h2>Channels</h2>
|
||||
} @else {
|
||||
<h2>Categories</h2>
|
||||
}
|
||||
</div>
|
||||
@if (player === 'videojs' || player === 'html5') {
|
||||
@if (selectedCategoryId()) {
|
||||
<app-portal-channels-list (playClicked)="playLive($event)" />
|
||||
} @else {
|
||||
<app-category-view (categoryClicked)="selectCategory($event)" />
|
||||
}
|
||||
</div>
|
||||
<div [class]="isEmbeddedPlayer ? 'itv-container' : 'itv-container-epg'">
|
||||
@if (isEmbeddedPlayer) {
|
||||
@if (streamUrl) {
|
||||
<div class="video-player">
|
||||
<app-web-player-view [streamUrl]="streamUrl" />
|
||||
@@ -77,13 +27,13 @@
|
||||
}
|
||||
@if (streamUrl) {
|
||||
<div class="epg">
|
||||
<div class="epg-content">
|
||||
<div [class]="isEmbeddedPlayer ? 'epg-content' : ''">
|
||||
@if (epgItems().length > 0) {
|
||||
<app-epg-view [epgItems]="epgItems()" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="no-channel-selected">Please select a channel</div>
|
||||
<div class="no-channel-selected">Select a category and channel</div>
|
||||
}
|
||||
</div>
|
||||
@@ -1,52 +1,41 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.itv-container {
|
||||
display: grid;
|
||||
grid-template-columns: 330px 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 50% 50%;
|
||||
gap: 0 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'sidebar video-player video-player'
|
||||
'sidebar epg epg';
|
||||
|
||||
'video-player video-player'
|
||||
'epg epg';
|
||||
}
|
||||
|
||||
.itv-container-epg {
|
||||
display: grid;
|
||||
grid-template-columns: 330px 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 0 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'sidebar epg epg'
|
||||
'sidebar epg epg'
|
||||
'sidebar epg epg';
|
||||
'epg epg'
|
||||
'epg epg'
|
||||
'epg epg';
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar { grid-area: sidebar; }
|
||||
|
||||
.video-player { grid-area: video-player; }
|
||||
|
||||
.epg { grid-area: epg; }
|
||||
|
||||
.search-panel {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: -25px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 90%;
|
||||
@include mat.form-field-density(-5);
|
||||
}
|
||||
|
||||
.epg-content {
|
||||
height: calc(50vh - 20px);
|
||||
overflow-y: auto;
|
||||
@@ -54,56 +43,19 @@
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-right: #111 1px solid;
|
||||
overflow-x: hidden;
|
||||
|
||||
mat-nav-list {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
@include mat.form-field-density(-5);
|
||||
}
|
||||
}
|
||||
|
||||
.no-channel-selected {
|
||||
margin: 10px;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
mat-list-option {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.search-bar {
|
||||
div {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-viewport-portals {
|
||||
min-height: calc(100vh - 59px) !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100%); // Account for avatar and favorite button
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
mat-list-item {
|
||||
width: 325px;
|
||||
min-width: 0; // Prevents flex items from overflowing
|
||||
box-sizing: border-box;
|
||||
|
||||
::ng-deep .mdc-list-item__content {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,24 @@
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
OnInit,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
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 {
|
||||
OPEN_MPV_PLAYER,
|
||||
OPEN_VLC_PLAYER,
|
||||
} from '../../../../shared/ipc-commands';
|
||||
import { XtreamItem } from '../../../../shared/xtream-item.interface';
|
||||
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';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { DatabaseService } from '../../services/database.service'; // Add this import
|
||||
import { PlayerService } from '../../services/player.service';
|
||||
import { SettingsStore } from '../../services/settings-store.service';
|
||||
import { VideoPlayer } from '../../settings/settings.interface';
|
||||
import { ExternalPlayerInfoDialogComponent } from '../../shared/components/external-player-info-dialog/external-player-info-dialog.component';
|
||||
import { FilterPipe } from '../../shared/pipes/filter.pipe';
|
||||
import {
|
||||
PlayerDialogComponent,
|
||||
PlayerDialogData,
|
||||
} from '../player-dialog/player-dialog.component';
|
||||
import { CategoryViewComponent } from '../category-view/category-view.component';
|
||||
import { PortalChannelsListComponent } from '../portal-channels-list/portal-channels-list.component';
|
||||
import { FavoritesService } from '../services/favorites.service';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
|
||||
@@ -39,47 +26,38 @@ import { XtreamStore } from '../xtream.store';
|
||||
standalone: true,
|
||||
selector: 'app-live-stream-layout',
|
||||
templateUrl: './live-stream-layout.component.html',
|
||||
styleUrls: ['./live-stream-layout.component.scss'],
|
||||
styleUrls: ['./live-stream-layout.component.scss', '../sidebar.scss'],
|
||||
imports: [
|
||||
CategoryViewComponent,
|
||||
EpgViewComponent,
|
||||
FilterPipe,
|
||||
FormsModule,
|
||||
MatIconButton,
|
||||
MatListModule,
|
||||
MatIcon,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
NgIf,
|
||||
ScrollingModule,
|
||||
WebPlayerViewComponent,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
PortalChannelsListComponent,
|
||||
TranslateModule,
|
||||
WebPlayerViewComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LiveStreamLayoutComponent implements OnInit {
|
||||
private settingsStore = inject(SettingsStore);
|
||||
private dialog = inject(MatDialog);
|
||||
private dataService = inject(DataService);
|
||||
private favoritesService = inject(FavoritesService);
|
||||
private dbService = inject(DatabaseService); // Add this injection
|
||||
|
||||
private readonly settings = this.settingsStore.getSettings();
|
||||
readonly xtreamStore = inject(XtreamStore);
|
||||
readonly channels = this.xtreamStore.liveStreams;
|
||||
private playerService = inject(PlayerService);
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
private readonly settingsStore = inject(SettingsStore);
|
||||
readonly epgItems = this.xtreamStore.epgItems;
|
||||
readonly selectedCategoryId = this.xtreamStore.selectedCategoryId;
|
||||
private readonly hideExternalInfoDialog =
|
||||
this.xtreamStore.hideExternalInfoDialog;
|
||||
private readonly selectedContentType = this.xtreamStore.selectedContentType;
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
player: VideoPlayer = VideoPlayer.VideoJs;
|
||||
player = this.settingsStore.player;
|
||||
streamUrl: string;
|
||||
searchString = signal<string>('');
|
||||
favorites = new Map<number, boolean>();
|
||||
|
||||
trackBy(_index: number, item: XtreamItem) {
|
||||
return item.xtream_id;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const playlist = this.xtreamStore.currentPlaylist();
|
||||
if (playlist) {
|
||||
@@ -92,6 +70,10 @@ export class LiveStreamLayoutComponent implements OnInit {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const { categoryId } = this.route.firstChild.snapshot.params;
|
||||
if (categoryId)
|
||||
this.xtreamStore.setSelectedCategory(Number(categoryId));
|
||||
}
|
||||
|
||||
playLive(item: any) {
|
||||
@@ -106,76 +88,22 @@ export class LiveStreamLayoutComponent implements OnInit {
|
||||
|
||||
openPlayer(streamUrl: string, title: string, thumbnail: string) {
|
||||
this.streamUrl = streamUrl;
|
||||
this.player = this.settingsStore.player() ?? VideoPlayer.VideoJs;
|
||||
if (this.player === VideoPlayer.MPV) {
|
||||
if (!this.hideExternalInfoDialog())
|
||||
this.dialog.open(ExternalPlayerInfoDialogComponent);
|
||||
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
|
||||
url: streamUrl,
|
||||
mpvPlayerPath: this.settingsStore.mpvPlayerPath(),
|
||||
title,
|
||||
thumbnail,
|
||||
});
|
||||
} else if (this.player === VideoPlayer.VLC) {
|
||||
if (!this.hideExternalInfoDialog())
|
||||
this.dialog.open(ExternalPlayerInfoDialogComponent);
|
||||
this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, {
|
||||
url: streamUrl,
|
||||
vlcPlayerPath: this.settingsStore.vlcPlayerPath(),
|
||||
});
|
||||
} else {
|
||||
if (this.selectedContentType() !== 'live') {
|
||||
this.dialog.open<PlayerDialogComponent, PlayerDialogData>(
|
||||
PlayerDialogComponent,
|
||||
{
|
||||
data: { streamUrl, title },
|
||||
width: '80%',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
this.playerService.openPlayer(
|
||||
streamUrl,
|
||||
title,
|
||||
thumbnail,
|
||||
this.hideExternalInfoDialog(),
|
||||
this.selectedContentType() === 'live'
|
||||
);
|
||||
}
|
||||
|
||||
async toggleFavorite(event: Event, item: any): Promise<void> {
|
||||
event.stopPropagation();
|
||||
const playlist = this.xtreamStore.currentPlaylist();
|
||||
selectCategory(category: XtreamCategory) {
|
||||
const categoryId = (category as any).category_id ?? category.id;
|
||||
console.log('Selected category:', categoryId);
|
||||
this.xtreamStore.setSelectedCategory(categoryId);
|
||||
}
|
||||
|
||||
// Update UI state immediately
|
||||
const currentFavoriteState =
|
||||
this.favorites.get(item.xtream_id) || false;
|
||||
this.favorites.set(item.xtream_id, !currentFavoriteState);
|
||||
|
||||
try {
|
||||
const db = await this.dbService.getConnection();
|
||||
const content: any = await db.select(
|
||||
'SELECT id FROM content WHERE xtream_id = ?',
|
||||
[item.xtream_id]
|
||||
);
|
||||
|
||||
if (!content || content.length === 0) {
|
||||
console.error('Content not found in database');
|
||||
// Revert UI state on error
|
||||
this.favorites.set(item.xtream_id, currentFavoriteState);
|
||||
return;
|
||||
}
|
||||
|
||||
const contentId = content[0].id;
|
||||
|
||||
if (!currentFavoriteState) {
|
||||
await this.favoritesService.addToFavorites({
|
||||
content_id: contentId,
|
||||
playlist_id: playlist.id,
|
||||
});
|
||||
} else {
|
||||
await this.favoritesService.removeFromFavorites(
|
||||
contentId,
|
||||
playlist.id
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling favorite:', error);
|
||||
// Revert UI state on error
|
||||
this.favorites.set(item.xtream_id, currentFavoriteState);
|
||||
}
|
||||
backToCategories() {
|
||||
this.xtreamStore.setSelectedCategory(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<div class="search-panel">
|
||||
<mat-form-field class="search-bar" subscriptSizing="dynamic">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
spellcheck="false"
|
||||
type="search"
|
||||
[placeholder]="'CHANNELS.SEARCH_CHANNEL' | translate"
|
||||
[(ngModel)]="searchString"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-nav-list>
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="48"
|
||||
class="scroll-viewport-portals"
|
||||
>
|
||||
<mat-list-item
|
||||
*cdkVirtualFor="
|
||||
let item of channels() | filterBy: searchString() : 'title';
|
||||
trackBy: trackBy;
|
||||
templateCacheSize: 0
|
||||
"
|
||||
(click)="playClicked.emit(item)"
|
||||
[activated]="
|
||||
xtreamStore.selectedItem()?.xtream_id === item.xtream_id
|
||||
"
|
||||
>
|
||||
<img
|
||||
matListItemAvatar
|
||||
class="stream-icon"
|
||||
[src]="item.poster_url"
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
/>
|
||||
<div class="title" matListItemTitle>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<button
|
||||
mat-icon-button
|
||||
matListItemMeta
|
||||
(click)="toggleFavorite($event, item)"
|
||||
>
|
||||
<mat-icon>
|
||||
{{
|
||||
favorites.get(item.xtream_id) ? 'star' : 'star_outline'
|
||||
}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</mat-nav-list>
|
||||
@@ -0,0 +1,58 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
mat-nav-list {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-list-item {
|
||||
width: 280px !important;
|
||||
}
|
||||
|
||||
.scroll-viewport-portals {
|
||||
min-height: calc(100vh - 114px) !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100%); // Account for avatar and favorite button
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
mat-list-item {
|
||||
width: 325px;
|
||||
min-width: 0; // Prevents flex items from overflowing
|
||||
box-sizing: border-box;
|
||||
|
||||
::ng-deep .mdc-list-item__content {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 90%;
|
||||
@include mat.form-field-density(-5);
|
||||
}
|
||||
|
||||
.search-panel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.search-bar {
|
||||
div {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
div[matformfieldlineripple] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { Component, EventEmitter, Output, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { XtreamCategory } from '../../../../shared/xtream-category.interface';
|
||||
import { XtreamItem } from '../../../../shared/xtream-item.interface';
|
||||
import { DatabaseService } from '../../services/database.service';
|
||||
import { FilterPipe } from '../../shared/pipes/filter.pipe';
|
||||
import { FavoritesService } from '../services/favorites.service';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-portal-channels-list',
|
||||
standalone: true,
|
||||
templateUrl: './portal-channels-list.component.html',
|
||||
styleUrls: ['./portal-channels-list.component.scss'],
|
||||
imports: [
|
||||
FilterPipe,
|
||||
FormsModule,
|
||||
MatFormFieldModule,
|
||||
ScrollingModule,
|
||||
MatCardModule,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatListModule,
|
||||
MatInputModule,
|
||||
TranslateModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
})
|
||||
export class PortalChannelsListComponent {
|
||||
@Output() playClicked = new EventEmitter<any>();
|
||||
|
||||
readonly xtreamStore = inject(XtreamStore);
|
||||
private readonly favoritesService = inject(FavoritesService);
|
||||
private readonly dbService = inject(DatabaseService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
readonly channels = this.xtreamStore.selectItemsFromSelectedCategory;
|
||||
|
||||
favorites = new Map<number, boolean>();
|
||||
searchString = signal<string>('');
|
||||
|
||||
trackBy(_index: number, item: XtreamItem) {
|
||||
return item.xtream_id;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const { categoryId } = this.route.snapshot.params;
|
||||
if (categoryId)
|
||||
this.xtreamStore.setSelectedCategory(Number(categoryId));
|
||||
|
||||
const playlist = this.xtreamStore.currentPlaylist();
|
||||
if (playlist) {
|
||||
this.favoritesService
|
||||
.getFavorites(playlist.id)
|
||||
.subscribe((favorites) => {
|
||||
// Map using content.id instead of xtream_id
|
||||
favorites.forEach((fav: any) => {
|
||||
this.favorites.set(fav.xtream_id, true);
|
||||
});
|
||||
console.log(this.favorites);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isSelected(item: XtreamCategory): boolean {
|
||||
const selectedCategory = this.xtreamStore.selectedCategoryId();
|
||||
const itemId = Number((item as any).category_id || item.id);
|
||||
return selectedCategory !== null && selectedCategory === itemId;
|
||||
}
|
||||
|
||||
async toggleFavorite(event: Event, item: any): Promise<void> {
|
||||
event.stopPropagation();
|
||||
const playlist = this.xtreamStore.currentPlaylist();
|
||||
|
||||
// Update UI state immediately
|
||||
const currentFavoriteState =
|
||||
this.favorites.get(item.xtream_id) || false;
|
||||
this.favorites.set(item.xtream_id, !currentFavoriteState);
|
||||
|
||||
try {
|
||||
const db = await this.dbService.getConnection();
|
||||
const content: any = await db.select(
|
||||
'SELECT id FROM content WHERE xtream_id = ?',
|
||||
[item.xtream_id]
|
||||
);
|
||||
|
||||
if (!content || content.length === 0) {
|
||||
console.error('Content not found in database');
|
||||
// Revert UI state on error
|
||||
this.favorites.set(item.xtream_id, currentFavoriteState);
|
||||
return;
|
||||
}
|
||||
|
||||
const contentId = content[0].id;
|
||||
|
||||
if (!currentFavoriteState) {
|
||||
await this.favoritesService.addToFavorites({
|
||||
content_id: contentId,
|
||||
playlist_id: playlist.id,
|
||||
});
|
||||
} else {
|
||||
await this.favoritesService.removeFromFavorites(
|
||||
contentId,
|
||||
playlist.id
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling favorite:', error);
|
||||
// Revert UI state on error
|
||||
this.favorites.set(item.xtream_id, currentFavoriteState);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user