mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
feat: enhance DashboardDataService with loading states and global favorites handling
This commit is contained in:
1 parent
eaa3e0e2bd
commit
8b5344ef26
2 files changed
+181
-9
No files matched your search
@@ -2,7 +2,10 @@ import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { selectAllPlaylistsMeta } from 'm3u-state';
|
||||
import {
|
||||
selectAllPlaylistsMeta,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from 'm3u-state';
|
||||
import { of } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from 'services';
|
||||
import { Playlist, PlaylistMeta } from 'shared-interfaces';
|
||||
@@ -10,6 +13,7 @@ import { DashboardDataService } from './dashboard-data.service';
|
||||
|
||||
describe('DashboardDataService', () => {
|
||||
let service: DashboardDataService;
|
||||
const playlistsLoadedSignal = signal(true);
|
||||
|
||||
const playlistsSignal = signal<PlaylistMeta[]>([
|
||||
{
|
||||
@@ -50,6 +54,9 @@ describe('DashboardDataService', () => {
|
||||
if (selector === selectAllPlaylistsMeta) {
|
||||
return playlistsSignal;
|
||||
}
|
||||
if (selector === selectPlaylistsLoadingFlag) {
|
||||
return playlistsLoadedSignal;
|
||||
}
|
||||
return signal(null);
|
||||
}),
|
||||
dispatch: jest.fn(),
|
||||
@@ -58,7 +65,7 @@ describe('DashboardDataService', () => {
|
||||
const dbServiceMock = {
|
||||
getGlobalRecentlyAdded: jest.fn().mockResolvedValue([]),
|
||||
getGlobalRecentlyViewed: jest.fn().mockResolvedValue([]),
|
||||
getGlobalFavorites: jest.fn().mockResolvedValue([]),
|
||||
getAllGlobalFavorites: jest.fn().mockResolvedValue([]),
|
||||
removeFromFavorites: jest.fn().mockResolvedValue(undefined),
|
||||
removeRecentItem: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
@@ -113,6 +120,7 @@ describe('DashboardDataService', () => {
|
||||
value: {} as Window['electron'],
|
||||
configurable: true,
|
||||
});
|
||||
playlistsLoadedSignal.set(true);
|
||||
playlistsServiceMock.getPlaylistById.mockClear();
|
||||
playlistsServiceMock.getPlaylistById.mockReturnValue(of(playlistMock));
|
||||
playlistsServiceMock.setFavorites.mockClear();
|
||||
@@ -124,8 +132,8 @@ describe('DashboardDataService', () => {
|
||||
recentlyViewed: [],
|
||||
})
|
||||
);
|
||||
dbServiceMock.getGlobalFavorites.mockClear();
|
||||
dbServiceMock.getGlobalFavorites.mockResolvedValue([]);
|
||||
dbServiceMock.getAllGlobalFavorites.mockClear();
|
||||
dbServiceMock.getAllGlobalFavorites.mockResolvedValue([]);
|
||||
dbServiceMock.getGlobalRecentlyAdded.mockClear();
|
||||
dbServiceMock.getGlobalRecentlyAdded.mockResolvedValue([]);
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockClear();
|
||||
@@ -186,6 +194,60 @@ describe('DashboardDataService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('includes Xtream movies and series in global favorite items', async () => {
|
||||
dbServiceMock.getAllGlobalFavorites.mockResolvedValue([
|
||||
{
|
||||
id: 51,
|
||||
category_id: 12,
|
||||
title: 'Action Movie',
|
||||
rating: '8.0',
|
||||
added_at: '2026-02-02T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/movie.png',
|
||||
xtream_id: 5001,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
{
|
||||
id: 52,
|
||||
category_id: 13,
|
||||
title: 'Sci-Fi Series',
|
||||
rating: '8.7',
|
||||
added_at: '2026-02-03T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/series.png',
|
||||
xtream_id: 5002,
|
||||
type: 'series',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalFavorites();
|
||||
|
||||
expect(service.globalFavoriteItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 51,
|
||||
title: 'Action Movie',
|
||||
type: 'movie',
|
||||
source: 'xtream',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 52,
|
||||
title: 'Sci-Fi Series',
|
||||
type: 'series',
|
||||
source: 'xtream',
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(
|
||||
service.globalFavoriteItems().filter((item) => item.type === 'movie')
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
service.globalFavoriteItems().filter((item) => item.type === 'series')
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('builds the M3U favorites route', async () => {
|
||||
await service.reloadGlobalFavorites();
|
||||
const m3uItem = service
|
||||
@@ -199,6 +261,16 @@ describe('DashboardDataService', () => {
|
||||
'm3u-1',
|
||||
'favorites',
|
||||
]);
|
||||
expect(service.getGlobalFavoriteNavigationState(m3uItem!)).toEqual({
|
||||
openLiveCollectionItem: {
|
||||
contentType: 'live',
|
||||
sourceType: 'm3u',
|
||||
playlistId: 'm3u-1',
|
||||
itemId: 'channel-1',
|
||||
title: 'Channel One',
|
||||
imageUrl: 'https://example.com/logo-1.png',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('removes M3U favorites via PlaylistsService', async () => {
|
||||
@@ -292,7 +364,7 @@ describe('DashboardDataService', () => {
|
||||
expect(service.getRecentlyAddedNavigationState(item)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('builds the M3U recent route with navigation state', () => {
|
||||
it('builds the M3U recent route with collection auto-open state', () => {
|
||||
const m3uItem = service
|
||||
.globalRecentItems()
|
||||
.find((item) => item.source === 'm3u');
|
||||
@@ -305,7 +377,14 @@ describe('DashboardDataService', () => {
|
||||
'recent',
|
||||
]);
|
||||
expect(service.getRecentItemNavigationState(m3uItem!)).toEqual({
|
||||
openRecentChannelUrl: 'https://example.com/stream-1.m3u8',
|
||||
openLiveCollectionItem: {
|
||||
contentType: 'live',
|
||||
sourceType: 'm3u',
|
||||
playlistId: 'm3u-1',
|
||||
itemId: 'https://example.com/stream-1.m3u8',
|
||||
title: 'Channel One',
|
||||
imageUrl: 'https://example.com/logo-1.png',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { Injectable, NgZone, computed, inject, signal } from '@angular/core';
|
||||
import {
|
||||
Injectable,
|
||||
NgZone,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
PlaylistActions,
|
||||
selectAllPlaylistsMeta,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from 'm3u-state';
|
||||
import { firstValueFrom, startWith } from 'rxjs';
|
||||
import {
|
||||
@@ -66,7 +74,34 @@ export class DashboardDataService {
|
||||
[]
|
||||
);
|
||||
private readonly m3uGlobalFavorites = signal<DashboardFavoriteItem[]>([]);
|
||||
private readonly globalRecentLoadingState = signal(true);
|
||||
private readonly globalRecentLoadedState = signal(false);
|
||||
private readonly globalRecentDbLoadedState = signal(!window.electron);
|
||||
private readonly globalFavoritesLoadingState = signal(true);
|
||||
private readonly globalFavoritesLoadedState = signal(false);
|
||||
readonly playlists = this.store.selectSignal(selectAllPlaylistsMeta);
|
||||
readonly playlistsLoaded = this.store.selectSignal(selectPlaylistsLoadingFlag);
|
||||
readonly globalRecentLoading = this.globalRecentLoadingState.asReadonly();
|
||||
readonly globalRecentLoaded = this.globalRecentLoadedState.asReadonly();
|
||||
readonly globalFavoritesLoading =
|
||||
this.globalFavoritesLoadingState.asReadonly();
|
||||
readonly globalFavoritesLoaded = this.globalFavoritesLoadedState.asReadonly();
|
||||
|
||||
private readonly playlistFavoritesReloadKey = computed(() => {
|
||||
if (!this.playlistsLoaded()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.playlists()
|
||||
.map((playlist) =>
|
||||
[
|
||||
playlist._id,
|
||||
playlist.serverUrl ? 'xtream' : playlist.macAddress ? 'stalker' : 'm3u',
|
||||
JSON.stringify(playlist.favorites ?? []),
|
||||
].join('::')
|
||||
)
|
||||
.join('|');
|
||||
});
|
||||
|
||||
readonly playlistBackedGlobalRecentItems = computed<GlobalRecentItem[]>(
|
||||
() => {
|
||||
@@ -112,6 +147,21 @@ export class DashboardDataService {
|
||||
|
||||
constructor() {
|
||||
void this.reloadGlobalFavorites();
|
||||
|
||||
effect(() => {
|
||||
this.playlistFavoritesReloadKey();
|
||||
if (!this.playlistsLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.refreshPlaylistBackedGlobalFavorites();
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
this.playlistsLoaded();
|
||||
this.finishInitialGlobalRecentLoadIfReady();
|
||||
});
|
||||
|
||||
if (window.electron) {
|
||||
void this.reloadGlobalRecentItems();
|
||||
}
|
||||
@@ -140,8 +190,14 @@ export class DashboardDataService {
|
||||
readonly quickRecent = computed(() => this.recentPlaylists().slice(0, 4));
|
||||
|
||||
async reloadGlobalRecentItems(): Promise<void> {
|
||||
if (!this.globalRecentLoaded()) {
|
||||
this.globalRecentLoadingState.set(true);
|
||||
}
|
||||
|
||||
if (!window.electron) {
|
||||
this.xtreamGlobalRecentItems.set([]);
|
||||
this.globalRecentDbLoadedState.set(true);
|
||||
this.finishInitialGlobalRecentLoadIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,12 +213,19 @@ export class DashboardDataService {
|
||||
err
|
||||
);
|
||||
this.ngZone.run(() => this.xtreamGlobalRecentItems.set([]));
|
||||
} finally {
|
||||
this.globalRecentDbLoadedState.set(true);
|
||||
this.finishInitialGlobalRecentLoadIfReady();
|
||||
}
|
||||
}
|
||||
|
||||
async reloadGlobalFavorites(): Promise<void> {
|
||||
if (!this.globalFavoritesLoaded()) {
|
||||
this.globalFavoritesLoadingState.set(true);
|
||||
}
|
||||
|
||||
const xtreamReload = this.reloadXtreamGlobalFavorites();
|
||||
const m3uReload = this.reloadM3uGlobalFavorites();
|
||||
const m3uReload = this.refreshPlaylistBackedGlobalFavorites();
|
||||
|
||||
await Promise.all([xtreamReload, m3uReload]);
|
||||
}
|
||||
@@ -184,11 +247,12 @@ export class DashboardDataService {
|
||||
private async reloadXtreamGlobalFavorites(): Promise<void> {
|
||||
if (!window.electron) {
|
||||
this.xtreamGlobalFavorites.set([]);
|
||||
this.finishInitialGlobalFavoritesLoadIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const favorites = await this.dbService.getGlobalFavorites();
|
||||
const favorites = await this.dbService.getAllGlobalFavorites();
|
||||
const normalized = favorites.map((item) =>
|
||||
mapDbFavoriteToItem(item)
|
||||
);
|
||||
@@ -199,9 +263,16 @@ export class DashboardDataService {
|
||||
err
|
||||
);
|
||||
this.ngZone.run(() => this.xtreamGlobalFavorites.set([]));
|
||||
} finally {
|
||||
this.finishInitialGlobalFavoritesLoadIfReady();
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshPlaylistBackedGlobalFavorites(): Promise<void> {
|
||||
await this.reloadM3uGlobalFavorites();
|
||||
this.finishInitialGlobalFavoritesLoadIfReady();
|
||||
}
|
||||
|
||||
private async reloadM3uGlobalFavorites(): Promise<void> {
|
||||
const m3uPlaylists = this.playlists().filter(
|
||||
(playlist) =>
|
||||
@@ -234,6 +305,28 @@ export class DashboardDataService {
|
||||
this.ngZone.run(() => this.m3uGlobalFavorites.set(flattenedFavorites));
|
||||
}
|
||||
|
||||
private finishInitialGlobalFavoritesLoadIfReady(): void {
|
||||
if (!this.playlistsLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ngZone.run(() => {
|
||||
this.globalFavoritesLoadedState.set(true);
|
||||
this.globalFavoritesLoadingState.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
private finishInitialGlobalRecentLoadIfReady(): void {
|
||||
if (!this.playlistsLoaded() || !this.globalRecentDbLoadedState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ngZone.run(() => {
|
||||
this.globalRecentLoadedState.set(true);
|
||||
this.globalRecentLoadingState.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
isTypeInKind(
|
||||
type: PortalActivityType,
|
||||
kind: DashboardContentKind
|
||||
|
||||
Reference in new issue
Block a user