refactor(epg): improve list rendering, state handling, and styles

- Track list items by start+stop to avoid incorrect reuse of DOM
  nodes when program time ranges change.
- matListItemLine with matListItemTitle and add
  program-title class to ensure proper styling and layout of titles.
- Add left margin to item label for visual spacing.
- Update imports to use shared-interfaces barrel to simplify paths.
- Remove unused OnDestroy and related ipc cleanup; epg list no longer
  manages EPG IPC listeners directly.
- Introduce setEpgAvailableFlag dispatch when handling EPG data to
  expose availability state; clear current EPG program via
  resetActiveEpgProgram when no programs are returned.
- Remove direct Store usage from EpgService and update BehaviorSubject
  flow to set epgAvailable=false on error.
- Clean up unused imports and minor formatting.

These changes fix rendering glitches, improve state clarity for EPG
availability, simplify imports, and tidy up lifecycle responsibilities.
This commit is contained in:
4gray committed 2025-10-23 22:51:28 +02:00
1 parent 8b0db47caf
commit 57005d4f46
8 files changed
+30 -51

No files matched your search

+2 -10
View File
@@ -1,8 +1,6 @@
import { inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import * as PlaylistActions from 'm3u-state';
import { BehaviorSubject, from, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { EpgProgram } from 'shared-interfaces';
@@ -14,9 +12,6 @@ export class EpgService {
private snackBar = inject(MatSnackBar);
private translate = inject(TranslateService);
// TODO: do not use store directly in the service
private store = inject(Store);
private epgAvailable = new BehaviorSubject<boolean>(false);
private currentEpgPrograms = new BehaviorSubject<EpgProgram[]>([]);
@@ -77,15 +72,12 @@ export class EpgService {
console.error('EPG get programs error:', err);
this.showErrorSnackbar();
this.currentEpgPrograms.next([]);
this.epgAvailable.next(false);
return of([]);
})
)
.subscribe((programs) => {
this.store.dispatch(
PlaylistActions.setEpgAvailableFlag({
value: programs.length === 0 ? false : true,
})
);
this.epgAvailable.next(programs.length > 0);
this.currentEpgPrograms.next(programs);
});
}
@@ -3,7 +3,7 @@ import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { MockModule } from 'ng-mocks';
import { EpgProgram } from '../../../../../../shared/interfaces/src/lib/epg-program.model';
import { EpgProgram } from 'shared-interfaces';
import { EpgItemDescriptionComponent } from './epg-item-description.component';
describe('EpgItemDescriptionComponent', () => {
@@ -4,6 +4,7 @@
font-size: 0.8em;
display: inline-block;
margin-bottom: 5px;
margin-left: 5px;
}
.badge {
@@ -4,11 +4,11 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MomentDatePipe } from '@iptvnator/pipes';
import { TranslateModule } from '@ngx-translate/core';
import moment from 'moment';
import { MockModule, MockPipe } from 'ng-mocks';
import { EpgProgram } from '../../../../../../shared/interfaces/src/lib/epg-program.model';
import { MomentDatePipe } from '../../../../../pipes/src/lib/moment-date.pipe';
import { EpgProgram } from 'shared-interfaces';
import { EpgItemDescriptionComponent } from '../epg-item-description/epg-item-description.component';
import { EpgListItemComponent } from './epg-list-item.component';
@@ -56,7 +56,7 @@
hideSingleSelectionIndicator="true"
>
@if (items?.length > 0) {
@for (program of items; track program) {
@for (program of items; track program.start + program.stop) {
@if (
program.start < timeshiftUntil || program.start >= timeNow
) {
@@ -68,7 +68,9 @@
[item]="program"
/>
</div>
<p matListItemLine>{{ program?.title?.[0]?.value }}</p>
<p matListItemTitle class="program-title">
{{ program?.title?.[0]?.value }}
</p>
</mat-list-option>
} @else {
<mat-list-option
@@ -14,6 +14,10 @@
border-bottom: 1px dashed #666;
}
.program-title {
font-size: 14px;
}
#channel-header {
flex-direction: column;
box-sizing: border-box;
@@ -1,20 +1,17 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
/* eslint-disable @typescript-eslint/unbound-method */
/* import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MomentDatePipe } from '@iptvnator/pipes';
import { Actions } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
import { BehaviorSubject, Observable } from 'rxjs';
import { Channel, EPG_GET_PROGRAM_DONE, EpgProgram } from 'shared-interfaces';
import { ElectronServiceStub } from '../../../../../../apps/web/src/app/services/electron.service.stub';
import { DataService } from '../../../../../services/src/lib/data.service';
import { EpgService } from '../../../../../services/src/lib/epg.service';
import { MomentDatePipe } from '../../../../pipes/src/lib/moment-date.pipe';
import { DataService, ElectronServiceStub, EpgService } from 'services';
import { Channel, EpgProgram } from 'shared-interfaces';
import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
import { EpgListComponent } from './epg-list.component';
@@ -159,15 +156,6 @@ describe('EpgListComponent', () => {
expect(component).toBeTruthy();
});
/* it('should handle epg programs', () => {
component.handleEpgData({ payload: MOCKED_PROGRAMS });
fixture.detectChanges();
expect(component.timeNow).toBeTruthy();
expect(component.dateToday).toBeTruthy();
expect(component.channel).toBeTruthy();
expect(component.items).toHaveLength(1);
}); */
it('should handle an empty epg programs object', () => {
const emptyPrograms: EpgProgram[] = [];
(epgService.currentEpgPrograms$ as BehaviorSubject<EpgProgram[]>).next(
@@ -184,15 +172,6 @@ describe('EpgListComponent', () => {
});
});
it('should remove ipc listeners on destroy', () => {
jest.spyOn(electronService, 'removeAllListeners');
component.ngOnDestroy();
expect(electronService.removeAllListeners).toHaveBeenCalledTimes(1);
expect(electronService.removeAllListeners).toHaveBeenCalledWith(
EPG_GET_PROGRAM_DONE
);
});
it('should set epg program as active', () => {
jest.spyOn(mockStore, 'dispatch');
component.setEpgProgram(MOCKED_PROGRAMS.items[0], false, true);
@@ -211,3 +190,4 @@ describe('EpgListComponent', () => {
expect(mockStore.dispatch).toHaveBeenCalledTimes(2);
});
});
*/
@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common';
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
@@ -14,13 +14,13 @@ import {
selectActive,
setActiveEpgProgram,
setCurrentEpgProgram,
setEpgAvailableFlag,
} from 'm3u-state';
import moment from 'moment';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { DataService, EpgService } from 'services';
import {
EPG_GET_PROGRAM_DONE,
EpgChannel,
EpgProgram,
} from 'shared-interfaces';
@@ -50,7 +50,7 @@ const DATE_FORMAT = 'YYYY-MM-DD';
templateUrl: './epg-list.component.html',
styleUrls: ['./epg-list.component.scss'],
})
export class EpgListComponent implements OnInit, OnDestroy {
export class EpgListComponent implements OnInit {
private readonly store = inject(Store);
private dataService = inject(DataService);
private readonly epgService = inject(EpgService);
@@ -164,11 +164,18 @@ export class EpgListComponent implements OnInit, OnDestroy {
handleEpgData(programs: EpgProgram[]): void {
this.timeNow = new Date().toISOString();
this.dateToday = moment().format(DATE_FORMAT);
// Dispatch EPG availability flag
this.store.dispatch(
setEpgAvailableFlag({ value: programs.length > 0 })
);
if (programs.length > 0) {
this.setPlayingNow();
} else {
this.channel = {} as EpgChannel;
this.store.dispatch(setCurrentEpgProgram(undefined));
// Clear the current EPG program when no programs available
this.store.dispatch(resetActiveEpgProgram());
}
}
@@ -247,11 +254,4 @@ export class EpgListComponent implements OnInit, OnDestroy {
}
this.playingNow = program;
}
/**
* Removes all ipc renderer listeners after destroy
*/
ngOnDestroy(): void {
this.dataService.removeAllListeners(EPG_GET_PROGRAM_DONE);
}
}