mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
refactor(favs-recent): adapt UI and routing behavior
- Introduced StalkerCollectionDetailComponent to handle detailed views for Stalker collections. - Added XtreamCollectionDetailComponent for managing Xtream collection details. - Updated routing components to integrate new detail components for both Stalker and Xtream. - Enhanced global favorites and recently viewed functionalities to support new detail navigation for Stalker and Xtream items. - Improved styling and structure for better responsiveness and user experience in detail views. Entire-Checkpoint: c6e522b4276c
This commit is contained in:
1 parent
562bfd009c
commit
5e41e3d1ce
19 files changed
+1671
-181
No files matched your search
@@ -2,10 +2,7 @@ import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
selectAllPlaylistsMeta,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from 'm3u-state';
|
||||
import { selectAllPlaylistsMeta, selectPlaylistsLoadingFlag } from 'm3u-state';
|
||||
import { of } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from 'services';
|
||||
import { Playlist, PlaylistMeta } from 'shared-interfaces';
|
||||
@@ -105,14 +102,12 @@ describe('DashboardDataService', () => {
|
||||
const playlistsServiceMock = {
|
||||
getPlaylistById: jest.fn().mockReturnValue(of(playlistMock)),
|
||||
setFavorites: jest.fn().mockReturnValue(of(undefined)),
|
||||
removeFromM3uRecentlyViewed: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
of({
|
||||
...playlistMock,
|
||||
recentlyViewed: [],
|
||||
})
|
||||
),
|
||||
removeFromM3uRecentlyViewed: jest.fn().mockReturnValue(
|
||||
of({
|
||||
...playlistMock,
|
||||
recentlyViewed: [],
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -241,10 +236,14 @@ describe('DashboardDataService', () => {
|
||||
])
|
||||
);
|
||||
expect(
|
||||
service.globalFavoriteItems().filter((item) => item.type === 'movie')
|
||||
service
|
||||
.globalFavoriteItems()
|
||||
.filter((item) => item.type === 'movie')
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
service.globalFavoriteItems().filter((item) => item.type === 'series')
|
||||
service
|
||||
.globalFavoriteItems()
|
||||
.filter((item) => item.type === 'series')
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -273,6 +272,45 @@ describe('DashboardDataService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('routes Xtream non-live favorites through the global favorites page with inline detail state', 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',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalFavorites();
|
||||
const movieItem = service
|
||||
.globalFavoriteItems()
|
||||
.find((item) => item.type === 'movie');
|
||||
|
||||
expect(movieItem).toBeDefined();
|
||||
expect(service.getGlobalFavoriteLink(movieItem!)).toEqual([
|
||||
'/workspace',
|
||||
'global-favorites',
|
||||
]);
|
||||
expect(service.getGlobalFavoriteNavigationState(movieItem!)).toEqual({
|
||||
openCollectionDetailItem: {
|
||||
item: expect.objectContaining({
|
||||
uid: 'xtream::xtream-1::5001',
|
||||
name: 'Action Movie',
|
||||
contentType: 'movie',
|
||||
sourceType: 'xtream',
|
||||
playlistId: 'xtream-1',
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('removes M3U favorites via PlaylistsService', async () => {
|
||||
await service.reloadGlobalFavorites();
|
||||
const m3uItem = service
|
||||
@@ -388,6 +426,45 @@ describe('DashboardDataService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('routes Xtream non-live recents through the global recent page with inline detail state', async () => {
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockResolvedValue([
|
||||
{
|
||||
id: 91,
|
||||
category_id: 18,
|
||||
title: 'Recent Movie',
|
||||
rating: '7.8',
|
||||
viewed_at: '2026-02-04T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/recent-movie.png',
|
||||
xtream_id: 7001,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalRecentItems();
|
||||
const movieItem = service
|
||||
.globalRecentItems()
|
||||
.find((item) => item.source === 'xtream');
|
||||
|
||||
expect(movieItem).toBeDefined();
|
||||
expect(service.getRecentItemLink(movieItem!)).toEqual([
|
||||
'/workspace',
|
||||
'global-recent',
|
||||
]);
|
||||
expect(service.getRecentItemNavigationState(movieItem!)).toEqual({
|
||||
openCollectionDetailItem: {
|
||||
item: expect.objectContaining({
|
||||
uid: 'xtream::xtream-1::7001',
|
||||
name: 'Recent Movie',
|
||||
contentType: 'movie',
|
||||
sourceType: 'xtream',
|
||||
playlistId: 'xtream-1',
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('removes M3U recently viewed via PlaylistsService', async () => {
|
||||
const m3uItem = service
|
||||
.globalRecentItems()
|
||||
@@ -399,9 +476,6 @@ describe('DashboardDataService', () => {
|
||||
|
||||
expect(
|
||||
playlistsServiceMock.removeFromM3uRecentlyViewed
|
||||
).toHaveBeenCalledWith(
|
||||
'm3u-1',
|
||||
'https://example.com/stream-1.m3u8'
|
||||
);
|
||||
).toHaveBeenCalledWith('m3u-1', 'https://example.com/stream-1.m3u8');
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,9 @@ import {
|
||||
toTimestamp,
|
||||
} from './dashboard-mappers';
|
||||
import {
|
||||
buildStalkerDetailNavigationTarget,
|
||||
buildStalkerStateItem,
|
||||
buildXtreamNavigationTarget,
|
||||
getGlobalFavoriteNavigation,
|
||||
getRecentItemNavigation,
|
||||
WorkspaceNavigationTarget,
|
||||
@@ -80,12 +83,15 @@ export class DashboardDataService {
|
||||
private readonly globalFavoritesLoadingState = signal(true);
|
||||
private readonly globalFavoritesLoadedState = signal(false);
|
||||
readonly playlists = this.store.selectSignal(selectAllPlaylistsMeta);
|
||||
readonly playlistsLoaded = this.store.selectSignal(selectPlaylistsLoadingFlag);
|
||||
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();
|
||||
readonly globalFavoritesLoaded =
|
||||
this.globalFavoritesLoadedState.asReadonly();
|
||||
|
||||
private readonly playlistFavoritesReloadKey = computed(() => {
|
||||
if (!this.playlistsLoaded()) {
|
||||
@@ -96,7 +102,11 @@ export class DashboardDataService {
|
||||
.map((playlist) =>
|
||||
[
|
||||
playlist._id,
|
||||
playlist.serverUrl ? 'xtream' : playlist.macAddress ? 'stalker' : 'm3u',
|
||||
playlist.serverUrl
|
||||
? 'xtream'
|
||||
: playlist.macAddress
|
||||
? 'stalker'
|
||||
: 'm3u',
|
||||
JSON.stringify(playlist.favorites ?? []),
|
||||
].join('::')
|
||||
)
|
||||
@@ -281,7 +291,8 @@ export class DashboardDataService {
|
||||
Array.isArray(playlist.favorites) &&
|
||||
playlist.favorites.some(
|
||||
(favorite): favorite is string =>
|
||||
typeof favorite === 'string' && favorite.trim().length > 0
|
||||
typeof favorite === 'string' &&
|
||||
favorite.trim().length > 0
|
||||
)
|
||||
);
|
||||
|
||||
@@ -478,9 +489,7 @@ export class DashboardDataService {
|
||||
return this.getActivityItemTypeLabel(item);
|
||||
}
|
||||
|
||||
getRecentlyAddedItemTypeLabel(
|
||||
item: DashboardRecentlyAddedItem
|
||||
): string {
|
||||
getRecentlyAddedItemTypeLabel(item: DashboardRecentlyAddedItem): string {
|
||||
return this.getActivityItemTypeLabel(item);
|
||||
}
|
||||
|
||||
@@ -502,13 +511,57 @@ export class DashboardDataService {
|
||||
}
|
||||
|
||||
getRecentlyAddedLink(item: DashboardRecentlyAddedItem): string[] {
|
||||
return getGlobalFavoriteNavigation(item).link;
|
||||
if (item.source === 'stalker' && item.type !== 'live') {
|
||||
return buildStalkerDetailNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
categoryId: item.category_id,
|
||||
item: buildStalkerStateItem(item.stalker_item, {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
category_id: item.category_id,
|
||||
poster_url: item.poster_url,
|
||||
}),
|
||||
}).link;
|
||||
}
|
||||
|
||||
return buildXtreamNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
categoryId: item.category_id,
|
||||
itemId: item.xtream_id,
|
||||
title: item.title,
|
||||
imageUrl: item.poster_url,
|
||||
}).link;
|
||||
}
|
||||
|
||||
getRecentlyAddedNavigationState(
|
||||
item: DashboardRecentlyAddedItem
|
||||
): WorkspaceNavigationTarget['state'] {
|
||||
return getGlobalFavoriteNavigation(item).state;
|
||||
if (item.source === 'stalker' && item.type !== 'live') {
|
||||
return buildStalkerDetailNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
categoryId: item.category_id,
|
||||
item: buildStalkerStateItem(item.stalker_item, {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
category_id: item.category_id,
|
||||
poster_url: item.poster_url,
|
||||
}),
|
||||
}).state;
|
||||
}
|
||||
|
||||
return buildXtreamNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
categoryId: item.category_id,
|
||||
itemId: item.xtream_id,
|
||||
title: item.title,
|
||||
imageUrl: item.poster_url,
|
||||
}).state;
|
||||
}
|
||||
|
||||
async removeGlobalFavorite(item: DashboardFavoriteItem): Promise<void> {
|
||||
@@ -619,8 +672,7 @@ export class DashboardDataService {
|
||||
const channelId = String(channel.id ?? '').trim();
|
||||
const channelUrl = String(channel.url ?? '').trim();
|
||||
const matchedFavoriteId = favorites.find(
|
||||
(favorite) =>
|
||||
favorite === channelId || favorite === channelUrl
|
||||
(favorite) => favorite === channelId || favorite === channelUrl
|
||||
);
|
||||
|
||||
if (!matchedFavoriteId) {
|
||||
|
||||
Reference in new issue
Block a user